Compare commits

..

28 Commits
3.7.2 ... 4.0

Author SHA1 Message Date
Ajay Ramachandran
fb74823c92 bump version 2022-01-06 20:59:54 -05:00
Ajay
024480513c Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-01-06 20:08:14 -05:00
Ajay
e9b217c685 Add tooltip about full video update 2022-01-06 20:08:12 -05:00
Ajay Ramachandran
7b917fb2b6 New Crowdin updates (#1112) 2022-01-06 16:57:12 -05:00
Ajay Ramachandran
2db35a624a Merge pull request #1116 from mchangrh/embeddedVideos
Support embedded videos
2022-01-06 16:50:58 -05:00
Ajay
c31866ff5f Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into pr/mchangrh/1116 2022-01-06 16:48:58 -05:00
Ajay Ramachandran
e347165073 Merge pull request #1122 from ajayyy/full-video
Full video labels
2022-01-06 16:40:23 -05:00
Ajay
57de51475f Add option to disable showing full video segments 2022-01-06 16:26:59 -05:00
Ajay
d16a409db2 Show black text on pill for unpaid promotion 2022-01-06 15:18:40 -05:00
Ajay
c63416fd7b Fix voting on category pill on mobile 2022-01-06 02:10:28 -05:00
Ajay
4d724deba3 Add title text and hide on downvote 2022-01-06 02:06:55 -05:00
Ajay
1aac863df0 Fix error 2022-01-06 01:54:47 -05:00
Ajay
c7d5011cc0 Add tooltip recommending full video report for large segments 2022-01-06 01:19:20 -05:00
Ajay
8e964b40b3 Add vote buttons to pill that open on click 2022-01-05 20:49:56 -05:00
Ajay
a6a9b7dd8c Decrease font size of pill 2022-01-05 17:26:05 -05:00
Ajay
d23e434209 Show full video on popup 2022-01-05 15:16:29 -05:00
Ajay
040bce2638 Make category pill work on invidious and mobile youtube 2022-01-05 15:13:42 -05:00
Ajay
388b9179ac Don't show full segments on preview bar 2022-01-05 02:39:13 -05:00
Ajay
2883a50f27 Add pill beside title for full video reports 2022-01-05 02:35:58 -05:00
Ajay
d36b4a54f3 Allow submitting as full video 2022-01-02 23:35:24 -05:00
Michael C
44bc8741ef fix UI issues with embeds
- add loadStart trigger to create & update preview and buttons
- show info button on /embed/ but not /channel/
2021-12-31 22:56:46 -05:00
Michael C
7a7b21cd87 add path for embedded videos and playlists 2021-12-31 18:17:15 -05:00
Michael C
229bd23a68 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into channelTrailer 2021-12-31 17:30:17 -05:00
Ajay
9b152a5525 Fix preview bar not being recreated 2021-12-28 20:46:31 -05:00
Ajay
7f374f0f86 Trigger changes even if videoid doesn't change if video element changes 2021-12-28 20:46:31 -05:00
Ajay
aca52abefc Make skip notice work on channel trailer 2021-12-28 20:46:31 -05:00
Michael C
6930980a4d set isInvidious to bypass UI bugs 2021-12-28 20:46:31 -05:00
Michael C
5af4833763 change parser to use document if applicable 2021-12-28 20:46:27 -05:00
26 changed files with 1011 additions and 214 deletions

View File

@@ -4,8 +4,8 @@
"serverAddressComment": "This specifies the default SponsorBlock server to connect to",
"categoryList": ["sponsor", "selfpromo", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "music_offtopic"],
"categorySupport": {
"sponsor": ["skip", "mute"],
"selfpromo": ["skip", "mute"],
"sponsor": ["skip", "mute", "full"],
"selfpromo": ["skip", "mute", "full"],
"interaction": ["skip", "mute"],
"intro": ["skip", "mute"],
"outro": ["skip", "mute"],

View File

@@ -1,7 +1,7 @@
{
"name": "__MSG_fullName__",
"short_name": "SponsorBlock",
"version": "3.7.2",
"version": "4.0",
"default_locale": "en",
"description": "__MSG_Description__",
"homepage_url": "https://sponsor.ajay.app",

View File

@@ -119,7 +119,7 @@
"message": "Er du sikker på, at du vil indsende dette?"
},
"whitelistChannel": {
"message": "Hvidliste kanal"
"message": "Hvidlist kanal"
},
"removeFromWhitelist": {
"message": "Fjern kanal fra hvidliste"
@@ -131,7 +131,7 @@
"message": "Indsendelser"
},
"savedPeopleFrom": {
"message": "Du har reddet folk fra "
"message": "Du har sparret folk "
},
"viewLeaderboard": {
"message": "Topliste"
@@ -430,12 +430,30 @@
"skipNoticeDuration": {
"message": "Spring meddelelsesvarighed over (sekunder):"
},
"skipNoticeDurationDescription": {
"message": "Overspringsmeddelelsen vil blive på skærmen i mindst så længe. For manuel spring, kan den være synlig i længere tid."
},
"shortCheck": {
"message": "Den følgende indsendelse er kortere end din minimums varighed indstilling. Dette kan betyde, at den allerede er indsendt, og bare bliver ignoreret på grund af denne indstilling. Er du sikker på, at du vil indsende?"
},
"showUploadButton": {
"message": "Vis Upload-Knap"
},
"customServerAddress": {
"message": "SponsorBlock Serveradresse"
},
"customServerAddressDescription": {
"message": "Adressen SponsorBlock bruger til at foretage opkald til serveren. Med mindre du har din egen serverinstans, bør dette ikke ændres."
},
"save": {
"message": "Gem"
},
"reset": {
"message": "Nulstil"
},
"customAddressError": {
"message": "Denne adresse er ikke i den rigtige form. Sørg for at du har http:// eller https:// i begyndelsen og ingen efterfølgende skråstreger."
},
"areYouSureReset": {
"message": "Er du sikker på, at du ønsker at nulstille dette?"
},
@@ -445,9 +463,18 @@
"exportOptions": {
"message": "Importer/Eksporter Alle Indstillinger"
},
"whatExportOptions": {
"message": "Dette er hele din konfiguration i JSON. Dette inkluderer dit bruger-ID, så sørg for at dele dette med omtanke."
},
"setOptions": {
"message": "Indstil Indstillinger"
},
"exportOptionsWarning": {
"message": "Advarsel: Ændring af indstillingerne er permanent, og kan ødelægge din installation. Er du sikker på, at du vil gøre dette? Sørg for at sikkerhedskopiere din gamle for en sikkerheds skyld."
},
"incorrectlyFormattedOptions": {
"message": "Denne JSON er ikke formateret korrekt. Dine indstillinger er ikke blevet ændret."
},
"confirmNoticeTitle": {
"message": "Indsend Segment"
},
@@ -472,9 +499,24 @@
"edit": {
"message": "Rediger"
},
"copyDebugInformation": {
"message": "Kopier Fejlretningsoplysninger Til Udklipsholder"
},
"copyDebugInformationFailed": {
"message": "Det lykkedes ikke at skrive til udklipsholderen"
},
"copyDebugInformationOptions": {
"message": "Kopierer information til udklipsholderen, der skal leveres til en udvikler, når en fejl indberettes / når en udvikler anmoder om det. Følsomme oplysninger som dit bruger-ID, hvidlistede kanaler og brugerdefineret serveradresse er blevet fjernet. Dog indeholder det oplysninger som din brugeragent, browser, operativsystem og versionsnummer for udvidelsen. "
},
"copyDebugInformationComplete": {
"message": "Fejlfindingsinformationen er blevet kopieret til klippebordet. Du er velkommen til at fjerne alle oplysninger, du helst ikke vil dele. Gem dette i en tekstfil eller indsæt i fejlrapporten."
},
"theKey": {
"message": "Tasten"
},
"keyAlreadyUsed": {
"message": "er bundet til en anden handling. Venligst vælg en anden nøgle."
},
"to": {
"message": "til",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -482,27 +524,115 @@
"category_sponsor": {
"message": "Sponsor"
},
"category_sponsor_description": {
"message": "Betalt kampagne, betalte henvisninger og direkte reklamer. Ikke for selvpromoverende eller gratis shoutouts til årsager/skabere/hjemmesider/produkter, de kan lide."
},
"category_selfpromo": {
"message": "Ubetalt/Egen Markedsføring"
},
"category_selfpromo_description": {
"message": "Ligesom \"sponsor\" bortset fra ubetalt- eller selfmarkedsføring. Dette inkluderer sektioner om merchandise, donationer eller oplysninger om hvem, de har samarbejdet med."
},
"category_interaction": {
"message": "Påmindelse Om Interaktion (Abonnement)"
},
"category_interaction_description": {
"message": "Når der er en kort påmindelse om at like, abonnere eller følge dem midt i indholdet. Hvis den er lang eller om noget specifikt, bør den i stedet være under selvpromovering."
},
"category_interaction_short": {
"message": "Påmindelse Om Interaktion"
},
"category_intro": {
"message": "Pause/Intro-Animation"
},
"category_intro_description": {
"message": "Et interval uden reelt indhold. Kunne være en pause, statisk ramme, gentagelse af animation. Dette bør ikke bruges til overgange som indeholder information."
},
"category_intro_short": {
"message": "Pause"
},
"category_outro": {
"message": "Slutkort/Kreditter"
},
"category_outro_description": {
"message": "Medvirkende eller når YouTube-endcards vises. Ikke for konklusioner med information."
},
"category_preview": {
"message": "Forhåndsvisning/Opsamling"
},
"category_preview_description": {
"message": "Hurtig opsummering af tidligere episoder eller en forsmag på, hvad der kommer senere i den aktuelle video. Er beregnet til sammenklippede klip, ikke til talte resuméer."
},
"category_filler": {
"message": "Fyldningstangent"
},
"category_filler_description": {
"message": "Tangential scener kun tilføjet for fyldstof eller humor, som ikke er nødvendige for at forstå videoens hovedindhold. Dette bør ikke omfatte segmenter, der gtiver kontekst eller bagrundsoplysninger."
},
"category_filler_short": {
"message": "Fyldstof"
},
"category_music_offtopic": {
"message": "Musik: Ikke-Musikalsk Sektion"
},
"category_music_offtopic_description": {
"message": "Kun til brug i musikvideoer. Dette bør kun bruges til sektioner af musikvideoer, der ikke allerede er dækket af en anden kategori."
},
"category_music_offtopic_short": {
"message": "Ikke-Musikalsk"
},
"category_poi_highlight": {
"message": "Fremhæv"
},
"category_poi_highlight_description": {
"message": "Den del af videoen, som de fleste mennesker leder efter. Svarende til \"Video starter ved x\" kommentarer."
},
"category_livestream_messages": {
"message": "Livestream: Donations-/Beskedsaflæsning"
},
"category_livestream_messages_short": {
"message": "Læsning Af Meddelelser"
},
"autoSkip": {
"message": "Auto Spring Over"
},
"manualSkip": {
"message": "Manuel Spring Over"
},
"showOverlay": {
"message": "Vis I Søgebar"
},
"disable": {
"message": "Deaktiver"
},
"autoSkip_POI": {
"message": "Spring automatisk til starten"
},
"manualSkip_POI": {
"message": "Spørg, når videoen indlæses"
},
"showOverlay_POI": {
"message": "Vis I Søgebar"
},
"autoSkipOnMusicVideos": {
"message": "Spring automatisk over alle segmenter, når der er et ikke-musik-segment"
},
"muteSegments": {
"message": "Tillad segmenter som dæmper lyden i stedet for at springe over"
},
"colorFormatIncorrect": {
"message": "Din farve er formateret forkert. Det skal være en 3-6 cifret hex-kode med et nummerskilt i begyndelsen."
},
"previewColor": {
"message": "Ikke-Indsendt Farve",
"description": "Referring to submissions that have not been sent to the server yet."
},
"seekBarColor": {
"message": "Søgebarsfarve"
},
"category": {
"message": "Kategori"
},
"skipOption": {
"message": "Spring Over Indstillinger",
"description": "Used on the options page to describe the ways to skip the segment (auto skip, manual, etc.)"
@@ -510,6 +640,12 @@
"enableTestingServer": {
"message": "Aktiver Betatestserver"
},
"whatEnableTestingServer": {
"message": "Dine indsendelser og stemmer TÆLLER IKKE med i hovedserveren. Brug kun dette til testformål."
},
"testingServerWarning": {
"message": "Alle indsendelser og stemmer TÆLLES IKKE med i hovedserveren, når du opretter forbindelse til testserveren. Sørg for at deaktivere dette, når du ønsker at foretage rigtige indsendelser."
},
"bracketNow": {
"message": "(Nu)"
},
@@ -519,6 +655,16 @@
"chooseACategory": {
"message": "Vælg en Kategori"
},
"enableThisCategoryFirst": {
"message": "Hvis du vil indsende segmenter med kategorien \"{0}\", skal du aktivere den i indstillingerne. Du vil blive omdirigeret til indstillingerne nu.",
"description": "Used when submitting segments to only let them select a certain category if they have it enabled in the options."
},
"poiOnlyOneSegment": {
"message": "Advarsel: Denne type segment kan have maksimalt en aktiv ad gangen. Indsendelse af flere vil få en tilfældig til at blive vist."
},
"youMustSelectACategory": {
"message": "Du skal vælge en kategori for alle segmenter, du indsender!"
},
"bracketEnd": {
"message": "(Slut)"
},
@@ -528,6 +674,16 @@
"hiddenDueToDuration": {
"message": "skjult: for kort"
},
"channelDataNotFound": {
"description": "This error appears in an alert when they try to whitelist a channel and the extension is unable to determine what channel they are looking at.",
"message": "Kanal-ID er ikke indlæst endnu. Hvis du bruger en integreret video, så prøv i stedet at bruge YouTube-hjemmesiden. Dette kunne også være forårsaget af ændringer i YouTube-layout. Hvis du mener det, så lav en kommentar her:"
},
"videoInfoFetchFailed": {
"message": "Det ser ud til, at noget blokerer SponsorBlock's evne til at hente videodata. Se https://github.com/ajayyy/SponsorBlock/issues/741 for mere info."
},
"youtubePermissionRequest": {
"message": "Det ser ud til, at SponsorBlock ikke kan nå YouTube APIen. Acceptér tilladelsesprompten som vises næste gang, vent et par sekunder, og genindlæs siden."
},
"acceptPermission": {
"message": "Accepter tilladelse"
},
@@ -537,12 +693,27 @@
"permissionRequestFailed": {
"message": "Tilladelsesanmodning mislykkedes, klikkede du på afvis?"
},
"adblockerIssueWhitelist": {
"message": "Hvis du ikke kan løse dette problem, skal du deaktivere indstillingen 'Tving Kanaltjek Inden Springning', da SponsorBlock ikke er i stand til at hente kanaloplysningerne for denne video"
},
"forceChannelCheck": {
"message": "Tving Kanaltjek Inden Springning"
},
"whatForceChannelCheck": {
"message": "Som standard vil den springe segmenter over med det samme, før den overhovedet ved, hvad kanalen er. Som standard kan nogle segmenter i starten af videoen blive sprunget over på kanaler på whitelisten. Hvis du aktiverer denne indstilling, forhindrer du dette, men det vil medføre en lille forsinkelse, da det kan tage noget tid at få kanal-ID'et. Denne forsinkelse kan være umærkelig, hvis du har hurtigt internet."
},
"forceChannelCheckPopup": {
"message": "Overvej At Aktivere \"Tving Kanaltjek Inden Springning\""
},
"downvoteDescription": {
"message": "Ukorrekt/Forkert Timing"
},
"incorrectCategory": {
"message": "Skift Kategori"
},
"nonMusicCategoryOnMusic": {
"message": "Denne video er kategoriseret som musik. Er du sikker på, at denne har en sponsor? Hvis dette faktisk er et \"Ikke-musik segment\", skal du åbne udvidelsesindstillingerne og aktivere denne kategori. Derefter kan du indsende dette segment som \"Ikke-musik\" i stedet for sponsor. Læs venligst retningslinjerne, hvis du er forvirret."
},
"multipleSegments": {
"message": "Adskillige Segmenter"
},
@@ -556,6 +727,9 @@
"categoryUpdate1": {
"message": "Kategorier er her!"
},
"categoryUpdate2": {
"message": "Åbn mulighederne for at springe intros, outros, merch osv. over."
},
"help": {
"message": "Hjælp"
},
@@ -570,6 +744,13 @@
"hideForever": {
"message": "Skjul for evigt"
},
"warningChatInfo": {
"message": "Du har fået en advarsel og kan midlertidigt ikke indsende segmenter. Det betyder, at vi har bemærket, at du har begået nogle almindelige fejl, som ikke er skadelige. Bekræft venligst, at du har forstået reglerne, så fjerner vi advarslen. Du kan også deltage i denne chat ved hjælp af discord.gg/SponsorBlock eller matrix.to/#/##sponsor:ajay.app"
},
"voteRejectedWarning": {
"message": "Afstemningen blev afvist på grund af en advarsel. Klik for at åbne en chat for at løse problemet, eller kom tilbage senere, når du har tid.",
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
},
"Donate": {
"message": "Doner"
},
@@ -582,13 +763,87 @@
"helpPageReviewOptions": {
"message": "Venligst gennemgå indstillingerne nedenfor"
},
"helpPageFeatureDisclaimer": {
"message": "Mange funktioner er deaktiveret som standard. Hvis du vil springe intros, outros over, bruge Invidious osv., skal du aktivere dem nedenfor. Du kan også skjule/vise brugergrænsefladeelementer."
},
"helpPageHowSkippingWorks": {
"message": "Hvordan spring over virker"
},
"helpPageHowSkippingWorks1": {
"message": "Videosegmenter vil automatisk blive sprunget over, hvis de findes i databasen. Du kan åbne popup-vinduet ved at klikke på ikonet for udvidelsen for at få et eksempel på, hvad de er."
},
"helpPageHowSkippingWorks2": {
"message": "Når du springer et segment over, får du besked, når du springer et segment over. Hvis timingen virker forkert, kan du stemme ned ved at klikke på downvote! Du kan også stemme i popup-vinduet."
},
"Submitting": {
"message": "Indsendelse"
},
"helpPageSubmitting1": {
"message": "Indsendelse kan enten ske i popup-vinduet ved at trykke på \"Segment Begynder Nu\"-knappen eller i videoafspilleren med knapperne på afspilleren."
},
"helpPageSubmitting2": {
"message": "Ved at klikke på play-knappen vises starten af et segment, og ved at klikke på stop-ikonet vises slutningen. Du kan forberede flere sponsorer, før du trykker på Send. Hvis du klikker på upload-knappen, sendes det. Hvis du klikker på skraldespanden, slettes den."
},
"Editing": {
"message": "Redigering"
},
"helpPageEditing1": {
"message": "Hvis du har lavet en fejl, kan du redigere eller slette dine segmenter, når du har klikket på pil op knappen."
},
"helpPageTooSlow": {
"message": "Det er for langsomt"
},
"helpPageTooSlow1": {
"message": "Der er genvejstaster, hvis du vil bruge dem. Tryk på semikolon-tasten for at angive start/slutning af en sponsor segment og klik på apostrof for at indsende. Disse kan ændres i valgmulighederne. Hvis du ikke bruger QWERTY, bør du sandsynligvis ændre tastebindingen."
},
"helpPageCopyOfDatabase": {
"message": "Kan jeg få en kopi af databasen? Hvad sker der, hvis du forsvinder?"
},
"helpPageCopyOfDatabase1": {
"message": "Databasen er offentlig og tilgængelig på"
},
"helpPageCopyOfDatabase2": {
"message": "Kildekoden er frit tilgængelig. Så selvom der sker noget med mig, går dine indsendelser ikke tabt."
},
"helpPageNews": {
"message": "Nyheder og hvordan det er lavet"
},
"helpPageSourceCode": {
"message": "Hvor kan jeg få kildekoden?"
},
"Credits": {
"message": "Anerkendelser"
},
"LearnMore": {
"message": "Læs mere"
},
"CopyDownvoteButtonInfo": {
"message": "Nedstemmer og opretter en lokal kopi for dig at genindsende"
},
"OpenCategoryWikiPage": {
"message": "Åbn denne kategoris wikiside."
},
"CopyAndDownvote": {
"message": "Kopier og nedstem"
},
"ContinueVoting": {
"message": "Fortsæt Afstemning"
},
"ChangeCategoryTooltip": {
"message": "Dette vil øjeblikkeligt gælde for dine indsendelser"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Brug musehjulet, mens du holder musen over redigeringsfeltet for hurtigt at justere tiden. Kombinationer af ctrl eller shift-tastenerne kan bruges til at finjustere ændringerne."
},
"fillerNewFeature": {
"message": "Nyt! Spring tangenter over og vittigheder med fyldstofkategorien. Aktivér i indstillinger"
},
"dayAbbreviation": {
"message": "d",
"description": "100d"
},
"hourAbbreviation": {
"message": "t",
"description": "100h"
}
}

View File

@@ -302,6 +302,10 @@
"mute": {
"message": "Mute"
},
"full": {
"message": "Full Video",
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
},
"skip_category": {
"message": "Skip {0}?"
},
@@ -620,6 +624,10 @@
"muteSegments": {
"message": "Allow segments that mute audio instead of skip"
},
"fullVideoSegments": {
"message": "Show an icon when a video is entirely an advertisement",
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
},
"colorFormatIncorrect": {
"message": "Your color is formatted incorrectly. It should be a 3 or 6 digit hex code with a number sign at the beginning."
},
@@ -737,6 +745,12 @@
"message": "Got it",
"description": "Used as the button to dismiss a tooltip"
},
"fullVideoTooltipWarning": {
"message": "This segment is large. If the whole video is about one topic, then change from \"Skip\" to \"Full Video\". See the guidelines for more information."
},
"categoryPillTitleText": {
"message": "This entire video is labeled as this category and is too tightly integrated to be able to separate"
},
"experiementOptOut": {
"message": "Opt-out of all future experiments",
"description": "This is used in a popup about a new experiment to get a list of unlisted videos to back up since all unlisted videos uploaded before 2017 will be set to private."
@@ -835,8 +849,8 @@
"SponsorTimeEditScrollNewFeature": {
"message": "Use your mousewheel while hovering over the edit box to quickly adjust the time. Combinations of the ctrl or shift key can be used to fine tune the changes."
},
"fillerNewFeature": {
"message": "New! Skip tangents and jokes with the filler category. Enable in options"
"categoryPillNewFeature": {
"message": "New! See when a video is entirely sponsored or self-promotion"
},
"dayAbbreviation": {
"message": "d",

View File

@@ -236,6 +236,12 @@
"noticeVisibilityMode2": {
"message": "Tüm Küçük Atlama Bildirimleri"
},
"noticeVisibilityMode3": {
"message": "Otomatik Atlama için Soluk Atlama Bildirimleri"
},
"noticeVisibilityMode4": {
"message": "Tüm Soluk Atlama Bildirimleri"
},
"longDescription": {
"message": "SponsorBlock, sponsorları, giriş ve bitiş kısımlarını, abonelik hatırlatıcılarını ve YouTube videolarının diğer can sıkıcı kısımlarını atlamanıza olanak tanır. SponsorBlock, herkesin sponsorlu kısımları ve YouTube videolarının diğer kısımlarının başlangıç ve bitiş zamanlarını göndermesine izin veren kitle kaynaklı bir tarayıcı uzantısıdır. Bir kişi bu bilgiyi gönderdikten sonra, bu uzantıya sahip diğer herkes sponsorlu kısımları hemen atlayacaktır. Müzik videolarının müzik dışı bölümlerini de atlayabilirsiniz.",
"description": "Full description of the extension on the store pages."
@@ -557,6 +563,15 @@
"category_preview_description": {
"message": "Önceki bölümlerin bir özeti veya geçerli videonun içeriğine yönelik bir ön izleme. Bu özellik birleştirilmiş klipler içindir, konuşarak anlatılan özetleri kapsamaz."
},
"category_filler": {
"message": "Alakasız Konu"
},
"category_filler_description": {
"message": "Videonun ana içeriğini anlamak için gerekli olmayan, yalnızca alakasız konu veya mizah için eklenen sahneler. Bu, alakalı veya arka plan ayrıntısı veren kısımları içermemelidir."
},
"category_filler_short": {
"message": "Alakasız Konu"
},
"category_music_offtopic": {
"message": "Müzik: Müzik Olmayan Bölüm"
},
@@ -693,6 +708,9 @@
"downvoteDescription": {
"message": "Hatalı/Yanlış Zaman"
},
"incorrectCategory": {
"message": "Kategoriyi değiştir"
},
"nonMusicCategoryOnMusic": {
"message": "Bu video müzik olarak sınıflandırılmıştır. Bunun bir sponsor olduğundan emin misin? Bu aslında bir \"Müzik Dışı bölüm\" ise, uzantı seçeneklerini açın ve bu kategoriyi etkinleştirin. Ardından, bu kısmı sponsor yerine \"Müzik Olmayan\" olarak gönderebilirsiniz. Kafanız karıştıysa lütfen yönergeleri okuyun."
},
@@ -726,6 +744,9 @@
"hideForever": {
"message": "Asla gösterme"
},
"warningChatInfo": {
"message": "Bir uyarı aldınız ve geçici olarak gönderim yapamazsınız. Bu, kötü niyetli olmayan bazı yaygın hatalar yaptığınızı fark ettiğimiz anlamına gelir, lütfen kuralları anladığınızı onaylayın, uyarıyı sonra kaldıracağız. Bu konuşmaya discord.gg/SponsorBlock ya da matrix.to/#/#sponsor:ajay.app kullanarak katılabilirsiniz."
},
"voteRejectedWarning": {
"message": "Bir uyarı nedeniyle oy reddedildi. Çözüm bulmak için buraya tıklayarak bir sohbet açın veya daha sonra vaktiniz olduğunda uğrayın.",
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
@@ -757,6 +778,12 @@
"Submitting": {
"message": "Gönderme"
},
"helpPageSubmitting1": {
"message": "Gönderi, açılır pencerede \"Kısım Şimdi Başlıyor\" düğmesine basılarak veya oynatıcıdaki düğmelerle video oynatıcıda yapılabilir."
},
"helpPageSubmitting2": {
"message": "Oynat düğmesine tıklamak bir kısımın başlangıcını, durdurma simgesine tıklamak ise bitişini gösterir. Gönder düğmesine basmadan önce birden fazla sponsor hazırlayabilirsiniz. Yükle düğmesine tıklamak kısımları gönderir. Çöp kutusuna tıkladığınızda silinir."
},
"Editing": {
"message": "Düzenleme"
},
@@ -766,6 +793,9 @@
"helpPageTooSlow": {
"message": "Bu fazla yavaş"
},
"helpPageTooSlow1": {
"message": "Kullanmak isterseniz kısayol tuşları var. Sponsorlu kısmın başlangıcını/sonunu belirtmek için noktalı virgül tuşuna basın ve göndermek için kesme işaretine tıklayın. Bu tuşlar ayarlarda değiştirilebilir. QWERTY klavye kullanmıyorsanız, tuş ayarlarını değiştirmelisiniz."
},
"helpPageCopyOfDatabase": {
"message": "Veri tabanının bir kopyasını alabilir miyim? Bir gün ortadan kaybolursanız ne olacak?"
},
@@ -775,6 +805,9 @@
"helpPageCopyOfDatabase2": {
"message": "Kaynak koduna serbestçe erişilebilir. Ben bir gün bu dünyada yalan olsam dahi, sizin gönderdiğiniz kısımlar kaybolmayacak."
},
"helpPageNews": {
"message": "Haberler ve nasıl yapılır"
},
"helpPageSourceCode": {
"message": "Kaynak koduna nereden ulaşabilirim?"
},
@@ -783,5 +816,34 @@
},
"LearnMore": {
"message": "Dahasını Öğren"
},
"CopyDownvoteButtonInfo": {
"message": "Olumsuz oy verir ve yeni bir kısım seçmeniz için bir kopya oluşturur"
},
"OpenCategoryWikiPage": {
"message": "Bu kategorinin wiki sayfasınıın."
},
"CopyAndDownvote": {
"message": "Kopyala ve olumsuz"
},
"ContinueVoting": {
"message": "Oylamaya devam et"
},
"ChangeCategoryTooltip": {
"message": "Bu, kısımlarınız için anında geçerli olur"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Zaman aralığını hızlı bir şekilde ayarlamak için düzenleme kutusunun üzerinde fare tekerini kullanın. Değişikliklere ince ayar yapmak için ctrl veya shift tuşunun kombinasyonları kullanılabilir."
},
"fillerNewFeature": {
"message": "Yeni! Alakasız Konu kategorisiyle boş muhabbetleri ve şakaları atlayın. Ayarlarda etkinleştir"
},
"dayAbbreviation": {
"message": "d",
"description": "100d"
},
"hourAbbreviation": {
"message": "h",
"description": "100h"
}
}

View File

@@ -586,6 +586,12 @@ input::-webkit-inner-spin-button {
max-width: 300px;
white-space: normal;
line-height: 1.5em;
color: white;
font-size: 12px;
}
.sponsorBlockTooltip a {
color: white;
}
.sponsorBlockTooltip::after {
@@ -613,3 +619,18 @@ input::-webkit-inner-spin-button {
line-height: 1.5em;
}
.sponsorBlockCategoryPill {
border-radius: 25px;
padding-left: 8px;
padding-right: 8px;
margin-right: 3px;
cursor: pointer;
font-size: 75%;
height: 100%;
align-items: center;
}
.sponsorBlockCategoryPillTitleSection {
display: flex;
align-items: center;
}

View File

@@ -66,6 +66,22 @@
<br/>
</div>
<div option-type="toggle" sync-option="fullVideoSegments">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_fullVideoSegments__
</div>
</label>
<br/>
<br/>
<br/>
</div>
<br/>
<br/>

View File

@@ -80,6 +80,9 @@ chrome.runtime.onInstalled.addListener(function () {
const newUserID = utils.generateUserID();
//save this UUID
Config.config.userID = newUserID;
// Don't show update notification
Config.config.categoryPillUpdate = true;
}
}, 1500);
});

View File

@@ -0,0 +1,107 @@
import * as React from "react";
import Config from "../config";
import { Category, SegmentUUID, SponsorTime } from "../types";
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
import { VoteResponse } from "../messageTypes";
import { AnimationUtils } from "../utils/animationUtils";
import { GenericUtils } from "../utils/genericUtils";
export interface CategoryPillProps {
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
}
export interface CategoryPillState {
segment?: SponsorTime;
show: boolean;
open?: boolean;
}
class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryPillState> {
constructor(props: CategoryPillProps) {
super(props);
this.state = {
segment: null,
show: false,
open: false
};
}
render(): React.ReactElement {
const style: React.CSSProperties = {
backgroundColor: Config.config.barTypes["preview-" + this.state.segment?.category]?.color,
display: this.state.show ? "flex" : "none",
color: this.state.segment?.category === "sponsor" ? "white" : "black",
}
return (
<span style={style}
className={"sponsorBlockCategoryPill"}
title={chrome.i18n.getMessage("categoryPillTitleText")}
onClick={(e) => this.toggleOpen(e)}>
<span className="sponsorBlockCategoryPillTitleSection">
<img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img>
<span className="sponsorBlockCategoryPillTitle">
{chrome.i18n.getMessage("category_" + this.state.segment?.category)}
</span>
</span>
{this.state.open && (
<>
{/* Upvote Button */}
<div id={"sponsorTimesDownvoteButtonsContainerUpvoteCategoryPill"}
className="voteButton"
style={{marginLeft: "5px"}}
title={chrome.i18n.getMessage("upvoteButtonInfo")}
onClick={(e) => this.vote(e, 1)}>
<ThumbsUpSvg fill={Config.config.colorPalette.white} />
</div>
{/* Downvote Button */}
<div id={"sponsorTimesDownvoteButtonsContainerDownvoteCategoryPill"}
className="voteButton"
title={chrome.i18n.getMessage("reportButtonInfo")}
onClick={(event) => this.vote(event, 0)}>
<ThumbsDownSvg fill={downvoteButtonColor(null, null, SkipNoticeAction.Downvote)} />
</div>
</>
)}
</span>
);
}
private toggleOpen(event: React.MouseEvent): void {
event.stopPropagation();
if (this.state.show) {
this.setState({ open: !this.state.open });
}
}
private async vote(event: React.MouseEvent, type: number): Promise<void> {
event.stopPropagation();
if (this.state.segment) {
const stopAnimation = AnimationUtils.applyLoadingAnimation(event.currentTarget as HTMLElement, 0.3);
const response = await this.props.vote(type, this.state.segment.UUID);
await stopAnimation();
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
this.setState({
open: false,
show: type === 1
});
} else if (response.statusCode !== 403) {
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
}
}
}
}
export default CategoryPillComponent;

View File

@@ -4,7 +4,6 @@ import Config from "../config"
import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import SubmissionNotice from "../render/SubmissionNotice";
import Utils from "../utils";
const utils = new Utils();
@@ -13,15 +12,7 @@ import { getCategoryActionType, getSkippingText } from "../utils/categoryUtils";
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
import PencilSvg from "../svg-icons/pencil_svg";
export enum SkipNoticeAction {
None,
Upvote,
Downvote,
CategoryVote,
CopyDownvote,
Unskip
}
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
export interface SkipNoticeProps {
segments: SponsorTime[];
@@ -216,7 +207,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
style={{marginRight: "5px", marginLeft: "5px"}}
title={chrome.i18n.getMessage("reportButtonInfo")}
onClick={() => this.prepAction(SkipNoticeAction.Downvote)}>
<ThumbsDownSvg fill={this.downvoteButtonColor(SkipNoticeAction.Downvote)} />
<ThumbsDownSvg fill={downvoteButtonColor(this.segments, this.state.actionState, SkipNoticeAction.Downvote)} />
</div>
{/* Copy and Downvote Button */}
@@ -279,7 +270,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
{/* Copy Segment */}
<button className="sponsorSkipObject sponsorSkipNoticeButton"
title={chrome.i18n.getMessage("CopyDownvoteButtonInfo")}
style={{color: this.downvoteButtonColor(SkipNoticeAction.Downvote)}}
style={{color: downvoteButtonColor(this.segments, this.state.actionState, SkipNoticeAction.Downvote)}}
onClick={() => this.prepAction(SkipNoticeAction.CopyDownvote)}>
{chrome.i18n.getMessage("CopyAndDownvote")}
</button>
@@ -727,16 +718,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
});
}
downvoteButtonColor(downvoteType: SkipNoticeAction): string {
// Also used for "Copy and Downvote"
if (this.segments.length > 1) {
return (this.state.actionState === downvoteType) ? this.selectedColor : this.unselectedColor;
} else {
// You dont have segment selectors so the lockbutton needs to be colored and cannot be selected.
return Config.config.isVip && this.segments[0].locked === 1 ? this.lockedColor : this.unselectedColor;
}
}
private getUnskipText(): string {
switch (this.props.segments[0].actionType) {
case ActionType.Mute: {

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import * as CompileConfig from "../../config.json";
import Config from "../config";
import { ActionType, ActionTypes, Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
import { ActionType, Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
import Utils from "../utils";
import { getCategoryActionType } from "../utils/categoryUtils";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
@@ -40,6 +40,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
previousSkipType: CategoryActionType;
timeBeforeChangingToPOI: number; // Initialized when first selecting POI
fullVideoWarningShown = false;
constructor(props: SponsorTimeEditProps) {
super(props);
@@ -73,6 +74,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.configUpdateListener = () => this.configUpdate();
Config.configListeners.push(this.configUpdate.bind(this));
}
this.checkToShowFullVideoWarning();
}
componentWillUnmount(): void {
@@ -82,6 +85,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
render(): React.ReactElement {
this.checkToShowFullVideoWarning();
const style: React.CSSProperties = {
textAlign: "center"
};
@@ -100,11 +105,14 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
};
// Create time display
let timeDisplay: JSX.Element;
const timeDisplayStyle: React.CSSProperties = {};
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
const segment = sponsorTime.segment;
if (sponsorTime?.actionType === ActionType.Full) timeDisplayStyle.display = "none";
if (this.state.editing) {
timeDisplay = (
<div id={"sponsorTimesContainer" + this.idSuffix}
style={timeDisplayStyle}
className="sponsorTimeDisplay">
<span id={"nowButton0" + this.idSuffix}
@@ -155,6 +163,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
timeDisplay = (
<div id={"sponsorTimesContainer" + this.idSuffix}
style={timeDisplayStyle}
className="sponsorTimeDisplay"
onClick={this.toggleEditTime.bind(this)}>
{utils.getFormattedTime(segment[0], true) +
@@ -246,7 +255,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
const after = utils.getFormattedTimeToSeconds(targetValue);
const difference = Math.abs(before - after);
if (0 < difference && difference< 0.5) this.showToolTip();
if (0 < difference && difference< 0.5) this.showScrollToEditToolTip();
sponsorTimeEdits[index] = targetValue;
if (index === 0 && getCategoryActionType(sponsorTime.category) === CategoryActionType.POI) sponsorTimeEdits[1] = targetValue;
@@ -254,6 +263,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.setState({sponsorTimeEdits});
this.saveEditTimes();
}
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void {
let step = 0;
// shift + ctrl = 1
@@ -284,11 +294,17 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
}
showToolTip(): void {
showScrollToEditToolTip(): void {
if (!Config.config.scrollToEditTimeUpdate && document.getElementById("sponsorRectangleTooltip" + "sponsorTimesContainer" + this.idSuffix) === null) {
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), () => { Config.config.scrollToEditTimeUpdate = true });
}
}
showToolTip(text: string, buttonFunction?: () => void): boolean {
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
if (element) {
new RectangleTooltip({
text: chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"),
text,
referenceNode: element.parentElement,
prependElement: element,
timeout: 15,
@@ -296,10 +312,27 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
leftOffset: -318 + "px",
backgroundColor: "rgba(28, 28, 28, 1.0)",
htmlId: "sponsorTimesContainer" + this.idSuffix,
buttonFunction: () => { Config.config.scrollToEditTimeUpdate = true },
buttonFunction,
fontSize: "14px",
maxHeight: "200px"
});
return true;
} else {
return false;
}
}
checkToShowFullVideoWarning(): void {
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
const segmentDuration = sponsorTime.segment[1] - sponsorTime.segment[0];
const videoPercentage = segmentDuration / this.props.contentContainer().v.duration;
if (videoPercentage > 0.6 && !this.fullVideoWarningShown
&& (sponsorTime.category === "sponsor" || sponsorTime.category === "selfpromo" || sponsorTime.category === "chooseACategory")) {
if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"))) {
this.fullVideoWarningShown = true;
}
}
}
@@ -444,6 +477,12 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting);
this.props.contentContainer().updatePreviewBar();
if (sponsorTimesSubmitting[this.props.index].actionType === ActionType.Full
&& (sponsorTimesSubmitting[this.props.index].segment[0] !== 0 || sponsorTimesSubmitting[this.props.index].segment[1] !== 0)) {
this.setTimeTo(0, 0);
this.setTimeTo(1, 0);
}
}
previewTime(ctrlPressed = false, shiftPressed = false): void {

View File

@@ -21,6 +21,7 @@ interface SBConfig {
showTimeWithSkips: boolean,
disableSkipping: boolean,
muteSegments: boolean,
fullVideoSegments: boolean,
trackViewCount: boolean,
trackViewCountInPrivate: boolean,
dontShowNotice: boolean,
@@ -52,6 +53,7 @@ interface SBConfig {
locked: string
},
scrollToEditTimeUpdate: boolean,
categoryPillUpdate: boolean,
// What categories should be skipped
categorySelections: CategorySelection[],
@@ -177,6 +179,7 @@ const Config: SBObject = {
showTimeWithSkips: true,
disableSkipping: false,
muteSegments: true,
fullVideoSegments: true,
trackViewCount: true,
trackViewCountInPrivate: true,
dontShowNotice: false,
@@ -203,6 +206,7 @@ const Config: SBObject = {
autoHideInfoButton: true,
autoSkipOnMusicVideos: false,
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
categoryPillUpdate: false,
categorySelections: [{
name: "sponsor" as Category,

View File

@@ -11,13 +11,16 @@ import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SubmissionNotice from "./render/SubmissionNotice";
import { Message, MessageResponse } from "./messageTypes";
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
import * as Chat from "./js-components/chat";
import { getCategoryActionType } from "./utils/categoryUtils";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { Tooltip } from "./render/Tooltip";
import { getStartTimeFromUrl } from "./utils/urlParser";
import { getControls } from "./utils/pageUtils";
import { findValidElement, getControls, isVisible } from "./utils/pageUtils";
import { CategoryPill } from "./render/CategoryPill";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@@ -75,9 +78,11 @@ let lastCheckVideoTime = -1;
//is this channel whitelised from getting sponsors skipped
let channelWhitelisted = false;
// create preview bar
let previewBar: PreviewBar = null;
// Skip to highlight button
let skipButtonControlBar: SkipButtonControlBar = null;
// For full video sponsors/selfpromo
let categoryPill: CategoryPill = null;
/** Element containing the player controls on the YouTube player. */
let controls: HTMLElement | null = null;
@@ -86,7 +91,8 @@ let controls: HTMLElement | null = null;
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement, setupListener: boolean}> = {};
// Direct Links after the config is loaded
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document.URL)));
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
addPageListeners();
addHotkeyListener();
//the amount of times the sponsor lookup has retried
@@ -137,7 +143,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
//messages from popup script
switch(request.message){
case "update":
videoIDChange(getYouTubeVideoID(document.URL));
videoIDChange(getYouTubeVideoID(document));
break;
case "sponsorStart":
startOrEndTimingNewSegment()
@@ -263,11 +269,12 @@ function resetValues() {
}
skipButtonControlBar?.disable();
categoryPill?.setVisibility(false);
}
async function videoIDChange(id) {
//if the id has not changed return
if (sponsorVideoID === id) return;
//if the id has not changed return unless the video element has changed
if (sponsorVideoID === id && isVisible(video)) return;
//set the global videoID
sponsorVideoID = id;
@@ -377,7 +384,7 @@ function createPreviewBar(): void {
];
for (const selector of progressElementSelectors) {
const el = document.querySelector<HTMLElement>(selector);
const el = findValidElement(document.querySelectorAll(selector));
if (el) {
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
@@ -398,6 +405,16 @@ function durationChangeListener(): void {
updatePreviewBar();
}
/**
* Triggered once the video is ready.
* This is mainly to attach to embedded players who don't have a video element visible.
*/
function videoOnReadyListener(): void {
createPreviewBar();
updatePreviewBar();
createButtons();
}
function cancelSponsorSchedule(): void {
if (currentSkipSchedule !== null) {
clearTimeout(currentSkipSchedule);
@@ -509,7 +526,7 @@ function inMuteSegment(currentTime: number): boolean {
* This makes sure the videoID is still correct and if the sponsorTime is included
*/
function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean {
const currentVideoID = getYouTubeVideoID(document.URL);
const currentVideoID = getYouTubeVideoID(document);
if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime
&& (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment))
&& !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) {
@@ -540,7 +557,7 @@ function setupVideoMutationListener() {
}
function refreshVideoAttachments() {
const newVideo = document.querySelector('video');
const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement;
if (newVideo && newVideo !== video) {
video = newVideo;
@@ -549,12 +566,22 @@ function refreshVideoAttachments() {
setupVideoListeners();
setupSkipButtonControlBar();
setupCategoryPill();
}
// Create a new bar in the new video element
if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
previewBar.remove();
previewBar = null;
createPreviewBar();
}
}
}
function setupVideoListeners() {
//wait until it is loaded
video.addEventListener('loadstart', videoOnReadyListener)
video.addEventListener('durationchange', durationChangeListener);
if (!Config.config.disableSkipping) {
@@ -637,8 +664,16 @@ function setupSkipButtonControlBar() {
skipButtonControlBar.attachToPage();
}
function setupCategoryPill() {
if (!categoryPill) {
categoryPill = new CategoryPill();
}
categoryPill.attachToPage(onMobileYouTube, onInvidious, voteAsync);
}
async function sponsorsLookup(id: string, keepOldSubmissions = true) {
if (!video) refreshVideoAttachments();
if (!video || !isVisible(video)) refreshVideoAttachments();
//there is still no video here
if (!video) {
setTimeout(() => sponsorsLookup(id), 100);
@@ -672,7 +707,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories,
actionTypes: Config.config.muteSegments ? [ActionType.Skip, ActionType.Mute] : [ActionType.Skip],
actionTypes: getEnabledActionTypes(),
userAgent: `${chrome.runtime.id}`,
...extraRequestData
});
@@ -753,6 +788,18 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
lookupVipInformation(id);
}
function getEnabledActionTypes(): ActionType[] {
const actionTypes = [ActionType.Skip];
if (Config.config.muteSegments) {
actionTypes.push(ActionType.Mute);
}
if (Config.config.fullVideoSegments) {
actionTypes.push(ActionType.Full);
}
return actionTypes;
}
function lookupVipInformation(id: string): void {
updateVipInfo().then((isVip) => {
if (isVip) {
@@ -864,6 +911,11 @@ function startSkipScheduleCheckingForStartSponsors() {
}
}
const fullVideoSegment = sponsorTimes.filter((time) => time.actionType === ActionType.Full)[0];
if (fullVideoSegment) {
categoryPill?.setSegment(fullVideoSegment);
}
if (startingSegmentTime !== -1) {
startSponsorSchedule(undefined, startingSegmentTime);
} else {
@@ -892,8 +944,30 @@ async function getVideoInfo(): Promise<void> {
}
}
function getYouTubeVideoID(url: string): string | boolean {
// For YouTube TV support
function getYouTubeVideoID(document: Document): string | boolean {
const url = document.URL;
// skip to URL if matches youtube watch or invidious or matches youtube pattern
if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url);
// skip to document and don't hide if on /embed/
if (url.includes("/embed/")) return getYouTubeVideoIDFromDocument(document, false);
// skip to document if matches pattern
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document);
// not sure, try URL then document
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document);
}
function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
// get ID from document (channel trailer / embedded playlist)
const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href");
if (videoURL) {
onInvidious = hideIcon;
return getYouTubeVideoIDFromURL(videoURL);
} else {
return false
}
}
function getYouTubeVideoIDFromURL(url: string): string | boolean {
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
//Attempt to parse url
@@ -913,7 +987,7 @@ function getYouTubeVideoID(url: string): string | boolean {
} else if (!["m.youtube.com", "www.youtube.com", "www.youtube-nocookie.com", "music.youtube.com"].includes(urlObject.host)) {
if (!Config.config) {
// Call this later, in case this is an Invidious tab
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoID(url)));
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoIDFromURL(url)));
}
return false
@@ -931,7 +1005,7 @@ function getYouTubeVideoID(url: string): string | boolean {
console.error("[SB] Video ID not valid for " + url);
return false;
}
}
}
return false;
}
@@ -963,6 +1037,7 @@ function updatePreviewBar(): void {
segment: segment.segment as [number, number],
category: segment.category,
unsubmitted: false,
actionType: segment.actionType,
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
});
});
@@ -973,11 +1048,12 @@ function updatePreviewBar(): void {
segment: segment.segment as [number, number],
category: segment.category,
unsubmitted: true,
actionType: segment.actionType,
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
});
});
previewBar.set(previewBarSegments, video?.duration)
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
if (Config.config.showTimeWithSkips) {
const skippedDuration = utils.getTimestampsDuration(previewBarSegments.map(({segment}) => segment));
@@ -1349,7 +1425,7 @@ async function createButtons(): Promise<void> {
&& playerButtons["info"]?.button && !controlsWithEventListeners.includes(controlsContainer)) {
controlsWithEventListeners.push(controlsContainer);
utils.setupAutoHideAnimation(playerButtons["info"].button, controlsContainer);
AnimationUtils.setupAutoHideAnimation(playerButtons["info"].button, controlsContainer);
}
}
@@ -1629,13 +1705,37 @@ function clearSponsorTimes() {
}
//if skipNotice is null, it will not affect the UI
function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) {
async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent): Promise<void> {
if (skipNotice !== null && skipNotice !== undefined) {
//add loading info
skipNotice.addVoteButtonInfo.bind(skipNotice)(chrome.i18n.getMessage("Loading"))
skipNotice.setNoticeInfoMessage.bind(skipNotice)();
}
const response = await voteAsync(type, UUID, category);
if (response != undefined) {
//see if it was a success or failure
if (skipNotice != null) {
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
//success (treat rate limits as a success)
skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
} else if (response.successType == -1) {
if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => {
Chat.openWarningChat(response.responseText);
skipNotice.closeListener.call(skipNotice);
}, chrome.i18n.getMessage("voteRejectedWarning"));
} else {
skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText))
}
skipNotice.resetVoteButtonInfo.bind(skipNotice)();
}
}
}
}
async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): Promise<VoteResponse> {
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
// Don't vote for preview sponsors
@@ -1655,33 +1755,14 @@ function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?:
Config.config.skipCount = Config.config.skipCount + factor;
}
chrome.runtime.sendMessage({
message: "submitVote",
type: type,
UUID: UUID,
category: category
}, function(response) {
if (response != undefined) {
//see if it was a success or failure
if (skipNotice != null) {
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
//success (treat rate limits as a success)
skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
} else if (response.successType == -1) {
if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => {
Chat.openWarningChat(response.responseText);
skipNotice.closeListener.call(skipNotice);
}, chrome.i18n.getMessage("voteRejectedWarning"));
} else {
skipNotice.setNoticeInfoMessage.bind(skipNotice)(utils.getErrorMessage(response.statusCode, response.responseText))
}
skipNotice.resetVoteButtonInfo.bind(skipNotice)();
}
}
}
return new Promise((resolve) => {
chrome.runtime.sendMessage({
message: "submitVote",
type: type,
UUID: UUID,
category: category
}, resolve);
});
}
@@ -1724,7 +1805,7 @@ function submitSponsorTimes() {
async function sendSubmitMessage() {
// Add loading animation
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker.svg");
const stopAnimation = utils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer());
const stopAnimation = AnimationUtils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer());
//check if a sponsor exceeds the duration of the video
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
@@ -1796,7 +1877,7 @@ async function sendSubmitMessage() {
if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a warning from a moderator.")) {
Chat.openWarningChat(response.responseText);
} else {
alert(utils.getErrorMessage(response.status, response.responseText));
alert(GenericUtils.getErrorMessage(response.status, response.responseText));
}
}
}
@@ -1823,6 +1904,16 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
return sponsorTimesMessage;
}
function addPageListeners(): void {
const refreshListners = () => {
if (!isVisible(video)) {
refreshVideoAttachments();
}
};
document.addEventListener("yt-navigate-finish", refreshListners);
}
function addHotkeyListener(): void {
document.addEventListener("keydown", hotkeyListener);
}

