Compare commits

...

79 Commits
5.0.5 ... 5.1.4

Author SHA1 Message Date
Ajay Ramachandran
8653059b13 bump version 2022-10-30 22:00:35 -04:00
Ajay
b3afd0403e Add configuration for segment failed to fetch warning 2022-10-30 20:38:48 -04:00
Ajay
6db498ccb1 Fix key moments check not working when multiple videos present 2022-10-30 20:36:05 -04:00
Ajay
ef8c5f58c5 Fix scrubbing bar missing when chapter bar using % widths 2022-10-30 14:49:19 -04:00
Ajay
71998831ee bump version 2022-10-30 13:23:12 -04:00
Ajay
8f19d3e83c Fix segment failed to fetch warning appearing for 404 2022-10-30 13:23:02 -04:00
Ajay
d68c3659be bump version 2022-10-27 21:47:00 -04:00
Ajay
715bcb6bd3 Added error when segments haven't loaded and improved popup message
Resolves #1553
2022-10-27 21:46:47 -04:00
Ajay
fea8a9a37e Disable virtual time in firefox again 2022-10-27 21:39:06 -04:00
Ajay
aec5845bce Show import button for errors 2022-10-17 09:25:11 -04:00
Ajay
5209c0ea04 Import chapters as chooseACategory if chapters is disabled 2022-10-17 09:23:59 -04:00
Ajay
b52132e311 Open submission men after importing 2022-10-17 08:52:55 -04:00
Ajay
c2e731ef89 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-17 08:50:56 -04:00
Ajay
a48269f254 Import short category names too 2022-10-17 08:50:55 -04:00
Ajay Ramachandran
562adb9d55 bump version 2022-10-16 22:41:46 -04:00
Ajay Ramachandran
851ceb553d New translations messages.json (Korean) (#1529) 2022-10-16 22:41:31 -04:00
Ajay
4bd7e9ab34 Fix progress bar with 0px chapters 2022-10-14 18:23:40 -04:00
Ajay
97fc8174b9 Add flag to disable virtual time 2022-10-12 23:48:20 -04:00
Ajay
9849c34ed3 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-12 23:46:02 -04:00
Ajay
56be762686 Add back virtual time to firefox and fix it with playback speed 2022-10-12 23:46:00 -04:00
Ajay Ramachandran
1648e104e1 bump version 2022-10-11 20:57:54 -04:00
Ajay Ramachandran
c4d7c4511e New Crowdin updates (#1511) 2022-10-11 20:57:36 -04:00
Ajay Ramachandran
cd78c46ef8 Merge pull request #1521 from mini-bomba/popup
Update popup on segment updates + some code cleanup
2022-10-11 20:57:21 -04:00
Ajay
758b6f18db update buttons when single time segment is edited 2022-10-11 18:30:53 -04:00
Ajay
6d05b2a849 Set endtime by default for outro 2022-10-11 18:24:52 -04:00
Ajay
4729268083 Reimport chapters if they are found to have changed 2022-10-11 17:59:46 -04:00
mini-bomba
b7a574fc16 Clear segment list & show loading animation in popup on video change
also removed the creatingSegment variable - results in "Start/End Segment Now" correctly updating when using buttons on the controls panel instead
also the "refreshSegments" message no longer sends a response, as we send an update manually now
2022-10-11 18:38:00 +02:00
mini-bomba
c8cbd893f7 Don't hide the popup on video change
also hide the info button if the popup was open when setting up buttons
2022-10-11 16:08:07 +02:00
mini-bomba
11b01fd3dd revert reordering of imports 2022-10-11 15:41:19 +02:00
Ajay
85e3d3bc18 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-11 00:08:16 -04:00
Ajay
47ea8cd017 Fix unused import 2022-10-11 00:08:14 -04:00
Ajay Ramachandran
61e78eb668 Merge pull request #1524 from caneleex/patch/full-type-skip
don't append skip button for full video labels
2022-10-11 00:07:38 -04:00
Ajay
5ebd44c0c7 update category pill for react 18 2022-10-11 00:04:02 -04:00
Ajay
9888dcc323 Fix chapter import not working with 0 time 2022-10-10 23:37:48 -04:00
Ajay
49a166a6b2 Fix key moments importing as chapters 2022-10-10 23:34:34 -04:00
caneleex
2bd1271575 don't append skip button for full video labels 2022-10-10 21:21:57 +02:00
Ajay
5d62b11a6d Enable chapters if not enabled after redeem successful 2022-10-10 00:38:09 -04:00
Ajay
f4cac58322 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-09 16:44:55 -04:00
Ajay
4a532e751c Add class for other extensions when displaying non chapter title 2022-10-09 16:44:53 -04:00
mini-bomba
4a3b33cb85 Dedupe & clean up popup -> content script communication code 2022-10-08 19:07:58 +02:00
mini-bomba
78e9f41854 Update popup when new segments are fetched 2022-10-08 18:34:20 +02:00
mini-bomba
48cfee57b7 Return null on fails in getYouTubeVideoID and add type annotations to videoIDChange 2022-10-08 16:40:48 +02:00
Ajay Ramachandran
b2ef9e5d6e Merge pull request #1518 from mchangrh/interface-delimiter
Interface delimiter
2022-10-08 10:07:58 -04:00
Ajay Ramachandran
c42ebce6e3 Merge pull request #1520 from mini-bomba/fix/empty-unsubmitted-video
Remove the unsubmittedSegments entry when removing the last segment
2022-10-08 10:07:14 -04:00
mini-bomba
bc1d6006eb Remove the unsubmittedSegments entry when removing the last segment 2022-10-08 13:10:25 +02:00
Ajay Ramachandran
727d925879 Merge pull request #1519 from ajayyy/ci/oss_attribution
Update OSS Attribution
2022-10-07 22:56:46 -04:00
github-actions[bot]
5d48d9ac74 Update OSS Attribution 2022-10-08 02:55:47 +00:00
Ajay Ramachandran
83ea183f58 Merge pull request #1517 from mchangrh/react-18
React 18
2022-10-07 22:54:51 -04:00
Michael C
a098858035 force delimiters to follow semi 2022-10-07 20:51:58 -04:00
Michael C
81e85c19ae fix typings and revert spacing 2022-10-07 20:06:03 -04:00
Michael C
fda4a03541 fix more components 2022-10-07 19:51:05 -04:00
Michael C
55c84662c0 add some changes that are rendering properly 2022-10-07 19:28:29 -04:00
Ajay Ramachandran
5c9e06468e Merge pull request #1515 from mchangrh/dependency-update
update dependencies (skips react (again))
2022-10-07 17:24:58 -04:00
Ajay
a3d38c57d7 Fix chapter icon with ytp big mode 2022-10-07 17:20:19 -04:00
Michael C
7ec09dd385 update dependencies (skips react) 2022-10-07 17:19:36 -04:00
Ajay
bb7f069254 Only check import duplicates against unsubmitted segments 2022-10-07 12:22:39 -04:00
Ajay
61fc1d2ed3 Fix zero start time breaking 2022-10-07 12:19:42 -04:00
Ajay
dabc63af73 Deletew all custom chapter bars we don't know about when clearing preview bar 2022-10-07 12:02:14 -04:00
Ajay
08181c1d5f Clear bote buttons when clearing preview bar 2022-10-07 11:59:56 -04:00
Ajay
03cd1b535b Count skips for chapters when viewed 2022-10-05 15:39:51 -04:00
Ajay
8cc3843ada Run chapters clear when preview bar cleared 2022-10-05 02:19:32 -04:00
Ajay
c4701092f4 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-05 02:15:32 -04:00
Ajay
63ef9b44c7 Make sure chapters are invalid if regex fails 2022-10-05 02:15:31 -04:00
Ajay Ramachandran
d564742339 bump version 2022-10-05 01:42:04 -04:00
Ajay Ramachandran
6477e4c1f8 New Crowdin updates (#1495) 2022-10-05 01:41:15 -04:00
Ajay Ramachandran
eb8a0ae307 Merge pull request #1498 from mchangrh/1496-fix
fix #1496
2022-10-05 01:02:37 -04:00
Ajay
5296c437cc Make the clip ignore less sketchy 2022-10-05 00:55:19 -04:00
Ajay
36efe139ba Fix wait for element not working on embed, causing segments not to load
Fix #1497
2022-10-04 22:08:19 -04:00
Ajay
eaabd3149e Fixing stacking timeout issue 2022-10-04 17:50:23 -04:00
Ajay
6166ab3006 Fix spamming user info on options page and improve popup values 2022-10-03 16:59:49 -04:00
Ajay
f1498d51fa Disable show info message if chapter enabled 2022-10-02 21:45:00 -04:00
Ajay
3aabc0d051 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-09-30 21:00:46 -04:00
Ajay
28904935da Remove chance from chapter tooltip 2022-09-30 21:00:45 -04:00
Ajay Ramachandran
35d83d257c bump version 2022-09-30 14:51:43 -04:00
Ajay Ramachandran
92e078b87c Merge pull request #1501 from mchangrh/fix-vip-lockcategories
fix VIP LockCategories
2022-09-29 00:01:15 -04:00
Michael C
75accad06e locked colour is important 2022-09-28 18:49:45 -04:00
Ajay
a99823d487 Fix import menu hidden with no segments
Fix #1499
2022-09-28 15:17:05 -04:00
Ajay
1ba1595d0e Swap inspect and preview back 2022-09-27 14:47:11 -04:00
Michael C
8f7408d815 fix #1496
- force return bad videoID on clip
- don't createbuttons on video ready, updateVisibility with more checks
- don't switch to falsy videoIDs if already available
2022-09-27 00:59:24 -04:00
65 changed files with 3958 additions and 2595 deletions

View File

@@ -23,7 +23,8 @@
"@typescript-eslint/no-unused-vars": "error",
"no-self-assign": "off",
"@typescript-eslint/no-empty-interface": "off",
"react/prop-types": [2, { "ignore": ["children"] }]
"react/prop-types": [2, { "ignore": ["children"] }],
"@typescript-eslint/member-delimiter-style": "warn"
},
"settings": {
"react": {

View File

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

File diff suppressed because one or more lines are too long

4136
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,36 +4,36 @@
"description": "",
"main": "background.js",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/chrome": "^0.0.193",
"@types/chrome": "^0.0.197",
"@types/firefox-webext-browser": "^94.0.1",
"@types/jest": "^28.1.6",
"@types/react": "^17.0.47",
"@types/react-dom": "^17.0.17",
"@types/selenium-webdriver": "^4.1.2",
"@types/wicg-mediasession": "^1.1.3",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"chromedriver": "^103.0.0",
"concurrently": "^7.3.0",
"@types/jest": "^29.1.2",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/selenium-webdriver": "^4.1.5",
"@types/wicg-mediasession": "^1.1.4",
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"chromedriver": "^106.0.1",
"concurrently": "^7.4.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.20.0",
"eslint-plugin-react": "^7.30.1",
"eslint": "^8.24.0",
"eslint-plugin-react": "^7.31.8",
"fork-ts-checker-webpack-plugin": "^7.2.13",
"jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.0",
"jest": "^29.1.2",
"jest-environment-jsdom": "^29.1.2",
"rimraf": "^3.0.2",
"schema-utils": "^4.0.0",
"selenium-webdriver": "^4.3.1",
"selenium-webdriver": "^4.5.0",
"speed-measure-webpack-plugin": "^1.5.0",
"ts-jest": "^28.0.7",
"ts-loader": "^9.3.1",
"ts-jest": "^29.0.3",
"ts-loader": "^9.4.1",
"ts-node": "^10.9.1",
"typescript": "4.7",
"web-ext": "^7.1.1",
"typescript": "4.8",
"web-ext": "^7.2.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-merge": "^5.8.0"

View File

@@ -25,6 +25,9 @@
"Segments": {
"message": "أجزاء"
},
"Chapters": {
"message": "الفصول"
},
"upvoteButtonInfo": {
"message": "التصويت على هذا الإرسال"
},

View File

@@ -35,6 +35,9 @@
"message": "Изобразяване на сегментите като глави",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Показване на текущия сегмент до времето на клипа"
},
"upvoteButtonInfo": {
"message": "Одобряване на това предложение"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Затваряне на прозореца"
},
"closeIcon": {
"message": "Икона - затваряне"
},
"SubmitTimes": {
"message": "Изпращане на сегментите"
},
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "Скриване на сегмента"
},
"skipSegment": {
"message": "Пропускане на сегмент"
},
"playChapter": {
"message": "Пускане на главата"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Използвайте колелото на мишката, докато държите курсора върху полето за редактиране, за да коригирате бързо времето. Комбинации с клавиша ctrl или shift могат да се използват за фина настройка на промените."
},

View File

@@ -35,6 +35,9 @@
"message": "Zobrazit segmenty jako kapitoly",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Zobrazit aktuální segment vedle času videa"
},
"upvoteButtonInfo": {
"message": "Hlasovat pro tento příspěvek"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Zavřít vyskakovací okno"
},
"closeIcon": {
"message": "Ikona pro zavření"
},
"SubmitTimes": {
"message": "Odeslat segmenty"
},
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "Skrýt segment"
},
"skipSegment": {
"message": "Přeskočit segment"
},
"playChapter": {
"message": "Přehrát kapitolu"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Použijte kolečko myši při přechodu přes editační okno pro rychlou úpravu času. Kombinace kláves Ctrl nebo Shift mohou být použity k doladění změn."
},

View File

@@ -35,6 +35,9 @@
"message": "Render segmenter som kapitler",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Vis nuværende segmenter ved siden af videotid"
},
"upvoteButtonInfo": {
"message": "Stem for dette forslag"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Luk Pop-op"
},
"closeIcon": {
"message": "Luk ikon"
},
"SubmitTimes": {
"message": "Indsend Segmenter"
},
@@ -414,9 +420,18 @@
"statusReminder": {
"message": "Tjek status.sponsor.ajay.app for serverstatus."
},
"changeUserID": {
"message": "Importer/Eksporter Dit Private User-ID"
},
"whatChangeUserID": {
"message": "Dette bør holdes privat. Det er ligesom en adgangskode og bør ikke deles med nogen. Hvis nogen har dette, kan de udgive sig for at være dig. Hvis du leder efter dit offentlige bruger-ID, skal du klikke på udklipsholderikonet i popup-vinduet."
},
"setUserID": {
"message": "Set Privat Bruger-ID"
},
"userIDChangeWarning": {
"message": "Advarsel: Ændring af private Bruger-IDet er permanent. Er du sikker på, at du vil gøre det? Sørg for at sikkerhedskopiere din gamle for en sikkerheds skyld."
},
"createdBy": {
"message": "Oprettet Af"
},
@@ -514,6 +529,9 @@
"exportOptionsUpload": {
"message": "Indlæs fra fil"
},
"whatExportOptions": {
"message": "Dette er hele din konfiguration i JSON. Dette inkluderer dit private bruger-ID, så sørg for at dele dette med omtanke."
},
"setOptions": {
"message": "Indstil Indstillinger"
},
@@ -1034,6 +1052,12 @@
"hideSegment": {
"message": "Skjul segment"
},
"skipSegment": {
"message": "Spring segment over"
},
"playChapter": {
"message": "Afspil kapitel"
},
"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."
},
@@ -1162,6 +1186,9 @@
"message": "Ny funktion: Crowd-sourced brugerdefinerede kapitler. Disse er brugerdefinerede sektioner i videoer, der kan stables for at få mere og mere præcise. Køb en licens til at se kapitlerne indsendt på denne video såsom: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Ny funktion: Crowd-sourced brugerdefinerede kapitler. Disse er brugerdefinerede sektioner i videoer, der kan stables for at få mere og mere præcise. Du har gratis adgang, aktiver i indstillinger."
},
"unsubmittedSegmentCounts": {
"message": "Du har lige nu {0} på {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"

View File

@@ -35,6 +35,9 @@
"message": "Zeige Segmente als Kapitel",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Aktuelles Segment neben der Videozeit anzeigen"
},
"upvoteButtonInfo": {
"message": "Diese Einreichung positiv bewerten"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Pop-up schließen"
},
"closeIcon": {
"message": "Symbol schließen"
},
"SubmitTimes": {
"message": "Senden"
},
@@ -250,7 +256,7 @@
"message": "Zeige Kategorien im Einsendungsmenü auch ohne Einreichungsberechtigungen"
},
"whatShowCategoryWithoutPermission": {
"message": "Einige Kategorien benötigen Erlaubnis zum übermitteln aufgrund von einem Mindestmass an Reputation"
"message": "Einige Kategorien benötigen Erlaubnis zum Übermitteln aufgrund von Mindestanforderungen an Reputation"
},
"showNotice": {
"message": "Hinweis erneut anzeigen"
@@ -414,9 +420,18 @@
"statusReminder": {
"message": "Prüfe status.sponsor.ajay.app für den Serverstatus."
},
"changeUserID": {
"message": "Private Benutzer-ID importieren/exportieren"
},
"whatChangeUserID": {
"message": "Dies sollte geheim gehalten und wie ein Passwort behandelt, also mit niemandem geteilt werden. Andere könnten sich damit als dich ausgeben. Wenn du nach deiner öffentlichen Profilkennung suchst, klicke auf das „Kopieren“-Symbol neben deinem Anmeldenamen im Pop-up."
},
"setUserID": {
"message": "Private Benutzer-ID festlegen"
},
"userIDChangeWarning": {
"message": "Warnung: Das Ändern deiner privaten Benutzer-ID ist permanent. Bist du dir sicher, dass du das tun möchtest? Zur Sicherheit solltest du vorher ein Backup deiner alten ID erstellen."
},
"createdBy": {
"message": "Erstellt von"
},
@@ -461,10 +476,10 @@
"message": "Videosegmente, die kürzer als der festgelegte Wert sind, werden nicht übersprungen oder im Player angezeigt."
},
"enableManualSkipOnFullVideo": {
"message": "Verwende manuelles überspringen wenn ein vollständiges Video Label existiert"
"message": "Verwende manuelles Überspringen, wenn ein vollständiges Video Label existiert"
},
"whatManualSkipOnFullVideo": {
"message": "Für Personen, welche ein Video ohne unterbrechungen schauen wollen, während es eine komplette Eigenwerbung oder gesponsert ist."
"message": "Für Personen, welche ein Video ohne Unterbrechungen schauen wollen, während es vollständig gesponsert oder Eigenwerbung ist."
},
"skipNoticeDuration": {
"message": "Dauer des Überspringenhinweises (Sekunden):"
@@ -514,6 +529,9 @@
"exportOptionsUpload": {
"message": "Aus Datei laden"
},
"whatExportOptions": {
"message": "Dies ist deine gesamte Konfiguration in JSON. Sie enthält deine private Benutzer-ID, stelle also sicher, sie nicht mit jedem zu teilen."
},
"setOptions": {
"message": "Optionen ändern"
},
@@ -676,6 +694,9 @@
"category_filler": {
"message": "Füller/Witze"
},
"category_filler_description": {
"message": "Nebensächliche Szenen, die nur als Füller oder Witz dienen und nicht benötigt sind, um den Hauptinhalt des Videos zu verstehen. Dies bezieht sich nicht auf Segmente, die Kontext oder Hintergrunddetails liefern. Dies ist eine sehr aggressive Kategorie, die dafür gedacht ist, wenn du nicht in der Stimmung für \"Spaß\" bist."
},
"category_filler_short": {
"message": "Füller"
},
@@ -722,7 +743,7 @@
"message": "Kapitel"
},
"category_chapter_description": {
"message": "Benutzerdefinierte Kapitel, welche grosse Bereiche eines Videos beschreiben."
"message": "Benutzerdefiniert benannte Kapitel, welche große Abschnitte eines Videos beschreiben."
},
"category_chapter_guideline1": {
"message": "Sponsor-Markennamen nicht erwähnen"
@@ -1031,6 +1052,12 @@
"hideSegment": {
"message": "Segment verbergen"
},
"skipSegment": {
"message": "Segment überspringen"
},
"playChapter": {
"message": "Kapitel abspielen"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Benutze das Mausrad während der Mauszeiger über dem Eingabefeld schwebt um die Zeit schnell anzupassen. Benutze Strg bzw. Shift für gröbere/genauere Änderungen."
},
@@ -1159,6 +1186,9 @@
"message": "Neues Feature: Crowdsourced benutzerdefinierte Kapitel. Dies sind benutzerdefinierte Abschnitte in Videos, die gestapelt werden können, um mehr und mehr präzise zu werden. Kauf eine Lizenz, um Kapitel, die in diesem Video eingereicht wurden, anzusehen: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Neues Feature: Crowd-gesourcte benutzerdefinierte Kapitel. Dies sind benutzerdefiniert benannte Abschnitte in Videos, die gestapelt werden können, um mehr und mehr präzise zu werden. Du hast kostenlosen Zugang, aktiviere es in den Optionen."
},
"unsubmittedSegmentCounts": {
"message": "Du hast derzeit {0} in {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "A connection error has occurred. Error code: "
},
"segmentsStillLoading": {
"message": "Segments still loading..."
},
"clearTimes": {
"message": "Clear Segments"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Share as URL"
},
"segmentFetchFailureWarning": {
"message": "Warning: The server hasn't responded with segments yet. There might actually be segments on this video already submitted but you just haven't recieved them due to issues with the server."
}
}

