BetterYTM.user.js 309 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460
  1. // ==UserScript==
  2. // @name BetterYTM
  3. // @namespace https://github.com/Sv443/BetterYTM
  4. // @version 1.1.1
  5. // @description Lots of configurable layout and user experience improvements for YouTube Music™
  6. // @description:de-DE Konfigurierbare Layout- und Benutzererfahrungs-Verbesserungen für YouTube Music™
  7. // @description:en-US Configurable layout and user experience improvements for YouTube Music™
  8. // @description:en-UK Configurable layout and user experience improvements for YouTube Music™
  9. // @description:es-ES Mejoras de diseño y experiencia de usuario configurables para YouTube Music™
  10. // @description:fr-FR Améliorations de la mise en page et de l'expérience utilisateur configurables pour YouTube Music™
  11. // @description:hi-IN YouTube Music™ के लिए विन्यास और यूजर अनुभव में सुधार करने योग्य लेआउट और यूजर अनुभव सुधार
  12. // @description:ja-JA YouTube Music™のレイアウトとユーザーエクスペリエンスの改善を設定可能にする
  13. // @description:pt-BR Melhorias configuráveis no layout e na experiência do usuário para o YouTube Music™
  14. // @description:zh-CN 可配置的布局和YouTube Music™的用户体验改进
  15. // @homepageURL https://github.com/Sv443/BetterYTM#readme
  16. // @supportURL https://github.com/Sv443/BetterYTM/issues
  17. // @license AGPL-3.0-only
  18. // @author Sv443
  19. // @copyright Sv443 (https://github.com/Sv443)
  20. // @icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=19c129f
  21. // @match https://music.youtube.com/*
  22. // @match https://www.youtube.com/*
  23. // @run-at document-start
  24. // @downloadURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
  25. // @updateURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
  26. // @connect api.sv443.net
  27. // @connect github.com
  28. // @connect raw.githubusercontent.com
  29. // @grant GM.getValue
  30. // @grant GM.setValue
  31. // @grant GM.deleteValue
  32. // @grant GM.getResourceUrl
  33. // @grant GM.setClipboard
  34. // @grant GM.xmlHttpRequest
  35. // @grant GM.openInTab
  36. // @grant unsafeWindow
  37. // @noframes
  38. // @resource css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=19c129f
  39. // @resource css-fix_spacing https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=19c129f
  40. // @resource doc-changelog https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=19c129f
  41. // @resource icon-advanced_mode https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=19c129f
  42. // @resource icon-arrow_down https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=19c129f
  43. // @resource icon-delete https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=19c129f
  44. // @resource icon-error https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=19c129f
  45. // @resource icon-experimental https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=19c129f
  46. // @resource icon-globe https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=19c129f
  47. // @resource icon-help https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=19c129f
  48. // @resource icon-image https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image.svg?b=19c129f
  49. // @resource icon-image_filled https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image_filled.svg?b=19c129f
  50. // @resource icon-link https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/link.svg?b=19c129f
  51. // @resource icon-lyrics https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=19c129f
  52. // @resource icon-skip_to https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=19c129f
  53. // @resource icon-spinner https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=19c129f
  54. // @resource img-logo https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=19c129f
  55. // @resource img-close https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=19c129f
  56. // @resource img-discord https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=19c129f
  57. // @resource img-github https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=19c129f
  58. // @resource img-greasyfork https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=19c129f
  59. // @resource img-openuserjs https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=19c129f
  60. // @resource trans-de_DE https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=19c129f
  61. // @resource trans-en_US https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=19c129f
  62. // @resource trans-en_UK https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=19c129f
  63. // @resource trans-es_ES https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=19c129f
  64. // @resource trans-fr_FR https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=19c129f
  65. // @resource trans-hi_IN https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=19c129f
  66. // @resource trans-ja_JA https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=19c129f
  67. // @resource trans-pt_BR https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=19c129f
  68. // @resource trans-zh_CN https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=19c129f
  69. // @require https://cdn.jsdelivr.net/npm/@sv443-network/[email protected]/dist/index.global.js
  70. // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.basic.js
  71. // @require https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
  72. // @grant GM.registerMenuCommand
  73. // @grant GM.listValues
  74. // ==/UserScript==
  75. /*
  76. ▄▄▄ ▄ ▄▄▄▄▄▄ ▄
  77. █ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
  78. █▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
  79. █▄▄▀ ▀▄▄ ▀▄▄ ▀▄▄ ▀▄▄ █ █ █ █ █
  80. Made with ❤️ by Sv443
  81. I welcome every contribution on GitHub!
  82. https://github.com/Sv443/BetterYTM
  83. */
  84. /* Disclaimer: I am not affiliated with or endorsed by YouTube, Google, Alphabet, Genius or anyone else */
  85. /* C&D this 🖕 */
  86. (function(UserUtils,marked,Fuse){'use strict';function _interopNamespaceDefault(e){var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var UserUtils__namespace=/*#__PURE__*/_interopNamespaceDefault(UserUtils);/******************************************************************************
  87. Copyright (c) Microsoft Corporation.
  88. Permission to use, copy, modify, and/or distribute this software for any
  89. purpose with or without fee is hereby granted.
  90. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
  91. REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
  92. AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
  93. INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
  94. LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
  95. OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
  96. PERFORMANCE OF THIS SOFTWARE.
  97. ***************************************************************************** */
  98. /* global Reflect, Promise, SuppressedError, Symbol */
  99. function __rest(s, e) {
  100. var t = {};
  101. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
  102. t[p] = s[p];
  103. if (s != null && typeof Object.getOwnPropertySymbols === "function")
  104. for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  105. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
  106. t[p[i]] = s[p[i]];
  107. }
  108. return t;
  109. }
  110. function __awaiter(thisArg, _arguments, P, generator) {
  111. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  112. return new (P || (P = Promise))(function (resolve, reject) {
  113. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  114. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  115. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  116. step((generator = generator.apply(thisArg, _arguments || [])).next());
  117. });
  118. }
  119. function __values(o) {
  120. var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
  121. if (m) return m.call(o);
  122. if (o && typeof o.length === "number") return {
  123. next: function () {
  124. if (o && i >= o.length) o = void 0;
  125. return { value: o && o[i++], done: !o };
  126. }
  127. };
  128. throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
  129. }
  130. function __asyncValues(o) {
  131. if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
  132. var m = o[Symbol.asyncIterator], i;
  133. return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
  134. function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
  135. function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
  136. }
  137. typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
  138. var e = new Error(message);
  139. return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
  140. };let createNanoEvents = () => ({
  141. emit(event, ...args) {
  142. for (
  143. let i = 0,
  144. callbacks = this.events[event] || [],
  145. length = callbacks.length;
  146. i < length;
  147. i++
  148. ) {
  149. callbacks[i](...args);
  150. }
  151. },
  152. events: {},
  153. on(event, cb) {
  154. (this.events[event] ||= []).push(cb);
  155. return () => {
  156. this.events[event] = this.events[event]?.filter(i => cb !== i);
  157. }
  158. }
  159. });// I know TS enums are impure but it doesn't really matter here, plus they look cooler
  160. var LogLevel;
  161. (function (LogLevel) {
  162. LogLevel[LogLevel["Debug"] = 0] = "Debug";
  163. LogLevel[LogLevel["Info"] = 1] = "Info";
  164. })(LogLevel || (LogLevel = {}));
  165. //#MARKER plugins
  166. /**
  167. * Intents (permissions) BYTM has to grant your plugin for it to be able to access certain features.
  168. * TODO: this feature is unfinished, but you should still specify the intents your plugin needs.
  169. */
  170. var PluginIntent;
  171. (function (PluginIntent) {
  172. /** Plugin has access to hidden config values */
  173. PluginIntent[PluginIntent["HiddenConfigValues"] = 1] = "HiddenConfigValues";
  174. /** Plugin can write to the feature configuration */
  175. PluginIntent[PluginIntent["WriteFeatureConfig"] = 2] = "WriteFeatureConfig";
  176. /** Plugin can write to the lyrics cache */
  177. PluginIntent[PluginIntent["WriteLyricsCache"] = 4] = "WriteLyricsCache";
  178. /** Plugin can add new translations and overwrite existing ones */
  179. PluginIntent[PluginIntent["WriteTranslations"] = 8] = "WriteTranslations";
  180. /** Plugin can create modal dialogs */
  181. PluginIntent[PluginIntent["CreateModalDialogs"] = 16] = "CreateModalDialogs";
  182. })(PluginIntent || (PluginIntent = {}));const modeRaw = "development";
  183. const branchRaw = "develop";
  184. const hostRaw = "github";
  185. const buildNumberRaw = "19c129f";
  186. /** The mode in which the script was built (production or development) */
  187. const mode = (modeRaw.match(/^#{{.+}}$/) ? "production" : modeRaw);
  188. /** The branch to use in various URLs that point to the GitHub repo */
  189. const branch = (branchRaw.match(/^#{{.+}}$/) ? "main" : branchRaw);
  190. /** Path to the GitHub repo */
  191. const repo = "Sv443/BetterYTM";
  192. /** Which host the userscript was installed from */
  193. const host = (hostRaw.match(/^#{{.+}}$/) ? "github" : hostRaw);
  194. /** The build number of the userscript */
  195. const buildNumber = (buildNumberRaw.match(/^#{{.+}}$/) ? "BUILD_ERROR!" : buildNumberRaw); // asserted as generic string instead of literal
  196. /** Default compression format used throughout BYTM */
  197. const compressionFormat = "deflate-raw";
  198. typeof sessionStorage !== "undefined"
  199. && (() => {
  200. try {
  201. const key = `_bytm_${UserUtils.randomId(4)}`;
  202. sessionStorage.setItem(key, "test");
  203. sessionStorage.removeItem(key);
  204. return true;
  205. }
  206. catch (_a) {
  207. return false;
  208. }
  209. })();
  210. /**
  211. * How much info should be logged to the devtools console
  212. * 0 = Debug (show everything) or 1 = Info (show only important stuff)
  213. */
  214. const defaultLogLevel = mode === "production" ? LogLevel.Info : LogLevel.Debug;
  215. /** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
  216. const scriptInfo = {
  217. name: GM.info.script.name,
  218. version: GM.info.script.version,
  219. namespace: GM.info.script.namespace,
  220. };var de_DE = {
  221. name: "Deutsch (Deutschland)",
  222. nameEnglish: "German",
  223. emoji: "🇩🇪",
  224. userscriptDesc: "Konfigurierbare Layout- und Benutzererfahrungs-Verbesserungen für YouTube Music™",
  225. authors: [
  226. "Sv443"
  227. ]
  228. };
  229. var en_US = {
  230. name: "English (United States)",
  231. nameEnglish: "English (US)",
  232. emoji: "🇺🇸",
  233. userscriptDesc: "Configurable layout and user experience improvements for YouTube Music™",
  234. authors: [
  235. "Sv443"
  236. ]
  237. };
  238. var en_UK = {
  239. name: "English (United Kingdom)",
  240. nameEnglish: "English (UK)",
  241. emoji: "🇬🇧",
  242. userscriptDesc: "Configurable layout and user experience improvements for YouTube Music™",
  243. authors: [
  244. "Sv443"
  245. ]
  246. };
  247. var es_ES = {
  248. name: "Español (España)",
  249. nameEnglish: "Spanish",
  250. emoji: "🇪🇸",
  251. userscriptDesc: "Mejoras de diseño y experiencia de usuario configurables para YouTube Music™",
  252. authors: [
  253. "Sv443"
  254. ]
  255. };
  256. var fr_FR = {
  257. name: "Français (France)",
  258. nameEnglish: "French",
  259. emoji: "🇫🇷",
  260. userscriptDesc: "Améliorations de la mise en page et de l'expérience utilisateur configurables pour YouTube Music™",
  261. authors: [
  262. "Sv443"
  263. ]
  264. };
  265. var hi_IN = {
  266. name: "हिंदी (भारत)",
  267. nameEnglish: "Hindi",
  268. emoji: "🇮🇳",
  269. userscriptDesc: "YouTube Music™ के लिए विन्यास और यूजर अनुभव में सुधार करने योग्य लेआउट और यूजर अनुभव सुधार",
  270. authors: [
  271. "Sv443"
  272. ]
  273. };
  274. var ja_JA = {
  275. name: "日本語 (日本)",
  276. nameEnglish: "Japanese",
  277. emoji: "🇯🇵",
  278. userscriptDesc: "YouTube Music™のレイアウトとユーザーエクスペリエンスの改善を設定可能にする",
  279. authors: [
  280. "Sv443"
  281. ]
  282. };
  283. var pt_BR = {
  284. name: "Português (Brasil)",
  285. nameEnglish: "Portuguese",
  286. emoji: "🇵🇹",
  287. userscriptDesc: "Melhorias configuráveis no layout e na experiência do usuário para o YouTube Music™",
  288. authors: [
  289. "Sv443"
  290. ]
  291. };
  292. var zh_CN = {
  293. name: "中文(简化,中国)",
  294. nameEnglish: "Chinese (simpl.)",
  295. emoji: "🇨🇳",
  296. userscriptDesc: "可配置的布局和YouTube Music™的用户体验改进",
  297. authors: [
  298. "Sv443"
  299. ]
  300. };
  301. var locales = {
  302. de_DE: de_DE,
  303. en_US: en_US,
  304. en_UK: en_UK,
  305. es_ES: es_ES,
  306. fr_FR: fr_FR,
  307. hi_IN: hi_IN,
  308. ja_JA: ja_JA,
  309. pt_BR: pt_BR,
  310. zh_CN: zh_CN
  311. };/** A fraction of this max value will be removed from the "last viewed" timestamp when adding penalized cache entries */
  312. const maxViewedPenalty = 1000 * 60 * 60 * 24 * 5; // 5 days
  313. /** A fraction of this max value will be removed from the "added" timestamp when adding penalized cache entries */
  314. const maxAddedPenalty = 1000 * 60 * 60 * 24 * 15; // 15 days
  315. let canCompress$1 = true;
  316. const lyricsCacheMgr = new UserUtils.DataStore({
  317. id: "bytm-lyrics-cache",
  318. defaultData: {
  319. cache: [],
  320. },
  321. formatVersion: 1,
  322. encodeData: (data) => canCompress$1 ? UserUtils.compress(data, compressionFormat, "string") : data,
  323. decodeData: (data) => canCompress$1 ? UserUtils.decompress(data, compressionFormat, "string") : data,
  324. });
  325. function initLyricsCache() {
  326. return __awaiter(this, void 0, void 0, function* () {
  327. canCompress$1 = yield compressionSupported();
  328. const data = yield lyricsCacheMgr.loadData();
  329. log(`Loaded lyrics cache (${data.cache.length} entries):`, data);
  330. emitInterface("bytm:lyricsCacheReady", data);
  331. return data;
  332. });
  333. }
  334. /**
  335. * Returns the cache entry for the passed artist and song, or undefined if it doesn't exist yet
  336. * {@linkcode artist} and {@linkcode song} need to be sanitized first!
  337. * @param refreshEntry If true, the timestamp of the entry will be set to the current time
  338. */
  339. function getLyricsCacheEntry(artist, song, refreshEntry = true) {
  340. const { cache } = lyricsCacheMgr.getData();
  341. const entry = cache.find(e => e.artist === artist && e.song === song);
  342. if (entry && Date.now() - (entry === null || entry === void 0 ? void 0 : entry.added) > getFeatures().lyricsCacheTTL * 1000 * 60 * 60 * 24) {
  343. deleteLyricsCacheEntry(artist, song);
  344. return undefined;
  345. }
  346. // refresh timestamp of the entry by mutating cache
  347. if (entry && refreshEntry)
  348. updateLyricsCacheEntry(artist, song);
  349. return entry;
  350. }
  351. /** Updates the "last viewed" timestamp of the cache entry for the passed artist and song */
  352. function updateLyricsCacheEntry(artist, song) {
  353. const { cache } = lyricsCacheMgr.getData();
  354. const idx = cache.findIndex(e => e.artist === artist && e.song === song);
  355. if (idx !== -1) {
  356. const newEntry = cache.splice(idx, 1)[0];
  357. newEntry.viewed = Date.now();
  358. log("Updating cache entry for", artist, "-", song, "to", newEntry);
  359. lyricsCacheMgr.setData({ cache: [newEntry, ...cache] });
  360. }
  361. }
  362. /** Deletes the cache entry for the passed artist and song */
  363. function deleteLyricsCacheEntry(artist, song) {
  364. const { cache } = lyricsCacheMgr.getData();
  365. const idx = cache.findIndex(e => e.artist === artist && e.song === song);
  366. if (idx !== -1) {
  367. cache.splice(idx, 1);
  368. lyricsCacheMgr.setData({ cache });
  369. }
  370. }
  371. /** Clears the lyrics cache locally and clears it in persistent storage */
  372. function clearLyricsCache() {
  373. emitInterface("bytm:lyricsCacheCleared");
  374. return lyricsCacheMgr.setData({ cache: [] });
  375. }
  376. /** Returns the full lyrics cache array */
  377. function getLyricsCache() {
  378. return lyricsCacheMgr.getData().cache;
  379. }
  380. /**
  381. * Adds the provided "best" (non-penalized) entry into the lyrics URL cache, synchronously to RAM and asynchronously to GM storage
  382. * {@linkcode artist} and {@linkcode song} need to be sanitized first!
  383. */
  384. function addLyricsCacheEntryBest(artist, song, url) {
  385. // refresh entry if it exists and don't overwrite / duplicate it
  386. const cachedEntry = getLyricsCacheEntry(artist, song, true);
  387. if (cachedEntry)
  388. return;
  389. const { cache } = lyricsCacheMgr.getData();
  390. const entry = {
  391. artist, song, url, viewed: Date.now(), added: Date.now(),
  392. };
  393. cache.push(entry);
  394. cache.sort((a, b) => b.viewed - a.viewed);
  395. if (cache.length > getFeatures().lyricsCacheMaxSize)
  396. cache.pop();
  397. log("Added cache entry for best result", artist, "-", song, "\n", entry);
  398. emitInterface("bytm:lyricsCacheEntryAdded", { entry, type: "best" });
  399. return lyricsCacheMgr.setData({ cache });
  400. }
  401. /**
  402. * Adds the provided entry into the lyrics URL cache, synchronously to RAM and asynchronously to GM storage
  403. * Also adds a penalty to the viewed timestamp and added timestamp to decrease entry's lifespan in cache
  404. *
  405. * ⚠️ {@linkcode artist} and {@linkcode song} need to be sanitized first!
  406. * @param penaltyFr Fraction to remove from the timestamp values - has to be between 0 and 1 - default is 0 (no penalty) - (0.25 = only penalized by a quarter of the predefined max penalty)
  407. */
  408. function addLyricsCacheEntryPenalized(artist, song, url, penaltyFr = 0) {
  409. // refresh entry if it exists and don't overwrite / duplicate it
  410. const cachedEntry = getLyricsCacheEntry(artist, song, true);
  411. if (cachedEntry)
  412. return;
  413. const { cache } = lyricsCacheMgr.getData();
  414. penaltyFr = UserUtils.clamp(penaltyFr, 0, 1);
  415. const viewedPenalty = maxViewedPenalty * penaltyFr;
  416. const addedPenalty = maxAddedPenalty * penaltyFr;
  417. const entry = {
  418. artist,
  419. song,
  420. url,
  421. viewed: Date.now() - viewedPenalty,
  422. added: Date.now() - addedPenalty,
  423. };
  424. cache.push(entry);
  425. cache.sort((a, b) => b.viewed - a.viewed);
  426. if (cache.length > getFeatures().lyricsCacheMaxSize)
  427. cache.pop();
  428. log("Added penalized cache entry for", artist, "-", song, "with penalty fraction", penaltyFr, "\n", entry);
  429. emitInterface("bytm:lyricsCacheEntryAdded", { entry, type: "penalized" });
  430. return lyricsCacheMgr.setData({ cache });
  431. }/** Abstract class that can be extended to create an event emitter with helper methods and a strongly typed event map */
  432. class NanoEmitter {
  433. constructor(settings = {}) {
  434. Object.defineProperty(this, "events", {
  435. enumerable: true,
  436. configurable: true,
  437. writable: true,
  438. value: createNanoEvents()
  439. });
  440. Object.defineProperty(this, "eventUnsubscribes", {
  441. enumerable: true,
  442. configurable: true,
  443. writable: true,
  444. value: []
  445. });
  446. Object.defineProperty(this, "emitterSettings", {
  447. enumerable: true,
  448. configurable: true,
  449. writable: true,
  450. value: void 0
  451. });
  452. this.emitterSettings = Object.assign({ publicEmit: false }, settings);
  453. }
  454. /** Subscribes to an event - returns a function that unsubscribes the event listener */
  455. on(event, cb) {
  456. // eslint-disable-next-line prefer-const
  457. let unsub;
  458. const unsubProxy = () => {
  459. if (!unsub)
  460. return;
  461. unsub();
  462. this.eventUnsubscribes = this.eventUnsubscribes.filter(u => u !== unsub);
  463. };
  464. unsub = this.events.on(event, cb);
  465. this.eventUnsubscribes.push(unsub);
  466. return unsubProxy;
  467. }
  468. /** Subscribes to an event and calls the callback or resolves the Promise only once */
  469. once(event, cb) {
  470. return new Promise((resolve) => {
  471. // eslint-disable-next-line prefer-const
  472. let unsub;
  473. const onceProxy = ((...args) => {
  474. unsub === null || unsub === void 0 ? void 0 : unsub();
  475. cb === null || cb === void 0 ? void 0 : cb(...args);
  476. resolve(args);
  477. });
  478. // eslint-disable-next-line prefer-const
  479. unsub = this.on(event, onceProxy);
  480. });
  481. }
  482. /** Emits an event on this instance - Needs `publicEmit` to be set to true in the constructor! */
  483. emit(event, ...args) {
  484. if (this.emitterSettings.publicEmit) {
  485. this.events.emit(event, ...args);
  486. return true;
  487. }
  488. return false;
  489. }
  490. /** Unsubscribes all event listeners */
  491. unsubscribeAll() {
  492. for (const unsub of this.eventUnsubscribes)
  493. unsub();
  494. this.eventUnsubscribes = [];
  495. }
  496. }const fetchOpts = {
  497. timeout: 10000,
  498. };
  499. /** Contains all translation keys of all initialized and loaded translations */
  500. const allTrKeys = new Map();
  501. /** Contains the identifiers of all initialized and loaded translation locales */
  502. const initializedLocales = new Set();
  503. /** Initializes the translations */
  504. function initTranslations(locale) {
  505. var _a;
  506. return __awaiter(this, void 0, void 0, function* () {
  507. if (initializedLocales.has(locale))
  508. return;
  509. initializedLocales.add(locale);
  510. try {
  511. const transUrl = yield getResourceUrl(`trans-${locale}`);
  512. const transFile = yield (yield UserUtils.fetchAdvanced(transUrl, fetchOpts)).json();
  513. // merge with base translations if specified
  514. const baseTransUrl = transFile.base ? yield getResourceUrl(`trans-${transFile.base}`) : undefined;
  515. const baseTransFile = baseTransUrl ? yield (yield UserUtils.fetchAdvanced(baseTransUrl, fetchOpts)).json() : undefined;
  516. const translations = Object.assign(Object.assign({}, ((_a = baseTransFile === null || baseTransFile === void 0 ? void 0 : baseTransFile.translations) !== null && _a !== void 0 ? _a : {})), transFile.translations);
  517. UserUtils.tr.addLanguage(locale, translations);
  518. allTrKeys.set(locale, new Set(Object.keys(translations)));
  519. info(`Loaded translations for locale '${locale}'`);
  520. }
  521. catch (err) {
  522. const errStr = `Couldn't load translations for locale '${locale}'`;
  523. error(errStr, err);
  524. throw new Error(errStr);
  525. }
  526. });
  527. }
  528. /** Sets the current language for translations */
  529. function setLocale(locale) {
  530. UserUtils.tr.setLanguage(locale);
  531. setGlobalProp("locale", locale);
  532. emitInterface("bytm:setLocale", { locale });
  533. }
  534. /** Returns the currently set language */
  535. function getLocale() {
  536. return UserUtils.tr.getLanguage();
  537. }
  538. /** Returns whether the given translation key exists in the current locale */
  539. function hasKey(key) {
  540. return hasKeyFor(getLocale(), key);
  541. }
  542. /** Returns whether the given translation key exists in the given locale */
  543. function hasKeyFor(locale, key) {
  544. var _a, _b;
  545. return (_b = (_a = allTrKeys.get(locale)) === null || _a === void 0 ? void 0 : _a.has(key)) !== null && _b !== void 0 ? _b : false;
  546. }
  547. /** Returns the translated string for the given key, after optionally inserting values */
  548. function t(key, ...values) {
  549. return UserUtils.tr(key, ...values);
  550. }
  551. /**
  552. * Returns the translated string for the given key with an added pluralization identifier based on the passed `num`
  553. * Tries to fall back to the non-pluralized syntax if no translation was found
  554. */
  555. function tp(key, num, ...values) {
  556. if (typeof num !== "number")
  557. num = num.length;
  558. const plNum = num === 1 ? "1" : "n";
  559. const trans = t(`${key}-${plNum}`, ...values);
  560. if (trans === key)
  561. return t(key, ...values);
  562. return trans;
  563. }/** ID of the last opened (top-most) dialog */
  564. let lastDialogId = null;
  565. /** Creates and manages a modal dialog element */
  566. class BytmDialog extends NanoEmitter {
  567. constructor(options) {
  568. super();
  569. Object.defineProperty(this, "options", {
  570. enumerable: true,
  571. configurable: true,
  572. writable: true,
  573. value: void 0
  574. });
  575. Object.defineProperty(this, "id", {
  576. enumerable: true,
  577. configurable: true,
  578. writable: true,
  579. value: void 0
  580. });
  581. Object.defineProperty(this, "dialogOpen", {
  582. enumerable: true,
  583. configurable: true,
  584. writable: true,
  585. value: false
  586. });
  587. Object.defineProperty(this, "dialogMounted", {
  588. enumerable: true,
  589. configurable: true,
  590. writable: true,
  591. value: false
  592. });
  593. Object.defineProperty(this, "listenersAttached", {
  594. enumerable: true,
  595. configurable: true,
  596. writable: true,
  597. value: false
  598. });
  599. this.options = Object.assign({ closeOnBgClick: true, closeOnEscPress: true, closeBtnEnabled: true, destroyOnClose: false, smallHeader: false }, options);
  600. this.id = options.id;
  601. }
  602. //#MARKER public
  603. /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
  604. mount() {
  605. return __awaiter(this, void 0, void 0, function* () {
  606. if (this.dialogMounted)
  607. return;
  608. this.dialogMounted = true;
  609. const bgElem = document.createElement("div");
  610. bgElem.id = `bytm-${this.id}-dialog-bg`;
  611. bgElem.classList.add("bytm-dialog-bg");
  612. if (this.options.closeOnBgClick)
  613. bgElem.ariaLabel = bgElem.title = t("close_menu_tooltip");
  614. bgElem.style.visibility = "hidden";
  615. bgElem.style.display = "none";
  616. bgElem.inert = true;
  617. bgElem.appendChild(yield this.getDialogContent());
  618. document.body.appendChild(bgElem);
  619. this.attachListeners(bgElem);
  620. UserUtils.addGlobalStyle(`\
  621. #bytm-${this.id}-dialog-bg {
  622. --bytm-dialog-width-max: ${this.options.maxWidth}px;
  623. --bytm-dialog-height-max: ${this.options.maxHeight}px;
  624. }`).id = `bytm-style-dialog-${this.id}`;
  625. this.events.emit("render");
  626. return bgElem;
  627. });
  628. }
  629. /** Clears all dialog contents (unmounts them from the DOM) in preparation for a new rendering call */
  630. unmount() {
  631. var _a;
  632. this.dialogMounted = false;
  633. const clearSelectors = [
  634. `#bytm-${this.id}-dialog-bg`,
  635. `#bytm-style-dialog-${this.id}`,
  636. ];
  637. for (const sel of clearSelectors) {
  638. const elem = document.querySelector(sel);
  639. (elem === null || elem === void 0 ? void 0 : elem.hasChildNodes()) && clearInner(elem);
  640. (_a = document.querySelector(sel)) === null || _a === void 0 ? void 0 : _a.remove();
  641. }
  642. this.events.emit("clear");
  643. }
  644. /** Clears the DOM of the dialog and then renders it again */
  645. remount() {
  646. return __awaiter(this, void 0, void 0, function* () {
  647. this.unmount();
  648. yield this.mount();
  649. });
  650. }
  651. /**
  652. * Opens the dialog - also mounts it if it hasn't been mounted yet
  653. * Prevents default action and immediate propagation of the passed event
  654. */
  655. open(e) {
  656. var _a;
  657. return __awaiter(this, void 0, void 0, function* () {
  658. e === null || e === void 0 ? void 0 : e.preventDefault();
  659. e === null || e === void 0 ? void 0 : e.stopImmediatePropagation();
  660. if (this.isOpen())
  661. return;
  662. this.dialogOpen = true;
  663. if (!this.isMounted())
  664. yield this.mount();
  665. document.body.classList.add("bytm-disable-scroll");
  666. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
  667. const dialogBg = document.querySelector(`#bytm-${this.id}-dialog-bg`);
  668. if (!dialogBg)
  669. return warn(`Couldn't find background element for dialog with ID '${this.id}'`);
  670. dialogBg.style.visibility = "visible";
  671. dialogBg.style.display = "block";
  672. dialogBg.inert = false;
  673. lastDialogId = this.id;
  674. this.events.emit("open");
  675. emitInterface("bytm:dialogOpened", this);
  676. emitInterface(`bytm:dialogOpened:${this.id}`, this);
  677. return dialogBg;
  678. });
  679. }
  680. /** Closes the dialog - prevents default action and immediate propagation of the passed event */
  681. close(e) {
  682. var _a;
  683. e === null || e === void 0 ? void 0 : e.preventDefault();
  684. e === null || e === void 0 ? void 0 : e.stopImmediatePropagation();
  685. if (!this.isOpen())
  686. return;
  687. this.dialogOpen = false;
  688. document.body.classList.remove("bytm-disable-scroll");
  689. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
  690. const dialogBg = document.querySelector(`#bytm-${this.id}-dialog-bg`);
  691. if (!dialogBg)
  692. return warn(`Couldn't find background element for dialog with ID '${this.id}'`);
  693. dialogBg.style.visibility = "hidden";
  694. dialogBg.style.display = "none";
  695. dialogBg.inert = true;
  696. if (BytmDialog.getLastDialogId() === this.id)
  697. lastDialogId = null;
  698. this.events.emit("close");
  699. if (this.options.destroyOnClose)
  700. this.destroy();
  701. }
  702. /** Returns true if the dialog is currently open */
  703. isOpen() {
  704. return this.dialogOpen;
  705. }
  706. /** Returns true if the dialog is currently mounted */
  707. isMounted() {
  708. return this.dialogMounted;
  709. }
  710. /** Clears the DOM of the dialog and removes all event listeners */
  711. destroy() {
  712. this.unmount();
  713. this.events.emit("destroy");
  714. this.unsubscribeAll();
  715. }
  716. //#MARKER static
  717. /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
  718. static getLastDialogId() {
  719. return lastDialogId;
  720. }
  721. //#MARKER protected
  722. /** Called once to attach all generic event listeners */
  723. attachListeners(bgElem) {
  724. if (this.listenersAttached)
  725. return;
  726. this.listenersAttached = true;
  727. if (this.options.closeOnBgClick) {
  728. bgElem.addEventListener("click", (e) => {
  729. var _a;
  730. if (this.isOpen() && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === `bytm-${this.id}-dialog-bg`)
  731. this.close(e);
  732. });
  733. }
  734. if (this.options.closeOnEscPress) {
  735. document.body.addEventListener("keydown", (e) => {
  736. if (e.key === "Escape" && this.isOpen() && BytmDialog.getLastDialogId() === this.id)
  737. this.close(e);
  738. });
  739. }
  740. }
  741. //#MARKER private
  742. /** Returns the dialog content element and all its children */
  743. getDialogContent() {
  744. var _a, _b, _c, _d;
  745. return __awaiter(this, void 0, void 0, function* () {
  746. const header = (_b = (_a = this.options).renderHeader) === null || _b === void 0 ? void 0 : _b.call(_a);
  747. const footer = (_d = (_c = this.options).renderFooter) === null || _d === void 0 ? void 0 : _d.call(_c);
  748. const dialogWrapperEl = document.createElement("div");
  749. dialogWrapperEl.id = `bytm-${this.id}-dialog`;
  750. dialogWrapperEl.classList.add("bytm-dialog");
  751. dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
  752. //#SECTION header
  753. const headerWrapperEl = document.createElement("div");
  754. headerWrapperEl.classList.add("bytm-dialog-header");
  755. this.options.smallDialog && headerWrapperEl.classList.add("small");
  756. if (header) {
  757. const headerTitleWrapperEl = document.createElement("div");
  758. headerTitleWrapperEl.classList.add("bytm-dialog-title-wrapper");
  759. headerTitleWrapperEl.role = "heading";
  760. headerTitleWrapperEl.ariaLevel = "1";
  761. headerTitleWrapperEl.appendChild(header instanceof Promise ? yield header : header);
  762. headerWrapperEl.appendChild(headerTitleWrapperEl);
  763. }
  764. else {
  765. // insert element to pad the header height
  766. const padEl = document.createElement("div");
  767. padEl.classList.add("bytm-dialog-header-pad", this.options.smallDialog ? "small" : "");
  768. headerWrapperEl.appendChild(padEl);
  769. }
  770. if (this.options.closeBtnEnabled) {
  771. const closeBtnEl = document.createElement("img");
  772. closeBtnEl.classList.add("bytm-dialog-close");
  773. this.options.smallDialog && closeBtnEl.classList.add("small");
  774. closeBtnEl.src = yield getResourceUrl("img-close");
  775. closeBtnEl.role = "button";
  776. closeBtnEl.tabIndex = 0;
  777. closeBtnEl.addEventListener("click", () => this.close());
  778. headerWrapperEl.appendChild(closeBtnEl);
  779. }
  780. dialogWrapperEl.appendChild(headerWrapperEl);
  781. //#SECTION body
  782. const menuBodyElem = document.createElement("div");
  783. menuBodyElem.id = `bytm-${this.id}-dialog-body`;
  784. menuBodyElem.classList.add("bytm-dialog-body");
  785. this.options.smallDialog && menuBodyElem.classList.add("small");
  786. const body = this.options.renderBody();
  787. menuBodyElem.appendChild(body instanceof Promise ? yield body : body);
  788. dialogWrapperEl.appendChild(menuBodyElem);
  789. //#SECTION footer
  790. if (footer) {
  791. const footerWrapper = document.createElement("div");
  792. footerWrapper.classList.add("bytm-dialog-footer-cont");
  793. dialogWrapperEl.appendChild(footerWrapper);
  794. footerWrapper.appendChild(footer instanceof Promise ? yield footer : footer);
  795. }
  796. return dialogWrapperEl;
  797. });
  798. }
  799. }/**
  800. * Creates a generic button element.
  801. * If `href` is provided, the button will be an anchor element.
  802. * If `onClick` is provided, the button will be a div element.
  803. */
  804. function createGenericBtn({ resourceName, title, href, onClick, }) {
  805. return __awaiter(this, void 0, void 0, function* () {
  806. let btnElem;
  807. if (href) {
  808. btnElem = document.createElement("a");
  809. btnElem.href = href;
  810. btnElem.role = "button";
  811. btnElem.target = "_blank";
  812. btnElem.rel = "noopener noreferrer";
  813. }
  814. else {
  815. btnElem = document.createElement("div");
  816. onClick && onInteraction(btnElem, onClick);
  817. }
  818. btnElem.classList.add("bytm-generic-btn");
  819. btnElem.ariaLabel = btnElem.title = title;
  820. const imgElem = document.createElement("img");
  821. imgElem.classList.add("bytm-generic-btn-img");
  822. imgElem.src = yield getResourceUrl(resourceName);
  823. btnElem.appendChild(imgElem);
  824. return btnElem;
  825. });
  826. }/** Array of all site events */
  827. const allSiteEvents = [
  828. "configChanged",
  829. "configOptionChanged",
  830. "rebuildCfgMenu",
  831. "cfgMenuClosed",
  832. "welcomeMenuClosed",
  833. "hotkeyInputActive",
  834. "queueChanged",
  835. "autoplayQueueChanged",
  836. "songTitleChanged",
  837. "watchIdChanged",
  838. ];
  839. /** EventEmitter instance that is used to detect changes to the site */
  840. const siteEvents = createNanoEvents();
  841. let observers = [];
  842. /** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
  843. function initSiteEvents() {
  844. return __awaiter(this, void 0, void 0, function* () {
  845. try {
  846. //#SECTION queue
  847. // the queue container always exists so it doesn't need an extra init function
  848. const queueObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
  849. if (addedNodes.length > 0 || removedNodes.length > 0) {
  850. info(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  851. emitSiteEvent("queueChanged", target);
  852. }
  853. });
  854. // only observe added or removed elements
  855. addSelectorListener("sidePanel", "#contents.ytmusic-player-queue", {
  856. listener: (el) => {
  857. queueObs.observe(el, {
  858. childList: true,
  859. });
  860. },
  861. });
  862. const autoplayObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
  863. if (addedNodes.length > 0 || removedNodes.length > 0) {
  864. info(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  865. emitSiteEvent("autoplayQueueChanged", target);
  866. }
  867. });
  868. addSelectorListener("sidePanel", "ytmusic-player-queue #automix-contents", {
  869. listener: (el) => {
  870. autoplayObs.observe(el, {
  871. childList: true,
  872. });
  873. },
  874. });
  875. //#SECTION player bar
  876. let lastTitle = null;
  877. let initialPlay = true;
  878. addSelectorListener("playerBarInfo", "yt-formatted-string.title", {
  879. continuous: true,
  880. listener: (titleElem) => {
  881. const oldTitle = lastTitle;
  882. const newTitle = titleElem.textContent;
  883. if (newTitle === lastTitle || !newTitle)
  884. return;
  885. lastTitle = newTitle;
  886. info(`Detected song change - old title: "${oldTitle}" - new title: "${newTitle}" - initial play: ${initialPlay}`);
  887. emitSiteEvent("songTitleChanged", newTitle, oldTitle, initialPlay);
  888. initialPlay = false;
  889. },
  890. });
  891. info("Successfully initialized SiteEvents observers");
  892. observers = observers.concat([
  893. queueObs,
  894. autoplayObs,
  895. ]);
  896. //#SECTION other
  897. let lastWatchId = null;
  898. const checkWatchId = () => {
  899. if (location.pathname.startsWith("/watch")) {
  900. const newWatchId = new URL(location.href).searchParams.get("v");
  901. if (newWatchId && newWatchId !== lastWatchId) {
  902. info(`Detected watch ID change - old ID: "${lastWatchId}" - new ID: "${newWatchId}"`);
  903. emitSiteEvent("watchIdChanged", newWatchId, lastWatchId);
  904. lastWatchId = newWatchId;
  905. }
  906. }
  907. setTimeout(checkWatchId, 200);
  908. };
  909. window.addEventListener("bytm:ready", () => checkWatchId(), { once: true });
  910. }
  911. catch (err) {
  912. error("Couldn't initialize SiteEvents observers due to an error:\n", err);
  913. }
  914. });
  915. }
  916. /** Emits a site event with the given key and arguments */
  917. function emitSiteEvent(key, ...args) {
  918. siteEvents.emit(key, ...args);
  919. emitInterface(`bytm:siteEvent:${key}`, args);
  920. }let initialHotkey;
  921. /** Creates a hotkey input element */
  922. function createHotkeyInput({ initialValue, onChange }) {
  923. var _a;
  924. initialHotkey = initialValue;
  925. const wrapperElem = document.createElement("div");
  926. wrapperElem.classList.add("bytm-hotkey-wrapper");
  927. const infoElem = document.createElement("span");
  928. infoElem.classList.add("bytm-hotkey-info");
  929. const inputElem = document.createElement("input");
  930. inputElem.type = "button";
  931. inputElem.classList.add("bytm-ftconf-input", "bytm-hotkey-input", "bytm-btn");
  932. inputElem.dataset.state = "inactive";
  933. inputElem.value = (_a = initialValue === null || initialValue === void 0 ? void 0 : initialValue.code) !== null && _a !== void 0 ? _a : t("hotkey_input_click_to_change");
  934. inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_change_tooltip");
  935. const resetElem = document.createElement("span");
  936. resetElem.classList.add("bytm-hotkey-reset", "bytm-link", "bytm-hidden");
  937. resetElem.role = "button";
  938. resetElem.tabIndex = 0;
  939. resetElem.textContent = `(${t("reset")})`;
  940. resetElem.ariaLabel = resetElem.title = t("reset");
  941. const deactivate = () => {
  942. var _a, _b;
  943. siteEvents.emit("hotkeyInputActive", false);
  944. const curVal = (_a = getFeatures().switchSitesHotkey) !== null && _a !== void 0 ? _a : initialValue;
  945. inputElem.value = (_b = curVal === null || curVal === void 0 ? void 0 : curVal.code) !== null && _b !== void 0 ? _b : t("hotkey_input_click_to_change");
  946. inputElem.dataset.state = "inactive";
  947. inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_change_tooltip");
  948. infoElem.innerHTML = curVal ? getHotkeyInfoHtml(curVal) : "";
  949. };
  950. const activate = () => {
  951. siteEvents.emit("hotkeyInputActive", true);
  952. inputElem.value = "< ... >";
  953. inputElem.dataset.state = "active";
  954. inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");
  955. };
  956. const resetClicked = (e) => {
  957. e.preventDefault();
  958. e.stopImmediatePropagation();
  959. onChange(initialValue);
  960. deactivate();
  961. inputElem.value = initialValue.code;
  962. infoElem.innerHTML = getHotkeyInfoHtml(initialValue);
  963. resetElem.classList.add("bytm-hidden");
  964. };
  965. onInteraction(resetElem, resetClicked);
  966. if (initialValue)
  967. infoElem.innerHTML = getHotkeyInfoHtml(initialValue);
  968. let lastKeyDown;
  969. const reservedKeys = ["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "Meta", "Tab", "Space", " "];
  970. document.addEventListener("keypress", (e) => {
  971. if (inputElem.dataset.state === "inactive")
  972. return;
  973. if ((lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.code) === e.code && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.shift) === e.shiftKey && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.ctrl) === e.ctrlKey && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.alt) === e.altKey)
  974. return;
  975. e.preventDefault();
  976. e.stopImmediatePropagation();
  977. const hotkey = {
  978. code: e.code,
  979. shift: e.shiftKey,
  980. ctrl: e.ctrlKey,
  981. alt: e.altKey,
  982. };
  983. inputElem.value = hotkey.code;
  984. inputElem.dataset.state = "inactive";
  985. infoElem.innerHTML = getHotkeyInfoHtml(hotkey);
  986. inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");
  987. onChange(hotkey);
  988. });
  989. document.addEventListener("keydown", (e) => {
  990. if (reservedKeys.filter(k => k !== "Tab").includes(e.code))
  991. return;
  992. if (inputElem.dataset.state !== "active")
  993. return;
  994. if (e.code === "Tab" || e.code === " " || e.code === "Space" || e.code === "Escape" || e.code === "Enter") {
  995. deactivate();
  996. return;
  997. }
  998. if (["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight"].includes(e.code))
  999. return;
  1000. e.preventDefault();
  1001. e.stopImmediatePropagation();
  1002. const hotkey = {
  1003. code: e.code,
  1004. shift: e.shiftKey,
  1005. ctrl: e.ctrlKey,
  1006. alt: e.altKey,
  1007. };
  1008. const keyChanged = (initialHotkey === null || initialHotkey === void 0 ? void 0 : initialHotkey.code) !== hotkey.code || (initialHotkey === null || initialHotkey === void 0 ? void 0 : initialHotkey.shift) !== hotkey.shift || (initialHotkey === null || initialHotkey === void 0 ? void 0 : initialHotkey.ctrl) !== hotkey.ctrl || (initialHotkey === null || initialHotkey === void 0 ? void 0 : initialHotkey.alt) !== hotkey.alt;
  1009. lastKeyDown = hotkey;
  1010. onChange(hotkey);
  1011. if (keyChanged) {
  1012. deactivate();
  1013. resetElem.classList.remove("bytm-hidden");
  1014. }
  1015. else
  1016. resetElem.classList.add("bytm-hidden");
  1017. inputElem.value = hotkey.code;
  1018. inputElem.dataset.state = "inactive";
  1019. infoElem.innerHTML = getHotkeyInfoHtml(hotkey);
  1020. });
  1021. siteEvents.on("cfgMenuClosed", deactivate);
  1022. inputElem.addEventListener("click", () => {
  1023. if (inputElem.dataset.state === "inactive")
  1024. activate();
  1025. else
  1026. deactivate();
  1027. });
  1028. inputElem.addEventListener("keydown", (e) => {
  1029. if (reservedKeys.includes(e.code))
  1030. return deactivate();
  1031. if (inputElem.dataset.state === "inactive")
  1032. activate();
  1033. });
  1034. wrapperElem.appendChild(resetElem);
  1035. wrapperElem.appendChild(infoElem);
  1036. wrapperElem.appendChild(inputElem);
  1037. return wrapperElem;
  1038. }
  1039. function getHotkeyInfoHtml(hotkey) {
  1040. const modifiers = [];
  1041. hotkey.ctrl && modifiers.push(`<kbd class="bytm-kbd">${t("hotkey_key_ctrl")}</kbd>`);
  1042. hotkey.shift && modifiers.push(`<kbd class="bytm-kbd">${t("hotkey_key_shift")}</kbd>`);
  1043. hotkey.alt && modifiers.push(`<kbd class="bytm-kbd">${getOS() === "mac" ? t("hotkey_key_mac_option") : t("hotkey_key_alt")}</kbd>`);
  1044. return `\
  1045. <div style="display: flex; align-items: center;">
  1046. <span>
  1047. ${modifiers.reduce((a, c) => `${a ? a + " " : ""}${c}`, "")}
  1048. </span>
  1049. <span style="padding: 0px 5px;">
  1050. ${modifiers.length > 0 ? "+" : ""}
  1051. </span>
  1052. </div>`;
  1053. }
  1054. /** Crude OS detection for keyboard layout purposes */
  1055. function getOS() {
  1056. if (navigator.userAgent.match(/mac(\s?os|intel)/i))
  1057. return "mac";
  1058. return "other";
  1059. }/** Creates a simple toggle element */
  1060. function createToggleInput({ onChange, initialValue = false, id = UserUtils.randomId(8, 26), labelPos = "left", }) {
  1061. return __awaiter(this, void 0, void 0, function* () {
  1062. const wrapperEl = document.createElement("div");
  1063. wrapperEl.classList.add("bytm-toggle-input-wrapper", "bytm-no-select");
  1064. wrapperEl.role = "switch";
  1065. wrapperEl.tabIndex = 0;
  1066. const labelEl = labelPos !== "off" && document.createElement("label");
  1067. if (labelEl) {
  1068. labelEl.classList.add("bytm-toggle-input-label");
  1069. labelEl.textContent = t(`toggled_${initialValue ? "on" : "off"}`);
  1070. if (id)
  1071. labelEl.htmlFor = `bytm-toggle-input-${id}`;
  1072. }
  1073. const toggleWrapperEl = document.createElement("div");
  1074. toggleWrapperEl.classList.add("bytm-toggle-input");
  1075. toggleWrapperEl.tabIndex = -1;
  1076. const toggleEl = document.createElement("input");
  1077. toggleEl.type = "checkbox";
  1078. toggleEl.checked = initialValue;
  1079. toggleEl.dataset.toggled = String(Boolean(initialValue));
  1080. toggleEl.tabIndex = -1;
  1081. if (id)
  1082. toggleEl.id = `bytm-toggle-input-${id}`;
  1083. const toggleKnobEl = document.createElement("div");
  1084. toggleKnobEl.classList.add("bytm-toggle-input-knob");
  1085. toggleKnobEl.innerHTML = "&nbsp;";
  1086. const toggleElClicked = (e) => {
  1087. e.preventDefault();
  1088. e.stopPropagation();
  1089. onChange(toggleEl.checked);
  1090. toggleEl.dataset.toggled = String(Boolean(toggleEl.checked));
  1091. if (labelEl)
  1092. labelEl.textContent = t(`toggled_${toggleEl.checked ? "on" : "off"}`);
  1093. wrapperEl.ariaValueText = t(`toggled_${toggleEl.checked ? "on" : "off"}`);
  1094. };
  1095. toggleEl.addEventListener("change", toggleElClicked);
  1096. wrapperEl.addEventListener("keydown", (e) => {
  1097. if (["Space", " ", "Enter"].includes(e.code)) {
  1098. toggleEl.checked = !toggleEl.checked;
  1099. toggleElClicked(e);
  1100. }
  1101. });
  1102. toggleEl.appendChild(toggleKnobEl);
  1103. toggleWrapperEl.appendChild(toggleEl);
  1104. labelEl && labelPos === "left" && wrapperEl.appendChild(labelEl);
  1105. wrapperEl.appendChild(toggleWrapperEl);
  1106. labelEl && labelPos === "right" && wrapperEl.appendChild(labelEl);
  1107. return wrapperEl;
  1108. });
  1109. }var name = "betterytm";
  1110. var userscriptName = "BetterYTM";
  1111. var version = "1.1.1";
  1112. var description = "Lots of configurable layout and user experience improvements for YouTube Music™";
  1113. var homepage = "https://github.com/Sv443/BetterYTM";
  1114. var main = "./src/index.ts";
  1115. var type = "module";
  1116. var scripts = {
  1117. dev: "concurrently \"nodemon --exec npm run build-watch\" \"npm run serve\"",
  1118. serve: "npm run node-ts -- ./src/tools/serve.ts",
  1119. lint: "tsc --noEmit && eslint .",
  1120. build: "rollup -c",
  1121. "build-watch": "rollup -c --config-mode development --config-host github --config-branch develop --config-assetSource=local",
  1122. "build-develop": "rollup -c --config-mode development --config-host github --config-branch develop",
  1123. "build-prod": "npm run build-prod-gh && npm run build-prod-gf && npm run build-prod-oujs",
  1124. "build-prod-base": "rollup -c --config-mode production --config-branch main",
  1125. "build-prod-gh": "npm run build-prod-base -- --config-host github",
  1126. "build-prod-gf": "npm run build-prod-base -- --config-host greasyfork --config-suffix _gf",
  1127. "build-prod-oujs": "npm run build-prod-base -- --config-host openuserjs --config-suffix _oujs",
  1128. "post-build": "npm run node-ts -- ./src/tools/post-build.ts",
  1129. "tr-progress": "npm run node-ts -- ./src/tools/tr-progress.ts",
  1130. "tr-format": "npm run node-ts -- ./src/tools/tr-format.ts",
  1131. "gen-readme": "npm run node-ts -- ./src/tools/gen-readme.ts",
  1132. "node-ts": "node --no-warnings=ExperimentalWarning --enable-source-maps --loader ts-node/esm",
  1133. invisible: "node --enable-source-maps src/tools/run-invisible.mjs",
  1134. test: "npm run node-ts -- ./test.ts"
  1135. };
  1136. var engines = {
  1137. node: ">=18",
  1138. npm: ">=8"
  1139. };
  1140. var repository = {
  1141. type: "git",
  1142. url: "git+https://github.com/Sv443/BetterYTM.git"
  1143. };
  1144. var author = {
  1145. name: "Sv443",
  1146. url: "https://github.com/Sv443"
  1147. };
  1148. var license = "AGPL-3.0-only";
  1149. var bugs = {
  1150. url: "https://github.com/Sv443/BetterYTM/issues"
  1151. };
  1152. var funding = {
  1153. type: "github",
  1154. url: "https://github.com/sponsors/Sv443"
  1155. };
  1156. var hosts = {
  1157. github: "https://github.com/Sv443/BetterYTM",
  1158. greasyfork: "https://greasyfork.org/en/scripts/475682-betterytm",
  1159. openuserjs: "https://openuserjs.org/scripts/Sv443/BetterYTM"
  1160. };
  1161. var updates = {
  1162. github: "https://github.com/Sv443/BetterYTM/releases",
  1163. greasyfork: "https://greasyfork.org/en/scripts/475682-betterytm",
  1164. openuserjs: "https://openuserjs.org/scripts/Sv443/BetterYTM"
  1165. };
  1166. var dependencies = {
  1167. "@sv443-network/userutils": "^6.2.0",
  1168. "fuse.js": "^7.0.0",
  1169. marked: "^12.0.0",
  1170. nanoevents: "^9.0.0"
  1171. };
  1172. var devDependencies = {
  1173. "@rollup/plugin-json": "^6.0.1",
  1174. "@rollup/plugin-node-resolve": "^15.2.3",
  1175. "@rollup/plugin-terser": "^0.4.4",
  1176. "@rollup/plugin-typescript": "^11.1.5",
  1177. "@types/express": "^4.17.17",
  1178. "@types/greasemonkey": "^4.0.4",
  1179. "@types/node": "^20.2.4",
  1180. "@typescript-eslint/eslint-plugin": "^6.7.4",
  1181. "@typescript-eslint/parser": "^6.7.4",
  1182. concurrently: "^8.1.0",
  1183. dotenv: "^16.4.1",
  1184. eslint: "^8.51.0",
  1185. express: "^4.18.2",
  1186. nodemon: "^3.0.1",
  1187. rollup: "^4.6.0",
  1188. "rollup-plugin-execute": "^1.1.1",
  1189. "rollup-plugin-html": "^0.2.1",
  1190. "rollup-plugin-import-css": "^3.3.5",
  1191. "ts-node": "^10.9.1",
  1192. tslib: "^2.5.2",
  1193. typescript: "^5.0.4"
  1194. };
  1195. var browserslist = [
  1196. "last 1 version",
  1197. "> 1%",
  1198. "not dead"
  1199. ];
  1200. var nodemonConfig = {
  1201. watch: [
  1202. "src/**",
  1203. "assets/**",
  1204. "rollup.config.mjs",
  1205. ".env",
  1206. "changelog.md",
  1207. "package.json"
  1208. ],
  1209. ext: "ts,mts,js,jsx,mjs,json,html,css,svg,png",
  1210. ignore: [
  1211. "dist/*",
  1212. "dev/*"
  1213. ]
  1214. };
  1215. var pkg = {
  1216. name: name,
  1217. userscriptName: userscriptName,
  1218. version: version,
  1219. description: description,
  1220. homepage: homepage,
  1221. main: main,
  1222. type: type,
  1223. scripts: scripts,
  1224. engines: engines,
  1225. repository: repository,
  1226. author: author,
  1227. license: license,
  1228. bugs: bugs,
  1229. funding: funding,
  1230. hosts: hosts,
  1231. updates: updates,
  1232. dependencies: dependencies,
  1233. devDependencies: devDependencies,
  1234. browserslist: browserslist,
  1235. nodemonConfig: nodemonConfig
  1236. };let verNotifDialog = null;
  1237. /** Creates and/or returns the dialog to be shown when a new version is available */
  1238. function getVersionNotifDialog({ latestTag, }) {
  1239. return __awaiter(this, void 0, void 0, function* () {
  1240. if (!verNotifDialog) {
  1241. const changelogMdFull = yield getChangelogMd();
  1242. const changelogMd = changelogMdFull.split("<div class=\"split\">")[1];
  1243. const changelogHtml = yield parseMarkdown(changelogMd);
  1244. verNotifDialog = new BytmDialog({
  1245. id: "version-notif",
  1246. maxWidth: 600,
  1247. maxHeight: 800,
  1248. closeBtnEnabled: false,
  1249. closeOnBgClick: false,
  1250. closeOnEscPress: true,
  1251. destroyOnClose: true,
  1252. smallDialog: true,
  1253. renderBody: () => renderBody({
  1254. latestTag,
  1255. changelogHtml,
  1256. }),
  1257. });
  1258. }
  1259. return verNotifDialog;
  1260. });
  1261. }
  1262. let disableUpdateCheck = false;
  1263. function renderBody({ latestTag, changelogHtml, }) {
  1264. return __awaiter(this, void 0, void 0, function* () {
  1265. disableUpdateCheck = false;
  1266. const hostPlatformNames = {
  1267. github: "GitHub",
  1268. greasyfork: "GreasyFork",
  1269. openuserjs: "OpenUserJS",
  1270. };
  1271. const wrapperEl = document.createElement("div");
  1272. const pEl = document.createElement("p");
  1273. pEl.textContent = t("new_version_available", scriptInfo.name, scriptInfo.version, latestTag, hostPlatformNames[host]);
  1274. wrapperEl.appendChild(pEl);
  1275. const changelogDetailsEl = document.createElement("details");
  1276. changelogDetailsEl.id = "bytm-version-notif-changelog-details";
  1277. changelogDetailsEl.open = false;
  1278. const changelogSummaryEl = document.createElement("summary");
  1279. changelogSummaryEl.role = "button";
  1280. changelogSummaryEl.tabIndex = 0;
  1281. changelogSummaryEl.ariaLabel = changelogSummaryEl.title = changelogSummaryEl.textContent = t("expand_release_notes");
  1282. changelogDetailsEl.appendChild(changelogSummaryEl);
  1283. changelogDetailsEl.addEventListener("toggle", () => {
  1284. changelogSummaryEl.ariaLabel = changelogSummaryEl.title = changelogSummaryEl.textContent = changelogDetailsEl.open ? t("collapse_release_notes") : t("expand_release_notes");
  1285. });
  1286. const changelogEl = document.createElement("p");
  1287. changelogEl.id = "bytm-version-notif-changelog-cont";
  1288. changelogEl.classList.add("bytm-markdown-container");
  1289. changelogEl.innerHTML = changelogHtml;
  1290. changelogEl.querySelectorAll("a").forEach((a) => {
  1291. a.target = "_blank";
  1292. a.rel = "noopener noreferrer";
  1293. });
  1294. changelogDetailsEl.appendChild(changelogEl);
  1295. wrapperEl.appendChild(changelogDetailsEl);
  1296. const disableUpdCheckEl = document.createElement("div");
  1297. disableUpdCheckEl.id = "bytm-disable-update-check-wrapper";
  1298. const disableToggleEl = yield createToggleInput({
  1299. id: "disable-update-check",
  1300. initialValue: false,
  1301. labelPos: "off",
  1302. onChange(checked) {
  1303. disableUpdateCheck = checked;
  1304. if (checked)
  1305. btnClose.textContent = t("close_and_ignore_until_reenabled");
  1306. else
  1307. btnClose.textContent = t("close_and_ignore_for_24h");
  1308. },
  1309. });
  1310. const labelWrapperEl = document.createElement("div");
  1311. labelWrapperEl.classList.add("bytm-disable-update-check-toggle-label-wrapper");
  1312. const labelEl = document.createElement("label");
  1313. labelEl.htmlFor = "bytm-toggle-disable-update-check";
  1314. labelEl.textContent = t("disable_update_check");
  1315. const secondaryLabelEl = document.createElement("span");
  1316. secondaryLabelEl.classList.add("bytm-secondary-label");
  1317. secondaryLabelEl.textContent = t("reenable_in_config_menu");
  1318. labelWrapperEl.appendChild(labelEl);
  1319. labelWrapperEl.appendChild(secondaryLabelEl);
  1320. disableUpdCheckEl.appendChild(disableToggleEl);
  1321. disableUpdCheckEl.appendChild(labelWrapperEl);
  1322. wrapperEl.appendChild(disableUpdCheckEl);
  1323. verNotifDialog === null || verNotifDialog === void 0 ? void 0 : verNotifDialog.on("close", () => __awaiter(this, void 0, void 0, function* () {
  1324. const config = getFeatures();
  1325. config.versionCheck = !disableUpdateCheck;
  1326. yield setFeatures(config);
  1327. }));
  1328. const btnWrapper = document.createElement("div");
  1329. btnWrapper.id = "bytm-version-notif-dialog-btns";
  1330. const btnUpdate = document.createElement("button");
  1331. btnUpdate.className = "bytm-btn";
  1332. btnUpdate.tabIndex = 0;
  1333. btnUpdate.textContent = t("open_update_page_install_manually", hostPlatformNames[host]);
  1334. onInteraction(btnUpdate, () => {
  1335. window.open(pkg.updates[host]);
  1336. verNotifDialog === null || verNotifDialog === void 0 ? void 0 : verNotifDialog.close();
  1337. });
  1338. const btnClose = document.createElement("button");
  1339. btnClose.className = "bytm-btn";
  1340. btnClose.tabIndex = 0;
  1341. btnClose.textContent = t("close_and_ignore_for_24h");
  1342. onInteraction(btnClose, () => verNotifDialog === null || verNotifDialog === void 0 ? void 0 : verNotifDialog.close());
  1343. btnWrapper.appendChild(btnUpdate);
  1344. btnWrapper.appendChild(btnClose);
  1345. wrapperEl.appendChild(btnWrapper);
  1346. return wrapperEl;
  1347. });
  1348. }const releaseURL = "https://github.com/Sv443/BetterYTM/releases/latest";
  1349. /** Initializes the version check feature */
  1350. function initVersionCheck() {
  1351. return __awaiter(this, void 0, void 0, function* () {
  1352. try {
  1353. if (getFeatures().versionCheck === false)
  1354. return info("Version check is disabled");
  1355. const lastCheck = yield GM.getValue("bytm-version-check", 0);
  1356. if (Date.now() - lastCheck < 1000 * 60 * 60 * 24)
  1357. return;
  1358. yield doVersionCheck(false);
  1359. }
  1360. catch (err) {
  1361. error("Version check failed:", err);
  1362. }
  1363. });
  1364. }
  1365. /**
  1366. * Checks for a new version of the script and shows a dialog.
  1367. * If {@linkcode notifyNoUpdatesFound} is set to true, a dialog is also shown if no updates were found.
  1368. */
  1369. function doVersionCheck(notifyNoUpdatesFound = false) {
  1370. var _a;
  1371. return __awaiter(this, void 0, void 0, function* () {
  1372. yield GM.setValue("bytm-version-check", Date.now());
  1373. const res = yield sendRequest({
  1374. method: "GET",
  1375. url: releaseURL,
  1376. });
  1377. // TODO: small dialog for "no update found" message?
  1378. const noUpdateFound = () => notifyNoUpdatesFound ? alert(t("no_updates_found")) : undefined;
  1379. const latestTag = (_a = res.finalUrl.split("/").pop()) === null || _a === void 0 ? void 0 : _a.replace(/[a-zA-Z]/g, "");
  1380. if (!latestTag)
  1381. return noUpdateFound();
  1382. const versionComp = compareVersions(scriptInfo.version, latestTag);
  1383. info("Version check - current version:", scriptInfo.version, "- latest version:", latestTag);
  1384. if (versionComp < 0) {
  1385. const dialog = yield getVersionNotifDialog({ latestTag });
  1386. yield dialog.open();
  1387. return;
  1388. }
  1389. return noUpdateFound();
  1390. });
  1391. }
  1392. /**
  1393. * Crudely compares two semver version strings.
  1394. * The format is assumed to *always* be `MAJOR.MINOR.PATCH`, where each part is a number.
  1395. * @returns Returns 1 if `a > b`, or -1 if `a < b`, or 0 if `a == b`
  1396. */
  1397. function compareVersions(a, b) {
  1398. a = String(a).trim();
  1399. b = String(b).trim();
  1400. if ([a, b].some(v => !v.match(/^\d+\.\d+\.\d+$/)))
  1401. throw new TypeError("Invalid version format, expected 'MAJOR.MINOR.PATCH'");
  1402. const pa = a.split(".");
  1403. const pb = b.split(".");
  1404. for (let i = 0; i < 3; i++) {
  1405. const na = Number(pa[i]);
  1406. const nb = Number(pb[i]);
  1407. if (na > nb)
  1408. return 1;
  1409. if (nb > na)
  1410. return -1;
  1411. if (!isNaN(na) && isNaN(nb))
  1412. return 1;
  1413. if (isNaN(na) && !isNaN(nb))
  1414. return -1;
  1415. }
  1416. return 0;
  1417. }
  1418. /**
  1419. * Compares two version arrays.
  1420. * The format is assumed to *always* be `[MAJOR, MINOR, PATCH]`, where each part is a positive integer number.
  1421. * @returns Returns 1 if `a > b`, or -1 if `a < b`, or 0 if `a == b`
  1422. */
  1423. function compareVersionArrays(a, b) {
  1424. if ([a, b].some(v => !Array.isArray(v) || v.length !== 3 || v.some(iv => !Number.isInteger(iv) || iv < 0)))
  1425. throw new TypeError("Invalid version format, expected '[MAJOR, MINOR, PATCH]' consisting only of positive integers");
  1426. for (let i = 0; i < 3; i++) {
  1427. if (a[i] > b[i])
  1428. return 1;
  1429. if (b[i] > a[i])
  1430. return -1;
  1431. }
  1432. return 0;
  1433. }//#MARKER init
  1434. /** Initializes all volume-related features */
  1435. function initVolumeFeatures() {
  1436. return __awaiter(this, void 0, void 0, function* () {
  1437. // not technically an input element but behaves pretty much the same
  1438. addSelectorListener("playerBarRightControls", "tp-yt-paper-slider#volume-slider", {
  1439. listener: (sliderElem) => __awaiter(this, void 0, void 0, function* () {
  1440. // TODO:FIXME: broken fsr
  1441. const volSliderCont = document.createElement("div");
  1442. volSliderCont.id = "bytm-vol-slider-cont";
  1443. if (getFeatures().volumeSliderScrollStep !== featInfo.volumeSliderScrollStep.default)
  1444. initScrollStep(volSliderCont, sliderElem);
  1445. UserUtils.addParent(sliderElem, volSliderCont);
  1446. if (typeof getFeatures().volumeSliderSize === "number")
  1447. setVolSliderSize();
  1448. if (getFeatures().volumeSliderLabel)
  1449. yield addVolumeSliderLabel(sliderElem, volSliderCont);
  1450. setVolSliderStep(sliderElem);
  1451. if (getFeatures().volumeSharedBetweenTabs) {
  1452. sliderElem.addEventListener("change", () => sharedVolumeChanged(Number(sliderElem.value)));
  1453. checkSharedVolume();
  1454. }
  1455. if (getFeatures().setInitialTabVolume)
  1456. setInitialTabVolume(sliderElem);
  1457. }),
  1458. });
  1459. });
  1460. }
  1461. //#MARKER scroll step
  1462. /** Initializes the volume slider scroll step features */
  1463. function initScrollStep(volSliderCont, sliderElem) {
  1464. for (const evtName of ["wheel", "scroll", "mousewheel", "DOMMouseScroll"]) {
  1465. volSliderCont.addEventListener(evtName, (e) => {
  1466. var _a, _b;
  1467. e.preventDefault();
  1468. // cancels all the other events that would be fired
  1469. e.stopImmediatePropagation();
  1470. const delta = (_b = (_a = e.deltaY) !== null && _a !== void 0 ? _a : e.detail) !== null && _b !== void 0 ? _b : 1;
  1471. const volumeDir = -Math.sign(delta);
  1472. const newVolume = String(Number(sliderElem.value) + (getFeatures().volumeSliderScrollStep * volumeDir));
  1473. sliderElem.value = newVolume;
  1474. sliderElem.setAttribute("aria-valuenow", newVolume);
  1475. // make the site actually change the volume
  1476. sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
  1477. }, {
  1478. // takes precedence over the slider's own event listener
  1479. capture: true,
  1480. });
  1481. }
  1482. }
  1483. // #MARKER volume slider
  1484. //#SECTION label
  1485. /** Adds a percentage label to the volume slider and tooltip */
  1486. function addVolumeSliderLabel(sliderElem, sliderContainer) {
  1487. return __awaiter(this, void 0, void 0, function* () {
  1488. const labelContElem = document.createElement("div");
  1489. labelContElem.id = "bytm-vol-slider-label";
  1490. const volShared = getFeatures().volumeSharedBetweenTabs;
  1491. if (volShared) {
  1492. const linkIconHtml = yield resourceToHTMLString("icon-link");
  1493. if (linkIconHtml) {
  1494. const linkIconElem = document.createElement("div");
  1495. linkIconElem.id = "bytm-vol-slider-shared";
  1496. linkIconElem.innerHTML = linkIconHtml;
  1497. linkIconElem.role = "alert";
  1498. linkIconElem.title = linkIconElem.ariaLabel = t("volume_shared_tooltip");
  1499. labelContElem.classList.add("has-icon");
  1500. labelContElem.appendChild(linkIconElem);
  1501. }
  1502. }
  1503. const getLabel = (value) => `${value}%`;
  1504. const labelElem = document.createElement("div");
  1505. labelElem.classList.add("label");
  1506. labelElem.textContent = getLabel(sliderElem.value);
  1507. labelContElem.appendChild(labelElem);
  1508. // prevent video from minimizing
  1509. labelContElem.addEventListener("click", (e) => e.stopPropagation());
  1510. labelContElem.addEventListener("keydown", (e) => ["Enter", "Space", " "].includes(e.key) && e.stopPropagation());
  1511. const getLabelText = (slider) => { var _a; return t("volume_tooltip", slider.value, (_a = getFeatures().volumeSliderStep) !== null && _a !== void 0 ? _a : slider.step); };
  1512. const labelFull = getLabelText(sliderElem);
  1513. sliderContainer.setAttribute("title", labelFull);
  1514. sliderElem.setAttribute("title", labelFull);
  1515. sliderElem.setAttribute("aria-valuetext", labelFull);
  1516. const updateLabel = () => {
  1517. const labelFull = getLabelText(sliderElem);
  1518. sliderContainer.setAttribute("title", labelFull);
  1519. sliderElem.setAttribute("title", labelFull);
  1520. sliderElem.setAttribute("aria-valuetext", labelFull);
  1521. const labelElem2 = document.querySelector("#bytm-vol-slider-label div.label");
  1522. if (labelElem2)
  1523. labelElem2.textContent = getLabel(sliderElem.value);
  1524. };
  1525. sliderElem.addEventListener("change", () => updateLabel());
  1526. siteEvents.on("configChanged", () => {
  1527. updateLabel();
  1528. });
  1529. addSelectorListener("playerBarRightControls", "#bytm-vol-slider-cont", {
  1530. listener: (volumeCont) => volumeCont.appendChild(labelContElem),
  1531. });
  1532. let lastSliderVal = Number(sliderElem.value);
  1533. // show label if hovering over slider or slider is focused
  1534. const sliderHoverObserver = new MutationObserver(() => {
  1535. if (sliderElem.classList.contains("on-hover") || document.activeElement === sliderElem)
  1536. labelContElem.classList.add("bytm-visible");
  1537. else if (labelContElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
  1538. labelContElem.classList.remove("bytm-visible");
  1539. if (Number(sliderElem.value) !== lastSliderVal) {
  1540. lastSliderVal = Number(sliderElem.value);
  1541. updateLabel();
  1542. }
  1543. });
  1544. sliderHoverObserver.observe(sliderElem, {
  1545. attributes: true,
  1546. });
  1547. });
  1548. }
  1549. //#SECTION size
  1550. /** Sets the volume slider to a set size */
  1551. function setVolSliderSize() {
  1552. const { volumeSliderSize: size } = getFeatures();
  1553. if (typeof size !== "number" || isNaN(Number(size)))
  1554. return;
  1555. UserUtils.addGlobalStyle(`\
  1556. #bytm-vol-slider-cont tp-yt-paper-slider#volume-slider {
  1557. width: ${size}px !important;
  1558. }`).id = "bytm-style-vol-slider-size";
  1559. }
  1560. //#SECTION step
  1561. /** Sets the `step` attribute of the volume slider */
  1562. function setVolSliderStep(sliderElem) {
  1563. sliderElem.setAttribute("step", String(getFeatures().volumeSliderStep));
  1564. }
  1565. //#MARKER shared volume
  1566. /** Saves the shared volume level to persistent storage */
  1567. function sharedVolumeChanged(vol) {
  1568. return __awaiter(this, void 0, void 0, function* () {
  1569. try {
  1570. yield GM.setValue("bytm-shared-volume", String(lastCheckedSharedVolume = ignoreVal = vol));
  1571. }
  1572. catch (err) {
  1573. error("Couldn't save shared volume level due to an error:", err);
  1574. }
  1575. });
  1576. }
  1577. let ignoreVal = -1;
  1578. let lastCheckedSharedVolume = -1;
  1579. /** Only call once as this calls itself after a timeout! - Checks if the shared volume has changed and updates the volume slider accordingly */
  1580. function checkSharedVolume() {
  1581. return __awaiter(this, void 0, void 0, function* () {
  1582. try {
  1583. const vol = yield GM.getValue("bytm-shared-volume");
  1584. if (vol && lastCheckedSharedVolume !== Number(vol)) {
  1585. if (ignoreVal === Number(vol))
  1586. return;
  1587. lastCheckedSharedVolume = Number(vol);
  1588. const sliderElem = document.querySelector("tp-yt-paper-slider#volume-slider");
  1589. if (sliderElem) {
  1590. sliderElem.value = String(vol);
  1591. sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
  1592. }
  1593. }
  1594. setTimeout(checkSharedVolume, 333);
  1595. }
  1596. catch (err) {
  1597. error("Couldn't check for shared volume level due to an error:", err);
  1598. }
  1599. });
  1600. }
  1601. function volumeSharedBetweenTabsDisabled() {
  1602. return __awaiter(this, void 0, void 0, function* () {
  1603. yield GM.deleteValue("bytm-shared-volume");
  1604. });
  1605. }
  1606. //#MARKER initial volume
  1607. /** Sets the volume slider to a set volume level when the session starts */
  1608. function setInitialTabVolume(sliderElem) {
  1609. return __awaiter(this, void 0, void 0, function* () {
  1610. yield waitVideoElementReady();
  1611. const initialVol = getFeatures().initialTabVolumeLevel;
  1612. if (getFeatures().volumeSharedBetweenTabs) {
  1613. lastCheckedSharedVolume = ignoreVal = initialVol;
  1614. if (getFeatures().volumeSharedBetweenTabs)
  1615. GM.setValue("bytm-shared-volume", String(initialVol));
  1616. }
  1617. sliderElem.value = String(initialVol);
  1618. sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
  1619. log(`Set initial tab volume to ${initialVol}%`);
  1620. });
  1621. }//#MARKER create menu elements
  1622. let isCfgMenuAdded = false;
  1623. let isCfgMenuOpen = false;
  1624. /** Threshold in pixels from the top of the options container that dictates for how long the scroll indicator is shown */
  1625. const scrollIndicatorOffsetThreshold = 30;
  1626. let scrollIndicatorEnabled = true;
  1627. /** Locale at the point of initializing the config menu */
  1628. let initLocale;
  1629. /** Stringified config at the point of initializing the config menu */
  1630. let initConfig$1;
  1631. /** Timeout id for the "copied" text in the hidden value copy button */
  1632. let hiddenCopiedTxtTimeout;
  1633. /**
  1634. * Adds an element to open the BetterYTM menu
  1635. * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
  1636. */
  1637. function addCfgMenu() {
  1638. var _a, _b, _c, _d, _f;
  1639. return __awaiter(this, void 0, void 0, function* () {
  1640. if (isCfgMenuAdded)
  1641. return;
  1642. isCfgMenuAdded = true;
  1643. initLocale = getFeatures().locale;
  1644. initConfig$1 = JSON.stringify(getFeatures());
  1645. const initLangReloadText = t("lang_changed_prompt_reload");
  1646. //#SECTION backdrop & menu container
  1647. const backgroundElem = document.createElement("div");
  1648. backgroundElem.id = "bytm-cfg-menu-bg";
  1649. backgroundElem.classList.add("bytm-menu-bg");
  1650. backgroundElem.ariaLabel = backgroundElem.title = t("close_menu_tooltip");
  1651. backgroundElem.style.visibility = "hidden";
  1652. backgroundElem.style.display = "none";
  1653. backgroundElem.addEventListener("click", (e) => {
  1654. var _a;
  1655. if (isCfgMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-cfg-menu-bg")
  1656. closeCfgMenu(e);
  1657. });
  1658. document.body.addEventListener("keydown", (e) => {
  1659. if (isCfgMenuOpen && e.key === "Escape")
  1660. closeCfgMenu(e);
  1661. });
  1662. const menuContainer = document.createElement("div");
  1663. menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
  1664. menuContainer.classList.add("bytm-menu");
  1665. menuContainer.id = "bytm-cfg-menu";
  1666. //#SECTION title bar
  1667. const headerElem = document.createElement("div");
  1668. headerElem.classList.add("bytm-menu-header");
  1669. const titleCont = document.createElement("div");
  1670. titleCont.className = "bytm-menu-titlecont";
  1671. titleCont.role = "heading";
  1672. titleCont.ariaLevel = "1";
  1673. const titleElem = document.createElement("h2");
  1674. titleElem.className = "bytm-menu-title";
  1675. const titleTextElem = document.createElement("div");
  1676. titleTextElem.textContent = t("config_menu_title", scriptInfo.name);
  1677. titleElem.appendChild(titleTextElem);
  1678. const linksCont = document.createElement("div");
  1679. linksCont.id = "bytm-menu-linkscont";
  1680. linksCont.role = "navigation";
  1681. const linkTitlesShort = {
  1682. github: "GitHub",
  1683. greasyfork: "GreasyFork",
  1684. openuserjs: "OpenUserJS",
  1685. discord: "Discord",
  1686. };
  1687. const addLink = (imgSrc, href, title, titleKey) => {
  1688. const anchorElem = document.createElement("a");
  1689. anchorElem.className = "bytm-menu-link bytm-no-select";
  1690. anchorElem.rel = "noopener noreferrer";
  1691. anchorElem.href = href;
  1692. anchorElem.target = "_blank";
  1693. anchorElem.tabIndex = 0;
  1694. anchorElem.role = "button";
  1695. anchorElem.ariaLabel = anchorElem.title = title;
  1696. const extendedAnchorEl = document.createElement("a");
  1697. extendedAnchorEl.className = "bytm-menu-link extended-link bytm-no-select";
  1698. extendedAnchorEl.rel = "noopener noreferrer";
  1699. extendedAnchorEl.href = href;
  1700. extendedAnchorEl.target = "_blank";
  1701. extendedAnchorEl.tabIndex = -1;
  1702. extendedAnchorEl.textContent = linkTitlesShort[titleKey];
  1703. extendedAnchorEl.ariaLabel = extendedAnchorEl.title = title;
  1704. const imgElem = document.createElement("img");
  1705. imgElem.classList.add("bytm-menu-img");
  1706. imgElem.src = imgSrc;
  1707. anchorElem.appendChild(imgElem);
  1708. anchorElem.appendChild(extendedAnchorEl);
  1709. linksCont.appendChild(anchorElem);
  1710. };
  1711. const links = [
  1712. ["github", yield getResourceUrl("img-github"), scriptInfo.namespace, t("open_github", scriptInfo.name), "github"],
  1713. ["greasyfork", yield getResourceUrl("img-greasyfork"), pkg.hosts.greasyfork, t("open_greasyfork", scriptInfo.name), "greasyfork"],
  1714. ["openuserjs", yield getResourceUrl("img-openuserjs"), pkg.hosts.openuserjs, t("open_openuserjs", scriptInfo.name), "openuserjs"],
  1715. ];
  1716. const hostLink = links.find(([name]) => name === host);
  1717. const otherLinks = links.filter(([name]) => name !== host);
  1718. const reorderedLinks = hostLink ? [hostLink, ...otherLinks] : links;
  1719. for (const [, ...args] of reorderedLinks)
  1720. addLink(...args);
  1721. addLink(yield getResourceUrl("img-discord"), "https://dc.sv443.net/", t("open_discord"), "discord");
  1722. const closeElem = document.createElement("img");
  1723. closeElem.classList.add("bytm-menu-close");
  1724. closeElem.role = "button";
  1725. closeElem.tabIndex = 0;
  1726. closeElem.src = yield getResourceUrl("img-close");
  1727. closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
  1728. onInteraction(closeElem, closeCfgMenu);
  1729. titleCont.appendChild(titleElem);
  1730. titleCont.appendChild(linksCont);
  1731. headerElem.appendChild(titleCont);
  1732. headerElem.appendChild(closeElem);
  1733. //#SECTION footer
  1734. const footerCont = document.createElement("div");
  1735. footerCont.className = "bytm-menu-footer-cont";
  1736. const footerElemCont = document.createElement("div");
  1737. const footerElem = document.createElement("div");
  1738. footerElem.classList.add("bytm-menu-footer", "hidden");
  1739. footerElem.textContent = t("reload_hint");
  1740. const reloadElem = document.createElement("button");
  1741. reloadElem.classList.add("bytm-btn");
  1742. reloadElem.style.marginLeft = "10px";
  1743. reloadElem.textContent = t("reload_now");
  1744. reloadElem.ariaLabel = reloadElem.title = t("reload_tooltip");
  1745. reloadElem.addEventListener("click", () => {
  1746. closeCfgMenu();
  1747. disableBeforeUnload();
  1748. location.reload();
  1749. });
  1750. footerElem.appendChild(reloadElem);
  1751. footerElemCont.appendChild(footerElem);
  1752. const resetElem = document.createElement("button");
  1753. resetElem.classList.add("bytm-btn");
  1754. resetElem.ariaLabel = resetElem.title = t("reset_tooltip");
  1755. resetElem.textContent = t("reset");
  1756. resetElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  1757. if (confirm(t("reset_confirm"))) {
  1758. yield setDefaultFeatures();
  1759. closeCfgMenu();
  1760. disableBeforeUnload();
  1761. location.reload();
  1762. }
  1763. }));
  1764. const exportElem = document.createElement("button");
  1765. exportElem.classList.add("bytm-btn");
  1766. exportElem.ariaLabel = exportElem.title = t("export_tooltip");
  1767. exportElem.textContent = t("export");
  1768. exportElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  1769. yield openExportMenu();
  1770. closeCfgMenu(undefined, false);
  1771. }));
  1772. const importElem = document.createElement("button");
  1773. importElem.classList.add("bytm-btn");
  1774. importElem.ariaLabel = importElem.title = t("import_tooltip");
  1775. importElem.textContent = t("import");
  1776. importElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  1777. yield openImportMenu();
  1778. closeCfgMenu(undefined, false);
  1779. }));
  1780. const buttonsCont = document.createElement("div");
  1781. buttonsCont.id = "bytm-menu-footer-buttons-cont";
  1782. buttonsCont.appendChild(exportElem);
  1783. buttonsCont.appendChild(importElem);
  1784. buttonsCont.appendChild(resetElem);
  1785. footerCont.appendChild(footerElemCont);
  1786. footerCont.appendChild(buttonsCont);
  1787. //#SECTION feature list
  1788. const featuresCont = document.createElement("div");
  1789. featuresCont.id = "bytm-menu-opts";
  1790. /** Gets called whenever the feature config is changed */
  1791. const confChanged = UserUtils.debounce((key, initialVal, newVal) => __awaiter(this, void 0, void 0, function* () {
  1792. var _g, _h;
  1793. const fmt = (val) => typeof val === "object" ? JSON.stringify(val) : String(val);
  1794. info(`Feature config changed at key '${key}', from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
  1795. const featConf = JSON.parse(JSON.stringify(getFeatures()));
  1796. featConf[key] = newVal;
  1797. yield setFeatures(featConf);
  1798. // @ts-ignore
  1799. (_h = (_g = featInfo[key]) === null || _g === void 0 ? void 0 : _g.change) === null || _h === void 0 ? void 0 : _h.call(_g, featConf);
  1800. if (initConfig$1 !== JSON.stringify(featConf))
  1801. footerElem.classList.remove("hidden");
  1802. else
  1803. footerElem.classList.add("hidden");
  1804. if (initLocale !== featConf.locale) {
  1805. yield initTranslations(featConf.locale);
  1806. setLocale(featConf.locale);
  1807. const newText = t("lang_changed_prompt_reload");
  1808. const confirmText = newText !== initLangReloadText ? `${newText}\n\n────────────────────────────────\n\n${initLangReloadText}` : newText;
  1809. if (confirm(confirmText)) {
  1810. closeCfgMenu();
  1811. disableBeforeUnload();
  1812. location.reload();
  1813. }
  1814. }
  1815. else if (getLocale() !== featConf.locale)
  1816. setLocale(featConf.locale);
  1817. }));
  1818. const featureCfg = getFeatures();
  1819. const featureCfgWithCategories = Object.entries(featInfo)
  1820. .reduce((acc, [key, { category }]) => {
  1821. if (!acc[category])
  1822. acc[category] = {};
  1823. acc[category][key] = featureCfg[key];
  1824. return acc;
  1825. }, {});
  1826. const fmtVal = (v) => {
  1827. try {
  1828. return (typeof v === "object" ? JSON.stringify(v) : String(v)).trim();
  1829. }
  1830. catch (_e) {
  1831. // because stringify throws on circular refs
  1832. return String(v).trim();
  1833. }
  1834. };
  1835. for (const category in featureCfgWithCategories) {
  1836. const featObj = featureCfgWithCategories[category];
  1837. const catHeaderElem = document.createElement("h3");
  1838. catHeaderElem.classList.add("bytm-ftconf-category-header");
  1839. catHeaderElem.role = "heading";
  1840. catHeaderElem.ariaLevel = "2";
  1841. catHeaderElem.textContent = `${t(`feature_category_${category}`)}:`;
  1842. featuresCont.appendChild(catHeaderElem);
  1843. for (const featKey in featObj) {
  1844. const ftInfo = featInfo[featKey];
  1845. // @ts-ignore
  1846. if (!ftInfo || ftInfo.hidden === true)
  1847. continue;
  1848. if (ftInfo.advanced && !featureCfg.advancedMode)
  1849. continue;
  1850. const { type, default: ftDefault } = ftInfo;
  1851. // @ts-ignore
  1852. const step = (_a = ftInfo === null || ftInfo === void 0 ? void 0 : ftInfo.step) !== null && _a !== void 0 ? _a : undefined;
  1853. const val = featureCfg[featKey];
  1854. const initialVal = (_b = val !== null && val !== void 0 ? val : ftDefault) !== null && _b !== void 0 ? _b : undefined;
  1855. const ftConfElem = document.createElement("div");
  1856. ftConfElem.classList.add("bytm-ftitem");
  1857. {
  1858. const featLeftSideElem = document.createElement("div");
  1859. featLeftSideElem.classList.add("bytm-ftitem-leftside");
  1860. if (getFeatures().advancedMode) {
  1861. const valFmtd = fmtVal(ftDefault);
  1862. featLeftSideElem.title = `${featKey}${ftInfo.advanced ? " (advanced)" : ""} - Default: ${valFmtd.length === 0 ? "(empty)" : valFmtd}`;
  1863. }
  1864. const textElem = document.createElement("span");
  1865. textElem.textContent = t(`feature_desc_${featKey}`);
  1866. let adornmentElem;
  1867. const adornContent = (_c = ftInfo.textAdornment) === null || _c === void 0 ? void 0 : _c.call(ftInfo);
  1868. const adornContentAw = adornContent instanceof Promise ? yield adornContent : adornContent;
  1869. if ((typeof adornContent === "string" || adornContent instanceof Promise) && typeof adornContentAw !== "undefined") {
  1870. adornmentElem = document.createElement("span");
  1871. adornmentElem.id = `bytm-ftitem-${featKey}-adornment`;
  1872. adornmentElem.classList.add("bytm-ftitem-adornment");
  1873. adornmentElem.innerHTML = adornContentAw;
  1874. }
  1875. let helpElem;
  1876. // @ts-ignore
  1877. const hasHelpTextFunc = typeof ((_d = featInfo[featKey]) === null || _d === void 0 ? void 0 : _d.helpText) === "function";
  1878. // @ts-ignore
  1879. const helpTextVal = hasHelpTextFunc && featInfo[featKey].helpText();
  1880. if (hasKey(`feature_helptext_${featKey}`) || (helpTextVal && hasKey(helpTextVal))) {
  1881. const helpElemImgHtml = yield resourceToHTMLString("icon-help");
  1882. if (helpElemImgHtml) {
  1883. helpElem = document.createElement("div");
  1884. helpElem.classList.add("bytm-ftitem-help-btn", "bytm-generic-btn");
  1885. helpElem.ariaLabel = helpElem.title = t("feature_help_button_tooltip");
  1886. helpElem.role = "button";
  1887. helpElem.tabIndex = 0;
  1888. helpElem.innerHTML = helpElemImgHtml;
  1889. onInteraction(helpElem, (e) => {
  1890. e.preventDefault();
  1891. e.stopPropagation();
  1892. openHelpDialog(featKey);
  1893. });
  1894. }
  1895. else {
  1896. error(`Couldn't create help button SVG element for feature '${featKey}'`);
  1897. }
  1898. }
  1899. adornmentElem && featLeftSideElem.appendChild(adornmentElem);
  1900. featLeftSideElem.appendChild(textElem);
  1901. helpElem && featLeftSideElem.appendChild(helpElem);
  1902. ftConfElem.appendChild(featLeftSideElem);
  1903. }
  1904. {
  1905. let inputType = "text";
  1906. let inputTag = "input";
  1907. switch (type) {
  1908. case "toggle":
  1909. inputTag = undefined;
  1910. inputType = undefined;
  1911. break;
  1912. case "slider":
  1913. inputType = "range";
  1914. break;
  1915. case "number":
  1916. inputType = "number";
  1917. break;
  1918. case "text":
  1919. inputType = "text";
  1920. break;
  1921. case "select":
  1922. inputTag = "select";
  1923. inputType = undefined;
  1924. break;
  1925. case "hotkey":
  1926. inputTag = undefined;
  1927. inputType = undefined;
  1928. break;
  1929. case "button":
  1930. inputTag = undefined;
  1931. inputType = undefined;
  1932. break;
  1933. }
  1934. const inputElemId = `bytm-ftconf-${featKey}-input`;
  1935. const ctrlElem = document.createElement("span");
  1936. ctrlElem.classList.add("bytm-ftconf-ctrl");
  1937. let advCopyHiddenCont;
  1938. if ((getFeatures().advancedMode || mode === "development") && ftInfo.valueHidden) {
  1939. const advCopyHintElem = document.createElement("span");
  1940. advCopyHintElem.classList.add("bytm-ftconf-adv-copy-hint");
  1941. advCopyHintElem.textContent = t("copied");
  1942. advCopyHintElem.style.display = "none";
  1943. const advCopyHiddenBtn = document.createElement("button");
  1944. advCopyHiddenBtn.classList.add("bytm-ftconf-adv-copy-btn");
  1945. advCopyHiddenBtn.tabIndex = 0;
  1946. advCopyHiddenBtn.textContent = t("copy_hidden_value");
  1947. advCopyHiddenBtn.ariaLabel = advCopyHiddenBtn.title = t("copy_hidden_tooltip");
  1948. const copyHiddenInteraction = (e) => {
  1949. e.preventDefault();
  1950. e.stopPropagation();
  1951. GM.setClipboard(String(getFeatures()[featKey]));
  1952. advCopyHintElem.style.display = "inline";
  1953. if (typeof hiddenCopiedTxtTimeout === "undefined") {
  1954. hiddenCopiedTxtTimeout = setTimeout(() => {
  1955. advCopyHintElem.style.display = "none";
  1956. hiddenCopiedTxtTimeout = undefined;
  1957. }, 3000);
  1958. }
  1959. };
  1960. onInteraction(advCopyHiddenBtn, copyHiddenInteraction);
  1961. advCopyHiddenCont = document.createElement("span");
  1962. advCopyHiddenCont.appendChild(advCopyHintElem);
  1963. advCopyHiddenCont.appendChild(advCopyHiddenBtn);
  1964. }
  1965. advCopyHiddenCont && ctrlElem.appendChild(advCopyHiddenCont);
  1966. if (inputTag) {
  1967. // standard input element:
  1968. const inputElem = document.createElement(inputTag);
  1969. inputElem.classList.add("bytm-ftconf-input");
  1970. inputElem.id = inputElemId;
  1971. if (inputType)
  1972. inputElem.type = inputType;
  1973. // @ts-ignore
  1974. if (typeof ftInfo.min !== "undefined") // @ts-ignore
  1975. inputElem.min = ftInfo.min;
  1976. // @ts-ignore
  1977. if (typeof ftInfo.max !== "undefined") // @ts-ignore
  1978. inputElem.max = ftInfo.max;
  1979. if (typeof initialVal !== "undefined")
  1980. inputElem.value = String(initialVal);
  1981. if (type === "text" && ftInfo.valueHidden)
  1982. inputElem.value = String(initialVal).length === 0 ? "" : "•".repeat(16);
  1983. if (type === "number" || type === "slider" && step)
  1984. inputElem.step = String(step);
  1985. if (type === "toggle" && typeof initialVal !== "undefined")
  1986. inputElem.checked = Boolean(initialVal);
  1987. // @ts-ignore
  1988. const unitTxt = (typeof ftInfo.unit === "string" ? ftInfo.unit : (
  1989. // @ts-ignore
  1990. typeof ftInfo.unit === "function" ? ftInfo.unit(Number(inputElem.value)) : ""));
  1991. let labelElem;
  1992. let lastDisplayedVal;
  1993. if (type === "slider") {
  1994. labelElem = document.createElement("label");
  1995. labelElem.classList.add("bytm-ftconf-label", "bytm-slider-label");
  1996. labelElem.textContent = `${fmtVal(initialVal)}${unitTxt}`;
  1997. inputElem.addEventListener("input", () => {
  1998. if (labelElem && lastDisplayedVal !== inputElem.value) {
  1999. labelElem.textContent = `${fmtVal(inputElem.value)}${unitTxt}`;
  2000. lastDisplayedVal = inputElem.value;
  2001. }
  2002. });
  2003. }
  2004. else if (type === "select") {
  2005. const ftOpts = typeof ftInfo.options === "function"
  2006. ? ftInfo.options()
  2007. : ftInfo.options;
  2008. for (const { value, label } of ftOpts) {
  2009. const optionElem = document.createElement("option");
  2010. optionElem.value = String(value);
  2011. optionElem.textContent = label;
  2012. if (value === initialVal)
  2013. optionElem.selected = true;
  2014. inputElem.appendChild(optionElem);
  2015. }
  2016. }
  2017. if (type === "text") {
  2018. let lastValue = inputElem.value && inputElem.value.length > 0 ? inputElem.value : ftInfo.default;
  2019. const textInputUpdate = () => {
  2020. let v = String(inputElem.value).trim();
  2021. if (type === "text" && ftInfo.normalize)
  2022. v = inputElem.value = ftInfo.normalize(String(v));
  2023. if (v === lastValue)
  2024. return;
  2025. lastValue = v;
  2026. if (v === "")
  2027. v = ftInfo.default;
  2028. if (typeof initialVal !== "undefined")
  2029. confChanged(featKey, initialVal, v);
  2030. };
  2031. const unsub = siteEvents.on("cfgMenuClosed", () => {
  2032. unsub();
  2033. textInputUpdate();
  2034. });
  2035. inputElem.addEventListener("blur", () => textInputUpdate());
  2036. inputElem.addEventListener("keydown", (e) => e.key === "Tab" && textInputUpdate());
  2037. }
  2038. else {
  2039. inputElem.addEventListener("input", () => {
  2040. let v = String(inputElem.value).trim();
  2041. if (["number", "slider"].includes(type) || v.match(/^-?\d+$/))
  2042. v = Number(v);
  2043. if (typeof initialVal !== "undefined")
  2044. confChanged(featKey, initialVal, (type !== "toggle" ? v : inputElem.checked));
  2045. });
  2046. }
  2047. if (labelElem) {
  2048. labelElem.id = `bytm-ftconf-${featKey}-label`;
  2049. labelElem.htmlFor = inputElemId;
  2050. ctrlElem.appendChild(labelElem);
  2051. }
  2052. ctrlElem.appendChild(inputElem);
  2053. }
  2054. else {
  2055. // custom input element:
  2056. let wrapperElem;
  2057. switch (type) {
  2058. case "hotkey":
  2059. wrapperElem = createHotkeyInput({
  2060. initialValue: typeof initialVal === "object" ? initialVal : undefined,
  2061. onChange: (hotkey) => confChanged(featKey, initialVal, hotkey),
  2062. });
  2063. break;
  2064. case "toggle":
  2065. wrapperElem = yield createToggleInput({
  2066. initialValue: Boolean(initialVal),
  2067. onChange: (checked) => confChanged(featKey, initialVal, checked),
  2068. id: `ftconf-${featKey}`,
  2069. labelPos: "left",
  2070. });
  2071. break;
  2072. case "button":
  2073. wrapperElem = document.createElement("button");
  2074. wrapperElem.tabIndex = 0;
  2075. wrapperElem.textContent = wrapperElem.ariaLabel = wrapperElem.title = hasKey(`feature_btn_${featKey}`) ? t(`feature_btn_${featKey}`) : t("trigger_btn_action");
  2076. onInteraction(wrapperElem, () => __awaiter(this, void 0, void 0, function* () {
  2077. if (wrapperElem.disabled)
  2078. return;
  2079. const startTs = Date.now();
  2080. const res = ftInfo.click();
  2081. wrapperElem.disabled = true;
  2082. wrapperElem.classList.add("bytm-busy");
  2083. wrapperElem.textContent = wrapperElem.ariaLabel = wrapperElem.title = hasKey(`feature_btn_${featKey}_running`) ? t(`feature_btn_${featKey}_running`) : t("trigger_btn_action_running");
  2084. if (res instanceof Promise)
  2085. yield res;
  2086. const finalize = () => {
  2087. wrapperElem.disabled = false;
  2088. wrapperElem.classList.remove("bytm-busy");
  2089. wrapperElem.textContent = wrapperElem.ariaLabel = wrapperElem.title = hasKey(`feature_btn_${featKey}`) ? t(`feature_btn_${featKey}`) : t("trigger_btn_action");
  2090. };
  2091. // artificial timeout ftw
  2092. if (Date.now() - startTs < 350)
  2093. setTimeout(finalize, 350 - (Date.now() - startTs));
  2094. else
  2095. finalize();
  2096. }));
  2097. break;
  2098. }
  2099. ctrlElem.appendChild(wrapperElem);
  2100. }
  2101. ftConfElem.appendChild(ctrlElem);
  2102. }
  2103. featuresCont.appendChild(ftConfElem);
  2104. }
  2105. }
  2106. //#SECTION set values of inputs on external change
  2107. siteEvents.on("rebuildCfgMenu", (newConfig) => {
  2108. for (const ftKey in featInfo) {
  2109. const ftElem = document.querySelector(`#bytm-ftconf-${ftKey}-input`);
  2110. const labelElem = document.querySelector(`#bytm-ftconf-${ftKey}-label`);
  2111. if (!ftElem)
  2112. continue;
  2113. const ftInfo = featInfo[ftKey];
  2114. const value = newConfig[ftKey];
  2115. if (ftInfo.type === "toggle")
  2116. ftElem.checked = Boolean(value);
  2117. else
  2118. ftElem.value = String(value);
  2119. // @ts-ignore
  2120. if (ftInfo.type === "text" && ftInfo.valueHidden)
  2121. ftElem.value = String(value).length === 0 ? "" : "•".repeat(16);
  2122. if (!labelElem)
  2123. continue;
  2124. // @ts-ignore
  2125. const unitTxt = " " + (typeof ftInfo.unit === "string" ? ftInfo.unit : (
  2126. // @ts-ignore
  2127. typeof ftInfo.unit === "function" ? ftInfo.unit(Number(ftElem.value)) : ""));
  2128. if (ftInfo.type === "slider")
  2129. labelElem.textContent = `${fmtVal(Number(value))}${unitTxt}`;
  2130. }
  2131. info("Rebuilt config menu");
  2132. });
  2133. //#SECTION scroll indicator
  2134. const scrollIndicator = document.createElement("img");
  2135. scrollIndicator.id = "bytm-menu-scroll-indicator";
  2136. scrollIndicator.src = yield getResourceUrl("icon-arrow_down");
  2137. scrollIndicator.role = "button";
  2138. scrollIndicator.ariaLabel = scrollIndicator.title = t("scroll_to_bottom");
  2139. featuresCont.appendChild(scrollIndicator);
  2140. scrollIndicator.addEventListener("click", () => {
  2141. const bottomAnchor = document.querySelector("#bytm-menu-bottom-anchor");
  2142. bottomAnchor === null || bottomAnchor === void 0 ? void 0 : bottomAnchor.scrollIntoView({
  2143. behavior: "smooth",
  2144. });
  2145. });
  2146. featuresCont.addEventListener("scroll", (evt) => {
  2147. var _a, _b;
  2148. const scrollPos = (_b = (_a = evt.target) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0;
  2149. const scrollIndicator = document.querySelector("#bytm-menu-scroll-indicator");
  2150. if (!scrollIndicator)
  2151. return;
  2152. if (scrollIndicatorEnabled && scrollPos > scrollIndicatorOffsetThreshold && !scrollIndicator.classList.contains("bytm-hidden")) {
  2153. scrollIndicator.classList.add("bytm-hidden");
  2154. }
  2155. else if (scrollIndicatorEnabled && scrollPos <= scrollIndicatorOffsetThreshold && scrollIndicator.classList.contains("bytm-hidden")) {
  2156. scrollIndicator.classList.remove("bytm-hidden");
  2157. }
  2158. });
  2159. const bottomAnchor = document.createElement("div");
  2160. bottomAnchor.id = "bytm-menu-bottom-anchor";
  2161. featuresCont.appendChild(bottomAnchor);
  2162. //#SECTION finalize
  2163. menuContainer.appendChild(headerElem);
  2164. menuContainer.appendChild(featuresCont);
  2165. const subtitleElemCont = document.createElement("div");
  2166. subtitleElemCont.id = "bytm-menu-subtitle-cont";
  2167. const versionEl = document.createElement("a");
  2168. versionEl.id = "bytm-menu-version-anchor";
  2169. versionEl.classList.add("bytm-link");
  2170. versionEl.role = "button";
  2171. versionEl.tabIndex = 0;
  2172. versionEl.ariaLabel = versionEl.title = t("version_tooltip", scriptInfo.version, buildNumber);
  2173. versionEl.textContent = `v${scriptInfo.version} (#${buildNumber})`;
  2174. onInteraction(versionEl, (e) => __awaiter(this, void 0, void 0, function* () {
  2175. e.preventDefault();
  2176. e.stopPropagation();
  2177. yield openChangelogMenu("cfgMenu");
  2178. closeCfgMenu(undefined, false);
  2179. }));
  2180. subtitleElemCont.appendChild(versionEl);
  2181. titleElem.appendChild(subtitleElemCont);
  2182. const modeItems = [];
  2183. mode === "development" && modeItems.push("dev_mode");
  2184. getFeatures().advancedMode && modeItems.push("advanced_mode");
  2185. if (modeItems.length > 0) {
  2186. const modeDisplayEl = document.createElement("span");
  2187. modeDisplayEl.id = "bytm-menu-mode-display";
  2188. modeDisplayEl.textContent = `[${t("active_mode_display", arrayWithSeparators(modeItems.map(v => t(`${v}_short`)), ", ", " & "))}]`;
  2189. modeDisplayEl.ariaLabel = modeDisplayEl.title = tp("active_mode_tooltip", modeItems, arrayWithSeparators(modeItems.map(t), ", ", " & "));
  2190. subtitleElemCont.appendChild(modeDisplayEl);
  2191. }
  2192. menuContainer.appendChild(footerCont);
  2193. backgroundElem.appendChild(menuContainer);
  2194. document.body.appendChild(backgroundElem);
  2195. window.addEventListener("resize", UserUtils.debounce(checkToggleScrollIndicator, 150));
  2196. log("Added menu element");
  2197. // ensure stuff is reset if menu was opened before being added
  2198. isCfgMenuOpen = false;
  2199. document.body.classList.remove("bytm-disable-scroll");
  2200. (_f = document.querySelector("ytmusic-app")) === null || _f === void 0 ? void 0 : _f.removeAttribute("inert");
  2201. backgroundElem.style.visibility = "hidden";
  2202. backgroundElem.style.display = "none";
  2203. });
  2204. }
  2205. /** Closes the config menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  2206. function closeCfgMenu(evt, enableScroll = true) {
  2207. var _a, _b;
  2208. if (!isCfgMenuOpen)
  2209. return;
  2210. isCfgMenuOpen = false;
  2211. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  2212. if (enableScroll) {
  2213. document.body.classList.remove("bytm-disable-scroll");
  2214. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
  2215. }
  2216. const menuBg = document.querySelector("#bytm-cfg-menu-bg");
  2217. siteEvents.emit("cfgMenuClosed");
  2218. if (!menuBg)
  2219. return;
  2220. (_b = menuBg.querySelectorAll(".bytm-ftconf-adv-copy-hint")) === null || _b === void 0 ? void 0 : _b.forEach((el) => el.style.display = "none");
  2221. clearTimeout(hiddenCopiedTxtTimeout);
  2222. menuBg.style.visibility = "hidden";
  2223. menuBg.style.display = "none";
  2224. }
  2225. /** Opens the config menu if it is closed */
  2226. function openCfgMenu() {
  2227. var _a;
  2228. return __awaiter(this, void 0, void 0, function* () {
  2229. if (!isCfgMenuAdded)
  2230. yield addCfgMenu();
  2231. if (isCfgMenuOpen)
  2232. return;
  2233. isCfgMenuOpen = true;
  2234. document.body.classList.add("bytm-disable-scroll");
  2235. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
  2236. const menuBg = document.querySelector("#bytm-cfg-menu-bg");
  2237. if (!menuBg)
  2238. return;
  2239. menuBg.style.visibility = "visible";
  2240. menuBg.style.display = "block";
  2241. checkToggleScrollIndicator();
  2242. });
  2243. }
  2244. /** Checks if the features container is scrollable and toggles the scroll indicator accordingly */
  2245. function checkToggleScrollIndicator() {
  2246. const featuresCont = document.querySelector("#bytm-menu-opts");
  2247. const scrollIndicator = document.querySelector("#bytm-menu-scroll-indicator");
  2248. // disable scroll indicator if container doesn't scroll
  2249. if (featuresCont && scrollIndicator) {
  2250. const verticalScroll = UserUtils.isScrollable(featuresCont).vertical;
  2251. /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
  2252. const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
  2253. if (!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
  2254. scrollIndicatorEnabled = true;
  2255. scrollIndicator.classList.remove("bytm-hidden");
  2256. }
  2257. if ((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
  2258. scrollIndicatorEnabled = false;
  2259. scrollIndicator.classList.add("bytm-hidden");
  2260. }
  2261. }
  2262. }
  2263. //#MARKER help dialog
  2264. let isHelpDialogOpen = false;
  2265. /** Key of the feature currently loaded in the help dialog */
  2266. let helpDialogCurFeature;
  2267. /** Opens the feature help dialog for the given feature */
  2268. function openHelpDialog(featureKey) {
  2269. var _a, _b, _c;
  2270. return __awaiter(this, void 0, void 0, function* () {
  2271. if (isHelpDialogOpen)
  2272. return;
  2273. isHelpDialogOpen = true;
  2274. let menuBgElem;
  2275. if (!helpDialogCurFeature) {
  2276. // create menu
  2277. const headerElem = document.createElement("div");
  2278. headerElem.classList.add("bytm-menu-header", "small");
  2279. const titleCont = document.createElement("div");
  2280. titleCont.className = "bytm-menu-titlecont-no-title";
  2281. titleCont.role = "heading";
  2282. titleCont.ariaLevel = "1";
  2283. const helpIconSvg = yield resourceToHTMLString("icon-help");
  2284. if (helpIconSvg)
  2285. titleCont.innerHTML = helpIconSvg;
  2286. const closeElem = document.createElement("img");
  2287. closeElem.classList.add("bytm-menu-close", "small");
  2288. closeElem.role = "button";
  2289. closeElem.tabIndex = 0;
  2290. closeElem.src = yield getResourceUrl("img-close");
  2291. closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
  2292. onInteraction(closeElem, closeHelpDialog);
  2293. headerElem.appendChild(titleCont);
  2294. headerElem.appendChild(closeElem);
  2295. menuBgElem = document.createElement("div");
  2296. menuBgElem.id = "bytm-feat-help-menu-bg";
  2297. menuBgElem.classList.add("bytm-menu-bg");
  2298. menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
  2299. menuBgElem.style.visibility = "hidden";
  2300. menuBgElem.style.display = "none";
  2301. menuBgElem.addEventListener("click", (e) => {
  2302. var _a;
  2303. if (isHelpDialogOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-feat-help-menu-bg")
  2304. closeHelpDialog(e);
  2305. });
  2306. document.body.addEventListener("keydown", (e) => {
  2307. if (isHelpDialogOpen && e.key === "Escape")
  2308. closeHelpDialog(e);
  2309. });
  2310. const menuContainer = document.createElement("div");
  2311. menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
  2312. menuContainer.classList.add("bytm-menu");
  2313. menuContainer.id = "bytm-feat-help-menu";
  2314. const featDescElem = document.createElement("h3");
  2315. featDescElem.id = "bytm-feat-help-menu-desc";
  2316. const helpTextElem = document.createElement("div");
  2317. helpTextElem.id = "bytm-feat-help-menu-text";
  2318. menuContainer.appendChild(headerElem);
  2319. menuContainer.appendChild(featDescElem);
  2320. menuContainer.appendChild(helpTextElem);
  2321. menuBgElem.appendChild(menuContainer);
  2322. document.body.appendChild(menuBgElem);
  2323. }
  2324. else
  2325. menuBgElem = document.querySelector("#bytm-feat-help-menu-bg");
  2326. if (helpDialogCurFeature !== featureKey) {
  2327. // update help text
  2328. const featDescElem = menuBgElem.querySelector("#bytm-feat-help-menu-desc");
  2329. const helpTextElem = menuBgElem.querySelector("#bytm-feat-help-menu-text");
  2330. featDescElem.textContent = t(`feature_desc_${featureKey}`);
  2331. // @ts-ignore
  2332. const helpText = (_b = (_a = featInfo[featureKey]) === null || _a === void 0 ? void 0 : _a.helpText) === null || _b === void 0 ? void 0 : _b.call(_a);
  2333. helpTextElem.textContent = helpText !== null && helpText !== void 0 ? helpText : t(`feature_helptext_${featureKey}`);
  2334. }
  2335. // show menu
  2336. const menuBg = document.querySelector("#bytm-feat-help-menu-bg");
  2337. if (!menuBg)
  2338. return warn("Couldn't find feature help dialog background element");
  2339. helpDialogCurFeature = featureKey;
  2340. menuBg.style.visibility = "visible";
  2341. menuBg.style.display = "block";
  2342. (_c = document.querySelector("#bytm-cfg-menu")) === null || _c === void 0 ? void 0 : _c.setAttribute("inert", "true");
  2343. });
  2344. }
  2345. function closeHelpDialog(evt) {
  2346. var _a;
  2347. if (!isHelpDialogOpen)
  2348. return;
  2349. isHelpDialogOpen = false;
  2350. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  2351. const menuBg = document.querySelector("#bytm-feat-help-menu-bg");
  2352. if (!menuBg)
  2353. return warn("Couldn't find feature help dialog background element");
  2354. menuBg.style.visibility = "hidden";
  2355. menuBg.style.display = "none";
  2356. (_a = document.querySelector("#bytm-cfg-menu")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
  2357. }
  2358. //#MARKER export menu
  2359. let isExportMenuAdded = false;
  2360. let isExportMenuOpen = false;
  2361. let copiedTxtTimeout = undefined;
  2362. let lastUncompressedCfgString;
  2363. /** Adds a menu to copy the current configuration as compressed (if supported) or uncompressed JSON (hidden by default) */
  2364. function addExportMenu() {
  2365. return __awaiter(this, void 0, void 0, function* () {
  2366. const canCompress = yield compressionSupported();
  2367. const menuBgElem = document.createElement("div");
  2368. menuBgElem.id = "bytm-export-menu-bg";
  2369. menuBgElem.classList.add("bytm-menu-bg");
  2370. menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
  2371. menuBgElem.style.visibility = "hidden";
  2372. menuBgElem.style.display = "none";
  2373. menuBgElem.addEventListener("click", (e) => {
  2374. var _a;
  2375. if (isExportMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-export-menu-bg") {
  2376. closeExportMenu(e);
  2377. openCfgMenu();
  2378. }
  2379. });
  2380. document.body.addEventListener("keydown", (e) => {
  2381. if (isExportMenuOpen && e.key === "Escape") {
  2382. closeExportMenu(e);
  2383. openCfgMenu();
  2384. }
  2385. });
  2386. const menuContainer = document.createElement("div");
  2387. menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
  2388. menuContainer.classList.add("bytm-menu");
  2389. menuContainer.id = "bytm-export-menu";
  2390. //#SECTION title bar
  2391. const headerElem = document.createElement("div");
  2392. headerElem.classList.add("bytm-menu-header");
  2393. const titleCont = document.createElement("div");
  2394. titleCont.className = "bytm-menu-titlecont";
  2395. titleCont.role = "heading";
  2396. titleCont.ariaLevel = "1";
  2397. const titleElem = document.createElement("h2");
  2398. titleElem.className = "bytm-menu-title";
  2399. titleElem.textContent = t("export_menu_title", scriptInfo.name);
  2400. const closeElem = document.createElement("img");
  2401. closeElem.classList.add("bytm-menu-close");
  2402. closeElem.role = "button";
  2403. closeElem.tabIndex = 0;
  2404. closeElem.src = yield getResourceUrl("img-close");
  2405. closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
  2406. onInteraction(closeElem, (e) => {
  2407. closeExportMenu(e);
  2408. openCfgMenu();
  2409. });
  2410. titleCont.appendChild(titleElem);
  2411. headerElem.appendChild(titleCont);
  2412. headerElem.appendChild(closeElem);
  2413. //#SECTION body
  2414. const menuBodyElem = document.createElement("div");
  2415. menuBodyElem.classList.add("bytm-menu-body");
  2416. const textElem = document.createElement("div");
  2417. textElem.id = "bytm-export-menu-text";
  2418. textElem.textContent = t("export_hint");
  2419. const textAreaElem = document.createElement("textarea");
  2420. textAreaElem.id = "bytm-export-menu-textarea";
  2421. textAreaElem.readOnly = true;
  2422. lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
  2423. textAreaElem.value = t("click_to_reveal_sensitive_info");
  2424. textAreaElem.setAttribute("revealed", "false");
  2425. const textAreaInteraction = () => __awaiter(this, void 0, void 0, function* () {
  2426. const cfgString = JSON.stringify({ formatVersion, data: getFeatures() });
  2427. lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
  2428. textAreaElem.value = canCompress ? yield UserUtils.compress(cfgString, compressionFormat, "string") : cfgString;
  2429. textAreaElem.setAttribute("revealed", "true");
  2430. });
  2431. onInteraction(textAreaElem, textAreaInteraction);
  2432. siteEvents.on("configChanged", (data) => __awaiter(this, void 0, void 0, function* () {
  2433. const textAreaElem = document.querySelector("#bytm-export-menu-textarea");
  2434. const cfgString = JSON.stringify({ formatVersion, data });
  2435. lastUncompressedCfgString = JSON.stringify({ formatVersion, data }, undefined, 2);
  2436. if (textAreaElem) {
  2437. if (textAreaElem.getAttribute("revealed") !== "true")
  2438. return;
  2439. textAreaElem.value = canCompress ? yield UserUtils.compress(cfgString, compressionFormat, "string") : cfgString;
  2440. }
  2441. }));
  2442. //#SECTION footer
  2443. const footerElem = document.createElement("div");
  2444. footerElem.classList.add("bytm-menu-footer-right");
  2445. const copyBtnElem = document.createElement("button");
  2446. copyBtnElem.classList.add("bytm-btn");
  2447. copyBtnElem.textContent = t("copy_to_clipboard");
  2448. copyBtnElem.ariaLabel = copyBtnElem.title = t("copy_config_tooltip");
  2449. const copiedTextElem = document.createElement("span");
  2450. copiedTextElem.id = "bytm-export-menu-copied-txt";
  2451. copiedTextElem.classList.add("bytm-menu-footer-copied");
  2452. copiedTextElem.textContent = t("copied");
  2453. copiedTextElem.style.display = "none";
  2454. onInteraction(copyBtnElem, (evt) => __awaiter(this, void 0, void 0, function* () {
  2455. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  2456. GM.setClipboard(String((evt === null || evt === void 0 ? void 0 : evt.shiftKey) || (evt === null || evt === void 0 ? void 0 : evt.ctrlKey) ? lastUncompressedCfgString : yield UserUtils.compress(JSON.stringify({ formatVersion, data: getFeatures() }), compressionFormat, "string")));
  2457. copiedTextElem.style.display = "inline-block";
  2458. if (typeof copiedTxtTimeout === "undefined") {
  2459. copiedTxtTimeout = setTimeout(() => {
  2460. copiedTextElem.style.display = "none";
  2461. copiedTxtTimeout = undefined;
  2462. }, 3000);
  2463. }
  2464. }));
  2465. // flex-direction is row-reverse
  2466. footerElem.appendChild(copyBtnElem);
  2467. footerElem.appendChild(copiedTextElem);
  2468. //#SECTION finalize
  2469. menuBodyElem.appendChild(textElem);
  2470. menuBodyElem.appendChild(textAreaElem);
  2471. menuBodyElem.appendChild(footerElem);
  2472. menuContainer.appendChild(headerElem);
  2473. menuContainer.appendChild(menuBodyElem);
  2474. menuBgElem.appendChild(menuContainer);
  2475. document.body.appendChild(menuBgElem);
  2476. });
  2477. }
  2478. /** Closes the export menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  2479. function closeExportMenu(evt) {
  2480. if (!isExportMenuOpen)
  2481. return;
  2482. isExportMenuOpen = false;
  2483. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  2484. const menuBg = document.querySelector("#bytm-export-menu-bg");
  2485. if (!menuBg)
  2486. return warn("Couldn't find export menu background element");
  2487. menuBg.style.visibility = "hidden";
  2488. menuBg.style.display = "none";
  2489. const textAreaElem = menuBg.querySelector("#bytm-export-menu-textarea");
  2490. if (textAreaElem) {
  2491. textAreaElem.value = t("click_to_reveal_sensitive_info");
  2492. textAreaElem.setAttribute("revealed", "false");
  2493. }
  2494. const copiedTxtElem = document.querySelector("#bytm-export-menu-copied-txt");
  2495. if (copiedTxtElem) {
  2496. copiedTxtElem.style.display = "none";
  2497. if (typeof copiedTxtTimeout === "number") {
  2498. clearTimeout(copiedTxtTimeout);
  2499. copiedTxtTimeout = undefined;
  2500. }
  2501. }
  2502. }
  2503. /** Opens the export menu if it is closed */
  2504. function openExportMenu() {
  2505. var _a;
  2506. return __awaiter(this, void 0, void 0, function* () {
  2507. if (!isExportMenuAdded)
  2508. yield addExportMenu();
  2509. isExportMenuAdded = true;
  2510. if (isExportMenuOpen)
  2511. return;
  2512. isExportMenuOpen = true;
  2513. document.body.classList.add("bytm-disable-scroll");
  2514. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
  2515. const menuBg = document.querySelector("#bytm-export-menu-bg");
  2516. if (!menuBg)
  2517. return warn("Couldn't find export menu background element");
  2518. menuBg.style.visibility = "visible";
  2519. menuBg.style.display = "block";
  2520. });
  2521. }
  2522. //#MARKER import menu
  2523. let isImportMenuAdded = false;
  2524. let isImportMenuOpen = false;
  2525. /** Adds a menu to import a configuration from compressed or uncompressed JSON (hidden by default) */
  2526. function addImportMenu() {
  2527. return __awaiter(this, void 0, void 0, function* () {
  2528. const menuBgElem = document.createElement("div");
  2529. menuBgElem.id = "bytm-import-menu-bg";
  2530. menuBgElem.classList.add("bytm-menu-bg");
  2531. menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
  2532. menuBgElem.style.visibility = "hidden";
  2533. menuBgElem.style.display = "none";
  2534. menuBgElem.addEventListener("click", (e) => {
  2535. var _a;
  2536. if (isImportMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-import-menu-bg") {
  2537. closeImportMenu(e);
  2538. openCfgMenu();
  2539. }
  2540. });
  2541. document.body.addEventListener("keydown", (e) => {
  2542. if (isImportMenuOpen && e.key === "Escape") {
  2543. closeImportMenu(e);
  2544. openCfgMenu();
  2545. }
  2546. });
  2547. const menuContainer = document.createElement("div");
  2548. menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
  2549. menuContainer.classList.add("bytm-menu");
  2550. menuContainer.id = "bytm-import-menu";
  2551. //#SECTION title bar
  2552. const headerElem = document.createElement("div");
  2553. headerElem.classList.add("bytm-menu-header");
  2554. const titleCont = document.createElement("div");
  2555. titleCont.className = "bytm-menu-titlecont";
  2556. titleCont.role = "heading";
  2557. titleCont.ariaLevel = "1";
  2558. const titleElem = document.createElement("h2");
  2559. titleElem.className = "bytm-menu-title";
  2560. titleElem.textContent = t("import_menu_title", scriptInfo.name);
  2561. const closeElem = document.createElement("img");
  2562. closeElem.classList.add("bytm-menu-close");
  2563. closeElem.role = "button";
  2564. closeElem.tabIndex = 0;
  2565. closeElem.src = yield getResourceUrl("img-close");
  2566. closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
  2567. onInteraction(closeElem, (e) => {
  2568. closeImportMenu(e);
  2569. openCfgMenu();
  2570. });
  2571. titleCont.appendChild(titleElem);
  2572. headerElem.appendChild(titleCont);
  2573. headerElem.appendChild(closeElem);
  2574. //#SECTION body
  2575. const menuBodyElem = document.createElement("div");
  2576. menuBodyElem.classList.add("bytm-menu-body");
  2577. const textElem = document.createElement("div");
  2578. textElem.id = "bytm-import-menu-text";
  2579. textElem.textContent = t("import_hint");
  2580. const textAreaElem = document.createElement("textarea");
  2581. textAreaElem.id = "bytm-import-menu-textarea";
  2582. //#SECTION footer
  2583. const footerElem = document.createElement("div");
  2584. footerElem.classList.add("bytm-menu-footer-right");
  2585. const importBtnElem = document.createElement("button");
  2586. importBtnElem.classList.add("bytm-btn");
  2587. importBtnElem.textContent = t("import");
  2588. importBtnElem.ariaLabel = importBtnElem.title = t("start_import_tooltip");
  2589. importBtnElem.addEventListener("click", (evt) => __awaiter(this, void 0, void 0, function* () {
  2590. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  2591. const textAreaElem = document.querySelector("#bytm-import-menu-textarea");
  2592. if (!textAreaElem)
  2593. return warn("Couldn't find import menu textarea element");
  2594. try {
  2595. /** Tries to parse an uncompressed or compressed input string as a JSON object */
  2596. const decode = (input) => __awaiter(this, void 0, void 0, function* () {
  2597. try {
  2598. return JSON.parse(input);
  2599. }
  2600. catch (_a) {
  2601. try {
  2602. return JSON.parse(yield UserUtils.decompress(input, compressionFormat, "string"));
  2603. }
  2604. catch (err) {
  2605. warn("Couldn't import configuration:", err);
  2606. return null;
  2607. }
  2608. }
  2609. });
  2610. const parsed = yield decode(textAreaElem.value.trim());
  2611. if (typeof parsed !== "object")
  2612. return alert(t("import_error_invalid"));
  2613. if (typeof parsed.formatVersion !== "number")
  2614. return alert(t("import_error_no_format_version"));
  2615. if (typeof parsed.data !== "object" || parsed.data === null || Object.keys(parsed.data).length === 0)
  2616. return alert(t("import_error_no_data"));
  2617. if (parsed.formatVersion < formatVersion) {
  2618. let newData = JSON.parse(JSON.stringify(parsed.data));
  2619. const sortedMigrations = Object.entries(migrations)
  2620. .sort(([a], [b]) => Number(a) - Number(b));
  2621. let curFmtVer = Number(parsed.formatVersion);
  2622. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  2623. const ver = Number(fmtVer);
  2624. if (curFmtVer < formatVersion && curFmtVer < ver) {
  2625. try {
  2626. const migRes = JSON.parse(JSON.stringify(migrationFunc(newData)));
  2627. newData = migRes instanceof Promise ? yield migRes : migRes;
  2628. curFmtVer = ver;
  2629. }
  2630. catch (err) {
  2631. error(`Error while running migration function for format version ${fmtVer}:`, err);
  2632. }
  2633. }
  2634. }
  2635. parsed.formatVersion = curFmtVer;
  2636. parsed.data = newData;
  2637. }
  2638. else if (parsed.formatVersion !== formatVersion)
  2639. return alert(t("import_error_wrong_format_version", formatVersion, parsed.formatVersion));
  2640. yield setFeatures(Object.assign(Object.assign({}, getFeatures()), parsed.data));
  2641. if (confirm(t("import_success_confirm_reload"))) {
  2642. disableBeforeUnload();
  2643. return location.reload();
  2644. }
  2645. emitSiteEvent("rebuildCfgMenu", parsed.data);
  2646. closeImportMenu();
  2647. openCfgMenu();
  2648. }
  2649. catch (err) {
  2650. warn("Couldn't import configuration:", err);
  2651. alert(t("import_error_invalid"));
  2652. }
  2653. }));
  2654. footerElem.appendChild(importBtnElem);
  2655. //#SECTION finalize
  2656. menuBodyElem.appendChild(textElem);
  2657. menuBodyElem.appendChild(textAreaElem);
  2658. menuBodyElem.appendChild(footerElem);
  2659. menuContainer.appendChild(headerElem);
  2660. menuContainer.appendChild(menuBodyElem);
  2661. menuBgElem.appendChild(menuContainer);
  2662. document.body.appendChild(menuBgElem);
  2663. });
  2664. }
  2665. /** Closes the import menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  2666. function closeImportMenu(evt) {
  2667. if (!isImportMenuOpen)
  2668. return;
  2669. isImportMenuOpen = false;
  2670. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  2671. const menuBg = document.querySelector("#bytm-import-menu-bg");
  2672. const textAreaElem = document.querySelector("#bytm-import-menu-textarea");
  2673. if (textAreaElem)
  2674. textAreaElem.value = "";
  2675. if (!menuBg)
  2676. return warn("Couldn't find import menu background element");
  2677. menuBg.style.visibility = "hidden";
  2678. menuBg.style.display = "none";
  2679. }
  2680. /** Opens the import menu if it is closed */
  2681. function openImportMenu() {
  2682. var _a;
  2683. return __awaiter(this, void 0, void 0, function* () {
  2684. if (!isImportMenuAdded)
  2685. yield addImportMenu();
  2686. isImportMenuAdded = true;
  2687. if (isImportMenuOpen)
  2688. return;
  2689. isImportMenuOpen = true;
  2690. document.body.classList.add("bytm-disable-scroll");
  2691. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
  2692. const menuBg = document.querySelector("#bytm-import-menu-bg");
  2693. if (!menuBg)
  2694. return warn("Couldn't find import menu background element");
  2695. menuBg.style.visibility = "visible";
  2696. menuBg.style.display = "block";
  2697. });
  2698. }
  2699. //#MARKER changelog menu
  2700. let isChangelogMenuAdded = false;
  2701. let isChangelogMenuOpen = false;
  2702. /** Adds a changelog menu (hidden by default) */
  2703. function addChangelogMenu() {
  2704. return __awaiter(this, void 0, void 0, function* () {
  2705. const menuBgElem = document.createElement("div");
  2706. menuBgElem.id = "bytm-changelog-menu-bg";
  2707. menuBgElem.classList.add("bytm-menu-bg");
  2708. menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
  2709. menuBgElem.style.visibility = "hidden";
  2710. menuBgElem.style.display = "none";
  2711. menuBgElem.addEventListener("click", (e) => {
  2712. var _a;
  2713. if (isChangelogMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-changelog-menu-bg") {
  2714. closeChangelogMenu(e);
  2715. if (menuBgElem.dataset.returnTo === "cfgMenu")
  2716. openCfgMenu();
  2717. }
  2718. });
  2719. document.body.addEventListener("keydown", (e) => {
  2720. if (isChangelogMenuOpen && e.key === "Escape") {
  2721. closeChangelogMenu(e);
  2722. if (menuBgElem.dataset.returnTo === "cfgMenu")
  2723. openCfgMenu();
  2724. }
  2725. });
  2726. const menuContainer = document.createElement("div");
  2727. menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
  2728. menuContainer.classList.add("bytm-menu", "top-aligned");
  2729. menuContainer.id = "bytm-changelog-menu";
  2730. //#SECTION title bar
  2731. const headerElem = document.createElement("div");
  2732. headerElem.classList.add("bytm-menu-header");
  2733. const titleCont = document.createElement("div");
  2734. titleCont.className = "bytm-menu-titlecont";
  2735. titleCont.role = "heading";
  2736. titleCont.ariaLevel = "1";
  2737. const titleElem = document.createElement("h2");
  2738. titleElem.className = "bytm-menu-title";
  2739. titleElem.textContent = t("changelog_menu_title", scriptInfo.name);
  2740. const closeElem = document.createElement("img");
  2741. closeElem.classList.add("bytm-menu-close");
  2742. closeElem.role = "button";
  2743. closeElem.tabIndex = 0;
  2744. closeElem.src = yield getResourceUrl("img-close");
  2745. closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
  2746. onInteraction(closeElem, (e) => {
  2747. closeChangelogMenu(e);
  2748. if (menuBgElem.dataset.returnTo === "cfgMenu")
  2749. openCfgMenu();
  2750. });
  2751. titleCont.appendChild(titleElem);
  2752. headerElem.appendChild(titleCont);
  2753. headerElem.appendChild(closeElem);
  2754. //#SECTION body
  2755. const menuBodyElem = document.createElement("div");
  2756. menuBodyElem.id = "bytm-changelog-menu-body";
  2757. menuBodyElem.classList.add("bytm-menu-body");
  2758. const textElem = document.createElement("div");
  2759. textElem.id = "bytm-changelog-menu-text";
  2760. textElem.classList.add("bytm-markdown-container");
  2761. textElem.innerHTML = yield getChangelogHtmlWithDetails();
  2762. //#SECTION finalize
  2763. menuBodyElem.appendChild(textElem);
  2764. menuContainer.appendChild(headerElem);
  2765. menuContainer.appendChild(menuBodyElem);
  2766. menuBgElem.appendChild(menuContainer);
  2767. document.body.appendChild(menuBgElem);
  2768. const anchors = document.querySelectorAll("#bytm-changelog-menu-text a");
  2769. for (const anchor of anchors) {
  2770. anchor.ariaLabel = anchor.title = anchor.href;
  2771. anchor.target = "_blank";
  2772. }
  2773. });
  2774. }
  2775. /** Closes the changelog menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  2776. function closeChangelogMenu(evt) {
  2777. if (!isChangelogMenuOpen)
  2778. return;
  2779. isChangelogMenuOpen = false;
  2780. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  2781. const menuBg = document.querySelector("#bytm-changelog-menu-bg");
  2782. if (!menuBg)
  2783. return warn("Couldn't find changelog menu background element");
  2784. menuBg.style.visibility = "hidden";
  2785. menuBg.style.display = "none";
  2786. }
  2787. /**
  2788. * Opens the changelog menu if it is closed
  2789. * @param returnTo What menu to open after the changelog menu is closed
  2790. */
  2791. function openChangelogMenu(returnTo = "cfgMenu") {
  2792. var _a;
  2793. return __awaiter(this, void 0, void 0, function* () {
  2794. if (!isChangelogMenuAdded)
  2795. yield addChangelogMenu();
  2796. isChangelogMenuAdded = true;
  2797. if (isChangelogMenuOpen)
  2798. return;
  2799. isChangelogMenuOpen = true;
  2800. document.body.classList.add("bytm-disable-scroll");
  2801. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
  2802. const menuBg = document.querySelector("#bytm-changelog-menu-bg");
  2803. if (!menuBg)
  2804. return warn("Couldn't find changelog menu background element");
  2805. const firstDetails = menuBg.querySelector("#bytm-changelog-menu-text details");
  2806. if (firstDetails)
  2807. firstDetails.open = true;
  2808. menuBg.dataset.returnTo = returnTo;
  2809. menuBg.style.visibility = "visible";
  2810. menuBg.style.display = "block";
  2811. });
  2812. }//#MARKER BYTM-Config buttons
  2813. let logoExchanged = false, improveLogoCalled = false;
  2814. /** Adds a watermark beneath the logo */
  2815. function addWatermark() {
  2816. return __awaiter(this, void 0, void 0, function* () {
  2817. const watermark = document.createElement("a");
  2818. watermark.role = "button";
  2819. watermark.id = "bytm-watermark";
  2820. watermark.className = "style-scope ytmusic-nav-bar bytm-no-select";
  2821. watermark.textContent = scriptInfo.name;
  2822. watermark.ariaLabel = watermark.title = t("open_menu_tooltip", scriptInfo.name);
  2823. watermark.tabIndex = 0;
  2824. improveLogo();
  2825. const watermarkOpenMenu = (e) => {
  2826. e.stopPropagation();
  2827. if ((!e.shiftKey && !e.ctrlKey) || logoExchanged)
  2828. openCfgMenu();
  2829. if (!logoExchanged && (e.shiftKey || e.ctrlKey))
  2830. exchangeLogo();
  2831. };
  2832. onInteraction(watermark, watermarkOpenMenu);
  2833. addSelectorListener("navBar", "ytmusic-nav-bar #left-content", {
  2834. listener: (logoElem) => UserUtils.insertAfter(logoElem, watermark),
  2835. });
  2836. log("Added watermark element");
  2837. });
  2838. }
  2839. /** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
  2840. function improveLogo() {
  2841. return __awaiter(this, void 0, void 0, function* () {
  2842. try {
  2843. if (improveLogoCalled)
  2844. return;
  2845. improveLogoCalled = true;
  2846. const res = yield UserUtils.fetchAdvanced("https://music.youtube.com/img/on_platform_logo_dark.svg");
  2847. const svg = yield res.text();
  2848. addSelectorListener("navBar", "ytmusic-logo a", {
  2849. listener: (logoElem) => {
  2850. var _a;
  2851. logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
  2852. logoElem.innerHTML = svg;
  2853. logoElem.querySelectorAll("ellipse").forEach((e) => {
  2854. e.classList.add("bytm-mod-logo-ellipse");
  2855. });
  2856. (_a = logoElem.querySelector("path")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-mod-logo-path");
  2857. log("Swapped logo to inline SVG");
  2858. },
  2859. });
  2860. }
  2861. catch (err) {
  2862. error("Couldn't improve logo due to an error:", err);
  2863. }
  2864. });
  2865. }
  2866. /** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
  2867. function exchangeLogo() {
  2868. addSelectorListener("navBar", ".bytm-mod-logo", {
  2869. listener: (logoElem) => __awaiter(this, void 0, void 0, function* () {
  2870. if (logoElem.classList.contains("bytm-logo-exchanged"))
  2871. return;
  2872. logoExchanged = true;
  2873. logoElem.classList.add("bytm-logo-exchanged");
  2874. const iconUrl = yield getResourceUrl("img-logo");
  2875. const newLogo = document.createElement("img");
  2876. newLogo.className = "bytm-mod-logo-img";
  2877. newLogo.src = iconUrl;
  2878. logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
  2879. document.head.querySelectorAll("link[rel=\"icon\"]").forEach((e) => {
  2880. e.href = iconUrl;
  2881. });
  2882. setTimeout(() => {
  2883. logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
  2884. }, 1000);
  2885. }),
  2886. });
  2887. }
  2888. /** Called whenever the avatar popover menu exists on YTM to add a BYTM config menu button to the user menu popover */
  2889. function addConfigMenuOptionYTM(container) {
  2890. return __awaiter(this, void 0, void 0, function* () {
  2891. const cfgOptElem = document.createElement("div");
  2892. cfgOptElem.className = "bytm-cfg-menu-option";
  2893. const cfgOptItemElem = document.createElement("div");
  2894. cfgOptItemElem.className = "bytm-cfg-menu-option-item";
  2895. cfgOptItemElem.role = "button";
  2896. cfgOptItemElem.tabIndex = 0;
  2897. cfgOptItemElem.ariaLabel = cfgOptItemElem.title = t("open_menu_tooltip", scriptInfo.name);
  2898. onInteraction(cfgOptItemElem, (e) => __awaiter(this, void 0, void 0, function* () {
  2899. const settingsBtnElem = document.querySelector("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
  2900. settingsBtnElem === null || settingsBtnElem === void 0 ? void 0 : settingsBtnElem.click();
  2901. yield UserUtils.pauseFor(20);
  2902. if ((!e.shiftKey && !e.ctrlKey) || logoExchanged)
  2903. openCfgMenu();
  2904. if (!logoExchanged && (e.shiftKey || e.ctrlKey))
  2905. exchangeLogo();
  2906. }));
  2907. const cfgOptIconElem = document.createElement("img");
  2908. cfgOptIconElem.className = "bytm-cfg-menu-option-icon";
  2909. cfgOptIconElem.src = yield getResourceUrl("img-logo");
  2910. const cfgOptTextElem = document.createElement("div");
  2911. cfgOptTextElem.className = "bytm-cfg-menu-option-text";
  2912. cfgOptTextElem.textContent = t("config_menu_option", scriptInfo.name);
  2913. cfgOptItemElem.appendChild(cfgOptIconElem);
  2914. cfgOptItemElem.appendChild(cfgOptTextElem);
  2915. cfgOptElem.appendChild(cfgOptItemElem);
  2916. container.appendChild(cfgOptElem);
  2917. improveLogo();
  2918. log("Added BYTM-Configuration button to menu popover");
  2919. });
  2920. }
  2921. /** Called whenever the titlebar (masthead) exists on YT to add a BYTM config menu button */
  2922. function addConfigMenuOptionYT(container) {
  2923. return __awaiter(this, void 0, void 0, function* () {
  2924. const btnElem = yield createGenericBtn({
  2925. resourceName: "img-logo",
  2926. title: t("open_menu_tooltip", scriptInfo.name),
  2927. onClick(e) {
  2928. if ((!e.shiftKey && !e.ctrlKey) || logoExchanged)
  2929. openCfgMenu();
  2930. if (!logoExchanged && (e.shiftKey || e.ctrlKey))
  2931. exchangeLogo();
  2932. },
  2933. });
  2934. const firstChild = container.firstElementChild;
  2935. if (firstChild)
  2936. container.insertBefore(btnElem, firstChild);
  2937. else {
  2938. const notifEl = container.querySelector("ytd-notification-topbar-button-renderer");
  2939. notifEl && UserUtils.insertAfter(notifEl, btnElem);
  2940. }
  2941. });
  2942. }
  2943. //#MARKER remove upgrade tab
  2944. /** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
  2945. function removeUpgradeTab() {
  2946. return __awaiter(this, void 0, void 0, function* () {
  2947. addSelectorListener("sideBar", "#contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
  2948. listener: (tabElemLarge) => {
  2949. tabElemLarge.remove();
  2950. log("Removed large upgrade tab");
  2951. },
  2952. });
  2953. addSelectorListener("sideBarMini", "ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
  2954. listener: (tabElemSmall) => {
  2955. tabElemSmall.remove();
  2956. log("Removed small upgrade tab");
  2957. },
  2958. });
  2959. });
  2960. }
  2961. //#MARKER anchor improvements
  2962. /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
  2963. function addAnchorImprovements() {
  2964. return __awaiter(this, void 0, void 0, function* () {
  2965. try {
  2966. const css = yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("css-anchor_improvements"))).text();
  2967. if (css)
  2968. UserUtils.addGlobalStyle(css).id = "bytm-style-anchor-improvements";
  2969. }
  2970. catch (err) {
  2971. error("Couldn't add anchor improvements CSS due to an error:", err);
  2972. }
  2973. //#SECTION carousel shelves
  2974. try {
  2975. const preventDefault = (e) => e.preventDefault();
  2976. /** Adds anchor improvements to &lt;ytmusic-responsive-list-item-renderer&gt; */
  2977. const addListItemAnchors = (items) => {
  2978. var _a;
  2979. for (const item of items) {
  2980. if (item.classList.contains("bytm-anchor-improved"))
  2981. continue;
  2982. item.classList.add("bytm-anchor-improved");
  2983. const thumbnailElem = item.querySelector(".left-items");
  2984. const titleElem = item.querySelector(".title-column .title a");
  2985. if (!thumbnailElem || !titleElem)
  2986. continue;
  2987. const anchorElem = document.createElement("a");
  2988. anchorElem.classList.add("bytm-anchor", "bytm-carousel-shelf-anchor");
  2989. anchorElem.href = (_a = titleElem === null || titleElem === void 0 ? void 0 : titleElem.href) !== null && _a !== void 0 ? _a : "#";
  2990. anchorElem.target = "_self";
  2991. anchorElem.role = "button";
  2992. anchorElem.addEventListener("click", preventDefault);
  2993. UserUtils.addParent(thumbnailElem, anchorElem);
  2994. }
  2995. };
  2996. // TODO: needs to be optimized
  2997. // home page
  2998. addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
  2999. continuous: true,
  3000. all: true,
  3001. listener: addListItemAnchors,
  3002. });
  3003. // related tab in /watch
  3004. addSelectorListener("body", "ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] ytmusic-responsive-list-item-renderer", {
  3005. continuous: true,
  3006. all: true,
  3007. listener: addListItemAnchors,
  3008. });
  3009. // playlists
  3010. addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer", {
  3011. continuous: true,
  3012. all: true,
  3013. listener: addListItemAnchors,
  3014. });
  3015. // generic shelves
  3016. addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer", {
  3017. continuous: true,
  3018. all: true,
  3019. listener: addListItemAnchors,
  3020. });
  3021. }
  3022. catch (err) {
  3023. error("Couldn't improve carousel shelf anchors due to an error:", err);
  3024. }
  3025. //#SECTION sidebar
  3026. try {
  3027. const addSidebarAnchors = (sidebarCont) => {
  3028. const items = sidebarCont.parentNode.querySelectorAll("ytmusic-guide-entry-renderer tp-yt-paper-item");
  3029. improveSidebarAnchors(items);
  3030. return items.length;
  3031. };
  3032. addSelectorListener("sideBar", "#contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
  3033. listener: (sidebarCont) => {
  3034. const itemsAmt = addSidebarAnchors(sidebarCont);
  3035. log(`Added anchors around ${itemsAmt} sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
  3036. },
  3037. });
  3038. addSelectorListener("sideBarMini", "ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
  3039. listener: (miniSidebarCont) => {
  3040. const itemsAmt = addSidebarAnchors(miniSidebarCont);
  3041. log(`Added anchors around ${itemsAmt} mini sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
  3042. },
  3043. });
  3044. }
  3045. catch (err) {
  3046. error("Couldn't add anchors to sidebar items due to an error:", err);
  3047. }
  3048. });
  3049. }
  3050. const sidebarPaths = [
  3051. "/",
  3052. "/explore",
  3053. "/library",
  3054. ];
  3055. /**
  3056. * Adds anchors to the sidebar items so they can be opened in a new tab
  3057. * @param sidebarItem
  3058. */
  3059. function improveSidebarAnchors(sidebarItems) {
  3060. sidebarItems.forEach((item, i) => {
  3061. var _a;
  3062. const anchorElem = document.createElement("a");
  3063. anchorElem.classList.add("bytm-anchor", "bytm-no-select");
  3064. anchorElem.role = "button";
  3065. anchorElem.target = "_self";
  3066. anchorElem.href = (_a = sidebarPaths[i]) !== null && _a !== void 0 ? _a : "#";
  3067. anchorElem.ariaLabel = anchorElem.title = t("middle_click_open_tab");
  3068. anchorElem.addEventListener("click", (e) => {
  3069. e.preventDefault();
  3070. });
  3071. UserUtils.addParent(item, anchorElem);
  3072. });
  3073. }
  3074. //#MARKER remove share tracking param
  3075. /** Removes the ?si tracking parameter from share URLs */
  3076. function initRemShareTrackParam() {
  3077. return __awaiter(this, void 0, void 0, function* () {
  3078. const removeSiParam = (inputElem) => {
  3079. try {
  3080. if (!inputElem.value.match(/(&|\?)si=/i))
  3081. return;
  3082. const url = new URL(inputElem.value);
  3083. url.searchParams.delete("si");
  3084. inputElem.value = String(url);
  3085. log(`Removed tracking parameter from share link -> ${url}`);
  3086. }
  3087. catch (err) {
  3088. warn("Couldn't remove tracking parameter from share link due to error:", err);
  3089. }
  3090. };
  3091. const [sharePanelSel, inputSel] = (() => {
  3092. switch (getDomain()) {
  3093. case "ytm": return ["tp-yt-paper-dialog ytmusic-unified-share-panel-renderer", "input#share-url"];
  3094. case "yt": return ["ytd-unified-share-panel-renderer", "input#share-url"];
  3095. }
  3096. })();
  3097. addSelectorListener("body", sharePanelSel, {
  3098. listener: (sharePanelEl) => {
  3099. const obs = new MutationObserver(() => {
  3100. const inputElem = sharePanelEl.querySelector(inputSel);
  3101. inputElem && removeSiParam(inputElem);
  3102. });
  3103. obs.observe(sharePanelEl, {
  3104. childList: true,
  3105. subtree: true,
  3106. attributeFilter: ["aria-hidden", "aria-checked", "checked"],
  3107. });
  3108. },
  3109. });
  3110. });
  3111. }
  3112. //#MARKER fix margins
  3113. /** Applies global CSS to fix various spacings */
  3114. function fixSpacing() {
  3115. return __awaiter(this, void 0, void 0, function* () {
  3116. try {
  3117. const css = yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("css-fix_spacing"))).text();
  3118. if (css)
  3119. UserUtils.addGlobalStyle(css).id = "bytm-style-fix-spacing";
  3120. }
  3121. catch (err) {
  3122. error("Couldn't fix spacing due to an error:", err);
  3123. }
  3124. });
  3125. }
  3126. //#MARKER scroll to active song
  3127. /** Adds a button to the queue to scroll to the active song */
  3128. function addScrollToActiveBtn() {
  3129. return __awaiter(this, void 0, void 0, function* () {
  3130. addSelectorListener("sidePanel", "#tabsContent tp-yt-paper-tab:nth-of-type(1)", {
  3131. listener: (tabElem) => __awaiter(this, void 0, void 0, function* () {
  3132. const containerElem = document.createElement("div");
  3133. containerElem.id = "bytm-scroll-to-active-btn-cont";
  3134. const linkElem = document.createElement("div");
  3135. linkElem.id = "bytm-scroll-to-active-btn";
  3136. linkElem.tabIndex = 0;
  3137. linkElem.classList.add("ytmusic-player-bar", "bytm-generic-btn");
  3138. linkElem.ariaLabel = linkElem.title = t("scroll_to_playing");
  3139. linkElem.role = "button";
  3140. const imgElem = document.createElement("img");
  3141. imgElem.classList.add("bytm-generic-btn-img");
  3142. imgElem.src = yield getResourceUrl("icon-skip_to");
  3143. const scrollToActiveInteraction = () => {
  3144. const activeItem = document.querySelector("#side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"loading\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"playing\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"paused\"]");
  3145. if (!activeItem)
  3146. return;
  3147. activeItem.scrollIntoView({
  3148. behavior: "smooth",
  3149. block: "center",
  3150. inline: "center",
  3151. });
  3152. };
  3153. onInteraction(linkElem, scrollToActiveInteraction, { capture: true });
  3154. linkElem.appendChild(imgElem);
  3155. containerElem.appendChild(linkElem);
  3156. tabElem.appendChild(containerElem);
  3157. }),
  3158. });
  3159. });
  3160. }
  3161. //#MARKER thumbnail overlay
  3162. /** To be changed when the toggle button is pressed - used to invert the state of "showOverlay" */
  3163. let invertOverlay = false;
  3164. function initThumbnailOverlay() {
  3165. return __awaiter(this, void 0, void 0, function* () {
  3166. const behavior = getFeatures().thumbnailOverlayBehavior;
  3167. const toggleBtnShown = getFeatures().thumbnailOverlayToggleBtnShown;
  3168. if (behavior === "never" && !toggleBtnShown)
  3169. return;
  3170. const playerSelector = "ytmusic-player#player";
  3171. const playerEl = document.querySelector(playerSelector);
  3172. if (!playerEl)
  3173. return error("Couldn't find video player element while adding thumbnail overlay");
  3174. /** Checks and updates the overlay and toggle button states based on the current song type (yt video or ytm song) */
  3175. const updateOverlayVisibility = () => __awaiter(this, void 0, void 0, function* () {
  3176. var _a, _b;
  3177. let showOverlay = behavior === "always";
  3178. const isVideo = (_b = (_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.hasAttribute("video-mode")) !== null && _b !== void 0 ? _b : false;
  3179. if (behavior === "videosOnly" && isVideo)
  3180. showOverlay = true;
  3181. else if (behavior === "songsOnly" && !isVideo)
  3182. showOverlay = true;
  3183. showOverlay = invertOverlay ? !showOverlay : showOverlay;
  3184. const overlayElem = document.querySelector("#bytm-thumbnail-overlay");
  3185. const thumbElem = document.querySelector("#bytm-thumbnail-overlay-img");
  3186. const indicatorElem = document.querySelector("#bytm-thumbnail-overlay-indicator");
  3187. if (overlayElem)
  3188. overlayElem.style.display = showOverlay ? "block" : "none";
  3189. if (thumbElem)
  3190. thumbElem.ariaHidden = String(!showOverlay);
  3191. if (indicatorElem) {
  3192. indicatorElem.style.display = showOverlay ? "block" : "none";
  3193. indicatorElem.ariaHidden = String(!showOverlay);
  3194. }
  3195. if (getFeatures().thumbnailOverlayToggleBtnShown) {
  3196. const toggleBtnElem = document.querySelector("#bytm-thumbnail-overlay-toggle");
  3197. const toggleBtnImgElem = document.querySelector("#bytm-thumbnail-overlay-toggle > img");
  3198. if (toggleBtnImgElem)
  3199. toggleBtnImgElem.src = yield getResourceUrl(`icon-image${showOverlay ? "_filled" : ""}`);
  3200. if (toggleBtnElem)
  3201. toggleBtnElem.ariaLabel = toggleBtnElem.title = t(`thumbnail_overlay_toggle_btn_tooltip${showOverlay ? "_hide" : "_show"}`);
  3202. }
  3203. });
  3204. const applyThumbUrl = (watchId) => __awaiter(this, void 0, void 0, function* () {
  3205. const thumbUrl = yield getBestThumbnailUrl(watchId);
  3206. if (thumbUrl) {
  3207. const toggleBtnElem = document.querySelector("#bytm-thumbnail-overlay-toggle");
  3208. const thumbImgElem = document.querySelector("#bytm-thumbnail-overlay-img");
  3209. if (toggleBtnElem)
  3210. toggleBtnElem.href = thumbUrl;
  3211. if (thumbImgElem)
  3212. thumbImgElem.src = thumbUrl;
  3213. }
  3214. });
  3215. const unsubWatchIdChanged = siteEvents.on("watchIdChanged", (watchId) => {
  3216. unsubWatchIdChanged();
  3217. addSelectorListener("body", "#bytm-thumbnail-overlay", {
  3218. listener: () => {
  3219. applyThumbUrl(watchId);
  3220. updateOverlayVisibility();
  3221. },
  3222. });
  3223. });
  3224. const createElements = () => __awaiter(this, void 0, void 0, function* () {
  3225. // overlay
  3226. const overlayElem = document.createElement("div");
  3227. overlayElem.id = "bytm-thumbnail-overlay";
  3228. overlayElem.classList.add("bytm-no-select");
  3229. overlayElem.style.display = "none";
  3230. let indicatorElem;
  3231. if (getFeatures().thumbnailOverlayShowIndicator) {
  3232. indicatorElem = document.createElement("img");
  3233. indicatorElem.id = "bytm-thumbnail-overlay-indicator";
  3234. indicatorElem.src = yield getResourceUrl("icon-image");
  3235. indicatorElem.role = "presentation";
  3236. indicatorElem.title = indicatorElem.ariaLabel = t("thumbnail_overlay_indicator_tooltip");
  3237. indicatorElem.ariaHidden = "true";
  3238. indicatorElem.style.display = "none";
  3239. }
  3240. const thumbImgElem = document.createElement("img");
  3241. thumbImgElem.id = "bytm-thumbnail-overlay-img";
  3242. thumbImgElem.role = "presentation";
  3243. thumbImgElem.ariaHidden = "true";
  3244. thumbImgElem.style.objectFit = getFeatures().thumbnailOverlayImageFit;
  3245. overlayElem.appendChild(thumbImgElem);
  3246. playerEl.appendChild(overlayElem);
  3247. indicatorElem && playerEl.appendChild(indicatorElem);
  3248. siteEvents.on("watchIdChanged", (watchId) => __awaiter(this, void 0, void 0, function* () {
  3249. invertOverlay = false;
  3250. applyThumbUrl(watchId);
  3251. updateOverlayVisibility();
  3252. }));
  3253. // toggle button
  3254. if (toggleBtnShown) {
  3255. const toggleBtnElem = document.createElement("a");
  3256. toggleBtnElem.id = "bytm-thumbnail-overlay-toggle";
  3257. toggleBtnElem.role = "button";
  3258. toggleBtnElem.tabIndex = 0;
  3259. toggleBtnElem.classList.add("ytmusic-player-bar", "bytm-generic-btn", "bytm-no-select");
  3260. onInteraction(toggleBtnElem, (e) => {
  3261. if (e instanceof MouseEvent && e.shiftKey)
  3262. return UserUtils.openInNewTab(toggleBtnElem.href);
  3263. invertOverlay = !invertOverlay;
  3264. updateOverlayVisibility();
  3265. });
  3266. const imgElem = document.createElement("img");
  3267. imgElem.classList.add("bytm-generic-btn-img");
  3268. toggleBtnElem.appendChild(imgElem);
  3269. addSelectorListener("playerBarMiddleButtons", "ytmusic-like-button-renderer#like-button-renderer", {
  3270. listener: (likeContainer) => UserUtils.insertAfter(likeContainer, toggleBtnElem),
  3271. });
  3272. }
  3273. log("Added thumbnail overlay");
  3274. });
  3275. addSelectorListener("mainPanel", playerSelector, {
  3276. listener(playerEl) {
  3277. if (playerEl.getAttribute("player-ui-state") === "INACTIVE") {
  3278. const obs = new MutationObserver(() => {
  3279. if (playerEl.getAttribute("player-ui-state") === "INACTIVE")
  3280. return;
  3281. createElements();
  3282. obs.disconnect();
  3283. });
  3284. obs.observe(playerEl, {
  3285. attributes: true,
  3286. attributeFilter: ["player-ui-state"],
  3287. });
  3288. }
  3289. else
  3290. createElements();
  3291. },
  3292. });
  3293. });
  3294. }
  3295. //#MARKER hide cursor on idle
  3296. function initHideCursorOnIdle() {
  3297. return __awaiter(this, void 0, void 0, function* () {
  3298. addSelectorListener("mainPanel", "ytmusic-player#player", {
  3299. listener(vidContainer) {
  3300. const overlaySelector = "ytmusic-player #song-media-window";
  3301. const overlayElem = document.querySelector(overlaySelector);
  3302. if (!overlayElem)
  3303. return warn("Couldn't find overlay element while initializing cursor hiding");
  3304. let cursorHideTimer;
  3305. let cursorHidden = false;
  3306. const hide = () => {
  3307. if (cursorHidden)
  3308. return;
  3309. cursorHidden = true;
  3310. overlayElem.style.opacity = "0 !important";
  3311. setTimeout(() => {
  3312. overlayElem.style.display = "none";
  3313. vidContainer.style.cursor = "none";
  3314. }, 200);
  3315. };
  3316. const show = () => {
  3317. if (!cursorHidden)
  3318. return;
  3319. cursorHidden = false;
  3320. vidContainer.style.cursor = "initial";
  3321. overlayElem.style.display = "initial";
  3322. overlayElem.style.opacity = "1 !important";
  3323. };
  3324. const cursorHideTimerCb = () => cursorHideTimer = setTimeout(hide, getFeatures().hideCursorOnIdleDelay * 1000);
  3325. const onMove = () => {
  3326. clearTimeout(cursorHideTimer);
  3327. show();
  3328. cursorHideTimerCb();
  3329. };
  3330. vidContainer.addEventListener("mouseenter", onMove);
  3331. vidContainer.addEventListener("mousemove", UserUtils.debounce(onMove, 25, "rising"));
  3332. vidContainer.addEventListener("mouseleave", () => {
  3333. clearTimeout(cursorHideTimer);
  3334. hide();
  3335. });
  3336. vidContainer.addEventListener("click", () => {
  3337. show();
  3338. cursorHideTimerCb();
  3339. setTimeout(hide, 3000);
  3340. });
  3341. log("Initialized cursor hiding on idle");
  3342. },
  3343. });
  3344. });
  3345. }//#MARKER beforeunload popup
  3346. let beforeUnloadEnabled = true;
  3347. /** Disables the popup before leaving the site */
  3348. function disableBeforeUnload() {
  3349. beforeUnloadEnabled = false;
  3350. info("Disabled popup before leaving the site");
  3351. }
  3352. /**
  3353. * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
  3354. * event listeners before they can be called by the site.
  3355. */
  3356. function initBeforeUnloadHook() {
  3357. return __awaiter(this, void 0, void 0, function* () {
  3358. Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
  3359. (function (original) {
  3360. // @ts-ignore
  3361. window.__proto__.addEventListener = function (...args) {
  3362. const origListener = typeof args[1] === "function" ? args[1] : args[1].handleEvent;
  3363. args[1] = function (...a) {
  3364. if (!beforeUnloadEnabled && args[0] === "beforeunload") {
  3365. info("Prevented 'beforeunload' event listener");
  3366. return false;
  3367. }
  3368. else
  3369. return origListener.apply(this, a);
  3370. };
  3371. original.apply(this, args);
  3372. };
  3373. // @ts-ignore
  3374. })(window.__proto__.addEventListener);
  3375. });
  3376. }
  3377. //#MARKER auto close toasts
  3378. /** Closes toasts after a set amount of time */
  3379. function initAutoCloseToasts() {
  3380. return __awaiter(this, void 0, void 0, function* () {
  3381. try {
  3382. const animTimeout = 300;
  3383. const closeTimeout = Math.max(getFeatures().closeToastsTimeout * 1000 + animTimeout, animTimeout);
  3384. addSelectorListener("popupContainer", "tp-yt-paper-toast#toast", {
  3385. all: true,
  3386. continuous: true,
  3387. listener: (toastElems) => __awaiter(this, void 0, void 0, function* () {
  3388. var _a;
  3389. for (const toastElem of toastElems) {
  3390. if (!toastElem.hasAttribute("allow-click-through"))
  3391. continue;
  3392. if (toastElem.classList.contains("bytm-closing"))
  3393. continue;
  3394. toastElem.classList.add("bytm-closing");
  3395. yield UserUtils.pauseFor(closeTimeout);
  3396. toastElem.classList.remove("paper-toast-open");
  3397. log(`Automatically closed toast '${(_a = toastElem.querySelector("#text-container yt-formatted-string")) === null || _a === void 0 ? void 0 : _a.textContent}' after ${getFeatures().closeToastsTimeout * 1000}ms`);
  3398. // wait for the transition to finish
  3399. yield UserUtils.pauseFor(animTimeout);
  3400. toastElem.style.display = "none";
  3401. }
  3402. }),
  3403. });
  3404. log("Initialized automatic toast closing");
  3405. }
  3406. catch (err) {
  3407. error("Error in automatic toast closing:", err);
  3408. }
  3409. });
  3410. }
  3411. let remSongsCache = [];
  3412. /**
  3413. * Remembers the time of the last played song and resumes playback from that time
  3414. * CALLED BEFORE DOM IS READY!
  3415. */
  3416. function initRememberSongTime() {
  3417. return __awaiter(this, void 0, void 0, function* () {
  3418. if (getFeatures().rememberSongTimeSites !== "all" && getFeatures().rememberSongTimeSites !== getDomain())
  3419. return;
  3420. const storedDataRaw = yield GM.getValue("bytm-rem-songs");
  3421. if (!storedDataRaw)
  3422. yield GM.setValue("bytm-rem-songs", "[]");
  3423. remSongsCache = JSON.parse(String(storedDataRaw !== null && storedDataRaw !== void 0 ? storedDataRaw : "[]"));
  3424. log(`Initialized song time remembering with ${remSongsCache.length} initial entries`);
  3425. if (location.pathname.startsWith("/watch"))
  3426. yield restoreSongTime();
  3427. if (!domLoaded)
  3428. document.addEventListener("DOMContentLoaded", remSongUpdateEntry);
  3429. else
  3430. remSongUpdateEntry();
  3431. });
  3432. }
  3433. /** Tries to restore the time of the currently playing song */
  3434. function restoreSongTime() {
  3435. var _a;
  3436. return __awaiter(this, void 0, void 0, function* () {
  3437. if (location.pathname.startsWith("/watch")) {
  3438. const { searchParams } = new URL(location.href);
  3439. const watchID = searchParams.get("v");
  3440. if (!watchID)
  3441. return;
  3442. const entry = remSongsCache.find(entry => entry.watchID === watchID);
  3443. if (entry) {
  3444. if (Date.now() - entry.updateTimestamp > getFeatures().rememberSongTimeDuration * 1000) {
  3445. yield delRemSongData(entry.watchID);
  3446. return;
  3447. }
  3448. else {
  3449. if (isNaN(entry.songTime))
  3450. return;
  3451. const vidElem = yield waitVideoElementReady();
  3452. const vidRestoreTime = entry.songTime - ((_a = getFeatures().rememberSongTimeReduction) !== null && _a !== void 0 ? _a : 0);
  3453. vidElem.currentTime = UserUtils.clamp(Math.max(vidRestoreTime, 0), 0, vidElem.duration);
  3454. yield delRemSongData(entry.watchID);
  3455. info(`Restored song time to ${Math.floor(vidRestoreTime / 60)}m, ${(vidRestoreTime % 60).toFixed(1)}s`, LogLevel.Info);
  3456. }
  3457. }
  3458. }
  3459. });
  3460. }
  3461. /** Only call once as this calls itself after a timeout! - Updates the currently playing song's entry in GM storage */
  3462. function remSongUpdateEntry() {
  3463. var _a, _b, _c;
  3464. return __awaiter(this, void 0, void 0, function* () {
  3465. if (location.pathname.startsWith("/watch")) {
  3466. const watchID = getWatchId();
  3467. if (!watchID)
  3468. return;
  3469. const songTime = (_a = yield getVideoTime()) !== null && _a !== void 0 ? _a : 0;
  3470. const paused = (_c = (_b = document.querySelector(videoSelector)) === null || _b === void 0 ? void 0 : _b.paused) !== null && _c !== void 0 ? _c : false;
  3471. // don't immediately update to reduce race conditions and only update if the video is playing
  3472. // also it just sounds better if the song starts at the beginning if only a couple seconds have passed
  3473. if (songTime > getFeatures().rememberSongTimeMinPlayTime && !paused) {
  3474. const entry = {
  3475. watchID,
  3476. songTime,
  3477. updateTimestamp: Date.now(),
  3478. };
  3479. yield setRemSongData(entry);
  3480. }
  3481. // if the song is rewound to the beginning, delete the entry
  3482. else {
  3483. const entry = remSongsCache.find(entry => entry.watchID === watchID);
  3484. if (entry && songTime <= getFeatures().rememberSongTimeMinPlayTime)
  3485. yield delRemSongData(entry.watchID);
  3486. }
  3487. }
  3488. const expiredEntries = remSongsCache.filter(entry => Date.now() - entry.updateTimestamp > getFeatures().rememberSongTimeDuration * 1000);
  3489. for (const entry of expiredEntries)
  3490. yield delRemSongData(entry.watchID);
  3491. // for no overlapping calls and better error handling
  3492. setTimeout(remSongUpdateEntry, 1000);
  3493. });
  3494. }
  3495. /** Adds an entry or updates it if it already exists */
  3496. function setRemSongData(data) {
  3497. return __awaiter(this, void 0, void 0, function* () {
  3498. const foundIdx = remSongsCache.findIndex(entry => entry.watchID === data.watchID);
  3499. if (foundIdx >= 0)
  3500. remSongsCache[foundIdx] = data;
  3501. else
  3502. remSongsCache.push(data);
  3503. yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
  3504. });
  3505. }
  3506. /** Deletes an entry */
  3507. function delRemSongData(watchID) {
  3508. return __awaiter(this, void 0, void 0, function* () {
  3509. remSongsCache = [...remSongsCache.filter(entry => entry.watchID !== watchID)];
  3510. yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
  3511. });
  3512. }
  3513. //#MARKER disable darkreader
  3514. /** Disables Dark Reader if it is enabled */
  3515. function disableDarkReader() {
  3516. if (document.querySelector(".darkreader")) {
  3517. const metaElem = document.createElement("meta");
  3518. metaElem.name = "darkreader-lock";
  3519. metaElem.classList.add("bytm-disable-darkreader");
  3520. document.head.appendChild(metaElem);
  3521. info("Sent hint to Dark Reader to disable itself");
  3522. }
  3523. }const inputIgnoreTagNames = ["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A"];
  3524. //#MARKER arrow key skip
  3525. function initArrowKeySkip() {
  3526. return __awaiter(this, void 0, void 0, function* () {
  3527. document.addEventListener("keydown", (evt) => {
  3528. var _a, _b, _c, _d;
  3529. if (!["ArrowLeft", "ArrowRight"].includes(evt.code))
  3530. return;
  3531. // discard the event when a (text) input is currently active, like when editing a playlist
  3532. if (inputIgnoreTagNames.includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) !== null && _b !== void 0 ? _b : ""))
  3533. return info(`Captured valid key to skip forward or backward but the current active element is <${(_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName.toLowerCase()}>, so the keypress is ignored`);
  3534. evt.preventDefault();
  3535. evt.stopImmediatePropagation();
  3536. let skipBy = (_d = getFeatures().arrowKeySkipBy) !== null && _d !== void 0 ? _d : featInfo.arrowKeySkipBy.default;
  3537. if (evt.code === "ArrowLeft")
  3538. skipBy *= -1;
  3539. log(`Captured arrow key '${evt.code}' - skipping by ${skipBy} seconds`);
  3540. const vidElem = document.querySelector(videoSelector);
  3541. if (vidElem)
  3542. vidElem.currentTime = UserUtils.clamp(vidElem.currentTime + skipBy, 0, vidElem.duration);
  3543. });
  3544. log("Added arrow key press listener");
  3545. });
  3546. }
  3547. //#MARKER site switch
  3548. /** switch sites only if current video time is greater than this value */
  3549. const videoTimeThreshold = 3;
  3550. let siteSwitchEnabled = true;
  3551. /** Initializes the site switch feature */
  3552. function initSiteSwitch(domain) {
  3553. return __awaiter(this, void 0, void 0, function* () {
  3554. document.addEventListener("keydown", (e) => {
  3555. const hotkey = getFeatures().switchSitesHotkey;
  3556. if (siteSwitchEnabled && e.code === hotkey.code && e.shiftKey === hotkey.shift && e.ctrlKey === hotkey.ctrl && e.altKey === hotkey.alt)
  3557. switchSite(domain === "yt" ? "ytm" : "yt");
  3558. });
  3559. siteEvents.on("hotkeyInputActive", (state) => {
  3560. siteSwitchEnabled = !state;
  3561. });
  3562. log("Initialized site switch listener");
  3563. });
  3564. }
  3565. /** Switches to the other site (between YT and YTM) */
  3566. function switchSite(newDomain) {
  3567. return __awaiter(this, void 0, void 0, function* () {
  3568. try {
  3569. if (!(["/watch", "/playlist"].some(v => location.pathname.startsWith(v))))
  3570. return warn("Not on a supported page, so the site switch is ignored");
  3571. let subdomain;
  3572. if (newDomain === "ytm")
  3573. subdomain = "music";
  3574. else if (newDomain === "yt")
  3575. subdomain = "www";
  3576. if (!subdomain)
  3577. throw new Error(`Unrecognized domain '${newDomain}'`);
  3578. disableBeforeUnload();
  3579. const { pathname, search, hash } = new URL(location.href);
  3580. const vt = yield getVideoTime();
  3581. log(`Found video time of ${vt} seconds`);
  3582. const cleanSearch = search.split("&")
  3583. .filter((param) => !param.match(/^\??t=/))
  3584. .join("&");
  3585. const newSearch = typeof vt === "number" && vt > videoTimeThreshold ?
  3586. cleanSearch.includes("?")
  3587. ? `${cleanSearch.startsWith("?")
  3588. ? cleanSearch
  3589. : "?" + cleanSearch}&t=${vt}`
  3590. : `?t=${vt}`
  3591. : cleanSearch;
  3592. const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
  3593. info(`Switching to domain '${newDomain}' at ${newUrl}`);
  3594. location.assign(newUrl);
  3595. }
  3596. catch (err) {
  3597. error("Error while switching site:", err);
  3598. }
  3599. });
  3600. }
  3601. //#MARKER number keys skip to time
  3602. const numKeysIgnoreTagNames = [...inputIgnoreTagNames, "TP-YT-PAPER-TAB"];
  3603. const numKeysIgnoreIds = ["progress-bar", "song-media-window"];
  3604. /** Adds the ability to skip to a certain time in the video by pressing a number key (0-9) */
  3605. function initNumKeysSkip() {
  3606. return __awaiter(this, void 0, void 0, function* () {
  3607. document.addEventListener("keydown", (e) => {
  3608. var _a, _b, _c, _d;
  3609. if (!e.key.trim().match(/^[0-9]$/))
  3610. return;
  3611. if (isCfgMenuOpen)
  3612. return;
  3613. // discard the event when an unexpected element is currently active or in focus, like when editing a playlist or when the search bar is focused
  3614. if (document.activeElement !== document.body // short-circuit if nothing is active
  3615. && !numKeysIgnoreIds.includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : "") // video element or player bar active
  3616. && !numKeysIgnoreTagNames.includes((_d = (_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName) !== null && _d !== void 0 ? _d : "") // other element active
  3617. )
  3618. return info("Captured valid key to skip video to, but ignored it since an unexpected element is active:", document.activeElement);
  3619. const vidElem = document.querySelector(videoSelector);
  3620. if (!vidElem)
  3621. return warn("Could not find video element, so the keypress is ignored");
  3622. const newVidTime = vidElem.duration / (10 / Number(e.key));
  3623. if (!isNaN(newVidTime)) {
  3624. log(`Captured number key [${e.key}], skipping to ${Math.floor(newVidTime / 60)}m ${(newVidTime % 60).toFixed(1)}s`);
  3625. vidElem.currentTime = newVidTime;
  3626. }
  3627. });
  3628. log("Added number key press listener");
  3629. });
  3630. }/** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
  3631. const geniUrlRatelimitTimeframe = 30;
  3632. //#MARKER media control bar
  3633. let currentSongTitle = "";
  3634. /** Adds a lyrics button to the media controls bar */
  3635. function addMediaCtrlLyricsBtn() {
  3636. return __awaiter(this, void 0, void 0, function* () {
  3637. addSelectorListener("playerBarMiddleButtons", "ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
  3638. });
  3639. }
  3640. /** Actually adds the lyrics button after the like button renderer has been verified to exist */
  3641. function addActualMediaCtrlLyricsBtn(likeContainer) {
  3642. return __awaiter(this, void 0, void 0, function* () {
  3643. const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
  3644. if (!songTitleElem)
  3645. return warn("Couldn't find song title element");
  3646. // run parallel without awaiting so the MutationObserver below can observe the title element in time
  3647. (() => __awaiter(this, void 0, void 0, function* () {
  3648. const gUrl = yield getCurrentLyricsUrl();
  3649. const lyricsBtnElem = yield createLyricsBtn(gUrl !== null && gUrl !== void 0 ? gUrl : undefined);
  3650. lyricsBtnElem.id = "betterytm-lyrics-button";
  3651. log("Inserted lyrics button into media controls bar");
  3652. const thumbToggleElem = document.querySelector("#bytm-thumbnail-overlay-toggle");
  3653. if (thumbToggleElem)
  3654. UserUtils.insertAfter(thumbToggleElem, lyricsBtnElem);
  3655. else
  3656. UserUtils.insertAfter(likeContainer, lyricsBtnElem);
  3657. }))();
  3658. currentSongTitle = songTitleElem.title;
  3659. const spinnerIconUrl = yield getResourceUrl("icon-spinner");
  3660. const lyricsIconUrl = yield getResourceUrl("icon-lyrics");
  3661. const errorIconUrl = yield getResourceUrl("icon-error");
  3662. const onMutation = (mutations) => { var _a, mutations_1, mutations_1_1; return __awaiter(this, void 0, void 0, function* () {
  3663. var _b, e_1, _c, _d;
  3664. try {
  3665. for (_a = true, mutations_1 = __asyncValues(mutations); mutations_1_1 = yield mutations_1.next(), _b = mutations_1_1.done, !_b; _a = true) {
  3666. _d = mutations_1_1.value;
  3667. _a = false;
  3668. const mut = _d;
  3669. const newTitle = mut.target.title;
  3670. if (newTitle !== currentSongTitle && newTitle.length > 0) {
  3671. const lyricsBtn = document.querySelector("#betterytm-lyrics-button");
  3672. if (!lyricsBtn)
  3673. continue;
  3674. lyricsBtn.style.cursor = "wait";
  3675. lyricsBtn.style.pointerEvents = "none";
  3676. const imgElem = lyricsBtn.querySelector("img");
  3677. imgElem.src = spinnerIconUrl;
  3678. imgElem.classList.add("bytm-spinner");
  3679. currentSongTitle = newTitle;
  3680. const url = yield getCurrentLyricsUrl(); // can take a second or two
  3681. imgElem.src = lyricsIconUrl;
  3682. imgElem.classList.remove("bytm-spinner");
  3683. if (!url) {
  3684. let artist, song;
  3685. if ("mediaSession" in navigator && navigator.mediaSession.metadata) {
  3686. artist = navigator.mediaSession.metadata.artist;
  3687. song = navigator.mediaSession.metadata.title;
  3688. }
  3689. const query = artist && song ? "?q=" + encodeURIComponent(sanitizeArtists(artist) + " - " + sanitizeSong(song)) : "";
  3690. imgElem.src = errorIconUrl;
  3691. lyricsBtn.ariaLabel = lyricsBtn.title = t("lyrics_not_found_click_open_search");
  3692. lyricsBtn.style.cursor = "pointer";
  3693. lyricsBtn.style.pointerEvents = "all";
  3694. lyricsBtn.style.display = "inline-flex";
  3695. lyricsBtn.style.visibility = "visible";
  3696. lyricsBtn.href = `https://genius.com/search${query}`;
  3697. continue;
  3698. }
  3699. lyricsBtn.href = url;
  3700. lyricsBtn.ariaLabel = lyricsBtn.title = t("open_current_lyrics");
  3701. lyricsBtn.style.cursor = "pointer";
  3702. lyricsBtn.style.visibility = "visible";
  3703. lyricsBtn.style.display = "inline-flex";
  3704. lyricsBtn.style.pointerEvents = "initial";
  3705. }
  3706. }
  3707. }
  3708. catch (e_1_1) { e_1 = { error: e_1_1 }; }
  3709. finally {
  3710. try {
  3711. if (!_a && !_b && (_c = mutations_1.return)) yield _c.call(mutations_1);
  3712. }
  3713. finally { if (e_1) throw e_1.error; }
  3714. }
  3715. }); };
  3716. // since YT and YTM don't reload the page on video change, MutationObserver needs to be used to watch for changes in the video title
  3717. const obs = new MutationObserver(onMutation);
  3718. obs.observe(songTitleElem, { attributes: true, attributeFilter: ["title"] });
  3719. });
  3720. }
  3721. //#MARKER utils
  3722. /** Removes everything in parentheses from the passed song name */
  3723. function sanitizeSong(songName) {
  3724. if (typeof songName !== "string")
  3725. return songName;
  3726. const parensRegex = /\(.+\)/gmi;
  3727. const squareParensRegex = /\[.+\]/gmi;
  3728. // trim right after the song name:
  3729. const sanitized = songName
  3730. .replace(parensRegex, "")
  3731. .replace(squareParensRegex, "");
  3732. return sanitized.trim();
  3733. }
  3734. /** Removes the secondary artist (if it exists) from the passed artists string */
  3735. function sanitizeArtists(artists) {
  3736. artists = artists.split(/\s*\u2022\s*/gmiu)[0]; // split at &bull; [•] character
  3737. if (artists.match(/&/))
  3738. artists = artists.split(/\s*&\s*/gm)[0];
  3739. if (artists.match(/,/))
  3740. artists = artists.split(/,\s*/gm)[0];
  3741. if (artists.match(/(f(ea)?t\.?|Remix|Edit|Flip|Cover|Night\s?Core|Bass\s?Boost|pro?d\.?)/i)) {
  3742. const parensRegex = /\(.+\)/gmi;
  3743. const squareParensRegex = /\[.+\]/gmi;
  3744. artists = artists
  3745. .replace(parensRegex, "")
  3746. .replace(squareParensRegex, "");
  3747. }
  3748. return artists.trim();
  3749. }
  3750. /** Returns the lyrics URL from genius for the currently selected song */
  3751. function getCurrentLyricsUrl() {
  3752. var _a;
  3753. return __awaiter(this, void 0, void 0, function* () {
  3754. try {
  3755. // In videos the video title contains both artist and song title, in "regular" YTM songs, the video title only contains the song title
  3756. const isVideo = typeof ((_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.hasAttribute("video-mode"));
  3757. const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
  3758. const songMetaElem = document.querySelector("span.subtitle > yt-formatted-string :first-child");
  3759. if (!songTitleElem || !songMetaElem)
  3760. return undefined;
  3761. const songNameRaw = songTitleElem.title;
  3762. let songName = songNameRaw;
  3763. let artistName = songMetaElem.textContent;
  3764. if (isVideo) {
  3765. // for some fucking reason some music videos have YTM-like song title and artist separation, some don't
  3766. if (songName.includes("-")) {
  3767. const split = splitVideoTitle(songName);
  3768. songName = split.song;
  3769. artistName = split.artist;
  3770. }
  3771. }
  3772. if (!artistName)
  3773. return undefined;
  3774. const url = yield fetchLyricsUrlTop(sanitizeArtists(artistName), sanitizeSong(songName));
  3775. if (url) {
  3776. emitInterface("bytm:lyricsLoaded", {
  3777. type: "current",
  3778. artists: artistName,
  3779. title: songName,
  3780. url,
  3781. });
  3782. }
  3783. return url;
  3784. }
  3785. catch (err) {
  3786. error("Couldn't resolve lyrics URL:", err);
  3787. return undefined;
  3788. }
  3789. });
  3790. }
  3791. /** Fetches the top lyrics URL result from geniURL - **the passed parameters need to be sanitized first!** */
  3792. function fetchLyricsUrlTop(artist, song) {
  3793. var _a, _b;
  3794. return __awaiter(this, void 0, void 0, function* () {
  3795. try {
  3796. return (_b = (_a = (yield fetchLyricsUrls(artist, song))) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.url;
  3797. }
  3798. catch (err) {
  3799. error("Couldn't get lyrics URL due to error:", err);
  3800. return undefined;
  3801. }
  3802. });
  3803. }
  3804. /**
  3805. * Fetches the 5 best matching lyrics URLs from geniURL using a combo exact-ish and fuzzy search
  3806. * **the passed parameters need to be sanitized first!**
  3807. */
  3808. function fetchLyricsUrls(artist, song) {
  3809. var _a, _b, _c;
  3810. return __awaiter(this, void 0, void 0, function* () {
  3811. try {
  3812. const cacheEntry = getLyricsCacheEntry(artist, song);
  3813. if (cacheEntry) {
  3814. info(`Found lyrics URL in cache: ${cacheEntry.url}`);
  3815. return [cacheEntry];
  3816. }
  3817. const startTs = Date.now();
  3818. const fetchUrl = constructUrlString(`${getFeatures().geniUrlBase}/search`, {
  3819. disableFuzzy: null,
  3820. utm_source: scriptInfo.name,
  3821. utm_content: `v${scriptInfo.version}${mode === "development" ? "-dev" : ""}`,
  3822. artist,
  3823. song,
  3824. });
  3825. log(`Requesting URLs from geniURL at '${fetchUrl}'`);
  3826. const { geniUrlToken } = getFeatures();
  3827. const fetchRes = yield UserUtils.fetchAdvanced(fetchUrl, Object.assign({}, (geniUrlToken ? {
  3828. headers: {
  3829. Authorization: `Bearer ${geniUrlToken}`,
  3830. },
  3831. } : {})));
  3832. if (fetchRes.status === 429) {
  3833. const waitSeconds = Number((_a = fetchRes.headers.get("retry-after")) !== null && _a !== void 0 ? _a : geniUrlRatelimitTimeframe);
  3834. alert(tp("lyrics_rate_limited", waitSeconds, waitSeconds));
  3835. return undefined;
  3836. }
  3837. else if (fetchRes.status < 200 || fetchRes.status >= 300) {
  3838. error(`Couldn't fetch lyrics URLs from geniURL - status: ${fetchRes.status} - response: ${(_c = (_b = (yield fetchRes.json()).message) !== null && _b !== void 0 ? _b : yield fetchRes.text()) !== null && _c !== void 0 ? _c : "(none)"}`);
  3839. return undefined;
  3840. }
  3841. const result = yield fetchRes.json();
  3842. if (typeof result === "object" && result.error || !result || !result.all) {
  3843. error("Couldn't fetch lyrics URL:", result.message);
  3844. return undefined;
  3845. }
  3846. const allResults = result.all;
  3847. if (allResults.length === 0) {
  3848. warn("No lyrics URL found for the provided song");
  3849. return undefined;
  3850. }
  3851. const allResultsSan = allResults
  3852. .filter(({ meta, url }) => (meta.title || meta.fullTitle) && meta.artists && url)
  3853. .map(({ meta, url }) => {
  3854. var _a;
  3855. return ({
  3856. meta: Object.assign(Object.assign({}, meta), { title: sanitizeSong(String((_a = meta.title) !== null && _a !== void 0 ? _a : meta.fullTitle)), artists: sanitizeArtists(String(meta.artists)) }),
  3857. url,
  3858. });
  3859. });
  3860. if (!getFeatures().advancedLyricsFilter) {
  3861. const topRes = allResultsSan[0];
  3862. topRes && addLyricsCacheEntryBest(topRes.meta.artists, topRes.meta.title, topRes.url);
  3863. return allResultsSan.map(r => ({
  3864. artist: r.meta.primaryArtist.name,
  3865. song: r.meta.title,
  3866. url: r.url,
  3867. }));
  3868. }
  3869. const exactish = (input) => input.toLowerCase()
  3870. .replace(/[\s\-_&,.()[\]]+/gm, "");
  3871. // exact-ish matches, best matching one first
  3872. const exactishResults = [...allResultsSan].sort((a, b) => {
  3873. const aTitleScore = exactish(a.meta.title).localeCompare(exactish(song));
  3874. const bTitleScore = exactish(b.meta.title).localeCompare(exactish(song));
  3875. const aArtistScore = exactish(a.meta.primaryArtist.name).localeCompare(exactish(artist));
  3876. const bArtistScore = exactish(b.meta.primaryArtist.name).localeCompare(exactish(artist));
  3877. return aTitleScore + aArtistScore - bTitleScore - bArtistScore;
  3878. });
  3879. // use fuse.js for fuzzy match
  3880. // search song title and artist separately, then combine the scores
  3881. const titleFuse = new Fuse([...allResultsSan], {
  3882. keys: ["title"],
  3883. includeScore: true,
  3884. threshold: 0.4,
  3885. });
  3886. const artistFuse = new Fuse([...allResultsSan], {
  3887. keys: ["primaryArtist.name"],
  3888. includeScore: true,
  3889. threshold: 0.4,
  3890. });
  3891. let fuzzyResults = allResultsSan.map(r => {
  3892. var _a, _b, _c, _d;
  3893. const titleRes = titleFuse.search(r.meta.title);
  3894. const artistRes = artistFuse.search(r.meta.primaryArtist.name);
  3895. const titleScore = (_b = (_a = titleRes[0]) === null || _a === void 0 ? void 0 : _a.score) !== null && _b !== void 0 ? _b : 0;
  3896. const artistScore = (_d = (_c = artistRes[0]) === null || _c === void 0 ? void 0 : _c.score) !== null && _d !== void 0 ? _d : 0;
  3897. return Object.assign(Object.assign({}, r), { score: titleScore + artistScore });
  3898. });
  3899. // I love TS
  3900. fuzzyResults = fuzzyResults
  3901. .map((_a) => {
  3902. var { score } = _a, rest = __rest(_a, ["score"]);
  3903. return rest;
  3904. });
  3905. const hasExactMatch = exactishResults.slice(0, 3).find(r => exactish(r.meta.title) === exactish(fuzzyResults[0].meta.title) && exactish(r.meta.primaryArtist.name) === exactish(fuzzyResults[0].meta.primaryArtist.name));
  3906. const finalResults = [
  3907. ...(hasExactMatch
  3908. ? [fuzzyResults[0], ...allResultsSan.filter(r => r.url !== fuzzyResults[0].url)]
  3909. : [...allResultsSan]),
  3910. ].slice(0, 5);
  3911. // add top 3 results to the cache with a penalty to their time to live
  3912. // so every entry is deleted faster if it's not considered as relevant
  3913. finalResults.slice(0, 3).forEach(({ meta: { artists, title }, url }, i) => {
  3914. const penaltyFraction = hasExactMatch
  3915. // if there's an exact match, give it 0 penalty and penalize all other results with the full value
  3916. ? i === 0 ? 0 : 1
  3917. // if there's no exact match, penalize all results with a fraction of the full penalty since they're more likely to be unrelated
  3918. : 0.6;
  3919. addLyricsCacheEntryPenalized(sanitizeArtists(artists), sanitizeSong(title), url, penaltyFraction);
  3920. });
  3921. finalResults.length > 0 && log("Found", finalResults.length, "lyrics", UserUtils.autoPlural("URL", finalResults), "in", Date.now() - startTs, "ms:", finalResults);
  3922. // returns search results sorted by relevance
  3923. return finalResults.map(r => ({
  3924. artist: r.meta.primaryArtist.name,
  3925. song: r.meta.title,
  3926. url: r.url,
  3927. }));
  3928. }
  3929. catch (err) {
  3930. error("Couldn't get lyrics URL due to error:", err);
  3931. return undefined;
  3932. }
  3933. });
  3934. }
  3935. /** Creates the base lyrics button element */
  3936. function createLyricsBtn(geniusUrl, hideIfLoading = true) {
  3937. return __awaiter(this, void 0, void 0, function* () {
  3938. const linkElem = document.createElement("a");
  3939. linkElem.classList.add("ytmusic-player-bar", "bytm-generic-btn");
  3940. linkElem.ariaLabel = linkElem.title = geniusUrl ? t("open_lyrics") : t("lyrics_loading");
  3941. if (geniusUrl)
  3942. linkElem.href = geniusUrl;
  3943. linkElem.role = "button";
  3944. linkElem.target = "_blank";
  3945. linkElem.rel = "noopener noreferrer";
  3946. linkElem.style.visibility = hideIfLoading && geniusUrl ? "initial" : "hidden";
  3947. linkElem.style.display = hideIfLoading && geniusUrl ? "inline-flex" : "none";
  3948. const imgElem = document.createElement("img");
  3949. imgElem.classList.add("bytm-generic-btn-img");
  3950. imgElem.src = yield getResourceUrl("icon-lyrics");
  3951. linkElem.appendChild(imgElem);
  3952. return linkElem;
  3953. });
  3954. }
  3955. /** Splits a video title that contains a hyphen into an artist and song */
  3956. function splitVideoTitle(title) {
  3957. const [artist, ...rest] = title.split("-").map((v, i) => i < 2 ? v.trim() : v);
  3958. return { artist, song: rest.join("-") };
  3959. }/** Initializes the queue buttons */
  3960. function initQueueButtons() {
  3961. return __awaiter(this, void 0, void 0, function* () {
  3962. const addCurrentQueueBtns = (evt) => {
  3963. let amt = 0;
  3964. for (const queueItm of evt.childNodes) {
  3965. if (!queueItm.classList.contains("bytm-has-queue-btns")) {
  3966. addQueueButtons(queueItm, undefined, "currentQueue");
  3967. amt++;
  3968. }
  3969. }
  3970. if (amt > 0)
  3971. log(`Added buttons to ${amt} new queue ${UserUtils.autoPlural("item", amt)}`);
  3972. };
  3973. // current queue
  3974. siteEvents.on("queueChanged", addCurrentQueueBtns);
  3975. siteEvents.on("autoplayQueueChanged", addCurrentQueueBtns);
  3976. const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
  3977. if (queueItems.length > 0) {
  3978. queueItems.forEach(itm => addQueueButtons(itm, undefined, "currentQueue"));
  3979. log(`Added buttons to ${queueItems.length} existing "current song queue" ${UserUtils.autoPlural("item", queueItems)}`);
  3980. }
  3981. // generic lists
  3982. const addGenericListQueueBtns = (listElem) => {
  3983. if (listElem.classList.contains("bytm-list-has-queue-btns"))
  3984. return;
  3985. const queueItems = listElem.querySelectorAll("ytmusic-responsive-list-item-renderer");
  3986. if (queueItems.length === 0)
  3987. return;
  3988. listElem.classList.add("bytm-list-has-queue-btns");
  3989. queueItems.forEach(itm => addQueueButtons(itm, ".flex-columns", "genericQueue", ["bytm-generic-list-queue-btn-container"]));
  3990. log(`Added buttons to ${queueItems.length} new "generic song list" ${UserUtils.autoPlural("item", queueItems)}`);
  3991. };
  3992. const listSelectors = [
  3993. "ytmusic-playlist-shelf-renderer #contents",
  3994. "ytmusic-section-list-renderer[main-page-type=\"MUSIC_PAGE_TYPE_ALBUM\"] ytmusic-shelf-renderer #contents",
  3995. "ytmusic-section-list-renderer[main-page-type=\"MUSIC_PAGE_TYPE_ARTIST\"] ytmusic-shelf-renderer #contents",
  3996. ];
  3997. if (getFeatures().listButtonsPlacement === "everywhere") {
  3998. for (const selector of listSelectors) {
  3999. addSelectorListener("body", selector, {
  4000. all: true,
  4001. continuous: true,
  4002. listener: (songLists) => {
  4003. for (const list of songLists)
  4004. addGenericListQueueBtns(list);
  4005. },
  4006. });
  4007. }
  4008. }
  4009. });
  4010. }
  4011. /**
  4012. * Adds the buttons to each item in the current song queue.
  4013. * Also observes for changes to add new buttons to new items in the queue.
  4014. * @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
  4015. * @param listType The type of list the queue item is in
  4016. * @param classes Extra CSS classes to apply to the container
  4017. */
  4018. function addQueueButtons(queueItem, containerParentSelector = ".song-info", listType = "currentQueue", classes = []) {
  4019. var _a;
  4020. return __awaiter(this, void 0, void 0, function* () {
  4021. //#SECTION general queue item stuff
  4022. const queueBtnsCont = document.createElement("div");
  4023. queueBtnsCont.classList.add("bytm-queue-btn-container", ...classes);
  4024. const lyricsIconUrl = yield getResourceUrl("icon-lyrics");
  4025. const deleteIconUrl = yield getResourceUrl("icon-delete");
  4026. //#SECTION lyrics btn
  4027. let lyricsBtnElem;
  4028. if (getFeatures().lyricsQueueButton) {
  4029. lyricsBtnElem = yield createLyricsBtn(undefined, false);
  4030. lyricsBtnElem.ariaLabel = lyricsBtnElem.title = t("open_lyrics");
  4031. lyricsBtnElem.style.display = "inline-flex";
  4032. lyricsBtnElem.style.visibility = "initial";
  4033. lyricsBtnElem.style.pointerEvents = "initial";
  4034. lyricsBtnElem.role = "link";
  4035. lyricsBtnElem.tabIndex = 0;
  4036. onInteraction(lyricsBtnElem, (e) => __awaiter(this, void 0, void 0, function* () {
  4037. var _b;
  4038. e.preventDefault();
  4039. e.stopImmediatePropagation();
  4040. let song, artist;
  4041. if (listType === "currentQueue") {
  4042. const songInfo = queueItem.querySelector(".song-info");
  4043. if (!songInfo)
  4044. return;
  4045. const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
  4046. song = songEl === null || songEl === void 0 ? void 0 : songEl.textContent;
  4047. artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.textContent;
  4048. }
  4049. else if (listType === "genericQueue") {
  4050. const songEl = queueItem.querySelector(".title-column yt-formatted-string a");
  4051. let artistEl = null;
  4052. if (location.pathname.startsWith("/playlist"))
  4053. artistEl = document.querySelector("ytmusic-detail-header-renderer .metadata .subtitle-container yt-formatted-string a");
  4054. else
  4055. artistEl = queueItem.querySelector(".secondary-flex-columns yt-formatted-string:first-child a");
  4056. song = songEl === null || songEl === void 0 ? void 0 : songEl.textContent;
  4057. artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.textContent;
  4058. }
  4059. else
  4060. return;
  4061. if (!song || !artist)
  4062. return error("Couldn't get song or artist name from queue item - song:", song, "- artist:", artist);
  4063. let lyricsUrl;
  4064. const artistsSan = sanitizeArtists(artist);
  4065. const songSan = sanitizeSong(song);
  4066. const splitTitle = splitVideoTitle(songSan);
  4067. const cachedLyricsEntry = songSan.includes("-")
  4068. ? getLyricsCacheEntry(splitTitle.artist, splitTitle.song)
  4069. : getLyricsCacheEntry(artistsSan, songSan);
  4070. if (cachedLyricsEntry)
  4071. lyricsUrl = cachedLyricsEntry.url;
  4072. else if (!queueItem.hasAttribute("data-bytm-loading")) {
  4073. const imgEl = lyricsBtnElem === null || lyricsBtnElem === void 0 ? void 0 : lyricsBtnElem.querySelector("img");
  4074. if (!imgEl)
  4075. return;
  4076. if (!cachedLyricsEntry) {
  4077. queueItem.setAttribute("data-bytm-loading", "");
  4078. imgEl.src = yield getResourceUrl("icon-spinner");
  4079. imgEl.classList.add("bytm-spinner");
  4080. }
  4081. lyricsUrl = (_b = cachedLyricsEntry === null || cachedLyricsEntry === void 0 ? void 0 : cachedLyricsEntry.url) !== null && _b !== void 0 ? _b : yield fetchLyricsUrlTop(artistsSan, songSan);
  4082. if (lyricsUrl) {
  4083. emitInterface("bytm:lyricsLoaded", {
  4084. type: "queue",
  4085. artists: artist,
  4086. title: song,
  4087. url: lyricsUrl,
  4088. });
  4089. }
  4090. const resetImgElem = () => {
  4091. imgEl.src = lyricsIconUrl;
  4092. imgEl.classList.remove("bytm-spinner");
  4093. };
  4094. if (!cachedLyricsEntry) {
  4095. queueItem.removeAttribute("data-bytm-loading");
  4096. // so the new image doesn't "blink"
  4097. setTimeout(resetImgElem, 100);
  4098. }
  4099. if (!lyricsUrl) {
  4100. resetImgElem();
  4101. if (confirm(t("lyrics_not_found_confirm_open_search")))
  4102. UserUtils.openInNewTab(`https://genius.com/search?q=${encodeURIComponent(`${artistsSan} - ${songSan}`)}`);
  4103. return;
  4104. }
  4105. }
  4106. lyricsUrl && UserUtils.openInNewTab(lyricsUrl);
  4107. }));
  4108. }
  4109. //#SECTION delete from queue btn
  4110. let deleteBtnElem;
  4111. if (getFeatures().deleteFromQueueButton) {
  4112. deleteBtnElem = document.createElement("a");
  4113. deleteBtnElem.ariaLabel = deleteBtnElem.title = (listType === "currentQueue" ? t("remove_from_queue") : t("delete_from_list"));
  4114. deleteBtnElem.classList.add("ytmusic-player-bar", "bytm-delete-from-queue", "bytm-generic-btn");
  4115. deleteBtnElem.role = "button";
  4116. deleteBtnElem.tabIndex = 0;
  4117. deleteBtnElem.style.visibility = "initial";
  4118. const imgElem = document.createElement("img");
  4119. imgElem.classList.add("bytm-generic-btn-img");
  4120. imgElem.src = deleteIconUrl;
  4121. onInteraction(deleteBtnElem, (e) => __awaiter(this, void 0, void 0, function* () {
  4122. e.preventDefault();
  4123. e.stopImmediatePropagation();
  4124. // container of the queue item popup menu - element gets reused for every queue item
  4125. let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
  4126. try {
  4127. // three dots button to open the popup menu of a queue item
  4128. const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape[id=\"button-shape\"] button");
  4129. if (dotsBtnElem) {
  4130. if (queuePopupCont)
  4131. queuePopupCont.setAttribute("data-bytm-hidden", "true");
  4132. dotsBtnElem.click();
  4133. yield UserUtils.pauseFor(10);
  4134. queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
  4135. queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.setAttribute("data-bytm-hidden", "true");
  4136. // a little bit janky and unreliable but the only way afaik
  4137. const removeFromQueueBtn = queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.querySelector("tp-yt-paper-listbox ytmusic-menu-service-item-renderer:nth-of-type(3)");
  4138. yield UserUtils.pauseFor(10);
  4139. removeFromQueueBtn === null || removeFromQueueBtn === void 0 ? void 0 : removeFromQueueBtn.click();
  4140. // queue items aren't removed automatically outside of the current queue
  4141. if (removeFromQueueBtn && listType === "genericQueue") {
  4142. yield UserUtils.pauseFor(500);
  4143. clearInner(queueItem);
  4144. queueItem.remove();
  4145. }
  4146. if (!removeFromQueueBtn) {
  4147. warn("Couldn't find 'remove from queue' button in queue item three dots menu");
  4148. dotsBtnElem.click();
  4149. imgElem.src = yield getResourceUrl("icon-error");
  4150. if (deleteBtnElem)
  4151. deleteBtnElem.ariaLabel = deleteBtnElem.title = (listType === "currentQueue" ? t("couldnt_remove_from_queue") : t("couldnt_delete_from_list"));
  4152. }
  4153. }
  4154. }
  4155. catch (err) {
  4156. error("Couldn't remove song from queue due to error:", err);
  4157. }
  4158. finally {
  4159. queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.removeAttribute("data-bytm-hidden");
  4160. }
  4161. }));
  4162. deleteBtnElem.appendChild(imgElem);
  4163. }
  4164. //#SECTION append elements to DOM
  4165. lyricsBtnElem && queueBtnsCont.appendChild(lyricsBtnElem);
  4166. deleteBtnElem && queueBtnsCont.appendChild(deleteBtnElem);
  4167. (_a = queueItem.querySelector(containerParentSelector)) === null || _a === void 0 ? void 0 : _a.appendChild(queueBtnsCont);
  4168. queueItem.classList.add("bytm-has-queue-btns");
  4169. });
  4170. }//#MARKER feature dependencies
  4171. /** List of all available locale SelectOptions */
  4172. const localeOptions = Object.entries(locales).reduce((a, [locale, { name }]) => {
  4173. return [...a, {
  4174. value: locale,
  4175. label: name,
  4176. }];
  4177. }, [])
  4178. .sort((a, b) => a.label.localeCompare(b.label));
  4179. /** Decoration elements that can be added next to the label */
  4180. const adornments = {
  4181. advanced: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return `<span class="bytm-advanced-mode-icon bytm-adorn-icon" title="${t("advanced_mode")}">${(_a = yield resourceToHTMLString("icon-advanced_mode")) !== null && _a !== void 0 ? _a : ""}</span>`; }),
  4182. experimental: () => __awaiter(void 0, void 0, void 0, function* () { var _b; return `<span class="bytm-experimental-icon bytm-adorn-icon" title="${t("experimental_feature")}" aria-label="${t("experimental_feature")}" role="alert">${(_b = yield resourceToHTMLString("icon-experimental")) !== null && _b !== void 0 ? _b : ""}</span>`; }),
  4183. globe: () => __awaiter(void 0, void 0, void 0, function* () { var _c; return (_c = yield resourceToHTMLString("icon-globe")) !== null && _c !== void 0 ? _c : ""; }),
  4184. warning: (text) => __awaiter(void 0, void 0, void 0, function* () { var _d; return `<span class="bytm-warning-icon bytm-adorn-icon" title="${text}" aria-label="${text}" role="alert">${(_d = yield resourceToHTMLString("icon-error")) !== null && _d !== void 0 ? _d : ""}</span>`; }),
  4185. };
  4186. /** Common options for config items of type "select" */
  4187. const options = {
  4188. siteSelection: () => [
  4189. { value: "all", label: t("site_selection_both_sites") },
  4190. { value: "yt", label: t("site_selection_only_yt") },
  4191. { value: "ytm", label: t("site_selection_only_ytm") },
  4192. ],
  4193. };
  4194. //#MARKER features
  4195. /**
  4196. * Contains all possible features with their default values and other configuration.
  4197. *
  4198. * **Required props:**
  4199. * | Property | Description |
  4200. * | :-- | :-- |
  4201. * | `type` | type of the feature configuration element - use autocomplete or check `FeatureTypeProps` in `src/types.ts` |
  4202. * | `category` | category of the feature - use autocomplete or check `FeatureCategory` in `src/types.ts` |
  4203. * | `default` | default value of the feature - type of the value depends on the given `type` |
  4204. * | `enable(value: any)` | function that will be called when the feature is enabled / initialized for the first time |
  4205. *
  4206. * **Optional props:**
  4207. * | Property | Description |
  4208. * | :-- | :-- |
  4209. * | `disable: (newValue: any) => void` | for type `toggle` only - function that will be called when the feature is disabled - can be a synchronous or asynchronous function |
  4210. * | `change: (prevValue: any, newValue: any)` => void | for types `number`, `select`, `slider` and `hotkey` only - function that will be called when the value is changed |
  4211. * | `click: () => void` | for type `button` only - function that will be called when the button is clicked |
  4212. * | `helpText: string / () => string` | function that returns an HTML string or the literal string itself that will be the help text for this feature - writing as function is useful for pluralizing or inserting values into the translation at runtime - if not set, translation with key `feature_helptext_featureKey` will be used instead, if available |
  4213. * | `textAdornment: () => string / Promise<string>` | function that returns an HTML string that will be appended to the text in the config menu as an adornment element - TODO: to be replaced in the big menu rework |
  4214. * | `unit: string / (val: number) => string` | Only if type is `number` or `slider` - The unit text that is displayed next to the input element, i.e. " px" - a leading space need to be added by hand! |
  4215. * | `min: number` | Only if type is `number` or `slider` - Overwrites the default of the `min` property of the HTML input element |
  4216. * | `max: number` | Only if type is `number` or `slider` - Overwrites the default of the `max` property of the HTML input element |
  4217. * | `step: number` | Only if type is `number` or `slider` - Overwrites the default of the `step` property of the HTML input element |
  4218. * | `options: SelectOption[] / () => SelectOption[]` | Only if type is `select` - function that returns an array of objects with `value` and `label` properties |
  4219. * | `advanced: boolean` | if true, the feature will only be shown if the advanced mode feature has been turned on |
  4220. * | `hidden: boolean` | if true, the feature will not be shown in the settings - default is undefined (false) |
  4221. * | `valueHidden: boolean` | If true, the value of the feature will be hidden in the settings and via the plugin interface - default is undefined (false) |
  4222. * | `normalize: (val: any) => any` | Function that will be called to normalize the value before it is saved - useful for trimming strings or other simple operations |
  4223. *
  4224. * **Notes:**
  4225. * - If no `disable()` or `change()` function is present, the page needs to be reloaded for the changes to take effect
  4226. */
  4227. const featInfo = {
  4228. //#SECTION layout
  4229. watermarkEnabled: {
  4230. type: "toggle",
  4231. category: "layout",
  4232. default: true,
  4233. enable: noopTODO,
  4234. disable: noopTODO,
  4235. },
  4236. removeShareTrackingParam: {
  4237. type: "toggle",
  4238. category: "layout",
  4239. default: true,
  4240. enable: noopTODO,
  4241. disable: noopTODO,
  4242. },
  4243. removeShareTrackingParamSites: {
  4244. type: "select",
  4245. category: "layout",
  4246. options: options.siteSelection,
  4247. default: "all",
  4248. enable: noopTODO,
  4249. disable: noopTODO,
  4250. },
  4251. fixSpacing: {
  4252. type: "toggle",
  4253. category: "layout",
  4254. default: true,
  4255. enable: noopTODO,
  4256. disable: noopTODO,
  4257. },
  4258. scrollToActiveSongBtn: {
  4259. type: "toggle",
  4260. category: "layout",
  4261. default: true,
  4262. enable: noopTODO,
  4263. disable: noopTODO,
  4264. },
  4265. removeUpgradeTab: {
  4266. type: "toggle",
  4267. category: "layout",
  4268. default: true,
  4269. enable: noopTODO,
  4270. },
  4271. thumbnailOverlayBehavior: {
  4272. type: "select",
  4273. category: "layout",
  4274. options: () => [
  4275. { value: "songsOnly", label: t("thumbnail_overlay_behavior_songs_only") },
  4276. { value: "videosOnly", label: t("thumbnail_overlay_behavior_videos_only") },
  4277. { value: "always", label: t("thumbnail_overlay_behavior_always") },
  4278. { value: "never", label: t("thumbnail_overlay_behavior_never") },
  4279. ],
  4280. default: "songsOnly",
  4281. enable: noopTODO,
  4282. change: noopTODO,
  4283. },
  4284. thumbnailOverlayToggleBtnShown: {
  4285. type: "toggle",
  4286. category: "layout",
  4287. default: true,
  4288. enable: noopTODO,
  4289. disable: noopTODO,
  4290. },
  4291. thumbnailOverlayShowIndicator: {
  4292. type: "toggle",
  4293. category: "layout",
  4294. default: true,
  4295. enable: noopTODO,
  4296. disable: noopTODO,
  4297. advanced: true,
  4298. // TODO: to be reworked or removed in the big menu rework
  4299. textAdornment: adornments.advanced,
  4300. },
  4301. thumbnailOverlayImageFit: {
  4302. type: "select",
  4303. category: "layout",
  4304. options: () => [
  4305. { value: "cover", label: t("thumbnail_overlay_image_fit_crop") },
  4306. { value: "contain", label: t("thumbnail_overlay_image_fit_full") },
  4307. { value: "fill", label: t("thumbnail_overlay_image_fit_stretch") },
  4308. ],
  4309. default: "cover",
  4310. enable: noopTODO,
  4311. change: noopTODO,
  4312. advanced: true,
  4313. // TODO: to be reworked or removed in the big menu rework
  4314. textAdornment: adornments.advanced,
  4315. },
  4316. hideCursorOnIdle: {
  4317. type: "toggle",
  4318. category: "layout",
  4319. default: true,
  4320. enable: noopTODO,
  4321. disable: noopTODO,
  4322. },
  4323. hideCursorOnIdleDelay: {
  4324. type: "slider",
  4325. category: "layout",
  4326. min: 0.5,
  4327. max: 10,
  4328. step: 0.5,
  4329. default: 3,
  4330. unit: "s",
  4331. enable: noopTODO,
  4332. change: noopTODO,
  4333. advanced: true,
  4334. // TODO: to be reworked or removed in the big menu rework
  4335. textAdornment: adornments.advanced,
  4336. },
  4337. //#SECTION volume
  4338. volumeSliderLabel: {
  4339. type: "toggle",
  4340. category: "volume",
  4341. default: true,
  4342. enable: noopTODO,
  4343. disable: noopTODO,
  4344. },
  4345. volumeSliderSize: {
  4346. type: "number",
  4347. category: "volume",
  4348. min: 50,
  4349. max: 500,
  4350. step: 5,
  4351. default: 150,
  4352. unit: "px",
  4353. enable: noopTODO,
  4354. change: noopTODO,
  4355. },
  4356. volumeSliderStep: {
  4357. type: "slider",
  4358. category: "volume",
  4359. min: 1,
  4360. max: 25,
  4361. default: 2,
  4362. unit: "%",
  4363. enable: noopTODO,
  4364. change: noopTODO,
  4365. },
  4366. volumeSliderScrollStep: {
  4367. type: "slider",
  4368. category: "volume",
  4369. min: 1,
  4370. max: 25,
  4371. default: 10,
  4372. unit: "%",
  4373. enable: noopTODO,
  4374. change: noopTODO,
  4375. },
  4376. volumeSharedBetweenTabs: {
  4377. type: "toggle",
  4378. category: "volume",
  4379. default: false,
  4380. enable: noopTODO,
  4381. disable: () => volumeSharedBetweenTabsDisabled,
  4382. },
  4383. setInitialTabVolume: {
  4384. type: "toggle",
  4385. category: "volume",
  4386. default: false,
  4387. enable: noopTODO,
  4388. disable: noopTODO,
  4389. textAdornment: () => getFeatures().volumeSharedBetweenTabs ? adornments.warning(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")) : undefined,
  4390. },
  4391. initialTabVolumeLevel: {
  4392. type: "slider",
  4393. category: "volume",
  4394. min: 0,
  4395. max: 100,
  4396. step: 1,
  4397. default: 100,
  4398. unit: "%",
  4399. enable: noopTODO,
  4400. change: noopTODO,
  4401. textAdornment: () => getFeatures().volumeSharedBetweenTabs ? adornments.warning(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")) : undefined,
  4402. },
  4403. //#SECTION song lists
  4404. lyricsQueueButton: {
  4405. type: "toggle",
  4406. category: "songLists",
  4407. default: true,
  4408. enable: noopTODO,
  4409. disable: noopTODO,
  4410. },
  4411. deleteFromQueueButton: {
  4412. type: "toggle",
  4413. category: "songLists",
  4414. default: true,
  4415. enable: noopTODO,
  4416. disable: noopTODO,
  4417. },
  4418. listButtonsPlacement: {
  4419. type: "select",
  4420. category: "songLists",
  4421. options: () => [
  4422. { value: "queueOnly", label: t("list_button_placement_queue_only") },
  4423. { value: "everywhere", label: t("list_button_placement_everywhere") },
  4424. ],
  4425. default: "everywhere",
  4426. enable: noopTODO,
  4427. disable: noopTODO,
  4428. },
  4429. //#SECTION behavior
  4430. disableBeforeUnloadPopup: {
  4431. type: "toggle",
  4432. category: "behavior",
  4433. default: false,
  4434. enable: noopTODO,
  4435. },
  4436. closeToastsTimeout: {
  4437. type: "number",
  4438. category: "behavior",
  4439. min: 0,
  4440. max: 30,
  4441. step: 0.5,
  4442. default: 0,
  4443. unit: "s",
  4444. enable: noopTODO,
  4445. change: noopTODO,
  4446. },
  4447. rememberSongTime: {
  4448. type: "toggle",
  4449. category: "behavior",
  4450. default: true,
  4451. enable: noopTODO,
  4452. disable: noopTODO, // TODO: feasible?
  4453. helpText: () => tp("feature_helptext_rememberSongTime", getFeatures().rememberSongTimeMinPlayTime, getFeatures().rememberSongTimeMinPlayTime)
  4454. },
  4455. rememberSongTimeSites: {
  4456. type: "select",
  4457. category: "behavior",
  4458. options: options.siteSelection,
  4459. default: "ytm",
  4460. enable: noopTODO,
  4461. change: noopTODO,
  4462. },
  4463. rememberSongTimeDuration: {
  4464. type: "number",
  4465. category: "behavior",
  4466. min: 3,
  4467. max: 60 * 60 * 24 * 7,
  4468. step: 1,
  4469. default: 60,
  4470. unit: "s",
  4471. enable: noopTODO,
  4472. change: noopTODO,
  4473. advanced: true,
  4474. // TODO: to be reworked or removed in the big menu rework
  4475. textAdornment: adornments.advanced,
  4476. },
  4477. rememberSongTimeReduction: {
  4478. type: "number",
  4479. category: "behavior",
  4480. min: 0,
  4481. max: 30,
  4482. step: 0.1,
  4483. default: 0,
  4484. unit: "s",
  4485. enable: noopTODO,
  4486. change: noopTODO,
  4487. advanced: true,
  4488. // TODO: to be reworked or removed in the big menu rework
  4489. textAdornment: adornments.advanced,
  4490. },
  4491. rememberSongTimeMinPlayTime: {
  4492. type: "slider",
  4493. category: "behavior",
  4494. min: 1,
  4495. max: 30,
  4496. step: 0.5,
  4497. default: 10,
  4498. unit: "s",
  4499. enable: noopTODO,
  4500. change: noopTODO,
  4501. advanced: true,
  4502. // TODO: to be reworked or removed in the big menu rework
  4503. textAdornment: adornments.advanced,
  4504. },
  4505. //#SECTION input
  4506. arrowKeySupport: {
  4507. type: "toggle",
  4508. category: "input",
  4509. default: true,
  4510. enable: noopTODO,
  4511. disable: noopTODO,
  4512. },
  4513. arrowKeySkipBy: {
  4514. type: "number",
  4515. category: "input",
  4516. min: 0.5,
  4517. max: 60,
  4518. step: 0.5,
  4519. default: 5,
  4520. enable: noopTODO,
  4521. change: noopTODO,
  4522. },
  4523. switchBetweenSites: {
  4524. type: "toggle",
  4525. category: "input",
  4526. default: true,
  4527. enable: noopTODO,
  4528. disable: noopTODO,
  4529. },
  4530. switchSitesHotkey: {
  4531. type: "hotkey",
  4532. category: "input",
  4533. default: {
  4534. code: "F9",
  4535. shift: false,
  4536. ctrl: false,
  4537. alt: false,
  4538. },
  4539. enable: noopTODO,
  4540. change: noopTODO,
  4541. },
  4542. anchorImprovements: {
  4543. type: "toggle",
  4544. category: "input",
  4545. default: true,
  4546. enable: noopTODO,
  4547. disable: noopTODO,
  4548. },
  4549. numKeysSkipToTime: {
  4550. type: "toggle",
  4551. category: "input",
  4552. default: true,
  4553. enable: noopTODO,
  4554. disable: noopTODO,
  4555. },
  4556. //#SECTION lyrics
  4557. geniusLyrics: {
  4558. type: "toggle",
  4559. category: "lyrics",
  4560. default: true,
  4561. enable: noopTODO,
  4562. disable: noopTODO,
  4563. },
  4564. geniUrlBase: {
  4565. type: "text",
  4566. category: "lyrics",
  4567. default: "https://api.sv443.net/geniurl",
  4568. normalize: (val) => val.trim().replace(/\/+$/, ""),
  4569. advanced: true,
  4570. // TODO: to be reworked or removed in the big menu rework
  4571. textAdornment: adornments.advanced,
  4572. },
  4573. geniUrlToken: {
  4574. type: "text",
  4575. valueHidden: true,
  4576. category: "lyrics",
  4577. default: "",
  4578. normalize: (val) => val.trim(),
  4579. advanced: true,
  4580. // TODO: to be reworked or removed in the big menu rework
  4581. textAdornment: adornments.advanced,
  4582. },
  4583. lyricsCacheMaxSize: {
  4584. type: "slider",
  4585. category: "lyrics",
  4586. default: 1000,
  4587. min: 100,
  4588. max: 5000,
  4589. step: 100,
  4590. unit: (val) => " " + tp("unit_entries", val),
  4591. enable: noopTODO,
  4592. change: noopTODO,
  4593. advanced: true,
  4594. // TODO: to be reworked or removed in the big menu rework
  4595. textAdornment: adornments.advanced,
  4596. },
  4597. lyricsCacheTTL: {
  4598. type: "slider",
  4599. category: "lyrics",
  4600. default: 21,
  4601. min: 1,
  4602. max: 100,
  4603. step: 1,
  4604. unit: (val) => " " + tp("unit_days", val),
  4605. enable: noopTODO,
  4606. change: noopTODO,
  4607. advanced: true,
  4608. // TODO: to be reworked or removed in the big menu rework
  4609. textAdornment: adornments.advanced,
  4610. },
  4611. clearLyricsCache: {
  4612. type: "button",
  4613. category: "lyrics",
  4614. default: undefined,
  4615. click() {
  4616. return __awaiter(this, void 0, void 0, function* () {
  4617. const entries = getLyricsCache().length;
  4618. if (confirm(tp("lyrics_clear_cache_confirm_prompt", entries, entries))) {
  4619. yield clearLyricsCache();
  4620. alert(t("lyrics_clear_cache_success"));
  4621. }
  4622. });
  4623. },
  4624. advanced: true,
  4625. // TODO: to be reworked or removed in the big menu rework
  4626. textAdornment: adornments.advanced,
  4627. },
  4628. advancedLyricsFilter: {
  4629. type: "toggle",
  4630. category: "lyrics",
  4631. default: false,
  4632. enable: noopTODO,
  4633. disable: noopTODO,
  4634. // TODO: use dialog here?
  4635. change: () => confirm(t("lyrics_cache_changed_clear_confirm")) && clearLyricsCache(),
  4636. advanced: true,
  4637. // TODO: to be reworked or removed in the big menu rework
  4638. textAdornment: adornments.experimental,
  4639. },
  4640. //#SECTION general
  4641. locale: {
  4642. type: "select",
  4643. category: "general",
  4644. options: localeOptions,
  4645. default: getPreferredLocale(),
  4646. enable: noopTODO,
  4647. // TODO: to be reworked or removed in the big menu rework
  4648. textAdornment: adornments.globe,
  4649. },
  4650. versionCheck: {
  4651. type: "toggle",
  4652. category: "general",
  4653. default: true,
  4654. enable: noopTODO,
  4655. disable: noopTODO,
  4656. },
  4657. checkVersionNow: {
  4658. type: "button",
  4659. category: "general",
  4660. default: undefined,
  4661. click: () => doVersionCheck(true),
  4662. },
  4663. logLevel: {
  4664. type: "select",
  4665. category: "general",
  4666. options: () => [
  4667. { value: 0, label: t("log_level_debug") },
  4668. { value: 1, label: t("log_level_info") },
  4669. ],
  4670. default: 1,
  4671. enable: noopTODO,
  4672. },
  4673. advancedMode: {
  4674. type: "toggle",
  4675. category: "general",
  4676. default: mode === "development",
  4677. enable: noopTODO,
  4678. disable: noopTODO,
  4679. // TODO: to be reworked or removed in the big menu rework
  4680. textAdornment: () => getFeatures().advancedMode ? adornments.advanced() : undefined,
  4681. },
  4682. };
  4683. function noopTODO() {
  4684. }/** If this number is incremented, the features object data will be migrated to the new format */
  4685. const formatVersion = 5;
  4686. /** Config data format migration dictionary */
  4687. const migrations = {
  4688. // 1 -> 2 (v1.0)
  4689. 2: (oldData) => {
  4690. const queueBtnsEnabled = Boolean(oldData.queueButtons);
  4691. delete oldData.queueButtons;
  4692. return Object.assign(Object.assign({}, oldData), { deleteFromQueueButton: queueBtnsEnabled, lyricsQueueButton: queueBtnsEnabled });
  4693. },
  4694. // 2 -> 3 (v1.0)
  4695. 3: (oldData) => useDefaultConfig([
  4696. "removeShareTrackingParam", "numKeysSkipToTime",
  4697. "fixSpacing", "scrollToActiveSongBtn", "logLevel",
  4698. ], oldData),
  4699. // 3 -> 4 (v1.1)
  4700. 4: (oldData) => {
  4701. var _a, _b, _c, _d;
  4702. const oldSwitchSitesHotkey = oldData.switchSitesHotkey;
  4703. return Object.assign(Object.assign({}, useDefaultConfig([
  4704. "rememberSongTime", "rememberSongTimeSites",
  4705. "volumeSliderScrollStep", "locale", "versionCheck",
  4706. ], oldData)), { arrowKeySkipBy: 10, switchSitesHotkey: {
  4707. code: (_a = oldSwitchSitesHotkey.key) !== null && _a !== void 0 ? _a : "F9",
  4708. shift: Boolean((_b = oldSwitchSitesHotkey.shift) !== null && _b !== void 0 ? _b : false),
  4709. ctrl: Boolean((_c = oldSwitchSitesHotkey.ctrl) !== null && _c !== void 0 ? _c : false),
  4710. alt: Boolean((_d = oldSwitchSitesHotkey.meta) !== null && _d !== void 0 ? _d : false),
  4711. }, listButtonsPlacement: "queueOnly" });
  4712. },
  4713. // 4 -> 5 (v1.2)
  4714. 5: (oldData) => (Object.assign({}, useDefaultConfig([
  4715. "geniUrlBase", "geniUrlToken",
  4716. "lyricsCacheMaxSize", "lyricsCacheTTL",
  4717. "clearLyricsCache", "advancedMode",
  4718. "checkVersionNow", "advancedLyricsFilter",
  4719. "rememberSongTimeDuration", "rememberSongTimeReduction",
  4720. "rememberSongTimeMinPlayTime", "volumeSharedBetweenTabs",
  4721. "setInitialTabVolume", "initialTabVolumeLevel",
  4722. "thumbnailOverlayBehavior", "thumbnailOverlayToggleBtnShown",
  4723. "thumbnailOverlayShowIndicator", "thumbnailOverlayImageFit",
  4724. "removeShareTrackingParamSites",
  4725. ], oldData))),
  4726. // TODO: once advanced filtering is fully implemented, clear cache on migration to fv6
  4727. // 5 -> 6 (v1.3)
  4728. // 6: (oldData: FeatureConfig) =>
  4729. };
  4730. /**
  4731. * Uses the passed {@linkcode baseData} as the base if given, and sets all passed feature {@linkcode keys} to their default value
  4732. * @returns Returns a copy of the object
  4733. */
  4734. function useDefaultConfig(keys, baseData) {
  4735. const newData = Object.assign({}, (baseData !== null && baseData !== void 0 ? baseData : {}));
  4736. for (const key of keys)
  4737. newData[key] = getFeatureDefault(key);
  4738. return newData;
  4739. }
  4740. /** Returns the default value for the given feature key */
  4741. function getFeatureDefault(key) {
  4742. return featInfo[key].default;
  4743. }
  4744. const defaultData = Object.keys(featInfo)
  4745. .reduce((acc, key) => {
  4746. acc[key] = featInfo[key].default;
  4747. return acc;
  4748. }, {});
  4749. let canCompress = true;
  4750. const bytmCfgStore = new UserUtils.DataStore({
  4751. id: "bytm-config",
  4752. formatVersion,
  4753. defaultData,
  4754. migrations,
  4755. encodeData: (data) => canCompress ? UserUtils.compress(data, compressionFormat, "string") : data,
  4756. decodeData: (data) => canCompress ? UserUtils.decompress(data, compressionFormat, "string") : data,
  4757. });
  4758. /** Initializes the DataStore instance and loads persistent data into memory */
  4759. function initConfig() {
  4760. return __awaiter(this, void 0, void 0, function* () {
  4761. canCompress = yield compressionSupported();
  4762. const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
  4763. const data = yield bytmCfgStore.loadData();
  4764. log(`Initialized feature config DataStore (formatVersion = ${bytmCfgStore.formatVersion})`);
  4765. if (isNaN(oldFmtVer))
  4766. info(" !- Config data was initialized with default values");
  4767. else if (oldFmtVer !== bytmCfgStore.formatVersion)
  4768. info(` !- Config data was migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
  4769. emitInterface("bytm:configReady", getFeaturesInterface());
  4770. return Object.assign({}, data);
  4771. });
  4772. }
  4773. /** Returns the current feature config from the in-memory cache as a copy */
  4774. function getFeatures() {
  4775. return bytmCfgStore.getData();
  4776. }
  4777. /** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */
  4778. function setFeatures(featureConf) {
  4779. const res = bytmCfgStore.setData(featureConf);
  4780. emitSiteEvent("configChanged", bytmCfgStore.getData());
  4781. info("Saved new feature config:", featureConf);
  4782. return res;
  4783. }
  4784. /** Saves the default feature config synchronously to the in-memory cache and asynchronously to persistent storage */
  4785. function setDefaultFeatures() {
  4786. const res = bytmCfgStore.saveDefaultData();
  4787. emitSiteEvent("configChanged", bytmCfgStore.getData());
  4788. info("Reset feature config to its default values");
  4789. return res;
  4790. }
  4791. /** Clears the feature config from the persistent storage - since the cache will be out of whack, this should only be run before a site re-/unload */
  4792. function clearConfig() {
  4793. return __awaiter(this, void 0, void 0, function* () {
  4794. yield bytmCfgStore.deleteData();
  4795. info("Deleted config from persistent storage");
  4796. });
  4797. }const { getUnsafeWindow } = UserUtils__namespace;
  4798. const globalFuncs = {
  4799. // meta
  4800. registerPlugin,
  4801. getPluginInfo,
  4802. // utils
  4803. addSelectorListener,
  4804. getResourceUrl,
  4805. getSessionId,
  4806. getVideoTime,
  4807. setLocale,
  4808. getLocale,
  4809. hasKey,
  4810. hasKeyFor,
  4811. t,
  4812. tp,
  4813. getFeatures: getFeaturesInterface,
  4814. saveFeatures: setFeatures,
  4815. fetchLyricsUrlTop,
  4816. getLyricsCacheEntry,
  4817. sanitizeArtists,
  4818. sanitizeSong,
  4819. compareVersions,
  4820. compareVersionArrays,
  4821. };
  4822. /** Plugins that are queued up for registration */
  4823. const pluginQueue = new Map();
  4824. /** Registered plugins including their event listener instance */
  4825. const pluginMap = new Map();
  4826. /** Initializes the BYTM interface */
  4827. function initInterface() {
  4828. const props = Object.assign(Object.assign(Object.assign({ mode,
  4829. branch,
  4830. host,
  4831. buildNumber,
  4832. compressionFormat }, scriptInfo), globalFuncs), { UserUtils: UserUtils__namespace,
  4833. NanoEmitter,
  4834. BytmDialog,
  4835. createHotkeyInput,
  4836. createToggleInput });
  4837. for (const [key, value] of Object.entries(props))
  4838. setGlobalProp(key, value);
  4839. log("Initialized BYTM interface");
  4840. }
  4841. /** Sets a global property on the unsafeWindow.BYTM object */
  4842. function setGlobalProp(key, value) {
  4843. // use unsafeWindow so the properties are available to plugins outside of the userscript's scope
  4844. const win = getUnsafeWindow();
  4845. if (typeof win.BYTM !== "object")
  4846. win.BYTM = {};
  4847. win.BYTM[key] = value;
  4848. }
  4849. /** Emits an event on the BYTM interface */
  4850. function emitInterface(type, ...data) {
  4851. getUnsafeWindow().dispatchEvent(new CustomEvent(type, { detail: data[0] }));
  4852. }
  4853. //#MARKER register plugins
  4854. /** Initializes plugins that have been registered already. Needs to be run after `bytm:ready`! */
  4855. function initPlugins() {
  4856. // TODO(v1.3): check perms and ask user for initial activation
  4857. for (const [key, { def, events }] of pluginQueue) {
  4858. pluginMap.set(key, { def, events });
  4859. pluginQueue.delete(key);
  4860. emitOnPlugins("pluginRegistered", (d) => sameDef(d, def), pluginDefToInfo(def));
  4861. }
  4862. for (const evt of allSiteEvents) // @ts-ignore
  4863. siteEvents.on(evt, (...args) => emitOnPlugins(evt, () => true, ...args));
  4864. emitInterface("bytm:pluginsLoaded");
  4865. }
  4866. /** Returns the key for a given plugin definition */
  4867. function getPluginKey(plugin) {
  4868. return `${plugin.plugin.namespace}/${plugin.plugin.name}`;
  4869. }
  4870. /** Converts a PluginDef object (full definition) into a PluginInfo object (restricted definition) or undefined, if undefined is passed */
  4871. function pluginDefToInfo(plugin) {
  4872. return plugin && {
  4873. name: plugin.plugin.name,
  4874. namespace: plugin.plugin.namespace,
  4875. version: plugin.plugin.version,
  4876. };
  4877. }
  4878. /** Checks whether two plugin definitions are the same */
  4879. function sameDef(def1, def2) {
  4880. return getPluginKey(def1) === getPluginKey(def2);
  4881. }
  4882. /** Emits an event on all plugins that match the predicate (all plugins by default) */
  4883. function emitOnPlugins(event, predicate = () => true, ...data) {
  4884. for (const { def, events } of pluginMap.values())
  4885. predicate(def) && events.emit(event, ...data);
  4886. }
  4887. /** Returns info about a registered plugin on the BYTM interface, or undefined if the plugin isn't registered */
  4888. function getPluginInfo(...args) {
  4889. var _a, _b;
  4890. return pluginDefToInfo(args.length === 2
  4891. ? (_a = pluginMap.get(`${args[1]}/${args[0]}`)) === null || _a === void 0 ? void 0 : _a.def
  4892. : (_b = pluginMap.get(getPluginKey(args[0]))) === null || _b === void 0 ? void 0 : _b.def);
  4893. }
  4894. /** Validates the passed PluginDef object and returns an array of errors */
  4895. function validatePluginDef(pluginDef) {
  4896. const errors = [];
  4897. const addNoPropErr = (prop, type) => errors.push(t("plugin_validation_error_no_property", prop, type));
  4898. // def.plugin and its properties:
  4899. typeof pluginDef.plugin !== "object" && addNoPropErr("plugin", "object");
  4900. const { plugin } = pluginDef;
  4901. !(plugin === null || plugin === void 0 ? void 0 : plugin.name) && addNoPropErr("plugin.name", "string");
  4902. !(plugin === null || plugin === void 0 ? void 0 : plugin.namespace) && addNoPropErr("plugin.namespace", "string");
  4903. !(plugin === null || plugin === void 0 ? void 0 : plugin.version) && addNoPropErr("plugin.version", "[major: number, minor: number, patch: number]");
  4904. return errors.length > 0 ? errors : undefined;
  4905. }
  4906. /** Registers a plugin on the BYTM interface */
  4907. function registerPlugin(def) {
  4908. var _a, _b;
  4909. const validationErrors = validatePluginDef(def);
  4910. if (validationErrors) {
  4911. error(`Failed to register plugin${((_a = def === null || def === void 0 ? void 0 : def.plugin) === null || _a === void 0 ? void 0 : _a.name) ? ` '${(_b = def === null || def === void 0 ? void 0 : def.plugin) === null || _b === void 0 ? void 0 : _b.name}'` : ""} with invalid definition:\n- ${validationErrors.join("\n- ")}`, LogLevel.Info);
  4912. throw new Error(`Invalid plugin definition:\n- ${validationErrors.join("\n- ")}`);
  4913. }
  4914. const events = createNanoEvents();
  4915. const { plugin: { name } } = def;
  4916. pluginQueue.set(getPluginKey(def), {
  4917. def: def,
  4918. events,
  4919. });
  4920. info(`Registered plugin: ${name}`, LogLevel.Info);
  4921. return {
  4922. info: getPluginInfo(def),
  4923. events,
  4924. };
  4925. }
  4926. //#MARKER proxy functions
  4927. /** Returns the current feature config, with sensitive values replaced by `undefined` */
  4928. function getFeaturesInterface() {
  4929. const features = getFeatures();
  4930. for (const ftKey of Object.keys(features)) {
  4931. const info = featInfo[ftKey];
  4932. if (info && info.valueHidden) // @ts-ignore
  4933. features[ftKey] = undefined;
  4934. }
  4935. return features;
  4936. }/** Options that are applied to every SelectorObserver instance */
  4937. const defaultObserverOptions = {
  4938. defaultDebounce: 100,
  4939. defaultDebounceEdge: "rising",
  4940. };
  4941. /** Global SelectorObserver instances usable throughout the script for improved performance */
  4942. const globservers = {};
  4943. /** Call after DOM load to initialize all SelectorObserver instances */
  4944. function initObservers() {
  4945. try {
  4946. //#MARKER both sites
  4947. // #SECTION body = the entire <body> element - use sparingly due to performance impacts!
  4948. globservers.body = new UserUtils.SelectorObserver(document.body, Object.assign(Object.assign({}, defaultObserverOptions), { defaultDebounce: 150, subtree: false }));
  4949. globservers.body.enable();
  4950. switch (getDomain()) {
  4951. case "ytm": {
  4952. //#MARKER YTM
  4953. //#SECTION navBar = the navigation / title bar at the top of the page
  4954. const navBarSelector = "ytmusic-nav-bar";
  4955. globservers.navBar = new UserUtils.SelectorObserver(navBarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: false }));
  4956. globservers.body.addListener(navBarSelector, {
  4957. listener: () => globservers.navBar.enable(),
  4958. });
  4959. // #SECTION mainPanel = the main content panel - includes things like the video element
  4960. const mainPanelSelector = "ytmusic-player-page #main-panel";
  4961. globservers.mainPanel = new UserUtils.SelectorObserver(mainPanelSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  4962. globservers.body.addListener(mainPanelSelector, {
  4963. listener: () => globservers.mainPanel.enable(),
  4964. });
  4965. // #SECTION sideBar = the sidebar on the left side of the page
  4966. const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
  4967. globservers.sideBar = new UserUtils.SelectorObserver(sidebarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  4968. globservers.body.addListener(sidebarSelector, {
  4969. listener: () => globservers.sideBar.enable(),
  4970. });
  4971. // #SECTION sideBarMini = the minimized sidebar on the left side of the page
  4972. const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
  4973. globservers.sideBarMini = new UserUtils.SelectorObserver(sideBarMiniSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  4974. globservers.body.addListener(sideBarMiniSelector, {
  4975. listener: () => globservers.sideBarMini.enable(),
  4976. });
  4977. // #SECTION sidePanel = the side panel on the right side of the /watch page
  4978. const sidePanelSelector = "#side-panel";
  4979. globservers.sidePanel = new UserUtils.SelectorObserver(sidePanelSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  4980. globservers.body.addListener(sidePanelSelector, {
  4981. listener: () => globservers.sidePanel.enable(),
  4982. });
  4983. // #SECTION playerBar = media controls bar at the bottom of the page
  4984. const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
  4985. globservers.playerBar = new UserUtils.SelectorObserver(playerBarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { defaultDebounce: 200 }));
  4986. globservers.body.addListener(playerBarSelector, {
  4987. listener: () => {
  4988. globservers.playerBar.enable();
  4989. },
  4990. });
  4991. // #SECTION playerBarInfo = song title, artist, album, etc. inside the player bar
  4992. const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
  4993. globservers.playerBarInfo = new UserUtils.SelectorObserver(playerBarInfoSelector, Object.assign(Object.assign({}, defaultObserverOptions), { attributes: true, attributeFilter: ["title"] }));
  4994. globservers.playerBarInfo.addListener(playerBarInfoSelector, {
  4995. listener: () => globservers.playerBarInfo.enable(),
  4996. });
  4997. // #SECTION playerBarMiddleButtons = the buttons inside the player bar (like, dislike, lyrics, etc.)
  4998. const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
  4999. globservers.playerBarMiddleButtons = new UserUtils.SelectorObserver(playerBarMiddleButtonsSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  5000. globservers.playerBar.addListener(playerBarMiddleButtonsSelector, {
  5001. listener: () => globservers.playerBarMiddleButtons.enable(),
  5002. });
  5003. // #SECTION playerBarRightControls = the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
  5004. const playerBarRightControls = ".right-controls .middle-controls-buttons";
  5005. globservers.playerBarRightControls = new UserUtils.SelectorObserver(playerBarRightControls, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  5006. globservers.playerBar.addListener(playerBarRightControls, {
  5007. listener: () => globservers.playerBarRightControls.enable(),
  5008. });
  5009. // #SECTION popupContainer = the container for popups (e.g. the queue popup)
  5010. const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
  5011. globservers.popupContainer = new UserUtils.SelectorObserver(popupContainerSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  5012. globservers.body.addListener(popupContainerSelector, {
  5013. listener: () => globservers.popupContainer.enable(),
  5014. });
  5015. break;
  5016. }
  5017. case "yt": {
  5018. //#MARKER YT
  5019. // #SECTION ytGuide = the left sidebar menu
  5020. const ytGuideSelector = "#content tp-yt-app-drawer#guide #guide-inner-content";
  5021. globservers.ytGuide = new UserUtils.SelectorObserver(ytGuideSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
  5022. globservers.body.addListener(ytGuideSelector, {
  5023. listener: () => globservers.ytGuide.enable(),
  5024. });
  5025. // // #SECTION ytMasthead = the masthead at the top of the page
  5026. // const mastheadSelector = "#content ytd-masthead#masthead";
  5027. // globservers.ytMasthead = new SelectorObserver(mastheadSelector, {
  5028. // ...defaultObserverOptions,
  5029. // subtree: true,
  5030. // });
  5031. // globservers.body.addListener(mastheadSelector, {
  5032. // listener: () => globservers.ytMasthead.enable(),
  5033. // });
  5034. }
  5035. }
  5036. //#SECTION finalize
  5037. emitInterface("bytm:observersReady");
  5038. }
  5039. catch (err) {
  5040. error("Failed to initialize observers:", err);
  5041. }
  5042. }
  5043. /**
  5044. * Interface function for adding listeners to the {@linkcode globservers}
  5045. * @param selector Relative to the observer's root element, so the selector can only start at of the root element's children at the earliest!
  5046. * @param options Options for the listener
  5047. * @template TElem The type of the element that the listener will be attached to. If set to `0`, the type HTMLElement will be used.
  5048. * @template TDomain This restricts which observers are available with the current domain
  5049. */
  5050. function addSelectorListener(observerName, selector, options) {
  5051. globservers[observerName].addListener(selector, options);
  5052. }//#MARKER video time & volume
  5053. const videoSelector = getDomain() === "ytm" ? "ytmusic-player video" : "#content ytd-player video";
  5054. /**
  5055. * Returns the current video time in seconds
  5056. * Dispatches mouse movement events in case the video time can't be read from the video or progress bar elements (needs a prior user interaction to work)
  5057. * @returns Returns null if the video time is unavailable or no user interaction has happened prior to calling in case of the fallback behavior being used
  5058. */
  5059. function getVideoTime() {
  5060. return new Promise((res) => {
  5061. const domain = getDomain();
  5062. try {
  5063. if (domain === "ytm") {
  5064. const vidElem = document.querySelector(videoSelector);
  5065. if (vidElem)
  5066. return res(Math.floor(vidElem.currentTime));
  5067. addSelectorListener("playerBar", "tp-yt-paper-slider#progress-bar tp-yt-paper-progress#sliderBar", {
  5068. listener: (pbEl) => res(!isNaN(Number(pbEl.value)) ? Math.floor(Number(pbEl.value)) : null)
  5069. });
  5070. }
  5071. else if (domain === "yt") {
  5072. const vidElem = document.querySelector(videoSelector);
  5073. if (vidElem)
  5074. return res(Math.floor(vidElem.currentTime));
  5075. // YT doesn't update the progress bar when it's hidden (contrary to YTM which never hides it)
  5076. ytForceShowVideoTime();
  5077. const pbSelector = ".ytp-chrome-bottom div.ytp-progress-bar[role=\"slider\"]";
  5078. let videoTime = -1;
  5079. const mut = new MutationObserver(() => {
  5080. // .observe() is only called when the element exists - no need to check for null
  5081. videoTime = Number(document.querySelector(pbSelector).getAttribute("aria-valuenow"));
  5082. });
  5083. const observe = (progElem) => {
  5084. mut.observe(progElem, {
  5085. attributes: true,
  5086. attributeFilter: ["aria-valuenow"],
  5087. });
  5088. if (videoTime >= 0 && !isNaN(videoTime)) {
  5089. res(Math.floor(videoTime));
  5090. mut.disconnect();
  5091. }
  5092. else
  5093. setTimeout(() => {
  5094. res(videoTime >= 0 && !isNaN(videoTime) ? Math.floor(videoTime) : null);
  5095. mut.disconnect();
  5096. }, 500);
  5097. };
  5098. addSelectorListener("body", pbSelector, { listener: observe });
  5099. }
  5100. }
  5101. catch (err) {
  5102. error("Couldn't get video time due to error:", err);
  5103. res(null);
  5104. }
  5105. });
  5106. }
  5107. /**
  5108. * Sends events that force the video controls to become visible for about 3 seconds.
  5109. * This only works once (for some reason), then the page needs to be reloaded!
  5110. */
  5111. function ytForceShowVideoTime() {
  5112. const player = document.querySelector("#movie_player");
  5113. if (!player)
  5114. return false;
  5115. const defaultProps = {
  5116. // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
  5117. view: UserUtils.getUnsafeWindow(),
  5118. bubbles: true,
  5119. cancelable: false,
  5120. };
  5121. player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
  5122. const { x, y, width, height } = player.getBoundingClientRect();
  5123. const screenY = Math.round(y + height / 2);
  5124. const screenX = x + Math.min(50, Math.round(width / 3));
  5125. player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
  5126. screenX, movementX: 5, movementY: 0 })));
  5127. return true;
  5128. }
  5129. /** Waits for the video element to be in its readyState 4 / canplay state and returns it */
  5130. function waitVideoElementReady() {
  5131. return new Promise((res) => {
  5132. addSelectorListener("body", videoSelector, {
  5133. listener: (vidElem) => __awaiter(this, void 0, void 0, function* () {
  5134. if (vidElem) {
  5135. // this is just after YT has finished doing their own shenanigans with the video time and volume
  5136. if (vidElem.readyState === 4)
  5137. res(vidElem);
  5138. else
  5139. vidElem.addEventListener("canplay", () => res(vidElem), { once: true });
  5140. }
  5141. }),
  5142. });
  5143. });
  5144. }
  5145. //#MARKER other
  5146. /** Whether the DOM has finished loading and elements can be added or modified */
  5147. let domLoaded = false;
  5148. document.addEventListener("DOMContentLoaded", () => domLoaded = true);
  5149. /** Removes all child nodes of an element without invoking the slow-ish HTML parser */
  5150. function clearInner(element) {
  5151. while (element.hasChildNodes())
  5152. clearNode(element.firstChild);
  5153. }
  5154. function clearNode(element) {
  5155. while (element.hasChildNodes())
  5156. clearNode(element.firstChild);
  5157. element.parentNode.removeChild(element);
  5158. }
  5159. /**
  5160. * Adds generic, accessible interaction listeners to the passed element.
  5161. * All listeners have the default behavior prevented and stop immediate propagation.
  5162. * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
  5163. */
  5164. function onInteraction(elem, listener, listenerOptions) {
  5165. const proxListener = (e) => {
  5166. if (e instanceof KeyboardEvent && !(["Enter", " ", "Space", "Spacebar"].includes(e.key)))
  5167. return;
  5168. e.preventDefault();
  5169. e.stopImmediatePropagation();
  5170. (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.once) && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOptions);
  5171. (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.once) && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOptions);
  5172. listener(e);
  5173. };
  5174. elem.addEventListener("click", proxListener, listenerOptions);
  5175. elem.addEventListener("keydown", proxListener, listenerOptions);
  5176. }let curLogLevel = LogLevel.Info;
  5177. /** Common prefix to be able to tell logged messages apart and filter them in devtools */
  5178. const consPrefix = `[${scriptInfo.name}]`;
  5179. `[${scriptInfo.name}/#DEBUG]`;
  5180. /** Sets the current log level. 0 = Debug, 1 = Info */
  5181. function setLogLevel(level) {
  5182. curLogLevel = level;
  5183. setGlobalProp("logLevel", level);
  5184. if (curLogLevel !== level)
  5185. log("Set the log level to", LogLevel[level]);
  5186. }
  5187. /** Extracts the log level from the last item from spread arguments - returns 0 if the last item is not a number or too low or high */
  5188. function getLogLevel(args) {
  5189. const minLogLvl = 0, maxLogLvl = 1;
  5190. if (typeof args.at(-1) === "number")
  5191. return UserUtils.clamp(args.splice(args.length - 1)[0], minLogLvl, maxLogLvl);
  5192. return LogLevel.Debug;
  5193. }
  5194. /**
  5195. * Logs all passed values to the console, as long as the log level is sufficient.
  5196. * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
  5197. */
  5198. function log(...args) {
  5199. if (curLogLevel <= getLogLevel(args))
  5200. console.log(consPrefix, ...args);
  5201. }
  5202. /**
  5203. * Logs all passed values to the console as info, as long as the log level is sufficient.
  5204. * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
  5205. */
  5206. function info(...args) {
  5207. if (curLogLevel <= getLogLevel(args))
  5208. console.info(consPrefix, ...args);
  5209. }
  5210. /** Logs all passed values to the console as a warning, no matter the log level. */
  5211. function warn(...args) {
  5212. console.warn(consPrefix, ...args);
  5213. }
  5214. /** Logs all passed values to the console as an error, no matter the log level. */
  5215. function error(...args) {
  5216. console.error(consPrefix, ...args);
  5217. }//#SECTION misc
  5218. /**
  5219. * Returns the current domain as a constant string representation
  5220. * @throws Throws if script runs on an unexpected website
  5221. */
  5222. function getDomain() {
  5223. if (location.hostname.match(/^music\.youtube/))
  5224. return "ytm";
  5225. else if (location.hostname.match(/youtube\./))
  5226. return "yt";
  5227. else
  5228. throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
  5229. }
  5230. /** Returns a pseudo-random ID unique to each session - returns null if sessionStorage is unavailable */
  5231. function getSessionId() {
  5232. try {
  5233. let sesId = window.sessionStorage.getItem("_bytm-session-id");
  5234. if (!sesId)
  5235. window.sessionStorage.setItem("_bytm-session-id", sesId = UserUtils.randomId(8, 36));
  5236. return sesId;
  5237. }
  5238. catch (err) {
  5239. warn("Couldn't get session ID, sessionStorage / cookies might be disabled:", err);
  5240. return null;
  5241. }
  5242. }
  5243. let isCompressionSupported;
  5244. /** Tests whether compression via the predefined {@linkcode compressionFormat} is supported */
  5245. function compressionSupported() {
  5246. return __awaiter(this, void 0, void 0, function* () {
  5247. if (typeof isCompressionSupported === "boolean")
  5248. return isCompressionSupported;
  5249. try {
  5250. yield UserUtils.compress(".", compressionFormat, "string");
  5251. return isCompressionSupported = true;
  5252. }
  5253. catch (e) {
  5254. return isCompressionSupported = false;
  5255. }
  5256. });
  5257. }
  5258. /** Returns a string with the given array's items separated by a default separator (`", "` by default), with an optional different separator for the last item */
  5259. function arrayWithSeparators(array, separator = ", ", lastSeparator) {
  5260. const arr = [...array];
  5261. if (!lastSeparator)
  5262. lastSeparator = separator;
  5263. if (arr.length === 0)
  5264. return "";
  5265. else if (arr.length <= 2)
  5266. return arr.join(lastSeparator);
  5267. else
  5268. return `${arr.slice(0, -1).join(separator)}${lastSeparator}${arr.at(-1)}`;
  5269. }
  5270. /** Returns the watch ID of the current video or null if not on a video page */
  5271. function getWatchId() {
  5272. const { searchParams, pathname } = new URL(location.href);
  5273. return pathname.includes("/watch") ? searchParams.get("v") : null;
  5274. }
  5275. /** Returns the thumbnail URL for a video with either a given quality identifier or index */
  5276. function getThumbnailUrl(watchId, qualityOrIndex = "hqdefault") {
  5277. return `https://i.ytimg.com/vi/${watchId}/${qualityOrIndex}.jpg`;
  5278. }
  5279. /** Returns the best available thumbnail URL for a video with the given watch ID */
  5280. function getBestThumbnailUrl(watchId) {
  5281. return __awaiter(this, void 0, void 0, function* () {
  5282. const priorityList = ["maxresdefault", "sddefault", 0];
  5283. for (const quality of priorityList) {
  5284. let response;
  5285. const url = getThumbnailUrl(watchId, quality);
  5286. try {
  5287. response = yield UserUtils.fetchAdvanced(url, { method: "HEAD", timeout: 5000 });
  5288. }
  5289. catch (e) {
  5290. }
  5291. if (response === null || response === void 0 ? void 0 : response.ok)
  5292. return url;
  5293. }
  5294. });
  5295. }
  5296. /** Copies a JSON-serializable object */
  5297. function reserialize(data) {
  5298. return JSON.parse(JSON.stringify(data));
  5299. }
  5300. //#SECTION resources
  5301. /**
  5302. * Returns the URL of a resource by its name, as defined in `assets/resources.json`, from GM resource cache - [see GM.getResourceUrl docs](https://wiki.greasespot.net/GM.getResourceUrl)
  5303. * Falls back to a `raw.githubusercontent.com` URL or base64-encoded data URI if the resource is not available in the GM resource cache
  5304. */
  5305. function getResourceUrl(name) {
  5306. var _a;
  5307. return __awaiter(this, void 0, void 0, function* () {
  5308. let url = yield GM.getResourceUrl(name);
  5309. if (!url || url.length === 0) {
  5310. const resource = (_a = GM.info.script.resources) === null || _a === void 0 ? void 0 : _a[name].url;
  5311. if (typeof resource === "string") {
  5312. const resourceUrl = new URL(resource);
  5313. const resourcePath = resourceUrl.pathname;
  5314. if (resourcePath)
  5315. return `https://raw.githubusercontent.com/${repo}/${branch}${resourcePath}`;
  5316. }
  5317. warn(`Couldn't get blob URL nor external URL for @resource '${name}', trying to use base64-encoded fallback`);
  5318. // @ts-ignore
  5319. url = yield GM.getResourceUrl(name, false);
  5320. }
  5321. return url;
  5322. });
  5323. }
  5324. /**
  5325. * Returns the preferred locale of the user, provided it is supported by the userscript.
  5326. * Prioritizes `navigator.language`, then `navigator.languages`, then `"en_US"` as a fallback.
  5327. */
  5328. function getPreferredLocale() {
  5329. var _a;
  5330. const navLang = navigator.language.replace(/-/g, "_");
  5331. const navLangs = navigator.languages
  5332. .filter(lang => lang.match(/^[a-z]{2}(-|_)[A-Z]$/) !== null)
  5333. .map(lang => lang.replace(/-/g, "_"));
  5334. if (Object.entries(locales).find(([key]) => key === navLang))
  5335. return navLang;
  5336. for (const loc of navLangs) {
  5337. if (Object.entries(locales).find(([key]) => key === loc))
  5338. return loc;
  5339. }
  5340. // if navigator.languages has entries that aren't locale codes in the format xx_XX
  5341. if (navigator.languages.some(lang => lang.match(/^[a-z]{2}$/))) {
  5342. for (const lang of navLangs) {
  5343. const foundLoc = (_a = Object.entries(locales).find(([key]) => key.startsWith(lang))) === null || _a === void 0 ? void 0 : _a[0];
  5344. if (foundLoc)
  5345. return foundLoc;
  5346. }
  5347. }
  5348. return "en_US";
  5349. }
  5350. /** Returns the content behind the passed resource identifier to be assigned to an element's innerHTML property */
  5351. function resourceToHTMLString(resource) {
  5352. return __awaiter(this, void 0, void 0, function* () {
  5353. try {
  5354. const resourceUrl = yield getResourceUrl(resource);
  5355. if (!resourceUrl)
  5356. throw new Error(`Couldn't find URL for resource '${resource}'`);
  5357. return yield (yield UserUtils.fetchAdvanced(resourceUrl)).text();
  5358. }
  5359. catch (err) {
  5360. error("Couldn't get SVG element from resource:", err);
  5361. return null;
  5362. }
  5363. });
  5364. }
  5365. /** Parses a markdown string using marked and turns it into an HTML string with default settings - doesn't sanitize against XSS! */
  5366. function parseMarkdown(mdString) {
  5367. return marked.marked.parse(mdString, {
  5368. async: true,
  5369. gfm: true,
  5370. });
  5371. }
  5372. /** Returns the content of the changelog markdown file */
  5373. function getChangelogMd() {
  5374. return __awaiter(this, void 0, void 0, function* () {
  5375. return yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("doc-changelog"))).text();
  5376. });
  5377. }
  5378. /** Returns the changelog as HTML with a details element for each version */
  5379. function getChangelogHtmlWithDetails() {
  5380. return __awaiter(this, void 0, void 0, function* () {
  5381. try {
  5382. const changelogMd = yield getChangelogMd();
  5383. let changelogHtml = yield parseMarkdown(changelogMd);
  5384. const getVerId = (verStr) => verStr.trim().replace(/[._#\s-]/g, "");
  5385. changelogHtml = changelogHtml.replace(/<div\s+class="split">\s*<\/div>\s*\n?\s*<br(\s\/)?>/gm, "</details>\n<br>\n<details class=\"bytm-changelog-version-details\">");
  5386. const h2Matches = Array.from(changelogHtml.matchAll(/<h2(\s+id=".+")?>([\d\w\s.]+)<\/h2>/gm));
  5387. for (const match of h2Matches) {
  5388. const [fullMatch, , verStr] = match;
  5389. const verId = getVerId(verStr);
  5390. const h2Elem = `<h2 id="${verId}" role="subheading" aria-level="1">Version ${verStr}</h2>`;
  5391. const summaryElem = `<summary tab-index="0">${h2Elem}</summary>`;
  5392. changelogHtml = changelogHtml.replace(fullMatch, `${summaryElem}`);
  5393. }
  5394. changelogHtml = `<details class="bytm-changelog-version-details">${changelogHtml}</details>`;
  5395. return changelogHtml;
  5396. }
  5397. catch (err) {
  5398. return `Error while preparing changelog: ${err}`;
  5399. }
  5400. });
  5401. }/**
  5402. * Constructs a URL from a base URL and a record of query parameters.
  5403. * If a value is null, the parameter will be valueless.
  5404. * All values will be stringified using their `toString()` method and then URI-encoded.
  5405. * @returns Returns a string instead of a URL object
  5406. */
  5407. function constructUrlString(baseUrl, params) {
  5408. return `${baseUrl}?${Object.entries(params)
  5409. .filter(([, v]) => v !== undefined)
  5410. .map(([key, val]) => `${key}${val === null ? "" : `=${encodeURIComponent(String(val))}`}`)
  5411. .join("&")}`;
  5412. }
  5413. /**
  5414. * Sends a request with the specified parameters and returns the response as a Promise.
  5415. * Ignores the CORS policy, contrary to fetch and fetchAdvanced.
  5416. */
  5417. function sendRequest(details) {
  5418. return new Promise((resolve, reject) => {
  5419. GM.xmlHttpRequest(Object.assign(Object.assign({ timeout: 10000 }, details), { onload: resolve, onerror: reject, ontimeout: reject, onabort: reject }));
  5420. });
  5421. }//#MARKER menu
  5422. let isWelcomeMenuOpen = false;
  5423. /** Adds the welcome menu to the DOM */
  5424. function addWelcomeMenu() {
  5425. return __awaiter(this, void 0, void 0, function* () {
  5426. //#SECTION backdrop & menu container
  5427. const backgroundElem = document.createElement("div");
  5428. backgroundElem.id = "bytm-welcome-menu-bg";
  5429. backgroundElem.classList.add("bytm-menu-bg");
  5430. backgroundElem.style.visibility = "hidden";
  5431. backgroundElem.style.display = "none";
  5432. const menuContainer = document.createElement("div");
  5433. menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
  5434. menuContainer.classList.add("bytm-menu");
  5435. menuContainer.id = "bytm-welcome-menu";
  5436. //#SECTION title bar
  5437. const headerElem = document.createElement("div");
  5438. headerElem.classList.add("bytm-menu-header");
  5439. const titleWrapperElem = document.createElement("div");
  5440. titleWrapperElem.id = "bytm-welcome-menu-title-wrapper";
  5441. const titleLogoElem = document.createElement("img");
  5442. titleLogoElem.id = "bytm-welcome-menu-title-logo";
  5443. titleLogoElem.classList.add("bytm-no-select");
  5444. titleLogoElem.src = yield getResourceUrl("img-logo");
  5445. const titleElem = document.createElement("h2");
  5446. titleElem.id = "bytm-welcome-menu-title";
  5447. titleElem.className = "bytm-menu-title";
  5448. titleElem.role = "heading";
  5449. titleElem.ariaLevel = "1";
  5450. titleWrapperElem.appendChild(titleLogoElem);
  5451. titleWrapperElem.appendChild(titleElem);
  5452. headerElem.appendChild(titleWrapperElem);
  5453. //#SECTION footer
  5454. const footerCont = document.createElement("div");
  5455. footerCont.id = "bytm-welcome-menu-footer-cont";
  5456. footerCont.className = "bytm-menu-footer-cont";
  5457. const openCfgElem = document.createElement("button");
  5458. openCfgElem.id = "bytm-welcome-menu-open-cfg";
  5459. openCfgElem.classList.add("bytm-btn");
  5460. openCfgElem.addEventListener("click", () => {
  5461. closeWelcomeMenu();
  5462. openCfgMenu();
  5463. });
  5464. const openChangelogElem = document.createElement("button");
  5465. openChangelogElem.id = "bytm-welcome-menu-open-changelog";
  5466. openChangelogElem.classList.add("bytm-btn");
  5467. openChangelogElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  5468. yield openChangelogMenu("exit");
  5469. closeWelcomeMenu();
  5470. }));
  5471. const closeBtnElem = document.createElement("button");
  5472. closeBtnElem.id = "bytm-welcome-menu-footer-close";
  5473. closeBtnElem.classList.add("bytm-btn");
  5474. closeBtnElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  5475. closeWelcomeMenu();
  5476. }));
  5477. const leftButtonsCont = document.createElement("div");
  5478. leftButtonsCont.id = "bytm-menu-footer-left-buttons-cont";
  5479. leftButtonsCont.appendChild(openCfgElem);
  5480. leftButtonsCont.appendChild(openChangelogElem);
  5481. footerCont.appendChild(leftButtonsCont);
  5482. footerCont.appendChild(closeBtnElem);
  5483. //#SECTION content
  5484. const contentWrapper = document.createElement("div");
  5485. contentWrapper.id = "bytm-welcome-menu-content-wrapper";
  5486. // locale switcher
  5487. const localeCont = document.createElement("div");
  5488. localeCont.id = "bytm-welcome-menu-locale-cont";
  5489. const localeImg = document.createElement("img");
  5490. localeImg.id = "bytm-welcome-menu-locale-img";
  5491. localeImg.classList.add("bytm-no-select");
  5492. localeImg.src = yield getResourceUrl("icon-globe");
  5493. const localeSelectElem = document.createElement("select");
  5494. localeSelectElem.id = "bytm-welcome-menu-locale-select";
  5495. for (const [locale, { name }] of Object.entries(locales)) {
  5496. const localeOptionElem = document.createElement("option");
  5497. localeOptionElem.value = locale;
  5498. localeOptionElem.textContent = name;
  5499. localeSelectElem.appendChild(localeOptionElem);
  5500. }
  5501. localeSelectElem.value = getFeatures().locale;
  5502. localeSelectElem.addEventListener("change", () => __awaiter(this, void 0, void 0, function* () {
  5503. const selectedLocale = localeSelectElem.value;
  5504. const feats = Object.assign({}, getFeatures());
  5505. feats.locale = selectedLocale;
  5506. setFeatures(feats);
  5507. yield initTranslations(selectedLocale);
  5508. setLocale(selectedLocale);
  5509. retranslateWelcomeMenu();
  5510. }));
  5511. localeCont.appendChild(localeImg);
  5512. localeCont.appendChild(localeSelectElem);
  5513. contentWrapper.appendChild(localeCont);
  5514. // text
  5515. const textCont = document.createElement("div");
  5516. textCont.id = "bytm-welcome-menu-text-cont";
  5517. const textElem = document.createElement("p");
  5518. textElem.id = "bytm-welcome-menu-text";
  5519. const textElems = [];
  5520. const line1Elem = document.createElement("span");
  5521. line1Elem.id = "bytm-welcome-text-line1";
  5522. textElems.push(line1Elem);
  5523. const br1Elem = document.createElement("br");
  5524. textElems.push(br1Elem);
  5525. const line2Elem = document.createElement("span");
  5526. line2Elem.id = "bytm-welcome-text-line2";
  5527. textElems.push(line2Elem);
  5528. const br2Elem = document.createElement("br");
  5529. textElems.push(br2Elem);
  5530. const br3Elem = document.createElement("br");
  5531. textElems.push(br3Elem);
  5532. const line3Elem = document.createElement("span");
  5533. line3Elem.id = "bytm-welcome-text-line3";
  5534. textElems.push(line3Elem);
  5535. const br4Elem = document.createElement("br");
  5536. textElems.push(br4Elem);
  5537. const line4Elem = document.createElement("span");
  5538. line4Elem.id = "bytm-welcome-text-line4";
  5539. textElems.push(line4Elem);
  5540. const br5Elem = document.createElement("br");
  5541. textElems.push(br5Elem);
  5542. const br6Elem = document.createElement("br");
  5543. textElems.push(br6Elem);
  5544. const line5Elem = document.createElement("span");
  5545. line5Elem.id = "bytm-welcome-text-line5";
  5546. textElems.push(line5Elem);
  5547. textElems.forEach((elem) => textElem.appendChild(elem));
  5548. textCont.appendChild(textElem);
  5549. contentWrapper.appendChild(textCont);
  5550. //#SECTION finalize
  5551. menuContainer.appendChild(headerElem);
  5552. menuContainer.appendChild(contentWrapper);
  5553. menuContainer.appendChild(footerCont);
  5554. backgroundElem.appendChild(menuContainer);
  5555. document.body.appendChild(backgroundElem);
  5556. retranslateWelcomeMenu();
  5557. });
  5558. }
  5559. //#MARKER (re-)translate
  5560. /** Retranslates all elements inside the welcome menu */
  5561. function retranslateWelcomeMenu() {
  5562. const getLink = (href) => {
  5563. return [`<a href="${href}" class="bytm-link" target="_blank" rel="noopener noreferrer">`, "</a>"];
  5564. };
  5565. const changes = {
  5566. "#bytm-welcome-menu-title": (e) => e.textContent = t("welcome_menu_title", scriptInfo.name),
  5567. "#bytm-welcome-menu-title-close": (e) => e.ariaLabel = e.title = t("close_menu_tooltip"),
  5568. "#bytm-welcome-menu-open-cfg": (e) => {
  5569. e.textContent = t("config_menu");
  5570. e.ariaLabel = e.title = t("open_config_menu_tooltip");
  5571. },
  5572. "#bytm-welcome-menu-open-changelog": (e) => {
  5573. e.textContent = t("open_changelog");
  5574. e.ariaLabel = e.title = t("open_changelog_tooltip");
  5575. },
  5576. "#bytm-welcome-menu-footer-close": (e) => {
  5577. e.textContent = t("close");
  5578. e.ariaLabel = e.title = t("close_menu_tooltip");
  5579. },
  5580. "#bytm-welcome-text-line1": (e) => e.innerHTML = t("welcome_text_line_1"),
  5581. "#bytm-welcome-text-line2": (e) => e.innerHTML = t("welcome_text_line_2", scriptInfo.name),
  5582. "#bytm-welcome-text-line3": (e) => e.innerHTML = t("welcome_text_line_3", scriptInfo.name, ...getLink(`${pkg.hosts.greasyfork}/feedback`), ...getLink(pkg.hosts.openuserjs)),
  5583. "#bytm-welcome-text-line4": (e) => e.innerHTML = t("welcome_text_line_4", ...getLink(pkg.funding.url)),
  5584. "#bytm-welcome-text-line5": (e) => e.innerHTML = t("welcome_text_line_5", ...getLink(pkg.bugs.url)),
  5585. };
  5586. for (const [selector, fn] of Object.entries(changes)) {
  5587. const el = document.querySelector(selector);
  5588. if (!el) {
  5589. warn(`Couldn't find element in welcome menu with selector '${selector}'`);
  5590. continue;
  5591. }
  5592. fn(el);
  5593. }
  5594. }
  5595. /** Closes the welcome menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  5596. function closeWelcomeMenu(evt) {
  5597. var _a;
  5598. if (!isWelcomeMenuOpen)
  5599. return;
  5600. isWelcomeMenuOpen = false;
  5601. (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
  5602. document.body.classList.remove("bytm-disable-scroll");
  5603. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
  5604. const menuBg = document.querySelector("#bytm-welcome-menu-bg");
  5605. siteEvents.emit("welcomeMenuClosed");
  5606. if (!menuBg)
  5607. return warn("Couldn't find welcome menu background element");
  5608. menuBg.style.visibility = "hidden";
  5609. menuBg.style.display = "none";
  5610. }
  5611. //#MARKER open, show & close
  5612. /** Opens the welcome menu if it is closed */
  5613. function openWelcomeMenu() {
  5614. var _a;
  5615. if (isWelcomeMenuOpen)
  5616. return;
  5617. isWelcomeMenuOpen = true;
  5618. document.body.classList.add("bytm-disable-scroll");
  5619. (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
  5620. const menuBg = document.querySelector("#bytm-welcome-menu-bg");
  5621. if (!menuBg)
  5622. return warn("Couldn't find welcome menu background element");
  5623. menuBg.style.visibility = "visible";
  5624. menuBg.style.display = "block";
  5625. }
  5626. /** Shows the welcome menu and returns a promise that resolves when the menu is closed */
  5627. function showWelcomeMenu() {
  5628. return new Promise((resolve) => {
  5629. const unsub = siteEvents.on("welcomeMenuClosed", () => {
  5630. unsub();
  5631. resolve();
  5632. });
  5633. openWelcomeMenu();
  5634. });
  5635. }{
  5636. // console watermark with sexy gradient
  5637. const styleGradient = "background: rgba(165, 38, 38, 1); background: linear-gradient(90deg, rgb(154, 31, 103) 0%, rgb(135, 31, 31) 40%, rgb(184, 64, 41) 100%);";
  5638. const styleCommon = "color: #fff; font-size: 1.5em; padding-left: 6px; padding-right: 6px;";
  5639. console.log();
  5640. console.log(`%c${scriptInfo.name}%cv${scriptInfo.version}%c\n\nBuild #${buildNumber} ─ ${scriptInfo.namespace}`, `font-weight: bold; ${styleCommon} ${styleGradient}`, `background-color: #333; ${styleCommon}`, "padding: initial;");
  5641. console.log([
  5642. "Powered by:",
  5643. "─ Lots of ambition and dedication",
  5644. "─ My song metadata API: https://api.sv443.net/geniurl",
  5645. "─ My userscript utility library: https://github.com/Sv443-Network/UserUtils",
  5646. "─ The fuse.js library: https://github.com/krisk/Fuse",
  5647. "─ This markdown parser library: https://github.com/markedjs/marked",
  5648. "─ This tiny event listener library: https://github.com/ai/nanoevents",
  5649. ].join("\n"));
  5650. console.log();
  5651. }
  5652. const domain = getDomain();
  5653. /** Stuff that needs to be called ASAP, before anything async happens */
  5654. function preInit() {
  5655. try {
  5656. log("Session ID:", getSessionId());
  5657. initInterface();
  5658. setLogLevel(defaultLogLevel);
  5659. if (domain === "ytm")
  5660. initBeforeUnloadHook();
  5661. init();
  5662. }
  5663. catch (err) {
  5664. return error("Fatal pre-init error:", err);
  5665. }
  5666. }
  5667. function init() {
  5668. var _a, _b;
  5669. return __awaiter(this, void 0, void 0, function* () {
  5670. try {
  5671. const features = yield initConfig();
  5672. setLogLevel(features.logLevel);
  5673. yield initLyricsCache();
  5674. yield initTranslations((_a = features.locale) !== null && _a !== void 0 ? _a : "en_US");
  5675. setLocale((_b = features.locale) !== null && _b !== void 0 ? _b : "en_US");
  5676. emitInterface("bytm:initPlugins");
  5677. if (features.disableBeforeUnloadPopup && domain === "ytm")
  5678. disableBeforeUnload();
  5679. if (!domLoaded)
  5680. document.addEventListener("DOMContentLoaded", onDomLoad, { once: true });
  5681. else
  5682. onDomLoad();
  5683. if (features.rememberSongTime)
  5684. initRememberSongTime();
  5685. }
  5686. catch (err) {
  5687. error("Fatal error:", err);
  5688. }
  5689. // init menu separately from features
  5690. try {
  5691. void "TODO(v1.2):";
  5692. // initMenu();
  5693. }
  5694. catch (err) {
  5695. error("Error while initializing menu:", err);
  5696. }
  5697. });
  5698. }
  5699. /** Called when the DOM has finished loading and can be queried and altered by the userscript */
  5700. function onDomLoad() {
  5701. return __awaiter(this, void 0, void 0, function* () {
  5702. const features = getFeatures();
  5703. const ftInit = [];
  5704. try {
  5705. insertGlobalStyle();
  5706. initObservers();
  5707. yield initVersionCheck();
  5708. }
  5709. catch (err) {
  5710. error("Fatal error in feature pre-init:", err);
  5711. return;
  5712. }
  5713. log(`DOM loaded and feature pre-init finished, now initializing all features for domain "${domain}"...`);
  5714. try {
  5715. if (domain === "ytm") {
  5716. disableDarkReader();
  5717. ftInit.push(initSiteEvents());
  5718. if (typeof (yield GM.getValue("bytm-installed")) !== "string") {
  5719. // open welcome menu with language selector
  5720. yield addWelcomeMenu();
  5721. info("Showing welcome menu");
  5722. yield showWelcomeMenu();
  5723. yield GM.setValue("bytm-installed", JSON.stringify({ timestamp: Date.now(), version: scriptInfo.version }));
  5724. }
  5725. addSelectorListener("body", "tp-yt-iron-dropdown #contentWrapper ytd-multi-page-menu-renderer #container.menu-container", {
  5726. listener: addConfigMenuOptionYTM,
  5727. });
  5728. if (features.arrowKeySupport)
  5729. ftInit.push(initArrowKeySkip());
  5730. if (features.removeUpgradeTab)
  5731. ftInit.push(removeUpgradeTab());
  5732. if (features.watermarkEnabled)
  5733. ftInit.push(addWatermark());
  5734. if (features.geniusLyrics)
  5735. ftInit.push(addMediaCtrlLyricsBtn());
  5736. if (features.deleteFromQueueButton || features.lyricsQueueButton)
  5737. ftInit.push(initQueueButtons());
  5738. if (features.anchorImprovements)
  5739. ftInit.push(addAnchorImprovements());
  5740. if (features.closeToastsTimeout > 0)
  5741. ftInit.push(initAutoCloseToasts());
  5742. if (features.numKeysSkipToTime)
  5743. ftInit.push(initNumKeysSkip());
  5744. if (features.fixSpacing)
  5745. ftInit.push(fixSpacing());
  5746. if (features.scrollToActiveSongBtn)
  5747. ftInit.push(addScrollToActiveBtn());
  5748. ftInit.push(initThumbnailOverlay());
  5749. if (features.hideCursorOnIdle)
  5750. ftInit.push(initHideCursorOnIdle());
  5751. ftInit.push(initVolumeFeatures());
  5752. }
  5753. if (domain === "yt") {
  5754. addSelectorListener("ytGuide", "#sections ytd-guide-section-renderer:nth-child(5) #items ytd-guide-entry-renderer:nth-child(1)", {
  5755. listener: (el) => el.parentElement && addConfigMenuOptionYT(el.parentElement),
  5756. });
  5757. }
  5758. if (["ytm", "yt"].includes(domain)) {
  5759. if (features.switchBetweenSites)
  5760. ftInit.push(initSiteSwitch(domain));
  5761. if (features.removeShareTrackingParamSites && (features.removeShareTrackingParamSites === domain || features.removeShareTrackingParamSites === "all"))
  5762. ftInit.push(initRemShareTrackParam());
  5763. // TODO: for hot reloading features
  5764. // ftInit.push(new Promise((resolve) => {
  5765. // for(const [k, v] of Object.entries(featInfo)) {
  5766. // try {
  5767. // const featVal = features[k as keyof typeof featInfo];
  5768. // // @ts-ignore
  5769. // if(v.enable && featVal === true) {
  5770. // console.log("###> enable", k);
  5771. // // @ts-ignore
  5772. // v.enable(features);
  5773. // console.log("###>> enable ok");
  5774. // }
  5775. // // @ts-ignore
  5776. // else if(v.disable && featVal === false) {
  5777. // console.log("###> disable", k);
  5778. // // @ts-ignore
  5779. // v.disable(features);
  5780. // console.log("###>> disable ok");
  5781. // }
  5782. // }
  5783. // catch(err) {
  5784. // error(`Couldn't initialize feature "${k}" due to error:`, err);
  5785. // }
  5786. // }
  5787. // console.log("###>>> done for loop");
  5788. // resolve();
  5789. // }));
  5790. }
  5791. yield Promise.allSettled(ftInit);
  5792. emitInterface("bytm:ready");
  5793. try {
  5794. initPlugins();
  5795. }
  5796. catch (err) {
  5797. error("Plugin loading error:", err);
  5798. }
  5799. try {
  5800. registerDevMenuCommands();
  5801. }
  5802. catch (e) {
  5803. warn("Couldn't register dev menu commands:", e);
  5804. }
  5805. }
  5806. catch (err) {
  5807. error("Feature error:", err);
  5808. }
  5809. });
  5810. }
  5811. // TODO(v1.2):
  5812. // async function initFeatures() {
  5813. // const ftInit = [] as Promise<void>[];
  5814. // log(`DOM loaded. Initializing features for domain "${domain}"...`);
  5815. // for(const [ftKey, ftInfo] of Object.entries(featInfo)) {
  5816. // try {
  5817. // // @ts-ignore
  5818. // const res = ftInfo?.enable?.() as undefined | Promise<void>;
  5819. // if(res instanceof Promise)
  5820. // ftInit.push(res);
  5821. // else
  5822. // ftInit.push(Promise.resolve());
  5823. // }
  5824. // catch(err) {
  5825. // error(`Couldn't initialize feature "${ftKey}" due to error:`, err);
  5826. // }
  5827. // }
  5828. // siteEvents.on("configOptionChanged", (ftKey, oldValue, newValue) => {
  5829. // try {
  5830. // // @ts-ignore
  5831. // if(featInfo[ftKey].change) {
  5832. // // @ts-ignore
  5833. // featInfo[ftKey].change(oldValue, newValue);
  5834. // }
  5835. // // @ts-ignore
  5836. // else if(featInfo[ftKey].disable) {
  5837. // // @ts-ignore
  5838. // const disableRes = featInfo[ftKey].disable();
  5839. // if(disableRes instanceof Promise) // @ts-ignore
  5840. // disableRes.then(() => featInfo[ftKey]?.enable?.());
  5841. // else // @ts-ignore
  5842. // featInfo[ftKey]?.enable?.();
  5843. // }
  5844. // else {
  5845. // // TODO: set "page reload required" flag in new menu
  5846. // if(confirm("[Work in progress]\nYou changed an option that requires a page reload to be applied.\nReload the page now?")) {
  5847. // disableBeforeUnload();
  5848. // location.reload();
  5849. // }
  5850. // }
  5851. // }
  5852. // catch(err) {
  5853. // error(`Couldn't change feature "${ftKey}" due to error:`, err);
  5854. // }
  5855. // });
  5856. // Promise.all(ftInit).then(() => {
  5857. // emitInterface("bytm:ready");
  5858. // });
  5859. // }
  5860. /** Inserts the bundled CSS files imported throughout the script into a <style> element in the <head> */
  5861. function insertGlobalStyle() {
  5862. // post-build these double quotes are replaced by backticks (because if backticks are used here, the bundler converts them to double quotes)
  5863. UserUtils.addGlobalStyle(`:root {
  5864. --bytm-dialog-accent-col: #3683d4;
  5865. --bytm-advanced-mode-color: #c5a73b;
  5866. --bytm-experimental-col: #d07ff0;
  5867. --bytm-warning-col: #ff5233;
  5868. }
  5869. /* TODO(v1.2): leave only dialog */
  5870. #bytm-cfg-dialog-bg,
  5871. #bytm-cfg-menu-bg
  5872. {
  5873. --bytm-dialog-height-max: 800px;
  5874. --bytm-dialog-width-max: 1150px;
  5875. --bytm-menu-height-max: 800px;
  5876. --bytm-menu-width-max: 1150px;
  5877. }
  5878. #bytm-changelog-dialog-bg,
  5879. #bytm-changelog-menu-bg
  5880. {
  5881. --bytm-dialog-height-max: 800px;
  5882. --bytm-dialog-width-max: 900px;
  5883. --bytm-menu-height-max: 800px;
  5884. --bytm-menu-width-max: 900px;
  5885. }
  5886. #bytm-export-dialog-bg, #bytm-import-dialog-bg,
  5887. #bytm-export-menu-bg, #bytm-import-menu-bg
  5888. {
  5889. --bytm-dialog-height-max: 500px;
  5890. --bytm-dialog-width-max: 600px;
  5891. --bytm-menu-height-max: 500px;
  5892. --bytm-menu-width-max: 600px;
  5893. }
  5894. #bytm-feat-help-dialog-bg,
  5895. #bytm-feat-help-menu-bg
  5896. {
  5897. --bytm-dialog-height-max: 400px;
  5898. --bytm-dialog-width-max: 600px;
  5899. --bytm-menu-height-max: 400px;
  5900. --bytm-menu-width-max: 600px;
  5901. }
  5902. .bytm-dialog-body p {
  5903. overflow-wrap: break-word;
  5904. }
  5905. .bytm-dialog-body details summary {
  5906. cursor: pointer;
  5907. font-style: italic;
  5908. }
  5909. #bytm-version-notif-dialog-btns {
  5910. display: flex;
  5911. flex-direction: row;
  5912. align-items: center;
  5913. justify-content: space-between;
  5914. margin-top: 20px;
  5915. }
  5916. #bytm-disable-update-check-wrapper {
  5917. display: flex;
  5918. align-items: center;
  5919. font-size: 1.5rem;
  5920. margin-top: 35px;
  5921. }
  5922. #bytm-disable-update-check-wrapper label {
  5923. padding-left: 12px;
  5924. }
  5925. #bytm-version-notif-changelog-cont {
  5926. max-height: calc(max(var(--calc-max-height) - 280px, 0px));
  5927. overflow-y: auto;
  5928. margin: 10px 0px;
  5929. }
  5930. #bytm-version-notif-changelog-details {
  5931. margin-top: 15px;
  5932. }
  5933. .bytm-disable-update-check-toggle-label-wrapper {
  5934. display: flex;
  5935. flex-direction: column;
  5936. justify-content: flex-start;
  5937. }
  5938. .bytm-secondary-label {
  5939. padding-left: 12px;
  5940. font-size: 1.3rem;
  5941. }
  5942. .bytm-adorn-icon {
  5943. display: inline-flex;
  5944. align-items: center;
  5945. cursor: help;
  5946. }
  5947. .bytm-ftconf-adv-copy-btn {
  5948. margin: 0px 10px;
  5949. }
  5950. .bytm-ftitem-adornment svg path {
  5951. fill: var(--bytm-dialog-accent-col, #fff);
  5952. }
  5953. .bytm-advanced-mode-icon svg path {
  5954. fill: var(--bytm-advanced-mode-color, #fff);
  5955. }
  5956. .bytm-experimental-icon svg path {
  5957. fill: var(--bytm-experimental-col, #fff);
  5958. }
  5959. .bytm-warning-icon svg {
  5960. width: 24px;
  5961. height: 24px;
  5962. }
  5963. .bytm-warning-icon svg path {
  5964. fill: var(--bytm-warning-col, #fff);
  5965. }
  5966. .bytm-dialog-bg {
  5967. --bytm-dialog-bg: #333333;
  5968. --bytm-dialog-bg-highlight: #252525;
  5969. --bytm-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
  5970. --bytm-dialog-separator-color: #797979;
  5971. --bytm-dialog-border-radius: 10px;
  5972. }
  5973. .bytm-dialog-bg {
  5974. display: block;
  5975. position: fixed;
  5976. width: 100%;
  5977. height: 100%;
  5978. top: 0;
  5979. left: 0;
  5980. z-index: 5;
  5981. background-color: rgba(0, 0, 0, 0.6);
  5982. }
  5983. .bytm-dialog {
  5984. --calc-max-height: calc(min(100vh - 40px, var(--bytm-dialog-height-max)));
  5985. position: absolute;
  5986. display: flex;
  5987. flex-direction: column;
  5988. width: calc(min(100% - 60px, var(--bytm-dialog-width-max)));
  5989. border-radius: var(--bytm-dialog-border-radius);
  5990. height: auto;
  5991. max-height: var(--calc-max-height);
  5992. left: 50%;
  5993. top: 50%;
  5994. transform: translate(-50%, -50%);
  5995. z-index: 6;
  5996. color: #fff;
  5997. background-color: var(--bytm-dialog-bg);
  5998. }
  5999. .bytm-dialog-body {
  6000. font-size: 1.5rem;
  6001. padding: 20px;
  6002. }
  6003. .bytm-dialog-body.small {
  6004. padding: 15px;
  6005. }
  6006. #bytm-dialog-opts {
  6007. display: flex;
  6008. flex-direction: column;
  6009. position: relative;
  6010. padding: 30px 0px;
  6011. overflow-y: auto;
  6012. }
  6013. .bytm-dialog-header {
  6014. display: flex;
  6015. justify-content: space-between;
  6016. align-items: center;
  6017. margin-bottom: 6px;
  6018. padding: 15px 20px 15px 20px;
  6019. background-color: var(--bytm-dialog-bg);
  6020. border: 2px solid var(--bytm-dialog-separator-color);
  6021. border-style: none none solid none;
  6022. border-radius: var(--bytm-dialog-border-radius) var(--bytm-dialog-border-radius) 0px 0px;
  6023. }
  6024. .bytm-dialog-header.small {
  6025. padding: 10px 15px;
  6026. }
  6027. .bytm-dialog-header-pad {
  6028. content: " ";
  6029. min-height: 32px;
  6030. }
  6031. .bytm-dialog-header-pad.small {
  6032. min-height: 24px;
  6033. }
  6034. .bytm-dialog-titlecont {
  6035. display: flex;
  6036. align-items: center;
  6037. }
  6038. .bytm-dialog-titlecont-no-title {
  6039. display: flex;
  6040. justify-content: flex-end;
  6041. align-items: center;
  6042. }
  6043. .bytm-dialog-title {
  6044. position: relative;
  6045. display: inline-block;
  6046. font-size: 22px;
  6047. }
  6048. #bytm-dialog-version {
  6049. position: absolute;
  6050. width: 100%;
  6051. bottom: -10px;
  6052. left: 0;
  6053. font-size: 10px;
  6054. font-weight: normal;
  6055. z-index: 7;
  6056. }
  6057. #bytm-dialog-version .bytm-link {
  6058. color: #c6d2db;
  6059. }
  6060. #bytm-dialog-linkscont {
  6061. display: flex;
  6062. align-items: center;
  6063. margin-left: 32px;
  6064. }
  6065. .bytm-dialog-link {
  6066. display: inline-flex;
  6067. align-items: center;
  6068. cursor: pointer;
  6069. }
  6070. .bytm-dialog-link:not(:last-of-type) {
  6071. margin-right: 10px;
  6072. }
  6073. .bytm-dialog-link .bytm-dialog-img {
  6074. position: relative;
  6075. border-radius: 50%;
  6076. bottom: 0px;
  6077. transition: bottom 0.15s ease-out;
  6078. }
  6079. .bytm-dialog-link:hover .bytm-dialog-img {
  6080. bottom: 5px;
  6081. }
  6082. .bytm-dialog-close {
  6083. width: 32px;
  6084. height: 32px;
  6085. cursor: pointer;
  6086. }
  6087. .bytm-dialog-close.small {
  6088. width: 24px;
  6089. height: 24px;
  6090. }
  6091. .bytm-dialog-footer {
  6092. font-size: 17px;
  6093. text-decoration: underline;
  6094. }
  6095. .bytm-dialog-footer.hidden {
  6096. display: none;
  6097. }
  6098. .bytm-dialog-footer-cont {
  6099. display: flex;
  6100. flex-direction: row;
  6101. justify-content: space-between;
  6102. margin-top: 6px;
  6103. padding: 15px 20px;
  6104. background: var(--bytm-dialog-bg);
  6105. background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bytm-dialog-bg) 30%, var(--bytm-dialog-bg) 100%);
  6106. border: 2px solid var(--bytm-dialog-separator-color);
  6107. border-style: solid none none none;
  6108. border-radius: 0px 0px var(--bytm-dialog-border-radius) var(--bytm-dialog-border-radius);
  6109. }
  6110. #bytm-dialog-footer-buttons-cont button:not(:last-of-type) {
  6111. margin-right: 15px;
  6112. }
  6113. .bytm-dialog-footer-right {
  6114. display: flex;
  6115. flex-direction: row-reverse;
  6116. align-items: center;
  6117. margin-top: 15px;
  6118. }
  6119. #bytm-dialog-footer-left-buttons-cont button:not(:last-of-type) {
  6120. margin-right: 15px;
  6121. }
  6122. #bytm-dialog-scroll-indicator {
  6123. --bytm-scroll-indicator-padding: 5px;
  6124. position: sticky;
  6125. bottom: -15px;
  6126. left: 50%;
  6127. margin-top: calc(-32px - var(--bytm-scroll-indicator-padding) * 2);
  6128. padding: var(--bytm-scroll-indicator-padding);
  6129. transform: translateX(-50%);
  6130. width: 32px;
  6131. height: 32px;
  6132. z-index: 7;
  6133. background-color: var(--bytm-scroll-indicator-bg);
  6134. border-radius: 50%;
  6135. cursor: pointer;
  6136. }
  6137. .bytm-hidden {
  6138. visibility: hidden !important;
  6139. }
  6140. .bytm-ftconf-category-header {
  6141. font-size: 20px;
  6142. margin-top: 32px;
  6143. margin-bottom: 8px;
  6144. padding: 0px 20px;
  6145. }
  6146. .bytm-ftconf-category-header:first-of-type {
  6147. margin-top: 0;
  6148. }
  6149. .bytm-dialog .bytm-ftitem {
  6150. display: flex;
  6151. flex-direction: row;
  6152. justify-content: space-between;
  6153. align-items: center;
  6154. font-size: 1.4em;
  6155. padding: 8px 20px;
  6156. transition: background-color 0.15s ease-out;
  6157. }
  6158. .bytm-dialog .bytm-ftitem:hover {
  6159. background-color: var(--bytm-dialog-bg-highlight);
  6160. }
  6161. .bytm-ftitem-leftside {
  6162. display: flex;
  6163. align-items: center;
  6164. min-height: 24px;
  6165. }
  6166. .bytm-ftconf-ctrl {
  6167. display: inline-flex;
  6168. align-items: center;
  6169. white-space: nowrap;
  6170. margin-left: 10px;
  6171. }
  6172. .bytm-ftconf-label {
  6173. user-select: none;
  6174. }
  6175. .bytm-slider-label {
  6176. margin-right: 10px;
  6177. }
  6178. .bytm-ftconf-input.bytm-hotkey-input {
  6179. cursor: pointer;
  6180. min-width: 80px;
  6181. }
  6182. .bytm-ftconf-input[type=number] {
  6183. width: 75px;
  6184. }
  6185. .bytm-ftconf-input[type=range] {
  6186. width: 200px;
  6187. }
  6188. .bytm-ftconf-input[type=text] {
  6189. width: 200px;
  6190. }
  6191. .bytm-ftconf-input[type=checkbox] {
  6192. margin-left: 5px;
  6193. }
  6194. #bytm-export-dialog-text, #bytm-import-dialog-text {
  6195. font-size: 1.6em;
  6196. margin-bottom: 15px;
  6197. }
  6198. .bytm-dialog-footer-copied {
  6199. font-size: 1.6em;
  6200. margin-right: 15px;
  6201. }
  6202. #bytm-changelog-dialog-body {
  6203. overflow-y: auto;
  6204. }
  6205. #bytm-export-dialog-textarea, #bytm-import-dialog-textarea {
  6206. width: 100%;
  6207. height: 150px;
  6208. resize: none;
  6209. }
  6210. .bytm-markdown-container {
  6211. display: flex;
  6212. flex-direction: column;
  6213. overflow-y: auto;
  6214. font-size: 1.4rem;
  6215. line-height: 20px;
  6216. }
  6217. /* Markdown stuff */
  6218. .bytm-markdown-container kbd, .bytm-kbd {
  6219. --bytm-easing: cubic-bezier(0.31, 0.58, 0.24, 1.15);
  6220. display: inline-block;
  6221. vertical-align: bottom;
  6222. padding: 4px;
  6223. padding-top: 2px;
  6224. font-size: 0.95em;
  6225. line-height: 11px;
  6226. background-color: #222;
  6227. border: 1px solid #777;
  6228. border-radius: 5px;
  6229. box-shadow: inset 0 -2px 0 #515559;
  6230. transition: padding 0.1s var(--bytm-easing), margin-top 0.1s var(--bytm-easing), box-shadow 0.1s var(--bytm-easing);
  6231. }
  6232. .bytm-markdown-container kbd:active, .bytm-kbd:active {
  6233. padding-bottom: 2px;
  6234. margin-top: 2px;
  6235. box-shadow: inset 0 0 0 initial;
  6236. }
  6237. .bytm-markdown-container kbd::selection, .bytm-kbd::selection {
  6238. background: rgba(0, 0, 0, 0);
  6239. }
  6240. .bytm-markdown-container code {
  6241. background-color: #222;
  6242. border-radius: 3px;
  6243. padding: 1px 5px;
  6244. }
  6245. .bytm-markdown-container h2 {
  6246. margin-bottom: 5px;
  6247. }
  6248. .bytm-markdown-container h2:not(:first-of-type) {
  6249. margin-top: 30px;
  6250. }
  6251. .bytm-markdown-container ul li::before {
  6252. content: "• ";
  6253. font-weight: bolder;
  6254. }
  6255. .bytm-markdown-container ul li > ul li::before {
  6256. white-space: pre-wrap;
  6257. content: " • ";
  6258. font-weight: bolder;
  6259. }
  6260. #bytm-feat-help-dialog-desc, #bytm-feat-help-dialog-text {
  6261. overflow-wrap: break-word;
  6262. white-space: pre-wrap;
  6263. padding: 10px 10px 15px 20px;
  6264. font-size: 1.5em;
  6265. }
  6266. #bytm-feat-help-dialog-desc {
  6267. font-size: 1.65em;
  6268. padding-bottom: 5px;
  6269. }
  6270. .bytm-ftitem-help-btn {
  6271. width: 24px !important;
  6272. height: 24px !important;
  6273. }
  6274. .bytm-ftitem-help-btn svg {
  6275. width: 18px !important;
  6276. height: 18px !important;
  6277. }
  6278. .bytm-ftitem-help-btn svg > path {
  6279. fill: #b3bec7 !important;
  6280. }
  6281. hr {
  6282. display: block;
  6283. margin: 8px 0px 12px 0px;
  6284. border: revert;
  6285. }
  6286. .bytm-ftitem-adornment {
  6287. display: inline-flex;
  6288. justify-content: flex-start;
  6289. align-items: center;
  6290. margin-right: 6px;
  6291. }
  6292. .bytm-hotkey-wrapper {
  6293. display: flex;
  6294. flex-direction: row;
  6295. align-items: center;
  6296. justify-content: flex-end;
  6297. }
  6298. .bytm-hotkey-reset {
  6299. font-size: 0.9em;
  6300. margin-right: 10px;
  6301. }
  6302. .bytm-hotkey-info {
  6303. font-size: 0.9em;
  6304. white-space: nowrap;
  6305. }
  6306. .bytm-toggle-input-wrapper {
  6307. --toggle-height: 20px;
  6308. --toggle-width: 40px;
  6309. --toggle-knob-offset: 4px;
  6310. --toggle-color-on: var(--bytm-dialog-accent-col, #4595c7);
  6311. --toggle-color-off: #707070;
  6312. --toggle-knob-color: #f5f5f5;
  6313. display: flex;
  6314. align-items: center;
  6315. }
  6316. .bytm-toggle-input-wrapper .bytm-toggle-input-label {
  6317. cursor: pointer;
  6318. font-size: 1.5rem;
  6319. padding: 3px 12px;
  6320. }
  6321. /* sauce: https://danklammer.com/articles/simple-css-toggle-switch/ */
  6322. .bytm-toggle-input {
  6323. display: flex;
  6324. align-items: center;
  6325. }
  6326. .bytm-toggle-input input {
  6327. appearance: none;
  6328. display: inline-block;
  6329. width: var(--toggle-width);
  6330. height: var(--toggle-height);
  6331. position: relative;
  6332. border-radius: 50px;
  6333. overflow: hidden;
  6334. outline: none;
  6335. border: none;
  6336. margin: 0;
  6337. padding: var(--toggle-knob-offset);
  6338. cursor: pointer;
  6339. background-color: var(--toggle-color-off);
  6340. transition: justify-content 0.2s ease, background-color 0.2s ease;
  6341. }
  6342. .bytm-toggle-input input[data-toggled="true"] {
  6343. background-color: var(--toggle-color-on);
  6344. }
  6345. .bytm-toggle-input input .bytm-toggle-input-knob {
  6346. --toggle-knob-calc-width: calc(var(--toggle-height) - (var(--toggle-knob-offset) * 2));
  6347. --toggle-knob-calc-height: calc(var(--toggle-height) - (var(--toggle-knob-offset) * 2));
  6348. width: var(--toggle-knob-calc-width);
  6349. height: var(--toggle-knob-calc-height);
  6350. background-color: var(--toggle-knob-color);
  6351. border-radius: 50%;
  6352. position: absolute;
  6353. top: 50%;
  6354. transform: translateY(-50%);
  6355. left: var(--toggle-knob-offset);
  6356. transition: left 0.2s ease;
  6357. }
  6358. .bytm-toggle-input input[data-toggled="true"] .bytm-toggle-input-knob {
  6359. left: calc(var(--toggle-width) - var(--toggle-knob-offset) - var(--toggle-knob-calc-width));
  6360. }
  6361. /* #MARKER volume slider */
  6362. #bytm-vol-slider-cont {
  6363. position: relative;
  6364. }
  6365. #bytm-vol-slider-label {
  6366. opacity: 0.000001;
  6367. position: absolute;
  6368. display: flex;
  6369. flex-direction: row;
  6370. align-items: center;
  6371. gap: 5px;
  6372. font-size: 1.4rem;
  6373. top: 50%;
  6374. left: 0;
  6375. transform: translate(calc(-50% - 10px), -50%);
  6376. text-align: right;
  6377. transition: opacity 0.2s ease;
  6378. }
  6379. #bytm-vol-slider-label.has-icon {
  6380. transform: translate(calc(-50% - 25px), -50%);
  6381. }
  6382. #bytm-vol-slider-label svg {
  6383. padding: 4px;
  6384. }
  6385. #bytm-vol-slider-label svg path {
  6386. fill: #909090;
  6387. }
  6388. #bytm-vol-slider-label.bytm-visible {
  6389. opacity: 1;
  6390. }
  6391. #bytm-vol-slider-shared {
  6392. display: flex;
  6393. flex-direction: row;
  6394. align-items: center;
  6395. }
  6396. #bytm-vol-slider-shared svg {
  6397. width: 20px;
  6398. height: 20px;
  6399. }
  6400. .bytm-menu-bg {
  6401. --bytm-menu-bg: #333333;
  6402. --bytm-menu-bg-highlight: #252525;
  6403. --bytm-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
  6404. --bytm-menu-separator-color: #797979;
  6405. --bytm-menu-border-radius: 10px;
  6406. }
  6407. .bytm-menu-bg {
  6408. display: block;
  6409. position: fixed;
  6410. width: 100%;
  6411. height: 100%;
  6412. top: 0;
  6413. left: 0;
  6414. z-index: 5;
  6415. background-color: rgba(0, 0, 0, 0.6);
  6416. }
  6417. .bytm-menu {
  6418. position: fixed;
  6419. display: flex;
  6420. flex-direction: column;
  6421. width: calc(min(100% - 60px, var(--bytm-menu-width-max)));
  6422. border-radius: var(--bytm-menu-border-radius);
  6423. height: auto;
  6424. max-height: calc(min(100% - 40px, var(--bytm-menu-height-max)));
  6425. left: 50%;
  6426. top: 50%;
  6427. transform: translate(-50%, -50%);
  6428. z-index: 6;
  6429. color: #fff;
  6430. background-color: var(--bytm-menu-bg);
  6431. }
  6432. .bytm-menu.top-aligned {
  6433. top: 0;
  6434. transform: translate(-50%, 40px);
  6435. }
  6436. .bytm-menu-body {
  6437. padding: 20px;
  6438. }
  6439. #bytm-menu-opts {
  6440. display: flex;
  6441. flex-direction: column;
  6442. position: relative;
  6443. padding: 20px 0px;
  6444. overflow-y: auto;
  6445. }
  6446. .bytm-menu-header {
  6447. display: flex;
  6448. justify-content: space-between;
  6449. align-items: center;
  6450. margin-bottom: 6px;
  6451. padding: 15px 20px 15px 20px;
  6452. background-color: var(--bytm-menu-bg);
  6453. border: 2px solid var(--bytm-menu-separator-color);
  6454. border-style: none none solid none;
  6455. border-radius: var(--bytm-menu-border-radius) var(--bytm-menu-border-radius) 0px 0px;
  6456. }
  6457. .bytm-menu-header.small {
  6458. padding: 10px 15px;
  6459. }
  6460. .bytm-menu-titlecont {
  6461. position: relative;
  6462. display: flex;
  6463. align-items: center;
  6464. }
  6465. .bytm-menu-titlecont-no-title {
  6466. display: flex;
  6467. justify-content: flex-end;
  6468. align-items: center;
  6469. }
  6470. .bytm-menu-title {
  6471. position: relative;
  6472. display: inline-block;
  6473. font-size: 22px;
  6474. }
  6475. #bytm-cfg-menu-bg .bytm-menu-title {
  6476. transform: translate(0px, -6px);
  6477. }
  6478. #bytm-cfg-menu {
  6479. --bytm-menu-subtitle-color: #c6d2db;
  6480. }
  6481. #bytm-menu-subtitle-cont {
  6482. width: 100%;
  6483. display: flex;
  6484. gap: 6px;
  6485. flex-direction: row;
  6486. justify-content: space-between;
  6487. align-items: end;
  6488. position: absolute;
  6489. bottom: -12px;
  6490. left: 0;
  6491. font-size: 10px;
  6492. font-weight: normal;
  6493. z-index: 7;
  6494. }
  6495. #bytm-menu-subtitle-cont, #bytm-menu-version-anchor {
  6496. color: var(--bytm-menu-subtitle-color);
  6497. }
  6498. #bytm-menu-subtitle-cont, #bytm-menu-mode-display {
  6499. overflow: hidden;
  6500. text-overflow: ellipsis;
  6501. white-space: nowrap;
  6502. }
  6503. #bytm-menu-version-anchor {
  6504. overflow: hidden;
  6505. text-wrap: nowrap;
  6506. text-overflow: ellipsis;
  6507. }
  6508. #bytm-menu-linkscont {
  6509. display: flex;
  6510. align-items: center;
  6511. margin-left: 32px;
  6512. }
  6513. .bytm-menu-link {
  6514. position: relative;
  6515. max-height: 32px;
  6516. max-width: 32px;
  6517. display: inline-flex;
  6518. flex-direction: column;
  6519. align-items: center;
  6520. cursor: pointer;
  6521. }
  6522. .bytm-menu-link:not(:last-of-type) {
  6523. margin-right: 10px;
  6524. }
  6525. .bytm-menu-link .bytm-menu-img {
  6526. width: 32px;
  6527. height: 32px;
  6528. border-radius: 50%;
  6529. padding: 0px;
  6530. transform: translateY(0px);
  6531. transition: transform 0.15s ease-out, padding 0.15s ease-out;
  6532. }
  6533. .bytm-menu-link:hover .bytm-menu-img {
  6534. padding: 7px;
  6535. transform: translateY(-14px);
  6536. }
  6537. .bytm-menu-link .extended-link {
  6538. visibility: hidden;
  6539. position: absolute;
  6540. top: 14px;
  6541. padding-top: 13px;
  6542. padding-bottom: 2px;
  6543. opacity: 0;
  6544. text-decoration: none;
  6545. color: var(--bytm-menu-subtitle-color);
  6546. white-space: pre;
  6547. font-size: 1.1rem;
  6548. transition: visibility 0.15s ease-out, opacity 0.15s ease-out;
  6549. }
  6550. .bytm-menu-link:hover .extended-link {
  6551. visibility: visible;
  6552. opacity: 1;
  6553. }
  6554. .bytm-menu-close {
  6555. width: 32px;
  6556. height: 32px;
  6557. cursor: pointer;
  6558. }
  6559. .bytm-menu-close.small {
  6560. width: 24px;
  6561. height: 24px;
  6562. }
  6563. .bytm-menu-footer {
  6564. font-size: 17px;
  6565. text-decoration: underline;
  6566. }
  6567. .bytm-menu-footer.hidden {
  6568. display: none;
  6569. }
  6570. .bytm-menu-footer-cont {
  6571. display: flex;
  6572. flex-direction: row;
  6573. justify-content: space-between;
  6574. margin-top: 6px;
  6575. padding: 15px 20px;
  6576. background: var(--bytm-menu-bg);
  6577. background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bytm-menu-bg) 30%, var(--bytm-menu-bg) 100%);
  6578. border: 2px solid var(--bytm-menu-separator-color);
  6579. border-style: solid none none none;
  6580. border-radius: 0px 0px var(--bytm-menu-border-radius) var(--bytm-menu-border-radius);
  6581. }
  6582. #bytm-menu-footer-buttons-cont button:not(:last-of-type) {
  6583. margin-right: 15px;
  6584. }
  6585. .bytm-menu-footer-right {
  6586. display: flex;
  6587. flex-direction: row-reverse;
  6588. align-items: center;
  6589. margin-top: 15px;
  6590. }
  6591. #bytm-menu-footer-left-buttons-cont button:not(:last-of-type) {
  6592. margin-right: 15px;
  6593. }
  6594. #bytm-menu-scroll-indicator {
  6595. --bytm-scroll-indicator-padding: 5px;
  6596. position: sticky;
  6597. bottom: -15px;
  6598. left: 50%;
  6599. margin-top: calc(-32px - var(--bytm-scroll-indicator-padding) * 2);
  6600. padding: var(--bytm-scroll-indicator-padding);
  6601. transform: translateX(-50%);
  6602. width: 32px;
  6603. height: 32px;
  6604. z-index: 7;
  6605. background-color: var(--bytm-scroll-indicator-bg);
  6606. border-radius: 50%;
  6607. cursor: pointer;
  6608. }
  6609. .bytm-hidden {
  6610. visibility: hidden !important;
  6611. }
  6612. .bytm-ftconf-category-header {
  6613. font-size: 20px;
  6614. margin-top: 32px;
  6615. margin-bottom: 8px;
  6616. padding: 0px 20px;
  6617. }
  6618. .bytm-ftconf-category-header:first-of-type {
  6619. margin-top: 0;
  6620. }
  6621. .bytm-ftitem {
  6622. display: flex;
  6623. flex-direction: row;
  6624. justify-content: space-between;
  6625. align-items: center;
  6626. font-size: 1.4rem;
  6627. padding: 8px 20px;
  6628. transition: background-color 0.15s ease-out;
  6629. }
  6630. .bytm-ftitem:hover {
  6631. background-color: var(--bytm-menu-bg-highlight);
  6632. }
  6633. .bytm-ftitem-leftside {
  6634. display: flex;
  6635. align-items: center;
  6636. min-height: 24px;
  6637. }
  6638. .bytm-ftconf-ctrl {
  6639. display: inline-flex;
  6640. align-items: center;
  6641. white-space: nowrap;
  6642. margin-left: 10px;
  6643. }
  6644. .bytm-ftconf-label {
  6645. user-select: none;
  6646. }
  6647. .bytm-slider-label {
  6648. margin-right: 10px;
  6649. }
  6650. .bytm-toggle-label {
  6651. padding-left: 10px;
  6652. padding-right: 5px;
  6653. }
  6654. .bytm-ftconf-input.bytm-hotkey-input {
  6655. cursor: pointer;
  6656. min-width: 50px;
  6657. }
  6658. .bytm-ftconf-input[type=number] {
  6659. width: 75px;
  6660. }
  6661. .bytm-ftconf-input[type=checkbox] {
  6662. margin-left: 5px;
  6663. }
  6664. #bytm-export-menu-text, #bytm-import-menu-text {
  6665. white-space: pre-wrap;
  6666. font-size: 1.6em;
  6667. margin-bottom: 15px;
  6668. }
  6669. .bytm-menu-footer-copied {
  6670. font-size: 1.6em;
  6671. margin-right: 15px;
  6672. }
  6673. #bytm-changelog-menu-body {
  6674. overflow-y: auto;
  6675. }
  6676. .bytm-changelog-version-details:not(:first-of-type) {
  6677. margin-top: 15px;
  6678. }
  6679. .bytm-changelog-version-details summary h2 {
  6680. display: inline-block;
  6681. }
  6682. .bytm-changelog-version-details summary {
  6683. cursor: pointer;
  6684. }
  6685. .bytm-changelog-version-details summary::marker {
  6686. font-size: 2rem;
  6687. }
  6688. #bytm-export-menu-textarea, #bytm-import-menu-textarea {
  6689. width: 100%;
  6690. height: 150px;
  6691. resize: none;
  6692. }
  6693. .bytm-markdown-container {
  6694. display: flex;
  6695. flex-direction: column;
  6696. overflow-y: auto;
  6697. font-size: 1.5em;
  6698. line-height: 20px;
  6699. }
  6700. /* Markdown stuff */
  6701. .bytm-markdown-container kbd {
  6702. --bytm-easing: cubic-bezier(0.31, 0.58, 0.24, 1.15);
  6703. display: inline-block;
  6704. vertical-align: bottom;
  6705. padding: 4px;
  6706. padding-top: 2px;
  6707. font-size: 0.95em;
  6708. line-height: 11px;
  6709. background-color: #222;
  6710. border: 1px solid #777;
  6711. border-radius: 5px;
  6712. box-shadow: inset 0 -2px 0 #515559;
  6713. transition: padding 0.1s var(--bytm-easing), box-shadow 0.1s var(--bytm-easing);
  6714. }
  6715. .bytm-markdown-container kbd:active {
  6716. padding-bottom: 2px;
  6717. box-shadow: inset 0 0 0 initial;
  6718. }
  6719. .bytm-markdown-container kbd::selection {
  6720. background: rgba(0, 0, 0, 0);
  6721. }
  6722. .bytm-markdown-container code {
  6723. background-color: #222;
  6724. border-radius: 3px;
  6725. padding: 1px 5px;
  6726. }
  6727. .bytm-markdown-container h2 {
  6728. margin-bottom: 5px;
  6729. }
  6730. .bytm-markdown-container h2:not(:first-of-type) {
  6731. margin-top: 30px;
  6732. }
  6733. .bytm-markdown-container ul li::before {
  6734. content: "• ";
  6735. font-weight: bolder;
  6736. }
  6737. .bytm-markdown-container ul li > ul li::before {
  6738. white-space: pre-wrap;
  6739. content: " • ";
  6740. font-weight: bolder;
  6741. }
  6742. #bytm-feat-help-menu-desc, #bytm-feat-help-menu-text {
  6743. overflow-wrap: break-word;
  6744. white-space: pre-wrap;
  6745. padding: 10px 10px 15px 20px;
  6746. font-size: 1.5em;
  6747. }
  6748. #bytm-feat-help-menu-desc {
  6749. font-size: 1.65em;
  6750. padding-bottom: 5px;
  6751. }
  6752. .bytm-ftitem-help-btn {
  6753. width: 24px !important;
  6754. height: 24px !important;
  6755. }
  6756. .bytm-ftitem-help-btn svg {
  6757. width: 18px !important;
  6758. height: 18px !important;
  6759. }
  6760. .bytm-ftitem-help-btn svg > path {
  6761. fill: #b3bec7 !important;
  6762. }
  6763. hr {
  6764. display: block;
  6765. margin: 8px 0px 12px 0px;
  6766. border: revert;
  6767. }
  6768. /* #MARKER misc */
  6769. .bytm-disable-scroll {
  6770. overflow: hidden !important;
  6771. }
  6772. .bytm-generic-btn {
  6773. display: inline-flex;
  6774. align-items: center;
  6775. justify-content: center;
  6776. position: relative;
  6777. vertical-align: middle;
  6778. cursor: pointer;
  6779. margin-left: 8px;
  6780. width: 36px;
  6781. height: 36px;
  6782. border: 1px solid transparent;
  6783. border-radius: 100%;
  6784. background-color: transparent;
  6785. transition: background-color 0.2s ease;
  6786. }
  6787. .bytm-generic-btn:hover {
  6788. background-color: rgba(255, 255, 255, 0.2);
  6789. }
  6790. .bytm-generic-btn:active {
  6791. background-color: #5f5f5f;
  6792. animation: flashBorder 0.4s ease 1;
  6793. }
  6794. @keyframes flashBorder {
  6795. 0% {
  6796. border: 1px solid transparent;
  6797. }
  6798. 20% {
  6799. border: 1px solid #808080;
  6800. }
  6801. 100% {
  6802. border: 1px solid transparent;
  6803. }
  6804. }
  6805. .bytm-generic-btn-img {
  6806. display: inline-block;
  6807. z-index: 10;
  6808. width: 24px;
  6809. height: 24px;
  6810. }
  6811. .bytm-spinner {
  6812. animation: rotate 1.2s linear infinite;
  6813. }
  6814. @keyframes rotate {
  6815. from {
  6816. transform: rotate(0deg);
  6817. }
  6818. to {
  6819. transform: rotate(360deg);
  6820. }
  6821. }
  6822. .bytm-anchor {
  6823. all: unset;
  6824. cursor: pointer;
  6825. }
  6826. /* ytmusic-logo a[bytm-animated="true"] .bytm-mod-logo-ellipse {
  6827. transform-origin: 12px 12px;
  6828. animation: rotate 1s ease-in-out infinite;
  6829. } */
  6830. ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-path {
  6831. transform-origin: 12px 12px;
  6832. animation: rotate 1s ease-in-out;
  6833. }
  6834. ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-img {
  6835. width: 24px;
  6836. height: 24px;
  6837. z-index: 1000;
  6838. position: absolute;
  6839. animation: rotate-fade-in 1s ease-in-out;
  6840. }
  6841. @keyframes rotate-fade-in {
  6842. 0% {
  6843. opacity: 0;
  6844. transform: rotate(0deg);
  6845. }
  6846. 30% {
  6847. opacity: 0;
  6848. }
  6849. 90% {
  6850. opacity: 1;
  6851. }
  6852. 100% {
  6853. transform: rotate(360deg);
  6854. }
  6855. }
  6856. .bytm-no-select {
  6857. user-select: none;
  6858. -ms-user-select: none;
  6859. -moz-user-select: none;
  6860. -webkit-user-select: none;
  6861. }
  6862. /* YTM does some weird styling that breaks everything, so this reverts all of BYTM's buttons to the browser default style */
  6863. button.bytm-btn {
  6864. padding: revert;
  6865. border: revert;
  6866. outline: revert;
  6867. font: revert;
  6868. text-transform: revert;
  6869. color: revert;
  6870. background: revert;
  6871. line-height: 1.4em;
  6872. }
  6873. .bytm-link, .bytm-markdown-container a {
  6874. color: #369bff;
  6875. text-decoration: none;
  6876. cursor: pointer;
  6877. }
  6878. .bytm-link:hover, .bytm-markdown-container a:hover {
  6879. text-decoration: underline;
  6880. }
  6881. button[disabled] {
  6882. cursor: not-allowed;
  6883. }
  6884. button[disabled].bytm-busy {
  6885. cursor: wait;
  6886. }
  6887. /* #MARKER menu */
  6888. .bytm-cfg-menu-option {
  6889. display: block;
  6890. padding: 8px 0;
  6891. }
  6892. .bytm-cfg-menu-option-item {
  6893. display: flex;
  6894. flex-direction: row;
  6895. align-items: center;
  6896. font-size: 1.4rem;
  6897. font-weight: 400;
  6898. line-height: 24px;
  6899. padding: var(--yt-compact-link-paper-item-padding, 0px 36px 0 16px);
  6900. min-height: var(--paper-item-min-height, 40px);
  6901. white-space: nowrap;
  6902. cursor: pointer;
  6903. }
  6904. .bytm-cfg-menu-option-item:hover {
  6905. background-color: var(--yt-spec-badge-chip-background, #3e3e3e);
  6906. }
  6907. .bytm-cfg-menu-option-icon {
  6908. width: 24px;
  6909. height: 24px;
  6910. margin-right: 16px;
  6911. display: flex;
  6912. align-items: center;
  6913. flex-direction: row;
  6914. flex: none;
  6915. }
  6916. .bytm-cfg-menu-option-text {
  6917. font-size: 1.4rem;
  6918. line-height: 2rem;
  6919. }
  6920. yt-multi-page-menu-section-renderer.ytd-multi-page-menu-renderer {
  6921. border-bottom: 1px solid var(--yt-spec-10-percent-layer, #3e3e3e);
  6922. }
  6923. /* #MARKER watermark */
  6924. #bytm-watermark {
  6925. font-size: 10px;
  6926. display: inline-block;
  6927. position: absolute;
  6928. left: 97px;
  6929. top: 45px;
  6930. z-index: 10;
  6931. color: #f1f1f1;
  6932. text-decoration: none;
  6933. cursor: pointer;
  6934. }
  6935. #bytm-watermark:hover {
  6936. text-decoration: underline;
  6937. }
  6938. /* #MARKER scroll to active */
  6939. #bytm-scroll-to-active-btn-cont {
  6940. display: flex;
  6941. flex-direction: column;
  6942. justify-content: center;
  6943. align-items: center;
  6944. position: absolute;
  6945. right: 5px;
  6946. top: 0;
  6947. height: 100%;
  6948. }
  6949. #bytm-scroll-to-active-btn {
  6950. display: inline-flex;
  6951. align-items: center;
  6952. justify-content: center;
  6953. border-radius: 50%;
  6954. cursor: pointer;
  6955. }
  6956. #bytm-scroll-to-active-btn {
  6957. width: revert;
  6958. height: revert;
  6959. }
  6960. #bytm-scroll-to-active-btn .bytm-generic-btn-img {
  6961. padding: 4px;
  6962. }
  6963. /** #MARKER thumbnail */
  6964. #bytm-thumbnail-overlay {
  6965. position: absolute;
  6966. top: 0;
  6967. left: 0;
  6968. width: 100%;
  6969. height: 100%;
  6970. display: none;
  6971. background-color: #030303;
  6972. z-index: 0;
  6973. }
  6974. #bytm-thumbnail-overlay-img {
  6975. position: relative;
  6976. width: 100%;
  6977. height: 100%;
  6978. }
  6979. #bytm-thumbnail-overlay-indicator {
  6980. position: absolute;
  6981. bottom: 16px;
  6982. right: 16px;
  6983. width: calc(min(56px, 100% - 16px));
  6984. height: calc(min(56px, 100% - 16px));
  6985. z-index: 1;
  6986. cursor: help;
  6987. filter: drop-shadow(0 0 3px #000);
  6988. }
  6989. ytmusic-player#player #bezel {
  6990. z-index: 1;
  6991. }
  6992. /* #MARKER queue buttons */
  6993. #side-panel ytmusic-player-queue-item .song-info.ytmusic-player-queue-item {
  6994. position: relative;
  6995. }
  6996. #side-panel ytmusic-player-queue-item .bytm-queue-btn-container {
  6997. background: rgb(0, 0, 0);
  6998. background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #030303 15%);
  6999. display: none;
  7000. position: absolute;
  7001. right: 0;
  7002. padding-left: 25px;
  7003. height: 100%;
  7004. }
  7005. #side-panel ytmusic-player-queue-item[selected] .bytm-queue-btn-container {
  7006. background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #1D1D1D 15%);
  7007. }
  7008. .bytm-generic-list-queue-btn-container {
  7009. /* otherwise the queue buttons render over the currently playing song page */
  7010. z-index: 1;
  7011. }
  7012. #side-panel ytmusic-player-queue-item:hover .bytm-queue-btn-container,
  7013. ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer:hover .bytm-queue-btn-container,
  7014. ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer:hover .bytm-queue-btn-container {
  7015. display: inline-flex;
  7016. align-items: center;
  7017. }
  7018. ytmusic-responsive-list-item-renderer .title-column {
  7019. align-items: center;
  7020. }
  7021. #side-panel ytmusic-player-queue-item[play-button-state="loading"] .bytm-queue-btn-container,
  7022. #side-panel ytmusic-player-queue-item[play-button-state="playing"] .bytm-queue-btn-container,
  7023. #side-panel ytmusic-player-queue-item[play-button-state="paused"] .bytm-queue-btn-container {
  7024. /* using a var() with predefined value from YTM is not viable since the nesting changes the actual value of the variable */
  7025. background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #030303 15%);
  7026. }
  7027. #side-panel ytmusic-player-queue-item[selected][play-button-state="loading"] .bytm-queue-btn-container,
  7028. #side-panel ytmusic-player-queue-item[selected][play-button-state="playing"] .bytm-queue-btn-container,
  7029. #side-panel ytmusic-player-queue-item[selected][play-button-state="paused"] .bytm-queue-btn-container {
  7030. /* using a var() with predefined value from YTM is not viable since the nesting changes the actual value of the variable */
  7031. background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #1D1D1D 15%);
  7032. }
  7033. ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown[data-bytm-hidden=true] {
  7034. display: none !important;
  7035. }
  7036. ytmusic-responsive-list-item-renderer.bytm-has-queue-btns .bytm-generic-list-queue-btn-container {
  7037. visibility: hidden;
  7038. }
  7039. ytmusic-responsive-list-item-renderer.bytm-has-queue-btns .bytm-generic-list-queue-btn-container a.bytm-generic-btn {
  7040. visibility: hidden !important;
  7041. }
  7042. ytmusic-responsive-list-item-renderer.bytm-has-queue-btns:hover .bytm-generic-list-queue-btn-container {
  7043. visibility: visible;
  7044. }
  7045. ytmusic-responsive-list-item-renderer.bytm-has-queue-btns:hover .bytm-generic-list-queue-btn-container a.bytm-generic-btn {
  7046. visibility: visible !important;
  7047. }
  7048. #bytm-welcome-menu-bg {
  7049. --bytm-menu-height-max: 500px;
  7050. --bytm-menu-width-max: 700px;
  7051. }
  7052. #bytm-welcome-menu-title-wrapper {
  7053. display: flex;
  7054. flex-direction: row;
  7055. align-items: center;
  7056. }
  7057. #bytm-welcome-menu-title-logo {
  7058. width: 32px;
  7059. height: 32px;
  7060. margin-right: 20px;
  7061. }
  7062. #bytm-welcome-menu-content-wrapper {
  7063. overflow-y: auto;
  7064. }
  7065. #bytm-welcome-menu-locale-cont {
  7066. display: flex;
  7067. flex-direction: column;
  7068. align-items: center;
  7069. justify-content: flex-start;
  7070. }
  7071. #bytm-welcome-menu-locale-img {
  7072. width: 80px;
  7073. height: 80px;
  7074. margin-bottom: 10px;
  7075. }
  7076. #bytm-welcome-menu-text {
  7077. font-size: 1.6em;
  7078. padding: 8px 20px;
  7079. margin: 10px 0px;
  7080. line-height: 20px;
  7081. }
  7082. #bytm-welcome-menu-locale-select {
  7083. font-size: 1.6em;
  7084. }
  7085. #bytm-welcome-menu-footer-cont {
  7086. border-radius: 0px 0px var(--bytm-menu-border-radius) var(--bytm-menu-border-radius);
  7087. padding: 20px;
  7088. }`).id = "bytm-style-global";
  7089. }
  7090. /** Registers dev commands using `GM.registerMenuCommand` */
  7091. function registerDevMenuCommands() {
  7092. if (mode !== "development")
  7093. return;
  7094. GM.registerMenuCommand("Reset config", () => __awaiter(this, void 0, void 0, function* () {
  7095. if (confirm("Reset the configuration to its default values?\nThis will automatically reload the page.")) {
  7096. yield clearConfig();
  7097. disableBeforeUnload();
  7098. location.reload();
  7099. }
  7100. }), "r");
  7101. GM.registerMenuCommand("Fix missing config values", () => __awaiter(this, void 0, void 0, function* () {
  7102. const oldFeats = reserialize(getFeatures());
  7103. yield setFeatures(Object.assign(Object.assign({}, defaultData), getFeatures()));
  7104. console.log("Fixed missing config values.\nFrom:", oldFeats, "\n\nTo:", getFeatures());
  7105. if (confirm("All missing or invalid config values were set to their default values.\nReload the page now?"))
  7106. location.reload();
  7107. }));
  7108. GM.registerMenuCommand("List GM values in console with decompression", () => __awaiter(this, void 0, void 0, function* () {
  7109. const keys = yield GM.listValues();
  7110. console.log(`GM values (${keys.length}):`);
  7111. if (keys.length === 0)
  7112. console.log(" No values found.");
  7113. const values = {};
  7114. let longestKey = 0;
  7115. for (const key of keys) {
  7116. const isEncoded = key.startsWith("_uucfg-") ? yield GM.getValue(`_uucfgenc-${key.substring(7)}`, false) : false;
  7117. const val = yield GM.getValue(key, undefined);
  7118. values[key] = typeof val !== "undefined" && isEncoded ? yield UserUtils.decompress(val, compressionFormat, "string") : val;
  7119. longestKey = Math.max(longestKey, key.length);
  7120. }
  7121. for (const [key, finalVal] of Object.entries(values)) {
  7122. const isEncoded = key.startsWith("_uucfg-") ? yield GM.getValue(`_uucfgenc-${key.substring(7)}`, false) : false;
  7123. const lengthStr = String(finalVal).length > 50 ? `(${String(finalVal).length} chars) ` : "";
  7124. console.log(` "${key}"${" ".repeat(longestKey - key.length)} -${isEncoded ? "-[decoded]-" : ""}> ${lengthStr}${finalVal}`);
  7125. }
  7126. }), "l");
  7127. GM.registerMenuCommand("List GM values in console, without decompression", () => __awaiter(this, void 0, void 0, function* () {
  7128. const keys = yield GM.listValues();
  7129. console.log(`GM values (${keys.length}):`);
  7130. if (keys.length === 0)
  7131. console.log(" No values found.");
  7132. const values = {};
  7133. let longestKey = 0;
  7134. for (const key of keys) {
  7135. const val = yield GM.getValue(key, undefined);
  7136. values[key] = val;
  7137. longestKey = Math.max(longestKey, key.length);
  7138. }
  7139. for (const [key, val] of Object.entries(values)) {
  7140. const lengthStr = String(val).length >= 16 ? `(${String(val).length} chars) ` : "";
  7141. console.log(` "${key}"${" ".repeat(longestKey - key.length)} -> ${lengthStr}${val}`);
  7142. }
  7143. }));
  7144. GM.registerMenuCommand("Delete all GM values", () => __awaiter(this, void 0, void 0, function* () {
  7145. const keys = yield GM.listValues();
  7146. if (confirm(`Clear all ${keys.length} GM values?\nSee console for details.`)) {
  7147. console.log(`Clearing ${keys.length} GM values:`);
  7148. if (keys.length === 0)
  7149. console.log(" No values found.");
  7150. for (const key of keys) {
  7151. yield GM.deleteValue(key);
  7152. console.log(` Deleted ${key}`);
  7153. }
  7154. }
  7155. }), "d");
  7156. GM.registerMenuCommand("Delete GM values by name (comma separated)", () => __awaiter(this, void 0, void 0, function* () {
  7157. var _a;
  7158. const keys = prompt("Enter the name(s) of the GM value to delete (comma separated).\nEmpty input cancels the operation.");
  7159. if (!keys)
  7160. return;
  7161. for (const key of (_a = keys === null || keys === void 0 ? void 0 : keys.split(",")) !== null && _a !== void 0 ? _a : []) {
  7162. if (key && key.length > 0) {
  7163. const truncLength = 400;
  7164. const oldVal = yield GM.getValue(key);
  7165. yield GM.deleteValue(key);
  7166. console.log(`Deleted GM value '${key}' with previous value '${oldVal && String(oldVal).length > truncLength ? String(oldVal).substring(0, truncLength) + `… (${String(oldVal).length} / ${truncLength} chars.)` : oldVal}'`);
  7167. }
  7168. }
  7169. }), "n");
  7170. GM.registerMenuCommand("Reset install timestamp", () => __awaiter(this, void 0, void 0, function* () {
  7171. yield GM.deleteValue("bytm-installed");
  7172. console.log("Reset install time.");
  7173. }), "t");
  7174. GM.registerMenuCommand("Reset version check timestamp", () => __awaiter(this, void 0, void 0, function* () {
  7175. yield GM.deleteValue("bytm-version-check");
  7176. console.log("Reset version check time.");
  7177. }), "v");
  7178. GM.registerMenuCommand("List active selector listeners in console", () => __awaiter(this, void 0, void 0, function* () {
  7179. const lines = [];
  7180. let listenersAmt = 0;
  7181. for (const [obsName, obs] of Object.entries(globservers)) {
  7182. const listeners = obs.getAllListeners();
  7183. lines.push(`- "${obsName}" (${listeners.size} listeners):`);
  7184. [...listeners].forEach(([k, v]) => {
  7185. listenersAmt += v.length;
  7186. lines.push(` [${v.length}] ${k}`);
  7187. v.forEach(({ all, continuous }, i) => {
  7188. lines.push(` ${v.length > 1 && i !== v.length - 1 ? "├" : "└"}> ${continuous ? "continuous" : "single-shot"}, ${all ? "select multiple" : "select single"}`);
  7189. });
  7190. });
  7191. }
  7192. console.log(`Showing currently active listeners for ${Object.keys(globservers).length} observers with ${listenersAmt} total listeners:\n${lines.join("\n")}`);
  7193. }), "s");
  7194. GM.registerMenuCommand("Compress value", () => __awaiter(this, void 0, void 0, function* () {
  7195. const input = prompt("Enter the value to compress.\nSee console for output.");
  7196. if (input && input.length > 0) {
  7197. const compressed = yield UserUtils.compress(input, compressionFormat);
  7198. console.log(`Compression result (${input.length} chars -> ${compressed.length} chars)\nValue: ${compressed}`);
  7199. }
  7200. }));
  7201. GM.registerMenuCommand("Decompress value", () => __awaiter(this, void 0, void 0, function* () {
  7202. const input = prompt("Enter the value to decompress.\nSee console for output.");
  7203. if (input && input.length > 0) {
  7204. const decompressed = yield UserUtils.decompress(input, compressionFormat);
  7205. console.log(`Decompresion result (${input.length} chars -> ${decompressed.length} chars)\nValue: ${decompressed}`);
  7206. }
  7207. }));
  7208. }
  7209. preInit();})(UserUtils,marked,Fuse);//# sourceMappingURL=http://localhost:8710/BetterYTM.user.js.map