Compare commits

...

49 Commits

Author SHA1 Message Date
github-actions[bot]
e763d155ed Update Invidious List 2025-11-01 00:31:22 +00:00
Ajay
44941acb38 bump version 2025-10-29 02:24:54 -04:00
Ajay
8dbd566b13 update translations 2025-10-29 02:24:43 -04:00
Ajay
145463ef2e Remove annoying gif from help page 2025-10-28 15:53:18 -04:00
Ajay
8882b1d912 Fix preview bar scaling on new youtube layout 2025-10-28 15:50:51 -04:00
Ajay
75288da07a Show inspect button when only when time selected 2025-10-28 15:22:35 -04:00
Ajay
eed5de9d41 Don't auto skip on music videos if non music category disabled 2025-10-27 23:40:38 -04:00
Ajay
dff3bdc2fa Disable selenium test again 2025-10-23 03:39:48 -04:00
Ajay
06ec260860 Upgrade chromedriver 2025-10-23 03:34:34 -04:00
Ajay
0a7fa754ff Try selenium test again 2025-10-23 03:29:44 -04:00
Ajay
9f0a13830b Fix import error 2025-10-23 03:26:21 -04:00
Ajay
4646f471bc Fix background script broken on chromium 2025-10-23 03:25:01 -04:00
Ajay
60c4a0507c Make advanced skip options live update 2025-10-20 03:32:54 -04:00
Ajay Ramachandran
bd0be90135 Merge pull request #2342 from mschae23/rules-parser
Rewrite advanced skip options parser to add "or" operator
2025-10-20 03:21:44 -04:00
Ajay
590281f8eb Adjust skip profile name box size 2025-10-19 03:16:36 -04:00
Ajay
df21d085e5 Fix current time being wrong when at end of video on new YouTube layout
Fixes #2343
2025-10-19 03:00:12 -04:00
Ajay
7a605b8a2c bump version 2025-10-14 16:32:44 -04:00
Ajay
b59241cb08 Fix skip profile menu appearing behind your work box
Fixes #2359
2025-10-14 15:10:33 -04:00
Ajay
03dc749440 Move buttons back to the left on new youtube layout 2025-10-09 04:00:48 -04:00
Ajay
685bda29a6 bump version 2025-10-09 03:20:41 -04:00
Ajay
da1d1ffa7f Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2025-10-09 03:10:39 -04:00
Ajay
fae3a355f5 Add pride theme 2025-10-09 03:10:24 -04:00
mschae23
f80ddc8841 Merge branch 'master' into rules-parser 2025-10-07 10:00:44 +02:00
Ajay Ramachandran
1609995aa5 Merge pull request #2355 from FelixFourcolor/segment-list-default-tab
Add option to configure segment list default tab
2025-10-06 13:15:49 -04:00
Ajay
d0978bff79 update translations 2025-10-06 13:15:37 -04:00
felistachio
0257791264 Add segment list default tab config 2025-10-05 21:02:02 -07:00
Ajay
e1b18944bb Colour time inputs as well 2025-10-03 02:29:11 -04:00
Ajay
068307f98a Colour segment selector based on category colour 2025-10-03 02:27:21 -04:00
Ajay
9deb69b71b Fix layout on donation button 2025-10-03 00:55:11 -04:00
mschae23
479c31d57a Add not operator support in configToText 2025-10-02 17:00:44 +02:00
mschae23
f36d63e9bb Merge branch 'master' into not-operator 2025-10-02 16:43:46 +02:00
mschae23
510029af40 Implement not operator 2025-10-02 16:25:41 +02:00
Ajay
26f9a85b5b Hide settings not affected by skip profile when non-default profile is selected 2025-10-02 03:41:42 -04:00
Ajay
bc516c21fb Delete temp config if config deleted 2025-10-02 03:34:34 -04:00
mschae23
7749a12a0a Merge branch 'master' into rules-parser 2025-09-27 21:35:55 +02:00
mschae23
0eb222ae05 Minor improvements 2025-09-27 21:30:50 +02:00
mschae23
1ae101405b Convert nested functions to classes 2025-09-24 22:00:21 +02:00
mschae23
f6109ace99 Remove dead code
and revert skip action being changed to sentence case 😭
2025-09-24 21:32:49 +02:00
mschae23
75caa40fe5 Add migration from old storage format 2025-09-24 20:12:10 +02:00
mschae23
fa8db7c3b3 Fix some attributes being shadowed 2025-09-24 14:13:42 +02:00
mschae23
d165e06d2b Fix various tokenization issues
- Keywords and operators were previously matching too eagerly. For example, `ifcategory` would be matched as two tokens `if` `category` and result in a valid file. This is now a single error token.
- Strings previously allowed line breaks in them. This has been fixed, strings only consume up to the end of the line now.
- the error message for error tokens has been improved by using JSON escape.
2025-09-24 13:46:21 +02:00
mschae23
7c61c8b44e Use error log level, prefix messages 2025-09-24 00:35:52 +02:00
mschae23
ebc323a83b Fix some operators being shadowed, improve errors 2025-09-24 00:24:36 +02:00
mschae23
f64a552bf7 Add missing documentation comments 2025-09-23 22:10:19 +02:00
mschae23
8d0d71b0f3 Always use parentheses in unclear nesting 2025-09-23 20:38:54 +02:00
mschae23
2a2d9de817 Implement parser and configToText, remove old parser 2025-09-23 20:05:02 +02:00
mschae23
2004f6bf1b Implement lexing strings and numbers 2025-09-23 17:59:59 +02:00
mschae23
2370adb8b2 Tokenize keywords 2025-09-23 12:45:29 +02:00
mschae23
3d84152fa4 Begin implementing lexer 2025-09-22 23:22:25 +02:00
34 changed files with 1672 additions and 460 deletions

