mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-07 12:07:11 +03:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44941acb38 | ||
|
|
8dbd566b13 | ||
|
|
145463ef2e | ||
|
|
8882b1d912 | ||
|
|
75288da07a | ||
|
|
eed5de9d41 | ||
|
|
dff3bdc2fa | ||
|
|
06ec260860 | ||
|
|
0a7fa754ff | ||
|
|
9f0a13830b | ||
|
|
4646f471bc | ||
|
|
60c4a0507c | ||
|
|
bd0be90135 | ||
|
|
590281f8eb | ||
|
|
df21d085e5 | ||
|
|
7a605b8a2c | ||
|
|
b59241cb08 | ||
|
|
03dc749440 | ||
|
|
685bda29a6 | ||
|
|
da1d1ffa7f | ||
|
|
fae3a355f5 | ||
|
|
f80ddc8841 | ||
|
|
1609995aa5 | ||
|
|
d0978bff79 | ||
|
|
0257791264 | ||
|
|
e1b18944bb | ||
|
|
068307f98a | ||
|
|
9deb69b71b | ||
|
|
479c31d57a | ||
|
|
f36d63e9bb | ||
|
|
510029af40 | ||
|
|
26f9a85b5b | ||
|
|
bc516c21fb | ||
|
|
c91df6e324 | ||
|
|
8b87a83f5f | ||
|
|
d7af742af8 | ||
|
|
c22d1b517c | ||
|
|
17896b2e22 | ||
|
|
458e33a0c5 | ||
|
|
7749a12a0a | ||
|
|
0eb222ae05 | ||
|
|
9fb3921141 | ||
|
|
7982ccba3d | ||
|
|
1ae101405b | ||
|
|
f6109ace99 | ||
|
|
75caa40fe5 | ||
|
|
fa8db7c3b3 | ||
|
|
d165e06d2b | ||
|
|
ff43268e96 | ||
|
|
7c61c8b44e | ||
|
|
ebc323a83b | ||
|
|
f64a552bf7 | ||
|
|
8d0d71b0f3 | ||
|
|
2a2d9de817 | ||
|
|
2004f6bf1b | ||
|
|
2370adb8b2 | ||
|
|
3d84152fa4 | ||
|
|
444b90cac2 | ||
|
|
1b2c3f5dbf | ||
|
|
aa04ed7bf4 | ||
|
|
9e501a5f66 | ||
|
|
7ccdf67195 | ||
|
|
14d7d79ce7 | ||
|
|
e63a677c81 | ||
|
|
1056d1ef5f | ||
|
|
c30f8ec5a3 | ||
|
|
a34e6e6d03 | ||
|
|
e5bccf95ea | ||
|
|
bf68ce14c4 | ||
|
|
9bf892c377 | ||
|
|
7cbb0ec7cf | ||
|
|
915fd70274 | ||
|
|
5043a5fbe2 | ||
|
|
808c3718a8 |
141
.github/workflows/release.yml
vendored
141
.github/workflows/release.yml
vendored
@@ -12,7 +12,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Initialization
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout release branch w/submodules
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -34,12 +35,27 @@ jobs:
|
||||
path: ../builds/SourceCodeUseThisOne.zip
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout source maps branch
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
path: source-maps
|
||||
ref: source-maps
|
||||
- name: Set up committer info
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- run: npm ci
|
||||
|
||||
# Create Firefox artifacts
|
||||
- name: Create Firefox artifacts
|
||||
run: npm run build:firefox
|
||||
run: npm run build:firefox -- --env ghpSourceMaps
|
||||
- run: mkdir ./builds
|
||||
- name: Move Firefox source maps to source map repo
|
||||
run: |
|
||||
VERSION=`jq -r '.version' ./dist/manifest.json`
|
||||
mkdir -p "./source-maps/firefox/$VERSION/"
|
||||
mv -v ./dist/**/*.js.map "./source-maps/firefox/$VERSION/"
|
||||
cp -v ./dist/**/*.js.LICENSE.txt "./source-maps/firefox/$VERSION/"
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/FirefoxExtension.zip *
|
||||
- name: Upload FirefoxExtension to release
|
||||
@@ -52,7 +68,13 @@ jobs:
|
||||
|
||||
# Create Chrome artifacts
|
||||
- name: Create Chrome artifacts
|
||||
run: npm run build:chrome
|
||||
run: npm run build:chrome -- --env ghpSourceMaps
|
||||
- name: Move Chrome source maps to source map repo
|
||||
run: |
|
||||
VERSION=`jq -r '.version' ./dist/manifest.json`
|
||||
mkdir -p "./source-maps/chrome/$VERSION/"
|
||||
mv -v ./dist/**/*.js.map "./source-maps/chrome/$VERSION/"
|
||||
cp -v ./dist/**/*.js.LICENSE.txt "./source-maps/chrome/$VERSION/"
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/ChromeExtension.zip *
|
||||
- name: Upload ChromeExtension to release
|
||||
@@ -67,7 +89,13 @@ jobs:
|
||||
- name: Clear dist for Edge
|
||||
run: rm -rf ./dist
|
||||
- name: Create Edge artifacts
|
||||
run: npm run build:edge
|
||||
run: npm run build:edge -- --env ghpSourceMaps
|
||||
- name: Move Edge source maps to source map repo
|
||||
run: |
|
||||
VERSION=`jq -r '.version' ./dist/manifest.json`
|
||||
mkdir -p "./source-maps/edge/$VERSION/"
|
||||
mv -v ./dist/**/*.js.map "./source-maps/edge/$VERSION/"
|
||||
cp -v ./dist/**/*.js.LICENSE.txt "./source-maps/edge/$VERSION/"
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/EdgeExtension.zip *
|
||||
- name: Upload EdgeExtension to release
|
||||
@@ -80,7 +108,13 @@ jobs:
|
||||
|
||||
# Create Safari artifacts
|
||||
- name: Create Safari artifacts
|
||||
run: npm run build:safari
|
||||
run: npm run build:safari -- --env ghpSourceMaps
|
||||
- name: Move Safari source maps to source map repo
|
||||
run: |
|
||||
VERSION=`jq -r '.version' ./dist/manifest.json`
|
||||
mkdir -p "./source-maps/safari/$VERSION/"
|
||||
mv -v ./dist/**/*.js.map "./source-maps/safari/$VERSION/"
|
||||
cp -v ./dist/**/*.js.LICENSE.txt "./source-maps/safari/$VERSION/"
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/SafariExtension.zip *
|
||||
- name: Upload SafariExtension to release
|
||||
@@ -93,7 +127,13 @@ jobs:
|
||||
|
||||
# Create Beta artifacts (Builds with the name changed to beta)
|
||||
- name: Create Chrome Beta artifacts
|
||||
run: npm run build:chrome -- --env stream=beta
|
||||
run: npm run build:chrome -- --env stream=beta --env ghpSourceMaps
|
||||
- name: Move Chrome Beta source maps to source map repo
|
||||
run: |
|
||||
VERSION=`jq -r '.version' ./dist/manifest.json`
|
||||
mkdir -p "./source-maps/chrome-beta/$VERSION/"
|
||||
mv -v ./dist/**/*.js.map "./source-maps/chrome-beta/$VERSION/"
|
||||
cp -v ./dist/**/*.js.LICENSE.txt "./source-maps/chrome-beta/$VERSION/"
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/ChromeExtensionBeta.zip *
|
||||
- name: Upload ChromeExtensionBeta to release
|
||||
@@ -106,11 +146,17 @@ jobs:
|
||||
|
||||
# Firefox Beta
|
||||
- name: Create Firefox Beta artifacts
|
||||
run: npm run build:firefox -- --env stream=beta
|
||||
run: npm run build:firefox -- --env stream=beta --env autoupdate --env ghpSourceMaps
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FirefoxExtensionBeta
|
||||
path: dist
|
||||
- name: Move Firefox Beta source maps to source map repo
|
||||
run: |
|
||||
VERSION=`jq -r '.version' ./dist/manifest.json`
|
||||
mkdir -p "./source-maps/firefox-beta/$VERSION/"
|
||||
mv -v ./dist/**/*.js.map "./source-maps/firefox-beta/$VERSION/"
|
||||
cp -v ./dist/**/*.js.LICENSE.txt "./source-maps/firefox-beta/$VERSION/"
|
||||
- name: Zip Artifacts
|
||||
run: cd ./dist ; zip -r ../builds/FirefoxExtensionBeta.zip *
|
||||
|
||||
@@ -120,10 +166,8 @@ jobs:
|
||||
env:
|
||||
WEB_EXT_API_KEY: ${{ secrets.WEB_EXT_API_KEY }}
|
||||
WEB_EXT_API_SECRET: ${{ secrets.WEB_EXT_API_SECRET }}
|
||||
- name: Install rename
|
||||
run: sudo apt-get install rename
|
||||
- name: Rename signed file
|
||||
run: cd ./web-ext-artifacts ; rename 's/.*/FirefoxSignedInstaller.xpi/' *
|
||||
run: mv ./web-ext-artifacts/* ./web-ext-artifacts/FirefoxSignedInstaller.xpi
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: FirefoxExtensionSigned.xpi
|
||||
@@ -137,3 +181,80 @@ jobs:
|
||||
path: ./web-ext-artifacts/FirefoxSignedInstaller.xpi
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Commit & push new source maps
|
||||
if: always()
|
||||
run: |
|
||||
VERSION=`jq -r '.version' ./dist/manifest.json`
|
||||
cd ./source-maps
|
||||
git add .
|
||||
git commit -m "Publish source maps for version $VERSION"
|
||||
git push
|
||||
|
||||
- name: Prepare new github pages deployment
|
||||
shell: python
|
||||
run: |
|
||||
from pathlib import Path
|
||||
import json
|
||||
import shutil
|
||||
import os
|
||||
|
||||
# config stuff
|
||||
installer_name = "FirefoxSignedInstaller.xpi"
|
||||
owner, repo_name = os.environ["GITHUB_REPOSITORY"].split("/")
|
||||
owner = owner.lower()
|
||||
|
||||
# create the github paged dir
|
||||
ghp_dir = Path("./github-pages")
|
||||
ghp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# move in the installer
|
||||
Path("./web-ext-artifacts", installer_name).rename(ghp_dir / installer_name)
|
||||
|
||||
# read manifest.json and extract parameters
|
||||
with open("./dist/manifest.json") as f:
|
||||
manifest = json.load(f)
|
||||
current_version = manifest["version"]
|
||||
ext_id = manifest["browser_specific_settings"]["gecko"]["id"]
|
||||
|
||||
# generate updates file
|
||||
updates = {
|
||||
"addons": {
|
||||
ext_id: {
|
||||
"updates": [
|
||||
{
|
||||
"version": current_version,
|
||||
# param doesn't actually matter, it's just a cachebuster
|
||||
"update_link": f"https://{owner}.github.io/{repo_name}/{installer_name}?v={current_version}",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
(ghp_dir / "updates.json").write_text(json.dumps(updates))
|
||||
|
||||
# copy in source maps
|
||||
def only_sourcemaps(cur, ls):
|
||||
if '/' in cur:
|
||||
return []
|
||||
return set(ls) - {"chrome", "chrome-beta", "edge", "firefox", "firefox-beta", "safari"}
|
||||
shutil.copytree("source-maps", ghp_dir, ignore=only_sourcemaps, dirs_exist_ok=True)
|
||||
|
||||
- name: Upload new github pages deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./github-pages
|
||||
|
||||
deploy-ghp:
|
||||
name: Deploy to github pages
|
||||
needs: build
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
"icons/PlayerInfoIconSponsorBlocker.svg",
|
||||
"icons/PlayerDeleteIconSponsorBlocker.svg",
|
||||
"icons/dearrow.svg",
|
||||
"icons/sb-pride.png",
|
||||
"icons/pride.svg",
|
||||
"popup.html",
|
||||
"popup.css",
|
||||
"content.css",
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
"icons/PlayerInfoIconSponsorBlocker.svg",
|
||||
"icons/PlayerDeleteIconSponsorBlocker.svg",
|
||||
"icons/dearrow.svg",
|
||||
"icons/sb-pride.png",
|
||||
"icons/pride.svg",
|
||||
"popup.html",
|
||||
"popup.css",
|
||||
"content.css",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "__MSG_fullName__",
|
||||
"short_name": "SponsorBlock",
|
||||
"version": "6.0",
|
||||
"version": "6.1.0",
|
||||
"default_locale": "en",
|
||||
"description": "__MSG_Description__",
|
||||
"homepage_url": "https://sponsor.ajay.app",
|
||||
|
||||
Submodule maze-utils updated: 092fdda896...49dad0e67b
596
package-lock.json
generated
596
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@
|
||||
"@types/wicg-mediasession": "^1.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.1",
|
||||
"@typescript-eslint/parser": "^5.54.1",
|
||||
"chromedriver": "^137.0.0",
|
||||
"chromedriver": "^140.0.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"eslint": "^8.35.0",
|
||||
@@ -38,7 +38,7 @@
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "4.9",
|
||||
"web-ext": "^8.9.0",
|
||||
"web-ext": "^8.10.0",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-merge": "^5.8.0"
|
||||
|
||||
Submodule public/_locales updated: f7d565226e...d1f95394ca
@@ -25,7 +25,7 @@
|
||||
pointer-events: none;
|
||||
|
||||
height: 100%;
|
||||
transform: scaleY(0.6) translateY(-30%) translateY(1.5px);
|
||||
transform: scaleY(0.667) translateY(-30%) translateY(1.5px);
|
||||
z-index: 42;
|
||||
|
||||
transition: transform .1s cubic-bezier(0,0,0.2,1);
|
||||
@@ -974,3 +974,25 @@ input::-webkit-inner-spin-button {
|
||||
.sponsorblock-chapter-visible {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Pride theme */
|
||||
|
||||
.playerButton.prideTheme:nth-of-type(1) {
|
||||
filter: brightness(50%) sepia(100) saturate(100);
|
||||
}
|
||||
|
||||
.playerButton.prideTheme:nth-of-type(2) {
|
||||
filter: sepia(100) saturate(100) hue-rotate(0deg);
|
||||
}
|
||||
|
||||
.playerButton.prideTheme:nth-of-type(3) {
|
||||
filter: sepia(100) saturate(100) hue-rotate(45deg);
|
||||
}
|
||||
|
||||
.playerButton.prideTheme:nth-of-type(4) {
|
||||
filter: sepia(100) saturate(100) invert() hue-rotate(5deg);
|
||||
}
|
||||
|
||||
.playerButton.prideTheme:nth-of-type(5) {
|
||||
filter: sepia(100) saturate(100) invert() hue-rotate(35deg);
|
||||
}
|
||||
BIN
public/help/images/notice.png
Normal file
BIN
public/help/images/notice.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB |
@@ -71,7 +71,7 @@
|
||||
__MSG_helpPageHowSkippingWorks2__
|
||||
</p>
|
||||
|
||||
<div class="center"><img src="images/voting on notice.gif"></div>
|
||||
<div class="center"><img src="images/notice.png"></div>
|
||||
|
||||
<p class="projectPreview">
|
||||
__MSG_helpPageHowSkippingWorks1__
|
||||
|
||||
90
public/icons/pride.svg
Normal file
90
public/icons/pride.svg
Normal file
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 11718.75 3810"
|
||||
width="1000"
|
||||
height="325"
|
||||
version="1.1"
|
||||
id="svg11"
|
||||
sodipodi:docname="pride.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs11" />
|
||||
<sodipodi:namedview
|
||||
id="namedview11"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.42575964"
|
||||
inkscape:cx="863.16308"
|
||||
inkscape:cy="32.882403"
|
||||
inkscape:window-width="1536"
|
||||
inkscape:window-height="799"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11" />
|
||||
<path
|
||||
fill="#750787"
|
||||
d="M 0,0 H 15033.943 V 3810 H 0 Z"
|
||||
id="path1"
|
||||
style="stroke-width:1.58293" />
|
||||
<path
|
||||
fill="#004dff"
|
||||
d="M 0,0 H 15033.943 V 3175 H 0 Z"
|
||||
id="path2"
|
||||
style="stroke-width:1.58293" />
|
||||
<path
|
||||
fill="#008026"
|
||||
d="M 0,0 H 15033.943 V 2540 H 0 Z"
|
||||
id="path3"
|
||||
style="stroke-width:1.58293" />
|
||||
<path
|
||||
fill="#ffed00"
|
||||
d="M 0,0 H 15033.943 V 1905 H 0 Z"
|
||||
id="path4"
|
||||
style="stroke-width:1.58293" />
|
||||
<path
|
||||
fill="#ff8c00"
|
||||
d="M 0,0 H 15033.943 V 1270 H 0 Z"
|
||||
id="path5"
|
||||
style="stroke-width:1.58293" />
|
||||
<path
|
||||
fill="#e40303"
|
||||
d="M 0,0 H 15033.943 V 635 H 0 Z"
|
||||
id="path6"
|
||||
style="stroke-width:1.58293" />
|
||||
<path
|
||||
d="M 0,-960.5 2864,1905 0,4770.5 Z"
|
||||
id="path7" />
|
||||
<path
|
||||
fill="#613915"
|
||||
d="M 0,-480 2384,1905 0,4290 Z"
|
||||
id="path8" />
|
||||
<path
|
||||
fill="#74d7ee"
|
||||
d="M 0,3810 V 0 l 1912,1905 z"
|
||||
id="path9" />
|
||||
<path
|
||||
fill="#ffafc8"
|
||||
d="M 0,3330 V 480 l 1420,1425 z"
|
||||
id="path10" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M 0,960 951,1905 0,2850 Z"
|
||||
id="path11" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:0.621027;stroke:#550000;stroke-width:42.5545"
|
||||
id="rect11"
|
||||
width="12283.648"
|
||||
height="4656.5938"
|
||||
x="-243.66638"
|
||||
y="-421.53296" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/icons/sb-pride.png
Normal file
BIN
public/icons/sb-pride.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -17,6 +17,10 @@
|
||||
--border-color: #484848;
|
||||
--black: black;
|
||||
--white: white;
|
||||
|
||||
--selector-red: #c00000;
|
||||
--selector-red-hover: #fc0303;
|
||||
--tab-selected: #950000;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
@@ -38,6 +42,10 @@
|
||||
--white: black;
|
||||
}
|
||||
|
||||
[data-theme="pride"] {
|
||||
--menu-background: #181818d0;
|
||||
}
|
||||
|
||||
.medium-description, .switch-container, .optionLabel, .categoryTableElement, .promotion-description {
|
||||
color: var(--white);
|
||||
}
|
||||
@@ -58,6 +66,12 @@ html, body {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
[data-theme="pride"] body {
|
||||
background: url("../icons/pride.svg");
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -107,15 +121,99 @@ html, body {
|
||||
}
|
||||
|
||||
.tab-heading.selected {
|
||||
background-color: #c00000;
|
||||
background-color: var(--selector-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-heading:active {
|
||||
background-color: #950000;
|
||||
background-color: var(--tab-selected);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(1) {
|
||||
background-color: #2f0000;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(2) {
|
||||
background-color: #3a2000;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(3) {
|
||||
background-color: #3e3a00;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(4) {
|
||||
background-color: #003e13;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(5) {
|
||||
background-color: #00164a;
|
||||
}
|
||||
|
||||
[data-theme="pride"] .tab-heading:hover:nth-of-type(1) {
|
||||
background-color: #550000;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:hover:nth-of-type(2),
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n) .slider,
|
||||
[data-theme="pride"] [data-type="toggle"]:nth-of-type(5n) .slider {
|
||||
background-color: #824700;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:hover:nth-of-type(3),
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 1) .slider,
|
||||
[data-theme="pride"] [data-type="toggle"]:nth-of-type(5n + 1) .slider {
|
||||
background-color: #867d00;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:hover:nth-of-type(4),
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 2) .slider,
|
||||
[data-theme="pride"] [data-type="toggle"]:nth-of-type(5n + 2) .slider {
|
||||
background-color: #00691f;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:hover:nth-of-type(5),
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 3) .slider,
|
||||
[data-theme="pride"] [data-type="toggle"]:nth-of-type(5n + 3) .slider {
|
||||
background-color: #002374;
|
||||
}
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 4) .slider,
|
||||
[data-theme="pride"] [data-type="toggle"]:nth-of-type(5n + 4) .slider {
|
||||
background-color: #400449;
|
||||
}
|
||||
|
||||
[data-theme="pride"] #category-type tr .optionsSelector {
|
||||
color: var(--white);
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(1).selected {
|
||||
background-color: #E40303;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(2).selected,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(10n + 2) .optionsSelector,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n) input:checked + .slider,
|
||||
[data-theme="pride"] [data-type]:nth-of-type(5n) :is(input:checked + .slider, .option-button, .optionsSelector) {
|
||||
background-color: #dd7a00;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(3).selected,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(10n + 4) .optionsSelector,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(2n + 1) .optionsSelector,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 1) input:checked + .slider,
|
||||
[data-theme="pride"] [data-type]:nth-of-type(5n + 1) :is(input:checked + .slider, .option-button, .optionsSelector) {
|
||||
background-color: #FFED00;
|
||||
color: rgb(23, 23, 23);
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(4).selected,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(10n + 6) .optionsSelector,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 2) input:checked + .slider,
|
||||
[data-theme="pride"] [data-type]:nth-of-type(5n + 2) :is(input:checked + .slider, .option-button, .optionsSelector) {
|
||||
background-color: #008026;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(5).selected,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(10n + 8) .optionsSelector,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 3) input:checked + .slider,
|
||||
[data-theme="pride"] [data-type]:nth-of-type(5n + 3) :is(input:checked + .slider, .option-button, .optionsSelector) {
|
||||
background-color: #004DFF;
|
||||
}
|
||||
[data-theme="pride"] .tab-heading:nth-of-type(5).selected,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(10n + 10) .optionsSelector,
|
||||
[data-theme="pride"] #category-type tr:nth-of-type(5n + 4) input:checked + .slider,
|
||||
[data-theme="pride"] [data-type]:nth-of-type(5n + 4) :is(input:checked + .slider, .option-button, .optionsSelector) {
|
||||
background-color: #750787;
|
||||
}
|
||||
|
||||
|
||||
.option-group > div, .extraOptionGroup {
|
||||
min-height: 50px;
|
||||
padding: 15px 0;
|
||||
@@ -135,7 +233,7 @@ html, body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.categoryChooserTable .categoryExtraOptions {
|
||||
#category-type .categoryExtraOptions {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -310,7 +408,7 @@ input[type='number'] {
|
||||
.option-button {
|
||||
cursor: pointer;
|
||||
|
||||
background-color: #c00000;
|
||||
background-color: var(--selector-red);
|
||||
padding: 10px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
@@ -320,7 +418,7 @@ input[type='number'] {
|
||||
}
|
||||
|
||||
.option-button:hover:not(.disabled) {
|
||||
background-color: #fc0303;
|
||||
background-color: var(--selector-red-hover);
|
||||
}
|
||||
|
||||
.option-button.disabled {
|
||||
@@ -430,7 +528,7 @@ input[type='number'] {
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #fc0303;
|
||||
background-color: var(--selector-red-hover);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
@@ -611,7 +709,7 @@ svg {
|
||||
}
|
||||
|
||||
.optionsSelector {
|
||||
background-color: #c00000;
|
||||
background-color: var(--selector-red);
|
||||
color: white;
|
||||
|
||||
border: none;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div id="menubar" class="center">
|
||||
|
||||
<div id="title" class="titleBar">
|
||||
<img src="../icons/LogoSponsorBlocker256px.png" class="profilepic" alt="SponsorBlock logo"/>
|
||||
<img id="title-bar-logo" src="../icons/LogoSponsorBlocker256px.png" class="profilepic" alt="SponsorBlock logo"/>
|
||||
SponsorBlock
|
||||
<div id="version"></div>
|
||||
</div>
|
||||
@@ -80,9 +80,9 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div data-type="react-AdvancedSkipOptionsComponent"></div>
|
||||
<div data-type="react-AdvancedSkipOptionsComponent" class="hide-when-skip-profile"></div>
|
||||
|
||||
<div data-type="toggle" data-sync="forceChannelCheck">
|
||||
<div data-type="toggle" data-sync="forceChannelCheck" class="hide-when-skip-profile">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="forceChannelCheck" type="checkbox" checked>
|
||||
@@ -96,7 +96,7 @@
|
||||
<div class="small-description">__MSG_whatForceChannelCheck__</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-sync="showCategoryWithoutPermission">
|
||||
<div data-type="toggle" data-sync="showCategoryWithoutPermission" class="hide-when-skip-profile">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="showCategoryWithoutPermission" type="checkbox" checked>
|
||||
@@ -305,15 +305,17 @@
|
||||
<div class="small-description">__MSG_showTimeWithSkipsDescription__</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-sync="cleanPopup">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="cleanPopup" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="cleanPopup">
|
||||
__MSG_cleanPopup__
|
||||
</label>
|
||||
<div>
|
||||
<div data-type="toggle" data-sync="cleanPopup">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="cleanPopup" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="cleanPopup">
|
||||
__MSG_cleanPopup__
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
@@ -329,6 +331,17 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div data-type="selector" data-sync="segmentListDefaultTab">
|
||||
<label class="optionLabel" for="segmentListDefaultTab">__MSG_segmentListDefaultTab__:</label>
|
||||
|
||||
<select id="segmentListDefaultTab" class="selector-element optionsSelector">
|
||||
<option value="0">__MSG_SegmentsCap__</option>
|
||||
<option value="1">__MSG_Chapters__</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-sync="darkMode">
|
||||
@@ -343,6 +356,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-sync="prideTheme">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
<input id="prideTheme" type="checkbox" checked>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<label class="switch-label" for="prideTheme">
|
||||
__MSG_prideTheme__
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-type="toggle" data-toggle-type="reverse" data-sync="showNewFeaturePopups">
|
||||
<div class="switch-container">
|
||||
<label class="switch">
|
||||
|
||||
@@ -3,8 +3,29 @@
|
||||
--sb-main-bg-color: #222;
|
||||
--sb-main-fg-color: #fff;
|
||||
--sb-grey-bg-color: #333;
|
||||
--sb-grey-bg-active-color: #444;
|
||||
--sb-grey-fg-color: #999;
|
||||
--sb-red-bg-color: #cc1717;
|
||||
--sb-red-bg-active-color: #ec1c1c;
|
||||
--sb-skip-profile-bg: #292828;
|
||||
--sb-skip-profile-disabled: #808080;
|
||||
--sb-skip-profile-option-bg: #222;
|
||||
--sb-skip-profile-highlighted: rgb(127, 0, 0);
|
||||
}
|
||||
|
||||
.prideTheme {
|
||||
--sb-main-fg-color: #fff;
|
||||
--sb-grey-bg-color: #008026;
|
||||
--sb-grey-bg-active-color: #FF8C00;
|
||||
--sb-grey-fg-color: #FFED00;
|
||||
--sb-red-bg-color: #732982;
|
||||
--sb-red-bg-active-color: #004CFF;
|
||||
--sb-skip-profile-bg: #FFAFC8;
|
||||
--sb-skip-profile-disabled: #74D7EE;
|
||||
--sb-skip-profile-option-bg: #f687aa;
|
||||
--sb-skip-profile-highlighted: #38153f;
|
||||
|
||||
background: url("icons/pride.svg");
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -300,6 +321,8 @@
|
||||
justify-content: space-evenly;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
.sbControlsMenu-item {
|
||||
display: flex;
|
||||
@@ -321,12 +344,22 @@
|
||||
border-radius: 0px 8px 8px 0px;
|
||||
}
|
||||
.sbControlsMenu-item:hover, .sbControlsMenu-item:focus {
|
||||
background-color: #444;
|
||||
background-color: var(--sb-grey-bg-active-color) !important;
|
||||
}
|
||||
.sbControlsMenu-itemIcon {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.prideTheme .sbControlsMenu-item:nth-child(1) {
|
||||
background-color: #750787;
|
||||
}
|
||||
.prideTheme .sbControlsMenu-item:nth-child(3) {
|
||||
background-color: #0035b1;
|
||||
}
|
||||
.prideTheme .sbControlsMenu-item:nth-child(4) {
|
||||
background-color: #008026;
|
||||
}
|
||||
|
||||
/*
|
||||
* Whitelist add/remove icon
|
||||
*/
|
||||
@@ -447,7 +480,7 @@
|
||||
}
|
||||
.sbMediumButton:hover,
|
||||
.sbMediumButton:focus {
|
||||
background-color: #ec1c1c;
|
||||
background-color: var(--sb-red-bg-active-color);
|
||||
outline: none;
|
||||
}
|
||||
.sbMediumButton:active {
|
||||
@@ -585,12 +618,42 @@
|
||||
margin: 2px 1px;
|
||||
}
|
||||
#sbFooter a:hover {
|
||||
background-color: #444;
|
||||
background-color: var(--sb-grey-bg-active-color) !important;
|
||||
}
|
||||
|
||||
.prideTheme #sbFooter a:nth-of-type(1) {
|
||||
background-color: #E40303;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(2) {
|
||||
background-color: #FF8C00;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(3) {
|
||||
background-color: #b9ad00;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(4) {
|
||||
background-color: #008026;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(5) {
|
||||
background-color: #004DFF;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(6) {
|
||||
background-color: #750787;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(7) {
|
||||
background-color: #c7899d;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(8) {
|
||||
background-color: #65b8cb;
|
||||
}
|
||||
.prideTheme #sbFooter a:nth-of-type(9) {
|
||||
background-color: #613915;
|
||||
}
|
||||
|
||||
#sponsorTimesDonateContainer a {
|
||||
color: var(--sb-main-fg-color);
|
||||
text-decoration: none;
|
||||
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -661,7 +724,7 @@
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
|
||||
background-color: #292828;
|
||||
background-color: var(--sb-skip-profile-bg);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
|
||||
@@ -678,7 +741,7 @@
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
|
||||
background-color: #222;
|
||||
background-color: var(--sb-skip-profile-option-bg);
|
||||
border-radius: 5px;
|
||||
|
||||
cursor: help;
|
||||
@@ -695,13 +758,13 @@
|
||||
border-color: var(--sb-red-bg-color);
|
||||
}
|
||||
.skipOptionAction.highlighted {
|
||||
border-color: rgb(127, 0, 0);
|
||||
border-color: var(--sb-skip-profile-highlighted);
|
||||
}
|
||||
.skipOptionAction:not(.highlighted, .disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.skipOptionAction.disabled {
|
||||
color: #808080
|
||||
color: var(--sb-skip-profile-disabled);
|
||||
}
|
||||
|
||||
.optionsSelector {
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.sponsorSkipNoticeTableContainer.prideTheme {
|
||||
background: url("icons/pride.svg");
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.sponsorSkipNotice {
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { isFirefoxOrSafari, waitFor } from "../maze-utils/src";
|
||||
import { injectUpdatedScripts } from "../maze-utils/src/cleanup";
|
||||
import { logWarn } from "./utils/logger";
|
||||
import { chromeP } from "../maze-utils/src/browserApi";
|
||||
import { getHash } from "../maze-utils/src/hash";
|
||||
const utils = new Utils({
|
||||
registerFirefoxContentScript,
|
||||
unregisterFirefoxContentScript
|
||||
@@ -136,6 +137,12 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
chrome.tabs.create({url: chrome.runtime.getURL("/permissions/index.html")});
|
||||
}
|
||||
}
|
||||
|
||||
getHash(Config.config!.userID!).then((userID) => {
|
||||
if (userID == "60eed03c8644b7efa32df06977b3a4c11b62f63518e74a0e29baa1fd449cb54f") {
|
||||
Config.config.prideTheme = true;
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
|
||||
if (!isFirefoxOrSafari()) {
|
||||
|
||||
@@ -60,7 +60,7 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
|
||||
|
||||
<span className="sponsorBlockCategoryPillTitleSection">
|
||||
<img className="sponsorSkipLogo sponsorSkipObject"
|
||||
src={chrome.runtime.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||
src={chrome.runtime.getURL(Config.config.prideTheme ? "icons/sb-pride.png" : "icons/IconSponsorBlocker256px.png")}>
|
||||
</img>
|
||||
|
||||
{
|
||||
|
||||
@@ -160,7 +160,8 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
style={noticeStyle} >
|
||||
<div className={"sponsorSkipNoticeTableContainer"
|
||||
+ (this.props.fadeIn ? " sponsorSkipNoticeFadeIn" : "")
|
||||
+ (this.state.startFaded ? " sponsorSkipNoticeFaded" : "") }>
|
||||
+ (this.state.startFaded ? " sponsorSkipNoticeFaded" : "")
|
||||
+ (Config.config.prideTheme ? " prideTheme" : "")}>
|
||||
<table className={"sponsorSkipObject sponsorSkipNotice"
|
||||
+ (this.props.limitWidth ? " sponsorSkipNoticeLimitWidth" : "")}>
|
||||
<tbody>
|
||||
@@ -172,10 +173,18 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
<td className="noticeLeftIcon">
|
||||
{/* Logo */}
|
||||
{!this.props.hideLogo &&
|
||||
<SbSvg
|
||||
id={"sponsorSkipLogo" + this.idSuffix}
|
||||
fill={this.props.logoFill}
|
||||
className="sponsorSkipLogo sponsorSkipObject"/>
|
||||
(
|
||||
!Config.config.prideTheme ?
|
||||
<SbSvg
|
||||
id={"sponsorSkipLogo" + this.idSuffix}
|
||||
fill={this.props.logoFill}
|
||||
className="sponsorSkipLogo sponsorSkipObject"/>
|
||||
:
|
||||
<img
|
||||
id={"sponsorSkipLogo" + this.idSuffix}
|
||||
src={chrome.runtime.getURL("icons/sb-pride.png")}
|
||||
className="sponsorSkipLogo sponsorSkipObject"/>
|
||||
)
|
||||
}
|
||||
|
||||
<span id={"sponsorSkipMessage" + this.idSuffix}
|
||||
|
||||
@@ -123,6 +123,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
style.marginTop = "15px";
|
||||
}
|
||||
|
||||
const borderColor = this.state.selectedCategory ? Config.config.barTypes[this.state.selectedCategory]?.color : null;
|
||||
|
||||
// Create time display
|
||||
let timeDisplay: JSX.Element;
|
||||
const timeDisplayStyle: React.CSSProperties = {};
|
||||
@@ -151,7 +153,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<input id={"submittingTime0" + this.idSuffix}
|
||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
style={{color: "inherit", backgroundColor: "inherit", borderColor}}
|
||||
value={this.state.sponsorTimeEdits[0] ?? ""}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => e.stopPropagation()}
|
||||
@@ -168,7 +170,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<input id={"submittingTime1" + this.idSuffix}
|
||||
className="sponsorTimeEdit sponsorTimeEditInput"
|
||||
type="text"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
style={{color: "inherit", backgroundColor: "inherit", borderColor}}
|
||||
value={this.state.sponsorTimeEdits[1] ?? ""}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => e.stopPropagation()}
|
||||
@@ -215,7 +217,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<select id={"sponsorTimeCategories" + this.idSuffix}
|
||||
className="sponsorTimeEditSelector sponsorTimeCategories"
|
||||
ref={this.categoryOptionRef}
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
style={{color: "inherit", backgroundColor: "inherit", borderColor}}
|
||||
value={this.state.selectedCategory}
|
||||
onChange={(event) => this.categorySelectionChange(event)}>
|
||||
{this.getCategoryOptions()}
|
||||
@@ -240,7 +242,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<select id={"sponsorTimeActionTypes" + this.idSuffix}
|
||||
className="sponsorTimeEditSelector sponsorTimeActionTypes"
|
||||
value={this.state.selectedActionType}
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
style={{color: "inherit", backgroundColor: "inherit", borderColor}}
|
||||
ref={this.actionTypeOptionRef}
|
||||
onChange={(e) => this.actionTypeSelectionChange(e)}>
|
||||
{this.getActionTypeOptions(sponsorTime)}
|
||||
@@ -269,7 +271,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
<div onBlur={() => this.setState({chapterNameSelectorOpen: false})}>
|
||||
<input id={"chapterName" + this.idSuffix}
|
||||
className="sponsorTimeEdit sponsorTimeEditInput sponsorChapterNameInput"
|
||||
style={{color: "inherit", backgroundColor: "inherit"}}
|
||||
style={{color: "inherit", backgroundColor: "inherit", borderColor}}
|
||||
ref={this.descriptionOptionRef}
|
||||
type="text"
|
||||
value={this.state.description}
|
||||
@@ -310,7 +312,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</span>
|
||||
): ""}
|
||||
|
||||
{(!isNaN(segment[1]) && this.state.selectedActionType != ActionType.Full) ? (
|
||||
{(!isNaN(segment[0]) && this.state.selectedActionType != ActionType.Full) ? (
|
||||
<span id={"sponsorTimeInspectButton" + this.idSuffix}
|
||||
className="sponsorTimeEditButton"
|
||||
onClick={this.inspectTime.bind(this)}>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as React from "react";
|
||||
import * as CompileConfig from "../../../config.json";
|
||||
|
||||
import Config from "../../config";
|
||||
import { AdvancedSkipRuleSet, SkipRuleAttribute, SkipRuleOperator } from "../../utils/skipRule";
|
||||
import { ActionType, ActionTypes, CategorySkipOption } from "../../types";
|
||||
import { configToText, parseConfig, } from "../../utils/skipRule";
|
||||
import { AdvancedSkipRule } from "../../utils/skipRule.type";
|
||||
|
||||
let configSaveTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -64,210 +63,16 @@ export function AdvancedSkipOptionsComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
function compileConfig(config: string): AdvancedSkipRuleSet[] | null {
|
||||
const ruleSets: AdvancedSkipRuleSet[] = [];
|
||||
function compileConfig(config: string): AdvancedSkipRule[] | null {
|
||||
const { rules, errors } = parseConfig(config);
|
||||
|
||||
let ruleSet: AdvancedSkipRuleSet = {
|
||||
rules: [],
|
||||
skipOption: null,
|
||||
comment: ""
|
||||
};
|
||||
|
||||
for (const line of config.split("\n")) {
|
||||
if (line.trim().length === 0) {
|
||||
// Skip empty lines
|
||||
continue;
|
||||
}
|
||||
|
||||
const comment = line.match(/^\s*\/\/(.+)$/);
|
||||
if (comment) {
|
||||
if (ruleSet.rules.length > 0) {
|
||||
// Rule has already been created, add it to list if valid
|
||||
if (ruleSet.skipOption !== null && ruleSet.rules.length > 0) {
|
||||
ruleSets.push(ruleSet);
|
||||
|
||||
ruleSet = {
|
||||
rules: [],
|
||||
skipOption: null,
|
||||
comment: ""
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (ruleSet.comment.length > 0) {
|
||||
ruleSet.comment += "; ";
|
||||
}
|
||||
|
||||
ruleSet.comment += comment[1].trim();
|
||||
|
||||
// Skip comment lines
|
||||
continue;
|
||||
} else if (line.startsWith("if ")) {
|
||||
if (ruleSet.rules.length > 0) {
|
||||
// Rule has already been created, add it to list if valid
|
||||
if (ruleSet.skipOption !== null && ruleSet.rules.length > 0) {
|
||||
ruleSets.push(ruleSet);
|
||||
|
||||
ruleSet = {
|
||||
rules: [],
|
||||
skipOption: null,
|
||||
comment: ""
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const ruleTexts = [...line.matchAll(/\S+ \S+ (?:"[^"\\]*(?:\\.[^"\\]*)*"|\d+)(?= and |$)/g)];
|
||||
for (const ruleText of ruleTexts) {
|
||||
if (!ruleText[0]) return null;
|
||||
|
||||
const ruleParts = ruleText[0].match(/(\S+) (\S+) ("[^"\\]*(?:\\.[^"\\]*)*"|\d+)/);
|
||||
if (ruleParts.length !== 4) {
|
||||
return null; // Invalid rule format
|
||||
}
|
||||
|
||||
const attribute = getSkipRuleAttribute(ruleParts[1]);
|
||||
const operator = getSkipRuleOperator(ruleParts[2]);
|
||||
const value = getSkipRuleValue(ruleParts[3]);
|
||||
if (attribute === null || operator === null || value === null) {
|
||||
return null; // Invalid attribute or operator
|
||||
}
|
||||
|
||||
if ([SkipRuleOperator.Equal, SkipRuleOperator.NotEqual].includes(operator)) {
|
||||
if (attribute === SkipRuleAttribute.Category
|
||||
&& !CompileConfig.categoryList.includes(value as string)) {
|
||||
return null; // Invalid category value
|
||||
} else if (attribute === SkipRuleAttribute.ActionType
|
||||
&& !ActionTypes.includes(value as ActionType)) {
|
||||
return null; // Invalid category value
|
||||
} else if (attribute === SkipRuleAttribute.Source
|
||||
&& !["local", "youtube", "autogenerated", "server"].includes(value as string)) {
|
||||
return null; // Invalid category value
|
||||
}
|
||||
}
|
||||
|
||||
ruleSet.rules.push({
|
||||
attribute,
|
||||
operator,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
// Make sure all rules were parsed
|
||||
if (ruleTexts.length === 0 || !line.endsWith(ruleTexts[ruleTexts.length - 1][0])) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Only continue if a rule has been defined
|
||||
if (ruleSet.rules.length === 0) {
|
||||
return null; // No rules defined yet
|
||||
}
|
||||
|
||||
switch (line.trim().toLowerCase()) {
|
||||
case "disabled":
|
||||
ruleSet.skipOption = CategorySkipOption.Disabled;
|
||||
break;
|
||||
case "show overlay":
|
||||
ruleSet.skipOption = CategorySkipOption.ShowOverlay;
|
||||
break;
|
||||
case "manual skip":
|
||||
ruleSet.skipOption = CategorySkipOption.ManualSkip;
|
||||
break;
|
||||
case "auto skip":
|
||||
ruleSet.skipOption = CategorySkipOption.AutoSkip;
|
||||
break;
|
||||
default:
|
||||
return null; // Invalid skip option
|
||||
}
|
||||
}
|
||||
for (const error of errors) {
|
||||
console.error(`[SB] Error on line ${error.span.start.line}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (ruleSet.rules.length > 0 && ruleSet.skipOption !== null) {
|
||||
ruleSets.push(ruleSet);
|
||||
} else if (ruleSet.rules.length > 0 || ruleSet.skipOption !== null) {
|
||||
// Incomplete rule set
|
||||
return null;
|
||||
}
|
||||
|
||||
return ruleSets;
|
||||
}
|
||||
|
||||
function getSkipRuleAttribute(attribute: string): SkipRuleAttribute | null {
|
||||
if (attribute && Object.values(SkipRuleAttribute).includes(attribute as SkipRuleAttribute)) {
|
||||
return attribute as SkipRuleAttribute;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSkipRuleOperator(operator: string): SkipRuleOperator | null {
|
||||
if (operator && Object.values(SkipRuleOperator).includes(operator as SkipRuleOperator)) {
|
||||
return operator as SkipRuleOperator;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSkipRuleValue(value: string): string | number | null {
|
||||
if (!value) return null;
|
||||
|
||||
if (value.startsWith('"')) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return null; // Invalid JSON string
|
||||
}
|
||||
if (errors.length === 0) {
|
||||
return rules;
|
||||
} else {
|
||||
const numValue = Number(value);
|
||||
if (!isNaN(numValue)) {
|
||||
return numValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function configToText(config: AdvancedSkipRuleSet[]): string {
|
||||
let result = "";
|
||||
|
||||
for (const ruleSet of config) {
|
||||
if (ruleSet.comment) {
|
||||
result += "// " + ruleSet.comment + "\n";
|
||||
}
|
||||
|
||||
result += "if ";
|
||||
let firstRule = true;
|
||||
for (const rule of ruleSet.rules) {
|
||||
if (!firstRule) {
|
||||
result += " and ";
|
||||
}
|
||||
|
||||
result += `${rule.attribute} ${rule.operator} ${JSON.stringify(rule.value)}`;
|
||||
firstRule = false;
|
||||
}
|
||||
|
||||
switch (ruleSet.skipOption) {
|
||||
case CategorySkipOption.Disabled:
|
||||
result += "\nDisabled";
|
||||
break;
|
||||
case CategorySkipOption.ShowOverlay:
|
||||
result += "\nShow Overlay";
|
||||
break;
|
||||
case CategorySkipOption.ManualSkip:
|
||||
result += "\nManual Skip";
|
||||
break;
|
||||
case CategorySkipOption.AutoSkip:
|
||||
result += "\nAuto Skip";
|
||||
break;
|
||||
default:
|
||||
return null; // Invalid skip option
|
||||
}
|
||||
|
||||
result += "\n\n";
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
@@ -23,6 +23,12 @@ export function CategoryChooserComponent() {
|
||||
|
||||
updateChannelList(setChannelListText, selectedConfigurationID!);
|
||||
setSelections(getConfigurationValue<CategorySelection[]>(selectedConfigurationID, "categorySelections"));
|
||||
|
||||
if (selectedConfigurationID === null) {
|
||||
document.querySelectorAll(".hide-when-skip-profile").forEach((e) => e.classList.remove("hidden"));
|
||||
} else {
|
||||
document.querySelectorAll(".hide-when-skip-profile").forEach((e) => e.classList.add("hidden"));
|
||||
}
|
||||
}, [selectedConfigurationID]);
|
||||
|
||||
const createNewConfig = () => {
|
||||
@@ -92,6 +98,7 @@ export function CategoryChooserComponent() {
|
||||
id="configurationName"
|
||||
value={configurationName}
|
||||
placeholder={chrome.i18n.getMessage("ConfigurationName")}
|
||||
style={{width: document.getElementById("channelProfiles")?.clientWidth ?? null}}
|
||||
onChange={(e) => {
|
||||
const newName = e.target.value;
|
||||
getConfig(selectedConfigurationID)!.name = newName;
|
||||
@@ -145,6 +152,10 @@ export function CategoryChooserComponent() {
|
||||
}
|
||||
forceUpdateConfigurationIDs();
|
||||
|
||||
if (Config.local.skipProfileTemp && Config.local.skipProfileTemp.configID === selectedConfigurationID) {
|
||||
Config.local.skipProfileTemp = null;
|
||||
}
|
||||
|
||||
setConfigurations(Config.local!.skipProfiles);
|
||||
const newID = Object.keys(Config.local!.skipProfiles)[0] as ConfigurationID;
|
||||
setSelectedConfigurationID(newID);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as CompileConfig from "../config.json";
|
||||
import * as invidiousList from "../ci/invidiouslist.json";
|
||||
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";
|
||||
import { Permission, AdvancedSkipRuleSet } from "./utils/skipRule";
|
||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorHideType, SponsorTime, VideoID, SegmentListDefaultTab } from "./types";
|
||||
import { Keybind, keybindEquals, ProtoConfig } from "../maze-utils/src/config";
|
||||
import type { HashedValue } from "../maze-utils/src/hash";
|
||||
import { AdvancedSkipCheck, AdvancedSkipPredicate, AdvancedSkipRule, Permission, PredicateOperator } from "./utils/skipRule.type";
|
||||
|
||||
interface SBConfig {
|
||||
userID: string;
|
||||
isVip: boolean;
|
||||
permissions: Record<Category, Permission>;
|
||||
defaultCategory: Category;
|
||||
segmentListDefaultTab: SegmentListDefaultTab;
|
||||
renderSegmentsAsChapters: boolean;
|
||||
forceChannelCheck: boolean;
|
||||
minutesSaved: number;
|
||||
@@ -78,6 +79,7 @@ interface SBConfig {
|
||||
showZoomToFillError2: boolean;
|
||||
cleanPopup: boolean;
|
||||
hideSegmentCreationInPopup: boolean;
|
||||
prideTheme: boolean;
|
||||
|
||||
// Used to cache calculated text color info
|
||||
categoryPillColors: {
|
||||
@@ -166,7 +168,7 @@ interface SBStorage {
|
||||
skipProfileTemp: { time: number; configID: ConfigurationID } | null;
|
||||
skipProfiles: Record<ConfigurationID, CustomConfiguration>;
|
||||
|
||||
skipRules: AdvancedSkipRuleSet[];
|
||||
skipRules: AdvancedSkipRule[];
|
||||
}
|
||||
|
||||
class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
|
||||
@@ -186,6 +188,43 @@ class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
|
||||
}
|
||||
|
||||
function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
|
||||
if (local["skipRules"] && local["skipRules"].length !== 0 && local["skipRules"][0]["rules"]) {
|
||||
const output: AdvancedSkipRule[] = [];
|
||||
|
||||
for (const rule of local["skipRules"]) {
|
||||
const rules: object[] = rule["rules"];
|
||||
|
||||
if (rules.length !== 0) {
|
||||
let predicate: AdvancedSkipPredicate = {
|
||||
kind: "check",
|
||||
...rules[0] as AdvancedSkipCheck,
|
||||
};
|
||||
|
||||
for (let i = 1; i < rules.length; i++) {
|
||||
predicate = {
|
||||
kind: "operator",
|
||||
operator: PredicateOperator.And,
|
||||
left: predicate,
|
||||
right: {
|
||||
kind: "check",
|
||||
...rules[i] as AdvancedSkipCheck,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const comment = rule["comment"] as string;
|
||||
|
||||
output.push({
|
||||
predicate,
|
||||
skipOption: rule.skipOption,
|
||||
comments: comment.length === 0 ? [] : comment.split(/;\s*/),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
local["skipRules"] = output;
|
||||
}
|
||||
|
||||
if (config["whitelistedChannels"]) {
|
||||
// convert to skipProfiles
|
||||
const whitelistedChannels = config["whitelistedChannels"] as string[];
|
||||
@@ -337,6 +376,7 @@ const syncDefaults = {
|
||||
isVip: false,
|
||||
permissions: {},
|
||||
defaultCategory: "chooseACategory" as Category,
|
||||
segmentListDefaultTab: SegmentListDefaultTab.Segments,
|
||||
renderSegmentsAsChapters: false,
|
||||
forceChannelCheck: false,
|
||||
minutesSaved: 0,
|
||||
@@ -400,6 +440,7 @@ const syncDefaults = {
|
||||
showZoomToFillError2: true,
|
||||
cleanPopup: false,
|
||||
hideSegmentCreationInPopup: false,
|
||||
prideTheme: false,
|
||||
|
||||
categoryPillColors: {},
|
||||
|
||||
|
||||
@@ -389,6 +389,7 @@ function contentLocalConfigUpdateListener(changes: StorageChangesObject) {
|
||||
case "channelSkipProfileIDs":
|
||||
case "skipProfiles":
|
||||
case "skipProfileTemp":
|
||||
case "skipRules":
|
||||
channelIDChange();
|
||||
break;
|
||||
}
|
||||
@@ -1917,6 +1918,7 @@ function createButton(baseID: string, title: string, callback: () => void, image
|
||||
newButton.id = baseID + "Button";
|
||||
newButton.classList.add("playerButton");
|
||||
newButton.classList.add("ytp-button");
|
||||
if (Config.config.prideTheme) newButton.classList.add("prideTheme");
|
||||
if (isOnYTTV()) {
|
||||
// Some style needs to be set here, but the numbers don't matter
|
||||
newButton.setAttribute("style", "width: 40px; height: 40px");
|
||||
@@ -1957,7 +1959,8 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
|
||||
|
||||
return (!getSkipProfileBool("manualSkipOnFullVideo") || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
|
||||
&& (getCategorySelection(segment)?.option === CategorySkipOption.AutoSkip ||
|
||||
(getSkipProfileBool("autoSkipOnMusicVideos") && canSkipNonMusic && sponsorTimes?.some((s) => s.category === "music_offtopic")
|
||||
(getSkipProfileBool("autoSkipOnMusicVideos") && canSkipNonMusic
|
||||
&& sponsorTimes?.some((s) => s.category === "music_offtopic" && getCategorySelection(segment)?.option === CategorySkipOption.AutoSkip)
|
||||
&& segment.actionType === ActionType.Skip)
|
||||
|| sponsorTimesSubmitting.some((s) => s.segment === segment.segment))
|
||||
|| isLoopedChapter(segment);
|
||||
@@ -1966,7 +1969,8 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
|
||||
function shouldSkip(segment: SponsorTime): boolean {
|
||||
return segment.hidden === SponsorHideType.Visible && (segment.actionType !== ActionType.Full
|
||||
&& getCategorySelection(segment)?.option > CategorySkipOption.ShowOverlay)
|
||||
|| (getSkipProfileBool("autoSkipOnMusicVideos") && sponsorTimes?.some((s) => s.category === "music_offtopic")
|
||||
|| (getSkipProfileBool("autoSkipOnMusicVideos")
|
||||
&& sponsorTimes?.some((s) => s.category === "music_offtopic" && getCategorySelection(segment)?.option === CategorySkipOption.AutoSkip)
|
||||
&& segment.actionType === ActionType.Skip)
|
||||
|| isLoopedChapter(segment);
|
||||
}
|
||||
@@ -2062,9 +2066,11 @@ function updateEditButtonsOnPlayer(): void {
|
||||
function getRealCurrentTime(): number {
|
||||
// Used to check if replay button
|
||||
const playButtonSVGData = document.querySelector(".ytp-play-button")?.querySelector(".ytp-svg-fill")?.getAttribute("d");
|
||||
const playButtonSVGDataNew = document.querySelector(".ytp-play-button")?.querySelector("path")?.getAttribute("d");
|
||||
const replaceSVGData = "M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z";
|
||||
const replaceSVGDataNew = "M11.29 2.92C14.85 1.33 18.87 1.06 22";
|
||||
|
||||
if (playButtonSVGData === replaceSVGData) {
|
||||
if (playButtonSVGData === replaceSVGData || playButtonSVGDataNew.startsWith(replaceSVGDataNew)) {
|
||||
// At the end of the video
|
||||
return getVideoDuration();
|
||||
} else {
|
||||
|
||||
@@ -72,6 +72,12 @@ async function init() {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
|
||||
if (Config.config.prideTheme) {
|
||||
document.documentElement.setAttribute("data-theme", "pride");
|
||||
|
||||
(document.getElementById("title-bar-logo") as HTMLImageElement).src = "../icons/sb-pride.png";
|
||||
}
|
||||
|
||||
const donate = document.getElementById("sbDonate");
|
||||
donate.addEventListener("click", () => Config.config.donateClicked = Config.config.donateClicked + 1);
|
||||
if (!showDonationLink()) {
|
||||
@@ -196,6 +202,17 @@ async function init() {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
break;
|
||||
case "prideTheme":
|
||||
if (checkbox.checked) {
|
||||
document.documentElement.setAttribute("data-theme", "pride");
|
||||
} else {
|
||||
if (Config.config.darkMode) {
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "trackDownvotes":
|
||||
if (!checkbox.checked) {
|
||||
Config.local.downvotedSegments = {};
|
||||
|
||||
@@ -102,7 +102,7 @@ export const PopupComponent = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="sponsorblockPopup">
|
||||
<div id="sponsorblockPopup" className={Config.config.prideTheme ? "prideTheme" : ""}>
|
||||
{
|
||||
window !== window.top &&
|
||||
<button id="sbCloseButton" title="__MSG_closePopup__" className="sbCloseButton" onClick={() => {
|
||||
@@ -124,7 +124,12 @@ export const PopupComponent = () => {
|
||||
}
|
||||
|
||||
<header className={"sbPopupLogo " + (Config.config.cleanPopup ? "hidden" : "")}>
|
||||
<img src="icons/IconSponsorBlocker256px.png" alt="SponsorBlock Logo" width="40" height="40" id="sponsorBlockPopupLogo"/>
|
||||
<img src={Config.config.prideTheme ? "icons/sb-pride.png" : "icons/IconSponsorBlocker256px.png"}
|
||||
alt="SponsorBlock Logo"
|
||||
width="40"
|
||||
height="40"
|
||||
id="sponsorBlockPopupLogo"
|
||||
/>
|
||||
<p className="u-mZ">
|
||||
SponsorBlock
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { ActionType, SegmentUUID, SponsorHideType, SponsorTime, VideoID } from "../types";
|
||||
import { ActionType, SegmentListDefaultTab, SegmentUUID, SponsorHideType, SponsorTime, VideoID } from "../types";
|
||||
import Config from "../config";
|
||||
import { waitFor } from "../../maze-utils/src";
|
||||
import { shortCategoryName } from "../utils/categoryUtils";
|
||||
@@ -28,8 +28,16 @@ enum SegmentListTab {
|
||||
Chapter
|
||||
}
|
||||
|
||||
interface segmentWithNesting extends SponsorTime {
|
||||
innerChapters?: (segmentWithNesting|SponsorTime)[];
|
||||
interface SegmentWithNesting extends SponsorTime {
|
||||
innerChapters?: (SegmentWithNesting|SponsorTime)[];
|
||||
}
|
||||
|
||||
function isSegment(segment) {
|
||||
return segment.actionType !== ActionType.Chapter;
|
||||
}
|
||||
|
||||
function isChapter(segment) {
|
||||
return segment.actionType === ActionType.Chapter;
|
||||
}
|
||||
|
||||
export const SegmentListComponent = (props: SegmentListComponentProps) => {
|
||||
@@ -46,53 +54,70 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [hasSegments, hasChapters] = React.useMemo(() => {
|
||||
const hasSegments = Boolean(props.segments.find(isSegment))
|
||||
const hasChapters = Boolean(props.segments.find(isChapter))
|
||||
return [hasSegments, hasChapters];
|
||||
}, [props.segments]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTab(SegmentListTab.Segments);
|
||||
}, [props.videoID]);
|
||||
|
||||
const tabFilter = (segment: SponsorTime) => {
|
||||
if (tab === SegmentListTab.Chapter) {
|
||||
return segment.actionType === ActionType.Chapter;
|
||||
} else {
|
||||
return segment.actionType !== ActionType.Chapter;
|
||||
}
|
||||
};
|
||||
|
||||
const segmentsWithNesting: segmentWithNesting[] = [];
|
||||
let nbTrailingNonChapters = 0;
|
||||
function nestChapters(segments: segmentWithNesting[], seg: SponsorTime, topLevel?: boolean) {
|
||||
if (seg.actionType === ActionType.Chapter && segments.length) {
|
||||
// trailing non-chapters can only exist at top level
|
||||
const lastElement = segments[segments.length - (topLevel ? nbTrailingNonChapters + 1 : 1)]
|
||||
|
||||
if (lastElement.actionType === ActionType.Chapter
|
||||
&& lastElement.segment[0] <= seg.segment[0]
|
||||
&& lastElement.segment[1] >= seg.segment[1]) {
|
||||
if (lastElement.innerChapters){
|
||||
nestChapters(lastElement.innerChapters, seg);
|
||||
} else {
|
||||
lastElement.innerChapters = [seg];
|
||||
}
|
||||
} else {
|
||||
if (topLevel) {
|
||||
nbTrailingNonChapters = 0;
|
||||
}
|
||||
|
||||
segments.push(seg);
|
||||
}
|
||||
} else {
|
||||
if (seg.actionType !== ActionType.Chapter) {
|
||||
nbTrailingNonChapters++;
|
||||
const setTabBasedOnConfig = () => {
|
||||
const preferChapters = Config.config.segmentListDefaultTab === SegmentListDefaultTab.Chapters;
|
||||
if (preferChapters) {
|
||||
setTab(hasChapters ? SegmentListTab.Chapter : SegmentListTab.Segments);
|
||||
} else {
|
||||
setTab(hasSegments ? SegmentListTab.Segments : SegmentListTab.Chapter);
|
||||
}
|
||||
};
|
||||
|
||||
segments.push(seg);
|
||||
if (Config.isReady()) {
|
||||
setTabBasedOnConfig();
|
||||
} else {
|
||||
waitFor(() => Config.isReady()).then(setTabBasedOnConfig);
|
||||
}
|
||||
}
|
||||
props.segments.forEach((seg) => nestChapters(segmentsWithNesting, {...seg}, true));
|
||||
}, [props.videoID, hasSegments, hasChapters]);
|
||||
|
||||
const segmentsWithNesting = React.useMemo(() => {
|
||||
const result: SegmentWithNesting[] = [];
|
||||
const chapterStack: SegmentWithNesting[] = [];
|
||||
for (let seg of props.segments) {
|
||||
seg = {...seg};
|
||||
// non-chapter, do not nest
|
||||
if (seg.actionType !== ActionType.Chapter) {
|
||||
result.push(seg);
|
||||
continue;
|
||||
}
|
||||
// traverse the stack
|
||||
while (chapterStack.length !== 0) {
|
||||
// where's Array.prototype.at() :sob:
|
||||
const lastChapter = chapterStack[chapterStack.length - 1];
|
||||
// we know lastChapter.startTime <= seg.startTime, as content.ts sorts these
|
||||
// so only compare endTime - if new ends before last, new is nested inside last
|
||||
if (lastChapter.segment[1] >= seg.segment[1]) {
|
||||
lastChapter.innerChapters ??= [];
|
||||
lastChapter.innerChapters.push(seg);
|
||||
chapterStack.push(seg);
|
||||
break;
|
||||
}
|
||||
// last did not match, pop it off the stack
|
||||
chapterStack.pop();
|
||||
}
|
||||
// chapter stack not empty = we found a place for the chapter
|
||||
if (chapterStack.length !== 0) {
|
||||
continue;
|
||||
}
|
||||
// push the chapter to the top-level list and to the stack
|
||||
result.push(seg);
|
||||
chapterStack.push(seg);
|
||||
|
||||
}
|
||||
return result;
|
||||
}, [props.segments])
|
||||
|
||||
|
||||
return (
|
||||
<div id="issueReporterContainer">
|
||||
<div id="issueReporterTabs" className={props.segments && props.segments.find(s => s.actionType === ActionType.Chapter) ? "" : "hidden"}>
|
||||
<div id="issueReporterTabs" className={hasSegments && hasChapters ? "" : "hidden"}>
|
||||
<span id="issueReporterTabSegments" className={tab === SegmentListTab.Segments ? "sbSelected" : ""} onClick={() => {
|
||||
setTab(SegmentListTab.Segments);
|
||||
}}>
|
||||
@@ -119,7 +144,7 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
|
||||
isVip={isVip}
|
||||
loopedChapter={props.loopedChapter} // UUID instead of boolean so it can be passed down to nested chapters
|
||||
|
||||
tabFilter={tabFilter}
|
||||
tabFilter={tab === SegmentListTab.Chapter ? isChapter : isSegment}
|
||||
sendMessage={props.sendMessage}
|
||||
/>
|
||||
))
|
||||
@@ -136,7 +161,7 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
|
||||
};
|
||||
|
||||
function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage }: {
|
||||
segment: segmentWithNesting;
|
||||
segment: SegmentWithNesting;
|
||||
videoID: VideoID;
|
||||
currentTime: number;
|
||||
isVip: boolean;
|
||||
@@ -146,18 +171,32 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
|
||||
sendMessage: (request: Message) => Promise<MessageResponse>;
|
||||
}) {
|
||||
const [voteMessage, setVoteMessage] = React.useState<string | null>(null);
|
||||
const [hidden, setHidden] = React.useState(segment.hidden || SponsorHideType.Visible);
|
||||
const [hidden, setHidden] = React.useState(segment.hidden ?? SponsorHideType.Visible); // undefined ?? undefined lol
|
||||
const [isLooped, setIsLooped] = React.useState(loopedChapter === segment.UUID);
|
||||
|
||||
let extraInfo = "";
|
||||
if (segment.hidden === SponsorHideType.Downvoted) {
|
||||
// This one is downvoted
|
||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
|
||||
} else if (segment.hidden === SponsorHideType.MinimumDuration) {
|
||||
// This one is too short
|
||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
|
||||
} else if (segment.hidden === SponsorHideType.Hidden) {
|
||||
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
|
||||
// Update internal state if the hidden property of the segment changes
|
||||
React.useEffect(() => {
|
||||
setHidden(segment.hidden ?? SponsorHideType.Visible);
|
||||
}, [segment.hidden])
|
||||
|
||||
let extraInfo: string;
|
||||
switch (hidden) {
|
||||
case SponsorHideType.Visible:
|
||||
extraInfo = "";
|
||||
break;
|
||||
case SponsorHideType.Downvoted:
|
||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
|
||||
break;
|
||||
case SponsorHideType.MinimumDuration:
|
||||
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
|
||||
break;
|
||||
case SponsorHideType.Hidden:
|
||||
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
|
||||
break;
|
||||
default:
|
||||
// hidden satisfies never; // need to upgrade TS
|
||||
console.warn(`[SB] Unhandled variant of SponsorHideType in SegmentListItem: ${hidden}`);
|
||||
extraInfo = "";
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -279,7 +318,7 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
|
||||
{
|
||||
(segment.actionType === ActionType.Skip || segment.actionType === ActionType.Mute
|
||||
|| segment.actionType === ActionType.Poi
|
||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segment.hidden)) &&
|
||||
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(hidden)) &&
|
||||
<img
|
||||
className="voteButton"
|
||||
title={chrome.i18n.getMessage("hideSegment")}
|
||||
@@ -288,17 +327,11 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(e.currentTarget, 0.4);
|
||||
stopAnimation();
|
||||
|
||||
if (segment.hidden === SponsorHideType.Hidden) {
|
||||
segment.hidden = SponsorHideType.Visible;
|
||||
setHidden(SponsorHideType.Visible);
|
||||
} else {
|
||||
segment.hidden = SponsorHideType.Hidden;
|
||||
setHidden(SponsorHideType.Hidden);
|
||||
}
|
||||
|
||||
const newState = hidden === SponsorHideType.Hidden ? SponsorHideType.Visible : SponsorHideType.Hidden;
|
||||
setHidden(newState);
|
||||
sendMessage({
|
||||
message: "hideSegment",
|
||||
type: segment.hidden,
|
||||
type: newState,
|
||||
UUID: segment.UUID
|
||||
});
|
||||
}}/>
|
||||
@@ -343,7 +376,7 @@ function SegmentListItem({ segment, videoID, currentTime, isVip, loopedChapter,
|
||||
}
|
||||
|
||||
function InnerChapterList({ chapters, videoID, currentTime, isVip, loopedChapter, tabFilter, sendMessage }: {
|
||||
chapters: (segmentWithNesting)[];
|
||||
chapters: (SegmentWithNesting)[];
|
||||
videoID: VideoID;
|
||||
currentTime: number;
|
||||
isVip: boolean;
|
||||
|
||||
@@ -40,6 +40,10 @@ export const YourWorkComponent = () => {
|
||||
setViewCount(userInfo.viewCount);
|
||||
setMinutesSaved(userInfo.minutesSaved);
|
||||
|
||||
if (username === "sponege") {
|
||||
Config.config.prideTheme = true;
|
||||
}
|
||||
|
||||
Config.config!.isVip = userInfo.vip;
|
||||
Config.config!.permissions = userInfo.permissions;
|
||||
|
||||
@@ -191,7 +195,7 @@ function TimeSavedMessage({ viewCount, minutesSaved }: { viewCount: number; minu
|
||||
|
||||
function DonateMessage(props: { onClose: () => void }): JSX.Element {
|
||||
return (
|
||||
<div id="sponsorTimesDonateContainer" style={{ alignItems: "center", justifyContent: "center" }}>
|
||||
<div id="sponsorTimesDonateContainer" style={{ alignItems: "center", justifyContent: "center", display: "flex" }}>
|
||||
<img className="sbHeart" src="/icons/heart.svg" alt="Heart icon" />
|
||||
<a id="sbConsiderDonateLink" href="https://sponsor.ajay.app/donate" target="_blank" rel="noreferrer" onClick={() => {
|
||||
Config.config.donateClicked = Config.config.donateClicked + 1;
|
||||
|
||||
@@ -228,3 +228,8 @@ export enum NoticeVisibilityMode {
|
||||
FadedForAutoSkip = 3,
|
||||
FadedForAll = 4
|
||||
}
|
||||
|
||||
export enum SegmentListDefaultTab {
|
||||
Segments,
|
||||
Chapters,
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import { getSkipProfileBool } from "./skipProfiles";
|
||||
|
||||
export function getControls(): HTMLElement {
|
||||
const controlsSelectors = [
|
||||
// New YouTube (2025 April)
|
||||
".ytp-right-controls-right",
|
||||
// YouTube
|
||||
".ytp-right-controls",
|
||||
// Mobile YouTube
|
||||
|
||||
@@ -1,63 +1,37 @@
|
||||
import { getCurrentPageTitle } from "../../maze-utils/src/elements";
|
||||
import { getChannelIDInfo, getVideoDuration } from "../../maze-utils/src/video";
|
||||
import Config from "../config";
|
||||
import { CategorySelection, CategorySkipOption, SponsorSourceType, SponsorTime } from "../types";
|
||||
import {ActionType, ActionTypes, CategorySelection, CategorySkipOption, SponsorSourceType, SponsorTime} from "../types";
|
||||
import { getSkipProfile, getSkipProfileBool } from "./skipProfiles";
|
||||
import { VideoLabelsCacheData } from "./videoLabels";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { AdvancedSkipCheck, AdvancedSkipPredicate, AdvancedSkipRule, PredicateOperator, SkipRuleAttribute, SkipRuleOperator } from "./skipRule.type";
|
||||
|
||||
export interface Permission {
|
||||
canSubmit: boolean;
|
||||
}
|
||||
|
||||
export enum SkipRuleAttribute {
|
||||
StartTime = "time.start",
|
||||
EndTime = "time.end",
|
||||
Duration = "time.duration",
|
||||
StartTimePercent = "time.startPercent",
|
||||
EndTimePercent = "time.endPercent",
|
||||
DurationPercent = "time.durationPercent",
|
||||
Category = "category",
|
||||
ActionType = "actionType",
|
||||
Description = "chapter.name",
|
||||
Source = "chapter.source",
|
||||
ChannelID = "channel.id",
|
||||
ChannelName = "channel.name",
|
||||
VideoDuration = "video.duration",
|
||||
Title = "video.title"
|
||||
}
|
||||
|
||||
export enum SkipRuleOperator {
|
||||
Less = "<",
|
||||
LessOrEqual = "<=",
|
||||
Greater = ">",
|
||||
GreaterOrEqual = ">=",
|
||||
Equal = "==",
|
||||
NotEqual = "!=",
|
||||
Contains = "*=",
|
||||
NotContains = "!*=",
|
||||
Regex = "~=",
|
||||
RegexIgnoreCase = "~i=",
|
||||
NotRegex = "!~=",
|
||||
NotRegexIgnoreCase = "!~i="
|
||||
}
|
||||
|
||||
export interface AdvancedSkipRule {
|
||||
attribute: SkipRuleAttribute;
|
||||
operator: SkipRuleOperator;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface AdvancedSkipRuleSet {
|
||||
rules: AdvancedSkipRule[];
|
||||
skipOption: CategorySkipOption;
|
||||
comment: string;
|
||||
}
|
||||
const SKIP_RULE_ATTRIBUTES = Object.values(SkipRuleAttribute);
|
||||
const SKIP_RULE_OPERATORS = Object.values(SkipRuleOperator);
|
||||
const INVERTED_SKIP_RULE_OPERATORS = {
|
||||
"<=": SkipRuleOperator.Greater,
|
||||
"<": SkipRuleOperator.GreaterOrEqual,
|
||||
">=": SkipRuleOperator.Less,
|
||||
">": SkipRuleOperator.LessOrEqual,
|
||||
"!=": SkipRuleOperator.Equal,
|
||||
"==": SkipRuleOperator.NotEqual,
|
||||
"!*=": SkipRuleOperator.Contains,
|
||||
"*=": SkipRuleOperator.NotContains,
|
||||
"!~=": SkipRuleOperator.Regex,
|
||||
"~=": SkipRuleOperator.NotRegex,
|
||||
"!~i=": SkipRuleOperator.RegexIgnoreCase,
|
||||
"~i=": SkipRuleOperator.NotRegexIgnoreCase,
|
||||
};
|
||||
const WORD_EXTRA_CHARACTER = /[a-zA-Z0-9.]/;
|
||||
const OPERATOR_EXTRA_CHARACTER = /[<>=!~*&|-]/;
|
||||
const ANY_EXTRA_CHARACTER = /[a-zA-Z0-9<>=!~*&|.-]/;
|
||||
|
||||
export function getCategorySelection(segment: SponsorTime | VideoLabelsCacheData): CategorySelection {
|
||||
// First check skip rules
|
||||
for (const ruleSet of Config.local.skipRules) {
|
||||
if (ruleSet.rules.every((rule) => isSkipRulePassing(segment, rule))) {
|
||||
return { name: segment.category, option: ruleSet.skipOption } as CategorySelection;
|
||||
for (const rule of Config.local.skipRules) {
|
||||
if (isSkipPredicatePassing(segment, rule.predicate)) {
|
||||
return { name: segment.category, option: rule.skipOption } as CategorySelection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +58,7 @@ export function getCategorySelection(segment: SponsorTime | VideoLabelsCacheData
|
||||
return { name: segment.category, option: CategorySkipOption.Disabled} as CategorySelection;
|
||||
}
|
||||
|
||||
function getSkipRuleValue(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipRule): string | number | undefined {
|
||||
function getSkipCheckValue(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipCheck): string | number | undefined {
|
||||
switch (rule.attribute) {
|
||||
case SkipRuleAttribute.StartTime:
|
||||
return (segment as SponsorTime).segment?.[0];
|
||||
@@ -143,8 +117,8 @@ function getSkipRuleValue(segment: SponsorTime | VideoLabelsCacheData, rule: Adv
|
||||
}
|
||||
}
|
||||
|
||||
function isSkipRulePassing(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipRule): boolean {
|
||||
const value = getSkipRuleValue(segment, rule);
|
||||
function isSkipCheckPassing(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipCheck): boolean {
|
||||
const value = getSkipCheckValue(segment, rule);
|
||||
|
||||
switch (rule.operator) {
|
||||
case SkipRuleOperator.Less:
|
||||
@@ -176,6 +150,19 @@ function isSkipRulePassing(segment: SponsorTime | VideoLabelsCacheData, rule: Ad
|
||||
}
|
||||
}
|
||||
|
||||
function isSkipPredicatePassing(segment: SponsorTime | VideoLabelsCacheData, predicate: AdvancedSkipPredicate): boolean {
|
||||
if (predicate.kind === "check") {
|
||||
return isSkipCheckPassing(segment, predicate as AdvancedSkipCheck);
|
||||
} else { // predicate.kind === "operator"
|
||||
// TODO Is recursion fine to use here?
|
||||
if (predicate.operator == PredicateOperator.And) {
|
||||
return isSkipPredicatePassing(segment, predicate.left) && isSkipPredicatePassing(segment, predicate.right);
|
||||
} else { // predicate.operator === PredicateOperator.Or
|
||||
return isSkipPredicatePassing(segment, predicate.left) || isSkipPredicatePassing(segment, predicate.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCategoryDefaultSelection(category: string): CategorySelection {
|
||||
for (const selection of Config.config.categorySelections) {
|
||||
if (selection.name === category) {
|
||||
@@ -184,3 +171,761 @@ export function getCategoryDefaultSelection(category: string): CategorySelection
|
||||
}
|
||||
return { name: category, option: CategorySkipOption.Disabled} as CategorySelection;
|
||||
}
|
||||
|
||||
type TokenType =
|
||||
| "if" // Keywords
|
||||
| "disabled" | "show overlay" | "manual skip" | "auto skip" // Skip option
|
||||
| `${SkipRuleAttribute}` // Segment attributes
|
||||
| `${SkipRuleOperator}` // Segment attribute operators
|
||||
| "and" | "or" | "not" // Expression operators
|
||||
| "(" | ")" | "comment" // Syntax
|
||||
| "string" | "number" // Literal values
|
||||
| "eof" | "error"; // Sentinel and special tokens
|
||||
|
||||
export interface SourcePos {
|
||||
line: number;
|
||||
}
|
||||
|
||||
export interface Span {
|
||||
start: SourcePos;
|
||||
end: SourcePos;
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: TokenType;
|
||||
span: Span;
|
||||
value: string;
|
||||
}
|
||||
|
||||
class Lexer {
|
||||
private readonly source: string;
|
||||
private start: number;
|
||||
private current: number;
|
||||
|
||||
private start_pos: SourcePos;
|
||||
private current_pos: SourcePos;
|
||||
|
||||
public constructor(source: string) {
|
||||
this.source = source;
|
||||
this.start = 0;
|
||||
this.current = 0;
|
||||
this.start_pos = { line: 1 };
|
||||
this.current_pos = { line: 1 };
|
||||
}
|
||||
|
||||
private makeToken(type: TokenType): Token {
|
||||
return {
|
||||
type,
|
||||
span: { start: this.start_pos, end: this.current_pos, },
|
||||
value: this.source.slice(this.start, this.current),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UTF-16 value at the current position and advances it forward.
|
||||
* If the end of the source string has been reached, returns <code>null</code>.
|
||||
*
|
||||
* @return current UTF-16 value, or <code>null</code> on EOF
|
||||
*/
|
||||
private consume(): string | null {
|
||||
if (this.source.length > this.current) {
|
||||
// The UTF-16 value at the current position, which could be either a Unicode code point or a lone surrogate.
|
||||
// The check above this is also based on the UTF-16 value count, so this should not be able to fail on “weird” inputs.
|
||||
const c = this.source[this.current];
|
||||
this.current++;
|
||||
|
||||
if (c === "\n") {
|
||||
// Cannot use this.current_pos.line++, because SourcePos is mutable and used in tokens without copying
|
||||
this.current_pos = { line: this.current_pos.line + 1, };
|
||||
}
|
||||
|
||||
return c;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UTF-16 value at the current position without advancing it.
|
||||
* If the end of the source string has been reached, returns <code>null</code>.
|
||||
*
|
||||
* @return current UTF-16 value, or <code>null</code> on EOF
|
||||
*/
|
||||
private peek(): string | null {
|
||||
if (this.source.length > this.current) {
|
||||
// See comment in consume() for Unicode expectations here
|
||||
return this.source[this.current];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the word at the current position against a list of
|
||||
* expected keywords. The keyword can consist of multiple characters.
|
||||
* If a match is found, the current position is advanced by the length
|
||||
* of the keyword found.
|
||||
*
|
||||
* @param keywords the expected set of keywords at the current position
|
||||
* @param caseSensitive whether to do a case-sensitive comparison
|
||||
* @return the matching keyword, or <code>null</code>
|
||||
*/
|
||||
private expectKeyword(keywords: readonly string[], caseSensitive: boolean): string | null {
|
||||
for (const keyword of keywords) {
|
||||
// slice() clamps to string length, so cannot cause out of bounds errors
|
||||
const actual = this.source.slice(this.current, this.current + keyword.length);
|
||||
|
||||
if (caseSensitive && keyword === actual || !caseSensitive && keyword.toLowerCase() === actual.toLowerCase()) {
|
||||
// Does not handle keywords containing line feeds, which shouldn't happen anyway
|
||||
this.current += keyword.length;
|
||||
return keyword;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips a series of whitespace characters starting at the current
|
||||
* position. May advance the current position multiple times, once,
|
||||
* or not at all.
|
||||
*/
|
||||
private skipWhitespace() {
|
||||
let c = this.peek();
|
||||
const whitespace = /\s+/;
|
||||
|
||||
while (c != null) {
|
||||
if (!whitespace.test(c)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.consume();
|
||||
c = this.peek();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips all characters until the next <code>"\n"</code> (line feed)
|
||||
* character occurs (inclusive). Will always advance the current position
|
||||
* at least once.
|
||||
*/
|
||||
private skipLine() {
|
||||
let c = this.consume();
|
||||
while (c != null) {
|
||||
if (c == '\n') {
|
||||
return;
|
||||
}
|
||||
|
||||
c = this.consume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the lexer has reached the end of input
|
||||
*/
|
||||
private isEof(): boolean {
|
||||
return this.current >= this.source.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the start position of the next token that will be emitted
|
||||
* to the current position.
|
||||
*
|
||||
* More characters need to be consumed after calling this, as
|
||||
* an empty token would be emitted otherwise.
|
||||
*/
|
||||
private resetToCurrent() {
|
||||
this.start = this.current;
|
||||
this.start_pos = this.current_pos;
|
||||
}
|
||||
|
||||
public nextToken(): Token {
|
||||
this.skipWhitespace();
|
||||
this.resetToCurrent();
|
||||
|
||||
if (this.isEof()) {
|
||||
return this.makeToken("eof");
|
||||
}
|
||||
|
||||
const keyword = this.expectKeyword([
|
||||
"if", "and", "or", "not",
|
||||
"(", ")",
|
||||
"//",
|
||||
].concat(SKIP_RULE_ATTRIBUTES)
|
||||
.concat(SKIP_RULE_OPERATORS), true);
|
||||
let type: TokenType | null = null;
|
||||
let kind: "word" | "operator" | null = null;
|
||||
|
||||
if (keyword !== null) {
|
||||
if ((SKIP_RULE_ATTRIBUTES as string[]).includes(keyword)) {
|
||||
kind = "word";
|
||||
type = keyword as TokenType;
|
||||
} else if ((SKIP_RULE_OPERATORS as string[]).includes(keyword)) {
|
||||
kind = "operator";
|
||||
type = keyword as TokenType;
|
||||
} else {
|
||||
switch (keyword) {
|
||||
case "if": // Fallthrough
|
||||
case "and": // Fallthrough
|
||||
case "or": // Fallthrough
|
||||
case "not": kind = "word"; type = keyword as TokenType; break;
|
||||
|
||||
case "(": return this.makeToken("(");
|
||||
case ")": return this.makeToken(")");
|
||||
|
||||
case "//":
|
||||
this.resetToCurrent();
|
||||
this.skipLine();
|
||||
return this.makeToken("comment");
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const keyword2 = this.expectKeyword(
|
||||
[ "disabled", "show overlay", "manual skip", "auto skip" ], false);
|
||||
|
||||
if (keyword2 !== null) {
|
||||
kind = "word";
|
||||
type = keyword2 as TokenType;
|
||||
}
|
||||
}
|
||||
|
||||
if (type !== null) {
|
||||
const more = kind == "operator" ? OPERATOR_EXTRA_CHARACTER : kind == "word" ? WORD_EXTRA_CHARACTER : ANY_EXTRA_CHARACTER;
|
||||
|
||||
let c = this.peek();
|
||||
let error = false;
|
||||
while (c !== null && more.test(c)) {
|
||||
error = true;
|
||||
this.consume();
|
||||
c = this.peek();
|
||||
}
|
||||
|
||||
return this.makeToken(error ? "error" : type);
|
||||
}
|
||||
|
||||
let c = this.consume();
|
||||
|
||||
if (c === '"') {
|
||||
// Parses string according to ECMA-404 2nd edition (JSON), section 9 “String”
|
||||
let output = "";
|
||||
let c = this.consume();
|
||||
let error = false;
|
||||
|
||||
while (c !== null && c !== '"') {
|
||||
if (c == '\\') {
|
||||
c = this.consume();
|
||||
|
||||
switch (c) {
|
||||
case '"':
|
||||
output = output.concat('"');
|
||||
break;
|
||||
case '\\':
|
||||
output = output.concat('\\');
|
||||
break;
|
||||
case '/':
|
||||
output = output.concat('/');
|
||||
break;
|
||||
case 'b':
|
||||
output = output.concat('\b');
|
||||
break;
|
||||
case 'f':
|
||||
output = output.concat('\f');
|
||||
break;
|
||||
case 'n':
|
||||
output = output.concat('\n');
|
||||
break;
|
||||
case 'r':
|
||||
output = output.concat('\r');
|
||||
break;
|
||||
case 't':
|
||||
output = output.concat('\t');
|
||||
break;
|
||||
case 'u': {
|
||||
// UTF-16 value sequence
|
||||
const digits = this.source.slice(this.current, this.current + 4);
|
||||
|
||||
if (digits.length < 4 || !/[0-9a-zA-Z]{4}/.test(digits)) {
|
||||
error = true;
|
||||
output = output.concat(`\\u`);
|
||||
c = this.consume();
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = parseInt(digits, 16);
|
||||
// fromCharCode() takes a UTF-16 value without performing validity checks,
|
||||
// which is exactly what is needed here – in JSON, code units outside the
|
||||
// BMP are represented by two Unicode escape sequences.
|
||||
output = output.concat(String.fromCharCode(value));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
error = true;
|
||||
output = output.concat(`\\${c}`);
|
||||
break;
|
||||
}
|
||||
} else if (c === '\n') {
|
||||
// Unterminated / multi-line string, unsupported
|
||||
error = true;
|
||||
// Prevent unterminated strings from consuming the entire rest of the input
|
||||
break;
|
||||
} else {
|
||||
output = output.concat(c);
|
||||
}
|
||||
|
||||
c = this.consume();
|
||||
}
|
||||
|
||||
return {
|
||||
type: error || c !== '"' ? "error" : "string",
|
||||
span: { start: this.start_pos, end: this.current_pos, },
|
||||
value: output,
|
||||
};
|
||||
} else if (/[0-9-]/.test(c)) {
|
||||
// Parses number according to ECMA-404 2nd edition (JSON), section 8 “Numbers”
|
||||
if (c === '-') {
|
||||
c = this.consume();
|
||||
|
||||
if (!/[0-9]/.test(c)) {
|
||||
return this.makeToken("error");
|
||||
}
|
||||
}
|
||||
|
||||
const leadingZero = c === '0';
|
||||
let next = this.peek();
|
||||
let error = false;
|
||||
|
||||
while (next !== null && /[0-9]/.test(next)) {
|
||||
this.consume();
|
||||
next = this.peek();
|
||||
|
||||
if (leadingZero) {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (next !== null && next === '.') {
|
||||
this.consume();
|
||||
next = this.peek();
|
||||
|
||||
if (next === null || !/[0-9]/.test(next)) {
|
||||
return this.makeToken("error");
|
||||
}
|
||||
|
||||
do {
|
||||
this.consume();
|
||||
next = this.peek();
|
||||
} while (next !== null && /[0-9]/.test(next));
|
||||
}
|
||||
|
||||
next = this.peek();
|
||||
|
||||
if (next != null && (next === 'e' || next === 'E')) {
|
||||
this.consume();
|
||||
next = this.peek();
|
||||
|
||||
if (next === null) {
|
||||
return this.makeToken("error");
|
||||
}
|
||||
|
||||
if (next === '+' || next === '-') {
|
||||
this.consume();
|
||||
next = this.peek();
|
||||
}
|
||||
|
||||
while (next !== null && /[0-9]/.test(next)) {
|
||||
this.consume();
|
||||
next = this.peek();
|
||||
}
|
||||
}
|
||||
|
||||
return this.makeToken(error ? "error" : "number");
|
||||
}
|
||||
|
||||
// Consume common characters up to a space for a more useful value in the error token
|
||||
const common = ANY_EXTRA_CHARACTER;
|
||||
c = this.peek();
|
||||
while (c !== null && common.test(c)) {
|
||||
this.consume();
|
||||
c = this.peek();
|
||||
}
|
||||
|
||||
return this.makeToken("error");
|
||||
}
|
||||
}
|
||||
|
||||
export interface ParseError {
|
||||
span: Span;
|
||||
message: string;
|
||||
}
|
||||
|
||||
class Parser {
|
||||
private lexer: Lexer;
|
||||
|
||||
private previous: Token;
|
||||
private current: Token;
|
||||
|
||||
private readonly rules: AdvancedSkipRule[];
|
||||
private readonly errors: ParseError[];
|
||||
|
||||
private erroring: boolean;
|
||||
private panicMode: boolean;
|
||||
|
||||
public constructor(lexer: Lexer) {
|
||||
this.lexer = lexer;
|
||||
this.previous = null;
|
||||
this.current = lexer.nextToken();
|
||||
this.rules = [];
|
||||
this.errors = [];
|
||||
this.erroring = false;
|
||||
this.panicMode = false;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
* Adds an error message. The current skip rule will be marked as erroring.
|
||||
*
|
||||
* @param span the range of the error
|
||||
* @param message the message to report
|
||||
* @param panic if <code>true</code>, all further errors will be silenced
|
||||
* until panic mode is disabled again
|
||||
*/
|
||||
private errorAt(span: Span, message: string, panic: boolean) {
|
||||
if (!this.panicMode) {
|
||||
this.errors.push({span, message,});
|
||||
}
|
||||
|
||||
this.panicMode ||= panic;
|
||||
this.erroring = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an error message for an error occurring at the previous token
|
||||
* (which was just consumed).
|
||||
*
|
||||
* @param message the message to report
|
||||
* @param panic if <code>true</code>, all further errors will be silenced
|
||||
* until panic mode is disabled again
|
||||
*/
|
||||
private error(message: string, panic: boolean) {
|
||||
this.errorAt(this.previous.span, message, panic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an error message for an error occurring at the current token
|
||||
* (which has not been consumed yet).
|
||||
*
|
||||
* @param message the message to report
|
||||
* @param panic if <code>true</code>, all further errors will be silenced
|
||||
* until panic mode is disabled again
|
||||
*/
|
||||
private errorAtCurrent(message: string, panic: boolean) {
|
||||
this.errorAt(this.current.span, message, panic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the current token, which can then be accessed at <code>previous</code>.
|
||||
* The next token will be at <code>current</code> after this call.
|
||||
*
|
||||
* If a token of type <code>error</code> is found, issues an error message.
|
||||
*/
|
||||
private consume() {
|
||||
this.previous = this.current;
|
||||
// Intentionally ignoring `error` tokens here;
|
||||
// by handling those in later privates with more context (match(), expect(), ...),
|
||||
// the user gets better errors
|
||||
this.current = this.lexer.nextToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the current token (that has not been consumed yet) against a set of expected token types.
|
||||
*
|
||||
* @param expected the set of expected token types
|
||||
* @return whether the actual current token matches any expected token type
|
||||
*/
|
||||
private match(expected: readonly TokenType[]): boolean {
|
||||
if (expected.includes(this.current.type)) {
|
||||
this.consume();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the current token (that has not been consumed yet) against a set of expected token types.
|
||||
*
|
||||
* If there is no match, issues an error message which will be prepended to <code>, got: <token type></code>.
|
||||
*
|
||||
* @param expected the set of expected token types
|
||||
* @param message the error message to report in case the actual token doesn't match
|
||||
* @param panic if <code>true</code>, all further errors will be silenced
|
||||
* until panic mode is disabled again
|
||||
*/
|
||||
private expect(expected: readonly TokenType[], message: string, panic: boolean) {
|
||||
if (!this.match(expected)) {
|
||||
this.errorAtCurrent(message.concat(this.current.type === "error" ? `, got: ${JSON.stringify(this.current.value)}` : `, got: \`${this.current.type}\``), panic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize with the next rule block and disable panic mode.
|
||||
* Skips all tokens until the <code>if</code> keyword is found.
|
||||
*/
|
||||
private synchronize() {
|
||||
this.panicMode = false;
|
||||
|
||||
while (!this.isEof()) {
|
||||
if (this.current.type === "if") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the parser has reached the end of input
|
||||
*/
|
||||
private isEof(): boolean {
|
||||
return this.current.type === "eof";
|
||||
}
|
||||
|
||||
// Parsing functions
|
||||
|
||||
/**
|
||||
* Parse the config. Should only ever be called once on a given
|
||||
* <code>Parser</code> instance.
|
||||
*/
|
||||
public parse(): { rules: AdvancedSkipRule[]; errors: ParseError[] } {
|
||||
while (!this.isEof()) {
|
||||
this.erroring = false;
|
||||
const rule = this.parseRule();
|
||||
|
||||
if (!this.erroring && rule) {
|
||||
this.rules.push(rule);
|
||||
}
|
||||
|
||||
if (this.panicMode) {
|
||||
this.synchronize();
|
||||
}
|
||||
}
|
||||
|
||||
return { rules: this.rules, errors: this.errors, };
|
||||
}
|
||||
|
||||
private parseRule(): AdvancedSkipRule | null {
|
||||
const rule: AdvancedSkipRule = {
|
||||
predicate: null,
|
||||
skipOption: null,
|
||||
comments: [],
|
||||
};
|
||||
|
||||
while (this.match(["comment"])) {
|
||||
rule.comments.push(this.previous.value.trim());
|
||||
}
|
||||
|
||||
this.expect(["if"], rule.comments.length !== 0 ? "expected `if` after `comment`" : "expected `if`", true);
|
||||
rule.predicate = this.parsePredicate();
|
||||
|
||||
this.expect(["disabled", "show overlay", "manual skip", "auto skip"], "expected skip option after condition", true);
|
||||
switch (this.previous.type) {
|
||||
case "disabled":
|
||||
rule.skipOption = CategorySkipOption.Disabled;
|
||||
break;
|
||||
case "show overlay":
|
||||
rule.skipOption = CategorySkipOption.ShowOverlay;
|
||||
break;
|
||||
case "manual skip":
|
||||
rule.skipOption = CategorySkipOption.ManualSkip;
|
||||
break;
|
||||
case "auto skip":
|
||||
rule.skipOption = CategorySkipOption.AutoSkip;
|
||||
break;
|
||||
default:
|
||||
// Ignore, should have already errored
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
private parsePredicate(): AdvancedSkipPredicate | null {
|
||||
return this.parseOr();
|
||||
}
|
||||
|
||||
private parseOr(): AdvancedSkipPredicate | null {
|
||||
let left = this.parseAnd();
|
||||
|
||||
while (this.match(["or"])) {
|
||||
const right = this.parseAnd();
|
||||
|
||||
left = {
|
||||
kind: "operator",
|
||||
operator: PredicateOperator.Or,
|
||||
left, right,
|
||||
};
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private parseAnd(): AdvancedSkipPredicate | null {
|
||||
let left = this.parseUnary();
|
||||
|
||||
while (this.match(["and"])) {
|
||||
const right = this.parseUnary();
|
||||
|
||||
left = {
|
||||
kind: "operator",
|
||||
operator: PredicateOperator.And,
|
||||
left, right,
|
||||
};
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private parseUnary(): AdvancedSkipPredicate | null {
|
||||
if (this.match(["not"])) {
|
||||
const predicate = this.parseUnary();
|
||||
return predicate ? invertPredicate(predicate) : null;
|
||||
}
|
||||
|
||||
return this.parsePrimary();
|
||||
}
|
||||
|
||||
private parsePrimary(): AdvancedSkipPredicate | null {
|
||||
if (this.match(["("])) {
|
||||
const predicate = this.parsePredicate();
|
||||
this.expect([")"], "expected `)` after condition", true);
|
||||
return predicate;
|
||||
} else {
|
||||
return this.parseCheck();
|
||||
}
|
||||
}
|
||||
|
||||
private parseCheck(): AdvancedSkipCheck | null {
|
||||
this.expect(SKIP_RULE_ATTRIBUTES, `expected attribute after \`${this.previous.type}\``, true);
|
||||
|
||||
if (this.erroring) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attribute = this.previous.type as SkipRuleAttribute;
|
||||
this.expect(SKIP_RULE_OPERATORS, `expected operator after \`${attribute}\``, true);
|
||||
|
||||
if (this.erroring) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const operator = this.previous.type as SkipRuleOperator;
|
||||
this.expect(["string", "number"], `expected string or number after \`${operator}\``, true);
|
||||
|
||||
if (this.erroring) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = this.previous.type === "number" ? Number(this.previous.value) : this.previous.value;
|
||||
|
||||
if ([SkipRuleOperator.Equal, SkipRuleOperator.NotEqual].includes(operator)) {
|
||||
if (attribute === SkipRuleAttribute.Category
|
||||
&& !CompileConfig.categoryList.includes(value as string)) {
|
||||
this.error(`unknown category: \`${value}\``, false);
|
||||
return null;
|
||||
} else if (attribute === SkipRuleAttribute.ActionType
|
||||
&& !ActionTypes.includes(value as ActionType)) {
|
||||
this.error(`unknown action type: \`${value}\``, false);
|
||||
return null;
|
||||
} else if (attribute === SkipRuleAttribute.Source
|
||||
&& !["local", "youtube", "autogenerated", "server"].includes(value as string)) {
|
||||
this.error(`unknown chapter source: \`${value}\``, false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "check",
|
||||
attribute, operator, value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseConfig(config: string): { rules: AdvancedSkipRule[]; errors: ParseError[] } {
|
||||
const parser = new Parser(new Lexer(config));
|
||||
return parser.parse();
|
||||
}
|
||||
|
||||
export function configToText(config: AdvancedSkipRule[]): string {
|
||||
let result = "";
|
||||
|
||||
for (const rule of config) {
|
||||
for (const comment of rule.comments) {
|
||||
result += "// " + comment + "\n";
|
||||
}
|
||||
|
||||
result += "if ";
|
||||
result += predicateToText(rule.predicate, null);
|
||||
|
||||
switch (rule.skipOption) {
|
||||
case CategorySkipOption.Disabled:
|
||||
result += "\nDisabled";
|
||||
break;
|
||||
case CategorySkipOption.ShowOverlay:
|
||||
result += "\nShow Overlay";
|
||||
break;
|
||||
case CategorySkipOption.ManualSkip:
|
||||
result += "\nManual Skip";
|
||||
break;
|
||||
case CategorySkipOption.AutoSkip:
|
||||
result += "\nAuto Skip";
|
||||
break;
|
||||
default:
|
||||
return null; // Invalid skip option
|
||||
}
|
||||
|
||||
result += "\n\n";
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
function predicateToText(predicate: AdvancedSkipPredicate, outerPrecedence: "or" | "and" | "not" | null): string {
|
||||
if (predicate.kind === "check") {
|
||||
return `${predicate.attribute} ${predicate.operator} ${JSON.stringify(predicate.value)}`;
|
||||
} else if (predicate.displayInverted) {
|
||||
// Should always be fine, considering `not` has the highest precedence
|
||||
return `not ${predicateToText(invertPredicate(predicate), "not")}`;
|
||||
} else {
|
||||
let text: string;
|
||||
|
||||
if (predicate.operator === PredicateOperator.And) {
|
||||
text = `${predicateToText(predicate.left, "and")} and ${predicateToText(predicate.right, "and")}`;
|
||||
} else { // Or
|
||||
text = `${predicateToText(predicate.left, "or")} or ${predicateToText(predicate.right, "or")}`;
|
||||
}
|
||||
|
||||
return outerPrecedence !== null && outerPrecedence !== predicate.operator ? `(${text})` : text;
|
||||
}
|
||||
}
|
||||
|
||||
function invertPredicate(predicate: AdvancedSkipPredicate): AdvancedSkipPredicate {
|
||||
if (predicate.kind === "check") {
|
||||
return {
|
||||
...predicate,
|
||||
operator: INVERTED_SKIP_RULE_OPERATORS[predicate.operator],
|
||||
};
|
||||
} else {
|
||||
// not (a and b) == (not a or not b)
|
||||
// not (a or b) == (not a and not b)
|
||||
return {
|
||||
kind: "operator",
|
||||
operator: predicate.operator === "and" ? PredicateOperator.Or : PredicateOperator.And,
|
||||
left: predicate.left ? invertPredicate(predicate.left) : null,
|
||||
right: predicate.right ? invertPredicate(predicate.right) : null,
|
||||
displayInverted: !predicate.displayInverted,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
71
src/utils/skipRule.type.ts
Normal file
71
src/utils/skipRule.type.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { CategorySkipOption } from "../types";
|
||||
|
||||
export interface Permission {
|
||||
canSubmit: boolean;
|
||||
}
|
||||
|
||||
// Note that attributes that are prefixes of other attributes (like `time.start`) need to be ordered *after*
|
||||
// the longer attributes, because these are matched sequentially. Using the longer attribute would otherwise result
|
||||
// in an error token.
|
||||
export enum SkipRuleAttribute {
|
||||
StartTimePercent = "time.startPercent",
|
||||
StartTime = "time.start",
|
||||
EndTimePercent = "time.endPercent",
|
||||
EndTime = "time.end",
|
||||
DurationPercent = "time.durationPercent",
|
||||
Duration = "time.duration",
|
||||
Category = "category",
|
||||
ActionType = "actionType",
|
||||
Description = "chapter.name",
|
||||
Source = "chapter.source",
|
||||
ChannelID = "channel.id",
|
||||
ChannelName = "channel.name",
|
||||
VideoDuration = "video.duration",
|
||||
Title = "video.title"
|
||||
}
|
||||
|
||||
// Note that operators that are prefixes of other attributes (like `<`) need to be ordered *after* the longer
|
||||
// operators, because these are matched sequentially. Using the longer operator would otherwise result
|
||||
// in an error token.
|
||||
export enum SkipRuleOperator {
|
||||
LessOrEqual = "<=",
|
||||
Less = "<",
|
||||
GreaterOrEqual = ">=",
|
||||
Greater = ">",
|
||||
NotEqual = "!=",
|
||||
Equal = "==",
|
||||
NotContains = "!*=",
|
||||
Contains = "*=",
|
||||
NotRegex = "!~=",
|
||||
Regex = "~=",
|
||||
NotRegexIgnoreCase = "!~i=",
|
||||
RegexIgnoreCase = "~i="
|
||||
}
|
||||
|
||||
export interface AdvancedSkipCheck {
|
||||
kind: "check";
|
||||
attribute: SkipRuleAttribute;
|
||||
operator: SkipRuleOperator;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export enum PredicateOperator {
|
||||
And = "and",
|
||||
Or = "or",
|
||||
}
|
||||
|
||||
export interface AdvancedSkipOperator {
|
||||
kind: "operator";
|
||||
operator: PredicateOperator;
|
||||
left: AdvancedSkipPredicate;
|
||||
right: AdvancedSkipPredicate;
|
||||
displayInverted?: boolean;
|
||||
}
|
||||
|
||||
export type AdvancedSkipPredicate = AdvancedSkipCheck | AdvancedSkipOperator;
|
||||
|
||||
export interface AdvancedSkipRule {
|
||||
predicate: AdvancedSkipPredicate;
|
||||
skipOption: CategorySkipOption;
|
||||
comments: string[];
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"noImplicitAny": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": false,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist/js",
|
||||
"noEmitOnError": false,
|
||||
"typeRoots": [ "node_modules/@types" ],
|
||||
|
||||
@@ -189,7 +189,8 @@ module.exports = env => {
|
||||
new BuildManifest({
|
||||
browser: env.browser,
|
||||
pretty: env.mode === "production",
|
||||
stream: env.stream
|
||||
stream: env.stream,
|
||||
autoupdate: env.autoupdate,
|
||||
}),
|
||||
new configDiffPlugin()
|
||||
],
|
||||
|
||||
@@ -23,8 +23,11 @@ const schema = {
|
||||
pretty: {
|
||||
type: 'boolean'
|
||||
},
|
||||
steam: {
|
||||
stream: {
|
||||
type: 'string'
|
||||
},
|
||||
autoupdate: {
|
||||
type: 'boolean',
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -39,6 +42,7 @@ class BuildManifest {
|
||||
apply() {
|
||||
const distFolder = path.resolve(__dirname, "../dist/");
|
||||
const distManifestFile = path.resolve(distFolder, "manifest.json");
|
||||
const [owner, repo_name] = (process.env.GITHUB_REPOSITORY ?? "ajayyy/SponsorBlock").split("/");
|
||||
|
||||
// Add missing manifest elements
|
||||
if (this.options.browser.toLowerCase() === "firefox") {
|
||||
@@ -62,6 +66,10 @@ class BuildManifest {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.autoupdate === true && this.options.browser.toLowerCase() === "firefox") {
|
||||
manifest.browser_specific_settings.gecko.update_url = `https://${owner.toLowerCase()}.github.io/${repo_name}/updates.json`;
|
||||
}
|
||||
|
||||
let result = JSON.stringify(manifest);
|
||||
if (this.options.pretty) result = JSON.stringify(manifest, null, 2);
|
||||
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { SourceMapDevToolPlugin } = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = env => {
|
||||
async function createGHPSourceMapURL(env) {
|
||||
const manifest = require("../manifest/manifest.json");
|
||||
const version = manifest.version;
|
||||
const [owner, repo_name] = (process.env.GITHUB_REPOSITORY ?? "ajayyy/SponsorBlock").split("/");
|
||||
const ghpUrl = `https://${owner.toLowerCase()}.github.io/${repo_name}/${env.browser}${env.stream === "beta" ? "-beta" : ""}/${version}/`;
|
||||
// make a request to the url and check if we got redirected
|
||||
// firefox doesn't seem to like getting redirected on a source map request
|
||||
try {
|
||||
const resp = await fetch(ghpUrl);
|
||||
return resp.url;
|
||||
} catch {
|
||||
return ghpUrl;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async env => {
|
||||
let mode = "production";
|
||||
env.mode = mode;
|
||||
|
||||
return merge(common(env), {
|
||||
mode
|
||||
mode,
|
||||
...(env.ghpSourceMaps
|
||||
? {
|
||||
devtool: false,
|
||||
plugins: [new SourceMapDevToolPlugin({
|
||||
publicPath: await createGHPSourceMapURL(env),
|
||||
filename: '[file].map[query]',
|
||||
})],
|
||||
}
|
||||
: {
|
||||
devtool: "source-map",
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user