View File

@@ -35,6 +35,9 @@
"message": "Procesar segmentos como capítulos",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Mostrar segmento actual al lado del tiempo del video"
},
"upvoteButtonInfo": {
"message": "Votar positivamente este envío"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Cerrar la ventana"
},
"closeIcon": {
"message": "Icono de cerrar"
},
"SubmitTimes": {
"message": "Enviar Segmentos"
},
@@ -175,7 +181,7 @@
"message": "Escoger Nombre De Usuario"
},
"copyPublicID": {
"message": "Copiar el ID de usuario público"
"message": "Copiar el ID de usuario Público"
},
"copySegmentID": {
"message": "Copiar ID de Segmento"
@@ -414,8 +420,17 @@
"statusReminder": {
"message": "Comprueba status.sponsor.ajay.app para ver el estado del servidor."
},
"changeUserID": {
"message": "Importar/Exportar su ID de usuario Privado"
},
"whatChangeUserID": {
"message": "Esto se debería mantener privado. Esto es como una contraseña y no debe ser compartido con nadie. Si alguien tiene esto, puede suplantarte. Si estás buscando tu ID de usuario público, haz clic en el icono de portapapeles en la ventana emergente."
"message": "Esto se debería mantener privado. Esto es como una contraseña y no debe ser compartido con nadie. Si alguien tiene esto, puede suplantarte. Si estás buscando tu ID de usuario Público, haz clic en el icono de portapapeles en la ventana emergente."
},
"setUserID": {
"message": "Establecer ID de usuario Privado"
},
"userIDChangeWarning": {
"message": "Advertencia: El cambio del ID de usuario Privado es permanente. ¿Está seguro/a de que desea hacer esto? Asegúrese de respaldar el antiguo por si acaso."
},
"createdBy": {
"message": "Creado Por"
@@ -514,6 +529,9 @@
"exportOptionsUpload": {
"message": "Cargar desde archivo"
},
"whatExportOptions": {
"message": "Esta es toda su configuración en JSON. Esto incluye tu ID de usuario Privado, asegúrese de compartir esto con prudencia."
},
"setOptions": {
"message": "Configurar opciones"
},
@@ -676,6 +694,9 @@
"category_filler": {
"message": "Tangentes de Relleno/Chistes"
},
"category_filler_description": {
"message": "Escenas tangenciales añadidas solo de relleno o humor que no son necesarias para entender el contenido principal del video. Esto no debe incluir segmentos que proporcionen contexto o detalles de fondo. Esta es una categoría muy agresiva para cuando no está de humor para la \"diversión\"."
},
"category_filler_short": {
"message": "Relleno"
},
@@ -1031,6 +1052,12 @@
"hideSegment": {
"message": "Ocultar segmento"
},
"skipSegment": {
"message": "Omitir segmento"
},
"playChapter": {
"message": "Reproducir capítulo"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Utilice la rueda del ratón mientras pasa el cursor por encima del cuadro de edición para ajustar el tiempo. Se pueden utilizar combinaciones de la tecla ctrl o shift para afinar los cambios."
},
@@ -1159,6 +1186,9 @@
"message": "Nueva Función: Capítulos personalizados marcados por la comunidad. Estos son secciones con nombres personalizados en los videos que pueden ser acumulados para ser cada vez más precisos. Compre una licencia para ver los capítulos enviados en este video, como: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Nueva Función: Capítulos personalizados marcados por la comunidad. Estos son secciones con nombres personalizados en los videos que pueden ser acumulados para ser cada vez más precisos. Tiene acceso gratuito, habilítelo en opciones."
},
"unsubmittedSegmentCounts": {
"message": "Actualmente tienes {0} en {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"

View File

@@ -35,6 +35,9 @@
"message": "Kuva segmendid peatükkidena",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Kuva praegune segment video kõrval"
},
"upvoteButtonInfo": {
"message": "Anna segmendile poolthääl"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Sulge hüpik"
},
"closeIcon": {
"message": "Sulgemisikoon"
},
"SubmitTimes": {
"message": "Esita segmendid"
},
@@ -968,6 +974,12 @@
"hideSegment": {
"message": "Peida segment"
},
"skipSegment": {
"message": "Jäta segment vahele"
},
"playChapter": {
"message": "Esita peatükk"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Kiireks aja reguleerimiseks keri hiirega muutmiskasti kohal. Täpsemaks reguleerimiseks hoia kerimise ajal all Ctrl või Shift klahvi."
},

View File

@@ -23,18 +23,21 @@
"message": "osio"
},
"Segments": {
"message": "osiot"
"message": "osiota"
},
"SegmentsCap": {
"message": "Osiot"
},
"Chapters": {
"message": "Osiot"
"message": "Kappaleet"
},
"renderAsChapters": {
"message": "Piirrä osiot kappaleina",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Näytä nykyinen osio videon ajan vieressä"
},
"upvoteButtonInfo": {
"message": "Äänestä lähetystä"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Sulje ponnahdusikkuna"
},
"closeIcon": {
"message": "Sulkukuvake"
},
"SubmitTimes": {
"message": "Lähetä osiot"
},
@@ -232,7 +238,7 @@
"message": "Negatiivisesti äänestämäsi osiot pysyvät piilotettuina myös päivityksen jälkeen"
},
"trackDownvotesWarning": {
"message": "Varoitus: Tämän käytöstä poisto poistaa kaikki negatiiviset äänet"
"message": "Varoitus: Tämän poistaminen käytöstä poistaa kaikki aiemmin tallennetut alaäänet"
},
"enableQueryByHashPrefix": {
"message": "Kysely tiiviste-etuliittellä"
@@ -309,11 +315,11 @@
"description": "Keybind label"
},
"nextChapterKeybind": {
"message": "Seuraava osio",
"message": "Seuraava kappale",
"description": "Keybind label"
},
"previousChapterKeybind": {
"message": "Edellinen osio",
"message": "Edellinen kappale",
"description": "Keybind label"
},
"keybindDescription": {
@@ -524,7 +530,7 @@
"message": "Lataa tiedostosta"
},
"whatExportOptions": {
"message": "Tässä ovat kaikki määrityksesi JSON-muodossa. Myös yksityinen UserID sisältyy näihin tietoihin, joten jaa teidostoa harkiten."
"message": "Tässä ovat kaikki määrityksesi JSON-muodossa. Myös yksityinen UserID sisältyy näihin tietoihin, joten jaa tiedostoa harkiten."
},
"setOptions": {
"message": "Käytä asetuksia"
@@ -734,19 +740,19 @@
"message": "Voi ohittaa otsikon tai pikkukuvan viittaamaan kohtaan"
},
"category_chapter": {
"message": "Osio"
"message": "Kappale"
},
"category_chapter_description": {
"message": "Nimetyt osiot, jotka kuvaavat videon merkittäviä jaksoja."
"message": "Nimetyt kappaleet, jotka kuvaavat videon merkittäviä osia."
},
"category_chapter_guideline1": {
"message": "Älä mainitse sponsoreiden tuotemerkkejä"
},
"category_chapter_guideline2": {
"message": "Käytä yleisille jaksoille suurempia osioita"
"message": "Käytä yleisille osille suurempia kappaleita"
},
"category_chapter_guideline3": {
"message": "Pienemmät osiot voidaan sijoittaa suurempien osioiden sisään"
"message": "Pienemmät kappaleet voidaan sijoittaa suurempien kappaleiden sisälle"
},
"category_livestream_messages": {
"message": "Livestream: lahjoitusten/viestien lukeminen"
@@ -779,7 +785,7 @@
"message": "Näytä merkki"
},
"showOverlay_chapter": {
"message": "Näytä osiot"
"message": "Näytä kappaleet"
},
"autoSkipOnMusicVideos": {
"message": "Ohita kaikki osiot automaattisesti, kun videossa on \"Musiikiton\" osio"
@@ -923,7 +929,7 @@
"message": "Koko video on merkitty tällä kategorialla ja on erotettavaksi liian tiiviisti integroitu"
},
"chapterNameTooltipWarning": {
"message": "Jonkin osion ja kategorian nimet ovat samankaltaisia. Kun mahdollista, osioiden sijaan on suositeltavaa käyttää kategorioita."
"message": "Jonkin kappaleesi nimi on samankaltainen kategorian kanssa. Kun mahdollista, kappaleiden sijaan on suositeltavaa käyttää kategorioita."
},
"experiementOptOut": {
"message": "Jättäydy pois kaikista tulevista kokeiluista",
@@ -933,7 +939,7 @@
"message": "Piilota ikuisesti"
},
"warningChatInfo": {
"message": "Huomasimme, että teit joitakin yleisiä virheitä, jotka eivät ole pahansuopia"
"message": "Huomasimme, että teit joitakin yleisiä virheitä, jotka eivät ole tahalleen tehtyjä"
},
"warningTitle": {
"message": "Sait varoituksen"
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "Piilota osio"
},
"skipSegment": {
"message": "Ohita osio"
},
"playChapter": {
"message": "Toista kappale"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Käytä hiiren rullaa samalla kun osoitin on muokkauslaatikon päällä säätääksesi aikaa nopeasti. Ctrl- tai Shift-näppäimen yhdistelmiä voi käyttää muutoksien hienosäätelyyn."
},
@@ -1135,7 +1147,7 @@
"message": "Et ole oikeutettu alennukseen"
},
"discountLink": {
"message": "Alennuslinkki (ks. vaaleanpunainen hinta)"
"message": "Alennuslinkki (ks. pinkki hinta)"
},
"selectYourCountry": {
"message": "Valitse maasi"
@@ -1164,12 +1176,19 @@
"message": "Syötä lisenssiavain"
},
"chaptersPage1": {
"message": "SponsorBlockin joukkolähteinen osiotoiminto on vain lisenssin ostaneiden tai aiempien tukien perusteella veloituksettoman käyttöoikeuden saaneiden henkilöiden käytettävissä"
"message": "SponsorBlockin joukkolähteinen kappaletoiminto on vain lisenssin ostaneiden tai aiempien tukien perusteella veloituksettoman käyttöoikeuden saaneiden henkilöiden käytettävissä"
},
"chaptersPage2": {
"message": "Huomioi: Osioiden lähetysoikeus perustuu edelleen laskettuun maineeseen. Lisenssin hankinta sallii sinun ainoastaan tarkastella muiden lähettämiä osioita",
"message": "Huomioi: Kappaleiden lähetysoikeus perustuu edelleen laskettuun maineeseen. Lisenssin hankinta sallii sinun ainoastaan tarkastella muiden lähettämiä kappaleita",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Uusi ominaisuus: Joukkolähteisesti nimetyt kappaleet. Nämä ovat nimettyjä osia videoissa, joita voi pinota yhä paremman tarkkuuden saavuttamiseksi. Osta lisenssi nähdäksesi tähän videoon lisätyt kappaleet, kuten: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Uusi ominaisuus: Joukkolähteisesti nimetyt kappaleet. Nämä ovat nimettyjä osia videoissa, joita voi pinota yhä paremman tarkkuuden saavuttamiseksi. Sinulla on pääsy ominaisuuteen ilmaiseksi, ota se käyttöön asetuksissa."
},
"unsubmittedSegmentCounts": {
"message": "Sinulla on tällä hetkellä {0} {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
@@ -1211,7 +1230,7 @@
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "Videon tunniste",
"message": "Videon ID",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
@@ -1223,6 +1242,6 @@
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Jaa URLina"
"message": "Jaa URL-osoitteena"
}
}

View File

@@ -25,6 +25,12 @@
"Segments": {
"message": "מקטעים"
},
"SegmentsCap": {
"message": "מקטעים"
},
"Chapters": {
"message": "פרקים"
},
"upvoteButtonInfo": {
"message": "הצבע לדיווח הזה"
},
@@ -211,6 +217,14 @@
"message": "קוד מקור",
"description": "Used on Firefox Store Page"
},
"nextChapterKeybind": {
"message": "הפרק הבא",
"description": "Keybind label"
},
"yourWork": {
"message": "העבודה שלך",
"description": "Used to describe the section that will show you the statistics from your submissions."
},
"errorCode": {
"message": "קוד שגיאה: "
},

View File

@@ -35,6 +35,9 @@
"message": "Szegmensek megjelenítése fejezetekként",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Aktuális szegmens megjelenítése a videó ideje mellett"
},
"upvoteButtonInfo": {
"message": "Részlet felszavazása"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Felugró ablak bezárása"
},
"closeIcon": {
"message": "Bezárás ikon"
},
"SubmitTimes": {
"message": "Szegmensek beküldése"
},
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "Szegmens elrejtése"
},
"skipSegment": {
"message": "Szegmens átugrása"
},
"playChapter": {
"message": "Fejezet lejátszása"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Használd egérgörgődet a szerkesztő mező fölött, hogy gyorsan módosíthasd az időt. A ctrl vagy shift billentyűk kombinációjával finomhangolhatod a változás mértékét."
},

View File

@@ -35,6 +35,9 @@
"message": "Visualizza segmenti come capitoli",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Mostra il Segmento Corrente Affianco al Tempo del Video"
},
"upvoteButtonInfo": {
"message": "Vota questo contributo"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Chiudi il popup"
},
"closeIcon": {
"message": "Icona Chiudi"
},
"SubmitTimes": {
"message": "Invia i segmenti"
},
@@ -414,9 +420,18 @@
"statusReminder": {
"message": "Controlla status.sponsor.ajay.app per lo stato del server."
},
"changeUserID": {
"message": "Importa/Esporta il Tuo UserID Privato"
},
"whatChangeUserID": {
"message": "Questo dovrebbe esser mantenuto privato. È come una password e non dovrebbe esser condiviso con nessuno. Se qualcuno lo possiede, può impersonarti. Se stai cercando il tuo userID pubblico, clicca l'icona degli appunti nel popup."
},
"setUserID": {
"message": "Imposta UserID Privato"
},
"userIDChangeWarning": {
"message": "Attenzione: La modifica dell'UserID Privato è permanente. Sei sicuro di volerlo fare? Assicurati di effettuare il backup di quello precedente."
},
"createdBy": {
"message": "Creato da"
},
@@ -514,6 +529,9 @@
"exportOptionsUpload": {
"message": "Carica da file"
},
"whatExportOptions": {
"message": "Questa è la tua intera configurazione in JSON. Ciò include il tuo UserID Privato, quindi assicurati di condividerla saggiamente."
},
"setOptions": {
"message": "Imposta Opzioni"
},
@@ -676,6 +694,9 @@
"category_filler": {
"message": "Riempitivi irrilevanti/Battute"
},
"category_filler_description": {
"message": "Scene tangenziali aggiunte solo come riempimento o per umorismo, non necessarie alla comprensione del contenuto principale del video. Questo non dovrebbe includere i segmenti che forniscono contesto o dettagli di background. Questa è una categoria molto aggressiva, pensata per quanto non hai voglia di \"divertirti\"."
},
"category_filler_short": {
"message": "Filler"
},
@@ -1031,6 +1052,12 @@
"hideSegment": {
"message": "Nascondi segmento"
},
"skipSegment": {
"message": "Salta segmento"
},
"playChapter": {
"message": "Riproduci capitolo"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Usa la rotellina del mouse passando sulla casella di modifica per regolare rapidamente il tempo. Le combinazioni dei tasti ctrl o shift sono utilizzabili per perfezionare le modifiche."
},
@@ -1155,6 +1182,13 @@
"message": "Nota: L'autorizzazione a inviare i capitoli si basa ancora sulla reputazione calcolata. Acquistare una licenza ti consente di visualizzare soltanto i capitoli inviati dagli altri",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Nuova Funzionalità: Capitoli personalizzati in crowdsourcing. Sono sezioni dal nome personalizzato nei video, che possono esser impilate per essere sempre più precise. Acquista una licenza per visualizzare i capitoli inviati in questo video come: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Nuova Funzionalità: Capitoli personalizzati in crowdsourcing. Sono sezioni dal nome personalizzato nei video, che possono esser impilate per essere sempre più precise. Hai accesso gratuitamente, abilitalo nelle opzioni."
},
"unsubmittedSegmentCounts": {
"message": "Al momento hai {0} su {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"

View File

@@ -35,6 +35,9 @@
"message": "セグメントをチャプターとしてレンダリングする",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "動画時間の横に現在のセグメントを表示"
},
"upvoteButtonInfo": {
"message": "この提案を支持"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "ポップアップを閉じる"
},
"closeIcon": {
"message": "閉じるアイコン"
},
"SubmitTimes": {
"message": "セグメントを送信"
},
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "セグメントを表示しない"
},
"skipSegment": {
"message": "セグメントをスキップ"
},
"playChapter": {
"message": "チャプターを再生"
},
"SponsorTimeEditScrollNewFeature": {
"message": "編集ボックスにカーソルを合わせながらマウスホイールを使用すると、時間をすばやく調整できます。 CtrlキーまたはShiftキーの組み合わせを使用して変更を微調整できます。"
},

View File