View File

@@ -1 +1 @@
["www.youtubekids.com","inv.nadeko.net","inv.tux.pizza","invidious.adminforge.de","invidious.jing.rocks","invidious.nerdvpn.de","invidious.perennialte.ch","invidious.privacyredirect.com","invidious.reallyaweso.me","invidious.yourdevice.ch","iv.ggtyler.dev","iv.nboeck.de","yewtu.be"]
["www.youtubekids.com","inv.nadeko.net","invidious.f5.si","invidious.nerdvpn.de","yewtu.be"]

View File

@@ -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",

View File

@@ -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",

View File

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

438
package-lock.json generated
View File

@@ -41,7 +41,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",
@@ -3072,31 +3072,17 @@
}
},
"node_modules/axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-jest": {
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz",
@@ -3535,6 +3521,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
@@ -3734,15 +3734,15 @@
}
},
"node_modules/chromedriver": {
"version": "137.0.4",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-137.0.4.tgz",
"integrity": "sha512-IxipIe0AnoQhWvG6eSTOLhVf1Tt586LA1IH4RdhGhXrrK6nZx8+VcKdjtlyZIV60S0CttUij/YnYuHAI+7qzWg==",
"version": "140.0.4",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-140.0.4.tgz",
"integrity": "sha512-/NUoxYBNkJeoNj1B5ux3KxGShITlxJctkbApgVAa3ZC8EvCLKaBclwU3/IEj5MJHnBJzqOVDxs/eTyaF9k2fOg==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@testim/chrome-version": "^1.1.4",
"axios": "^1.7.4",
"axios": "^1.12.0",
"compare-versions": "^6.1.0",
"extract-zip": "^2.0.1",
"proxy-agent": "^6.4.0",
@@ -4577,6 +4577,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.15",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.15.tgz",
@@ -4701,21 +4716,56 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
},
"node_modules/es-set-tostringtag": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
"integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3",
"has": "^1.0.3",
"has-tostringtag": "^1.0.0"
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -5655,9 +5705,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"dev": true,
"funding": [
{
@@ -5665,6 +5715,7 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -5820,6 +5871,23 @@
"node": ">=8"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -5860,9 +5928,14 @@
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/function.prototype.name": {
"version": "1.1.5",
@@ -5969,14 +6042,25 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -5991,6 +6075,20 @@
"node": ">=8.0.0"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -6173,12 +6271,13 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.1.3"
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -6267,10 +6366,11 @@
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -6279,12 +6379,13 @@
}
},
"node_modules/has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.2"
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -6293,6 +6394,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@@ -8880,20 +8994,6 @@
}
}
},
"node_modules/jsdom/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -9172,6 +9272,16 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"dev": true
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
@@ -14957,27 +15067,14 @@
"dev": true
},
"axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"dev": true,
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"babel-jest": {
@@ -15284,6 +15381,16 @@
"get-intrinsic": "^1.0.2"
}
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"callsites": {
"version": "3.1.0",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
@@ -15417,13 +15524,13 @@
}
},
"chromedriver": {
"version": "137.0.4",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-137.0.4.tgz",
"integrity": "sha512-IxipIe0AnoQhWvG6eSTOLhVf1Tt586LA1IH4RdhGhXrrK6nZx8+VcKdjtlyZIV60S0CttUij/YnYuHAI+7qzWg==",
"version": "140.0.4",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-140.0.4.tgz",
"integrity": "sha512-/NUoxYBNkJeoNj1B5ux3KxGShITlxJctkbApgVAa3ZC8EvCLKaBclwU3/IEj5MJHnBJzqOVDxs/eTyaF9k2fOg==",
"dev": true,
"requires": {
"@testim/chrome-version": "^1.1.4",
"axios": "^1.7.4",
"axios": "^1.12.0",
"compare-versions": "^6.1.0",
"extract-zip": "^2.0.1",
"proxy-agent": "^6.4.0",
@@ -16033,6 +16140,17 @@
}
}
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"electron-to-chromium": {
"version": "1.5.15",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.15.tgz",
@@ -16130,21 +16248,43 @@
"which-typed-array": "^1.1.9"
}
},
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true
},
"es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
},
"es-set-tostringtag": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
"integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"requires": {
"get-intrinsic": "^1.1.3",
"has": "^1.0.3",
"has-tostringtag": "^1.0.0"
"es-errors": "^1.3.0"
}
},
"es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"requires": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
}
},
"es-shim-unscopables": {
@@ -16813,9 +16953,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"dev": true
},
"for-each": {
@@ -16918,6 +17058,19 @@
}
}
},
"form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
}
},
"fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -16948,8 +17101,9 @@
"optional": true
},
"function-bind": {
"version": "1.1.1",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true
},
"function.prototype.name": {
@@ -17029,14 +17183,21 @@
"dev": true
},
"get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.3"
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-package-type": {
@@ -17045,6 +17206,16 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -17168,13 +17339,10 @@
}
},
"gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"requires": {
"get-intrinsic": "^1.1.3"
}
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true
},
"graceful-fs": {
"version": "4.2.11",
@@ -17241,18 +17409,27 @@
"dev": true
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true
},
"has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"requires": {
"has-symbols": "^1.0.2"
"has-symbols": "^1.0.3"
}
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"requires": {
"function-bind": "^1.1.2"
}
},
"html-encoding-sniffer": {
@@ -19126,19 +19303,6 @@
"whatwg-url": "^11.0.0",
"ws": "^8.9.0",
"xml-name-validator": "^4.0.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"jsesc": {
@@ -19357,6 +19521,12 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"dev": true
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true
},
"memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",

