mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-12 06:27:14 +03:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48e8abb247 | ||
|
|
a10f95f900 | ||
|
|
70b415b67a | ||
|
|
c22a763da6 | ||
|
|
abbab887c6 | ||
|
|
169350a5bf | ||
|
|
a4bb17f935 | ||
|
|
1fa886690a | ||
|
|
5540728fc6 | ||
|
|
9ab235c017 | ||
|
|
e5cc0587e2 | ||
|
|
e8b3753a17 | ||
|
|
b0984e9bbc | ||
|
|
1a3964e238 | ||
|
|
5903d1a023 | ||
|
|
8b0b670ea8 | ||
|
|
9ef948670c | ||
|
|
5a7e16b51c | ||
|
|
be98dc7b73 | ||
|
|
3174eda29a | ||
|
|
6a4bb50006 | ||
|
|
66876283b4 | ||
|
|
75dc6a1980 | ||
|
|
2823e51b1d | ||
|
|
c9c02e91ff | ||
|
|
19efcb5a98 | ||
|
|
5c7e3b5cb3 | ||
|
|
c3cbc44d28 | ||
|
|
6965279d36 | ||
|
|
eb61ad76c1 | ||
|
|
7a1fa601da | ||
|
|
945ac5aad3 | ||
|
|
617f0e25e8 | ||
|
|
ac52a1cb1d | ||
|
|
2cefb9822b | ||
|
|
b81de96277 | ||
|
|
236c95d2f6 | ||
|
|
0f75953ad1 | ||
|
|
0f4c1c50cb | ||
|
|
9e4939e2e3 | ||
|
|
f4dc98112a | ||
|
|
138e57b720 | ||
|
|
de1d6bd76a | ||
|
|
4a217300a2 | ||
|
|
f4822fa587 | ||
|
|
e4d6fdbd97 | ||
|
|
1bd72e4db1 | ||
|
|
307eadc2da | ||
|
|
99014dd113 | ||
|
|
89b7cb0230 | ||
|
|
e3da6c8643 | ||
|
|
136f5e7d61 | ||
|
|
16f24978ff | ||
|
|
091d67e6b3 | ||
|
|
fcf9bdc68e | ||
|
|
af73aabbae | ||
|
|
778b433ad4 | ||
|
|
dc9c08f1fa | ||
|
|
4efa8b464e | ||
|
|
e7d0180e15 | ||
|
|
0fa24dbae4 | ||
|
|
975b876a74 | ||
|
|
557e2ea2df | ||
|
|
11f576eb69 | ||
|
|
2a9e036986 | ||
|
|
15976777ed | ||
|
|
9ad636fdad | ||
|
|
6bfc66740e | ||
|
|
7e824df73d | ||
|
|
62c3bc1c7a | ||
|
|
266508ca2d | ||
|
|
19ccb5e9e9 | ||
|
|
46b8c41db7 | ||
|
|
4e5ddd07c9 | ||
|
|
2291658371 | ||
|
|
cfa8eee193 | ||
|
|
14f8ee565a | ||
|
|
b28c72bc52 | ||
|
|
b98300596c | ||
|
|
38e6d5469f | ||
|
|
4d868055e7 | ||
|
|
086525c178 | ||
|
|
32a35aa19c | ||
|
|
80148804fb | ||
|
|
8ce15e69fb | ||
|
|
d3cd0962d5 | ||
|
|
c1168582a9 | ||
|
|
dbd51d026b | ||
|
|
61f1a8918c | ||
|
|
3a488a9110 | ||
|
|
842f7b33b8 | ||
|
|
fcc7ce8dfc | ||
|
|
9c4946a3fe | ||
|
|
7d2e4c8515 | ||
|
|
3b2fe51973 | ||
|
|
b2ac881b9b | ||
|
|
4a134d7604 | ||
|
|
ad7d3a320b | ||
|
|
0c3aa922ca | ||
|
|
875ec357d9 | ||
|
|
510c3723ad | ||
|
|
f5910407ba | ||
|
|
2691dc5df5 | ||
|
|
31ef4f39bf | ||
|
|
4044a22e04 | ||
|
|
3c2c140eb5 | ||
|
|
5cdbc233e4 | ||
|
|
86f8809de7 | ||
|
|
5163b3362f | ||
|
|
443fb1f448 | ||
|
|
7cd7089980 | ||
|
|
61f5d7a902 | ||
|
|
6ab88e9fa0 | ||
|
|
3bddd0405c | ||
|
|
e4b05a316a | ||
|
|
451ceb370b | ||
|
|
ab5291702a | ||
|
|
bec183d595 | ||
|
|
a2f4f34dfe | ||
|
|
7ba6fee9e5 | ||
|
|
9d9713ee82 | ||
|
|
91556f981a | ||
|
|
9f86aa6740 | ||
|
|
466573e2a4 | ||
|
|
4c417b10fe | ||
|
|
36549e7898 | ||
|
|
0d2d5f2eb0 | ||
|
|
e089b83797 | ||
|
|
dbd0d157a8 | ||
|
|
aef2b113f8 | ||
|
|
e610e03e46 | ||
|
|
17cbd068b9 | ||
|
|
7064d3fe82 | ||
|
|
abfe0b3f38 | ||
|
|
dfdc052f4b | ||
|
|
7ca3b69965 | ||
|
|
5577b38e2b | ||
|
|
ae4f850ff4 |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -10,10 +10,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Initialization
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
# Create Chrome artifacts
|
||||
- name: Create Chrome artifacts
|
||||
run: npm run build:chrome
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ChromeExtension
|
||||
path: dist
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
# Create Firefox artifacts
|
||||
- name: Create Firefox artifacts
|
||||
run: npm run build:firefox
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FirefoxExtension
|
||||
path: dist
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
# Create Beta artifacts (Builds with the name changed to beta)
|
||||
- name: Create Chrome Beta artifacts
|
||||
run: npm run build:chrome -- --env stream=beta
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ChromeExtensionBeta
|
||||
path: dist
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
- name: Create Firefox Beta artifacts
|
||||
run: npm run build:firefox -- --env stream=beta
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FirefoxExtensionBeta
|
||||
path: dist
|
||||
|
||||
81
.github/workflows/release.yml
vendored
81
.github/workflows/release.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Initialization
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Copy configuration
|
||||
@@ -35,24 +35,11 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: npm ci
|
||||
|
||||
# Create Chrome artifacts
|
||||
- name: Create Chrome artifacts
|
||||
run: npm run build:chrome
|
||||
- run: mkdir ./builds
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/ChromeExtension.zip *
|
||||
- name: Upload ChromeExtension to release
|
||||
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
|
||||
with:
|
||||
args: builds/ChromeExtension.zip
|
||||
name: ChromeExtension.zip
|
||||
path: ./builds/ChromeExtension.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Create Firefox artifacts
|
||||
- name: Create Firefox artifacts
|
||||
run: npm run build:firefox
|
||||
- run: mkdir ./builds
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/FirefoxExtension.zip *
|
||||
- name: Upload FirefoxExtension to release
|
||||
@@ -62,32 +49,18 @@ jobs:
|
||||
name: FirefoxExtension.zip
|
||||
path: ./builds/FirefoxExtension.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Create Beta artifacts (Builds with the name changed to beta)
|
||||
- name: Create Chrome Beta artifacts
|
||||
run: npm run build:chrome -- --env stream=beta
|
||||
|
||||
# Create Chrome artifacts
|
||||
- name: Create Chrome artifacts
|
||||
run: npm run build:chrome
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/ChromeExtensionBeta.zip *
|
||||
- name: Upload ChromeExtensionBeta to release
|
||||
run: cd ./dist ; zip -r ../builds/ChromeExtension.zip *
|
||||
- name: Upload ChromeExtension to release
|
||||
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
|
||||
with:
|
||||
args: builds/ChromeExtensionBeta.zip
|
||||
name: ChromeExtensionBeta.zip
|
||||
path: ./builds/ChromeExtensionBeta.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
# Create Safari artifacts
|
||||
- name: Create Safari artifacts
|
||||
run: npm run build:safari
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/SafariExtension.zip *
|
||||
- name: Upload SafariExtension to release
|
||||
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
|
||||
with:
|
||||
args: builds/SafariExtension.zip
|
||||
name: SafariExtension.zip
|
||||
path: ./builds/SafariExtension.zip
|
||||
args: builds/ChromeExtension.zip
|
||||
name: ChromeExtension.zip
|
||||
path: ./builds/ChromeExtension.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Create Edge artifacts
|
||||
@@ -105,10 +78,36 @@ jobs:
|
||||
path: ./builds/EdgeExtension.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Create Safari artifacts
|
||||
- name: Create Safari artifacts
|
||||
run: npm run build:safari
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/SafariExtension.zip *
|
||||
- name: Upload SafariExtension to release
|
||||
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
|
||||
with:
|
||||
args: builds/SafariExtension.zip
|
||||
name: SafariExtension.zip
|
||||
path: ./builds/SafariExtension.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Create Beta artifacts (Builds with the name changed to beta)
|
||||
- name: Create Chrome Beta artifacts
|
||||
run: npm run build:chrome -- --env stream=beta
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/ChromeExtensionBeta.zip *
|
||||
- name: Upload ChromeExtensionBeta to release
|
||||
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
|
||||
with:
|
||||
args: builds/ChromeExtensionBeta.zip
|
||||
name: ChromeExtensionBeta.zip
|
||||
path: ./builds/ChromeExtensionBeta.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Firefox Beta
|
||||
- name: Create Firefox Beta artifacts
|
||||
run: npm run build:firefox -- --env stream=beta
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FirefoxExtensionBeta
|
||||
path: dist
|
||||
@@ -125,7 +124,7 @@ jobs:
|
||||
run: sudo apt-get install rename
|
||||
- name: Rename signed file
|
||||
run: cd ./web-ext-artifacts ; rename 's/.*/FirefoxSignedInstaller.xpi/' *
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FirefoxExtensionSigned.xpi
|
||||
path: ./web-ext-artifacts/FirefoxSignedInstaller.xpi
|
||||
|
||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@@ -9,10 +9,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Initialization
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
- name: Upload results on fail
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Test Results
|
||||
path: ./test-results
|
||||
6
.github/workflows/update-oss-attribution.yml
vendored
6
.github/workflows/update-oss-attribution.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
||||
update-oss:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install and generate attribution
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
cd ci && npx ts-node prettify.ts
|
||||
|
||||
- name: Create pull request to update list
|
||||
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
# v4.2.3
|
||||
with:
|
||||
commit-message: Update OSS Attribution
|
||||
|
||||
4
.github/workflows/updateInvidous.yml
vendored
4
.github/workflows/updateInvidous.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
check-list:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Download instance lists
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
run: npm run ci:invidious
|
||||
|
||||
- name: Create pull request to update list
|
||||
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
# v4.2.3
|
||||
with:
|
||||
commit-message: Update Invidious List
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"intro": "https://wiki.sponsor.ajay.app/w/Intermission/Intro_Animation",
|
||||
"outro": "https://wiki.sponsor.ajay.app/w/Endcards/Credits",
|
||||
"preview": "https://wiki.sponsor.ajay.app/w/Preview/Recap",
|
||||
"filler": "https://wiki.sponsor.ajay.app/w/Filler_Tangent",
|
||||
"filler": "https://wiki.sponsor.ajay.app/w/Tangents/Jokes",
|
||||
"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",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"icons/close.png",
|
||||
"icons/skipIcon.svg",
|
||||
"icons/refresh.svg",
|
||||
"icons/beep.ogg",
|
||||
"icons/beep.oga",
|
||||
"icons/pause.svg",
|
||||
"icons/stop.svg",
|
||||
"icons/skip.svg",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "sponsorBlocker@ajay.app",
|
||||
"strict_min_version": "56.0"
|
||||
"strict_min_version": "102.0"
|
||||
},
|
||||
"gecko_android": {
|
||||
"strict_min_version": "113.0"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"icons/close.png",
|
||||
"icons/skipIcon.svg",
|
||||
"icons/refresh.svg",
|
||||
"icons/beep.ogg",
|
||||
"icons/beep.oga",
|
||||
"icons/pause.svg",
|
||||
"icons/stop.svg",
|
||||
"icons/skip.svg",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "__MSG_fullName__",
|
||||
"short_name": "SponsorBlock",
|
||||
"version": "5.8",
|
||||
"version": "5.11.8",
|
||||
"default_locale": "en",
|
||||
"description": "__MSG_Description__",
|
||||
"homepage_url": "https://sponsor.ajay.app",
|
||||
|
||||
@@ -4,5 +4,13 @@
|
||||
},
|
||||
"optional_permissions": [
|
||||
"webNavigation"
|
||||
]
|
||||
],
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"16": "icons/SafariIconSponsorBlocker16px.png",
|
||||
"32": "icons/SafariIconSponsorBlocker32px.png",
|
||||
"64": "icons/SafariIconSponsorBlocker64px.png",
|
||||
"128": "icons/SafariIconSponsorBlocker128px.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule maze-utils updated: e9523b0c5d...d47ec4cca4
Submodule public/_locales updated: 3c5a80ca9c...010e475341
@@ -11,6 +11,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Vorapi compatibility */
|
||||
#player-api_VORAPI_ELEMENT_ID #previewbar {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#previewbar {
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
@@ -26,6 +31,15 @@
|
||||
transition: transform .1s cubic-bezier(0,0,0.2,1);
|
||||
}
|
||||
|
||||
/* Prevent bar from covering highlights on YTTV */
|
||||
#previewbar.sponsorblock-yttv-container {
|
||||
z-index: unset;
|
||||
}
|
||||
|
||||
ytu-time-bar.ytu-storyboard {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* May 2024 hover preview */
|
||||
.YtPlayerProgressBarProgressBar #previewbar {
|
||||
transform: none;
|
||||
@@ -44,7 +58,16 @@
|
||||
}
|
||||
|
||||
div:hover > #previewbar.sbNotInvidious {
|
||||
transform: scaleY(1)
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
/* Vorapis */
|
||||
.v3 #previewbar.sbNotInvidious {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
.sponsorCategoryTooltipVisible.ytp-progress-tooltip {
|
||||
width: 216px !important;
|
||||
/* left: 264.308px !important; */
|
||||
}
|
||||
|
||||
.previewbar {
|
||||
@@ -53,6 +76,11 @@ div:hover > #previewbar.sbNotInvidious {
|
||||
min-width: 1px;
|
||||
}
|
||||
|
||||
.previewbar-yttv {
|
||||
height: 10px;
|
||||
top: 14px;
|
||||
}
|
||||
|
||||
.previewbar.requiredSegment {
|
||||
transform: scaleY(3);
|
||||
}
|
||||
@@ -74,7 +102,7 @@ div:hover > #previewbar.sbNotInvidious {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||
.ytp-tooltip.sponsorCategoryTooltipVisible:not(.sponsorTooltipHasYTChapters) {
|
||||
transform: translateY(-1em) !important;
|
||||
}
|
||||
|
||||
@@ -170,6 +198,16 @@ div:hover > .sponsorBlockChapterBar {
|
||||
padding-right: 3.6px;
|
||||
}
|
||||
|
||||
.sbButtonYTTV {
|
||||
padding-left: 5px !important;
|
||||
}
|
||||
|
||||
/* YTTV only */
|
||||
.ytu-player-controls > .skipButtonControlBarContainer > div {
|
||||
padding-left: 5px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.autoHiding {
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -218,6 +256,11 @@ div:hover > .sponsorBlockChapterBar {
|
||||
from { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fadeInToFaded {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to { opacity: 0; }
|
||||
}
|
||||
@@ -290,6 +333,10 @@ div:hover > .sponsorBlockChapterBar {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeFadeIn.sponsorSkipNoticeFaded {
|
||||
animation: fadeInToFaded 0.5s ease-out;
|
||||
}
|
||||
|
||||
.exportCopiedNotice .sponsorSkipNoticeFadeIn {
|
||||
animation: none;
|
||||
}
|
||||
@@ -826,6 +873,15 @@ input::-webkit-inner-spin-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Vorapis V3 support */
|
||||
#watch7-content .sponsorBlockCategoryPill {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
#watch7-content .sponsorBlockCategoryPillTitle {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.categoryPillClose {
|
||||
display: none;
|
||||
height: 10px;
|
||||
|
||||
BIN
public/icons/SafariIconSponsorBlocker128px.png
Normal file
BIN
public/icons/SafariIconSponsorBlocker128px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/icons/SafariIconSponsorBlocker16px.png
Normal file
BIN
public/icons/SafariIconSponsorBlocker16px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/icons/SafariIconSponsorBlocker32px.png
Normal file
BIN
public/icons/SafariIconSponsorBlocker32px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/icons/SafariIconSponsorBlocker64px.png
Normal file
BIN
public/icons/SafariIconSponsorBlocker64px.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
@@ -118,13 +118,13 @@ html, body {
|
||||
|
||||
.option-group > div {
|
||||
min-height: 50px;
|
||||
padding: 20px 0;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
|
||||
}
|
||||
|
||||
.categoryExtraOptions {
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
#music_offtopic_autoSkipOnMusicVideos {
|
||||
@@ -271,11 +271,11 @@ input[type='number'] {
|
||||
|
||||
.small-description {
|
||||
font-size: 13px;
|
||||
padding: 15px 0 0 20px;
|
||||
padding: 5px 0 0 20px;
|
||||
}
|
||||
|
||||
.small-description td {
|
||||
padding: 10px 0 20px 20px;
|
||||
padding: 2.5px 0 10px 20px;
|
||||
}
|
||||
|
||||
.indent {
|
||||
@@ -283,7 +283,7 @@ input[type='number'] {
|
||||
}
|
||||
|
||||
.categoryTableElement td {
|
||||
padding-top: 10px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
@@ -353,7 +353,8 @@ input[type='number'] {
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
display: table;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-container .switch-label {
|
||||
@@ -372,6 +373,7 @@ input[type='number'] {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
|
||||
@@ -154,20 +154,6 @@
|
||||
<div class="small-description">__MSG_whatForceChannelCheck__</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-sync="refetchWhenNotFound">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="refetchWhenNotFound" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="refetchWhenNotFound">
|
||||
__MSG_enableRefetchWhenNotFound__
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="small-description">__MSG_whatRefetchWhenNotFound__</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-sync="showCategoryWithoutPermission">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
@@ -209,15 +195,31 @@
|
||||
<div class="small-description">__MSG_skipNoticeDurationDescription__</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-toggle-type="reverse" data-sync="dontShowNotice">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="dontShowNotice" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="dontShowNotice">
|
||||
__MSG_showSkipNotice__
|
||||
</label>
|
||||
<div>
|
||||
<div data-type="toggle" data-sync="showUpcomingNotice">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="showUpcomingNotice" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="showUpcomingNotice">
|
||||
__MSG_showUpcomingNotice__
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div data-type="toggle" data-toggle-type="reverse" data-sync="dontShowNotice">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="dontShowNotice" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="dontShowNotice">
|
||||
__MSG_showSkipNotice__
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-type="selector" data-sync="noticeVisibilityMode" data-dependent-on="dontShowNotice">
|
||||
@@ -470,6 +472,16 @@
|
||||
<div class="inline"></div>
|
||||
</div>
|
||||
|
||||
<div data-type="keybind-change" data-sync="upvoteKeybind">
|
||||
<label class="optionLabel">__MSG_setUpvoteKeybind__:</label>
|
||||
<div class="inline"></div>
|
||||
</div>
|
||||
|
||||
<div data-type="keybind-change" data-sync="downvoteKeybind">
|
||||
<label class="optionLabel">__MSG_setDownvoteKeybind__:</label>
|
||||
<div class="inline"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="import" class="option-group hidden">
|
||||
|
||||
@@ -43,7 +43,7 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
|
||||
chrome.tabs.create({url: chrome.runtime.getURL(request.url)});
|
||||
return false;
|
||||
case "submitVote":
|
||||
submitVote(request.type, request.UUID, request.category).then(callback);
|
||||
submitVote(request.type, request.UUID, request.category, request.videoID).then(callback);
|
||||
|
||||
//this allows the callback to be called later
|
||||
return true;
|
||||
@@ -214,7 +214,7 @@ async function unregisterFirefoxContentScript(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitVote(type: number, UUID: string, category: string) {
|
||||
async function submitVote(type: number, UUID: string, category: string, videoID: string) {
|
||||
let userID = Config.config.userID;
|
||||
|
||||
if (userID == undefined || userID === "undefined") {
|
||||
@@ -226,7 +226,7 @@ async function submitVote(type: number, UUID: string, category: string) {
|
||||
const typeSection = (type !== undefined) ? "&type=" + type : "&category=" + category;
|
||||
|
||||
try {
|
||||
const response = await asyncRequestToServer("POST", "/api/voteOnSponsorTime?UUID=" + UUID + "&userID=" + userID + typeSection);
|
||||
const response = await asyncRequestToServer("POST", "/api/voteOnSponsorTime?UUID=" + UUID + "&videoID=" + videoID + "&userID=" + userID + typeSection);
|
||||
|
||||
if (response.ok) {
|
||||
return {
|
||||
|
||||
@@ -44,7 +44,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
|
||||
<>
|
||||
{/* Upvote Button */}
|
||||
<button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
|
||||
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "sbhidden" : "")}
|
||||
className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "sbhidden " : " ") + (document.location.host === "tv.youtube.com" ? "sbButtonYTTV" : "")}
|
||||
draggable="false"
|
||||
title={chrome.i18n.getMessage("upvoteButtonInfo")}
|
||||
onClick={(e) => this.vote(e, 1)}>
|
||||
@@ -55,7 +55,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
|
||||
|
||||
{/* Downvote Button */}
|
||||
<button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
|
||||
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "sbhidden" : "")}
|
||||
className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "sbhidden " : " ") + (document.location.host === "tv.youtube.com" ? "sbButtonYTTV" : "")}
|
||||
draggable="false"
|
||||
title={chrome.i18n.getMessage("reportButtonInfo")}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface NoticeProps {
|
||||
idSuffix?: string;
|
||||
|
||||
fadeIn?: boolean;
|
||||
fadeOut?: boolean;
|
||||
startFaded?: boolean;
|
||||
firstColumn?: React.ReactElement[] | React.ReactElement;
|
||||
firstRow?: React.ReactElement;
|
||||
@@ -244,7 +245,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
id={"skipNoticeTimerText" + this.idSuffix}
|
||||
key="skipNoticeTimerText"
|
||||
className={this.state.countdownMode !== CountdownMode.Timer ? "sbhidden" : ""} >
|
||||
{chrome.i18n.getMessage("NoticeTimeAfterSkip").replace("{seconds}", this.state.countdownTime.toString())}
|
||||
{chrome.i18n.getMessage("NoticeTimeAfterSkip").replace("{seconds}", Math.ceil(this.state.countdownTime).toString())}
|
||||
</span>
|
||||
),(
|
||||
<img
|
||||
@@ -326,7 +327,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (countdownTime == 3) {
|
||||
if (countdownTime == 3 && this.props.fadeOut) {
|
||||
//start fade out animation
|
||||
const notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
|
||||
notice?.style.removeProperty("animation");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from "react";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import Config from "../config"
|
||||
import { Category, ContentContainer, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
|
||||
import { Category, ContentContainer, SponsorTime, NoticeVisibilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
|
||||
import NoticeComponent from "./NoticeComponent";
|
||||
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
import { getSkippingText } from "../utils/categoryUtils";
|
||||
import { getSkippingText, getUpcomingText, getVoteText } from "../utils/categoryUtils";
|
||||
|
||||
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
|
||||
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
|
||||
@@ -28,12 +28,18 @@ export interface SkipNoticeProps {
|
||||
|
||||
autoSkip: boolean;
|
||||
startReskip?: boolean;
|
||||
upcomingNotice?: boolean;
|
||||
voteNotice?: boolean;
|
||||
// Contains functions and variables from the content script needed by the skip notice
|
||||
contentContainer: ContentContainer;
|
||||
|
||||
closeListener: () => void;
|
||||
showKeybindHint?: boolean;
|
||||
smaller: boolean;
|
||||
fadeIn: boolean;
|
||||
maxCountdownTime?: number;
|
||||
|
||||
componentDidMount?: () => void;
|
||||
|
||||
unskipTime?: number;
|
||||
}
|
||||
@@ -97,9 +103,9 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
this.autoSkip = props.autoSkip;
|
||||
this.contentContainer = props.contentContainer;
|
||||
|
||||
const noticeTitle = getSkippingText(this.segments, this.props.autoSkip);
|
||||
const noticeTitle = this.props.voteNotice ? getVoteText(this.segments) : !this.props.upcomingNotice ? getSkippingText(this.segments, this.props.autoSkip) : getUpcomingText(this.segments);
|
||||
|
||||
const previousSkipNotices = document.getElementsByClassName("sponsorSkipNoticeParent");
|
||||
const previousSkipNotices = document.querySelectorAll(".sponsorSkipNoticeParent:not(.sponsorSkipUpcomingNotice)");
|
||||
this.amountOfPreviousNotices = previousSkipNotices.length;
|
||||
// If there is at least one already in the first slot
|
||||
this.showInSecondSlot = previousSkipNotices.length > 0 && [...previousSkipNotices].some(notice => !notice.classList.contains("secondSkipNotice"));
|
||||
@@ -120,7 +126,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
this.lockedColor = Config.config.colorPalette.locked;
|
||||
|
||||
const isMuteSegment = this.segments[0].actionType === ActionType.Mute;
|
||||
const maxCountdownTime = isMuteSegment ? this.getFullDurationCountdown(0) : () => Config.config.skipNoticeDuration;
|
||||
const maxCountdownTime = props.maxCountdownTime ? () => props.maxCountdownTime : (isMuteSegment ? this.getFullDurationCountdown(0) : () => Config.config.skipNoticeDuration);
|
||||
|
||||
const defaultSkipButtonState = this.props.startReskip ? SkipButtonState.Redo : SkipButtonState.Undo;
|
||||
const skipButtonStates = [defaultSkipButtonState, isMuteSegment ? SkipButtonState.Start : defaultSkipButtonState];
|
||||
@@ -171,12 +177,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
noticeStyle.transform = "scale(0.8) translate(10%, 10%)";
|
||||
}
|
||||
|
||||
// If it started out as smaller, always keep the
|
||||
// skip button there
|
||||
const showFirstSkipButton = this.props.smaller || this.segments[0].actionType === ActionType.Mute;
|
||||
const firstColumn = showFirstSkipButton ? (
|
||||
this.getSkipButton(0)
|
||||
) : null;
|
||||
const firstColumn = this.getSkipButton(0);
|
||||
|
||||
return (
|
||||
<NoticeComponent
|
||||
@@ -184,9 +185,10 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
amountOfPreviousNotices={this.amountOfPreviousNotices}
|
||||
showInSecondSlot={this.showInSecondSlot}
|
||||
idSuffix={this.idSuffix}
|
||||
fadeIn={true}
|
||||
startFaded={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAll
|
||||
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAutoSkip && this.autoSkip)}
|
||||
fadeIn={this.props.fadeIn}
|
||||
fadeOut={!this.props.upcomingNotice}
|
||||
startFaded={Config.config.noticeVisibilityMode >= NoticeVisibilityMode.FadedForAll
|
||||
|| (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.FadedForAutoSkip && this.autoSkip)}
|
||||
timed={true}
|
||||
maxCountdownTime={this.state.maxCountdownTime}
|
||||
style={noticeStyle}
|
||||
@@ -197,12 +199,20 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
logoFill={Config.config.barTypes[this.segments[0].category].color}
|
||||
limitWidth={true}
|
||||
firstColumn={firstColumn}
|
||||
dontPauseCountdown={!!this.props.upcomingNotice}
|
||||
bottomRow={[...this.getMessageBoxes(), ...this.getBottomRow() ]}
|
||||
extraClass={this.props.upcomingNotice ? "sponsorSkipUpcomingNotice" : ""}
|
||||
onMouseEnter={() => this.onMouseEnter() } >
|
||||
</NoticeComponent>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (this.props.componentDidMount) {
|
||||
this.props.componentDidMount();
|
||||
}
|
||||
}
|
||||
|
||||
getBottomRow(): JSX.Element[] {
|
||||
return [
|
||||
/* Bottom Row */
|
||||
@@ -233,15 +243,18 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
</div>
|
||||
|
||||
{/* Copy and Downvote Button */}
|
||||
<div id={"sponsorTimesDownvoteButtonsContainerCopyDownvote" + this.idSuffix}
|
||||
className="voteButton"
|
||||
style={{marginLeft: "5px"}}
|
||||
onClick={() => this.openEditingOptions()}>
|
||||
<PencilSvg fill={this.state.editing === true
|
||||
|| this.state.actionState === SkipNoticeAction.CopyDownvote
|
||||
|| this.state.choosingCategory === true
|
||||
? this.selectedColor : this.unselectedColor} />
|
||||
</div>
|
||||
{
|
||||
!this.props.voteNotice &&
|
||||
<div id={"sponsorTimesDownvoteButtonsContainerCopyDownvote" + this.idSuffix}
|
||||
className="voteButton"
|
||||
style={{marginLeft: "5px"}}
|
||||
onClick={() => this.openEditingOptions()}>
|
||||
<PencilSvg fill={this.state.editing === true
|
||||
|| this.state.actionState === SkipNoticeAction.CopyDownvote
|
||||
|| this.state.choosingCategory === true
|
||||
? this.selectedColor : this.unselectedColor} />
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
|
||||
:
|
||||
@@ -269,11 +282,11 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
}
|
||||
|
||||
{/* Unskip/Skip Button */}
|
||||
{!this.props.smaller || this.segments[0].actionType === ActionType.Mute
|
||||
{!this.props.voteNotice && (!this.props.smaller || this.segments[0].actionType === ActionType.Mute)
|
||||
? this.getSkipButton(1) : null}
|
||||
|
||||
{/* Never show button */}
|
||||
{!this.autoSkip || this.props.startReskip ? "" :
|
||||
{!this.autoSkip || this.props.startReskip || this.props.voteNotice ? "" :
|
||||
<td className="sponsorSkipNoticeRightSection"
|
||||
key={1}>
|
||||
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
|
||||
@@ -365,8 +378,10 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
style.minWidth = "100px";
|
||||
}
|
||||
|
||||
const showSkipButton = (buttonIndex !== 0 || this.props.smaller || !this.props.voteNotice || this.segments[0].actionType === ActionType.Mute) && !this.props.upcomingNotice;
|
||||
|
||||
return (
|
||||
<span className="sponsorSkipNoticeUnskipSection">
|
||||
<span className="sponsorSkipNoticeUnskipSection" style={{ visibility: !showSkipButton ? "hidden" : null }}>
|
||||
<button id={"sponsorSkipUnskipButton" + this.idSuffix}
|
||||
className="sponsorSkipObject sponsorSkipNoticeButton"
|
||||
style={style}
|
||||
@@ -420,7 +435,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
}
|
||||
|
||||
onMouseEnter(): void {
|
||||
if (this.state.smaller) {
|
||||
if (this.state.smaller && !this.props.upcomingNotice) {
|
||||
this.setState({
|
||||
smaller: false
|
||||
});
|
||||
@@ -613,14 +628,17 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
}
|
||||
|
||||
unskip(buttonIndex: number, index: number, forceSeek: boolean): void {
|
||||
this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime, forceSeek);
|
||||
this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime, forceSeek, this.props.voteNotice);
|
||||
|
||||
this.unskippedMode(buttonIndex, index, SkipButtonState.Redo);
|
||||
this.unskippedMode(buttonIndex, index, this.segments[0].actionType === ActionType.Poi ? SkipButtonState.Undo : SkipButtonState.Redo);
|
||||
}
|
||||
|
||||
reskip(buttonIndex: number, index: number, forceSeek: boolean): void {
|
||||
this.contentContainer().reskipSponsorTime(this.segments[index], forceSeek);
|
||||
this.reskippedMode(buttonIndex);
|
||||
}
|
||||
|
||||
reskippedMode(buttonIndex: number): void {
|
||||
const skipButtonStates = this.state.skipButtonStates;
|
||||
skipButtonStates[buttonIndex] = SkipButtonState.Undo;
|
||||
|
||||
@@ -650,7 +668,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
}
|
||||
|
||||
getUnskippedModeInfo(buttonIndex: number, index: number, skipButtonState: SkipButtonState): SkipNoticeState {
|
||||
const changeCountdown = this.segments[index].actionType !== ActionType.Poi;
|
||||
const changeCountdown = !this.props.voteNotice && this.segments[index].actionType !== ActionType.Poi;
|
||||
|
||||
const maxCountdownTime = changeCountdown ?
|
||||
this.getFullDurationCountdown(index) : this.state.maxCountdownTime;
|
||||
@@ -658,7 +676,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
const skipButtonStates = this.state.skipButtonStates;
|
||||
const skipButtonCallbacks = this.state.skipButtonCallbacks;
|
||||
if (buttonIndex === null) {
|
||||
for (let i = 0; i < this.segments.length; i++) {
|
||||
for (let i = 0; i < skipButtonStates.length; i++) {
|
||||
skipButtonStates[i] = skipButtonState;
|
||||
skipButtonCallbacks[i] = this.reskip.bind(this);
|
||||
}
|
||||
@@ -686,7 +704,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
getFullDurationCountdown(index: number): () => number {
|
||||
return () => {
|
||||
const sponsorTime = this.segments[index];
|
||||
const duration = Math.round((sponsorTime.segment[1] - getCurrentTime()) * (1 / getVideo().playbackRate));
|
||||
const duration = Math.round((sponsorTime.segment[1] - getCurrentTime()) * (1 / (getVideo()?.playbackRate ?? 1)));
|
||||
|
||||
return Math.max(duration, Config.config.skipNoticeDuration);
|
||||
};
|
||||
|
||||
@@ -234,6 +234,10 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
configKey: "showSegmentNameInChapterBar",
|
||||
label: chrome.i18n.getMessage("showSegmentNameInChapterBar"),
|
||||
dontDisable: true
|
||||
}, {
|
||||
configKey: "showAutogeneratedChapters",
|
||||
label: chrome.i18n.getMessage("showAutogeneratedChapters"),
|
||||
dontDisable: true
|
||||
}];
|
||||
case "music_offtopic":
|
||||
return [{
|
||||
|
||||
@@ -146,7 +146,9 @@ class KeybindDialogComponent extends React.Component<KeybindDialogProps, Keybind
|
||||
this.props.option !== "actuallySubmitKeybind" && this.equals(Config.config['actuallySubmitKeybind']) ||
|
||||
this.props.option !== "previewKeybind" && this.equals(Config.config['previewKeybind']) ||
|
||||
this.props.option !== "closeSkipNoticeKeybind" && this.equals(Config.config['closeSkipNoticeKeybind']) ||
|
||||
this.props.option !== "startSponsorKeybind" && this.equals(Config.config['startSponsorKeybind']))
|
||||
this.props.option !== "startSponsorKeybind" && this.equals(Config.config['startSponsorKeybind']) ||
|
||||
this.props.option !== "downvoteKeybind" && this.equals(Config.config['downvoteKeybind']) ||
|
||||
this.props.option !== "upvoteKeybind" && this.equals(Config.config['upvoteKeybind']))
|
||||
return {message: chrome.i18n.getMessage("keyAlreadyUsed"), blocking: true};
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as CompileConfig from "../config.json";
|
||||
import * as invidiousList from "../ci/invidiouslist.json";
|
||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType } from "./types";
|
||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType } from "./types";
|
||||
import { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config";
|
||||
import { HashedValue } from "../maze-utils/src/hash";
|
||||
|
||||
@@ -31,7 +31,8 @@ interface SBConfig {
|
||||
trackDownvotes: boolean;
|
||||
trackDownvotesInPrivate: boolean;
|
||||
dontShowNotice: boolean;
|
||||
noticeVisibilityMode: NoticeVisbilityMode;
|
||||
showUpcomingNotice: boolean;
|
||||
noticeVisibilityMode: NoticeVisibilityMode;
|
||||
hideVideoPlayerControls: boolean;
|
||||
hideInfoButtonPlayerControls: boolean;
|
||||
hideDeleteButtonPlayerControls: boolean;
|
||||
@@ -47,7 +48,6 @@ interface SBConfig {
|
||||
audioNotificationOnSkip: boolean;
|
||||
checkForUnlistedVideos: boolean;
|
||||
testingServer: boolean;
|
||||
refetchWhenNotFound: boolean;
|
||||
ytInfoPermissionGranted: boolean;
|
||||
allowExpirements: boolean;
|
||||
showDonationLink: boolean;
|
||||
@@ -69,6 +69,7 @@ interface SBConfig {
|
||||
showCategoryGuidelines: boolean;
|
||||
showCategoryWithoutPermission: boolean;
|
||||
showSegmentNameInChapterBar: boolean;
|
||||
showAutogeneratedChapters: boolean;
|
||||
useVirtualTime: boolean;
|
||||
showSegmentFailedToFetchWarning: boolean;
|
||||
allowScrollingToEdit: boolean;
|
||||
@@ -96,6 +97,8 @@ interface SBConfig {
|
||||
nextChapterKeybind: Keybind;
|
||||
previousChapterKeybind: Keybind;
|
||||
closeSkipNoticeKeybind: Keybind;
|
||||
upvoteKeybind: Keybind;
|
||||
downvoteKeybind: Keybind;
|
||||
|
||||
// What categories should be skipped
|
||||
categorySelections: CategorySelection[];
|
||||
@@ -293,7 +296,8 @@ const syncDefaults = {
|
||||
trackDownvotes: true,
|
||||
trackDownvotesInPrivate: false,
|
||||
dontShowNotice: false,
|
||||
noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip,
|
||||
showUpcomingNotice: false,
|
||||
noticeVisibilityMode: NoticeVisibilityMode.FadedForAutoSkip,
|
||||
hideVideoPlayerControls: false,
|
||||
hideInfoButtonPlayerControls: false,
|
||||
hideDeleteButtonPlayerControls: false,
|
||||
@@ -309,7 +313,6 @@ const syncDefaults = {
|
||||
audioNotificationOnSkip: false,
|
||||
checkForUnlistedVideos: false,
|
||||
testingServer: false,
|
||||
refetchWhenNotFound: true,
|
||||
ytInfoPermissionGranted: false,
|
||||
allowExpirements: true,
|
||||
showDonationLink: true,
|
||||
@@ -326,6 +329,7 @@ const syncDefaults = {
|
||||
showCategoryGuidelines: true,
|
||||
showCategoryWithoutPermission: false,
|
||||
showSegmentNameInChapterBar: true,
|
||||
showAutogeneratedChapters: true,
|
||||
useVirtualTime: true,
|
||||
showSegmentFailedToFetchWarning: true,
|
||||
allowScrollingToEdit: true,
|
||||
@@ -354,6 +358,8 @@ const syncDefaults = {
|
||||
nextChapterKeybind: { key: "ArrowRight", ctrl: true },
|
||||
previousChapterKeybind: { key: "ArrowLeft", ctrl: true },
|
||||
closeSkipNoticeKeybind: { key: "Backspace" },
|
||||
downvoteKeybind: { key: "h", shift: true },
|
||||
upvoteKeybind: { key: "g", shift: true },
|
||||
|
||||
categorySelections: [{
|
||||
name: "sponsor" as Category,
|
||||
@@ -464,7 +470,11 @@ const syncDefaults = {
|
||||
"preview-filler": {
|
||||
color: "#2E0066",
|
||||
opacity: "0.7"
|
||||
}
|
||||
},
|
||||
"chapter": {
|
||||
color: "#fff",
|
||||
opacity: "0"
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
282
src/content.ts
282
src/content.ts
@@ -20,11 +20,12 @@ import Utils from "./utils";
|
||||
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
|
||||
import SkipNotice from "./render/SkipNotice";
|
||||
import SkipNoticeComponent from "./components/SkipNoticeComponent";
|
||||
import UpcomingNotice from "./render/UpcomingNotice";
|
||||
import SubmissionNotice from "./render/SubmissionNotice";
|
||||
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
|
||||
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||
import { getControls, getExistingChapters, getHashParams, isPlayingPlaylist, isVisible } from "./utils/pageUtils";
|
||||
import { getControls, getExistingChapters, getHashParams, hasAutogeneratedChapters, isPlayingPlaylist, isVisible } from "./utils/pageUtils";
|
||||
import { CategoryPill } from "./render/CategoryPill";
|
||||
import { AnimationUtils } from "../maze-utils/src/animationUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
@@ -34,7 +35,7 @@ import { ChapterVote } from "./render/ChapterVote";
|
||||
import { openWarningDialog } from "./utils/warnings";
|
||||
import { isFirefoxOrSafari, waitFor } from "../maze-utils/src";
|
||||
import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating";
|
||||
import { getChannelIDInfo, getVideo, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID, isOnInvidious, isOnMobileYouTube, getLastNonInlineVideoID, triggerVideoIDChange, triggerVideoElementChange, getIsInline, getCurrentTime, setCurrentTime, getVideoDuration, verifyCurrentTime } from "../maze-utils/src/video";
|
||||
import { getChannelIDInfo, getVideo, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID, isOnInvidious, isOnMobileYouTube, isOnYTTV, getLastNonInlineVideoID, triggerVideoIDChange, triggerVideoElementChange, getIsInline, getCurrentTime, setCurrentTime, getVideoDuration, verifyCurrentTime, waitForVideo } from "../maze-utils/src/video";
|
||||
import { Keybind, StorageChangesObject, isSafari, keybindEquals, keybindToString } from "../maze-utils/src/config";
|
||||
import { findValidElement } from "../maze-utils/src/dom"
|
||||
import { getHash, HashedValue } from "../maze-utils/src/hash";
|
||||
@@ -42,7 +43,7 @@ import { generateUserID } from "../maze-utils/src/setup";
|
||||
import { updateAll } from "../maze-utils/src/thumbnailManagement";
|
||||
import { setupThumbnailListener } from "./utils/thumbnails";
|
||||
import * as documentScript from "../dist/js/document.js";
|
||||
import { runCompatibilityChecks } from "./utils/compatibility";
|
||||
import { isVorapisInstalled, runCompatibilityChecks } from "./utils/compatibility";
|
||||
import { cleanPage } from "./utils/pageCleaner";
|
||||
import { addCleanupListener } from "../maze-utils/src/cleanup";
|
||||
import { hideDeArrowPromotion, tryShowingDeArrowPromotion } from "./dearrowPromotion";
|
||||
@@ -75,11 +76,10 @@ let sponsorTimes: SponsorTime[] = [];
|
||||
let existingChaptersImported = false;
|
||||
let importingChaptersWaitingForFocus = false;
|
||||
let importingChaptersWaiting = false;
|
||||
let triedImportingChapters = false;
|
||||
// List of open skip notices
|
||||
const skipNotices: SkipNotice[] = [];
|
||||
let upcomingNotice: UpcomingNotice | null = null;
|
||||
let activeSkipKeybindElement: ToggleSkippable = null;
|
||||
let retryFetchTimeout: NodeJS.Timeout = null;
|
||||
let shownSegmentFailedToFetchWarning = false;
|
||||
let selectedSegment: SegmentUUID | null = null;
|
||||
let previewedSegment = false;
|
||||
@@ -108,6 +108,7 @@ const lastNextChapterKeybind = {
|
||||
let currentSkipSchedule: NodeJS.Timeout = null;
|
||||
let currentSkipInterval: NodeJS.Timeout = null;
|
||||
let currentVirtualTimeInterval: NodeJS.Timeout = null;
|
||||
let currentUpcomingSchedule: NodeJS.Timeout = null;
|
||||
|
||||
/** Has the sponsor been skipped */
|
||||
let sponsorSkipped: boolean[] = [];
|
||||
@@ -171,7 +172,6 @@ let popupInitialised = false;
|
||||
let submissionNotice: SubmissionNotice = null;
|
||||
|
||||
let lastResponseStatus: number;
|
||||
let retryCount = 0;
|
||||
|
||||
// Contains all of the functions and variables needed by the skip notice
|
||||
const skipNoticeContentContainer: ContentContainer = () => ({
|
||||
@@ -240,7 +240,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
break;
|
||||
case "getChannelID":
|
||||
sendResponse({
|
||||
channelID: getChannelIDInfo().id
|
||||
channelID: getChannelIDInfo().id,
|
||||
isYTTV: (document.location.host === "tv.youtube.com")
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -306,7 +307,8 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
for (const segment of importedSegments) {
|
||||
if (!sponsorTimesSubmitting.some(
|
||||
(s) => Math.abs(s.segment[0] - segment.segment[0]) < 1
|
||||
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)) {
|
||||
&& Math.abs(s.segment[1] - segment.segment[1]) < 1
|
||||
&& s.description === segment.description)) {
|
||||
const hasChaptersPermission = (Config.config.showCategoryWithoutPermission
|
||||
|| Config.config.permissions["chapter"]);
|
||||
if (segment.category === "chapter" && (!utils.getCategorySelection("chapter") || !hasChaptersPermission)) {
|
||||
@@ -389,13 +391,11 @@ if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
|
||||
function resetValues() {
|
||||
lastCheckTime = 0;
|
||||
lastCheckVideoTime = -1;
|
||||
retryCount = 0;
|
||||
previewedSegment = false;
|
||||
firstPlay = true;
|
||||
|
||||
sponsorTimes = [];
|
||||
existingChaptersImported = false;
|
||||
triedImportingChapters = false;
|
||||
sponsorSkipped = [];
|
||||
lastResponseStatus = 0;
|
||||
shownSegmentFailedToFetchWarning = false;
|
||||
@@ -428,6 +428,11 @@ function resetValues() {
|
||||
skipNotices.pop()?.close();
|
||||
}
|
||||
|
||||
if (upcomingNotice) {
|
||||
upcomingNotice.close();
|
||||
upcomingNotice = null;
|
||||
}
|
||||
|
||||
hideDeArrowPromotion();
|
||||
}
|
||||
|
||||
@@ -512,12 +517,8 @@ function handleMobileControlsMutations(): void {
|
||||
|
||||
function getPreviewBarAttachElement(): HTMLElement | null {
|
||||
const progressElementOptions = [{
|
||||
// For new mobile YouTube (#1287)
|
||||
selector: ".progress-bar-line",
|
||||
isVisibleCheck: true
|
||||
}, {
|
||||
// For newer mobile YouTube (Jan 2024)
|
||||
selector: ".YtProgressBarProgressBarLine",
|
||||
// For newer mobile YouTube (Sept 2024)
|
||||
selector: ".ytChapteredProgressBarHost, .ytProgressBarLineHost, .YtProgressBarLineHost, .YtChapteredProgressBarHost",
|
||||
isVisibleCheck: true
|
||||
}, {
|
||||
// For newer mobile YouTube (May 2024)
|
||||
@@ -550,6 +551,13 @@ function getPreviewBarAttachElement(): HTMLElement | null {
|
||||
// For piped
|
||||
selector: ".shaka-ad-markers",
|
||||
isVisibleCheck: false
|
||||
}, {
|
||||
// For Vorapis v3
|
||||
selector: ".ytp-progress-bar-container > .html5-progress-bar > .ytp-progress-list"
|
||||
}, {
|
||||
// For YTTV
|
||||
selector: ".yssi-slider > div.ytu-ss-timeline-container",
|
||||
isVisibleCheck: false
|
||||
}
|
||||
];
|
||||
|
||||
@@ -575,7 +583,7 @@ function createPreviewBar(): void {
|
||||
|
||||
if (el) {
|
||||
const chapterVote = new ChapterVote(voteAsync);
|
||||
previewBar = new PreviewBar(el, isOnMobileYouTube(), isOnInvidious(), chapterVote, () => importExistingChapters(true));
|
||||
previewBar = new PreviewBar(el, isOnMobileYouTube(), isOnInvidious(), isOnYTTV(), chapterVote, () => importExistingChapters(true));
|
||||
|
||||
updatePreviewBar();
|
||||
}
|
||||
@@ -612,6 +620,11 @@ function cancelSponsorSchedule(): void {
|
||||
clearInterval(currentSkipInterval);
|
||||
currentSkipInterval = null;
|
||||
}
|
||||
|
||||
if (currentUpcomingSchedule !== null) {
|
||||
clearTimeout(currentUpcomingSchedule);
|
||||
currentUpcomingSchedule = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -750,7 +763,8 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
|
||||
skippingFunction(currentTime);
|
||||
} else {
|
||||
let delayTime = timeUntilSponsor * 1000 * (1 / getVideo().playbackRate);
|
||||
if (delayTime < (isFirefoxOrSafari() && !isSafari() ? 750 : 300)) {
|
||||
if (delayTime < (isFirefoxOrSafari() && !isSafari() ? 750 : 300)
|
||||
&& shouldAutoSkip(skippingSegments[0])) {
|
||||
let forceStartIntervalTime: number | null = null;
|
||||
if (isFirefoxOrSafari() && !isSafari() && delayTime > 300) {
|
||||
forceStartIntervalTime = await waitForNextTimeChange();
|
||||
@@ -758,7 +772,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
|
||||
|
||||
// Use interval instead of timeout near the end to combat imprecise video time
|
||||
const startIntervalTime = forceStartIntervalTime || performance.now();
|
||||
const startVideoTime = Math.max(currentTime, getCurrentTime());
|
||||
const startVideoTime = Math.max(currentTime, getVirtualTime());
|
||||
delayTime = (skipTime?.[0] - startVideoTime) * 1000 * (1 / getVideo().playbackRate);
|
||||
|
||||
let startWaitingForReportedTimeToChange = true;
|
||||
@@ -777,7 +791,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
|
||||
}
|
||||
|
||||
const intervalDuration = performance.now() - startIntervalTime;
|
||||
if (intervalDuration + skipBuffer * 1000 >= delayTime || getCurrentTime() >= skipTime[0]) {
|
||||
if (intervalDuration + skipBuffer * 1000 >= delayTime || getVirtualTime() + skipBuffer >= skipTime[0]) {
|
||||
clearInterval(currentSkipInterval);
|
||||
if (!isFirefoxOrSafari() && !getVideo().muted && !inMuteSegment(getCurrentTime(), true)) {
|
||||
// Workaround for more accurate skipping on Chromium
|
||||
@@ -785,7 +799,7 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
|
||||
getVideo().muted = false;
|
||||
}
|
||||
|
||||
skippingFunction(Math.max(getCurrentTime(), startVideoTime + getVideo().playbackRate * Math.max(delayTime, intervalDuration) / 1000));
|
||||
skippingFunction(Math.max(getVirtualTime(), startVideoTime + getVideo().playbackRate * Math.max(delayTime, intervalDuration) / 1000));
|
||||
}
|
||||
}, 0);
|
||||
} else {
|
||||
@@ -793,7 +807,22 @@ async function startSponsorSchedule(includeIntersectingSegments = false, current
|
||||
|
||||
const offset = (isFirefoxOrSafari() && !isSafari() ? 600 : 150);
|
||||
// Schedule for right before to be more precise than normal timeout
|
||||
currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - offset));
|
||||
const offsetDelayTime = Math.max(0, delayTime - offset);
|
||||
currentSkipSchedule = setTimeout(skippingFunction, offsetDelayTime);
|
||||
|
||||
// Show the notice only if the segment hasn't already started
|
||||
if (Config.config.showUpcomingNotice && getCurrentTime() < skippingSegments[0].segment[0]
|
||||
&& !sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)
|
||||
&& [ActionType.Skip, ActionType.Mute].includes(skippingSegments[0].actionType)
|
||||
&& !getVideo()?.paused) {
|
||||
const maxPopupTime = 3000;
|
||||
const timeUntilPopup = Math.max(0, offsetDelayTime - maxPopupTime);
|
||||
const popupTime = offsetDelayTime - timeUntilPopup;
|
||||
const autoSkip = shouldAutoSkip(skippingSegments[0]);
|
||||
|
||||
if (currentUpcomingSchedule) clearTimeout(currentUpcomingSchedule);
|
||||
currentUpcomingSchedule = setTimeout(createUpcomingNotice, timeUntilPopup, [skippingSegments[0]], popupTime, autoSkip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -810,10 +839,10 @@ function waitForNextTimeChange(): Promise<DOMHighResTimeStamp | null> {
|
||||
|
||||
function getVirtualTime(): number {
|
||||
const virtualTime = lastTimeFromWaitingEvent ?? (lastKnownVideoTime.videoTime !== null ?
|
||||
(performance.now() - lastKnownVideoTime.preciseTime) * getVideo().playbackRate / 1000 + lastKnownVideoTime.videoTime : null);
|
||||
(performance.now() - lastKnownVideoTime.preciseTime) * (getVideo()?.playbackRate || 1) / 1000 + lastKnownVideoTime.videoTime : null);
|
||||
|
||||
if (Config.config.useVirtualTime && !isSafari() && virtualTime
|
||||
&& Math.abs(virtualTime - getCurrentTime()) < 0.2 && getCurrentTime() !== 0) {
|
||||
&& virtualTime > getCurrentTime() && virtualTime - getCurrentTime() < 0.8 && getCurrentTime() !== 0) {
|
||||
return Math.max(virtualTime, getCurrentTime());
|
||||
} else {
|
||||
return getCurrentTime();
|
||||
@@ -856,8 +885,8 @@ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boole
|
||||
let playbackRateCheckInterval: NodeJS.Timeout | null = null;
|
||||
let lastPlaybackSpeed = 1;
|
||||
let setupVideoListenersFirstTime = true;
|
||||
function setupVideoListeners() {
|
||||
const video = getVideo();
|
||||
function setupVideoListeners(video: HTMLVideoElement) {
|
||||
if (!video) return; // Maybe video became invisible
|
||||
|
||||
//wait until it is loaded
|
||||
video.addEventListener('loadstart', videoOnReadyListener)
|
||||
@@ -1123,7 +1152,8 @@ function setupSkipButtonControlBar() {
|
||||
forceAutoSkip: true
|
||||
}),
|
||||
selectSegment,
|
||||
onMobileYouTube: isOnMobileYouTube()
|
||||
onMobileYouTube: isOnMobileYouTube(),
|
||||
onYTTV: isOnYTTV(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1139,7 +1169,7 @@ function setupCategoryPill() {
|
||||
}
|
||||
|
||||
async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
|
||||
const videoID = getVideoID()
|
||||
const videoID = getVideoID();
|
||||
if (!videoID) {
|
||||
console.error("[SponsorBlock] Attempted to fetch segments with a null/undefined videoID.");
|
||||
return;
|
||||
@@ -1147,6 +1177,9 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
|
||||
|
||||
const segmentData = await getSegmentsForVideo(videoID, ignoreCache);
|
||||
|
||||
// Make sure an old pending request doesn't get used.
|
||||
if (videoID !== getVideoID()) return;
|
||||
|
||||
// store last response status
|
||||
lastResponseStatus = segmentData.status;
|
||||
if (segmentData.status === 200) {
|
||||
@@ -1205,7 +1238,7 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
|
||||
|
||||
if (!getVideo()) {
|
||||
//there is still no video here
|
||||
await waitFor(() => getVideo(), 5000, 10);
|
||||
await waitForVideo();
|
||||
}
|
||||
|
||||
startSkipScheduleCheckingForStartSponsors();
|
||||
@@ -1213,11 +1246,7 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
|
||||
if (!isNaN(getVideoDuration())) {
|
||||
updatePreviewBar();
|
||||
}
|
||||
} else {
|
||||
retryFetch(404);
|
||||
}
|
||||
} else {
|
||||
retryFetch(lastResponseStatus);
|
||||
}
|
||||
|
||||
importExistingChapters(true);
|
||||
@@ -1238,7 +1267,7 @@ async function sponsorsLookup(keepOldSubmissions = true, ignoreCache = false) {
|
||||
}
|
||||
|
||||
function importExistingChapters(wait: boolean) {
|
||||
if (!existingChaptersImported && !importingChaptersWaiting && !triedImportingChapters && onVideoPage() && !isOnMobileYouTube()) {
|
||||
if (!existingChaptersImported && !importingChaptersWaiting && onVideoPage() && !isOnMobileYouTube()) {
|
||||
const waitCondition = () => getVideoDuration() && getExistingChapters(getVideoID(), getVideoDuration());
|
||||
|
||||
if (wait && !document.hasFocus() && !importingChaptersWaitingForFocus && !waitCondition()) {
|
||||
@@ -1259,7 +1288,13 @@ function importExistingChapters(wait: boolean) {
|
||||
existingChaptersImported = true;
|
||||
updatePreviewBar();
|
||||
}
|
||||
}).catch(() => { importingChaptersWaiting = false; triedImportingChapters = true; }); // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
}).catch(() => { importingChaptersWaiting = false; });
|
||||
|
||||
if (!Config.config.showAutogeneratedChapters) {
|
||||
waitFor(() => hasAutogeneratedChapters(), wait ? 15000 : 0, 400).then(() => {
|
||||
updateActiveSegment(getCurrentTime());
|
||||
}).catch(() => { }); // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1278,27 +1313,6 @@ async function lockedCategoriesLookup(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function retryFetch(errorCode: number): void {
|
||||
sponsorDataFound = false;
|
||||
if (!Config.config.refetchWhenNotFound) return;
|
||||
|
||||
if (retryFetchTimeout) clearTimeout(retryFetchTimeout);
|
||||
if ((errorCode !== 404 && retryCount > 1) || (errorCode !== 404 && retryCount > 10)) {
|
||||
// Too many errors (50x), give up
|
||||
return;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
|
||||
const delay = errorCode === 404 ? (30000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
|
||||
retryFetchTimeout = setTimeout(() => {
|
||||
if (getVideoID() && sponsorTimes?.length === 0
|
||||
|| sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
|
||||
// sponsorsLookup();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only should be used when it is okay to skip a sponsor when in the middle of it
|
||||
*
|
||||
@@ -1350,7 +1364,9 @@ function startSkipScheduleCheckingForStartSponsors() {
|
||||
|
||||
const fullVideoSegment = sponsorTimes.filter((time) => time.actionType === ActionType.Full)[0];
|
||||
if (fullVideoSegment) {
|
||||
categoryPill?.setSegment(fullVideoSegment);
|
||||
waitFor(() => categoryPill).then(() => {
|
||||
categoryPill?.setSegment(fullVideoSegment);
|
||||
});
|
||||
}
|
||||
|
||||
if (startingSegmentTime !== -1) {
|
||||
@@ -1391,22 +1407,24 @@ function updatePreviewBar(): void {
|
||||
showLarger: segment.actionType === ActionType.Poi,
|
||||
description: segment.description,
|
||||
source: segment.source,
|
||||
requiredSegment: requiredSegment && (segment.UUID === requiredSegment || segment.UUID?.startsWith(requiredSegment)),
|
||||
requiredSegment: requiredSegment && (segment.UUID === requiredSegment || segment.UUID?.startsWith(requiredSegment) || requiredSegment.startsWith(segment.UUID)),
|
||||
selectedSegment: selectedSegment && segment.UUID === selectedSegment
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sponsorTimesSubmitting.forEach((segment) => {
|
||||
previewBarSegments.push({
|
||||
segment: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
actionType: segment.actionType,
|
||||
unsubmitted: true,
|
||||
showLarger: segment.actionType === ActionType.Poi,
|
||||
description: segment.description,
|
||||
source: segment.source
|
||||
});
|
||||
if (segment.actionType !== ActionType.Chapter || segment.segment.length > 1) {
|
||||
previewBarSegments.push({
|
||||
segment: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
actionType: segment.actionType,
|
||||
unsubmitted: true,
|
||||
showLarger: segment.actionType === ActionType.Poi,
|
||||
description: segment.description,
|
||||
source: segment.source
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), getVideoDuration())
|
||||
@@ -1435,10 +1453,10 @@ async function channelIDChange(channelIDInfo: ChannelIDInfo) {
|
||||
if (Config.config.forceChannelCheck && sponsorTimes?.length > 0) startSkipScheduleCheckingForStartSponsors();
|
||||
}
|
||||
|
||||
function videoElementChange(newVideo: boolean): void {
|
||||
function videoElementChange(newVideo: boolean, video: HTMLVideoElement): void {
|
||||
waitFor(() => Config.isReady()).then(() => {
|
||||
if (newVideo) {
|
||||
setupVideoListeners();
|
||||
setupVideoListeners(video);
|
||||
setupSkipButtonControlBar();
|
||||
setupCategoryPill();
|
||||
}
|
||||
@@ -1669,7 +1687,7 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped:
|
||||
counted = true;
|
||||
}
|
||||
|
||||
if (fullSkip) asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID);
|
||||
if (fullSkip) asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID + "&videoID=" + getVideoID());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1723,7 +1741,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
|
||||
|
||||
if (autoSkip && Config.config.audioNotificationOnSkip
|
||||
&& !isSubmittingSegment && !getVideo()?.muted) {
|
||||
const beep = new Audio(chrome.runtime.getURL("icons/beep.ogg"));
|
||||
const beep = new Audio(chrome.runtime.getURL("icons/beep.oga"));
|
||||
beep.volume = getVideo().volume * 0.1;
|
||||
const oldMetadata = navigator.mediaSession.metadata
|
||||
beep.play();
|
||||
@@ -1739,11 +1757,13 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
|
||||
if (!autoSkip
|
||||
&& skippingSegments.length === 1
|
||||
&& skippingSegments[0].actionType === ActionType.Poi) {
|
||||
skipButtonControlBar.enable(skippingSegments[0]);
|
||||
if (isOnMobileYouTube() || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false);
|
||||
|
||||
activeSkipKeybindElement?.setShowKeybindHint(false);
|
||||
activeSkipKeybindElement = skipButtonControlBar;
|
||||
waitFor(() => skipButtonControlBar).then(() => {
|
||||
skipButtonControlBar.enable(skippingSegments[0]);
|
||||
if (isOnMobileYouTube() || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false);
|
||||
|
||||
activeSkipKeybindElement?.setShowKeybindHint(false);
|
||||
activeSkipKeybindElement = skipButtonControlBar;
|
||||
})
|
||||
} else {
|
||||
if (openNotice) {
|
||||
//send out the message saying that a sponsor message was skipped
|
||||
@@ -1767,7 +1787,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
|
||||
if (autoSkip || isSubmittingSegment) sendTelemetryAndCount(skippingSegments, skipTime[1] - skipTime[0], true);
|
||||
}
|
||||
|
||||
function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, unskipTime: number, startReskip: boolean) {
|
||||
function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, unskipTime: number, startReskip: boolean, voteNotice = false) {
|
||||
for (const skipNotice of skipNotices) {
|
||||
if (skippingSegments.length === skipNotice.segments.length
|
||||
&& skippingSegments.every((segment) => skipNotice.segments.some((s) => s.UUID === segment.UUID))) {
|
||||
@@ -1776,7 +1796,12 @@ function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, un
|
||||
}
|
||||
}
|
||||
|
||||
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, unskipTime, startReskip);
|
||||
const upcomingNoticeShown = !!upcomingNotice && !upcomingNotice.closed;
|
||||
|
||||
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, () => {
|
||||
upcomingNotice?.close();
|
||||
upcomingNotice = null;
|
||||
}, unskipTime, startReskip, upcomingNoticeShown, voteNotice);
|
||||
if (isOnMobileYouTube() || Config.config.skipKeybind == null) newSkipNotice.setShowKeybindHint(false);
|
||||
skipNotices.push(newSkipNotice);
|
||||
|
||||
@@ -1784,13 +1809,24 @@ function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, un
|
||||
activeSkipKeybindElement = newSkipNotice;
|
||||
}
|
||||
|
||||
function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forceSeek = false) {
|
||||
function createUpcomingNotice(skippingSegments: SponsorTime[], timeLeft: number, autoSkip: boolean): void {
|
||||
if (upcomingNotice
|
||||
&& !upcomingNotice.closed
|
||||
&& upcomingNotice.sameNotice(skippingSegments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
upcomingNotice?.close();
|
||||
upcomingNotice = new UpcomingNotice(skippingSegments, skipNoticeContentContainer, timeLeft / 1000, autoSkip);
|
||||
}
|
||||
|
||||
function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forceSeek = false, voteNotice = false) {
|
||||
if (segment.actionType === ActionType.Mute) {
|
||||
getVideo().muted = false;
|
||||
videoMuted = false;
|
||||
}
|
||||
|
||||
if (forceSeek || segment.actionType === ActionType.Skip) {
|
||||
if (forceSeek || segment.actionType === ActionType.Skip || voteNotice) {
|
||||
//add a tiny bit of time to make sure it is not skipped again
|
||||
setCurrentTime(unskipTime ?? segment.segment[0] + 0.001);
|
||||
}
|
||||
@@ -1822,6 +1858,10 @@ function createButton(baseID: string, title: string, callback: () => void, image
|
||||
newButton.id = baseID + "Button";
|
||||
newButton.classList.add("playerButton");
|
||||
newButton.classList.add("ytp-button");
|
||||
if (isOnYTTV()) {
|
||||
// Some style needs to be set here, but the numbers don't matter
|
||||
newButton.setAttribute("style", "width: 40px; height: 40px");
|
||||
}
|
||||
newButton.setAttribute("title", chrome.i18n.getMessage(title));
|
||||
newButton.addEventListener("click", () => {
|
||||
callback();
|
||||
@@ -1896,7 +1936,7 @@ async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
|
||||
updateEditButtonsOnPlayer();
|
||||
|
||||
// Don't show the info button on embeds
|
||||
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || isOnInvidious()
|
||||
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || isOnInvidious() || isOnYTTV()
|
||||
|| document.getElementById("sponsorBlockPopupContainer") != null) {
|
||||
playerButtons.info.button.style.display = "none";
|
||||
} else {
|
||||
@@ -1963,6 +2003,11 @@ function getRealCurrentTime(): number {
|
||||
}
|
||||
|
||||
function startOrEndTimingNewSegment() {
|
||||
if (isOnYTTV() && getIsLivePremiere()) {
|
||||
alert(chrome.i18n.getMessage("yttvLiveContentWarning"));
|
||||
return;
|
||||
}
|
||||
|
||||
verifyCurrentTime();
|
||||
const roundedTime = Math.round((getRealCurrentTime() + Number.EPSILON) * 1000) / 1000;
|
||||
if (!isSegmentCreationInProgress()) {
|
||||
@@ -2241,7 +2286,8 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
|
||||
message: "submitVote",
|
||||
type: type,
|
||||
UUID: UUID,
|
||||
category: category
|
||||
category: category,
|
||||
videoID: getVideoID()
|
||||
}, (response) => {
|
||||
if (response.successType === 1) {
|
||||
// Change the sponsor locally
|
||||
@@ -2457,6 +2503,8 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
|
||||
}
|
||||
|
||||
function updateActiveSegment(currentTime: number): void {
|
||||
if (!chrome.runtime?.id) return;
|
||||
|
||||
previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
@@ -2504,6 +2552,23 @@ function previousChapter(): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKeybindVote(type: number): Promise<void>{
|
||||
let lastSkipNotice = skipNotices[0]?.skipNoticeRef.current;
|
||||
lastSkipNotice?.onMouseEnter();
|
||||
|
||||
if (!lastSkipNotice) {
|
||||
const lastSegment = [...sponsorTimes].reverse()?.find((s) => s.source == SponsorSourceType.Server && (s.segment[0] <= getCurrentTime() && getCurrentTime() - (s.segment[1] || s.segment[0]) <= Config.config.skipNoticeDuration));
|
||||
if (!lastSegment) return;
|
||||
|
||||
createSkipNotice([lastSegment], shouldAutoSkip(lastSegment), lastSegment?.segment[0] + 0.001,false, true);
|
||||
lastSkipNotice = await skipNotices[0].waitForSkipNoticeRef();
|
||||
lastSkipNotice?.reskippedMode(0);
|
||||
}
|
||||
|
||||
vote(type,lastSkipNotice?.segments[0]?.UUID, undefined, lastSkipNotice);
|
||||
return;
|
||||
}
|
||||
|
||||
function addHotkeyListener(): void {
|
||||
document.addEventListener("keydown", hotkeyListener);
|
||||
|
||||
@@ -2525,8 +2590,10 @@ function addHotkeyListener(): void {
|
||||
}
|
||||
|
||||
function hotkeyListener(e: KeyboardEvent): void {
|
||||
if (["textarea", "input"].includes(document.activeElement?.tagName?.toLowerCase())
|
||||
|| document.activeElement?.id?.toLowerCase()?.includes("editable")) return;
|
||||
if ((["textarea", "input"].includes(document.activeElement?.tagName?.toLowerCase())
|
||||
|| (document.activeElement as HTMLElement)?.isContentEditable
|
||||
|| document.activeElement?.id?.toLowerCase()?.match(/editable|input/))
|
||||
&& document.hasFocus()) return;
|
||||
|
||||
const key: Keybind = {
|
||||
key: e.key,
|
||||
@@ -2545,6 +2612,8 @@ function hotkeyListener(e: KeyboardEvent): void {
|
||||
const openSubmissionMenuKey = Config.config.submitKeybind;
|
||||
const nextChapterKey = Config.config.nextChapterKeybind;
|
||||
const previousChapterKey = Config.config.previousChapterKeybind;
|
||||
const upvoteKey = Config.config.upvoteKeybind;
|
||||
const downvoteKey = Config.config.downvoteKeybind;
|
||||
|
||||
if (keybindEquals(key, skipKey)) {
|
||||
if (activeSkipKeybindElement) {
|
||||
@@ -2562,7 +2631,9 @@ function hotkeyListener(e: KeyboardEvent): void {
|
||||
for (let i = 0; i < skipNotices.length; i++) {
|
||||
skipNotices.pop().close();
|
||||
}
|
||||
|
||||
|
||||
upcomingNotice?.close();
|
||||
upcomingNotice = null;
|
||||
return;
|
||||
} else if (keybindEquals(key, startSponsorKey)) {
|
||||
startOrEndTimingNewSegment();
|
||||
@@ -2571,6 +2642,8 @@ function hotkeyListener(e: KeyboardEvent): void {
|
||||
submitSegments();
|
||||
return;
|
||||
} else if (keybindEquals(key, openSubmissionMenuKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
openSubmissionMenu();
|
||||
return;
|
||||
} else if (keybindEquals(key, previewKey)) {
|
||||
@@ -2584,16 +2657,12 @@ function hotkeyListener(e: KeyboardEvent): void {
|
||||
if (sponsorTimes.length > 0) e.stopPropagation();
|
||||
previousChapter();
|
||||
return;
|
||||
}
|
||||
|
||||
//legacy - to preserve keybinds for skipKey, startSponsorKey and submitKey for people who set it before the update. (shouldn't be changed for future keybind options)
|
||||
if (key.key == skipKey?.key && skipKey.code == null && !keybindEquals(Config.syncDefaults.skipKeybind, skipKey)) {
|
||||
if (activeSkipKeybindElement)
|
||||
activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
|
||||
} else if (key.key == startSponsorKey?.key && startSponsorKey.code == null && !keybindEquals(Config.syncDefaults.startSponsorKeybind, startSponsorKey)) {
|
||||
startOrEndTimingNewSegment();
|
||||
} else if (key.key == submitKey?.key && submitKey.code == null && !keybindEquals(Config.syncDefaults.submitKeybind, submitKey)) {
|
||||
openSubmissionMenu();
|
||||
} else if (keybindEquals(key, upvoteKey)) {
|
||||
handleKeybindVote(1);
|
||||
return;
|
||||
} else if (keybindEquals(key, downvoteKey)) {
|
||||
handleKeybindVote(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2642,11 +2711,12 @@ function showTimeWithoutSkips(skippedDuration: number): void {
|
||||
}
|
||||
|
||||
// YouTube player time display
|
||||
const displayClass =
|
||||
isOnInvidious() ? "vjs-duration" :
|
||||
isOnMobileYouTube() ? "ytm-time-display" :
|
||||
"ytp-time-display.notranslate";
|
||||
const display = document.querySelector(`.${displayClass}`);
|
||||
const selector =
|
||||
isOnInvidious() ? ".vjs-duration" :
|
||||
isOnYTTV() ? ".ypl-full-controls .ypmcs-control .time-info-bar" :
|
||||
isOnMobileYouTube() ? ".ytwPlayerTimeDisplayContent" :
|
||||
".ytp-time-display.notranslate .ytp-time-wrapper";
|
||||
const display = document.querySelector(selector);
|
||||
if (!display) return;
|
||||
|
||||
const durationID = "sponsorBlockDurationAfterSkips";
|
||||
@@ -2656,9 +2726,13 @@ function showTimeWithoutSkips(skippedDuration: number): void {
|
||||
if (duration === null) {
|
||||
duration = document.createElement('span');
|
||||
duration.id = durationID;
|
||||
if (!isOnInvidious()) duration.classList.add(displayClass);
|
||||
|
||||
display.appendChild(duration);
|
||||
if (isOnMobileYouTube()) {
|
||||
duration.style.paddingLeft = "4px";
|
||||
display.insertBefore(duration, display.lastChild);
|
||||
} else {
|
||||
display.appendChild(duration);
|
||||
}
|
||||
}
|
||||
|
||||
const durationAfterSkips = getFormattedTime(getVideoDuration() - skippedDuration);
|
||||
@@ -2705,6 +2779,10 @@ function setCategoryColorCSSVariables() {
|
||||
if (!styleContainer) {
|
||||
styleContainer = document.createElement("style");
|
||||
styleContainer.id = "sbCategoryColorStyle";
|
||||
if (isVorapisInstalled()) {
|
||||
// Vorapi deletes styles
|
||||
styleContainer.className = 'stylus';
|
||||
}
|
||||
|
||||
const head = (document.head || document.documentElement);
|
||||
head.appendChild(styleContainer)
|
||||
|
||||
@@ -13,7 +13,9 @@ import { DEFAULT_CATEGORY, shortCategoryName } from "../utils/categoryUtils";
|
||||
import { normalizeChapterName } from "../utils/exporter";
|
||||
import { findValidElement } from "../../maze-utils/src/dom";
|
||||
import { addCleanupListener } from "../../maze-utils/src/cleanup";
|
||||
import { isVisible } from "../utils/pageUtils";
|
||||
import { hasAutogeneratedChapters, isVisible } from "../utils/pageUtils";
|
||||
import { isVorapisInstalled } from "../utils/compatibility";
|
||||
import { isOnYTTV } from "../../maze-utils/src/video";
|
||||
|
||||
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||
const MIN_CHAPTER_SIZE = 0.003;
|
||||
@@ -40,6 +42,12 @@ class PreviewBar {
|
||||
categoryTooltip?: HTMLDivElement;
|
||||
categoryTooltipContainer?: HTMLElement;
|
||||
chapterTooltip?: HTMLDivElement;
|
||||
|
||||
// ScrubTooltips for YTTV only
|
||||
categoryScrubTooltip?: HTMLDivElement;
|
||||
categoryScrubTooltipContainer?: HTMLElement;
|
||||
chapterScrubTooltip?: HTMLDivElement;
|
||||
|
||||
lastSmallestSegment: Record<string, {
|
||||
index: number;
|
||||
segment: PreviewBarSegment;
|
||||
@@ -48,9 +56,11 @@ class PreviewBar {
|
||||
parent: HTMLElement;
|
||||
onMobileYouTube: boolean;
|
||||
onInvidious: boolean;
|
||||
onYTTV: boolean;
|
||||
progressBar: HTMLElement;
|
||||
|
||||
segments: PreviewBarSegment[] = [];
|
||||
hasYouTubeChapters = false;
|
||||
existingChapters: PreviewBarSegment[] = [];
|
||||
videoDuration = 0;
|
||||
updateExistingChapters: () => void;
|
||||
@@ -68,14 +78,19 @@ class PreviewBar {
|
||||
unfilteredChapterGroups: ChapterGroup[];
|
||||
chapterGroups: ChapterGroup[];
|
||||
|
||||
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
|
||||
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, onYTTV: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
|
||||
if (test) return;
|
||||
this.container = document.createElement('ul');
|
||||
this.container.id = 'previewbar';
|
||||
|
||||
if (onYTTV) {
|
||||
this.container.classList.add("sponsorblock-yttv-container");
|
||||
}
|
||||
|
||||
this.parent = parent;
|
||||
this.onMobileYouTube = onMobileYouTube;
|
||||
this.onInvidious = onInvidious;
|
||||
this.onYTTV = onYTTV;
|
||||
this.chapterVote = chapterVote;
|
||||
this.updateExistingChapters = updateExistingChapters;
|
||||
|
||||
@@ -95,24 +110,49 @@ class PreviewBar {
|
||||
|
||||
// Create label placeholder
|
||||
this.categoryTooltip = document.createElement("div");
|
||||
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||
if (isOnYTTV()) {
|
||||
this.categoryTooltip.className = "sponsorCategoryTooltip";
|
||||
} else {
|
||||
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||
}
|
||||
this.chapterTooltip = document.createElement("div");
|
||||
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||
if (isOnYTTV()) {
|
||||
this.chapterTooltip.className = "sponsorCategoryTooltip";
|
||||
} else {
|
||||
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||
}
|
||||
|
||||
// global chaper tooltip or duration tooltip
|
||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
|
||||
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip)") as HTMLElement;
|
||||
if (isOnYTTV()) {
|
||||
this.categoryScrubTooltip = document.createElement("div");
|
||||
this.categoryScrubTooltip.className = "sponsorCategoryTooltip";
|
||||
this.chapterScrubTooltip = document.createElement("div");
|
||||
this.chapterScrubTooltip.className = "sponsorCategoryTooltip";
|
||||
}
|
||||
|
||||
// global chapter tooltip or duration tooltip
|
||||
// YT, Vorapis, unknown, YTTV
|
||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper, .ytp-progress-tooltip-text-container, .yssi-slider .ys-seek-details .time-info-bar") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
|
||||
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip), .ytp-progress-tooltip-text:not(.sponsorCategoryTooltip), .current-time:not(.sponsorCategoryTooltip)") as HTMLElement;
|
||||
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
|
||||
|
||||
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
|
||||
this.categoryTooltipContainer = tooltipTextWrapper.parentElement;
|
||||
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title") as HTMLElement;
|
||||
// YT, Vorapis, YTTV
|
||||
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title, .ytp-progress-tooltip-text, .current-time") as HTMLElement;
|
||||
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 (isOnYTTV()) {
|
||||
const scrubTooltipTextWrapper = document.querySelector(".yssi-slider .ysl-filmstrip-lens .time-info-bar")
|
||||
if (!this.categoryTooltipContainer) return;
|
||||
|
||||
scrubTooltipTextWrapper.appendChild(this.categoryScrubTooltip);
|
||||
scrubTooltipTextWrapper.appendChild(this.chapterScrubTooltip);
|
||||
}
|
||||
|
||||
const seekBar = (document.querySelector(".ytp-progress-bar-container, .ypcs-scrub-slider-slot.ytu-player-controls"));
|
||||
if (!seekBar) return;
|
||||
|
||||
let mouseOnSeekBar = false;
|
||||
@@ -128,12 +168,12 @@ class PreviewBar {
|
||||
seekBar.addEventListener("mousemove", (e: MouseEvent) => {
|
||||
if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer || !chrome.runtime?.id) return;
|
||||
|
||||
let noYoutubeChapters = !!tooltipTextWrapper.querySelector(".ytp-tooltip-text.ytp-tooltip-text-no-title");
|
||||
let noYoutubeChapters = !!tooltipTextWrapper.querySelector(".ytp-tooltip-text.ytp-tooltip-text-no-title, .ytp-progress-tooltip-timestamp");
|
||||
const timeInSeconds = this.decimalToTime((e.clientX - seekBar.getBoundingClientRect().x) / seekBar.clientWidth);
|
||||
|
||||
// Find the segment at that location, using the shortest if multiple found
|
||||
const [normalSegments, chapterSegments] =
|
||||
partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
|
||||
partition(this.segments,
|
||||
(segment) => segment.actionType !== ActionType.Chapter);
|
||||
let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments, "normal");
|
||||
let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments, "chapter");
|
||||
@@ -142,19 +182,49 @@ class PreviewBar {
|
||||
secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
|
||||
}
|
||||
|
||||
const hasAYouTubeChapterRemoved = this.hasYouTubeChapters
|
||||
|| (!Config.config.showAutogeneratedChapters && hasAutogeneratedChapters());
|
||||
if (hasAYouTubeChapterRemoved) {
|
||||
// Hide original tooltip if some chapter has been filtered out
|
||||
originalTooltip.style.display = "none";
|
||||
noYoutubeChapters = true;
|
||||
|
||||
originalTooltip.classList.add("sponsorTooltipHasYTChapters");
|
||||
} else {
|
||||
originalTooltip.classList.remove("sponsorTooltipHasYTChapters");
|
||||
}
|
||||
|
||||
if (mainSegment === null && secondarySegment === null) {
|
||||
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
originalTooltip.style.removeProperty("display");
|
||||
if (!hasAYouTubeChapterRemoved) {
|
||||
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||
originalTooltip.style.removeProperty("display");
|
||||
}
|
||||
if (this.onYTTV) {
|
||||
this.setTooltipTitle(mainSegment, this.categoryTooltip);
|
||||
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
|
||||
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
|
||||
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
|
||||
}
|
||||
} else {
|
||||
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||
if (mainSegment !== null && secondarySegment !== null) {
|
||||
this.categoryTooltipContainer.classList.add("sponsorTwoTooltips");
|
||||
originalTooltip.classList.remove("sponsorTooltipHasYTChapters");
|
||||
} else {
|
||||
this.categoryTooltipContainer.classList.remove("sponsorTwoTooltips");
|
||||
}
|
||||
|
||||
this.setTooltipTitle(mainSegment, this.categoryTooltip);
|
||||
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
|
||||
if (this.onYTTV) {
|
||||
this.setTooltipTitle(mainSegment, this.categoryScrubTooltip);
|
||||
this.setTooltipTitle(secondarySegment, this.chapterScrubTooltip);
|
||||
}
|
||||
|
||||
if (isVorapisInstalled()) {
|
||||
const tooltipParent = tooltipTextWrapper.parentElement!;
|
||||
tooltipParent.classList.add("with-text");
|
||||
}
|
||||
|
||||
if (normalizeChapterName(originalTooltip.textContent) === normalizeChapterName(this.categoryTooltip.textContent)
|
||||
|| normalizeChapterName(originalTooltip.textContent) === normalizeChapterName(this.chapterTooltip.textContent)) {
|
||||
@@ -164,16 +234,16 @@ class PreviewBar {
|
||||
originalTooltip.style.removeProperty("display");
|
||||
}
|
||||
|
||||
// Used to prevent overlapping
|
||||
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||
this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||
|
||||
// To prevent offset issue
|
||||
this.categoryTooltip.style.right = titleTooltip.style.right;
|
||||
this.chapterTooltip.style.right = titleTooltip.style.right;
|
||||
this.categoryTooltip.style.textAlign = titleTooltip.style.textAlign;
|
||||
this.chapterTooltip.style.textAlign = titleTooltip.style.textAlign;
|
||||
}
|
||||
|
||||
// Used to prevent overlapping
|
||||
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||
this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,13 +267,17 @@ class PreviewBar {
|
||||
|
||||
if (this.onMobileYouTube) {
|
||||
this.container.style.transform = "none";
|
||||
this.container.style.height = "var(--yt-progress-bar-height)";
|
||||
} else if (!this.onInvidious) {
|
||||
this.container.classList.add("sbNotInvidious");
|
||||
}
|
||||
|
||||
// On the seek bar
|
||||
this.parent.prepend(this.container);
|
||||
if (this.onYTTV) {
|
||||
// order of sibling elements matters on YTTV
|
||||
this.parent.insertBefore(this.container, this.parent.firstChild.nextSibling.nextSibling);
|
||||
} else {
|
||||
this.parent.prepend(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
@@ -225,7 +299,22 @@ class PreviewBar {
|
||||
set(segments: PreviewBarSegment[], videoDuration: number): void {
|
||||
this.segments = segments ?? [];
|
||||
this.videoDuration = videoDuration ?? 0;
|
||||
this.hasYouTubeChapters = segments.some((segment) => segment.source === SponsorSourceType.YouTube);
|
||||
|
||||
// Remove unnecessary original chapters if submitted replacements exist
|
||||
for (const chapter of this.segments.filter((s) => s.actionType === ActionType.Chapter && s.source === SponsorSourceType.Server)) {
|
||||
const segmentDuration = chapter.segment[1] - chapter.segment[0];
|
||||
|
||||
const duplicate = this.segments.find((s) => s.actionType === ActionType.Chapter
|
||||
&& s.source === SponsorSourceType.YouTube
|
||||
&& Math.abs(s.segment[0] - chapter.segment[0]) < Math.min(3, segmentDuration / 3)
|
||||
&& Math.abs(s.segment[1] - chapter.segment[1]) < Math.min(3, segmentDuration / 3));
|
||||
|
||||
if (duplicate) {
|
||||
const index = this.segments.indexOf(duplicate);
|
||||
this.segments.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePageElements();
|
||||
// Sometimes video duration is inaccurate, pull from accessibility info
|
||||
@@ -239,7 +328,8 @@ class PreviewBar {
|
||||
}
|
||||
|
||||
private updatePageElements(): void {
|
||||
const allProgressBars = document.querySelectorAll(".ytp-progress-bar") as NodeListOf<HTMLElement>;
|
||||
// YT, Vorapis v3
|
||||
const allProgressBars = document.querySelectorAll(".ytp-progress-bar, .ytp-progress-bar-container > .html5-progress-bar > .ytp-progress-list") as NodeListOf<HTMLElement>;
|
||||
this.progressBar = findValidElement(allProgressBars) ?? allProgressBars?.[0];
|
||||
|
||||
if (this.progressBar) {
|
||||
@@ -323,6 +413,10 @@ class PreviewBar {
|
||||
bar.style.marginRight = `${this.chapterMargin}px`;
|
||||
}
|
||||
|
||||
if (this.onYTTV) {
|
||||
bar.classList.add("previewbar-yttv");
|
||||
}
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
@@ -343,11 +437,13 @@ class PreviewBar {
|
||||
// Merge overlapping chapters
|
||||
this.unfilteredChapterGroups = this.createChapterRenderGroups(segments);
|
||||
}
|
||||
|
||||
if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|
||||
|
||||
if ((segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|
||||
|| (!Config.config.renderSegmentsAsChapters
|
||||
&& segments.every((segment) => segment.actionType !== ActionType.Chapter
|
||||
|| segment.source === SponsorSourceType.YouTube))) {
|
||||
|| segment.source === SponsorSourceType.YouTube)))
|
||||
&& !(hasAutogeneratedChapters() && !Config.config.showAutogeneratedChapters)) {
|
||||
|
||||
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
|
||||
this.originalChapterBar.style.removeProperty("display");
|
||||
return;
|
||||
@@ -373,6 +469,15 @@ class PreviewBar {
|
||||
this.chapterGroups = this.unfilteredChapterGroups;
|
||||
}
|
||||
|
||||
if (this.chapterGroups.length === 0 && !Config.config.showAutogeneratedChapters && hasAutogeneratedChapters()) {
|
||||
// Add placeholder chapter group for whole video
|
||||
this.chapterGroups = [{
|
||||
segment: [0, this.videoDuration],
|
||||
originalDuration: 0,
|
||||
actionType: null
|
||||
}];
|
||||
}
|
||||
|
||||
if (!this.chapterGroups || this.chapterGroups.length <= 0) {
|
||||
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
|
||||
this.originalChapterBar.style.removeProperty("display");
|
||||
@@ -648,8 +753,21 @@ class PreviewBar {
|
||||
if (changedData.scale !== null) {
|
||||
const transformScale = (changedData.scale) / progressBar.clientWidth;
|
||||
|
||||
const scale = Math.max(0, Math.min(1 - calculatedLeft, (transformScale - cursor) / fullSectionWidth - calculatedLeft));
|
||||
customChangedElement.style.transform =
|
||||
`scaleX(${Math.max(0, Math.min(1 - calculatedLeft, (transformScale - cursor) / fullSectionWidth - calculatedLeft))}`;
|
||||
`scaleX(${scale})`;
|
||||
if (customChangedElement.style.backgroundSize) {
|
||||
const backgroundSize = Math.max(changedData.scale / scale, fullSectionWidth * progressBar.clientWidth);
|
||||
customChangedElement.style.backgroundSize = `${backgroundSize}px`;
|
||||
|
||||
if (changedData.scale < (cursor + fullSectionWidth) * progressBar.clientWidth) {
|
||||
customChangedElement.style.backgroundPosition = `-${backgroundSize - fullSectionWidth * progressBar.clientWidth}px`;
|
||||
} else {
|
||||
// Passed this section
|
||||
customChangedElement.style.backgroundPosition = `-${cursor * progressBar.clientWidth}px`;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstUpdate) {
|
||||
customChangedElement.style.transition = "none";
|
||||
setTimeout(() => customChangedElement.style.removeProperty("transition"), 50);
|
||||
@@ -752,7 +870,8 @@ class PreviewBar {
|
||||
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): SponsorTime[] {
|
||||
if (!Config.config.showSegmentNameInChapterBar
|
||||
|| Config.config.disableSkipping
|
||||
|| ((!segments || segments.length <= 0) && submittingSegments?.length <= 0)) {
|
||||
|| ((!segments || segments.length <= 0) && submittingSegments?.length <= 0
|
||||
&& (Config.config.showAutogeneratedChapters || !hasAutogeneratedChapters()))) {
|
||||
const chaptersContainer = this.getChaptersContainer();
|
||||
if (chaptersContainer) {
|
||||
chaptersContainer.querySelector(".sponsorChapterText")?.remove();
|
||||
@@ -792,14 +911,22 @@ class PreviewBar {
|
||||
return -1;
|
||||
} else if (a.actionType !== ActionType.Chapter && b.actionType === ActionType.Chapter) {
|
||||
return 1;
|
||||
} else if (a.actionType === ActionType.Chapter && b.actionType === ActionType.Chapter
|
||||
&& a.source === SponsorSourceType.Server && b.source !== SponsorSourceType.Server) {
|
||||
return -0.5;
|
||||
} else if (a.actionType === ActionType.Chapter && b.actionType === ActionType.Chapter
|
||||
&& a.source !== SponsorSourceType.Server && b.source === SponsorSourceType.Server) {
|
||||
return 0.5;
|
||||
} else {
|
||||
return (b.segment[0] - a.segment[0]);
|
||||
return (b.segment[0] - a.segment[0]) * 4;
|
||||
}
|
||||
})[0];
|
||||
|
||||
const chapterButton = this.getChapterButton(chaptersContainer);
|
||||
chapterButton.classList.remove("ytp-chapter-container-disabled");
|
||||
chapterButton.disabled = false;
|
||||
if (chapterButton) {
|
||||
chapterButton.classList.remove("ytp-chapter-container-disabled");
|
||||
chapterButton.disabled = false;
|
||||
}
|
||||
|
||||
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
|
||||
chapterTitle.style.display = "none";
|
||||
@@ -808,6 +935,9 @@ class PreviewBar {
|
||||
const elem = document.createElement("div");
|
||||
chapterTitle.parentElement.insertBefore(elem, chapterTitle);
|
||||
elem.classList.add("sponsorChapterText");
|
||||
if (document.location.host === "tv.youtube.com") {
|
||||
elem.style.lineHeight = "initial";
|
||||
}
|
||||
return elem;
|
||||
})()) as HTMLDivElement;
|
||||
chapterCustomText.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
|
||||
@@ -820,7 +950,15 @@ class PreviewBar {
|
||||
|
||||
if (chosenSegment.source === SponsorSourceType.Server) {
|
||||
const chapterVoteContainer = this.chapterVote.getContainer();
|
||||
if (!chapterButton.contains(chapterVoteContainer)) {
|
||||
if (document.location.host === "tv.youtube.com") {
|
||||
if (!chaptersContainer.contains(chapterVoteContainer)) {
|
||||
const oldVoteContainers = document.querySelectorAll("#chapterVote");
|
||||
if (oldVoteContainers.length > 0) {
|
||||
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
|
||||
}
|
||||
chaptersContainer.appendChild(chapterVoteContainer);
|
||||
}
|
||||
} else if (!chapterButton.contains(chapterVoteContainer)) {
|
||||
const oldVoteContainers = document.querySelectorAll("#chapterVote");
|
||||
if (oldVoteContainers.length > 0) {
|
||||
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
|
||||
@@ -834,6 +972,18 @@ class PreviewBar {
|
||||
} else {
|
||||
this.chapterVote.setVisibility(false);
|
||||
}
|
||||
} else if (!Config.config.showAutogeneratedChapters && hasAutogeneratedChapters()) {
|
||||
// Keep original hidden
|
||||
chaptersContainer.querySelector(".sponsorChapterText")?.remove();
|
||||
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
|
||||
|
||||
chapterTitle.style.display = "none";
|
||||
chaptersContainer.classList.remove("sponsorblock-chapter-visible");
|
||||
|
||||
const titlePrefixDot = chaptersContainer.querySelector(".ytp-chapter-title-prefix") as HTMLElement;
|
||||
if (titlePrefixDot) titlePrefixDot.style.display = "none";
|
||||
|
||||
this.chapterVote.setVisibility(false);
|
||||
} else {
|
||||
chaptersContainer.querySelector(".sponsorChapterText")?.remove();
|
||||
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
|
||||
@@ -847,6 +997,18 @@ class PreviewBar {
|
||||
}
|
||||
|
||||
private getChaptersContainer(): HTMLElement {
|
||||
if (document.location.host === "tv.youtube.com") {
|
||||
if (!document.querySelector(".ytp-chapter-container")) {
|
||||
const dest = document.querySelector(".ypcs-control-buttons-left");
|
||||
if (!dest) return null;
|
||||
const sbChapterContainer = document.createElement("div");
|
||||
sbChapterContainer.className = "ytp-chapter-container";
|
||||
const sbChapterTitleContent = document.createElement("div");
|
||||
sbChapterTitleContent.className = "ytp-chapter-title-content";
|
||||
sbChapterContainer.appendChild(sbChapterTitleContent);
|
||||
dest.appendChild(sbChapterContainer);
|
||||
}
|
||||
}
|
||||
return document.querySelector(".ytp-chapter-container") as HTMLElement;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface SkipButtonControlBarProps {
|
||||
skip: (segment: SponsorTime) => void;
|
||||
selectSegment: (UUID: SegmentUUID) => void;
|
||||
onMobileYouTube: boolean;
|
||||
onYTTV: boolean;
|
||||
}
|
||||
|
||||
export class SkipButtonControlBar {
|
||||
@@ -21,6 +22,7 @@ export class SkipButtonControlBar {
|
||||
|
||||
showKeybindHint = true;
|
||||
onMobileYouTube: boolean;
|
||||
onYTTV: boolean;
|
||||
|
||||
enabled = false;
|
||||
|
||||
@@ -40,6 +42,7 @@ export class SkipButtonControlBar {
|
||||
constructor(props: SkipButtonControlBarProps) {
|
||||
this.skip = props.skip;
|
||||
this.onMobileYouTube = props.onMobileYouTube;
|
||||
this.onYTTV = props.onYTTV;
|
||||
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("skipButtonControlBarContainer");
|
||||
@@ -50,6 +53,10 @@ export class SkipButtonControlBar {
|
||||
this.skipIcon.src = chrome.runtime.getURL("icons/skipIcon.svg");
|
||||
this.skipIcon.classList.add("ytp-button");
|
||||
this.skipIcon.id = "sbSkipIconControlBarImage";
|
||||
if (this.onYTTV) {
|
||||
this.skipIcon.style.width = "24px";
|
||||
this.skipIcon.style.height = "24px";
|
||||
}
|
||||
|
||||
this.textContainer = document.createElement("div");
|
||||
|
||||
@@ -84,7 +91,7 @@ export class SkipButtonControlBar {
|
||||
this.chapterText = document.querySelector(".ytp-chapter-container");
|
||||
|
||||
if (mountingContainer && !mountingContainer.contains(this.container)) {
|
||||
if (this.onMobileYouTube) {
|
||||
if (this.onMobileYouTube || this.onYTTV) {
|
||||
mountingContainer.appendChild(this.container);
|
||||
} else {
|
||||
mountingContainer.insertBefore(this.container, this.chapterText);
|
||||
@@ -101,8 +108,10 @@ export class SkipButtonControlBar {
|
||||
}
|
||||
|
||||
private getMountingContainer(): HTMLElement {
|
||||
if (!this.onMobileYouTube) {
|
||||
if (!this.onMobileYouTube && !this.onYTTV) {
|
||||
return document.querySelector(".ytp-left-controls");
|
||||
} else if (this.onYTTV) {
|
||||
return document.querySelector(".ypcs-control-buttons-left");
|
||||
} else {
|
||||
return document.getElementById("player-container-id");
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ interface GetVideoIdResponse {
|
||||
|
||||
export interface GetChannelIDResponse {
|
||||
channelID: string;
|
||||
isYTTV: boolean;
|
||||
}
|
||||
|
||||
export interface SponsorStartResponse {
|
||||
|
||||
23
src/popup.ts
23
src/popup.ts
@@ -681,9 +681,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
uuidButton.className = "voteButton";
|
||||
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
|
||||
uuidButton.title = chrome.i18n.getMessage("copySegmentID");
|
||||
uuidButton.addEventListener("click", () => {
|
||||
copyToClipboard(UUID);
|
||||
uuidButton.addEventListener("click", async () => {
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
|
||||
|
||||
if (UUID.length > 60) {
|
||||
copyToClipboard(UUID);
|
||||
} else {
|
||||
const segmentIDData = await asyncRequestToServer("GET", "/api/segmentID", {
|
||||
UUID: UUID,
|
||||
videoID: currentVideoID
|
||||
});
|
||||
|
||||
if (segmentIDData.ok && segmentIDData.responseText) {
|
||||
copyToClipboard(segmentIDData.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
stopAnimation();
|
||||
});
|
||||
|
||||
@@ -917,7 +930,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
//get the channel url
|
||||
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
|
||||
if (!response.channelID) {
|
||||
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
|
||||
if (response.isYTTV) {
|
||||
alert(chrome.i18n.getMessage("yttvNoChannelWhitelist"));
|
||||
} else {
|
||||
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export class CategoryPill {
|
||||
}
|
||||
|
||||
private async attachToPageInternal(): Promise<void> {
|
||||
let referenceNode =
|
||||
let referenceNode =
|
||||
await waitFor(() => getYouTubeTitleNode());
|
||||
|
||||
// Experimental YouTube layout with description on right
|
||||
@@ -119,6 +119,8 @@ export class CategoryPill {
|
||||
}
|
||||
|
||||
async setSegment(segment: SponsorTime): Promise<void> {
|
||||
await waitFor(() => this.ref.current);
|
||||
|
||||
if (this.ref.current?.state?.segment !== segment) {
|
||||
const newState = {
|
||||
segment,
|
||||
|
||||
@@ -20,6 +20,10 @@ export class ChapterVote {
|
||||
this.container.id = "chapterVote";
|
||||
this.container.style.height = "100%";
|
||||
|
||||
if (document.location.host === "tv.youtube.com") {
|
||||
this.container.style.lineHeight = "initial";
|
||||
}
|
||||
|
||||
this.root = createRoot(this.container);
|
||||
this.root.render(<ChapterVoteComponent ref={this.ref} vote={vote} />);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
|
||||
import SkipNoticeComponent from "../components/SkipNoticeComponent";
|
||||
import { SponsorTime, ContentContainer, NoticeVisbilityMode } from "../types";
|
||||
import { SponsorTime, ContentContainer, NoticeVisibilityMode } from "../types";
|
||||
import Config from "../config";
|
||||
import { SkipNoticeAction } from "../utils/noticeUtils";
|
||||
|
||||
@@ -20,7 +20,7 @@ class SkipNotice {
|
||||
skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
|
||||
root: Root;
|
||||
|
||||
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, unskipTime: number = null, startReskip = false) {
|
||||
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, componentDidMount: () => void, unskipTime: number = null, startReskip = false, upcomingNoticeShown: boolean, voteNotice = false) {
|
||||
this.skipNoticeRef = React.createRef();
|
||||
|
||||
this.segments = segments;
|
||||
@@ -42,18 +42,20 @@ class SkipNotice {
|
||||
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
|
||||
|
||||
referenceNode.prepend(this.noticeElement);
|
||||
|
||||
this.root = createRoot(this.noticeElement);
|
||||
this.root.render(
|
||||
<SkipNoticeComponent segments={segments}
|
||||
autoSkip={autoSkip}
|
||||
startReskip={startReskip}
|
||||
voteNotice={voteNotice}
|
||||
contentContainer={contentContainer}
|
||||
ref={this.skipNoticeRef}
|
||||
closeListener={() => this.close()}
|
||||
smaller={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAll
|
||||
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAutoSkip && autoSkip)}
|
||||
unskipTime={unskipTime} />
|
||||
smaller={!voteNotice && (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.MiniForAll
|
||||
|| (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.MiniForAutoSkip && autoSkip))}
|
||||
fadeIn={!upcomingNoticeShown && !voteNotice}
|
||||
unskipTime={unskipTime}
|
||||
componentDidMount={componentDidMount} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,6 +81,26 @@ class SkipNotice {
|
||||
unmutedListener(time: number): void {
|
||||
this.skipNoticeRef?.current?.unmutedListener(time);
|
||||
}
|
||||
|
||||
async waitForSkipNoticeRef(): Promise<SkipNoticeComponent> {
|
||||
const waitForRef = () => new Promise<SkipNoticeComponent>((resolve) => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (this.skipNoticeRef.current) {
|
||||
observer.disconnect();
|
||||
resolve(this.skipNoticeRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.getElementsByClassName("sponsorSkipNoticeContainer")[0], { childList: true, subtree: true});
|
||||
|
||||
if (this.skipNoticeRef.current) {
|
||||
observer.disconnect();
|
||||
resolve(this.skipNoticeRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
return this.skipNoticeRef?.current || await waitForRef();
|
||||
}
|
||||
}
|
||||
|
||||
export default SkipNotice;
|
||||
66
src/render/UpcomingNotice.tsx
Normal file
66
src/render/UpcomingNotice.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as React from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { ContentContainer, SponsorTime } from "../types";
|
||||
|
||||
import Utils from "../utils";
|
||||
import SkipNoticeComponent from "../components/SkipNoticeComponent";
|
||||
const utils = new Utils();
|
||||
|
||||
class UpcomingNotice {
|
||||
segments: SponsorTime[];
|
||||
// Contains functions and variables from the content script needed by the skip notice
|
||||
contentContainer: ContentContainer;
|
||||
|
||||
noticeElement: HTMLDivElement;
|
||||
|
||||
upcomingNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
|
||||
root: Root;
|
||||
|
||||
closed = false;
|
||||
|
||||
constructor(segments: SponsorTime[], contentContainer: ContentContainer, timeLeft: number, autoSkip: boolean) {
|
||||
this.upcomingNoticeRef = React.createRef();
|
||||
|
||||
this.segments = segments;
|
||||
this.contentContainer = contentContainer;
|
||||
|
||||
const referenceNode = utils.findReferenceNode();
|
||||
|
||||
this.noticeElement = document.createElement("div");
|
||||
this.noticeElement.className = "sponsorSkipNoticeContainer";
|
||||
|
||||
referenceNode.prepend(this.noticeElement);
|
||||
|
||||
this.root = createRoot(this.noticeElement);
|
||||
this.root.render(
|
||||
<SkipNoticeComponent segments={segments}
|
||||
autoSkip={autoSkip}
|
||||
upcomingNotice={true}
|
||||
contentContainer={contentContainer}
|
||||
ref={this.upcomingNoticeRef}
|
||||
closeListener={() => this.close()}
|
||||
smaller={true}
|
||||
fadeIn={true}
|
||||
maxCountdownTime={timeLeft} />
|
||||
);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.root.unmount();
|
||||
this.noticeElement.remove();
|
||||
|
||||
this.closed = true;
|
||||
}
|
||||
|
||||
sameNotice(segments: SponsorTime[]): boolean {
|
||||
if (segments.length !== this.segments.length) return false;
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (segments[i].UUID !== this.segments[i].UUID) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default UpcomingNotice;
|
||||
@@ -6,7 +6,7 @@ export interface ContentContainer {
|
||||
(): {
|
||||
vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void;
|
||||
dontShowNoticeAgain: () => void;
|
||||
unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean) => void;
|
||||
unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean, voteNotice?: boolean) => void;
|
||||
sponsorTimes: SponsorTime[];
|
||||
sponsorTimesSubmitting: SponsorTime[];
|
||||
skipNotices: SkipNotice[];
|
||||
@@ -219,7 +219,7 @@ export interface ToggleSkippable {
|
||||
setShowKeybindHint: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export enum NoticeVisbilityMode {
|
||||
export enum NoticeVisibilityMode {
|
||||
FullSize = 0,
|
||||
MiniForAutoSkip = 1,
|
||||
MiniForAll = 2,
|
||||
|
||||
16
src/utils.ts
16
src/utils.ts
@@ -5,6 +5,7 @@ import { getHash, HashedValue } from "../maze-utils/src/hash";
|
||||
import { waitFor } from "../maze-utils/src";
|
||||
import { findValidElementFromSelector } from "../maze-utils/src/dom";
|
||||
import { isSafari } from "../maze-utils/src/config";
|
||||
import { asyncRequestToServer } from "./utils/requests";
|
||||
|
||||
export default class Utils {
|
||||
|
||||
@@ -198,7 +199,7 @@ export default class Utils {
|
||||
|
||||
getSponsorIndexFromUUID(sponsorTimes: SponsorTime[], UUID: string): number {
|
||||
for (let i = 0; i < sponsorTimes.length; i++) {
|
||||
if (sponsorTimes[i].UUID === UUID) {
|
||||
if (sponsorTimes[i].UUID && (sponsorTimes[i].UUID.startsWith(UUID) || UUID.startsWith(sponsorTimes[i].UUID))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -248,6 +249,7 @@ export default class Utils {
|
||||
".main-video-section > .video-container", // Cloudtube
|
||||
".shaka-video-container", // Piped
|
||||
"#player-container.ytk-player", // YT Kids
|
||||
"#id-tv-container" // YTTV
|
||||
];
|
||||
|
||||
let referenceNode = findValidElementFromSelector(selectors)
|
||||
@@ -282,6 +284,17 @@ export default class Utils {
|
||||
if ((chrome.extension.inIncognitoContext && !Config.config.trackDownvotesInPrivate)
|
||||
|| !Config.config.trackDownvotes) return;
|
||||
|
||||
if (segmentUUID.length < 60) {
|
||||
const segmentIDData = await asyncRequestToServer("GET", "/api/segmentID", {
|
||||
UUID: segmentUUID,
|
||||
videoID
|
||||
});
|
||||
|
||||
if (segmentIDData.ok && segmentIDData.responseText) {
|
||||
segmentUUID = segmentIDData.responseText;
|
||||
}
|
||||
}
|
||||
|
||||
const hashedVideoID = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
|
||||
const UUIDHash = await getHash(segmentUUID, 1);
|
||||
|
||||
@@ -308,6 +321,7 @@ export default class Utils {
|
||||
|
||||
allDownvotes[hashedVideoID] = currentVideoData;
|
||||
}
|
||||
console.log(allDownvotes)
|
||||
|
||||
const entries = Object.entries(allDownvotes);
|
||||
if (entries.length > 10000) {
|
||||
|
||||
@@ -36,6 +36,23 @@ export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): str
|
||||
}
|
||||
}
|
||||
|
||||
export function getUpcomingText(segments: SponsorTime[]): string {
|
||||
const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments"
|
||||
: "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category);
|
||||
|
||||
const messageId = "upcoming";
|
||||
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
|
||||
}
|
||||
|
||||
export function getVoteText(segments: SponsorTime[]): string {
|
||||
const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments"
|
||||
: "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category);
|
||||
|
||||
const messageId = "voted_on";
|
||||
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
|
||||
}
|
||||
|
||||
|
||||
export function getCategorySuffix(category: Category): string {
|
||||
if (category.startsWith("poi_")) {
|
||||
return "_POI";
|
||||
@@ -51,5 +68,4 @@ export function getCategorySuffix(category: Category): string {
|
||||
export function shortCategoryName(categoryName: string): string {
|
||||
return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
|
||||
}
|
||||
|
||||
export const DEFAULT_CATEGORY = "chooseACategory";
|
||||
@@ -12,4 +12,8 @@ export function runCompatibilityChecks() {
|
||||
Config.config.showZoomToFillError2 = false;
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
export function isVorapisInstalled() {
|
||||
return document.querySelector(`.v3`);
|
||||
}
|
||||
@@ -34,9 +34,12 @@ function exportTime(segment: SponsorTime): string {
|
||||
|
||||
export function importTimes(data: string, videoDuration: number): SponsorTime[] {
|
||||
const lines = data.split("\n");
|
||||
const timeRegex = /(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g;
|
||||
const anyLineHasTime = lines.some((line) => timeRegex.test(line));
|
||||
|
||||
const result: SponsorTime[] = [];
|
||||
for (const line of lines) {
|
||||
const match = line.match(/(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
|
||||
const match = line.match(timeRegex);
|
||||
if (match) {
|
||||
const startTime = getFormattedTimeToSeconds(match[0]);
|
||||
if (startTime !== null) {
|
||||
@@ -71,6 +74,18 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
|
||||
|
||||
result.push(segment);
|
||||
}
|
||||
} else if (!anyLineHasTime) {
|
||||
// Adding chapters with placeholder times
|
||||
const segment: SponsorTime = {
|
||||
segment: [0, 0],
|
||||
category: "chapter" as Category,
|
||||
actionType: ActionType.Chapter,
|
||||
description: line,
|
||||
source: SponsorSourceType.Local,
|
||||
UUID: generateUserID() as SegmentUUID
|
||||
};
|
||||
|
||||
result.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
|
||||
import { getFormattedTimeToSeconds } from "../../maze-utils/src/formating";
|
||||
import Config from "../config";
|
||||
|
||||
export function getControls(): HTMLElement {
|
||||
const controlsSelectors = [
|
||||
@@ -10,7 +11,11 @@ export function getControls(): HTMLElement {
|
||||
// Invidious/videojs video element's controls element
|
||||
".vjs-control-bar",
|
||||
// Piped shaka player
|
||||
".shaka-bottom-controls"
|
||||
".shaka-bottom-controls",
|
||||
// Vorapis v3
|
||||
".html5-player-chrome",
|
||||
// tv.youtube.com
|
||||
".ypcs-control-buttons-right"
|
||||
];
|
||||
|
||||
for (const controlsSelector of controlsSelectors) {
|
||||
@@ -53,10 +58,15 @@ export function getHashParams(): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function hasAutogeneratedChapters(): boolean {
|
||||
return !!document.querySelector("ytd-engagement-panel-section-list-renderer ytd-macro-markers-list-renderer #menu");
|
||||
}
|
||||
|
||||
export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] {
|
||||
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
|
||||
const title = document.querySelector("[target-id=engagement-panel-macro-markers-auto-chapters] #title-text");
|
||||
const title = chaptersBox?.closest("ytd-engagement-panel-section-list-renderer")?.querySelector("#title-text.ytd-engagement-panel-title-header-renderer");
|
||||
if (title?.textContent?.includes("Key moment")) return [];
|
||||
if (!Config.config.showAutogeneratedChapters && hasAutogeneratedChapters()) return [];
|
||||
|
||||
const chapters: SponsorTime[] = [];
|
||||
// .ytp-timed-markers-container indicates that key-moments are present, which should not be divided
|
||||
|
||||
@@ -48,11 +48,13 @@ async function fetchSegmentsForVideo(videoID: VideoID): Promise<SegmentResponse>
|
||||
const extraRequestData: Record<string, unknown> = {};
|
||||
const hashParams = getHashParams();
|
||||
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
|
||||
|
||||
|
||||
const hashPrefix = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
|
||||
const hasDownvotedSegments = !!Config.local.downvotedSegments[hashPrefix];
|
||||
const response = await asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
|
||||
categories: CompileConfig.categoryList,
|
||||
actionTypes: ActionTypes,
|
||||
trimUUIDs: hasDownvotedSegments ? null : 5,
|
||||
...extraRequestData
|
||||
}, {
|
||||
"X-CLIENT-NAME": `${chrome.runtime.id}/v${chrome.runtime.getManifest().version}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video";
|
||||
import Config from "../config";
|
||||
import { getVideoLabel } from "./videoLabels";
|
||||
import { getHasStartSegment, getVideoLabel } from "./videoLabels";
|
||||
import { getThumbnailSelector, setThumbnailListener } from "../../maze-utils/src/thumbnailManagement";
|
||||
import { VideoID } from "../types";
|
||||
import { getSegmentsForVideo } from "./segmentData";
|
||||
@@ -58,14 +58,41 @@ function thumbnailHoverListener(e: MouseEvent) {
|
||||
if (!thumbnail) return;
|
||||
|
||||
// Pre-fetch data for this video
|
||||
const videoID = extractVideoID(thumbnail);
|
||||
if (videoID) {
|
||||
void getSegmentsForVideo(videoID, false);
|
||||
let fetched = false;
|
||||
const preFetch = async () => {
|
||||
fetched = true;
|
||||
const videoID = extractVideoID(thumbnail);
|
||||
if (videoID && await getHasStartSegment(videoID)) {
|
||||
void getSegmentsForVideo(videoID, false);
|
||||
}
|
||||
};
|
||||
const timeout = setTimeout(preFetch, 100);
|
||||
const onMouseDown = () => {
|
||||
clearTimeout(timeout);
|
||||
if (!fetched) {
|
||||
preFetch();
|
||||
}
|
||||
};
|
||||
|
||||
e.target.addEventListener("mousedown", onMouseDown, { once: true });
|
||||
e.target.addEventListener("mouseleave", () => {
|
||||
clearTimeout(timeout);
|
||||
e.target.removeEventListener("mousedown", onMouseDown);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
function getLink(thumbnail: HTMLImageElement): HTMLAnchorElement | null {
|
||||
if (isOnInvidious()) {
|
||||
return thumbnail.parentElement as HTMLAnchorElement | null;
|
||||
} else if (thumbnail.nodeName.toLowerCase() === "yt-thumbnail-view-model") {
|
||||
return thumbnail.closest("yt-lockup-view-model")?.querySelector("a.yt-lockup-metadata-view-model-wiz__title");
|
||||
} else {
|
||||
return thumbnail.querySelector("#thumbnail");
|
||||
}
|
||||
}
|
||||
|
||||
function extractVideoID(thumbnail: HTMLImageElement): VideoID | null {
|
||||
const link = (isOnInvidious() ? thumbnail.parentElement : thumbnail.querySelector("#thumbnail")) as HTMLAnchorElement
|
||||
const link = getLink(thumbnail);
|
||||
if (!link || link.nodeName !== "A" || !link.href) return null; // no link found
|
||||
|
||||
return parseYouTubeVideoIDFromURL(link.href)?.videoID;
|
||||
|
||||
@@ -6,9 +6,14 @@ import { asyncRequestToServer } from "./requests";
|
||||
|
||||
const utils = new Utils();
|
||||
|
||||
interface VideoLabelsCacheData {
|
||||
category: Category;
|
||||
hasStartSegment: boolean;
|
||||
}
|
||||
|
||||
export interface LabelCacheEntry {
|
||||
timestamp: number;
|
||||
videos: Record<VideoID, Category>;
|
||||
videos: Record<VideoID, VideoLabelsCacheData>;
|
||||
}
|
||||
|
||||
const labelCache: Record<string, LabelCacheEntry> = {};
|
||||
@@ -21,7 +26,7 @@ async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry |
|
||||
return cachedEntry;
|
||||
}
|
||||
|
||||
const response = await asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}`);
|
||||
const response = await asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}?hasStartSegment=true`);
|
||||
if (response.status !== 200) {
|
||||
// No video labels or server down
|
||||
labelCache[hashPrefix] = {
|
||||
@@ -36,7 +41,10 @@ async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry |
|
||||
|
||||
const newEntry: LabelCacheEntry = {
|
||||
timestamp: Date.now(),
|
||||
videos: Object.fromEntries(data.map(video => [video.videoID, video.segments[0].category])),
|
||||
videos: Object.fromEntries(data.map(video => [video.videoID, {
|
||||
category: video.segments[0]?.category,
|
||||
hasStartSegment: video.hasStartSegment
|
||||
}])),
|
||||
};
|
||||
labelCache[hashPrefix] = newEntry;
|
||||
|
||||
@@ -55,11 +63,11 @@ async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry |
|
||||
}
|
||||
|
||||
export async function getVideoLabel(videoID: VideoID): Promise<Category | null> {
|
||||
const prefix = (await getHash(videoID, 1)).slice(0, 3);
|
||||
const prefix = (await getHash(videoID, 1)).slice(0, 4);
|
||||
const result = await getLabelHashBlock(prefix);
|
||||
|
||||
if (result) {
|
||||
const category = result.videos[videoID];
|
||||
const category = result.videos[videoID]?.category;
|
||||
if (category && utils.getCategorySelection(category).option !== CategorySkipOption.Disabled) {
|
||||
return category;
|
||||
} else {
|
||||
@@ -67,5 +75,16 @@ export async function getVideoLabel(videoID: VideoID): Promise<Category | null>
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getHasStartSegment(videoID: VideoID): Promise<boolean | null> {
|
||||
const prefix = (await getHash(videoID, 1)).slice(0, 4);
|
||||
const result = await getLabelHashBlock(prefix);
|
||||
|
||||
if (result) {
|
||||
return result?.videos[videoID]?.hasStartSegment ?? false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
|
||||
|
||||
describe("createChapterRenderGroups", () => {
|
||||
let previewBar: PreviewBar;
|
||||
beforeEach(() => {
|
||||
previewBar = new PreviewBar(null, null, null, null, null, true);
|
||||
previewBar = new PreviewBar(null, null, null, null, null, null, true);
|
||||
})
|
||||
|
||||
it("Two unrelated times", () => {
|
||||
|
||||
@@ -170,6 +170,13 @@ module.exports = env => {
|
||||
parsed.Description.message = parsed.Description.message.slice(0, 77) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
if (env.browser.toLowerCase() === "edge") {
|
||||
parsed.Description.message = parsed.Description.message.match(/^.+(?=\. )/)?.[0] || parsed.Description.message;
|
||||
if (parsed.Description.message.length > 132) {
|
||||
parsed.Description.message = parsed.Description.message.slice(0, 129) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
return Buffer.from(JSON.stringify(parsed));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user