@@ -35,6 +35,9 @@
"message": "챕터처럼 구간을 불러오기",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "동영상 시간 옆에 현재 구간 표시"
},
"upvoteButtonInfo": {
"message": "이 제출을 추천해요"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "팝업 닫기"
},
"closeIcon": {
"message": "닫기 아이콘"
},
"SubmitTimes": {
"message": "구간 제출"
},
@@ -144,7 +150,7 @@
"message": "제출"
},
"savedPeopleFrom": {
"message": "나 덕분에 다른 사람들이 건너뛴 구간: "
"message": "나 덕분에 다른 이 건너뛴 구간: "
},
"viewLeaderboard": {
"message": "순위표"
@@ -217,13 +223,13 @@
"message": "YouTube 탐색 바에서 삭제 버튼 표시"
},
"enableViewTracking": {
"message": "건너뛴 횟수 추적 활성화"
"message": "건너뛴 횟수 추적 사용"
},
"whatViewTracking": {
"message": "이 기능으로 건너뛴 구간을 추적해서 사용자가 제출한 내용이 다른 분께 얼마나 도움이 되는지 알려주고 잘못된 구간이 데이터베이스에 들어가지 않도록 추천과 함께 분석에 사용해요. 이 확장 프로그램이 구간을 건너뛸 때마다 서버에 메시지를 보낼 거예요. 조회수가 정확하기 위해서는 이 설정을 변경하지 않기를 바라요. :)"
},
"enableViewTrackingInPrivate": {
"message": "시크릿/사생활 보호 탭에서 건너뛴 횟수 추적 활성화"
"message": "시크릿/사생활 보호 탭에서 건너뛴 횟수 추적 사용"
},
"enableTrackDownvotes": {
"message": "비추천한 구간 저장"
@@ -232,7 +238,7 @@
"message": "비추천한 구간을 새로고침 이후에도 계속 숨겨요"
},
"trackDownvotesWarning": {
"message": "경고: 비활성화하면 이전에 저장된 비추천 구간이 삭제돼요"
"message": "경고: 사용하지 않으면 이전에 저장된 비추천 구간이 삭제돼요"
},
"enableQueryByHashPrefix": {
"message": "해시 접두사로 요청 전송"
@@ -371,22 +377,22 @@
"description": "Used for skipping to things (Skipped to Highlight)"
},
"disableAutoSkip": {
"message": "자동 건너뛰기 비활성화"
"message": "자동 건너뛰기 사용 안 함"
},
"enableAutoSkip": {
"message": "자동 건너뛰기 활성화"
"message": "자동 건너뛰기 사용"
},
"audioNotification": {
"message": "건너뛸 때 소리 재생"
},
"audioNotificationDescription": {
"message": "구간을 건너뛸 때마다 소리를 재생해요. 자동 건너뛰기가 비활성화된 경우, 아무 소리도 재생되지 않아요."
"message": "구간을 건너뛸 때마다 소리를 재생해요. 자동 건너뛰기를 사용하지 않는 경우, 아무 소리도 재생되지 않아요."
},
"showTimeWithSkips": {
"message": "건너뛰기로 제외된 시간 표시"
},
"showTimeWithSkipsDescription": {
"message": "탐색 바 아래에 있는 동영상 시간 옆 괄호에 시간이 표시돼요. 건너뛸 구간을 제외할 실제로 재생하게 될 동영상의 길이를 보여줘요. \"탐색 바에 표시\"로만 지정된 구간도 포함해요."
"message": "이 시간은 탐색 바 아래에 있고 현재 시간 옆에 있는 대괄호로 표시돼요. 건너뛸 구간을 제외할 실제로 재생하게 될 동영상의 길이를 보여줘요. \"탐색 바에 표시\"로만 지정된 구간도 포함해요."
},
"youHaveSkipped": {
"message": "건너뛴 구간: "
@@ -404,11 +410,11 @@
"message": "시간"
},
"youHaveSavedTime": {
"message": "나 덕분에 다른 사람들이 건너뛴 구간:",
"message": "나 덕분에 다른 이 건너뛴 구간:",
"description": "You've saved people from 887,362 segments (236d 15h 5.3 minutes of their lives)."
},
"youHaveSavedTimeEnd": {
"message": "의 시간을 절약함",
"message": " 정도를 절약함",
"description": "You've saved people from 887,362 segments (236d 15h 5.3 minutes of their lives)."
},
"statusReminder": {
@@ -418,7 +424,7 @@
"message": "비공개 사용자 ID 가져오기/내보내기"
},
"whatChangeUserID": {
"message": "이 정보를 다른 분께 공개하지 마세요. 비밀번호처럼 알려주면 위험한 정보랍니다. 다른 분이 이 정보를 가지고 나를 사칭할 수도 있어요. 공개 사용자 ID를 찾고 있다면, 팝업 내 클립보드 아이콘을 눌러주세요."
"message": "이 정보를 다른 분께 공개하지 마세요. 이건 비밀번호와 같으며 누구와도 공유해서는 안 되는 정보랍니다. 다른 분이 이 정보를 습득한다면, 나를 사칭할 수도 있어요. 공개 사용자 ID를 찾고 있다면, 팝업 내 클립보드 아이콘을 눌러주세요."
},
"setUserID": {
"message": "비공개 사용자 ID 설정"
@@ -440,7 +446,7 @@
"message": "지원되는 사이트: "
},
"optionsInfo": {
"message": "Invidious 지원 활성화하고, 자동 건너뛰기 기능을 끄거나, 사용하지 않는 버튼겨보세요."
"message": "Invidious 지원 활성화, 자동 건너뛰기 사용 안 함, 버튼 숨기기 등이 있어요."
},
"addInvidiousInstance": {
"message": "제3자 클라이언트 인스턴스 추가"
@@ -452,13 +458,13 @@
"message": "추가"
},
"addInvidiousInstanceError": {
"message": "잘못된 도메인이에요. 도메인 부분만 포함해야 해요. 예: invious.ajay.app"
"message": "잘못된 도메인이에요. 도메인 부분만 포함해야 해요. 예: invious.ajay.app"
},
"resetInvidiousInstance": {
"message": "Invidious 인스턴스 목록 초기화"
},
"resetInvidiousInstanceAlert": {
"message": "Invidious 인스턴스 목록을 초기화할까요?"
"message": "Invidious 인스턴스 목록을 초기화하시겠어요?"
},
"currentInstances": {
"message": "현재 인스턴스:"
@@ -824,7 +830,7 @@
"message": "카테고리 선택"
},
"enableThisCategoryFirst": {
"message": "\"{0}\" 카테고리의 구간을 제출하려면, 설정에서 활성화를 해주셔야 해요. 설정 창으로 이동할까요?",
"message": "\"{0}\" 카테고리의 구간을 제출하려면, 설정에서 사용을 해주셔야 해요. 바로 설정 창으로 이동하실 거예요.",
"description": "Used when submitting segments to only let them select a certain category if they have it enabled in the options."
},
"poiOnlyOneSegment": {
@@ -866,7 +872,7 @@
"message": "권한 요청에 실패했어요. 거부를 누르셨나요?"
},
"adblockerIssueWhitelist": {
"message": "이 문제를 해결할 수 없는 경우 SponsorBlock이 이 동영상의 채널 정보를 찾을 수 없는 것일 수 있으니, '건너뛰기 전 채널 강제 확인' 설정을 비활성화해주세요."
"message": "이 문제를 해결할 수 없는 경우 SponsorBlock이 이 동영상의 채널 정보를 찾을 수 없는 것일 수 있으니, '건너뛰기 전 채널 강제 확인' 설정을 사용하지 않아야 해요"
},
"forceChannelCheck": {
"message": "건너뛰기 전 채널 강제 확인"
@@ -891,7 +897,7 @@
"message": "카테고리 변경"
},
"nonMusicCategoryOnMusic": {
"message": "이 동영상은 음악 동영상으로 분류되어 있어요. 동영상에 스폰서 광고 구간이 있나요? \"음악이 아닌 구간\"으로 지정된 카테고리인 경우, 확장 프로그램 설정을 열어 이 카테고리를 활성화하세요. 그리고, 이 구간을 \"스폰서 광고 구간\" 대신 \"음악이 아닌 구간\"으로 지정하세요. 혼동된다면 가이드라인을 읽어주세요."
"message": "이 동영상은 음악 동영상으로 분류되어 있어요. 동영상에 스폰서 광고 구간이 있나요? \"음악이 아닌 구간\"으로 지정된 카테고리인 경우, 확장 프로그램 설정을 열어 이 카테고리를 사용하세요. 그리고, 이 구간을 \"스폰서 광고 구간\" 대신 \"음악이 아닌 구간\"으로 지정하세요. 혼동된다면 가이드라인을 읽어주세요."
},
"multipleSegments": {
"message": "여러 구간"
@@ -966,7 +972,7 @@
"message": "아래 설정을 확인해 보세요"
},
"helpPageFeatureDisclaimer": {
"message": "기본값으로 많은 기능이 비활성화되어 있어요. 인트로, 아웃트로 같은 부분을 건너뛰고 싶으시다면 아래 설정을 야 해요. 또한 UI 요소를 숨기거나 표시할 수 있답니다."
"message": "기본값으로 많은 기능이 사용되지 않아요. 인트로, 아웃트로 같은 부분을 건너뛰고 싶으시다면 아래 설정을 사용해야 해요. 또한 UI 요소를 숨기거나 표시할 수 있답니다."
},
"helpPageHowSkippingWorks": {
"message": "건너뛰기가 작동하는 방법"
@@ -993,10 +999,10 @@
"message": "구간을 잘못 설정했다면, 위쪽 화살표 버튼을 누른 다음 구간을 수정하거나 삭제할 수 있어요."
},
"helpPageTooSlow": {
"message": "구간 수정이 오래 걸리시나요?"
"message": "이건 너무 느린 거 같아요"
},
"helpPageTooSlow1": {
"message": "원하는 경우 단축키를 용할 수 있어요. 쌍반점(세미콜론) 키를 눌러 스폰서 광고 구간의 시점/종점을 설정할 수 있으며 작은따옴표 키를 눌러 구간을 제출할 수 있답니다. 언제든지 설정에서 변경할 수 있어요. QWERTY 자판을 사용하지 않는 경우, 단축키를 변경해야 할 수도 있어요."
"message": "원하는 경우 단축키를 용할 수 있어요. 쌍반점(세미콜론) 키를 눌러 스폰서 광고 구간의 시점/종점을 설정할 수 있으며 작은따옴표 키를 눌러 구간을 제출할 수 있답니다. 언제든지 설정에서 변경할 수 있어요. QWERTY 자판을 사용하지 않는 경우, 단축키를 변경해야 할 수도 있어요."
},
"helpPageCopyOfDatabase": {
"message": "데이터베이스의 복사본을 구할 수 있나요? 개발자분께 무슨 일이 생기면 어떻게 되는 거죠?"
@@ -1005,7 +1011,7 @@
"message": "데이터베이스는 여기에서 확인하실 수 있어요:"
},
"helpPageCopyOfDatabase2": {
"message": "또한 소스 코드는 자유롭게 용할 수 있어요. 따라서 데이터베이스에 무슨 일이 생기더라도, 제출된 구간이 사라지는 일은 없을 거예요."
"message": "또한 소스 코드는 자유롭게 용할 수 있어요. 따라서 데이터베이스에 무슨 일이 생기더라도, 제출된 구간이 사라지는 일은 없을 거예요."
},
"helpPageNews": {
"message": "새로운 변경 사항은 어디에서 확인하나요?"
@@ -1020,7 +1026,7 @@
"message": "자세히 보기"
},
"FullDetails": {
"message": "자세한 정보"
"message": "전체 세부 정보"
},
"CopyDownvoteButtonInfo": {
"message": "비추천 후 다시 제출할 수 있는 제출되지 않은 복사본을 생성해요"
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "구간 숨기기"
},
"skipSegment": {
"message": "구간 건너뛰기"
},
"playChapter": {
"message": "챕터 재생"
},
"SponsorTimeEditScrollNewFeature": {
"message": "편집 상자 위에 마우스 커서를 올린 채 스크롤해서 시간을 빠르게 조정해 보세요. Ctrl이나 Shift 키를 누른 채로 스크롤하면 세밀하게 조정할 수 있어요."
},
@@ -1095,10 +1107,10 @@
"message": "변경"
},
"youtubeKeybindWarning": {
"message": "설정한 키와 기존 YouTube 단축키가 중복돼요. 그래도 진행하시겠어요?"
"message": "설정한 키와 기존 YouTube 단축키가 중복돼요. 그래도 사용하시겠어요?"
},
"betaServerWarning": {
"message": "현재 테스트 서버를 이용 중이에요!"
"message": "테스트 서버가 활성화됐어요!"
},
"openOptionsPage": {
"message": "설정 페이지 열기"
@@ -1107,7 +1119,7 @@
"message": "기본 설정으로 초기화"
},
"confirmResetToDefault": {
"message": "모든 설정을 기본값으로 초기화하시겠어요? 되돌릴 수 없어요!"
"message": "모든 설정을 기본값으로 초기화하시겠어요? 되돌릴 수 없어요."
},
"exportSegments": {
"message": "구간 내보내기"
@@ -1126,7 +1138,7 @@
"message": "라이선스 키가 유효하지 않아요"
},
"hideUpsells": {
"message": "추가 결제 없이는 숨김 설정을 이용하실 수 없어요"
"message": "(추가 결제 없이는 숨김 설정을 사용할 수 없어요)"
},
"chooseACountry": {
"message": "국가 선택"
@@ -1145,7 +1157,7 @@
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "요금을 결제할 여력이 아니시라면, {여기}서 할인 혜택을 받을 수 있는지 확인해보세요",
"message": "요금을 결제할 여력이 아니시라면, {여기}서 할인 혜택을 받을 수 있는지 확인해 보세요",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
@@ -1164,10 +1176,10 @@
"message": "라이선스 키를 입력하세요"
},
"chaptersPage1": {
"message": "SponsorBlock 사용자 참여 챕터 기능은 라이선스 결제 사용자나, 이전 기여를 통해 접근을 허가받은 사용자만 용할 수 있어요"
"message": "SponsorBlock 사용자 참여 챕터 기능은 라이선스 결제 사용자나, 이전 기여를 통해 접근을 허가받은 사용자만 용할 수 있어요"
},
"chaptersPage2": {
"message": "참고: 여전히 챕터 제출 권한은 산정된 평판만을 바탕으로 부여돼요. 라이선스를 결제하면 다른 분이 제출한 챕터를 확인하는 기능만 추가로 용할 수 있어요",
"message": "참고: 여전히 챕터 제출 권한은 산정된 평판만을 바탕으로 부여돼요. 라이선스를 결제하면 다른 분이 제출한 챕터를 확인하는 기능만 추가로 용할 수 있어요",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
@@ -1175,7 +1187,31 @@
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "새로운 기능: 사용자 참여 챕터 기능. 챕터는 사용자가 직접 이름을 지정할 수 있고 중첩시킬 수 있어 더욱 더 정확해요. 지금 설정에서 활성화해서 무료로 사용하세요."
"message": "새로운 기능: 사용자 참여 챕터 기능. 챕터는 사용자가 직접 이름을 지정할 수 있고 중첩시킬 수 있어 더욱 더 정확해요. 설정에서 사용해서 무료로 사용할 수 있어요."
},
"unsubmittedSegmentCounts": {
"message": "You currently have {0} on {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "You currently have no unsubmitted segments",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "unsubmitted segment",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "unsubmitted segments",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "video",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "videos",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "모든 구간 초기화",

View File

@@ -35,6 +35,9 @@
"message": "Pokazuj segmenty jako rozdziały",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Pokaż bieżący segment poza czasem wideo"
},
"upvoteButtonInfo": {
"message": "Zagłosuj na ten segment"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Zamknij okno"
},
"closeIcon": {
"message": "Wyłącz ikonę"
},
"SubmitTimes": {
"message": "Prześlij segmenty"
},
@@ -246,6 +252,9 @@
"whatRefetchWhenNotFound": {
"message": "Jeśli film jest nowy i nie znaleziono żadnych segmentów, dane będą pobierane na nowo co kilka minut w czasie oglądania."
},
"enableShowCategoryWithoutPermission": {
"message": "Pokaż kategorie w menu zgłoszeń, nawet bez uprawnień do zgłaszania"
},
"showNotice": {
"message": "Pokaż informacje ponownie"
},
@@ -408,9 +417,18 @@
"statusReminder": {
"message": "Sprawdź status serwera na status.sponsor.ajay.app"
},
"changeUserID": {
"message": "Importuj/Eksportuj swój prywatny UserID"
},
"whatChangeUserID": {
"message": "To powinno pozostać prywatne. Jest to niczym hasło i nie powinno zostać nikomu udostępnione. Przy jego użyciu ktoś może się pod ciebie podszywać. Jeśli szukasz publicznego ID użytkownika, kliknij ikonę schowka w wyskakującym oknie."
},
"setUserID": {
"message": "Ustaw prywatny UserID"
},
"userIDChangeWarning": {
"message": "Uwaga: Zmiana ID użytkownika jest trwała. Czy na pewno chcesz to zrobić? Na wszelki wypadek skopiuj swój poprzedni ID."
},
"createdBy": {
"message": "Stworzony przez"
},
@@ -454,6 +472,9 @@
"minDurationDescription": {
"message": "Segmenty krótsze niż ustawiona wartość nie będą pomijane ani pokazywane w odtwarzaczu."
},
"enableManualSkipOnFullVideo": {
"message": "Użyj ręcznego pomijania, gdy istnieje etykieta na całym filmie"
},
"skipNoticeDuration": {
"message": "Czas trwania powiadomienia pominięcia (sekundy):"
},
@@ -502,6 +523,9 @@
"exportOptionsUpload": {
"message": "Wczytaj z pliku"
},
"whatExportOptions": {
"message": "Jest to cała twoja konfiguracja w formacie JSON. Zawarty jest w niej twój prywatny UserID, więc uważaj, komu ją udostępniasz."
},
"setOptions": {
"message": "Zapisz ustawienia"
},
@@ -1019,6 +1043,12 @@
"hideSegment": {
"message": "Ukryj segment"
},
"skipSegment": {
"message": "Pomiń segment"
},
"playChapter": {
"message": "Odtwórz rozdział"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Użyj scroll'a myszy po najechaniu nad pole edycji, aby szybko dostosować czas. Kombinacje z ctrl'em i shift'em mogą być użyte, aby doszlifować zmiany."
},
@@ -1085,6 +1115,9 @@
"exportSegments": {
"message": "Eksportuj segmenty"
},
"importSegments": {
"message": "Importuj segmenty"
},
"Import": {
"message": "Importuj",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
@@ -1101,9 +1134,23 @@
"chooseACountry": {
"message": "Wybierz kraj"
},
"noDiscount": {
"message": "Nie kwalifikujesz się do przeceny"
},
"discountLink": {
"message": "Link rabatowy"
},
"selectYourCountry": {
"message": "Wybierz swój kraj"
},
"alreadyDonated": {
"message": "Jeśli do tej pory przekazałeś jakąkolwiek darowiznę, możesz odebrać darmowy dostęp poprzez wysyłanie maila do:",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Jeśli nie możesz sobie pozwolić na zakup licencji, kliknij {tutaj} aby sprawdzić, czy kwalifikujesz się do zniżki",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Zaloguj się za pomocą Patreon"
},
@@ -1119,10 +1166,20 @@
"enterLicenseKey": {
"message": "Wprowadź klucz licencyjny"
},
"chaptersPage1": {
"message": "Funkcja społecznościowych rozdziałów SponsorBlock jest dostępna tylko dla osób, które wykupią licencję, albo którym przyznano dostęp za darmo ze względu na swoje wcześniejszy wkład"
},
"chaptersPage2": {
"message": "Uwaga: Przesyłanie rozdziałów jest nadal oparte na skalkulowanej reputacji. Kupowanie licencji pozwala tylko przeglądać rozdziały przesłane przez innych",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Nowa funkcja: niestandardowe rozdziały ze źródeł społecznościowych. Są to sekcje niestandardowo nazwane w filmach, które mogą być ustawione w sposób bardziej precyzyjny. Kup licencję, aby wyświetlić rozdziały przedstawione na tym filmie, takie jak: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Nowa funkcja: niestandardowe rozdziały ze źródeł społecznościowych. Są to sekcje niestandardowo nazwane w filmach, które mogą być ustawione w sposób bardziej precyzyjny."
},
"unsubmittedSegmentCounts": {
"message": "Aktualnie masz {0} na {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"

View File

@@ -35,6 +35,9 @@
"message": "Renderizar segmentos como capítulos",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Mostrar Segmento Atual ao Lado do Tempo do Vídeo"
},
"upvoteButtonInfo": {
"message": "Votar nesse segmento positivamente"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Fechar Popup"
},
"closeIcon": {
"message": "Ícone de Fechar"
},
"SubmitTimes": {
"message": "Enviar Segmentos"
},
@@ -246,6 +252,12 @@
"whatRefetchWhenNotFound": {
"message": "Se o vídeo for novo e nenhum segmento for encontrado, continuaremos buscando enquanto você assiste."
},
"enableShowCategoryWithoutPermission": {
"message": "Mostrar categorias no menu de envios mesmo sem permissão de envio"
},
"whatShowCategoryWithoutPermission": {
"message": "Algumas categorias exigem autorização de envio devido a requisitos mínimos de reputação"
},
"showNotice": {
"message": "Mostrar notificação outra vez"
},
@@ -408,9 +420,18 @@
"statusReminder": {
"message": "Verifique status.sponsor.ajay.app para o status do servidor."
},
"changeUserID": {
"message": "Importar/Exportar Seu UserID Privado"
},
"whatChangeUserID": {
"message": "Esta informação deve se mantida privada. Ela é como uma senha e não deve ser compartilhada. Outras pessoas poderão se passar por você caso obtenham acesso. Se estiver procurando por sua ID Pública de Usuário, clique no ícone de prancheta no popup."
},
"setUserID": {
"message": "Definir UserID Privado"
},
"userIDChangeWarning": {
"message": "Aviso: A modificação do ID de usuário privado é permanente. Você tem certeza de que quer fazer isso? Certifique-se de fazer backup do anterior."
},
"createdBy": {
"message": "Criado por"
},
@@ -454,6 +475,12 @@
"minDurationDescription": {
"message": "Segmentos menores do que o valor definido não serão pulados ou mostrados no reprodutor."
},
"enableManualSkipOnFullVideo": {
"message": "Usar o pulo manual quando houver um rótulo de vídeo completo"
},
"whatManualSkipOnFullVideo": {
"message": "Para pessoas que desejam assistir ao vídeo sem interrupção se ele for totalmente patrocinado ou autopromoção."
},
"skipNoticeDuration": {
"message": "Duração do aviso prévio de pular (segundos):"
},
@@ -709,6 +736,12 @@
"category_chapter": {
"message": "Capítulo"
},
"category_chapter_description": {
"message": "Capítulos personalizados que descrevem as principais seções de um vídeo."
},
"category_chapter_guideline1": {
"message": "Não mencione os nomes dos patrocinadores"
},
"category_livestream_messages": {
"message": "Livestream: Leituras de Doação/Mensagem"
},
@@ -739,6 +772,9 @@
"showOverlay_full": {
"message": "Mostrar Rótulo"
},
"showOverlay_chapter": {
"message": "Mostrar Capítulos"
},
"autoSkipOnMusicVideos": {
"message": "Pular automaticamente todos os segmentos quando há um segmento que não é música"
},
@@ -866,6 +902,9 @@
"categoryPillTitleText": {
"message": "Este vídeo inteiro está rotulado como esta categoria e está muito integrado para poder ser separado"
},
"chapterNameTooltipWarning": {
"message": "Um de seus nomes de capítulo é semelhante a uma categoria. Sempre que possível, você deve usar categorias."
},
"experiementOptOut": {
"message": "Optar por sair de todos os experimentos futuros",
"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."
@@ -873,6 +912,9 @@
"hideForever": {
"message": "Ocultar para sempre"
},
"warningChatInfo": {
"message": "Percebemos que você estava cometendo alguns erros comuns que não são prejudiciais"
},
"Donate": {
"message": "Doar"
},
@@ -945,6 +987,9 @@
"LearnMore": {
"message": "Saiba mais"
},
"FullDetails": {
"message": "Ver Detalhes Completos"
},
"CopyDownvoteButtonInfo": {
"message": "Dá voto negativo e cria uma cópia local para você reenviar"
},
@@ -969,6 +1014,12 @@
"hideSegment": {
"message": "Ocultar segmento"
},
"skipSegment": {
"message": "Pular segmento"
},
"playChapter": {
"message": "Reproduzir capítulo"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Use a roda do mouse enquanto mantêm o cursor sobre a caixa de edição para ajustar o tempo rapidamente. Combinações das teclas ctrl e shift podem ser usadas para refinar as mudanças."
},
@@ -1031,5 +1082,69 @@
},
"confirmResetToDefault": {
"message": "Tem certeza de que deseja redefinir todas as configurações para os valores padrão? Essa ação não poderá ser desfeita."
},
"exportSegments": {
"message": "Exportar segmentos"
},
"importSegments": {
"message": "Importar segmentos"
},
"Import": {
"message": "Importar",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"selectYourCountry": {
"message": "Selecione o seu país"
},
"unsubmittedSegmentCountsZero": {
"message": "No momento, você não tem segmentos não enviados",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "segmento não enviado",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "segmentos não enviados",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "vídeo",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "vídeos",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Excluir todos os segmentos",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Tem certeza de que deseja excluir todos os segmentos não enviados?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Mostrar segmentos",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Ocultar segmentos",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "ID do Vídeo",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Número de segmentos",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "Ações",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Compartilhar como URL"
}
}

View File

@@ -35,6 +35,9 @@
"message": "Отображать сегменты как эпизоды",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Показывать название текущего сегмента рядом со временем"
},
"upvoteButtonInfo": {
"message": "Проголосовать за этот сегмент"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Закрыть окно"
},
"closeIcon": {
"message": "Кнопка закрытия"
},
"SubmitTimes": {
"message": "Отправить сегменты"
},
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "Скрыть сегмент"
},
"skipSegment": {
"message": "Пропустить сегмент"
},
"playChapter": {
"message": "Воспроизвести эпизод"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Наведите курсор на поле редактирования и используйте колесо мыши для быстрой настройки времени. Клавиши Ctrl или Shift могут быть использованы для точной настройки."
},