View File

@@ -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",

View File

@@ -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);
@@ -973,4 +973,26 @@ 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -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;

View File

@@ -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">

View File

@@ -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 {

View File

@@ -28,6 +28,11 @@
min-width: 100%;
}
.sponsorSkipNoticeTableContainer.prideTheme {
background: url("icons/pride.svg");
background-size: cover;
}
.sponsorSkipNotice {
transition: all 0.1s ease-out;
}

View File

@@ -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()) {

View File

@@ -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>
{

View File

@@ -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}

View File

@@ -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)}>

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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: {
@@ -155,7 +157,7 @@ interface SBStorage {
/* VideoID prefixes to UUID prefixes */
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>;
navigationApiAvailable: boolean;
// Used when sync storage disabled
alreadyInstalled: boolean;
@@ -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[];
@@ -212,7 +251,7 @@ function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
for (const channelID of whitelistedChannels) {
local.channelSkipProfileIDs[channelID] = skipProfileID;
}
local.channelSkipProfileIDs = local.channelSkipProfileIDs;
local.channelSkipProfileIDs = local.channelSkipProfileIDs;
chrome.storage.sync.remove("whitelistedChannels");
}
@@ -246,7 +285,7 @@ function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
name: "chapter" as Category,
option: CategorySkipOption.ShowOverlay
});
config.categorySelections = config.categorySelections;
}
}
@@ -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: {},

View File

@@ -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 {

View File

@@ -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 = {};

View File

@@ -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>

View File

@@ -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";
@@ -61,12 +61,21 @@ export const SegmentListComponent = (props: SegmentListComponentProps) => {
}, [props.segments]);
React.useEffect(() => {
if (hasSegments){
setTab(SegmentListTab.Segments);
const setTabBasedOnConfig = () => {
const preferChapters = Config.config.segmentListDefaultTab === SegmentListDefaultTab.Chapters;
if (preferChapters) {
setTab(hasChapters ? SegmentListTab.Chapter : SegmentListTab.Segments);
} else {
setTab(hasSegments ? SegmentListTab.Segments : SegmentListTab.Chapter);
}
};
if (Config.isReady()) {
setTabBasedOnConfig();
} else {
setTab(SegmentListTab.Chapter);
waitFor(() => Config.isReady()).then(setTabBasedOnConfig);
}
}, [props.videoID, hasSegments]);
}, [props.videoID, hasSegments, hasChapters]);
const segmentsWithNesting = React.useMemo(() => {
const result: SegmentWithNesting[] = [];

View File

@@ -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;

View File

@@ -227,4 +227,9 @@ export enum NoticeVisibilityMode {
MiniForAll = 2,
FadedForAutoSkip = 3,
FadedForAll = 4
}
export enum SegmentListDefaultTab {
Segments,
Chapters,
}

View File

@@ -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

View File

@@ -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,9 +117,9 @@ 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:
return typeof value === "number" && value < (rule.value as number);
@@ -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) {
@@ -183,4 +170,762 @@ 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,
};
}
}

View 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[];
}