mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-08 20:47:11 +03:00
Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f0f8655f4 | ||
|
|
668f6856d1 | ||
|
|
c8e2bb0c13 | ||
|
|
39ed7ea83c | ||
|
|
f1b2ff801a | ||
|
|
1d9c3a8b80 | ||
|
|
29c6151fe3 | ||
|
|
1377be9915 | ||
|
|
c479a601cd | ||
|
|
f66a4d25bf | ||
|
|
9c7d153f15 | ||
|
|
bbea534781 | ||
|
|
df2586e76d | ||
|
|
59093cdf21 | ||
|
|
5f6307041a | ||
|
|
26f2143247 | ||
|
|
bd292ff886 | ||
|
|
9915d46ad4 | ||
|
|
2b5a02e068 | ||
|
|
1f68f512fa | ||
|
|
d18f7c6195 | ||
|
|
015ac7d46e | ||
|
|
6631dfdea3 | ||
|
|
212fbb83fe | ||
|
|
9e08d6012c | ||
|
|
69c0fe1caf | ||
|
|
fcecd1163d | ||
|
|
29ea112b4f | ||
|
|
2b96fd5f57 | ||
|
|
3e40745621 | ||
|
|
c6e30236e9 | ||
|
|
34c4ecf940 | ||
|
|
3550c168e2 | ||
|
|
901d6e6c92 | ||
|
|
f05d081cd6 | ||
|
|
aadc1be56c | ||
|
|
19e230ea6a | ||
|
|
bc1263c341 | ||
|
|
42d76cf257 | ||
|
|
d06b7411dc | ||
|
|
b14d766ffb | ||
|
|
32ff8db241 | ||
|
|
ea87c8ca24 | ||
|
|
780ea4a9d0 | ||
|
|
6ce4797772 | ||
|
|
8e738a6097 | ||
|
|
7d3f86ded1 | ||
|
|
faeb5dede0 | ||
|
|
eae8485713 | ||
|
|
87ca0a8a50 | ||
|
|
99c5375c6a | ||
|
|
a62f6ca696 | ||
|
|
6eb1d5d954 | ||
|
|
81b01ac5cc | ||
|
|
6f47b66837 | ||
|
|
cf43e04d47 | ||
|
|
cda57e2d2b | ||
|
|
a9186a35e5 | ||
|
|
1a6e6279c8 | ||
|
|
522a04eecb | ||
|
|
d8dfbef1a7 | ||
|
|
60ea7190f9 | ||
|
|
804870f18a | ||
|
|
7c302af207 | ||
|
|
2cc1dcc6fd | ||
|
|
31cc4b4960 | ||
|
|
af86534992 | ||
|
|
0f9122aa1c | ||
|
|
d0e35032a5 | ||
|
|
acf26d3127 | ||
|
|
d352c6efb4 | ||
|
|
5ff9b10f21 | ||
|
|
80c67d8340 | ||
|
|
fdbcf47149 | ||
|
|
b1ef8a5d47 | ||
|
|
4cb6baaff0 | ||
|
|
6cb4fac041 | ||
|
|
d7176a9c97 | ||
|
|
2eb0a34858 | ||
|
|
cf86e91988 | ||
|
|
058c41dd7e | ||
|
|
969b303c59 | ||
|
|
8114e0dcf7 | ||
|
|
561b3a2263 | ||
|
|
e0edb63501 | ||
|
|
70ef867ec5 | ||
|
|
23336fa65b | ||
|
|
fea90d024e | ||
|
|
de85d93602 | ||
|
|
7badfd9b32 | ||
|
|
d0497d60e8 | ||
|
|
cfecb9f94a | ||
|
|
fc81e02026 | ||
|
|
e12d5ff10a | ||
|
|
355572ba04 | ||
|
|
70731e42a5 | ||
|
|
023b875b0f | ||
|
|
82b027159e | ||
|
|
c6405fc0c1 | ||
|
|
1f6b8f6c53 | ||
|
|
caafba5f53 | ||
|
|
32052c17f1 | ||
|
|
5545a516be | ||
|
|
0fb2d8df79 | ||
|
|
b28d881a1b | ||
|
|
b8cbdb55d5 | ||
|
|
94fa649a17 | ||
|
|
c3cb450e92 | ||
|
|
621e28c7e7 | ||
|
|
9f02bf4ce2 | ||
|
|
6325d3539c | ||
|
|
9477ad425c | ||
|
|
f81cfbecfe | ||
|
|
eb35f5c543 | ||
|
|
7d8188d575 | ||
|
|
00ab317a3e | ||
|
|
0536d419e5 | ||
|
|
4d5c9005ae | ||
|
|
0513a36a9a | ||
|
|
1ec184048c | ||
|
|
2b811c5ab4 | ||
|
|
ea91701430 | ||
|
|
9654fabc3c | ||
|
|
2ebc5489cd | ||
|
|
cf3b3c5c48 | ||
|
|
930bc113fe | ||
|
|
7aaa28b5c2 | ||
|
|
bd3976e4c6 | ||
|
|
4e5a883d2e | ||
|
|
e1de84dce3 | ||
|
|
9ce714fd36 | ||
|
|
46983bec24 | ||
|
|
9d65df84be | ||
|
|
e2d56d32fe | ||
|
|
7895b9d2c1 | ||
|
|
63f6702f86 | ||
|
|
02bc554b0e | ||
|
|
c3933a4eee | ||
|
|
68c1f780d5 | ||
|
|
496ef87a28 | ||
|
|
22e85f715d | ||
|
|
1a6a07744e | ||
|
|
4a19fececf | ||
|
|
322a1483df | ||
|
|
ab1520c560 | ||
|
|
e4f4a10965 | ||
|
|
05153a152d | ||
|
|
798fd8b3f3 | ||
|
|
c38cc07e0a | ||
|
|
af547ce745 | ||
|
|
0d0459a3a3 | ||
|
|
7dfee81188 | ||
|
|
3a2d9c0e0e | ||
|
|
8e022bfb28 | ||
|
|
a69c19581d | ||
|
|
a3e67b6cde | ||
|
|
9e6e3b023d | ||
|
|
33cfe3f5d3 | ||
|
|
4a2ebe4b03 | ||
|
|
374f0992ff | ||
|
|
7814493974 | ||
|
|
f5fa758ac1 | ||
|
|
517e53a2e3 | ||
|
|
fb3635cdf8 | ||
|
|
a804da06f5 | ||
|
|
9ed9f9b873 | ||
|
|
b4a2f31520 | ||
|
|
c7acb902a4 | ||
|
|
37ac5c8cbd | ||
|
|
bf4eb8fafc | ||
|
|
7c4a0628b7 | ||
|
|
2d3e293d83 | ||
|
|
6dee56dc95 | ||
|
|
4c9548b303 | ||
|
|
fd69e91880 | ||
|
|
0f4eeb4fe9 | ||
|
|
9a24b906f9 | ||
|
|
496528be65 | ||
|
|
f2c1ee4894 | ||
|
|
acd2720372 | ||
|
|
e20b60979c |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
name: ChromeExtension
|
name: ChromeExtension
|
||||||
path: dist
|
path: dist
|
||||||
- run: mkdir ./builds
|
- run: mkdir ./builds
|
||||||
- uses: montudor/action-zip@v1
|
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||||
with:
|
with:
|
||||||
args: zip -qq -r ./builds/ChromeExtension.zip ./dist
|
args: zip -qq -r ./builds/ChromeExtension.zip ./dist
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: FirefoxExtension
|
name: FirefoxExtension
|
||||||
path: dist
|
path: dist
|
||||||
- uses: montudor/action-zip@v1
|
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||||
with:
|
with:
|
||||||
args: zip -qq -r ./builds/FirefoxExtension.zip ./dist
|
args: zip -qq -r ./builds/FirefoxExtension.zip ./dist
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: ChromeExtensionBeta
|
name: ChromeExtensionBeta
|
||||||
path: dist
|
path: dist
|
||||||
- uses: montudor/action-zip@v1
|
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||||
with:
|
with:
|
||||||
args: zip -qq -r ./builds/ChromeExtensionBeta.zip ./dist
|
args: zip -qq -r ./builds/ChromeExtensionBeta.zip ./dist
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: FirefoxExtensionBeta
|
name: FirefoxExtensionBeta
|
||||||
path: dist
|
path: dist
|
||||||
- uses: montudor/action-zip@v1
|
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||||
with:
|
with:
|
||||||
args: zip -qq -r ./builds/FirefoxExtensionBeta.zip ./dist
|
args: zip -qq -r ./builds/FirefoxExtensionBeta.zip ./dist
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/take-action.yml
vendored
2
.github/workflows/take-action.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: take the issue
|
- name: take the issue
|
||||||
uses: bdougie/take-action@main
|
uses: bdougie/take-action@28b86cd8d25593f037406ecbf96082db2836e928
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|||||||
2
.github/workflows/update-oss-attribution.yml
vendored
2
.github/workflows/update-oss-attribution.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt
|
mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt
|
||||||
|
|
||||||
- name: Create pull request to update list
|
- name: Create pull request to update list
|
||||||
uses: peter-evans/create-pull-request@v3
|
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
|
||||||
with:
|
with:
|
||||||
commit-message: Update OSS Attribution
|
commit-message: Update OSS Attribution
|
||||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||||
|
|||||||
2
.github/workflows/updateInvidous.yml
vendored
2
.github/workflows/updateInvidous.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
run: npm run ci:invidious
|
run: npm run ci:invidious
|
||||||
|
|
||||||
- name: Create pull request to update list
|
- name: Create pull request to update list
|
||||||
uses: peter-evans/create-pull-request@v3
|
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
|
||||||
with:
|
with:
|
||||||
commit-message: Update Invidious List
|
commit-message: Update Invidious List
|
||||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"serverAddress": "https://sponsor.ajay.app",
|
"serverAddress": "https://sponsor.ajay.app",
|
||||||
"testingServerAddress": "https://sponsor.ajay.app/test",
|
"testingServerAddress": "https://sponsor.ajay.app/test",
|
||||||
"serverAddressComment": "This specifies the default SponsorBlock server to connect to",
|
"serverAddressComment": "This specifies the default SponsorBlock server to connect to",
|
||||||
"categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "music_offtopic"],
|
"categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "chapter", "music_offtopic"],
|
||||||
"categorySupport": {
|
"categorySupport": {
|
||||||
"sponsor": ["skip", "mute", "full"],
|
"sponsor": ["skip", "mute", "full"],
|
||||||
"selfpromo": ["skip", "mute", "full"],
|
"selfpromo": ["skip", "mute", "full"],
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"preview": ["skip", "mute"],
|
"preview": ["skip", "mute"],
|
||||||
"filler": ["skip", "mute"],
|
"filler": ["skip", "mute"],
|
||||||
"music_offtopic": ["skip"],
|
"music_offtopic": ["skip"],
|
||||||
"poi_highlight": ["poi"]
|
"poi_highlight": ["poi"],
|
||||||
|
"chapter": ["chapter"]
|
||||||
},
|
},
|
||||||
"wikiLinks": {
|
"wikiLinks": {
|
||||||
"sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor",
|
"sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section",
|
"music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section",
|
||||||
"poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight",
|
"poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight",
|
||||||
"guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines",
|
"guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines",
|
||||||
"mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment"
|
"mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment",
|
||||||
|
"chapter": "https://wiki.sponsor.ajay.app/w/Chapter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "__MSG_fullName__",
|
"name": "__MSG_fullName__",
|
||||||
"short_name": "SponsorBlock",
|
"short_name": "SponsorBlock",
|
||||||
"version": "4.6.3",
|
"version": "5.0",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"description": "__MSG_Description__",
|
"description": "__MSG_Description__",
|
||||||
"homepage_url": "https://sponsor.ajay.app",
|
"homepage_url": "https://sponsor.ajay.app",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"content.css",
|
"content.css",
|
||||||
|
"shared.css",
|
||||||
"./libs/Source+Sans+Pro.css",
|
"./libs/Source+Sans+Pro.css",
|
||||||
"popup.css"
|
"popup.css"
|
||||||
]
|
]
|
||||||
@@ -48,9 +49,11 @@
|
|||||||
"icons/beep.ogg",
|
"icons/beep.ogg",
|
||||||
"icons/pause.svg",
|
"icons/pause.svg",
|
||||||
"icons/stop.svg",
|
"icons/stop.svg",
|
||||||
|
"icons/skip.svg",
|
||||||
"icons/heart.svg",
|
"icons/heart.svg",
|
||||||
"icons/visible.svg",
|
"icons/visible.svg",
|
||||||
"icons/not_visible.svg",
|
"icons/not_visible.svg",
|
||||||
|
"icons/sort.svg",
|
||||||
"icons/money.svg",
|
"icons/money.svg",
|
||||||
"icons/segway.png",
|
"icons/segway.png",
|
||||||
"icons/close-smaller.svg",
|
"icons/close-smaller.svg",
|
||||||
@@ -61,10 +64,13 @@
|
|||||||
"icons/bolt.svg",
|
"icons/bolt.svg",
|
||||||
"icons/stopwatch.svg",
|
"icons/stopwatch.svg",
|
||||||
"icons/music-note.svg",
|
"icons/music-note.svg",
|
||||||
|
"icons/import.svg",
|
||||||
|
"icons/export.svg",
|
||||||
"icons/PlayerInfoIconSponsorBlocker.svg",
|
"icons/PlayerInfoIconSponsorBlocker.svg",
|
||||||
"icons/PlayerDeleteIconSponsorBlocker.svg",
|
"icons/PlayerDeleteIconSponsorBlocker.svg",
|
||||||
"popup.html",
|
"popup.html",
|
||||||
"content.css"
|
"content.css",
|
||||||
|
"js/document.js"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
|
|||||||
14744
package-lock.json
generated
14744
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -8,33 +8,34 @@
|
|||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.0.188",
|
"@types/chrome": "^0.0.193",
|
||||||
"@types/firefox-webext-browser": "^94.0.1",
|
"@types/firefox-webext-browser": "^94.0.1",
|
||||||
"@types/jest": "^27.5.1",
|
"@types/jest": "^28.1.6",
|
||||||
"@types/react": "^17.0.43",
|
"@types/react": "^17.0.47",
|
||||||
"@types/react-dom": "^17.0.14",
|
"@types/react-dom": "^17.0.17",
|
||||||
"@types/selenium-webdriver": "^4.1.1",
|
"@types/selenium-webdriver": "^4.1.2",
|
||||||
"@types/wicg-mediasession": "^1.1.3",
|
"@types/wicg-mediasession": "^1.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.26.0",
|
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||||
"@typescript-eslint/parser": "^5.26.0",
|
"@typescript-eslint/parser": "^5.31.0",
|
||||||
"chromedriver": "^101.0.0",
|
"chromedriver": "^103.0.0",
|
||||||
"concurrently": "^7.2.1",
|
"concurrently": "^7.3.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"eslint": "^8.16.0",
|
"eslint": "^8.20.0",
|
||||||
"eslint-plugin-react": "^7.30.0",
|
"eslint-plugin-react": "^7.30.1",
|
||||||
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
"fork-ts-checker-webpack-plugin": "^7.2.13",
|
||||||
"jest": "^28.1.0",
|
"jest": "^28.1.3",
|
||||||
|
"jest-environment-jsdom": "^28.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"schema-utils": "^4.0.0",
|
"schema-utils": "^4.0.0",
|
||||||
"selenium-webdriver": "^4.2.0",
|
"selenium-webdriver": "^4.3.1",
|
||||||
"speed-measure-webpack-plugin": "^1.5.0",
|
"speed-measure-webpack-plugin": "^1.5.0",
|
||||||
"ts-jest": "^28.0.3",
|
"ts-jest": "^28.0.7",
|
||||||
"ts-loader": "^9.3.0",
|
"ts-loader": "^9.3.1",
|
||||||
"ts-node": "^10.8.0",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "4.7",
|
"typescript": "4.7",
|
||||||
"web-ext": "^6.8.0",
|
"web-ext": "^7.1.1",
|
||||||
"webpack": "^5.72.1",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^4.9.2",
|
"webpack-cli": "^4.10.0",
|
||||||
"webpack-merge": "^5.8.0"
|
"webpack-merge": "^5.8.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -25,6 +25,16 @@
|
|||||||
"Segments": {
|
"Segments": {
|
||||||
"message": "segments"
|
"message": "segments"
|
||||||
},
|
},
|
||||||
|
"SegmentsCap": {
|
||||||
|
"message": "Segments"
|
||||||
|
},
|
||||||
|
"Chapters": {
|
||||||
|
"message": "Chapters"
|
||||||
|
},
|
||||||
|
"renderAsChapters": {
|
||||||
|
"message": "Render segments as chapters",
|
||||||
|
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
|
||||||
|
},
|
||||||
"upvoteButtonInfo": {
|
"upvoteButtonInfo": {
|
||||||
"message": "Upvote this submission"
|
"message": "Upvote this submission"
|
||||||
},
|
},
|
||||||
@@ -115,6 +125,9 @@
|
|||||||
"SubmitTimes": {
|
"SubmitTimes": {
|
||||||
"message": "Submit Segments"
|
"message": "Submit Segments"
|
||||||
},
|
},
|
||||||
|
"sortSegments": {
|
||||||
|
"message": "Sort Segments"
|
||||||
|
},
|
||||||
"submitCheck": {
|
"submitCheck": {
|
||||||
"message": "Are you sure you want to submit this?"
|
"message": "Are you sure you want to submit this?"
|
||||||
},
|
},
|
||||||
@@ -289,6 +302,14 @@
|
|||||||
"message": "Submit segments",
|
"message": "Submit segments",
|
||||||
"description": "Keybind label"
|
"description": "Keybind label"
|
||||||
},
|
},
|
||||||
|
"nextChapterKeybind": {
|
||||||
|
"message": "Next chapter",
|
||||||
|
"description": "Keybind label"
|
||||||
|
},
|
||||||
|
"previousChapterKeybind": {
|
||||||
|
"message": "Previous chapter",
|
||||||
|
"description": "Keybind label"
|
||||||
|
},
|
||||||
"keybindDescription": {
|
"keybindDescription": {
|
||||||
"message": "Select a key by typing it and choose any modifier keys you wish to use."
|
"message": "Select a key by typing it and choose any modifier keys you wish to use."
|
||||||
},
|
},
|
||||||
@@ -545,6 +566,10 @@
|
|||||||
"message": "to",
|
"message": "to",
|
||||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||||
},
|
},
|
||||||
|
"CopiedExclamation": {
|
||||||
|
"message": "Copied!",
|
||||||
|
"description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
|
||||||
|
},
|
||||||
"generic_guideline1": {
|
"generic_guideline1": {
|
||||||
"message": "Include segue transitions"
|
"message": "Include segue transitions"
|
||||||
},
|
},
|
||||||
@@ -637,7 +662,7 @@
|
|||||||
"message": "Preview/Recap"
|
"message": "Preview/Recap"
|
||||||
},
|
},
|
||||||
"category_preview_description": {
|
"category_preview_description": {
|
||||||
"message": "Quick recap of previous episodes, or a preview of what's coming up later in the current video. Meant for edited together clips, not for spoken summaries."
|
"message": "Collection of clips that show what is coming up in in this video or other videos in a series where all information is repeated later in the video."
|
||||||
},
|
},
|
||||||
"category_preview_guideline1": {
|
"category_preview_guideline1": {
|
||||||
"message": "Clips that appear later, or in a future video"
|
"message": "Clips that appear later, or in a future video"
|
||||||
@@ -696,6 +721,21 @@
|
|||||||
"category_poi_highlight_guideline3": {
|
"category_poi_highlight_guideline3": {
|
||||||
"message": "Can skip to the title or thumbnail"
|
"message": "Can skip to the title or thumbnail"
|
||||||
},
|
},
|
||||||
|
"category_chapter": {
|
||||||
|
"message": "Chapter"
|
||||||
|
},
|
||||||
|
"category_chapter_description": {
|
||||||
|
"message": "Custom named chapters describing major sections of a video."
|
||||||
|
},
|
||||||
|
"category_chapter_guideline1": {
|
||||||
|
"message": "Don't mention sponsor brand names"
|
||||||
|
},
|
||||||
|
"category_chapter_guideline2": {
|
||||||
|
"message": "Use larger chapters for general sections"
|
||||||
|
},
|
||||||
|
"category_chapter_guideline3": {
|
||||||
|
"message": "Smaller chapters can be placed inside larger ones"
|
||||||
|
},
|
||||||
"category_livestream_messages": {
|
"category_livestream_messages": {
|
||||||
"message": "Livestream: Donation/Message Readings"
|
"message": "Livestream: Donation/Message Readings"
|
||||||
},
|
},
|
||||||
@@ -726,6 +766,9 @@
|
|||||||
"showOverlay_full": {
|
"showOverlay_full": {
|
||||||
"message": "Show Label"
|
"message": "Show Label"
|
||||||
},
|
},
|
||||||
|
"showOverlay_chapter": {
|
||||||
|
"message": "Show Chapters"
|
||||||
|
},
|
||||||
"autoSkipOnMusicVideos": {
|
"autoSkipOnMusicVideos": {
|
||||||
"message": "Auto skip all segments when there is a non-music segment"
|
"message": "Auto skip all segments when there is a non-music segment"
|
||||||
},
|
},
|
||||||
@@ -781,6 +824,10 @@
|
|||||||
"bracketEnd": {
|
"bracketEnd": {
|
||||||
"message": "(End)"
|
"message": "(End)"
|
||||||
},
|
},
|
||||||
|
"End": {
|
||||||
|
"message": "End",
|
||||||
|
"description": "Button that skips to the end of a segment"
|
||||||
|
},
|
||||||
"hiddenDueToDownvote": {
|
"hiddenDueToDownvote": {
|
||||||
"message": "hidden: downvote"
|
"message": "hidden: downvote"
|
||||||
},
|
},
|
||||||
@@ -794,11 +841,8 @@
|
|||||||
"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.",
|
"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": "Channel ID is not loaded yet. If you are using an embedded video, try using the YouTube homepage instead. This could also be caused by changes in the YouTube layout, if you think so, make a comment here:"
|
"message": "Channel ID is not loaded yet. If you are using an embedded video, try using the YouTube homepage instead. This could also be caused by changes in the YouTube layout, if you think so, make a comment here:"
|
||||||
},
|
},
|
||||||
"videoInfoFetchFailed": {
|
"invidiousPermissionRefresh": {
|
||||||
"message": "It seems that something is blocking SponsorBlock's ability to get video data. Please see https://github.com/ajayyy/SponsorBlock/issues/741 for more info."
|
"message": "The browser has revoked the permission needed to function on Invidious and other 3rd-party sites. Please click the button below to reactivate this permission."
|
||||||
},
|
|
||||||
"youtubePermissionRequest": {
|
|
||||||
"message": "It seems that SponsorBlock is unable to reach the YouTube API. To fix this, accept the permission prompt that will appear next, wait a few seconds, and then reload the page."
|
|
||||||
},
|
},
|
||||||
"acceptPermission": {
|
"acceptPermission": {
|
||||||
"message": "Accept permission"
|
"message": "Accept permission"
|
||||||
@@ -824,6 +868,13 @@
|
|||||||
"downvoteDescription": {
|
"downvoteDescription": {
|
||||||
"message": "Incorrect/Wrong Timing"
|
"message": "Incorrect/Wrong Timing"
|
||||||
},
|
},
|
||||||
|
"incorrectVote": {
|
||||||
|
"message": "Incorrect"
|
||||||
|
},
|
||||||
|
"harmfulVote": {
|
||||||
|
"message": "Harmful",
|
||||||
|
"description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
|
||||||
|
},
|
||||||
"incorrectCategory": {
|
"incorrectCategory": {
|
||||||
"message": "Change Category"
|
"message": "Change Category"
|
||||||
},
|
},
|
||||||
@@ -859,6 +910,9 @@
|
|||||||
"categoryPillTitleText": {
|
"categoryPillTitleText": {
|
||||||
"message": "This entire video is labeled as this category and is too tightly integrated to be able to separate"
|
"message": "This entire video is labeled as this category and is too tightly integrated to be able to separate"
|
||||||
},
|
},
|
||||||
|
"chapterNameTooltipWarning": {
|
||||||
|
"message": "One of your chapter names is similar to a category. You should use categories when possible instead."
|
||||||
|
},
|
||||||
"experiementOptOut": {
|
"experiementOptOut": {
|
||||||
"message": "Opt-out of all future experiments",
|
"message": "Opt-out of all future experiments",
|
||||||
"description": "This is used in a popup about a new experiment to get a list of unlisted videos to back up since all unlisted videos uploaded before 2017 will be set to private."
|
"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."
|
||||||
@@ -867,11 +921,19 @@
|
|||||||
"message": "Hide forever"
|
"message": "Hide forever"
|
||||||
},
|
},
|
||||||
"warningChatInfo": {
|
"warningChatInfo": {
|
||||||
"message": "You got a warning and cannot submit segments temporarily. This means that we noticed you were making some common mistakes that are not malicious, please just confirm that you understand the rules and we will remove the warning. You can also join this chat using discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app"
|
"message": "We noticed you were making some common mistakes that are not malicious"
|
||||||
},
|
},
|
||||||
"voteRejectedWarning": {
|
"warningTitle": {
|
||||||
"message": "Vote rejected due to a warning. Click to open a chat to resolve it, or come back later when you have time.",
|
"message": "You got a warning"
|
||||||
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
|
},
|
||||||
|
"questionButton": {
|
||||||
|
"message": "I have a question"
|
||||||
|
},
|
||||||
|
"warningConfirmButton": {
|
||||||
|
"message": "I understand the reason"
|
||||||
|
},
|
||||||
|
"warningError": {
|
||||||
|
"message": "Error when trying to acknowledge warning:"
|
||||||
},
|
},
|
||||||
"Donate": {
|
"Donate": {
|
||||||
"message": "Donate"
|
"message": "Donate"
|
||||||
@@ -1034,5 +1096,117 @@
|
|||||||
},
|
},
|
||||||
"confirmResetToDefault": {
|
"confirmResetToDefault": {
|
||||||
"message": "Are you sure you want to reset all settings to their default values? This cannot be undone."
|
"message": "Are you sure you want to reset all settings to their default values? This cannot be undone."
|
||||||
|
},
|
||||||
|
"exportSegments": {
|
||||||
|
"message": "Export segments"
|
||||||
|
},
|
||||||
|
"importSegments": {
|
||||||
|
"message": "Import chapters"
|
||||||
|
},
|
||||||
|
"Import": {
|
||||||
|
"message": "Import",
|
||||||
|
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
|
||||||
|
},
|
||||||
|
"redeemSuccess": {
|
||||||
|
"message": "Redeem Successful!"
|
||||||
|
},
|
||||||
|
"redeemFailed": {
|
||||||
|
"message": "License key is invalid"
|
||||||
|
},
|
||||||
|
"hideUpsells": {
|
||||||
|
"message": "Hide options not available without extra payment"
|
||||||
|
},
|
||||||
|
"chooseACountry": {
|
||||||
|
"message": "Choose a country"
|
||||||
|
},
|
||||||
|
"noDiscount": {
|
||||||
|
"message": "You do not qualify for a discount"
|
||||||
|
},
|
||||||
|
"discountLink": {
|
||||||
|
"message": "Discount Link (See the pink price)"
|
||||||
|
},
|
||||||
|
"selectYourCountry": {
|
||||||
|
"message": "Select your country"
|
||||||
|
},
|
||||||
|
"alreadyDonated": {
|
||||||
|
"message": "If you've donated any amount before now, you may redeem free access by emailing:",
|
||||||
|
"description": "After the colon is an email address"
|
||||||
|
},
|
||||||
|
"cantAfford": {
|
||||||
|
"message": "If you can't afford to purchase a license, click {here} to see if you are eligible for a discount",
|
||||||
|
"description": "Keep the curly braces"
|
||||||
|
},
|
||||||
|
"patreonSignIn": {
|
||||||
|
"message": "Sign in with Patreon"
|
||||||
|
},
|
||||||
|
"redeem": {
|
||||||
|
"message": "Redeem"
|
||||||
|
},
|
||||||
|
"joinOnPatreon": {
|
||||||
|
"message": "Subscribe on Patreon"
|
||||||
|
},
|
||||||
|
"oneTimePurchase": {
|
||||||
|
"message": "One Time Purchase"
|
||||||
|
},
|
||||||
|
"enterLicenseKey": {
|
||||||
|
"message": "Enter License Key"
|
||||||
|
},
|
||||||
|
"chaptersPage1": {
|
||||||
|
"message": "SponsorBlock crowd-sourced chapters feature is only available to people who purchase a license, or for people who are granted access for free due their past contributions"
|
||||||
|
},
|
||||||
|
"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": "Clear all segments",
|
||||||
|
"description": "Label for a button in settings"
|
||||||
|
},
|
||||||
|
"clearUnsubmittedSegmentsConfirm": {
|
||||||
|
"message": "Are you sure you want to clear all your unsubmitted segments?",
|
||||||
|
"description": "Confirmation message for the Clear unsubmitted segments button"
|
||||||
|
},
|
||||||
|
"showUnsubmittedSegments": {
|
||||||
|
"message": "Show segments",
|
||||||
|
"description": "Show/hide button for the unsubmitted segments list"
|
||||||
|
},
|
||||||
|
"hideUnsubmittedSegments": {
|
||||||
|
"message": "Hide segments",
|
||||||
|
"description": "Show/hide button for the unsubmitted segments list"
|
||||||
|
},
|
||||||
|
"videoID": {
|
||||||
|
"message": "Video ID",
|
||||||
|
"description": "Header of the unsubmitted segments list"
|
||||||
|
},
|
||||||
|
"segmentCount": {
|
||||||
|
"message": "Segment Count",
|
||||||
|
"description": "Header of the unsubmitted segments list"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"message": "Actions",
|
||||||
|
"description": "Header of the unsubmitted segments list"
|
||||||
|
},
|
||||||
|
"exportSegmentsAsURL": {
|
||||||
|
"message": "Share as URL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
:root {
|
||||||
|
--skip-notice-right: 10px;
|
||||||
|
--skip-notice-padding: 5px;
|
||||||
|
--skip-notice-margin: 5px;
|
||||||
|
--skip-notice-border-horizontal: 5px;
|
||||||
|
--skip-notice-border-vertical: 10px;
|
||||||
|
--sb-dark-red-outline: rgb(130,0,0,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -12,7 +21,7 @@
|
|||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transform: scaleY(0.6) translateY(-30%) translateY(1.5px);
|
transform: scaleY(0.6) translateY(-30%) translateY(1.5px);
|
||||||
z-index: 40;
|
z-index: 42;
|
||||||
|
|
||||||
transition: transform .1s cubic-bezier(0,0,0.2,1);
|
transition: transform .1s cubic-bezier(0,0,0.2,1);
|
||||||
}
|
}
|
||||||
@@ -30,6 +39,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.previewbar.requiredSegment {
|
||||||
|
transform: scaleY(3)
|
||||||
|
}
|
||||||
|
|
||||||
/* Make sure settings are upfront */
|
/* Make sure settings are upfront */
|
||||||
.ytp-settings-menu {
|
.ytp-settings-menu {
|
||||||
z-index: 6000 !important;
|
z-index: 6000 !important;
|
||||||
@@ -45,23 +58,48 @@
|
|||||||
transform: translateY(-1em) !important;
|
transform: translateY(-1em) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
|
||||||
|
transform: translateY(-2em) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||||
transform: translateY(-2em) !important;
|
transform: translateY(-2em) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
|
||||||
|
transform: translateY(-4em) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||||
transform: translateY(1em) !important;
|
transform: translateY(1em) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper {
|
||||||
|
transform: translateY(2em) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||||
transform: translateY(0.5em) !important;
|
transform: translateY(0.5em) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper {
|
||||||
|
transform: translateY(1em) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
transform: translateY(1em) !important;
|
transform: translateY(1em) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
|
||||||
|
display: block !important;
|
||||||
|
transform: translateY(2em) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:hover > .sponsorBlockChapterBar {
|
||||||
|
z-index: 41 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* */
|
/* */
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
@@ -88,6 +126,16 @@
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Removes auto width from being a ytp-player-button */
|
||||||
|
.sbPlayerDownvote {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adds back the padding */
|
||||||
|
.sbPlayerDownvote svg {
|
||||||
|
padding-right: 3.6px;
|
||||||
|
}
|
||||||
|
|
||||||
.autoHiding {
|
.autoHiding {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
@@ -113,8 +161,8 @@
|
|||||||
.sponsorSkipObject {
|
.sponsorSkipObject {
|
||||||
font-family: Roboto, Arial, Helvetica, sans-serif;
|
font-family: Roboto, Arial, Helvetica, sans-serif;
|
||||||
|
|
||||||
margin-left: 2px;
|
margin-left: var(--skip-notice-margin);
|
||||||
margin-right: 2px;
|
margin-right: var(--skip-notice-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sponsorSkipLogo {
|
.sponsorSkipLogo {
|
||||||
@@ -145,7 +193,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
bottom: 100px;
|
bottom: 100px;
|
||||||
right: 10px;
|
right: var(--skip-notice-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sponsorSkipNoticeParent {
|
.sponsorSkipNoticeParent {
|
||||||
@@ -173,6 +221,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sponsorSkipNoticeTableContainer {
|
.sponsorSkipNoticeTableContainer {
|
||||||
|
color: white;
|
||||||
background-color: rgba(28, 28, 28, 0.9);
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
@@ -351,6 +400,7 @@
|
|||||||
.sponsorTimesInfoMessage {
|
.sponsorTimesInfoMessage {
|
||||||
font-size: 13.3333px;
|
font-size: 13.3333px;
|
||||||
color: rgb(235, 235, 235);
|
color: rgb(235, 235, 235);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-guidelines-notice .sponsorTimesInfoMessage td {
|
.sb-guidelines-notice .sponsorTimesInfoMessage td {
|
||||||
@@ -523,12 +573,56 @@ input::-webkit-inner-spin-button {
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
background-color: rgba(28, 28, 28, 0.9);
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
border-color: rgb(130,0,0,0.9);
|
border-color: var(--sb-dark-red-outline);
|
||||||
color: white;
|
color: white;
|
||||||
border-width: 3px;
|
border-width: 3px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sponsorTimeEditSelector > option {
|
||||||
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Start SelectorComponent */
|
||||||
|
|
||||||
|
.sbSelector {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: calc(100% - var(--skip-notice-right) - var(--skip-notice-padding) * 2 - var(--skip-notice-margin) * 2 - var(--skip-notice-border-horizontal) * 2);
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sbSelectorBackground {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px;
|
||||||
|
margin: auto;
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sbSelectorOption {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: rgb(43, 43, 43);
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sbSelectorOption:hover {
|
||||||
|
background-color: #3a0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* End SelectorComponent */
|
||||||
|
|
||||||
.helpButton {
|
.helpButton {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -543,17 +637,6 @@ input::-webkit-inner-spin-button {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sbChatNotice iframe {
|
|
||||||
height: 32px;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sbChatClose {
|
|
||||||
height: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skipButtonControlBarContainer {
|
.skipButtonControlBarContainer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -627,6 +710,11 @@ input::-webkit-inner-spin-button {
|
|||||||
border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent;
|
border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sponsorBlockTooltip.sbTriangle.centeredSBTriangle::after {
|
||||||
|
left: 50%;
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.sponsorBlockLockedColor {
|
.sponsorBlockLockedColor {
|
||||||
color: #ffc83d;
|
color: #ffc83d;
|
||||||
}
|
}
|
||||||
|
|||||||
106
public/icons/export.svg
Normal file
106
public/icons/export.svg
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 67.671 67.671"
|
||||||
|
style="enable-background:new 0 0 67.671 67.671;"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="export.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs41" /><sodipodi:namedview
|
||||||
|
id="namedview39"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="9.309749"
|
||||||
|
inkscape:cx="33.835499"
|
||||||
|
inkscape:cy="16.649214"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="731"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Capa_1" />
|
||||||
|
<g
|
||||||
|
id="g6"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
<path
|
||||||
|
d="M 52.946,23.348 H 42.834 v 6 h 10.112 c 3.007,0 5.34,1.536 5.34,2.858 v 26.606 c 0,1.322 -2.333,2.858 -5.34,2.858 H 14.724 c -3.007,0 -5.34,-1.536 -5.34,-2.858 V 32.207 c 0,-1.322 2.333,-2.858 5.34,-2.858 h 10.11 v -6 h -10.11 c -6.359,0 -11.34,3.891 -11.34,8.858 v 26.606 c 0,4.968 4.981,8.858 11.34,8.858 h 38.223 c 6.358,0 11.34,-3.891 11.34,-8.858 V 32.207 C 64.286,27.239 59.305,23.348 52.946,23.348 Z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="m 24.957,14.955 c 0.768,0 1.535,-0.293 2.121,-0.879 l 3.756,-3.756 v 13.028 6 11.494 c 0,1.657 1.343,3 3,3 1.657,0 3,-1.343 3,-3 v -11.494 -6 -13.231 l 3.959,3.959 c 0.586,0.586 1.354,0.879 2.121,0.879 0.767,0 1.535,-0.293 2.121,-0.879 1.172,-1.171 1.172,-3.071 0,-4.242 L 36.078,0.877 C 35.492,0.291 34.725,0 33.958,0 33.95,0 33.943,0 33.935,0 33.927,0 33.92,0 33.912,0 33.145,0 32.378,0.291 31.792,0.877 l -8.957,8.957 c -1.172,1.171 -1.172,3.071 0,4.242 0.587,0.586 1.354,0.879 2.122,0.879 z"
|
||||||
|
id="path4"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g8"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g10"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g12"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g14"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g16"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g18"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g20"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g22"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g24"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g26"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g28"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g30"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g32"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g34"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g36"
|
||||||
|
style="fill:#ffffff">
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
91
public/icons/import.svg
Normal file
91
public/icons/import.svg
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Capa_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 67.671 67.671"
|
||||||
|
style="enable-background:new 0 0 67.671 67.671;"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="import.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs41" /><sodipodi:namedview
|
||||||
|
id="namedview39"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="9.309749"
|
||||||
|
inkscape:cx="33.835499"
|
||||||
|
inkscape:cy="33.835499"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="983"
|
||||||
|
inkscape:window-x="482"
|
||||||
|
inkscape:window-y="768"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g6" />
|
||||||
|
<g
|
||||||
|
id="g6">
|
||||||
|
<path
|
||||||
|
d="M52.946,23.348H42.834v6h10.112c3.007,0,5.34,1.536,5.34,2.858v26.606c0,1.322-2.333,2.858-5.34,2.858H14.724 c-3.007,0-5.34-1.536-5.34-2.858V32.207c0-1.322,2.333-2.858,5.34-2.858h10.11v-6h-10.11c-6.359,0-11.34,3.891-11.34,8.858v26.606 c0,4.968,4.981,8.858,11.34,8.858h38.223c6.358,0,11.34-3.891,11.34-8.858V32.207C64.286,27.239,59.305,23.348,52.946,23.348z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
<path
|
||||||
|
d="m 42.913,34.887 c -0.768,0 -1.370265,0.528017 -2.121,0.879 l -3.756,3.756 v -19.028 -6 V 3 c 0,-1.657 -1.343,-3 -3,-3 -1.657,0 -3,1.343 -3,3 v 11.494 12 13.231 l -3.959,-3.959 c -0.586,-0.586 -1.354,-0.879 -2.121,-0.879 -0.767,0 -1.535,0.293 -2.121,0.879 -1.172,1.171 -1.172,3.071 0,4.242 l 8.957,8.957 c 0.586,0.586 1.353,0.877 2.12,0.877 h 0.023 0.023 c 0.767,0 1.534,-0.291 2.12,-0.877 l 8.957,-8.957 c 1.172,-1.171 1.172,-3.071 0,-4.242 -0.587,-0.586 -1.354,-0.879 -2.122,-0.879 z"
|
||||||
|
id="path4"
|
||||||
|
sodipodi:nodetypes="sscccssscccssccsscssccs"
|
||||||
|
style="fill:#ffffff" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g8">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g10">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g12">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g14">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g16">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g18">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g20">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g22">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g24">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g26">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g28">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g30">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g32">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g34">
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g36">
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
1
public/icons/skip.svg
Normal file
1
public/icons/skip.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 196 B |
1
public/icons/sort.svg
Normal file
1
public/icons/sort.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 201 B |
@@ -123,6 +123,14 @@ html, body {
|
|||||||
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
|
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.categoryExtraOptions {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#music_offtopic_autoSkipOnMusicVideos {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.option-group > div:last-child, .option-group > #keybind-dialog {
|
.option-group > div:last-child, .option-group > #keybind-dialog {
|
||||||
border-bottom: inherit;
|
border-bottom: inherit;
|
||||||
}
|
}
|
||||||
@@ -309,6 +317,14 @@ input[type='number'] {
|
|||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled .slider {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
#options {
|
#options {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
flex-basis: 80%;
|
flex-basis: 80%;
|
||||||
@@ -346,6 +362,10 @@ input[type='number'] {
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
width: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -671,3 +691,8 @@ svg {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upsellButton {
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
@@ -66,18 +66,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-type="toggle" data-sync="autoSkipOnMusicVideos">
|
|
||||||
<div class="switch-container">
|
|
||||||
<label class="switch">
|
|
||||||
<input id="autoSkipOnMusicVideos" type="checkbox" checked>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
<label class="switch-label" for="autoSkipOnMusicVideos">
|
|
||||||
__MSG_autoSkipOnMusicVideos__
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-type="toggle" data-sync="muteSegments">
|
<div data-type="toggle" data-sync="muteSegments">
|
||||||
<div class="switch-container">
|
<div class="switch-container">
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
@@ -314,6 +302,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div data-type="toggle" data-toggle-type="reverse" data-sync="showUpsells" data-no-safari="true">
|
||||||
|
<div class="switch-container">
|
||||||
|
<label class="switch">
|
||||||
|
<input id="showUpsell" type="checkbox" checked>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<label class="switch-label" for="showUpsells">
|
||||||
|
__MSG_hideUpsells__
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="keybinds" class="option-group hidden">
|
<div id="keybinds" class="option-group hidden">
|
||||||
@@ -333,6 +333,16 @@
|
|||||||
<div class="inline"></div>
|
<div class="inline"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div data-type="keybind-change" data-sync="nextChapterKeybind">
|
||||||
|
<label class="optionLabel">__MSG_nextChapterKeybind__:</label>
|
||||||
|
<div class="inline"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-type="keybind-change" data-sync="previousChapterKeybind">
|
||||||
|
<label class="optionLabel">__MSG_previousChapterKeybind__:</label>
|
||||||
|
<div class="inline"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="import" class="option-group hidden">
|
<div id="import" class="option-group hidden">
|
||||||
@@ -353,6 +363,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div data-type="react-UnsubmittedVideosComponent"></div>
|
||||||
|
|
||||||
<div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
|
<div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
|
||||||
<h2>__MSG_exportOptions__</h2>
|
<h2>__MSG_exportOptions__</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
__MSG_invidiousPermissionRefresh__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<div id="acceptPermissionButton" class="option-button inline">
|
<div id="acceptPermissionButton" class="option-button inline">
|
||||||
__MSG_acceptPermission__
|
__MSG_acceptPermission__
|
||||||
|
|||||||
@@ -100,6 +100,10 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorBlockPopupContainer iframe {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Disable popup max height when displayed in-page (content.ts)
|
* Disable popup max height when displayed in-page (content.ts)
|
||||||
*/
|
*/
|
||||||
@@ -110,7 +114,7 @@
|
|||||||
/*
|
/*
|
||||||
* Disable fixed popup width when displayed in-page (content.ts)
|
* Disable fixed popup width when displayed in-page (content.ts)
|
||||||
*/
|
*/
|
||||||
#sponsorBlockPopupContainer #sponsorBlockPopupBody {
|
#sponsorBlockPopupBody.is-embedded {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,22 +152,46 @@
|
|||||||
margin: 8px;
|
margin: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Refresh segments button
|
|
||||||
*/
|
|
||||||
#refreshSegmentsButton {
|
#refreshSegmentsButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#issueReporterImportExport {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refreshSegmentsButton, #issueReporterImportExport button {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin: 5px auto;
|
|
||||||
border: none;
|
border: none;
|
||||||
padding: 5px;
|
|
||||||
}
|
}
|
||||||
#refreshSegmentsButton:hover {
|
|
||||||
|
#refreshSegmentsButton:hover, #issueReporterImportExport button:hover {
|
||||||
background-color: var(--sb-grey-bg-color);
|
background-color: var(--sb-grey-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#issueReporterImportExport button {
|
||||||
|
padding: 5px;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#issueReporterImportExport img {
|
||||||
|
width: 24px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#importSegmentsText {
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#importSegmentsMenu button {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* <details> wrapper around each segment
|
* <details> wrapper around each segment
|
||||||
*/
|
*/
|
||||||
@@ -195,6 +223,15 @@
|
|||||||
.segmentSummary > div {
|
.segmentSummary > div {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.segmentActive {
|
||||||
|
color: #bdfffb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmentPassed {
|
||||||
|
color: #adadad;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Category dot in segment
|
* Category dot in segment
|
||||||
*/
|
*/
|
||||||
@@ -556,3 +593,45 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorBlockPopupBody .u-mZ {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sponsorBlockPopupBody .hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#issueReporterTabs {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#issueReporterTabs > span {
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin: 0 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #444848;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#issueReporterTabs > span > span {
|
||||||
|
position: relative;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#issueReporterTabs > span > span::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.1em;
|
||||||
|
background-color: rgb(145, 0, 0);
|
||||||
|
transition: transform 300ms;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#issueReporterTabs > span.sbSelected > span::after {
|
||||||
|
transform: scaleX(0.8);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link id="sponsorBlockPopupFont" href="/libs/Source+Sans+Pro.css" rel="stylesheet">
|
<link id="sponsorBlockPopupFont" href="/libs/Source+Sans+Pro.css" rel="stylesheet">
|
||||||
<link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet">
|
<link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet">
|
||||||
|
<link id="sponsorBlockStyleSheet" href="shared.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="sponsorBlockPopupBody" style="visibility: hidden">
|
<body id="sponsorBlockPopupBody" style="visibility: hidden">
|
||||||
@@ -34,7 +35,33 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- Video Segments -->
|
<!-- Video Segments -->
|
||||||
<div id="issueReporterContainer">
|
<div id="issueReporterContainer">
|
||||||
|
<div id="issueReporterTabs" class="hidden">
|
||||||
|
<span id="issueReporterTabSegments" class="sbSelected">
|
||||||
|
<span>__MSG_SegmentsCap__</span>
|
||||||
|
</span>
|
||||||
|
<span id="issueReporterTabChapters">
|
||||||
|
<span>__MSG_Chapters__</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div id="issueReporterTimeButtons"></div>
|
<div id="issueReporterTimeButtons"></div>
|
||||||
|
<div id="issueReporterImportExport">
|
||||||
|
<div id="importExportButtons">
|
||||||
|
<button id="importSegmentsButton" title="__MSG_importSegments__">
|
||||||
|
<img src="/icons/import.svg" alt="Refresh icon" id="importSegments" />
|
||||||
|
</button>
|
||||||
|
<button id="exportSegmentsButton" class="hidden" title="__MSG_exportSegments__">
|
||||||
|
<img src="/icons/export.svg" alt="Export icon" id="exportSegments" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span id="importSegmentsMenu" class="hidden">
|
||||||
|
<textarea id="importSegmentsText" rows="5" style="width:80%"></textarea>
|
||||||
|
|
||||||
|
<button id="importSegmentsSubmit" title="__MSG_importSegments__">
|
||||||
|
__MSG_Import__
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
1
public/res/countries.json
Normal file
1
public/res/countries.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"Albania":{"allowed":true},"Algeria":{"allowed":true},"Angola":{"allowed":true},"Argentina":{"allowed":true},"Armenia":{"allowed":true},"Australia":{"allowed":false},"Austria":{"allowed":false},"Azerbaijan":{"allowed":true},"Bangladesh":{"allowed":true},"Belarus":{"allowed":true},"Belgium":{"allowed":false},"Belize":{"allowed":true},"Benin":{"allowed":true},"Bhutan":{"allowed":true},"Bolivia":{"allowed":true},"Bosnia and Herzegovina":{"allowed":true},"Botswana":{"allowed":true},"Brazil":{"allowed":true},"Bulgaria":{"allowed":true},"Burkina Faso":{"allowed":true},"Burundi":{"allowed":true},"Cameroon":{"allowed":true},"Canada":{"allowed":false},"Central African Republic":{"allowed":true},"Chad":{"allowed":true},"Chile":{"allowed":true},"China":{"allowed":true},"Colombia":{"allowed":true},"Comoros":{"allowed":true},"Costa Rica":{"allowed":true},"Croatia":{"allowed":true},"Cyprus":{"allowed":false},"Czech Republic":{"allowed":false},"Denmark":{"allowed":false},"Djibouti":{"allowed":true},"Dominican Republic":{"allowed":true},"DR Congo":{"allowed":true},"Ecuador":{"allowed":true},"Egypt":{"allowed":true},"El Salvador":{"allowed":true},"Estonia":{"allowed":false},"Eswatini":{"allowed":true},"Ethiopia":{"allowed":true},"Fiji":{"allowed":true},"Finland":{"allowed":false},"France":{"allowed":false},"Gabon":{"allowed":true},"Gambia":{"allowed":true},"Georgia":{"allowed":true},"Germany":{"allowed":false},"Ghana":{"allowed":true},"Greece":{"allowed":true},"Guatemala":{"allowed":true},"Guinea":{"allowed":true},"Guinea-Bissau":{"allowed":true},"Guyana":{"allowed":true},"Haiti":{"allowed":true},"Honduras":{"allowed":true},"Hungary":{"allowed":true},"Iceland":{"allowed":false},"India":{"allowed":true},"Iran":{"allowed":true},"Iraq":{"allowed":true},"Ireland":{"allowed":false},"Israel":{"allowed":false},"Italy":{"allowed":false},"Ivory Coast":{"allowed":true},"Jamaica":{"allowed":true},"Japan":{"allowed":false},"Jordan":{"allowed":true},"Kazakhstan":{"allowed":true},"Kenya":{"allowed":true},"Kiribati":{"allowed":true},"Kyrgyzstan":{"allowed":true},"Laos":{"allowed":true},"Latvia":{"allowed":true},"Lebanon":{"allowed":true},"Lesotho":{"allowed":true},"Liberia":{"allowed":true},"Lithuania":{"allowed":true},"Luxembourg":{"allowed":false},"Madagascar":{"allowed":true},"Malawi":{"allowed":true},"Malaysia":{"allowed":true},"Maldives":{"allowed":true},"Mali":{"allowed":true},"Malta":{"allowed":false},"Mauritania":{"allowed":true},"Mauritius":{"allowed":true},"Mexico":{"allowed":true},"Micronesia":{"allowed":true},"Moldova":{"allowed":true},"Mongolia":{"allowed":true},"Montenegro":{"allowed":true},"Morocco":{"allowed":true},"Mozambique":{"allowed":true},"Myanmar":{"allowed":true},"Namibia":{"allowed":true},"Nepal":{"allowed":true},"Netherlands":{"allowed":false},"Nicaragua":{"allowed":true},"Niger":{"allowed":true},"Nigeria":{"allowed":true},"North Macedonia":{"allowed":true},"Norway":{"allowed":false},"Pakistan":{"allowed":true},"Panama":{"allowed":true},"Papua New Guinea":{"allowed":true},"Paraguay":{"allowed":true},"Peru":{"allowed":true},"Philippines":{"allowed":true},"Poland":{"allowed":true},"Portugal":{"allowed":true},"Republic of the Congo":{"allowed":true},"Romania":{"allowed":true},"Russia":{"allowed":true},"Rwanda":{"allowed":true},"Saint Lucia":{"allowed":true},"Samoa":{"allowed":true},"Sao Tome and Principe":{"allowed":true},"Senegal":{"allowed":true},"Serbia":{"allowed":true},"Seychelles":{"allowed":true},"Sierra Leone":{"allowed":true},"Slovakia":{"allowed":true},"Slovenia":{"allowed":false},"Solomon Islands":{"allowed":true},"South Africa":{"allowed":true},"South Korea":{"allowed":false},"South Sudan":{"allowed":true},"Spain":{"allowed":false},"Sri Lanka":{"allowed":true},"Sudan":{"allowed":true},"Suriname":{"allowed":true},"Sweden":{"allowed":false},"Switzerland":{"allowed":false},"Syria":{"allowed":true},"Taiwan":{"allowed":false},"Tajikistan":{"allowed":true},"Tanzania":{"allowed":true},"Thailand":{"allowed":true},"Timor-Leste":{"allowed":true},"Togo":{"allowed":true},"Tonga":{"allowed":true},"Trinidad and Tobago":{"allowed":true},"Tunisia":{"allowed":true},"Turkey":{"allowed":true},"Turkmenistan":{"allowed":true},"Tuvalu":{"allowed":true},"Uganda":{"allowed":true},"Ukraine":{"allowed":true},"United Arab Emirates":{"allowed":false},"United Kingdom":{"allowed":false},"United States":{"allowed":false},"Uruguay":{"allowed":true},"Uzbekistan":{"allowed":true},"Vanuatu":{"allowed":true},"Venezuela":{"allowed":true},"Vietnam":{"allowed":true},"Yemen":{"allowed":true},"Zambia":{"allowed":true},"Zimbabwe":{"allowed":true}}
|
||||||
219
public/shared.css
Normal file
219
public/shared.css
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
.sponsorSkipNoticeParent {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
bottom: 100px;
|
||||||
|
right: var(--skip-notice-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeParent, .sponsorSkipNotice {
|
||||||
|
border-spacing: var(--skip-notice-border-horizontal) var(--skip-notice-border-vertical);
|
||||||
|
padding-left: var(--skip-notice-padding);
|
||||||
|
padding-right: var(--skip-notice-padding);
|
||||||
|
|
||||||
|
border-collapse: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeParent {
|
||||||
|
min-width: 350px;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeTableContainer {
|
||||||
|
background-color: rgba(28, 28, 28, 0.9);
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice {
|
||||||
|
transition: all 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeLimitWidth {
|
||||||
|
max-width: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice .hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Cloudtube */
|
||||||
|
.sponsorSkipNotice td, .sponsorSkipNotice table, .sponsorSkipNotice th {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFadeIn {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFaded {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFadeOut {
|
||||||
|
transition: opacity 3s cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||||
|
opacity: 0 !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice .sponsorSkipNoticeTimeLeft {
|
||||||
|
color: #eeeeee;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border: 1px solid #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeTimeLeft img {
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 13px;
|
||||||
|
|
||||||
|
padding-top: 7.8%;
|
||||||
|
padding-bottom: 7.8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if two are very close to eachother */
|
||||||
|
.secondSkipNotice {
|
||||||
|
bottom: 290px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noticeLeftIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNotice .sponsorSkipNoticeUnskipSection {
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
border-left: 1px solid rgb(150, 150, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeButton {
|
||||||
|
background: none;
|
||||||
|
color: rgb(235, 235, 235);
|
||||||
|
border: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 13.3333px !important;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeButton:hover {
|
||||||
|
background-color: rgba(235, 235, 235,0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
transition: background-color 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeFirstRow .sponsorSkipNoticeButton.sponsorSkipSmallButton {
|
||||||
|
height: 1.3em;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesVoteButtonsContainer {
|
||||||
|
float: left;
|
||||||
|
vertical-align:middle;
|
||||||
|
padding: 2px 5px;
|
||||||
|
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesVoteButtonsContainer div{
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeRightSection {
|
||||||
|
right: 0;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeRightButton {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeCloseButton {
|
||||||
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
|
box-sizing: unset;
|
||||||
|
|
||||||
|
padding: 2px 5px;
|
||||||
|
|
||||||
|
margin-left: 2px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipNoticeCloseButton.biggerCloseButton {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipMessage {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgb(235, 235, 235);
|
||||||
|
|
||||||
|
margin-top: auto;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorSkipInfo {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #000000;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sponsorTimesThanksForVotingText {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sponsorTimesThanksForVotingInfoText {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesVoteButtonMessage {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsorTimesInfoMessage {
|
||||||
|
font-size: 13.3333px;
|
||||||
|
color: rgb(235, 235, 235);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sb-guidelines-notice .sponsorTimesInfoMessage td {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
94
public/upsell/index.html
Normal file
94
public/upsell/index.html
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Upsell - SponsorBlock</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
|
||||||
|
<link href="styles.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script src="../js/vendor.js"></script>
|
||||||
|
<script src="../js/upsell.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="sponsorBlockPageBody">
|
||||||
|
|
||||||
|
<div id="title" class="titleBar">
|
||||||
|
<img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic" />
|
||||||
|
SponsorBlock
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
<p>
|
||||||
|
__MSG_chaptersPage1__
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/H_mP7bpbA_c?modestbranding=1&rel=0" title="Demo Video"
|
||||||
|
frameborder="0" allow="autoplay; clipboard-write; encrypted-media; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="center row-item">
|
||||||
|
<a href="https://buy.ajay.app/l/sponsorblock" class="option-link side-by-side" target="_blank" rel="noreferrer">
|
||||||
|
<div id="oneTimePurchase" class="option-button inline">
|
||||||
|
__MSG_oneTimePurchase__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://www.patreon.com/ajayyy" class="option-link side-by-side" target="_blank" rel="noreferrer">
|
||||||
|
<div class="option-button side-by-side inline">
|
||||||
|
__MSG_joinOnPatreon__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center row-item">
|
||||||
|
<input id="redeemCodeInput" class="option-text-box" type="text" placeholder="__MSG_enterLicenseKey__">
|
||||||
|
<div id="redeemButton" class="option-button inline">
|
||||||
|
__MSG_redeem__
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center row-item">
|
||||||
|
<a href="https://www.patreon.com/oauth2/authorize?response_type=code&client_id=-W7ib8J-LB3jowb1fqE07A7RDUovy45_pOoWcjby6yr5upo6At8Jlg2BPhWDXO2k&redirect_uri=https%3A%2F%2Fsponsor.ajay.app%2Fapi%2FgenerateToken%2Fpatreon"
|
||||||
|
class="option-link" target="_blank" rel="noreferrer">
|
||||||
|
<div class="option-button inline">
|
||||||
|
__MSG_patreonSignIn__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="cantAfford" class="center">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
__MSG_alreadyDonated__ sponsorblock-free@ajay.app
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="subsidizedPrice" class="center hidden">
|
||||||
|
__MSG_selectYourCountry__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="subsidizedLink" class="center hidden">
|
||||||
|
<a href="https://buy.ajay.app/l/sponsorblock/purchasing-power" class="option-link" target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
<div class="option-button inline">
|
||||||
|
__MSG_discountLink__
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="noSubsidizedLink" class="center hidden">
|
||||||
|
__MSG_noDiscount__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
387
public/upsell/styles.css
Normal file
387
public/upsell/styles.css
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
/* Based on options page CSS */
|
||||||
|
html {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center p {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-item {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keybind-status {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-description {
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-description {
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text-box {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background-color: #c00000;
|
||||||
|
padding: 10px;
|
||||||
|
color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-link.side-by-side {
|
||||||
|
padding: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-button:hover {
|
||||||
|
background-color: #fc0303;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-button.disabled {
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
background-color: #520000;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
#options {
|
||||||
|
max-width: 60%;
|
||||||
|
text-align: left;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-container:after {
|
||||||
|
content: attr(label-name);
|
||||||
|
position: absolute;
|
||||||
|
padding: 4px;
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-label-container {
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated * {
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated .slider:before {
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #fc0303;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(16px);
|
||||||
|
-ms-transform: translateX(16px);
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Boilerplate CSS from https://ajay.app */
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreview {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImage {
|
||||||
|
position: absolute;
|
||||||
|
left: -90px;
|
||||||
|
width: 80px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImageLarge {
|
||||||
|
position: absolute;
|
||||||
|
left: -210px;
|
||||||
|
width: 200px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImageLargeRight {
|
||||||
|
position: absolute;
|
||||||
|
right: -210px;
|
||||||
|
width: 200px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.createdBy {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
background-color: #636363;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
font-size: 50px;
|
||||||
|
color: #212121;
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
transition: font-size 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #dad8d8;
|
||||||
|
|
||||||
|
padding-top: 10px;
|
||||||
|
|
||||||
|
transition: font-size 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle:hover {
|
||||||
|
font-size: 45px;
|
||||||
|
|
||||||
|
transition: font-size 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilepic {
|
||||||
|
background-color: #636363 !important;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilepiccircle {
|
||||||
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
height: 80px;
|
||||||
|
|
||||||
|
transition: height 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
height: 95px;
|
||||||
|
|
||||||
|
transition: height 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact,.smalllink {
|
||||||
|
font-size: 25px;
|
||||||
|
color: #e8e8e8;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contact {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,li {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #c4c4c4;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,li,code,a {
|
||||||
|
max-width: 60%;
|
||||||
|
text-align: left;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation:portrait) {
|
||||||
|
p,li,code,a {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projectPreviewImage {
|
||||||
|
position: unset;
|
||||||
|
width: 130px;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewImage {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recentPostTitle {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #dad8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recentPostDate {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #dad8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,h2,h3,h4,h5,h6 {
|
||||||
|
color: #dad8d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-container:before {
|
||||||
|
content: attr(label-name);
|
||||||
|
padding-right: 4px;
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* React styles */
|
||||||
|
|
||||||
|
.categoryTableElement {
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryTableElement > * {
|
||||||
|
padding-right: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionsSelector {
|
||||||
|
background-color: #c00000;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryColorTextBox {
|
||||||
|
width: 60px;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subsidizedPrice {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#discountButton {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -8,11 +8,14 @@ import { Registration } from "./types";
|
|||||||
window.SB = Config;
|
window.SB = Config;
|
||||||
|
|
||||||
import Utils from "./utils";
|
import Utils from "./utils";
|
||||||
|
import { GenericUtils } from "./utils/genericUtils";
|
||||||
const utils = new Utils({
|
const utils = new Utils({
|
||||||
registerFirefoxContentScript,
|
registerFirefoxContentScript,
|
||||||
unregisterFirefoxContentScript
|
unregisterFirefoxContentScript
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const popupPort: Record<string, chrome.runtime.Port> = {};
|
||||||
|
|
||||||
// Used only on Firefox, which does not support non persistent background pages.
|
// Used only on Firefox, which does not support non persistent background pages.
|
||||||
const contentScriptRegistrations = {};
|
const contentScriptRegistrations = {};
|
||||||
|
|
||||||
@@ -52,7 +55,7 @@ if (!Config.configSyncListeners.includes(onNavigationApiAvailableChange)) {
|
|||||||
Config.configSyncListeners.push(onNavigationApiAvailableChange);
|
Config.configSyncListeners.push(onNavigationApiAvailableChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(function (request, _, callback) {
|
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
|
||||||
switch(request.message) {
|
switch(request.message) {
|
||||||
case "openConfig":
|
case "openConfig":
|
||||||
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
|
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
|
||||||
@@ -99,14 +102,30 @@ chrome.runtime.onMessage.addListener(function (request, _, callback) {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case "time":
|
||||||
|
if (sender.tab) {
|
||||||
|
popupPort[sender.tab.id]?.postMessage(request);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onConnect.addListener((port) => {
|
||||||
|
if (port.name === "popup") {
|
||||||
|
chrome.tabs.query({
|
||||||
|
active: true,
|
||||||
|
currentWindow: true
|
||||||
|
}, tabs => {
|
||||||
|
popupPort[tabs[0].id] = port;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//add help page on install
|
//add help page on install
|
||||||
chrome.runtime.onInstalled.addListener(function () {
|
chrome.runtime.onInstalled.addListener(function () {
|
||||||
// This let's the config sync to run fully before checking.
|
// This let's the config sync to run fully before checking.
|
||||||
// This is required on Firefox
|
// This is required on Firefox
|
||||||
setTimeout(function() {
|
setTimeout(async () => {
|
||||||
const userID = Config.config.userID;
|
const userID = Config.config.userID;
|
||||||
|
|
||||||
// If there is no userID, then it is the first install.
|
// If there is no userID, then it is the first install.
|
||||||
@@ -115,13 +134,19 @@ chrome.runtime.onInstalled.addListener(function () {
|
|||||||
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
|
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
|
||||||
|
|
||||||
//generate a userID
|
//generate a userID
|
||||||
const newUserID = utils.generateUserID();
|
const newUserID = GenericUtils.generateUserID();
|
||||||
//save this UUID
|
//save this UUID
|
||||||
Config.config.userID = newUserID;
|
Config.config.userID = newUserID;
|
||||||
|
|
||||||
// Don't show update notification
|
// Don't show update notification
|
||||||
Config.config.categoryPillUpdate = true;
|
Config.config.categoryPillUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Config.config.supportInvidious) {
|
||||||
|
if (!(await utils.containsInvidiousPermission())) {
|
||||||
|
chrome.tabs.create({url: chrome.extension.getURL("/permissions/index.html")});
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,7 +183,7 @@ async function submitVote(type: number, UUID: string, category: string) {
|
|||||||
|
|
||||||
if (userID == undefined || userID === "undefined") {
|
if (userID == undefined || userID === "undefined") {
|
||||||
//generate one
|
//generate one
|
||||||
userID = utils.generateUserID();
|
userID = GenericUtils.generateUserID();
|
||||||
Config.config.userID = userID;
|
Config.config.userID = userID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +230,7 @@ async function asyncRequestToServer(type: string, address: string, data = {}) {
|
|||||||
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
|
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
|
||||||
// If GET, convert JSON to parameters
|
// If GET, convert JSON to parameters
|
||||||
if (type.toLowerCase() === "get") {
|
if (type.toLowerCase() === "get") {
|
||||||
url = utils.objectToURI(url, data, true);
|
url = GenericUtils.objectToURI(url, data, true);
|
||||||
|
|
||||||
data = null;
|
data = null;
|
||||||
}
|
}
|
||||||
|
|||||||
121
src/components/ChapterVoteComponent.tsx
Normal file
121
src/components/ChapterVoteComponent.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Config from "../config";
|
||||||
|
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||||
|
|
||||||
|
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
|
||||||
|
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
||||||
|
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
|
||||||
|
import { VoteResponse } from "../messageTypes";
|
||||||
|
import { AnimationUtils } from "../utils/animationUtils";
|
||||||
|
import { GenericUtils } from "../utils/genericUtils";
|
||||||
|
import { Tooltip } from "../render/Tooltip";
|
||||||
|
|
||||||
|
export interface ChapterVoteProps {
|
||||||
|
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChapterVoteState {
|
||||||
|
segment?: SponsorTime;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVoteState> {
|
||||||
|
tooltip?: Tooltip;
|
||||||
|
|
||||||
|
constructor(props: ChapterVoteProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
segment: null,
|
||||||
|
show: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Upvote Button */}
|
||||||
|
<button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
|
||||||
|
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "hidden" : "")}
|
||||||
|
draggable="false"
|
||||||
|
title={chrome.i18n.getMessage("upvoteButtonInfo")}
|
||||||
|
onClick={(e) => this.vote(e, 1)}>
|
||||||
|
<ThumbsUpSvg className="playerButtonImage"
|
||||||
|
fill={Config.config.colorPalette.white}
|
||||||
|
width={"inherit"} height={"inherit"} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Downvote Button */}
|
||||||
|
<button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
|
||||||
|
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "hidden" : "")}
|
||||||
|
draggable="false"
|
||||||
|
title={chrome.i18n.getMessage("reportButtonInfo")}
|
||||||
|
onClick={(e) => {
|
||||||
|
const chapterNode = document.querySelector(".ytp-chapter-container") as HTMLElement;
|
||||||
|
|
||||||
|
if (this.tooltip) {
|
||||||
|
this.tooltip.close();
|
||||||
|
this.tooltip = null;
|
||||||
|
} else {
|
||||||
|
const referenceNode = chapterNode?.parentElement?.parentElement;
|
||||||
|
if (referenceNode) {
|
||||||
|
const outerBounding = referenceNode.getBoundingClientRect();
|
||||||
|
const buttonBounding = (e.target as HTMLElement)?.parentElement?.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.tooltip = new Tooltip({
|
||||||
|
referenceNode: chapterNode?.parentElement?.parentElement,
|
||||||
|
prependElement: chapterNode?.parentElement,
|
||||||
|
showLogo: false,
|
||||||
|
showGotIt: false,
|
||||||
|
bottomOffset: `${outerBounding.height + 25}px`,
|
||||||
|
leftOffset: `${buttonBounding.x - outerBounding.x}px`,
|
||||||
|
extraClass: "centeredSBTriangle",
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: chrome.i18n.getMessage("incorrectVote"),
|
||||||
|
listener: (event) => this.vote(event, 0, e.target as HTMLElement).then(() => {
|
||||||
|
this.tooltip?.close();
|
||||||
|
this.tooltip = null;
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
name: chrome.i18n.getMessage("harmfulVote"),
|
||||||
|
listener: (event) => this.vote(event, 30, e.target as HTMLElement).then(() => {
|
||||||
|
this.tooltip?.close();
|
||||||
|
this.tooltip = null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<ThumbsDownSvg
|
||||||
|
className="playerButtonImage"
|
||||||
|
fill={downvoteButtonColor(this.state.segment ? [this.state.segment] : null, SkipNoticeAction.Downvote, SkipNoticeAction.Downvote)}
|
||||||
|
width={"inherit"}
|
||||||
|
height={"inherit"} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async vote(event: React.MouseEvent, type: number, element?: HTMLElement): Promise<void> {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (this.state.segment) {
|
||||||
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(element ?? event.currentTarget as HTMLElement, 0.3);
|
||||||
|
|
||||||
|
const response = await this.props.vote(type, this.state.segment.UUID);
|
||||||
|
await stopAnimation();
|
||||||
|
|
||||||
|
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
|
||||||
|
this.setState({
|
||||||
|
show: type === 1
|
||||||
|
});
|
||||||
|
} else if (response.statusCode !== 403) {
|
||||||
|
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChapterVoteComponent;
|
||||||
@@ -11,6 +11,7 @@ export interface NoticeProps {
|
|||||||
noticeTitle: string,
|
noticeTitle: string,
|
||||||
|
|
||||||
maxCountdownTime?: () => number,
|
maxCountdownTime?: () => number,
|
||||||
|
dontPauseCountdown?: boolean,
|
||||||
amountOfPreviousNotices?: number,
|
amountOfPreviousNotices?: number,
|
||||||
showInSecondSlot?: boolean,
|
showInSecondSlot?: boolean,
|
||||||
timed?: boolean,
|
timed?: boolean,
|
||||||
@@ -25,6 +26,8 @@ export interface NoticeProps {
|
|||||||
smaller?: boolean,
|
smaller?: boolean,
|
||||||
limitWidth?: boolean,
|
limitWidth?: boolean,
|
||||||
extraClass?: string,
|
extraClass?: string,
|
||||||
|
hideLogo?: boolean,
|
||||||
|
hideRightInfo?: boolean,
|
||||||
|
|
||||||
// Callback for when this is closed
|
// Callback for when this is closed
|
||||||
closeListener: () => void,
|
closeListener: () => void,
|
||||||
@@ -117,13 +120,15 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
|||||||
{/* Left column */}
|
{/* Left column */}
|
||||||
<td className="noticeLeftIcon">
|
<td className="noticeLeftIcon">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<img id={"sponsorSkipLogo" + this.idSuffix}
|
{!this.props.hideLogo &&
|
||||||
className="sponsorSkipLogo sponsorSkipObject"
|
<img id={"sponsorSkipLogo" + this.idSuffix}
|
||||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
className="sponsorSkipLogo sponsorSkipObject"
|
||||||
</img>
|
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||||
|
</img>
|
||||||
|
}
|
||||||
|
|
||||||
<span id={"sponsorSkipMessage" + this.idSuffix}
|
<span id={"sponsorSkipMessage" + this.idSuffix}
|
||||||
style={{float: "left"}}
|
style={{float: "left", marginRight: this.props.hideLogo ? "0px" : null}}
|
||||||
className="sponsorSkipMessage sponsorSkipObject">
|
className="sponsorSkipMessage sponsorSkipObject">
|
||||||
|
|
||||||
{this.props.noticeTitle}
|
{this.props.noticeTitle}
|
||||||
@@ -135,28 +140,30 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
|||||||
{this.props.firstRow}
|
{this.props.firstRow}
|
||||||
|
|
||||||
{/* Right column */}
|
{/* Right column */}
|
||||||
<td className="sponsorSkipNoticeRightSection"
|
{!this.props.hideRightInfo &&
|
||||||
style={{top: "9.32px"}}>
|
<td className="sponsorSkipNoticeRightSection"
|
||||||
|
style={{top: "9.32px"}}>
|
||||||
|
|
||||||
{/* Time left */}
|
{/* Time left */}
|
||||||
{this.props.timed ? (
|
{this.props.timed ? (
|
||||||
<span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
|
<span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
|
||||||
onClick={() => this.toggleManualPause()}
|
onClick={() => this.toggleManualPause()}
|
||||||
className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
|
className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
|
||||||
|
|
||||||
{this.getCountdownElements()}
|
{this.getCountdownElements()}
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
) : ""}
|
) : ""}
|
||||||
|
|
||||||
|
|
||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<img src={chrome.extension.getURL("icons/close.png")}
|
<img src={chrome.extension.getURL("icons/close.png")}
|
||||||
className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
|
className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
|
||||||
+ (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
|
+ (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
|
||||||
onClick={() => this.close()}>
|
onClick={() => this.close()}>
|
||||||
</img>
|
</img>
|
||||||
</td>
|
</td>
|
||||||
|
}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
@@ -289,7 +296,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pauseCountdown(): void {
|
pauseCountdown(): void {
|
||||||
if (!this.props.timed) return;
|
if (!this.props.timed || this.props.dontPauseCountdown) return;
|
||||||
|
|
||||||
//remove setInterval
|
//remove setInterval
|
||||||
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
||||||
|
|||||||
@@ -36,12 +36,31 @@ class NoticeTextSelectionComponent extends React.Component<NoticeTextSelectionPr
|
|||||||
: null}
|
: null}
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{this.props.text}
|
{this.getTextElements(this.props.text)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getTextElements(text: string): Array<string | React.ReactElement> {
|
||||||
|
const elements: Array<string | React.ReactElement> = [];
|
||||||
|
const textParts = text.split(/(?=\s+)/);
|
||||||
|
for (const textPart of textParts) {
|
||||||
|
if (textPart.match(/^\s*http/)) {
|
||||||
|
elements.push(
|
||||||
|
<a href={textPart} target="_blank" rel="noreferrer">
|
||||||
|
{textPart}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
elements.push(textPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NoticeTextSelectionComponent;
|
export default NoticeTextSelectionComponent;
|
||||||
55
src/components/SelectorComponent.tsx
Normal file
55
src/components/SelectorComponent.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface SelectorOption {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectorProps {
|
||||||
|
id: string;
|
||||||
|
options: SelectorOption[];
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectorState {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectorComponent extends React.Component<SelectorProps, SelectorState> {
|
||||||
|
|
||||||
|
constructor(props: SelectorProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Setup state
|
||||||
|
this.state = {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div id={this.props.id}
|
||||||
|
className="sbSelector">
|
||||||
|
<div className="sbSelectorBackground">
|
||||||
|
{this.getOptions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions(): React.ReactElement[] {
|
||||||
|
const result: React.ReactElement[] = [];
|
||||||
|
for (const option of this.props.options) {
|
||||||
|
result.push(
|
||||||
|
<div className="sbSelectorOption"
|
||||||
|
onClick={() => this.props.onChange(option.label)}
|
||||||
|
key={option.label}>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectorComponent;
|
||||||
@@ -13,6 +13,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
|
|||||||
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
||||||
import PencilSvg from "../svg-icons/pencil_svg";
|
import PencilSvg from "../svg-icons/pencil_svg";
|
||||||
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
|
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
|
||||||
|
import { GenericUtils } from "../utils/genericUtils";
|
||||||
|
|
||||||
enum SkipButtonState {
|
enum SkipButtonState {
|
||||||
Undo, // Unskip
|
Undo, // Unskip
|
||||||
@@ -540,7 +541,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
|||||||
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
|
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
|
||||||
const sponsorTimesSubmitting : SponsorTime = {
|
const sponsorTimesSubmitting : SponsorTime = {
|
||||||
segment: this.segments[index].segment,
|
segment: this.segments[index].segment,
|
||||||
UUID: utils.generateUserID() as SegmentUUID,
|
UUID: GenericUtils.generateUserID() as SegmentUUID,
|
||||||
category: this.segments[index].category,
|
category: this.segments[index].category,
|
||||||
actionType: this.segments[index].actionType,
|
actionType: this.segments[index].actionType,
|
||||||
source: SponsorSourceType.Local
|
source: SponsorSourceType.Local
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as CompileConfig from "../../config.json";
|
import * as CompileConfig from "../../config.json";
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
import { ActionType, Category, ContentContainer, SponsorTime } from "../types";
|
import { ActionType, Category, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
|
||||||
import Utils from "../utils";
|
import Utils from "../utils";
|
||||||
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
||||||
import { RectangleTooltip } from "../render/RectangleTooltip";
|
import { RectangleTooltip } from "../render/RectangleTooltip";
|
||||||
|
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
|
||||||
|
import { GenericUtils } from "../utils/genericUtils";
|
||||||
|
import { noRefreshFetchingChaptersAllowed } from "../utils/licenseKey";
|
||||||
|
|
||||||
|
|
||||||
const utils = new Utils();
|
const utils = new Utils();
|
||||||
@@ -25,16 +28,23 @@ export interface SponsorTimeEditState {
|
|||||||
editing: boolean;
|
editing: boolean;
|
||||||
sponsorTimeEdits: [string, string];
|
sponsorTimeEdits: [string, string];
|
||||||
selectedCategory: Category;
|
selectedCategory: Category;
|
||||||
|
description: string;
|
||||||
|
suggestedNames: SelectorOption[];
|
||||||
|
chapterNameSelectorOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CATEGORY = "chooseACategory";
|
const DEFAULT_CATEGORY = "chooseACategory";
|
||||||
|
|
||||||
|
const categoryNamesGrams: string[] = [].concat(...CompileConfig.categoryList.filter((name) => name !== "chapter")
|
||||||
|
.map((name) => chrome.i18n.getMessage("category_" + name).split(/\/|\s|-/)));
|
||||||
|
|
||||||
class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, SponsorTimeEditState> {
|
class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, SponsorTimeEditState> {
|
||||||
|
|
||||||
idSuffix: string;
|
idSuffix: string;
|
||||||
|
|
||||||
categoryOptionRef: React.RefObject<HTMLSelectElement>;
|
categoryOptionRef: React.RefObject<HTMLSelectElement>;
|
||||||
actionTypeOptionRef: React.RefObject<HTMLSelectElement>;
|
actionTypeOptionRef: React.RefObject<HTMLSelectElement>;
|
||||||
|
descriptionOptionRef: React.RefObject<HTMLInputElement>;
|
||||||
|
|
||||||
configUpdateListener: () => void;
|
configUpdateListener: () => void;
|
||||||
|
|
||||||
@@ -42,26 +52,35 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
// Used when selecting POI or Full
|
// Used when selecting POI or Full
|
||||||
timesBeforeChanging: number[] = [];
|
timesBeforeChanging: number[] = [];
|
||||||
fullVideoWarningShown = false;
|
fullVideoWarningShown = false;
|
||||||
|
categoryNameWarningShown = false;
|
||||||
|
|
||||||
|
// For description auto-complete
|
||||||
|
fetchingSuggestions: boolean;
|
||||||
|
|
||||||
constructor(props: SponsorTimeEditProps) {
|
constructor(props: SponsorTimeEditProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.categoryOptionRef = React.createRef();
|
this.categoryOptionRef = React.createRef();
|
||||||
this.actionTypeOptionRef = React.createRef();
|
this.actionTypeOptionRef = React.createRef();
|
||||||
|
this.descriptionOptionRef = React.createRef();
|
||||||
|
|
||||||
this.idSuffix = this.props.idSuffix;
|
this.idSuffix = this.props.idSuffix;
|
||||||
|
|
||||||
this.previousSkipType = ActionType.Skip;
|
this.previousSkipType = ActionType.Skip;
|
||||||
|
|
||||||
|
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||||
this.state = {
|
this.state = {
|
||||||
editing: false,
|
editing: false,
|
||||||
sponsorTimeEdits: [null, null],
|
sponsorTimeEdits: [null, null],
|
||||||
selectedCategory: DEFAULT_CATEGORY as Category
|
selectedCategory: DEFAULT_CATEGORY as Category,
|
||||||
|
description: sponsorTime.description || "",
|
||||||
|
suggestedNames: [],
|
||||||
|
chapterNameSelectorOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
// Prevent inputs from triggering key events
|
// Prevent inputs from triggering key events
|
||||||
document.getElementById("sponsorTimesContainer" + this.idSuffix).addEventListener('keydown', function (event) {
|
document.getElementById("sponsorTimeEditContainer" + this.idSuffix).addEventListener('keydown', function (event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,6 +106,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
|
|
||||||
render(): React.ReactElement {
|
render(): React.ReactElement {
|
||||||
this.checkToShowFullVideoWarning();
|
this.checkToShowFullVideoWarning();
|
||||||
|
this.checkToShowChapterWarning();
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
textAlign: "center"
|
textAlign: "center"
|
||||||
@@ -96,14 +116,6 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
style.marginTop = "15px";
|
style.marginTop = "15px";
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method is required to get !important
|
|
||||||
// https://stackoverflow.com/a/45669262/1985387
|
|
||||||
const oldYouTubeDarkStyles = (node) => {
|
|
||||||
if (node) {
|
|
||||||
node.style.setProperty("color", "black", "important");
|
|
||||||
node.style.setProperty("text-shadow", "none", "important");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Create time display
|
// Create time display
|
||||||
let timeDisplay: JSX.Element;
|
let timeDisplay: JSX.Element;
|
||||||
const timeDisplayStyle: React.CSSProperties = {};
|
const timeDisplayStyle: React.CSSProperties = {};
|
||||||
@@ -123,11 +135,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
</span>
|
</span>
|
||||||
<input id={"submittingTime0" + this.idSuffix}
|
<input id={"submittingTime0" + this.idSuffix}
|
||||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||||
ref={oldYouTubeDarkStyles}
|
|
||||||
type="text"
|
type="text"
|
||||||
|
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||||
value={this.state.sponsorTimeEdits[0]}
|
value={this.state.sponsorTimeEdits[0]}
|
||||||
onChange={(e) => {this.handleOnChange(0, e, sponsorTime, e.target.value)}}
|
onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
|
||||||
onWheel={(e) => {this.changeTimesWhenScrolling(0, e, sponsorTime)}}>
|
onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
|
||||||
</input>
|
</input>
|
||||||
|
|
||||||
{sponsorTime.actionType !== ActionType.Poi ? (
|
{sponsorTime.actionType !== ActionType.Poi ? (
|
||||||
@@ -138,11 +150,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
|
|
||||||
<input id={"submittingTime1" + this.idSuffix}
|
<input id={"submittingTime1" + this.idSuffix}
|
||||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||||
ref={oldYouTubeDarkStyles}
|
|
||||||
type="text"
|
type="text"
|
||||||
|
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||||
value={this.state.sponsorTimeEdits[1]}
|
value={this.state.sponsorTimeEdits[1]}
|
||||||
onChange={(e) => {this.handleOnChange(1, e, sponsorTime, e.target.value)}}
|
onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)}
|
||||||
onWheel={(e) => {this.changeTimesWhenScrolling(1, e, sponsorTime)}}>
|
onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}>
|
||||||
</input>
|
</input>
|
||||||
|
|
||||||
<span id={"nowButton1" + this.idSuffix}
|
<span id={"nowButton1" + this.idSuffix}
|
||||||
@@ -167,15 +179,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
style={timeDisplayStyle}
|
style={timeDisplayStyle}
|
||||||
className="sponsorTimeDisplay"
|
className="sponsorTimeDisplay"
|
||||||
onClick={this.toggleEditTime.bind(this)}>
|
onClick={this.toggleEditTime.bind(this)}>
|
||||||
{utils.getFormattedTime(segment[0], true) +
|
{GenericUtils.getFormattedTime(segment[0], true) +
|
||||||
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi)
|
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi)
|
||||||
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segment[1], true) : "")}
|
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(segment[1], true) : "")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div id={"sponsorTimeEditContainer" + this.idSuffix} style={style}>
|
||||||
|
|
||||||
{timeDisplay}
|
{timeDisplay}
|
||||||
|
|
||||||
@@ -185,7 +197,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
className="sponsorTimeEditSelector sponsorTimeCategories"
|
className="sponsorTimeEditSelector sponsorTimeCategories"
|
||||||
defaultValue={sponsorTime.category}
|
defaultValue={sponsorTime.category}
|
||||||
ref={this.categoryOptionRef}
|
ref={this.categoryOptionRef}
|
||||||
onChange={this.categorySelectionChange.bind(this)}>
|
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||||
|
onChange={(event) => this.categorySelectionChange(event)}>
|
||||||
{this.getCategoryOptions()}
|
{this.getCategoryOptions()}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@@ -208,6 +221,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
<select id={"sponsorTimeActionTypes" + this.idSuffix}
|
<select id={"sponsorTimeActionTypes" + this.idSuffix}
|
||||||
className="sponsorTimeEditSelector sponsorTimeActionTypes"
|
className="sponsorTimeEditSelector sponsorTimeActionTypes"
|
||||||
defaultValue={sponsorTime.actionType}
|
defaultValue={sponsorTime.actionType}
|
||||||
|
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||||
ref={this.actionTypeOptionRef}
|
ref={this.actionTypeOptionRef}
|
||||||
onChange={(e) => this.actionTypeSelectionChange(e)}>
|
onChange={(e) => this.actionTypeSelectionChange(e)}>
|
||||||
{this.getActionTypeOptions(sponsorTime)}
|
{this.getActionTypeOptions(sponsorTime)}
|
||||||
@@ -215,6 +229,27 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
</div>
|
</div>
|
||||||
): ""}
|
): ""}
|
||||||
|
|
||||||
|
{/* Chapter Name */}
|
||||||
|
{sponsorTime.actionType === ActionType.Chapter ? (
|
||||||
|
<div onMouseLeave={() => this.setState({chapterNameSelectorOpen: false})}>
|
||||||
|
<input id={"chapterName" + this.idSuffix}
|
||||||
|
className="sponsorTimeEdit"
|
||||||
|
ref={this.descriptionOptionRef}
|
||||||
|
type="text"
|
||||||
|
value={this.state.description}
|
||||||
|
onChange={(e) => this.descriptionUpdate(e.target.value)}
|
||||||
|
onFocus={() => this.setState({chapterNameSelectorOpen: true})}>
|
||||||
|
</input>
|
||||||
|
{this.state.chapterNameSelectorOpen && this.state.description &&
|
||||||
|
<SelectorComponent
|
||||||
|
id={"chapterNameSelector" + this.idSuffix}
|
||||||
|
options={this.state.suggestedNames}
|
||||||
|
onChange={(v) => this.descriptionUpdate(v)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
): ""}
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
{/* Editing Tools */}
|
{/* Editing Tools */}
|
||||||
@@ -229,7 +264,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
<span id={"sponsorTimePreviewButton" + this.idSuffix}
|
<span id={"sponsorTimePreviewButton" + this.idSuffix}
|
||||||
className="sponsorTimeEditButton"
|
className="sponsorTimeEditButton"
|
||||||
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}>
|
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}>
|
||||||
{chrome.i18n.getMessage("preview")}
|
{sponsorTime.actionType !== ActionType.Chapter ? chrome.i18n.getMessage("preview")
|
||||||
|
: chrome.i18n.getMessage("End")}
|
||||||
</span>
|
</span>
|
||||||
): ""}
|
): ""}
|
||||||
|
|
||||||
@@ -256,16 +292,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
||||||
|
|
||||||
// check if change is small engough to show tooltip
|
// check if change is small engough to show tooltip
|
||||||
const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
const before = GenericUtils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||||
const after = utils.getFormattedTimeToSeconds(targetValue);
|
const after = GenericUtils.getFormattedTimeToSeconds(targetValue);
|
||||||
const difference = Math.abs(before - after);
|
const difference = Math.abs(before - after);
|
||||||
if (0 < difference && difference< 0.5) this.showScrollToEditToolTip();
|
if (0 < difference && difference < 0.5) this.showScrollToEditToolTip();
|
||||||
|
|
||||||
sponsorTimeEdits[index] = targetValue;
|
sponsorTimeEdits[index] = targetValue;
|
||||||
if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue;
|
if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue;
|
||||||
|
|
||||||
this.setState({sponsorTimeEdits});
|
this.setState({sponsorTimeEdits}, () => this.saveEditTimes());
|
||||||
this.saveEditTimes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void {
|
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void {
|
||||||
@@ -281,7 +316,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
||||||
let timeAsNumber = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
let timeAsNumber = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
||||||
if (timeAsNumber !== null && e.deltaY != 0) {
|
if (timeAsNumber !== null && e.deltaY != 0) {
|
||||||
if (e.deltaY < 0) {
|
if (e.deltaY < 0) {
|
||||||
timeAsNumber += step;
|
timeAsNumber += step;
|
||||||
@@ -290,7 +325,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
} else {
|
} else {
|
||||||
timeAsNumber = 0;
|
timeAsNumber = 0;
|
||||||
}
|
}
|
||||||
sponsorTimeEdits[index] = utils.getFormattedTime(timeAsNumber, true);
|
|
||||||
|
sponsorTimeEdits[index] = GenericUtils.getFormattedTime(timeAsNumber, true);
|
||||||
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
|
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
|
||||||
|
|
||||||
this.setState({sponsorTimeEdits});
|
this.setState({sponsorTimeEdits});
|
||||||
@@ -300,26 +336,29 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
|
|
||||||
showScrollToEditToolTip(): void {
|
showScrollToEditToolTip(): void {
|
||||||
if (!Config.config.scrollToEditTimeUpdate && document.getElementById("sponsorRectangleTooltip" + "sponsorTimesContainer" + this.idSuffix) === null) {
|
if (!Config.config.scrollToEditTimeUpdate && document.getElementById("sponsorRectangleTooltip" + "sponsorTimesContainer" + this.idSuffix) === null) {
|
||||||
this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), () => { Config.config.scrollToEditTimeUpdate = true });
|
this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), "scrollToEdit", () => { Config.config.scrollToEditTimeUpdate = true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showToolTip(text: string, buttonFunction?: () => void): boolean {
|
showToolTip(text: string, id: string, buttonFunction?: () => void): boolean {
|
||||||
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
|
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
|
||||||
if (element) {
|
if (element) {
|
||||||
new RectangleTooltip({
|
const htmlId = `sponsorRectangleTooltip${id + this.idSuffix}`;
|
||||||
text,
|
if (!document.getElementById(htmlId)) {
|
||||||
referenceNode: element.parentElement,
|
new RectangleTooltip({
|
||||||
prependElement: element,
|
text,
|
||||||
timeout: 15,
|
referenceNode: element.parentElement,
|
||||||
bottomOffset: 0 + "px",
|
prependElement: element,
|
||||||
leftOffset: -318 + "px",
|
timeout: 15,
|
||||||
backgroundColor: "rgba(28, 28, 28, 1.0)",
|
bottomOffset: 0 + "px",
|
||||||
htmlId: "sponsorTimesContainer" + this.idSuffix,
|
leftOffset: -318 + "px",
|
||||||
buttonFunction,
|
backgroundColor: "rgba(28, 28, 28, 1.0)",
|
||||||
fontSize: "14px",
|
htmlId,
|
||||||
maxHeight: "200px"
|
buttonFunction,
|
||||||
});
|
fontSize: "14px",
|
||||||
|
maxHeight: "200px"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -334,12 +373,25 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
|
|
||||||
if (videoPercentage > 0.6 && !this.fullVideoWarningShown
|
if (videoPercentage > 0.6 && !this.fullVideoWarningShown
|
||||||
&& (sponsorTime.category === "sponsor" || sponsorTime.category === "selfpromo" || sponsorTime.category === "chooseACategory")) {
|
&& (sponsorTime.category === "sponsor" || sponsorTime.category === "selfpromo" || sponsorTime.category === "chooseACategory")) {
|
||||||
if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"))) {
|
if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"), "fullVideoWarning")) {
|
||||||
this.fullVideoWarningShown = true;
|
this.fullVideoWarningShown = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkToShowChapterWarning(): void {
|
||||||
|
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||||
|
|
||||||
|
if (sponsorTime.actionType === ActionType.Chapter && sponsorTime.description
|
||||||
|
&& !this.categoryNameWarningShown
|
||||||
|
&& categoryNamesGrams.some(
|
||||||
|
(category) => sponsorTime.description.toLowerCase().includes(category.toLowerCase()))) {
|
||||||
|
if (this.showToolTip(chrome.i18n.getMessage("chapterNameTooltipWarning"), "chapterWarning")) {
|
||||||
|
this.categoryNameWarningShown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCategoryOptions(): React.ReactElement[] {
|
getCategoryOptions(): React.ReactElement[] {
|
||||||
const elements = [(
|
const elements = [(
|
||||||
<option value={DEFAULT_CATEGORY}
|
<option value={DEFAULT_CATEGORY}
|
||||||
@@ -349,6 +401,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) {
|
for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) {
|
||||||
|
// If permission not loaded, treat it like we have permission except chapter
|
||||||
|
const defaultBlockCategories = ["chapter"];
|
||||||
|
const permission = Config.config.permissions[category as Category] && (category !== "chapter" || noRefreshFetchingChaptersAllowed());
|
||||||
|
if ((defaultBlockCategories.includes(category) || permission !== undefined) && !permission) continue;
|
||||||
|
|
||||||
elements.push(
|
elements.push(
|
||||||
<option value={category}
|
<option value={category}
|
||||||
key={category}
|
key={category}
|
||||||
@@ -369,7 +426,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
const chosenCategory = event.target.value as Category;
|
const chosenCategory = event.target.value as Category;
|
||||||
|
|
||||||
// See if show more categories was pressed
|
// See if show more categories was pressed
|
||||||
if (event.target.value !== DEFAULT_CATEGORY && !Config.config.categorySelections.some((category) => category.name === event.target.value)) {
|
if (chosenCategory !== DEFAULT_CATEGORY && !Config.config.categorySelections.some((category) => category.name === chosenCategory)) {
|
||||||
event.target.value = DEFAULT_CATEGORY;
|
event.target.value = DEFAULT_CATEGORY;
|
||||||
|
|
||||||
// Alert that they have to enable this category first
|
// Alert that they have to enable this category first
|
||||||
@@ -470,7 +527,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
|
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
|
||||||
}, this.saveEditTimes);
|
}, () => this.saveEditTimes());
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleEditTime(): void {
|
toggleEditTime(): void {
|
||||||
@@ -493,16 +550,16 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
|
|
||||||
/** Returns an array in the sponsorTimeEdits form (formatted time string) from a normal seconds sponsor time */
|
/** Returns an array in the sponsorTimeEdits form (formatted time string) from a normal seconds sponsor time */
|
||||||
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
|
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
|
||||||
return [utils.getFormattedTime(sponsorTime.segment[0], true),
|
return [GenericUtils.getFormattedTime(sponsorTime.segment[0], true),
|
||||||
utils.getFormattedTime(sponsorTime.segment[1], true)];
|
GenericUtils.getFormattedTime(sponsorTime.segment[1], true)];
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEditTimes(): void {
|
saveEditTimes(): void {
|
||||||
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
|
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
|
||||||
|
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
const startTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
||||||
const endTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
const endTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
||||||
|
|
||||||
// Change segment time only if the format was correct
|
// Change segment time only if the format was correct
|
||||||
if (startTime !== null && endTime !== null) {
|
if (startTime !== null && endTime !== null) {
|
||||||
@@ -513,8 +570,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
const category = this.categoryOptionRef.current.value as Category
|
const category = this.categoryOptionRef.current.value as Category
|
||||||
sponsorTimesSubmitting[this.props.index].category = category;
|
sponsorTimesSubmitting[this.props.index].category = category;
|
||||||
|
|
||||||
const inputActionType = this.actionTypeOptionRef?.current?.value as ActionType;
|
const actionType = this.getNextActionType(category, this.actionTypeOptionRef?.current?.value as ActionType);
|
||||||
sponsorTimesSubmitting[this.props.index].actionType = this.getNextActionType(category, inputActionType);
|
sponsorTimesSubmitting[this.props.index].actionType = actionType;
|
||||||
|
|
||||||
|
const description = actionType === ActionType.Chapter ? this.descriptionOptionRef?.current?.value : "";
|
||||||
|
sponsorTimesSubmitting[this.props.index].description = description;
|
||||||
|
|
||||||
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
|
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
|
||||||
Config.forceSyncUpdate("unsubmittedSegments");
|
Config.forceSyncUpdate("unsubmittedSegments");
|
||||||
@@ -536,19 +596,19 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
previewTime(ctrlPressed = false, shiftPressed = false): void {
|
previewTime(ctrlPressed = false, shiftPressed = false): void {
|
||||||
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
|
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
|
||||||
const index = this.props.index;
|
const index = this.props.index;
|
||||||
|
|
||||||
const skipTime = sponsorTimes[index].segment[0];
|
|
||||||
// If segment starts at 0:00, start playback at the end of the segment
|
|
||||||
if (skipTime === 0) {
|
|
||||||
this.props.contentContainer().previewTime(sponsorTimes[index].segment[1]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seekTime = 2;
|
let seekTime = 2;
|
||||||
if (ctrlPressed) seekTime = 0.5;
|
if (ctrlPressed) seekTime = 0.5;
|
||||||
if (shiftPressed) seekTime = 0.25;
|
if (shiftPressed) seekTime = 0.25;
|
||||||
|
|
||||||
this.props.contentContainer().previewTime(skipTime - (seekTime * this.props.contentContainer().v.playbackRate));
|
const startTime = sponsorTimes[index].segment[0];
|
||||||
|
const endTime = sponsorTimes[index].segment[1];
|
||||||
|
const isChapter = sponsorTimes[index].actionType === ActionType.Chapter;
|
||||||
|
|
||||||
|
// If segment starts at 0:00, start playback at the end of the segment
|
||||||
|
const skipToEndTime = startTime === 0 || isChapter;
|
||||||
|
const skipTime = skipToEndTime ? endTime : (startTime - (seekTime * this.props.contentContainer().v.playbackRate));
|
||||||
|
|
||||||
|
this.props.contentContainer().previewTime(skipTime, !isChapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
inspectTime(): void {
|
inspectTime(): void {
|
||||||
@@ -592,6 +652,41 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
descriptionUpdate(description: string): void {
|
||||||
|
this.setState({
|
||||||
|
description
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.fetchingSuggestions) {
|
||||||
|
this.fetchSuggestions(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveEditTimes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchSuggestions(description: string): Promise<void> {
|
||||||
|
if (this.props.contentContainer().channelIDInfo.status !== ChannelIDStatus.Found) return;
|
||||||
|
|
||||||
|
this.fetchingSuggestions = true;
|
||||||
|
const result = await utils.asyncRequestToServer("GET", "/api/chapterNames", {
|
||||||
|
description,
|
||||||
|
channelID: this.props.contentContainer().channelIDInfo.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
try {
|
||||||
|
const names = JSON.parse(result.responseText) as {description: string}[];
|
||||||
|
this.setState({
|
||||||
|
suggestedNames: names.map(n => ({
|
||||||
|
label: n.description
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (e) {} //eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchingSuggestions = false;
|
||||||
|
}
|
||||||
|
|
||||||
configUpdate(): void {
|
configUpdate(): void {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,12 +73,20 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(): React.ReactElement {
|
render(): React.ReactElement {
|
||||||
|
const sortButton =
|
||||||
|
<img id={"sponsorSkipSortButton" + this.state.idSuffix}
|
||||||
|
className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipSmallButton"
|
||||||
|
onClick={() => this.sortSegments()}
|
||||||
|
title={chrome.i18n.getMessage("sortSegments")}
|
||||||
|
src={chrome.extension.getURL("icons/sort.svg")}>
|
||||||
|
</img>;
|
||||||
return (
|
return (
|
||||||
<NoticeComponent noticeTitle={this.state.noticeTitle}
|
<NoticeComponent noticeTitle={this.state.noticeTitle}
|
||||||
idSuffix={this.state.idSuffix}
|
idSuffix={this.state.idSuffix}
|
||||||
ref={this.noticeRef}
|
ref={this.noticeRef}
|
||||||
closeListener={this.cancel.bind(this)}
|
closeListener={this.cancel.bind(this)}
|
||||||
zIndex={5000}>
|
zIndex={5000}
|
||||||
|
firstColumn={sortButton}>
|
||||||
|
|
||||||
{/* Text Boxes */}
|
{/* Text Boxes */}
|
||||||
{this.getMessageBoxes()}
|
{this.getMessageBoxes()}
|
||||||
@@ -198,6 +206,16 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
|||||||
this.cancel();
|
this.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sortSegments(): void {
|
||||||
|
let sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
|
||||||
|
sponsorTimesSubmitting = sponsorTimesSubmitting.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||||
|
|
||||||
|
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
|
||||||
|
Config.forceSyncUpdate("unsubmittedSegments");
|
||||||
|
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
categoryChangeListener(index: number, category: Category): void {
|
categoryChangeListener(index: number, category: Category): void {
|
||||||
const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth;
|
const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth;
|
||||||
if (category !== "chooseACategory" && Config.config.showCategoryGuidelines
|
if (category !== "chooseACategory" && Config.config.showCategoryGuidelines
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import * as CompileConfig from "../../config.json";
|
import * as CompileConfig from "../../../config.json";
|
||||||
import { Category } from "../types";
|
import { Category } from "../../types";
|
||||||
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
|
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
|
||||||
|
|
||||||
export interface CategoryChooserProps {
|
export interface CategoryChooserProps {
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import Config from "../config"
|
import Config from "../../config"
|
||||||
import * as CompileConfig from "../../config.json";
|
import * as CompileConfig from "../../../config.json";
|
||||||
import { Category, CategorySkipOption } from "../types";
|
import { Category, CategorySkipOption } from "../../types";
|
||||||
|
|
||||||
import { getCategorySuffix } from "../utils/categoryUtils";
|
import { getCategorySuffix } from "../../utils/categoryUtils";
|
||||||
|
import ToggleOptionComponent, { ToggleOptionProps } from "./ToggleOptionComponent";
|
||||||
|
import { fetchingChaptersAllowed } from "../../utils/licenseKey";
|
||||||
|
import LockSvg from "../../svg-icons/lock_svg";
|
||||||
|
|
||||||
export interface CategorySkipOptionsProps {
|
export interface CategorySkipOptionsProps {
|
||||||
category: Category;
|
category: Category;
|
||||||
@@ -15,6 +18,7 @@ export interface CategorySkipOptionsProps {
|
|||||||
export interface CategorySkipOptionsState {
|
export interface CategorySkipOptionsState {
|
||||||
color: string;
|
color: string;
|
||||||
previewColor: string;
|
previewColor: string;
|
||||||
|
hideChapter: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
|
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
|
||||||
@@ -27,10 +31,28 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
this.state = {
|
this.state = {
|
||||||
color: props.defaultColor || Config.config.barTypes[this.props.category]?.color,
|
color: props.defaultColor || Config.config.barTypes[this.props.category]?.color,
|
||||||
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color,
|
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color,
|
||||||
}
|
hideChapter: true
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchingChaptersAllowed().then((allowed) => {
|
||||||
|
this.setState({
|
||||||
|
hideChapter: !allowed
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React.ReactElement {
|
render(): React.ReactElement {
|
||||||
|
if (this.state.hideChapter) {
|
||||||
|
// Ensure force update refreshes this
|
||||||
|
fetchingChaptersAllowed().then((allowed) => {
|
||||||
|
if (allowed) {
|
||||||
|
this.setState({
|
||||||
|
hideChapter: !allowed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let defaultOption = "disable";
|
let defaultOption = "disable";
|
||||||
// Set the default opton properly
|
// Set the default opton properly
|
||||||
for (const categorySelection of Config.config.categorySelections) {
|
for (const categorySelection of Config.config.categorySelections) {
|
||||||
@@ -51,10 +73,20 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let extraClasses = "";
|
||||||
|
const disabled = this.props.category === "chapter" && this.state.hideChapter;
|
||||||
|
if (disabled) {
|
||||||
|
extraClasses += " disabled";
|
||||||
|
|
||||||
|
if (!Config.config.showUpsells) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr id={this.props.category + "OptionsRow"}
|
<tr id={this.props.category + "OptionsRow"}
|
||||||
className="categoryTableElement">
|
className={`categoryTableElement${extraClasses}`} >
|
||||||
<td id={this.props.category + "OptionName"}
|
<td id={this.props.category + "OptionName"}
|
||||||
className="categoryTableLabel">
|
className="categoryTableLabel">
|
||||||
{chrome.i18n.getMessage("category_" + this.props.category)}
|
{chrome.i18n.getMessage("category_" + this.props.category)}
|
||||||
@@ -65,21 +97,29 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
<select
|
<select
|
||||||
className="optionsSelector"
|
className="optionsSelector"
|
||||||
defaultValue={defaultOption}
|
defaultValue={defaultOption}
|
||||||
|
disabled={disabled}
|
||||||
onChange={this.skipOptionSelected.bind(this)}>
|
onChange={this.skipOptionSelected.bind(this)}>
|
||||||
{this.getCategorySkipOptions()}
|
{this.getCategorySkipOptions()}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{disabled &&
|
||||||
|
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td id={this.props.category + "ColorOption"}
|
{this.props.category !== "chapter" &&
|
||||||
className="colorOption">
|
<td id={this.props.category + "ColorOption"}
|
||||||
<input
|
className="colorOption">
|
||||||
className="categoryColorTextBox option-text-box"
|
<input
|
||||||
type="color"
|
className="categoryColorTextBox option-text-box"
|
||||||
onChange={(event) => this.setColorState(event, false)}
|
type="color"
|
||||||
value={this.state.color} />
|
disabled={disabled}
|
||||||
</td>
|
onChange={(event) => this.setColorState(event, false)}
|
||||||
|
value={this.state.color} />
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
|
||||||
{this.props.category !== "exclusive_access" &&
|
{!["chapter", "exclusive_access"].includes(this.props.category) &&
|
||||||
<td id={this.props.category + "PreviewColorOption"}
|
<td id={this.props.category + "PreviewColorOption"}
|
||||||
className="previewColorOption">
|
className="previewColorOption">
|
||||||
<input
|
<input
|
||||||
@@ -93,7 +133,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr id={this.props.category + "DescriptionRow"}
|
<tr id={this.props.category + "DescriptionRow"}
|
||||||
className="small-description categoryTableDescription">
|
className={`small-description categoryTableDescription${extraClasses}`}>
|
||||||
<td
|
<td
|
||||||
colSpan={2}>
|
colSpan={2}>
|
||||||
{chrome.i18n.getMessage("category_" + this.props.category + "_description")}
|
{chrome.i18n.getMessage("category_" + this.props.category + "_description")}
|
||||||
@@ -104,6 +144,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{this.getExtraOptionComponents(this.props.category, extraClasses, disabled)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,6 +155,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
|
|
||||||
switch (event.target.value) {
|
switch (event.target.value) {
|
||||||
case "disable":
|
case "disable":
|
||||||
|
Config.config.categorySelections = Config.config.categorySelections.filter(
|
||||||
|
categorySelection => categorySelection.name !== this.props.category);
|
||||||
return;
|
return;
|
||||||
case "showOverlay":
|
case "showOverlay":
|
||||||
option = CategorySkipOption.ShowOverlay;
|
option = CategorySkipOption.ShowOverlay;
|
||||||
@@ -145,7 +189,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
const elements: JSX.Element[] = [];
|
const elements: JSX.Element[] = [];
|
||||||
|
|
||||||
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
||||||
if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
if (this.props.category === "chapter") optionNames = ["disable", "showOverlay"]
|
||||||
|
else if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
||||||
|
|
||||||
for (const optionName of optionNames) {
|
for (const optionName of optionNames) {
|
||||||
elements.push(
|
elements.push(
|
||||||
@@ -182,6 +227,43 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
|||||||
Config.config.barTypes = Config.config.barTypes;
|
Config.config.barTypes = Config.config.barTypes;
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExtraOptionComponents(category: string, extraClasses: string, disabled: boolean): JSX.Element[] {
|
||||||
|
const result = [];
|
||||||
|
for (const option of this.getExtraOptions(category)) {
|
||||||
|
result.push(
|
||||||
|
<tr key={option.configKey} className={extraClasses}>
|
||||||
|
<td id={`${category}_${option.configKey}`} className="categoryExtraOptions">
|
||||||
|
<ToggleOptionComponent
|
||||||
|
configKey={option.configKey}
|
||||||
|
label={option.label}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{width: "inherit"}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtraOptions(category: string): ToggleOptionProps[] {
|
||||||
|
switch (category) {
|
||||||
|
case "chapter":
|
||||||
|
return [{
|
||||||
|
configKey: "renderSegmentsAsChapters",
|
||||||
|
label: chrome.i18n.getMessage("renderAsChapters"),
|
||||||
|
}];
|
||||||
|
case "music_offtopic":
|
||||||
|
return [{
|
||||||
|
configKey: "autoSkipOnMusicVideos",
|
||||||
|
label: chrome.i18n.getMessage("autoSkipOnMusicVideos"),
|
||||||
|
}];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CategorySkipOptionsComponent;
|
export default CategorySkipOptionsComponent;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import Config from "../config";
|
import Config from "../../config";
|
||||||
import { Keybind } from "../types";
|
import { Keybind } from "../../types";
|
||||||
import KeybindDialogComponent from "./KeybindDialogComponent";
|
import KeybindDialogComponent from "./KeybindDialogComponent";
|
||||||
import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils";
|
import { keybindEquals, keybindToString, formatKey } from "../../utils/configUtils";
|
||||||
|
|
||||||
export interface KeybindProps {
|
export interface KeybindProps {
|
||||||
option: string;
|
option: string;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ChangeEvent } from "react";
|
import { ChangeEvent } from "react";
|
||||||
import Config from "../config";
|
import Config from "../../config";
|
||||||
import { Keybind } from "../types";
|
import { Keybind } from "../../types";
|
||||||
import { keybindEquals, formatKey } from "../utils/configUtils";
|
import { keybindEquals, formatKey } from "../../utils/configUtils";
|
||||||
|
|
||||||
export interface KeybindDialogProps {
|
export interface KeybindDialogProps {
|
||||||
option: string;
|
option: string;
|
||||||
57
src/components/options/ToggleOptionComponent.tsx
Normal file
57
src/components/options/ToggleOptionComponent.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import Config from "../../config";
|
||||||
|
|
||||||
|
export interface ToggleOptionProps {
|
||||||
|
configKey: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleOptionState {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToggleOptionComponent extends React.Component<ToggleOptionProps, ToggleOptionState> {
|
||||||
|
|
||||||
|
constructor(props: ToggleOptionProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Setup state
|
||||||
|
this.state = {
|
||||||
|
enabled: Config.config[props.configKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="switch-container" style={this.props.style}>
|
||||||
|
<label className="switch">
|
||||||
|
<input id={this.props.configKey}
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.enabled}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
onChange={(e) => this.clicked(e)}/>
|
||||||
|
<span className="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<label className="switch-label" htmlFor={this.props.configKey}>
|
||||||
|
{this.props.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clicked(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
|
Config.config[this.props.configKey] = event.target.checked;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
enabled: event.target.checked
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToggleOptionComponent;
|
||||||
72
src/components/options/UnsubmittedVideoListComponent.tsx
Normal file
72
src/components/options/UnsubmittedVideoListComponent.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import Config from "../../config";
|
||||||
|
import UnsubmittedVideoListItem from "./UnsubmittedVideoListItem";
|
||||||
|
|
||||||
|
export interface UnsubmittedVideoListProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnsubmittedVideoListState {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnsubmittedVideoListComponent extends React.Component<UnsubmittedVideoListProps, UnsubmittedVideoListState> {
|
||||||
|
|
||||||
|
constructor(props: UnsubmittedVideoListProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Setup state
|
||||||
|
this.state = {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
// Render nothing if there are no unsubmitted segments
|
||||||
|
if (Object.keys(Config.config.unsubmittedSegments).length == 0)
|
||||||
|
return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table id="unsubmittedVideosList"
|
||||||
|
className="categoryChooserTable"
|
||||||
|
style={{marginTop: "10px"}} >
|
||||||
|
<tbody>
|
||||||
|
{/* Headers */}
|
||||||
|
<tr id="UnsubmittedVideosListHeader"
|
||||||
|
className="categoryTableElement categoryTableHeader">
|
||||||
|
<th id="UnsubmittedVideoID">
|
||||||
|
{chrome.i18n.getMessage("videoID")}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th id="UnsubmittedSegmentCount">
|
||||||
|
{chrome.i18n.getMessage("segmentCount")}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th id="UnsubmittedVideoActions">
|
||||||
|
{chrome.i18n.getMessage("actions")}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{this.getUnsubmittedVideos()}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnsubmittedVideos(): JSX.Element[] {
|
||||||
|
const elements: JSX.Element[] = [];
|
||||||
|
|
||||||
|
for (const videoID of Object.keys(Config.config.unsubmittedSegments)) {
|
||||||
|
elements.push(
|
||||||
|
<UnsubmittedVideoListItem videoID={videoID} key={videoID}>
|
||||||
|
</UnsubmittedVideoListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnsubmittedVideoListComponent;
|
||||||
95
src/components/options/UnsubmittedVideoListItem.tsx
Normal file
95
src/components/options/UnsubmittedVideoListItem.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import Config from "../../config";
|
||||||
|
import { exportTimes, exportTimesAsHashParam } from "../../utils/exporter";
|
||||||
|
|
||||||
|
export interface UnsubmittedVideosListItemProps {
|
||||||
|
videoID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnsubmittedVideosListItemState {
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnsubmittedVideoListItem extends React.Component<UnsubmittedVideosListItemProps, UnsubmittedVideosListItemState> {
|
||||||
|
|
||||||
|
constructor(props: UnsubmittedVideosListItemProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Setup state
|
||||||
|
this.state = {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
const segmentCount = Config.config.unsubmittedSegments[this.props.videoID]?.length ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr id={this.props.videoID + "UnsubmittedSegmentsRow"}
|
||||||
|
className="categoryTableElement">
|
||||||
|
<td id={this.props.videoID + "UnsubmittedVideoID"}
|
||||||
|
className="categoryTableLabel">
|
||||||
|
<a href={`https://youtu.be/${this.props.videoID}`}
|
||||||
|
target="_blank" rel="noreferrer">
|
||||||
|
{this.props.videoID}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td id={this.props.videoID + "UnsubmittedSegmentCount"}>
|
||||||
|
{segmentCount}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td id={this.props.videoID + "UnsubmittedVideoActions"}>
|
||||||
|
<div id={this.props.videoID + "ExportSegmentsAction"}
|
||||||
|
className="option-button inline low-profile"
|
||||||
|
onClick={this.exportSegments.bind(this)}>
|
||||||
|
{chrome.i18n.getMessage("exportSegments")}
|
||||||
|
</div>
|
||||||
|
{" "}
|
||||||
|
<div id={this.props.videoID + "ExportSegmentsAsURLAction"}
|
||||||
|
className="option-button inline low-profile"
|
||||||
|
onClick={this.exportSegmentsAsURL.bind(this)}>
|
||||||
|
{chrome.i18n.getMessage("exportSegmentsAsURL")}
|
||||||
|
</div>
|
||||||
|
{" "}
|
||||||
|
<div id={this.props.videoID + "ClearSegmentsAction"}
|
||||||
|
className="option-button inline low-profile"
|
||||||
|
onClick={this.clearSegments.bind(this)}>
|
||||||
|
{chrome.i18n.getMessage("clearTimes")}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSegments(): void {
|
||||||
|
if (confirm(chrome.i18n.getMessage("clearThis"))) {
|
||||||
|
delete Config.config.unsubmittedSegments[this.props.videoID];
|
||||||
|
Config.forceSyncUpdate("unsubmittedSegments");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportSegments(): void {
|
||||||
|
this.copyToClipboard(exportTimes(Config.config.unsubmittedSegments[this.props.videoID]));
|
||||||
|
}
|
||||||
|
|
||||||
|
exportSegmentsAsURL(): void {
|
||||||
|
this.copyToClipboard(`https://youtube.com/watch?v=${this.props.videoID}${exportTimesAsHashParam(Config.config.unsubmittedSegments[this.props.videoID])}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard(text: string): void {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
alert(chrome.i18n.getMessage("CopiedExclamation"));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert(chrome.i18n.getMessage("copyDebugInformationFailed"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnsubmittedVideoListItem;
|
||||||
55
src/components/options/UnsubmittedVideosComponent.tsx
Normal file
55
src/components/options/UnsubmittedVideosComponent.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Config from "../../config";
|
||||||
|
import UnsubmittedVideoListComponent from "./UnsubmittedVideoListComponent";
|
||||||
|
|
||||||
|
export interface UnsubmittedVideosProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnsubmittedVideosState {
|
||||||
|
tableVisible: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnsubmittedVideosComponent extends React.Component<UnsubmittedVideosProps, UnsubmittedVideosState> {
|
||||||
|
|
||||||
|
constructor(props: UnsubmittedVideosProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
tableVisible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
const videoCount = Object.keys(Config.config.unsubmittedSegments).length;
|
||||||
|
const segmentCount = Object.values(Config.config.unsubmittedSegments).reduce((acc: number, vid: Array<unknown>) => acc + vid.length, 0);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div style={{marginBottom: "10px"}}>
|
||||||
|
{segmentCount == 0 ?
|
||||||
|
chrome.i18n.getMessage("unsubmittedSegmentCountsZero") :
|
||||||
|
chrome.i18n.getMessage("unsubmittedSegmentCounts")
|
||||||
|
.replace("{0}", `${segmentCount} ${chrome.i18n.getMessage("unsubmittedSegments" + (segmentCount == 1 ? "Singular" : "Plural"))}`)
|
||||||
|
.replace("{1}", `${videoCount} ${chrome.i18n.getMessage("videos" + (videoCount == 1 ? "Singular" : "Plural"))}`)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{videoCount > 0 && <div className="option-button inline" onClick={() => this.setState({tableVisible: !this.state.tableVisible})}>
|
||||||
|
{chrome.i18n.getMessage(this.state.tableVisible ? "hideUnsubmittedSegments" : "showUnsubmittedSegments")}
|
||||||
|
</div>}
|
||||||
|
{" "}
|
||||||
|
<div className="option-button inline" onClick={this.clearAllSegments}>
|
||||||
|
{chrome.i18n.getMessage("clearUnsubmittedSegments")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.tableVisible && <UnsubmittedVideoListComponent/>}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllSegments(): void {
|
||||||
|
if (confirm(chrome.i18n.getMessage("clearUnsubmittedSegmentsConfirm")))
|
||||||
|
Config.config.unsubmittedSegments = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnsubmittedVideosComponent;
|
||||||
@@ -3,12 +3,18 @@ import * as invidiousList from "../ci/invidiouslist.json";
|
|||||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
|
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
|
||||||
import { keybindEquals } from "./utils/configUtils";
|
import { keybindEquals } from "./utils/configUtils";
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
canSubmit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface SBConfig {
|
interface SBConfig {
|
||||||
userID: string,
|
userID: string,
|
||||||
isVip: boolean,
|
isVip: boolean,
|
||||||
|
permissions: Record<Category, Permission>,
|
||||||
/* Contains unsubmitted segments that the user has created. */
|
/* Contains unsubmitted segments that the user has created. */
|
||||||
unsubmittedSegments: Record<string, SponsorTime[]>,
|
unsubmittedSegments: Record<string, SponsorTime[]>,
|
||||||
defaultCategory: Category,
|
defaultCategory: Category,
|
||||||
|
renderSegmentsAsChapters: boolean,
|
||||||
whitelistedChannels: string[],
|
whitelistedChannels: string[],
|
||||||
forceChannelCheck: boolean,
|
forceChannelCheck: boolean,
|
||||||
minutesSaved: number,
|
minutesSaved: number,
|
||||||
@@ -44,6 +50,7 @@ interface SBConfig {
|
|||||||
allowExpirements: boolean,
|
allowExpirements: boolean,
|
||||||
showDonationLink: boolean,
|
showDonationLink: boolean,
|
||||||
showPopupDonationCount: number,
|
showPopupDonationCount: number,
|
||||||
|
showUpsells: boolean,
|
||||||
donateClicked: number,
|
donateClicked: number,
|
||||||
autoHideInfoButton: boolean,
|
autoHideInfoButton: boolean,
|
||||||
autoSkipOnMusicVideos: boolean,
|
autoSkipOnMusicVideos: boolean,
|
||||||
@@ -56,6 +63,7 @@ interface SBConfig {
|
|||||||
categoryPillUpdate: boolean,
|
categoryPillUpdate: boolean,
|
||||||
darkMode: boolean,
|
darkMode: boolean,
|
||||||
showCategoryGuidelines: boolean,
|
showCategoryGuidelines: boolean,
|
||||||
|
chaptersAvailable: boolean,
|
||||||
|
|
||||||
// Used to cache calculated text color info
|
// Used to cache calculated text color info
|
||||||
categoryPillColors: {
|
categoryPillColors: {
|
||||||
@@ -68,10 +76,19 @@ interface SBConfig {
|
|||||||
skipKeybind: Keybind,
|
skipKeybind: Keybind,
|
||||||
startSponsorKeybind: Keybind,
|
startSponsorKeybind: Keybind,
|
||||||
submitKeybind: Keybind,
|
submitKeybind: Keybind,
|
||||||
|
nextChapterKeybind: Keybind,
|
||||||
|
previousChapterKeybind: Keybind,
|
||||||
|
|
||||||
// What categories should be skipped
|
// What categories should be skipped
|
||||||
categorySelections: CategorySelection[],
|
categorySelections: CategorySelection[],
|
||||||
|
|
||||||
|
payments: {
|
||||||
|
licenseKey: string,
|
||||||
|
lastCheck: number,
|
||||||
|
freeAccess: boolean,
|
||||||
|
chaptersAllowed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// Preview bar
|
// Preview bar
|
||||||
barTypes: {
|
barTypes: {
|
||||||
"preview-chooseACategory": PreviewBarOption,
|
"preview-chooseACategory": PreviewBarOption,
|
||||||
@@ -128,8 +145,10 @@ const Config: SBObject = {
|
|||||||
syncDefaults: {
|
syncDefaults: {
|
||||||
userID: null,
|
userID: null,
|
||||||
isVip: false,
|
isVip: false,
|
||||||
|
permissions: {},
|
||||||
unsubmittedSegments: {},
|
unsubmittedSegments: {},
|
||||||
defaultCategory: "chooseACategory" as Category,
|
defaultCategory: "chooseACategory" as Category,
|
||||||
|
renderSegmentsAsChapters: false,
|
||||||
whitelistedChannels: [],
|
whitelistedChannels: [],
|
||||||
forceChannelCheck: false,
|
forceChannelCheck: false,
|
||||||
minutesSaved: 0,
|
minutesSaved: 0,
|
||||||
@@ -165,6 +184,7 @@ const Config: SBObject = {
|
|||||||
allowExpirements: true,
|
allowExpirements: true,
|
||||||
showDonationLink: true,
|
showDonationLink: true,
|
||||||
showPopupDonationCount: 0,
|
showPopupDonationCount: 0,
|
||||||
|
showUpsells: true,
|
||||||
donateClicked: 0,
|
donateClicked: 0,
|
||||||
autoHideInfoButton: true,
|
autoHideInfoButton: true,
|
||||||
autoSkipOnMusicVideos: false,
|
autoSkipOnMusicVideos: false,
|
||||||
@@ -172,6 +192,7 @@ const Config: SBObject = {
|
|||||||
categoryPillUpdate: false,
|
categoryPillUpdate: false,
|
||||||
darkMode: true,
|
darkMode: true,
|
||||||
showCategoryGuidelines: true,
|
showCategoryGuidelines: true,
|
||||||
|
chaptersAvailable: true,
|
||||||
|
|
||||||
categoryPillColors: {},
|
categoryPillColors: {},
|
||||||
|
|
||||||
@@ -185,6 +206,8 @@ const Config: SBObject = {
|
|||||||
skipKeybind: {key: "Enter"},
|
skipKeybind: {key: "Enter"},
|
||||||
startSponsorKeybind: {key: ";"},
|
startSponsorKeybind: {key: ";"},
|
||||||
submitKeybind: {key: "'"},
|
submitKeybind: {key: "'"},
|
||||||
|
nextChapterKeybind: {key: "]"},
|
||||||
|
previousChapterKeybind: {key: "["},
|
||||||
|
|
||||||
categorySelections: [{
|
categorySelections: [{
|
||||||
name: "sponsor" as Category,
|
name: "sponsor" as Category,
|
||||||
@@ -197,6 +220,13 @@ const Config: SBObject = {
|
|||||||
option: CategorySkipOption.ShowOverlay
|
option: CategorySkipOption.ShowOverlay
|
||||||
}],
|
}],
|
||||||
|
|
||||||
|
payments: {
|
||||||
|
licenseKey: null,
|
||||||
|
lastCheck: 0,
|
||||||
|
freeAccess: false,
|
||||||
|
chaptersAllowed: false
|
||||||
|
},
|
||||||
|
|
||||||
colorPalette: {
|
colorPalette: {
|
||||||
red: "#780303",
|
red: "#780303",
|
||||||
white: "#ffffff",
|
white: "#ffffff",
|
||||||
@@ -516,6 +546,8 @@ function migrateOldSyncFormats(config: SBConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupConfig() {
|
async function setupConfig() {
|
||||||
|
if (typeof(chrome) === "undefined") return;
|
||||||
|
|
||||||
await fetchConfig();
|
await fetchConfig();
|
||||||
addDefaults();
|
addDefaults();
|
||||||
const config = configProxy();
|
const config = configProxy();
|
||||||
|
|||||||
502
src/content.ts
502
src/content.ts
@@ -1,24 +1,44 @@
|
|||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime, HashedValue } from "./types";
|
import {
|
||||||
|
ActionType,
|
||||||
import { ContentContainer, Keybind } from "./types";
|
Category,
|
||||||
|
CategorySkipOption,
|
||||||
|
ChannelIDInfo,
|
||||||
|
ChannelIDStatus,
|
||||||
|
ContentContainer,
|
||||||
|
HashedValue,
|
||||||
|
Keybind,
|
||||||
|
ScheduledTime,
|
||||||
|
SegmentUUID,
|
||||||
|
SkipToTimeParams,
|
||||||
|
SponsorHideType,
|
||||||
|
SponsorSourceType,
|
||||||
|
SponsorTime,
|
||||||
|
StorageChangesObject,
|
||||||
|
ToggleSkippable,
|
||||||
|
VideoID,
|
||||||
|
VideoInfo,
|
||||||
|
PageType
|
||||||
|
} from "./types";
|
||||||
import Utils from "./utils";
|
import Utils from "./utils";
|
||||||
const utils = new Utils();
|
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
|
||||||
|
|
||||||
import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
|
|
||||||
import SkipNotice from "./render/SkipNotice";
|
import SkipNotice from "./render/SkipNotice";
|
||||||
import SkipNoticeComponent from "./components/SkipNoticeComponent";
|
import SkipNoticeComponent from "./components/SkipNoticeComponent";
|
||||||
import SubmissionNotice from "./render/SubmissionNotice";
|
import SubmissionNotice from "./render/SubmissionNotice";
|
||||||
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
|
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
|
||||||
import * as Chat from "./js-components/chat";
|
|
||||||
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
||||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||||
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
|
import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils";
|
||||||
import { isSafari, keybindEquals } from "./utils/configUtils";
|
import { isSafari, keybindEquals } from "./utils/configUtils";
|
||||||
import { CategoryPill } from "./render/CategoryPill";
|
import { CategoryPill } from "./render/CategoryPill";
|
||||||
import { AnimationUtils } from "./utils/animationUtils";
|
import { AnimationUtils } from "./utils/animationUtils";
|
||||||
import { GenericUtils } from "./utils/genericUtils";
|
import { GenericUtils } from "./utils/genericUtils";
|
||||||
import { logDebug } from "./utils/logger";
|
import { logDebug } from "./utils/logger";
|
||||||
|
import { importTimes } from "./utils/exporter";
|
||||||
|
import { ChapterVote } from "./render/ChapterVote";
|
||||||
|
import { openWarningDialog } from "./utils/warnings";
|
||||||
|
|
||||||
|
const utils = new Utils();
|
||||||
|
|
||||||
// Hack to get the CSS loaded on permission-based sites (Invidious)
|
// Hack to get the CSS loaded on permission-based sites (Invidious)
|
||||||
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
|
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
|
||||||
@@ -26,7 +46,8 @@ utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
|
|||||||
//was sponsor data found when doing SponsorsLookup
|
//was sponsor data found when doing SponsorsLookup
|
||||||
let sponsorDataFound = false;
|
let sponsorDataFound = false;
|
||||||
//the actual sponsorTimes if loaded and UUIDs associated with them
|
//the actual sponsorTimes if loaded and UUIDs associated with them
|
||||||
let sponsorTimes: SponsorTime[] = null;
|
let sponsorTimes: SponsorTime[] = [];
|
||||||
|
let existingChaptersImported = false;
|
||||||
//what video id are these sponsors for
|
//what video id are these sponsors for
|
||||||
let sponsorVideoID: VideoID = null;
|
let sponsorVideoID: VideoID = null;
|
||||||
// List of open skip notices
|
// List of open skip notices
|
||||||
@@ -35,6 +56,10 @@ let activeSkipKeybindElement: ToggleSkippable = null;
|
|||||||
|
|
||||||
// JSON video info
|
// JSON video info
|
||||||
let videoInfo: VideoInfo = null;
|
let videoInfo: VideoInfo = null;
|
||||||
|
// Page Type - browse/watch etc...
|
||||||
|
let pageType: PageType;
|
||||||
|
// if video is live or premiere
|
||||||
|
let isLivePremiere: boolean
|
||||||
// The channel this video is about
|
// The channel this video is about
|
||||||
let channelIDInfo: ChannelIDInfo;
|
let channelIDInfo: ChannelIDInfo;
|
||||||
// Locked Categories in this tab, like: ["sponsor","intro","outro"]
|
// Locked Categories in this tab, like: ["sponsor","intro","outro"]
|
||||||
@@ -60,6 +85,7 @@ let sponsorSkipped: boolean[] = [];
|
|||||||
let video: HTMLVideoElement;
|
let video: HTMLVideoElement;
|
||||||
let videoMuted = false; // Has it been attempted to be muted
|
let videoMuted = false; // Has it been attempted to be muted
|
||||||
let videoMutationObserver: MutationObserver = null;
|
let videoMutationObserver: MutationObserver = null;
|
||||||
|
let waitingForNewVideo = false;
|
||||||
// List of videos that have had event listeners added to them
|
// List of videos that have had event listeners added to them
|
||||||
const videosWithEventListeners: HTMLVideoElement[] = [];
|
const videosWithEventListeners: HTMLVideoElement[] = [];
|
||||||
const controlsWithEventListeners: HTMLElement[] = []
|
const controlsWithEventListeners: HTMLElement[] = []
|
||||||
@@ -69,7 +95,7 @@ let onInvidious: boolean;
|
|||||||
let onMobileYouTube: boolean;
|
let onMobileYouTube: boolean;
|
||||||
|
|
||||||
//the video id of the last preview bar update
|
//the video id of the last preview bar update
|
||||||
let lastPreviewBarUpdate;
|
let lastPreviewBarUpdate: VideoID;
|
||||||
|
|
||||||
// Is the video currently being switched
|
// Is the video currently being switched
|
||||||
let switchingVideos = null;
|
let switchingVideos = null;
|
||||||
@@ -97,10 +123,8 @@ const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImage
|
|||||||
// Direct Links after the config is loaded
|
// Direct Links after the config is loaded
|
||||||
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
|
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
|
||||||
// wait for hover preview to appear, and refresh attachments if ever found
|
// wait for hover preview to appear, and refresh attachments if ever found
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments())
|
||||||
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("[data-sessionlink='feature=player-title']").then(() => videoIDChange(getYouTubeVideoID(document)))
|
|
||||||
});
|
|
||||||
addPageListeners();
|
addPageListeners();
|
||||||
addHotkeyListener();
|
addHotkeyListener();
|
||||||
|
|
||||||
@@ -139,7 +163,8 @@ const skipNoticeContentContainer: ContentContainer = () => ({
|
|||||||
previewTime,
|
previewTime,
|
||||||
videoInfo,
|
videoInfo,
|
||||||
getRealCurrentTime: getRealCurrentTime,
|
getRealCurrentTime: getRealCurrentTime,
|
||||||
lockedCategories
|
lockedCategories,
|
||||||
|
channelIDInfo
|
||||||
});
|
});
|
||||||
|
|
||||||
// value determining when to count segment as skipped and send telemetry to server (percent based)
|
// value determining when to count segment as skipped and send telemetry to server (percent based)
|
||||||
@@ -168,6 +193,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
found: sponsorDataFound,
|
found: sponsorDataFound,
|
||||||
status: lastResponseStatus,
|
status: lastResponseStatus,
|
||||||
sponsorTimes: sponsorTimes,
|
sponsorTimes: sponsorTimes,
|
||||||
|
time: video.currentTime,
|
||||||
onMobileYouTube
|
onMobileYouTube
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,10 +239,17 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
found: sponsorDataFound,
|
found: sponsorDataFound,
|
||||||
status: lastResponseStatus,
|
status: lastResponseStatus,
|
||||||
sponsorTimes: sponsorTimes,
|
sponsorTimes: sponsorTimes,
|
||||||
|
time: video.currentTime,
|
||||||
onMobileYouTube
|
onMobileYouTube
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
case "unskip":
|
||||||
|
unskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), null, true);
|
||||||
|
break;
|
||||||
|
case "reskip":
|
||||||
|
reskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), true);
|
||||||
|
break;
|
||||||
case "submitVote":
|
case "submitVote":
|
||||||
vote(request.type, request.UUID).then((response) => sendResponse(response));
|
vote(request.type, request.UUID).then((response) => sendResponse(response));
|
||||||
return true;
|
return true;
|
||||||
@@ -231,6 +264,32 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
case "copyToClipboard":
|
case "copyToClipboard":
|
||||||
navigator.clipboard.writeText(request.text);
|
navigator.clipboard.writeText(request.text);
|
||||||
break;
|
break;
|
||||||
|
case "importSegments": {
|
||||||
|
const importedSegments = importTimes(request.data, video.duration);
|
||||||
|
let addedSegments = false;
|
||||||
|
for (const segment of importedSegments) {
|
||||||
|
if (!sponsorTimesSubmitting.concat(sponsorTimes ?? []).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"))) {
|
||||||
|
sponsorTimesSubmitting.push(segment);
|
||||||
|
addedSegments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedSegments) {
|
||||||
|
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
|
||||||
|
Config.forceSyncUpdate("unsubmittedSegments");
|
||||||
|
|
||||||
|
updateEditButtonsOnPlayer();
|
||||||
|
updateSponsorTimesSubmitting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse({
|
||||||
|
importedSegments
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "keydown":
|
case "keydown":
|
||||||
document.dispatchEvent(new KeyboardEvent('keydown', {
|
document.dispatchEvent(new KeyboardEvent('keydown', {
|
||||||
key: request.key,
|
key: request.key,
|
||||||
@@ -250,8 +309,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the config is updated
|
* Called when the config is updated
|
||||||
*
|
|
||||||
* @param {String} changes
|
|
||||||
*/
|
*/
|
||||||
function contentConfigUpdateListener(changes: StorageChangesObject) {
|
function contentConfigUpdateListener(changes: StorageChangesObject) {
|
||||||
for (const key in changes) {
|
for (const key in changes) {
|
||||||
@@ -277,17 +334,19 @@ function resetValues() {
|
|||||||
lastCheckVideoTime = -1;
|
lastCheckVideoTime = -1;
|
||||||
retryCount = 0;
|
retryCount = 0;
|
||||||
|
|
||||||
//reset sponsor times
|
sponsorTimes = [];
|
||||||
sponsorTimes = null;
|
existingChaptersImported = false;
|
||||||
sponsorSkipped = [];
|
sponsorSkipped = [];
|
||||||
|
|
||||||
videoInfo = null;
|
videoInfo = null;
|
||||||
|
pageType = null;
|
||||||
channelWhitelisted = false;
|
channelWhitelisted = false;
|
||||||
channelIDInfo = {
|
channelIDInfo = {
|
||||||
status: ChannelIDStatus.Fetching,
|
status: ChannelIDStatus.Fetching,
|
||||||
id: null
|
id: null
|
||||||
};
|
};
|
||||||
lockedCategories = [];
|
lockedCategories = [];
|
||||||
|
isLivePremiere = false;
|
||||||
|
|
||||||
//empty the preview bar
|
//empty the preview bar
|
||||||
if (previewBar !== null) {
|
if (previewBar !== null) {
|
||||||
@@ -316,7 +375,7 @@ function resetValues() {
|
|||||||
categoryPill?.setVisibility(false);
|
categoryPill?.setVisibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function videoIDChange(id) {
|
async function videoIDChange(id): Promise<void> {
|
||||||
//if the id has not changed return unless the video element has changed
|
//if the id has not changed return unless the video element has changed
|
||||||
if (sponsorVideoID === id && (isVisible(video) || !video)) return;
|
if (sponsorVideoID === id && (isVisible(video) || !video)) return;
|
||||||
|
|
||||||
@@ -424,7 +483,7 @@ function createPreviewBar(): void {
|
|||||||
isVisibleCheck: true
|
isVisibleCheck: true
|
||||||
}, {
|
}, {
|
||||||
// For Desktop YouTube
|
// For Desktop YouTube
|
||||||
selector: ".ytp-progress-bar-container",
|
selector: ".ytp-progress-bar",
|
||||||
isVisibleCheck: true
|
isVisibleCheck: true
|
||||||
}, {
|
}, {
|
||||||
// For Desktop YouTube
|
// For Desktop YouTube
|
||||||
@@ -442,7 +501,8 @@ function createPreviewBar(): void {
|
|||||||
const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0];
|
const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0];
|
||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
|
const chapterVote = new ChapterVote(voteAsync);
|
||||||
|
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote);
|
||||||
|
|
||||||
updatePreviewBar();
|
updatePreviewBar();
|
||||||
|
|
||||||
@@ -500,15 +560,33 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
|
// ensure we are on the correct video
|
||||||
|
const newVideoID = getYouTubeVideoID(document);
|
||||||
|
if (newVideoID !== sponsorVideoID) {
|
||||||
|
videoIDChange(newVideoID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!video || video.paused) return;
|
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
|
||||||
|
if (!video) return;
|
||||||
if (currentTime === undefined || currentTime === null) {
|
if (currentTime === undefined || currentTime === null) {
|
||||||
currentTime = getVirtualTime();
|
currentTime = getVirtualTime();
|
||||||
}
|
}
|
||||||
lastTimeFromWaitingEvent = null;
|
lastTimeFromWaitingEvent = null;
|
||||||
|
|
||||||
if (videoMuted && !inMuteSegment(currentTime)) {
|
updateActiveSegment(currentTime);
|
||||||
|
|
||||||
|
if (video.paused) return;
|
||||||
|
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||||
|
|
||||||
|
const currentSkip = skipInfo.array[skipInfo.index];
|
||||||
|
const skipTime: number[] = [currentSkip?.scheduledTime, skipInfo.array[skipInfo.endIndex]?.segment[1]];
|
||||||
|
const timeUntilSponsor = skipTime?.[0] - currentTime;
|
||||||
|
const videoID = sponsorVideoID;
|
||||||
|
const skipBuffer = 0.003;
|
||||||
|
|
||||||
|
if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1
|
||||||
|
&& timeUntilSponsor < skipBuffer && shouldAutoSkip(currentSkip))) {
|
||||||
video.muted = false;
|
video.muted = false;
|
||||||
videoMuted = false;
|
videoMuted = false;
|
||||||
|
|
||||||
@@ -518,22 +596,15 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`);
|
||||||
|
if (skipInfo.index === -1) return;
|
||||||
|
|
||||||
if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){
|
if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incorrectVideoCheck()) return;
|
if (incorrectVideoCheck()) return;
|
||||||
|
|
||||||
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
|
|
||||||
|
|
||||||
logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`);
|
|
||||||
if (skipInfo.index === -1) return;
|
|
||||||
|
|
||||||
const currentSkip = skipInfo.array[skipInfo.index];
|
|
||||||
const skipTime: number[] = [currentSkip.scheduledTime, skipInfo.array[skipInfo.endIndex].segment[1]];
|
|
||||||
const timeUntilSponsor = skipTime[0] - currentTime;
|
|
||||||
const videoID = sponsorVideoID;
|
|
||||||
|
|
||||||
// Find all indexes in between the start and end
|
// Find all indexes in between the start and end
|
||||||
let skippingSegments = [skipInfo.array[skipInfo.index]];
|
let skippingSegments = [skipInfo.array[skipInfo.index]];
|
||||||
if (skipInfo.index !== skipInfo.endIndex) {
|
if (skipInfo.index !== skipInfo.endIndex) {
|
||||||
@@ -552,7 +623,6 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
|||||||
// Don't skip if this category should not be skipped
|
// Don't skip if this category should not be skipped
|
||||||
if (!shouldSkip(currentSkip) && !sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)) return;
|
if (!shouldSkip(currentSkip) && !sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)) return;
|
||||||
|
|
||||||
const skipBuffer = 0.003;
|
|
||||||
const skippingFunction = (forceVideoTime?: number) => {
|
const skippingFunction = (forceVideoTime?: number) => {
|
||||||
let forcedSkipTime: number = null;
|
let forcedSkipTime: number = null;
|
||||||
let forcedIncludeIntersectingSegments = false;
|
let forcedIncludeIntersectingSegments = false;
|
||||||
@@ -561,35 +631,41 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
|
|||||||
if (incorrectVideoCheck(videoID, currentSkip)) return;
|
if (incorrectVideoCheck(videoID, currentSkip)) return;
|
||||||
forceVideoTime ||= Math.max(video.currentTime, getVirtualTime());
|
forceVideoTime ||= Math.max(video.currentTime, getVirtualTime());
|
||||||
|
|
||||||
if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) {
|
if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))) {
|
||||||
skipToTime({
|
if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) {
|
||||||
v: video,
|
skipToTime({
|
||||||
skipTime,
|
v: video,
|
||||||
skippingSegments,
|
skipTime,
|
||||||
openNotice: skipInfo.openNotice
|
skippingSegments,
|
||||||
});
|
openNotice: skipInfo.openNotice
|
||||||
|
});
|
||||||
|
|
||||||
// These are segments that start at the exact same time but need seperate notices
|
// These are segments that start at the exact same time but need seperate notices
|
||||||
for (const extra of skipInfo.extraIndexes) {
|
for (const extra of skipInfo.extraIndexes) {
|
||||||
const extraSkip = skipInfo.array[extra];
|
const extraSkip = skipInfo.array[extra];
|
||||||
if (shouldSkip(extraSkip)) {
|
if (shouldSkip(extraSkip)) {
|
||||||
skipToTime({
|
skipToTime({
|
||||||
v: video,
|
v: video,
|
||||||
skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]],
|
skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]],
|
||||||
skippingSegments: [extraSkip],
|
skippingSegments: [extraSkip],
|
||||||
openNotice: skipInfo.openNotice
|
openNotice: skipInfo.openNotice
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|
if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
|
||||||
|| currentSkip.actionType === ActionType.Mute) {
|
|| currentSkip.actionType === ActionType.Mute) {
|
||||||
forcedSkipTime = skipTime[0] + 0.001;
|
forcedSkipTime = skipTime[0] + 0.001;
|
||||||
|
} else {
|
||||||
|
forcedSkipTime = skipTime[1];
|
||||||
|
forcedIncludeIntersectingSegments = true;
|
||||||
|
forcedIncludeNonIntersectingSegments = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
forcedSkipTime = skipTime[1];
|
forcedSkipTime = forceVideoTime + 0.001;
|
||||||
forcedIncludeIntersectingSegments = true;
|
|
||||||
forcedIncludeNonIntersectingSegments = false;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
forcedSkipTime = forceVideoTime + 0.001;
|
||||||
}
|
}
|
||||||
|
|
||||||
startSponsorSchedule(forcedIncludeIntersectingSegments, forcedSkipTime, forcedIncludeNonIntersectingSegments);
|
startSponsorSchedule(forcedIncludeIntersectingSegments, forcedSkipTime, forcedIncludeNonIntersectingSegments);
|
||||||
@@ -632,15 +708,17 @@ function getVirtualTime(): number {
|
|||||||
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
|
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
|
||||||
|
|
||||||
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
|
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
|
||||||
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6) {
|
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) {
|
||||||
return virtualTime;
|
return virtualTime;
|
||||||
} else {
|
} else {
|
||||||
return video.currentTime;
|
return video.currentTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inMuteSegment(currentTime: number): boolean {
|
function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean {
|
||||||
const checkFunction = (segment) => segment.actionType === ActionType.Mute && segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
|
const checkFunction = (segment) => segment.actionType === ActionType.Mute
|
||||||
|
&& segment.segment[0] <= currentTime
|
||||||
|
&& (segment.segment[1] > currentTime || (includeOverlap && segment.segment[1] + 0.02 > currentTime));
|
||||||
return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction);
|
return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,27 +756,30 @@ function setupVideoMutationListener() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshVideoAttachments() {
|
async function refreshVideoAttachments(): Promise<void> {
|
||||||
const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement;
|
if (waitingForNewVideo) return;
|
||||||
if (newVideo && newVideo !== video) {
|
|
||||||
video = newVideo;
|
|
||||||
|
|
||||||
if (!videosWithEventListeners.includes(video)) {
|
waitingForNewVideo = true;
|
||||||
videosWithEventListeners.push(video);
|
const newVideo = await utils.waitForElement("video", true) as HTMLVideoElement;
|
||||||
|
waitingForNewVideo = false;
|
||||||
|
|
||||||
setupVideoListeners();
|
video = newVideo;
|
||||||
setupSkipButtonControlBar();
|
if (!videosWithEventListeners.includes(video)) {
|
||||||
setupCategoryPill();
|
videosWithEventListeners.push(video);
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new bar in the new video element
|
setupVideoListeners();
|
||||||
if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
|
setupSkipButtonControlBar();
|
||||||
previewBar.remove();
|
setupCategoryPill();
|
||||||
previewBar = null;
|
|
||||||
|
|
||||||
createPreviewBar();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
|
||||||
|
previewBar.remove();
|
||||||
|
previewBar = null;
|
||||||
|
|
||||||
|
createPreviewBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
videoIDChange(getYouTubeVideoID(document));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupVideoListeners() {
|
function setupVideoListeners() {
|
||||||
@@ -710,6 +791,7 @@ function setupVideoListeners() {
|
|||||||
switchingVideos = false;
|
switchingVideos = false;
|
||||||
|
|
||||||
let startedWaiting = false;
|
let startedWaiting = false;
|
||||||
|
let lastPausedAtZero = true;
|
||||||
|
|
||||||
video.addEventListener('play', () => {
|
video.addEventListener('play', () => {
|
||||||
// If it is not the first event, then the only way to get to 0 is if there is a seek event
|
// If it is not the first event, then the only way to get to 0 is if there is a seek event
|
||||||
@@ -720,7 +802,7 @@ function setupVideoListeners() {
|
|||||||
|
|
||||||
updateVirtualTime();
|
updateVirtualTime();
|
||||||
|
|
||||||
if (switchingVideos) {
|
if (switchingVideos || lastPausedAtZero) {
|
||||||
switchingVideos = false;
|
switchingVideos = false;
|
||||||
logDebug("Setting switching videos to false");
|
logDebug("Setting switching videos to false");
|
||||||
|
|
||||||
@@ -728,6 +810,8 @@ function setupVideoListeners() {
|
|||||||
if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
|
if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastPausedAtZero = false;
|
||||||
|
|
||||||
// Check if an ad is playing
|
// Check if an ad is playing
|
||||||
updateAdFlag();
|
updateAdFlag();
|
||||||
|
|
||||||
@@ -743,6 +827,7 @@ function setupVideoListeners() {
|
|||||||
});
|
});
|
||||||
video.addEventListener('playing', () => {
|
video.addEventListener('playing', () => {
|
||||||
updateVirtualTime();
|
updateVirtualTime();
|
||||||
|
lastPausedAtZero = false;
|
||||||
|
|
||||||
if (startedWaiting) {
|
if (startedWaiting) {
|
||||||
startedWaiting = false;
|
startedWaiting = false;
|
||||||
@@ -750,6 +835,14 @@ function setupVideoListeners() {
|
|||||||
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)}`);
|
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (switchingVideos) {
|
||||||
|
switchingVideos = false;
|
||||||
|
logDebug("Setting switching videos to false");
|
||||||
|
|
||||||
|
// If already segments loaded before video, retry to skip starting segments
|
||||||
|
if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure it doesn't get double called with the play event
|
// Make sure it doesn't get double called with the play event
|
||||||
if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3
|
if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3
|
||||||
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) {
|
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) {
|
||||||
@@ -769,6 +862,12 @@ function setupVideoListeners() {
|
|||||||
lastTimeFromWaitingEvent = null;
|
lastTimeFromWaitingEvent = null;
|
||||||
|
|
||||||
startSponsorSchedule();
|
startSponsorSchedule();
|
||||||
|
} else {
|
||||||
|
updateActiveSegment(video.currentTime);
|
||||||
|
|
||||||
|
if (video.currentTime === 0) {
|
||||||
|
lastPausedAtZero = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
video.addEventListener('ratechange', () => startSponsorSchedule());
|
video.addEventListener('ratechange', () => startSponsorSchedule());
|
||||||
@@ -862,7 +961,12 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
|||||||
if (response?.ok) {
|
if (response?.ok) {
|
||||||
const recievedSegments: SponsorTime[] = JSON.parse(response.responseText)
|
const recievedSegments: SponsorTime[] = JSON.parse(response.responseText)
|
||||||
?.filter((video) => video.videoID === sponsorVideoID)
|
?.filter((video) => video.videoID === sponsorVideoID)
|
||||||
?.map((video) => video.segments)[0];
|
?.map((video) => video.segments)?.[0]
|
||||||
|
?.map((segment) => ({
|
||||||
|
...segment,
|
||||||
|
source: SponsorSourceType.Server
|
||||||
|
}))
|
||||||
|
?.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||||
if (!recievedSegments || !recievedSegments.length) {
|
if (!recievedSegments || !recievedSegments.length) {
|
||||||
// return if no video found
|
// return if no video found
|
||||||
retryFetch(404);
|
retryFetch(404);
|
||||||
@@ -883,6 +987,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
|||||||
|
|
||||||
const oldSegments = sponsorTimes || [];
|
const oldSegments = sponsorTimes || [];
|
||||||
sponsorTimes = recievedSegments;
|
sponsorTimes = recievedSegments;
|
||||||
|
existingChaptersImported = false;
|
||||||
|
|
||||||
// Hide all submissions smaller than the minimum duration
|
// Hide all submissions smaller than the minimum duration
|
||||||
if (Config.config.minDuration !== 0) {
|
if (Config.config.minDuration !== 0) {
|
||||||
@@ -930,13 +1035,28 @@ async function sponsorsLookup(keepOldSubmissions = true) {
|
|||||||
retryFetch(lastResponseStatus);
|
retryFetch(lastResponseStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importExistingChapters(true);
|
||||||
|
|
||||||
if (Config.config.isVip) {
|
if (Config.config.isVip) {
|
||||||
lockedCategoriesLookup();
|
lockedCategoriesLookup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function importExistingChapters(wait: boolean) {
|
||||||
|
if (!existingChaptersImported) {
|
||||||
|
GenericUtils.wait(() => video && getExistingChapters(sponsorVideoID, video.duration),
|
||||||
|
wait ? 5000 : 0, 100, (c) => c?.length > 0).then((chapters) => {
|
||||||
|
if (!existingChaptersImported && chapters?.length > 0) {
|
||||||
|
sponsorTimes = (sponsorTimes ?? []).concat(...chapters).sort((a, b) => a.segment[0] - b.segment[0]);
|
||||||
|
existingChaptersImported = true;
|
||||||
|
updatePreviewBar();
|
||||||
|
}
|
||||||
|
}).catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getEnabledActionTypes(): ActionType[] {
|
function getEnabledActionTypes(): ActionType[] {
|
||||||
const actionTypes = [ActionType.Skip, ActionType.Poi];
|
const actionTypes = [ActionType.Skip, ActionType.Poi, ActionType.Chapter];
|
||||||
if (Config.config.muteSegments) {
|
if (Config.config.muteSegments) {
|
||||||
actionTypes.push(ActionType.Mute);
|
actionTypes.push(ActionType.Mute);
|
||||||
}
|
}
|
||||||
@@ -974,7 +1094,8 @@ function retryFetch(errorCode: number): void {
|
|||||||
|
|
||||||
const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
|
const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (sponsorVideoID && sponsorTimes?.length === 0) {
|
if (sponsorVideoID && sponsorTimes?.length === 0
|
||||||
|
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
|
||||||
sponsorsLookup();
|
sponsorsLookup();
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
@@ -1041,28 +1162,33 @@ function startSkipScheduleCheckingForStartSponsors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getYouTubeVideoID(document: Document): string | boolean {
|
function getYouTubeVideoID(document: Document, url?: string): string | boolean {
|
||||||
const url = document.URL;
|
url ||= document.URL;
|
||||||
|
// pageType shortcut
|
||||||
|
if (pageType === PageType.Channel) return getYouTubeVideoIDFromDocument()
|
||||||
// clips should never skip, going from clip to full video has no indications.
|
// 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 false;
|
||||||
// skip to document and don't hide if on /embed/
|
// skip to document and don't hide if on /embed/
|
||||||
if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(document, false);
|
if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false);
|
||||||
// skip to URL if matches youtube watch or invidious or matches youtube pattern
|
// 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);
|
if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url);
|
||||||
// skip to document if matches pattern
|
// skip to document if matches pattern
|
||||||
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document);
|
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(true, PageType.Channel);
|
||||||
// not sure, try URL then document
|
// not sure, try URL then document
|
||||||
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document, false);
|
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
|
function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watch): string | boolean {
|
||||||
// get ID from document (channel trailer / embedded playlist)
|
// get ID from document (channel trailer / embedded playlist)
|
||||||
const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href");
|
const element = video?.parentElement?.parentElement?.querySelector("a.ytp-title-link[data-sessionlink='feature=player-title']");
|
||||||
|
const videoURL = element?.getAttribute("href");
|
||||||
if (videoURL) {
|
if (videoURL) {
|
||||||
onInvidious = hideIcon;
|
onInvidious = hideIcon;
|
||||||
|
// if href found, hint was correct
|
||||||
|
pageType = pageHint;
|
||||||
return getYouTubeVideoIDFromURL(videoURL);
|
return getYouTubeVideoIDFromURL(videoURL);
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,6 +1255,7 @@ function updatePreviewBar(): void {
|
|||||||
|
|
||||||
if (video === null) return;
|
if (video === null) return;
|
||||||
|
|
||||||
|
const hashParams = getHashParams();
|
||||||
const previewBarSegments: PreviewBarSegment[] = [];
|
const previewBarSegments: PreviewBarSegment[] = [];
|
||||||
if (sponsorTimes) {
|
if (sponsorTimes) {
|
||||||
sponsorTimes.forEach((segment) => {
|
sponsorTimes.forEach((segment) => {
|
||||||
@@ -1137,9 +1264,12 @@ function updatePreviewBar(): void {
|
|||||||
previewBarSegments.push({
|
previewBarSegments.push({
|
||||||
segment: segment.segment as [number, number],
|
segment: segment.segment as [number, number],
|
||||||
category: segment.category,
|
category: segment.category,
|
||||||
unsubmitted: false,
|
|
||||||
actionType: segment.actionType,
|
actionType: segment.actionType,
|
||||||
showLarger: segment.actionType === ActionType.Poi
|
unsubmitted: false,
|
||||||
|
showLarger: segment.actionType === ActionType.Poi,
|
||||||
|
description: segment.description,
|
||||||
|
source: segment.source,
|
||||||
|
requiredSegment: hashParams.requiredSegment && segment.UUID === hashParams.requiredSegment
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1148,16 +1278,21 @@ function updatePreviewBar(): void {
|
|||||||
previewBarSegments.push({
|
previewBarSegments.push({
|
||||||
segment: segment.segment as [number, number],
|
segment: segment.segment as [number, number],
|
||||||
category: segment.category,
|
category: segment.category,
|
||||||
unsubmitted: true,
|
|
||||||
actionType: segment.actionType,
|
actionType: segment.actionType,
|
||||||
showLarger: segment.actionType === ActionType.Poi
|
unsubmitted: true,
|
||||||
|
showLarger: segment.actionType === ActionType.Poi,
|
||||||
|
description: segment.description,
|
||||||
|
source: segment.source
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
|
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
|
||||||
|
updateActiveSegment(video.currentTime);
|
||||||
|
|
||||||
if (Config.config.showTimeWithSkips) {
|
if (Config.config.showTimeWithSkips) {
|
||||||
const skippedDuration = utils.getTimestampsDuration(previewBarSegments.map(({segment}) => segment));
|
const skippedDuration = utils.getTimestampsDuration(previewBarSegments
|
||||||
|
.filter(({actionType}) => actionType !== ActionType.Chapter)
|
||||||
|
.map(({segment}) => segment));
|
||||||
|
|
||||||
showTimeWithoutSkips(skippedDuration);
|
showTimeWithoutSkips(skippedDuration);
|
||||||
}
|
}
|
||||||
@@ -1170,25 +1305,29 @@ function updatePreviewBar(): void {
|
|||||||
async function whitelistCheck() {
|
async function whitelistCheck() {
|
||||||
const whitelistedChannels = Config.config.whitelistedChannels;
|
const whitelistedChannels = Config.config.whitelistedChannels;
|
||||||
|
|
||||||
const getChannelID = () =>
|
|
||||||
(document.querySelector("a.ytd-video-owner-renderer") // YouTube
|
|
||||||
?? document.querySelector("a.ytp-title-channel-logo") // YouTube Embed
|
|
||||||
?? document.querySelector(".channel-profile #channel-name")?.parentElement.parentElement // Invidious
|
|
||||||
?? document.querySelector("a.slim-owner-icon-and-title")) // Mobile YouTube
|
|
||||||
?.getAttribute("href")?.match(/\/(?:channel|c|user)\/(UC[a-zA-Z0-9_-]{22}|[a-zA-Z0-9_-]+)/)?.[1];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await utils.wait(() => !!getChannelID(), 6000, 20);
|
await utils.wait(() => channelIDInfo.status === ChannelIDStatus.Found, 6000, 20);
|
||||||
|
|
||||||
channelIDInfo = {
|
// If found, continue on, it was set by the listener
|
||||||
status: ChannelIDStatus.Found,
|
|
||||||
id: getChannelID().match(/^\/?([^\s/]+)/)[0]
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
channelIDInfo = {
|
// Try fallback
|
||||||
status: ChannelIDStatus.Failed,
|
const channelIDFallback = (document.querySelector("a.ytd-video-owner-renderer") // YouTube
|
||||||
id: null
|
?? document.querySelector("a.ytp-title-channel-logo") // YouTube Embed
|
||||||
};
|
?? document.querySelector(".channel-profile #channel-name")?.parentElement.parentElement // Invidious
|
||||||
|
?? document.querySelector("a.slim-owner-icon-and-title")) // Mobile YouTube
|
||||||
|
?.getAttribute("href")?.match(/\/(?:channel|c|user)\/(UC[a-zA-Z0-9_-]{22}|[a-zA-Z0-9_-]+)/)?.[1];
|
||||||
|
|
||||||
|
if (channelIDFallback) {
|
||||||
|
channelIDInfo = {
|
||||||
|
status: ChannelIDStatus.Found,
|
||||||
|
id: channelIDFallback
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
channelIDInfo = {
|
||||||
|
status: ChannelIDStatus.Failed,
|
||||||
|
id: null
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//see if this is a whitelisted channel
|
//see if this is a whitelisted channel
|
||||||
@@ -1209,7 +1348,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
|
|||||||
|
|
||||||
const autoSkipSorter = (segment: ScheduledTime) => {
|
const autoSkipSorter = (segment: ScheduledTime) => {
|
||||||
const skipOption = utils.getCategorySelection(segment.category)?.option;
|
const skipOption = utils.getCategorySelection(segment.category)?.option;
|
||||||
if (skipOption === CategorySkipOption.AutoSkip
|
if ((skipOption === CategorySkipOption.AutoSkip || shouldAutoSkip(segment))
|
||||||
&& segment.actionType === ActionType.Skip) {
|
&& segment.actionType === ActionType.Skip) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (skipOption !== CategorySkipOption.ShowOverlay) {
|
} else if (skipOption !== CategorySkipOption.ShowOverlay) {
|
||||||
@@ -1221,7 +1360,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
|
|||||||
|
|
||||||
const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } =
|
const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } =
|
||||||
getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments);
|
getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||||
const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true);
|
const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true);
|
||||||
|
|
||||||
// This is an array in-case multiple segments have the exact same start time
|
// This is an array in-case multiple segments have the exact same start time
|
||||||
const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime));
|
const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime));
|
||||||
@@ -1230,13 +1369,13 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
|
|||||||
(a, b) => ((autoSkipSorter(submittedArray[a]) - autoSkipSorter(submittedArray[b]))
|
(a, b) => ((autoSkipSorter(submittedArray[a]) - autoSkipSorter(submittedArray[b]))
|
||||||
|| (submittedArray[a].segment[1] - submittedArray[a].segment[0]) - (submittedArray[b].segment[1] - submittedArray[b].segment[0])))[0] ?? -1;
|
|| (submittedArray[a].segment[1] - submittedArray[a].segment[0]) - (submittedArray[b].segment[1] - submittedArray[b].segment[0])))[0] ?? -1;
|
||||||
// Store extra indexes for the non-auto skipping segments if others occur at the exact same start time
|
// Store extra indexes for the non-auto skipping segments if others occur at the exact same start time
|
||||||
const extraIndexes = minSponsorTimeIndexes.filter((i) => i === minSponsorTimeIndex || autoSkipSorter(submittedArray[i]) !== 0);
|
const extraIndexes = minSponsorTimeIndexes.filter((i) => i !== minSponsorTimeIndex && autoSkipSorter(submittedArray[i]) !== 0);
|
||||||
|
|
||||||
const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex);
|
const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex);
|
||||||
|
|
||||||
const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } =
|
const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } =
|
||||||
getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments);
|
getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments);
|
||||||
const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false);
|
const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false);
|
||||||
|
|
||||||
const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime));
|
const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime));
|
||||||
const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex);
|
const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex);
|
||||||
@@ -1317,7 +1456,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
|
|||||||
* the current time, but end after
|
* the current time, but end after
|
||||||
*/
|
*/
|
||||||
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean,
|
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean,
|
||||||
minimum?: number, onlySkippableSponsors = false, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
|
minimum?: number, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
|
||||||
if (!sponsorTimes) return {includedTimes: [], scheduledTimes: []};
|
if (!sponsorTimes) return {includedTimes: [], scheduledTimes: []};
|
||||||
|
|
||||||
const includedTimes: ScheduledTime[] = [];
|
const includedTimes: ScheduledTime[] = [];
|
||||||
@@ -1328,9 +1467,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
|
|||||||
scheduledTime: sponsorTime.segment[0]
|
scheduledTime: sponsorTime.segment[0]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Schedule at the end time to know when to unmute
|
// Schedule at the end time to know when to unmute and remove title from seek bar
|
||||||
sponsorTimes.filter(sponsorTime => sponsorTime.actionType === ActionType.Mute)
|
sponsorTimes.forEach(sponsorTime => {
|
||||||
.forEach(sponsorTime => {
|
|
||||||
if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime)) {
|
if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime)) {
|
||||||
possibleTimes.push({
|
possibleTimes.push({
|
||||||
...sponsorTime,
|
...sponsorTime,
|
||||||
@@ -1343,8 +1481,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
|
|||||||
if ((minimum === undefined
|
if ((minimum === undefined
|
||||||
|| ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum)
|
|| ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum)
|
||||||
|| (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
|
|| (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
|
||||||
&& (!onlySkippableSponsors || shouldSkip(possibleTimes[i]))
|
|
||||||
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
|
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
|
||||||
|
&& possibleTimes[i].segment.length === 2
|
||||||
&& possibleTimes[i].actionType !== ActionType.Poi) {
|
&& possibleTimes[i].actionType !== ActionType.Poi) {
|
||||||
|
|
||||||
scheduledTimes.push(possibleTimes[i].scheduledTime);
|
scheduledTimes.push(possibleTimes[i].scheduledTime);
|
||||||
@@ -1508,7 +1646,7 @@ function reskipSponsorTime(segment: SponsorTime, forceSeek = false) {
|
|||||||
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
|
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
|
||||||
|
|
||||||
video.currentTime = segment.segment[1];
|
video.currentTime = segment.segment[1];
|
||||||
sendTelemetryAndCount([segment], skippedTime, fullSkip);
|
sendTelemetryAndCount([segment], segment.actionType !== ActionType.Chapter ? skippedTime : 0, fullSkip);
|
||||||
startSponsorSchedule(true, segment.segment[1], false);
|
startSponsorSchedule(true, segment.segment[1], false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1559,6 +1697,7 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
|
|||||||
|
|
||||||
function shouldSkip(segment: SponsorTime): boolean {
|
function shouldSkip(segment: SponsorTime): boolean {
|
||||||
return (segment.actionType !== ActionType.Full
|
return (segment.actionType !== ActionType.Full
|
||||||
|
&& segment.source !== SponsorSourceType.YouTube
|
||||||
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|
||||||
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
|
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
|
||||||
}
|
}
|
||||||
@@ -1607,7 +1746,7 @@ function updateEditButtonsOnPlayer(): void {
|
|||||||
// Don't try to update the buttons if we aren't on a YouTube video page
|
// Don't try to update the buttons if we aren't on a YouTube video page
|
||||||
if (!sponsorVideoID || onMobileYouTube) return;
|
if (!sponsorVideoID || onMobileYouTube) return;
|
||||||
|
|
||||||
const buttonsEnabled = !Config.config.hideVideoPlayerControls && !onInvidious;
|
const buttonsEnabled = !(Config.config.hideVideoPlayerControls || onInvidious);
|
||||||
|
|
||||||
let creatingSegment = false;
|
let creatingSegment = false;
|
||||||
let submitButtonVisible = false;
|
let submitButtonVisible = false;
|
||||||
@@ -1665,7 +1804,7 @@ function startOrEndTimingNewSegment() {
|
|||||||
if (!isSegmentCreationInProgress()) {
|
if (!isSegmentCreationInProgress()) {
|
||||||
sponsorTimesSubmitting.push({
|
sponsorTimesSubmitting.push({
|
||||||
segment: [roundedTime],
|
segment: [roundedTime],
|
||||||
UUID: utils.generateUserID() as SegmentUUID,
|
UUID: GenericUtils.generateUserID() as SegmentUUID,
|
||||||
category: Config.config.defaultCategory,
|
category: Config.config.defaultCategory,
|
||||||
actionType: ActionType.Skip,
|
actionType: ActionType.Skip,
|
||||||
source: SponsorSourceType.Local
|
source: SponsorSourceType.Local
|
||||||
@@ -1689,6 +1828,8 @@ function startOrEndTimingNewSegment() {
|
|||||||
|
|
||||||
updateEditButtonsOnPlayer();
|
updateEditButtonsOnPlayer();
|
||||||
updateSponsorTimesSubmitting(false);
|
updateSponsorTimesSubmitting(false);
|
||||||
|
|
||||||
|
importExistingChapters(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIncompleteSegment(): SponsorTime {
|
function getIncompleteSegment(): SponsorTime {
|
||||||
@@ -1727,9 +1868,14 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
|
|||||||
UUID: segmentTime.UUID,
|
UUID: segmentTime.UUID,
|
||||||
category: segmentTime.category,
|
category: segmentTime.category,
|
||||||
actionType: segmentTime.actionType,
|
actionType: segmentTime.actionType,
|
||||||
|
description: segmentTime.description,
|
||||||
source: segmentTime.source
|
source: segmentTime.source
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sponsorTimesSubmitting.length > 0) {
|
||||||
|
importExistingChapters(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreviewBar();
|
updatePreviewBar();
|
||||||
@@ -1834,10 +1980,7 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo
|
|||||||
skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
|
skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
|
||||||
} else if (response.successType == -1) {
|
} else if (response.successType == -1) {
|
||||||
if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
|
if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
|
||||||
skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => {
|
openWarningDialog(skipNoticeContentContainer);
|
||||||
Chat.openWarningChat(response.responseText);
|
|
||||||
skipNotice.closeListener.call(skipNotice);
|
|
||||||
}, chrome.i18n.getMessage("voteRejectedWarning"));
|
|
||||||
} else {
|
} else {
|
||||||
skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText))
|
skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText))
|
||||||
}
|
}
|
||||||
@@ -1854,7 +1997,7 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
|
|||||||
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
|
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
|
||||||
|
|
||||||
// Don't vote for preview sponsors
|
// Don't vote for preview sponsors
|
||||||
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source === SponsorSourceType.Local) return;
|
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source !== SponsorSourceType.Server) return;
|
||||||
|
|
||||||
// See if the local time saved count and skip count should be saved
|
// See if the local time saved count and skip count should be saved
|
||||||
if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) {
|
if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) {
|
||||||
@@ -1941,7 +2084,7 @@ function submitSponsorTimes() {
|
|||||||
//called after all the checks have been made that it's okay to do so
|
//called after all the checks have been made that it's okay to do so
|
||||||
async function sendSubmitMessage() {
|
async function sendSubmitMessage() {
|
||||||
// Block if submitting on a running livestream or premiere
|
// Block if submitting on a running livestream or premiere
|
||||||
if (isVisible(document.querySelector(".ytp-live-badge"))) {
|
if (isLivePremiere || isVisible(document.querySelector(".ytp-live-badge"))) {
|
||||||
alert(chrome.i18n.getMessage("liveOrPremiere"));
|
alert(chrome.i18n.getMessage("liveOrPremiere"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2001,7 +2144,7 @@ async function sendSubmitMessage() {
|
|||||||
} catch(e) {} // eslint-disable-line no-empty
|
} catch(e) {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
// Add submissions to current sponsors list
|
// Add submissions to current sponsors list
|
||||||
sponsorTimes = (sponsorTimes || []).concat(newSegments);
|
sponsorTimes = (sponsorTimes || []).concat(newSegments).sort((a, b) => a.segment[0] - b.segment[0]);
|
||||||
|
|
||||||
// Increase contribution count
|
// Increase contribution count
|
||||||
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
|
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
|
||||||
@@ -2025,7 +2168,7 @@ async function sendSubmitMessage() {
|
|||||||
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg");
|
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg");
|
||||||
|
|
||||||
if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a warning from a moderator.")) {
|
if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a warning from a moderator.")) {
|
||||||
Chat.openWarningChat(response.responseText);
|
openWarningDialog(skipNoticeContentContainer);
|
||||||
} else {
|
} else {
|
||||||
alert(GenericUtils.getErrorMessage(response.status, response.responseText));
|
alert(GenericUtils.getErrorMessage(response.status, response.responseText));
|
||||||
}
|
}
|
||||||
@@ -2038,7 +2181,7 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
|
|||||||
|
|
||||||
for (let i = 0; i < sponsorTimes.length; i++) {
|
for (let i = 0; i < sponsorTimes.length; i++) {
|
||||||
for (let s = 0; s < sponsorTimes[i].segment.length; s++) {
|
for (let s = 0; s < sponsorTimes[i].segment.length; s++) {
|
||||||
let timeMessage = utils.getFormattedTime(sponsorTimes[i].segment[s]);
|
let timeMessage = GenericUtils.getFormattedTime(sponsorTimes[i].segment[s]);
|
||||||
//if this is an end time
|
//if this is an end time
|
||||||
if (s == 1) {
|
if (s == 1) {
|
||||||
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
|
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
|
||||||
@@ -2054,6 +2197,74 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
|
|||||||
return sponsorTimesMessage;
|
return sponsorTimesMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function windowListenerHandler(event: MessageEvent): void {
|
||||||
|
const data = event.data;
|
||||||
|
const dataType = data.type;
|
||||||
|
if (data.source !== "sponsorblock") return;
|
||||||
|
|
||||||
|
if (dataType === "navigation") {
|
||||||
|
sponsorVideoID = data.videoID;
|
||||||
|
pageType = data.pageType;
|
||||||
|
|
||||||
|
if (data.channelID) {
|
||||||
|
channelIDInfo = {
|
||||||
|
id: data.channelID,
|
||||||
|
status: ChannelIDStatus.Found
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (dataType === "ad") {
|
||||||
|
if (isAdPlaying != data.playing) {
|
||||||
|
isAdPlaying = data.playing
|
||||||
|
updatePreviewBar();
|
||||||
|
updateVisibilityOfPlayerControlsButton();
|
||||||
|
}
|
||||||
|
} else if (dataType === "data") {
|
||||||
|
if (data.video !== sponsorVideoID) {
|
||||||
|
sponsorVideoID = data.videoID;
|
||||||
|
videoIDChange(sponsorVideoID);
|
||||||
|
}
|
||||||
|
isLivePremiere = data.isLive || data.isPremiere
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActiveSegment(currentTime: number): void {
|
||||||
|
previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
message: "time",
|
||||||
|
time: currentTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextChapter(): void {
|
||||||
|
const chapters = sponsorTimes.filter((time) => time.actionType === ActionType.Chapter)
|
||||||
|
.sort((a, b) => a.segment[1] - b.segment[1]);
|
||||||
|
if (chapters.length <= 0) return;
|
||||||
|
|
||||||
|
const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
|
||||||
|
&& time.segment[1] > video.currentTime);
|
||||||
|
if (nextChapter !== -1) {
|
||||||
|
reskipSponsorTime(chapters[nextChapter], true);
|
||||||
|
} else {
|
||||||
|
video.currentTime = video.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousChapter(): void {
|
||||||
|
const chapters = sponsorTimes.filter((time) => time.actionType === ActionType.Chapter);
|
||||||
|
if (chapters.length <= 0) return;
|
||||||
|
|
||||||
|
// subtract 5 seconds to allow skipping back to the previous chapter if close to start of
|
||||||
|
// the current one
|
||||||
|
const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
|
||||||
|
&& time.segment[0] > video.currentTime - Math.min(5, time.segment[1] - time.segment[0]));
|
||||||
|
const previousChapter = nextChapter !== -1 ? (nextChapter - 1) : (chapters.length - 1);
|
||||||
|
if (previousChapter !== -1) {
|
||||||
|
unskipSponsorTime(chapters[previousChapter], null, true);
|
||||||
|
} else {
|
||||||
|
video.currentTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addPageListeners(): void {
|
function addPageListeners(): void {
|
||||||
const refreshListners = () => {
|
const refreshListners = () => {
|
||||||
if (!isVisible(video)) {
|
if (!isVisible(video)) {
|
||||||
@@ -2061,7 +2272,14 @@ function addPageListeners(): void {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// inject into document
|
||||||
|
const docScript = document.createElement("script");
|
||||||
|
docScript.src = chrome.runtime.getURL("js/document.js");
|
||||||
|
(document.head || document.documentElement).appendChild(docScript);
|
||||||
|
|
||||||
|
document.addEventListener("yt-navigate-start", resetValues);
|
||||||
document.addEventListener("yt-navigate-finish", refreshListners);
|
document.addEventListener("yt-navigate-finish", refreshListners);
|
||||||
|
window.addEventListener("message", windowListenerHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHotkeyListener(): void {
|
function addHotkeyListener(): void {
|
||||||
@@ -2083,6 +2301,8 @@ function hotkeyListener(e: KeyboardEvent): void {
|
|||||||
const skipKey = Config.config.skipKeybind;
|
const skipKey = Config.config.skipKeybind;
|
||||||
const startSponsorKey = Config.config.startSponsorKeybind;
|
const startSponsorKey = Config.config.startSponsorKeybind;
|
||||||
const submitKey = Config.config.submitKeybind;
|
const submitKey = Config.config.submitKeybind;
|
||||||
|
const nextChapterKey = Config.config.nextChapterKeybind;
|
||||||
|
const previousChapterKey = Config.config.previousChapterKeybind;
|
||||||
|
|
||||||
if (keybindEquals(key, skipKey)) {
|
if (keybindEquals(key, skipKey)) {
|
||||||
if (activeSkipKeybindElement)
|
if (activeSkipKeybindElement)
|
||||||
@@ -2094,6 +2314,12 @@ function hotkeyListener(e: KeyboardEvent): void {
|
|||||||
} else if (keybindEquals(key, submitKey)) {
|
} else if (keybindEquals(key, submitKey)) {
|
||||||
submitSponsorTimes();
|
submitSponsorTimes();
|
||||||
return;
|
return;
|
||||||
|
} else if (keybindEquals(key, nextChapterKey)) {
|
||||||
|
nextChapter();
|
||||||
|
return;
|
||||||
|
} else if (keybindEquals(key, previousChapterKey)) {
|
||||||
|
previousChapter();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//legacy - to preserve keybinds for skipKey, startSponsorKey and submitKey for people who set it before the update. (shouldn't be changed for future keybind options)
|
//legacy - to preserve keybinds for skipKey, startSponsorKey and submitKey for people who set it before the update. (shouldn't be changed for future keybind options)
|
||||||
@@ -2164,7 +2390,7 @@ function showTimeWithoutSkips(skippedDuration: number): void {
|
|||||||
display.appendChild(duration);
|
display.appendChild(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
const durationAfterSkips = utils.getFormattedTime(video?.duration - skippedDuration)
|
const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration);
|
||||||
|
|
||||||
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
|
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
|
||||||
}
|
}
|
||||||
@@ -2183,9 +2409,10 @@ function checkForPreloadedSegment() {
|
|||||||
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
|
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
|
||||||
sponsorTimesSubmitting.push({
|
sponsorTimesSubmitting.push({
|
||||||
segment: segment.segment,
|
segment: segment.segment,
|
||||||
UUID: utils.generateUserID() as SegmentUUID,
|
UUID: GenericUtils.generateUserID() as SegmentUUID,
|
||||||
category: segment.category ? segment.category : Config.config.defaultCategory,
|
category: segment.category ? segment.category : Config.config.defaultCategory,
|
||||||
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
|
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
|
||||||
|
description: segment.description ?? "",
|
||||||
source: SponsorSourceType.Local
|
source: SponsorSourceType.Local
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2205,7 +2432,8 @@ function checkForPreloadedSegment() {
|
|||||||
const navigationApiAvailable = "navigation" in window;
|
const navigationApiAvailable = "navigation" in window;
|
||||||
if (navigationApiAvailable) {
|
if (navigationApiAvailable) {
|
||||||
// TODO: Remove type cast once type declarations are updated
|
// TODO: Remove type cast once type declarations are updated
|
||||||
(window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", () => videoIDChange(getYouTubeVideoID(document)));
|
(window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", (e) =>
|
||||||
|
videoIDChange(getYouTubeVideoID(document, (e as unknown as Record<string, Record<string, string>>).destination.url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record availability of Navigation API
|
// Record availability of Navigation API
|
||||||
|
|||||||
80
src/document.ts
Normal file
80
src/document.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
Content script are run in an isolated DOM so it is not possible to access some key details that are sanitized when passed cross-dom
|
||||||
|
This script is used to get the details from the page and make them available for the content script by being injected directly into the page
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PageType } from "./types";
|
||||||
|
|
||||||
|
interface StartMessage {
|
||||||
|
type: "navigation",
|
||||||
|
pageType: PageType
|
||||||
|
videoID: string | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FinishMessage extends StartMessage {
|
||||||
|
channelID: string,
|
||||||
|
channelTitle: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AdMessage {
|
||||||
|
type: "ad",
|
||||||
|
playing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoData {
|
||||||
|
type: "data",
|
||||||
|
videoID: string,
|
||||||
|
isLive: boolean,
|
||||||
|
isPremiere: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowMessage = StartMessage | FinishMessage | AdMessage | VideoData;
|
||||||
|
|
||||||
|
// global playerClient - too difficult to type
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let playerClient: any;
|
||||||
|
|
||||||
|
const sendMessage = (message: WindowMessage): void => {
|
||||||
|
window.postMessage({ source: "sponsorblock", ...message }, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupPlayerClient(e: CustomEvent): void {
|
||||||
|
if (playerClient) return; // early exit if already defined
|
||||||
|
|
||||||
|
playerClient = e.detail;
|
||||||
|
sendVideoData(); // send playerData after setup
|
||||||
|
|
||||||
|
e.detail.addEventListener('onAdStart', () => sendMessage({ type: "ad", playing: true } as AdMessage));
|
||||||
|
e.detail.addEventListener('onAdFinish', () => sendMessage({ type: "ad", playing: false } as AdMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("yt-player-updated", setupPlayerClient);
|
||||||
|
document.addEventListener("yt-navigate-start", navigationStartSend);
|
||||||
|
document.addEventListener("yt-navigate-finish", navigateFinishSend);
|
||||||
|
|
||||||
|
function navigationParser(event: CustomEvent): StartMessage {
|
||||||
|
const pageType: PageType = event.detail.pageType;
|
||||||
|
const result: StartMessage = { type: "navigation", pageType, videoID: null };
|
||||||
|
if (pageType === "shorts" || pageType === "watch") {
|
||||||
|
const endpoint = event.detail.endpoint
|
||||||
|
result.videoID = (pageType === "shorts" ? endpoint.reelWatchEndpoint : endpoint.watchEndpoint).videoId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigationStartSend(event: CustomEvent): void {
|
||||||
|
sendMessage(navigationParser(event) as StartMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateFinishSend(event: CustomEvent): void {
|
||||||
|
sendVideoData(); // arrived at new video, send video data
|
||||||
|
const videoDetails = event.detail?.response?.playerResponse?.videoDetails;
|
||||||
|
sendMessage({ channelID: videoDetails.channelId, channelTitle: videoDetails.author, ...navigationParser(event) } as FinishMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendVideoData(): void {
|
||||||
|
if (!playerClient) return;
|
||||||
|
const { video_id, isLive, isPremiere } = playerClient.getVideoData();
|
||||||
|
sendMessage({ type: "data", videoID: video_id, isLive, isPremiere } as VideoData);
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import Config from "../config";
|
|
||||||
import Utils from "../utils";
|
|
||||||
const utils = new Utils();
|
|
||||||
|
|
||||||
export interface ChatConfig {
|
|
||||||
displayName: string,
|
|
||||||
composerInitialValue?: string,
|
|
||||||
customDescription?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openChat(config: ChatConfig): void {
|
|
||||||
const chat = document.createElement("div");
|
|
||||||
chat.classList.add("sbChatNotice");
|
|
||||||
chat.style.zIndex = "2000";
|
|
||||||
|
|
||||||
const iframe= document.createElement("iframe");
|
|
||||||
iframe.src = "https://chat.sponsor.ajay.app/#" + utils.objectToURI("", config, false);
|
|
||||||
chat.appendChild(iframe);
|
|
||||||
|
|
||||||
const closeButton = document.createElement("img");
|
|
||||||
closeButton.classList.add("sbChatClose");
|
|
||||||
closeButton.src = chrome.extension.getURL("icons/close.png");
|
|
||||||
closeButton.addEventListener("click", () => {
|
|
||||||
chat.remove();
|
|
||||||
closeButton.remove();
|
|
||||||
});
|
|
||||||
chat.appendChild(closeButton);
|
|
||||||
|
|
||||||
const referenceNode = utils.findReferenceNode();
|
|
||||||
referenceNode.prepend(chat);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function openWarningChat(warningMessage: string): Promise<void> {
|
|
||||||
const warningReasonMatch = warningMessage.match(/Warning reason: '(.+)'/);
|
|
||||||
alert(chrome.i18n.getMessage("warningChatInfo") + `\n\n${warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``}`);
|
|
||||||
|
|
||||||
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
|
|
||||||
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
|
|
||||||
const publicUserID = await utils.getHash(Config.config.userID);
|
|
||||||
|
|
||||||
openChat({
|
|
||||||
displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`,
|
|
||||||
composerInitialValue: `I got a warning and confirm I [REMOVE THIS CAPITAL TEXT TO CONFIRM] reread the guidelines.` +
|
|
||||||
warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``,
|
|
||||||
customDescription: chrome.i18n.getMessage("warningChatInfo")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -6,41 +6,64 @@ https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
import { ActionType } from "../types";
|
import { ChapterVote } from "../render/ChapterVote";
|
||||||
import Utils from "../utils";
|
import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceType, SponsorTime } from "../types";
|
||||||
const utils = new Utils();
|
import { partition } from "../utils/arrayUtils";
|
||||||
|
import { shortCategoryName } from "../utils/categoryUtils";
|
||||||
|
import { GenericUtils } from "../utils/genericUtils";
|
||||||
|
|
||||||
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||||
|
const MIN_CHAPTER_SIZE = 0.003;
|
||||||
|
|
||||||
export interface PreviewBarSegment {
|
export interface PreviewBarSegment {
|
||||||
segment: [number, number];
|
segment: [number, number];
|
||||||
category: string;
|
category: Category;
|
||||||
unsubmitted: boolean;
|
|
||||||
actionType: ActionType;
|
actionType: ActionType;
|
||||||
|
unsubmitted: boolean;
|
||||||
showLarger: boolean;
|
showLarger: boolean;
|
||||||
|
description: string;
|
||||||
|
source: SponsorSourceType;
|
||||||
|
requiredSegment?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChapterGroup extends SegmentContainer {
|
||||||
|
originalDuration: number
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreviewBar {
|
class PreviewBar {
|
||||||
container: HTMLUListElement;
|
container: HTMLUListElement;
|
||||||
categoryTooltip?: HTMLDivElement;
|
categoryTooltip?: HTMLDivElement;
|
||||||
tooltipContainer?: HTMLElement;
|
categoryTooltipContainer?: HTMLElement;
|
||||||
|
chapterTooltip?: HTMLDivElement;
|
||||||
|
|
||||||
parent: HTMLElement;
|
parent: HTMLElement;
|
||||||
onMobileYouTube: boolean;
|
onMobileYouTube: boolean;
|
||||||
onInvidious: boolean;
|
onInvidious: boolean;
|
||||||
|
|
||||||
segments: PreviewBarSegment[] = [];
|
segments: PreviewBarSegment[] = [];
|
||||||
|
existingChapters: PreviewBarSegment[] = [];
|
||||||
videoDuration = 0;
|
videoDuration = 0;
|
||||||
|
|
||||||
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
|
// For chapter bar
|
||||||
|
hoveredSection: HTMLElement;
|
||||||
|
customChaptersBar: HTMLElement;
|
||||||
|
chaptersBarSegments: PreviewBarSegment[];
|
||||||
|
chapterVote: ChapterVote;
|
||||||
|
originalChapterBar: HTMLElement;
|
||||||
|
originalChapterBarBlocks: NodeListOf<HTMLElement>;
|
||||||
|
|
||||||
|
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, test=false) {
|
||||||
|
if (test) return;
|
||||||
this.container = document.createElement('ul');
|
this.container = document.createElement('ul');
|
||||||
this.container.id = 'previewbar';
|
this.container.id = 'previewbar';
|
||||||
|
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.onMobileYouTube = onMobileYouTube;
|
this.onMobileYouTube = onMobileYouTube;
|
||||||
this.onInvidious = onInvidious;
|
this.onInvidious = onInvidious;
|
||||||
|
this.chapterVote = chapterVote;
|
||||||
|
|
||||||
this.createElement(parent);
|
this.createElement(parent);
|
||||||
|
this.createChapterMutationObservers();
|
||||||
|
|
||||||
this.setupHoverText();
|
this.setupHoverText();
|
||||||
}
|
}
|
||||||
@@ -51,16 +74,19 @@ class PreviewBar {
|
|||||||
// Create label placeholder
|
// Create label placeholder
|
||||||
this.categoryTooltip = document.createElement("div");
|
this.categoryTooltip = document.createElement("div");
|
||||||
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||||
|
this.chapterTooltip = document.createElement("div");
|
||||||
|
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||||
|
|
||||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
|
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
|
||||||
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
|
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
|
||||||
|
|
||||||
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
|
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
|
||||||
this.tooltipContainer = tooltipTextWrapper.parentElement;
|
this.categoryTooltipContainer = tooltipTextWrapper.parentElement;
|
||||||
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title");
|
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title");
|
||||||
if (!this.tooltipContainer || !titleTooltip) return;
|
if (!this.categoryTooltipContainer || !titleTooltip) return;
|
||||||
|
|
||||||
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
|
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
|
||||||
|
tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
|
||||||
|
|
||||||
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
||||||
if (!seekBar) return;
|
if (!seekBar) return;
|
||||||
@@ -76,7 +102,7 @@ class PreviewBar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return;
|
if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer) return;
|
||||||
|
|
||||||
// If the mutation observed is only for our tooltip text, ignore
|
// If the mutation observed is only for our tooltip text, ignore
|
||||||
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
|
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
|
||||||
@@ -93,7 +119,7 @@ class PreviewBar {
|
|||||||
const tooltipText = tooltipTextElement.textContent;
|
const tooltipText = tooltipTextElement.textContent;
|
||||||
if (tooltipText === null || tooltipText.length === 0) continue;
|
if (tooltipText === null || tooltipText.length === 0) continue;
|
||||||
|
|
||||||
timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText);
|
timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText);
|
||||||
|
|
||||||
if (timeInSeconds !== null) break;
|
if (timeInSeconds !== null) break;
|
||||||
}
|
}
|
||||||
@@ -101,36 +127,32 @@ class PreviewBar {
|
|||||||
if (timeInSeconds === null) return;
|
if (timeInSeconds === null) return;
|
||||||
|
|
||||||
// Find the segment at that location, using the shortest if multiple found
|
// Find the segment at that location, using the shortest if multiple found
|
||||||
let segment: PreviewBarSegment | null = null;
|
const [normalSegments, chapterSegments] =
|
||||||
let currentSegmentLength = Infinity;
|
partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
|
||||||
|
(segment) => segment.actionType !== ActionType.Chapter);
|
||||||
for (const seg of this.segments) {//
|
let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments);
|
||||||
const segmentLength = seg.segment[1] - seg.segment[0];
|
let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments);
|
||||||
const minSize = this.getMinimumSize(seg.showLarger);
|
if (mainSegment === null && secondarySegment !== null) {
|
||||||
|
mainSegment = secondarySegment;
|
||||||
const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]);
|
secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
|
||||||
const endTime = segmentLength > minSize ? seg.segment[1] : Math.ceil(seg.segment[0] + minSize);
|
|
||||||
if (startTime <= timeInSeconds && endTime >= timeInSeconds) {
|
|
||||||
if (segmentLength < currentSegmentLength) {
|
|
||||||
currentSegmentLength = segmentLength;
|
|
||||||
segment = seg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
|
if (mainSegment === null && secondarySegment === null) {
|
||||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||||
} else if (segment !== null) {
|
} else {
|
||||||
this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||||
|
if (mainSegment !== null && secondarySegment !== null) {
|
||||||
if (segment.unsubmitted) {
|
this.categoryTooltipContainer.classList.add("sponsorTwoTooltips");
|
||||||
this.categoryTooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + utils.shortCategoryName(segment.category);
|
|
||||||
} else {
|
} else {
|
||||||
this.categoryTooltip.textContent = utils.shortCategoryName(segment.category);
|
this.categoryTooltipContainer.classList.remove("sponsorTwoTooltips");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the class if the timestamp text uses it to prevent overlapping
|
this.setTooltipTitle(mainSegment, this.categoryTooltip);
|
||||||
|
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
|
||||||
|
|
||||||
|
// Used to prevent overlapping
|
||||||
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||||
|
this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,6 +162,21 @@ class PreviewBar {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setTooltipTitle(segment: PreviewBarSegment, tooltip: HTMLElement): void {
|
||||||
|
if (segment) {
|
||||||
|
const name = segment.description || shortCategoryName(segment.category);
|
||||||
|
if (segment.unsubmitted) {
|
||||||
|
tooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + name;
|
||||||
|
} else {
|
||||||
|
tooltip.textContent = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.style.removeProperty("display");
|
||||||
|
} else {
|
||||||
|
tooltip.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createElement(parent: HTMLElement): void {
|
createElement(parent: HTMLElement): void {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
|
||||||
@@ -157,41 +194,65 @@ class PreviewBar {
|
|||||||
this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered"));
|
this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// On the seek bar
|
// On the seek bar
|
||||||
this.parent.prepend(this.container);
|
this.parent.prepend(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.videoDuration = 0;
|
|
||||||
this.segments = [];
|
|
||||||
|
|
||||||
while (this.container.firstChild) {
|
while (this.container.firstChild) {
|
||||||
this.container.removeChild(this.container.firstChild);
|
this.container.removeChild(this.container.firstChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set(segments: PreviewBarSegment[], videoDuration: number): void {
|
set(segments: PreviewBarSegment[], videoDuration: number): void {
|
||||||
|
this.segments = segments ?? [];
|
||||||
|
this.videoDuration = videoDuration ?? 0;
|
||||||
|
|
||||||
|
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
|
||||||
|
// Sometimes video duration is inaccurate, pull from accessibility info
|
||||||
|
const ariaDuration = parseInt(progressBar?.getAttribute('aria-valuemax')) ?? 0;
|
||||||
|
if (ariaDuration && Math.abs(ariaDuration - this.videoDuration) > 3) {
|
||||||
|
this.videoDuration = ariaDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(): void {
|
||||||
this.clear();
|
this.clear();
|
||||||
if (!segments) return;
|
if (!this.segments) return;
|
||||||
|
|
||||||
this.segments = segments;
|
this.originalChapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
|
||||||
this.videoDuration = videoDuration;
|
this.originalChapterBarBlocks = this.originalChapterBar.querySelectorAll(":scope > div") as NodeListOf<HTMLElement>
|
||||||
|
this.existingChapters = this.segments.filter((s) => s.source === SponsorSourceType.YouTube).sort((a, b) => a.segment[0] - b.segment[0])
|
||||||
|
|
||||||
this.segments.sort(({segment: a}, {segment: b}) => {
|
const sortedSegments = this.segments.sort(({ segment: a }, { segment: b }) => {
|
||||||
// Sort longer segments before short segments to make shorter segments render later
|
// Sort longer segments before short segments to make shorter segments render later
|
||||||
return (b[1] - b[0]) - (a[1] - a[0]);
|
return (b[1] - b[0]) - (a[1] - a[0]);
|
||||||
}).forEach((segment) => {
|
});
|
||||||
|
for (const segment of sortedSegments) {
|
||||||
const bar = this.createBar(segment);
|
const bar = this.createBar(segment);
|
||||||
|
|
||||||
this.container.appendChild(bar);
|
this.container.appendChild(bar);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
this.createChaptersBar(this.segments.sort((a, b) => a.segment[0] - b.segment[0]));
|
||||||
|
|
||||||
|
const chapterChevron = this.getChapterChevron();
|
||||||
|
if (this.segments.some((segment) => segment.actionType !== ActionType.Chapter
|
||||||
|
&& segment.source === SponsorSourceType.YouTube)) {
|
||||||
|
chapterChevron.style.removeProperty("display");
|
||||||
|
} else {
|
||||||
|
chapterChevron.style.display = "none";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createBar({category, unsubmitted, segment, showLarger}: PreviewBarSegment): HTMLLIElement {
|
createBar(barSegment: PreviewBarSegment): HTMLLIElement {
|
||||||
|
const { category, unsubmitted, segment, showLarger } = barSegment;
|
||||||
|
|
||||||
const bar = document.createElement('li');
|
const bar = document.createElement('li');
|
||||||
bar.classList.add('previewbar');
|
bar.classList.add('previewbar');
|
||||||
|
if (barSegment.requiredSegment) bar.classList.add("requiredSegment");
|
||||||
bar.innerHTML = showLarger ? ' ' : ' ';
|
bar.innerHTML = showLarger ? ' ' : ' ';
|
||||||
|
|
||||||
const fullCategoryName = (unsubmitted ? 'preview-' : '') + category;
|
const fullCategoryName = (unsubmitted ? 'preview-' : '') + category;
|
||||||
@@ -202,7 +263,9 @@ class PreviewBar {
|
|||||||
|
|
||||||
bar.style.position = "absolute";
|
bar.style.position = "absolute";
|
||||||
const duration = Math.min(segment[1], this.videoDuration) - segment[0];
|
const duration = Math.min(segment[1], this.videoDuration) - segment[0];
|
||||||
if (duration > 0) bar.style.width = this.timeToPercentage(duration);
|
if (duration > 0) {
|
||||||
|
bar.style.width = `calc(${this.intervalToPercentage(segment[0], segment[1])}${this.chapterFilter(barSegment) ? ' - 2px' : ''})`;
|
||||||
|
}
|
||||||
|
|
||||||
const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
|
const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
|
||||||
bar.style.left = this.timeToPercentage(time);
|
bar.style.left = this.timeToPercentage(time);
|
||||||
@@ -210,6 +273,413 @@ class PreviewBar {
|
|||||||
return bar;
|
return bar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createChaptersBar(segments: PreviewBarSegment[]): void {
|
||||||
|
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
|
||||||
|
if (!progressBar || !this.originalChapterBar || this.originalChapterBar.childElementCount <= 0) return;
|
||||||
|
|
||||||
|
if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|
||||||
|
|| (!Config.config.renderSegmentsAsChapters
|
||||||
|
&& segments.every((segment) => segment.actionType !== ActionType.Chapter
|
||||||
|
|| segment.source === SponsorSourceType.YouTube))) {
|
||||||
|
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
|
||||||
|
this.originalChapterBar.style.removeProperty("display");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge overlapping chapters
|
||||||
|
const filteredSegments = segments?.filter((segment) => this.chapterFilter(segment));
|
||||||
|
const chaptersToRender = this.createChapterRenderGroups(filteredSegments).filter((segment) => this.chapterGroupFilter(segment));
|
||||||
|
|
||||||
|
if (chaptersToRender?.length <= 0) {
|
||||||
|
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
|
||||||
|
this.originalChapterBar.style.removeProperty("display");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create it from cloning
|
||||||
|
let createFromScratch = false;
|
||||||
|
if (!this.customChaptersBar) {
|
||||||
|
createFromScratch = true;
|
||||||
|
this.customChaptersBar = this.originalChapterBar.cloneNode(true) as HTMLElement;
|
||||||
|
this.customChaptersBar.classList.add("sponsorBlockChapterBar");
|
||||||
|
}
|
||||||
|
this.customChaptersBar.style.removeProperty("display");
|
||||||
|
const originalSections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container");
|
||||||
|
const originalSection = originalSections[0];
|
||||||
|
|
||||||
|
this.customChaptersBar = this.customChaptersBar;
|
||||||
|
|
||||||
|
// For switching to a video with less chapters
|
||||||
|
if (originalSections.length > chaptersToRender.length) {
|
||||||
|
for (let i = originalSections.length - 1; i >= chaptersToRender.length; i--) {
|
||||||
|
this.customChaptersBar.removeChild(originalSections[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify it to have sections for each segment
|
||||||
|
for (let i = 0; i < chaptersToRender.length; i++) {
|
||||||
|
const chapter = chaptersToRender[i].segment;
|
||||||
|
let newSection = originalSections[i] as HTMLElement;
|
||||||
|
if (!newSection) {
|
||||||
|
newSection = originalSection.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
this.firstTimeSetupChapterSection(newSection);
|
||||||
|
this.customChaptersBar.appendChild(newSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupChapterSection(newSection, chapter[0], chapter[1], i !== chaptersToRender.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide old bar
|
||||||
|
this.originalChapterBar.style.display = "none";
|
||||||
|
|
||||||
|
if (createFromScratch) {
|
||||||
|
if (this.container?.parentElement === progressBar) {
|
||||||
|
progressBar.insertBefore(this.customChaptersBar, this.container.nextSibling);
|
||||||
|
} else {
|
||||||
|
progressBar.prepend(this.customChaptersBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateChapterAllMutation(this.originalChapterBar, progressBar, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
createChapterRenderGroups(segments: PreviewBarSegment[]): ChapterGroup[] {
|
||||||
|
const result: ChapterGroup[] = [];
|
||||||
|
|
||||||
|
segments?.forEach((segment, index) => {
|
||||||
|
const latestChapter = result[result.length - 1];
|
||||||
|
if (latestChapter && latestChapter.segment[1] > segment.segment[0]) {
|
||||||
|
const segmentDuration = segment.segment[1] - segment.segment[0];
|
||||||
|
if (segment.segment[0] < latestChapter.segment[0]
|
||||||
|
|| segmentDuration < latestChapter.originalDuration) {
|
||||||
|
// Remove latest if it starts too late
|
||||||
|
let latestValidChapter = latestChapter;
|
||||||
|
const chaptersToAddBack: ChapterGroup[] = []
|
||||||
|
while (latestValidChapter?.segment[0] >= segment.segment[0]) {
|
||||||
|
const invalidChapter = result.pop();
|
||||||
|
if (invalidChapter.segment[1] > segment.segment[1]) {
|
||||||
|
if (invalidChapter.segment[0] === segment.segment[0]) {
|
||||||
|
invalidChapter.segment[0] = segment.segment[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
chaptersToAddBack.push(invalidChapter);
|
||||||
|
}
|
||||||
|
latestValidChapter = result[result.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the latest chapter if smaller
|
||||||
|
result.push({
|
||||||
|
segment: [segment.segment[0], segment.segment[1]],
|
||||||
|
originalDuration: segmentDuration,
|
||||||
|
});
|
||||||
|
if (latestValidChapter?.segment[1] > segment.segment[1]) {
|
||||||
|
result.push({
|
||||||
|
segment: [segment.segment[1], latestValidChapter.segment[1]],
|
||||||
|
originalDuration: latestValidChapter.originalDuration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chaptersToAddBack.reverse();
|
||||||
|
let lastChapterChecked: number[] = segment.segment;
|
||||||
|
for (const chapter of chaptersToAddBack) {
|
||||||
|
if (chapter.segment[0] < lastChapterChecked[1]) {
|
||||||
|
chapter.segment[0] = lastChapterChecked[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
lastChapterChecked = chapter.segment;
|
||||||
|
}
|
||||||
|
result.push(...chaptersToAddBack);
|
||||||
|
if (latestValidChapter) latestValidChapter.segment[1] = segment.segment[0];
|
||||||
|
} else {
|
||||||
|
// Start at end of old one otherwise
|
||||||
|
result.push({
|
||||||
|
segment: [latestChapter.segment[1], segment.segment[1]],
|
||||||
|
originalDuration: segmentDuration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add empty buffer before segment if needed
|
||||||
|
const lastTime = latestChapter?.segment[1] || 0;
|
||||||
|
if (segment.segment[0] > lastTime) {
|
||||||
|
result.push({
|
||||||
|
segment: [lastTime, segment.segment[0]],
|
||||||
|
originalDuration: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal case
|
||||||
|
const endTime = Math.min(segment.segment[1], this.videoDuration);
|
||||||
|
result.push({
|
||||||
|
segment: [segment.segment[0], endTime],
|
||||||
|
originalDuration: endTime - segment.segment[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add empty buffer after segment if needed
|
||||||
|
if (index === segments.length - 1) {
|
||||||
|
const nextSegment = segments[index + 1];
|
||||||
|
const nextTime = nextSegment ? nextSegment.segment[0] : this.videoDuration;
|
||||||
|
const lastTime = result[result.length - 1]?.segment[1] || segment.segment[1];
|
||||||
|
if (this.intervalToDecimal(lastTime, nextTime) > MIN_CHAPTER_SIZE) {
|
||||||
|
result.push({
|
||||||
|
segment: [lastTime, nextTime],
|
||||||
|
originalDuration: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupChapterSection(section: HTMLElement, startTime: number, endTime: number, addMargin: boolean): void {
|
||||||
|
const sizePercent = this.intervalToPercentage(startTime, endTime);
|
||||||
|
if (addMargin) {
|
||||||
|
section.style.marginRight = "2px";
|
||||||
|
section.style.width = `calc(${sizePercent} - 2px)`;
|
||||||
|
} else {
|
||||||
|
section.style.marginRight = "0";
|
||||||
|
section.style.width = sizePercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.setAttribute("decimal-width", String(this.intervalToDecimal(startTime, endTime)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private firstTimeSetupChapterSection(section: HTMLElement): void {
|
||||||
|
section.addEventListener("mouseenter", () => {
|
||||||
|
this.hoveredSection?.classList.remove("ytp-exp-chapter-hover-effect");
|
||||||
|
section.classList.add("ytp-exp-chapter-hover-effect");
|
||||||
|
this.hoveredSection = section;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createChapterMutationObservers(): void {
|
||||||
|
const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
|
||||||
|
const chapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
|
||||||
|
if (!progressBar || !chapterBar) return;
|
||||||
|
|
||||||
|
const attributeObserver = new MutationObserver((mutations) => {
|
||||||
|
const changes: Record<string, HTMLElement> = {};
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
const currentElement = mutation.target as HTMLElement;
|
||||||
|
if (mutation.type === "attributes"
|
||||||
|
&& currentElement.parentElement?.classList.contains("ytp-progress-list")) {
|
||||||
|
changes[currentElement.classList[0]] = mutation.target as HTMLElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateChapterMutation(changes, progressBar);
|
||||||
|
});
|
||||||
|
|
||||||
|
attributeObserver.observe(chapterBar, {
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["style", "class"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const childListObserver = new MutationObserver((mutations) => {
|
||||||
|
const changes: Record<string, HTMLElement> = {};
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.type === "childList") {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateChapterMutation(changes, progressBar);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only direct children, no subtree
|
||||||
|
childListObserver.observe(chapterBar, {
|
||||||
|
childList: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateChapterAllMutation(originalChapterBar: HTMLElement, progressBar: HTMLElement, firstUpdate = false): void {
|
||||||
|
const elements = originalChapterBar.querySelectorAll(".ytp-progress-list > *");
|
||||||
|
const changes: Record<string, HTMLElement> = {};
|
||||||
|
for (const element of elements) {
|
||||||
|
changes[element.classList[0]] = element as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateChapterMutation(changes, progressBar, firstUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateChapterMutation(changes: Record<string, HTMLElement>, progressBar: HTMLElement, firstUpdate = false): void {
|
||||||
|
// Go through each newly generated chapter bar and update the width based on changes array
|
||||||
|
if (this.customChaptersBar) {
|
||||||
|
// Width reached so far in decimal percent
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
const sections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container") as NodeListOf<HTMLElement>;
|
||||||
|
for (let i = 0; i < sections.length; i++) {
|
||||||
|
const section = sections[i];
|
||||||
|
|
||||||
|
const sectionWidthDecimal = parseFloat(section.getAttribute("decimal-width"));
|
||||||
|
const sectionWidthDecimalNoMargin = sectionWidthDecimal - 2 / progressBar.clientWidth;
|
||||||
|
|
||||||
|
for (const className in changes) {
|
||||||
|
const selector = `.${className}`
|
||||||
|
const customChangedElement = section.querySelector(selector) as HTMLElement;
|
||||||
|
if (customChangedElement) {
|
||||||
|
const fullSectionWidth = i === sections.length - 1 ? sectionWidthDecimal : sectionWidthDecimalNoMargin;
|
||||||
|
const changedElement = changes[className];
|
||||||
|
const changedData = this.findLeftAndScale(selector, changedElement, progressBar);
|
||||||
|
|
||||||
|
const left = (changedData.left) / progressBar.clientWidth;
|
||||||
|
const calculatedLeft = Math.max(0, Math.min(1, (left - cursor) / fullSectionWidth));
|
||||||
|
if (!isNaN(left) && !isNaN(calculatedLeft)) {
|
||||||
|
customChangedElement.style.left = `${calculatedLeft * 100}%`;
|
||||||
|
customChangedElement.style.removeProperty("display");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedData.scale !== null) {
|
||||||
|
const transformScale = (changedData.scale) / progressBar.clientWidth;
|
||||||
|
|
||||||
|
customChangedElement.style.transform =
|
||||||
|
`scaleX(${Math.max(0, Math.min(1 - calculatedLeft, (transformScale - cursor) / fullSectionWidth - calculatedLeft))}`;
|
||||||
|
if (firstUpdate) {
|
||||||
|
customChangedElement.style.transition = "none";
|
||||||
|
setTimeout(() => customChangedElement.style.removeProperty("transition"), 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customChangedElement.className !== changedElement.className) {
|
||||||
|
customChangedElement.className = changedElement.className;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor += sectionWidthDecimal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findLeftAndScale(selector: string, currentElement: HTMLElement, progressBar: HTMLElement):
|
||||||
|
{ left: number, scale: number } {
|
||||||
|
const sections = currentElement.parentElement.parentElement.parentElement.children;
|
||||||
|
let currentWidth = 0;
|
||||||
|
|
||||||
|
let left = 0;
|
||||||
|
let leftPosition = 0;
|
||||||
|
|
||||||
|
let scale = null;
|
||||||
|
let scalePosition = 0;
|
||||||
|
let scaleWidth = 0;
|
||||||
|
|
||||||
|
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 currentSectionWidth = currentSectionWidthNoMargin
|
||||||
|
+ this.getPartialChapterSectionStyle(section, "marginRight");
|
||||||
|
|
||||||
|
// First check for left
|
||||||
|
const checkLeft = parseFloat(checkElement.style.left.replace("px", ""));
|
||||||
|
if (checkLeft !== 0) {
|
||||||
|
left = checkLeft;
|
||||||
|
leftPosition = currentWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for scale
|
||||||
|
const transformMatch = checkElement.style.transform.match(/scaleX\(([0-9.]+?)\)/);
|
||||||
|
if (transformMatch) {
|
||||||
|
const transformScale = parseFloat(transformMatch[1]);
|
||||||
|
if (i === sections.length - 1 || (transformScale < 1 && transformScale + checkLeft / currentSectionWidthNoMargin < 0.99999)) {
|
||||||
|
scale = transformScale;
|
||||||
|
scaleWidth = currentSectionWidthNoMargin;
|
||||||
|
|
||||||
|
if (transformScale > 0) {
|
||||||
|
// reached the end of this section for sure, since the scale is now between 0 and 1
|
||||||
|
// if the scale is always zero, then it will go through all sections but still return 0
|
||||||
|
|
||||||
|
scalePosition = currentWidth;
|
||||||
|
if (checkLeft !== 0) {
|
||||||
|
scalePosition += left;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWidth += currentSectionWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: left + leftPosition,
|
||||||
|
scale: scale !== null ? scale * scaleWidth + scalePosition : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPartialChapterSectionStyle(element: HTMLElement, param: string): number {
|
||||||
|
const data = element.style[param];
|
||||||
|
if (data?.includes("100%")) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return parseInt(element.style[param].match(/\d+/g)?.[0]) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): void {
|
||||||
|
if (!segments && submittingSegments?.length <= 0) return;
|
||||||
|
|
||||||
|
segments ??= [];
|
||||||
|
if (submittingSegments?.length > 0) segments = segments.concat(submittingSegments);
|
||||||
|
const activeSegments = segments.filter((segment) => {
|
||||||
|
return segment.hidden === SponsorHideType.Visible
|
||||||
|
&& segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setActiveSegments(activeSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the text to the chapters slot if not filled by default
|
||||||
|
*/
|
||||||
|
private setActiveSegments(segments: SponsorTime[]): void {
|
||||||
|
const chaptersContainer = document.querySelector(".ytp-chapter-container") as HTMLDivElement;
|
||||||
|
|
||||||
|
if (chaptersContainer) {
|
||||||
|
// TODO: Check if existing chapters exist (if big chapters menu is available?)
|
||||||
|
|
||||||
|
if (segments.length > 0) {
|
||||||
|
chaptersContainer.style.removeProperty("display");
|
||||||
|
|
||||||
|
const chosenSegment = segments.sort((a, b) => {
|
||||||
|
if (a.actionType === ActionType.Chapter && b.actionType !== ActionType.Chapter) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.actionType !== ActionType.Chapter && b.actionType === ActionType.Chapter) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return (b.segment[0] - a.segment[0]);
|
||||||
|
}
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
const chapterButton = chaptersContainer.querySelector("button.ytp-chapter-title") as HTMLButtonElement;
|
||||||
|
chapterButton.classList.remove("ytp-chapter-container-disabled");
|
||||||
|
chapterButton.disabled = false;
|
||||||
|
|
||||||
|
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
|
||||||
|
chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
|
||||||
|
|
||||||
|
const chapterVoteContainer = this.chapterVote.getContainer();
|
||||||
|
if (chosenSegment.source === SponsorSourceType.Server) {
|
||||||
|
if (!chapterButton.contains(chapterVoteContainer)) {
|
||||||
|
chapterButton.insertBefore(chapterVoteContainer, this.getChapterChevron());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chapterVote.setVisibility(true);
|
||||||
|
this.chapterVote.setSegment(chosenSegment);
|
||||||
|
} else {
|
||||||
|
this.chapterVote.setVisibility(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide chapters menu again
|
||||||
|
chaptersContainer.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
remove(): void {
|
remove(): void {
|
||||||
this.container.remove();
|
this.container.remove();
|
||||||
|
|
||||||
@@ -218,14 +688,66 @@ class PreviewBar {
|
|||||||
this.categoryTooltip = undefined;
|
this.categoryTooltip = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tooltipContainer) {
|
if (this.categoryTooltipContainer) {
|
||||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||||
this.tooltipContainer = undefined;
|
this.categoryTooltipContainer = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private chapterFilter(segment: PreviewBarSegment): boolean {
|
||||||
|
return (Config.config.renderSegmentsAsChapters || segment.actionType === ActionType.Chapter)
|
||||||
|
&& segment.actionType !== ActionType.Poi
|
||||||
|
&& this.chapterGroupFilter(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private chapterGroupFilter(segment: SegmentContainer): boolean {
|
||||||
|
return segment.segment.length === 2 && this.intervalToDecimal(segment.segment[0], segment.segment[1]) > MIN_CHAPTER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalToPercentage(startTime: number, endTime: number) {
|
||||||
|
return `${this.intervalToDecimal(startTime, endTime) * 100}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalToDecimal(startTime: number, endTime: number) {
|
||||||
|
return (this.timeToDecimal(endTime) - this.timeToDecimal(startTime));
|
||||||
|
}
|
||||||
|
|
||||||
timeToPercentage(time: number): string {
|
timeToPercentage(time: number): string {
|
||||||
return Math.min(100, time / this.videoDuration * 100) + '%';
|
return `${this.timeToDecimal(time) * 100}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
timeToDecimal(time: number): number {
|
||||||
|
if (this.originalChapterBarBlocks?.length > 1 && this.existingChapters.length === this.originalChapterBarBlocks?.length) {
|
||||||
|
// Parent element to still work when display: none
|
||||||
|
const totalPixels = this.originalChapterBar.parentElement.clientWidth;
|
||||||
|
let pixelOffset = 0;
|
||||||
|
let lastCheckedChapter = -1;
|
||||||
|
for (let i = 0; i < this.originalChapterBarBlocks.length; i++) {
|
||||||
|
const chapterElement = this.originalChapterBarBlocks[i];
|
||||||
|
const widthPixels = parseFloat(chapterElement.style.width.replace("px", ""));
|
||||||
|
|
||||||
|
if (time >= this.existingChapters[i].segment[1]) {
|
||||||
|
const marginPixels = chapterElement.style.marginRight ? parseFloat(chapterElement.style.marginRight.replace("px", "")) : 0;
|
||||||
|
pixelOffset += widthPixels + marginPixels;
|
||||||
|
lastCheckedChapter = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next chapter is the one we are currently inside of
|
||||||
|
const latestChapter = this.existingChapters[lastCheckedChapter + 1];
|
||||||
|
if (latestChapter) {
|
||||||
|
const latestWidth = parseFloat(this.originalChapterBarBlocks[lastCheckedChapter + 1].style.width.replace("px", ""));
|
||||||
|
const latestChapterDuration = latestChapter.segment[1] - latestChapter.segment[0];
|
||||||
|
|
||||||
|
const percentageInCurrentChapter = (time - latestChapter.segment[0]) / latestChapterDuration;
|
||||||
|
const sizeOfCurrentChapter = latestWidth / totalPixels;
|
||||||
|
return Math.min(1, ((pixelOffset / totalPixels) + (percentageInCurrentChapter * sizeOfCurrentChapter)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(1, time / this.videoDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -234,6 +756,31 @@ class PreviewBar {
|
|||||||
getMinimumSize(showLarger = false): number {
|
getMinimumSize(showLarger = false): number {
|
||||||
return this.videoDuration * (showLarger ? 0.006 : 0.003);
|
return this.videoDuration * (showLarger ? 0.006 : 0.003);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSmallestSegment(timeInSeconds: number, segments: PreviewBarSegment[]): PreviewBarSegment | null {
|
||||||
|
let segment: PreviewBarSegment | null = null;
|
||||||
|
let currentSegmentLength = Infinity;
|
||||||
|
|
||||||
|
for (const seg of segments) { //
|
||||||
|
const segmentLength = seg.segment[1] - seg.segment[0];
|
||||||
|
const minSize = this.getMinimumSize(seg.showLarger);
|
||||||
|
|
||||||
|
const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]);
|
||||||
|
const endTime = segmentLength > minSize ? seg.segment[1] : Math.ceil(seg.segment[0] + minSize);
|
||||||
|
if (startTime <= timeInSeconds && endTime >= timeInSeconds) {
|
||||||
|
if (segmentLength < currentSegmentLength) {
|
||||||
|
currentSegmentLength = segmentLength;
|
||||||
|
segment = seg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getChapterChevron(): HTMLElement {
|
||||||
|
return document.querySelector(".ytp-chapter-title-chevron");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PreviewBar;
|
export default PreviewBar;
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ interface IsInfoFoundMessage {
|
|||||||
updating: boolean;
|
updating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SkipMessage {
|
||||||
|
message: "unskip" | "reskip";
|
||||||
|
UUID: SegmentUUID;
|
||||||
|
}
|
||||||
|
|
||||||
interface SubmitVoteMessage {
|
interface SubmitVoteMessage {
|
||||||
message: "submitVote";
|
message: "submitVote";
|
||||||
type: number;
|
type: number;
|
||||||
@@ -47,6 +52,11 @@ interface CopyToClipboardMessage {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ImportSegmentsMessage {
|
||||||
|
message: "importSegments";
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface KeyDownMessage {
|
interface KeyDownMessage {
|
||||||
message: "keydown";
|
message: "keydown";
|
||||||
key: string;
|
key: string;
|
||||||
@@ -59,12 +69,13 @@ interface KeyDownMessage {
|
|||||||
metaKey: boolean;
|
metaKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | KeyDownMessage);
|
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage);
|
||||||
|
|
||||||
export interface IsInfoFoundMessageResponse {
|
export interface IsInfoFoundMessageResponse {
|
||||||
found: boolean;
|
found: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
sponsorTimes: SponsorTime[];
|
sponsorTimes: SponsorTime[];
|
||||||
|
time: number;
|
||||||
onMobileYouTube: boolean;
|
onMobileYouTube: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +101,23 @@ export type MessageResponse =
|
|||||||
| GetChannelIDResponse
|
| GetChannelIDResponse
|
||||||
| SponsorStartResponse
|
| SponsorStartResponse
|
||||||
| IsChannelWhitelistedResponse
|
| IsChannelWhitelistedResponse
|
||||||
| Record<never, never> // empty object response {}
|
| Record<string, never> // empty object response {}
|
||||||
| VoteResponse;
|
| VoteResponse
|
||||||
|
| ImportSegmentsResponse;
|
||||||
|
|
||||||
export interface VoteResponse {
|
export interface VoteResponse {
|
||||||
successType: number;
|
successType: number;
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
responseText: string;
|
responseText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImportSegmentsResponse {
|
||||||
|
importedSegments: SponsorTime[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeUpdateMessage {
|
||||||
|
message: "time";
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PopupMessage = TimeUpdateMessage;
|
||||||
|
|||||||
@@ -10,12 +10,17 @@ window.SB = Config;
|
|||||||
|
|
||||||
import Utils from "./utils";
|
import Utils from "./utils";
|
||||||
import CategoryChooser from "./render/CategoryChooser";
|
import CategoryChooser from "./render/CategoryChooser";
|
||||||
import KeybindComponent from "./components/KeybindComponent";
|
import UnsubmittedVideos from "./render/UnsubmittedVideos";
|
||||||
|
import KeybindComponent from "./components/options/KeybindComponent";
|
||||||
import { showDonationLink } from "./utils/configUtils";
|
import { showDonationLink } from "./utils/configUtils";
|
||||||
import { localizeHtmlPage } from "./utils/pageUtils";
|
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||||
|
import { StorageChangesObject } from "./types";
|
||||||
const utils = new Utils();
|
const utils = new Utils();
|
||||||
let embed = false;
|
let embed = false;
|
||||||
|
|
||||||
|
const categoryChoosers: CategoryChooser[] = [];
|
||||||
|
const unsubmittedVideos: UnsubmittedVideos[] = [];
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', init);
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@@ -290,7 +295,10 @@ async function init() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "react-CategoryChooserComponent":
|
case "react-CategoryChooserComponent":
|
||||||
new CategoryChooser(optionsElements[i]);
|
categoryChoosers.push(new CategoryChooser(optionsElements[i]));
|
||||||
|
break;
|
||||||
|
case "react-UnsubmittedVideosComponent":
|
||||||
|
unsubmittedVideos.push(new UnsubmittedVideos(optionsElements[i]));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,10 +356,8 @@ async function shouldHideOption(element: Element): Promise<boolean> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the config is updated
|
* Called when the config is updated
|
||||||
*
|
|
||||||
* @param {String} element
|
|
||||||
*/
|
*/
|
||||||
function optionsConfigUpdateListener() {
|
function optionsConfigUpdateListener(changes: StorageChangesObject) {
|
||||||
const optionsContainer = document.getElementById("options");
|
const optionsContainer = document.getElementById("options");
|
||||||
const optionsElements = optionsContainer.querySelectorAll("*");
|
const optionsElements = optionsContainer.querySelectorAll("*");
|
||||||
|
|
||||||
@@ -359,6 +365,17 @@ function optionsConfigUpdateListener() {
|
|||||||
switch (optionsElements[i].getAttribute("data-type")) {
|
switch (optionsElements[i].getAttribute("data-type")) {
|
||||||
case "display":
|
case "display":
|
||||||
updateDisplayElement(<HTMLElement> optionsElements[i])
|
updateDisplayElement(<HTMLElement> optionsElements[i])
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.categorySelections || changes.payments) {
|
||||||
|
for (const chooser of categoryChoosers) {
|
||||||
|
chooser.update();
|
||||||
|
}
|
||||||
|
} else if (changes.unsubmittedSegments) {
|
||||||
|
for (const chooser of unsubmittedVideos) {
|
||||||
|
chooser.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,13 +469,7 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
|
|||||||
* @param option
|
* @param option
|
||||||
*/
|
*/
|
||||||
function invidiousInit(checkbox: HTMLInputElement, option: string) {
|
function invidiousInit(checkbox: HTMLInputElement, option: string) {
|
||||||
let permissions = ["declarativeContent"];
|
utils.containsInvidiousPermission().then((result) => {
|
||||||
if (utils.isFirefox()) permissions = [];
|
|
||||||
|
|
||||||
chrome.permissions.contains({
|
|
||||||
origins: utils.getPermissionRegex(),
|
|
||||||
permissions: permissions
|
|
||||||
}, function (result) {
|
|
||||||
if (result != checkbox.checked) {
|
if (result != checkbox.checked) {
|
||||||
Config.config[option] = result;
|
Config.config[option] = result;
|
||||||
|
|
||||||
@@ -474,22 +485,8 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
|
|||||||
* @param option
|
* @param option
|
||||||
*/
|
*/
|
||||||
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
|
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option);
|
||||||
if (checkbox.checked) {
|
checkbox.checked = enabled;
|
||||||
utils.setupExtraSitePermissions(function (granted) {
|
|
||||||
if (!granted) {
|
|
||||||
Config.config[option] = false;
|
|
||||||
checkbox.checked = false;
|
|
||||||
} else {
|
|
||||||
checkbox.checked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
utils.removeExtraSiteRegistration();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,25 +12,17 @@ window.addEventListener('DOMContentLoaded', init);
|
|||||||
async function init() {
|
async function init() {
|
||||||
localizeHtmlPage();
|
localizeHtmlPage();
|
||||||
|
|
||||||
const domains = document.location.hash.replace("#", "").split(",");
|
|
||||||
|
|
||||||
const acceptButton = document.getElementById("acceptPermissionButton");
|
const acceptButton = document.getElementById("acceptPermissionButton");
|
||||||
acceptButton.addEventListener("click", () => {
|
acceptButton.addEventListener("click", () => {
|
||||||
chrome.permissions.request({
|
utils.applyInvidiousPermissions(Config.config.supportInvidious).then((enabled) => {
|
||||||
origins: utils.getPermissionRegex(domains),
|
Config.config.supportInvidious = enabled;
|
||||||
permissions: []
|
|
||||||
}, (granted) => {
|
if (enabled) {
|
||||||
if (granted) {
|
|
||||||
alert(chrome.i18n.getMessage("permissionRequestSuccess"));
|
alert(chrome.i18n.getMessage("permissionRequestSuccess"));
|
||||||
|
window.close();
|
||||||
Config.config.ytInfoPermissionGranted = true;
|
|
||||||
|
|
||||||
chrome.tabs.getCurrent((tab) => {
|
|
||||||
chrome.tabs.remove(tab.id);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
alert(chrome.i18n.getMessage("permissionRequestFailed"));
|
alert(chrome.i18n.getMessage("permissionRequestFailed"));
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
465
src/popup.ts
465
src/popup.ts
@@ -1,12 +1,15 @@
|
|||||||
import Config from "./config";
|
import Config from "./config";
|
||||||
|
|
||||||
import Utils from "./utils";
|
import Utils from "./utils";
|
||||||
import { SponsorTime, SponsorHideType, ActionType, StorageChangesObject } from "./types";
|
import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject } from "./types";
|
||||||
import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes";
|
import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
|
||||||
import { showDonationLink } from "./utils/configUtils";
|
import { showDonationLink } from "./utils/configUtils";
|
||||||
import { AnimationUtils } from "./utils/animationUtils";
|
import { AnimationUtils } from "./utils/animationUtils";
|
||||||
import { GenericUtils } from "./utils/genericUtils";
|
import { GenericUtils } from "./utils/genericUtils";
|
||||||
|
import { shortCategoryName } from "./utils/categoryUtils";
|
||||||
import { localizeHtmlPage } from "./utils/pageUtils";
|
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||||
|
import { exportTimes } from "./utils/exporter";
|
||||||
|
import GenericNotice from "./render/GenericNotice";
|
||||||
const utils = new Utils();
|
const utils = new Utils();
|
||||||
|
|
||||||
interface MessageListener {
|
interface MessageListener {
|
||||||
@@ -68,13 +71,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
|
|
||||||
//the start and end time pairs (2d)
|
//the start and end time pairs (2d)
|
||||||
let sponsorTimes: SponsorTime[] = [];
|
let sponsorTimes: SponsorTime[] = [];
|
||||||
|
let downloadedTimes: SponsorTime[] = [];
|
||||||
|
|
||||||
//current video ID of this tab
|
//current video ID of this tab
|
||||||
let currentVideoID = null;
|
let currentVideoID = null;
|
||||||
|
|
||||||
|
enum SegmentTab {
|
||||||
|
Segments,
|
||||||
|
Chapters
|
||||||
|
}
|
||||||
|
let segmentTab = SegmentTab.Segments;
|
||||||
|
let port: chrome.runtime.Port = null;
|
||||||
|
|
||||||
const PageElements: PageElements = {};
|
const PageElements: PageElements = {};
|
||||||
|
|
||||||
[
|
[
|
||||||
|
"sponsorBlockPopupBody",
|
||||||
"sponsorblockPopup",
|
"sponsorblockPopup",
|
||||||
"sponsorStart",
|
"sponsorStart",
|
||||||
// Top toggles
|
// Top toggles
|
||||||
@@ -123,16 +135,26 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
"refreshSegmentsButton",
|
"refreshSegmentsButton",
|
||||||
"whitelistButton",
|
"whitelistButton",
|
||||||
"sbDonate",
|
"sbDonate",
|
||||||
|
"issueReporterTabs",
|
||||||
|
"issueReporterTabSegments",
|
||||||
|
"issueReporterTabChapters",
|
||||||
"sponsorTimesDonateContainer",
|
"sponsorTimesDonateContainer",
|
||||||
"sbConsiderDonateLink",
|
"sbConsiderDonateLink",
|
||||||
"sbCloseDonate",
|
"sbCloseDonate",
|
||||||
"sbBetaServerWarning",
|
"sbBetaServerWarning",
|
||||||
"sbCloseButton"
|
"sbCloseButton",
|
||||||
|
"issueReporterImportExport",
|
||||||
|
"importSegmentsButton",
|
||||||
|
"exportSegmentsButton",
|
||||||
|
"importSegmentsMenu",
|
||||||
|
"importSegmentsText",
|
||||||
|
"importSegmentsSubmit"
|
||||||
|
|
||||||
].forEach(id => PageElements[id] = document.getElementById(id));
|
].forEach(id => PageElements[id] = document.getElementById(id));
|
||||||
|
|
||||||
getSegmentsFromContentScript(false);
|
getSegmentsFromContentScript(false);
|
||||||
await utils.wait(() => Config.config !== null && allowPopup, 5000, 5);
|
await utils.wait(() => Config.config !== null && allowPopup, 5000, 5);
|
||||||
document.querySelector("body").style.removeProperty("visibility");
|
PageElements.sponsorBlockPopupBody.style.removeProperty("visibility");
|
||||||
if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
|
if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
|
||||||
Config.configSyncListeners.push(contentConfigUpdateListener);
|
Config.configSyncListeners.push(contentConfigUpdateListener);
|
||||||
}
|
}
|
||||||
@@ -145,6 +167,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
|
|
||||||
if (window !== window.top) {
|
if (window !== window.top) {
|
||||||
PageElements.sbCloseButton.classList.remove("hidden");
|
PageElements.sbCloseButton.classList.remove("hidden");
|
||||||
|
PageElements.sponsorBlockPopupBody.classList.add("is-embedded");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide donate button if wanted (Safari, or user choice)
|
// Hide donate button if wanted (Safari, or user choice)
|
||||||
@@ -160,7 +183,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//setup click listeners
|
PageElements.exportSegmentsButton.addEventListener("click", exportSegments);
|
||||||
|
PageElements.importSegmentsButton.addEventListener("click",
|
||||||
|
() => PageElements.importSegmentsMenu.classList.toggle("hidden"));
|
||||||
|
PageElements.importSegmentsSubmit.addEventListener("click", importSegments);
|
||||||
|
|
||||||
PageElements.sponsorStart.addEventListener("click", sendSponsorStartMessage);
|
PageElements.sponsorStart.addEventListener("click", sendSponsorStartMessage);
|
||||||
PageElements.whitelistToggle.addEventListener("change", function () {
|
PageElements.whitelistToggle.addEventListener("change", function () {
|
||||||
if (this.checked) {
|
if (this.checked) {
|
||||||
@@ -213,6 +240,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupComPort();
|
||||||
|
|
||||||
//show proper disable skipping button
|
//show proper disable skipping button
|
||||||
const disableSkipping = Config.config.disableSkipping;
|
const disableSkipping = Config.config.disableSkipping;
|
||||||
if (disableSkipping != undefined && disableSkipping) {
|
if (disableSkipping != undefined && disableSkipping) {
|
||||||
@@ -228,7 +257,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
PageElements.showNoticeAgain.style.display = "unset";
|
PageElements.showNoticeAgain.style.display = "unset";
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&userID=" + Config.config.userID, (res) => {
|
utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&value=permissions&value=freeChaptersAccess&userID="
|
||||||
|
+ Config.config.userID, (res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const userInfo = JSON.parse(res.responseText);
|
const userInfo = JSON.parse(res.responseText);
|
||||||
PageElements.usernameValue.innerText = userInfo.userName;
|
PageElements.usernameValue.innerText = userInfo.userName;
|
||||||
@@ -257,6 +287,14 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Config.config.isVip = userInfo.vip;
|
Config.config.isVip = userInfo.vip;
|
||||||
|
Config.config.permissions = userInfo.permissions;
|
||||||
|
|
||||||
|
if (userInfo.freeChaptersAccess) {
|
||||||
|
Config.config.payments.chaptersAllowed = userInfo.freeChaptersAccess;
|
||||||
|
Config.config.payments.freeAccess = userInfo.freeChaptersAccess;
|
||||||
|
Config.config.payments.lastCheck = Date.now();
|
||||||
|
Config.forceSyncUpdate("payments");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -292,6 +330,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
// Must be delayed so it only happens once loaded
|
// Must be delayed so it only happens once loaded
|
||||||
setTimeout(() => PageElements.sponsorblockPopup.classList.remove("preload"), 250);
|
setTimeout(() => PageElements.sponsorblockPopup.classList.remove("preload"), 250);
|
||||||
|
|
||||||
|
PageElements.issueReporterTabSegments.addEventListener("click", () => {
|
||||||
|
PageElements.issueReporterTabSegments.classList.add("sbSelected");
|
||||||
|
PageElements.issueReporterTabChapters.classList.remove("sbSelected");
|
||||||
|
|
||||||
|
segmentTab = SegmentTab.Segments;
|
||||||
|
getSegmentsFromContentScript(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
PageElements.issueReporterTabChapters.addEventListener("click", () => {
|
||||||
|
PageElements.issueReporterTabSegments.classList.remove("sbSelected");
|
||||||
|
PageElements.issueReporterTabChapters.classList.add("sbSelected");
|
||||||
|
|
||||||
|
segmentTab = SegmentTab.Chapters;
|
||||||
|
getSegmentsFromContentScript(true);
|
||||||
|
});
|
||||||
|
|
||||||
function showDonateWidget(viewCount: number) {
|
function showDonateWidget(viewCount: number) {
|
||||||
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
|
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
|
||||||
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) {
|
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) {
|
||||||
@@ -363,10 +417,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
PageElements.whitelistButton.classList.remove("hidden");
|
PageElements.whitelistButton.classList.remove("hidden");
|
||||||
PageElements.loadingIndicator.style.display = "none";
|
PageElements.loadingIndicator.style.display = "none";
|
||||||
|
|
||||||
|
downloadedTimes = request.sponsorTimes ?? [];
|
||||||
if (request.found) {
|
if (request.found) {
|
||||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
|
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
|
||||||
|
|
||||||
displayDownloadedSponsorTimes(request);
|
if (request.sponsorTimes) {
|
||||||
|
displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
|
||||||
|
}
|
||||||
} else if (request.status == 404 || request.status == 200) {
|
} else if (request.status == 404 || request.status == 200) {
|
||||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
|
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
|
||||||
} else {
|
} else {
|
||||||
@@ -439,165 +496,205 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//display the video times from the array at the top, in a different section
|
//display the video times from the array at the top, in a different section
|
||||||
function displayDownloadedSponsorTimes(request: { found: boolean, sponsorTimes: SponsorTime[] }) {
|
function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
|
||||||
if (request.sponsorTimes != undefined) {
|
let currentSegmentTab = segmentTab;
|
||||||
// Sort list by start time
|
if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter)) {
|
||||||
const segmentTimes = request.sponsorTimes
|
PageElements.issueReporterTabs.classList.add("hidden");
|
||||||
.sort((a, b) => a.segment[1] - b.segment[1])
|
currentSegmentTab = SegmentTab.Segments;
|
||||||
.sort((a, b) => a.segment[0] - b.segment[0]);
|
} else {
|
||||||
|
PageElements.issueReporterTabs.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
//add them as buttons to the issue reporting container
|
// Sort list by start time
|
||||||
const container = document.getElementById("issueReporterTimeButtons");
|
const downloadedTimes = sponsorTimes
|
||||||
while (container.firstChild) {
|
.filter((segment) => {
|
||||||
container.removeChild(container.firstChild);
|
if (currentSegmentTab === SegmentTab.Segments) {
|
||||||
|
return segment.actionType !== ActionType.Chapter;
|
||||||
|
} else if (currentSegmentTab === SegmentTab.Chapters) {
|
||||||
|
return segment.actionType === ActionType.Chapter
|
||||||
|
&& segment.source !== SponsorSourceType.YouTube;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.segment[1] - b.segment[1])
|
||||||
|
.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||||
|
|
||||||
|
//add them as buttons to the issue reporting container
|
||||||
|
const container = document.getElementById("issueReporterTimeButtons");
|
||||||
|
while (container.firstChild) {
|
||||||
|
container.removeChild(container.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadedTimes.length > 0) {
|
||||||
|
PageElements.exportSegmentsButton.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
PageElements.exportSegmentsButton.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVip = Config.config.isVip;
|
||||||
|
for (let i = 0; i < downloadedTimes.length; i++) {
|
||||||
|
const UUID = downloadedTimes[i].UUID;
|
||||||
|
const locked = downloadedTimes[i].locked;
|
||||||
|
const category = downloadedTimes[i].category;
|
||||||
|
const actionType = downloadedTimes[i].actionType;
|
||||||
|
|
||||||
|
const segmentSummary = document.createElement("summary");
|
||||||
|
segmentSummary.classList.add("segmentSummary");
|
||||||
|
if (time >= downloadedTimes[i].segment[0]) {
|
||||||
|
if (time < downloadedTimes[i].segment[1]) {
|
||||||
|
segmentSummary.classList.add("segmentActive");
|
||||||
|
} else {
|
||||||
|
segmentSummary.classList.add("segmentPassed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isVip = Config.config.isVip;
|
const categoryColorCircle = document.createElement("span");
|
||||||
for (let i = 0; i < segmentTimes.length; i++) {
|
categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
|
||||||
const UUID = segmentTimes[i].UUID;
|
categoryColorCircle.style.backgroundColor = Config.config.barTypes[category]?.color;
|
||||||
const locked = segmentTimes[i].locked;
|
categoryColorCircle.classList.add("dot");
|
||||||
|
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
|
||||||
|
|
||||||
const segmentSummary = document.createElement("summary");
|
let extraInfo = "";
|
||||||
segmentSummary.className = "segmentSummary";
|
if (downloadedTimes[i].hidden === SponsorHideType.Downvoted) {
|
||||||
|
//this one is downvoted
|
||||||
|
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
|
||||||
|
} else if (downloadedTimes[i].hidden === SponsorHideType.MinimumDuration) {
|
||||||
|
//this one is too short
|
||||||
|
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
|
||||||
|
} else if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||||
|
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
|
||||||
|
}
|
||||||
|
|
||||||
const categoryColorCircle = document.createElement("span");
|
const name = downloadedTimes[i].description || shortCategoryName(category);
|
||||||
categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
|
const textNode = document.createTextNode(name + extraInfo);
|
||||||
categoryColorCircle.style.backgroundColor = Config.config.barTypes[segmentTimes[i].category]?.color;
|
const segmentTimeFromToNode = document.createElement("div");
|
||||||
categoryColorCircle.classList.add("dot");
|
if (downloadedTimes[i].actionType === ActionType.Full) {
|
||||||
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
|
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
|
||||||
|
} else {
|
||||||
|
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
|
||||||
|
(actionType !== ActionType.Poi
|
||||||
|
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(downloadedTimes[i].segment[1], true)
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
|
||||||
let extraInfo = "";
|
segmentTimeFromToNode.style.margin = "5px";
|
||||||
if (segmentTimes[i].hidden === SponsorHideType.Downvoted) {
|
|
||||||
//this one is downvoted
|
|
||||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
|
|
||||||
} else if (segmentTimes[i].hidden === SponsorHideType.MinimumDuration) {
|
|
||||||
//this one is too short
|
|
||||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
|
|
||||||
} else if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
|
|
||||||
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo);
|
// for inline-styling purposes
|
||||||
const segmentTimeFromToNode = document.createElement("div");
|
const labelContainer = document.createElement("div");
|
||||||
if (segmentTimes[i].actionType === ActionType.Full) {
|
if (actionType !== ActionType.Chapter) labelContainer.appendChild(categoryColorCircle);
|
||||||
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
|
|
||||||
} else {
|
|
||||||
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
|
|
||||||
(segmentTimes[i].actionType !== ActionType.Poi
|
|
||||||
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
|
|
||||||
: "");
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentTimeFromToNode.style.margin = "5px";
|
const span = document.createElement('span');
|
||||||
|
span.className = "summaryLabel";
|
||||||
|
span.appendChild(textNode);
|
||||||
|
labelContainer.appendChild(span);
|
||||||
|
|
||||||
// for inline-styling purposes
|
segmentSummary.appendChild(labelContainer);
|
||||||
const labelContainer = document.createElement("div");
|
segmentSummary.appendChild(segmentTimeFromToNode);
|
||||||
labelContainer.appendChild(categoryColorCircle);
|
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const votingButtons = document.createElement("details");
|
||||||
span.className = "summaryLabel";
|
votingButtons.classList.add("votingButtons");
|
||||||
span.appendChild(textNode);
|
|
||||||
labelContainer.appendChild(span);
|
|
||||||
// for inline-styling purposes
|
|
||||||
|
|
||||||
segmentSummary.appendChild(labelContainer);
|
//thumbs up and down buttons
|
||||||
segmentSummary.appendChild(segmentTimeFromToNode);
|
const voteButtonsContainer = document.createElement("div");
|
||||||
|
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
|
||||||
|
voteButtonsContainer.classList.add("sbVoteButtonsContainer");
|
||||||
|
|
||||||
const votingButtons = document.createElement("details");
|
const upvoteButton = document.createElement("img");
|
||||||
votingButtons.classList.add("votingButtons");
|
upvoteButton.id = "sponsorTimesUpvoteButtonsContainer" + UUID;
|
||||||
|
upvoteButton.className = "voteButton";
|
||||||
|
upvoteButton.title = chrome.i18n.getMessage("upvote");
|
||||||
|
upvoteButton.src = chrome.runtime.getURL("icons/thumbs_up.svg");
|
||||||
|
upvoteButton.addEventListener("click", () => vote(1, UUID));
|
||||||
|
|
||||||
//thumbs up and down buttons
|
const downvoteButton = document.createElement("img");
|
||||||
const voteButtonsContainer = document.createElement("div");
|
downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
|
||||||
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
|
downvoteButton.className = "voteButton";
|
||||||
voteButtonsContainer.classList.add("sbVoteButtonsContainer");
|
downvoteButton.title = chrome.i18n.getMessage("downvote");
|
||||||
|
downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg");
|
||||||
|
downvoteButton.addEventListener("click", () => vote(0, UUID));
|
||||||
|
|
||||||
const upvoteButton = document.createElement("img");
|
const uuidButton = document.createElement("img");
|
||||||
upvoteButton.id = "sponsorTimesUpvoteButtonsContainer" + UUID;
|
uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
|
||||||
upvoteButton.className = "voteButton";
|
uuidButton.className = "voteButton";
|
||||||
upvoteButton.title = chrome.i18n.getMessage("upvote");
|
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
|
||||||
upvoteButton.src = chrome.runtime.getURL("icons/thumbs_up.svg");
|
uuidButton.title = chrome.i18n.getMessage("copySegmentID");
|
||||||
upvoteButton.addEventListener("click", () => vote(1, UUID));
|
uuidButton.addEventListener("click", () => {
|
||||||
|
copyToClipboard(UUID);
|
||||||
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
|
||||||
|
stopAnimation();
|
||||||
|
});
|
||||||
|
|
||||||
const downvoteButton = document.createElement("img");
|
const hideButton = document.createElement("img");
|
||||||
downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
|
hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
|
||||||
downvoteButton.className = "voteButton";
|
hideButton.className = "voteButton";
|
||||||
downvoteButton.title = chrome.i18n.getMessage("downvote");
|
hideButton.title = chrome.i18n.getMessage("hideSegment");
|
||||||
downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg");
|
if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||||
downvoteButton.addEventListener("click", () => vote(0, UUID));
|
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
||||||
|
} else {
|
||||||
|
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
||||||
|
}
|
||||||
|
hideButton.addEventListener("click", () => {
|
||||||
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
|
||||||
|
stopAnimation();
|
||||||
|
|
||||||
const uuidButton = document.createElement("img");
|
if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||||
uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
|
|
||||||
uuidButton.className = "voteButton";
|
|
||||||
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
|
|
||||||
uuidButton.title = chrome.i18n.getMessage("copySegmentID");
|
|
||||||
uuidButton.addEventListener("click", () => {
|
|
||||||
copyToClipboard(UUID);
|
|
||||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
|
|
||||||
stopAnimation();
|
|
||||||
});
|
|
||||||
|
|
||||||
const hideButton = document.createElement("img");
|
|
||||||
hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
|
|
||||||
hideButton.className = "voteButton";
|
|
||||||
hideButton.title = chrome.i18n.getMessage("hideSegment");
|
|
||||||
if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
|
|
||||||
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
|
||||||
} else {
|
|
||||||
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
||||||
|
downloadedTimes[i].hidden = SponsorHideType.Visible;
|
||||||
|
} else {
|
||||||
|
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
||||||
|
downloadedTimes[i].hidden = SponsorHideType.Hidden;
|
||||||
}
|
}
|
||||||
hideButton.addEventListener("click", () => {
|
|
||||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
|
|
||||||
stopAnimation();
|
|
||||||
|
|
||||||
if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
|
messageHandler.query({
|
||||||
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
active: true,
|
||||||
segmentTimes[i].hidden = SponsorHideType.Visible;
|
currentWindow: true
|
||||||
} else {
|
}, tabs => {
|
||||||
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
messageHandler.sendMessage(
|
||||||
segmentTimes[i].hidden = SponsorHideType.Hidden;
|
tabs[0].id,
|
||||||
}
|
{
|
||||||
|
message: "hideSegment",
|
||||||
messageHandler.query({
|
type: downloadedTimes[i].hidden,
|
||||||
active: true,
|
UUID: UUID
|
||||||
currentWindow: true
|
}
|
||||||
}, tabs => {
|
);
|
||||||
messageHandler.sendMessage(
|
|
||||||
tabs[0].id,
|
|
||||||
{
|
|
||||||
message: "hideSegment",
|
|
||||||
type: segmentTimes[i].hidden,
|
|
||||||
UUID: UUID
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//add thumbs up, thumbs down and uuid copy buttons to the container
|
const skipButton = document.createElement("img");
|
||||||
voteButtonsContainer.appendChild(upvoteButton);
|
skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
|
||||||
voteButtonsContainer.appendChild(downvoteButton);
|
skipButton.className = "voteButton";
|
||||||
voteButtonsContainer.appendChild(uuidButton);
|
skipButton.src = chrome.runtime.getURL("icons/skip.svg");
|
||||||
if (segmentTimes[i].actionType === ActionType.Skip
|
skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
|
||||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segmentTimes[i].hidden)) {
|
votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
|
||||||
voteButtonsContainer.appendChild(hideButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Will contain request status
|
//add thumbs up, thumbs down and uuid copy buttons to the container
|
||||||
const voteStatusContainer = document.createElement("div");
|
voteButtonsContainer.appendChild(upvoteButton);
|
||||||
voteStatusContainer.id = "sponsorTimesVoteStatusContainer" + UUID;
|
voteButtonsContainer.appendChild(downvoteButton);
|
||||||
voteStatusContainer.classList.add("sponsorTimesVoteStatusContainer");
|
voteButtonsContainer.appendChild(uuidButton);
|
||||||
voteStatusContainer.style.display = "none";
|
if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
|
||||||
|
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
|
||||||
const thanksForVotingText = document.createElement("div");
|
voteButtonsContainer.appendChild(hideButton);
|
||||||
thanksForVotingText.id = "sponsorTimesThanksForVotingText" + UUID;
|
|
||||||
thanksForVotingText.classList.add("sponsorTimesThanksForVotingText");
|
|
||||||
voteStatusContainer.appendChild(thanksForVotingText);
|
|
||||||
|
|
||||||
votingButtons.append(segmentSummary);
|
|
||||||
votingButtons.append(voteButtonsContainer);
|
|
||||||
votingButtons.append(voteStatusContainer);
|
|
||||||
|
|
||||||
container.appendChild(votingButtons);
|
|
||||||
}
|
}
|
||||||
|
voteButtonsContainer.appendChild(skipButton);
|
||||||
|
|
||||||
|
|
||||||
|
// Will contain request status
|
||||||
|
const voteStatusContainer = document.createElement("div");
|
||||||
|
voteStatusContainer.id = "sponsorTimesVoteStatusContainer" + UUID;
|
||||||
|
voteStatusContainer.classList.add("sponsorTimesVoteStatusContainer");
|
||||||
|
voteStatusContainer.style.display = "none";
|
||||||
|
|
||||||
|
const thanksForVotingText = document.createElement("div");
|
||||||
|
thanksForVotingText.id = "sponsorTimesThanksForVotingText" + UUID;
|
||||||
|
thanksForVotingText.classList.add("sponsorTimesThanksForVotingText");
|
||||||
|
voteStatusContainer.appendChild(thanksForVotingText);
|
||||||
|
|
||||||
|
votingButtons.append(segmentSummary);
|
||||||
|
votingButtons.append(voteButtonsContainer);
|
||||||
|
votingButtons.append(voteStatusContainer);
|
||||||
|
|
||||||
|
container.appendChild(votingButtons);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,6 +803,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
//this is not a YouTube video page
|
//this is not a YouTube video page
|
||||||
function displayNoVideo() {
|
function displayNoVideo() {
|
||||||
document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID");
|
document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID");
|
||||||
|
|
||||||
|
PageElements.issueReporterTabs.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
function addVoteMessage(message, UUID) {
|
function addVoteMessage(message, UUID) {
|
||||||
@@ -879,6 +978,37 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skipSegment(actionType: ActionType, UUID: SegmentUUID, element?: HTMLElement): void {
|
||||||
|
if (actionType === ActionType.Chapter) {
|
||||||
|
sendMessage({
|
||||||
|
message: "unskip",
|
||||||
|
UUID: UUID
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendMessage({
|
||||||
|
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)
|
* Should skipping be disabled (visuals stay)
|
||||||
*/
|
*/
|
||||||
@@ -908,6 +1038,41 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function importSegments() {
|
||||||
|
const text = (PageElements.importSegmentsText as HTMLInputElement).value;
|
||||||
|
|
||||||
|
await sendTabMessage({
|
||||||
|
message: "importSegments",
|
||||||
|
data: text
|
||||||
|
}) as ImportSegmentsResponse;
|
||||||
|
|
||||||
|
PageElements.importSegmentsMenu.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSegments() {
|
||||||
|
copyToClipboard(exportTimes(downloadedTimes));
|
||||||
|
|
||||||
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.exportSegmentsButton, 0.3);
|
||||||
|
stopAnimation();
|
||||||
|
new GenericNotice(null, "exportCopied", {
|
||||||
|
title: chrome.i18n.getMessage(`CopiedExclamation`),
|
||||||
|
timed: true,
|
||||||
|
maxCountdownTime: () => 0.6,
|
||||||
|
referenceNode: PageElements.exportSegmentsButton.parentElement,
|
||||||
|
dontPauseCountdown: true,
|
||||||
|
style: {
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
right: "30px",
|
||||||
|
margin: "auto",
|
||||||
|
height: "max-content"
|
||||||
|
},
|
||||||
|
hideLogo: true,
|
||||||
|
hideRightInfo: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts time in minutes to 2d 5h 25.1
|
* Converts time in minutes to 2d 5h 25.1
|
||||||
* If less than 1 hour, just returns minutes
|
* If less than 1 hour, just returns minutes
|
||||||
@@ -932,6 +1097,20 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupComPort(): void {
|
||||||
|
port = chrome.runtime.connect({ name: "popup" });
|
||||||
|
port.onDisconnect.addListener(() => setupComPort());
|
||||||
|
port.onMessage.addListener((msg) => onMessage(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(msg: PopupMessage) {
|
||||||
|
switch (msg.message) {
|
||||||
|
case "time":
|
||||||
|
displayDownloadedSponsorTimes(downloadedTimes, msg.time);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runThePopup();
|
runThePopup();
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import CategoryChooserComponent from "../components/CategoryChooserComponent";
|
import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
|
||||||
|
|
||||||
class CategoryChooser {
|
class CategoryChooser {
|
||||||
|
|
||||||
|
ref: React.RefObject<CategoryChooserComponent>;
|
||||||
|
|
||||||
constructor(element: Element) {
|
constructor(element: Element) {
|
||||||
|
this.ref = React.createRef();
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<CategoryChooserComponent/>,
|
<CategoryChooserComponent ref={this.ref} />,
|
||||||
element
|
element
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(): void {
|
||||||
|
this.ref.current?.forceUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CategoryChooser;
|
export default CategoryChooser;
|
||||||
63
src/render/ChapterVote.tsx
Normal file
63
src/render/ChapterVote.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent";
|
||||||
|
import { VoteResponse } from "../messageTypes";
|
||||||
|
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||||
|
|
||||||
|
export class ChapterVote {
|
||||||
|
container: HTMLElement;
|
||||||
|
ref: React.RefObject<ChapterVoteComponent>;
|
||||||
|
|
||||||
|
unsavedState: ChapterVoteState;
|
||||||
|
|
||||||
|
mutationObserver?: MutationObserver;
|
||||||
|
|
||||||
|
constructor(vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>) {
|
||||||
|
this.ref = React.createRef();
|
||||||
|
|
||||||
|
this.container = document.createElement('span');
|
||||||
|
this.container.id = "chapterVote";
|
||||||
|
this.container.style.height = "100%";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<ChapterVoteComponent ref={this.ref} vote={vote} />,
|
||||||
|
this.container
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getContainer(): HTMLElement {
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
ReactDOM.unmountComponentAtNode(this.container);
|
||||||
|
this.container.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisibility(show: boolean): void {
|
||||||
|
const newState = {
|
||||||
|
show,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.ref.current) {
|
||||||
|
this.ref.current?.setState(newState);
|
||||||
|
} else {
|
||||||
|
this.unsavedState = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSegment(segment: SponsorTime): Promise<void> {
|
||||||
|
if (this.ref.current?.state?.segment !== segment) {
|
||||||
|
const newState = {
|
||||||
|
segment,
|
||||||
|
show: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.ref.current) {
|
||||||
|
this.ref.current?.setState(newState);
|
||||||
|
} else {
|
||||||
|
this.unsavedState = newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,9 @@ import NoticeComponent from "../components/NoticeComponent";
|
|||||||
import Utils from "../utils";
|
import Utils from "../utils";
|
||||||
const utils = new Utils();
|
const utils = new Utils();
|
||||||
|
|
||||||
import { ContentContainer } from "../types";
|
import { ButtonListener, ContentContainer } from "../types";
|
||||||
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
|
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
|
||||||
|
|
||||||
export interface ButtonListener {
|
|
||||||
name: string,
|
|
||||||
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TextBox {
|
export interface TextBox {
|
||||||
icon: string,
|
icon: string,
|
||||||
text: string
|
text: string
|
||||||
@@ -20,12 +15,17 @@ export interface TextBox {
|
|||||||
|
|
||||||
export interface NoticeOptions {
|
export interface NoticeOptions {
|
||||||
title: string,
|
title: string,
|
||||||
|
referenceNode?: HTMLElement,
|
||||||
textBoxes?: TextBox[],
|
textBoxes?: TextBox[],
|
||||||
buttons?: ButtonListener[],
|
buttons?: ButtonListener[],
|
||||||
fadeIn?: boolean,
|
fadeIn?: boolean,
|
||||||
timed?: boolean
|
timed?: boolean
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
extraClass?: string;
|
extraClass?: string;
|
||||||
|
maxCountdownTime?: () => number;
|
||||||
|
dontPauseCountdown?: boolean;
|
||||||
|
hideLogo?: boolean;
|
||||||
|
hideRightInfo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GenericNotice {
|
export default class GenericNotice {
|
||||||
@@ -42,7 +42,7 @@ export default class GenericNotice {
|
|||||||
|
|
||||||
this.contentContainer = contentContainer;
|
this.contentContainer = contentContainer;
|
||||||
|
|
||||||
const referenceNode = utils.findReferenceNode();
|
const referenceNode = options.referenceNode ?? utils.findReferenceNode();
|
||||||
|
|
||||||
this.noticeElement = document.createElement("div");
|
this.noticeElement = document.createElement("div");
|
||||||
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
|
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
|
||||||
@@ -62,9 +62,19 @@ export default class GenericNotice {
|
|||||||
ref={this.noticeRef}
|
ref={this.noticeRef}
|
||||||
style={options.style}
|
style={options.style}
|
||||||
extraClass={options.extraClass}
|
extraClass={options.extraClass}
|
||||||
|
maxCountdownTime={options.maxCountdownTime}
|
||||||
|
dontPauseCountdown={options.dontPauseCountdown}
|
||||||
|
hideLogo={options.hideLogo}
|
||||||
|
hideRightInfo={options.hideRightInfo}
|
||||||
closeListener={() => this.close()} >
|
closeListener={() => this.close()} >
|
||||||
|
|
||||||
{this.getMessageBox(this.idSuffix, options.textBoxes)}
|
<tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}
|
||||||
|
className="sponsorTimeMessagesRow"
|
||||||
|
style={{maxHeight: (this.contentContainer().v.offsetHeight - 200) + "px"}}>
|
||||||
|
<td style={{width: "100%"}}>
|
||||||
|
{this.getMessageBoxes(this.idSuffix, options.textBoxes)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr id={"sponsorSkipNoticeSpacer" + this.idSuffix}
|
<tr id={"sponsorSkipNoticeSpacer" + this.idSuffix}
|
||||||
className="sponsorBlockSpacer">
|
className="sponsorBlockSpacer">
|
||||||
@@ -81,7 +91,7 @@ export default class GenericNotice {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMessageBox(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
|
getMessageBoxes(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
|
||||||
if (textBoxes) {
|
if (textBoxes) {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let i = 0; i < textBoxes.length; i++) {
|
for (let i = 0; i < textBoxes.length; i++) {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export class RectangleTooltip {
|
|||||||
props.fontSize ??= "10px";
|
props.fontSize ??= "10px";
|
||||||
|
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement('div');
|
||||||
props.htmlId ??= props.text;
|
props.htmlId ??= "sponsorRectangleTooltip" + props.text;
|
||||||
this.container.id = "sponsorRectangleTooltip" + props.htmlId;
|
this.container.id = props.htmlId;
|
||||||
this.container.style.display = "relative";
|
this.container.style.display = "relative";
|
||||||
|
|
||||||
if (props.prependElement) {
|
if (props.prependElement) {
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
|
import { ButtonListener } from "../types";
|
||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
text: string,
|
text?: string;
|
||||||
link?: string,
|
link?: string;
|
||||||
referenceNode: HTMLElement,
|
referenceNode: HTMLElement;
|
||||||
prependElement?: HTMLElement, // Element to append before
|
prependElement?: HTMLElement; // Element to append before
|
||||||
bottomOffset?: string
|
bottomOffset?: string;
|
||||||
|
leftOffset?: string;
|
||||||
|
rightOffset?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
displayTriangle?: boolean;
|
displayTriangle?: boolean;
|
||||||
|
extraClass?: string;
|
||||||
showLogo?: boolean;
|
showLogo?: boolean;
|
||||||
showGotIt?: boolean;
|
showGotIt?: boolean;
|
||||||
|
buttons?: ButtonListener[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Tooltip {
|
export class Tooltip {
|
||||||
text: string;
|
text?: string;
|
||||||
container: HTMLDivElement;
|
container: HTMLDivElement;
|
||||||
|
|
||||||
timer: NodeJS.Timeout;
|
timer: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(props: TooltipProps) {
|
constructor(props: TooltipProps) {
|
||||||
props.bottomOffset ??= "70px";
|
props.bottomOffset ??= "70px";
|
||||||
|
props.leftOffset ??= "inherit";
|
||||||
|
props.rightOffset ??= "inherit";
|
||||||
props.opacity ??= 0.7;
|
props.opacity ??= 0.7;
|
||||||
props.displayTriangle ??= true;
|
props.displayTriangle ??= true;
|
||||||
|
props.extraClass ??= "";
|
||||||
props.showLogo ??= true;
|
props.showLogo ??= true;
|
||||||
props.showGotIt ??= true;
|
props.showGotIt ??= true;
|
||||||
this.text = props.text;
|
this.text = props.text;
|
||||||
@@ -45,25 +53,29 @@ export class Tooltip {
|
|||||||
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
|
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<div style={{bottom: props.bottomOffset, backgroundColor}}
|
<div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
|
||||||
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "")} >
|
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
|
||||||
<div>
|
<div>
|
||||||
{props.showLogo ?
|
{props.showLogo ?
|
||||||
<img className="sponsorSkipLogo sponsorSkipObject"
|
<img className="sponsorSkipLogo sponsorSkipObject"
|
||||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||||
</img>
|
</img>
|
||||||
: null}
|
: null}
|
||||||
<span className="sponsorSkipObject">
|
{this.text ?
|
||||||
{this.text + (props.link ? ". " : "")}
|
<span className="sponsorSkipObject">
|
||||||
{props.link ?
|
{this.text + (props.link ? ". " : "")}
|
||||||
<a style={{textDecoration: "underline"}}
|
{props.link ?
|
||||||
target="_blank"
|
<a style={{textDecoration: "underline"}}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
href={props.link}>
|
rel="noopener noreferrer"
|
||||||
{chrome.i18n.getMessage("LearnMore")}
|
href={props.link}>
|
||||||
</a>
|
{chrome.i18n.getMessage("LearnMore")}
|
||||||
: null}
|
</a>
|
||||||
</span>
|
: null}
|
||||||
|
</span>
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{this.getButtons(props.buttons)}
|
||||||
</div>
|
</div>
|
||||||
{props.showGotIt ?
|
{props.showGotIt ?
|
||||||
<button className="sponsorSkipObject sponsorSkipNoticeButton"
|
<button className="sponsorSkipObject sponsorSkipNoticeButton"
|
||||||
@@ -78,6 +90,27 @@ export class Tooltip {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getButtons(buttons?: ButtonListener[]): JSX.Element[] {
|
||||||
|
if (buttons) {
|
||||||
|
const result: JSX.Element[] = [];
|
||||||
|
|
||||||
|
for (const button of buttons) {
|
||||||
|
result.push(
|
||||||
|
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
|
||||||
|
key={button.name}
|
||||||
|
onClick={(e) => button.listener(e)}>
|
||||||
|
|
||||||
|
{button.name}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
ReactDOM.unmountComponentAtNode(this.container);
|
ReactDOM.unmountComponentAtNode(this.container);
|
||||||
this.container.remove();
|
this.container.remove();
|
||||||
|
|||||||
24
src/render/UnsubmittedVideos.tsx
Normal file
24
src/render/UnsubmittedVideos.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import UnsubmittedVideosComponent from "../components/options/UnsubmittedVideosComponent";
|
||||||
|
|
||||||
|
class UnsubmittedVideos {
|
||||||
|
|
||||||
|
ref: React.RefObject<UnsubmittedVideosComponent>;
|
||||||
|
|
||||||
|
constructor(element: Element) {
|
||||||
|
this.ref = React.createRef();
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<UnsubmittedVideosComponent ref={this.ref} />,
|
||||||
|
element
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(): void {
|
||||||
|
this.ref.current?.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnsubmittedVideos;
|
||||||
22
src/svg-icons/lock_svg.tsx
Normal file
22
src/svg-icons/lock_svg.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const lockSvg = ({
|
||||||
|
fill = "#fcba03",
|
||||||
|
className = "",
|
||||||
|
width = "20",
|
||||||
|
height = "20",
|
||||||
|
onClick
|
||||||
|
}): JSX.Element => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={width}
|
||||||
|
width={height}
|
||||||
|
className={className}
|
||||||
|
fill={fill}
|
||||||
|
onClick={onClick} >
|
||||||
|
<path
|
||||||
|
d="M5.5 18q-.625 0-1.062-.438Q4 17.125 4 16.5v-8q0-.625.438-1.062Q4.875 7 5.5 7H6V5q0-1.667 1.167-2.833Q8.333 1 10 1q1.667 0 2.833 1.167Q14 3.333 14 5v2h.5q.625 0 1.062.438Q16 7.875 16 8.5v8q0 .625-.438 1.062Q15.125 18 14.5 18Zm4.5-4q.625 0 1.062-.438.438-.437.438-1.062t-.438-1.062Q10.625 11 10 11t-1.062.438Q8.5 11.875 8.5 12.5t.438 1.062Q9.375 14 10 14ZM7.5 7h5V5q0-1.042-.729-1.771Q11.042 2.5 10 2.5q-1.042 0-1.771.729Q7.5 3.958 7.5 5Z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default lockSvg;
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
const thumbsDownSvg = ({
|
const thumbsDownSvg = ({
|
||||||
fill = "#ffffff"
|
fill = "#ffffff",
|
||||||
|
className = "",
|
||||||
|
width = "18",
|
||||||
|
height = "18"
|
||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="18"
|
width={width}
|
||||||
height="18"
|
height={height}
|
||||||
fill={fill}
|
fill={fill}
|
||||||
|
className={className}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
const thumbsUpSvg = ({
|
const thumbsUpSvg = ({
|
||||||
fill = "#ffffff"
|
fill = "#ffffff",
|
||||||
|
className = "",
|
||||||
|
width = "18",
|
||||||
|
height = "18"
|
||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
fill={fill}
|
fill={fill}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
|
|||||||
28
src/types.ts
28
src/types.ts
@@ -21,7 +21,8 @@ export interface ContentContainer {
|
|||||||
previewTime: (time: number, unpause?: boolean) => void,
|
previewTime: (time: number, unpause?: boolean) => void,
|
||||||
videoInfo: VideoInfo,
|
videoInfo: VideoInfo,
|
||||||
getRealCurrentTime: () => number,
|
getRealCurrentTime: () => number,
|
||||||
lockedCategories: string[]
|
lockedCategories: string[],
|
||||||
|
channelIDInfo: ChannelIDInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ export enum SponsorHideType {
|
|||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
Skip = "skip",
|
Skip = "skip",
|
||||||
Mute = "mute",
|
Mute = "mute",
|
||||||
|
Chapter = "chapter",
|
||||||
Full = "full",
|
Full = "full",
|
||||||
Poi = "poi"
|
Poi = "poi"
|
||||||
}
|
}
|
||||||
@@ -69,19 +71,24 @@ export type Category = string & { __categoryBrand: unknown };
|
|||||||
|
|
||||||
export enum SponsorSourceType {
|
export enum SponsorSourceType {
|
||||||
Server = undefined,
|
Server = undefined,
|
||||||
Local = 1
|
Local = 1,
|
||||||
|
YouTube = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SponsorTime {
|
export interface SegmentContainer {
|
||||||
segment: [number] | [number, number];
|
segment: [number] | [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SponsorTime extends SegmentContainer {
|
||||||
UUID: SegmentUUID;
|
UUID: SegmentUUID;
|
||||||
locked?: number;
|
locked?: number;
|
||||||
|
|
||||||
category: Category;
|
category: Category;
|
||||||
actionType: ActionType;
|
actionType: ActionType;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
hidden?: SponsorHideType;
|
hidden?: SponsorHideType;
|
||||||
source?: SponsorSourceType;
|
source: SponsorSourceType;
|
||||||
videoDuration?: number;
|
videoDuration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,3 +238,16 @@ export type Keybind = {
|
|||||||
alt?: boolean,
|
alt?: boolean,
|
||||||
shift?: boolean
|
shift?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PageType {
|
||||||
|
Shorts = "shorts",
|
||||||
|
Watch = "watch",
|
||||||
|
Search = "search",
|
||||||
|
Browse = "browse",
|
||||||
|
Channel = "channel"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonListener {
|
||||||
|
name: string,
|
||||||
|
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||||
|
}
|
||||||
72
src/upsell.ts
Normal file
72
src/upsell.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import Config from "./config";
|
||||||
|
import { checkLicenseKey } from "./utils/licenseKey";
|
||||||
|
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||||
|
|
||||||
|
import * as countries from "../public/res/countries.json";
|
||||||
|
|
||||||
|
// This is needed, if Config is not imported before Utils, things break.
|
||||||
|
// Probably due to cyclic dependencies
|
||||||
|
Config.config;
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
localizeHtmlPage();
|
||||||
|
|
||||||
|
const cantAfford = document.getElementById("cantAfford");
|
||||||
|
const cantAffordTexts = chrome.i18n.getMessage("cantAfford").split(/{|}/);
|
||||||
|
cantAfford.appendChild(document.createTextNode(cantAffordTexts[0]));
|
||||||
|
const discountButton = document.createElement("span");
|
||||||
|
discountButton.id = "discountButton";
|
||||||
|
discountButton.innerText = cantAffordTexts[1];
|
||||||
|
cantAfford.appendChild(discountButton);
|
||||||
|
cantAfford.appendChild(document.createTextNode(cantAffordTexts[2]));
|
||||||
|
|
||||||
|
const redeemButton = document.getElementById("redeemButton") as HTMLInputElement;
|
||||||
|
const redeemInput = document.getElementById("redeemCodeInput") as HTMLInputElement;
|
||||||
|
redeemButton.addEventListener("click", async () => {
|
||||||
|
const licenseKey = redeemInput.value;
|
||||||
|
|
||||||
|
if (await checkLicenseKey(licenseKey)) {
|
||||||
|
Config.config.payments.licenseKey = licenseKey;
|
||||||
|
Config.forceSyncUpdate("payments");
|
||||||
|
|
||||||
|
alert(chrome.i18n.getMessage("redeemSuccess"));
|
||||||
|
} else {
|
||||||
|
alert(chrome.i18n.getMessage("redeemFailed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
discountButton.addEventListener("click", async () => {
|
||||||
|
const subsidizedSection = document.getElementById("subsidizedPrice");
|
||||||
|
subsidizedSection.classList.remove("hidden");
|
||||||
|
|
||||||
|
const oldSelector = document.getElementById("countrySelector");
|
||||||
|
if (oldSelector) oldSelector.remove();
|
||||||
|
const countrySelector = document.createElement("select");
|
||||||
|
countrySelector.id = "countrySelector";
|
||||||
|
countrySelector.className = "optionsSelector";
|
||||||
|
const defaultOption = document.createElement("option");
|
||||||
|
defaultOption.innerText = chrome.i18n.getMessage("chooseACountry");
|
||||||
|
countrySelector.appendChild(defaultOption);
|
||||||
|
|
||||||
|
for (const country of Object.keys(countries)) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = country;
|
||||||
|
option.innerText = country;
|
||||||
|
countrySelector.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
countrySelector.addEventListener("change", () => {
|
||||||
|
if (countries[countrySelector.value]?.allowed) {
|
||||||
|
document.getElementById("subsidizedLink").classList.remove("hidden");
|
||||||
|
document.getElementById("noSubsidizedLink").classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
document.getElementById("subsidizedLink").classList.add("hidden");
|
||||||
|
document.getElementById("noSubsidizedLink").classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
subsidizedSection.appendChild(countrySelector);
|
||||||
|
});
|
||||||
|
}
|
||||||
191
src/utils.ts
191
src/utils.ts
@@ -2,7 +2,7 @@ import Config, { VideoDownvotes } from "./config";
|
|||||||
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types";
|
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types";
|
||||||
|
|
||||||
import * as CompileConfig from "../config.json";
|
import * as CompileConfig from "../config.json";
|
||||||
import { findValidElementFromSelector } from "./utils/pageUtils";
|
import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils";
|
||||||
import { GenericUtils } from "./utils/genericUtils";
|
import { GenericUtils } from "./utils/genericUtils";
|
||||||
|
|
||||||
export default class Utils {
|
export default class Utils {
|
||||||
@@ -22,52 +22,79 @@ export default class Utils {
|
|||||||
];
|
];
|
||||||
|
|
||||||
/* Used for waitForElement */
|
/* Used for waitForElement */
|
||||||
waitingMutationObserver:MutationObserver = null;
|
creatingWaitingMutationObserver = false;
|
||||||
waitingElements: { selector: string, callback: (element: Element) => void }[] = [];
|
waitingMutationObserver: MutationObserver = null;
|
||||||
|
waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = [];
|
||||||
|
|
||||||
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
|
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
|
||||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
async wait<T>(condition: () => T, timeout = 5000, check = 100): Promise<T> {
|
||||||
return GenericUtils.wait(condition, timeout, check);
|
return GenericUtils.wait(condition, timeout, check);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Uses a mutation observer to wait asynchronously */
|
/* Uses a mutation observer to wait asynchronously */
|
||||||
async waitForElement(selector: string): Promise<Element> {
|
async waitForElement(selector: string, visibleCheck = false): Promise<Element> {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
|
const initialElement = this.getElement(selector, visibleCheck);
|
||||||
|
if (initialElement) {
|
||||||
|
resolve(initialElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.waitingElements.push({
|
this.waitingElements.push({
|
||||||
selector,
|
selector,
|
||||||
|
visibleCheck,
|
||||||
callback: resolve
|
callback: resolve
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.waitingMutationObserver) {
|
if (!this.creatingWaitingMutationObserver) {
|
||||||
this.waitingMutationObserver = new MutationObserver(() => {
|
this.creatingWaitingMutationObserver = true;
|
||||||
const foundSelectors = [];
|
|
||||||
for (const { selector, callback } of this.waitingElements) {
|
|
||||||
const element = document.querySelector(selector);
|
|
||||||
if (element) {
|
|
||||||
callback(element);
|
|
||||||
foundSelectors.push(selector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
|
if (document.body) {
|
||||||
|
this.setupWaitingMutationListener();
|
||||||
if (this.waitingElements.length === 0) {
|
} else {
|
||||||
this.waitingMutationObserver.disconnect();
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
this.waitingMutationObserver = null;
|
this.setupWaitingMutationListener();
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
this.waitingMutationObserver.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupWaitingMutationListener(): void {
|
||||||
|
if (!this.waitingMutationObserver) {
|
||||||
|
this.waitingMutationObserver = new MutationObserver(() => {
|
||||||
|
const foundSelectors = [];
|
||||||
|
for (const { selector, visibleCheck, callback } of this.waitingElements) {
|
||||||
|
const element = this.getElement(selector, visibleCheck);
|
||||||
|
if (element) {
|
||||||
|
callback(element);
|
||||||
|
foundSelectors.push(selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
|
||||||
|
|
||||||
|
if (this.waitingElements.length === 0) {
|
||||||
|
this.waitingMutationObserver.disconnect();
|
||||||
|
this.waitingMutationObserver = null;
|
||||||
|
this.creatingWaitingMutationObserver = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.waitingMutationObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getElement(selector: string, visibleCheck: boolean) {
|
||||||
|
return visibleCheck ? findValidElement(document.querySelectorAll(selector)) : document.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
|
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
chrome.permissions.contains(permissions, resolve)
|
chrome.permissions.contains(permissions, resolve)
|
||||||
@@ -183,6 +210,37 @@ export default class Utils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyInvidiousPermissions(enable: boolean, option = "supportInvidious"): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (enable) {
|
||||||
|
this.setupExtraSitePermissions((granted) => {
|
||||||
|
if (!granted) {
|
||||||
|
Config.config[option] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(granted);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.removeExtraSiteRegistration();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
containsInvidiousPermission(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let permissions = ["declarativeContent"];
|
||||||
|
if (this.isFirefox()) permissions = [];
|
||||||
|
|
||||||
|
chrome.permissions.contains({
|
||||||
|
origins: this.getPermissionRegex(),
|
||||||
|
permissions: permissions
|
||||||
|
}, function (result) {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
|
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
|
||||||
*/
|
*/
|
||||||
@@ -273,24 +331,6 @@ export default class Utils {
|
|||||||
return permissionRegex;
|
return permissionRegex;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUserID(length = 36): string {
|
|
||||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
let result = "";
|
|
||||||
if (window.crypto && window.crypto.getRandomValues) {
|
|
||||||
const values = new Uint32Array(length);
|
|
||||||
window.crypto.getRandomValues(values);
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += charset[values[i] % charset.length];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += charset[Math.floor(Math.random() * charset.length)];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a request to a custom server
|
* Sends a request to a custom server
|
||||||
*
|
*
|
||||||
@@ -376,67 +416,6 @@ export default class Utils {
|
|||||||
return referenceNode;
|
return referenceNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
|
|
||||||
let counter = 0;
|
|
||||||
for (const key in data) {
|
|
||||||
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
|
|
||||||
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
|
|
||||||
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
|
|
||||||
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedTime(seconds: number, precise?: boolean): string {
|
|
||||||
seconds = Math.max(seconds, 0);
|
|
||||||
|
|
||||||
const hours = Math.floor(seconds / 60 / 60);
|
|
||||||
const minutes = Math.floor(seconds / 60) % 60;
|
|
||||||
let minutesDisplay = String(minutes);
|
|
||||||
let secondsNum = seconds % 60;
|
|
||||||
if (!precise) {
|
|
||||||
secondsNum = Math.floor(secondsNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
|
|
||||||
|
|
||||||
if (secondsNum < 10) {
|
|
||||||
//add a zero
|
|
||||||
secondsDisplay = "0" + secondsDisplay;
|
|
||||||
}
|
|
||||||
if (hours && minutes < 10) {
|
|
||||||
//add a zero
|
|
||||||
minutesDisplay = "0" + minutesDisplay;
|
|
||||||
}
|
|
||||||
if (isNaN(hours) || isNaN(minutes)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedTimeToSeconds(formatted: string): number | null {
|
|
||||||
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
|
|
||||||
|
|
||||||
if (fragments === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
|
|
||||||
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
|
|
||||||
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
|
|
||||||
|
|
||||||
return hours * 3600 + minutes * 60 + seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
shortCategoryName(categoryName: string): string {
|
|
||||||
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
isContentScript(): boolean {
|
isContentScript(): boolean {
|
||||||
return window.location.protocol === "http:" || window.location.protocol === "https:";
|
return window.location.protocol === "http:" || window.location.protocol === "https:";
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/utils/arrayUtils.ts
Normal file
6
src/utils/arrayUtils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function partition<T>(array: T[], filter: (element: T) => boolean): [T[], T[]] {
|
||||||
|
const pass = [], fail = [];
|
||||||
|
array.forEach((element) => (filter(element) ? pass : fail).push(element));
|
||||||
|
|
||||||
|
return [pass, fail];
|
||||||
|
}
|
||||||
@@ -41,7 +41,13 @@ export function getCategorySuffix(category: Category): string {
|
|||||||
return "_POI";
|
return "_POI";
|
||||||
} else if (category === "exclusive_access") {
|
} else if (category === "exclusive_access") {
|
||||||
return "_full";
|
return "_full";
|
||||||
|
} else if (category === "chapter") {
|
||||||
|
return "_chapter";
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shortCategoryName(categoryName: string): string {
|
||||||
|
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
|
||||||
|
}
|
||||||
@@ -137,5 +137,16 @@ export function getGuidelineInfo(category: Category): TextBox[] {
|
|||||||
icon: "icons/bolt.svg",
|
icon: "icons/bolt.svg",
|
||||||
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
|
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
|
||||||
}];
|
}];
|
||||||
|
case "chapter":
|
||||||
|
return [{
|
||||||
|
icon: "icons/close-smaller.svg",
|
||||||
|
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
|
||||||
|
}, {
|
||||||
|
icon: "icons/check-smaller.svg",
|
||||||
|
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
|
||||||
|
}, {
|
||||||
|
icon: "icons/check-smaller.svg",
|
||||||
|
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
87
src/utils/exporter.ts
Normal file
87
src/utils/exporter.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
|
||||||
|
import { shortCategoryName } from "./categoryUtils";
|
||||||
|
import { GenericUtils } from "./genericUtils";
|
||||||
|
import * as CompileConfig from "../../config.json";
|
||||||
|
|
||||||
|
const inTest = typeof chrome === "undefined";
|
||||||
|
|
||||||
|
const chapterNames = CompileConfig.categoryList.filter((code) => code !== "chapter")
|
||||||
|
.map((code) => ({
|
||||||
|
code,
|
||||||
|
name: !inTest ? chrome.i18n.getMessage("category_" + code) : code
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function exportTimes(segments: SponsorTime[]): string {
|
||||||
|
let result = "";
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (![ActionType.Full, ActionType.Mute].includes(segment.actionType)
|
||||||
|
&& segment.source !== SponsorSourceType.YouTube) {
|
||||||
|
result += exportTime(segment) + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.replace(/\n$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportTime(segment: SponsorTime): string {
|
||||||
|
const name = segment.description || shortCategoryName(segment.category);
|
||||||
|
|
||||||
|
return `${GenericUtils.getFormattedTime(segment.segment[0], true)}${
|
||||||
|
segment.segment[1] && segment.segment[0] !== segment.segment[1]
|
||||||
|
? ` - ${GenericUtils.getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importTimes(data: string, videoDuration: number): SponsorTime[] {
|
||||||
|
const lines = data.split("\n");
|
||||||
|
const result: SponsorTime[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const match = line.match(/(?:(\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
|
||||||
|
if (match) {
|
||||||
|
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
|
||||||
|
if (startTime) {
|
||||||
|
const specialCharsMatcher = /^(?:\s+seconds?)?[-:()\s]*|(?:\s+at)?[-:()\s]+$/g
|
||||||
|
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
|
||||||
|
let titleRight = null;
|
||||||
|
const split2 = line.split(match[1] || match[0]);
|
||||||
|
titleRight = split2[split2.length - 1].replace(specialCharsMatcher, "");
|
||||||
|
|
||||||
|
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
|
||||||
|
if (title) {
|
||||||
|
const determinedCategory = chapterNames.find(c => c.name === title)?.code as Category;
|
||||||
|
|
||||||
|
const segment: SponsorTime = {
|
||||||
|
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
|
||||||
|
category: determinedCategory ?? ("chapter" as Category),
|
||||||
|
actionType: determinedCategory ? ActionType.Skip : ActionType.Chapter,
|
||||||
|
description: title,
|
||||||
|
source: SponsorSourceType.Local,
|
||||||
|
UUID: GenericUtils.generateUserID() as SegmentUUID
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
|
||||||
|
result[result.length - 1].segment[1] = segment.segment[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
|
||||||
|
result[result.length - 1].segment[1] = videoDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportTimesAsHashParam(segments: SponsorTime[]): string {
|
||||||
|
const hashparamSegments = segments.map(segment => ({
|
||||||
|
actionType: segment.actionType,
|
||||||
|
category: segment.category,
|
||||||
|
segment: segment.segment,
|
||||||
|
...(segment.description ? {description: segment.description} : {}) // don't include the description param if empty
|
||||||
|
}));
|
||||||
|
|
||||||
|
return `#segments=${JSON.stringify(hashparamSegments)}`;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/** Function that can be used to wait for a condition before returning. */
|
/** Function that can be used to wait for a condition before returning. */
|
||||||
async function wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
async function wait<T>(condition: () => T, timeout = 5000, check = 100, predicate?: (obj: T) => boolean): Promise<T> {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
@@ -8,7 +8,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
|||||||
|
|
||||||
const intervalCheck = () => {
|
const intervalCheck = () => {
|
||||||
const result = condition();
|
const result = condition();
|
||||||
if (result) {
|
if (predicate ? predicate(result) : result) {
|
||||||
resolve(result);
|
resolve(result);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,50 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFormattedTimeToSeconds(formatted: string): number | null {
|
||||||
|
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
|
||||||
|
|
||||||
|
if (fragments === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
|
||||||
|
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
|
||||||
|
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
|
||||||
|
|
||||||
|
return hours * 3600 + minutes * 60 + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedTime(seconds: number, precise?: boolean): string {
|
||||||
|
seconds = Math.max(seconds, 0);
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 60 / 60);
|
||||||
|
const minutes = Math.floor(seconds / 60) % 60;
|
||||||
|
let minutesDisplay = String(minutes);
|
||||||
|
let secondsNum = seconds % 60;
|
||||||
|
if (!precise) {
|
||||||
|
secondsNum = Math.floor(secondsNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
|
||||||
|
|
||||||
|
if (secondsNum < 10) {
|
||||||
|
//add a zero
|
||||||
|
secondsDisplay = "0" + secondsDisplay;
|
||||||
|
}
|
||||||
|
if (hours && minutes < 10) {
|
||||||
|
//add a zero
|
||||||
|
minutesDisplay = "0" + minutesDisplay;
|
||||||
|
}
|
||||||
|
if (isNaN(hours) || isNaN(minutes)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the error message in a nice string
|
* Gets the error message in a nice string
|
||||||
*
|
*
|
||||||
@@ -28,7 +72,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
|||||||
* @returns {string} errorMessage
|
* @returns {string} errorMessage
|
||||||
*/
|
*/
|
||||||
function getErrorMessage(statusCode: number, responseText: string): string {
|
function getErrorMessage(statusCode: number, responseText: string): string {
|
||||||
const postFix = ((responseText && !responseText.includes(`cf-wrapper`)) ? "\n\n" + responseText : "");
|
const postFix = ((responseText && !(responseText.includes(`cf-wrapper`) || responseText.includes("<!DOCTYPE html>"))) ? "\n\n" + responseText : "");
|
||||||
// display response body for 4xx
|
// display response body for 4xx
|
||||||
if([400, 429, 409, 0].includes(statusCode)) {
|
if([400, 429, 409, 0].includes(statusCode)) {
|
||||||
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode + postFix;
|
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode + postFix;
|
||||||
@@ -72,9 +116,44 @@ function indexesOf<T>(array: T[], value: T): number[] {
|
|||||||
return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1);
|
return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
|
||||||
|
let counter = 0;
|
||||||
|
for (const key in data) {
|
||||||
|
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
|
||||||
|
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
|
||||||
|
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
|
||||||
|
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUserID(length = 36): string {
|
||||||
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
let result = "";
|
||||||
|
if (window.crypto && window.crypto.getRandomValues) {
|
||||||
|
const values = new Uint32Array(length);
|
||||||
|
window.crypto.getRandomValues(values);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += charset[values[i] % charset.length];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += charset[Math.floor(Math.random() * charset.length)];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const GenericUtils = {
|
export const GenericUtils = {
|
||||||
wait,
|
wait,
|
||||||
|
getFormattedTime,
|
||||||
|
getFormattedTimeToSeconds,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
getLuminance,
|
getLuminance,
|
||||||
indexesOf
|
generateUserID,
|
||||||
|
indexesOf,
|
||||||
|
objectToURI
|
||||||
}
|
}
|
||||||
72
src/utils/licenseKey.ts
Normal file
72
src/utils/licenseKey.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import Config from "../config";
|
||||||
|
import Utils from "../utils";
|
||||||
|
import * as CompileConfig from "../../config.json";
|
||||||
|
|
||||||
|
const utils = new Utils();
|
||||||
|
|
||||||
|
export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
|
||||||
|
const result = await utils.asyncRequestToServer("GET", "/api/verifyToken", {
|
||||||
|
licenseKey
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (result.ok && JSON.parse(result.responseText).allowed) {
|
||||||
|
Config.config.payments.chaptersAllowed = true;
|
||||||
|
Config.config.payments.lastCheck = Date.now();
|
||||||
|
Config.forceSyncUpdate("payments");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) { } //eslint-disable-line no-empty
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The other one also tried refreshing, so returns a promise
|
||||||
|
*/
|
||||||
|
export function noRefreshFetchingChaptersAllowed(): boolean {
|
||||||
|
return Config.config.payments.chaptersAllowed || CompileConfig["freeChapterAccess"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchingChaptersAllowed(): Promise<boolean> {
|
||||||
|
if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//more than 14 days
|
||||||
|
if (Config.config.payments.licenseKey && Date.now() - Config.config.payments.lastCheck > 14 * 24 * 60 * 60 * 1000) {
|
||||||
|
const licensePromise = checkLicenseKey(Config.config.payments.licenseKey);
|
||||||
|
|
||||||
|
if (!Config.config.payments.chaptersAllowed) {
|
||||||
|
return licensePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.config.payments.chaptersAllowed) return true;
|
||||||
|
|
||||||
|
if (Config.config.payments.lastCheck === 0) {
|
||||||
|
// Check for free access if no license key, and it is the first time
|
||||||
|
const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||||
|
value: "freeChaptersAccess",
|
||||||
|
userID: Config.config.userID
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (result.ok) {
|
||||||
|
const userInfo = JSON.parse(result.responseText);
|
||||||
|
|
||||||
|
Config.config.payments.lastCheck = Date.now();
|
||||||
|
if (userInfo.freeChaptersAccess) {
|
||||||
|
Config.config.payments.freeAccess = true;
|
||||||
|
Config.config.payments.chaptersAllowed = true;
|
||||||
|
Config.forceSyncUpdate("payments");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { } //eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
export function getControls(): HTMLElement | false {
|
import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
|
||||||
|
import { GenericUtils } from "./genericUtils";
|
||||||
|
|
||||||
|
export function getControls(): HTMLElement {
|
||||||
const controlsSelectors = [
|
const controlsSelectors = [
|
||||||
// YouTube
|
// YouTube
|
||||||
".ytp-right-controls",
|
".ytp-right-controls",
|
||||||
@@ -16,7 +19,7 @@ export function getControls(): HTMLElement | false {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVisible(element: HTMLElement): boolean {
|
export function isVisible(element: HTMLElement): boolean {
|
||||||
@@ -63,6 +66,44 @@ export function getHashParams(): Record<string, unknown> {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] {
|
||||||
|
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
|
||||||
|
|
||||||
|
const chapters: SponsorTime[] = [];
|
||||||
|
if (chaptersBox) {
|
||||||
|
let lastSegment: SponsorTime = null;
|
||||||
|
const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
|
||||||
|
for (const link of links) {
|
||||||
|
const timeElement = link.querySelector("#time") as HTMLElement;
|
||||||
|
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 (lastSegment) {
|
||||||
|
lastSegment.segment[1] = time;
|
||||||
|
chapters.push(lastSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSegment = {
|
||||||
|
segment: [time, null],
|
||||||
|
category: "chapter" as Category,
|
||||||
|
actionType: ActionType.Chapter,
|
||||||
|
description: description.innerText,
|
||||||
|
source: SponsorSourceType.YouTube,
|
||||||
|
UUID: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSegment) {
|
||||||
|
lastSegment.segment[1] = duration;
|
||||||
|
chapters.push(lastSegment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
export function localizeHtmlPage(): void {
|
export function localizeHtmlPage(): void {
|
||||||
//Localize by replacing __MSG_***__ meta tags
|
//Localize by replacing __MSG_***__ meta tags
|
||||||
const localizedTitle = getLocalizedMessage(document.title);
|
const localizedTitle = getLocalizedMessage(document.title);
|
||||||
|
|||||||
66
src/utils/warnings.ts
Normal file
66
src/utils/warnings.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import Config from "../config";
|
||||||
|
import GenericNotice, { NoticeOptions } from "../render/GenericNotice";
|
||||||
|
import { ContentContainer } from "../types";
|
||||||
|
import Utils from "../utils";
|
||||||
|
import { GenericUtils } from "./genericUtils";
|
||||||
|
const utils = new Utils();
|
||||||
|
|
||||||
|
export interface ChatConfig {
|
||||||
|
displayName: string,
|
||||||
|
composerInitialValue?: string,
|
||||||
|
customDescription?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {
|
||||||
|
const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||||
|
userID: Config.config.userID,
|
||||||
|
values: ["warningReason"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userInfo.ok) {
|
||||||
|
const warningReason = JSON.parse(userInfo.responseText)?.warningReason;
|
||||||
|
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
|
||||||
|
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
|
||||||
|
const publicUserID = await utils.getHash(Config.config.userID);
|
||||||
|
|
||||||
|
let notice: GenericNotice = null;
|
||||||
|
const options: NoticeOptions = {
|
||||||
|
title: chrome.i18n.getMessage("warningTitle"),
|
||||||
|
textBoxes: [{
|
||||||
|
text: chrome.i18n.getMessage("warningChatInfo"),
|
||||||
|
icon: null
|
||||||
|
}, ...warningReason.split("\n").map((reason) => ({
|
||||||
|
text: reason,
|
||||||
|
icon: null
|
||||||
|
}))],
|
||||||
|
buttons: [{
|
||||||
|
name: chrome.i18n.getMessage("questionButton"),
|
||||||
|
listener: () => openChat({
|
||||||
|
displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: chrome.i18n.getMessage("warningConfirmButton"),
|
||||||
|
listener: async () => {
|
||||||
|
const result = await utils.asyncRequestToServer("POST", "/api/warnUser", {
|
||||||
|
userID: Config.config.userID,
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
notice?.close();
|
||||||
|
} else {
|
||||||
|
alert(`${chrome.i18n.getMessage("warningError")} ${result.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
timed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
notice = new GenericNotice(contentContainer, "warningNotice", options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openChat(config: ChatConfig): void {
|
||||||
|
window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false));
|
||||||
|
}
|
||||||
241
test/exporter.test.ts
Normal file
241
test/exporter.test.ts
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../src/types";
|
||||||
|
import { exportTimes, importTimes } from "../src/utils/exporter";
|
||||||
|
|
||||||
|
describe("Export segments", () => {
|
||||||
|
it("Some segments", () => {
|
||||||
|
const segments: SponsorTime[] = [{
|
||||||
|
segment: [0, 10],
|
||||||
|
category: "chapter" as Category,
|
||||||
|
actionType: ActionType.Chapter,
|
||||||
|
description: "Chapter 1",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "1" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [20, 20],
|
||||||
|
category: "poi_highlight" as Category,
|
||||||
|
actionType: ActionType.Poi,
|
||||||
|
description: "Highlight",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "2" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [30, 40],
|
||||||
|
category: "sponsor" as Category,
|
||||||
|
actionType: ActionType.Skip,
|
||||||
|
description: "Sponsor", // Force a description since chrome is not defined
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "3" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [50, 60],
|
||||||
|
category: "selfpromo" as Category,
|
||||||
|
actionType: ActionType.Mute,
|
||||||
|
description: "Selfpromo",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "4" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [0, 0],
|
||||||
|
category: "selfpromo" as Category,
|
||||||
|
actionType: ActionType.Full,
|
||||||
|
description: "Selfpromo",
|
||||||
|
source: SponsorSourceType.Server,
|
||||||
|
UUID: "5" as SegmentUUID
|
||||||
|
}, {
|
||||||
|
segment: [80, 90],
|
||||||
|
category: "interaction" as Category,
|
||||||
|
actionType: ActionType.Skip,
|
||||||
|
description: "Interaction",
|
||||||
|
source: SponsorSourceType.YouTube,
|
||||||
|
UUID: "6" as SegmentUUID
|
||||||
|
}];
|
||||||
|
|
||||||
|
const result = exportTimes(segments);
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
"0:00.000 - 0:10.000 Chapter 1\n" +
|
||||||
|
"0:20.000 Highlight\n" +
|
||||||
|
"0:30.000 - 0:40.000 Sponsor"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Import segments", () => {
|
||||||
|
it("1:20 to 1:21 thing", () => {
|
||||||
|
const input = ` 1:20 to 1:21 thing
|
||||||
|
1:25 to 1:28 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing 1:20 to 1:21", () => {
|
||||||
|
const input = ` thing 1:20 to 1:21
|
||||||
|
another thing 1:25 to 1:28 ext`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 - 1:21 thing", () => {
|
||||||
|
const input = ` 1:20 - 1:21 thing
|
||||||
|
1:25 - 1:28 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 1:21 thing", () => {
|
||||||
|
const input = ` 1:20 1:21 thing
|
||||||
|
1:25 1:28 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 81],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 88],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 thing", () => {
|
||||||
|
const input = ` 1:20 thing
|
||||||
|
1:25 another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20: thing", () => {
|
||||||
|
const input = ` 1:20: thing
|
||||||
|
1:25: another thing`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1:20 (thing)", () => {
|
||||||
|
const input = ` 1:20 (thing)
|
||||||
|
1:25 (another thing)`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing 1:20", () => {
|
||||||
|
const input = ` thing 1:20
|
||||||
|
another thing 1:25`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing at 1:20", () => {
|
||||||
|
const input = ` thing at 1:20
|
||||||
|
another thing at 1:25`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [80, 85],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [85, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing at 1s", () => {
|
||||||
|
const input = ` thing at 1s
|
||||||
|
another thing at 5s`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [1, 5],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [5, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thing at 1 second", () => {
|
||||||
|
const input = ` thing at 1 second
|
||||||
|
another thing at 5 seconds`;
|
||||||
|
|
||||||
|
const result = importTimes(input, 120);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
segment: [1, 5],
|
||||||
|
description: "thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}, {
|
||||||
|
segment: [5, 120],
|
||||||
|
description: "another thing",
|
||||||
|
category: "chapter" as Category
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
665
test/previewBar.test.ts
Normal file
665
test/previewBar.test.ts
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
|
||||||
|
|
||||||
|
describe("createChapterRenderGroups", () => {
|
||||||
|
let previewBar: PreviewBar;
|
||||||
|
beforeEach(() => {
|
||||||
|
previewBar = new PreviewBar(null, null, null, null, true);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Two unrelated times", () => {
|
||||||
|
previewBar.videoDuration = 315;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
segment: [2, 30],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}, {
|
||||||
|
segment: [50, 80],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}] as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([{
|
||||||
|
segment: [0, 2],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [2, 30],
|
||||||
|
originalDuration: 30 - 2
|
||||||
|
}, {
|
||||||
|
segment: [30, 50],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [50, 80],
|
||||||
|
originalDuration: 80 - 50
|
||||||
|
}, {
|
||||||
|
segment: [80, 315],
|
||||||
|
originalDuration: 0
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Small time in bigger time", () => {
|
||||||
|
previewBar.videoDuration = 315;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
segment: [2.52, 30],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}, {
|
||||||
|
segment: [20, 25],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}] as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([{
|
||||||
|
segment: [0, 2.52],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [2.52, 20],
|
||||||
|
originalDuration: 30 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [20, 25],
|
||||||
|
originalDuration: 25 - 20
|
||||||
|
}, {
|
||||||
|
segment: [25, 30],
|
||||||
|
originalDuration: 30 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [30, 315],
|
||||||
|
originalDuration: 0
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Same start time", () => {
|
||||||
|
previewBar.videoDuration = 315;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
segment: [2.52, 30],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}, {
|
||||||
|
segment: [2.52, 40],
|
||||||
|
category: "sponsor",
|
||||||
|
unsubmitted: false,
|
||||||
|
showLarger: false,
|
||||||
|
description: ""
|
||||||
|
}] as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([{
|
||||||
|
segment: [0, 2.52],
|
||||||
|
originalDuration: 0
|
||||||
|
}, {
|
||||||
|
segment: [2.52, 30],
|
||||||
|
originalDuration: 30 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [30, 40],
|
||||||
|
originalDuration: 40 - 2.52
|
||||||
|
}, {
|
||||||
|
segment: [40, 315],
|
||||||
|
originalDuration: 0
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Lots of overlapping segments", () => {
|
||||||
|
previewBar.videoDuration = 315.061;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([
|
||||||
|
{
|
||||||
|
"category": "chapter",
|
||||||
|
"segment": [
|
||||||
|
0,
|
||||||
|
49.977
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
|
||||||
|
"description": "Start of video"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
2.926,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"locked": 1,
|
||||||
|
"votes": 2,
|
||||||
|
"videoDuration": 316,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "chapter",
|
||||||
|
"segment": [
|
||||||
|
14.487,
|
||||||
|
37.133
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
|
||||||
|
"description": "Subset of start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
23.450537,
|
||||||
|
34.486084
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "interaction",
|
||||||
|
"segment": [
|
||||||
|
50.015343,
|
||||||
|
56.775314
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "b2a85e8cdfbf21dd504babbcaca7f751b55a5a2df8179c1a83a121d0f5d56c0e",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
62.51888,
|
||||||
|
74.33331
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 316,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
88.71328,
|
||||||
|
96.05933
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "6c08c092db2b7a31210717cc1f2652e7e97d032e03c82b029a27c81cead1f90c",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
101.50703,
|
||||||
|
115.088326
|
||||||
|
],
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "2db207ad4b7a535a548fab293f4567bf97373997e67aadb47df8f91b673f6e53",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
122.211845,
|
||||||
|
137.42178
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 1,
|
||||||
|
"videoDuration": 0,
|
||||||
|
"userID": "0312cbfa514d9d2dfb737816dc45f52aba7c23f0a3f0911273a6993b2cb57fcc",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
144.08913,
|
||||||
|
160.14084
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 316,
|
||||||
|
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
164.22084,
|
||||||
|
170.98082
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "845c4377060d5801f5324f8e1be1e8373bfd9addcf6c68fc5a3c038111b506a3",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
180.56674,
|
||||||
|
189.16516
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "7c6b015687db7800c05756a0fd226fd8d101f5a1e1bfb1e5d97c440331fd6cb7",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
204.10468,
|
||||||
|
211.87865
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "3472e8ee00b5da957377ae32d59ddd3095c2b634c2c0c970dfabfb81d143699f",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
214.92064,
|
||||||
|
222.0186
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 0,
|
||||||
|
"userID": "62a00dffb344d27de7adf8ea32801c2fc0580087dc8d282837923e4bda6a1745",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
233.0754,
|
||||||
|
244.56734
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": -1,
|
||||||
|
"videoDuration": 315,
|
||||||
|
"userID": "dcf7fb0a6c071d5a93273ebcbecaee566e0ff98181057a36ed855e9b92bf25ea",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
260.64053,
|
||||||
|
269.35938
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
288.686,
|
||||||
|
301.96
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "sponsor",
|
||||||
|
"segment": [
|
||||||
|
288.686,
|
||||||
|
295
|
||||||
|
],
|
||||||
|
"locked": 0,
|
||||||
|
"votes": 0,
|
||||||
|
"videoDuration": 315.061,
|
||||||
|
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
|
||||||
|
"description": ""
|
||||||
|
}] as unknown as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
0,
|
||||||
|
2.926
|
||||||
|
],
|
||||||
|
"originalDuration": 49.977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
2.926,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"originalDuration": 2.074
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
5,
|
||||||
|
14.487
|
||||||
|
],
|
||||||
|
"originalDuration": 49.977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
14.487,
|
||||||
|
23.450537
|
||||||
|
],
|
||||||
|
"originalDuration": 22.646
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
23.450537,
|
||||||
|
34.486084
|
||||||
|
],
|
||||||
|
"originalDuration": 11.035546999999998
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
34.486084,
|
||||||
|
37.133
|
||||||
|
],
|
||||||
|
"originalDuration": 22.646
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
37.133,
|
||||||
|
49.977
|
||||||
|
],
|
||||||
|
"originalDuration": 49.977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
49.977,
|
||||||
|
50.015343
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
50.015343,
|
||||||
|
56.775314
|
||||||
|
],
|
||||||
|
"originalDuration": 6.759971
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
56.775314,
|
||||||
|
62.51888
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
62.51888,
|
||||||
|
74.33331
|
||||||
|
],
|
||||||
|
"originalDuration": 11.814429999999994
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
74.33331,
|
||||||
|
88.71328
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
88.71328,
|
||||||
|
96.05933
|
||||||
|
],
|
||||||
|
"originalDuration": 7.346050000000005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
96.05933,
|
||||||
|
101.50703
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
101.50703,
|
||||||
|
115.088326
|
||||||
|
],
|
||||||
|
"originalDuration": 13.581295999999995
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
115.088326,
|
||||||
|
122.211845
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
122.211845,
|
||||||
|
137.42178
|
||||||
|
],
|
||||||
|
"originalDuration": 15.209935000000016
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
137.42178,
|
||||||
|
144.08913
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
144.08913,
|
||||||
|
160.14084
|
||||||
|
],
|
||||||
|
"originalDuration": 16.051709999999986
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
160.14084,
|
||||||
|
164.22084
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
164.22084,
|
||||||
|
170.98082
|
||||||
|
],
|
||||||
|
"originalDuration": 6.759979999999985
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
170.98082,
|
||||||
|
180.56674
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
180.56674,
|
||||||
|
189.16516
|
||||||
|
],
|
||||||
|
"originalDuration": 8.598419999999976
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
189.16516,
|
||||||
|
204.10468
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
204.10468,
|
||||||
|
211.87865
|
||||||
|
],
|
||||||
|
"originalDuration": 7.773969999999991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
211.87865,
|
||||||
|
214.92064
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
214.92064,
|
||||||
|
222.0186
|
||||||
|
],
|
||||||
|
"originalDuration": 7.0979600000000005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
222.0186,
|
||||||
|
233.0754
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
233.0754,
|
||||||
|
244.56734
|
||||||
|
],
|
||||||
|
"originalDuration": 11.49194
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
244.56734,
|
||||||
|
260.64053
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
260.64053,
|
||||||
|
269.35938
|
||||||
|
],
|
||||||
|
"originalDuration": 8.718849999999975
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
269.35938,
|
||||||
|
288.686
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
288.686,
|
||||||
|
295
|
||||||
|
],
|
||||||
|
"originalDuration": 6.314000000000021
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
295,
|
||||||
|
301.96
|
||||||
|
],
|
||||||
|
"originalDuration": 13.274000000000001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
301.96,
|
||||||
|
315.061
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Multiple overlapping", () => {
|
||||||
|
previewBar.videoDuration = 3615.161;
|
||||||
|
const groups = previewBar.createChapterRenderGroups([{
|
||||||
|
"segment": [
|
||||||
|
160,
|
||||||
|
2797.323
|
||||||
|
],
|
||||||
|
"category": "chooseACategory",
|
||||||
|
"unsubmitted": true,
|
||||||
|
"showLarger": false,
|
||||||
|
},{
|
||||||
|
"segment": [
|
||||||
|
169,
|
||||||
|
3432.255
|
||||||
|
],
|
||||||
|
"category": "chooseACategory",
|
||||||
|
"unsubmitted": true,
|
||||||
|
"showLarger": false,
|
||||||
|
},{
|
||||||
|
"segment": [
|
||||||
|
169,
|
||||||
|
3412.413
|
||||||
|
],
|
||||||
|
"category": "chooseACategory",
|
||||||
|
"unsubmitted": true,
|
||||||
|
"showLarger": false,
|
||||||
|
},{
|
||||||
|
"segment": [
|
||||||
|
1594.92,
|
||||||
|
1674.286
|
||||||
|
],
|
||||||
|
"category": "sponsor",
|
||||||
|
"unsubmitted": false,
|
||||||
|
"showLarger": false,
|
||||||
|
}
|
||||||
|
] as unknown as PreviewBarSegment[]);
|
||||||
|
|
||||||
|
expect(groups).toStrictEqual([
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
0,
|
||||||
|
160
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
160,
|
||||||
|
169
|
||||||
|
],
|
||||||
|
"originalDuration": 2637.323
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
169,
|
||||||
|
1594.92
|
||||||
|
],
|
||||||
|
"originalDuration": 3243.413
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
1594.92,
|
||||||
|
1674.286
|
||||||
|
],
|
||||||
|
"originalDuration": 79.36599999999999
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
1674.286,
|
||||||
|
3412.413
|
||||||
|
],
|
||||||
|
"originalDuration": 3243.413
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
3412.413,
|
||||||
|
3432.255
|
||||||
|
],
|
||||||
|
"originalDuration": 3263.255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"segment": [
|
||||||
|
3432.255,
|
||||||
|
3615.161
|
||||||
|
],
|
||||||
|
"originalDuration": 0
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
})
|
||||||
@@ -29,9 +29,11 @@ module.exports = env => ({
|
|||||||
popup: path.join(__dirname, srcDir + 'popup.ts'),
|
popup: path.join(__dirname, srcDir + 'popup.ts'),
|
||||||
background: path.join(__dirname, srcDir + 'background.ts'),
|
background: path.join(__dirname, srcDir + 'background.ts'),
|
||||||
content: path.join(__dirname, srcDir + 'content.ts'),
|
content: path.join(__dirname, srcDir + 'content.ts'),
|
||||||
options: path.join(__dirname, srcDir + 'options.ts'),
|
options: path.join(__dirname, srcDir + 'options.ts'),
|
||||||
help: path.join(__dirname, srcDir + 'help.ts'),
|
help: path.join(__dirname, srcDir + 'help.ts'),
|
||||||
permissions: path.join(__dirname, srcDir + 'permissions.ts')
|
permissions: path.join(__dirname, srcDir + 'permissions.ts'),
|
||||||
|
document: path.join(__dirname, srcDir + 'document.ts'),
|
||||||
|
upsell: path.join(__dirname, srcDir + 'upsell.ts')
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, '../dist/js'),
|
path: path.join(__dirname, '../dist/js'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const merge = require('webpack-merge');
|
const { merge } = require('webpack-merge');
|
||||||
const common = require('./webpack.common.js');
|
const common = require('./webpack.common.js');
|
||||||
|
|
||||||
module.exports = env => merge(common(env), {
|
module.exports = env => merge(common(env), {
|
||||||
|
|||||||
Reference in New Issue
Block a user