mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-06 19:47:04 +03:00
Compare commits
235 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 | ||
|
|
3ee2e2517a | ||
|
|
dd7f227305 | ||
|
|
c1d3c7d680 | ||
|
|
fae6d0d0cf | ||
|
|
60d106fc52 | ||
|
|
a4df2eab8f | ||
|
|
fdbcf47149 | ||
|
|
b1ef8a5d47 | ||
|
|
4cb6baaff0 | ||
|
|
6cb4fac041 | ||
|
|
d7176a9c97 | ||
|
|
2eb0a34858 | ||
|
|
cf86e91988 | ||
|
|
058c41dd7e | ||
|
|
7a50167222 | ||
|
|
969b303c59 | ||
|
|
8114e0dcf7 | ||
|
|
561b3a2263 | ||
|
|
e0edb63501 | ||
|
|
70ef867ec5 | ||
|
|
23336fa65b | ||
|
|
fea90d024e | ||
|
|
de85d93602 | ||
|
|
e48d956577 | ||
|
|
efec6a113f | ||
|
|
0121a2aebd | ||
|
|
7badfd9b32 | ||
|
|
d0497d60e8 | ||
|
|
e223d12520 | ||
|
|
27e8e83c59 | ||
|
|
c7f254db70 | ||
|
|
85c3cd4a81 | ||
|
|
8d9042aeeb | ||
|
|
373edf883d | ||
|
|
7ed01a181e | ||
|
|
4119fd8433 | ||
|
|
cc7d7c0a0c | ||
|
|
61b39a99db | ||
|
|
98f776fa3a | ||
|
|
75f426f456 | ||
|
|
67b510e628 | ||
|
|
7cc0847db1 | ||
|
|
b92132bf47 | ||
|
|
cfecb9f94a | ||
|
|
fc81e02026 | ||
|
|
e12d5ff10a | ||
|
|
355572ba04 | ||
|
|
70731e42a5 | ||
|
|
023b875b0f | ||
|
|
82b027159e | ||
|
|
6b4da25847 | ||
|
|
529db4d6ca | ||
|
|
d132342ffe | ||
|
|
c6405fc0c1 | ||
|
|
1f6b8f6c53 | ||
|
|
6bdac234b0 | ||
|
|
337e9680b9 | ||
|
|
986630d0a1 | ||
|
|
ae23bfffe1 | ||
|
|
619683e842 | ||
|
|
1b5ba96bd9 | ||
|
|
bbc5b436e0 | ||
|
|
91311787df | ||
|
|
74e9a98afd | ||
|
|
b369dcc117 | ||
|
|
5a05e01b7d | ||
|
|
8097eff9bb | ||
|
|
c61c97ccad | ||
|
|
9a7baa7325 | ||
|
|
5ac577c99b | ||
|
|
3f421a2fb0 | ||
|
|
e94ce0ffef | ||
|
|
2dfcf2141f | ||
|
|
47220e0abc | ||
|
|
05eed6ee20 | ||
|
|
9eec62d59f | ||
|
|
25c04a49c1 | ||
|
|
efe6b0483c | ||
|
|
caafba5f53 | ||
|
|
32052c17f1 | ||
|
|
32a3cb2cfe | ||
|
|
d9970bf110 | ||
|
|
6514b41418 | ||
|
|
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 |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
- name: Copy configuration
|
||||
run: cp config.json.example config.json
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
name: ChromeExtension
|
||||
path: dist
|
||||
- run: mkdir ./builds
|
||||
- uses: montudor/action-zip@v1
|
||||
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||
with:
|
||||
args: zip -qq -r ./builds/ChromeExtension.zip ./dist
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
with:
|
||||
name: FirefoxExtension
|
||||
path: dist
|
||||
- uses: montudor/action-zip@v1
|
||||
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||
with:
|
||||
args: zip -qq -r ./builds/FirefoxExtension.zip ./dist
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
with:
|
||||
name: ChromeExtensionBeta
|
||||
path: dist
|
||||
- uses: montudor/action-zip@v1
|
||||
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||
with:
|
||||
args: zip -qq -r ./builds/ChromeExtensionBeta.zip ./dist
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
with:
|
||||
name: FirefoxExtensionBeta
|
||||
path: dist
|
||||
- uses: montudor/action-zip@v1
|
||||
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
|
||||
with:
|
||||
args: zip -qq -r ./builds/FirefoxExtensionBeta.zip ./dist
|
||||
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
- name: Copy configuration
|
||||
run: cp config.json.example config.json
|
||||
|
||||
2
.github/workflows/take-action.yml
vendored
2
.github/workflows/take-action.yml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: take the issue
|
||||
uses: bdougie/take-action@main
|
||||
uses: bdougie/take-action@28b86cd8d25593f037406ecbf96082db2836e928
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
19
.github/workflows/tests.yml
vendored
19
.github/workflows/tests.yml
vendored
@@ -3,8 +3,7 @@ name: Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -13,8 +12,18 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
|
||||
- run: sudo apt-get install chromium-chromedriver
|
||||
|
||||
- name: Copy configuration
|
||||
run: cp config.json.example config.json
|
||||
- name: Run tests
|
||||
run: npm run test-without-building
|
||||
run: npm run test
|
||||
|
||||
- name: Upload results on fail
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Test Results
|
||||
path: ./test-results
|
||||
4
.github/workflows/update-oss-attribution.yml
vendored
4
.github/workflows/update-oss-attribution.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
- name: Install and generate attribution
|
||||
run: |
|
||||
npm ci
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt
|
||||
|
||||
- name: Create pull request to update list
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
|
||||
with:
|
||||
commit-message: Update OSS Attribution
|
||||
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
|
||||
|
||||
- name: Create pull request to update list
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
|
||||
with:
|
||||
commit-message: Update Invidious List
|
||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@ web-ext-artifacts
|
||||
dist/
|
||||
tmp/
|
||||
.DS_Store
|
||||
ci/data.json
|
||||
ci/data.json
|
||||
test-results
|
||||
@@ -49,7 +49,7 @@ const reliableCheck = mapped
|
||||
.filter(instance => instance.url.includes(instance.name))
|
||||
|
||||
// finally map to array
|
||||
const result: string[] = reliableCheck.map(instance => instance.name)
|
||||
const result: string[] = reliableCheck.map(instance => instance.name).sort()
|
||||
writeFile(join(__dirname, "./invidiouslist.json"), JSON.stringify(result), (err) => {
|
||||
if (err) return console.log(err);
|
||||
})
|
||||
@@ -1 +1 @@
|
||||
["yewtu.be","vid.puffyan.us","invidious.snopyta.org","inv.riverside.rocks","invidious-us.kavin.rocks","invidious.osi.kr","tube.cthd.icu","invidious.flokinet.to","yt.artemislena.eu","invidious.mutahar.rocks","invidious.esmailelbob.xyz","youtube.076.ne.jp","invidious.weblibre.org","invidious.namazso.eu","invidious.kavin.rocks"]
|
||||
["inv.cthd.icu","inv.riverside.rocks","invidio.xamh.de","invidious.kavin.rocks","invidious.namazso.eu","invidious.osi.kr","invidious.snopyta.org","vid.puffyan.us","yewtu.be","youtube.076.ne.jp","yt.artemislena.eu"]
|
||||
@@ -2,7 +2,7 @@
|
||||
"serverAddress": "https://sponsor.ajay.app",
|
||||
"testingServerAddress": "https://sponsor.ajay.app/test",
|
||||
"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": {
|
||||
"sponsor": ["skip", "mute", "full"],
|
||||
"selfpromo": ["skip", "mute", "full"],
|
||||
@@ -13,7 +13,8 @@
|
||||
"preview": ["skip", "mute"],
|
||||
"filler": ["skip", "mute"],
|
||||
"music_offtopic": ["skip"],
|
||||
"poi_highlight": ["poi"]
|
||||
"poi_highlight": ["poi"],
|
||||
"chapter": ["chapter"]
|
||||
},
|
||||
"wikiLinks": {
|
||||
"sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor",
|
||||
@@ -27,6 +28,7 @@
|
||||
"music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section",
|
||||
"poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight",
|
||||
"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__",
|
||||
"short_name": "SponsorBlock",
|
||||
"version": "4.5.1",
|
||||
"version": "5.0",
|
||||
"default_locale": "en",
|
||||
"description": "__MSG_Description__",
|
||||
"homepage_url": "https://sponsor.ajay.app",
|
||||
@@ -18,6 +18,7 @@
|
||||
],
|
||||
"css": [
|
||||
"content.css",
|
||||
"shared.css",
|
||||
"./libs/Source+Sans+Pro.css",
|
||||
"popup.css"
|
||||
]
|
||||
@@ -48,9 +49,11 @@
|
||||
"icons/beep.ogg",
|
||||
"icons/pause.svg",
|
||||
"icons/stop.svg",
|
||||
"icons/skip.svg",
|
||||
"icons/heart.svg",
|
||||
"icons/visible.svg",
|
||||
"icons/not_visible.svg",
|
||||
"icons/sort.svg",
|
||||
"icons/money.svg",
|
||||
"icons/segway.png",
|
||||
"icons/close-smaller.svg",
|
||||
@@ -61,10 +64,13 @@
|
||||
"icons/bolt.svg",
|
||||
"icons/stopwatch.svg",
|
||||
"icons/music-note.svg",
|
||||
"icons/import.svg",
|
||||
"icons/export.svg",
|
||||
"icons/PlayerInfoIconSponsorBlocker.svg",
|
||||
"icons/PlayerDeleteIconSponsorBlocker.svg",
|
||||
"popup.html",
|
||||
"content.css"
|
||||
"content.css",
|
||||
"js/document.js"
|
||||
],
|
||||
"permissions": [
|
||||
"storage",
|
||||
|
||||
14812
package-lock.json
generated
14812
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -3,39 +3,39 @@
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "background.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.188",
|
||||
"@types/chrome": "^0.0.193",
|
||||
"@types/firefox-webext-browser": "^94.0.1",
|
||||
"@types/jest": "^27.5.1",
|
||||
"@types/react": "^17.0.43",
|
||||
"@types/react-dom": "^17.0.14",
|
||||
"@types/selenium-webdriver": "^4.1.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/react": "^17.0.47",
|
||||
"@types/react-dom": "^17.0.17",
|
||||
"@types/selenium-webdriver": "^4.1.2",
|
||||
"@types/wicg-mediasession": "^1.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.26.0",
|
||||
"@typescript-eslint/parser": "^5.26.0",
|
||||
"chromedriver": "^101.0.0",
|
||||
"concurrently": "^7.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||
"@typescript-eslint/parser": "^5.31.0",
|
||||
"chromedriver": "^103.0.0",
|
||||
"concurrently": "^7.3.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"eslint": "^8.16.0",
|
||||
"eslint-plugin-react": "^7.30.0",
|
||||
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
||||
"jest": "^28.1.0",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"fork-ts-checker-webpack-plugin": "^7.2.13",
|
||||
"jest": "^28.1.3",
|
||||
"jest-environment-jsdom": "^28.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"schema-utils": "^4.0.0",
|
||||
"selenium-webdriver": "^4.1.2",
|
||||
"selenium-webdriver": "^4.3.1",
|
||||
"speed-measure-webpack-plugin": "^1.5.0",
|
||||
"ts-jest": "^28.0.3",
|
||||
"ts-loader": "^9.3.0",
|
||||
"ts-node": "^10.8.0",
|
||||
"ts-jest": "^28.0.7",
|
||||
"ts-loader": "^9.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "4.7",
|
||||
"web-ext": "^6.8.0",
|
||||
"webpack": "^5.72.1",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"web-ext": "^7.1.1",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -71,7 +71,7 @@
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "hhttps://sponsor.ajay.app/donate"
|
||||
"url": "https://sponsor.ajay.app/donate"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"copyPublicID": {
|
||||
"message": "نسخ معرف المستخدم العام"
|
||||
},
|
||||
"copySegmentID": {
|
||||
"message": "نسخ معرف الجزء"
|
||||
},
|
||||
"discordAdvert": {
|
||||
"message": "انضم إلى سيرفر \"ديسكورد\" الرسمي لتقديم اقتراحات وتعليقات!"
|
||||
},
|
||||
@@ -200,6 +203,9 @@
|
||||
"showDeleteButton": {
|
||||
"message": "إظهار زر \"حذف\" على مشغّل اليوتيوب"
|
||||
},
|
||||
"enableViewTracking": {
|
||||
"message": "تمكين تتبع مرات التخطي"
|
||||
},
|
||||
"showNotice": {
|
||||
"message": "إظهار الإشعار مرة أخرى"
|
||||
},
|
||||
@@ -401,12 +407,18 @@
|
||||
"Donate": {
|
||||
"message": "تبرع"
|
||||
},
|
||||
"considerDonating": {
|
||||
"message": "ساعد في تمويل التطوير"
|
||||
},
|
||||
"hideDonationLink": {
|
||||
"message": "إخفاء رابط التبرع"
|
||||
},
|
||||
"darkModeOptionsPage": {
|
||||
"message": "الوضع الداكن في صفحة الخيارات"
|
||||
},
|
||||
"helpPageThanksForInstalling": {
|
||||
"message": "شكرا على تثبيت SponsorBlock."
|
||||
},
|
||||
"Editing": {
|
||||
"message": "التعديل"
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"message": "Deixa de silenciar"
|
||||
},
|
||||
"paused": {
|
||||
"message": "Pausat"
|
||||
"message": "En pausa"
|
||||
},
|
||||
"manualPaused": {
|
||||
"message": "S'ha aturat el temporitzador"
|
||||
@@ -222,5 +222,8 @@
|
||||
},
|
||||
"whatChangeUserID": {
|
||||
"message": "Això hauria de romandre privat: és semblant a una contrasenya i no s'hauria de compartir amb cap persona. Si algú hi té accés, poden suplantar-vos. Si esteu cercant el vostre identificador d'usuari públic, premeu la icona del portanotes a la finestra."
|
||||
},
|
||||
"help": {
|
||||
"message": "Ajuda"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
"message": "Versteckt die Schaltflächen im YouTube-Videoplayer, um Segmente einzusenden."
|
||||
},
|
||||
"showSkipButton": {
|
||||
"message": "\"Zum Highlight springen\"-Button im Youtube-Player anzeigen"
|
||||
"message": "Behalte \"Zum Highlight springen\"-Knopf in der Leiste"
|
||||
},
|
||||
"showInfoButton": {
|
||||
"message": "Zeige Info-Knopf im Youtube-Videoplayer"
|
||||
@@ -546,7 +546,7 @@
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "Geschmeidige Übergänge einbeziehen"
|
||||
"message": "Fließende Übergänge einbeziehen"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "Spielt, als ob nichts übersprungen worden wäre"
|
||||
@@ -558,7 +558,7 @@
|
||||
"message": "Bezahlte Werbung, bezahlte Empfehlungen und direkte Werbung, nicht für Eigenwerbung, kostenlose Fremdwerbung oder Empfehlungen für Anlässe/Personen/Webseiten/Produkte."
|
||||
},
|
||||
"category_sponsor_guideline1": {
|
||||
"message": "Bezahlte Aktionen"
|
||||
"message": "Bezahlte Werbeaktionen"
|
||||
},
|
||||
"category_sponsor_guideline2": {
|
||||
"message": "Nicht für Spenden oder eigenen Merch"
|
||||
@@ -598,10 +598,10 @@
|
||||
"message": "Wenn es im Video eine kurze Erinnerung gibt, den Kanal zu abonnieren oder das Video mit \"Mag ich\" zu markieren."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "Kurze Erinnerungen um zu Liken, Abonnieren oder zu Folgen"
|
||||
"message": "Kurze Erinnerungen zum Liken, Abonnieren oder Folgen"
|
||||
},
|
||||
"category_interaction_guideline2": {
|
||||
"message": "Enthält indirekte Erinnerungen zu kommentieren"
|
||||
"message": "Enthält indirekte Erinnerungen zum Kommentieren"
|
||||
},
|
||||
"category_interaction_guideline3": {
|
||||
"message": "Nicht für allgemeine Förderung, nur Aufruf zum Handeln"
|
||||
@@ -619,7 +619,7 @@
|
||||
"message": "Unterbrechung"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "Intervall ohne aktuellen Inhalt"
|
||||
"message": "Intervall ohne tatsächlichen Inhalt"
|
||||
},
|
||||
"category_intro_guideline2": {
|
||||
"message": "Nicht für Übergänge mit Informationen"
|
||||
@@ -867,7 +867,7 @@
|
||||
"message": "Dauerhaft verbergen"
|
||||
},
|
||||
"warningChatInfo": {
|
||||
"message": "Du hast eine Warnung erhalten und kannst vorübergehend keine Segmente einreichen. Uns ist nämlich aufgefallen, dass du nicht bösartige Fehler in deinen Einreichungen machst. Bitte bestätige, dass du die Regeln verstanden hast. Darauffolgend können wir die Warnung entfernen. Du kannst diesem Chat auch mit discord.gg/SponsorBlock oder matrix.to/#/#sponsor:ajay.app beitreten"
|
||||
"message": "Du hast eine Warnung erhalten und kannst vorübergehend keine Segmente einreichen. Dies bedeutet, dass du Fehler gemacht hast welche nicht bösartig sind, bitte bestätige, dass du die Regeln verstanden hast, wir werden dann die Warnung entfernen. Du kannst diesem Chat auch mit discord.gg/SponsorBlock oder matrix.to/#/#sponsor:ajay.app beitreten"
|
||||
},
|
||||
"voteRejectedWarning": {
|
||||
"message": "Abstimmung wegen einer Warnung abgelehnt. Klicke hier um einen Chat zu öffnen, oder versuch es später erneut, wenn du Zeit hast.",
|
||||
@@ -964,7 +964,7 @@
|
||||
"message": "Dies wirkt sich sofort auf eigene Segmente aus"
|
||||
},
|
||||
"downvote": {
|
||||
"message": "Negativ bewertet"
|
||||
"message": "Dagegen stimmen"
|
||||
},
|
||||
"upvote": {
|
||||
"message": "Positiv bewerten"
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
"copyPublicID": {
|
||||
"message": "Αντιγραφή δημόσιου αναγνωριστικού χρήστη"
|
||||
},
|
||||
"copySegmentID": {
|
||||
"message": "Αντιγραφή ταυτότητας τμήματος"
|
||||
},
|
||||
"discordAdvert": {
|
||||
"message": "Μπείτε στον επίσημο διακομιστή μας στο Discord για ανατροφοδότηση και προτάσεις!"
|
||||
},
|
||||
@@ -183,7 +186,7 @@
|
||||
"message": "Αυτό αποκρύπτει τα κουμπιά που εμφανίζονται στο πρόγραμμα αναπαραγωγής YouTube ώστε να υποβάλετε τμήματα προς παράλειψη."
|
||||
},
|
||||
"showSkipButton": {
|
||||
"message": "Διατήρηση του κουμπιού παράλειψης προς την καλύτερη στιγμή στο πρόγραμμα αναπαραγωγής"
|
||||
"message": "Διατήρηση του κουμπιού παράλειψης προς το κυριότερο περιεχόμενο του βίντεο στην οθόνη αναπαραγωγής"
|
||||
},
|
||||
"showInfoButton": {
|
||||
"message": "Εμφάνιση κουμπιού «πληροφορίες» στο πρόγραμμα αναπαραγωγής YouTube"
|
||||
@@ -209,6 +212,15 @@
|
||||
"enableViewTrackingInPrivate": {
|
||||
"message": "Ενεργοποίηση του μετρητή παραλείψεων σε καρτέλες Ιδιωτικής/Ανώνυμης περιήγησης"
|
||||
},
|
||||
"enableTrackDownvotes": {
|
||||
"message": "Αποθήκευση καταψηφίσεων τμημάτων"
|
||||
},
|
||||
"whatTrackDownvotes": {
|
||||
"message": "Όποια τμήματα καταψηφίζετε θα παραμένουν κρυμμένα ακόμα και μετά από ανανέωση της σελίδας"
|
||||
},
|
||||
"trackDownvotesWarning": {
|
||||
"message": "Προειδοποίηση: Η απενεργοποίηση αυτής της επιλογής θα διαγράψει όλες τις προηγούμενες αποθηκευμένες καταψηφίσεις"
|
||||
},
|
||||
"enableQueryByHashPrefix": {
|
||||
"message": "Αιτήματα μέσω προθέματος Hash"
|
||||
},
|
||||
@@ -265,6 +277,21 @@
|
||||
"message": "Εάν δεν σας αρέσουν, πατήστε το κουμπί «Να μην ξαναεμφανιστεί».",
|
||||
"description": "The second line of the message displayed after the notice was upgraded."
|
||||
},
|
||||
"setSkipShortcut": {
|
||||
"message": "Παράλειψη τμήματος",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"setStartSponsorShortcut": {
|
||||
"message": "Έναρξη/Τέλος τμήματος",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"setSubmitKeybind": {
|
||||
"message": "Υποβολή Τμημάτων",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"keybindDescription": {
|
||||
"message": "Επιλέξτε ένα πλήκτρο πατώντας το και επιλέξτε όποιο πρόσθετο πλήκτρο ελέγχου επιθυμείτε."
|
||||
},
|
||||
"0": {
|
||||
"message": "Λήξη χρονικού ορίου σύνδεσης. Παρακαλώ ελέγξτε ότι η σύνδεσή σας με το διαδίκτυο λειτουργεί, αλλιώς ο διακομιστής μπορεί να υπερφορτώθηκε ή να έπεσε."
|
||||
},
|
||||
@@ -285,14 +312,21 @@
|
||||
"message": "Κωδικός σφάλματος: "
|
||||
},
|
||||
"skip": {
|
||||
"message": "Παράκαμψη"
|
||||
"message": "Παράλειψη"
|
||||
},
|
||||
"mute": {
|
||||
"message": "Σίγαση"
|
||||
},
|
||||
"full": {
|
||||
"message": "Όλο το βίντεο",
|
||||
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
|
||||
},
|
||||
"skip_category": {
|
||||
"message": "Παράλειψη {0};"
|
||||
},
|
||||
"mute_category": {
|
||||
"message": "Σίγαση {0};"
|
||||
},
|
||||
"skip_to_category": {
|
||||
"message": "Παράλειψη προς το σημείο «{0}»;",
|
||||
"description": "Used for skipping to things (Skip to Highlight)"
|
||||
@@ -301,15 +335,19 @@
|
||||
"message": "Παραλείφθηκε «{0}»",
|
||||
"description": "Example: Sponsor Skipped"
|
||||
},
|
||||
"muted": {
|
||||
"message": "Σίγαση {0}",
|
||||
"description": "Example: Sponsor Muted"
|
||||
},
|
||||
"skipped_to_category": {
|
||||
"message": "Παραλείφθηκε προς το σημείο «{0}»",
|
||||
"description": "Used for skipping to things (Skipped to Highlight)"
|
||||
},
|
||||
"disableAutoSkip": {
|
||||
"message": "Απενεργοποίηση Αυτόματης Παράληψης"
|
||||
"message": "Απενεργοποίηση Αυτόματης Παράλειψης"
|
||||
},
|
||||
"enableAutoSkip": {
|
||||
"message": "Ενεργοποίηση Αυτόματης Παράληψης"
|
||||
"message": "Ενεργοποίηση Αυτόματης Παράλειψης"
|
||||
},
|
||||
"audioNotification": {
|
||||
"message": "Ηχητική ειδοποίηση κατά την παράληψη"
|
||||
@@ -318,7 +356,7 @@
|
||||
"message": "Οι ειδοποιήσεις μετά την παράλειψη κάθε τμήματος θα αναπαράγουν ήχο. Εάν είναι ανενεργή αυτή η επιλογή εκτός αν η αυτόματη παράλειψη είναι ανενεργή."
|
||||
},
|
||||
"showTimeWithSkips": {
|
||||
"message": "Εμφάνιση χρόνου συμπεριλαμβάνοντας τις παραλήψεις"
|
||||
"message": "Εμφάνιση χρόνου συμπεριλαμβάνοντας τις παραλείψεις"
|
||||
},
|
||||
"showTimeWithSkipsDescription": {
|
||||
"message": "Αυτός ο χρόνος εμφανίζεται σε παρενθέσεις δίπλα από τον τρέχων χρόνο που είναι κάτω από την μπάρα χρόνου. Αυτό δείχνει την συνολική διάρκεια του βίντεο πλην οποιουδήποτε τμήματος. Αυτό περιλαμβάνει και τα τμήματα που εμφανίζονται μόνο στην μπάρα χρόνου."
|
||||
@@ -352,6 +390,9 @@
|
||||
"changeUserID": {
|
||||
"message": "Εισαγωγή/Εξαγωγή της Ταυτότητας Χρήστη σας"
|
||||
},
|
||||
"whatChangeUserID": {
|
||||
"message": "Κρατείστε το μυστικό. Αυτό είναι σαν ένα κωδικό πρόσβασης όπου δε θα έπρεπε να μοιράζεστε με κανένα. Εάν το αποκτήσει κάποιος, μπορεί να σας υποδυθεί. Εάν ψάχνετε για τη δημόσια ταυτότητα χρήστη σας, πατήστε πάνω στο αναδυόμενο εικονίδιο πρόχειρου."
|
||||
},
|
||||
"setUserID": {
|
||||
"message": "Ορισμός Ταυτότητας Χρήστη"
|
||||
},
|
||||
@@ -372,7 +413,7 @@
|
||||
"message": "Υποστηριζόμενες Ιστοσελίδες: "
|
||||
},
|
||||
"optionsInfo": {
|
||||
"message": "Ενεργοποίηση υποστήριξης «Invidious», απενεργοποίηση αυτόματης παράληψης, απόκρυψη κουμπιών και άλλα."
|
||||
"message": "Ενεργοποίηση υποστήριξης «Invidious», απενεργοποίηση αυτόματης παράλειψης, απόκρυψη κουμπιών και άλλα."
|
||||
},
|
||||
"addInvidiousInstance": {
|
||||
"message": "Προσθήκη 3ου πελάτη συνεδρίας"
|
||||
@@ -410,6 +451,9 @@
|
||||
"shortCheck": {
|
||||
"message": "Η ακόλουθη υποβολή είναι μικρότερη από την επιλεγμένη ελάχιστη διάρκεια. Αυτό θα μπορούσε να σημαίνει ότι αυτό το τμήμα έχει ήδη υποβληθεί, και απλά αγνοείται λόγω αυτής της επιλογής. Είστε σίγουροι ότι θα θέλατε να το υποβάλετε;"
|
||||
},
|
||||
"liveOrPremiere": {
|
||||
"message": "Η υποβολή σε μια ζωντανή μετάδοση σε εξέλιξη ή σε μια πρεμιέρα δεν επιτρέπεται. Παρακαλώ περιμένετε μέχρι να τελειώσει, στην συνέχεια ανανεώστε την σελίδα και επαληθεύστε ότι τα τμήματα είναι έγκυρα."
|
||||
},
|
||||
"showUploadButton": {
|
||||
"message": "Εμφάνιση κουμπιού υποβολής"
|
||||
},
|
||||
@@ -437,6 +481,15 @@
|
||||
"exportOptions": {
|
||||
"message": "Εισαγωγή/Εξαγωγή όλων των ρυθμίσεων"
|
||||
},
|
||||
"exportOptionsCopy": {
|
||||
"message": "Επεξεργασία/αντιγραφή"
|
||||
},
|
||||
"exportOptionsDownload": {
|
||||
"message": "Αποθήκευση σε αρχείο"
|
||||
},
|
||||
"exportOptionsUpload": {
|
||||
"message": "Φόρτωση από αρχείο"
|
||||
},
|
||||
"whatExportOptions": {
|
||||
"message": "Αυτές είναι όλες σας οι ρυθμίσεις σε αρχείο JSON. Αυτό περιλαμβάνει και την Ταυτότητα Χρήστη, οπότε μοιραστείτε το με προσοχή."
|
||||
},
|
||||
@@ -485,28 +538,74 @@
|
||||
"copyDebugInformationComplete": {
|
||||
"message": "Οι πληροφορίες εντοπισμού σφαλμάτων έχουν αντιγραφεί στο πρόχειρο. Μη διστάσετε να αφαιρέσετε οποιαδήποτε πληροφορία που προτιμάτε να μη μοιραστείτε. Αποθηκεύστε τες σε ένα αρχείο κειμένου ή επικολλήστε στην αναφορά σφάλματός σας."
|
||||
},
|
||||
"keyAlreadyUsed": {
|
||||
"message": "Αυτό το πλήκτρο έχει οριστεί ως συντόμευση για άλλη ενέργεια. Παρακαλώ επιλέξτε ένα άλλο πλήκτρο."
|
||||
},
|
||||
"to": {
|
||||
"message": "έως",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "Συμπερίληψη τμημάτων μεταβάσεων"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "Ροή περιεχομένου σαν να μην υπήρξε ποτέ παράλειψη"
|
||||
},
|
||||
"category_sponsor": {
|
||||
"message": "Χορηγία"
|
||||
},
|
||||
"category_sponsor_description": {
|
||||
"message": "Προώθηση επί πληρωμή, παραπομπές επί πληρωμή και άμεσες διαφημίσεις. Όχι, για προσωπική προώθηση ή δωρεάν αναφορές σε δημιουργούς/ιστοσελίδες/προϊόντα που τους αρέσουν."
|
||||
},
|
||||
"category_sponsor_guideline1": {
|
||||
"message": "Προώθηση επί πληρωμή"
|
||||
},
|
||||
"category_sponsor_guideline2": {
|
||||
"message": "Όχι για δωρεές ή προσαρμοσμένα προϊόντα"
|
||||
},
|
||||
"category_selfpromo": {
|
||||
"message": "Αφιλοκέρδεια/Προσωπική Προώθηση"
|
||||
},
|
||||
"category_selfpromo_description": {
|
||||
"message": "Παρόμοιο με τη «χορηγία» αλλά για μη κερδοσκοπικό σκοπό ή για προσωπική προώθηση. Αυτό συμπεριλαμβάνει τμήματα από εμπορεύματα, δωρεές, ή πληροφορίες σχετικές με το ποιους συνεργάστηκαν."
|
||||
},
|
||||
"category_selfpromo_guideline1": {
|
||||
"message": "Δωρεές, συνδρομές και προσαρμοσμένα προϊόντα"
|
||||
},
|
||||
"category_selfpromo_guideline2": {
|
||||
"message": "Δωρεάν αναφορές που δεν προστίθενται στο βίντεο"
|
||||
},
|
||||
"category_selfpromo_guideline3": {
|
||||
"message": "Όχι για προϊόντα ή ρουχισμό που έχει κατασκευαστεί από αλυσίδες επιχειρήσεων"
|
||||
},
|
||||
"category_exclusive_access": {
|
||||
"message": "Αποκλειστική Πρόσβαση"
|
||||
},
|
||||
"category_exclusive_access_description": {
|
||||
"message": "Μόνο για χαρακτηρισμό ολοκλήρου του βίντεο. Χρησιμοποιείται όταν ένα βίντεο παρουσιάζει ένα προϊόν, υπηρεσία, ή τοποθεσία αποκλειστικής ή δωρεάν πρόσβασής τους."
|
||||
},
|
||||
"category_exclusive_access_pill": {
|
||||
"message": "Αυτό το βίντεο παρουσιάζει ένα προϊόν, υπηρεσία, ή τοποθεσία αποκλειστικής ή δωρεάν πρόσβασής τους",
|
||||
"description": "Short description for this category"
|
||||
},
|
||||
"category_exclusive_access_guideline1": {
|
||||
"message": "Ολόκληρο το βίντεο παρουσιάζει κάτι με δωρεάν ή επιδοτούμενη πρόσβαση"
|
||||
},
|
||||
"category_interaction": {
|
||||
"message": "Υπενθύμιση Αλληλεπίδρασης (Εγγραφή)"
|
||||
},
|
||||
"category_interaction_description": {
|
||||
"message": "Όταν υπάρχει μια σύντομη υπενθύμιση για να προσθέσετε το βίντεο στα βίντεο που σας αρέσουν, να εγγραφείτε ή να τους ακολουθήσετε στη μέση του περιεχομένου. Εάν διαρκεί για αρκετή ώρα, τότε αυτό περιλαμβάνεται στη κατηγορία «προσωπική προώθηση»."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "Σύντομες υπενθυμίσεις εγγραφής, μου αρέσει και ακολούθησης"
|
||||
},
|
||||
"category_interaction_guideline2": {
|
||||
"message": "Περιλαμβάνει έμμεσες υπενθυμίσεις για σχόλια"
|
||||
},
|
||||
"category_interaction_guideline3": {
|
||||
"message": "Όχι για γενικότερες προωθήσεις, μόνο για εκκλήσεις βοήθειας"
|
||||
},
|
||||
"category_interaction_short": {
|
||||
"message": "Υπενθύμιση Αλληλεπίδρασης"
|
||||
},
|
||||
@@ -519,18 +618,54 @@
|
||||
"category_intro_short": {
|
||||
"message": "Διάλειμμα"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "Χρονικό διάστημα χωρίς ουσιαστικό περιεχόμενο"
|
||||
},
|
||||
"category_intro_guideline2": {
|
||||
"message": "Όχι για μεταβάσεις που συμπεριλαμβάνουν πληροφορίες"
|
||||
},
|
||||
"category_outro": {
|
||||
"message": "Προτεινόμενα βίντεο καναλιών/Εύσημα"
|
||||
},
|
||||
"category_outro_description": {
|
||||
"message": "Όταν εμφανίζονται τα εύσημα ή τα προτεινόμενα βίντεο των καναλιών. Όχι, για επίλογους που περιέχουν πληροφορίες."
|
||||
},
|
||||
"category_outro_guideline1": {
|
||||
"message": "Μη συμπερίληψη περιεχομένου, ακόμα και αν τα προτεινόμενα βίντεο βρίσκονται στην οθόνη"
|
||||
},
|
||||
"category_preview": {
|
||||
"message": "Προεπισκόπηση/Αναθεώρηση"
|
||||
},
|
||||
"category_preview_description": {
|
||||
"message": "Γρήγορη ανακεφαλαίωση προηγουμένων επεισοδίων, ή προεπισκόπηση του τι ακολουθεί στο τρέχων βίντεο. Εννοώντας επεξεργασία μερικών κλιπ μαζί, όχι για προφορικές περιγραφές."
|
||||
},
|
||||
"category_preview_guideline1": {
|
||||
"message": "Κλιπ που εμφανίζονται μετά, ή σε μελλοντικά βίντεο"
|
||||
},
|
||||
"category_preview_guideline2": {
|
||||
"message": "Ανακεφαλαίωση προηγούμενου βίντεο"
|
||||
},
|
||||
"category_preview_guideline3": {
|
||||
"message": "Όχι για μέρη που περιλαμβάνουν πρόσθετο περιεχόμενο"
|
||||
},
|
||||
"category_filler": {
|
||||
"message": "Σπατάλη Χρόνου/Περιττό σχόλιο"
|
||||
},
|
||||
"category_filler_description": {
|
||||
"message": "Σκηνές εκτός θέματος προστίθενται μόνο για σπατάλη χρόνου και περιττά σχόλια τα οποία δεν απαιτούνται για να κατανοήσετε το κύριο περιεχόμενο του βίντεο. Δεν πρέπει να περιλαμβάνονται τμήματα που συμβάλουν στην κατανόηση του θέματος του βίντεο ή επιπλέον πληροφορίες."
|
||||
},
|
||||
"category_filler_short": {
|
||||
"message": "Περιττό"
|
||||
},
|
||||
"category_filler_guideline1": {
|
||||
"message": "Άσχετες σκηνές μόνο για σπατάλη χρόνου ή περιττά σχόλια"
|
||||
},
|
||||
"category_filler_guideline2": {
|
||||
"message": "Αποσπάσεις, επαναλήψεις, αποτυχημένες λήψεις"
|
||||
},
|
||||
"category_filler_guideline3": {
|
||||
"message": "Όχι για σκηνές που απαιτούνται για την κατανόηση του θέματος"
|
||||
},
|
||||
"category_music_offtopic": {
|
||||
"message": "Μουσική: Τμήμα χωρίς μουσική"
|
||||
},
|
||||
@@ -540,12 +675,27 @@
|
||||
"category_music_offtopic_short": {
|
||||
"message": "Χωρίς Μουσική"
|
||||
},
|
||||
"category_music_offtopic_guideline1": {
|
||||
"message": "Μέρη τα οποία δεν έχουν κυκλοφορήσει επίσημα"
|
||||
},
|
||||
"category_music_offtopic_guideline2": {
|
||||
"message": "Χωρίς μουσική σε ζωντανή συναυλία"
|
||||
},
|
||||
"category_poi_highlight": {
|
||||
"message": "Καλύτερη στιγμή"
|
||||
},
|
||||
"category_poi_highlight_description": {
|
||||
"message": "Το κομμάτι του βίντεο που ψάχνουν να δουν οι περισσότεροι άνθρωποι. Παρόμοιο με το «Το βίντεο ξεκινάει από το x σημείο» στα σχόλια."
|
||||
},
|
||||
"category_poi_highlight_guideline1": {
|
||||
"message": "Το μέρος που ψάχνουν οι περισσότεροι"
|
||||
},
|
||||
"category_poi_highlight_guideline2": {
|
||||
"message": "Ίσως παραλειφθεί κάτι που συμβάλει στην κατανόηση"
|
||||
},
|
||||
"category_poi_highlight_guideline3": {
|
||||
"message": "Παράλειψη προς το περιεχόμενο όπου παρουσιάζεται στον τίτλο ή το εικονίδιο"
|
||||
},
|
||||
"category_livestream_messages": {
|
||||
"message": "Ζωντανή μετάδοση: Δωρεές/Ανάγνωση Μηνυμάτων από δωρεές"
|
||||
},
|
||||
@@ -553,10 +703,10 @@
|
||||
"message": "Ανάγνωση Μηνυμάτων"
|
||||
},
|
||||
"autoSkip": {
|
||||
"message": "Αυτόματη Παράληψη"
|
||||
"message": "Αυτόματη Παράλειψη"
|
||||
},
|
||||
"manualSkip": {
|
||||
"message": "Χειροκίνητη Παράληψη"
|
||||
"message": "Χειροκίνητη Παράλειψη"
|
||||
},
|
||||
"showOverlay": {
|
||||
"message": "Εμφάνιση στην μπάρα χρόνου"
|
||||
@@ -573,9 +723,19 @@
|
||||
"showOverlay_POI": {
|
||||
"message": "Εμφάνιση στην μπάρα χρόνου"
|
||||
},
|
||||
"showOverlay_full": {
|
||||
"message": "Εμφάνιση Ετικέτας"
|
||||
},
|
||||
"autoSkipOnMusicVideos": {
|
||||
"message": "Αυτόματη παράλειψη όλων των τμημάτων όταν είναι «Χωρίς μουσική»"
|
||||
},
|
||||
"muteSegments": {
|
||||
"message": "Επιτρέψτε τμήματα που κάνουν σίγαση αντί για παράλειψη"
|
||||
},
|
||||
"fullVideoSegments": {
|
||||
"message": "Εμφάνιση εικονιδίου όταν όλο το βίντεο είναι διαφημιστικό εξολοκλήρου",
|
||||
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
|
||||
},
|
||||
"previewColor": {
|
||||
"message": "Χρώμα Μη Υποβληθέντων",
|
||||
"description": "Referring to submissions that have not been sent to the server yet."
|
||||
@@ -612,6 +772,9 @@
|
||||
"message": "Για να υποβάλετε τμήματα της «{0}» κατηγορίας, πρέπει να την ενεργοποιήσετε στις ρυθμίσεις. Θα μεταφερθείτε στις ρυθμίσεις τώρα.",
|
||||
"description": "Used when submitting segments to only let them select a certain category if they have it enabled in the options."
|
||||
},
|
||||
"poiOnlyOneSegment": {
|
||||
"message": "Προσοχή: Αυτός ο τύπος τμήματος μπορεί να υπάρξει μόνο μια φορά. Εάν υποβάλετε πολλαπλά τότε εμφανίζεται ένα από τα τμήματα στην τύχη."
|
||||
},
|
||||
"youMustSelectACategory": {
|
||||
"message": "Πρέπει να επιλέξετε Κατηγορία για όλα τα τμήματα που υποβάλετε!"
|
||||
},
|
||||
@@ -624,6 +787,9 @@
|
||||
"hiddenDueToDuration": {
|
||||
"message": "κρυφό: πολύ κοντό"
|
||||
},
|
||||
"manuallyHidden": {
|
||||
"message": "χειροκίνητη απόκρυψη"
|
||||
},
|
||||
"channelDataNotFound": {
|
||||
"description": "This error appears in an alert when they try to whitelist a channel and the extension is unable to determine what channel they are looking at.",
|
||||
"message": "Η ταυτότητα καναλιού δεν έχει φορτώσει ακόμα. Εάν χρησιμοποιείτε ενσωματωμένο βίντεο, δοκιμάστε αντιθέτως να το ανοίξετε στο YouTube. Μπορεί επίσης να ευθύνονται αλλαγές στη διάταξη του YouTube, εάν το πιστεύετε, γράψτε ένα σχόλιο εδώ:"
|
||||
@@ -658,6 +824,9 @@
|
||||
"downvoteDescription": {
|
||||
"message": "Λάθος Συγχρονισμός"
|
||||
},
|
||||
"incorrectCategory": {
|
||||
"message": "Αλλαγή κατηγορίας"
|
||||
},
|
||||
"nonMusicCategoryOnMusic": {
|
||||
"message": "Αυτό το βίντεο έχει κατηγοριοποιηθεί ως μουσική. Είσαστε σίγουροι ότι έχει χορηγία; Εάν είναι ένα τμήμα «Χωρίς μουσική», ανοίξτε τις επιλογές της επέκτασης και ενεργοποιήστε αυτήν την κατηγορία. Μετά μπορείτε, να υποβάλετε αυτό το τμήμα ως «Χωρίς μουσική», αντί για χορηγία. Παρακαλώ διαβάστε τις οδηγίες, εάν είστε μπερδεμένοι."
|
||||
},
|
||||
@@ -684,6 +853,12 @@
|
||||
"message": "Το' χω",
|
||||
"description": "Used as the button to dismiss a tooltip"
|
||||
},
|
||||
"fullVideoTooltipWarning": {
|
||||
"message": "Αυτό το τμήμα είναι μεγάλο. Αν όλο το βίντεο έχει να κάνει με ένα θέμα, τότε αλλάξτε την επιλογή από \"Παράλειψη\" σε \"Όλο το βίντεο\". Δείτε τις οδηγίες για περισσότερες πληροφορίες."
|
||||
},
|
||||
"categoryPillTitleText": {
|
||||
"message": "Ολόκληρο το βίντεο έχει χαρακτηριστεί ως αυτής της κατηγορίας οπότε είναι αυστηρά ενσωματωμένη ώστε να μπορείτε να το διαφοροποιήσετε σε τμήματα"
|
||||
},
|
||||
"experiementOptOut": {
|
||||
"message": "Αποχή από μελλοντικά πειράματα",
|
||||
"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."
|
||||
@@ -691,6 +866,9 @@
|
||||
"hideForever": {
|
||||
"message": "Απόκρυψη για πάντα"
|
||||
},
|
||||
"warningChatInfo": {
|
||||
"message": "Λάβατε προειδοποίηση οπότε δεν μπορείτε να υποβάλετε τμήματα προσωρινά. Αυτό σημαίνει ότι παρατηρήσαμε ότι κάνατε κάποια λάθη των πρωτάριδων και όχι κάτι ύποπτο, παρακαλώ επιβεβαιώστε ότι κατανοείτε τους κανόνες και θα σας αφαιρέσουμε την προειδοποίηση. Μπορείτε και να μας στείλετε μήνυμα στο discord.gg/SponsorBlock ή στο matrix.to/#/#sponsor:ajay.app"
|
||||
},
|
||||
"voteRejectedWarning": {
|
||||
"message": "Η ψήφος απορρίφθηκε λόγο μιας προειδοποίησης. Πατήστε για να ανοίξετε μια συνομιλία ώστε να το διορθώσετε, ή επιστρέψτε όταν έχετε περισσότερο χρόνο.",
|
||||
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
|
||||
@@ -698,9 +876,15 @@
|
||||
"Donate": {
|
||||
"message": "Δωρεά"
|
||||
},
|
||||
"considerDonating": {
|
||||
"message": "Χρηματική βοήθεια για την ανάπτυξή μας"
|
||||
},
|
||||
"hideDonationLink": {
|
||||
"message": "Απόκρυψη Συνδέσμου Δωρεάς"
|
||||
},
|
||||
"darkModeOptionsPage": {
|
||||
"message": "Σκουρόχρωμη Λειτουργία στη σελίδα Ρυθμίσεις"
|
||||
},
|
||||
"helpPageThanksForInstalling": {
|
||||
"message": "Ευχαριστούμε που εγκαταστήσατε το SponsorBlock."
|
||||
},
|
||||
@@ -734,25 +918,116 @@
|
||||
"helpPageEditing1": {
|
||||
"message": "Αν κάνατε κάτι λάθος, μπορείτε να διορθώσετε ή να διαγράψετε τα τμήματα σας πατώντας το εικονίδιο πάνω βελάκι."
|
||||
},
|
||||
"helpPageTooSlow": {
|
||||
"message": "Το να υποβάλεις τμήματα παίρνει χρόνο, υπάρχει ποιο γρήγορος τρόπος;"
|
||||
},
|
||||
"helpPageTooSlow1": {
|
||||
"message": "Συντομεύσεις που μπορείτε να χρησιμοποιήσετε εάν το επιθυμείτε. Πατήστε την αγγλική άνω τελεία «;» για να δείξετε την αρχή/το τέλος ενός τμήματος χορηγίας και πατήστε την απόστροφο για υποβολή. Αυτές οι συντομεύσεις αλλάζουν στις ρυθμίσεις. Εάν δεν χρησιμοποιείτε το σύστημα πληκτρολόγησης «QWERTY», θα ήταν καλύτερο τα τις αλλάξετε."
|
||||
},
|
||||
"helpPageCopyOfDatabase": {
|
||||
"message": "Μπορώ να λάβω ένα αντίγραφο της βάσης δεδομένων; Τι θα συμβεί αν η επέκταση εξαφανιστεί;"
|
||||
},
|
||||
"helpPageCopyOfDatabase1": {
|
||||
"message": "Η βάση δεδομένων είναι δημόσια διαθέσιμη στη σελίδα"
|
||||
},
|
||||
"helpPageCopyOfDatabase2": {
|
||||
"message": "Ο πηγαίος κώδικας είναι δημόσια διαθέσιμος. Έτσι ώστε, ακόμα και αν συμβεί κάτι σε εμάς, οι υποβολές σας δε θα χαθούν."
|
||||
},
|
||||
"helpPageNews": {
|
||||
"message": "Νέα και πως δημιουργήθηκε η επέκταση"
|
||||
},
|
||||
"helpPageSourceCode": {
|
||||
"message": "Πού μπορώ να βρω τον πηγαίο κώδικα;"
|
||||
},
|
||||
"Credits": {
|
||||
"message": "Εύσημα"
|
||||
},
|
||||
"LearnMore": {
|
||||
"message": "Μάθετε περισσότερα"
|
||||
},
|
||||
"FullDetails": {
|
||||
"message": "Πλήρεις Λεπτομέρειες"
|
||||
},
|
||||
"CopyDownvoteButtonInfo": {
|
||||
"message": "Καταψήφιση και δημιουργία τοπικού αντίγραφου για επανάληψη υποβολής"
|
||||
},
|
||||
"OpenCategoryWikiPage": {
|
||||
"message": "Άνοιγμα σελίδας wiki της κατηγορίας."
|
||||
},
|
||||
"CopyAndDownvote": {
|
||||
"message": "Αντίγραφο και καταψήφιση"
|
||||
},
|
||||
"ContinueVoting": {
|
||||
"message": "Συνεχίστε την ψηφοφορία"
|
||||
},
|
||||
"ChangeCategoryTooltip": {
|
||||
"message": "Αυτό θα εφαρμοστεί απευθείας στα τμήματά σας"
|
||||
},
|
||||
"downvote": {
|
||||
"message": "Αρνητική ψήφος"
|
||||
},
|
||||
"upvote": {
|
||||
"message": "Θετική ψήφος"
|
||||
},
|
||||
"hideSegment": {
|
||||
"message": "Απόκρυψη τμήματος"
|
||||
},
|
||||
"SponsorTimeEditScrollNewFeature": {
|
||||
"message": "Χρήση της ροδέλας ποντικιού καθώς είναι πάνω από το παράθυρο επεξεργασίας για να αλλάξετε γρηγορότερα τον χρόνο. Συνδυασμοί όπως το ctrl ή το shift χρησιμοποιούνται για να τελειοποιήσετε τις αλλαγές."
|
||||
},
|
||||
"categoryPillNewFeature": {
|
||||
"message": "Νέο! Δείτε ποτέ ένα βίντεο αποτελείται εξολοκλήρου από προσωπικές προωθήσεις ή χορηγίες"
|
||||
},
|
||||
"dayAbbreviation": {
|
||||
"message": "ημ",
|
||||
"description": "100d"
|
||||
},
|
||||
"hourAbbreviation": {
|
||||
"message": "ω",
|
||||
"description": "100h"
|
||||
},
|
||||
"optionsTabBehavior": {
|
||||
"message": "Συμπεριφορά",
|
||||
"message": "Συμπεριφορά παραλ.",
|
||||
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
|
||||
},
|
||||
"optionsTabInterface": {
|
||||
"message": "Διεπαφή",
|
||||
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
|
||||
},
|
||||
"optionsTabKeyBinds": {
|
||||
"message": "Συντομεύσεις πλήκτρων",
|
||||
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
|
||||
},
|
||||
"optionsTabBackup": {
|
||||
"message": "Αντίγραφο/Επαναφορά",
|
||||
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
|
||||
},
|
||||
"optionsTabAdvanced": {
|
||||
"message": "Διάφορα",
|
||||
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
|
||||
},
|
||||
"noticeVisibilityLabel": {
|
||||
"message": "Εμφάνιση ειδοποίησης παράλειψης",
|
||||
"description": "Option label"
|
||||
},
|
||||
"unbind": {
|
||||
"message": "Αφαίρεση",
|
||||
"description": "Unbind keyboard shortcut"
|
||||
},
|
||||
"notSet": {
|
||||
"message": "Δεν έχει οριστεί"
|
||||
},
|
||||
"change": {
|
||||
"message": "Αλλαγή"
|
||||
},
|
||||
"youtubeKeybindWarning": {
|
||||
"message": "Αυτή η συντόμευση χρησιμοποιείται από το YouTube. Σίγουρα θέλετε να την χρησιμοποιήσετε;"
|
||||
},
|
||||
"betaServerWarning": {
|
||||
"message": "Ο Δοκιμαστικός Διακομιστής είναι ενεργός!"
|
||||
},
|
||||
"openOptionsPage": {
|
||||
"message": "Άνοιγμα σελίδας επιλογών"
|
||||
"message": "Άνοιγμα σελίδας ρυθμίσεων"
|
||||
},
|
||||
"resetToDefault": {
|
||||
"message": "Επαναφορά των προκαθορισμένων ρυθμίσεων"
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
"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": {
|
||||
"message": "Upvote this submission"
|
||||
},
|
||||
@@ -115,6 +125,9 @@
|
||||
"SubmitTimes": {
|
||||
"message": "Submit Segments"
|
||||
},
|
||||
"sortSegments": {
|
||||
"message": "Sort Segments"
|
||||
},
|
||||
"submitCheck": {
|
||||
"message": "Are you sure you want to submit this?"
|
||||
},
|
||||
@@ -289,6 +302,14 @@
|
||||
"message": "Submit segments",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"nextChapterKeybind": {
|
||||
"message": "Next chapter",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"previousChapterKeybind": {
|
||||
"message": "Previous chapter",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"keybindDescription": {
|
||||
"message": "Select a key by typing it and choose any modifier keys you wish to use."
|
||||
},
|
||||
@@ -545,6 +566,10 @@
|
||||
"message": "to",
|
||||
"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": {
|
||||
"message": "Include segue transitions"
|
||||
},
|
||||
@@ -637,7 +662,7 @@
|
||||
"message": "Preview/Recap"
|
||||
},
|
||||
"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": {
|
||||
"message": "Clips that appear later, or in a future video"
|
||||
@@ -696,6 +721,21 @@
|
||||
"category_poi_highlight_guideline3": {
|
||||
"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": {
|
||||
"message": "Livestream: Donation/Message Readings"
|
||||
},
|
||||
@@ -726,6 +766,9 @@
|
||||
"showOverlay_full": {
|
||||
"message": "Show Label"
|
||||
},
|
||||
"showOverlay_chapter": {
|
||||
"message": "Show Chapters"
|
||||
},
|
||||
"autoSkipOnMusicVideos": {
|
||||
"message": "Auto skip all segments when there is a non-music segment"
|
||||
},
|
||||
@@ -781,6 +824,10 @@
|
||||
"bracketEnd": {
|
||||
"message": "(End)"
|
||||
},
|
||||
"End": {
|
||||
"message": "End",
|
||||
"description": "Button that skips to the end of a segment"
|
||||
},
|
||||
"hiddenDueToDownvote": {
|
||||
"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.",
|
||||
"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": {
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
"invidiousPermissionRefresh": {
|
||||
"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."
|
||||
},
|
||||
"acceptPermission": {
|
||||
"message": "Accept permission"
|
||||
@@ -824,6 +868,13 @@
|
||||
"downvoteDescription": {
|
||||
"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": {
|
||||
"message": "Change Category"
|
||||
},
|
||||
@@ -859,6 +910,9 @@
|
||||
"categoryPillTitleText": {
|
||||
"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": {
|
||||
"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."
|
||||
@@ -867,11 +921,19 @@
|
||||
"message": "Hide forever"
|
||||
},
|
||||
"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": {
|
||||
"message": "Vote rejected due to a warning. Click to open a chat to resolve it, or come back later when you have time.",
|
||||
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
|
||||
"warningTitle": {
|
||||
"message": "You got a warning"
|
||||
},
|
||||
"questionButton": {
|
||||
"message": "I have a question"
|
||||
},
|
||||
"warningConfirmButton": {
|
||||
"message": "I understand the reason"
|
||||
},
|
||||
"warningError": {
|
||||
"message": "Error when trying to acknowledge warning:"
|
||||
},
|
||||
"Donate": {
|
||||
"message": "Donate"
|
||||
@@ -1034,5 +1096,117 @@
|
||||
},
|
||||
"confirmResetToDefault": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,6 +545,9 @@
|
||||
"message": "a",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "Incluye transiciones entre segmentos"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "Se reproduce como si no se hubiera omitido nada"
|
||||
},
|
||||
@@ -570,7 +573,10 @@
|
||||
"message": "Donaciones, membresías y mercancía personalizada"
|
||||
},
|
||||
"category_selfpromo_guideline2": {
|
||||
"message": "Agradecimientos gratuitos que no añaden al video"
|
||||
"message": "Agradecimientos gratuitos que no contribuyen al video"
|
||||
},
|
||||
"category_selfpromo_guideline3": {
|
||||
"message": "No para productos ni mercancía diseñados por empresas"
|
||||
},
|
||||
"category_exclusive_access": {
|
||||
"message": "Acceso Exclusivo"
|
||||
@@ -582,11 +588,23 @@
|
||||
"message": "Este video exhibe un producto, servicio o ubicación al que han recibido acceso gratuito o subsidiado",
|
||||
"description": "Short description for this category"
|
||||
},
|
||||
"category_exclusive_access_guideline1": {
|
||||
"message": "Todo el video muestra algo con acceso gratuito o subsidiado"
|
||||
},
|
||||
"category_interaction": {
|
||||
"message": "Recordatorio de Interacción (Suscribir)"
|
||||
},
|
||||
"category_interaction_description": {
|
||||
"message": "Cuando hay un breve recordatorio para dar like, suscribirse o seguirlos en medio del contenido. Si es largo o sobre algo específico, debería estar bajo autopromoción en su lugar."
|
||||
"message": "Cuando hay un breve recordatorio para dar me gusta, suscribirse o seguirlos en medio del contenido. Si es largo o sobre algo específico, debería estar bajo promoción propia en su lugar."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "Breve recordatorio para dar me gusta, suscribirse o seguir"
|
||||
},
|
||||
"category_interaction_guideline2": {
|
||||
"message": "Incluye recordatorios indirectos para comentar"
|
||||
},
|
||||
"category_interaction_guideline3": {
|
||||
"message": "No para promoción general, solo llamadas a la acción"
|
||||
},
|
||||
"category_interaction_short": {
|
||||
"message": "Recordatorio de Interacción"
|
||||
@@ -600,18 +618,36 @@
|
||||
"category_intro_short": {
|
||||
"message": "Intermisión"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "Intervalo sin contenido real"
|
||||
},
|
||||
"category_intro_guideline2": {
|
||||
"message": "No para transiciones con información"
|
||||
},
|
||||
"category_outro": {
|
||||
"message": "Tarjetas/Créditos"
|
||||
},
|
||||
"category_outro_description": {
|
||||
"message": "Créditos o cuando aparecen las tarjetas finales de YouTube. No para conclusiones con información."
|
||||
},
|
||||
"category_outro_guideline1": {
|
||||
"message": "No incluyas contenido, aun si las tarjetas finales están en pantalla"
|
||||
},
|
||||
"category_preview": {
|
||||
"message": "Vista previa/Recapitulación"
|
||||
},
|
||||
"category_preview_description": {
|
||||
"message": "Recapitulación rápida de los episodios anteriores, o una vista previa de lo que va a ocurrir más adelante en el vídeo actual. Está pensado para clips editados juntos, no para resúmenes hablados."
|
||||
},
|
||||
"category_preview_guideline1": {
|
||||
"message": "Clips que aparecen más tarde o en un video futuro"
|
||||
},
|
||||
"category_preview_guideline2": {
|
||||
"message": "Recapitulación de un video anterior"
|
||||
},
|
||||
"category_preview_guideline3": {
|
||||
"message": "No para secciones que añaden contenido adicional"
|
||||
},
|
||||
"category_filler": {
|
||||
"message": "Tangentes de Relleno/Chistes"
|
||||
},
|
||||
@@ -621,6 +657,15 @@
|
||||
"category_filler_short": {
|
||||
"message": "Relleno"
|
||||
},
|
||||
"category_filler_guideline1": {
|
||||
"message": "Escenas tangenciales solo de relleno o humor"
|
||||
},
|
||||
"category_filler_guideline2": {
|
||||
"message": "Distracciones, bloopers, repeticiones"
|
||||
},
|
||||
"category_filler_guideline3": {
|
||||
"message": "No para escenas requeridas para entender el tema"
|
||||
},
|
||||
"category_music_offtopic": {
|
||||
"message": "Música: Sección sin musica"
|
||||
},
|
||||
@@ -630,12 +675,27 @@
|
||||
"category_music_offtopic_short": {
|
||||
"message": "Sin Música"
|
||||
},
|
||||
"category_music_offtopic_guideline1": {
|
||||
"message": "Secciones que no están en versiones oficiales"
|
||||
},
|
||||
"category_music_offtopic_guideline2": {
|
||||
"message": "Secciones sin música en un espectáculo en vivo"
|
||||
},
|
||||
"category_poi_highlight": {
|
||||
"message": "Destacado"
|
||||
},
|
||||
"category_poi_highlight_description": {
|
||||
"message": "La parte del video que la mayoría de gente está buscando. Similar a los comentarios que dicen \"El video comienza en x\"."
|
||||
},
|
||||
"category_poi_highlight_guideline1": {
|
||||
"message": "La sección que la mayoría de personas están buscando"
|
||||
},
|
||||
"category_poi_highlight_guideline2": {
|
||||
"message": "Puede omitir contexto"
|
||||
},
|
||||
"category_poi_highlight_guideline3": {
|
||||
"message": "Puede saltar al título o miniatura"
|
||||
},
|
||||
"category_livestream_messages": {
|
||||
"message": "Directo: Lecturas de donaciones y mensajes"
|
||||
},
|
||||
@@ -885,6 +945,9 @@
|
||||
"LearnMore": {
|
||||
"message": "Aprenda Más"
|
||||
},
|
||||
"FullDetails": {
|
||||
"message": "Detalles Completos"
|
||||
},
|
||||
"CopyDownvoteButtonInfo": {
|
||||
"message": "Vota negativamente y crea una copia local para que la puedas volver a enviar"
|
||||
},
|
||||
|
||||
@@ -1 +1,118 @@
|
||||
{}
|
||||
{
|
||||
"fullName": {
|
||||
"message": "SponsorBlock para YouTube - I-skip ang mga Sponsorships",
|
||||
"description": "Name of the extension."
|
||||
},
|
||||
"Description": {
|
||||
"message": "I-skip ang mga sponsorships, subscription begging at marami pa sa mga YouTube videos. I-report ang mga sponsor sa videos na napapanood mo upang makatipid sa oras ng iba.",
|
||||
"description": "Description of the extension."
|
||||
},
|
||||
"429": {
|
||||
"message": "Masyadong kang maraming beses na nagsumite ng sponsor times para sa video na ito, sigurado ka na marami yan?"
|
||||
},
|
||||
"409": {
|
||||
"message": "Naisumite na ito noon"
|
||||
},
|
||||
"channelWhitelisted": {
|
||||
"message": "Whitelisted na ang channel na ito!"
|
||||
},
|
||||
"Segment": {
|
||||
"message": "segment"
|
||||
},
|
||||
"Segments": {
|
||||
"message": "segments"
|
||||
},
|
||||
"upvoteButtonInfo": {
|
||||
"message": "I-upvote ang submission na ito"
|
||||
},
|
||||
"reportButtonTitle": {
|
||||
"message": "I-report"
|
||||
},
|
||||
"reportButtonInfo": {
|
||||
"message": "I-report ang submission na ito bilang mali."
|
||||
},
|
||||
"Dismiss": {
|
||||
"message": "I-dismiss"
|
||||
},
|
||||
"Loading": {
|
||||
"message": "Loading..."
|
||||
},
|
||||
"Hide": {
|
||||
"message": "Hindi ipakita"
|
||||
},
|
||||
"hitGoBack": {
|
||||
"message": "Pindutin ang \"unskip\" para bumalik ka sa iyong dating posisyon sa video."
|
||||
},
|
||||
"unskip": {
|
||||
"message": "I-unskip"
|
||||
},
|
||||
"reskip": {
|
||||
"message": "I-reskip"
|
||||
},
|
||||
"unmute": {
|
||||
"message": "I-unmute"
|
||||
},
|
||||
"paused": {
|
||||
"message": "Naka-pause na"
|
||||
},
|
||||
"manualPaused": {
|
||||
"message": "Tigilan ang timer"
|
||||
},
|
||||
"confirmMSG": {
|
||||
"message": "Para sa pag-edit o tanggalin ng mga individual values, pindutin ang info button o ang extension icon sa kanang sulok sa itaas."
|
||||
},
|
||||
"clearThis": {
|
||||
"message": "Sigurado ka bang gusto mong tanggalin ito?"
|
||||
},
|
||||
"Unknown": {
|
||||
"message": "May error na nagkaroon sa pagsusumite sa iyong sponsor times, subukang muli mamaya."
|
||||
},
|
||||
"sponsorFound": {
|
||||
"message": "May mga segments sa database para sa video na ito!"
|
||||
},
|
||||
"sponsor404": {
|
||||
"message": "Walang nakitang segment"
|
||||
},
|
||||
"sponsorStart": {
|
||||
"message": "Simula ang Segment Ngayon"
|
||||
},
|
||||
"sponsorEnd": {
|
||||
"message": "Nagtatapos ang Segment Ngayon"
|
||||
},
|
||||
"sponsorCancel": {
|
||||
"message": "Kanselahin ang Paggawa ng Segment"
|
||||
},
|
||||
"noVideoID": {
|
||||
"message": "Walang YouTube video na nakita.\nKung mali ito, i-refresh ang tab mo."
|
||||
},
|
||||
"refreshSegments": {
|
||||
"message": "I-refresh ang mga segments"
|
||||
},
|
||||
"success": {
|
||||
"message": "Tagumpay na!"
|
||||
},
|
||||
"voted": {
|
||||
"message": "Bumoto na!"
|
||||
},
|
||||
"connectionError": {
|
||||
"message": "Nagkaroon ng error sa koneksyon. Error code: "
|
||||
},
|
||||
"clearTimes": {
|
||||
"message": "I-clear ang mga segments"
|
||||
},
|
||||
"openPopup": {
|
||||
"message": "I-buksan ang SponsorBlock popup"
|
||||
},
|
||||
"closePopup": {
|
||||
"message": "Isara ang Popup"
|
||||
},
|
||||
"SubmitTimes": {
|
||||
"message": "I-submit ang mga segments"
|
||||
},
|
||||
"submitCheck": {
|
||||
"message": "Sigurado ka bang gusto mong isumite ito?"
|
||||
},
|
||||
"whitelistChannel": {
|
||||
"message": "I-whitelist itong channel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,9 @@
|
||||
"showSkipNotice": {
|
||||
"message": "Notifier après qu'un segment ait été sauté"
|
||||
},
|
||||
"showCategoryGuidelines": {
|
||||
"message": "Affiche l'aide de la catégorie"
|
||||
},
|
||||
"noticeVisibilityMode0": {
|
||||
"message": "Notifications de passage"
|
||||
},
|
||||
@@ -542,18 +545,39 @@
|
||||
"message": "à",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "Inclure les transitions entre les segments"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "Jouer comme si rien n'avait été passé"
|
||||
},
|
||||
"category_sponsor": {
|
||||
"message": "Message sponsorisé"
|
||||
},
|
||||
"category_sponsor_description": {
|
||||
"message": "Promotion rémunérée, parrainage rémunéré et publicité directe. Pas pour l'autopromotion ou les présentations gratuites de causes, de créateurs, de sites web ou de produits qu'ils aiment."
|
||||
},
|
||||
"category_sponsor_guideline1": {
|
||||
"message": "Promotions rémunérées"
|
||||
},
|
||||
"category_sponsor_guideline2": {
|
||||
"message": "Pas pour les dons ou les produits dérivés"
|
||||
},
|
||||
"category_selfpromo": {
|
||||
"message": "Non rémunéré/autopromotion"
|
||||
},
|
||||
"category_selfpromo_description": {
|
||||
"message": "Semblable aux \"messages commerciaux\", excepté pour la promotion non rémunérée ou l'autopromotion. Cela inclut les marchandises, les dons et les informations sur leurs collaborateurs."
|
||||
},
|
||||
"category_selfpromo_guideline1": {
|
||||
"message": "Dons, abonnements (payant) et produits dérivés"
|
||||
},
|
||||
"category_selfpromo_guideline2": {
|
||||
"message": "Remerciements gratuits qui n'apportent aucune information à la vidéo"
|
||||
},
|
||||
"category_selfpromo_guideline3": {
|
||||
"message": "Pas pour des produits dérivés fait par des marques"
|
||||
},
|
||||
"category_exclusive_access": {
|
||||
"message": "Accès exclusif"
|
||||
},
|
||||
@@ -564,12 +588,24 @@
|
||||
"message": "Cette vidéo présente un produit, un service ou un lieu pour lequel un accès gratuit ou subventionné a été reçu",
|
||||
"description": "Short description for this category"
|
||||
},
|
||||
"category_exclusive_access_guideline1": {
|
||||
"message": "Toute la vidéo présente quelque chose dont le créateur a eu un accès gratuit ou subventionné"
|
||||
},
|
||||
"category_interaction": {
|
||||
"message": "Rappel d'interaction (abonnement)"
|
||||
},
|
||||
"category_interaction_description": {
|
||||
"message": "Lorsqu'il y a un bref rappel pour aimer, s'abonner ou les suivre parmi le contenu. Si le message est long ou porte sur quelque chose de spécifique, cela devrait plutôt être classé comme une autopromotion."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "Rappels courts à like, s'abonner ou suivre"
|
||||
},
|
||||
"category_interaction_guideline2": {
|
||||
"message": "Inclut des rappels indirects à commenter"
|
||||
},
|
||||
"category_interaction_guideline3": {
|
||||
"message": "Pas pour la promotion générale, seulement les appels à l'interaction"
|
||||
},
|
||||
"category_interaction_short": {
|
||||
"message": "Rappel d'interaction"
|
||||
},
|
||||
@@ -582,18 +618,36 @@
|
||||
"category_intro_short": {
|
||||
"message": "Entracte"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "Intervalle sans contenu réel"
|
||||
},
|
||||
"category_intro_guideline2": {
|
||||
"message": "Pas pour les transitions avec des informations"
|
||||
},
|
||||
"category_outro": {
|
||||
"message": "Générique de fin"
|
||||
},
|
||||
"category_outro_description": {
|
||||
"message": "Crédits ou écrans de fin YouTube. Pas pour les conclusions contenant des informations."
|
||||
},
|
||||
"category_outro_guideline1": {
|
||||
"message": "Ne dois pas inclure de contenu, même si les écrans de fin sont apparus"
|
||||
},
|
||||
"category_preview": {
|
||||
"message": "Aperçu/Résumé"
|
||||
},
|
||||
"category_preview_description": {
|
||||
"message": "Résumé rapide des épisodes précédents, ou aperçu de ce qui se passera plus tard dans la vidéo en cours. Pour les plans collectifs édités, pas pour les résumés parlés."
|
||||
},
|
||||
"category_preview_guideline1": {
|
||||
"message": "Clips apparaissant plus tard ou dans une prochaine vidéo"
|
||||
},
|
||||
"category_preview_guideline2": {
|
||||
"message": "Récapitulatif d'une vidéo précédente"
|
||||
},
|
||||
"category_preview_guideline3": {
|
||||
"message": "Pas pour les sections qui ajoutent du contenu supplémentaire"
|
||||
},
|
||||
"category_filler": {
|
||||
"message": "Digressions/Blagues"
|
||||
},
|
||||
@@ -603,6 +657,15 @@
|
||||
"category_filler_short": {
|
||||
"message": "Remplissage"
|
||||
},
|
||||
"category_filler_guideline1": {
|
||||
"message": "Scènes digressives uniquement pour le remplissage ou l'humour"
|
||||
},
|
||||
"category_filler_guideline2": {
|
||||
"message": "Distractions, bêtisiers, replays"
|
||||
},
|
||||
"category_filler_guideline3": {
|
||||
"message": "Pas pour les scènes requises pour comprendre le sujet"
|
||||
},
|
||||
"category_music_offtopic": {
|
||||
"message": "Musique : Segment non musical"
|
||||
},
|
||||
@@ -612,12 +675,27 @@
|
||||
"category_music_offtopic_short": {
|
||||
"message": "Hors musique"
|
||||
},
|
||||
"category_music_offtopic_guideline1": {
|
||||
"message": "Sections qui ne sont pas dans la musique officielle"
|
||||
},
|
||||
"category_music_offtopic_guideline2": {
|
||||
"message": "Pas de musique pendant les concerts en direct"
|
||||
},
|
||||
"category_poi_highlight": {
|
||||
"message": "Point essentiel"
|
||||
},
|
||||
"category_poi_highlight_description": {
|
||||
"message": "La partie de la vidéo que la plupart des gens veulent voir. Similaire à \"la vidéo commence à x mins\"."
|
||||
},
|
||||
"category_poi_highlight_guideline1": {
|
||||
"message": "Section la plus regardée"
|
||||
},
|
||||
"category_poi_highlight_guideline2": {
|
||||
"message": "Peut sauter le contexte"
|
||||
},
|
||||
"category_poi_highlight_guideline3": {
|
||||
"message": "Peut passer au sujet cité dans le titre ou la miniature"
|
||||
},
|
||||
"category_livestream_messages": {
|
||||
"message": "Stream : lecture de dons et messages"
|
||||
},
|
||||
@@ -867,6 +945,9 @@
|
||||
"LearnMore": {
|
||||
"message": "En savoir plus"
|
||||
},
|
||||
"FullDetails": {
|
||||
"message": "Tous les détails"
|
||||
},
|
||||
"CopyDownvoteButtonInfo": {
|
||||
"message": "Vote contre et crée une copie locale à soumettre à nouveau"
|
||||
},
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
"message": "Nasconde i pulsanti che appaiono sul video per inviare i segmenti da nascondere."
|
||||
},
|
||||
"showSkipButton": {
|
||||
"message": "Mantieni Salta Per Evidenziare il Pulsante Sul Lettore"
|
||||
"message": "Mantieni l'Highlight del Video sulla Barra del Video"
|
||||
},
|
||||
"showInfoButton": {
|
||||
"message": "Mostra il pulsante delle informazioni sopra al video"
|
||||
@@ -216,7 +216,7 @@
|
||||
"message": "Memorizza i voti negativi del segmento"
|
||||
},
|
||||
"whatTrackDownvotes": {
|
||||
"message": "Qualsiasi segmento voti negativamente rimarrà nascosto anche dopo aver ricaricato"
|
||||
"message": "Segmenti votati negativamente rimarranno nascosti anche dopo aver ricaricato la pagina"
|
||||
},
|
||||
"trackDownvotesWarning": {
|
||||
"message": "Attenzione: Disabilitarlo eliminerà tutti i voti negativi precedentemente memorizzati"
|
||||
@@ -282,7 +282,7 @@
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"setStartSponsorShortcut": {
|
||||
"message": "Inizio/Fine segmento",
|
||||
"message": "Inizia/Finisci segmento",
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"setSubmitKeybind": {
|
||||
@@ -290,7 +290,7 @@
|
||||
"description": "Keybind label"
|
||||
},
|
||||
"keybindDescription": {
|
||||
"message": "Selezionare una chiave digitandola e scegliere qualsiasi tasto modificatore che si desidera utilizzare."
|
||||
"message": "Seleziona un tasto digitandolo e scegli qualsiasi tasto modificatore che desideri utilizzare."
|
||||
},
|
||||
"0": {
|
||||
"message": "Timeout della connessione. Controlla la tua connessione a Internet. Se internet sta funzionando, il server è probabilmente sovraccarico oppure giù."
|
||||
@@ -328,7 +328,7 @@
|
||||
"message": "Silenziare {0}?"
|
||||
},
|
||||
"skip_to_category": {
|
||||
"message": "Saltare a {0}?",
|
||||
"message": "Salta a {0}?",
|
||||
"description": "Used for skipping to things (Skip to Highlight)"
|
||||
},
|
||||
"skipped": {
|
||||
@@ -545,6 +545,9 @@
|
||||
"message": "a",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "Includi transizioni"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "Riproduci come se nulla fosse stato saltato"
|
||||
},
|
||||
@@ -569,6 +572,9 @@
|
||||
"category_selfpromo_guideline1": {
|
||||
"message": "Donazioni, abbonamenti e merce personalizzata"
|
||||
},
|
||||
"category_selfpromo_guideline2": {
|
||||
"message": "Shoutout non pagati che non aggiungono nulla al video"
|
||||
},
|
||||
"category_selfpromo_guideline3": {
|
||||
"message": "Non per prodotti progettati da aziende e merce"
|
||||
},
|
||||
@@ -589,7 +595,7 @@
|
||||
"message": "Promemoria d'Interazione (Iscrizione)"
|
||||
},
|
||||
"category_interaction_description": {
|
||||
"message": "Quando nel punto centrale del contenuto è presente un breve promemoria per aggiunta di mi piace, iscrizione o seguito. Se dovesse risultare esteso o riguardare qualcosa di specifico, potrebbe essere un'autopromozione."
|
||||
"message": "Quando nel punto centrale del contenuto è presente un breve promemoria per like, iscrizione o follow. Se dovesse risultare esteso o riguardante qualcosa di specifico, potrebbe essere auto-promozione."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "Brevi promemoria per mi piace, iscrizioni o follow"
|
||||
@@ -601,16 +607,16 @@
|
||||
"message": "Non per promozione generale, solo chiamata all'azione"
|
||||
},
|
||||
"category_interaction_short": {
|
||||
"message": "Promemoria di Interazione"
|
||||
"message": "Promemoria d'Interazione"
|
||||
},
|
||||
"category_intro": {
|
||||
"message": "Animazione Interruzione/Introduzione"
|
||||
"message": "Intermezzo/Intro Animata"
|
||||
},
|
||||
"category_intro_description": {
|
||||
"message": "Un intervallo senza contenuto effettivo. Potrebbe essere una pausa, una schermata statica, un'animazione ripetuta. Non dovrebbe essere usato per transizioni contenenti informazioni."
|
||||
},
|
||||
"category_intro_short": {
|
||||
"message": "Interruzione"
|
||||
"message": "Intermezzo"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "Intervallo senza contenuto effettivo"
|
||||
@@ -633,6 +639,15 @@
|
||||
"category_preview_description": {
|
||||
"message": "Riepilogo rapido degli episodi precedenti, o un'anteprima di ciò che sta arrivando più tardi nel video attuale. Inteso per clip, non per riassunti a voce."
|
||||
},
|
||||
"category_preview_guideline1": {
|
||||
"message": "Clip che appaiono più tardi in questo video, oppure in un video futuro"
|
||||
},
|
||||
"category_preview_guideline2": {
|
||||
"message": "Riepilogo o riassunto di un video precedente"
|
||||
},
|
||||
"category_preview_guideline3": {
|
||||
"message": "Non per sezioni che aggiungono contenuti in più"
|
||||
},
|
||||
"category_filler": {
|
||||
"message": "Riempitivi irrilevanti/Battute"
|
||||
},
|
||||
@@ -640,7 +655,16 @@
|
||||
"message": "Le scene riempitive sono aggiunte solo per riempire o per umorismo che non sono richieste per comprendere il contenuto principale del video. Questo non dovrebbe includere segmenti che forniscono contesto o dettagli di sfondo."
|
||||
},
|
||||
"category_filler_short": {
|
||||
"message": "Riempimento"
|
||||
"message": "Filler"
|
||||
},
|
||||
"category_filler_guideline1": {
|
||||
"message": "Scene non correlate usate solo per filler o umorismo"
|
||||
},
|
||||
"category_filler_guideline2": {
|
||||
"message": "Distrazioni, blooper, replay"
|
||||
},
|
||||
"category_filler_guideline3": {
|
||||
"message": "Non per scene necessarie a capire l'argomento"
|
||||
},
|
||||
"category_music_offtopic": {
|
||||
"message": "Musica: Sezione Non-Musicale"
|
||||
@@ -651,12 +675,27 @@
|
||||
"category_music_offtopic_short": {
|
||||
"message": "Non-Musicale"
|
||||
},
|
||||
"category_music_offtopic_guideline1": {
|
||||
"message": "Sezioni non presenti nelle release ufficiali"
|
||||
},
|
||||
"category_music_offtopic_guideline2": {
|
||||
"message": "Sezioni senza musica in una performance dal vivo"
|
||||
},
|
||||
"category_poi_highlight": {
|
||||
"message": "Evidenzia"
|
||||
"message": "Highlight"
|
||||
},
|
||||
"category_poi_highlight_description": {
|
||||
"message": "La parte del video che gran parte delle persone stanno cercando. Simile ai commenti \"Il video inizia a x\"."
|
||||
},
|
||||
"category_poi_highlight_guideline1": {
|
||||
"message": "La parte che la maggior parte delle persone sta cercando"
|
||||
},
|
||||
"category_poi_highlight_guideline2": {
|
||||
"message": "Può ignorare il contesto"
|
||||
},
|
||||
"category_poi_highlight_guideline3": {
|
||||
"message": "Può portare al titolo o alla miniatura del video"
|
||||
},
|
||||
"category_livestream_messages": {
|
||||
"message": "Livestream: Donazione/Letture dei Messaggi"
|
||||
},
|
||||
@@ -777,7 +816,7 @@
|
||||
"message": "Forza controllo canale prima di andare avanti"
|
||||
},
|
||||
"whatForceChannelCheck": {
|
||||
"message": "Per impostazione predefinita, si salteranno subito i segmenti prima che si sappia anche che canale è. Per impostazione predefinita, alcuni segmenti all'inizio del video potrebbero essere saltati sui canali sulla whitelist. Abilitare questa opzione impedirà questo, ma fare saltare tutti hanno un leggero ritardo in quanto ottenere il channelID può richiedere un certo tempo. Questo ritardo potrebbe essere invisibile se si dispone di internet veloce."
|
||||
"message": "Di default, verranno saltati i segmenti subito, anche prima che si sappia il canale. Di default, alcuni segmenti all'inizio del video potrebbero essere saltati sui canali nella whitelist. L'attivazione di questa opzione eviterà che ciò accada, ma ogni salto avrà un leggero ritardo in quanto ottenere l'ID del canale può richiedere un certo tempo. Questo ritardo potrebbe essere impercettibile se si dispone di una connessione internet veloce."
|
||||
},
|
||||
"forceChannelCheckPopup": {
|
||||
"message": "Considera l'Attivazione dell'opzione \"Forza la Verifica del Canale Prima del Salto\""
|
||||
@@ -906,6 +945,9 @@
|
||||
"LearnMore": {
|
||||
"message": "Scopri di Più"
|
||||
},
|
||||
"FullDetails": {
|
||||
"message": "Visualizza Dettagli Completi"
|
||||
},
|
||||
"CopyDownvoteButtonInfo": {
|
||||
"message": "Vota negativamente e crea una copia locale da reinviare"
|
||||
},
|
||||
@@ -921,6 +963,12 @@
|
||||
"ChangeCategoryTooltip": {
|
||||
"message": "Questo si applicherà istantaneamente ai tuoi segmenti"
|
||||
},
|
||||
"downvote": {
|
||||
"message": "Voto negativo"
|
||||
},
|
||||
"upvote": {
|
||||
"message": "Voto positivo"
|
||||
},
|
||||
"hideSegment": {
|
||||
"message": "Nascondi segmento"
|
||||
},
|
||||
|
||||
@@ -545,18 +545,39 @@
|
||||
"message": "-",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "연속적인 전환 포함"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "건너뛰지 않은 것처럼 자연스럽게"
|
||||
},
|
||||
"category_sponsor": {
|
||||
"message": "스폰서 광고"
|
||||
},
|
||||
"category_sponsor_description": {
|
||||
"message": "유료 광고, 유료 협찬과 직접 광고입니다. 원인/크리에이터/웹사이트/제품에 자체 홍보나 대가 없는 홍보는 여기에 해당되지 않습니다."
|
||||
},
|
||||
"category_sponsor_guideline1": {
|
||||
"message": "유료 광고"
|
||||
},
|
||||
"category_sponsor_guideline2": {
|
||||
"message": "후원이나 자체 상품은 해당되지 않음"
|
||||
},
|
||||
"category_selfpromo": {
|
||||
"message": "자체 홍보 구간"
|
||||
},
|
||||
"category_selfpromo_description": {
|
||||
"message": "'스폰서 광고'와 비슷하지만 협찬 없이 자기 채널을 홍보하는 구간입니다. 여기에는 채널 굿즈 광고, 기부 광고와 영상에 참여한 사람들을 홍보하는 광고가 해당됩니다."
|
||||
},
|
||||
"category_selfpromo_guideline1": {
|
||||
"message": "후원, 멤버십 및 자체 상품"
|
||||
},
|
||||
"category_selfpromo_guideline2": {
|
||||
"message": "동영상과 무관한 무료 홍보"
|
||||
},
|
||||
"category_selfpromo_guideline3": {
|
||||
"message": "기업 제품 및 상품은 해당되지 않음"
|
||||
},
|
||||
"category_exclusive_access": {
|
||||
"message": "협찬"
|
||||
},
|
||||
@@ -567,12 +588,24 @@
|
||||
"message": "본 동영상은 무료/유료 협찬을 받은 제품, 서비스, 장소를 소개합니다",
|
||||
"description": "Short description for this category"
|
||||
},
|
||||
"category_exclusive_access_guideline1": {
|
||||
"message": "전체 동영상이 유/무료 협찬을 받은 대상을 소개함"
|
||||
},
|
||||
"category_interaction": {
|
||||
"message": "상호 작용 알림 (구독)"
|
||||
},
|
||||
"category_interaction_description": {
|
||||
"message": "컨텐츠 중앙의 좋아요, 구독이나 팔로우에 대한 짧은 설명이 뜨는 경우입니다. 길거나 특정적인 거라면 자가 홍보에 해당됩니다."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "좋아요, 구독, 팔로우를 요청하는 구간"
|
||||
},
|
||||
"category_interaction_guideline2": {
|
||||
"message": "간접적인 댓글 작성 유도도 포함"
|
||||
},
|
||||
"category_interaction_guideline3": {
|
||||
"message": "일반적인 홍보는 해당되지 않음, 행동을 요청하는 경우만"
|
||||
},
|
||||
"category_interaction_short": {
|
||||
"message": "상호 작용 알림"
|
||||
},
|
||||
@@ -585,18 +618,36 @@
|
||||
"category_intro_short": {
|
||||
"message": "휴식 시간"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "실제 콘텐츠가 없는 구간"
|
||||
},
|
||||
"category_intro_guideline2": {
|
||||
"message": "정보가 포함된 전환은 해당되지 않음"
|
||||
},
|
||||
"category_outro": {
|
||||
"message": "최종 화면 / 크레딧"
|
||||
},
|
||||
"category_outro_description": {
|
||||
"message": "엔딩 크레딧이나 최종 화면이 나타나는 구간입니다. 단순히 결론을 말하는 부분은 여기에 포함되지 않습니다."
|
||||
},
|
||||
"category_outro_guideline1": {
|
||||
"message": "최종 화면 카드가 표시되더라도, 콘텐츠가 포함되도록 하지 말 것"
|
||||
},
|
||||
"category_preview": {
|
||||
"message": "미리보기/요약"
|
||||
},
|
||||
"category_preview_description": {
|
||||
"message": "이전 에피소드를 간략히 요약하거나 현재 동영상에서 나중에 나올 내용을 예고해줍니다. 음성 요약이 아니라 편집된 동영상을 통한 요약입니다."
|
||||
},
|
||||
"category_preview_guideline1": {
|
||||
"message": "다음 동영상이나 이후 구간에 나타나는 클립"
|
||||
},
|
||||
"category_preview_guideline2": {
|
||||
"message": "이전 동영상 요약"
|
||||
},
|
||||
"category_preview_guideline3": {
|
||||
"message": "추가적인 콘텐츠가 들어가는 구간은 해당되지 않음"
|
||||
},
|
||||
"category_filler": {
|
||||
"message": "쓸데없는 잡담/농담"
|
||||
},
|
||||
@@ -606,6 +657,15 @@
|
||||
"category_filler_short": {
|
||||
"message": "잡담"
|
||||
},
|
||||
"category_filler_guideline1": {
|
||||
"message": "잡담이나 유머를 구사하고 주제에서 벗어난 장면"
|
||||
},
|
||||
"category_filler_guideline2": {
|
||||
"message": "집중을 방해하는 구간, 실수, 리플레이"
|
||||
},
|
||||
"category_filler_guideline3": {
|
||||
"message": "주제를 이해하는 데 필요한 장면은 해당되지 않음"
|
||||
},
|
||||
"category_music_offtopic": {
|
||||
"message": "음악이 아닌 구간"
|
||||
},
|
||||
@@ -615,12 +675,27 @@
|
||||
"category_music_offtopic_short": {
|
||||
"message": "음악이 아닌 구간"
|
||||
},
|
||||
"category_music_offtopic_guideline1": {
|
||||
"message": "정식 음악에는 없는 구간"
|
||||
},
|
||||
"category_music_offtopic_guideline2": {
|
||||
"message": "실시간 공연에서 음악이 아닌 부분"
|
||||
},
|
||||
"category_poi_highlight": {
|
||||
"message": "하이라이트"
|
||||
},
|
||||
"category_poi_highlight_description": {
|
||||
"message": "대부분의 사람들이 찾는 동영상의 파트를 말합니다. \"바쁘신 분들은...\" 댓글과 유사합니다."
|
||||
},
|
||||
"category_poi_highlight_guideline1": {
|
||||
"message": "대부분의 사람들이 찾는 구간"
|
||||
},
|
||||
"category_poi_highlight_guideline2": {
|
||||
"message": "맥락을 건너뛰어도 됨"
|
||||
},
|
||||
"category_poi_highlight_guideline3": {
|
||||
"message": "제목, 썸네일 구간으로 건너뛰어도 됨"
|
||||
},
|
||||
"category_livestream_messages": {
|
||||
"message": "라이브스트림: 후원/메시지 읽기"
|
||||
},
|
||||
@@ -870,6 +945,9 @@
|
||||
"LearnMore": {
|
||||
"message": "더보기"
|
||||
},
|
||||
"FullDetails": {
|
||||
"message": "전체 자세한 정보"
|
||||
},
|
||||
"CopyDownvoteButtonInfo": {
|
||||
"message": "비추천에 투표한 뒤 다시 제출할 수 있도록 미제출 사본을 생성합니다"
|
||||
},
|
||||
|
||||
@@ -1 +1,25 @@
|
||||
{}
|
||||
{
|
||||
"fullName": {
|
||||
"message": "SponsorBlock priekš YouTube - Izlaid sponsorus",
|
||||
"description": "Name of the extension."
|
||||
},
|
||||
"Description": {
|
||||
"message": "Izlaidiet sponsorus, abonēšanas lūgumus un vairāk, skatoties YouTube video. Ziņojiet par sponsoriem video, kurus jūs skatāties, lai ietaupītu citu laiku.",
|
||||
"description": "Description of the extension."
|
||||
},
|
||||
"400": {
|
||||
"message": "Serveris ziņo, ka šis pieprasījums ir nederīgs"
|
||||
},
|
||||
"429": {
|
||||
"message": "Jūs esat aizsūtījis pārāk daudz sponsoru laika sprīžus šim video; vai esat pārliecināts, ka šeit ir tik daudz?"
|
||||
},
|
||||
"409": {
|
||||
"message": "Šis jau ir ticis aizsūtīts iepriekš"
|
||||
},
|
||||
"channelWhitelisted": {
|
||||
"message": "Kanāls iekļauts baltajā sarakstā!"
|
||||
},
|
||||
"Segment": {
|
||||
"message": "segments"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"message": "Serwer odpowiedział, że to zapytanie jest niepoprawne"
|
||||
},
|
||||
"429": {
|
||||
"message": "Zgłosiłeś za dużo segmentów sponsora dla tego jednego filmu. Jesteś pewien, że jest ich tak dużo?"
|
||||
"message": "Zgłoszono za dużo segmentów sponsora dla tego jednego filmu. Czy na pewno jest ich tak dużo?"
|
||||
},
|
||||
"409": {
|
||||
"message": "To już zostało wysłane wcześniej"
|
||||
@@ -53,7 +53,7 @@
|
||||
"message": "Pomiń"
|
||||
},
|
||||
"unmute": {
|
||||
"message": "Odcisz"
|
||||
"message": "Anuluj wyciszenie"
|
||||
},
|
||||
"paused": {
|
||||
"message": "Zatrzymany"
|
||||
@@ -239,20 +239,23 @@
|
||||
"showSkipNotice": {
|
||||
"message": "Pokaż informację po pominięciu segmentu"
|
||||
},
|
||||
"showCategoryGuidelines": {
|
||||
"message": "Pokaż pomoc kategorii"
|
||||
},
|
||||
"noticeVisibilityMode0": {
|
||||
"message": "Pełnowymiarowe powiadomienia o przewinięciu"
|
||||
},
|
||||
"noticeVisibilityMode1": {
|
||||
"message": "Małe powiadomienia o automatycznym przewijaniu"
|
||||
"message": "Małe powiadomienia o automatycznym przewinięciu"
|
||||
},
|
||||
"noticeVisibilityMode2": {
|
||||
"message": "Małe powiadomienia o przewinięciu"
|
||||
},
|
||||
"noticeVisibilityMode3": {
|
||||
"message": "Znikające powiadomienia o automatycznym przewijaniu"
|
||||
"message": "Półprzezroczyste powiadomienie o automatycznym przewinięciu"
|
||||
},
|
||||
"noticeVisibilityMode4": {
|
||||
"message": "Znikające powiadomienia o przewijaniu"
|
||||
"message": "Półprzezroczyste powiadomienie dla wszystkich przewinięć"
|
||||
},
|
||||
"longDescription": {
|
||||
"message": "SponsorBlock pozwala pomijać sponsorów, intra, outra, przypomnienia o subskrypcjach i inne irytujące fragmenty filmów na YouTube. SponsorBlock jest opartym na crowdsourcingu rozszerzeniem do przeglądarki, które pozwala każdemu zgłosić początek i koniec segmentów sponsorowanych oraz innych segmentów w filmach na YouTube. Kiedy ktoś już zamieści te informacje, wszyscy pozostali z tym rozszerzeniem będą pomijać segment sponsorowany. Możesz również pomijać fragmenty teledysków bez muzyki.",
|
||||
@@ -322,7 +325,7 @@
|
||||
"message": "{0} — pominąć?"
|
||||
},
|
||||
"mute_category": {
|
||||
"message": "Wyciszyć {0}?"
|
||||
"message": "{0} — wyciszyć?"
|
||||
},
|
||||
"skip_to_category": {
|
||||
"message": "Przejść do {0}?",
|
||||
@@ -449,7 +452,7 @@
|
||||
"message": "Ten segment jest krótszy od ustawionego przez Ciebie minimalnego czasu trwania. Może to oznaczać, że ktoś już to zamieścił, ale nie widzisz tego przez to ustawienie. Czy na pewno chcesz to zamieścić?"
|
||||
},
|
||||
"liveOrPremiere": {
|
||||
"message": "Przesyłanie na aktywnej transmisji na żywo lub premierze jest niedozwolone. Poczekaj aż ona się zakończy, a następnie odśwież stronę i sprawdź, czy te segmenty są nadal prawidłowe."
|
||||
"message": "Przesyłanie segmentów podczas transmisji na żywo lub trwającej premiery jest niedozwolone. Poczekaj, aż się zakończy, a następnie odśwież stronę i sprawdź, czy te segmenty są nadal prawidłowe."
|
||||
},
|
||||
"showUploadButton": {
|
||||
"message": "Pokaż przycisk wysyłania"
|
||||
@@ -464,7 +467,7 @@
|
||||
"message": "Zapisz"
|
||||
},
|
||||
"reset": {
|
||||
"message": "Reset"
|
||||
"message": "Resetuj"
|
||||
},
|
||||
"customAddressError": {
|
||||
"message": "Ten adres nie jest w prawidłowej formie. Upewnij się, że http:// lub https:// znajduje się na początku i nie ma końcowych ukośników."
|
||||
@@ -536,40 +539,73 @@
|
||||
"message": "Informacje do debugowania zostały skopiowane do schowka. Możesz usunąć dane, których nie chcesz udostępniać. Zapisz je w pliku tekstowym albo wklej do raportu podczas zgłaszania błędu."
|
||||
},
|
||||
"keyAlreadyUsed": {
|
||||
"message": "Ten skrót jest przypisany do innej akcji. Proszę wybrać inny."
|
||||
"message": "Ten skrót jest przypisany do innej czynności. Wybierz inny."
|
||||
},
|
||||
"to": {
|
||||
"message": "do",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline1": {
|
||||
"message": "Zawiera płynne przejścia"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "Pominięcie bez zauważalnego przeskoku"
|
||||
},
|
||||
"category_sponsor": {
|
||||
"message": "Sponsor"
|
||||
},
|
||||
"category_sponsor_description": {
|
||||
"message": "Płatna promocja, płatne rekomendacje oraz bezpośrednie reklamy. Nie do autopromocji ani darmowych wyrazów uznania dla kwestii/twórców/stron/produktów, które im się podobają."
|
||||
},
|
||||
"category_sponsor_guideline1": {
|
||||
"message": "Segmenty sponsorowane"
|
||||
},
|
||||
"category_sponsor_guideline2": {
|
||||
"message": "Nie dla donateów lub merchu"
|
||||
},
|
||||
"category_selfpromo": {
|
||||
"message": "Nieopłacona/Własna promocja"
|
||||
},
|
||||
"category_selfpromo_description": {
|
||||
"message": "Podobnie jak \"sponsor\", ale nieodpłatnie bądź w ramach promocji własnej. Obejmuje to sekcje o własnych produktach, donacjach czy informacje o tym, z kim współpracowali."
|
||||
},
|
||||
"category_selfpromo_guideline1": {
|
||||
"message": "Dotacje, płatne członkostwo i merch"
|
||||
},
|
||||
"category_selfpromo_guideline2": {
|
||||
"message": "Szybkie przypomnienia, które nie wnoszą nic do filmu"
|
||||
},
|
||||
"category_selfpromo_guideline3": {
|
||||
"message": "Nie dla produktów zaprojektowanych przez duże firmy"
|
||||
},
|
||||
"category_exclusive_access": {
|
||||
"message": "Ekskluzywny dostęp"
|
||||
"message": "Dostęp na wyłączność"
|
||||
},
|
||||
"category_exclusive_access_description": {
|
||||
"message": "Tylko do oznaczania całych filmów. Używane, gdy wideo wyświetla produkt, usługę lub lokalizację, do których otrzymali darmowy lub subsydiowany dostęp."
|
||||
"message": "Tylko do oznaczania całych filmów. Używane, gdy materiał wideo przedstawia produkt, usługę lub miejsce, do którego dostęp został otrzymany bezpłatnie lub dzięki dofinansowaniu."
|
||||
},
|
||||
"category_exclusive_access_pill": {
|
||||
"message": "Ten film pokazuje produkt, usługę lub lokalizację, do których otrzymali darmowy lub subsydiowany dostęp",
|
||||
"message": "Ten materiał wideo przedstawia produkt, usługę lub miejsce, do którego dostęp został otrzymany bezpłatnie lub dzięki dofinansowaniu",
|
||||
"description": "Short description for this category"
|
||||
},
|
||||
"category_exclusive_access_guideline1": {
|
||||
"message": "Cały film jest poświęcony czemuś z darmowym lub płatnym dostępem"
|
||||
},
|
||||
"category_interaction": {
|
||||
"message": "Przypomnienie o interakcji (Subskrybuj)"
|
||||
},
|
||||
"category_interaction_description": {
|
||||
"message": "Gdy ma miejsce krótkie przypomnienie, by lajkować, subskrybować lub śledzić ich w trakcie kontentu. Jeśli trwa to długo lub dotyczy czegoś konkretnego, powinno być zamiast tego jako promocja własna."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "Krótkie przypomnienia, by polubić lub zasubskrybować"
|
||||
},
|
||||
"category_interaction_guideline2": {
|
||||
"message": "Zawiera niebezpośrednie zachęcanie do komentowania"
|
||||
},
|
||||
"category_interaction_guideline3": {
|
||||
"message": "Nie dla ogólnej promocji, tylko zaproszenia do działania"
|
||||
},
|
||||
"category_interaction_short": {
|
||||
"message": "Przypomnienie o interakcji"
|
||||
},
|
||||
@@ -582,20 +618,38 @@
|
||||
"category_intro_short": {
|
||||
"message": "Przerwa"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "Przerwy bez rzeczywistej zawartości"
|
||||
},
|
||||
"category_intro_guideline2": {
|
||||
"message": "Nie dla przejść zawierających informacje"
|
||||
},
|
||||
"category_outro": {
|
||||
"message": "Ekran końcowy/Napisy"
|
||||
},
|
||||
"category_outro_description": {
|
||||
"message": "Napisy końcowe lub gdy pojawia się ekran końcowy. Nie do konkluzji zawierających informacje."
|
||||
},
|
||||
"category_outro_guideline1": {
|
||||
"message": "Nie zawiera treści, nawet jeśli na ekranie są karty końcowe"
|
||||
},
|
||||
"category_preview": {
|
||||
"message": "Zapowiedź/Podsumowanie"
|
||||
},
|
||||
"category_preview_description": {
|
||||
"message": "Szybkie podsumowanie poprzednich odcinków lub podgląd tego, co pojawia się później w bieżącym filmie. Dotyczy zmontowanych klipów, a nie ustnych podsumowań."
|
||||
},
|
||||
"category_preview_guideline1": {
|
||||
"message": "Klipy, które pojawiają się później lub w następnym filmie"
|
||||
},
|
||||
"category_preview_guideline2": {
|
||||
"message": "Podsumowanie poprzedniego filmu"
|
||||
},
|
||||
"category_preview_guideline3": {
|
||||
"message": "Nie dla sekcji, które zawierają potrzebne informacje"
|
||||
},
|
||||
"category_filler": {
|
||||
"message": "Wypełniacz Nietematyczny/Żart"
|
||||
"message": "Wypełniacz nietematyczny/żart"
|
||||
},
|
||||
"category_filler_description": {
|
||||
"message": "Sceny nietematyczne dodawane tylko jako wypełniacz lub dla humoru, które nie są wymagane do zrozumienia głównej treści filmu. Nie powinno to obejmować segmentów zawierających informacje kontekstowe lub szczegółowe."
|
||||
@@ -603,6 +657,15 @@
|
||||
"category_filler_short": {
|
||||
"message": "Wypełniacz"
|
||||
},
|
||||
"category_filler_guideline1": {
|
||||
"message": "Przerywniki lub sceny czysto humorystyczne"
|
||||
},
|
||||
"category_filler_guideline2": {
|
||||
"message": "Rozpraszacze, wpadki, powtórki"
|
||||
},
|
||||
"category_filler_guideline3": {
|
||||
"message": "Nie nadaje się do scen wymaganych do zrozumienia tematu"
|
||||
},
|
||||
"category_music_offtopic": {
|
||||
"message": "Muzyka: Sekcja niemuzyczna"
|
||||
},
|
||||
@@ -612,12 +675,27 @@
|
||||
"category_music_offtopic_short": {
|
||||
"message": "Bez muzyki"
|
||||
},
|
||||
"category_music_offtopic_guideline1": {
|
||||
"message": "Sekcje, których nie ma w oficjalnych wydaniach"
|
||||
},
|
||||
"category_music_offtopic_guideline2": {
|
||||
"message": "Niemuzyczna część wystąpienia na żywo"
|
||||
},
|
||||
"category_poi_highlight": {
|
||||
"message": "Wyróżnione"
|
||||
},
|
||||
"category_poi_highlight_description": {
|
||||
"message": "Część filmu, która interesuje większość osób. Podobne do komentarzy typu „Filmik zaczyna się od x”."
|
||||
},
|
||||
"category_poi_highlight_guideline1": {
|
||||
"message": "Część filmu, której szuka większość osób"
|
||||
},
|
||||
"category_poi_highlight_guideline2": {
|
||||
"message": "Może pomóc pominąć kontekst"
|
||||
},
|
||||
"category_poi_highlight_guideline3": {
|
||||
"message": "Może pominąć do karty tytułowej lub miniaturki"
|
||||
},
|
||||
"category_livestream_messages": {
|
||||
"message": "Transmisja live: Dotacja/Czytanie wiadomości"
|
||||
},
|
||||
@@ -659,7 +737,7 @@
|
||||
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
|
||||
},
|
||||
"previewColor": {
|
||||
"message": "Nieprzesłany kolor",
|
||||
"message": "Kolor nieprzesłanego segmentu",
|
||||
"description": "Referring to submissions that have not been sent to the server yet."
|
||||
},
|
||||
"seekBarColor": {
|
||||
@@ -695,7 +773,7 @@
|
||||
"description": "Used when submitting segments to only let them select a certain category if they have it enabled in the options."
|
||||
},
|
||||
"poiOnlyOneSegment": {
|
||||
"message": "Ostrzeżenie: Ten typ segmentu, może być maksymalnie jeden. Przesyłanie kilku na raz spowoduje, że pojawi się losowy."
|
||||
"message": "Ostrzeżenie: Ten typ segmentu może być maksymalnie jeden. Przesyłanie kilku na raz spowoduje, że pojawi się losowy."
|
||||
},
|
||||
"youMustSelectACategory": {
|
||||
"message": "Musisz wybrać kategorię dla każdego segmentu, który zamieszczasz!"
|
||||
@@ -709,6 +787,9 @@
|
||||
"hiddenDueToDuration": {
|
||||
"message": "ukryty: zbyt krótki"
|
||||
},
|
||||
"manuallyHidden": {
|
||||
"message": "ręcznie ukryty"
|
||||
},
|
||||
"channelDataNotFound": {
|
||||
"description": "This error appears in an alert when they try to whitelist a channel and the extension is unable to determine what channel they are looking at.",
|
||||
"message": "ID kanału nie zostało jeszcze załadowane. Jeśli używasz embeddowanego filmu, spróbuj użyć strony głównej YouTube'a. Może to być również spowodowane zmianami w layout'cie YouTube'a, jeśli myślisz, że to przez to, dodaj swój komentarz tutaj:"
|
||||
@@ -793,13 +874,13 @@
|
||||
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
|
||||
},
|
||||
"Donate": {
|
||||
"message": "Dotacje"
|
||||
"message": "Wesprzyj nas"
|
||||
},
|
||||
"considerDonating": {
|
||||
"message": "Wesprzyj"
|
||||
"message": "Wesprzyj rozwój wtyczki"
|
||||
},
|
||||
"hideDonationLink": {
|
||||
"message": "Ukryj Link Do Dotacji"
|
||||
"message": "Ukryj link do darowizny"
|
||||
},
|
||||
"darkModeOptionsPage": {
|
||||
"message": "Tryb ciemny na stronie opcji"
|
||||
@@ -823,10 +904,10 @@
|
||||
"message": "Za każdym razem, gdy pominiesz segment, otrzymasz powiadomienie. Jeśli moment nie wydaje się być poprawny, kliknij łapkę w dół! Możesz również głosować w okienku pop-up."
|
||||
},
|
||||
"Submitting": {
|
||||
"message": "Wysyłanie"
|
||||
"message": "Przesyłanie"
|
||||
},
|
||||
"helpPageSubmitting1": {
|
||||
"message": "Wysyłanie może być wykonane w wyskakującym okienku, poprzez kliknięcie przycisku \"Początek segmentu\" lub za pomocą przycisków na odtwarzaczu wideo."
|
||||
"message": "Zgłoszenia można dokonać w wyskakującym okienku, naciskając przycisk „Początek segmentu”, lub za pomocą przycisków w odtwarzaczu wideo."
|
||||
},
|
||||
"helpPageSubmitting2": {
|
||||
"message": "Kliknięcie przycisku odtwarzania wskazuje początek segmentu a kliknięcie ikony stop wskazuje koniec segmentu. Możesz przygotować wielu segmentów przed wysłaniem. Aby wysłać kliknij przycisk potwierdzający, a aby usunąć - na śmietnik."
|
||||
@@ -864,6 +945,9 @@
|
||||
"LearnMore": {
|
||||
"message": "Dowiedz się więcej"
|
||||
},
|
||||
"FullDetails": {
|
||||
"message": "Pełne szczegóły"
|
||||
},
|
||||
"CopyDownvoteButtonInfo": {
|
||||
"message": "Daje łapkę w dół i tworzy lokalną kopię, abyś mógł przesłać poprawioną wersję"
|
||||
},
|
||||
@@ -879,6 +963,12 @@
|
||||
"ChangeCategoryTooltip": {
|
||||
"message": "To natychmiastowo zostanie zastosowane do twoich segmentów"
|
||||
},
|
||||
"downvote": {
|
||||
"message": "Głos przeciw"
|
||||
},
|
||||
"upvote": {
|
||||
"message": "Głos za"
|
||||
},
|
||||
"hideSegment": {
|
||||
"message": "Ukryj segment"
|
||||
},
|
||||
@@ -917,7 +1007,7 @@
|
||||
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
|
||||
},
|
||||
"noticeVisibilityLabel": {
|
||||
"message": "Pomiń wygląd wpisu",
|
||||
"message": "Wygląd okna pomijania",
|
||||
"description": "Option label"
|
||||
},
|
||||
"unbind": {
|
||||
@@ -936,6 +1026,9 @@
|
||||
"betaServerWarning": {
|
||||
"message": "Serwer BETA jest włączony!"
|
||||
},
|
||||
"openOptionsPage": {
|
||||
"message": "Otwórz stronę ustawień"
|
||||
},
|
||||
"resetToDefault": {
|
||||
"message": "Resetuj do ustawień domyślnych"
|
||||
},
|
||||
|
||||
@@ -239,6 +239,9 @@
|
||||
"showSkipNotice": {
|
||||
"message": "Mostrar Aviso Após Um Segmento Ser Ignorado"
|
||||
},
|
||||
"showCategoryGuidelines": {
|
||||
"message": "Mostrar Categoria de Ajuda"
|
||||
},
|
||||
"noticeVisibilityMode0": {
|
||||
"message": "Avisos de Ignorar em Tamanho Inteiro"
|
||||
},
|
||||
|
||||
@@ -239,6 +239,9 @@
|
||||
"showSkipNotice": {
|
||||
"message": "Показувати сповіщення після пропуску сегмента"
|
||||
},
|
||||
"showCategoryGuidelines": {
|
||||
"message": "Показати Довідку по Категоріях"
|
||||
},
|
||||
"noticeVisibilityMode0": {
|
||||
"message": "Повнорозмірні сповіщення про пропуски"
|
||||
},
|
||||
|
||||
@@ -239,6 +239,9 @@
|
||||
"showSkipNotice": {
|
||||
"message": "Hiển thị thông báo sau khi bỏ qua phân đoạn"
|
||||
},
|
||||
"showCategoryGuidelines": {
|
||||
"message": "Hiển thị Danh mục Trợ giúp"
|
||||
},
|
||||
"noticeVisibilityMode0": {
|
||||
"message": "Thông báo bỏ qua với kích thước đầy đủ"
|
||||
},
|
||||
@@ -542,18 +545,36 @@
|
||||
"message": "đến",
|
||||
"description": "Used between segments. Example: 1:20 to 1:30"
|
||||
},
|
||||
"generic_guideline2": {
|
||||
"message": "Chơi như thể không có gì bị bỏ qua"
|
||||
},
|
||||
"category_sponsor": {
|
||||
"message": "Nhà tài trợ"
|
||||
},
|
||||
"category_sponsor_description": {
|
||||
"message": "Nội dung được trả tiền để quảng cáo, giới thiệu và quảng cáo trực tiếp. Không phải là quảng cáo không trả công hay được đề cập miễn phí."
|
||||
},
|
||||
"category_sponsor_guideline1": {
|
||||
"message": "Quảng cáo trả phí"
|
||||
},
|
||||
"category_sponsor_guideline2": {
|
||||
"message": "Không dành cho các khoản đóng góp"
|
||||
},
|
||||
"category_selfpromo": {
|
||||
"message": "Quảng cáo không trả công/Tự quảng cáo"
|
||||
},
|
||||
"category_selfpromo_description": {
|
||||
"message": "Tương tự như 'nhà tài trợ' ngoại trừ việc quảng cáo không được trả tiền hay tự quảng cáo. Điều này bao gồm các phần hàng hóa, đóng góp, hoặc thông tin về người mà họ hợp tác cùng."
|
||||
},
|
||||
"category_selfpromo_guideline1": {
|
||||
"message": "Quyên góp, tư cách thành viên và hàng hóa tùy chỉnh"
|
||||
},
|
||||
"category_selfpromo_guideline2": {
|
||||
"message": "Lời cảm ơn miễn phí không thêm vào video"
|
||||
},
|
||||
"category_selfpromo_guideline3": {
|
||||
"message": "Không dành cho các sản phẩm và hàng hóa do công ty thiết kế"
|
||||
},
|
||||
"category_exclusive_access": {
|
||||
"message": "Truy cập riêng"
|
||||
},
|
||||
@@ -564,12 +585,24 @@
|
||||
"message": "Video này giới thiệu sản phẩm, dịch vụ hoặc vị trí mà họ đã nhận được quyền truy cập miễn phí hoặc được trợ cấp",
|
||||
"description": "Short description for this category"
|
||||
},
|
||||
"category_exclusive_access_guideline1": {
|
||||
"message": "Toàn bộ video giới thiệu nội dung nào đó có quyền truy cập miễn phí hoặc được trợ cấp"
|
||||
},
|
||||
"category_interaction": {
|
||||
"message": "Nhắc tương tác (Đăng ký)"
|
||||
},
|
||||
"category_interaction_description": {
|
||||
"message": "Nhắc nhở người xem Thích, Đăng ký hoặc Theo dõi. Nếu nó dài hoặc là một cái gì cụ thể, nó nên là danh mục \"Tự quảng cáo\"."
|
||||
},
|
||||
"category_interaction_guideline1": {
|
||||
"message": "Lời nhắc ngắn gọn để thích, đăng ký hoặc theo dõi"
|
||||
},
|
||||
"category_interaction_guideline2": {
|
||||
"message": "Bao gồm lời nhắc gián tiếp để nhận xét"
|
||||
},
|
||||
"category_interaction_guideline3": {
|
||||
"message": "Không dành cho quảng cáo chung, chỉ dành cho lời kêu gọi hành động"
|
||||
},
|
||||
"category_interaction_short": {
|
||||
"message": "Nhắc nhở tương tác"
|
||||
},
|
||||
@@ -582,18 +615,36 @@
|
||||
"category_intro_short": {
|
||||
"message": "Tạm ngừng"
|
||||
},
|
||||
"category_intro_guideline1": {
|
||||
"message": "Khoảng thời gian không có nội dung thực tế"
|
||||
},
|
||||
"category_intro_guideline2": {
|
||||
"message": "Không dành cho chuyển tiếp với thông tin"
|
||||
},
|
||||
"category_outro": {
|
||||
"message": "Màn hình kết thúc/Danh đề"
|
||||
},
|
||||
"category_outro_description": {
|
||||
"message": "Credits hoặc khi thẻ màn hình kết thúc của YouTube xuất hiện. Không dùng với những đoạn có thông tin."
|
||||
},
|
||||
"category_outro_guideline1": {
|
||||
"message": "Không bao gồm nội dung, ngay cả khi thẻ kết thúc ở trên màn hình"
|
||||
},
|
||||
"category_preview": {
|
||||
"message": "Xem trước/Tóm tắt"
|
||||
},
|
||||
"category_preview_description": {
|
||||
"message": "Tóm tắt nhanh về tập trước/tập sau trong 1 chuỗi video (series) dài (hoặc cũng có thể là tóm tắt trước về video sắp chiếu)."
|
||||
},
|
||||
"category_preview_guideline1": {
|
||||
"message": "Các clip xuất hiện sau đó hoặc trong một video trong tương lai"
|
||||
},
|
||||
"category_preview_guideline2": {
|
||||
"message": "Tóm tắt video trước đó"
|
||||
},
|
||||
"category_preview_guideline3": {
|
||||
"message": "Không dành cho các phần thêm nội dung bổ sung"
|
||||
},
|
||||
"category_filler_description": {
|
||||
"message": "Tập hợp các cảnh không bắt buộc để xem trong video. Điều này không bao gồm các đoạn chứa nội dung hoặc nói về ngữ cảnh của video."
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
display: none;
|
||||
}
|
||||
@@ -12,12 +21,12 @@
|
||||
|
||||
height: 100%;
|
||||
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);
|
||||
}
|
||||
|
||||
.ytm-progress-bar > #previewbar {
|
||||
.progress-bar-line > #previewbar {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
@@ -30,6 +39,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.previewbar.requiredSegment {
|
||||
transform: scaleY(3)
|
||||
}
|
||||
|
||||
/* Make sure settings are upfront */
|
||||
.ytp-settings-menu {
|
||||
z-index: 6000 !important;
|
||||
@@ -45,23 +58,48 @@
|
||||
transform: translateY(-1em) !important;
|
||||
}
|
||||
|
||||
.ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
|
||||
transform: translateY(-2em) !important;
|
||||
}
|
||||
|
||||
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
display: block !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 {
|
||||
@@ -88,6 +126,16 @@
|
||||
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 {
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -113,8 +161,8 @@
|
||||
.sponsorSkipObject {
|
||||
font-family: Roboto, Arial, Helvetica, sans-serif;
|
||||
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
margin-left: var(--skip-notice-margin);
|
||||
margin-right: var(--skip-notice-margin);
|
||||
}
|
||||
|
||||
.sponsorSkipLogo {
|
||||
@@ -145,7 +193,7 @@
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 100px;
|
||||
right: 10px;
|
||||
right: var(--skip-notice-right);
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeParent {
|
||||
@@ -173,6 +221,7 @@
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeTableContainer {
|
||||
color: white;
|
||||
background-color: rgba(28, 28, 28, 0.9);
|
||||
border-radius: 5px;
|
||||
min-width: 100%;
|
||||
@@ -351,6 +400,7 @@
|
||||
.sponsorTimesInfoMessage {
|
||||
font-size: 13.3333px;
|
||||
color: rgb(235, 235, 235);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sb-guidelines-notice .sponsorTimesInfoMessage td {
|
||||
@@ -523,12 +573,56 @@ input::-webkit-inner-spin-button {
|
||||
margin-bottom: 5px;
|
||||
|
||||
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;
|
||||
border-width: 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 {
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
@@ -543,17 +637,6 @@ input::-webkit-inner-spin-button {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sbChatNotice iframe {
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sbChatClose {
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.skipButtonControlBarContainer {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -627,6 +710,11 @@ input::-webkit-inner-spin-button {
|
||||
border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.sponsorBlockTooltip.sbTriangle.centeredSBTriangle::after {
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.sponsorBlockLockedColor {
|
||||
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;
|
||||
}
|
||||
|
||||
.categoryExtraOptions {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#music_offtopic_autoSkipOnMusicVideos {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.option-group > div:last-child, .option-group > #keybind-dialog {
|
||||
border-bottom: inherit;
|
||||
}
|
||||
@@ -309,6 +317,14 @@ input[type='number'] {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.disabled .slider {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
tr.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#options {
|
||||
height: 100vh;
|
||||
flex-basis: 80%;
|
||||
@@ -346,6 +362,10 @@ input[type='number'] {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -670,4 +690,9 @@ svg {
|
||||
#options > div {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upsellButton {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -66,18 +66,6 @@
|
||||
|
||||
</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 class="switch-container">
|
||||
<label class="switch">
|
||||
@@ -314,6 +302,18 @@
|
||||
</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 id="keybinds" class="option-group hidden">
|
||||
@@ -333,6 +333,16 @@
|
||||
<div class="inline"></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 id="import" class="option-group hidden">
|
||||
@@ -352,6 +362,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-type="react-UnsubmittedVideosComponent"></div>
|
||||
|
||||
<div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
|
||||
<h2>__MSG_exportOptions__</h2>
|
||||
@@ -480,7 +492,7 @@
|
||||
|
||||
<div class="small-description">__MSG_copyDebugInformationOptions__</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="center">
|
||||
__MSG_invidiousPermissionRefresh__
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="center">
|
||||
<div id="acceptPermissionButton" class="option-button inline">
|
||||
__MSG_acceptPermission__
|
||||
|
||||
109
public/popup.css
109
public/popup.css
@@ -100,6 +100,10 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#sponsorBlockPopupContainer iframe {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
* Disable popup max height when displayed in-page (content.ts)
|
||||
*/
|
||||
@@ -110,7 +114,7 @@
|
||||
/*
|
||||
* Disable fixed popup width when displayed in-page (content.ts)
|
||||
*/
|
||||
#sponsorBlockPopupContainer #sponsorBlockPopupBody {
|
||||
#sponsorBlockPopupBody.is-embedded {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -148,22 +152,46 @@
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Refresh segments button
|
||||
*/
|
||||
#refreshSegmentsButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
#issueReporterImportExport {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#refreshSegmentsButton, #issueReporterImportExport button {
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
margin: 5px auto;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
}
|
||||
#refreshSegmentsButton:hover {
|
||||
|
||||
#refreshSegmentsButton:hover, #issueReporterImportExport button:hover {
|
||||
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
|
||||
*/
|
||||
@@ -195,6 +223,15 @@
|
||||
.segmentSummary > div {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.segmentActive {
|
||||
color: #bdfffb;
|
||||
}
|
||||
|
||||
.segmentPassed {
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
/*
|
||||
* Category dot in segment
|
||||
*/
|
||||
@@ -207,7 +244,7 @@
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
/*
|
||||
/*
|
||||
* Category name in segment
|
||||
*/
|
||||
.summaryLabel {
|
||||
@@ -424,6 +461,10 @@
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.sbStatsSentence .sbExtraInfo {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Increase font size of username input and display
|
||||
*/
|
||||
@@ -458,9 +499,11 @@
|
||||
*/
|
||||
#usernameElement {
|
||||
padding: 8px;
|
||||
min-width: 50%;
|
||||
}
|
||||
#setUsernameContainer {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
}
|
||||
#setUsernameContainer > button {
|
||||
display: flex;
|
||||
@@ -490,7 +533,6 @@
|
||||
* Set username form container with "expanded" state
|
||||
*/
|
||||
#setUsername.SBExpanded {
|
||||
width: calc(200% - 130px);
|
||||
text-align: left;
|
||||
}
|
||||
/*
|
||||
@@ -506,7 +548,7 @@
|
||||
background: var(--sb-grey-bg-color);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Submissions
|
||||
*/
|
||||
#sponsorTimesContributionsContainer {
|
||||
@@ -535,6 +577,11 @@
|
||||
background: #444;
|
||||
}
|
||||
|
||||
#sponsorTimesDonateContainer a {
|
||||
color: var(--sb-main-fg-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* "Show Notice Again" button
|
||||
*/
|
||||
@@ -546,3 +593,45 @@
|
||||
margin-bottom: 20px;
|
||||
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" />
|
||||
<link id="sponsorBlockPopupFont" href="/libs/Source+Sans+Pro.css" rel="stylesheet">
|
||||
<link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet">
|
||||
<link id="sponsorBlockStyleSheet" href="shared.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body id="sponsorBlockPopupBody" style="visibility: hidden">
|
||||
@@ -34,7 +35,33 @@
|
||||
</button>
|
||||
<!-- Video Segments -->
|
||||
<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="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>
|
||||
|
||||
@@ -85,7 +112,7 @@
|
||||
|
||||
<!-- Your Work box -->
|
||||
<div class="sbYourWorkCols">
|
||||
<p class="sbHeader" style="padding: 8px 16px; cursor: pointer;">
|
||||
<p class="sbHeader" style="padding: 8px 16px;">
|
||||
__MSG_yourWork__
|
||||
</p>
|
||||
<div>
|
||||
@@ -125,13 +152,15 @@
|
||||
</b>
|
||||
<span id="sponsorTimesViewsDisplayEndWord">__MSG_Segments__</span>
|
||||
<br />
|
||||
(
|
||||
<b>
|
||||
<span id="sponsorTimesOthersTimeSavedDisplay">0</span>
|
||||
<span id="sponsorTimesOthersTimeSavedEndWord">__MSG_minsLower__</span>
|
||||
</b>
|
||||
<span>__MSG_youHaveSavedTimeEnd__</span>
|
||||
)
|
||||
<span class="sbExtraInfo">
|
||||
(
|
||||
<b>
|
||||
<span id="sponsorTimesOthersTimeSavedDisplay">0</span>
|
||||
<span id="sponsorTimesOthersTimeSavedEndWord">__MSG_minsLower__</span>
|
||||
</b>
|
||||
<span>__MSG_youHaveSavedTimeEnd__</span>
|
||||
)
|
||||
</span>
|
||||
</p>
|
||||
<p id="sponsorTimesSkipsDoneContainer" style="display: none" class="u-mZ sbStatsSentence">
|
||||
__MSG_youHaveSkipped__
|
||||
@@ -139,24 +168,26 @@
|
||||
<span id="sponsorTimesSkipsDoneDisplay">0</span>
|
||||
</b>
|
||||
<span id="sponsorTimesSkipsDoneEndWord">__MSG_Segments__</span>
|
||||
(
|
||||
<b>
|
||||
<span id="sponsorTimeSavedDisplay">0</span>
|
||||
<span id="sponsorTimeSavedEndWord">__MSG_minsLower__</span>
|
||||
</b>
|
||||
)
|
||||
<span class="sbExtraInfo">
|
||||
(
|
||||
<b>
|
||||
<span id="sponsorTimeSavedDisplay">0</span>
|
||||
<span id="sponsorTimeSavedEndWord">__MSG_minsLower__</span>
|
||||
</b>
|
||||
)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer id="sbFooter">
|
||||
<div id="sponsorTimesDonateContainer" style="display: none; align-items: center; justify-content: center;">
|
||||
<img class="sbHeart" src="/icons/heart.svg" alt="Heart icon" />
|
||||
<a id="sbConsiderDonateLink" href="https://sponsor.ajay.app/donate" target="_blank" rel="noopener">
|
||||
__MSG_considerDonating__
|
||||
</a>
|
||||
<img id="sbCloseDonate" src="/icons/close.png" alt="Close icon" height="8" style="padding-left: 5px; cursor: pointer;" />
|
||||
</div>
|
||||
<div id="sponsorTimesDonateContainer" style="display: none; align-items: center; justify-content: center;">
|
||||
<img class="sbHeart" src="/icons/heart.svg" alt="Heart icon" />
|
||||
<a id="sbConsiderDonateLink" href="https://sponsor.ajay.app/donate" target="_blank" rel="noopener">
|
||||
__MSG_considerDonating__
|
||||
</a>
|
||||
<img id="sbCloseDonate" src="/icons/close.png" alt="Close icon" height="8" style="padding-left: 5px; cursor: pointer;" />
|
||||
</div>
|
||||
|
||||
<footer id="sbFooter">
|
||||
<a id="helpButton">__MSG_help__</a>
|
||||
<a href="https://sponsor.ajay.app" target="_blank" rel="noopener">__MSG_website__</a>
|
||||
<a href="https://sponsor.ajay.app/stats" target="_blank" rel="noopener">__MSG_viewLeaderboard__</a>
|
||||
|
||||
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;
|
||||
|
||||
import Utils from "./utils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
const utils = new Utils({
|
||||
registerFirefoxContentScript,
|
||||
unregisterFirefoxContentScript
|
||||
});
|
||||
|
||||
const popupPort: Record<string, chrome.runtime.Port> = {};
|
||||
|
||||
// Used only on Firefox, which does not support non persistent background pages.
|
||||
const contentScriptRegistrations = {};
|
||||
|
||||
@@ -52,7 +55,7 @@ if (!Config.configSyncListeners.includes(onNavigationApiAvailableChange)) {
|
||||
Config.configSyncListeners.push(onNavigationApiAvailableChange);
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(function (request, _, callback) {
|
||||
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
|
||||
switch(request.message) {
|
||||
case "openConfig":
|
||||
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
|
||||
@@ -84,7 +87,7 @@ chrome.runtime.onMessage.addListener(function (request, _, callback) {
|
||||
case "unregisterContentScript":
|
||||
unregisterFirefoxContentScript(request.id)
|
||||
return false;
|
||||
case "tabs":
|
||||
case "tabs": {
|
||||
chrome.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
@@ -92,19 +95,37 @@ chrome.runtime.onMessage.addListener(function (request, _, callback) {
|
||||
chrome.tabs.sendMessage(
|
||||
tabs[0].id,
|
||||
request.data,
|
||||
(response) => callback(response)
|
||||
(response) => {
|
||||
callback(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
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
|
||||
chrome.runtime.onInstalled.addListener(function () {
|
||||
// This let's the config sync to run fully before checking.
|
||||
// This is required on Firefox
|
||||
setTimeout(function() {
|
||||
setTimeout(async () => {
|
||||
const userID = Config.config.userID;
|
||||
|
||||
// If there is no userID, then it is the first install.
|
||||
@@ -113,13 +134,19 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
|
||||
|
||||
//generate a userID
|
||||
const newUserID = utils.generateUserID();
|
||||
const newUserID = GenericUtils.generateUserID();
|
||||
//save this UUID
|
||||
Config.config.userID = newUserID;
|
||||
|
||||
// Don't show update notification
|
||||
Config.config.categoryPillUpdate = true;
|
||||
}
|
||||
|
||||
if (Config.config.supportInvidious) {
|
||||
if (!(await utils.containsInvidiousPermission())) {
|
||||
chrome.tabs.create({url: chrome.extension.getURL("/permissions/index.html")});
|
||||
}
|
||||
}
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
@@ -156,7 +183,7 @@ async function submitVote(type: number, UUID: string, category: string) {
|
||||
|
||||
if (userID == undefined || userID === "undefined") {
|
||||
//generate one
|
||||
userID = utils.generateUserID();
|
||||
userID = GenericUtils.generateUserID();
|
||||
Config.config.userID = userID;
|
||||
}
|
||||
|
||||
@@ -203,7 +230,7 @@ async function asyncRequestToServer(type: string, address: string, data = {}) {
|
||||
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
|
||||
// If GET, convert JSON to parameters
|
||||
if (type.toLowerCase() === "get") {
|
||||
url = utils.objectToURI(url, data, true);
|
||||
url = GenericUtils.objectToURI(url, data, true);
|
||||
|
||||
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,
|
||||
|
||||
maxCountdownTime?: () => number,
|
||||
dontPauseCountdown?: boolean,
|
||||
amountOfPreviousNotices?: number,
|
||||
showInSecondSlot?: boolean,
|
||||
timed?: boolean,
|
||||
@@ -25,6 +26,8 @@ export interface NoticeProps {
|
||||
smaller?: boolean,
|
||||
limitWidth?: boolean,
|
||||
extraClass?: string,
|
||||
hideLogo?: boolean,
|
||||
hideRightInfo?: boolean,
|
||||
|
||||
// Callback for when this is closed
|
||||
closeListener: () => void,
|
||||
@@ -117,13 +120,15 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
{/* Left column */}
|
||||
<td className="noticeLeftIcon">
|
||||
{/* Logo */}
|
||||
<img id={"sponsorSkipLogo" + this.idSuffix}
|
||||
className="sponsorSkipLogo sponsorSkipObject"
|
||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||
</img>
|
||||
{!this.props.hideLogo &&
|
||||
<img id={"sponsorSkipLogo" + this.idSuffix}
|
||||
className="sponsorSkipLogo sponsorSkipObject"
|
||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||
</img>
|
||||
}
|
||||
|
||||
<span id={"sponsorSkipMessage" + this.idSuffix}
|
||||
style={{float: "left"}}
|
||||
style={{float: "left", marginRight: this.props.hideLogo ? "0px" : null}}
|
||||
className="sponsorSkipMessage sponsorSkipObject">
|
||||
|
||||
{this.props.noticeTitle}
|
||||
@@ -135,28 +140,30 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
{this.props.firstRow}
|
||||
|
||||
{/* Right column */}
|
||||
<td className="sponsorSkipNoticeRightSection"
|
||||
style={{top: "9.32px"}}>
|
||||
{!this.props.hideRightInfo &&
|
||||
<td className="sponsorSkipNoticeRightSection"
|
||||
style={{top: "9.32px"}}>
|
||||
|
||||
{/* Time left */}
|
||||
{this.props.timed ? (
|
||||
<span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
|
||||
onClick={() => this.toggleManualPause()}
|
||||
className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
|
||||
|
||||
{this.getCountdownElements()}
|
||||
|
||||
</span>
|
||||
) : ""}
|
||||
|
||||
{/* Time left */}
|
||||
{this.props.timed ? (
|
||||
<span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
|
||||
onClick={() => this.toggleManualPause()}
|
||||
className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
|
||||
|
||||
{this.getCountdownElements()}
|
||||
|
||||
</span>
|
||||
) : ""}
|
||||
|
||||
|
||||
{/* Close button */}
|
||||
<img src={chrome.extension.getURL("icons/close.png")}
|
||||
className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
|
||||
+ (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
|
||||
onClick={() => this.close()}>
|
||||
</img>
|
||||
</td>
|
||||
{/* Close button */}
|
||||
<img src={chrome.extension.getURL("icons/close.png")}
|
||||
className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
|
||||
+ (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
|
||||
onClick={() => this.close()}>
|
||||
</img>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
|
||||
{this.props.children}
|
||||
@@ -289,7 +296,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
}
|
||||
|
||||
pauseCountdown(): void {
|
||||
if (!this.props.timed) return;
|
||||
if (!this.props.timed || this.props.dontPauseCountdown) return;
|
||||
|
||||
//remove setInterval
|
||||
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
||||
|
||||
@@ -36,12 +36,31 @@ class NoticeTextSelectionComponent extends React.Component<NoticeTextSelectionPr
|
||||
: null}
|
||||
|
||||
<span>
|
||||
{this.props.text}
|
||||
{this.getTextElements(this.props.text)}
|
||||
</span>
|
||||
</td>
|
||||
</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;
|
||||
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 PencilSvg from "../svg-icons/pencil_svg";
|
||||
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
|
||||
enum SkipButtonState {
|
||||
Undo, // Unskip
|
||||
@@ -540,7 +541,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
|
||||
const sponsorTimesSubmitting : SponsorTime = {
|
||||
segment: this.segments[index].segment,
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
UUID: GenericUtils.generateUserID() as SegmentUUID,
|
||||
category: this.segments[index].category,
|
||||
actionType: this.segments[index].actionType,
|
||||
source: SponsorSourceType.Local
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import * as React from "react";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import Config from "../config";
|
||||
import { ActionType, Category, ContentContainer, SponsorTime } from "../types";
|
||||
import { ActionType, Category, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
|
||||
import Utils from "../utils";
|
||||
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
||||
import { RectangleTooltip } from "../render/RectangleTooltip";
|
||||
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
import { noRefreshFetchingChaptersAllowed } from "../utils/licenseKey";
|
||||
|
||||
|
||||
const utils = new Utils();
|
||||
@@ -25,16 +28,23 @@ export interface SponsorTimeEditState {
|
||||
editing: boolean;
|
||||
sponsorTimeEdits: [string, string];
|
||||
selectedCategory: Category;
|
||||
description: string;
|
||||
suggestedNames: SelectorOption[];
|
||||
chapterNameSelectorOpen: boolean;
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
idSuffix: string;
|
||||
|
||||
categoryOptionRef: React.RefObject<HTMLSelectElement>;
|
||||
actionTypeOptionRef: React.RefObject<HTMLSelectElement>;
|
||||
descriptionOptionRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
configUpdateListener: () => void;
|
||||
|
||||
@@ -42,26 +52,35 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
// Used when selecting POI or Full
|
||||
timesBeforeChanging: number[] = [];
|
||||
fullVideoWarningShown = false;
|
||||
categoryNameWarningShown = false;
|
||||
|
||||
// For description auto-complete
|
||||
fetchingSuggestions: boolean;
|
||||
|
||||
constructor(props: SponsorTimeEditProps) {
|
||||
super(props);
|
||||
|
||||
this.categoryOptionRef = React.createRef();
|
||||
this.actionTypeOptionRef = React.createRef();
|
||||
this.descriptionOptionRef = React.createRef();
|
||||
|
||||
this.idSuffix = this.props.idSuffix;
|
||||
|
||||
this.previousSkipType = ActionType.Skip;
|
||||
|
||||
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||
this.state = {
|
||||
editing: false,
|
||||
sponsorTimeEdits: [null, null],
|
||||
selectedCategory: DEFAULT_CATEGORY as Category
|
||||
selectedCategory: DEFAULT_CATEGORY as Category,
|
||||
description: sponsorTime.description || "",
|
||||
suggestedNames: [],
|
||||
chapterNameSelectorOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
// 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();
|
||||
});
|
||||
|
||||
@@ -87,6 +106,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
render(): React.ReactElement {
|
||||
this.checkToShowFullVideoWarning();
|
||||
this.checkToShowChapterWarning();
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
textAlign: "center"
|
||||
@@ -96,14 +116,6 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
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
|
||||
let timeDisplay: JSX.Element;
|
||||
const timeDisplayStyle: React.CSSProperties = {};
|
||||
@@ -123,11 +135,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</span>
|
||||
<input id={"submittingTime0" + this.idSuffix}
|
||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||
ref={oldYouTubeDarkStyles}
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
value={this.state.sponsorTimeEdits[0]}
|
||||
onChange={(e) => {this.handleOnChange(0, e, sponsorTime, e.target.value)}}
|
||||
onWheel={(e) => {this.changeTimesWhenScrolling(0, e, sponsorTime)}}>
|
||||
onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
|
||||
onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
|
||||
</input>
|
||||
|
||||
{sponsorTime.actionType !== ActionType.Poi ? (
|
||||
@@ -138,11 +150,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
<input id={"submittingTime1" + this.idSuffix}
|
||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||
ref={oldYouTubeDarkStyles}
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
value={this.state.sponsorTimeEdits[1]}
|
||||
onChange={(e) => {this.handleOnChange(1, e, sponsorTime, e.target.value)}}
|
||||
onWheel={(e) => {this.changeTimesWhenScrolling(1, e, sponsorTime)}}>
|
||||
onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)}
|
||||
onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}>
|
||||
</input>
|
||||
|
||||
<span id={"nowButton1" + this.idSuffix}
|
||||
@@ -167,15 +179,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
style={timeDisplayStyle}
|
||||
className="sponsorTimeDisplay"
|
||||
onClick={this.toggleEditTime.bind(this)}>
|
||||
{utils.getFormattedTime(segment[0], true) +
|
||||
{GenericUtils.getFormattedTime(segment[0], true) +
|
||||
((!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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div id={"sponsorTimeEditContainer" + this.idSuffix} style={style}>
|
||||
|
||||
{timeDisplay}
|
||||
|
||||
@@ -185,7 +197,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
className="sponsorTimeEditSelector sponsorTimeCategories"
|
||||
defaultValue={sponsorTime.category}
|
||||
ref={this.categoryOptionRef}
|
||||
onChange={this.categorySelectionChange.bind(this)}>
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
onChange={(event) => this.categorySelectionChange(event)}>
|
||||
{this.getCategoryOptions()}
|
||||
</select>
|
||||
|
||||
@@ -208,6 +221,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<select id={"sponsorTimeActionTypes" + this.idSuffix}
|
||||
className="sponsorTimeEditSelector sponsorTimeActionTypes"
|
||||
defaultValue={sponsorTime.actionType}
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
ref={this.actionTypeOptionRef}
|
||||
onChange={(e) => this.actionTypeSelectionChange(e)}>
|
||||
{this.getActionTypeOptions(sponsorTime)}
|
||||
@@ -215,6 +229,27 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</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/>
|
||||
|
||||
{/* Editing Tools */}
|
||||
@@ -229,7 +264,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<span id={"sponsorTimePreviewButton" + this.idSuffix}
|
||||
className="sponsorTimeEditButton"
|
||||
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>
|
||||
): ""}
|
||||
|
||||
@@ -256,16 +292,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const sponsorTimeEdits = this.state.sponsorTimeEdits;
|
||||
|
||||
// check if change is small engough to show tooltip
|
||||
const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||
const after = utils.getFormattedTimeToSeconds(targetValue);
|
||||
const before = GenericUtils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||
const after = GenericUtils.getFormattedTimeToSeconds(targetValue);
|
||||
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;
|
||||
if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue;
|
||||
|
||||
this.setState({sponsorTimeEdits});
|
||||
this.saveEditTimes();
|
||||
this.setState({sponsorTimeEdits}, () => this.saveEditTimes());
|
||||
}
|
||||
|
||||
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;
|
||||
let timeAsNumber = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
||||
let timeAsNumber = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
|
||||
if (timeAsNumber !== null && e.deltaY != 0) {
|
||||
if (e.deltaY < 0) {
|
||||
timeAsNumber += step;
|
||||
@@ -290,7 +325,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
} else {
|
||||
timeAsNumber = 0;
|
||||
}
|
||||
sponsorTimeEdits[index] = utils.getFormattedTime(timeAsNumber, true);
|
||||
|
||||
sponsorTimeEdits[index] = GenericUtils.getFormattedTime(timeAsNumber, true);
|
||||
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
|
||||
|
||||
this.setState({sponsorTimeEdits});
|
||||
@@ -300,26 +336,29 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
showScrollToEditToolTip(): void {
|
||||
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);
|
||||
if (element) {
|
||||
new RectangleTooltip({
|
||||
text,
|
||||
referenceNode: element.parentElement,
|
||||
prependElement: element,
|
||||
timeout: 15,
|
||||
bottomOffset: 0 + "px",
|
||||
leftOffset: -318 + "px",
|
||||
backgroundColor: "rgba(28, 28, 28, 1.0)",
|
||||
htmlId: "sponsorTimesContainer" + this.idSuffix,
|
||||
buttonFunction,
|
||||
fontSize: "14px",
|
||||
maxHeight: "200px"
|
||||
});
|
||||
if (element) {
|
||||
const htmlId = `sponsorRectangleTooltip${id + this.idSuffix}`;
|
||||
if (!document.getElementById(htmlId)) {
|
||||
new RectangleTooltip({
|
||||
text,
|
||||
referenceNode: element.parentElement,
|
||||
prependElement: element,
|
||||
timeout: 15,
|
||||
bottomOffset: 0 + "px",
|
||||
leftOffset: -318 + "px",
|
||||
backgroundColor: "rgba(28, 28, 28, 1.0)",
|
||||
htmlId,
|
||||
buttonFunction,
|
||||
fontSize: "14px",
|
||||
maxHeight: "200px"
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
@@ -334,12 +373,25 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
if (videoPercentage > 0.6 && !this.fullVideoWarningShown
|
||||
&& (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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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[] {
|
||||
const elements = [(
|
||||
<option value={DEFAULT_CATEGORY}
|
||||
@@ -349,6 +401,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
)];
|
||||
|
||||
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(
|
||||
<option value={category}
|
||||
key={category}
|
||||
@@ -369,7 +426,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const chosenCategory = event.target.value as Category;
|
||||
|
||||
// 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;
|
||||
|
||||
// Alert that they have to enable this category first
|
||||
@@ -470,7 +527,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
this.setState({
|
||||
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
|
||||
}, this.saveEditTimes);
|
||||
}, () => this.saveEditTimes());
|
||||
}
|
||||
|
||||
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 */
|
||||
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
|
||||
return [utils.getFormattedTime(sponsorTime.segment[0], true),
|
||||
utils.getFormattedTime(sponsorTime.segment[1], true)];
|
||||
return [GenericUtils.getFormattedTime(sponsorTime.segment[0], true),
|
||||
GenericUtils.getFormattedTime(sponsorTime.segment[1], true)];
|
||||
}
|
||||
|
||||
saveEditTimes(): void {
|
||||
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
|
||||
|
||||
if (this.state.editing) {
|
||||
const startTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
||||
const endTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
||||
const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
|
||||
const endTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
|
||||
|
||||
// Change segment time only if the format was correct
|
||||
if (startTime !== null && endTime !== null) {
|
||||
@@ -513,8 +570,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const category = this.categoryOptionRef.current.value as Category
|
||||
sponsorTimesSubmitting[this.props.index].category = category;
|
||||
|
||||
const inputActionType = this.actionTypeOptionRef?.current?.value as ActionType;
|
||||
sponsorTimesSubmitting[this.props.index].actionType = this.getNextActionType(category, inputActionType);
|
||||
const actionType = this.getNextActionType(category, this.actionTypeOptionRef?.current?.value as ActionType);
|
||||
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.forceSyncUpdate("unsubmittedSegments");
|
||||
@@ -536,19 +596,19 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
previewTime(ctrlPressed = false, shiftPressed = false): void {
|
||||
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
|
||||
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;
|
||||
if (ctrlPressed) seekTime = 0.5;
|
||||
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 {
|
||||
@@ -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 {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -73,12 +73,20 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
||||
}
|
||||
|
||||
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 (
|
||||
<NoticeComponent noticeTitle={this.state.noticeTitle}
|
||||
idSuffix={this.state.idSuffix}
|
||||
ref={this.noticeRef}
|
||||
closeListener={this.cancel.bind(this)}
|
||||
zIndex={5000}>
|
||||
zIndex={5000}
|
||||
firstColumn={sortButton}>
|
||||
|
||||
{/* Text Boxes */}
|
||||
{this.getMessageBoxes()}
|
||||
@@ -163,7 +171,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
||||
this.guidelinesReminder?.close();
|
||||
this.noticeRef.current.close(true);
|
||||
|
||||
this.contentContainer().resetSponsorSubmissionNotice();
|
||||
this.contentContainer().resetSponsorSubmissionNotice(false);
|
||||
|
||||
this.props.closeListener();
|
||||
}
|
||||
@@ -198,6 +206,16 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
||||
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 {
|
||||
const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth;
|
||||
if (category !== "chooseACategory" && Config.config.showCategoryGuidelines
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { Category } from "../types";
|
||||
import * as CompileConfig from "../../../config.json";
|
||||
import { Category } from "../../types";
|
||||
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
|
||||
|
||||
export interface CategoryChooserProps {
|
||||
@@ -1,10 +1,13 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Config from "../config"
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { Category, CategorySkipOption } from "../types";
|
||||
import Config from "../../config"
|
||||
import * as CompileConfig from "../../../config.json";
|
||||
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 {
|
||||
category: Category;
|
||||
@@ -15,6 +18,7 @@ export interface CategorySkipOptionsProps {
|
||||
export interface CategorySkipOptionsState {
|
||||
color: string;
|
||||
previewColor: string;
|
||||
hideChapter: boolean;
|
||||
}
|
||||
|
||||
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
|
||||
@@ -27,10 +31,28 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
this.state = {
|
||||
color: props.defaultColor || Config.config.barTypes[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 {
|
||||
if (this.state.hideChapter) {
|
||||
// Ensure force update refreshes this
|
||||
fetchingChaptersAllowed().then((allowed) => {
|
||||
if (allowed) {
|
||||
this.setState({
|
||||
hideChapter: !allowed
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let defaultOption = "disable";
|
||||
// Set the default opton properly
|
||||
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 (
|
||||
<>
|
||||
<tr id={this.props.category + "OptionsRow"}
|
||||
className="categoryTableElement">
|
||||
className={`categoryTableElement${extraClasses}`} >
|
||||
<td id={this.props.category + "OptionName"}
|
||||
className="categoryTableLabel">
|
||||
{chrome.i18n.getMessage("category_" + this.props.category)}
|
||||
@@ -65,21 +97,29 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
<select
|
||||
className="optionsSelector"
|
||||
defaultValue={defaultOption}
|
||||
disabled={disabled}
|
||||
onChange={this.skipOptionSelected.bind(this)}>
|
||||
{this.getCategorySkipOptions()}
|
||||
</select>
|
||||
|
||||
{disabled &&
|
||||
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
|
||||
}
|
||||
</td>
|
||||
|
||||
<td id={this.props.category + "ColorOption"}
|
||||
className="colorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
onChange={(event) => this.setColorState(event, false)}
|
||||
value={this.state.color} />
|
||||
</td>
|
||||
{this.props.category !== "chapter" &&
|
||||
<td id={this.props.category + "ColorOption"}
|
||||
className="colorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
disabled={disabled}
|
||||
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"}
|
||||
className="previewColorOption">
|
||||
<input
|
||||
@@ -93,7 +133,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
</tr>
|
||||
|
||||
<tr id={this.props.category + "DescriptionRow"}
|
||||
className="small-description categoryTableDescription">
|
||||
className={`small-description categoryTableDescription${extraClasses}`}>
|
||||
<td
|
||||
colSpan={2}>
|
||||
{chrome.i18n.getMessage("category_" + this.props.category + "_description")}
|
||||
@@ -103,6 +143,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{this.getExtraOptionComponents(this.props.category, extraClasses, disabled)}
|
||||
|
||||
</>
|
||||
);
|
||||
@@ -111,10 +153,10 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
skipOptionSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
|
||||
let option: CategorySkipOption;
|
||||
|
||||
this.removeCurrentCategorySelection();
|
||||
|
||||
switch (event.target.value) {
|
||||
case "disable":
|
||||
case "disable":
|
||||
Config.config.categorySelections = Config.config.categorySelections.filter(
|
||||
categorySelection => categorySelection.name !== this.props.category);
|
||||
return;
|
||||
case "showOverlay":
|
||||
option = CategorySkipOption.ShowOverlay;
|
||||
@@ -130,35 +172,25 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
break;
|
||||
}
|
||||
|
||||
Config.config.categorySelections.push({
|
||||
name: this.props.category,
|
||||
option: option
|
||||
});
|
||||
|
||||
// Forces the Proxy to send this to the chrome storage API
|
||||
Config.config.categorySelections = Config.config.categorySelections;
|
||||
}
|
||||
|
||||
/** Removes this category from the config list of category selections */
|
||||
removeCurrentCategorySelection(): void {
|
||||
// Remove it if it exists
|
||||
for (let i = 0; i < Config.config.categorySelections.length; i++) {
|
||||
if (Config.config.categorySelections[i].name === this.props.category) {
|
||||
Config.config.categorySelections.splice(i, 1);
|
||||
|
||||
// Forces the Proxy to send this to the chrome storage API
|
||||
Config.config.categorySelections = Config.config.categorySelections;
|
||||
|
||||
break;
|
||||
}
|
||||
const existingSelection = Config.config.categorySelections.find(selection => selection.name === this.props.category);
|
||||
if (existingSelection) {
|
||||
existingSelection.option = option;
|
||||
} else {
|
||||
Config.config.categorySelections.push({
|
||||
name: this.props.category,
|
||||
option: option
|
||||
});
|
||||
}
|
||||
|
||||
Config.forceSyncUpdate("categorySelections");
|
||||
}
|
||||
|
||||
getCategorySkipOptions(): JSX.Element[] {
|
||||
const elements: JSX.Element[] = [];
|
||||
|
||||
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) {
|
||||
elements.push(
|
||||
@@ -195,6 +227,43 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
Config.config.barTypes = Config.config.barTypes;
|
||||
}, 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;
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Config from "../config";
|
||||
import { Keybind } from "../types";
|
||||
import Config from "../../config";
|
||||
import { Keybind } from "../../types";
|
||||
import KeybindDialogComponent from "./KeybindDialogComponent";
|
||||
import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils";
|
||||
import { keybindEquals, keybindToString, formatKey } from "../../utils/configUtils";
|
||||
|
||||
export interface KeybindProps {
|
||||
option: string;
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { ChangeEvent } from "react";
|
||||
import Config from "../config";
|
||||
import { Keybind } from "../types";
|
||||
import { keybindEquals, formatKey } from "../utils/configUtils";
|
||||
import Config from "../../config";
|
||||
import { Keybind } from "../../types";
|
||||
import { keybindEquals, formatKey } from "../../utils/configUtils";
|
||||
|
||||
export interface KeybindDialogProps {
|
||||
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 { keybindEquals } from "./utils/configUtils";
|
||||
|
||||
export interface Permission {
|
||||
canSubmit: boolean;
|
||||
}
|
||||
|
||||
interface SBConfig {
|
||||
userID: string,
|
||||
isVip: boolean,
|
||||
permissions: Record<Category, Permission>,
|
||||
/* Contains unsubmitted segments that the user has created. */
|
||||
unsubmittedSegments: Record<string, SponsorTime[]>,
|
||||
defaultCategory: Category,
|
||||
renderSegmentsAsChapters: boolean,
|
||||
whitelistedChannels: string[],
|
||||
forceChannelCheck: boolean,
|
||||
minutesSaved: number,
|
||||
@@ -44,6 +50,7 @@ interface SBConfig {
|
||||
allowExpirements: boolean,
|
||||
showDonationLink: boolean,
|
||||
showPopupDonationCount: number,
|
||||
showUpsells: boolean,
|
||||
donateClicked: number,
|
||||
autoHideInfoButton: boolean,
|
||||
autoSkipOnMusicVideos: boolean,
|
||||
@@ -56,6 +63,7 @@ interface SBConfig {
|
||||
categoryPillUpdate: boolean,
|
||||
darkMode: boolean,
|
||||
showCategoryGuidelines: boolean,
|
||||
chaptersAvailable: boolean,
|
||||
|
||||
// Used to cache calculated text color info
|
||||
categoryPillColors: {
|
||||
@@ -68,10 +76,19 @@ interface SBConfig {
|
||||
skipKeybind: Keybind,
|
||||
startSponsorKeybind: Keybind,
|
||||
submitKeybind: Keybind,
|
||||
nextChapterKeybind: Keybind,
|
||||
previousChapterKeybind: Keybind,
|
||||
|
||||
// What categories should be skipped
|
||||
categorySelections: CategorySelection[],
|
||||
|
||||
payments: {
|
||||
licenseKey: string,
|
||||
lastCheck: number,
|
||||
freeAccess: boolean,
|
||||
chaptersAllowed: boolean
|
||||
}
|
||||
|
||||
// Preview bar
|
||||
barTypes: {
|
||||
"preview-chooseACategory": PreviewBarOption,
|
||||
@@ -128,8 +145,10 @@ const Config: SBObject = {
|
||||
syncDefaults: {
|
||||
userID: null,
|
||||
isVip: false,
|
||||
permissions: {},
|
||||
unsubmittedSegments: {},
|
||||
defaultCategory: "chooseACategory" as Category,
|
||||
renderSegmentsAsChapters: false,
|
||||
whitelistedChannels: [],
|
||||
forceChannelCheck: false,
|
||||
minutesSaved: 0,
|
||||
@@ -165,6 +184,7 @@ const Config: SBObject = {
|
||||
allowExpirements: true,
|
||||
showDonationLink: true,
|
||||
showPopupDonationCount: 0,
|
||||
showUpsells: true,
|
||||
donateClicked: 0,
|
||||
autoHideInfoButton: true,
|
||||
autoSkipOnMusicVideos: false,
|
||||
@@ -172,6 +192,7 @@ const Config: SBObject = {
|
||||
categoryPillUpdate: false,
|
||||
darkMode: true,
|
||||
showCategoryGuidelines: true,
|
||||
chaptersAvailable: true,
|
||||
|
||||
categoryPillColors: {},
|
||||
|
||||
@@ -185,6 +206,8 @@ const Config: SBObject = {
|
||||
skipKeybind: {key: "Enter"},
|
||||
startSponsorKeybind: {key: ";"},
|
||||
submitKeybind: {key: "'"},
|
||||
nextChapterKeybind: {key: "]"},
|
||||
previousChapterKeybind: {key: "["},
|
||||
|
||||
categorySelections: [{
|
||||
name: "sponsor" as Category,
|
||||
@@ -197,6 +220,13 @@ const Config: SBObject = {
|
||||
option: CategorySkipOption.ShowOverlay
|
||||
}],
|
||||
|
||||
payments: {
|
||||
licenseKey: null,
|
||||
lastCheck: 0,
|
||||
freeAccess: false,
|
||||
chaptersAllowed: false
|
||||
},
|
||||
|
||||
colorPalette: {
|
||||
red: "#780303",
|
||||
white: "#ffffff",
|
||||
@@ -516,6 +546,8 @@ function migrateOldSyncFormats(config: SBConfig) {
|
||||
}
|
||||
|
||||
async function setupConfig() {
|
||||
if (typeof(chrome) === "undefined") return;
|
||||
|
||||
await fetchConfig();
|
||||
addDefaults();
|
||||
const config = configProxy();
|
||||
|
||||
610
src/content.ts
610
src/content.ts
File diff suppressed because it is too large
Load Diff
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';
|
||||
|
||||
import Config from "../config";
|
||||
import { ActionType } from "../types";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
import { ChapterVote } from "../render/ChapterVote";
|
||||
import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceType, SponsorTime } from "../types";
|
||||
import { partition } from "../utils/arrayUtils";
|
||||
import { shortCategoryName } from "../utils/categoryUtils";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
|
||||
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||
const MIN_CHAPTER_SIZE = 0.003;
|
||||
|
||||
export interface PreviewBarSegment {
|
||||
segment: [number, number];
|
||||
category: string;
|
||||
unsubmitted: boolean;
|
||||
category: Category;
|
||||
actionType: ActionType;
|
||||
unsubmitted: boolean;
|
||||
showLarger: boolean;
|
||||
description: string;
|
||||
source: SponsorSourceType;
|
||||
requiredSegment?: boolean;
|
||||
}
|
||||
|
||||
interface ChapterGroup extends SegmentContainer {
|
||||
originalDuration: number
|
||||
}
|
||||
|
||||
class PreviewBar {
|
||||
container: HTMLUListElement;
|
||||
categoryTooltip?: HTMLDivElement;
|
||||
tooltipContainer?: HTMLElement;
|
||||
categoryTooltipContainer?: HTMLElement;
|
||||
chapterTooltip?: HTMLDivElement;
|
||||
|
||||
parent: HTMLElement;
|
||||
onMobileYouTube: boolean;
|
||||
onInvidious: boolean;
|
||||
|
||||
segments: PreviewBarSegment[] = [];
|
||||
existingChapters: PreviewBarSegment[] = [];
|
||||
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.id = 'previewbar';
|
||||
|
||||
this.parent = parent;
|
||||
this.onMobileYouTube = onMobileYouTube;
|
||||
this.onInvidious = onInvidious;
|
||||
this.chapterVote = chapterVote;
|
||||
|
||||
this.createElement(parent);
|
||||
this.createChapterMutationObservers();
|
||||
|
||||
this.setupHoverText();
|
||||
}
|
||||
@@ -51,16 +74,19 @@ class PreviewBar {
|
||||
// Create label placeholder
|
||||
this.categoryTooltip = document.createElement("div");
|
||||
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");
|
||||
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
|
||||
|
||||
// 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");
|
||||
if (!this.tooltipContainer || !titleTooltip) return;
|
||||
if (!this.categoryTooltipContainer || !titleTooltip) return;
|
||||
|
||||
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
|
||||
tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
|
||||
|
||||
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
||||
if (!seekBar) return;
|
||||
@@ -76,7 +102,7 @@ class PreviewBar {
|
||||
});
|
||||
|
||||
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 (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
|
||||
@@ -93,7 +119,7 @@ class PreviewBar {
|
||||
const tooltipText = tooltipTextElement.textContent;
|
||||
if (tooltipText === null || tooltipText.length === 0) continue;
|
||||
|
||||
timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText);
|
||||
timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText);
|
||||
|
||||
if (timeInSeconds !== null) break;
|
||||
}
|
||||
@@ -101,36 +127,32 @@ class PreviewBar {
|
||||
if (timeInSeconds === null) return;
|
||||
|
||||
// Find the segment at that location, using the shortest if multiple found
|
||||
let segment: PreviewBarSegment | null = null;
|
||||
let currentSegmentLength = Infinity;
|
||||
|
||||
for (const seg of this.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;
|
||||
}
|
||||
}
|
||||
const [normalSegments, chapterSegments] =
|
||||
partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
|
||||
(segment) => segment.actionType !== ActionType.Chapter);
|
||||
let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments);
|
||||
let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments);
|
||||
if (mainSegment === null && secondarySegment !== null) {
|
||||
mainSegment = secondarySegment;
|
||||
secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
|
||||
}
|
||||
|
||||
if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
|
||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
} else if (segment !== null) {
|
||||
this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||
|
||||
if (segment.unsubmitted) {
|
||||
this.categoryTooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + utils.shortCategoryName(segment.category);
|
||||
if (mainSegment === null && secondarySegment === null) {
|
||||
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
} else {
|
||||
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||
if (mainSegment !== null && secondarySegment !== null) {
|
||||
this.categoryTooltipContainer.classList.add("sponsorTwoTooltips");
|
||||
} 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.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 {
|
||||
this.parent = parent;
|
||||
|
||||
@@ -148,7 +185,7 @@ class PreviewBar {
|
||||
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
|
||||
parent.style.opacity = "1";
|
||||
}
|
||||
|
||||
|
||||
this.container.style.transform = "none";
|
||||
} else if (!this.onInvidious) {
|
||||
// Hover listener
|
||||
@@ -157,41 +194,65 @@ class PreviewBar {
|
||||
this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// On the seek bar
|
||||
this.parent.prepend(this.container);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.videoDuration = 0;
|
||||
this.segments = [];
|
||||
|
||||
while (this.container.firstChild) {
|
||||
this.container.removeChild(this.container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (!segments) return;
|
||||
if (!this.segments) return;
|
||||
|
||||
this.segments = segments;
|
||||
this.videoDuration = videoDuration;
|
||||
this.originalChapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
|
||||
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
|
||||
return (b[1] - b[0]) - (a[1] - a[0]);
|
||||
}).forEach((segment) => {
|
||||
});
|
||||
for (const segment of sortedSegments) {
|
||||
const bar = this.createBar(segment);
|
||||
|
||||
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');
|
||||
bar.classList.add('previewbar');
|
||||
if (barSegment.requiredSegment) bar.classList.add("requiredSegment");
|
||||
bar.innerHTML = showLarger ? ' ' : ' ';
|
||||
|
||||
const fullCategoryName = (unsubmitted ? 'preview-' : '') + category;
|
||||
@@ -202,7 +263,9 @@ class PreviewBar {
|
||||
|
||||
bar.style.position = "absolute";
|
||||
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];
|
||||
bar.style.left = this.timeToPercentage(time);
|
||||
@@ -210,6 +273,413 @@ class PreviewBar {
|
||||
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 {
|
||||
this.container.remove();
|
||||
|
||||
@@ -218,14 +688,66 @@ class PreviewBar {
|
||||
this.categoryTooltip = undefined;
|
||||
}
|
||||
|
||||
if (this.tooltipContainer) {
|
||||
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
this.tooltipContainer = undefined;
|
||||
if (this.categoryTooltipContainer) {
|
||||
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
@@ -30,6 +30,11 @@ interface IsInfoFoundMessage {
|
||||
updating: boolean;
|
||||
}
|
||||
|
||||
interface SkipMessage {
|
||||
message: "unskip" | "reskip";
|
||||
UUID: SegmentUUID;
|
||||
}
|
||||
|
||||
interface SubmitVoteMessage {
|
||||
message: "submitVote";
|
||||
type: number;
|
||||
@@ -47,11 +52,30 @@ interface CopyToClipboardMessage {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage);
|
||||
interface ImportSegmentsMessage {
|
||||
message: "importSegments";
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface KeyDownMessage {
|
||||
message: "keydown";
|
||||
key: string;
|
||||
keyCode: number;
|
||||
code: string;
|
||||
which: number;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
altKey: boolean;
|
||||
metaKey: boolean;
|
||||
}
|
||||
|
||||
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage);
|
||||
|
||||
export interface IsInfoFoundMessageResponse {
|
||||
found: boolean;
|
||||
status: number;
|
||||
sponsorTimes: SponsorTime[];
|
||||
time: number;
|
||||
onMobileYouTube: boolean;
|
||||
}
|
||||
|
||||
@@ -77,11 +101,23 @@ export type MessageResponse =
|
||||
| GetChannelIDResponse
|
||||
| SponsorStartResponse
|
||||
| IsChannelWhitelistedResponse
|
||||
| Record<string, never>
|
||||
| VoteResponse;
|
||||
| Record<string, never> // empty object response {}
|
||||
| VoteResponse
|
||||
| ImportSegmentsResponse;
|
||||
|
||||
export interface VoteResponse {
|
||||
successType: number;
|
||||
statusCode: number;
|
||||
responseText: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ImportSegmentsResponse {
|
||||
importedSegments: SponsorTime[];
|
||||
}
|
||||
|
||||
export interface TimeUpdateMessage {
|
||||
message: "time";
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type PopupMessage = TimeUpdateMessage;
|
||||
|
||||
110
src/options.ts
110
src/options.ts
@@ -10,12 +10,17 @@ window.SB = Config;
|
||||
|
||||
import Utils from "./utils";
|
||||
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 { localizeHtmlPage } from "./utils/pageUtils";
|
||||
import { StorageChangesObject } from "./types";
|
||||
const utils = new Utils();
|
||||
let embed = false;
|
||||
|
||||
const categoryChoosers: CategoryChooser[] = [];
|
||||
const unsubmittedVideos: UnsubmittedVideos[] = [];
|
||||
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
async function init() {
|
||||
@@ -103,7 +108,7 @@ async function init() {
|
||||
// Add click listener
|
||||
checkbox.addEventListener("click", async () => {
|
||||
// Confirm if required
|
||||
if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked))
|
||||
if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked))
|
||||
&& !confirm(chrome.i18n.getMessage(confirmMessage))){
|
||||
checkbox.checked = !checkbox.checked;
|
||||
return;
|
||||
@@ -120,7 +125,7 @@ async function init() {
|
||||
if (!checkbox.checked) {
|
||||
// Enable the notice
|
||||
Config.config["dontShowNotice"] = false;
|
||||
|
||||
|
||||
const showNoticeSwitch = <HTMLInputElement> document.querySelector("[data-sync='dontShowNotice'] > div > label > input");
|
||||
showNoticeSwitch.checked = true;
|
||||
}
|
||||
@@ -162,7 +167,7 @@ async function init() {
|
||||
}
|
||||
case "text-change": {
|
||||
const textChangeInput = <HTMLInputElement> optionsElements[i].querySelector(".option-text-box");
|
||||
|
||||
|
||||
const textChangeSetButton = <HTMLElement> optionsElements[i].querySelector(".text-change-set");
|
||||
|
||||
textChangeInput.value = Config.config[option];
|
||||
@@ -290,7 +295,10 @@ async function init() {
|
||||
break;
|
||||
}
|
||||
case "react-CategoryChooserComponent":
|
||||
new CategoryChooser(optionsElements[i]);
|
||||
categoryChoosers.push(new CategoryChooser(optionsElements[i]));
|
||||
break;
|
||||
case "react-UnsubmittedVideosComponent":
|
||||
unsubmittedVideos.push(new UnsubmittedVideos(optionsElements[i]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -338,8 +346,8 @@ function createStickyHeader() {
|
||||
|
||||
/**
|
||||
* Handle special cases where an option shouldn't show
|
||||
*
|
||||
* @param {String} element
|
||||
*
|
||||
* @param {String} element
|
||||
*/
|
||||
async function shouldHideOption(element: Element): Promise<boolean> {
|
||||
return (element.getAttribute("data-private-only") === "true" && !(await isIncognitoAllowed()))
|
||||
@@ -348,10 +356,8 @@ async function shouldHideOption(element: Element): Promise<boolean> {
|
||||
|
||||
/**
|
||||
* Called when the config is updated
|
||||
*
|
||||
* @param {String} element
|
||||
*/
|
||||
function optionsConfigUpdateListener() {
|
||||
function optionsConfigUpdateListener(changes: StorageChangesObject) {
|
||||
const optionsContainer = document.getElementById("options");
|
||||
const optionsElements = optionsContainer.querySelectorAll("*");
|
||||
|
||||
@@ -359,14 +365,25 @@ function optionsConfigUpdateListener() {
|
||||
switch (optionsElements[i].getAttribute("data-type")) {
|
||||
case "display":
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will set display elements to the proper text
|
||||
*
|
||||
* @param element
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
function updateDisplayElement(element: HTMLElement) {
|
||||
const displayOption = element.getAttribute("data-sync")
|
||||
@@ -393,9 +410,9 @@ function updateDisplayElement(element: HTMLElement) {
|
||||
|
||||
/**
|
||||
* Initializes the option to add Invidious instances
|
||||
*
|
||||
* @param element
|
||||
* @param option
|
||||
*
|
||||
* @param element
|
||||
* @param option
|
||||
*/
|
||||
function invidiousInstanceAddInit(element: HTMLElement, option: string) {
|
||||
const textBox = <HTMLInputElement> element.querySelector(".option-text-box");
|
||||
@@ -447,18 +464,12 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
|
||||
|
||||
/**
|
||||
* Run when the invidious button is being initialized
|
||||
*
|
||||
* @param checkbox
|
||||
* @param option
|
||||
*
|
||||
* @param checkbox
|
||||
* @param option
|
||||
*/
|
||||
function invidiousInit(checkbox: HTMLInputElement, option: string) {
|
||||
let permissions = ["declarativeContent"];
|
||||
if (utils.isFirefox()) permissions = [];
|
||||
|
||||
chrome.permissions.contains({
|
||||
origins: utils.getPermissionRegex(),
|
||||
permissions: permissions
|
||||
}, function (result) {
|
||||
utils.containsInvidiousPermission().then((result) => {
|
||||
if (result != checkbox.checked) {
|
||||
Config.config[option] = result;
|
||||
|
||||
@@ -469,33 +480,19 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
|
||||
|
||||
/**
|
||||
* Run whenever the invidious checkbox is clicked
|
||||
*
|
||||
* @param checkbox
|
||||
* @param option
|
||||
*
|
||||
* @param checkbox
|
||||
* @param option
|
||||
*/
|
||||
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (checkbox.checked) {
|
||||
utils.setupExtraSitePermissions(function (granted) {
|
||||
if (!granted) {
|
||||
Config.config[option] = false;
|
||||
checkbox.checked = false;
|
||||
} else {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
utils.removeExtraSiteRegistration();
|
||||
}
|
||||
});
|
||||
const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option);
|
||||
checkbox.checked = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will trigger the textbox to appear to be able to change an option's text.
|
||||
*
|
||||
* @param element
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
function activatePrivateTextChange(element: HTMLElement) {
|
||||
const button = element.querySelector(".trigger-button");
|
||||
@@ -512,7 +509,7 @@ function activatePrivateTextChange(element: HTMLElement) {
|
||||
element.querySelector(".option-hidden-section").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let result = Config.config[option];
|
||||
// See if anything extra must be done
|
||||
switch (option) {
|
||||
@@ -523,7 +520,7 @@ function activatePrivateTextChange(element: HTMLElement) {
|
||||
}
|
||||
|
||||
textBox.value = result;
|
||||
|
||||
|
||||
const setButton = element.querySelector(".text-change-set");
|
||||
setButton.addEventListener("click", async () => {
|
||||
setTextOption(option, element, textBox.value);
|
||||
@@ -552,7 +549,7 @@ function activatePrivateTextChange(element: HTMLElement) {
|
||||
|
||||
/**
|
||||
* Function to run when a textbox change is submitted
|
||||
*
|
||||
*
|
||||
* @param option data-sync value
|
||||
* @param element main container div
|
||||
* @param value new text
|
||||
@@ -562,7 +559,7 @@ async function setTextOption(option: string, element: HTMLElement, value: string
|
||||
const confirmMessage = element.getAttribute("data-confirm-message");
|
||||
|
||||
if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
|
||||
|
||||
|
||||
// See if anything extra must be done
|
||||
switch (option) {
|
||||
case "*":
|
||||
@@ -574,13 +571,13 @@ async function setTextOption(option: string, element: HTMLElement, value: string
|
||||
|
||||
if (newConfig.supportInvidious) {
|
||||
const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > div > label > input");
|
||||
|
||||
|
||||
checkbox.checked = true;
|
||||
await invidiousOnClick(checkbox, "supportInvidious");
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
|
||||
|
||||
} catch (e) {
|
||||
alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
|
||||
}
|
||||
@@ -598,8 +595,9 @@ async function setTextOption(option: string, element: HTMLElement, value: string
|
||||
function downloadConfig() {
|
||||
const file = document.createElement("a");
|
||||
const jsonData = JSON.parse(JSON.stringify(Config.cachedSyncConfig));
|
||||
file.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData)));
|
||||
file.setAttribute("download", "SponsorBlockConfig.json");
|
||||
const dateTimeString = new Date().toJSON().replace("T", "_").replace(/:/g, ".").replace(/.\d+Z/g, "")
|
||||
file.setAttribute("href", `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(jsonData))}`);
|
||||
file.setAttribute("download", `SponsorBlockConfig_${dateTimeString}.json`);
|
||||
document.body.append(file);
|
||||
file.click();
|
||||
file.remove();
|
||||
@@ -622,7 +620,7 @@ function uploadConfig(e) {
|
||||
/**
|
||||
* Validates the value used for the database server address.
|
||||
* Returns null and alerts the user if there is an issue.
|
||||
*
|
||||
*
|
||||
* @param input Input server address
|
||||
*/
|
||||
function validateServerAddress(input: string): string {
|
||||
@@ -656,7 +654,7 @@ function copyDebugOutputToClipboard() {
|
||||
|
||||
// Sanitise sensitive user config values
|
||||
delete output.config.userID;
|
||||
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
|
||||
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
|
||||
? "Default server address" : "Custom server address";
|
||||
output.config.invidiousInstances = output.config.invidiousInstances.length;
|
||||
output.config.whitelistedChannels = output.config.whitelistedChannels.length;
|
||||
|
||||
@@ -12,25 +12,17 @@ window.addEventListener('DOMContentLoaded', init);
|
||||
async function init() {
|
||||
localizeHtmlPage();
|
||||
|
||||
const domains = document.location.hash.replace("#", "").split(",");
|
||||
|
||||
const acceptButton = document.getElementById("acceptPermissionButton");
|
||||
acceptButton.addEventListener("click", () => {
|
||||
chrome.permissions.request({
|
||||
origins: utils.getPermissionRegex(domains),
|
||||
permissions: []
|
||||
}, (granted) => {
|
||||
if (granted) {
|
||||
utils.applyInvidiousPermissions(Config.config.supportInvidious).then((enabled) => {
|
||||
Config.config.supportInvidious = enabled;
|
||||
|
||||
if (enabled) {
|
||||
alert(chrome.i18n.getMessage("permissionRequestSuccess"));
|
||||
|
||||
Config.config.ytInfoPermissionGranted = true;
|
||||
|
||||
chrome.tabs.getCurrent((tab) => {
|
||||
chrome.tabs.remove(tab.id);
|
||||
});
|
||||
window.close();
|
||||
} else {
|
||||
alert(chrome.i18n.getMessage("permissionRequestFailed"));
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
527
src/popup.ts
527
src/popup.ts
@@ -1,12 +1,15 @@
|
||||
import Config from "./config";
|
||||
|
||||
import Utils from "./utils";
|
||||
import { SponsorTime, SponsorHideType, ActionType } from "./types";
|
||||
import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes";
|
||||
import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject } from "./types";
|
||||
import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
|
||||
import { showDonationLink } from "./utils/configUtils";
|
||||
import { AnimationUtils } from "./utils/animationUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
import { shortCategoryName } from "./utils/categoryUtils";
|
||||
import { localizeHtmlPage } from "./utils/pageUtils";
|
||||
import { exportTimes } from "./utils/exporter";
|
||||
import GenericNotice from "./render/GenericNotice";
|
||||
const utils = new Utils();
|
||||
|
||||
interface MessageListener {
|
||||
@@ -68,13 +71,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
|
||||
//the start and end time pairs (2d)
|
||||
let sponsorTimes: SponsorTime[] = [];
|
||||
let downloadedTimes: SponsorTime[] = [];
|
||||
|
||||
//current video ID of this tab
|
||||
let currentVideoID = null;
|
||||
|
||||
enum SegmentTab {
|
||||
Segments,
|
||||
Chapters
|
||||
}
|
||||
let segmentTab = SegmentTab.Segments;
|
||||
let port: chrome.runtime.Port = null;
|
||||
|
||||
const PageElements: PageElements = {};
|
||||
|
||||
[
|
||||
"sponsorBlockPopupBody",
|
||||
"sponsorblockPopup",
|
||||
"sponsorStart",
|
||||
// Top toggles
|
||||
@@ -123,16 +135,29 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
"refreshSegmentsButton",
|
||||
"whitelistButton",
|
||||
"sbDonate",
|
||||
"issueReporterTabs",
|
||||
"issueReporterTabSegments",
|
||||
"issueReporterTabChapters",
|
||||
"sponsorTimesDonateContainer",
|
||||
"sbConsiderDonateLink",
|
||||
"sbCloseDonate",
|
||||
"sbBetaServerWarning",
|
||||
"sbCloseButton"
|
||||
"sbCloseButton",
|
||||
"issueReporterImportExport",
|
||||
"importSegmentsButton",
|
||||
"exportSegmentsButton",
|
||||
"importSegmentsMenu",
|
||||
"importSegmentsText",
|
||||
"importSegmentsSubmit"
|
||||
|
||||
].forEach(id => PageElements[id] = document.getElementById(id));
|
||||
|
||||
getSegmentsFromContentScript(false);
|
||||
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)) {
|
||||
Config.configSyncListeners.push(contentConfigUpdateListener);
|
||||
}
|
||||
|
||||
PageElements.sbCloseButton.addEventListener("click", () => {
|
||||
sendTabMessage({
|
||||
@@ -142,6 +167,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
|
||||
if (window !== window.top) {
|
||||
PageElements.sbCloseButton.classList.remove("hidden");
|
||||
PageElements.sponsorBlockPopupBody.classList.add("is-embedded");
|
||||
}
|
||||
|
||||
// Hide donate button if wanted (Safari, or user choice)
|
||||
@@ -157,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.whitelistToggle.addEventListener("change", function () {
|
||||
if (this.checked) {
|
||||
@@ -180,6 +210,38 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
PageElements.refreshSegmentsButton.addEventListener("click", refreshSegments);
|
||||
PageElements.sbPopupIconCopyUserID.addEventListener("click", async () => copyToClipboard(await utils.getHash(Config.config.userID)));
|
||||
|
||||
// Forward click events
|
||||
if (window !== window.top) {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === "INPUT"
|
||||
|| target.tagName === "TEXTAREA"
|
||||
|| e.key === "ArrowUp"
|
||||
|| e.key === "ArrowDown") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === " ") {
|
||||
// No scrolling
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
sendTabMessage({
|
||||
message: "keydown",
|
||||
key: e.key,
|
||||
keyCode: e.keyCode,
|
||||
code: e.code,
|
||||
which: e.which,
|
||||
shiftKey: e.shiftKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
altKey: e.altKey,
|
||||
metaKey: e.metaKey
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupComPort();
|
||||
|
||||
//show proper disable skipping button
|
||||
const disableSkipping = Config.config.disableSkipping;
|
||||
if (disableSkipping != undefined && disableSkipping) {
|
||||
@@ -195,7 +257,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
PageElements.showNoticeAgain.style.display = "unset";
|
||||
}
|
||||
|
||||
utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&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) {
|
||||
const userInfo = JSON.parse(res.responseText);
|
||||
PageElements.usernameValue.innerText = userInfo.userName;
|
||||
@@ -222,8 +285,16 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
}
|
||||
PageElements.sponsorTimesOthersTimeSavedDisplay.innerText = getFormattedHours(minutesSaved);
|
||||
}
|
||||
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -259,6 +330,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
// Must be delayed so it only happens once loaded
|
||||
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) {
|
||||
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
|
||||
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) {
|
||||
@@ -330,12 +417,17 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
PageElements.whitelistButton.classList.remove("hidden");
|
||||
PageElements.loadingIndicator.style.display = "none";
|
||||
|
||||
downloadedTimes = request.sponsorTimes ?? [];
|
||||
if (request.found) {
|
||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
|
||||
|
||||
displayDownloadedSponsorTimes(request);
|
||||
} else {
|
||||
if (request.sponsorTimes) {
|
||||
displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
|
||||
}
|
||||
} else if (request.status == 404 || request.status == 200) {
|
||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
|
||||
} else {
|
||||
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,165 +496,205 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
}
|
||||
|
||||
//display the video times from the array at the top, in a different section
|
||||
function displayDownloadedSponsorTimes(request: { found: boolean, sponsorTimes: SponsorTime[] }) {
|
||||
if (request.sponsorTimes != undefined) {
|
||||
// Sort list by start time
|
||||
const segmentTimes = request.sponsorTimes
|
||||
.sort((a, b) => a.segment[1] - b.segment[1])
|
||||
.sort((a, b) => a.segment[0] - b.segment[0]);
|
||||
function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
|
||||
let currentSegmentTab = segmentTab;
|
||||
if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter)) {
|
||||
PageElements.issueReporterTabs.classList.add("hidden");
|
||||
currentSegmentTab = SegmentTab.Segments;
|
||||
} else {
|
||||
PageElements.issueReporterTabs.classList.remove("hidden");
|
||||
}
|
||||
|
||||
//add them as buttons to the issue reporting container
|
||||
const container = document.getElementById("issueReporterTimeButtons");
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
// Sort list by start time
|
||||
const downloadedTimes = sponsorTimes
|
||||
.filter((segment) => {
|
||||
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;
|
||||
for (let i = 0; i < segmentTimes.length; i++) {
|
||||
const UUID = segmentTimes[i].UUID;
|
||||
const locked = segmentTimes[i].locked;
|
||||
const categoryColorCircle = document.createElement("span");
|
||||
categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
|
||||
categoryColorCircle.style.backgroundColor = Config.config.barTypes[category]?.color;
|
||||
categoryColorCircle.classList.add("dot");
|
||||
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
|
||||
|
||||
const segmentSummary = document.createElement("summary");
|
||||
segmentSummary.className = "segmentSummary";
|
||||
let extraInfo = "";
|
||||
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");
|
||||
categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
|
||||
categoryColorCircle.style.backgroundColor = Config.config.barTypes[segmentTimes[i].category]?.color;
|
||||
categoryColorCircle.classList.add("dot");
|
||||
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
|
||||
const name = downloadedTimes[i].description || shortCategoryName(category);
|
||||
const textNode = document.createTextNode(name + extraInfo);
|
||||
const segmentTimeFromToNode = document.createElement("div");
|
||||
if (downloadedTimes[i].actionType === ActionType.Full) {
|
||||
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 = "";
|
||||
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") + ")";
|
||||
}
|
||||
segmentTimeFromToNode.style.margin = "5px";
|
||||
|
||||
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo);
|
||||
const segmentTimeFromToNode = document.createElement("div");
|
||||
if (segmentTimes[i].actionType === ActionType.Full) {
|
||||
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)
|
||||
: "");
|
||||
}
|
||||
// for inline-styling purposes
|
||||
const labelContainer = document.createElement("div");
|
||||
if (actionType !== ActionType.Chapter) labelContainer.appendChild(categoryColorCircle);
|
||||
|
||||
segmentTimeFromToNode.style.margin = "5px";
|
||||
|
||||
// for inline-styling purposes
|
||||
const labelContainer = document.createElement("div");
|
||||
labelContainer.appendChild(categoryColorCircle);
|
||||
const span = document.createElement('span');
|
||||
span.className = "summaryLabel";
|
||||
span.appendChild(textNode);
|
||||
labelContainer.appendChild(span);
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = "summaryLabel";
|
||||
span.appendChild(textNode);
|
||||
labelContainer.appendChild(span);
|
||||
// for inline-styling purposes
|
||||
segmentSummary.appendChild(labelContainer);
|
||||
segmentSummary.appendChild(segmentTimeFromToNode);
|
||||
|
||||
segmentSummary.appendChild(labelContainer);
|
||||
segmentSummary.appendChild(segmentTimeFromToNode);
|
||||
const votingButtons = document.createElement("details");
|
||||
votingButtons.classList.add("votingButtons");
|
||||
|
||||
const votingButtons = document.createElement("details");
|
||||
votingButtons.classList.add("votingButtons");
|
||||
//thumbs up and down buttons
|
||||
const voteButtonsContainer = document.createElement("div");
|
||||
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
|
||||
voteButtonsContainer.classList.add("sbVoteButtonsContainer");
|
||||
|
||||
//thumbs up and down buttons
|
||||
const voteButtonsContainer = document.createElement("div");
|
||||
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
|
||||
voteButtonsContainer.classList.add("sbVoteButtonsContainer");
|
||||
const upvoteButton = document.createElement("img");
|
||||
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));
|
||||
|
||||
const upvoteButton = document.createElement("img");
|
||||
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));
|
||||
const downvoteButton = document.createElement("img");
|
||||
downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
|
||||
downvoteButton.className = "voteButton";
|
||||
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 downvoteButton = document.createElement("img");
|
||||
downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
|
||||
downvoteButton.className = "voteButton";
|
||||
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 uuidButton = document.createElement("img");
|
||||
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 uuidButton = document.createElement("img");
|
||||
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 (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
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 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 {
|
||||
if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
||||
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) {
|
||||
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
||||
segmentTimes[i].hidden = SponsorHideType.Visible;
|
||||
} else {
|
||||
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
||||
segmentTimes[i].hidden = SponsorHideType.Hidden;
|
||||
}
|
||||
|
||||
messageHandler.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
}, tabs => {
|
||||
messageHandler.sendMessage(
|
||||
tabs[0].id,
|
||||
{
|
||||
message: "hideSegment",
|
||||
type: segmentTimes[i].hidden,
|
||||
UUID: UUID
|
||||
}
|
||||
);
|
||||
});
|
||||
messageHandler.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
}, tabs => {
|
||||
messageHandler.sendMessage(
|
||||
tabs[0].id,
|
||||
{
|
||||
message: "hideSegment",
|
||||
type: downloadedTimes[i].hidden,
|
||||
UUID: UUID
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
//add thumbs up, thumbs down and uuid copy buttons to the container
|
||||
voteButtonsContainer.appendChild(upvoteButton);
|
||||
voteButtonsContainer.appendChild(downvoteButton);
|
||||
voteButtonsContainer.appendChild(uuidButton);
|
||||
if (segmentTimes[i].actionType === ActionType.Skip
|
||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segmentTimes[i].hidden)) {
|
||||
voteButtonsContainer.appendChild(hideButton);
|
||||
}
|
||||
const skipButton = document.createElement("img");
|
||||
skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
|
||||
skipButton.className = "voteButton";
|
||||
skipButton.src = chrome.runtime.getURL("icons/skip.svg");
|
||||
skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
|
||||
votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
|
||||
|
||||
// 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);
|
||||
//add thumbs up, thumbs down and uuid copy buttons to the container
|
||||
voteButtonsContainer.appendChild(upvoteButton);
|
||||
voteButtonsContainer.appendChild(downvoteButton);
|
||||
voteButtonsContainer.appendChild(uuidButton);
|
||||
if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
|
||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
|
||||
voteButtonsContainer.appendChild(hideButton);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,6 +803,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
//this is not a YouTube video page
|
||||
function displayNoVideo() {
|
||||
document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID");
|
||||
|
||||
PageElements.issueReporterTabs.classList.add("hidden");
|
||||
}
|
||||
|
||||
function addVoteMessage(message, UUID) {
|
||||
@@ -684,6 +818,17 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
thanksForVotingText.innerText = message;
|
||||
}
|
||||
|
||||
function removeVoteMessage(UUID) {
|
||||
const voteButtonsContainer = document.getElementById("sponsorTimesVoteButtonsContainer" + UUID);
|
||||
voteButtonsContainer.style.display = "block";
|
||||
|
||||
const voteStatusContainer = document.getElementById("sponsorTimesVoteStatusContainer" + UUID);
|
||||
voteStatusContainer.style.display = "none";
|
||||
|
||||
const thanksForVotingText = document.getElementById("sponsorTimesThanksForVotingText" + UUID);
|
||||
thanksForVotingText.removeAttribute("innerText");
|
||||
}
|
||||
|
||||
function vote(type, UUID) {
|
||||
//add loading info
|
||||
addVoteMessage(chrome.i18n.getMessage("Loading"), UUID);
|
||||
@@ -707,6 +852,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
} else if (response.successType == -1) {
|
||||
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
|
||||
}
|
||||
setTimeout(() => removeVoteMessage(UUID), 1500);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -832,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)
|
||||
*/
|
||||
@@ -861,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
|
||||
* If less than 1 hour, just returns minutes
|
||||
@@ -875,7 +1087,30 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
return (days > 0 ? days + chrome.i18n.getMessage("dayAbbreviation") + " " : "") + (hours > 0 ? hours + chrome.i18n.getMessage("hourAbbreviation") + " " : "") + (minutes % 60).toFixed(1);
|
||||
}
|
||||
|
||||
//end of function
|
||||
function contentConfigUpdateListener(changes: StorageChangesObject) {
|
||||
for (const key in changes) {
|
||||
switch(key) {
|
||||
case "unsubmittedSegments":
|
||||
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
|
||||
updateSegmentEditingUI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import CategoryChooserComponent from "../components/CategoryChooserComponent";
|
||||
import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
|
||||
|
||||
class CategoryChooser {
|
||||
|
||||
ref: React.RefObject<CategoryChooserComponent>;
|
||||
|
||||
constructor(element: Element) {
|
||||
this.ref = React.createRef();
|
||||
|
||||
ReactDOM.render(
|
||||
<CategoryChooserComponent/>,
|
||||
<CategoryChooserComponent ref={this.ref} />,
|
||||
element
|
||||
);
|
||||
}
|
||||
|
||||
update(): void {
|
||||
this.ref.current?.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
const utils = new Utils();
|
||||
|
||||
import { ContentContainer } from "../types";
|
||||
import { ButtonListener, ContentContainer } from "../types";
|
||||
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
|
||||
|
||||
export interface ButtonListener {
|
||||
name: string,
|
||||
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
}
|
||||
|
||||
export interface TextBox {
|
||||
icon: string,
|
||||
text: string
|
||||
@@ -20,12 +15,17 @@ export interface TextBox {
|
||||
|
||||
export interface NoticeOptions {
|
||||
title: string,
|
||||
referenceNode?: HTMLElement,
|
||||
textBoxes?: TextBox[],
|
||||
buttons?: ButtonListener[],
|
||||
fadeIn?: boolean,
|
||||
timed?: boolean
|
||||
style?: React.CSSProperties;
|
||||
extraClass?: string;
|
||||
maxCountdownTime?: () => number;
|
||||
dontPauseCountdown?: boolean;
|
||||
hideLogo?: boolean;
|
||||
hideRightInfo?: boolean;
|
||||
}
|
||||
|
||||
export default class GenericNotice {
|
||||
@@ -42,7 +42,7 @@ export default class GenericNotice {
|
||||
|
||||
this.contentContainer = contentContainer;
|
||||
|
||||
const referenceNode = utils.findReferenceNode();
|
||||
const referenceNode = options.referenceNode ?? utils.findReferenceNode();
|
||||
|
||||
this.noticeElement = document.createElement("div");
|
||||
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
|
||||
@@ -62,9 +62,19 @@ export default class GenericNotice {
|
||||
ref={this.noticeRef}
|
||||
style={options.style}
|
||||
extraClass={options.extraClass}
|
||||
maxCountdownTime={options.maxCountdownTime}
|
||||
dontPauseCountdown={options.dontPauseCountdown}
|
||||
hideLogo={options.hideLogo}
|
||||
hideRightInfo={options.hideRightInfo}
|
||||
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}
|
||||
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) {
|
||||
const result = [];
|
||||
for (let i = 0; i < textBoxes.length; i++) {
|
||||
|
||||
@@ -33,8 +33,8 @@ export class RectangleTooltip {
|
||||
props.fontSize ??= "10px";
|
||||
|
||||
this.container = document.createElement('div');
|
||||
props.htmlId ??= props.text;
|
||||
this.container.id = "sponsorRectangleTooltip" + props.htmlId;
|
||||
props.htmlId ??= "sponsorRectangleTooltip" + props.text;
|
||||
this.container.id = props.htmlId;
|
||||
this.container.style.display = "relative";
|
||||
|
||||
if (props.prependElement) {
|
||||
|
||||
@@ -35,7 +35,7 @@ class SubmissionNotice {
|
||||
contentContainer={contentContainer}
|
||||
callback={callback}
|
||||
ref={this.noticeRef}
|
||||
closeListener={() => this.close()} />,
|
||||
closeListener={() => this.close(false)} />,
|
||||
this.noticeElement
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,8 @@ class SubmissionNotice {
|
||||
this.noticeRef.current.forceUpdate();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
close(callRef = true): void {
|
||||
if (callRef) this.noticeRef.current.cancel();
|
||||
ReactDOM.unmountComponentAtNode(this.noticeElement);
|
||||
|
||||
this.noticeElement.remove();
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { ButtonListener } from "../types";
|
||||
|
||||
export interface TooltipProps {
|
||||
text: string,
|
||||
link?: string,
|
||||
referenceNode: HTMLElement,
|
||||
prependElement?: HTMLElement, // Element to append before
|
||||
bottomOffset?: string
|
||||
text?: string;
|
||||
link?: string;
|
||||
referenceNode: HTMLElement;
|
||||
prependElement?: HTMLElement; // Element to append before
|
||||
bottomOffset?: string;
|
||||
leftOffset?: string;
|
||||
rightOffset?: string;
|
||||
timeout?: number;
|
||||
opacity?: number;
|
||||
displayTriangle?: boolean;
|
||||
extraClass?: string;
|
||||
showLogo?: boolean;
|
||||
showGotIt?: boolean;
|
||||
buttons?: ButtonListener[];
|
||||
}
|
||||
|
||||
export class Tooltip {
|
||||
text: string;
|
||||
text?: string;
|
||||
container: HTMLDivElement;
|
||||
|
||||
timer: NodeJS.Timeout;
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
props.bottomOffset ??= "70px";
|
||||
props.leftOffset ??= "inherit";
|
||||
props.rightOffset ??= "inherit";
|
||||
props.opacity ??= 0.7;
|
||||
props.displayTriangle ??= true;
|
||||
props.extraClass ??= "";
|
||||
props.showLogo ??= true;
|
||||
props.showGotIt ??= true;
|
||||
this.text = props.text;
|
||||
@@ -45,25 +53,29 @@ export class Tooltip {
|
||||
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
|
||||
|
||||
ReactDOM.render(
|
||||
<div style={{bottom: props.bottomOffset, backgroundColor}}
|
||||
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "")} >
|
||||
<div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
|
||||
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
|
||||
<div>
|
||||
{props.showLogo ?
|
||||
<img className="sponsorSkipLogo sponsorSkipObject"
|
||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||
</img>
|
||||
: null}
|
||||
<span className="sponsorSkipObject">
|
||||
{this.text + (props.link ? ". " : "")}
|
||||
{props.link ?
|
||||
<a style={{textDecoration: "underline"}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={props.link}>
|
||||
{chrome.i18n.getMessage("LearnMore")}
|
||||
</a>
|
||||
: null}
|
||||
</span>
|
||||
{this.text ?
|
||||
<span className="sponsorSkipObject">
|
||||
{this.text + (props.link ? ". " : "")}
|
||||
{props.link ?
|
||||
<a style={{textDecoration: "underline"}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={props.link}>
|
||||
{chrome.i18n.getMessage("LearnMore")}
|
||||
</a>
|
||||
: null}
|
||||
</span>
|
||||
: null}
|
||||
|
||||
{this.getButtons(props.buttons)}
|
||||
</div>
|
||||
{props.showGotIt ?
|
||||
<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 {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
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";
|
||||
|
||||
const thumbsDownSvg = ({
|
||||
fill = "#ffffff"
|
||||
fill = "#ffffff",
|
||||
className = "",
|
||||
width = "18",
|
||||
height = "18"
|
||||
}): JSX.Element => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import * as React from "react";
|
||||
|
||||
const thumbsUpSvg = ({
|
||||
fill = "#ffffff"
|
||||
fill = "#ffffff",
|
||||
className = "",
|
||||
width = "18",
|
||||
height = "18"
|
||||
}): JSX.Element => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
fill={fill}
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
|
||||
30
src/types.ts
30
src/types.ts
@@ -16,12 +16,13 @@ export interface ContentContainer {
|
||||
updatePreviewBar: () => void,
|
||||
onMobileYouTube: boolean,
|
||||
sponsorSubmissionNotice: SubmissionNotice,
|
||||
resetSponsorSubmissionNotice: () => void,
|
||||
resetSponsorSubmissionNotice: (callRef?: boolean) => void,
|
||||
updateEditButtonsOnPlayer: () => void,
|
||||
previewTime: (time: number, unpause?: boolean) => void,
|
||||
videoInfo: VideoInfo,
|
||||
getRealCurrentTime: () => number,
|
||||
lockedCategories: string[]
|
||||
lockedCategories: string[],
|
||||
channelIDInfo: ChannelIDInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ export enum SponsorHideType {
|
||||
export enum ActionType {
|
||||
Skip = "skip",
|
||||
Mute = "mute",
|
||||
Chapter = "chapter",
|
||||
Full = "full",
|
||||
Poi = "poi"
|
||||
}
|
||||
@@ -69,19 +71,24 @@ export type Category = string & { __categoryBrand: unknown };
|
||||
|
||||
export enum SponsorSourceType {
|
||||
Server = undefined,
|
||||
Local = 1
|
||||
Local = 1,
|
||||
YouTube = 2
|
||||
}
|
||||
|
||||
export interface SponsorTime {
|
||||
export interface SegmentContainer {
|
||||
segment: [number] | [number, number];
|
||||
}
|
||||
|
||||
export interface SponsorTime extends SegmentContainer {
|
||||
UUID: SegmentUUID;
|
||||
locked?: number;
|
||||
|
||||
category: Category;
|
||||
actionType: ActionType;
|
||||
description?: string;
|
||||
|
||||
hidden?: SponsorHideType;
|
||||
source?: SponsorSourceType;
|
||||
source: SponsorSourceType;
|
||||
videoDuration?: number;
|
||||
}
|
||||
|
||||
@@ -230,4 +237,17 @@ export type Keybind = {
|
||||
ctrl?: boolean,
|
||||
alt?: 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 * as CompileConfig from "../config.json";
|
||||
import { findValidElementFromSelector } from "./utils/pageUtils";
|
||||
import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
|
||||
export default class Utils {
|
||||
@@ -22,52 +22,79 @@ export default class Utils {
|
||||
];
|
||||
|
||||
/* Used for waitForElement */
|
||||
waitingMutationObserver:MutationObserver = null;
|
||||
waitingElements: { selector: string, callback: (element: Element) => void }[] = [];
|
||||
creatingWaitingMutationObserver = false;
|
||||
waitingMutationObserver: MutationObserver = null;
|
||||
waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = [];
|
||||
|
||||
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
|
||||
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);
|
||||
}
|
||||
|
||||
/* 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) => {
|
||||
const initialElement = this.getElement(selector, visibleCheck);
|
||||
if (initialElement) {
|
||||
resolve(initialElement);
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitingElements.push({
|
||||
selector,
|
||||
visibleCheck,
|
||||
callback: resolve
|
||||
});
|
||||
|
||||
if (!this.waitingMutationObserver) {
|
||||
this.waitingMutationObserver = new MutationObserver(() => {
|
||||
const foundSelectors = [];
|
||||
for (const { selector, callback } of this.waitingElements) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
callback(element);
|
||||
foundSelectors.push(selector);
|
||||
}
|
||||
}
|
||||
if (!this.creatingWaitingMutationObserver) {
|
||||
this.creatingWaitingMutationObserver = true;
|
||||
|
||||
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
|
||||
|
||||
if (this.waitingElements.length === 0) {
|
||||
this.waitingMutationObserver.disconnect();
|
||||
this.waitingMutationObserver = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.waitingMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
if (document.body) {
|
||||
this.setupWaitingMutationListener();
|
||||
} else {
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
this.setupWaitingMutationListener();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
return new Promise((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.
|
||||
*/
|
||||
@@ -273,24 +331,6 @@ export default class Utils {
|
||||
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
|
||||
*
|
||||
@@ -376,67 +416,6 @@ export default class Utils {
|
||||
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 {
|
||||
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";
|
||||
} else if (category === "exclusive_access") {
|
||||
return "_full";
|
||||
} else if (category === "chapter") {
|
||||
return "_chapter";
|
||||
} else {
|
||||
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",
|
||||
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. */
|
||||
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) => {
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
@@ -8,7 +8,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
||||
|
||||
const intervalCheck = () => {
|
||||
const result = condition();
|
||||
if (result) {
|
||||
if (predicate ? predicate(result) : result) {
|
||||
resolve(result);
|
||||
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
|
||||
*
|
||||
@@ -28,7 +72,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
|
||||
* @returns {string} errorMessage
|
||||
*/
|
||||
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
|
||||
if([400, 429, 409, 0].includes(statusCode)) {
|
||||
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode + postFix;
|
||||
@@ -64,8 +108,52 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} {
|
||||
} : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all indexes that have the specified value
|
||||
* https://stackoverflow.com/a/54954694/1985387
|
||||
*/
|
||||
function indexesOf<T>(array: T[], value: T): number[] {
|
||||
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 = {
|
||||
wait,
|
||||
getFormattedTime,
|
||||
getFormattedTimeToSeconds,
|
||||
getErrorMessage,
|
||||
getLuminance
|
||||
getLuminance,
|
||||
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 = [
|
||||
// YouTube
|
||||
".ytp-right-controls",
|
||||
@@ -16,7 +19,7 @@ export function getControls(): HTMLElement | false {
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isVisible(element: HTMLElement): boolean {
|
||||
@@ -63,6 +66,44 @@ export function getHashParams(): Record<string, unknown> {
|
||||
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 {
|
||||
//Localize by replacing __MSG_***__ meta tags
|
||||
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
|
||||
}
|
||||
]);
|
||||
});
|
||||
})
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Builder, By, until, WebDriver } from "selenium-webdriver";
|
||||
import { Builder, By, until, WebDriver, WebElement } from "selenium-webdriver";
|
||||
import * as Chrome from "selenium-webdriver/chrome";
|
||||
import * as Path from "path";
|
||||
|
||||
import * as fs from "fs";
|
||||
|
||||
test("Selenium Chrome test", async () => {
|
||||
let driver;
|
||||
let driver: WebDriver;
|
||||
try {
|
||||
driver = await setup();
|
||||
} catch (e) {
|
||||
@@ -27,6 +29,22 @@ test("Selenium Chrome test", async () => {
|
||||
await setSegmentActionType(driver, 0, 1, false);
|
||||
await editSegments(driver, 0, "0:05.000", "0:13.211", "5", "7.5", "0:05.000 to 0:07.500", false);
|
||||
await muteSkipSegment(driver, 5, 7.5);
|
||||
|
||||
// Full video
|
||||
await setSegmentActionType(driver, 0, 2, false);
|
||||
await driver.wait(until.elementIsNotVisible(await getDisplayTimeBox(driver, 0)));
|
||||
|
||||
await toggleWhitelist(driver);
|
||||
await toggleWhitelist(driver);
|
||||
|
||||
} catch (e) {
|
||||
// Save file incase there is a layout change
|
||||
const source = await driver.getPageSource();
|
||||
|
||||
fs.mkdirSync("./test-results");
|
||||
fs.writeFileSync("./test-results/source.html", source);
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
await driver.quit();
|
||||
}
|
||||
@@ -37,6 +55,8 @@ async function setup(): Promise<WebDriver> {
|
||||
options.addArguments("--load-extension=" + Path.join(__dirname, "../dist/"));
|
||||
options.addArguments("--mute-audio");
|
||||
options.addArguments("--disable-features=PreloadMediaEngagementData, MediaEngagementBypassAutoplayPolicies");
|
||||
options.addArguments("--headless=chrome");
|
||||
options.addArguments("--window-size=1920,1080");
|
||||
|
||||
const driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build();
|
||||
driver.manage().setTimeouts({
|
||||
@@ -57,7 +77,7 @@ async function waitForInstall(driver: WebDriver, startingTab = 0): Promise<void>
|
||||
|
||||
async function goToVideo(driver: WebDriver, videoId: string): Promise<void> {
|
||||
await driver.get("https://www.youtube.com/watch?v=" + videoId);
|
||||
await driver.wait(until.elementIsVisible(await driver.findElement(By.className("ytd-video-primary-info-renderer"))));
|
||||
await driver.wait(until.elementIsVisible(await driver.findElement(By.css(".ytd-video-primary-info-renderer, #above-the-fold"))));
|
||||
}
|
||||
|
||||
async function createSegment(driver: WebDriver, startTime: string, endTime: string, expectedDisplayedTime: string): Promise<void> {
|
||||
@@ -90,8 +110,7 @@ async function editSegments(driver: WebDriver, index: number, expectedStartTimeB
|
||||
}
|
||||
|
||||
let editButton = await driver.findElement(By.id("sponsorTimeEditButtonSubmissionNotice" + index));
|
||||
let sponsorTimeDisplays = await driver.findElements(By.className("sponsorTimeDisplay"));
|
||||
let sponsorTimeDisplay = sponsorTimeDisplays[index];
|
||||
const sponsorTimeDisplay = await getDisplayTimeBox(driver, index);
|
||||
await sponsorTimeDisplay.click();
|
||||
// Ensure edit time appears
|
||||
await driver.findElement(By.id("submittingTime0SubmissionNotice" + index));
|
||||
@@ -100,22 +119,39 @@ async function editSegments(driver: WebDriver, index: number, expectedStartTimeB
|
||||
await editButton.click();
|
||||
await editButton.click();
|
||||
|
||||
const startTimeBox = await driver.findElement(By.id("submittingTime0SubmissionNotice" + index));
|
||||
expect((await startTimeBox.getAttribute("value"))).toBe(expectedStartTimeBox);
|
||||
const startTimeBox = await getStartTimeBox(driver, index, expectedStartTimeBox);
|
||||
await startTimeBox.clear();
|
||||
await startTimeBox.sendKeys(startTime);
|
||||
|
||||
const endTimeBox = await driver.findElement(By.id("submittingTime1SubmissionNotice" + index));
|
||||
expect((await endTimeBox.getAttribute("value"))).toBe(expectedEndTimeBox);
|
||||
const endTimeBox = await getEndTimeBox(driver, index, expectedEndTimeBox);
|
||||
await endTimeBox.clear();
|
||||
await endTimeBox.sendKeys(endTime);
|
||||
|
||||
editButton = await driver.findElement(By.id("sponsorTimeEditButtonSubmissionNotice" + index));
|
||||
await editButton.click();
|
||||
|
||||
sponsorTimeDisplays = await driver.findElements(By.className("sponsorTimeDisplay"));
|
||||
sponsorTimeDisplay = sponsorTimeDisplays[index];
|
||||
await driver.wait(until.elementTextIs(sponsorTimeDisplay, expectedDisplayedTime));
|
||||
await getDisplayTimeBox(driver, index, expectedDisplayedTime);
|
||||
}
|
||||
|
||||
async function getStartTimeBox(driver: WebDriver, index: number, expectedStartTimeBox: string): Promise<WebElement> {
|
||||
const startTimeBox = await driver.findElement(By.id("submittingTime0SubmissionNotice" + index));
|
||||
expect((await startTimeBox.getAttribute("value"))).toBe(expectedStartTimeBox);
|
||||
return startTimeBox;
|
||||
}
|
||||
|
||||
async function getEndTimeBox(driver: WebDriver, index: number, expectedEndTimeBox: string): Promise<WebElement> {
|
||||
const endTimeBox = await driver.findElement(By.id("submittingTime1SubmissionNotice" + index));
|
||||
expect((await endTimeBox.getAttribute("value"))).toBe(expectedEndTimeBox);
|
||||
return endTimeBox;
|
||||
}
|
||||
|
||||
async function getDisplayTimeBox(driver: WebDriver, index: number, expectedDisplayedTime?: string): Promise<WebElement> {
|
||||
const sponsorTimeDisplay = (await driver.findElements(By.className("sponsorTimeDisplay")))[index];
|
||||
if (expectedDisplayedTime) {
|
||||
driver.wait(until.elementTextIs(sponsorTimeDisplay, expectedDisplayedTime));
|
||||
}
|
||||
|
||||
return sponsorTimeDisplay;
|
||||
}
|
||||
|
||||
async function setSegmentCategory(driver: WebDriver, index: number, categoryIndex: number, openSubmitBox: boolean): Promise<void> {
|
||||
@@ -162,4 +198,31 @@ async function muteSkipSegment(driver: WebDriver, startTime: number, endTime: nu
|
||||
await driver.sleep(duration * 1000 + 300);
|
||||
expect(await video.getAttribute("muted")).toBeNull(); // Default is null for some reason
|
||||
await driver.executeScript("document.querySelector('video').pause()");
|
||||
}
|
||||
|
||||
async function toggleWhitelist(driver: WebDriver): Promise<void> {
|
||||
const popupButton = await driver.findElement(By.id("infoButton"));
|
||||
if ((await popupButton.getCssValue("display")) !== "none") {
|
||||
await driver.actions().move({ origin: popupButton }).perform();
|
||||
await popupButton.click();
|
||||
}
|
||||
|
||||
const popupFrame = await driver.findElement(By.css("#sponsorBlockPopupContainer iframe"));
|
||||
await driver.switchTo().frame(popupFrame);
|
||||
|
||||
const whitelistButton = await driver.findElement(By.id("whitelistButton"));
|
||||
await driver.wait(until.elementIsVisible(whitelistButton));
|
||||
|
||||
const whitelistText = await driver.findElement(By.id("whitelistChannel"));
|
||||
const whitelistDisplayed = await whitelistText.isDisplayed();
|
||||
|
||||
await whitelistButton.click();
|
||||
if (whitelistDisplayed) {
|
||||
const unwhitelistText = await driver.findElement(By.id("unwhitelistChannel"));
|
||||
await driver.wait(until.elementIsVisible(unwhitelistText));
|
||||
} else {
|
||||
await driver.wait(until.elementIsVisible(whitelistText));
|
||||
}
|
||||
|
||||
await driver.switchTo().defaultContent();
|
||||
}
|
||||
18
tsconfig-production.json
Normal file
18
tsconfig-production.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": false,
|
||||
"outDir": "dist/js",
|
||||
"noEmitOnError": false,
|
||||
"typeRoots": [ "node_modules/@types" ],
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"es2019",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist/js",
|
||||
"noEmitOnError": false,
|
||||
"typeRoots": [ "node_modules/@types" ],
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"es2019",
|
||||
"es2019",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import webpack from "webpack"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import CopyPlugin from "copy-webpack-plugin"
|
||||
import BuildManifest from "./webpack.manifest.cjs";
|
||||
const srcDir = "../src/";
|
||||
import fs from "fs";
|
||||
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const webpack = require("webpack");
|
||||
const path = require('path');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const BuildManifest = require('./webpack.manifest');
|
||||
const srcDir = '../src/';
|
||||
const fs = require("fs");
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
|
||||
const edgeLanguages = [
|
||||
"de",
|
||||
@@ -27,14 +24,16 @@ const edgeLanguages = [
|
||||
"zh_CN"
|
||||
]
|
||||
|
||||
export default env => ({
|
||||
module.exports = env => ({
|
||||
entry: {
|
||||
popup: path.join(__dirname, srcDir + 'popup.ts'),
|
||||
background: path.join(__dirname, srcDir + 'background.ts'),
|
||||
content: path.join(__dirname, srcDir + 'content.ts'),
|
||||
options: path.join(__dirname, srcDir + 'options.ts'),
|
||||
help: path.join(__dirname, srcDir + 'help.ts'),
|
||||
permissions: path.join(__dirname, srcDir + 'permissions.ts')
|
||||
options: path.join(__dirname, srcDir + 'options.ts'),
|
||||
help: path.join(__dirname, srcDir + 'help.ts'),
|
||||
permissions: path.join(__dirname, srcDir + 'permissions.ts'),
|
||||
document: path.join(__dirname, srcDir + 'document.ts'),
|
||||
upsell: path.join(__dirname, srcDir + 'upsell.ts')
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, '../dist/js'),
|
||||
@@ -53,7 +52,8 @@ export default env => ({
|
||||
exclude: /node_modules/,
|
||||
options: {
|
||||
// disable type checker for user in fork plugin
|
||||
transpileOnly: true
|
||||
transpileOnly: true,
|
||||
configFile: env.mode === "production" ? "tsconfig-production.json" : "tsconfig.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { merge } from "webpack-merge";
|
||||
import common from './webpack.common.js';
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
export default env => merge(common(env), {
|
||||
module.exports = env => merge(common(env), {
|
||||
devtool: 'inline-source-map',
|
||||
mode: 'development'
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { merge } from "webpack-merge";
|
||||
import common from './webpack.common.js';
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
export default env => {
|
||||
module.exports = env => {
|
||||
let mode = "production";
|
||||
env.mode = mode;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user