View File

@@ -35,6 +35,9 @@
"message": "Zobraziť segmenty ako kapitoly",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Zobraziť aktuálny segment vedľa času videa"
},
"upvoteButtonInfo": {
"message": "Hlasovať pre tento príspevok"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Zavrieť okno"
},
"closeIcon": {
"message": "Ikona pre zatvorenie"
},
"SubmitTimes": {
"message": "Odoslať segmenty"
},
@@ -974,6 +980,12 @@
"hideSegment": {
"message": "Skryť segment"
},
"skipSegment": {
"message": "Preskočiť segment"
},
"playChapter": {
"message": "Prehrať kapitolu"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Čas môžete rýchlo zmeniť kolieskom myši, ak je kurzor nad zadávacím políčkom. Pre jemné zmeny pritom držte kláves ctrl alebo shift."
},

View File

@@ -35,6 +35,9 @@
"message": "Gör segment som kapitel",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Visa nuvarande segment bredvid videotid"
},
"upvoteButtonInfo": {
"message": "Rösta på detta inskick"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Stäng popup"
},
"closeIcon": {
"message": "Stäng ikon"
},
"SubmitTimes": {
"message": "Skicka in segment"
},
@@ -1046,6 +1052,12 @@
"hideSegment": {
"message": "Dölj segment"
},
"skipSegment": {
"message": "Hoppa över segment"
},
"playChapter": {
"message": "Spela kapitel"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Använd mushjulet medan du håller muspekaren över redigeringsrutan för att snabbt justera tiden. Kombinationer av CTRL- eller SKIFT-tangenten kan användas för att finjustera tiden."
},

View File

@@ -26,11 +26,18 @@
"message": "kısım"
},
"SegmentsCap": {
"message": "Segmentler"
"message": "Kısımlar"
},
"Chapters": {
"message": "Bölümler"
},
"renderAsChapters": {
"message": "Kısımları bölüm olarak göster",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Geçerli kısmı video zamanının yanında göster"
},
"upvoteButtonInfo": {
"message": "Bu öneriye oy ver"
},
@@ -118,6 +125,9 @@
"closePopup": {
"message": "Açılır Pencereyi kapat"
},
"closeIcon": {
"message": "Kapat Simgesi"
},
"SubmitTimes": {
"message": "Kısımları gönder"
},
@@ -245,6 +255,9 @@
"enableShowCategoryWithoutPermission": {
"message": "Gönderim izni olmasa bile gönderim menüsünde kategorileri gösterme"
},
"whatShowCategoryWithoutPermission": {
"message": "Bazı kategoriler, minimum itibar gereksinimleri nedeniyle göndermek için izin gerektirir"
},
"showNotice": {
"message": "Uyarıyı Tekrar Göster"
},
@@ -462,6 +475,12 @@
"minDurationDescription": {
"message": "Ayarlanan değerden daha kısa kısımlar atlanmayacak veya oynatıcıda gösterilmeyecektir."
},
"enableManualSkipOnFullVideo": {
"message": "Tam bir video etiketi mevcut olduğunda manuel atlama özelliğini kullanın"
},
"whatManualSkipOnFullVideo": {
"message": "Video boyunca sponsorlu veya kendi reklamını yapıyorsa videoyu kesintisiz izlemek isteyenler için."
},
"skipNoticeDuration": {
"message": "Atlandı uyarısı süresi (saniye cinsinden):"
},
@@ -510,6 +529,9 @@
"exportOptionsUpload": {
"message": "Dosyadan yükle"
},
"whatExportOptions": {
"message": "Bu, JSON'daki tüm yapılandırmanızdır. Buna Özel Kullanıcı Kimliğiniz de dahildir, bu yüzden bunu paylaşırken dikkatli olun."
},
"setOptions": {
"message": "Seçenekleri Ayarla"
},
@@ -657,6 +679,9 @@
"category_preview": {
"message": "Ön İzleme/Özet"
},
"category_preview_description": {
"message": "Bu videoda veya diğer videolarda neler olduğunu gösteren, videonun ilerleyen zamanında tüm bilgilerin tekrarlandığı bir dizi klip koleksiyonudur."
},
"category_preview_guideline1": {
"message": "Daha sonra veya gelecekteki bir videoda görünen klipler"
},
@@ -669,6 +694,9 @@
"category_filler": {
"message": "Konuyla Alakasız / Şaka"
},
"category_filler_description": {
"message": "Videonun ana içeriğini anlamak için gerekli olmayan, yalnızca zaman geçirme veya mizah için eklenen teğet sahnelerdir. Bu, bağlam veya arka plan ayrıntılarını sağlayan kısımları içermemelidir. Bu, \"eğlence\" havasında olmadığınız zamanlar için tasarlanmış çok agresif bir kategoridir."
},
"category_filler_short": {
"message": "Alakasız Konu"
},
@@ -714,9 +742,18 @@
"category_chapter": {
"message": "Bölüm"
},
"category_chapter_description": {
"message": "Bir videonun ana kısımlarınııklayan özel adlandırılmış bölümler."
},
"category_chapter_guideline1": {
"message": "Sponsor marka isimlerinden bahsetme"
},
"category_chapter_guideline2": {
"message": "Genel kısımlar için daha büyük bölümler kullanın"
},
"category_chapter_guideline3": {
"message": "Daha küçük bölümler daha büyük bölümlerin içine yerleştirilebilir"
},
"category_livestream_messages": {
"message": "Canlı Yayın: Bağış/Mesaj Okuma"
},
@@ -822,6 +859,9 @@
"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 kimliği henüz yüklenmedi. Gömülü bir video kullanıyorsanız, bunun yerine YouTube ana sayfasından izlemeyi deneyin. Bu, YouTube düzenindeki değişikliklerden de kaynaklanabilir, eğer öyleyse, buraya bir yorum yazın:"
},
"invidiousPermissionRefresh": {
"message": "Tarayıcınız, eklentinin Invidious ve diğer 3. taraf sitelerde çalışması için gerekli izni kaldırdı. Bu izni yeniden etkinleştirmek için lütfen aşağıdaki butona tıklayın."
},
"acceptPermission": {
"message": "İzni kabul et"
},
@@ -888,6 +928,9 @@
"categoryPillTitleText": {
"message": "Bu videonun bütünü bu şekilde sınıflandırılmış ve kısımları birbirinden ayrılamayacak kadar sıkı bütünleştirilmiş"
},
"chapterNameTooltipWarning": {
"message": "Bölüm adlarınızdan biri bir kategoriye benziyor. Bunun yerine mümkün olduğunda kategorileri kullanmalısınız."
},
"experiementOptOut": {
"message": "Gelecekteki deneylerin hiçbirine katılma",
"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."
@@ -895,6 +938,9 @@
"hideForever": {
"message": "Asla gösterme"
},
"warningChatInfo": {
"message": "Kötü amaçlı olmayan bazı yaygın hatalar yaptığınızı fark ettik"
},
"warningTitle": {
"message": "Bir uyarı aldın"
},
@@ -904,6 +950,9 @@
"warningConfirmButton": {
"message": "Nedenini anlıyorum"
},
"warningError": {
"message": "Uyarıyı tanımlamaya çalışırken hata oluştu:"
},
"Donate": {
"message": "Bağış Yap"
},
@@ -1003,6 +1052,12 @@
"hideSegment": {
"message": "Segmenti gizleyin"
},
"skipSegment": {
"message": "Kısmı atla"
},
"playChapter": {
"message": "Bölümü oynat"
},
"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."
},
@@ -1010,11 +1065,11 @@
"message": "Yeni! Videonun bütünü sponsor veya kendi reklamıysa bu uyarıyı görün"
},
"dayAbbreviation": {
"message": "d",
"message": "g",
"description": "100d"
},
"hourAbbreviation": {
"message": "h",
"message": "s",
"description": "100h"
},
"optionsTabBehavior": {
@@ -1069,22 +1124,48 @@
"exportSegments": {
"message": "Segmentleri dışa aktar"
},
"importSegments": {
"message": "Kısımları içe aktar"
},
"Import": {
"message": "İçeri aktar",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"redeemSuccess": {
"message": "Kullanım Başarılı!"
},
"redeemFailed": {
"message": "Lisans anahtarı geçersiz"
},
"hideUpsells": {
"message": "Ekstra ödeme olmadan kullanılamayan özellikleri gizle"
},
"chooseACountry": {
"message": "Bir ülke seçiniz"
},
"noDiscount": {
"message": "İndirim için uygun değilsiniz"
},
"discountLink": {
"message": "İndirim Bağlantısı (Pembe fiyatı gör)"
},
"selectYourCountry": {
"message": "Ülkenizi seçin"
},
"alreadyDonated": {
"message": "Şimdiye kadar herhangi bir miktarda bağış yaptıysanız, bu adrese e-posta göndererek ücretsiz erişim hakkına sahip olabilirsin:",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Lisans satın almaya gücünüz yetmiyorsa, indirim almaya uygun olup olmadığınızı görmek için {here} tıklayın",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Patreon ile giriş yap"
},
"redeem": {
"message": "Kullan"
},
"joinOnPatreon": {
"message": "Patreon'da Destekle"
},
@@ -1093,5 +1174,74 @@
},
"enterLicenseKey": {
"message": "Lisans Anahtarını Girin"
},
"chaptersPage1": {
"message": "SponsorBlock'un topluluk kaynaklı bölümler özelliği yalnızca lisans satın alan veya geçmiş katkılarından dolayı ücretsiz erişim hakkı verilen kişiler tarafından kullanılabilir"
},
"chaptersPage2": {
"message": "Not: Bölüm gönderme izni hala hesaplanan itibara dayanmaktadır. Bir lisans satın almak, yalnızca başkaları tarafından gönderilen bölümleri görüntülemenize izin verir",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Yeni Özellik: Topluluk kaynaklı özel bölümler. Bunlar, giderek daha doğru hale gelmek için istiflenebilen videolardaki özel adlandırılmış bölümlerdir. Bu videodaki gibi gönderilen bölümleri görüntülemek için bir lisans satın alın: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Yeni Özellik: Topluluk kaynaklı özel bölümler. Bunlar, giderek daha doğru hale gelmek için istiflenebilen videolardaki özel adlandırılmış bölümlerdir. Ücretsiz erişiminiz var, seçeneklerden etkinleştirin."
},
"unsubmittedSegmentCounts": {
"message": "Şu anda {1} da {0} var",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "Gönderilmemiş kısmınız bulunmamaktadır",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "gönderilmemiş kısım",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "gönderilmemiş kısımlar",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "video",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "videolar",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Tüm kısımları temizle",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Gönderilmemiş tüm kısımları temizlemek istediğinizden emin misiniz?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Kısımları göster",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Kısımları gizle",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "Video ID",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Kısım Sayısı",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "İşlemler",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "URL olarak paylaş"
}
}

View File

@@ -25,6 +25,19 @@
"Segments": {
"message": "片段"
},
"SegmentsCap": {
"message": "片段"
},
"Chapters": {
"message": "章节"
},
"renderAsChapters": {
"message": "将片段显示为章节",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "在视频时间旁显示当前片段"
},
"upvoteButtonInfo": {
"message": "为这个提交点赞"
},
@@ -52,6 +65,9 @@
"reskip": {
"message": "继续跳过"
},
"unmute": {
"message": "取消静音"
},
"paused": {
"message": "已暂停"
},
@@ -85,6 +101,9 @@
"noVideoID": {
"message": "未找到 YouTube 视频。\n如果识别错误请刷新此页面。"
},
"refreshSegments": {
"message": "刷新片段"
},
"success": {
"message": "成功 "
},
@@ -106,6 +125,9 @@
"closePopup": {
"message": "关闭弹窗"
},
"closeIcon": {
"message": "关闭图标"
},
"SubmitTimes": {
"message": "提交片段"
},
@@ -155,6 +177,9 @@
"setUsername": {
"message": "设定用户名"
},
"copySegmentID": {
"message": "复制片段 ID"
},
"discordAdvert": {
"message": "快加入官方 Discord 服务器来提供建议与反馈!"
},
@@ -179,6 +204,9 @@
"hideInfoButton": {
"message": "在 Youtube 播放器上隐藏信息按钮"
},
"autoHideInfoButton": {
"message": "自动隐藏信息按钮"
},
"hideDeleteButton": {
"message": "在 Youtube 播放器上隐藏删除按钮"
},
@@ -251,6 +279,13 @@
"skip": {
"message": "跳过"
},
"mute": {
"message": "静音"
},
"full": {
"message": "整个视频",
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
},
"skip_category": {
"message": "跳过{0}"
},
@@ -304,6 +339,9 @@
"supportOtherSites": {
"message": "支持第三方 YouTube 网站"
},
"supportedSites": {
"message": "支持的站点: "
},
"optionsInfo": {
"message": "启用 Invidious 支持,禁用自动跳过,隐藏按钮等等。"
},
@@ -358,6 +396,15 @@
"exportOptions": {
"message": "导入/导出所有选项"
},
"exportOptionsCopy": {
"message": "编辑/复制"
},
"exportOptionsDownload": {
"message": "保存到文件"
},
"exportOptionsUpload": {
"message": "从文件加载"
},
"setOptions": {
"message": "设定选项"
},
@@ -382,6 +429,9 @@
"preview": {
"message": "预览"
},
"unsubmitted": {
"message": "未提交"
},
"inspect": {
"message": "检查"
},
@@ -400,22 +450,38 @@
"copyDebugInformationComplete": {
"message": "调试信息已复制到剪切板中。 您可以随意移除任何您不想分享的信息。请将其另存为 .txt 文件或粘贴到错误报告中。"
},
"keyAlreadyUsed": {
"message": "此快捷键已绑定到另一个动作。请选择其他快捷键。"
},
"to": {
"message": "到",
"description": "Used between segments. Example: 1:20 to 1:30"
},
"CopiedExclamation": {
"message": "复制成功!",
"description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
},
"category_sponsor": {
"message": "赞助商广告"
},
"category_sponsor_description": {
"message": "付费推广、付费推荐和直接广告。不应用于自我推广或免费提及、推荐他们喜欢的事物/创作者/网站/产品。"
},
"category_sponsor_guideline1": {
"message": "付费推广"
},
"category_sponsor_guideline2": {
"message": "捐赠或自制周边不属于此项"
},
"category_selfpromo": {
"message": "未收钱的/自我推销"
},
"category_selfpromo_description": {
"message": "类似于 “赞助商广告” ,但为无报酬或自我推广。包括有关商品、捐赠的部分或合作者的信息。"
},
"category_selfpromo_guideline1": {
"message": "捐赠、会员和自制周边"
},
"category_interaction": {
"message": "互动提醒(订阅)"
},