View File

@@ -6,6 +6,7 @@ https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd
'use strict';
import Config from "../config";
import { ActionType } from "../types";
import Utils from "../utils";
const utils = new Utils();
@@ -15,6 +16,7 @@ export interface PreviewBarSegment {
segment: [number, number];
category: string;
unsubmitted: boolean;
actionType: ActionType;
showLarger: boolean;
}

View File

@@ -3,6 +3,7 @@ import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils";
import Utils from "../utils";
import { AnimationUtils } from "../utils/animationUtils";
const utils = new Utils();
export interface SkipButtonControlBarProps {
@@ -80,9 +81,9 @@ export class SkipButtonControlBar {
}
if (!this.onMobileYouTube) {
utils.setupAutoHideAnimation(this.skipIcon, mountingContainer, false, false);
AnimationUtils.setupAutoHideAnimation(this.skipIcon, mountingContainer, false, false);
} else {
const { hide, show } = utils.setupCustomHideAnimation(this.skipIcon, mountingContainer, false, false);
const { hide, show } = AnimationUtils.setupCustomHideAnimation(this.skipIcon, mountingContainer, false, false);
this.hideButton = hide;
this.showButton = show;
}
@@ -104,7 +105,7 @@ export class SkipButtonControlBar {
this.refreshText();
this.textContainer?.classList?.remove("hidden");
utils.disableAutoHideAnimation(this.skipIcon);
AnimationUtils.disableAutoHideAnimation(this.skipIcon);
this.startTimer();
}
@@ -160,7 +161,7 @@ export class SkipButtonControlBar {
this.getChapterPrefix()?.classList?.add("hidden");
utils.enableAutoHideAnimation(this.skipIcon);
AnimationUtils.enableAutoHideAnimation(this.skipIcon);
if (this.onMobileYouTube) {
this.hideButton();
}

View File

@@ -61,3 +61,8 @@ export type MessageResponse =
| IsChannelWhitelistedResponse
| Record<string, never>;
export interface VoteResponse {
successType: number;
statusCode: number;
responseText: string;
}

View File

@@ -1,10 +1,12 @@
import Config from "./config";
import Utils from "./utils";
import { SponsorTime, SponsorHideType, CategoryActionType } from "./types";
import { SponsorTime, SponsorHideType, CategoryActionType, ActionType } from "./types";
import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { getCategoryActionType } from "./utils/categoryUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
const utils = new Utils();
interface MessageListener {
@@ -405,10 +407,15 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo);
const segmentTimeFromToNode = document.createElement("div");
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
if (segmentTimes[i].actionType === ActionType.Full) {
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
} else {
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
(getCategoryActionType(segmentTimes[i].category) !== CategoryActionType.POI
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
: "");
}
segmentTimeFromToNode.style.margin = "5px";
sponsorTimeButton.appendChild(categoryColorCircle);
@@ -444,7 +451,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
uuidButton.addEventListener("click", () => {
navigator.clipboard.writeText(UUID);
const stopAnimation = utils.applyLoadingAnimation(uuidButton, 0.3);
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
stopAnimation();
});
@@ -550,7 +557,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.sponsorTimesContributionsContainer.classList.remove("hidden");
} else {
PageElements.setUsernameStatus.innerText = utils.getErrorMessage(response.status, response.responseText);
PageElements.setUsernameStatus.innerText = GenericUtils.getErrorMessage(response.status, response.responseText);
}
});
@@ -591,7 +598,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//success (treat rate limits as a success)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
} else if (response.successType == -1) {
addVoteMessage(utils.getErrorMessage(response.statusCode, response.responseText), UUID);
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
}
});
@@ -694,7 +701,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
function refreshSegments() {
const stopAnimation = utils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
messageHandler.query({
active: true,

116
src/render/CategoryPill.tsx Normal file
View File

@@ -0,0 +1,116 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import CategoryPillComponent, { CategoryPillState } from "../components/CategoryPillComponent";
import Config from "../config";
import { VoteResponse } from "../messageTypes";
import { Category, SegmentUUID, SponsorTime } from "../types";
import { GenericUtils } from "../utils/genericUtils";
import { Tooltip } from "./Tooltip";
export class CategoryPill {
container: HTMLElement;
ref: React.RefObject<CategoryPillComponent>;
unsavedState: CategoryPillState;
mutationObserver?: MutationObserver;
constructor() {
this.ref = React.createRef();
}
async attachToPage(onMobileYouTube: boolean, onInvidious: boolean,
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>): Promise<void> {
const referenceNode =
await GenericUtils.wait(() =>
// YouTube, Mobile YouTube, Invidious
document.querySelector(".ytd-video-primary-info-renderer.title, .slim-video-information-title, #player-container + .h-box > h1") as HTMLElement);
if (referenceNode && !referenceNode.contains(this.container)) {
this.container = document.createElement('span');
this.container.id = "categoryPill";
this.container.style.display = "relative";
referenceNode.prepend(this.container);
referenceNode.style.display = "flex";
if (this.ref.current) {
this.unsavedState = this.ref.current.state;
}
ReactDOM.render(
<CategoryPillComponent ref={this.ref} vote={vote} />,
this.container
);
if (this.unsavedState) {
this.ref.current?.setState(this.unsavedState);
this.unsavedState = null;
}
if (onMobileYouTube) {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
this.mutationObserver = new MutationObserver(() => this.attachToPage(onMobileYouTube, onInvidious, vote));
this.mutationObserver.observe(referenceNode, {
childList: true,
subtree: true
});
}
}
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.container.remove();
}
setVisibility(show: boolean): void {
const newState = {
show,
open: show ? this.ref.current?.state.open : false
};
if (this.ref.current) {
this.ref.current?.setState(newState);
} else {
this.unsavedState = newState;
}
}
async setSegment(segment: SponsorTime): Promise<void> {
if (this.ref.current?.state?.segment !== segment) {
const newState = {
segment,
show: true,
open: false
};
if (this.ref.current) {
this.ref.current?.setState(newState);
} else {
this.unsavedState = newState;
}
if (!Config.config.categoryPillUpdate) {
Config.config.categoryPillUpdate = true;
const watchDiv = await GenericUtils.wait(() => document.querySelector("#info.ytd-watch-flexy") as HTMLElement);
if (watchDiv) {
new Tooltip({
text: chrome.i18n.getMessage("categoryPillNewFeature"),
link: "https://blog.ajay.app/full-video-sponsorblock",
referenceNode: watchDiv,
prependElement: watchDiv.firstChild as HTMLElement,
bottomOffset: "-10px",
opacity: 0.95,
timeout: 50000
});
}
}
}
}
}

View File

@@ -4,9 +4,10 @@ import * as ReactDOM from "react-dom";
import Utils from "../utils";
const utils = new Utils();
import SkipNoticeComponent, { SkipNoticeAction } from "../components/SkipNoticeComponent";
import SkipNoticeComponent from "../components/SkipNoticeComponent";
import { SponsorTime, ContentContainer, NoticeVisbilityMode } from "../types";
import Config from "../config";
import { SkipNoticeAction } from "../utils/noticeUtils";
class SkipNotice {
segments: SponsorTime[];

View File

@@ -8,6 +8,7 @@ export interface TooltipProps {
prependElement?: HTMLElement, // Element to append before
bottomOffset?: string
timeout?: number;
opacity?: number;
}
export class Tooltip {
@@ -18,11 +19,12 @@ export class Tooltip {
constructor(props: TooltipProps) {
props.bottomOffset ??= "70px";
props.opacity ??= 0.7;
this.text = props.text;
this.container = document.createElement('div');
this.container.id = "sponsorTooltip" + props.text;
this.container.style.display = "relative";
this.container.style.position = "relative";
if (props.prependElement) {
props.referenceNode.insertBefore(this.container, props.prependElement);
@@ -34,8 +36,10 @@ export class Tooltip {
this.timer = setTimeout(() => this.close(), props.timeout * 1000);
}
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
ReactDOM.render(
<div style={{bottom: props.bottomOffset}}
<div style={{bottom: props.bottomOffset, backgroundColor}}
className="sponsorBlockTooltip" >
<div>
<img className="sponsorSkipLogo sponsorSkipObject"

View File

@@ -59,7 +59,8 @@ export enum CategoryActionType {
export enum ActionType {
Skip = "skip",
Mute = "mute"
Mute = "mute",
Full = "full"
}
export const ActionTypes = [ActionType.Skip, ActionType.Mute];

View File

@@ -2,6 +2,8 @@ import Config from "./config";
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration } from "./types";
import * as CompileConfig from "../config.json";
import { findValidElementFromSelector } from "./utils/pageUtils";
import { GenericUtils } from "./utils/genericUtils";
export default class Utils {
@@ -23,27 +25,8 @@ export default class Utils {
this.backgroundScriptContainer = backgroundScriptContainer;
}
/** Function that can be used to wait for a condition before returning. */
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
return await new Promise((resolve, reject) => {
setTimeout(() => {
clearInterval(interval);
reject("TIMEOUT");
}, timeout);
const intervalCheck = () => {
const result = condition();
if (result !== false) {
resolve(result);
clearInterval(interval);
}
};
const interval = setInterval(intervalCheck, check);
//run the check once first, this speeds it up a lot
intervalCheck();
});
return GenericUtils.wait(condition, timeout, check);
}
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
@@ -161,75 +144,6 @@ export default class Utils {
});
}
/**
* Starts a spinning animation and returns a function to be called when it should be stopped
* The callback will be called when the animation is finished
* It waits until a full rotation is complete
*/
applyLoadingAnimation(element: HTMLElement, time: number, callback?: () => void): () => void {
element.style.animation = `rotate ${time}s 0s infinite`;
return () => {
// Make the animation finite
element.style.animation = `rotate ${time}s`;
// When the animation is over, hide the button
const animationEndListener = () => {
if (callback) callback();
element.style.animation = "none";
element.removeEventListener("animationend", animationEndListener);
};
element.addEventListener("animationend", animationEndListener);
}
}
setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void, show: () => void } {
if (enabled) element.classList.add("autoHiding");
element.classList.add("hidden");
element.classList.add("animationDone");
if (!rightSlide) element.classList.add("autoHideLeft");
let mouseEntered = false;
return {
hide: () => {
mouseEntered = false;
if (element.classList.contains("autoHiding")) {
element.classList.add("hidden");
}
},
show: () => {
mouseEntered = true;
element.classList.remove("animationDone");
// Wait for next event loop
setTimeout(() => {
if (mouseEntered) element.classList.remove("hidden")
}, 10);
}
};
}
setupAutoHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): void {
const { hide, show } = this.setupCustomHideAnimation(element, container, enabled, rightSlide);
container.addEventListener("mouseleave", () => hide());
container.addEventListener("mouseenter", () => show());
}
enableAutoHideAnimation(element: Element): void {
element.classList.add("autoHiding");
element.classList.add("hidden");
}
disableAutoHideAnimation(element: Element): void {
element.classList.remove("autoHiding");
element.classList.remove("hidden");
}
/**
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
*/
@@ -361,29 +275,6 @@ export default class Utils {
}
}
/**
* Gets the error message in a nice string
*
* @param {int} statusCode
* @returns {string} errorMessage
*/
getErrorMessage(statusCode: number, responseText: string): string {
let errorMessage = "";
const postFix = (responseText ? "\n\n" + responseText : "");
if([400, 429, 409, 502, 503, 0].includes(statusCode)) {
//treat them the same
if (statusCode == 503) statusCode = 502;
errorMessage = chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
+ "\n\n" + chrome.i18n.getMessage("statusReminder");
} else {
errorMessage = chrome.i18n.getMessage("connectionError") + statusCode;
}
return errorMessage + postFix;
}
/**
* Sends a request to a custom server
*
@@ -439,11 +330,15 @@ export default class Utils {
}
findReferenceNode(): HTMLElement {
let referenceNode = document.getElementById("player-container-id")
?? document.getElementById("movie_player")
?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music
?? document.querySelector("#player-container .video-js") // Invidious
?? document.querySelector(".main-video-section > .video-container"); // Cloudtube
const selectors = [
"#player-container-id",
"#movie_player",
"#c4-player", // Channel Trailer
"#main-panel.ytmusic-player-page", // YouTube music
"#player-container .video-js", // Invidious
".main-video-section > .video-container" // Cloudtube
]
let referenceNode = findValidElementFromSelector(selectors)
if (referenceNode == null) {
//for embeds
const player = document.getElementById("player");

View File

@@ -0,0 +1,78 @@
/**
* Starts a spinning animation and returns a function to be called when it should be stopped
* The callback will be called when the animation is finished
* It waits until a full rotation is complete
*/
function applyLoadingAnimation(element: HTMLElement, time: number, callback?: () => void): () => Promise<void> {
element.style.animation = `rotate ${time}s 0s infinite`;
return async () => new Promise((resolve) => {
// Make the animation finite
element.style.animation = `rotate ${time}s`;
// When the animation is over, hide the button
const animationEndListener = () => {
if (callback) callback();
element.style.animation = "none";
element.removeEventListener("animationend", animationEndListener);
resolve();
};
element.addEventListener("animationend", animationEndListener);
});
}
function setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void, show: () => void } {
if (enabled) element.classList.add("autoHiding");
element.classList.add("hidden");
element.classList.add("animationDone");
if (!rightSlide) element.classList.add("autoHideLeft");
let mouseEntered = false;
return {
hide: () => {
mouseEntered = false;
if (element.classList.contains("autoHiding")) {
element.classList.add("hidden");
}
},
show: () => {
mouseEntered = true;
element.classList.remove("animationDone");
// Wait for next event loop
setTimeout(() => {
if (mouseEntered) element.classList.remove("hidden")
}, 10);
}
};
}
function setupAutoHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): void {
const { hide, show } = this.setupCustomHideAnimation(element, container, enabled, rightSlide);
container.addEventListener("mouseleave", () => hide());
container.addEventListener("mouseenter", () => show());
}
function enableAutoHideAnimation(element: Element): void {
element.classList.add("autoHiding");
element.classList.add("hidden");
}
function disableAutoHideAnimation(element: Element): void {
element.classList.remove("autoHiding");
element.classList.remove("hidden");
}
export const AnimationUtils = {
applyLoadingAnimation,
setupAutoHideAnimation,
setupCustomHideAnimation,
enableAutoHideAnimation,
disableAutoHideAnimation
};

50
src/utils/genericUtils.ts Normal file
View File

@@ -0,0 +1,50 @@
/** Function that can be used to wait for a condition before returning. */
async function wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
return await new Promise((resolve, reject) => {
setTimeout(() => {
clearInterval(interval);
reject("TIMEOUT");
}, timeout);
const intervalCheck = () => {
const result = condition();
if (result) {
resolve(result);
clearInterval(interval);
}
};
const interval = setInterval(intervalCheck, check);
//run the check once first, this speeds it up a lot
intervalCheck();
});
}
/**
* Gets the error message in a nice string
*
* @param {int} statusCode
* @returns {string} errorMessage
*/
function getErrorMessage(statusCode: number, responseText: string): string {
let errorMessage = "";
const postFix = (responseText ? "\n\n" + responseText : "");
if([400, 429, 409, 502, 503, 0].includes(statusCode)) {
//treat them the same
if (statusCode == 503) statusCode = 502;
errorMessage = chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
+ "\n\n" + chrome.i18n.getMessage("statusReminder");
} else {
errorMessage = chrome.i18n.getMessage("connectionError") + statusCode;
}
return errorMessage + postFix;
}
export const GenericUtils = {
wait,
getErrorMessage
}

21
src/utils/noticeUtils.ts Normal file
View File

@@ -0,0 +1,21 @@
import Config from "../config";
import { SponsorTime } from "../types";
export enum SkipNoticeAction {
None,
Upvote,
Downvote,
CategoryVote,
CopyDownvote,
Unskip
}
export function downvoteButtonColor(segments: SponsorTime[], actionState: SkipNoticeAction, downvoteType: SkipNoticeAction): string {
// Also used for "Copy and Downvote"
if (segments?.length > 1) {
return (actionState === downvoteType) ? Config.config.colorPalette.red : Config.config.colorPalette.white;
} else {
// You dont have segment selectors so the lockbutton needs to be colored and cannot be selected.
return Config.config.isVip && segments[0].locked === 1 ? Config.config.colorPalette.locked : Config.config.colorPalette.white;
}
}

View File

@@ -17,4 +17,27 @@ export function getControls(): HTMLElement | false {
}
return false;
}
export function isVisible(element: HTMLElement): boolean {
return element && element.offsetWidth > 0 && element.offsetHeight > 0;
}
export function findValidElementFromSelector(selectors: string[]): HTMLElement {
return findValidElementFromGenerator(selectors, (selector) => document.querySelector(selector));
}
export function findValidElement(elements: HTMLElement[] | NodeListOf<HTMLElement>): HTMLElement {
return findValidElementFromGenerator(elements);
}
function findValidElementFromGenerator<T>(objects: T[] | NodeListOf<HTMLElement>, generator?: (obj: T) => HTMLElement): HTMLElement {
for (const obj of objects) {
const element = generator ? generator(obj as T) : obj as HTMLElement;
if (element && isVisible(element)) {
return element;
}
}
return null;
}