View File

@@ -30,6 +30,10 @@
transform: scaleY(0.625) translateY(-30%) translateY(1.5px);
}
.ytp-big-mode .sponsorTwoTooltips .sponsorCategoryTooltip {
top: 75px !important;
}
.progress-bar-line > #previewbar {
height: 3px;
}
@@ -738,7 +742,7 @@ input::-webkit-inner-spin-button {
}
.sponsorBlockLockedColor {
color: #ffc83d;
color: #ffc83d !important;
}
.sponsorBlockRectangleTooltip {

View File

@@ -50,37 +50,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
******************************
object-assign
4.1.1 <https://github.com/sindresorhus/object-assign>
The MIT License (MIT)
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
******************************
react
17.0.2 <https://github.com/facebook/react>
18.2.0 <https://github.com/facebook/react>
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
@@ -107,7 +80,7 @@ SOFTWARE.
******************************
react-dom
17.0.2 <https://github.com/facebook/react>
18.2.0 <https://github.com/facebook/react>
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
@@ -134,7 +107,7 @@ SOFTWARE.
******************************
scheduler
0.20.2 <https://github.com/facebook/react>
0.23.0 <https://github.com/facebook/react>
MIT License
Copyright (c) Facebook, Inc. and its affiliates.

View File

@@ -24,7 +24,7 @@ if (utils.isFirefox()) {
utils.wait(() => Config.config !== null).then(function() {
if (Config.config.supportInvidious) utils.setupExtraSiteContentScripts();
});
}
}
function onTabUpdatedListener(tabId: number) {
chrome.tabs.sendMessage(tabId, {
@@ -77,17 +77,17 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
ok: response.ok
});
});
return true;
case "submitVote":
submitVote(request.type, request.UUID, request.category).then(callback);
//this allows the callback to be called later
return true;
case "registerContentScript":
case "registerContentScript":
registerFirefoxContentScript(request);
return false;
case "unregisterContentScript":
case "unregisterContentScript":
unregisterFirefoxContentScript(request.id)
return false;
case "tabs": {
@@ -106,6 +106,8 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
return true;
}
case "time":
case "infoUpdated":
case "videoChanged":
if (sender.tab) {
popupPort[sender.tab.id]?.postMessage(request);
}
@@ -156,8 +158,8 @@ chrome.runtime.onInstalled.addListener(function () {
/**
* Only works on Firefox.
* Firefox requires that it be applied after every extension restart.
*
* @param {JSON} options
*
* @param {JSON} options
*/
function registerFirefoxContentScript(options: Registration) {
const oldRegistration = contentScriptRegistrations[options.id];
@@ -174,7 +176,7 @@ function registerFirefoxContentScript(options: Registration) {
/**
* Only works on Firefox.
* Firefox requires that this is handled by the background script
*
*
*/
function unregisterFirefoxContentScript(id: string) {
contentScriptRegistrations[id].unregister();
@@ -225,10 +227,10 @@ async function asyncRequestToServer(type: string, address: string, data = {}) {
/**
* Sends a request to the specified url
*
*
* @param type The request type "GET", "POST", etc.
* @param address The address to add to the SponsorBlock server address
* @param callback
* @param callback
*/
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
// If GET, convert JSON to parameters
@@ -248,4 +250,4 @@ async function sendRequestToCustomServer(type: string, url: string, data = {}) {
});
return response;
}
}

View File

@@ -8,41 +8,42 @@ enum CountdownMode {
}
export interface NoticeProps {
noticeTitle: string,
noticeTitle: string;
maxCountdownTime?: () => number,
dontPauseCountdown?: boolean,
amountOfPreviousNotices?: number,
showInSecondSlot?: boolean,
timed?: boolean,
idSuffix?: string,
maxCountdownTime?: () => number;
dontPauseCountdown?: boolean;
amountOfPreviousNotices?: number;
showInSecondSlot?: boolean;
timed?: boolean;
idSuffix?: string;
fadeIn?: boolean,
startFaded?: boolean,
firstColumn?: React.ReactElement[] | React.ReactElement,
firstRow?: React.ReactElement,
bottomRow?: React.ReactElement[],
fadeIn?: boolean;
startFaded?: boolean;
firstColumn?: React.ReactElement[] | React.ReactElement;
firstRow?: React.ReactElement;
bottomRow?: React.ReactElement[];
smaller?: boolean,
limitWidth?: boolean,
extraClass?: string,
hideLogo?: boolean,
hideRightInfo?: boolean,
smaller?: boolean;
limitWidth?: boolean;
extraClass?: string;
hideLogo?: boolean;
hideRightInfo?: boolean;
// Callback for when this is closed
closeListener: () => void,
onMouseEnter?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void,
closeListener: () => void;
onMouseEnter?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
zIndex?: number,
style?: React.CSSProperties
zIndex?: number;
style?: React.CSSProperties;
biggerCloseButton?: boolean;
children?: React.ReactNode;
}
export interface NoticeState {
maxCountdownTime: () => number,
maxCountdownTime: () => number;
countdownTime: number,
countdownMode: CountdownMode,
countdownTime: number;
countdownMode: CountdownMode;
mouseHovering: boolean;

View File

@@ -1,10 +1,11 @@
import * as React from "react";
export interface NoticeTextSelectionProps {
icon?: string,
text: string,
idSuffix: string,
onClick?: (event: React.MouseEvent) => unknown
icon?: string;
text: string;
idSuffix: string;
onClick?: (event: React.MouseEvent) => unknown;
children?: React.ReactNode;
}
export interface NoticeTextSelectionState {

View File

@@ -14,15 +14,16 @@ import { DEFAULT_CATEGORY } from "../utils/categoryUtils";
const utils = new Utils();
export interface SponsorTimeEditProps {
index: number,
index: number;
idSuffix: string,
idSuffix: string;
// Contains functions and variables from the content script needed by the skip notice
contentContainer: ContentContainer,
contentContainer: ContentContainer;
submissionNotice: SubmissionNoticeComponent;
categoryList?: Category[];
categoryChangeListener?: (index: number, category: Category) => void;
children?: React.ReactNode;
}
export interface SponsorTimeEditState {
@@ -266,6 +267,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
{chrome.i18n.getMessage("delete")}
</span>
{(!isNaN(segment[1]) && ![ActionType.Poi, ActionType.Full].includes(sponsorTime.actionType))
&& sponsorTime.actionType !== ActionType.Chapter ? (
<span id={"sponsorTimePreviewButton" + this.idSuffix}
className="sponsorTimeEditButton"
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}>
{chrome.i18n.getMessage("preview")}
</span>
): ""}
{(!isNaN(segment[1]) && sponsorTime.actionType != ActionType.Full) ? (
<span id={"sponsorTimeInspectButton" + this.idSuffix}
className="sponsorTimeEditButton"
@@ -274,12 +284,12 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
</span>
): ""}
{(!isNaN(segment[1]) && ![ActionType.Poi, ActionType.Full].includes(sponsorTime.actionType)) ? (
{(!isNaN(segment[1]) && ![ActionType.Poi, ActionType.Full].includes(sponsorTime.actionType))
&& sponsorTime.actionType === ActionType.Chapter ? (
<span id={"sponsorTimePreviewButton" + this.idSuffix}
className="sponsorTimeEditButton"
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}>
{sponsorTime.actionType !== ActionType.Chapter ? chrome.i18n.getMessage("preview")
: chrome.i18n.getMessage("End")}
{chrome.i18n.getMessage("End")}
</span>
): ""}
@@ -531,9 +541,14 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
if (time === null) time = sponsorTime.segment[0];
const addedTime = sponsorTime.segment.length === 1;
sponsorTime.segment[index] = time;
if (sponsorTime.actionType === ActionType.Poi) sponsorTime.segment[1] = time;
if (addedTime) {
this.props.contentContainer().updateEditButtonsOnPlayer();
}
this.setState({
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
}, () => this.saveEditTimes());
@@ -565,6 +580,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
saveEditTimes(): void {
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
const category = this.categoryOptionRef.current.value as Category
if (this.state.editing) {
const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
@@ -572,11 +588,18 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
// Change segment time only if the format was correct
if (startTime !== null && endTime !== null) {
const addingTime = sponsorTimesSubmitting[this.props.index].segment.length === 1;
sponsorTimesSubmitting[this.props.index].segment = [startTime, endTime];
if (addingTime) {
this.props.contentContainer().updateEditButtonsOnPlayer();
}
}
} else if (this.state.sponsorTimeEdits[1] === null && category === "outro") {
sponsorTimesSubmitting[this.props.index].segment[1] = this.props.contentContainer().v.duration;
this.props.contentContainer().updateEditButtonsOnPlayer();
}
const category = this.categoryOptionRef.current.value as Category
sponsorTimesSubmitting[this.props.index].category = category;
const actionType = this.getNextActionType(category, this.actionTypeOptionRef?.current?.value as ActionType);

View File

@@ -20,8 +20,8 @@ export interface SubmissionNoticeProps {
}
export interface SubmissionNoticeState {
noticeTitle: string,
messages: string[],
noticeTitle: string;
messages: string[];
idSuffix: string;
}

View File

@@ -13,6 +13,7 @@ export interface CategorySkipOptionsProps {
category: Category;
defaultColor?: string;
defaultPreviewColor?: string;
children?: React.ReactNode;
}
export interface CategorySkipOptionsState {

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import Config from "../../config";
import { Keybind } from "../../types";
import KeybindDialogComponent from "./KeybindDialogComponent";
@@ -14,6 +14,7 @@ export interface KeybindState {
}
let dialog;
let root: Root;
class KeybindComponent extends React.Component<KeybindProps, KeybindState> {
constructor(props: KeybindProps) {
@@ -56,11 +57,12 @@ class KeybindComponent extends React.Component<KeybindProps, KeybindState> {
dialog = parent.document.createElement("div");
dialog.id = "keybind-dialog";
parent.document.body.prepend(dialog);
ReactDOM.render(<KeybindDialogComponent option={this.props.option} closeListener={(updateWith) => this.closeEditDialog(updateWith)} />, dialog);
root = createRoot(dialog);
root.render(<KeybindDialogComponent option={this.props.option} closeListener={(updateWith) => this.closeEditDialog(updateWith)} />);
}
closeEditDialog(updateWith: Keybind): void {
ReactDOM.unmountComponentAtNode(dialog);
root.unmount();
dialog.remove();
if (updateWith != null)
this.setState({keybind: updateWith});

View File

@@ -5,6 +5,7 @@ import { exportTimes, exportTimesAsHashParam } from "../../utils/exporter";
export interface UnsubmittedVideosListItemProps {
videoID: string;
children?: React.ReactNode;
}
export interface UnsubmittedVideosListItemState {

View File

@@ -7,7 +7,7 @@ export interface UnsubmittedVideosProps {
}
export interface UnsubmittedVideosState {
tableVisible: boolean,
tableVisible: boolean;
}
class UnsubmittedVideosComponent extends React.Component<UnsubmittedVideosProps, UnsubmittedVideosState> {

View File

@@ -8,121 +8,124 @@ export interface Permission {
}
interface SBConfig {
userID: string,
isVip: boolean,
permissions: Record<Category, Permission>,
userID: string;
isVip: boolean;
permissions: Record<Category, Permission>;
/* Contains unsubmitted segments that the user has created. */
unsubmittedSegments: Record<string, SponsorTime[]>,
defaultCategory: Category,
renderSegmentsAsChapters: boolean,
whitelistedChannels: string[],
forceChannelCheck: boolean,
minutesSaved: number,
skipCount: number,
sponsorTimesContributed: number,
submissionCountSinceCategories: number, // New count used to show the "Read The Guidelines!!" message
showTimeWithSkips: boolean,
disableSkipping: boolean,
muteSegments: boolean,
fullVideoSegments: boolean,
manualSkipOnFullVideo: boolean,
trackViewCount: boolean,
trackViewCountInPrivate: boolean,
trackDownvotes: boolean,
dontShowNotice: boolean,
noticeVisibilityMode: NoticeVisbilityMode,
hideVideoPlayerControls: boolean,
hideInfoButtonPlayerControls: boolean,
hideDeleteButtonPlayerControls: boolean,
hideUploadButtonPlayerControls: boolean,
hideSkipButtonPlayerControls: boolean,
hideDiscordLaunches: number,
hideDiscordLink: boolean,
invidiousInstances: string[],
supportInvidious: boolean,
serverAddress: string,
minDuration: number,
skipNoticeDuration: number,
audioNotificationOnSkip: boolean,
checkForUnlistedVideos: boolean,
testingServer: boolean,
refetchWhenNotFound: boolean,
ytInfoPermissionGranted: boolean,
allowExpirements: boolean,
showDonationLink: boolean,
showPopupDonationCount: number,
showUpsells: boolean,
donateClicked: number,
autoHideInfoButton: boolean,
autoSkipOnMusicVideos: boolean,
unsubmittedSegments: Record<string, SponsorTime[]>;
defaultCategory: Category;
renderSegmentsAsChapters: boolean;
whitelistedChannels: string[];
forceChannelCheck: boolean;
minutesSaved: number;
skipCount: number;
sponsorTimesContributed: number;
submissionCountSinceCategories: number; // New count used to show the "Read The Guidelines!!" message
showTimeWithSkips: boolean;
disableSkipping: boolean;
muteSegments: boolean;
fullVideoSegments: boolean;
manualSkipOnFullVideo: boolean;
trackViewCount: boolean;
trackViewCountInPrivate: boolean;
trackDownvotes: boolean;
dontShowNotice: boolean;
noticeVisibilityMode: NoticeVisbilityMode;
hideVideoPlayerControls: boolean;
hideInfoButtonPlayerControls: boolean;
hideDeleteButtonPlayerControls: boolean;
hideUploadButtonPlayerControls: boolean;
hideSkipButtonPlayerControls: boolean;
hideDiscordLaunches: number;
hideDiscordLink: boolean;
invidiousInstances: string[];
supportInvidious: boolean;
serverAddress: string;
minDuration: number;
skipNoticeDuration: number;
audioNotificationOnSkip: boolean;
checkForUnlistedVideos: boolean;
testingServer: boolean;
refetchWhenNotFound: boolean;
ytInfoPermissionGranted: boolean;
allowExpirements: boolean;
showDonationLink: boolean;
showPopupDonationCount: number;
showUpsells: boolean;
donateClicked: number;
autoHideInfoButton: boolean;
autoSkipOnMusicVideos: boolean;
colorPalette: {
red: string,
white: string,
locked: string
},
scrollToEditTimeUpdate: boolean,
categoryPillUpdate: boolean,
showChapterInfoMessage: boolean,
darkMode: boolean,
showCategoryGuidelines: boolean,
showCategoryWithoutPermission: boolean,
showSegmentNameInChapterBar: boolean,
red: string;
white: string;
locked: string;
};
scrollToEditTimeUpdate: boolean;
categoryPillUpdate: boolean;
showChapterInfoMessage: boolean;
darkMode: boolean;
showCategoryGuidelines: boolean;
showCategoryWithoutPermission: boolean;
showSegmentNameInChapterBar: boolean;
useVirtualTime: boolean;
showSegmentFailedToFetchWarning: boolean;
// Used to cache calculated text color info
categoryPillColors: {
[key in Category]: {
lastColor: string,
textColor: string
lastColor: string;
textColor: string;
}
}
};
skipKeybind: Keybind,
startSponsorKeybind: Keybind,
submitKeybind: Keybind,
nextChapterKeybind: Keybind,
previousChapterKeybind: Keybind,
skipKeybind: Keybind;
startSponsorKeybind: Keybind;
submitKeybind: Keybind;
nextChapterKeybind: Keybind;
previousChapterKeybind: Keybind;
// What categories should be skipped
categorySelections: CategorySelection[],
categorySelections: CategorySelection[];
payments: {
licenseKey: string,
lastCheck: number,
freeAccess: boolean,
chaptersAllowed: boolean
}
licenseKey: string;
lastCheck: number;
lastFreeCheck: number;
freeAccess: boolean;
chaptersAllowed: boolean;
};
// Preview bar
barTypes: {
"preview-chooseACategory": PreviewBarOption,
"sponsor": PreviewBarOption,
"preview-sponsor": PreviewBarOption,
"selfpromo": PreviewBarOption,
"preview-selfpromo": PreviewBarOption,
"exclusive_access": PreviewBarOption,
"interaction": PreviewBarOption,
"preview-interaction": PreviewBarOption,
"intro": PreviewBarOption,
"preview-intro": PreviewBarOption,
"outro": PreviewBarOption,
"preview-outro": PreviewBarOption,
"preview": PreviewBarOption,
"preview-preview": PreviewBarOption,
"music_offtopic": PreviewBarOption,
"preview-music_offtopic": PreviewBarOption,
"poi_highlight": PreviewBarOption,
"preview-poi_highlight": PreviewBarOption,
"filler": PreviewBarOption,
"preview-filler": PreviewBarOption,
}
"preview-chooseACategory": PreviewBarOption;
"sponsor": PreviewBarOption;
"preview-sponsor": PreviewBarOption;
"selfpromo": PreviewBarOption;
"preview-selfpromo": PreviewBarOption;
"exclusive_access": PreviewBarOption;
"interaction": PreviewBarOption;
"preview-interaction": PreviewBarOption;
"intro": PreviewBarOption;
"preview-intro": PreviewBarOption;
"outro": PreviewBarOption;
"preview-outro": PreviewBarOption;
"preview": PreviewBarOption;
"preview-preview": PreviewBarOption;
"music_offtopic": PreviewBarOption;
"preview-music_offtopic": PreviewBarOption;
"poi_highlight": PreviewBarOption;
"preview-poi_highlight": PreviewBarOption;
"filler": PreviewBarOption;
"preview-filler": PreviewBarOption;
};
}
export type VideoDownvotes = { segments: { uuid: HashedValue, hidden: SponsorHideType }[] , lastAccess: number };
export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[] ; lastAccess: number };
interface SBStorage {
/* VideoID prefixes to UUID prefixes */
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>,
navigationApiAvailable: boolean,
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>;
navigationApiAvailable: boolean;
}
export interface SBObject {
@@ -199,6 +202,8 @@ const Config: SBObject = {
showCategoryGuidelines: true,
showCategoryWithoutPermission: false,
showSegmentNameInChapterBar: true,
useVirtualTime: true,
showSegmentFailedToFetchWarning: true,
categoryPillColors: {},
@@ -229,6 +234,7 @@ const Config: SBObject = {
payments: {
licenseKey: null,
lastCheck: 0,
lastFreeCheck: 0,
freeAccess: false,
chaptersAllowed: false
},
@@ -338,7 +344,7 @@ const Config: SBObject = {
// Function setup
function configProxy(): { sync: SBConfig, local: SBStorage } {
function configProxy(): { sync: SBConfig; local: SBStorage } {
chrome.storage.onChanged.addListener((changes: {[key: string]: chrome.storage.StorageChange}, areaName) => {
if (areaName === "sync") {
for (const key in changes) {

View File

@@ -8,6 +8,7 @@ import {
ContentContainer,
HashedValue,
Keybind,
PageType,
ScheduledTime,
SegmentUUID,
SkipToTimeParams,
@@ -18,7 +19,6 @@ import {
ToggleSkippable,
VideoID,
VideoInfo,
PageType
} from "./types";
import Utils from "./utils";
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
@@ -55,6 +55,8 @@ let sponsorVideoID: VideoID = null;
// List of open skip notices
const skipNotices: SkipNotice[] = [];
let activeSkipKeybindElement: ToggleSkippable = null;
let retryFetchTimeout: NodeJS.Timeout = null;
let shownSegmentFailedToFetchWarning = false;
// JSON video info
let videoInfo: VideoInfo = null;
@@ -67,7 +69,7 @@ let channelIDInfo: ChannelIDInfo;
// Locked Categories in this tab, like: ["sponsor","intro","outro"]
let lockedCategories: Category[] = [];
// Used to calculate a more precise "virtual" video time
let lastKnownVideoTime: { videoTime: number, preciseTime: number } = {
let lastKnownVideoTime: { videoTime: number; preciseTime: number } = {
videoTime: null,
preciseTime: null
};
@@ -124,13 +126,14 @@ let categoryPill: CategoryPill = null;
let controls: HTMLElement | null = null;
/** Contains buttons created by `createButton()`. */
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement, setupListener: boolean}> = {};
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)));
// wait for hover preview to appear, and refresh attachments if ever found
utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments())
utils.waitForElement("a.ytp-title-link[data-sessionlink='feature=player-title']").then(() => videoIDChange(getYouTubeVideoID(document)).then())
utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments());
utils.waitForElement("a.ytp-title-link[data-sessionlink='feature=player-title']")
.then(() => videoIDChange(getYouTubeVideoID(document)));
addPageListeners();
addHotkeyListener();
@@ -213,7 +216,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
case "getVideoID":
sendResponse({
videoID: sponsorVideoID,
creatingSegment: isSegmentCreationInProgress(),
});
break;
@@ -241,15 +243,9 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
// update video on refresh if videoID invalid
if (!sponsorVideoID) videoIDChange(getYouTubeVideoID(document));
// fetch segments
sponsorsLookup(false).then(() => sendResponse({
found: sponsorDataFound,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
}));
sponsorsLookup(false);
return true;
break;
case "unskip":
unskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), null, true);
break;
@@ -274,10 +270,15 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
const importedSegments = importTimes(request.data, video.duration);
let addedSegments = false;
for (const segment of importedSegments) {
if (!sponsorTimesSubmitting.concat(sponsorTimes ?? []).some(
if (!sponsorTimesSubmitting.some(
(s) => Math.abs(s.segment[0] - segment.segment[0]) < 1
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)
&& (segment.category !== "chapter" || utils.getCategorySelection("chapter"))) {
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)) {
if (segment.category === "chapter" && !utils.getCategorySelection("chapter")) {
segment.category = "chooseACategory" as Category;
segment.actionType = ActionType.Skip;
segment.description = "";
}
sponsorTimesSubmitting.push(segment);
addedSegments = true;
}
@@ -289,6 +290,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false);
submitSponsorTimes();
}
sendResponse({
@@ -343,6 +345,8 @@ function resetValues() {
sponsorTimes = [];
existingChaptersImported = false;
sponsorSkipped = [];
lastResponseStatus = 0;
shownSegmentFailedToFetchWarning = false;
sponsorVideoID = null;
videoInfo = null;
@@ -382,7 +386,9 @@ function resetValues() {
categoryPill?.setVisibility(false);
}
async function videoIDChange(id): Promise<void> {
async function videoIDChange(id: string): Promise<void> {
// don't switch to invalid value
if (!id && sponsorVideoID && !document?.URL?.includes("youtube.com/clip/")) return;
//if the id has not changed return unless the video element has changed
if (sponsorVideoID === id && (isVisible(video) || !video)) return;
@@ -434,10 +440,14 @@ async function videoIDChange(id): Promise<void> {
}
}
//close popup
closeInfoMenu();
// Notify the popup about the video change
chrome.runtime.sendMessage({
message: "videoChanged",
videoID: sponsorVideoID,
whitelisted: channelWhitelisted
});
sponsorsLookup(id);
sponsorsLookup();
// Make sure all player buttons are properly added
updateVisibilityOfPlayerControlsButton();
@@ -507,7 +517,7 @@ function createPreviewBar(): void {
if (el) {
const chapterVote = new ChapterVote(voteAsync);
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote);
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote, () => importExistingChapters(false));
updatePreviewBar();
@@ -532,7 +542,7 @@ function durationChangeListener(): void {
function videoOnReadyListener(): void {
createPreviewBar();
updatePreviewBar();
createButtons();
updateVisibilityOfPlayerControlsButton()
}
function cancelSponsorSchedule(): void {
@@ -708,10 +718,10 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
function getVirtualTime(): number {
const virtualTime = lastTimeFromWaitingEvent ?? (lastKnownVideoTime.videoTime ?
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
(performance.now() - lastKnownVideoTime.preciseTime) * video.playbackRate / 1000 + lastKnownVideoTime.videoTime : null);
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) {
if (Config.config.useVirtualTime && !utils.isFirefox() && !isSafari() && virtualTime
&& Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) {
return virtualTime;
} else {
return video.currentTime;
@@ -873,9 +883,19 @@ function setupVideoListeners() {
}
}
});
video.addEventListener('ratechange', () => startSponsorSchedule());
video.addEventListener('ratechange', () => {
updateVirtualTime();
lastTimeFromWaitingEvent = null;
startSponsorSchedule();
});
// Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740)
video.addEventListener('videoSpeed_ratechange', () => startSponsorSchedule());
video.addEventListener('videoSpeed_ratechange', () => {
updateVirtualTime();
lastTimeFromWaitingEvent = null;
startSponsorSchedule();
});
const paused = () => {
// Reset lastCheckVideoTime
lastCheckVideoTime = -1;
@@ -944,29 +964,30 @@ async function sponsorsLookup(keepOldSubmissions = true) {
setupVideoMutationListener();
const showChapterMessage = Config.config.showUpsells
&& Config.config.payments.lastCheck !== 0
&& Config.config.payments.lastCheck !== 0
&& !noRefreshFetchingChaptersAllowed()
&& Config.config.showChapterInfoMessage
&& Config.config.skipCount > 200
&& Math.random() > 0.8;
&& Config.config.skipCount > 200;
if (!showChapterMessage
&& Config.config.showChapterInfoMessage
&& Config.config.payments.freeAccess
&& !utils.getCategorySelection("chapter")) {
if (!showChapterMessage
&& Config.config.showChapterInfoMessage
&& Config.config.payments.freeAccess) {
Config.config.showChapterInfoMessage = false;
const prependElement = document.querySelector(".ytp-chrome-bottom") as HTMLElement;
if (prependElement) {
Config.config.showChapterInfoMessage = false;
new Tooltip({
text: chrome.i18n.getMessage("chapterNewFeature2"),
linkOnClick: () => void chrome.runtime.sendMessage({ "message": "openConfig" }),
referenceNode: prependElement.parentElement,
prependElement,
timeout: 1500,
leftOffset: "20px",
positionRealtive: false
});
if (!utils.getCategorySelection("chapter")) {
const prependElement = document.querySelector(".ytp-chrome-bottom") as HTMLElement;
if (prependElement) {
Config.config.showChapterInfoMessage = false;
new Tooltip({
text: chrome.i18n.getMessage("chapterNewFeature2"),
linkOnClick: () => void chrome.runtime.sendMessage({ "message": "openConfig" }),
referenceNode: prependElement.parentElement,
prependElement,
timeout: 1500,
leftOffset: "20px",
positionRealtive: false
});
}
}
}
@@ -999,6 +1020,14 @@ async function sponsorsLookup(keepOldSubmissions = true) {
?.sort((a, b) => a.segment[0] - b.segment[0]);
if (!recievedSegments || !recievedSegments.length) {
// return if no video found
chrome.runtime.sendMessage({
message: "infoUpdated",
found: false,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
});
retryFetch(404);
return;
}
@@ -1088,6 +1117,16 @@ async function sponsorsLookup(keepOldSubmissions = true) {
importExistingChapters(true);
// notify popup of segment changes
chrome.runtime.sendMessage({
message: "infoUpdated",
found: sponsorDataFound,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
});
if (Config.config.isVip) {
lockedCategoriesLookup();
}
@@ -1133,18 +1172,19 @@ async function lockedCategoriesLookup(): Promise<void> {
}
function retryFetch(errorCode: number): void {
if (!Config.config.refetchWhenNotFound) return;
sponsorDataFound = false;
if (!Config.config.refetchWhenNotFound) return;
if (errorCode !== 404 && retryCount > 1) {
if (retryFetchTimeout) clearTimeout(retryFetchTimeout);
if ((errorCode !== 404 && retryCount > 1) || (errorCode !== 404 && retryCount > 10)) {
// Too many errors (50x), give up
return;
}
retryCount++;
const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
setTimeout(() => {
const delay = errorCode === 404 ? (30000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
retryFetchTimeout = setTimeout(() => {
if (sponsorVideoID && sponsorTimes?.length === 0
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
sponsorsLookup();
@@ -1213,14 +1253,14 @@ function startSkipScheduleCheckingForStartSponsors() {
}
}
function getYouTubeVideoID(document: Document, url?: string): string | boolean {
function getYouTubeVideoID(document: Document, url?: string): string {
url ||= document.URL;
// pageType shortcut
if (pageType === PageType.Channel) return getYouTubeVideoIDFromDocument()
if (pageType === PageType.Channel) return getYouTubeVideoIDFromDocument();
// clips should never skip, going from clip to full video has no indications.
if (url.includes("youtube.com/clip/")) return false;
if (url.includes("youtube.com/clip/")) return null;
// skip to document and don't hide if on /embed/
if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false);
if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false, PageType.Embed);
// 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 if matches pattern
@@ -1229,9 +1269,11 @@ function getYouTubeVideoID(document: Document, url?: string): string | boolean {
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false);
}
function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watch): string | boolean {
function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watch): string {
const selector = "a.ytp-title-link[data-sessionlink='feature=player-title']";
// get ID from document (channel trailer / embedded playlist)
const element = video?.parentElement?.parentElement?.querySelector("a.ytp-title-link[data-sessionlink='feature=player-title']");
const element = pageHint === PageType.Embed ? document.querySelector(selector)
: video?.parentElement?.parentElement?.querySelector(selector);
const videoURL = element?.getAttribute("href");
if (videoURL) {
onInvidious = hideIcon;
@@ -1239,11 +1281,11 @@ function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watc
pageType = pageHint;
return getYouTubeVideoIDFromURL(videoURL);
} else {
return false;
return null;
}
}
function getYouTubeVideoIDFromURL(url: string): string | boolean {
function getYouTubeVideoIDFromURL(url: string): string {
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
//Attempt to parse url
@@ -1252,7 +1294,7 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
urlObject = new URL(url);
} catch (e) {
console.error("[SB] Unable to parse URL: " + url);
return false;
return null;
}
// Check if valid hostname
@@ -1266,7 +1308,7 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoIDFromURL(url)));
}
return false;
return null;
} else {
onInvidious = false;
}
@@ -1274,17 +1316,17 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
//Get ID from searchParam
if (urlObject.searchParams.has("v") && ["/watch", "/watch/"].includes(urlObject.pathname) || urlObject.pathname.startsWith("/tv/watch")) {
const id = urlObject.searchParams.get("v");
return id.length == 11 ? id : false;
return id.length == 11 ? id : null;
} else if (urlObject.pathname.startsWith("/embed/") || urlObject.pathname.startsWith("/shorts/")) {
try {
const id = urlObject.pathname.split("/")[2]
if (id?.length >=11 ) return id.slice(0, 11);
} catch (e) {
console.error("[SB] Video ID not valid for " + url);
return false;
return null;
}
}
return false;
return null;
}
/**
@@ -1395,7 +1437,7 @@ async function whitelistCheck() {
* Returns info about the next upcoming sponsor skip
*/
function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean):
{array: ScheduledTime[], index: number, endIndex: number, extraIndexes: number[], openNotice: boolean} {
{array: ScheduledTime[]; index: number; endIndex: number; extraIndexes: number[]; openNotice: boolean} {
const autoSkipSorter = (segment: ScheduledTime) => {
const skipOption = utils.getCategorySelection(segment.category)?.option;
@@ -1507,7 +1549,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
* the current time, but end after
*/
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean,
minimum?: number, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
minimum?: number, hideHiddenSponsors = false): {includedTimes: ScheduledTime[]; scheduledTimes: number[]} {
if (!sponsorTimes) return {includedTimes: [], scheduledTimes: []};
const includedTimes: ScheduledTime[] = [];
@@ -1569,7 +1611,9 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped:
sponsorSkipped[index] = true;
if (!counted) {
Config.config.minutesSaved = Config.config.minutesSaved + secondsSkipped / 60;
Config.config.skipCount = Config.config.skipCount + 1;
if (segment.actionType !== ActionType.Chapter) {
Config.config.skipCount = Config.config.skipCount + 1;
}
counted = true;
}
@@ -1741,7 +1785,7 @@ function createButton(baseID: string, title: string, callback: () => void, image
}
function shouldAutoSkip(segment: SponsorTime): boolean {
return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
&& (utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip ||
(Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& segment.actionType !== ActionType.Poi));
@@ -1756,16 +1800,14 @@ function shouldSkip(segment: SponsorTime): boolean {
/** Creates any missing buttons on the YouTube player if possible. */
async function createButtons(): Promise<void> {
if (onMobileYouTube) return;
controls = await utils.wait(getControls).catch();
// Add button if does not already exist in html
createButton("startSegment", "sponsorStart", () => startOrEndTimingNewSegment(), "PlayerStartIconSponsorBlocker.svg");
createButton("cancelSegment", "sponsorCancel", () => cancelCreatingSegment(), "PlayerCancelSegmentIconSponsorBlocker.svg");
createButton("delete", "clearTimes", () => clearSponsorTimes(), "PlayerDeleteIconSponsorBlocker.svg");
createButton("submit", "SubmitTimes", submitSponsorTimes, "PlayerUploadIconSponsorBlocker.svg");
createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker.svg");
createButton("submit", "SubmitTimes", () => submitSponsorTimes(), "PlayerUploadIconSponsorBlocker.svg");
createButton("info", "openPopup", () => openInfoMenu(), "PlayerInfoIconSponsorBlocker.svg");
const controlsContainer = getControls();
if (Config.config.autoHideInfoButton && !onInvidious && controlsContainer
@@ -1786,7 +1828,8 @@ async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
updateEditButtonsOnPlayer();
// Don't show the info button on embeds
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious) {
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious
|| document.getElementById("sponsorBlockPopupContainer") != null) {
playerButtons.info.button.style.display = "none";
} else {
playerButtons.info.button.style.removeProperty("display");
@@ -1882,6 +1925,13 @@ function startOrEndTimingNewSegment() {
updateSponsorTimesSubmitting(false);
importExistingChapters(false);
if (lastResponseStatus !== 200 && lastResponseStatus !== 404
&& !shownSegmentFailedToFetchWarning && Config.config.showSegmentFailedToFetchWarning) {
alert(chrome.i18n.getMessage("segmentFetchFailureWarning"));
shownSegmentFailedToFetchWarning = true;
}
}
function getIncompleteSegment(): SponsorTime {
@@ -1896,11 +1946,15 @@ function isSegmentCreationInProgress(): boolean {
function cancelCreatingSegment() {
if (isSegmentCreationInProgress()) {
sponsorTimesSubmitting.splice(sponsorTimesSubmitting.length - 1, 1);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
if (sponsorTimesSubmitting.length > 1) { // If there's more than one segment: remove last
sponsorTimesSubmitting.pop();
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
} else { // Otherwise delete the video entry & close submission menu
resetSponsorSubmissionNotice();
sponsorTimesSubmitting = [];
delete Config.config.unsubmittedSegments[sponsorVideoID];
}
Config.forceSyncUpdate("unsubmittedSegments");
if (sponsorTimesSubmitting.length <= 0) resetSponsorSubmissionNotice();
}
updateEditButtonsOnPlayer();
@@ -2252,7 +2306,8 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
function windowListenerHandler(event: MessageEvent): void {
const data = event.data;
const dataType = data.type;
if (data.source !== "sponsorblock") return;
if (data.source !== "sponsorblock" || document?.URL?.includes("youtube.com/clip/")) return;
if (dataType === "navigation" && data.videoID) {
pageType = data.pageType;
@@ -2279,11 +2334,16 @@ function windowListenerHandler(event: MessageEvent): void {
}
function updateActiveSegment(currentTime: number): void {
previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
const activeSegments = previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
chrome.runtime.sendMessage({
message: "time",
time: currentTime
});
const chapterSegments = activeSegments?.filter((segment) => segment.actionType === ActionType.Chapter);
if (chapterSegments?.length > 0) {
sendTelemetryAndCount(chapterSegments, 0, true);
}
}
function nextChapter(): void {

View File

@@ -6,26 +6,26 @@
import { PageType } from "./types";
interface StartMessage {
type: "navigation",
pageType: PageType
videoID: string | null,
type: "navigation";
pageType: PageType;
videoID: string | null;
}
interface FinishMessage extends StartMessage {
channelID: string,
channelTitle: string
channelID: string;
channelTitle: string;
}
interface AdMessage {
type: "ad",
playing: boolean
type: "ad";
playing: boolean;
}
interface VideoData {
type: "data",
videoID: string,
isLive: boolean,
isPremiere: boolean
type: "data";
videoID: string;
isLive: boolean;
isPremiere: boolean;
}
type WindowMessage = StartMessage | FinishMessage | AdMessage | VideoData;

2
src/globals.d.ts vendored
View File

@@ -1,6 +1,6 @@
import { SBObject } from "./config";
declare global {
interface Window { SB: SBObject; }
interface Window { SB: SBObject }
// Remove this once the API becomes stable and types are shipped in @types/chrome
namespace chrome {
namespace declarativeContent {

View File

@@ -46,6 +46,8 @@ class PreviewBar {
segments: PreviewBarSegment[] = [];
existingChapters: PreviewBarSegment[] = [];
videoDuration = 0;
updateExistingChapters: () => void;
lastChapterUpdate = 0;
// For chapter bar
hoveredSection: HTMLElement;
@@ -58,7 +60,7 @@ class PreviewBar {
unfilteredChapterGroups: ChapterGroup[];
chapterGroups: ChapterGroup[];
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, test=false) {
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
if (test) return;
this.container = document.createElement('ul');
this.container.id = 'previewbar';
@@ -67,6 +69,7 @@ class PreviewBar {
this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious;
this.chapterVote = chapterVote;
this.updateExistingChapters = updateExistingChapters;
this.updatePageElements();
this.createElement(parent);
@@ -214,6 +217,16 @@ class PreviewBar {
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
this.originalChapterBar?.style?.removeProperty("display");
this.chapterVote?.setVisibility(false);
document.querySelectorAll(`.sponsorBlockChapterBar`).forEach((e) => {
if (e !== this.customChaptersBar) {
e.remove();
}
});
}
set(segments: PreviewBarSegment[], videoDuration: number): void {
@@ -637,11 +650,17 @@ class PreviewBar {
cursor += sectionWidthDecimal;
}
if (sections.length !== 0 && sections.length !== this.existingChapters?.length
&& Date.now() - this.lastChapterUpdate > 3000) {
this.lastChapterUpdate = Date.now();
this.updateExistingChapters();
}
}
}
private findLeftAndScale(selector: string, currentElement: HTMLElement, progressBar: HTMLElement):
{ left: number, scale: number } {
{ left: number; scale: number } {
const sections = currentElement.parentElement.parentElement.parentElement.children;
let currentWidth = 0;
let lastWidth = 0;
@@ -657,7 +676,7 @@ class PreviewBar {
for (let i = 0; i < sections.length; i++) {
const section = sections[i] as HTMLElement;
const checkElement = section.querySelector(selector) as HTMLElement;
const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") || progressBar.clientWidth;
const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") ?? progressBar.clientWidth;
const currentSectionWidth = currentSectionWidthNoMargin
+ this.getPartialChapterSectionStyle(section, "marginRight");
@@ -709,14 +728,14 @@ class PreviewBar {
private getPartialChapterSectionStyle(element: HTMLElement, param: string): number {
const data = element.style[param];
if (data?.includes("100%")) {
return 0;
if (data?.includes("%")) {
return this.customChaptersBar.clientWidth * (parseFloat(data.replace("%", "")) / 100);
} else {
return parseInt(element.style[param].match(/\d+/g)?.[0]) || 0;
}
}
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): void {
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): SponsorTime[] {
if (!Config.config.showSegmentNameInChapterBar
|| ((!segments || segments.length <= 0) && submittingSegments?.length <= 0)) {
const chaptersContainer = this.getChaptersContainer();
@@ -737,6 +756,7 @@ class PreviewBar {
});
this.setActiveSegments(activeSegments);
return activeSegments;
}
/**
@@ -765,9 +785,14 @@ class PreviewBar {
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
if (chosenSegment.actionType !== ActionType.Chapter) {
chapterTitle.classList.add("sponsorBlock-segment-title");
} else {
chapterTitle.classList.remove("sponsorBlock-segment-title");
}
const chapterVoteContainer = this.chapterVote.getContainer();
if (chosenSegment.source === SponsorSourceType.Server) {
const chapterVoteContainer = this.chapterVote.getContainer();
if (!chapterButton.contains(chapterVoteContainer)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) {

View File

@@ -9,7 +9,7 @@ interface BaseMessage {
}
interface DefaultMessage {
message:
message:
"update"
| "sponsorStart"
| "getVideoID"
@@ -83,19 +83,19 @@ interface GetVideoIdResponse {
videoID: string;
}
interface GetChannelIDResponse {
export interface GetChannelIDResponse {
channelID: string;
}
interface SponsorStartResponse {
export interface SponsorStartResponse {
creatingSegment: boolean;
}
interface IsChannelWhitelistedResponse {
export interface IsChannelWhitelistedResponse {
value: boolean;
}
export type MessageResponse =
export type MessageResponse =
IsInfoFoundMessageResponse
| GetVideoIdResponse
| GetChannelIDResponse
@@ -111,7 +111,7 @@ export interface VoteResponse {
responseText: string;
}
export interface ImportSegmentsResponse {
interface ImportSegmentsResponse {
importedSegments: SponsorTime[];
}
@@ -120,4 +120,14 @@ export interface TimeUpdateMessage {
time: number;
}
export type PopupMessage = TimeUpdateMessage;
export type InfoUpdatedMessage = IsInfoFoundMessageResponse & {
message: "infoUpdated";
}
export interface VideoChangedPopupMessage {
message: "videoChanged";
videoID: string;
whitelisted: boolean;
}
export type PopupMessage = TimeUpdateMessage | InfoUpdatedMessage | VideoChangedPopupMessage;

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot } from 'react-dom/client';
import Config from "./config";
import * as CompileConfig from "../config.json";
@@ -258,7 +258,8 @@ async function init() {
break;
}
case "keybind-change": {
ReactDOM.render(React.createElement(KeybindComponent, {option: option}), optionsElements[i].querySelector("div"));
const root = createRoot(optionsElements[i].querySelector("div"));
root.render(React.createElement(KeybindComponent, {option: option}));
break;
}
case "display": {

View File

@@ -1,8 +1,24 @@
import Config from "./config";
import Utils from "./utils";
import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject } from "./types";
import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
import {
ActionType,
SegmentUUID,
SponsorHideType,
SponsorSourceType,
SponsorTime,
StorageChangesObject,
} from "./types";
import {
GetChannelIDResponse,
IsChannelWhitelistedResponse,
IsInfoFoundMessageResponse,
Message,
MessageResponse,
PopupMessage,
SponsorStartResponse,
VoteResponse,
} from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
@@ -10,6 +26,8 @@ import { shortCategoryName } from "./utils/categoryUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
import { exportTimes } from "./utils/exporter";
import GenericNotice from "./render/GenericNotice";
import { noRefreshFetchingChaptersAllowed } from "./utils/licenseKey";
const utils = new Utils();
interface MessageListener {
@@ -60,14 +78,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
localizeHtmlPage();
type InputPageElements = {
whitelistToggle?: HTMLInputElement,
toggleSwitch?: HTMLInputElement,
usernameInput?: HTMLInputElement,
whitelistToggle?: HTMLInputElement;
toggleSwitch?: HTMLInputElement;
usernameInput?: HTMLInputElement;
};
type PageElements = { [key: string]: HTMLElement } & InputPageElements
/** If true, the content script is in the process of creating a new segment. */
let creatingSegment = false;
let stopLoadingAnimation = null;
//the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = [];
@@ -187,7 +204,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
PageElements.exportSegmentsButton.addEventListener("click", exportSegments);
PageElements.importSegmentsButton.addEventListener("click",
PageElements.importSegmentsButton.addEventListener("click",
() => PageElements.importSegmentsMenu.classList.toggle("hidden"));
PageElements.importSegmentsSubmit.addEventListener("click", importSegments);
@@ -260,8 +277,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.showNoticeAgain.style.display = "unset";
}
utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&value=permissions&value=freeChaptersAccess&userID="
+ Config.config.userID, (res) => {
const values = ["userName", "viewCount", "minutesSaved", "vip", "permissions"];
if (!Config.config.payments.freeAccess && !noRefreshFetchingChaptersAllowed()) values.push("freeChaptersAccess");
utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config.userID,
values
}).then((res) => {
if (res.status === 200) {
const userInfo = JSON.parse(res.responseText);
PageElements.usernameValue.innerText = userInfo.userName;
@@ -370,7 +392,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
messageHandler.sendMessage(tabs[0].id, { message: 'getVideoID' }, function (result) {
if (result !== undefined && result.videoID) {
currentVideoID = result.videoID;
creatingSegment = result.creatingSegment;
loadTabData(tabs, updating);
} else if (result === undefined && chrome.runtime.lastError) {
@@ -405,7 +426,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}, (tabs) => onTabs(tabs, updating));
}
function infoFound(request: IsInfoFoundMessageResponse) {
async function infoFound(request: IsInfoFoundMessageResponse) {
// End any loading animation
if (stopLoadingAnimation != null) {
stopLoadingAnimation();
stopLoadingAnimation = null;
}
if (chrome.runtime.lastError) {
//This page doesn't have the injected content script, or at least not yet
displayNoVideo();
@@ -421,79 +448,59 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.loadingIndicator.style.display = "none";
downloadedTimes = request.sponsorTimes ?? [];
displayDownloadedSponsorTimes(downloadedTimes, request.time);
if (request.found) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
PageElements.issueReporterImportExport.classList.remove("hidden");
if (request.sponsorTimes) {
displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
}
} else if (request.status == 404 || request.status == 200) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
PageElements.issueReporterImportExport.classList.add("hidden");
PageElements.issueReporterImportExport.classList.remove("hidden");
} else {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
PageElements.issueReporterImportExport.classList.add("hidden");
if (request.status) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
} else {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("segmentsStillLoading");
}
PageElements.issueReporterImportExport.classList.remove("hidden");
}
}
//see if whitelist button should be swapped
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'isChannelWhitelisted' },
function (response) {
if (response.value) {
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
PageElements.whitelistToggle.checked = true;
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
}
});
const response = await sendTabMessageAsync({ message: 'isChannelWhitelisted' }) as IsChannelWhitelistedResponse;
if (response.value) {
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
PageElements.whitelistToggle.checked = true;
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
}
);
}
function sendSponsorStartMessage() {
async function sendSponsorStartMessage() {
//the content script will get the message if a YouTube page is open
messageHandler.query({
active: true,
currentWindow: true,
}, (tabs) => {
messageHandler.sendMessage(
tabs[0].id,
{ from: 'popup', message: 'sponsorStart' },
async (response) => {
startSponsorCallback(response);
const response = await sendTabMessageAsync({ from: 'popup', message: 'sponsorStart' }) as SponsorStartResponse;
startSponsorCallback(response);
// Perform a second update after the config changes take effect as a workaround for a race condition
const removeListener = (listener: typeof lateUpdate) => {
const index = Config.configSyncListeners.indexOf(listener);
if (index !== -1) Config.configSyncListeners.splice(index, 1);
};
// Perform a second update after the config changes take effect as a workaround for a race condition
const removeListener = (listener: typeof lateUpdate) => {
const index = Config.configSyncListeners.indexOf(listener);
if (index !== -1) Config.configSyncListeners.splice(index, 1);
};
const lateUpdate = () => {
startSponsorCallback(response);
removeListener(lateUpdate);
};
const lateUpdate = () => {
startSponsorCallback(response);
removeListener(lateUpdate);
};
Config.configSyncListeners.push(lateUpdate);
Config.configSyncListeners.push(lateUpdate);
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
setTimeout(() => removeListener(lateUpdate), 200);
},
);
});
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
setTimeout(() => removeListener(lateUpdate), 200);
}
function startSponsorCallback(response: { creatingSegment: boolean }) {
creatingSegment = response.creatingSegment;
function startSponsorCallback(response: SponsorStartResponse) {
// Only update the segments after a segment was created
if (!creatingSegment) {
if (!response.creatingSegment) {
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] || [];
}
@@ -508,7 +515,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.issueReporterTabs.classList.add("hidden");
currentSegmentTab = SegmentTab.Segments;
} else {
if (currentSegmentTab === SegmentTab.Segments
if (currentSegmentTab === SegmentTab.Segments
&& sponsorTimes.every((segment) => segment.actionType === ActionType.Chapter)) {
PageElements.issueReporterTabs.classList.add("hidden");
currentSegmentTab = SegmentTab.Chapters;
@@ -523,7 +530,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (currentSegmentTab === SegmentTab.Segments) {
return segment.actionType !== ActionType.Chapter;
} else if (currentSegmentTab === SegmentTab.Chapters) {
return segment.actionType === ActionType.Chapter
return segment.actionType === ActionType.Chapter
&& segment.source !== SponsorSourceType.YouTube;
} else {
return true;
@@ -540,7 +547,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (downloadedTimes.length > 0) {
PageElements.exportSegmentsButton.classList.remove("hidden");
} else {
} else {
PageElements.exportSegmentsButton.classList.add("hidden");
}
@@ -584,7 +591,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (downloadedTimes[i].actionType === ActionType.Full) {
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
} else {
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
(actionType !== ActionType.Poi
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(downloadedTimes[i].segment[1], true)
: "");
@@ -670,26 +677,18 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
downloadedTimes[i].hidden = SponsorHideType.Hidden;
}
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{
message: "hideSegment",
type: downloadedTimes[i].hidden,
UUID: UUID
}
);
});
sendTabMessage({
message: "hideSegment",
type: downloadedTimes[i].hidden,
UUID: UUID
})
});
const skipButton = document.createElement("img");
skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
skipButton.className = "voteButton";
skipButton.src = chrome.runtime.getURL("icons/skip.svg");
skipButton.title = actionType === ActionType.Chapter ? chrome.i18n.getMessage("playChapter")
skipButton.title = actionType === ActionType.Chapter ? chrome.i18n.getMessage("playChapter")
: chrome.i18n.getMessage("skipSegment");
skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
@@ -702,8 +701,9 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
voteButtonsContainer.appendChild(hideButton);
}
voteButtonsContainer.appendChild(skipButton);
if (downloadedTimes[i].actionType !== ActionType.Full) {
voteButtonsContainer.appendChild(skipButton);
}
// Will contain request status
const voteStatusContainer = document.createElement("div");
@@ -726,15 +726,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
function submitTimes() {
if (sponsorTimes.length > 0) {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'submitTimes' },
);
});
sendTabMessage({ message: 'submitTimes' })
}
}
@@ -744,9 +736,16 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.showNoticeAgain.style.display = "none";
}
function isCreatingSegment(): boolean {
const segments = Config.config.unsubmittedSegments[currentVideoID];
if (!segments) return false;
const lastSegment = segments[segments.length - 1];
return lastSegment && lastSegment?.segment?.length !== 2;
}
/** Updates any UI related to segment editing and submission according to the current state. */
function updateSegmentEditingUI() {
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(creatingSegment ? "sponsorEnd" : "sponsorStart");
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(isCreatingSegment() ? "sponsorEnd" : "sponsorStart");
PageElements.submitTimes.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
PageElements.submissionHint.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
@@ -765,20 +764,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
chrome.runtime.sendMessage({ "message": "openHelp" });
}
function sendTabMessage(data: Message): Promise<unknown> {
return new Promise((resolve) => {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
data,
(response) => resolve(response)
);
}
function sendTabMessage(data: Message, callback?) {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
data,
callback
);
});
}
);
}
function sendTabMessageAsync(data: Message): Promise<unknown> {
return new Promise((resolve) => sendTabMessage(data, (response) => resolve(response)))
}
//make the options username setting option visible
@@ -855,186 +856,122 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
thanksForVotingText.removeAttribute("innerText");
}
function vote(type, UUID) {
async function vote(type, UUID) {
//add loading info
addVoteMessage(chrome.i18n.getMessage("Loading"), UUID);
const response = await sendTabMessageAsync({
message: "submitVote",
type: type,
UUID: UUID
}) as VoteResponse;
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{
message: "submitVote",
type: type,
UUID: UUID
}, function (response) {
if (response != undefined) {
//see if it was a success or failure
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
//success (treat rate limits as a success)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
} else if (response.successType == -1) {
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
setTimeout(() => removeVoteMessage(UUID), 1500);
}
}
);
if (response != undefined) {
//see if it was a success or failure
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
//success (treat rate limits as a success)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
} else if (response.successType == -1) {
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
setTimeout(() => removeVoteMessage(UUID), 1500);
}
}
async function whitelistChannel() {
//get the channel url
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
if (!response.channelID) {
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
return;
}
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//add on this channel
whitelistedChannels.push(response.channelID);
//change button
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
//show 'consider force channel check' alert
if (!Config.config.forceChannelCheck) PageElements.whitelistForceCheck.classList.remove("hidden");
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//send a message to the client
sendTabMessage({
message: 'whitelistChange',
value: true
});
}
function whitelistChannel() {
async function unwhitelistChannel() {
//get the channel url
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'getChannelID' },
function (response) {
if (!response.channelID) {
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
return;
}
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//add on this channel
whitelistedChannels.push(response.channelID);
//remove this channel
const index = whitelistedChannels.indexOf(response.channelID);
whitelistedChannels.splice(index, 1);
//change button
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
//change button
PageElements.whitelistChannel.style.display = "unset";
PageElements.unwhitelistChannel.style.display = "none";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.remove("rotated");
//show 'consider force channel check' alert
if (!Config.config.forceChannelCheck) PageElements.whitelistForceCheck.classList.remove("hidden");
//hide 'consider force channel check' alert
PageElements.whitelistForceCheck.classList.add("hidden");
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//send a message to the client
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id, {
message: 'whitelistChange',
value: true
});
}
);
}
);
//send a message to the client
sendTabMessage({
message: 'whitelistChange',
value: false
});
}
function unwhitelistChannel() {
//get the channel url
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'getChannelID' },
function (response) {
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//remove this channel
const index = whitelistedChannels.indexOf(response.channelID);
whitelistedChannels.splice(index, 1);
//change button
PageElements.whitelistChannel.style.display = "unset";
PageElements.unwhitelistChannel.style.display = "none";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.remove("rotated");
//hide 'consider force channel check' alert
PageElements.whitelistForceCheck.classList.add("hidden");
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//send a message to the client
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id, {
message: 'whitelistChange',
value: false
});
}
);
}
);
});
function startLoadingAnimation() {
stopLoadingAnimation = AnimationUtils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
}
function refreshSegments() {
const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'refreshSegments' },
(response) => {
infoFound(response);
stopAnimation();
}
)
}
);
startLoadingAnimation();
sendTabMessage({ message: 'refreshSegments' });
}
function skipSegment(actionType: ActionType, UUID: SegmentUUID, element?: HTMLElement): void {
if (actionType === ActionType.Chapter) {
sendMessage({
sendTabMessage({
message: "unskip",
UUID: UUID
});
} else {
sendMessage({
sendTabMessage({
message: "reskip",
UUID: UUID
});
}
if (element) {
const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3);
stopAnimation();
}
}
function sendMessage(request: Message): void {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
request
);
});
}
/**
* Should skipping be disabled (visuals stay)
*/
@@ -1067,10 +1004,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
async function importSegments() {
const text = (PageElements.importSegmentsText as HTMLInputElement).value;
await sendTabMessage({
sendTabMessage({
message: "importSegments",
data: text
}) as ImportSegmentsResponse;
});
PageElements.importSegmentsMenu.classList.add("hidden");
}
@@ -1135,6 +1072,27 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
case "time":
displayDownloadedSponsorTimes(downloadedTimes, msg.time);
break;
case "infoUpdated":
infoFound(msg);
break;
case "videoChanged":
currentVideoID = msg.videoID
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
updateSegmentEditingUI();
if (msg.whitelisted) {
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
PageElements.whitelistToggle.checked = true;
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
}
// Clear segments list & start loading animation
// We'll get a ping once they're loaded
startLoadingAnimation();
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("Loading");
displayDownloadedSponsorTimes([], 0);
break;
}
}
}

View File

@@ -1,5 +1,6 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot } from 'react-dom/client';
import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
class CategoryChooser {
@@ -9,9 +10,9 @@ class CategoryChooser {
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<CategoryChooserComponent ref={this.ref} />,
element
const root = createRoot(element);
root.render(
<CategoryChooserComponent ref={this.ref} />
);
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from "react-dom/client";
import CategoryPillComponent, { CategoryPillState } from "../components/CategoryPillComponent";
import Config from "../config";
import { VoteResponse } from "../messageTypes";
@@ -10,6 +10,7 @@ import { Tooltip } from "./Tooltip";
export class CategoryPill {
container: HTMLElement;
ref: React.RefObject<CategoryPillComponent>;
root: Root;
unsavedState: CategoryPillState;
@@ -38,10 +39,8 @@ export class CategoryPill {
this.unsavedState = this.ref.current.state;
}
ReactDOM.render(
<CategoryPillComponent ref={this.ref} vote={vote} />,
this.container
);
this.root = createRoot(this.container);
this.root.render(<CategoryPillComponent ref={this.ref} vote={vote} />);
if (this.unsavedState) {
this.ref.current?.setState(this.unsavedState);
@@ -64,7 +63,7 @@ export class CategoryPill {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent";
import { VoteResponse } from "../messageTypes";
import { Category, SegmentUUID, SponsorTime } from "../types";
@@ -7,6 +7,7 @@ import { Category, SegmentUUID, SponsorTime } from "../types";
export class ChapterVote {
container: HTMLElement;
ref: React.RefObject<ChapterVoteComponent>;
root: Root;
unsavedState: ChapterVoteState;
@@ -19,10 +20,8 @@ export class ChapterVote {
this.container.id = "chapterVote";
this.container.style.height = "100%";
ReactDOM.render(
<ChapterVoteComponent ref={this.ref} vote={vote} />,
this.container
);
this.root = createRoot(this.container);
this.root.render(<ChapterVoteComponent ref={this.ref} vote={vote} />);
}
getContainer(): HTMLElement {
@@ -30,7 +29,7 @@ export class ChapterVote {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import NoticeComponent from "../components/NoticeComponent";
import Utils from "../utils";
@@ -9,17 +9,17 @@ import { ButtonListener, ContentContainer } from "../types";
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
export interface TextBox {
icon: string,
text: string
icon: string;
text: string;
}
export interface NoticeOptions {
title: string,
referenceNode?: HTMLElement,
textBoxes?: TextBox[],
buttons?: ButtonListener[],
fadeIn?: boolean,
timed?: boolean
title: string;
referenceNode?: HTMLElement;
textBoxes?: TextBox[];
buttons?: ButtonListener[];
fadeIn?: boolean;
timed?: boolean;
style?: React.CSSProperties;
extraClass?: string;
maxCountdownTime?: () => number;
@@ -35,6 +35,7 @@ export default class GenericNotice {
noticeElement: HTMLDivElement;
noticeRef: React.MutableRefObject<NoticeComponent>;
idSuffix: string;
root: Root;
constructor(contentContainer: ContentContainer, idSuffix: string, options: NoticeOptions) {
this.noticeRef = React.createRef();
@@ -49,11 +50,13 @@ export default class GenericNotice {
referenceNode.prepend(this.noticeElement);
this.update(options);
this.root = createRoot(this.noticeElement);
this.update(options);
}
update(options: NoticeOptions): void {
ReactDOM.render(
this.root.render(
<NoticeComponent
noticeTitle={options.title}
idSuffix={this.idSuffix}
@@ -92,8 +95,7 @@ export default class GenericNotice {
</>
: null}
</NoticeComponent>,
this.noticeElement
</NoticeComponent>
);
}
@@ -137,7 +139,7 @@ export default class GenericNotice {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.noticeElement);
this.root.unmount();
this.noticeElement.remove();
}

View File

@@ -1,26 +1,26 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
export interface RectangleTooltipProps {
text: string,
link?: string,
referenceNode: HTMLElement,
prependElement?: HTMLElement, // Element to append before
bottomOffset?: string,
leftOffset?: string,
timeout?: number,
htmlId?: string,
maxHeight?: string,
maxWidth?: string,
backgroundColor?: string,
fontSize?: string,
text: string;
link?: string;
referenceNode: HTMLElement;
prependElement?: HTMLElement; // Element to append before
bottomOffset?: string;
leftOffset?: string;
timeout?: number;
htmlId?: string;
maxHeight?: string;
maxWidth?: string;
backgroundColor?: string;
fontSize?: string;
buttonFunction?: () => void;
}
export class RectangleTooltip {
text: string;
container: HTMLDivElement;
root: Root;
timer: NodeJS.Timeout;
constructor(props: RectangleTooltipProps) {
@@ -47,7 +47,8 @@ export class RectangleTooltip {
this.timer = setTimeout(() => this.close(), props.timeout * 1000);
}
ReactDOM.render(
this.root = createRoot(this.container);
this.root.render(
<div style={{
bottom: props.bottomOffset,
left: props.leftOffset,
@@ -81,13 +82,12 @@ export class RectangleTooltip {
{chrome.i18n.getMessage("GotIt")}
</button>
</div>,
this.container
</div>
)
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
if (this.timer) clearTimeout(this.timer);

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import Utils from "../utils";
const utils = new Utils();
@@ -18,6 +18,7 @@ class SkipNotice {
noticeElement: HTMLDivElement;
skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
root: Root;
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, unskipTime: number = null, startReskip = false) {
this.skipNoticeRef = React.createRef();
@@ -41,7 +42,8 @@ class SkipNotice {
referenceNode.prepend(this.noticeElement);
ReactDOM.render(
this.root = createRoot(this.noticeElement);
this.root.render(
<SkipNoticeComponent segments={segments}
autoSkip={autoSkip}
startReskip={startReskip}
@@ -50,8 +52,7 @@ class SkipNotice {
closeListener={() => this.close()}
smaller={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAll
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAutoSkip && autoSkip)}
unskipTime={unskipTime} />,
this.noticeElement
unskipTime={unskipTime} />
);
}
@@ -62,7 +63,7 @@ class SkipNotice {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.noticeElement);
this.root.unmount();
this.noticeElement.remove();

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import Utils from "../utils";
const utils = new Utils();
@@ -17,6 +17,8 @@ class SubmissionNotice {
noticeElement: HTMLDivElement;
root: Root;
constructor(contentContainer: ContentContainer, callback: () => unknown) {
this.noticeRef = React.createRef();
@@ -30,13 +32,13 @@ class SubmissionNotice {
referenceNode.prepend(this.noticeElement);
ReactDOM.render(
this.root = createRoot(this.noticeElement);
this.root.render(
<SubmissionNoticeComponent
contentContainer={contentContainer}
callback={callback}
ref={this.noticeRef}
closeListener={() => this.close(false)} />,
this.noticeElement
closeListener={() => this.close(false)} />
);
}
@@ -46,7 +48,7 @@ class SubmissionNotice {
close(callRef = true): void {
if (callRef) this.noticeRef.current.cancel();
ReactDOM.unmountComponentAtNode(this.noticeElement);
this.root.unmount();
this.noticeElement.remove();
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import { ButtonListener } from "../types";
export interface TooltipProps {
@@ -26,6 +26,7 @@ export class Tooltip {
container: HTMLDivElement;
timer: NodeJS.Timeout;
root: Root;
constructor(props: TooltipProps) {
props.bottomOffset ??= "70px";
@@ -54,8 +55,9 @@ export class Tooltip {
}
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
ReactDOM.render(
this.root = createRoot(this.container);
this.root.render(
<div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
<div>
@@ -93,8 +95,7 @@ export class Tooltip {
{chrome.i18n.getMessage("GotIt")}
</button>
: null}
</div>,
this.container
</div>
)
}
@@ -120,7 +121,7 @@ export class Tooltip {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
if (this.timer) clearTimeout(this.timer);

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot } from 'react-dom/client';
import UnsubmittedVideosComponent from "../components/options/UnsubmittedVideosComponent";
class UnsubmittedVideos {
@@ -9,9 +9,9 @@ class UnsubmittedVideos {
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<UnsubmittedVideosComponent ref={this.ref} />,
element
const root = createRoot(element);
root.render(
<UnsubmittedVideosComponent ref={this.ref} />
);
}

View File

@@ -4,32 +4,32 @@ import SkipNotice from "./render/SkipNotice";
export interface ContentContainer {
(): {
vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void,
dontShowNoticeAgain: () => void,
unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean) => void,
sponsorTimes: SponsorTime[],
sponsorTimesSubmitting: SponsorTime[],
skipNotices: SkipNotice[],
v: HTMLVideoElement,
sponsorVideoID,
reskipSponsorTime: (segment: SponsorTime, forceSeek?: boolean) => void,
updatePreviewBar: () => void,
onMobileYouTube: boolean,
sponsorSubmissionNotice: SubmissionNotice,
resetSponsorSubmissionNotice: (callRef?: boolean) => void,
updateEditButtonsOnPlayer: () => void,
previewTime: (time: number, unpause?: boolean) => void,
videoInfo: VideoInfo,
getRealCurrentTime: () => number,
lockedCategories: string[],
channelIDInfo: ChannelIDInfo
}
vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void;
dontShowNoticeAgain: () => void;
unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean) => void;
sponsorTimes: SponsorTime[];
sponsorTimesSubmitting: SponsorTime[];
skipNotices: SkipNotice[];
v: HTMLVideoElement;
sponsorVideoID;
reskipSponsorTime: (segment: SponsorTime, forceSeek?: boolean) => void;
updatePreviewBar: () => void;
onMobileYouTube: boolean;
sponsorSubmissionNotice: SubmissionNotice;
resetSponsorSubmissionNotice: (callRef?: boolean) => void;
updateEditButtonsOnPlayer: () => void;
previewTime: (time: number, unpause?: boolean) => void;
videoInfo: VideoInfo;
getRealCurrentTime: () => number;
lockedCategories: string[];
channelIDInfo: ChannelIDInfo;
};
}
export interface FetchResponse {
responseText: string,
status: number,
ok: boolean
responseText: string;
status: number;
ok: boolean;
}
export type HashedValue = string & { __hashBrand: unknown };
@@ -46,7 +46,7 @@ export enum CategorySkipOption {
export interface CategorySelection {
name: Category;
option: CategorySkipOption
option: CategorySkipOption;
}
export enum SponsorHideType {
@@ -97,95 +97,95 @@ export interface ScheduledTime extends SponsorTime {
}
export interface PreviewBarOption {
color: string,
opacity: string
color: string;
opacity: string;
}
export interface Registration {
message: string,
id: string,
allFrames: boolean,
js: browser.extensionTypes.ExtensionFileOrCode[],
css: browser.extensionTypes.ExtensionFileOrCode[],
matches: string[]
message: string;
id: string;
allFrames: boolean;
js: browser.extensionTypes.ExtensionFileOrCode[];
css: browser.extensionTypes.ExtensionFileOrCode[];
matches: string[];
}
export interface BackgroundScriptContainer {
registerFirefoxContentScript: (opts: Registration) => void,
unregisterFirefoxContentScript: (id: string) => void
registerFirefoxContentScript: (opts: Registration) => void;
unregisterFirefoxContentScript: (id: string) => void;
}
export interface VideoInfo {
responseContext: {
serviceTrackingParams: Array<{service: string, params: Array<{key: string, value: string}>}>,
serviceTrackingParams: Array<{service: string; params: Array<{key: string; value: string}>}>;
webResponseContextExtensionData: {
hasDecorated: boolean
}
},
hasDecorated: boolean;
};
};
playabilityStatus: {
status: string,
playableInEmbed: boolean,
status: string;
playableInEmbed: boolean;
miniplayer: {
miniplayerRenderer: {
playbackMode: string
}
}
playbackMode: string;
};
};
};
streamingData: unknown;
playbackTracking: unknown;
videoDetails: {
videoId: string,
title: string,
lengthSeconds: string,
keywords: string[],
channelId: string,
isOwnerViewing: boolean,
shortDescription: string,
isCrawlable: boolean,
videoId: string;
title: string;
lengthSeconds: string;
keywords: string[];
channelId: string;
isOwnerViewing: boolean;
shortDescription: string;
isCrawlable: boolean;
thumbnail: {
thumbnails: Array<{url: string, width: number, height: number}>
},
averageRating: number,
allowRatings: boolean,
viewCount: string,
author: string,
isPrivate: boolean,
isUnpluggedCorpus: boolean,
isLiveContent: boolean,
thumbnails: Array<{url: string; width: number; height: number}>;
};
averageRating: number;
allowRatings: boolean;
viewCount: string;
author: string;
isPrivate: boolean;
isUnpluggedCorpus: boolean;
isLiveContent: boolean;
};
playerConfig: unknown;
storyboards: unknown;
microformat: {
playerMicroformatRenderer: {
thumbnail: {
thumbnails: Array<{url: string, width: number, height: number}>
},
thumbnails: Array<{url: string; width: number; height: number}>;
};
embed: {
iframeUrl: string,
flashUrl: string,
width: number,
height: number,
flashSecureUrl: string,
},
iframeUrl: string;
flashUrl: string;
width: number;
height: number;
flashSecureUrl: string;
};
title: {
simpleText: string,
},
simpleText: string;
};
description: {
simpleText: string,
},
lengthSeconds: string,
ownerProfileUrl: string,
externalChannelId: string,
availableCountries: string[],
isUnlisted: boolean,
hasYpcMetadata: boolean,
viewCount: string,
category: Category,
publishDate: string,
ownerChannelName: string,
uploadDate: string,
}
simpleText: string;
};
lengthSeconds: string;
ownerProfileUrl: string;
externalChannelId: string;
availableCountries: string[];
isUnlisted: boolean;
hasYpcMetadata: boolean;
viewCount: string;
category: Category;
publishDate: string;
ownerChannelName: string;
uploadDate: string;
};
};
trackingParams: string;
attestation: unknown;
@@ -205,17 +205,17 @@ export enum ChannelIDStatus {
}
export interface ChannelIDInfo {
id: string,
status: ChannelIDStatus
id: string;
status: ChannelIDStatus;
}
export interface SkipToTimeParams {
v: HTMLVideoElement,
skipTime: number[],
skippingSegments: SponsorTime[],
openNotice: boolean,
forceAutoSkip?: boolean,
unskipTime?: number
v: HTMLVideoElement;
skipTime: number[];
skippingSegments: SponsorTime[];
openNotice: boolean;
forceAutoSkip?: boolean;
unskipTime?: number;
}
export interface ToggleSkippable {
@@ -232,11 +232,11 @@ export enum NoticeVisbilityMode {
}
export type Keybind = {
key: string,
code?: string,
ctrl?: boolean,
alt?: boolean,
shift?: boolean
key: string;
code?: string;
ctrl?: boolean;
alt?: boolean;
shift?: boolean;
}
export enum PageType {
@@ -244,10 +244,11 @@ export enum PageType {
Watch = "watch",
Search = "search",
Browse = "browse",
Channel = "channel"
Channel = "channel",
Embed = "embed"
}
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
name: string;
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}

View File

@@ -3,11 +3,15 @@ import { checkLicenseKey } from "./utils/licenseKey";
import { localizeHtmlPage } from "./utils/pageUtils";
import * as countries from "../public/res/countries.json";
import Utils from "./utils";
import { Category, CategorySkipOption } from "./types";
// This is needed, if Config is not imported before Utils, things break.
// Probably due to cyclic dependencies
Config.config;
const utils = new Utils();
window.addEventListener('DOMContentLoaded', init);
async function init() {
@@ -31,6 +35,13 @@ async function init() {
Config.config.payments.licenseKey = licenseKey;
Config.forceSyncUpdate("payments");
if (!utils.getCategorySelection("chapter")) {
Config.config.categorySelections.push({
name: "chapter" as Category,
option: CategorySkipOption.ShowOverlay
});
}
alert(chrome.i18n.getMessage("redeemSuccess"));
} else {
alert(chrome.i18n.getMessage("redeemFailed"));

View File

@@ -24,7 +24,7 @@ export default class Utils {
/* Used for waitForElement */
creatingWaitingMutationObserver = false;
waitingMutationObserver: MutationObserver = null;
waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = [];
waitingElements: { selector: string; visibleCheck: boolean; callback: (element: Element) => void }[] = [];
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
this.backgroundScriptContainer = backgroundScriptContainer;
@@ -65,7 +65,7 @@ export default class Utils {
private setupWaitingMutationListener(): void {
if (!this.waitingMutationObserver) {
this.waitingMutationObserver = new MutationObserver(() => {
const checkForObjects = () => {
const foundSelectors = [];
for (const { selector, visibleCheck, callback } of this.waitingElements) {
const element = this.getElement(selector, visibleCheck);
@@ -78,16 +78,23 @@ export default class Utils {
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
if (this.waitingElements.length === 0) {
this.waitingMutationObserver.disconnect();
this.waitingMutationObserver?.disconnect();
this.waitingMutationObserver = null;
this.creatingWaitingMutationObserver = false;
}
});
};
this.waitingMutationObserver.observe(document.body, {
childList: true,
subtree: true
});
// Do an initial check over all objects
checkForObjects();
if (this.waitingElements.length > 0) {
this.waitingMutationObserver = new MutationObserver(checkForObjects);
this.waitingMutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
}

View File

@@ -25,7 +25,7 @@ function applyLoadingAnimation(element: HTMLElement, time: number, callback?: ()
});
}
function setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void, show: () => void } {
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");

View File

@@ -8,7 +8,7 @@ const inTest = typeof chrome === "undefined";
const chapterNames = CompileConfig.categoryList.filter((code) => code !== "chapter")
.map((code) => ({
code,
name: !inTest ? chrome.i18n.getMessage("category_" + code) : code
names: !inTest ? [chrome.i18n.getMessage("category_" + code), shortCategoryName(code)] : [code]
}));
export function exportTimes(segments: SponsorTime[]): string {
@@ -38,7 +38,7 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
const match = line.match(/(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
if (match) {
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
if (startTime) {
if (startTime !== null) {
const specialCharsMatcher = /^(?:\s+seconds?)?[-:()\s]*|(?:\s+at)?[-:()\s]+$/g
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
let titleRight = null;
@@ -47,7 +47,7 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
if (title) {
const determinedCategory = chapterNames.find(c => c.name === title)?.code as Category;
const determinedCategory = chapterNames.find(c => c.names.includes(title))?.code as Category;
const segment: SponsorTime = {
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],

View File

@@ -93,7 +93,7 @@ function getLuminance(color: string): number {
}
/* From https://stackoverflow.com/a/5624139 */
function hexToRgb(hex: string): {r: number, g: number, b: number} {
function hexToRgb(hex: string): {r: number; g: number; b: number} {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {

View File

@@ -46,7 +46,10 @@ export async function fetchingChaptersAllowed(): Promise<boolean> {
if (Config.config.payments.chaptersAllowed) return true;
if (Config.config.payments.lastCheck === 0) {
if (Config.config.payments.lastCheck === 0 && Date.now() - Config.config.payments.lastFreeCheck > 2 * 24 * 60 * 60 * 1000) {
Config.config.payments.lastFreeCheck = Date.now();
Config.forceSyncUpdate("payments");
// Check for free access if no license key, and it is the first time
const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
value: "freeChaptersAccess",
@@ -56,7 +59,7 @@ export async function fetchingChaptersAllowed(): Promise<boolean> {
try {
if (result.ok) {
const userInfo = JSON.parse(result.responseText);
Config.config.payments.lastCheck = Date.now();
if (userInfo.freeChaptersAccess) {
Config.config.payments.freeAccess = true;

View File

@@ -70,7 +70,9 @@ export function getExistingChapters(currentVideoID: VideoID, duration: number):
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
const chapters: SponsorTime[] = [];
if (chaptersBox) {
// .ytp-timed-markers-container indicates that key-moments are present, which should not be divided
if (chaptersBox && !(getControls()?.parentElement?.parentElement
?.querySelector(".ytp-timed-markers-container")?.childElementCount > 0)) {
let lastSegment: SponsorTime = null;
const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
for (const link of links) {
@@ -78,6 +80,7 @@ export function getExistingChapters(currentVideoID: VideoID, duration: number):
const description = link.querySelector("#details h4") as HTMLElement;
if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) {
const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText);
if (time === null) return [];
if (lastSegment) {
lastSegment.segment[1] = time;

View File

@@ -6,9 +6,9 @@ import { GenericUtils } from "./genericUtils";
const utils = new Utils();
export interface ChatConfig {
displayName: string,
composerInitialValue?: string,
customDescription?: string
displayName: string;
composerInitialValue?: string;
customDescription?: string;
}
export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {

View File

@@ -254,4 +254,25 @@ describe("Import segments", () => {
category: "chapter" as Category
}]);
});
it ("00:00", () => {
const input = ` 00:00 Cap 1
00:10 Cap 2
00:12 Cap 3`;
const result = importTimes(input, 8000);
expect(result).toMatchObject([{
segment: [0, 10],
description: "Cap 1",
category: "chapter" as Category
}, {
segment: [10, 12],
description: "Cap 2",
category: "chapter" as Category
}, {
segment: [12, 8000],
description: "Cap 3",
category: "chapter" as Category
}]);
});
});

View File

@@ -3,7 +3,7 @@ import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
describe("createChapterRenderGroups", () => {
let previewBar: PreviewBar;
beforeEach(() => {
previewBar = new PreviewBar(null, null, null, null, true);
previewBar = new PreviewBar(null, null, null, null, null, true);
})
it("Two unrelated times", () => {