Compare commits

..

116 Commits
4.0.2 ... 4.1

Author SHA1 Message Date
Ajay
84d4c9da4c Fix version 2022-02-06 23:37:38 -05:00
Ajay
e623af859f Fix release ci 2022-02-06 23:34:32 -05:00
Ajay
8c60f25d4a Don't save downvotes in incognito 2022-02-06 23:20:01 -05:00
Ajay
7896b474db bump version 2022-02-06 23:17:49 -05:00
Ajay
4d60dec7f9 Save downvotes and segment hides in local storage 2022-02-06 23:17:34 -05:00
Ajay
53d0ac8677 Add animation to segment hiding 2022-02-06 21:08:15 -05:00
Ajay
e4ba67999c Add segment hiding in popup 2022-02-06 21:03:39 -05:00
Ajay
0b6ade4a1d Improve typing on getHash 2022-02-06 20:01:19 -05:00
Ajay
816e9a78be Add local storage options to config 2022-02-06 19:17:20 -05:00
Ajay
ccb0e8bed9 Fix parameters not optional 2022-02-06 18:55:27 -05:00
Ajay
459de6654a Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-02-06 18:48:05 -05:00
Ajay
8b6f0d8a2e Better title text for full video labels 2022-02-06 18:48:03 -05:00
Ajay
cf792e849f remove log 2022-02-06 18:19:42 -05:00
Ajay
fce82b48b0 Add popup donation prompt 2022-02-06 18:12:58 -05:00
Ajay Ramachandran
3d2fe8c409 Merge pull request #1176 from ajayyy/ci/update_invidious_list
Update Invidious List
2022-02-06 17:23:09 -05:00
github-actions[bot]
f513c3b8cb Update Invidious List 2022-02-06 22:21:30 +00:00
Ajay
c66e624c16 Delete unused unsubmitted objects 2022-02-06 17:01:14 -05:00
Ajay
ddca15ca01 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-02-06 15:10:48 -05:00
Ajay
199ccb1b12 Move segment times from encoded array to normal js object 2022-02-06 15:10:46 -05:00
Ajay Ramachandran
552b33104b Remove instructions for attribute generation, now handled by github action 2022-02-06 14:37:08 -05:00
Ajay Ramachandran
57b952f64c Merge pull request #1175 from ajayyy/ci/oss_attribution
Update OSS Attribution
2022-02-06 14:35:55 -05:00
github-actions[bot]
6867fa88d3 Update OSS Attribution 2022-02-06 19:17:39 +00:00
Ajay Ramachandran
93af8d966b Merge pull request #1174 from mchangrh/fix-oss-ci
Fix OSS CI
2022-02-06 14:16:09 -05:00
Ajay
32fa9c3398 Hide segments when voting from the popup 2022-02-06 14:04:54 -05:00
Ajay Ramachandran
4a8e769596 Merge pull request #1155 from mchangrh/misc-fix
Misc fixes
2022-02-06 13:45:28 -05:00
Ajay
9c836a89a3 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into misc-fix 2022-02-06 13:42:58 -05:00
Ajay Ramachandran
b21b6edb0c semicolon 2022-02-06 13:39:37 -05:00
Michael M. Chang
ca49b48814 workflow_dispatch 2022-02-06 13:35:48 -05:00
Michael M. Chang
2b9a93deb0 npm ci to check node_modules 2022-02-06 13:33:02 -05:00
Ajay Ramachandran
d9dede6578 Merge pull request #1167 from mchangrh/update-dependencies
update dependencies
2022-02-06 13:27:33 -05:00
Ajay Ramachandran
2a8a9ba667 New Crowdin updates (#1148) 2022-02-06 13:27:08 -05:00
Michael C
1bd7c6a33a update depndencies
- add engines targeting dev
- bump webpack
- update-oss-attribution CI
- appease eslint for webpack
- clean up packages
  - move @types to devDependencies
  - moved concurrently to devDependencies
  - remove unused babel
2022-02-06 13:24:47 -05:00
Ajay Ramachandran
f5301fc374 Merge pull request #1168 from mchangrh/youtubeClip
early return if url is youtube clips url
2022-02-06 13:22:52 -05:00
Ajay Ramachandran
159e7598d7 Merge pull request #1164 from ajayyy/dependabot/npm_and_yarn/tmpl-1.0.5
Bump tmpl from 1.0.4 to 1.0.5
2022-02-06 13:22:28 -05:00
Ajay Ramachandran
2bd1688d8d Merge pull request #1165 from ajayyy/dependabot/npm_and_yarn/nth-check-2.0.1
Bump nth-check from 2.0.0 to 2.0.1
2022-02-06 13:22:23 -05:00
Ajay Ramachandran
26143460b7 Merge pull request #1163 from ajayyy/dependabot/npm_and_yarn/nanoid-3.2.0
Bump nanoid from 3.1.23 to 3.2.0
2022-02-06 13:22:16 -05:00
Ajay
2575141d35 Don't load segments from url multiple times 2022-02-06 13:13:08 -05:00
Michael C
48124e56d6 early return if url is youtube clips url 2022-02-06 01:05:19 -05:00
Ajay
e8307a2af7 Add text color based off luminance for full video label 2022-02-05 22:23:11 -05:00
dependabot[bot]
d01ed86803 Bump nth-check from 2.0.0 to 2.0.1
Bumps [nth-check](https://github.com/fb55/nth-check) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/fb55/nth-check/releases)
- [Commits](https://github.com/fb55/nth-check/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: nth-check
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-04 15:38:35 +00:00
dependabot[bot]
628c978286 Bump tmpl from 1.0.4 to 1.0.5
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-04 15:38:34 +00:00
dependabot[bot]
a0ba496d83 Bump nanoid from 3.1.23 to 3.2.0
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.23 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.23...3.2.0)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-04 15:38:34 +00:00
Ajay Ramachandran
0ac4ef7a4b Merge pull request #1162 from mchangrh/invidious-ci-fix
fix updateInvidious ci
2022-02-04 10:37:54 -05:00
Michael C
ba2efe59cb fix updateInvidious ci
- add ts-node
- fix path
- add npm ci
2022-02-04 02:38:14 -05:00
Ajay
88f61cf292 Fix width with left bar options and make it appear more often 2022-01-29 23:33:30 -05:00
Michael C
86e5ef7523 merge only one 2022-01-27 15:02:07 -05:00
Michael C
0451dcd975 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into misc-fix 2022-01-27 14:56:47 -05:00
Michael C
ba7d46dd73 add mediaSession, remove bindMediaKey 2022-01-27 14:52:13 -05:00
Ajay
1115ffd8cf Fix ugly long category pill titles
FIx #1152
2022-01-27 11:57:14 -05:00
Ajay
fa926ef686 Don't hide exclusive access for min duration
Fix #1156
2022-01-27 11:36:26 -05:00
Michael C
22fcea99f5 revert package-lock 2022-01-27 01:52:37 -05:00
Michael C
876b55dcc1 add bindMediaKey to options 2022-01-27 01:43:40 -05:00
Michael C
6d0d533fa7 force listen for mediaKeys 2022-01-27 00:53:30 -05:00
Michael C
9457aff44f linting + audio override 2022-01-26 17:54:58 -05:00
Michael C
193374db30 substr -> slice 2022-01-26 17:54:35 -05:00
Ajay Ramachandran
2a7f08f360 Merge pull request #1154 from AronHK/localization
Add missing localizations
2022-01-26 16:47:08 -05:00
Áron Hegymegi-Kiss
b02e4c1b4f missing localization 2022-01-26 22:32:15 +01:00
Ajay Ramachandran
8922c6cb67 Fix typo 2022-01-25 19:51:06 -05:00
Ajay Ramachandran
f555a8e7bb Merge pull request #1093 from AronHK/settings
Settings rework
2022-01-24 23:41:21 -05:00
Ajay
aac2572b4e Add live update for dark mode 2022-01-24 23:38:44 -05:00
Ajay
56e7dc842e Fix tab button contrast on light theme 2022-01-24 23:36:35 -05:00
Ajay
8452bfb32a lightmode -> dark mode 2022-01-24 23:33:27 -05:00
Ajay
c49ebbe371 Fix slow animation transitioning to light mode 2022-01-24 23:31:12 -05:00
Ajay
e87ec3842b fix merge issue 2022-01-24 23:28:51 -05:00
Ajay Ramachandran
595cb84941 Merge branch 'master' into settings 2022-01-24 23:27:52 -05:00
Ajay
8d5f244d2f Dark mode unless explicitely disabled (sorry) 2022-01-24 23:27:00 -05:00
Ajay
8455ac8b39 Make server address buttons bigger 2022-01-24 23:20:08 -05:00
Ajay
666aaab7f2 Use color for hover 2022-01-24 23:16:38 -05:00
Ajay
963ead0a30 Fix weird scrolling on help page 2022-01-24 22:59:43 -05:00
Ajay
28a711de75 use addeventlistener 2022-01-24 22:58:52 -05:00
Ajay
1fca10a1c1 Max width on options page 2022-01-24 22:58:03 -05:00
Ajay
96cefeaaf3 Remove stick header on help page 2022-01-24 20:53:36 -05:00
Ajay
23a6940894 Clarify that skip notice time is in seconds 2022-01-21 21:54:33 -05:00
Ajay
7f2b1c60c7 Only display single action type for full video 2022-01-21 20:20:17 -05:00
Ajay
c542cd9f73 Save times when accidentally switching to exclusive access 2022-01-21 19:37:14 -05:00
Ajay
7e091d1c67 Remove sponsor lookup retries 2022-01-21 18:39:07 -05:00
Ajay
6a07a9f693 Use new Poi action type fetch method 2022-01-21 18:38:04 -05:00
Ajay Ramachandran
c34d1e0bc1 Merge branch 'master' into settings 2022-01-21 12:28:47 -05:00
Ajay Ramachandran
b4c8e54d3e New Crowdin updates (#1137) 2022-01-21 12:27:41 -05:00
Ajay Ramachandran
b83426caba bump version 2022-01-21 12:26:04 -05:00
Ajay
9da99688d3 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-01-20 16:03:43 -05:00
Ajay
9bcee6afce Remove 502 retry logic 2022-01-20 16:03:41 -05:00
Ajay Ramachandran
1b5d7ccf3e Merge pull request #1141 from mchangrh/videoPreviewButtons
fix hover preview segments
2022-01-19 15:26:42 -05:00
Ajay
41a8b2718a Remove deleted import 2022-01-19 11:30:04 -05:00
Ajay
002f22c040 Wait for hover preview using mutation observer 2022-01-19 11:27:19 -05:00
Ajay
bf735f47b0 Fix reference node order to fix issue with player-container always being visible 2022-01-19 10:57:28 -05:00
Michael C
e4aa7efd3c add infinite wait for hover preview 2022-01-19 01:47:43 -05:00
Michael C
b210b22af3 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into videoPreviewButtons 2022-01-18 14:30:29 -05:00
Ajay Ramachandran
154e12f201 bump version 2022-01-18 13:20:46 -05:00
Ajay
b6c1ee1743 Don't allow submissions on active premieres or livestreams
Fix #523
Fix #367
2022-01-16 13:56:57 -05:00
Ajay
6b6ca6198f Don't schedule skips for full video segments 2022-01-16 01:17:49 -05:00
Ajay
6788394be1 Fix typo 2022-01-16 00:12:27 -05:00
Michael C
059a674ae1 fix hover preview segments 2022-01-15 22:49:22 -05:00
Ajay
a45bd4c5c7 Fix potential null issue 2022-01-15 18:28:53 -05:00
Ajay
bb47863080 Add safari extra manifest and use chrome's manifest for edge 2022-01-14 21:51:42 -05:00
Ajay
d50a69f1fd New hash format for previewing segments 2022-01-14 19:34:54 -05:00
Ajay
31014b78ac Add method to create segments from url parameters 2022-01-14 19:21:09 -05:00
Ajay
cf9e016581 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-01-14 17:35:54 -05:00
Ajay
dee836c783 Fix making dir automatically 2022-01-14 17:35:52 -05:00
Ajay Ramachandran
5c7f8cef45 bump version 2022-01-14 17:08:24 -05:00
Ajay
9f5ec09789 Add safari and edge build to release 2022-01-14 17:07:10 -05:00
Ajay
a82062da5b Add build mode for edge languages 2022-01-14 16:07:24 -05:00
Ajay
35da572f3f Add build option for safari that fixes locales
Fix #801 and #1061
2022-01-14 15:56:38 -05:00
Ajay
cf01679f16 null check for skip notice 2022-01-14 14:57:46 -05:00
Ajay
16f4ab53b6 Don't warn about minimum duration for full video
Fix #1133
2022-01-14 11:51:51 -05:00
Áron Hegymegi-Kiss
881cea4f6d refine settings usragent check 2022-01-13 19:13:08 +01:00
Áron Hegymegi-Kiss
5dd78e1cb3 show icon 2022-01-12 21:22:43 +01:00
Áron Hegymegi-Kiss
83db64d084 address isssues for pull request 2022-01-09 21:28:30 +01:00
Áron Hegymegi-Kiss
ed17d4859f Merge remote-tracking branch 'origin/master' into settings
# Conflicts:
#	public/options/options.html
#	src/config.ts
#	src/content.ts
#	src/options.ts
2022-01-08 19:30:49 +01:00
Áron Hegymegi-Kiss
dc560cd73e import/export settings to file 2022-01-08 19:03:46 +01:00
Áron Hegymegi-Kiss
90eaab4e50 multikey shortcuts 2022-01-08 17:07:37 +01:00
Áron Hegymegi-Kiss
3b54fde3f9 fixes 2022-01-08 16:34:49 +01:00
Áron Hegymegi-Kiss
46e515b130 design fixes 2021-12-15 22:22:06 +01:00
Áron Hegymegi-Kiss
9a9e564dbf show version + clickable labels 2021-12-13 12:14:51 +01:00
Áron Hegymegi-Kiss
4971824067 missing labels, hide options when sth they depend on gets turned off 2021-12-13 02:37:15 +01:00
Áron Hegymegi-Kiss
dface28c84 group settings into tabs 2021-12-12 23:31:09 +01:00
82 changed files with 10937 additions and 29193 deletions

View File

@@ -47,7 +47,7 @@ jobs:
# Create Beta artifacts (Builds with the name changed to beta)
- name: Create Chrome Beta artifacts
run: npm run build:chrome -- --env.stream=beta
run: npm run build:chrome -- --env stream=beta
- uses: actions/upload-artifact@v2
with:
name: ChromeExtensionBeta
@@ -57,7 +57,7 @@ jobs:
args: zip -qq -r ./builds/ChromeExtensionBeta.zip ./dist
- name: Create Firefox Beta artifacts
run: npm run build:firefox -- --env.stream=beta
run: npm run build:firefox -- --env stream=beta
- uses: actions/upload-artifact@v2
with:
name: FirefoxExtensionBeta

View File

@@ -43,7 +43,7 @@ jobs:
# Create Beta artifacts (Builds with the name changed to beta)
- name: Create Chrome Beta artifacts
run: npm run build:chrome -- --env.stream=beta
run: npm run build:chrome -- --env stream=beta
- uses: actions/upload-artifact@v2
with:
name: ChromeExtensionBeta
@@ -51,6 +51,28 @@ jobs:
- name: Zip Artifacts
run: cd ./dist ; zip -r ../builds/ChromeExtensionBeta.zip *
# Create Safari artifacts
- name: Create Safari artifacts
run: npm run build:safari
- uses: actions/upload-artifact@v2
with:
name: SafariExtension
path: dist
- name: Zip Artifacts
run: cd ./dist ; zip -r ../builds/SafariExtension.zip *
# Create Edge artifacts
- name: Clear dist for Edge
run: rm -rf ./dist
- name: Create Edge artifacts
run: npm run build:edge
- uses: actions/upload-artifact@v2
with:
name: EdgeExtension
path: dist
- name: Zip Artifacts
run: cd ./dist ; zip -r ../builds/EdgeExtension.zip *
# Upload each release asset
- name: Upload ChromeExtension to release
uses: Shopify/upload-to-release@master
@@ -73,10 +95,24 @@ jobs:
name: FirefoxExtension.zip
path: ./builds/FirefoxExtension.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SafariExtension to release
uses: Shopify/upload-to-release@master
with:
args: builds/SafariExtension.zip
name: SafariExtension.zip
path: ./builds/SafariExtension.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload EdgeExtension to release
uses: Shopify/upload-to-release@master
with:
args: builds/EdgeExtension.zip
name: EdgeExtension.zip
path: ./builds/EdgeExtension.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Firefox Beta
- name: Create Firefox Beta artifacts
run: npm run build:firefox -- --env.stream=beta
run: npm run build:firefox -- --env stream=beta
- uses: actions/upload-artifact@v2
with:
name: FirefoxExtensionBeta

View File

@@ -0,0 +1,34 @@
name: update oss attributions
on:
push:
branches:
- master
paths:
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
jobs:
update-oss:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install and generate attribution
run: |
npm ci
npm i -g oss-attribution-generator
generate-attribution
mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt
- name: Create pull request to update list
uses: peter-evans/create-pull-request@v3
with:
commit-message: Update OSS Attribution
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
branch: ci/oss_attribution
title: Update OSS Attribution
body: Automated OSS Attribution update

View File

@@ -12,7 +12,9 @@ jobs:
uses: actions/checkout@v2
- name: Download instance list
run: |
wget https://api.invidious.io/instances.json -O data.json
wget https://api.invidious.io/instances.json -O ci/data.json
- name: Install dependencies
run: npm ci
- name: "Run CI"
run: npm run ci:invidious

View File

@@ -82,17 +82,6 @@ Known chromium bug: Extension is not loaded properly on first start. Visit `chro
For Firefox for Android, use `npm run dev:firefox-android -- --adb-device <ip-address of the device>`. See the [Firefox documentation](https://extensionworkshop.com/documentation/develop/developing-extensions-for-firefox-for-android/#debug-your-extension) for more information.
### Attribution Generation
If you contribute and add a dependency, update the attribution file using the following steps:
Make sure the attribution generator is installed: `npm i -g oss-attribution-generator`
```bash
generate-attribution
mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt
```
# Credit
The awesome [Invidious API](https://docs.invidious.io/API.md) was previously used, and the server is now using [NewLeaf](https://git.sr.ht/~cadence/NewLeaf) as a to get video info from YouTube.

View File

@@ -8,12 +8,12 @@ import { writeFile, existsSync } from 'fs';
import { join } from 'path';
// import file from https://api.invidious.io/instances.json
if (!existsSync('./data.json')) {
if (!existsSync(join(__dirname, "data.json"))) {
process.exit(1);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as data from "./data.json";
import * as data from "../ci/data.json";
type instanceMap = {
name: string,

View File

@@ -1 +1 @@
["yewtu.be","invidious.snopyta.org","vid.puffyan.us","invidious.kavin.rocks","invidio.xamh.de","invidious-us.kavin.rocks","inv.riverside.rocks","vid.mint.lgbt","youtube.076.ne.jp","invidious.namazso.eu"]
["yewtu.be","invidious.snopyta.org","vid.puffyan.us","invidious.kavin.rocks","invidio.xamh.de","inv.riverside.rocks","invidious.osi.kr","inv.cthd.icu","yt.artemislena.eu","youtube.076.ne.jp","invidious.namazso.eu"]

View File

@@ -13,7 +13,7 @@
"preview": ["skip", "mute"],
"filler": ["skip", "mute"],
"music_offtopic": ["skip"],
"poi_highlight": ["skip"]
"poi_highlight": ["poi"]
},
"wikiLinks": {
"sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor",

View File

@@ -1,7 +1,7 @@
{
"name": "__MSG_fullName__",
"short_name": "SponsorBlock",
"version": "4.0.2",
"version": "4.1",
"default_locale": "en",
"description": "__MSG_Description__",
"homepage_url": "https://sponsor.ajay.app",
@@ -47,6 +47,9 @@
"icons/beep.ogg",
"icons/pause.svg",
"icons/stop.svg",
"icons/heart.svg",
"icons/visible.svg",
"icons/not_visible.svg",
"icons/PlayerInfoIconSponsorBlocker.svg",
"icons/PlayerDeleteIconSponsorBlocker.svg",
"popup.html",

View File

@@ -0,0 +1,5 @@
{
"background": {
"persistent": false
}
}

File diff suppressed because one or more lines are too long

23881
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,38 +4,36 @@
"description": "",
"main": "background.js",
"dependencies": {
"@types/react": "^16.9.22",
"@types/react-dom": "^16.9.5",
"@types/selenium-webdriver": "^4.0.15",
"babel": "^6.23.0",
"babel-core": "^6.26.3",
"babel-loader": "^8.0.6",
"babel-preset-env": "^1.7.0",
"concurrently": "^5.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/chrome": "0.0.91",
"@types/firefox-webext-browser": "70.0.1",
"@types/jest": "^24.0.23",
"@types/jquery": "^3.3.31",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"chromedriver": "^92.0.0",
"copy-webpack-plugin": "^6.0.3",
"eslint": "^7.15.0",
"eslint-plugin-react": "^7.21.5",
"jest": "^27.0.6",
"rimraf": "^3.0.0",
"selenium-webdriver": "^4.0.0-beta.4",
"ts-jest": "^27.0.3",
"ts-loader": "^6.2.1",
"typescript": "~4.3",
"web-ext": "^6.2.0",
"webpack": "~4.41.2",
"webpack-cli": "~3.3.10",
"webpack-merge": "~4.2.2"
"@types/chrome": "^0.0.178",
"@types/firefox-webext-browser": "^94.0.1",
"@types/jest": "^27.4.0",
"@types/wicg-mediasession": "^1.1.3",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/selenium-webdriver": "^4.0.17",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"chromedriver": "^97.0.4",
"concurrently": "^7.0.0",
"copy-webpack-plugin": "^10.2.4",
"eslint": "^8.8.0",
"eslint-plugin-react": "^7.28.0",
"jest": "^27.5.0",
"rimraf": "^3.0.2",
"schema-utils": "^4.0.0",
"selenium-webdriver": "^4.1.1",
"ts-jest": "^27.1.3",
"ts-loader": "^9.2.6",
"ts-node": "^10.4.0",
"typescript": "4.5",
"web-ext": "^6.6.0",
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2",
"webpack-merge": "^4.2.2"
},
"scripts": {
"web-run": "npm run web-run:chrome",
@@ -44,14 +42,16 @@
"web-run:firefox-android": "cd dist && web-ext run -t firefox-android --firefox-apk org.mozilla.fenix",
"web-run:chrome": "cd dist && web-ext run --start-url https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm -t chromium",
"build": "npm run build:chrome",
"build:chrome": "webpack --env.browser=chrome --config webpack/webpack.prod.js",
"build:firefox": "webpack --env.browser=firefox --config webpack/webpack.prod.js",
"build:chrome": "webpack --env browser=chrome --config webpack/webpack.prod.js",
"build:firefox": "webpack --env browser=firefox --config webpack/webpack.prod.js",
"build:safari": "webpack --env browser=safari --config webpack/webpack.prod.js",
"build:edge": "webpack --env browser=edge --config webpack/webpack.prod.js",
"build:dev": "npm run build:dev:chrome",
"build:dev:chrome": "webpack --env.browser=chrome --config webpack/webpack.dev.js",
"build:dev:firefox": "webpack --env.browser=firefox --config webpack/webpack.dev.js",
"build:dev:chrome": "webpack --env browser=chrome --config webpack/webpack.dev.js",
"build:dev:firefox": "webpack --env browser=firefox --config webpack/webpack.dev.js",
"build:watch": "npm run build:watch:chrome",
"build:watch:chrome": "webpack --env.browser=chrome --config webpack/webpack.dev.js --watch",
"build:watch:firefox": "webpack --env.browser=firefox --config webpack/webpack.dev.js --watch",
"build:watch:chrome": "webpack --env browser=chrome --config webpack/webpack.dev.js --watch",
"build:watch:firefox": "webpack --env browser=firefox --config webpack/webpack.dev.js --watch",
"ci:invidious": "ts-node ci/invidiousCI.ts",
"dev": "npm run build:dev && concurrently \"npm run web-run\" \"npm run build:watch\"",
"dev:firefox": "npm run build:dev:firefox && concurrently \"npm run web-run:firefox\" \"npm run build:watch:firefox\"",
@@ -62,6 +62,22 @@
"lint": "eslint src",
"lint:fix": "eslint src --fix"
},
"engines": {
"node": ">=12.20.0"
},
"funding": [{
"type": "individual",
"url": "hhttps://sponsor.ajay.app/donate"
}, {
"type": "github",
"url": "https://github.com/sponsors/ajayyy-org"
}, {
"type": "patreon",
"url": "https://www.patreon.com/ajayyy"
}, {
"type": "individual",
"url": "https://paypal.me/ajayyy"
}],
"repository": {
"type": "git",
"url": "git+https://github.com/ajayyy/SponsorBlock.git"

View File

@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Задайте клавиш за пропускане на сегмент"
"message": "Пропускане на сегмент",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Задайте клавиш за начало/край на сегмент"
"message": "Начало/край на сегмент",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Задайте клавиш за изпращане"
"message": "Изпращане на сегментите",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Изберете клавиш, като го натиснете"
},
"keybindDescriptionComplete": {
"message": "Клавишът е зададен за: "
"message": "Изберете клавиш, като го натиснете, заедно с всички клавишни модификатори, които искате да използвате."
},
"0": {
"message": "Времето за изчакване на връзката изтече. Проверете връзката си с интернет. Ако вашият интернет работи, вероятно сървърът е претоварен или не работи."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Създаден от"
},
"keybindCurrentlySet": {
"message": ". В момента е настроено на:"
},
"supportOtherSites": {
"message": "Поддръжка на YouTube-сайтове на трети страни"
},
@@ -440,6 +437,9 @@
"shortCheck": {
"message": "Следното предложение е по-кратко от опцията за минимална продължителност. Това може да означава, че вече е изпратено и просто е игнорирано поради тази опция. Наистина ли искате да го изпратите?"
},
"liveOrPremiere": {
"message": "Не е разрешено изпращането по време на активен поток на живо или премиера. Моля, изчакайте, докато приключи, след което опреснете страницата и проверете дали сегментите са все още валидни."
},
"showUploadButton": {
"message": "Показване на бутона за качване"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "Импортиране/експортиране на всички опции"
},
"exportOptionsCopy": {
"message": "Редактиране/копиране"
},
"exportOptionsDownload": {
"message": "Записване във файл"
},
"exportOptionsUpload": {
"message": "Зареждане от файл"
},
"whatExportOptions": {
"message": "Това е цялата ви конфигурация в JSON. Това включва вашия userID, така че се уверете, че споделяте това разумно."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "Информацията за отстраняване на грешки е копирана в клипборда. Чувствайте се свободни да премахнете всяка информация, която предпочитате да не споделяте. Запазете това в текстов файл или го поставете в доклада за грешки."
},
"theKey": {
"message": "Клавишът"
},
"keyAlreadyUsed": {
"message": "е обвързан с друго действие. Моля, изберете друг клавиш."
"message": "Тази клавишна комбинация е свързана с друго действие. Моля, изберете друга."
},
"to": {
"message": "до",
@@ -784,6 +790,9 @@
"hideDonationLink": {
"message": "Скриване на връзката за дарение"
},
"darkModeOptionsPage": {
"message": "Тъмен режим на страницата с опции"
},
"helpPageThanksForInstalling": {
"message": "Благодарим ви, че инсталирахте SponsorBlock."
},
@@ -872,5 +881,42 @@
"hourAbbreviation": {
"message": "ч",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Поведение",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Интерфейс",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Клавишни комбинации",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "Архивиране/Възстановяване",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Други",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "Изглед на известията за пропускане",
"description": "Option label"
},
"unbind": {
"message": "Освобождаване",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Не е зададено"
},
"change": {
"message": "Промяна"
},
"youtubeKeybindWarning": {
"message": "Това е вградена клавишна комбинация в YouTube. Наистина ли искате да я използвате?"
}
}

View File

@@ -262,21 +262,6 @@
"message": "Pokud se vám to stále nelíbí, klikněte na tlačítko Nikdy nezobrazovat.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Nastavit klávesu pro přeskočení segmentu"
},
"setStartSponsorShortcut": {
"message": "Nastavte klávesu pro spuštění/zastavení segmentu"
},
"setSubmitKeybind": {
"message": "Nastavte klávesu pro odeslání"
},
"keybindDescription": {
"message": "Nastavte klávesu jejím zadáním"
},
"keybindDescriptionComplete": {
"message": "Klávesa byla nastavena na: "
},
"0": {
"message": "Vypršel časový limit připojení. Zkontrolujte vaše připojení k internetu. Pokud váš internet funguje, server je nejspíš přetížený nebo spadnul."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Vytvořil"
},
"keybindCurrentlySet": {
"message": ". Je momentálně nastaveno na:"
},
"supportOtherSites": {
"message": "Podpora pro weby YouTube třetích stran"
},
@@ -440,6 +422,9 @@
"shortCheck": {
"message": "Váš příspěvek je kratší než vaše možnost nejkratší doby trvání. To by mohlo znamenat, že někdo segment již odeslal, a je jenom ignorován kvůli této možnosti. Opravdu chcete odeslat váš příspěvek?"
},
"liveOrPremiere": {
"message": "Odesílání na aktivním streamu nebo premiéře. Počkejte prosím, než skončí, poté obnovte stránku a ověřte, zda jsou segmenty stále platné."
},
"showUploadButton": {
"message": "Zobrazit tlačítko Nahrát"
},
@@ -515,12 +500,6 @@
"copyDebugInformationComplete": {
"message": "Ladící informace byly zkopírovány do schránky. Můžete odstranit jakékoli informace, které raději nechcete sdílet. Uložte text do textového souboru nebo vložte do chybového hlášení."
},
"theKey": {
"message": "Klávesa"
},
"keyAlreadyUsed": {
"message": "je přiřazena k jiné akci. Zvolte prosím jinou klávesu."
},
"to": {
"message": "do",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -262,21 +262,6 @@
"message": "Hvis du stadig ikke kan lide det, så tryk på aldrig vis knappen.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Indstil tast for at springe et segment over"
},
"setStartSponsorShortcut": {
"message": "Indstil tast til start/stop segment tastaturbinding"
},
"setSubmitKeybind": {
"message": "Indstil tast til indsendelse tastaturbinding"
},
"keybindDescription": {
"message": "Vælg en tast ved at skrive den"
},
"keybindDescriptionComplete": {
"message": "Tastaturbindingen er blevet sat til: "
},
"0": {
"message": "Forbindelsestimeout. Tjek din internetforbindelse. Hvis dit internet fungerer, er serveren sandsynligvis overbelastet eller nede."
},
@@ -384,9 +369,6 @@
"createdBy": {
"message": "Oprettet Af"
},
"keybindCurrentlySet": {
"message": ". Det er i øjeblikket sat til:"
},
"supportOtherSites": {
"message": "Understøtter tredjeparts YouTube sider"
},
@@ -511,12 +493,6 @@
"copyDebugInformationComplete": {
"message": "Fejlfindingsinformationen er blevet kopieret til klippebordet. Du er velkommen til at fjerne alle oplysninger, du helst ikke vil dele. Gem dette i en tekstfil eller indsæt i fejlrapporten."
},
"theKey": {
"message": "Tasten"
},
"keyAlreadyUsed": {
"message": "er bundet til en anden handling. Venligst vælg en anden nøgle."
},
"to": {
"message": "til",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -262,21 +262,6 @@
"message": "Falls es dir trotzdem nicht gefällt, drücke auf \"Verstecken\".",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Taste zum Überspringen eines Segments festlegen"
},
"setStartSponsorShortcut": {
"message": "Taste für das Starten/Stoppen eines Segments festlegen"
},
"setSubmitKeybind": {
"message": "Taste für das Einsenden festlegen"
},
"keybindDescription": {
"message": "Zum Festlegen eine Taste drücken"
},
"keybindDescriptionComplete": {
"message": "Die Taste wurde festgelegt auf: "
},
"0": {
"message": "Zeitüberschreibung. Überprüfe deine Internetverbindung. Bist du mit dem Internet verbunden, ist der Server wahrscheinlich offline."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Erstellt von"
},
"keybindCurrentlySet": {
"message": ". Aktuelle Einstellung:"
},
"supportOtherSites": {
"message": "Drittanbieter YouTube-Sites unterstützen"
},
@@ -435,11 +417,14 @@
"message": "Benachrichtigungsdauer überspringen (Sekunden):"
},
"skipNoticeDurationDescription": {
"message": "Die Überspringen Benachrichtigung wird mindestens so lange angezeigt. Für manuelles Überspringen kann sie länger sichtbar sein."
"message": "Die überspringen Benachrichtigung wird mindestens für so viele Sekunden angezeigt. Für manuelles Überspringen kann sie länger sichtbar sein."
},
"shortCheck": {
"message": "Die folgende Einreichung ist kürzer als deine Mindestdauer. Das könnte bedeuten, dass dieses Videosegment bereits eingereicht wurde und aufgrund dieser Option einfach ignoriert wird. Bist du dir sicher, dass du es übermitteln möchtest?"
},
"liveOrPremiere": {
"message": "Das Einreichen eines aktiven Livestreams oder Premiere ist nicht erlaubt. Bitte warte bis es zu ende ist, lade die Seite neu und überprüfe, dass die Segmente noch immer korrekt sind."
},
"showUploadButton": {
"message": "Upload-Knopf anzeigen"
},
@@ -515,12 +500,6 @@
"copyDebugInformationComplete": {
"message": "Die Debug-Informationen wurden in die Zwischenablage kopiert. Du kannst alle Informationen entfernen, die du nicht teilen möchtest. Verwende einen Texteditor um die Informationen in einer Textdatei zu speichern (um diese ggf. einem Fehlerbericht hinzuzufügen)."
},
"theKey": {
"message": "Die Taste"
},
"keyAlreadyUsed": {
"message": "wird bereits für eine andere Aktion verwendet. Bitte wähle eine andere Taste."
},
"to": {
"message": "bis",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -544,7 +523,7 @@
"message": "Nur für das Kennzeichnen ganzer Videos. Wird verwendet wenn ein Video ein Produkt, eine Dienstleistung oder einen Ort präsentiert, zu welchem sie freien oder subventionierten Zugriff erhalten haben."
},
"category_exclusive_access_pill": {
"message": "Dieses Video präsentiert ein Produkt, eine Dienstleistung oder einen Ort, zu welchem sie freien oder subventionierten Zigriff erhalten haben",
"message": "Dieses Video präsentiert ein Produkt, eine Dienstleistung oder einen Ort, zu welchem sie freien oder subventionierten Zugriff erhalten haben",
"description": "Short description for this category"
},
"category_interaction": {
@@ -638,7 +617,7 @@
"message": "Segmente zulassen, die den Ton ausschalten anstatt zu überspringen"
},
"fullVideoSegments": {
"message": "Zeige an, wenn das gesamte Video Werbung ist",
"message": "Zeige ein Icon, wenn ein ganzes Video Werbung ist",
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
},
"colorFormatIncorrect": {

View File

@@ -243,7 +243,7 @@
"message": "All Faded Skip Notices"
},
"longDescription": {
"message": "SponsorBlock lets you skip over sponsors, intros, outros, subscription reminders, and other annoying parts of YouTube videos. SponsorBlock is a crowdsourced browser extension that let's anyone submit the start and end time's of sponsored segments and other segments of YouTube videos. Once one person submits this information, everyone else with this extension will skip right over the sponsored segment. You can also skip over non music sections of music videos.",
"message": "SponsorBlock lets you skip over sponsors, intros, outros, subscription reminders, and other annoying parts of YouTube videos. SponsorBlock is a crowdsourced browser extension that lets anyone submit the start and end times of sponsored segments and other segments of YouTube videos. Once one person submits this information, everyone else with this extension will skip right over the sponsored segment. You can also skip over non music sections of music videos.",
"description": "Full description of the extension on the store pages."
},
"website": {
@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Set key for skipping a segment"
"message": "Skip segment",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Set key for start/stop segment keybind"
"message": "Start/stop segment",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Set key for submission keybind"
"message": "Submit segments",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Select a key by typing it"
},
"keybindDescriptionComplete": {
"message": "The keybind has been set to: "
"message": "Select a key by typing it and choose any modifier keys you wish to use."
},
"0": {
"message": "Connection Timeout. Check your internet connection. If your internet is working, the server is probably overloaded or down."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Created By"
},
"keybindCurrentlySet": {
"message": ". It is currently set to:"
},
"supportOtherSites": {
"message": "Support 3rd Party YouTube-Sites"
},
@@ -435,11 +432,14 @@
"message": "Skip notice duration (seconds):"
},
"skipNoticeDurationDescription": {
"message": "The skip notice will stay on screen for at least this long. For manual skipping, it may be visible for longer."
"message": "The skip notice will stay on screen for at least this many seconds. For manual skipping, it may be visible for longer."
},
"shortCheck": {
"message": "The following submission is shorter than your minimum duration option. This could mean that this is already submitted, and just being ignored due to this option. Are you sure you would like to submit?"
},
"liveOrPremiere": {
"message": "Submitting on an active livestream or premiere is not allowed. Please wait until it finishes, then refresh the page and verify that the segments are still valid."
},
"showUploadButton": {
"message": "Show Upload Button"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "Import/Export All Options"
},
"exportOptionsCopy": {
"message": "Edit/copy"
},
"exportOptionsDownload": {
"message": "Save to file"
},
"exportOptionsUpload": {
"message": "Load from file"
},
"whatExportOptions": {
"message": "This is your entire configuration in JSON. This includes your userID, so be sure to share this wisely."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "The debug information has been copied to the clip board. Feel free to remove any information you would rather not share. Save this in a text file or paste into the bug report."
},
"theKey": {
"message": "The key"
},
"keyAlreadyUsed": {
"message": "is bound to another action. Please select another key."
"message": "This shortcut is bound to another action. Please select a different one."
},
"to": {
"message": "to",
@@ -541,10 +547,10 @@
"message": "Exclusive Access"
},
"category_exclusive_access_description": {
"message": "Only for labeling entire videos. Used when a video showcases a product, service or location that they've recieved free or subsidized access to."
"message": "Only for labeling entire videos. Used when a video showcases a product, service or location that they've received free or subsidized access to."
},
"category_exclusive_access_pill": {
"message": "This video showcases a product, service or location that they've recieved free or subsidized access to",
"message": "This video showcases a product, service or location that they've received free or subsidized access to",
"description": "Short description for this category"
},
"category_interaction": {
@@ -695,6 +701,9 @@
"hiddenDueToDuration": {
"message": "hidden: too short"
},
"manuallyHidden": {
"message": "manually hidden"
},
"channelDataNotFound": {
"description": "This error appears in an alert when they try to whitelist a channel and the extension is unable to determine what channel they are looking at.",
"message": "Channel ID is not loaded yet. If you are using an embedded video, try using the YouTube homepage instead. This could also be caused by changes in the YouTube layout, if you think so, make a comment here:"
@@ -781,9 +790,15 @@
"Donate": {
"message": "Donate"
},
"considerDonating": {
"message": "Help fund development"
},
"hideDonationLink": {
"message": "Hide Donation Link"
},
"darkModeOptionsPage": {
"message": "Dark Mode On Options Page"
},
"helpPageThanksForInstalling": {
"message": "Thanks for installing SponsorBlock."
},
@@ -872,5 +887,42 @@
"hourAbbreviation": {
"message": "h",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Behavior",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Interface",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Keyboard shortcuts",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "Backup/Restore",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Miscellaneous",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "Skip notice appearance",
"description": "Option label"
},
"unbind": {
"message": "Unbind",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Not set"
},
"change": {
"message": "Change"
},
"youtubeKeybindWarning": {
"message": "This is a built-in YouTube shortcut. Are you sure you want to use it?"
}
}

View File

@@ -262,21 +262,6 @@
"message": "Si aún no te gusta, pulsa el botón de nunca mostrar.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Establecer tecla para omitir un segmento"
},
"setStartSponsorShortcut": {
"message": "Establecer tecla para iniciar/detener un segmento"
},
"setSubmitKeybind": {
"message": "Establecer tecla para el envío"
},
"keybindDescription": {
"message": "Seleccione una tecla escribiéndola"
},
"keybindDescriptionComplete": {
"message": "Ese atajo de teclas se ha establecido como: "
},
"0": {
"message": "Tiempo de espera agotado. Compruebe su conexión a Internet. Si su internet está funcionando, el servidor probablemente esta sobrecargado o desconectado."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Creado Por"
},
"keybindCurrentlySet": {
"message": ". Actualmente está configurado para:"
},
"supportOtherSites": {
"message": "Soportar sitios de YouTube de terceros"
},
@@ -440,6 +422,9 @@
"shortCheck": {
"message": "El siguiente envío es más corto que su opción de duración mínima. Esto podría significar que esto ya se ha enviado y que simplemente se ha ignorado debido a esta opción. ¿Está seguro de que desea enviar?"
},
"liveOrPremiere": {
"message": "No se permite enviar en una transmisión en vivo activa o estreno. Espere hasta que finalice, luego actualice la página y verifique que los segmentos aún sean válidos."
},
"showUploadButton": {
"message": "Mostrar botón de subida"
},
@@ -515,12 +500,6 @@
"copyDebugInformationComplete": {
"message": "La información de depuración ha sido copiada al portapapeles. Siéntase libre de eliminar cualquier información que prefiera no compartir. Guarde esto en un archivo de texto o péguelo en el informe de errores."
},
"theKey": {
"message": "La tecla"
},
"keyAlreadyUsed": {
"message": "está enlazada a otra acción. Por favor, seleccione otra tecla."
},
"to": {
"message": "a",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -537,6 +516,16 @@
"category_selfpromo_description": {
"message": "Similar a \"sponsor\", excepto que para la promoción propia o no remunerada. Esto incluye secciones sobre mercancía, donaciones o información sobre con quiénes colaboraron."
},
"category_exclusive_access": {
"message": "Acceso Exclusivo"
},
"category_exclusive_access_description": {
"message": "Solo para etiquetar videos completos. Utilizado cuando un video exhibe un producto, servicio o ubicación al que han recibido acceso gratuito o subsidiado."
},
"category_exclusive_access_pill": {
"message": "Este video exhibe un producto, servicio o ubicación al que han recibido acceso gratuito o subsidiado",
"description": "Short description for this category"
},
"category_interaction": {
"message": "Recordatorio de interacción (subscribir)"
},
@@ -618,6 +607,9 @@
"showOverlay_POI": {
"message": "Mostrar en la barra de búsqueda"
},
"showOverlay_full": {
"message": "Mostrar Etiqueta"
},
"autoSkipOnMusicVideos": {
"message": "Omitir automáticamente todos los segmentos cuando hay un segmento sin música"
},

View File

@@ -262,21 +262,6 @@
"message": "Kui sulle see ikka ei meeldi, vajuta \"ära kunagi näita\" nuppu.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Seadista segmendi vahelejätmise klahv"
},
"setStartSponsorShortcut": {
"message": "Seadista segmendi alustamise/lõpetamise klahv"
},
"setSubmitKeybind": {
"message": "Seadista segmendi saatmise klahv"
},
"keybindDescription": {
"message": "Vali klahv, seda vajutades"
},
"keybindDescriptionComplete": {
"message": "Otsetee on seatud: "
},
"0": {
"message": "Ühenduse ajalõpp. Kontrolli oma võrguühendust. Kui internet töötab, on ilmselt server ülekoormatud või maas."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Autor"
},
"keybindCurrentlySet": {
"message": ". Hetkel on selleks määratud:"
},
"supportOtherSites": {
"message": "Toeta 3. osapoole YouTube-saite"
},
@@ -515,12 +497,6 @@
"copyDebugInformationComplete": {
"message": "Silumisinfo on lõikelauale kopeeritud. Võid sellelt vabalt eemaldada mistahes info, mida ei soovi jagada. Salvesta see tekstifaili või kleebi vearaportisse."
},
"theKey": {
"message": "Klahv"
},
"keyAlreadyUsed": {
"message": "on juba teisele tegevusele määratud. Palun vali teine klahv."
},
"to": {
"message": "kuni",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -537,6 +513,16 @@
"category_selfpromo_description": {
"message": "Sarnaneb \"sponsorile\", ent on mõeldud tasumata või enesepromo jaoks. Selle alla kuuluvad jaotised oma müüdava kauba, annetuste ja koostööpartnerite kohta."
},
"category_exclusive_access": {
"message": "Eksklusiivne ligipääs"
},
"category_exclusive_access_description": {
"message": "Ainult tervete videote märkimiseks. Kasutatakse, kui video esitleb toodet, teenust või asukohta, millele isik on tasuta või toetusega ligipääsu saanud."
},
"category_exclusive_access_pill": {
"message": "See video esitleb toodet, teenust või asukohta, millele isik on tasuta või toetusega ligipääsu saanud",
"description": "Short description for this category"
},
"category_interaction": {
"message": "Tegutsemise meeldetuletus (kanali tellimine)"
},
@@ -618,6 +604,9 @@
"showOverlay_POI": {
"message": "Kuva mängija ajaribal"
},
"showOverlay_full": {
"message": "Kuva silt"
},
"autoSkipOnMusicVideos": {
"message": "Jäta automaatselt kõik segmendid vahele, kui eksisteerib mitte-muusika segment"
},
@@ -745,6 +734,9 @@
"message": "Sain aru",
"description": "Used as the button to dismiss a tooltip"
},
"categoryPillTitleText": {
"message": "See terve video on selle kategooriaga sildistatud ning on liiga tihedalt integreeritud, et eraldada saaks"
},
"experiementOptOut": {
"message": "Keeldu kõigist tulevikus tehtavatatest eksperimentidest",
"description": "This is used in a popup about a new experiment to get a list of unlisted videos to back up since all unlisted videos uploaded before 2017 will be set to private."

View File

@@ -3,11 +3,15 @@
"message": "اسپانسربلاک برای یوتیوب - اسپانسر ها را رد کنید",
"description": "Name of the extension."
},
"Description": {
"message": "بخش های اسپانسر شده، درخواست ساب اسکرایب و خیلی چیز های دیگر در ویدیو های یوتیوب را رد کنید. قسمت های اسپانسری ویدیو هایی که میبینید را گزارش کنید تا در وقت دیگران صرفه جویی شود.",
"description": "Description of the extension."
},
"400": {
"message": "سرور گفت که این درخواست نامعتبر است"
},
"429": {
"message": "شما برای این یک ویدیو تعداد بیش از حدی زمان اسپانسر ثبت کردهاید، آیا مطمئن هستید که به این تعداد وجود دارد؟"
"message": "شما برای این ویدیو قسمت های اسپانسری خیلی زیادی ثبت کرده اید، آیا مطمئنید که به این تعداد وجوددارد؟"
},
"409": {
"message": "این قبلاً ثبت شده است"
@@ -127,7 +131,7 @@
"message": "شما دیگران را نجات دادید از "
},
"viewLeaderboard": {
"message": "لیست سرنشینان"
"message": "نفرات برتر"
},
"recordTimesDescription": {
"message": "ثبت"
@@ -144,6 +148,9 @@
"setUsername": {
"message": "تنظیم نام کاربری"
},
"discordAdvert": {
"message": "به سرور رسمی دیسکورد بپیوندید تا پیشنهادات و بازخورد‌های خود را ارائه دهید!"
},
"hideThis": {
"message": "مخفی‌سازی"
},
@@ -156,6 +163,9 @@
"hideButtons": {
"message": "مخفی‌سازی کلید ها در پخش‌کننده یوتیوب"
},
"showNotice": {
"message": "نمایش مجدد اطلاعیه"
},
"longDescription": {
"message": "افزونه اسپانسر بلاک به شما امکان رد کردن بخش‌های تبلیغاتی (اسپانسر شده)، قسمت‌های شروع و پایان ویدیو، درخواست ساب‌اسکرایب و سایر قسمت‌های آزار دهنده یوتیوب را می‌دهد. اسپانسر بلاک یک افزونه مرورگر است که به هر کسی امکان ثبت زمان شروع و پایان بخش های اسپانسر شده و سایر بخش های ویدیو های یوتیوب را می‌دهد. پس از اینکه هر کاربر این اطلاعات را ثبت کرده و گزارش دهد، بقیه کاربرانی که از این افزونه استفاده می‌کنند مستقیماً بخش اسپانسر شده ویدیو را رد خواهند کرد. شما همچنین می‌توانید در ویدیو های نماهنگ (موزیک ویدیو)، قسمت‌های غیر موسیقی ویدیو را رد کنید.",
"description": "Full description of the extension on the store pages."
@@ -194,6 +204,20 @@
"mute": {
"message": "بی‌صدا"
},
"skip_category": {
"message": "{0} رد شود؟"
},
"mute_category": {
"message": "{0} بی‌صدا شود؟"
},
"skipped": {
"message": "{0} رد شد",
"description": "Example: Sponsor Skipped"
},
"muted": {
"message": "{0} بی‌صدا شد",
"description": "Example: Sponsor Muted"
},
"minLower": {
"message": "دقیقه"
},
@@ -203,6 +227,9 @@
"createdBy": {
"message": "ایجاد شده توسط"
},
"supportedSites": {
"message": "وب‌سایت‌های پشتیبانی شده: "
},
"add": {
"message": "افزودن"
},
@@ -221,6 +248,9 @@
"setOptions": {
"message": "تنظیم گزینه‌ها"
},
"confirmNoticeTitle": {
"message": "ثبت بخش"
},
"submit": {
"message": "ثبت"
},
@@ -249,6 +279,12 @@
"category_sponsor": {
"message": "اسپانسر"
},
"category_exclusive_access": {
"message": "دسترسی اختصاصی"
},
"category_filler_short": {
"message": "پر کننده"
},
"category_music_offtopic_short": {
"message": "غیر موسیقی"
},
@@ -261,12 +297,43 @@
"manualSkip": {
"message": "ردکردن دستی"
},
"showOverlay": {
"message": "نمایش در نوار پیشرفت"
},
"disable": {
"message": "غیرفعال کردن"
},
"showOverlay_POI": {
"message": "نمایش در نوار پیشرفت"
},
"showOverlay_full": {
"message": "نمایش نام"
},
"category": {
"message": "دسته بندی"
},
"bracketNow": {
"message": "(اکنون)"
},
"moreCategories": {
"message": "نمایش دسته‌بندی‌ها"
},
"bracketEnd": {
"message": "(پایان)"
},
"acceptPermission": {
"message": "تأیید دسترسی"
},
"incorrectCategory": {
"message": "تغییر دسته بندی"
},
"guidelines": {
"message": "دستورالعمل‌ها"
},
"readTheGuidelines": {
"message": "دستورالعمل‌ها را بخوانید!!",
"description": "Show the first time they submit or if they are \"high risk\""
},
"help": {
"message": "راهنما"
},
@@ -274,7 +341,61 @@
"message": "فهمیدم",
"description": "Used as the button to dismiss a tooltip"
},
"hideForever": {
"message": "مخفی‌سازی برای همیشه"
},
"warningChatInfo": {
"message": "شما یک اخطار دریافت کرده اید و موقتاً نمیتوانید بخشی را ثبت کنید. این یعنی ما متوجه شدیم که شما یک سری اشتباهات متداول داشتید اما قصد و نیت خرابکاری نداشتید، لطفاً فقط تایید کنید که شما از قوانین آگاهی دارید و سپس ما این اخطار را حذف خواهیم کرد. شما میتوانید به کمک discord.gg/SponsorBlock یا matrix.to/#/#sponsor:ajay.app عضو گروه ما بشوید."
},
"voteRejectedWarning": {
"message": "رای شما رد شد چون شما یک اخطار دارید. برای حل این مشکل یک کلیک کنید تا گروه چت ما باز بشود، یا اینکه بعداً هر موقع وقت داشتید این کار را انجام بدهید.",
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
},
"Donate": {
"message": "کمک مالی"
},
"hideDonationLink": {
"message": "پنهان کردن لینک کمک مالی"
},
"helpPageThanksForInstalling": {
"message": "ازینکه افزونه SponserBlock را نصب کردید سپاسگزاریم."
},
"helpPageReviewOptions": {
"message": "لطفاً تنظیمات زیر را بررسی کنید"
},
"helpPageHowSkippingWorks": {
"message": "چگونه ردکردن کار می‌کند"
},
"Submitting": {
"message": "ثبت نمودن"
},
"Editing": {
"message": "ویرایش"
},
"helpPageCopyOfDatabase": {
"message": "آیا می‌توان یک کپی از پایگاه داده را دریافت کرد؟ اگر شما یک روز ناپدید شدید چه می‌شود؟"
},
"helpPageCopyOfDatabase1": {
"message": "پایگاه داده به‌صورت عمومی در دسترس است در"
},
"helpPageCopyOfDatabase2": {
"message": "سورس کد به‌صورت رایگان ارائه شده است. بنابراین، اگر اتفاقی برای من بیفتد، ارسالات شما از بین نمی‌روند."
},
"helpPageSourceCode": {
"message": "از کجا سورس کد را بگیرم؟"
},
"Credits": {
"message": "سازندگان"
},
"LearnMore": {
"message": "بیشتر بدانید"
},
"dayAbbreviation": {
"message": "روز",
"description": "100d"
},
"hourAbbreviation": {
"message": "ساعت",
"description": "100h"
}
}

View File

@@ -20,10 +20,10 @@
"message": "Kanava lisätty valkolistalle!"
},
"Segment": {
"message": "segmentti"
"message": "osio"
},
"Segments": {
"message": "segmentit"
"message": "osiot"
},
"upvoteButtonInfo": {
"message": "Äänestä tätä lähetystä"
@@ -74,10 +74,10 @@
"message": "Tällä videolla on segmenttejä tietokannassa!"
},
"sponsor404": {
"message": "Segmenttejä ei löytynyt"
"message": "Osioita ei löytynyt"
},
"sponsorStart": {
"message": "Segmentti alkaa nyt"
"message": "Osio alkaa nyt"
},
"sponsorEnd": {
"message": "Segmentti päättyy nyt"
@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Aseta näppäin segmentin ohittamista varten"
"message": "Ohita osio",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Aseta pikanäppäin segmentin aloittamiseen/lopettamiseen"
"message": "Aloita/lopeta osio",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Aseta pikanäppäin tietojen lähetykseen"
"message": "Lähetä osiot",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Valitse näppäin painamalla sitä"
},
"keybindDescriptionComplete": {
"message": "Pikanäppäin on asetettu näppäimeen: "
"message": "Valitse näppäin painamalla sitä ja valitse haluamasi vaihtonäppäin."
},
"0": {
"message": "Yhteyden aikakatkaisu. Tarkista internet-yhteytesi. Jos internetyhteytesi toimii, palvelin on todennäköisesti ylikuormittunut tai alhaalla."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Luonut"
},
"keybindCurrentlySet": {
"message": ". Tällä hetkellä se on asetettu näppäimeen:"
},
"supportOtherSites": {
"message": "Tue kolmansien osapuolien YouTube-sivustoja"
},
@@ -440,6 +437,9 @@
"shortCheck": {
"message": "Seuraava lähetys on lyhyempi kuin vähimmäiskeston asetuksesi. Tämä voi tarkoittaa sitä, että tämä on jo lähetetty, ja sitä ei vain oteta huomioon tämän asetuksen vuoksi. Oletko varma, että haluat lähettää?"
},
"liveOrPremiere": {
"message": "Aktiivisen livestriimin tai ensiesityksen aikana lähettäminen ei ole sallittua. Odota kunnes se loppuu, sitten päivitä sivu ja varmista, että segmentit ovat yhä oikein."
},
"showUploadButton": {
"message": "Näytä lähetä-painike"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "Vie/tuo kaikki asetukset"
},
"exportOptionsCopy": {
"message": "Muokkaa/kopioi"
},
"exportOptionsDownload": {
"message": "Tallenna tiedostoon"
},
"exportOptionsUpload": {
"message": "Lataa tiedostosta"
},
"whatExportOptions": {
"message": "Tämä on koko konfiguraatiosi JSON-tiedostona. Tämä sisältää userID:si, joten jaa sitä viisaasti."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "Virheenkorjaustiedot on kopioitu leikepöydälle. Voit poistaa mitä tahansa tietoa mitä et mielummin jakaisi. Tallenna tämä tekstitiedostoon tai liitä se virheraporttiin."
},
"theKey": {
"message": "Näppäin"
},
"keyAlreadyUsed": {
"message": "on jo liitetty toiseen toimintoon. Valitse toinen näppäin."
"message": "Pikänäppäin on jo liitetty toiselle toiminnolle. Valitse eri näppäin."
},
"to": {
"message": "-",
@@ -537,6 +543,16 @@
"category_selfpromo_description": {
"message": "Samankaltainen \"sponsorin\" kanssa, mutta maksamattomalle tai itsensä mainostukselle. Tämä sisältää osioita kauppatavarasta, lahjoituksista tai tietoa siitä, kenen kanssa he ovat tehneet yhteistyötä."
},
"category_exclusive_access": {
"message": "Yksinoikeudellinen ensikatsaus"
},
"category_exclusive_access_description": {
"message": "Vain kokonaisten videoiden merkitsemiseen. Käytetään kun videossa esitellään tuote, palvelu tai sijainti, johon he ovat saaneet ilmaisen tai tuetun käyttöoikeuden."
},
"category_exclusive_access_pill": {
"message": "Tämä video esittelee tuotteen, palvelun tai sijainnin, johon he ovat saaneet ilmaisen tai tuetun käyttöoikeuden",
"description": "Short description for this category"
},
"category_interaction": {
"message": "Vuorovaikutusmuistutus (tilaaminen)"
},
@@ -571,7 +587,7 @@
"message": "Epäolennainen täytesisältö"
},
"category_filler_description": {
"message": "Täytteeksi tai huumoriksi lisättyjä sekundaarisia kohtauksia, joita ei vaadita videon pääsisällön ymmärtämiseen. Tämän ei tulisi sisältää segmenttejä, jotka tarjoavat kontekstia tai taustatietoja."
"message": "Täytteeksi tai huumoriksi lisättyjä toissijaisia kohtauksia, joita videon pääsisällön ymmärrys ei edellytä. Tämän ei tulisi sisältää aiheeseen liittyviä tai taustatietoja tarjoavia osioita."
},
"category_filler_short": {
"message": "Täytesisältö"
@@ -618,6 +634,9 @@
"showOverlay_POI": {
"message": "Näytä liukusäätimessä"
},
"showOverlay_full": {
"message": "Näytä merkki"
},
"autoSkipOnMusicVideos": {
"message": "Ohita kaikki segmentit automaattisesti, kun videossa on \"Musiikiton\" segmentti"
},
@@ -771,6 +790,9 @@
"hideDonationLink": {
"message": "Piilota lahjoituslinkki"
},
"darkModeOptionsPage": {
"message": "Tumma tila asetussivulla"
},
"helpPageThanksForInstalling": {
"message": "Kiitos, että asensit SponsorBlockin."
},
@@ -859,5 +881,42 @@
"hourAbbreviation": {
"message": "t",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Käyttäytyminen",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Käyttöliittymä",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Pikanäppäimet",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "Varmuuskopioi/palauta",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Sekalaiset",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "Ohitusilmoituksen ulkoasu",
"description": "Option label"
},
"unbind": {
"message": "Poista valinta",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Ei määritetty"
},
"change": {
"message": "Vaihda"
},
"youtubeKeybindWarning": {
"message": "Tämä on YouTuben sisäänrakennettu pikanäppäin. Haluatko varmasti käyttää sitä?"
}
}

View File

@@ -262,21 +262,6 @@
"message": "Si elle ne vous plaît pas, cliquez sur le bouton \"Ne plus montrer\".",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Définir une touche pour passer un segment"
},
"setStartSponsorShortcut": {
"message": "Définir le raccourci pour démarrer/terminer un segment"
},
"setSubmitKeybind": {
"message": "Changer le raccourci pour soumettre les segments"
},
"keybindDescription": {
"message": "Appuyez sur une touche"
},
"keybindDescriptionComplete": {
"message": "Le raccourci choisi est : "
},
"0": {
"message": "Délai de connexion dépassé. Vérifiez votre connexion internet. Si votre connexion internet fonctionne, le serveur est probablement surchargé ou hors service."
},
@@ -384,9 +369,6 @@
"createdBy": {
"message": "Créé par"
},
"keybindCurrentlySet": {
"message": ". Il est actuellement réglé sur :"
},
"supportOtherSites": {
"message": "Support de YouTube-Sites tierces"
},
@@ -463,6 +445,9 @@
"exportOptions": {
"message": "Importer/Exporter toutes les options"
},
"exportOptionsUpload": {
"message": "Charger à partir du fichier"
},
"whatExportOptions": {
"message": "C'est votre configuration complète au format JSON. Elle inclut votre identifiant utilisateur, gardez-la pour vous."
},
@@ -511,12 +496,6 @@
"copyDebugInformationComplete": {
"message": "Les informations de débogage ont été copiées dans le presse-papiers. N'hésitez pas à supprimer toute information que vous ne préférez pas partager. Enregistrez-les dans un fichier texte ou collez-les dans le rapport de bug."
},
"theKey": {
"message": "La clé"
},
"keyAlreadyUsed": {
"message": "est lié à une autre action. Veuillez sélectionner une autre clé."
},
"to": {
"message": "à",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -842,5 +821,27 @@
"hourAbbreviation": {
"message": "h",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Comportement",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Interface",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Raccourcis clavier",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Divers",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"change": {
"message": "Changer"
},
"youtubeKeybindWarning": {
"message": "Il s'agit d'un raccourci YouTube intégré. Êtes-vous sûr de vouloir l'utiliser ?"
}
}

View File

@@ -280,9 +280,6 @@
"createdBy": {
"message": "Izradio"
},
"keybindCurrentlySet": {
"message": ". Trenutno je postavljeno na:"
},
"supportedSites": {
"message": "Podržane stranice: "
},
@@ -358,12 +355,6 @@
"copyDebugInformationOptions": {
"message": "Kopira informacije u međuspremnik koje treba dati razvojnom programeru kada otkrije grešku / kada to programer zatraži. Osjetljive informacije kao što su vaš korisnički ID, kanali s popisa dopuštenih i prilagođena adresa poslužitelja uklonjeni su. Međutim, sadrži informacije kao što su vaš korisnički agent, preglednik, operativni sustav i broj verzije proširenja. "
},
"theKey": {
"message": "Tipka"
},
"keyAlreadyUsed": {
"message": "je vezana za drugu radnju. Molimo odaberite drugu tipku."
},
"to": {
"message": "do",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Billentyű beállítása szegmens átugráshoz"
"message": "Szegmens átugrása",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Billentyű beállítása a szegmens kezdéséhez/befejezéséhez"
"message": "Szegmens kezdése/befejezése",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Billentyű beállítása a beküldés gombhoz"
"message": "Szegmensek beküldése",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Válassz egy billentyűt azzal, hogy lenyomod"
},
"keybindDescriptionComplete": {
"message": "A funkció erre a billentyűre lett állítva: "
"message": "Válassz egy billentyűt azzal, hogy lenyomod és válaszd ki a módosító billentyű(ke)t, amiket használni szeretnél."
},
"0": {
"message": "Kapcsolati időtúllépés. Ellenőrizd az internetkapcsolatodat! Ha az internet működik, a kiszolgáló valószínűleg túlterhelt vagy leállt."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Készítette"
},
"keybindCurrentlySet": {
"message": ". Jelenleg erre van állítva:"
},
"supportOtherSites": {
"message": "Harmadik fél Youtube oldalainak támogatása"
},
@@ -440,6 +437,9 @@
"shortCheck": {
"message": "A következő szegmens rövidebb, mint az általad beállított minimális időtartam. Ez azt jelentheti, hogy már beküldhették, csak emiatt az opció miatt nálad nem jelenik meg. Biztosan beküldöd?"
},
"liveOrPremiere": {
"message": "Aktív élő közvetítés vagy premier közben nem lehet szegmenseket beküldeni. Kérjük várd meg a végét, majd frissítsd az oldalt és ellenőrizd, hogy a szegmensek nem csúsztak-e el."
},
"showUploadButton": {
"message": "Feltöltés gomb megjelenítése"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "Összes beállítás importálása / exportálása"
},
"exportOptionsCopy": {
"message": "Szerkesztés/másolás"
},
"exportOptionsDownload": {
"message": "Mentés fájlba"
},
"exportOptionsUpload": {
"message": "Betöltés fájlból"
},
"whatExportOptions": {
"message": "Ez az összes beállításod JSON formátumban. Ebbe bele tartozik a userID-d is, szóval csak ésszel oszd meg."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "A hibakeresési információ másolva lett a vágólapjára. Nyugodtan távolíts el belőle olyan információkat, amiket nem szívesen osztanál meg. Mentsd el szöveges fájlként, vagy másold a hibajelentésbe."
},
"theKey": {
"message": "A(z)"
},
"keyAlreadyUsed": {
"message": "billentyű már máshoz van állítva. Kérlek, válassz egy másik billentyűt."
"message": "Ez a billentyűparancs egy másik művelethez tartozik. Kérk, válassz egy újat."
},
"to": {
"message": "",
@@ -541,10 +547,10 @@
"message": "Exkluzív hozzáférés"
},
"category_exclusive_access_description": {
"message": "Csak teljes videók megjelölésére. Akkor használt, amikor egy videó egy olyan terméket, szolgáltatást vagy helyszínt mutat be, amihez ingyen vagy kedvezményes hozzáférést kaptak."
"message": "Csak teljes videók megjelölésére. Akkor használt, amikor egy videó egy olyan terméket, szolgáltatást vagy helyszínt mutat be, amihez ingyenes vagy kedvezményes hozzáférést kaptak."
},
"category_exclusive_access_pill": {
"message": "Ez a videó olyan terméket, szolgáltatást vagy helyszínt mutat be, amihez ingyen vagy kedvezményes hozzáférést kaptak",
"message": "Ez a videó olyan terméket, szolgáltatást vagy helyszínt mutat be, amihez ingyenes vagy kedvezményes hozzáférést kaptak",
"description": "Short description for this category"
},
"category_interaction": {
@@ -784,6 +790,9 @@
"hideDonationLink": {
"message": "Támogatás link elrejtése"
},
"darkModeOptionsPage": {
"message": "Sötét téma a Beállításokban"
},
"helpPageThanksForInstalling": {
"message": "Köszönjük, hogy telepítetted a SponsorBlockot."
},
@@ -872,5 +881,42 @@
"hourAbbreviation": {
"message": "ó",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Működés",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Felület",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Gyorsbillentyűk",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "Biztonsági mentés",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Egyéb",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "Átugrási értesítés kinézete",
"description": "Option label"
},
"unbind": {
"message": "Megszüntetés",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Nincs beállítva"
},
"change": {
"message": "Megváltoztat"
},
"youtubeKeybindWarning": {
"message": "Ez egy beépített YouTube gyorsbillentyű. Biztosan ezt szeretnéd használni?"
}
}

View File

@@ -262,21 +262,6 @@
"message": "Jika anda masih tidak menyukainya, tekan tombol jangan tampilkan.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Atur tombol untuk melewati sebuah segmen"
},
"setStartSponsorShortcut": {
"message": "Atur tombol untuk mulai/hentikan segmen"
},
"setSubmitKeybind": {
"message": "Atur tombol untuk kirim submisi"
},
"keybindDescription": {
"message": "Pilih tombol dengan mengetikkannya"
},
"keybindDescriptionComplete": {
"message": "Tombol diatur menjadi: "
},
"0": {
"message": "Koneksi Timeout. Cek koneksi internet anda. Jika internet anda berfungsi, server mungkin kewalahan atau down."
},
@@ -384,9 +369,6 @@
"createdBy": {
"message": "Dibuat Oleh"
},
"keybindCurrentlySet": {
"message": ". Saat ini diatur pada:"
},
"supportOtherSites": {
"message": "Dukung Situs Youtube Pihak Ketiga"
},
@@ -511,12 +493,6 @@
"copyDebugInformationComplete": {
"message": "Informasi debug telah disalin ke papan klip. Jangan ragu untuk menghapus informasi yang tidak ingin anda bagikan. Simpan ini di file teks atau salin ke laporan bug."
},
"theKey": {
"message": "Tombolnya"
},
"keyAlreadyUsed": {
"message": "terikat pada tindakan lain. Mohon memilih tombol lain."
},
"to": {
"message": "sampai",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -262,21 +262,6 @@
"message": "Se non ti piace ancora, premi il pulsante \"non mostrare mai\".",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Imposta un tasto per saltare un segmento"
},
"setStartSponsorShortcut": {
"message": "Imposta un tasto personalizzato per iniziare/finire il segmento"
},
"setSubmitKeybind": {
"message": "Imposta un comando rapido per l'invio"
},
"keybindDescription": {
"message": "Seleziona un tasto premendolo sulla tastiera"
},
"keybindDescriptionComplete": {
"message": "Il comando rapido è stato impostato come: "
},
"0": {
"message": "Timeout della connessione. Controlla la tua connessione a Internet. Se internet sta funzionando, il server è probabilmente sovraccarico oppure giù."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Creato da"
},
"keybindCurrentlySet": {
"message": ". Attualmente è impostato su:"
},
"supportOtherSites": {
"message": "Supporta siti di YouTube di terze parti"
},
@@ -440,6 +422,9 @@
"shortCheck": {
"message": "Il seguente contributo è più breve della tua opzione di durata minima. Ciò potrebbe significare che questo è già stato inviato e viene semplicemente ignorato a causa di questa opzione. Sei sicuro di voler inviare?"
},
"liveOrPremiere": {
"message": "Inviare segmenti per una live o premiere non è consentito. Per favore aspetta che finisca, poi ricarica la pagina e verifica che i segmenti siano ancora validi."
},
"showUploadButton": {
"message": "Mostra Pulsante di Caricamento"
},
@@ -515,12 +500,6 @@
"copyDebugInformationComplete": {
"message": "Le informazioni di debug sono state copiate nel clip board. Sentiti libero di rimuovere tutte le informazioni che preferisci non condividere. Salva in un file di testo o incollale nella segnalazione di bug."
},
"theKey": {
"message": "La chiave"
},
"keyAlreadyUsed": {
"message": "è abbinata ad un'altra azione. Si prega di selezionare un'altra chiave."
},
"to": {
"message": "a",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -537,6 +516,16 @@
"category_selfpromo_description": {
"message": "Simile alle \"sponsorizzazioni\" tranne che per promozioni non pagate o autopromozioni. Ciò include sezioni riguardanti vendita di merce, donazioni o informazioni in merito a collaboratori."
},
"category_exclusive_access": {
"message": "Accesso Esclusivo"
},
"category_exclusive_access_description": {
"message": "Solo per etichettare interi video. Usato quando un video mostra un prodotto, un servizio o un posto che hanno ricevuto gratuitamente o a cui hanno avuto un accesso sovvenzionato."
},
"category_exclusive_access_pill": {
"message": "Questo video mostra un prodotto, un servizio o un posto che hanno ricevuto gratuitamente o a cui hanno avuto un accesso sovvenzionato",
"description": "Short description for this category"
},
"category_interaction": {
"message": "Promemoria di Interazione (Sottoscrizione)"
},
@@ -568,10 +557,10 @@
"message": "Riepilogo rapido degli episodi precedenti, o un'anteprima di ciò che sta arrivando più tardi nel video attuale. Inteso per clip, non per riassunti a voce."
},
"category_filler": {
"message": "Tangente di Riempimento"
"message": "Tangente riempitiva"
},
"category_filler_description": {
"message": "Le scene tangenziali aggiunte solo per riempire o per umorismo che non sono richieste per comprendere il contenuto principale del video. Questo non dovrebbe includere segmenti che forniscono contesto o dettagli di sfondo."
"message": "Le scene riempitive sono aggiunte solo per riempire o per umorismo che non sono richieste per comprendere il contenuto principale del video. Questo non dovrebbe includere segmenti che forniscono contesto o dettagli di sfondo."
},
"category_filler_short": {
"message": "Riempimento"
@@ -618,6 +607,9 @@
"showOverlay_POI": {
"message": "Mostra Nella Barra di Ricerca"
},
"showOverlay_full": {
"message": "Mostra Etichetta"
},
"autoSkipOnMusicVideos": {
"message": "Salta automaticamente tutti i segmenti quando c'è un segmento non musicale"
},
@@ -832,7 +824,7 @@
"message": "Scopri di Più"
},
"CopyDownvoteButtonInfo": {
"message": "Vota negativamente e crea una copia locale per reinviare"
"message": "Vota negativamente e crea una copia locale da reinviare"
},
"OpenCategoryWikiPage": {
"message": "Apri la pagina della wiki di questa categoria."

View File

@@ -262,21 +262,6 @@
"message": "もしそれでも気に入らない場合は、非表示ボタンをクリックしてください。",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "セグメントをスキップするキーを設定"
},
"setStartSponsorShortcut": {
"message": "セグメントの開始/停止キーのバインドを設定"
},
"setSubmitKeybind": {
"message": "提案キーのバインドを設定"
},
"keybindDescription": {
"message": "キーを入力して設定します"
},
"keybindDescriptionComplete": {
"message": "キーバインドは次のように設定されました: "
},
"0": {
"message": "接続がタイムアウトになりました。インターネット接続をご確認ください。接続に問題がない場合、サーバーが混雑またはダウンしている可能性があります。"
},
@@ -384,9 +369,6 @@
"createdBy": {
"message": "作成者:"
},
"keybindCurrentlySet": {
"message": "。現在の設定は:"
},
"supportOtherSites": {
"message": "第三者製のYouTube関連サイトに対応"
},
@@ -511,12 +493,6 @@
"copyDebugInformationComplete": {
"message": "診断用情報がクリップボードに複製されました。共有したくない情報があればそこから削除できます。これをファイルに保存したり,不具合報告に貼り付けてください。"
},
"theKey": {
"message": "キー"
},
"keyAlreadyUsed": {
"message": "は他の動作に割り当てられています。異なったキーを選択してください。"
},
"to": {
"message": "",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "구간 건너뛰기 키 설정"
"message": "구간 건너뛰기",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "시작/끝 부분 키 할당 설정"
"message": "구간 시작/중지",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "제출 키 할당 설정"
"message": "구간 제출",
"description": "Keybind label"
},
"keybindDescription": {
"message": "입력해서 키를 선택하세요"
},
"keybindDescriptionComplete": {
"message": "키가 다음에 할당되었습니다: "
"message": "키를 눌러서 선택하고 함께 사용하고 싶은 조합 키를 선택하십시오."
},
"0": {
"message": "연결 타임아웃 오류입니다. 인터넷이 연결되어 있는지 확인해주세요. 인터넷이 연결되어 있는 경우, 서버가 과부하되어 있거나 다운되어 있어서 오류가 발생하는 것일 수도 있습니다."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "개발자: "
},
"keybindCurrentlySet": {
"message": ". 현재 다음으로 설정되어 있습니다:"
},
"supportOtherSites": {
"message": "제3자 YouTube 사이트 지원"
},
@@ -440,6 +437,9 @@
"shortCheck": {
"message": "다음 제출은 최소 기간 옵션보다 짧습니다. 이 말은 이미 제출되었으며, 이 옵션으로 인해 무시될 수 있습니다. 제출하시겠습니까?"
},
"liveOrPremiere": {
"message": "진행 중인 실시간 스트림 및 최초 공개 동영상에는 제출할 수 없습니다. 끝날 때까지 대기한 후, 페이지를 새로고침하고 구간이 여전히 유효한지 확인해주십시오."
},
"showUploadButton": {
"message": "업로드 버튼 표시"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "모든 설정 가져오기/내보내기"
},
"exportOptionsCopy": {
"message": "수정/복사"
},
"exportOptionsDownload": {
"message": "파일로 저장"
},
"exportOptionsUpload": {
"message": "파일에서 불러오기"
},
"whatExportOptions": {
"message": "JSON의 전체 구성입니다. 사용자ID가 포함되므로 주의하세요."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "디버그 정보가 복사되었습니다. 공유하고 싶지 않은 내용이 있는 경우 자유롭게 수정하신 후, 텍스트 파일로 저장하시거나 버그 신고 페이지에 붙여넣으시면 됩니다."
},
"theKey": {
"message": "키"
},
"keyAlreadyUsed": {
"message": "이 키는 다른 행동과 연동되었습니다. 다른 키를 선택하세요."
"message": "이 단축키는 다른 동작에 할당되어 있습니다. 다른 키를 선택하세요."
},
"to": {
"message": "-",
@@ -537,6 +543,16 @@
"category_selfpromo_description": {
"message": "'스폰서 광고'와 비슷하지만 협찬 없이 자기 채널을 홍보하는 구간입니다. 여기에는 채널 굿즈 광고, 기부 광고와 영상에 참여한 사람들을 홍보하는 광고가 해당됩니다."
},
"category_exclusive_access": {
"message": "협찬"
},
"category_exclusive_access_description": {
"message": "전체 동영상 전용 카테고리입니다. 동영상이 유/무료 협찬을 받은 제품, 서비스, 장소를 소개하는 경우 사용합니다."
},
"category_exclusive_access_pill": {
"message": "본 동영상은 무료/유료 협찬을 받은 제품, 서비스, 장소를 소개합니다",
"description": "Short description for this category"
},
"category_interaction": {
"message": "상호 작용 알림 (구독)"
},
@@ -774,6 +790,9 @@
"hideDonationLink": {
"message": "후원 링크 숨기기"
},
"darkModeOptionsPage": {
"message": "설정 페이지 내 다크 모드"
},
"helpPageThanksForInstalling": {
"message": "SponsorBlock을 설치해 주셔서 감사드립니다."
},
@@ -862,5 +881,42 @@
"hourAbbreviation": {
"message": "시간",
"description": "100h"
},
"optionsTabBehavior": {
"message": "동작",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "인터페이스",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "키보드 단축키",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "백업/복원",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "기타",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "건너뛰기 알림 표시",
"description": "Option label"
},
"unbind": {
"message": "할당 해제",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "설정되지 않음"
},
"change": {
"message": "변경"
},
"youtubeKeybindWarning": {
"message": "기본 YouTube 단축키와 겹칩니다. 그래도 사용하시겠습니까?"
}
}

View File

@@ -226,18 +226,6 @@
"message": "നിങ്ങൾക്ക് ഇപ്പോഴും ഇഷ്‌ടമായില്ലെങ്കിൽ, ഒരിക്കലും കാണിക്കരുത് ബട്ടൺ അമർത്തുക.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "ഒരു സെഗ്മെന്റ് ഒഴിവാക്കുന്നതിനായി കീ സജ്ജമാക്കുക"
},
"setSubmitKeybind": {
"message": "സമർപ്പിക്കൽ കീബൈൻഡിനായി കീ സജ്ജമാക്കുക"
},
"keybindDescription": {
"message": "ഒരു കീ ടൈപ്പുചെയ്ത് തിരഞ്ഞെടുക്കുക"
},
"keybindDescriptionComplete": {
"message": "കീബൈൻഡ് ഇനിപ്പറയുന്നതായി സജ്ജമാക്കി: "
},
"0": {
"message": "കണക്ഷൻ കാലഹരണപ്പെട്ടു. നിങ്ങളുടെ ഇന്റർനെറ്റ് കണക്ഷൻ പരിശോധിക്കുക. നിങ്ങളുടെ ഇൻറർനെറ്റ് പ്രവർത്തിക്കുന്നുണ്ടെങ്കിൽ, സെർവർ ഓവർലോഡ് അല്ലെങ്കിൽ ഡ. ൺ ആയിരിക്കാം."
},
@@ -320,9 +308,6 @@
"createdBy": {
"message": "ഉണ്ടാക്കിയത്"
},
"keybindCurrentlySet": {
"message": ". ഇത് നിലവിൽ ഇതായി സജ്ജീകരിച്ചിരിക്കുന്നു:"
},
"optionsInfo": {
"message": "ആക്രമണാത്മക പിന്തുണ പ്രാപ്തമാക്കുക, ഓട്ടോസ്കിപ്പ് അപ്രാപ്തമാക്കുക, ബട്ടണുകൾ മറയ്ക്കുക എന്നിവയും അതിലേറെയും."
},
@@ -422,12 +407,6 @@
"copyDebugInformationComplete": {
"message": "ഡീബഗ് വിവരങ്ങൾ ക്ലിപ്പ് ബോർഡിലേക്ക് പകർത്തി. നിങ്ങൾ പങ്കിടാൻ ആഗ്രഹിക്കാത്ത ഏതെങ്കിലും വിവരങ്ങൾ നീക്കംചെയ്യാൻ മടിക്കേണ്ട. ഇത് ഒരു ടെക്സ്റ്റ് ഫയലിൽ സംരക്ഷിക്കുക അല്ലെങ്കിൽ ബഗ് റിപ്പോർട്ടിൽ ഒട്ടിക്കുക."
},
"theKey": {
"message": "താക്കോല്"
},
"keyAlreadyUsed": {
"message": "മറ്റൊരു പ്രവർത്തനവുമായി ബന്ധപ്പെട്ടിരിക്കുന്നു. മറ്റൊരു കീ തിരഞ്ഞെടുക്കുക."
},
"to": {
"message": "ടു",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -226,18 +226,6 @@
"message": "Sekiranya anda masih tidak menyukainya, tekan butang jangan tunjukkan.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Tetapkan kunci untuk melangkau segmen"
},
"setSubmitKeybind": {
"message": "Tetapkan kunci untuk pengikat kunci penyerahan"
},
"keybindDescription": {
"message": "Pilih kekunci dengan menaipnya"
},
"keybindDescriptionComplete": {
"message": "Ikatan kunci telah ditetapkan ke: "
},
"0": {
"message": "Masa sambungan telah tamat. Periksa sambungan internet anda. Sekiranya internet anda berfungsi, pelayan mungkin berlebihan atau tidak berfungsi."
},
@@ -320,9 +308,6 @@
"createdBy": {
"message": "Dicipta oleh"
},
"keybindCurrentlySet": {
"message": ". Pada masa ini ditetapkan untuk:"
},
"optionsInfo": {
"message": "Dayakan sokongan Invidious, lumpuhkan autoskip, sembunyikan butang dan banyak lagi."
},
@@ -422,12 +407,6 @@
"copyDebugInformationComplete": {
"message": "Maklumat debug telah disalin ke papan klip. Jangan ragu untuk membuang maklumat yang anda tidak mahu kongsi Simpan ini dalam fail teks atau tampal ke laporan pepijat."
},
"theKey": {
"message": "Kunci"
},
"keyAlreadyUsed": {
"message": "terikat dengan tindakan lain. Sila pilih kunci lain."
},
"to": {
"message": "ke",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -262,21 +262,6 @@
"message": "Als het u nog steeds niet bevalt, druk dan op de knop \"nooit weergeven\".",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Sleutel instellen voor het overslaan van een segment"
},
"setStartSponsorShortcut": {
"message": "Sneltoets instellen voor begin/einde van segment"
},
"setSubmitKeybind": {
"message": "Sneltoets instellen voor inzending"
},
"keybindDescription": {
"message": "Selecteer een toets door hem in te drukken"
},
"keybindDescriptionComplete": {
"message": "Deze sneltoets is ingesteld op: "
},
"0": {
"message": "Time-out van de verbinding. Controleer uw internetverbinding. Als uw internet werkt, is de server waarschijnlijk overbelast of offline."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Gemaakt door"
},
"keybindCurrentlySet": {
"message": ". Hij is momenteel ingesteld op:"
},
"supportOtherSites": {
"message": "Ondersteuning voor YouTube-sites van derden"
},
@@ -440,6 +422,9 @@
"shortCheck": {
"message": "De volgende inzending is korter dan uw \"minimale tijdsduur\"-instelling. Dit kan betekenen dat dit al is ingediend en genegeerd wordt door deze optie. Weet u zeker dat u dit wilt indienen?"
},
"liveOrPremiere": {
"message": "Indienen op een actieve livesteam of première is niet toegestaan. Wacht tot hij geëindigd is, vernieuw dan de pagina en controleer of de segmenten nog geldig zijn."
},
"showUploadButton": {
"message": "Uploaden-knop weergeven"
},
@@ -515,12 +500,6 @@
"copyDebugInformationComplete": {
"message": "De foutopsporingsinformatie is gekopieerd naar het klembord. Voel u vrij om alle informatie die u liever niet wilt delen, te verwijderen. Sla dit op in een tekstbestand of plak het in het foutenrapport."
},
"theKey": {
"message": "De toets"
},
"keyAlreadyUsed": {
"message": "is gekoppeld aan een andere actie. Selecteer een andere toets."
},
"to": {
"message": "tot",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -537,6 +516,16 @@
"category_selfpromo_description": {
"message": "Vergelijkbaar met \"sponsor\", behalve voor onbetaalde of zelfpromotie. Dit is inclusief secties over koopwaar, donaties of informatie over met wie ze hebben samengewerkt."
},
"category_exclusive_access": {
"message": "Exclusieve toegang"
},
"category_exclusive_access_description": {
"message": "Alleen voor het labelen van volledige video's. Wordt gebruikt wanneer een video een product, dienst of locatie laat zien waartoe men gratis of gesubsidieerd toegang heeft gekregen."
},
"category_exclusive_access_pill": {
"message": "Deze video toont een product, dienst of locatie waartoe men gratis of gesubsidieerd toegang heeft gekregen",
"description": "Short description for this category"
},
"category_interaction": {
"message": "Interactieherinnering (abonneren)"
},
@@ -618,6 +607,9 @@
"showOverlay_POI": {
"message": "Weergeven in tijdbalk"
},
"showOverlay_full": {
"message": "Label weergeven"
},
"autoSkipOnMusicVideos": {
"message": "Automatisch alle segmenten overslaan wanneer er een niet-muziek-segment is"
},

View File

@@ -226,18 +226,6 @@
"message": "Hvis du fortsatt ikke liker det, klikk på «Aldri vis»-knappen.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Angi tast for å hoppe over et segment"
},
"setSubmitKeybind": {
"message": "Angi tast for innsending"
},
"keybindDescription": {
"message": "Velg en tast ved å trykke på den"
},
"keybindDescriptionComplete": {
"message": "Hurtigtasten har blitt satt til: "
},
"0": {
"message": "Tidsavbrudd for tilkobling. Sjekk internettilkoblingen din. Hvis internettet ditt virker, er tjeneren sannsynligvis overbelastet eller nede."
},
@@ -320,9 +308,6 @@
"createdBy": {
"message": "Opprettet av"
},
"keybindCurrentlySet": {
"message": ". Den er før øyeblikket satt til:"
},
"optionsInfo": {
"message": "Skru på Invidious-støtte, skru av autohopp, skjul knapper, og mer."
},
@@ -422,12 +407,6 @@
"copyDebugInformationComplete": {
"message": "Avlusingsinformasjonen har blitt kopiert til utklippstavlen. Du er velkommen til å fjerne det av informasjon som du helst ikke vil dele. Lagre dette i en tekstfil, eller lim det inn i feilrapporten."
},
"theKey": {
"message": "Nøkkelen"
},
"keyAlreadyUsed": {
"message": "er bundet til en annen handling. Vennligst velg en annen tast."
},
"to": {
"message": "til",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -263,19 +263,8 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Ustaw klawisz do pomijania segmentów"
},
"setStartSponsorShortcut": {
"message": "Ustaw klawisz do oznaczania początku/końca segmentu"
},
"setSubmitKeybind": {
"message": "Ustaw klawisz do wysyłania czasów"
},
"keybindDescription": {
"message": "Wybierz klawisz, wciskając go na klawiaturze"
},
"keybindDescriptionComplete": {
"message": "Ustawiony klawisz to: "
"message": "Pomiń segment",
"description": "Keybind label"
},
"0": {
"message": "Połączenie przerwane z powodu braku odpowiedzi. Sprawdź swoje połączenie z internetem. Jeśli wszystko z nim w porządku oznacza to, że serwer jest prawdopodobnie przeciążony lub nie działa."
@@ -307,7 +296,7 @@
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
},
"skip_category": {
"message": "Pominąć {0}?"
"message": "{0} — pominąć?"
},
"mute_category": {
"message": "Wyciszyć {0}?"
@@ -388,9 +377,6 @@
"createdBy": {
"message": "Stworzony przez"
},
"keybindCurrentlySet": {
"message": ". Obecnie ustawiony:"
},
"supportOtherSites": {
"message": "Obsługa stron YouTube firm trzecich"
},
@@ -515,12 +501,6 @@
"copyDebugInformationComplete": {
"message": "Informacje do debugowania zostały skopiowane do schowka. Możesz usunąć dane, których nie chcesz udostępniać. Zapisz je w pliku tekstowym albo wklej do raportu podczas zgłaszania błędu."
},
"theKey": {
"message": "Klucz"
},
"keyAlreadyUsed": {
"message": "jest przypisane do innej akcji. Wybierz proszę inny klawisz."
},
"to": {
"message": "do",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -859,5 +839,9 @@
"hourAbbreviation": {
"message": "h",
"description": "100h"
},
"optionsTabInterface": {
"message": "Interfejs",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
}
}

View File

@@ -262,21 +262,6 @@
"message": "Se você ainda não gostar dessa, aperte o botão não mostrar novamente.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Defina a tecla para ignorar um segmento"
},
"setStartSponsorShortcut": {
"message": "Defina a tecla para iniciar/interromper o segmento"
},
"setSubmitKeybind": {
"message": "Defina a tecla para enviar o segmento de patrocínio"
},
"keybindDescription": {
"message": "Selecione uma tecla apertando-a"
},
"keybindDescriptionComplete": {
"message": "A tecla foi definida para: "
},
"0": {
"message": "Tempo limite de conexão excedida. Cheque a sua conexão de internet. Se a sua internet estiver funcionando, o servidor está sobrecarregado ou fora do ar."
},
@@ -302,6 +287,10 @@
"mute": {
"message": "Silenciar"
},
"full": {
"message": "Vídeo completo",
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
},
"skip_category": {
"message": "Pular {0}?"
},
@@ -384,9 +373,6 @@
"createdBy": {
"message": "Criado por"
},
"keybindCurrentlySet": {
"message": ". Atualmente, está definido para:"
},
"supportOtherSites": {
"message": "Suporte a Sites do YouTube de Terceiros"
},
@@ -511,12 +497,6 @@
"copyDebugInformationComplete": {
"message": "A informação de depuração foi copiada para a área de transferência. Sinta-se à vontade para remover qualquer informação que prefira não compartilhar. Salve em um arquivo de texto ou cole-a no relatório de bug."
},
"theKey": {
"message": "A tecla"
},
"keyAlreadyUsed": {
"message": "está vinculado a outra ação. Por favor, selecione outra tecla."
},
"to": {
"message": "até",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -533,6 +513,9 @@
"category_selfpromo_description": {
"message": "Similar a \"patrocinador\", mas para auto promoções e segmentos não-pagos. Isso inclui seções sobre vendas, doações ou informações sobre com quem colaboraram."
},
"category_exclusive_access": {
"message": "Acesso Exclusivo"
},
"category_interaction": {
"message": "Lembrete de interação (inscrever-se)"
},
@@ -614,12 +597,19 @@
"showOverlay_POI": {
"message": "Mostrar na barra de progresso"
},
"showOverlay_full": {
"message": "Mostrar Rótulo"
},
"autoSkipOnMusicVideos": {
"message": "Pular automaticamente todos os segmentos quando há um segmento que não é música"
},
"muteSegments": {
"message": "Permitir segmentos que silenciem o áudio ao invés de pular"
},
"fullVideoSegments": {
"message": "Mostrar um ícone quando um vídeo é inteiramente um anúncio",
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
},
"colorFormatIncorrect": {
"message": "Sua cor está formatada incorretamente. Deve ser um código hexadecimal de 3 ou 6 dígitos com uma cerquilha (hashtag) no início."
},
@@ -737,6 +727,12 @@
"message": "Entendi",
"description": "Used as the button to dismiss a tooltip"
},
"fullVideoTooltipWarning": {
"message": "Este segmento é grande. Se o vídeo inteiro for sobre um tópico, altere de \"Pular\" para \"Vídeo completo\". Consulte as diretrizes para obter mais informações."
},
"categoryPillTitleText": {
"message": "Este vídeo inteiro está rotulado como esta categoria e está muito integrado para poder ser separado"
},
"experiementOptOut": {
"message": "Optar por sair de todos os experimentos futuros",
"description": "This is used in a popup about a new experiment to get a list of unlisted videos to back up since all unlisted videos uploaded before 2017 will be set to private."
@@ -835,6 +831,9 @@
"SponsorTimeEditScrollNewFeature": {
"message": "Use a roda do mouse enquanto mantêm o cursor sobre a caixa de edição para ajustar o tempo rapidamente. Combinações das teclas ctrl e shift podem ser usadas para refinar as mudanças."
},
"categoryPillNewFeature": {
"message": "Novo! Veja quando um vídeo é inteiramente patrocinado ou de autopromoção"
},
"dayAbbreviation": {
"message": "d",
"description": "100d"

View File

@@ -226,18 +226,6 @@
"message": "Dacă nu îți place, apasă butonul nu mai arăta niciodată.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Setați cheia pentru omiterea unui segment"
},
"setSubmitKeybind": {
"message": "Setează tasta pentru trimiterea sponsorizării"
},
"keybindDescription": {
"message": "Selectează o tastă apasând-o"
},
"keybindDescriptionComplete": {
"message": "Tasta a fost setată la: "
},
"0": {
"message": "Eroare de Conexiune. Verifică-ți conexiunea la internet. Daca internetul functionează, serverul este probabil supraîncărcat sau a căzut."
},
@@ -320,9 +308,6 @@
"createdBy": {
"message": "Creat De"
},
"keybindCurrentlySet": {
"message": ". În prezent este setat:"
},
"optionsInfo": {
"message": "Activează suportul Invidious, dezactivează autoskip-ul, ascunde butoanele și altele."
},
@@ -422,12 +407,6 @@
"copyDebugInformationComplete": {
"message": "Informația de depanare a fost copiată în clipboard. Puteți elimina orice informație pe care nu doriți să o partajați. Salvați într-un fișier sau lipiți-o în raportul de erori."
},
"theKey": {
"message": "Tasta"
},
"keyAlreadyUsed": {
"message": "este deja setată la o altă acțiune. Vă rugăm să selectați o altă tastă."
},
"to": {
"message": "până la",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Назначить горячую клавишу для пропуска сегмента"
"message": "Пропустить сегмент",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Назначить горячую клавишу для начала/остановки сегмента"
"message": "Начало/конец сегмента",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Назначить горячую клавишу для отправки"
"message": "Отправить сегменты",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Нажмите клавишу, чтобы выбрать её"
},
"keybindDescriptionComplete": {
"message": "Клавиша назначена на: "
"message": "Нажмите на любую кнопку, чтобы выбрать её, а так же выберите модификаторы, если нужно."
},
"0": {
"message": "Таймаут подключения. Проверьте ваше соединение с интернетом. Если ваш интернет работает, сервер, скорее всего, перегружен или лежит."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Создано"
},
"keybindCurrentlySet": {
"message": ". Он сейчас назначен на:"
},
"supportOtherSites": {
"message": "Поддерживать сторонние YouTube-сайты"
},
@@ -440,6 +437,9 @@
"shortCheck": {
"message": "Следующий диапазон времени короче, чем Ваша настройка минимальной длительности. Это может означать, что он уже был отправлен, и просто игнорируется из-за этой настройки. Вы действительно хотите отправить?"
},
"liveOrPremiere": {
"message": "Отправка сегментов на стримах или премьерах не допускается. Пожалуйста, дождитесь окончания видео, затем обновите страницу и убедитесь, что сегменты всё ещё верные."
},
"showUploadButton": {
"message": "Показывать кнопку отправки"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "Импорт/Экспорт всех настроек"
},
"exportOptionsCopy": {
"message": "Редактировать/скопировать"
},
"exportOptionsDownload": {
"message": "Сохранить в файл"
},
"exportOptionsUpload": {
"message": "Загрузить из файла"
},
"whatExportOptions": {
"message": "Все настройки в формате JSON. Этот файл содержит Ваш идентификатор пользователя, будьте осторожны когда делитесь этими данными."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "Отладочная информация скопирована в буфер обмена. Вы можете удалить любую информацию, которой не хотите делиться. Сохраните ее в текстовом файле или вставьте в отчет об ошибке."
},
"theKey": {
"message": "Клавиша"
},
"keyAlreadyUsed": {
"message": "привязана к другому действию. Пожалуйста, выберите другую клавишу."
"message": "Это сочетание клавиш привязано к другому действию. Пожалуйста, выберите другое сочетание."
},
"to": {
"message": "до",
@@ -541,7 +547,7 @@
"message": "Эксклюзивный доступ"
},
"category_exclusive_access_description": {
"message": "Категория для всего видео. Используется когда видео демонстрирует продукт, сервис или местоположение, к которому автор получил бесплатный или проспонсированный доступ."
"message": "Категория для всего видео. Используется, когда видео демонстрирует продукт, сервис или местоположение, к которому автор получил бесплатный или проспонсированный доступ."
},
"category_exclusive_access_pill": {
"message": "Это видео демонстрирует продукт, сервис или местоположение, к которому автор получил бесплатный или проспонсированный доступ",
@@ -629,7 +635,7 @@
"message": "Показывать в полосе прокрутки"
},
"showOverlay_full": {
"message": "Отображать название"
"message": "Показывать категорию"
},
"autoSkipOnMusicVideos": {
"message": "Пропускать все сегменты автоматически при наличии сегмента без музыки"
@@ -762,7 +768,7 @@
"message": "Этот сегмент большой. Если в видео только одна тема, то измените значение с «Пропустить» на «Всё видео». Дополнительную информацию смотрите в руководстве."
},
"categoryPillTitleText": {
"message": "Это видео помечено как данная категория и слишком сильно интегрировано для разделения"
"message": "Данная категория применена для всего видео, так как она тесно интегрирована в видео"
},
"experiementOptOut": {
"message": "Отказаться от всех будущих экспериментов",
@@ -784,6 +790,9 @@
"hideDonationLink": {
"message": "Скрыть ссылку на пожертвование"
},
"darkModeOptionsPage": {
"message": "Тёмный режим на странице настроек"
},
"helpPageThanksForInstalling": {
"message": "Спасибо за установку SponsorBlock."
},
@@ -872,5 +881,42 @@
"hourAbbreviation": {
"message": "ч",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Поведение",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Интерфейс",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Сочетания клавиш",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "Бэкап/Восстановление",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Разное",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "Внешний вид уведомления о пропуске",
"description": "Option label"
},
"unbind": {
"message": "Отвязать",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Не задано"
},
"change": {
"message": "Изменить"
},
"youtubeKeybindWarning": {
"message": "Это сочетание используется на сайте YouTube. Вы уверены, что хотите его использовать?"
}
}

View File

@@ -262,21 +262,6 @@
"message": "Ak sa vám to stále nepáčí, stlačte tlačidlo Nikdy nezobrazovať.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Nastaviť kláves pre preskočenie segmentu"
},
"setStartSponsorShortcut": {
"message": "Nastaviť kláves pre začiatok/ukončenie segmentu"
},
"setSubmitKeybind": {
"message": "Nastaviť kláves pre odoslanie segmentu"
},
"keybindDescription": {
"message": "Stlačte požadovaný kláves"
},
"keybindDescriptionComplete": {
"message": "Kláves bol nastavený na: "
},
"0": {
"message": "Spojenie vypršalo. Skontrolujte svoje internetové pripojenie. Ak vám internet funguje, server je pravdepodobne preťažený alebo nefunkčný."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Vytvoril"
},
"keybindCurrentlySet": {
"message": ". Aktuálne je nastavené na:"
},
"supportOtherSites": {
"message": "Podpora alternatívnych Youtube webov"
},
@@ -440,6 +422,9 @@
"shortCheck": {
"message": "Segment je kratší ako vami nastavená minimálna dĺžka. Možno už bol niekým odoslaný a kvôli tomuto nastaveniu je teraz ignorovaný. Naozaj ho chcete odoslať?"
},
"liveOrPremiere": {
"message": "Vytváranie segmentov v aktívnom živom prenose alebo premiére nie je dovolené. Počkajte prosím na jeho ukončenie, potom obnovte stránku a skontrolujte, či sú segmenty správne vytvorené."
},
"showUploadButton": {
"message": "Ukázať Nahrávacie Tlačidlo"
},
@@ -515,12 +500,6 @@
"copyDebugInformationComplete": {
"message": "Debug informácie boli skopírované do schránky. Ak chcete, môžete odstrániť akékoľvek informácie, ktoré nechcete zdieľať. Uložte si ich do textového súboru alebo ich vložte do nahlásenia chyby."
},
"theKey": {
"message": "Kľúč"
},
"keyAlreadyUsed": {
"message": "je pridelený pre inú akciu. Prosím zvoľte inú klávesu."
},
"to": {
"message": "do",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -537,6 +516,16 @@
"category_selfpromo_description": {
"message": "Podobné ako sponzor, okrem neplatenej alebo vlastnej propagácie. Patria sem sekcie týkajúce sa merchu, donatov alebo informácií o tom, s kým spolupracovali."
},
"category_exclusive_access": {
"message": "Exkluzívny Prístup"
},
"category_exclusive_access_description": {
"message": "Iba pre označovanie celých videí. Používa sa, keď video predstavuje produkt, službu alebo miesto, ku ktorým získali bezplatný alebo dotovaný prístup."
},
"category_exclusive_access_pill": {
"message": "Toto video predstavuje produkt, službu alebo miesto, ku ktorým získali bezplatný alebo dotovaný prístup",
"description": "Short description for this category"
},
"category_interaction": {
"message": "Pripomienka interakcie (Prihlásiť sa na odber)"
},
@@ -618,6 +607,9 @@
"showOverlay_POI": {
"message": "Zobraziť v časovej lište"
},
"showOverlay_full": {
"message": "Ukázať Označenie"
},
"autoSkipOnMusicVideos": {
"message": "Automaticky preskočiť všetky segmenty ak neexistuje segment bez hudby"
},

View File

@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Ställ in knapp för att hoppa över ett segment"
"message": "Hoppa över segment",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Ange den genväg som ska fungera som start-/stopptangent för ett segment"
"message": "Starta/stoppa segment",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Ange den tangent som ska fungera som inskickningstangent"
"message": "Skicka in segment",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Ange tangent genom att trycka på den"
},
"keybindDescriptionComplete": {
"message": "Kopplad till: "
"message": "Välj en tangent genom att trycka på den och välj sedan en av modifieringstangenterna som du vill använda."
},
"0": {
"message": "Anslutningsfel. Se över din internetanslutning. Om du kan komma åt internet så är servern förmodligen överbelastad eller nere."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Skapad av"
},
"keybindCurrentlySet": {
"message": ". Nuvarande snabbtangent är:"
},
"supportOtherSites": {
"message": "Stöd för tredjeparts YouTube-webbplatser"
},
@@ -440,6 +437,9 @@
"shortCheck": {
"message": "Följande rapport är kortare än ditt minstavärde i inställningarna. Det skulle kunna betyda att det redan är rapporterat och bara ignorerat på grund av denna inställning. Är du säker på att du vill rapportera?"
},
"liveOrPremiere": {
"message": "Att skicka in på en aktiv liveström eller premiär är inte tillåtet. Vänta tills den är färdig, uppdatera sedan sidan och kontrollera att segmenten fortfarande är giltiga."
},
"showUploadButton": {
"message": "Visa uppladdningsknapp"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "Importera/Exportera alla alternativen"
},
"exportOptionsCopy": {
"message": "Redigera/kopiera"
},
"exportOptionsDownload": {
"message": "Spara till fil"
},
"exportOptionsUpload": {
"message": "Ladda från fil"
},
"whatExportOptions": {
"message": "Detta är alla dina alternativ i JSON-format. Det inkluderar ditt användar-ID, så var noga med hur du hanterar informationen."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "Debuginformationen har kopierats till urklipp. Ta bort eventuell information du inte vill dela med dig av. Spara informationen i en textfil eller klistra in den i en buggrapport."
},
"theKey": {
"message": "Nyckeln"
},
"keyAlreadyUsed": {
"message": "är kopplad till en annan funktion. Välj en annan knapp."
"message": "Denna genväg är bunden till en annan åtgärd. Du måste välja en annan."
},
"to": {
"message": "till",
@@ -784,6 +790,9 @@
"hideDonationLink": {
"message": "Dölj donationslänk"
},
"darkModeOptionsPage": {
"message": "Mörkt läge på Alternativsidan"
},
"helpPageThanksForInstalling": {
"message": "Tack för att du installerade SponsorBlock."
},
@@ -872,5 +881,38 @@
"hourAbbreviation": {
"message": "h",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Beteende",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Gränssnitt",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Tangentbordsgenvägar",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "Säkerhetskopiera/Återställ",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Diverse",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"unbind": {
"message": "Ta bort genväg",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Inte inställd"
},
"change": {
"message": "Ändra"
},
"youtubeKeybindWarning": {
"message": "Detta är en inbyggd YouTube-genväg. Är du säker på att du vill använda den?"
}
}

View File

@@ -262,21 +262,6 @@
"message": "உங்களுக்கு இன்னும் பிடிக்கவில்லை என்றால், ஒருபோதும் காண்பி பொத்தானை அழுத்தவும்.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "ஒரு பகுதியைத் தவிர்ப்பதற்கான விசையை அமைக்கவும்"
},
"setStartSponsorShortcut": {
"message": "துவக்க/நிறுத்த பிரிவு விசைப்பலகைக்கு விசையை அமைக்கவும்"
},
"setSubmitKeybind": {
"message": "சமர்ப்பிக்கும் விசைப்பலகைக்கு விசையை அமைக்கவும்"
},
"keybindDescription": {
"message": "ஒரு விசையைத் தட்டச்சு செய்வதன் மூலம் அதைத் தேர்ந்தெடுக்கவும்"
},
"keybindDescriptionComplete": {
"message": "விசைப்பலகை இதற்கு அமைக்கப்பட்டுள்ளது: "
},
"0": {
"message": "இணைப்பு நேரம் முடிந்தது. உங்கள் இணைய இணைப்பைச் சரிபார்க்கவும். உங்கள் இணையம் இயங்கினால், சேவையகம் அதிக சுமை அல்லது கீழே இருக்கும்."
},
@@ -384,9 +369,6 @@
"createdBy": {
"message": "உருவாக்கியது"
},
"keybindCurrentlySet": {
"message": ". இது தற்போது அமைக்கப்பட்டுள்ளது:"
},
"supportOtherSites": {
"message": "3 வது தரப்பு YouTube-தளங்களை ஆதரிக்கவும்"
},
@@ -511,12 +493,6 @@
"copyDebugInformationComplete": {
"message": "பிழைத்திருத்த தகவல்கள் கிளிப் போர்டில் நகலெடுக்கப்பட்டுள்ளன. நீங்கள் பகிர விரும்பாத எந்த தகவலையும் அகற்ற தயங்க. இதை உரை கோப்பில் சேமிக்கவும் அல்லது பிழை அறிக்கையில் ஒட்டவும்."
},
"theKey": {
"message": "சாவி"
},
"keyAlreadyUsed": {
"message": "மற்றொரு செயலுடன் பிணைக்கப்பட்டுள்ளது. மற்றொரு விசையைத் தேர்ந்தெடுக்கவும்."
},
"to": {
"message": "க்கு",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -226,18 +226,6 @@
"message": "మీకు ఇంకా నచ్చకపోతే, ఎప్పుడూ చూపించు బటన్ నొక్కండి.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "విభాగాన్ని దాటవేయడానికి కీని సెట్ చేయండి"
},
"setSubmitKeybind": {
"message": "సమర్పణ కీబైండ్ కోసం కీని సెట్ చేయండి"
},
"keybindDescription": {
"message": "కీని టైప్ చేయడం ద్వారా దాన్ని ఎంచుకోండి"
},
"keybindDescriptionComplete": {
"message": "కీబైండ్ దీనికి సెట్ చేయబడింది: "
},
"0": {
"message": "అనుసంధాన సమయం సమాప్తం. మీ ఇంటర్నెట్ కనెక్షన్‌ను తనిఖీ చేయండి. మీ ఇంటర్నెట్ పనిచేస్తుంటే, సర్వర్ ఓవర్‌లోడ్ లేదా డౌన్ అయి ఉండవచ్చు."
},
@@ -320,9 +308,6 @@
"createdBy": {
"message": "సృష్టికర్త"
},
"keybindCurrentlySet": {
"message": ". ఇది ప్రస్తుతం దీనికి సెట్ చేయబడింది:"
},
"optionsInfo": {
"message": "ఇన్విడియస్ మద్దతును ప్రారంభించండి, ఆటోస్కిప్‌ను డిసేబుల్ చేయండి, బటన్లను దాచు మరియు మరిన్ని చేయండి."
},
@@ -422,12 +407,6 @@
"copyDebugInformationComplete": {
"message": "డీబగ్ సమాచారం క్లిప్ బోర్డ్‌కు కాపీ చేయబడింది. మీరు భాగస్వామ్యం చేయని సమాచారాన్ని తొలగించడానికి సంకోచించకండి. దీన్ని టెక్స్ట్ ఫైల్‌లో సేవ్ చేయండి లేదా బగ్ రిపోర్ట్‌లో అతికించండి."
},
"theKey": {
"message": "కీ"
},
"keyAlreadyUsed": {
"message": "మరొక చర్యకు కట్టుబడి ఉంటుంది. దయచేసి మరొక కీని ఎంచుకోండి."
},
"to": {
"message": "కు",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -71,7 +71,7 @@
"message": "Sponsor sürelerini yollarken bir sorun oluştur, lütfen tekrar deneyin."
},
"sponsorFound": {
"message": "Bu videonun kısımları veri tabanımızda mevut"
"message": "Bu videonun kısımları veri tabanımızda mevcut"
},
"sponsor404": {
"message": "Kısım bulunamadı"
@@ -262,21 +262,6 @@
"message": "Eğer hala beğenmediyseniz, asla gösterme butonuna basın.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Kısım atlamak için bir tuş ata"
},
"setStartSponsorShortcut": {
"message": "Kısım başlatmak/durdurmak için kısayol ayarla"
},
"setSubmitKeybind": {
"message": "Gönderim için bir tuş belirleyin"
},
"keybindDescription": {
"message": "Yazarak bir tuş seçin"
},
"keybindDescriptionComplete": {
"message": "Tuş seçimi şu tuşa ayarlandı: "
},
"0": {
"message": "Bağlantı zaman aşımına uğradı. İnternet bağlantınızı kontrol ediniz. Eğer internetiniz çalışıyor ise, büyük ihtimalle sunucuya erişilemiyor veya sunucuya aşırı yüklenilmiş olabilir."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Oluşturan"
},
"keybindCurrentlySet": {
"message": ". Şu an buna ayarlı:"
},
"supportOtherSites": {
"message": "3. Taraf Youtube Sitelerini Destekle"
},
@@ -440,6 +422,9 @@
"shortCheck": {
"message": "Sıradaki öneri belirlediğiniz minimum süre ayarından daha kısa. Bu zaten yollandığı ve bu ayardan dolayı yok sayıldığı anlamına gelebilir. Göndermek istediğinizden emin misiniz?"
},
"liveOrPremiere": {
"message": "Bir canlı yayın veya ön gösterim sırasında kısım gönderilemez. Yayının bitmesini bekleyin, sonra sayfayı tazeleyip kısımların geçerli olduğunu kontrol edin."
},
"showUploadButton": {
"message": "Karşıya Yükleme Butonunu Göster"
},
@@ -515,12 +500,6 @@
"copyDebugInformationComplete": {
"message": "Bu çözüm bilgisi panoya kopyalandı. Paylaşmak istemediğiniz herhangi bir bilgiyi silmekte özgürsünüz. Bir yazı dosyası olarak kaydedin veya hata raporuna kopyalayın."
},
"theKey": {
"message": "Anahtar"
},
"keyAlreadyUsed": {
"message": "başka bir eyleme bağlı. Lütfen başka bir anahtar seçin."
},
"to": {
"message": "'e",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -537,6 +516,16 @@
"category_selfpromo_description": {
"message": "\"Sponsor\" seçeneğinden farkı para karşılığı olmaması veya kendi reklamını yapmasıdır. Buna kendi markalı ürünlerini satmak, bağış toplamak ve videoda işbirliği yaptığı kimselerden bahsetmek dahildir."
},
"category_exclusive_access": {
"message": "Özel Erişim"
},
"category_exclusive_access_description": {
"message": "Yalnızca bütün videoyu etiketlemek için kullanın. Video; ücretli veya ücretsiz elde edilen bir ürünün, hizmetin veya bir yerin reklamını yapıyorsa kullanılır."
},
"category_exclusive_access_pill": {
"message": "Bu video; yayıncıya özel ücretle veya ücretsiz sunulan bir ürünün, hizmetin veya bir yerin reklamını yapıyor",
"description": "Short description for this category"
},
"category_interaction": {
"message": "Etkileşim Hatırlatıcısı (Abonelik)"
},
@@ -618,6 +607,9 @@
"showOverlay_POI": {
"message": "Arama Çubuğunda Göster"
},
"showOverlay_full": {
"message": "Etiketi Göster"
},
"autoSkipOnMusicVideos": {
"message": "Müzik olmayan kısım varsa tüm kısımları otomatik atla"
},
@@ -850,7 +842,7 @@
"message": "Zaman aralığını hızlı bir şekilde ayarlamak için düzenleme kutusunun üzerinde fare tekerini kullanın. Değişikliklere ince ayar yapmak için ctrl veya shift tuşunun kombinasyonları kullanılabilir."
},
"categoryPillNewFeature": {
"message": "Yeni! Videonun bütünü sponsor veya kendi reklamıysa öngörün"
"message": "Yeni! Videonun bütünü sponsor veya kendi reklamıysa bu uyarıyı görün"
},
"dayAbbreviation": {
"message": "d",

View File

@@ -263,19 +263,19 @@
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Призначити гарячу клавішу для пропуску сегмента"
"message": "Пропустити сегмент",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": ризначити гарячу клавішу для початку/зупинки сегмента"
"message": "Почати/зупинити сегмент",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Призначити гарячу клавішу для надсилання"
"message": "Надіслати сегменти",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Натисніть, щоб вибрати її"
},
"keybindDescriptionComplete": {
"message": "Кнопка призначена на: "
"message": "Виберіть клавішу, набравши її та виберіть бажану клавішу-модифікатор."
},
"0": {
"message": "Таймаут підключення. Перевірте ваше з'єднання з інтернетом. Якщо ваш інтернет працює, сервер, швидше за все, перевантажений або лежить."
@@ -388,9 +388,6 @@
"createdBy": {
"message": "Створено"
},
"keybindCurrentlySet": {
"message": ". Він зараз призначений на:"
},
"supportOtherSites": {
"message": "Підтримувати сторонні YouTube-сайти"
},
@@ -440,6 +437,9 @@
"shortCheck": {
"message": "Наступний діапазон часу коротше, ніж Ваше налаштування мінімальної тривалості. Це може означати, що він вже був надісланий, і просто ігнорується через це налаштування. Ви дійсно хочете надіслати?"
},
"liveOrPremiere": {
"message": "Надсилання під час прямого ефіру чи прем'єри не дозволено. Будь ласка, зачекайте до завершення, потім оновіть сторінку і переконайтеся, що сегменти все ще дійсні."
},
"showUploadButton": {
"message": "Показувати кнопку надсилання"
},
@@ -467,6 +467,15 @@
"exportOptions": {
"message": "Імпорт/Експорт всіх налаштувань"
},
"exportOptionsCopy": {
"message": "Змінити/копіювати"
},
"exportOptionsDownload": {
"message": "Зберегти до файлу"
},
"exportOptionsUpload": {
"message": "Завантажити з файлу"
},
"whatExportOptions": {
"message": "Це вся конфігурація в форматі JSON. Цей файл містить Ваш ідентифікатор користувача, тому не забудьте ділитися з цим розумно."
},
@@ -515,11 +524,8 @@
"copyDebugInformationComplete": {
"message": "Інформація про налагодження скопійована в буфер обміну. Ви можете видалити будь-яку інформацію, якої не хочете ділитися. Збережіть її в текстовий файл, вставте в звіт про помилку."
},
"theKey": {
"message": "Ключ"
},
"keyAlreadyUsed": {
"message": "прив'язана до іншого дії. Будь ласка, оберіть іншу клавішу."
"message": "Ця клавіша/комбінація клавіш прив'язана до іншої дії. Будь ласка виберіть іншу клавішу."
},
"to": {
"message": "до",
@@ -784,6 +790,9 @@
"hideDonationLink": {
"message": "Приховати посилання на пожертвування"
},
"darkModeOptionsPage": {
"message": "Темний режим на сторінці налаштувань"
},
"helpPageThanksForInstalling": {
"message": "Дякую за встановлення SponsorBlock."
},
@@ -872,5 +881,42 @@
"hourAbbreviation": {
"message": "г",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Поведінка",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "Інтерфейс",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "Комбінації клавіш",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "Резервування/Відновлення",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "Різне",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "Вигляд сповіщення про пропуск",
"description": "Option label"
},
"unbind": {
"message": "Відв'язати",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Не задано"
},
"change": {
"message": "Змінити"
},
"youtubeKeybindWarning": {
"message": "Це вбудована комбінація YouTube. Ви впевнені що хочете її використовувати?"
}
}

View File

@@ -262,21 +262,6 @@
"message": "Nếu bạn vẫn không thích, hãy nhấn nút không hiển thị lại.",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "Chọn phím tắt để bỏ qua phân đoạn"
},
"setStartSponsorShortcut": {
"message": "Đặt phím nóng đánh dấu một phân đoạn bắt đầu/kết thúc"
},
"setSubmitKeybind": {
"message": "Đặt phím tắt đăng đoạn quảng cáo"
},
"keybindDescription": {
"message": "Chọn phím bằng cách gõ phím"
},
"keybindDescriptionComplete": {
"message": "Phím tắt đã được đặt thành: "
},
"0": {
"message": "Kết nối quá hạn thời gian. Hãy kiểm tra đường truyền mạng của bạn. Nếu mạng của bạn vẫn hoạt động, có thể máy chủ đang bị quá tải hoặc không hoạt động."
},
@@ -388,9 +373,6 @@
"createdBy": {
"message": "Được tạo bởi"
},
"keybindCurrentlySet": {
"message": ". Phím đang được đặt:"
},
"supportOtherSites": {
"message": "Hỗ trợ các trang web Youtube thuộc bên thứ 3"
},
@@ -515,12 +497,6 @@
"copyDebugInformationComplete": {
"message": "Thông tin gỡ lỗi đã được sao chép vào bộ nhớ tạm. Hãy thoải mái xóa thông tin mà bạn không muốn chia sẻ. Lưu thông tin này vào một tập tin văn bản hoặc dán nó vào bản báo cáo lỗi."
},
"theKey": {
"message": "Phím"
},
"keyAlreadyUsed": {
"message": "đang được đặt thực hiện hành động khác. Xin hãy chọn phím khác."
},
"to": {
"message": "đến",
"description": "Used between segments. Example: 1:20 to 1:30"
@@ -537,6 +513,9 @@
"category_selfpromo_description": {
"message": "Tương tự như 'nhà tài trợ' ngoại trừ việc quảng cáo không được trả tiền hay tự quảng cáo. Điều này bao gồm các phần hàng hóa, đóng góp, hoặc thông tin về người mà họ hợp tác cùng."
},
"category_exclusive_access": {
"message": "Truy cập riêng"
},
"category_interaction": {
"message": "Nhắc tương tác (Đăng ký)"
},

View File

@@ -229,18 +229,6 @@
"message": "如果您依然不喜欢它,请按下不再显示按钮。",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "设置用于跳过片段的按键"
},
"setSubmitKeybind": {
"message": "设定提交的绑定按键"
},
"keybindDescription": {
"message": "点击您想选择的按键"
},
"keybindDescriptionComplete": {
"message": "绑定按键已被设定为: "
},
"0": {
"message": "连接超时。请检查您的网络连接。如果您的网络运行正常,则可能是服务器过载或宕机。"
},
@@ -323,9 +311,6 @@
"createdBy": {
"message": "创建者"
},
"keybindCurrentlySet": {
"message": "。目前被设定为:"
},
"supportOtherSites": {
"message": "支持第三方 YouTube 网站"
},
@@ -428,12 +413,6 @@
"copyDebugInformationComplete": {
"message": "调试信息已复制到剪切板中。 您可以随意移除任何您不想分享的信息。请将其另存为 .txt 文件或粘贴到错误报告中。"
},
"theKey": {
"message": "按键"
},
"keyAlreadyUsed": {
"message": "已绑定其他操作。请选择其他按键。"
},
"to": {
"message": "到",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -241,21 +241,6 @@
"message": "如果您還是不喜歡它,請按下永不顯示按鈕",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "設定跳過段落的快捷鍵"
},
"setStartSponsorShortcut": {
"message": "設定開始/結束片段快捷鍵"
},
"setSubmitKeybind": {
"message": "設定提交快捷鍵"
},
"keybindDescription": {
"message": "按下您想選擇的按鍵"
},
"keybindDescriptionComplete": {
"message": "快捷鍵已設定為: "
},
"0": {
"message": "連線超時。請檢查您的網路連線。若您的網路運作正常,則可能是伺服器超載或離線"
},
@@ -342,9 +327,6 @@
"createdBy": {
"message": "作者"
},
"keybindCurrentlySet": {
"message": "。它目前被設定為:"
},
"supportOtherSites": {
"message": "支援第三方的 YouTube 網站"
},
@@ -466,12 +448,6 @@
"copyDebugInformationComplete": {
"message": "除錯資訊已複製到剪貼板中。您可以任意移除任何您不想分享的資訊。請將其另存為文字文件或貼到錯誤報告中。"
},
"theKey": {
"message": "按鍵"
},
"keyAlreadyUsed": {
"message": "已經綁定其它動作。請選擇其他按鍵"
},
"to": {
"message": "到",
"description": "Used between segments. Example: 1:20 to 1:30"

View File

@@ -594,7 +594,7 @@ input::-webkit-inner-spin-button {
color: white;
}
.sponsorBlockTooltip::after {
.sponsorBlockTooltip.sbTriangle::after {
content: " ";
position: absolute;
top: 100%;
@@ -628,6 +628,7 @@ input::-webkit-inner-spin-button {
font-size: 75%;
height: 100%;
align-items: center;
inline-size: max-content;
}
.sponsorBlockCategoryPillTitleSection {

View File

@@ -4,6 +4,7 @@
<title> SponsorBlock </title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="../icons/IconSponsorBlocker32px.png" type="image/png">
<link href="styles.css" rel="stylesheet"/>

View File

@@ -1,5 +1,31 @@
:root {
--color-scheme: dark;
--background: #333333;
--header-color: #212121;
--dialog-background: #181818;
--dialog-border: white;
--text: #c4c4c4;
--title: #dad8d8;
--disabled: #520000;
--black: black;
--white: white;
}
[data-theme="light"] {
--color-scheme: light;
--background: #f9f9f9;
--header-color: white;
--dialog-background: #f9f9f9;
--dialog-border: #282828;
--text: #262626;
--title: #707070;
--disabled: #ffcaca;
--black: white;
--white: black;
}
html {
color-scheme: dark;
color-scheme: var(--color-scheme);
}
.bigText {
@@ -7,7 +33,7 @@ html {
}
body {
background-color: #333333;
background-color: var(--background);
font-family: sans-serif;
}
@@ -15,6 +41,10 @@ body {
text-align: center;
}
.inline {
display: inline-block;
}
.container {
max-width: 60%;
margin: auto;
@@ -54,12 +84,14 @@ body {
vertical-align: middle;
font-size: 50px;
color: #212121;
color: var(--header-color);
padding: 20px;
text-decoration: none;
border-radius: 15px;
transition: font-size 1s;
}
@@ -125,8 +157,8 @@ p,li {
font-size: 16px;
}
p,li,a {
color: #c4c4c4;
p,li,a,span,div {
color: var(--text);
}
p,li,code,a {
@@ -160,7 +192,7 @@ img {
}
h1,h2,h3,h4,h5,h6 {
color: #dad8d8;
color: var(--title);
text-align: center;
}
@@ -200,3 +232,94 @@ svg {
text-align: center;
}
}
/* keybind dialog */
.key {
border-width: 1px;
border-style: solid;
border-radius: 5px;
display: inline-block;
min-width: 33px;
text-align: center;
font-weight: bold;
border-color: var(--white);
box-sizing: border-box;
}
.unbound, .key {
padding: 8px;
}
#keybind-dialog .dialog {
position: fixed;
border-width: 3px;
border-style: solid;
border-radius: 15px;
max-height: 100vh;
width: 400px;
overflow-x: auto;
z-index: 100;
padding: 15px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
background-color: var(--dialog-background);
border-color: var(--dialog-border);
}
#change-keybind-buttons {
float: right;
}
#change-keybind-buttons > .option-button {
margin: 0 2px;
}
#change-keybind-settings {
margin: 15px 15px 30px;
}
#change-keybind-settings .key {
vertical-align: top;
margin: 15px 0 0 40px;
height: 34px;
}
#change-keybind-error {
margin-bottom: 15px;
color: red;
font-weight: bold;
}
.blocker {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 90;
background-color: #00000080;
}
.option-button {
cursor: pointer;
background-color: #c00000;
padding: 10px;
color: white;
border-radius: 5px;
font-size: 14px;
width: max-content;
}
.option-button:hover:not(.disabled) {
background-color: #fc0303;
}
.option-button.disabled {
cursor: default;
background-color: var(--disabled);
color: grey;
}

43
public/icons/heart.svg Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg6"
sodipodi:docname="heart.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="32.916667"
inkscape:cx="11.98481"
inkscape:cy="12.01519"
inkscape:window-width="1366"
inkscape:window-height="731"
inkscape:window-x="1366"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0V0z"
fill="none"
id="path2" />
<path
d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z"
id="path4"
style="fill:#800000" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg6"
sodipodi:docname="not_visible.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<sodipodi:namedview
id="namedview8"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="16.458334"
inkscape:cx="3.8582278"
inkscape:cy="9.1443037"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="426"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"
id="path4"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

1
public/icons/visible.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -1,10 +1,232 @@
/* Options page CSS */
html {
color-scheme: dark;
:root {
--color-scheme: dark;
--background: #333333;
--menu-background: #181818;
--menu-foreground: white;
--dialog-background: #181818;
--dialog-border: white;
--tab-color: #242424;
--tab-button-hover: #4d0000;
--tab-hover: white;
--description: #dfdfdf;
--disabled: #520000;
--slider: #707070;
--title: #dad8d8;
--border-color: #484848;
--black: black;
--white: white;
}
body {
[data-theme="light"] {
--color-scheme: light;
--background: #f9f9f9;
--menu-background: #dbdbdb;
--menu-foreground: #212121;
--dialog-background: #f9f9f9;
--dialog-border: #282828;
--tab-color: #ababab;
--tab-button-hover: #750000;
--tab-hover: #2e2e2e;
--description: #262626;
--disabled: #ffcaca;
--slider: #bfbebe;
--title: #707070;
--border-color: #d9d9d9;
--black: white;
--white: black;
}
.medium-description, .switch-container, .optionLabel, .categoryTableElement {
color: var(--white);
}
.small-description, p, li, span, div {
color: var(--description);
}
h1,h2,h3,h4,h5,h6 {
color: var(--title);
}
html, body {
color-scheme: var(--color-scheme);
font-family: sans-serif;
margin: 0;
font-size: 14px;
background-color: var(--background);
}
* {
box-sizing: border-box;
}
#options-container {
display: flex;
}
#menubar {
display: flex;
flex-direction: column;
gap: 20px;
flex-basis: 20%;
min-width: 300px;
max-width: 600px;
border-radius: 15px;
margin: 15px;
z-index: 10;
background-color: var(--menu-background);
color: var(--menu-foreground);
}
#navigation {
display: flex;
flex-direction: column;
gap: 30px;
}
.tab-heading {
font-size: 18px;
height: 55px;
line-height: 55px;
width: 80%;
margin: 0 auto;
border-radius: 15px;
cursor: pointer;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background-color: var(--tab-color);
color: var(--white);
}
.tab-heading:hover {
background-color: var(--tab-button-hover);
color: white;
}
.tab-heading.selected {
background-color: #c00000;
color: white;
}
.tab-heading:active {
background-color: #950000;
color: white;
}
.option-group > div {
min-height: 50px;
padding: 20px 0;
border-bottom: 1px solid var(--border-color);
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
}
.option-group > div:last-child, .option-group > #keybind-dialog {
border-bottom: inherit;
}
.optionLabel, #version {
font-size: 14px;
height: 15px;
}
div[data-type="keybind-change"] .optionLabel {
display: inline-block;
min-width: 150px;
margin-right: 20px;
}
input[type='number'] {
width: 50px;
}
.key {
border-width: 1px;
border-style: solid;
border-radius: 5px;
display: inline-block;
min-width: 33px;
text-align: center;
font-weight: bold;
border-color: var(--white);
}
.unbound, .key {
padding: 8px;
}
.keybind-buttons {
border-radius: 5px;
padding: 5px 3px;
cursor: pointer;
margin-right: 10px;
}
.keybind-buttons:hover {
background-color: #00000030;
}
.keybind-buttons > div, .keybind-buttons > span {
margin: 0 2px;
}
#keybind-dialog .dialog {
position: fixed;
border-width: 3px;
border-style: solid;
border-radius: 15px;
max-height: 100vh;
width: 400px;
overflow-x: auto;
z-index: 100;
padding: 15px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: var(--dialog-background);
border-color: var(--dialog-border);
}
#change-keybind-buttons {
float: right;
}
#change-keybind-buttons > .option-button {
margin: 0 2px;
}
#change-keybind-settings {
margin: 15px 15px 30px;
}
#change-keybind-settings .key {
vertical-align: top;
margin: 15px 0 0 40px;
height: 34px;
}
#change-keybind-error {
margin-bottom: 15px;
color: red;
font-weight: bold;
}
.blocker {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 90;
background-color: #00000080;
}
.low-profile {
height: 23px;
line-height: 5px;
vertical-align: middle;
}
.center {
@@ -15,25 +237,49 @@ body {
display: inline-block;
}
.next-line {
padding: 15px 0 0 0;
}
.bold {
font-weight: bold;
}
.hiding {
opacity: 0;
}
.hidden {
display: none !important;
}
.spacing {
margin-top: 15px;
}
.keybind-status {
display: inline;
}
.small-description {
color: white;
font-size: 13px;
padding: 15px 0 0 20px;
}
.small-description td {
padding: 10px 0 20px 20px;
}
.indent {
padding-left: 20px;
}
.categoryTableElement td {
padding-top: 10px;
border-top: 1px solid var(--border-color);
}
.medium-description {
color: white;
font-size: 15px;
}
@@ -53,36 +299,42 @@ body {
width: max-content;
}
.option-button:hover {
.option-button:hover:not(.disabled) {
background-color: #fc0303;
}
.option-button.disabled {
cursor: default;
background-color: #520000;
background-color: var(--disabled);
color: grey;
}
#options {
max-width: 60%;
height: 100vh;
flex-basis: 80%;
overflow: auto;
text-align: left;
display: inline-block;
padding: 80px 15% 0 3%;
box-sizing: border-box;
display: flex;
justify-content: center;
transition: padding 0.3s;
}
#options.embed {
#options.embed > div {
max-width: 100%;
text-align: left;
display: inline-block;
}
#title .profilepic {
height: 60px;
}
.switch-container {
content: attr(label-name);
position: absolute;
width: max-content;
font-size: 14px;
color: white;
display: table;
}
@@ -94,11 +346,6 @@ body {
padding: 4px;
}
.text-label-container {
font-size: 14px;
color: white;
}
.switch {
position: relative;
display: inline-block;
@@ -119,7 +366,7 @@ body {
left: 0;
right: 0;
bottom: 0;
background-color: #707070;
background-color: var(--slider);
}
.animated * {
@@ -162,11 +409,8 @@ input:checked + .slider:before {
}
/* Boilerplate CSS from https://ajay.app */
body {
background-color: #333333;
}
/* Boilerplate CSS from https://ajay.app (edited) */
.projectPreview {
position: relative;
@@ -196,29 +440,25 @@ body {
transform: translateY(-50%);
}
.createdBy {
font-size: 14px;
#createdBy {
text-align: center;
padding-top: 0px;
padding-bottom: 0px;
margin: auto 0 10px 0;
height: 50px;
}
display: inline-block;
#createdBy > * {
font-size: 14px;
}
#title {
background-color: #636363;
text-align: center;
vertical-align: middle;
font-size: 50px;
color: #212121;
font-size: 40px;
padding: 20px;
padding: 40px 20px;
text-decoration: none;
transition: font-size 1s;
}
.subtitle {
@@ -237,7 +477,6 @@ body {
}
.profilepic {
background-color: #636363 !important;
vertical-align: middle;
}
@@ -281,21 +520,9 @@ a {
p,li {
font-size: 20px;
color: #c4c4c4;
padding: 10px;
}
@media screen and (orientation:portrait) {
#options {
max-width: 100%;
}
.previewColorOption {
display: none;
}
}
.previewImage {
max-height: 200px;
}
@@ -316,10 +543,6 @@ img {
color: #dad8d8;
}
h1,h2,h3,h4,h5,h6 {
color: #dad8d8;
}
svg {
text-decoration: none;
}
@@ -337,8 +560,6 @@ svg {
.categoryTableElement {
font-size: 16px;
color: white;
}
.categoryTableElement > * {
@@ -369,3 +590,82 @@ svg {
#sbDonate {
font-size: 10px;
}
/* Top bar navigation for smaller screens */
@media only screen and (max-height: 650px), only screen and (max-width: 1200px) {
#options-container {
flex-direction: column;
}
#menubar {
gap: 8px;
min-width: unset;
max-width: unset;
padding: 8px;
}
#navigation {
gap: 8px;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
#options {
padding: 0 50px;
}
#options > div {
max-width: 70%;
}
.tab-heading {
width: unset;
min-width: unset;
height: 35px;
line-height: 35px;
font-size: 16px;
padding: 0 10px;
margin: 0;
}
#title {
width: 100%;
font-size: 30px;
padding: 10px;
}
#title .profilepic {
height: 40px;
}
#createdBy {
margin: 10px 0 0 0;
height: unset;
width: 100%;
}
#createdBy > div {
display: inline-block;
}
#sbDonate {
position: absolute;
right: 30px;
margin-top: 10px;
}
#version {
font-size: 10px;
height: 10px;
transform: translate(-50px, -5px);
}
.sticky #menubar {
position: fixed;
left: 0;
right: 0;
margin: 0 15px;
}
.sticky #title, .sticky #createdBy {
display: none;
}
}
@media only screen and (max-width: 800px) {
#options {
padding: 0 15px;
justify-content: left;
}
}

View File

@@ -1,9 +1,11 @@
<!DOCTYPE html>
<!-- Link to specific tabs by using their ID in the URL like: options.html#keybinds -->
<head>
<title>Options - SponsorBlock</title>
<title>__MSG_Options__ - SponsorBlock</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="../icons/IconSponsorBlocker32px.png" type="image/png">
<link href="options.css" rel="stylesheet"/>
@@ -13,247 +15,159 @@
<body class="sponsorBlockPageBody">
<div id="options-container">
<div id="menubar" class="center">
<div id="title" class="titleBar">
<img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic"/>
<img src="../icons/LogoSponsorBlocker256px.png" class="profilepic" alt="SponsorBlock logo"/>
SponsorBlock
<div id="version"></div>
</div>
<div class="center">
<p class="createdBy titleBar">
<img src="../icons/newprofilepic.jpg" height="30" class="profilepiccircle"/>
<div id="navigation">
<div class="tab-heading" data-for="behavior">
__MSG_optionsTabBehavior__
</div>
<div class="tab-heading" data-for="interface">
__MSG_optionsTabInterface__
</div>
<div class="tab-heading" data-for="keybinds">
__MSG_optionsTabKeyBinds__
</div>
<div class="tab-heading" data-for="import">
__MSG_optionsTabBackup__
</div>
<div class="tab-heading" data-for="advanced">
__MSG_optionsTabAdvanced__
</div>
</div>
<div id="createdBy" class="titleBar">
<div>
<img src="../icons/newprofilepic.jpg" height="30" class="profilepiccircle" alt="profile picture of creator"/>
__MSG_createdBy__
<a href="https://ajay.app">Ajay Ramachandran</a>
</div>
<a href="https://sponsor.ajay.app/donate" target="_blank" rel="noopener" id="sbDonate">(__MSG_Donate__)</a>
</p>
<h1>__MSG_Options__</h1>
<div id="options" class="hidden">
<div id="category-type" option-type="react-CategoryChooserComponent">
</div>
</div>
<div option-type="toggle" sync-option="autoSkipOnMusicVideos">
<label class="switch-container">
<div id="options">
<div id="behavior" class="option-group hidden">
<div id="category-type" data-type="react-CategoryChooserComponent">
</div>
<div data-type="toggle" data-sync="autoSkipOnMusicVideos">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="autoSkipOnMusicVideos" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="autoSkipOnMusicVideos">
__MSG_autoSkipOnMusicVideos__
</div>
</label>
<br/>
<br/>
<br/>
</div>
</div>
<div option-type="toggle" sync-option="muteSegments">
<label class="switch-container">
<div data-type="toggle" data-sync="muteSegments">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="muteSegments" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="muteSegments">
__MSG_muteSegments__
</div>
</label>
<br/>
<br/>
<br/>
</div>
</div>
<div option-type="toggle" sync-option="fullVideoSegments">
<label class="switch-container">
<div option-type="toggle" data-sync="fullVideoSegments">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="fullVideoSegments" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="fullVideoSegments">
__MSG_fullVideoSegments__
</div>
</label>
<br/>
<br/>
<br/>
</div>
<br/>
<br/>
<div id="support-invidious" option-type="toggle" sync-option="supportInvidious" no-safari="true">
<label class="switch-container">
<label class="switch">
<input type="checkbox">
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_supportOtherSites__
</div>
</label>
<br/>
<br/>
<br/>
<div class="small-description">(__MSG_supportedSites__ Invidious, CloudTube)</div>
<br/>
<span class="small-description">__MSG_supportOtherSitesDescription__ </span>
<br/>
<br/>
<br/>
</div>
<div option-type="private-text-change" sync-option="invidiousInstances" no-safari="true">
<div class="option-button trigger-button">
__MSG_addInvidiousInstance__
</div>
<br/>
<div class="small-description">__MSG_addInvidiousInstanceDescription__</div>
<div class="option-hidden-section hidden">
<br/>
<input class="option-text-box" type="text">
<br/>
<br/>
<div class="option-button text-change-set inline">
__MSG_add__
</div>
<div class="option-button invidious-instance-reset inline">
__MSG_resetInvidiousInstance__
</div>
<br/>
<br/>
<span class="small-description">__MSG_currentInstances__</span>
<span class="small-description" option-type="display" sync-option="invidiousInstances"></span>
</div>
<br/>
<br/>
</div>
<div option-type="keybind-change" sync-option="skipKeybind">
<div class="option-button trigger-button">
__MSG_setSkipShortcut__
</div>
<div class="option-hidden-section hidden">
<br/>
<div class="medium-description keybind-status">
__MSG_keybindDescription__
</div>
<span class="medium-description bold keybind-status-key">
</span>
</div>
</div>
<br/>
<br/>
<div option-type="keybind-change" sync-option="startSponsorKeybind">
<div class="option-button trigger-button">
__MSG_setStartSponsorShortcut__
</div>
<div class="option-hidden-section hidden">
<br/>
<div class="medium-description keybind-status">
__MSG_keybindDescription__
</div>
<span class="medium-description bold keybind-status-key">
</span>
</div>
</div>
<br/>
<br/>
<div option-type="keybind-change" sync-option="submitKeybind">
<div class="option-button trigger-button">
__MSG_setSubmitKeybind__
</div>
<div class="option-hidden-section hidden">
<br/>
<div class="medium-description keybind-status">
__MSG_keybindDescription__
</div>
<span class="medium-description bold keybind-status-key">
</span>
</div>
</div>
<br/>
<br/>
<div option-type="number-change" sync-option="skipNoticeDuration">
<label class="number-container">
<input type="number" step="1" min="1">
</label>
<br/>
<br/>
<div class="small-description">__MSG_skipNoticeDurationDescription__</div>
</div>
<br/>
<br/>
<div option-type="number-change" sync-option="minDuration">
<div data-type="number-change" data-sync="minDuration">
<label class="number-container">
<span class="optionLabel">__MSG_minDuration__</span>
<input type="number" step="0.1" min="0">
</label>
<br/>
<br/>
<div class="small-description">__MSG_minDurationDescription__</div>
</div>
<br/>
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="dontShowNotice">
<label class="switch-container">
<div data-type="toggle" data-sync="forceChannelCheck">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="forceChannelCheck" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_showSkipNotice__
</div>
<label class="switch-label" for="forceChannelCheck">
__MSG_forceChannelCheck__
</label>
</div>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_whatForceChannelCheck__</div>
</div>
<div option-type="selector" sync-option="noticeVisibilityMode">
<select class="selector-element optionsSelector" >
<div data-type="toggle" data-sync="refetchWhenNotFound">
<div class="switch-container">
<label class="switch">
<input id="refetchWhenNotFound" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="refetchWhenNotFound">
__MSG_enableRefetchWhenNotFound__
</label>
</div>
<div class="small-description">__MSG_whatRefetchWhenNotFound__</div>
</div>
</div>
<div id="interface" class="option-group hidden">
<div data-type="number-change" data-sync="skipNoticeDuration">
<label class="number-container">
<span class="optionLabel">__MSG_skipNoticeDuration__</span>
<input type="number" step="1" min="1">
</label>
<div class="small-description">__MSG_skipNoticeDurationDescription__</div>
</div>
<div data-type="toggle" data-toggle-type="reverse" data-sync="dontShowNotice">
<div class="switch-container">
<label class="switch">
<input id="dontShowNotice" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="dontShowNotice">
__MSG_showSkipNotice__
</label>
</div>
</div>
<div data-type="selector" data-sync="noticeVisibilityMode">
<label class="optionLabel" for="noticeVisibilityMode">__MSG_noticeVisibilityLabel__:</label>
<select id="noticeVisibilityMode" class="selector-element optionsSelector" >
<option value="0">__MSG_noticeVisibilityMode0__</option>
<option value="1">__MSG_noticeVisibilityMode1__</option>
<option value="2">__MSG_noticeVisibilityMode2__</option>
@@ -262,292 +176,191 @@
</select>
</div>
<br/>
<br/>
<div option-type="toggle" sync-option="forceChannelCheck">
<label class="switch-container">
<div data-type="toggle" data-toggle-type="reverse" data-sync="hideVideoPlayerControls">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="hideVideoPlayerControls" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_forceChannelCheck__
</div>
</label>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_whatForceChannelCheck__</div>
</div>
<br/>
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="hideVideoPlayerControls">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="hideVideoPlayerControls">
__MSG_showButtons__
</div>
</label>
<br/>
<br/>
<br/>
</div>
<div class="small-description">__MSG_hideButtonsDescription__</div>
</div>
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="hideSkipButtonPlayerControls">
<label class="switch-container">
<div data-type="toggle" data-toggle-type="reverse" data-sync="hideDeleteButtonPlayerControls" data-dependent-on="hideVideoPlayerControls">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="hideDeleteButtonPlayerControls" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_showSkipButton__
</div>
</label>
</div>
<br/>
<br/>
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="hideInfoButtonPlayerControls">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_showInfoButton__
</div>
</label>
</div>
<br/>
<br/>
<br/>
<div option-type="toggle" sync-option="autoHideInfoButton" if-false="hideInfoButtonPlayerControls">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_autoHideInfoButton__
</div>
</label>
</div>
<br/>
<br/>
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="hideDeleteButtonPlayerControls">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="hideDeleteButtonPlayerControls">
__MSG_showDeleteButton__
</div>
</label>
</div>
</div>
<br/>
<br/>
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="hideUploadButtonPlayerControls">
<label class="switch-container">
<div data-type="toggle" data-toggle-type="reverse" data-sync="hideUploadButtonPlayerControls" data-dependent-on="hideVideoPlayerControls">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="hideUploadButtonPlayerControls" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="hideUploadButtonPlayerControls">
__MSG_showUploadButton__
</div>
</label>
</div>
</div>
<br/>
<br/>
<br/>
<br/>
<div option-type="toggle" sync-option="audioNotificationOnSkip">
<label class="switch-container">
<div data-type="toggle" data-toggle-type="reverse" data-sync="hideSkipButtonPlayerControls">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="hideSkipButtonPlayerControls" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_audioNotification__
</div>
<label class="switch-label" for="hideSkipButtonPlayerControls">
__MSG_showSkipButton__
</label>
</div>
</div>
<br/>
<br/>
<br/>
<div data-type="toggle" data-toggle-type="reverse" data-sync="hideInfoButtonPlayerControls">
<div class="switch-container">
<label class="switch">
<input id="hideInfoButtonPlayerControls" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="hideInfoButtonPlayerControls">
__MSG_showInfoButton__
</label>
</div>
</div>
<div data-type="toggle" data-sync="autoHideInfoButton" data-dependent-on="hideInfoButtonPlayerControls">
<div class="switch-container">
<label class="switch">
<input id="autoHideInfoButton" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="autoHideInfoButton">
__MSG_autoHideInfoButton__
</label>
</div>
</div>
<div data-type="toggle" data-sync="audioNotificationOnSkip">
<div class="switch-container">
<label class="switch">
<input id="audioNotificationOnSkip" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="audioNotificationOnSkip">
__MSG_audioNotification__
</label>
</div>
<div class="small-description">__MSG_audioNotificationDescription__</div>
</div>
<br/>
<br/>
<div option-type="toggle" sync-option="showTimeWithSkips">
<label class="switch-container">
<div data-type="toggle" data-sync="showTimeWithSkips">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="showTimeWithSkips" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="showTimeWithSkips">
__MSG_showTimeWithSkips__
</div>
</label>
<br/>
<br/>
<br/>
</div>
<div class="small-description">__MSG_showTimeWithSkipsDescription__</div>
</div>
<br/>
<br/>
<div option-type="toggle" sync-option="trackViewCount">
<label class="switch-container">
<div data-type="toggle" data-sync="darkMode">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="darkMode" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_enableViewTracking__
</div>
<label class="switch-label" for="darkMode">
__MSG_darkModeOptionsPage__
</label>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_whatViewTracking__</div>
</div>
</div>
<br/>
<br/>
<div option-type="toggle" sync-option="trackViewCountInPrivate" private-mode-only="true">
<label class="switch-container">
<div data-type="toggle" data-toggle-type="reverse" data-sync="showDonationLink" data-no-safari="true">
<div class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<input id="showDonationLink" type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_enableViewTrackingInPrivate__
</div>
</label>
<br/>
<br/>
<br/>
<br/>
</div>
<div option-type="toggle" sync-option="refetchWhenNotFound">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
__MSG_enableRefetchWhenNotFound__
</div>
</label>
<br/>
<br/>
<br/>
<div class="small-description">__MSG_whatRefetchWhenNotFound__</div>
</div>
<br/>
<br/>
<div option-type="toggle" toggle-type="reverse" sync-option="showDonationLink" no-safari="true">
<label class="switch-container">
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="showDonationLink">
__MSG_hideDonationLink__
</div>
</label>
</div>
</div>
</div>
<br/>
<br/>
<br/>
<br/>
<div id="keybinds" class="option-group hidden">
<div option-type="private-text-change" sync-option="userID" confirm-message="userIDChangeWarning">
<div data-type="keybind-change" data-sync="skipKeybind">
<label class="optionLabel">__MSG_setSkipShortcut__:</label>
<div class="inline"></div>
</div>
<div data-type="keybind-change" data-sync="startSponsorKeybind">
<label class="optionLabel">__MSG_setStartSponsorShortcut__:</label>
<div class="inline"></div>
</div>
<div data-type="keybind-change" data-sync="submitKeybind">
<label class="optionLabel">__MSG_setSubmitKeybind__:</label>
<div class="inline"></div>
</div>
</div>
<div id="import" class="option-group hidden">
<div data-type="private-text-change" data-sync="userID" data-confirm-message="userIDChangeWarning">
<div class="option-button trigger-button">
__MSG_changeUserID__
</div>
<br/>
<div class="small-description">__MSG_whatChangeUserID__</div>
<div class="option-hidden-section hidden">
<br/>
<div class="option-hidden-section hidden spacing indent">
<input class="option-text-box" type="text">
<br/>
<br/>
<div class="option-button text-change-set">
<div class="option-button text-change-set inline low-profile">
__MSG_setUserID__
</div>
</div>
</div>
<br/>
<br/>
<div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
<h2>__MSG_exportOptions__</h2>
<div option-type="private-text-change" sync-option="*" confirm-message="exportOptionsWarning">
<div class="option-button trigger-button">
__MSG_exportOptions__
<div>
<div class="option-button trigger-button inline">
__MSG_exportOptionsCopy__
</div>
<div class="option-button download-button inline">
__MSG_exportOptionsDownload__
</div>
<label for="importOptions" class="option-button inline">
__MSG_exportOptionsUpload__
</label>
<input id="importOptions" type="file" class="upload-button hidden" />
</div>
<br/>
<div class="small-description">__MSG_whatExportOptions__</div>
<div class="option-hidden-section hidden">
<br/>
<input class="option-text-box" type="text">
<br/>
<br/>
<div class="option-hidden-section hidden spacing indent">
<textarea class="option-text-box" rows="10" style="width:80%"></textarea>
<div class="option-button text-change-set">
__MSG_setOptions__
@@ -555,51 +368,111 @@
</div>
</div>
<br/>
<br/>
</div>
<div option-type="button-press" sync-option="copyDebugInformation" confirm-message="copyDebugInformation">
<div id="advanced" class="option-group hidden">
<div id="support-invidious" data-type="toggle" data-sync="supportInvidious" data-no-safari="true">
<div class="switch-container">
<label class="switch">
<input id="supportInvidious" type="checkbox">
<span class="slider round"></span>
</label>
<label class="switch-label" for="supportInvidious">
__MSG_supportOtherSites__
</label>
</div>
<div class="small-description">(__MSG_supportedSites__ Invidious, CloudTube)</div>
<div class="small-description">__MSG_supportOtherSitesDescription__ </div>
</div>
<div data-type="private-text-change" data-sync="invidiousInstances" data-no-safari="true" data-dependent-on="supportInvidious">
<div class="option-button trigger-button">
__MSG_addInvidiousInstance__
</div>
<div class="small-description">__MSG_addInvidiousInstanceDescription__</div>
<div class="indent option-hidden-section hidden spacing">
<input class="option-text-box" type="text">
<div class="inline">
<div class="option-button text-change-set inline low-profile">
__MSG_add__
</div>
<div class="option-button text-change-reset inline low-profile">
__MSG_cancel__
</div>
</div>
</div>
<div style="margin-top:15px">
<span>__MSG_currentInstances__</span>
<span data-type="display" data-sync="invidiousInstances"></span>
<div class="option-button invidious-instance-reset spacing hidden">
__MSG_resetInvidiousInstance__
</div>
</div>
</div>
<div data-type="toggle" data-sync="trackViewCount">
<div class="switch-container">
<label class="switch">
<input id="trackViewCount" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="trackViewCount">
__MSG_enableViewTracking__
</label>
</div>
<div class="small-description">__MSG_whatViewTracking__</div>
</div>
<div data-type="toggle" data-sync="trackViewCountInPrivate" data-dependent-on="trackViewCount" data-private-only="true">
<div class="switch-container">
<label class="switch">
<input id="trackViewCountInPrivate" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="trackViewCountInPrivate">
__MSG_enableViewTrackingInPrivate__
</label>
</div>
</div>
<div data-type="button-press" data-sync="copyDebugInformation" data-confirm-message="copyDebugInformation">
<div class="option-button trigger-button">
__MSG_copyDebugInformation__
</div>
<br/>
<div class="small-description">__MSG_copyDebugInformationOptions__</div>
</div>
<br/>
<br/>
<div option-type="toggle" sync-option="testingServer" confirm-message="testingServerWarning" no-safari="true">
<label class="switch-container">
<div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true">
<div class="switch-container">
<label class="switch">
<input type="checkbox">
<input id="testingServer" type="checkbox">
<span class="slider round"></span>
</label>
<div class="switch-label">
<label class="switch-label" for="testingServer">
__MSG_enableTestingServer__
</div>
</label>
<br/>
<br/>
<br/>
</div>
<div class="small-description">__MSG_whatEnableTestingServer__</div>
<br/>
<br/>
<br/>
</div>
<div option-type="text-change" sync-option="serverAddress">
<label class="text-label-container">
<div>__MSG_customServerAddress__</div>
<div data-type="text-change" data-sync="serverAddress" data-dependent-on="testingServer" data-dependent-on-inverted="true">
<label class="optionLabel inline">
<span class="optionLabel">__MSG_customServerAddress__:</span>
<input class="option-text-box" type="text">
<input class="option-text-box" type="text" style="margin-right:10px">
</label>
<div class="small-description">__MSG_customServerAddressDescription__</div>
<div class="next-line">
<div class="option-button text-change-set inline">
__MSG_save__
</div>
@@ -607,14 +480,13 @@
<div class="option-button text-change-reset inline">
__MSG_reset__
</div>
<br/>
<br/>
<div class="small-description">__MSG_customServerAddressDescription__</div>
</div>
</div>
</div>
</div>
</div>
</body>

File diff suppressed because it is too large Load Diff

View File

@@ -393,7 +393,7 @@ label>p, #disableExtension>p, #usernameValue, #usernameElement > div > p,#sponso
/* footer */
#sbFooter > a {
#sbFooter a {
color: var(--sb-main-fg-color);
text-decoration: none;
}

View File

@@ -138,6 +138,14 @@
<span id="sponsorTimeSavedEndWord">__MSG_minsLower__</span></b>).
</div>
<footer id="sbFooter">
<div id="sponsorTimesDonateContainer" style="display: none; align-items: center;">
<img src="/icons/heart.svg"/>
<a id="sbConsiderDonateLink" href="https://sponsor.ajay.app/donate" target="_blank" rel="noopener">
__MSG_considerDonating__
</a>
<img id="sbCloseDonate" src="/icons/close.png" height="8px" style="padding-left: 5px; cursor: pointer;"/>
</div>
<a href="https://sponsor.ajay.app" target="_blank" rel="noopener">__MSG_website__</a> |
<a href="https://sponsor.ajay.app/stats" target="_blank" rel="noopener">__MSG_viewLeaderboard__</a> |
<a href="https://github.com/ajayyy/SponsorBlock" target="_blank" rel="noopener">GitHub</a>

View File

@@ -31,24 +31,24 @@ class CategoryChooserComponent extends React.Component<CategoryChooserProps, Cat
{/* Headers */}
<tr id={"CategoryOptionsRow"}
className="categoryTableElement categoryTableHeader">
<td id={"CategoryOptionName"}>
<th id={"CategoryOptionName"}>
{chrome.i18n.getMessage("category")}
</td>
</th>
<td id={"CategorySkipOption"}
<th id={"CategorySkipOption"}
className="skipOption">
{chrome.i18n.getMessage("skipOption")}
</td>
</th>
<td id={"CategoryColorOption"}
<th id={"CategoryColorOption"}
className="colorOption">
{chrome.i18n.getMessage("seekBarColor")}
</td>
</th>
<td id={"CategoryPreviewColorOption"}
<th id={"CategoryPreviewColorOption"}
className="previewColorOption">
{chrome.i18n.getMessage("previewColor")}
</td>
</th>
</tr>
{this.getCategorySkipOptions()}

View File

@@ -8,6 +8,7 @@ import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
import { VoteResponse } from "../messageTypes";
import { AnimationUtils } from "../utils/animationUtils";
import { GenericUtils } from "../utils/genericUtils";
import { Tooltip } from "../render/Tooltip";
export interface CategoryPillProps {
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
@@ -21,6 +22,8 @@ export interface CategoryPillState {
class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryPillState> {
tooltip?: Tooltip;
constructor(props: CategoryPillProps) {
super(props);
@@ -35,15 +38,16 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
const style: React.CSSProperties = {
backgroundColor: this.getColor(),
display: this.state.show ? "flex" : "none",
color: this.state.segment?.category === "sponsor"
|| this.state.segment?.category === "exclusive_access" ? "white" : "black",
color: this.getTextColor(),
}
return (
<span style={style}
className={"sponsorBlockCategoryPill"}
title={this.getTitleText()}
onClick={(e) => this.toggleOpen(e)}>
aria-label={this.getTitleText()}
onClick={(e) => this.toggleOpen(e)}
onMouseEnter={() => this.openTooltip()}
onMouseLeave={() => this.closeTooltip()}>
<span className="sponsorBlockCategoryPillTitleSection">
<img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
@@ -116,6 +120,45 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
return configObject?.color;
}
private getTextColor(): string {
const color = this.getColor();
if (!color) return null;
const existingCalculatedColor = Config.config.categoryPillColors[this.state.segment?.category];
if (existingCalculatedColor && existingCalculatedColor.lastColor === color) {
return existingCalculatedColor.textColor;
} else {
const luminance = GenericUtils.getLuminance(color);
const textColor = luminance > 128 ? "black" : "white";
Config.config.categoryPillColors[this.state.segment?.category] = {
lastColor: color,
textColor
};
return textColor;
}
}
private openTooltip(): void {
const tooltipMount = document.querySelector("ytd-video-primary-info-renderer > #container") as HTMLElement;
if (tooltipMount) {
this.tooltip = new Tooltip({
text: this.getTitleText(),
referenceNode: tooltipMount,
bottomOffset: "70px",
opacity: 0.95,
displayTriangle: false,
showLogo: false,
showGotIt: false
});
}
}
private closeTooltip(): void {
this.tooltip?.close();
this.tooltip = null;
}
getTitleText(): string {
const shortDescription = chrome.i18n.getMessage(`category_${this.state.segment?.category}_pill`);
return (shortDescription ? shortDescription + ". ": "") + chrome.i18n.getMessage("categoryPillTitleText");

View File

@@ -0,0 +1,75 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import Config from "../config";
import { Keybind } from "../types";
import KeybindDialogComponent from "./KeybindDialogComponent";
import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils";
export interface KeybindProps {
option: string;
}
export interface KeybindState {
keybind: Keybind;
}
let dialog;
class KeybindComponent extends React.Component<KeybindProps, KeybindState> {
constructor(props: KeybindProps) {
super(props);
this.state = {keybind: Config.config[this.props.option]};
}
render(): React.ReactElement {
return(
<>
<div className="keybind-buttons inline" title={chrome.i18n.getMessage("change")} onClick={() => this.openEditDialog()}>
{this.state.keybind?.ctrl && <div className="key keyControl">Ctrl</div>}
{this.state.keybind?.ctrl && <span className="keyControl">+</span>}
{this.state.keybind?.alt && <div className="key keyAlt">Alt</div>}
{this.state.keybind?.alt && <span className="keyAlt">+</span>}
{this.state.keybind?.shift && <div className="key keyShift">Shift</div>}
{this.state.keybind?.shift && <span className="keyShift">+</span>}
{this.state.keybind?.key != null && <div className="key keyBase">{formatKey(this.state.keybind.key)}</div>}
{this.state.keybind == null && <span className="unbound">{chrome.i18n.getMessage("notSet")}</span>}
</div>
{this.state.keybind != null &&
<div className="option-button trigger-button inline" onClick={() => this.unbind()}>
{chrome.i18n.getMessage("unbind")}
</div>
}
</>
);
}
equals(other: Keybind): boolean {
return keybindEquals(this.state.keybind, other);
}
toString(): string {
return keybindToString(this.state.keybind);
}
openEditDialog(): void {
dialog = parent.document.createElement("div");
dialog.id = "keybind-dialog";
parent.document.body.prepend(dialog);
ReactDOM.render(<KeybindDialogComponent option={this.props.option} closeListener={(updateWith) => this.closeEditDialog(updateWith)} />, dialog);
}
closeEditDialog(updateWith: Keybind): void {
ReactDOM.unmountComponentAtNode(dialog);
dialog.remove();
if (updateWith != null)
this.setState({keybind: updateWith});
}
unbind(): void {
this.setState({keybind: null});
Config.config[this.props.option] = null;
}
}
export default KeybindComponent;

View File

@@ -0,0 +1,165 @@
import * as React from "react";
import { ChangeEvent } from "react";
import Config from "../config";
import { Keybind } from "../types";
import { keybindEquals, formatKey } from "../utils/configUtils";
export interface KeybindDialogProps {
option: string;
closeListener: (updateWith) => void;
}
export interface KeybindDialogState {
key: Keybind;
error: ErrorMessage;
}
interface ErrorMessage {
message: string;
blocking: boolean;
}
class KeybindDialogComponent extends React.Component<KeybindDialogProps, KeybindDialogState> {
constructor(props: KeybindDialogProps) {
super(props);
this.state = {
key: {
key: null,
code: null,
ctrl: false,
alt: false,
shift: false
},
error: {
message: null,
blocking: false
}
};
}
render(): React.ReactElement {
return(
<>
<div className="blocker"></div>
<div className="dialog">
<div id="change-keybind-description">{chrome.i18n.getMessage("keybindDescription")}</div>
<div id="change-keybind-settings">
<div id="change-keybind-modifiers" className="inline">
<div>
<input id="change-keybind-ctrl" type="checkbox" onChange={this.keybindModifierChecked} />
<label htmlFor="change-keybind-ctrl">Ctrl</label>
</div>
<div>
<input id="change-keybind-alt" type="checkbox" onChange={this.keybindModifierChecked} />
<label htmlFor="change-keybind-alt">Alt</label>
</div>
<div>
<input id="change-keybind-shift" type="checkbox" onChange={this.keybindModifierChecked} />
<label htmlFor="change-keybind-shift">Shift</label>
</div>
</div>
<div className="key inline">{formatKey(this.state.key.key)}</div>
</div>
<div id="change-keybind-error">{this.state.error?.message}</div>
<div id="change-keybind-buttons">
<div className={"option-button save-button inline" + ((this.state.error?.blocking || this.state.key.key == null) ? " disabled" : "")} onClick={() => this.save()}>
{chrome.i18n.getMessage("save")}
</div>
<div className="option-button cancel-button inline" onClick={() => this.props.closeListener(null)}>
{chrome.i18n.getMessage("cancel")}
</div>
</div>
</div>
</>
);
}
componentDidMount(): void {
parent.document.addEventListener("keydown", this.keybindKeyPressed);
document.addEventListener("keydown", this.keybindKeyPressed);
}
componentWillUnmount(): void {
parent.document.removeEventListener("keydown", this.keybindKeyPressed);
document.removeEventListener("keydown", this.keybindKeyPressed);
}
keybindKeyPressed = (e: KeyboardEvent): void => {
if (!e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.getModifierState("AltGraph")) {
if (e.code == "Escape") {
this.props.closeListener(null);
return;
}
this.setState({
key: {
key: e.key,
code: e.code,
ctrl: this.state.key.ctrl,
alt: this.state.key.alt,
shift: this.state.key.shift}
}, () => this.setState({ error: this.isKeybindAvailable() }));
}
}
keybindModifierChecked = (e: ChangeEvent<HTMLInputElement>): void => {
const id = e.target.id;
const val = e.target.checked;
this.setState({
key: {
key: this.state.key.key,
code: this.state.key.code,
ctrl: id == "change-keybind-ctrl" ? val: this.state.key.ctrl,
alt: id == "change-keybind-alt" ? val: this.state.key.alt,
shift: id == "change-keybind-shift" ? val: this.state.key.shift}
}, () => this.setState({ error: this.isKeybindAvailable() }));
}
isKeybindAvailable(): ErrorMessage {
if (this.state.key.key == null)
return null;
let youtubeShortcuts: Keybind[];
if (/[a-zA-Z0-9,.+\-\][:]/.test(this.state.key.key)) {
youtubeShortcuts = [{key: "k"}, {key: "j"}, {key: "l"}, {key: "p", shift: true}, {key: "n", shift: true}, {key: ","}, {key: "."}, {key: ",", shift: true}, {key: ".", shift: true},
{key: "ArrowRight"}, {key: "ArrowLeft"}, {key: "ArrowUp"}, {key: "ArrowDown"}, {key: "ArrowRight", ctrl: true}, {key: "ArrowLeft", ctrl: true}, {key: "c"}, {key: "o"},
{key: "w"}, {key: "+"}, {key: "-"}, {key: "f"}, {key: "t"}, {key: "i"}, {key: "m"}, {key: "a"}, {key: "s"}, {key: "d"}, {key: "Home"}, {key: "End"},
{key: "0"}, {key: "1"}, {key: "2"}, {key: "3"}, {key: "4"}, {key: "5"}, {key: "6"}, {key: "7"}, {key: "8"}, {key: "9"}, {key: "]"}, {key: "["}];
} else {
youtubeShortcuts = [{key: null, code: "KeyK"}, {key: null, code: "KeyJ"}, {key: null, code: "KeyL"}, {key: null, code: "KeyP", shift: true}, {key: null, code: "KeyN", shift: true},
{key: null, code: "Comma"}, {key: null, code: "Period"}, {key: null, code: "Comma", shift: true}, {key: null, code: "Period", shift: true}, {key: null, code: "Space"},
{key: null, code: "KeyC"}, {key: null, code: "KeyO"}, {key: null, code: "KeyW"}, {key: null, code: "Equal"}, {key: null, code: "Minus"}, {key: null, code: "KeyF"}, {key: null, code: "KeyT"},
{key: null, code: "KeyI"}, {key: null, code: "KeyM"}, {key: null, code: "KeyA"}, {key: null, code: "KeyS"}, {key: null, code: "KeyD"}, {key: null, code: "BracketLeft"}, {key: null, code: "BracketRight"}];
}
for (const shortcut of youtubeShortcuts) {
const withShift = Object.assign({}, shortcut);
if (!/[0-9]/.test(this.state.key.key)) //shift+numbers don't seem to do anything on youtube, all other keys do
withShift.shift = true;
if (this.equals(shortcut) || this.equals(withShift))
return {message: chrome.i18n.getMessage("youtubeKeybindWarning"), blocking: false};
}
if (this.props.option != "skipKeybind" && this.equals(Config.config['skipKeybind']) ||
this.props.option != "submitKeybind" && this.equals(Config.config['submitKeybind']) ||
this.props.option != "startSponsorKeybind" && this.equals(Config.config['startSponsorKeybind']))
return {message: chrome.i18n.getMessage("keyAlreadyUsed"), blocking: true};
return null;
}
equals(other: Keybind): boolean {
return keybindEquals(this.state.key, other);
}
save(): void {
if (this.state.key.key != null && !this.state.error?.blocking) {
Config.config[this.props.option] = this.state.key;
this.props.closeListener(this.state.key);
}
}
}
export default KeybindDialogComponent;

View File

@@ -1,13 +1,13 @@
import * as React from "react";
import * as CompileConfig from "../../config.json";
import Config from "../config"
import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
import { Category, ContentContainer, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import Utils from "../utils";
const utils = new Utils();
import { getCategoryActionType, getSkippingText } from "../utils/categoryUtils";
import { getSkippingText } from "../utils/categoryUtils";
import { keybindToString } from "../utils/configUtils";
import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
@@ -326,7 +326,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
getSkipButton(): JSX.Element {
if (this.state.showSkipButton && (this.segments.length > 1
|| getCategoryActionType(this.segments[0].category) !== CategoryActionType.POI
|| this.segments[0].actionType !== ActionType.Poi
|| this.props.unskipTime)) {
const style: React.CSSProperties = {
@@ -344,7 +344,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
className="sponsorSkipObject sponsorSkipNoticeButton"
style={style}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}>
{this.state.skipButtonText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")}
{this.state.skipButtonText + (this.state.showKeybindHint ? " (" + keybindToString(Config.config.skipKeybind) + ")" : "")}
</button>
</span>
);
@@ -517,9 +517,10 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
source: SponsorSourceType.Local
};
const segmentTimes = Config.config.segmentTimes.get(sponsorVideoID) || [];
const segmentTimes = Config.config.unsubmittedSegments[sponsorVideoID] || [];
segmentTimes.push(sponsorTimesSubmitting);
Config.config.segmentTimes.set(sponsorVideoID, segmentTimes);
Config.config.unsubmittedSegments[sponsorVideoID] = segmentTimes;
Config.forceSyncUpdate("unsubmittedSegments");
this.props.contentContainer().sponsorTimesSubmitting.push(sponsorTimesSubmitting);
this.props.contentContainer().updatePreviewBar();
@@ -547,7 +548,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
getCategoryOptions(): React.ReactElement[] {
const elements = [];
const categories = (CompileConfig.categoryList.filter((cat => getCategoryActionType(cat as Category) === CategoryActionType.Skippable))) as Category[];
const categories = (CompileConfig.categoryList.filter((cat => CompileConfig.categorySupport[cat].includes(ActionType.Skip)))) as Category[];
for (const category of categories) {
elements.push(
<option value={category}
@@ -601,7 +602,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
}
getUnskippedModeInfo(index: number, buttonText: string): SkipNoticeState {
const changeCountdown = getCategoryActionType(this.segments[index].category) === CategoryActionType.Skippable;
const changeCountdown = this.segments[index].actionType !== ActionType.Poi;
const maxCountdownTime = changeCountdown ? () => {
const sponsorTime = this.segments[index];
@@ -645,18 +646,9 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
this.addVoteButtonInfo(chrome.i18n.getMessage("voted"));
// Change the sponsor locally
if (segment) {
if (type === 0) {
segment.hidden = SponsorHideType.Downvoted;
} else if (category) {
segment.category = category; // This is the actual segment on the video page
this.segments[index].category = category; //this is the segment inside the skip notice.
} else if (type === 1) {
segment.hidden = SponsorHideType.Visible;
}
this.contentContainer().updatePreviewBar();
if (segment && category) {
// This is the segment inside the skip notice
this.segments[index].category = category;
}
}
@@ -693,7 +685,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
clearConfigListener(): void {
if (this.configListener) {
Config.configListeners.splice(Config.configListeners.indexOf(this.configListener), 1);
Config.configSyncListeners.splice(Config.configSyncListeners.indexOf(this.configListener), 1);
this.configListener = null;
}
}

View File

@@ -1,9 +1,8 @@
import * as React from "react";
import * as CompileConfig from "../../config.json";
import Config from "../config";
import { ActionType, Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
import { ActionType, Category, ContentContainer, SponsorTime } from "../types";
import Utils from "../utils";
import { getCategoryActionType } from "../utils/categoryUtils";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
import { RectangleTooltip } from "../render/RectangleTooltip";
@@ -38,8 +37,9 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
configUpdateListener: () => void;
previousSkipType: CategoryActionType;
timeBeforeChangingToPOI: number; // Initialized when first selecting POI
previousSkipType: ActionType;
// Used when selecting POI or Full
timesBeforeChanging: number[] = [];
fullVideoWarningShown = false;
constructor(props: SponsorTimeEditProps) {
@@ -50,7 +50,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.idSuffix = this.props.idSuffix;
this.previousSkipType = CategoryActionType.Skippable;
this.previousSkipType = ActionType.Skip;
this.state = {
editing: false,
sponsorTimeEdits: [null, null],
@@ -72,7 +72,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
// Add as a config listener
if (!this.configUpdateListener) {
this.configUpdateListener = () => this.configUpdate();
Config.configListeners.push(this.configUpdate.bind(this));
Config.configSyncListeners.push(this.configUpdate.bind(this));
}
this.checkToShowFullVideoWarning();
@@ -80,7 +80,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
componentWillUnmount(): void {
if (this.configUpdateListener) {
Config.configListeners.splice(Config.configListeners.indexOf(this.configUpdate.bind(this)), 1);
Config.configSyncListeners.splice(Config.configSyncListeners.indexOf(this.configUpdate.bind(this)), 1);
}
}
@@ -129,7 +129,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
onWheel={(e) => {this.changeTimesWhenScrolling(0, e, sponsorTime)}}>
</input>
{getCategoryActionType(sponsorTime.category) === CategoryActionType.Skippable ? (
{sponsorTime.actionType !== ActionType.Poi ? (
<span>
<span>
{" " + chrome.i18n.getMessage("to") + " "}
@@ -167,7 +167,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
className="sponsorTimeDisplay"
onClick={this.toggleEditTime.bind(this)}>
{utils.getFormattedTime(segment[0], true) +
((!isNaN(segment[1]) && getCategoryActionType(sponsorTime.category) === CategoryActionType.Skippable)
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi)
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segment[1], true) : "")}
</div>
);
@@ -202,13 +202,13 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
{/* Action Type */}
{CompileConfig.categorySupport[sponsorTime.category] &&
(CompileConfig.categorySupport[sponsorTime.category]?.length > 1
|| CompileConfig.categorySupport[sponsorTime.category]?.[0] !== "skip") ? (
|| CompileConfig.categorySupport[sponsorTime.category]?.[0] === ActionType.Full) ? (
<div style={{position: "relative"}}>
<select id={"sponsorTimeActionTypes" + this.idSuffix}
className="sponsorTimeEditSelector sponsorTimeActionTypes"
defaultValue={sponsorTime.actionType}
ref={this.actionTypeOptionRef}
onChange={() => this.saveEditTimes()}>
onChange={(e) => this.actionTypeSelectionChange(e)}>
{this.getActionTypeOptions(sponsorTime)}
</select>
</div>
@@ -224,7 +224,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
{chrome.i18n.getMessage("delete")}
</span>
{(!isNaN(segment[1]) && getCategoryActionType(sponsorTime.category) === CategoryActionType.Skippable) ? (
{(!isNaN(segment[1]) && ![ActionType.Poi, ActionType.Full].includes(sponsorTime.actionType)) ? (
<span id={"sponsorTimePreviewButton" + this.idSuffix}
className="sponsorTimeEditButton"
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}>
@@ -261,7 +261,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (0 < difference && difference< 0.5) this.showScrollToEditToolTip();
sponsorTimeEdits[index] = targetValue;
if (index === 0 && getCategoryActionType(sponsorTime.category) === CategoryActionType.POI) sponsorTimeEdits[1] = targetValue;
if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue;
this.setState({sponsorTimeEdits});
this.saveEditTimes();
@@ -290,7 +290,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
timeAsNumber = 0;
}
sponsorTimeEdits[index] = utils.getFormattedTime(timeAsNumber, true);
if (getCategoryActionType(sponsorTime.category) === CategoryActionType.POI) sponsorTimeEdits[1] = sponsorTimeEdits[0];
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
this.setState({sponsorTimeEdits});
this.saveEditTimes();
@@ -374,27 +374,57 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (confirm(chrome.i18n.getMessage("enableThisCategoryFirst")
.replace("{0}", chrome.i18n.getMessage("category_" + chosenCategory)))) {
// Open options page
chrome.runtime.sendMessage({message: "openConfig", hash: chosenCategory + "OptionsName"});
chrome.runtime.sendMessage({message: "openConfig", hash: "behavior"});
}
return;
}
if (getCategoryActionType(event.target.value as Category) === CategoryActionType.POI) {
if (this.previousSkipType === CategoryActionType.Skippable) this.timeBeforeChangingToPOI = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
this.handleReplacingLostTimes(event.target.value as Category, sponsorTime.actionType);
this.saveEditTimes();
}
actionTypeSelectionChange(event: React.ChangeEvent<HTMLSelectElement>): void {
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
this.handleReplacingLostTimes(sponsorTime.category, event.target.value as ActionType);
this.saveEditTimes();
}
private handleReplacingLostTimes(category: Category, actionType: ActionType): void {
if (CompileConfig.categorySupport[category]?.includes(ActionType.Poi)) {
if (this.previousSkipType !== ActionType.Poi) {
this.timesBeforeChanging = [null, utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1])];
}
this.setTimeTo(1, null);
this.props.contentContainer().updateEditButtonsOnPlayer();
if (this.props.contentContainer().sponsorTimesSubmitting
.some((segment, i) => segment.category === event.target.value && i !== this.props.index)) {
.some((segment, i) => segment.category === category && i !== this.props.index)) {
alert(chrome.i18n.getMessage("poiOnlyOneSegment"));
}
} else if (getCategoryActionType(event.target.value as Category) === CategoryActionType.Skippable && this.previousSkipType === CategoryActionType.POI) {
this.setTimeTo(1, this.timeBeforeChangingToPOI);
this.previousSkipType = ActionType.Poi;
} else if (CompileConfig.categorySupport[category]?.length === 1
&& CompileConfig.categorySupport[category]?.[0] === ActionType.Full) {
if (this.previousSkipType !== ActionType.Full) {
this.timesBeforeChanging = [utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]), utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1])];
}
this.previousSkipType = getCategoryActionType(event.target.value as Category);
this.saveEditTimes();
this.previousSkipType = ActionType.Full;
} else if (CompileConfig.categorySupport[category]?.includes(ActionType.Skip)
&& ![ActionType.Poi, ActionType.Full].includes(this.getNextActionType(category, actionType)) && this.previousSkipType !== ActionType.Skip) {
if (this.timesBeforeChanging[0]) {
this.setTimeTo(0, this.timesBeforeChanging[0]);
}
if (this.timesBeforeChanging[1]) {
this.setTimeTo(1, this.timesBeforeChanging[1]);
}
this.previousSkipType = ActionType.Skip;
}
}
getActionTypeOptions(sponsorTime: SponsorTime): React.ReactElement[] {
@@ -429,7 +459,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (time === null) time = sponsorTime.segment[0];
sponsorTime.segment[index] = time;
if (getCategoryActionType(sponsorTime.category) === CategoryActionType.POI) sponsorTime.segment[1] = time;
if (sponsorTime.actionType === ActionType.Poi) sponsorTime.segment[1] = time;
this.setState({
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
@@ -477,11 +507,10 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
sponsorTimesSubmitting[this.props.index].category = category;
const inputActionType = this.actionTypeOptionRef?.current?.value as ActionType;
const actionType = inputActionType && CompileConfig.categorySupport[category]?.includes(inputActionType) ? inputActionType as ActionType
: CompileConfig.categorySupport[category]?.[0] ?? ActionType.Skip;
sponsorTimesSubmitting[this.props.index].actionType = actionType;
sponsorTimesSubmitting[this.props.index].actionType = this.getNextActionType(category, inputActionType);
Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting);
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
this.props.contentContainer().updatePreviewBar();
@@ -492,6 +521,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
}
private getNextActionType(category: Category, actionType: ActionType): ActionType {
return actionType && CompileConfig.categorySupport[category]?.includes(actionType) ? actionType
: CompileConfig.categorySupport[category]?.[0] ?? ActionType.Skip
}
previewTime(ctrlPressed = false, shiftPressed = false): void {
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
const index = this.props.index;
@@ -522,7 +556,12 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
sponsorTimes.splice(index, 1);
//save this
Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimes);
if (sponsorTimes.length > 0) {
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimes;
} else {
delete Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID];
}
Config.forceSyncUpdate("unsubmittedSegments");
this.props.contentContainer().updatePreviewBar();

View File

@@ -0,0 +1,37 @@
import * as React from "react";
import Config from "../config";
import { Category, SegmentUUID, SponsorTime } from "../types";
export interface TooltipProps {
text: string;
show: boolean;
}
export interface TooltipState {
}
class TooltipComponent extends React.Component<TooltipProps, TooltipState> {
constructor(props: TooltipProps) {
super(props);
}
render(): React.ReactElement {
const style: React.CSSProperties = {
display: this.props.show ? "flex" : "none",
position: "absolute",
}
return (
<span style={style}
className={"sponsorBlockTooltip"} >
<span className="sponsorBlockTooltipText">
{this.props.text}
</span>
</span>
);
}
}
export default TooltipComponent;

View File

@@ -1,19 +1,17 @@
import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes } from "./types";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
import { keybindEquals } from "./utils/configUtils";
interface SBConfig {
userID: string,
isVip: boolean,
lastIsVipUpdate: number,
/* Contains unsubmitted segments that the user has created. */
segmentTimes: SBMap<string, SponsorTime[]>,
unsubmittedSegments: Record<string, SponsorTime[]>,
defaultCategory: Category,
whitelistedChannels: string[],
forceChannelCheck: boolean,
skipKeybind: string,
startSponsorKeybind: string,
submitKeybind: string,
minutesSaved: number,
skipCount: number,
sponsorTimesContributed: number,
@@ -38,13 +36,15 @@ interface SBConfig {
serverAddress: string,
minDuration: number,
skipNoticeDuration: number,
audioNotificationOnSkip,
audioNotificationOnSkip: boolean,
checkForUnlistedVideos: boolean,
testingServer: boolean,
refetchWhenNotFound: boolean,
ytInfoPermissionGranted: boolean,
allowExpirements: boolean,
showDonationLink: boolean,
showPopupDonationCount: number,
donateClicked: number,
autoHideInfoButton: boolean,
autoSkipOnMusicVideos: boolean,
colorPalette: {
@@ -54,6 +54,19 @@ interface SBConfig {
},
scrollToEditTimeUpdate: boolean,
categoryPillUpdate: boolean,
darkMode: boolean,
// Used to cache calculated text color info
categoryPillColors: {
[key in Category]: {
lastColor: string,
textColor: string
}
}
skipKeybind: Keybind,
startSponsorKeybind: Keybind,
submitKeybind: Keybind,
// What categories should be skipped
categorySelections: CategorySelection[],
@@ -83,96 +96,38 @@ interface SBConfig {
}
}
export type VideoDownvotes = { segments: { uuid: HashedValue, hidden: SponsorHideType }[] , lastAccess: number };
interface SBStorage {
/* VideoID prefixes to UUID prefixes */
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>,
}
export interface SBObject {
configListeners: Array<(changes: StorageChangesObject) => unknown>;
defaults: SBConfig;
localConfig: SBConfig;
configSyncListeners: Array<(changes: StorageChangesObject) => unknown>;
syncDefaults: SBConfig;
localDefaults: SBStorage;
cachedSyncConfig: SBConfig;
cachedLocalStorage: SBStorage;
config: SBConfig;
// Functions
encodeStoredItem<T>(data: T): T | UnencodedSegmentTimes;
convertJSON(): void;
}
// Allows a SBMap to be conveted into json form
// Currently used for local storage
class SBMap<T, U> extends Map {
id: string;
constructor(id: string, entries?: [T, U][]) {
super();
this.id = id;
// Import all entries if they were given
if (entries !== undefined) {
for (const item of entries) {
super.set(item[0], item[1])
}
}
}
get(key): U {
return super.get(key);
}
rawSet(key, value) {
return super.set(key, value);
}
update() {
// Store updated SBMap locally
chrome.storage.sync.set({
[this.id]: encodeStoredItem(this)
});
}
set(key: T, value: U) {
const result = super.set(key, value);
this.update();
return result;
}
delete(key) {
const result = super.delete(key);
// Make sure there are no empty elements
for (const entry of this.entries()) {
if (entry[1].length === 0) {
super.delete(entry[0]);
}
}
this.update();
return result;
}
clear() {
const result = super.clear();
this.update();
return result;
}
local: SBStorage;
forceSyncUpdate(prop: string): void;
forceLocalUpdate(prop: string): void;
}
const Config: SBObject = {
/**
* Callback function when an option is updated
*/
configListeners: [],
defaults: {
configSyncListeners: [],
syncDefaults: {
userID: null,
isVip: false,
lastIsVipUpdate: 0,
segmentTimes: new SBMap("segmentTimes"),
unsubmittedSegments: {},
defaultCategory: "chooseACategory" as Category,
whitelistedChannels: [],
forceChannelCheck: false,
skipKeybind: "Enter",
startSponsorKeybind: ";",
submitKeybind: "'",
minutesSaved: 0,
skipCount: 0,
sponsorTimesContributed: 0,
@@ -204,10 +159,26 @@ const Config: SBObject = {
ytInfoPermissionGranted: false,
allowExpirements: true,
showDonationLink: true,
showPopupDonationCount: 0,
donateClicked: 0,
autoHideInfoButton: true,
autoSkipOnMusicVideos: false,
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
categoryPillUpdate: false,
darkMode: true,
categoryPillColors: {},
/**
* Default keybinds should not set "code" as that's gonna be different based on the user's locale. They should also only use EITHER ctrl OR alt modifiers (or none).
* Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales.
* The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't.
* Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"!
* TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map?
*/
skipKeybind: {key: "Enter"},
startSponsorKeybind: {key: ";"},
submitKeybind: {key: "'"},
categorySelections: [{
name: "sponsor" as Category,
@@ -310,74 +281,49 @@ const Config: SBObject = {
}
}
},
localConfig: null,
localDefaults: {
downvotedSegments: {}
},
cachedSyncConfig: null,
cachedLocalStorage: null,
config: null,
// Functions
encodeStoredItem,
convertJSON
local: null,
forceSyncUpdate,
forceLocalUpdate
};
// Function setup
/**
* A SBMap cannot be stored in the chrome storage.
* This data will be encoded into an array instead
*
* @param data
*/
function encodeStoredItem<T>(data: T): T | UnencodedSegmentTimes {
// if data is SBMap convert to json for storing
if(!(data instanceof SBMap)) return data;
return Array.from(data.entries()).filter((element) => element[1].length > 0); // Remove empty entries
}
/**
* An SBMap cannot be stored in the chrome storage.
* This data will be decoded from the array it is stored in
*
* @param {*} data
*/
function decodeStoredItem<T>(id: string, data: T): T | SBMap<string, SponsorTime[]> {
if (!Config.defaults[id]) return data;
if (Config.defaults[id] instanceof SBMap) {
try {
if (!Array.isArray(data)) return data;
return new SBMap(id, data as UnencodedSegmentTimes);
} catch(e) {
console.error("Failed to parse SBMap: " + id);
}
}
// If all else fails, return the data
return data;
}
function configProxy(): SBConfig {
chrome.storage.onChanged.addListener((changes: {[key: string]: chrome.storage.StorageChange}) => {
function configProxy(): { sync: SBConfig, local: SBStorage } {
chrome.storage.onChanged.addListener((changes: {[key: string]: chrome.storage.StorageChange}, areaName) => {
if (areaName === "sync") {
for (const key in changes) {
Config.localConfig[key] = decodeStoredItem(key, changes[key].newValue);
Config.cachedSyncConfig[key] = changes[key].newValue;
}
for (const callback of Config.configListeners) {
for (const callback of Config.configSyncListeners) {
callback(changes);
}
} else if (areaName === "local") {
for (const key in changes) {
Config.cachedLocalStorage[key] = changes[key].newValue;
}
}
});
const handler: ProxyHandler<SBConfig> = {
const syncHandler: ProxyHandler<SBConfig> = {
set<K extends keyof SBConfig>(obj: SBConfig, prop: K, value: SBConfig[K]) {
Config.localConfig[prop] = value;
Config.cachedSyncConfig[prop] = value;
chrome.storage.sync.set({
[prop]: encodeStoredItem(value)
[prop]: value
});
return true;
},
get<K extends keyof SBConfig>(obj: SBConfig, prop: K): SBConfig[K] {
const data = Config.localConfig[prop];
const data = Config.cachedSyncConfig[prop];
return obj[prop] || data;
},
@@ -390,19 +336,72 @@ function configProxy(): SBConfig {
};
return new Proxy<SBConfig>({handler} as unknown as SBConfig, handler);
const localHandler: ProxyHandler<SBStorage> = {
set<K extends keyof SBStorage>(obj: SBStorage, prop: K, value: SBStorage[K]) {
Config.cachedLocalStorage[prop] = value;
chrome.storage.local.set({
[prop]: value
});
return true;
},
get<K extends keyof SBStorage>(obj: SBStorage, prop: K): SBStorage[K] {
const data = Config.cachedLocalStorage[prop];
return obj[prop] || data;
},
deleteProperty(obj: SBStorage, prop: keyof SBStorage) {
chrome.storage.local.remove(<string> prop);
return true;
}
function fetchConfig(): Promise<void> {
return new Promise((resolve) => {
};
return {
sync: new Proxy<SBConfig>({ handler: syncHandler } as unknown as SBConfig, syncHandler),
local: new Proxy<SBStorage>({ handler: localHandler } as unknown as SBStorage, localHandler)
};
}
function forceSyncUpdate(prop: string): void {
chrome.storage.sync.set({
[prop]: Config.cachedSyncConfig[prop]
});
}
function forceLocalUpdate(prop: string): void {
chrome.storage.local.set({
[prop]: Config.cachedLocalStorage[prop]
});
}
async function fetchConfig(): Promise<void> {
await Promise.all([new Promise<void>((resolve) => {
chrome.storage.sync.get(null, function(items) {
Config.localConfig = <SBConfig> <unknown> items; // Data is ready
Config.cachedSyncConfig = <SBConfig> <unknown> items;
resolve();
});
}), new Promise<void>((resolve) => {
chrome.storage.local.get(null, function(items) {
Config.cachedLocalStorage = <SBStorage> <unknown> items;
resolve();
});
})]);
}
function migrateOldSyncFormats(config: SBConfig) {
if (config["segmentTimes"]) {
for (const item of config["segmentTimes"]) {
config.unsubmittedSegments[item[0]] = item[1];
}
chrome.storage.sync.remove("segmentTimes");
}
function migrateOldFormats(config: SBConfig) {
if (!config["exclusive_accessCategoryAdded"] && !config.categorySelections.some((s) => s.name === "exclusive_access")) {
config["exclusive_accessCategoryAdded"] = true;
@@ -450,6 +449,29 @@ function migrateOldFormats(config: SBConfig) {
}
}
if (typeof config["skipKeybind"] == "string") {
config["skipKeybind"] = {key: config["skipKeybind"]};
}
if (typeof config["startSponsorKeybind"] == "string") {
config["startSponsorKeybind"] = {key: config["startSponsorKeybind"]};
}
if (typeof config["submitKeybind"] == "string") {
config["submitKeybind"] = {key: config["submitKeybind"]};
}
// Unbind key if it matches a previous one set by the user (should be ordered oldest to newest)
const keybinds = ["skipKeybind", "startSponsorKeybind", "submitKeybind"];
for (let i = keybinds.length-1; i >= 0; i--) {
for (let j = 0; j < keybinds.length; j++) {
if (i == j)
continue;
if (keybindEquals(config[keybinds[i]], config[keybinds[j]]))
config[keybinds[i]] = null;
}
}
// Remove some old unused options
if (config["sponsorVideoID"] !== undefined) {
chrome.storage.sync.remove("sponsorVideoID");
@@ -467,32 +489,32 @@ function migrateOldFormats(config: SBConfig) {
async function setupConfig() {
await fetchConfig();
addDefaults();
convertJSON();
const config = configProxy();
migrateOldFormats(config);
migrateOldSyncFormats(config.sync);
Config.config = config;
}
function convertJSON(): void {
Object.keys(Config.localConfig).forEach(key => {
Config.localConfig[key] = decodeStoredItem(key, Config.localConfig[key]);
});
Config.config = config.sync;
Config.local = config.local;
}
// Add defaults
function addDefaults() {
for (const key in Config.defaults) {
if(!Object.prototype.hasOwnProperty.call(Config.localConfig, key)) {
Config.localConfig[key] = Config.defaults[key];
for (const key in Config.syncDefaults) {
if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig, key)) {
Config.cachedSyncConfig[key] = Config.syncDefaults[key];
} else if (key === "barTypes") {
for (const key2 in Config.defaults[key]) {
if(!Object.prototype.hasOwnProperty.call(Config.localConfig[key], key2)) {
Config.localConfig[key][key2] = Config.defaults[key][key2];
for (const key2 in Config.syncDefaults[key]) {
if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig[key], key2)) {
Config.cachedSyncConfig[key][key2] = Config.syncDefaults[key][key2];
}
}
}
}
for (const key in Config.localDefaults) {
if(!Object.prototype.hasOwnProperty.call(Config.cachedLocalStorage, key)) {
Config.cachedLocalStorage[key] = Config.localDefaults[key];
}
}
}
// Sync config

View File

@@ -1,7 +1,7 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, CategoryActionType, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime } from "./types";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime, HashedValue } from "./types";
import { ContentContainer } from "./types";
import { ContentContainer, Keybind } from "./types";
import Utils from "./utils";
const utils = new Utils();
@@ -13,11 +13,10 @@ import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SubmissionNotice from "./render/SubmissionNotice";
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
import * as Chat from "./js-components/chat";
import { getCategoryActionType } from "./utils/categoryUtils";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { Tooltip } from "./render/Tooltip";
import { getStartTimeFromUrl } from "./utils/urlParser";
import { findValidElement, getControls, isVisible } from "./utils/pageUtils";
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
import { keybindEquals } from "./utils/configUtils";
import { CategoryPill } from "./render/CategoryPill";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
@@ -92,15 +91,14 @@ const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImage
// Direct Links after the config is loaded
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
// wait for hover preview to appear, and refresh attachments if ever found
window.addEventListener("DOMContentLoaded", () => utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()));
addPageListeners();
addHotkeyListener();
//the amount of times the sponsor lookup has retried
//this only happens if there is an error
let sponsorLookupRetries = 0;
/** Segments created by the user which have not yet been submitted. */
let sponsorTimesSubmitting: SponsorTime[] = [];
let loadedPreloadedSegment = false;
//becomes true when isInfoFound is called
//this is used to close the popup on YouTube when the other popup opens
@@ -139,6 +137,9 @@ const manualSkipPercentCount = 0.5;
//get messages from the background script and the popup
chrome.runtime.onMessage.addListener(messageListener);
//store pressed modifier keys
const pressedKeys = new Set();
function messageListener(request: Message, sender: unknown, sendResponse: (response: MessageResponse) => void): void | boolean {
//messages from popup script
switch(request.message){
@@ -203,6 +204,15 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
}));
return true;
case "submitVote":
vote(request.type, request.UUID).then((response) => sendResponse(response));
return true;
case "hideSegment":
utils.getSponsorTimeFromUUID(sponsorTimes, request.UUID).hidden = request.type;
utils.addHiddenSegment(sponsorVideoID, request.UUID, request.type);
updatePreviewBar();
break;
}
}
@@ -223,8 +233,8 @@ function contentConfigUpdateListener(changes: StorageChangesObject) {
}
}
if (!Config.configListeners.includes(contentConfigUpdateListener)) {
Config.configListeners.push(contentConfigUpdateListener);
if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
Config.configSyncListeners.push(contentConfigUpdateListener);
}
function resetValues() {
@@ -233,7 +243,6 @@ function resetValues() {
//reset sponsor times
sponsorTimes = null;
sponsorLookupRetries = 0;
sponsorSkipped = [];
videoInfo = null;
@@ -265,7 +274,7 @@ function resetValues() {
isAdPlaying = false;
for (let i = 0; i < skipNotices.length; i++) {
skipNotices.pop().close();
skipNotices.pop()?.close();
}
skipButtonControlBar?.disable();
@@ -682,29 +691,15 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
setupVideoMutationListener();
//check database for sponsor times
//made true once a setTimeout has been created to try again after a server error
let recheckStarted = false;
// Create categories list
const categories: string[] = [];
for (const categorySelection of Config.config.categorySelections) {
categories.push(categorySelection.name);
}
const categories: string[] = Config.config.categorySelections.map((category) => category.name);
const extraRequestData: Record<string, unknown> = {};
const windowHash = window.location.hash.substr(1);
if (windowHash) {
const params: Record<string, unknown> = windowHash.split('&').reduce((acc, param) => {
const [key, value] = param.split('=');
acc[key] = value;
return acc;
}, {});
if (params.requiredSegment) extraRequestData.requiredSegment = params.requiredSegment;
}
const hashParams = getHashParams();
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
// Check for hashPrefix setting
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
const hashPrefix = (await utils.getHash(id, 1)).slice(0, 4) as VideoID & HashedValue;
const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
categories,
actionTypes: getEnabledActionTypes(),
@@ -739,10 +734,10 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
// Hide all submissions smaller than the minimum duration
if (Config.config.minDuration !== 0) {
for (let i = 0; i < sponsorTimes.length; i++) {
if (sponsorTimes[i].segment[1] - sponsorTimes[i].segment[0] < Config.config.minDuration
&& getCategoryActionType(sponsorTimes[i].category) !== CategoryActionType.POI) {
sponsorTimes[i].hidden = SponsorHideType.MinimumDuration;
for (const segment of sponsorTimes) {
const duration = segment[1] - segment[0];
if (duration > 0 && duration < Config.config.minDuration) {
segment.hidden = SponsorHideType.MinimumDuration;
}
}
}
@@ -758,6 +753,18 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
}
}
// See if some segments should be hidden
const downvotedData = Config.local.downvotedSegments[hashPrefix];
if (downvotedData) {
for (const segment of sponsorTimes) {
const hashedUUID = await utils.getHash(segment.UUID, 1);
const segmentDownvoteData = downvotedData.segments.find((downvote) => downvote.uuid === hashedUUID);
if (segmentDownvoteData) {
segment.hidden = segmentDownvoteData.hidden;
}
}
}
startSkipScheduleCheckingForStartSponsors();
//update the preview bar
@@ -767,29 +774,15 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
//otherwise the listener can handle it
updatePreviewBar();
}
sponsorLookupRetries = 0;
} else if (response?.status === 404) {
retryFetch();
} else if (sponsorLookupRetries < 15 && !recheckStarted) {
recheckStarted = true;
//TODO lower when server becomes better (back to 1 second)
//some error occurred, try again in a second
setTimeout(() => {
if (sponsorVideoID && sponsorTimes?.length === 0) {
sponsorsLookup(sponsorVideoID);
}
}, 5000 + Math.random() * 15000 + 5000 * sponsorLookupRetries);
sponsorLookupRetries++;
}
lookupVipInformation(id);
}
function getEnabledActionTypes(): ActionType[] {
const actionTypes = [ActionType.Skip];
const actionTypes = [ActionType.Skip, ActionType.Poi];
if (Config.config.muteSegments) {
actionTypes.push(ActionType.Mute);
}
@@ -834,7 +827,7 @@ async function updateVipInfo(): Promise<boolean> {
}
async function lockedCategoriesLookup(id: string): Promise<void> {
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
const hashPrefix = (await utils.getHash(id, 1)).slice(0, 4);
const response = await utils.asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix);
if (response.ok) {
@@ -857,8 +850,6 @@ function retryFetch(): void {
sponsorsLookup(sponsorVideoID);
}
}, 10000 + Math.random() * 30000);
sponsorLookupRetries = 0;
}
/**
@@ -874,7 +865,7 @@ function startSkipScheduleCheckingForStartSponsors() {
let startingSegment: SponsorTime = null;
for (const time of sponsorTimes) {
if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime
&& getCategoryActionType(time.category) === CategoryActionType.Skippable) {
&& time.actionType !== ActionType.Poi) {
startingSegmentTime = time.segment[0];
startingSegment = time;
found = true;
@@ -884,7 +875,7 @@ function startSkipScheduleCheckingForStartSponsors() {
if (!found) {
for (const time of sponsorTimesSubmitting) {
if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime
&& getCategoryActionType(time.category) === CategoryActionType.Skippable) {
&& time.actionType !== ActionType.Poi) {
startingSegmentTime = time.segment[0];
startingSegment = time;
found = true;
@@ -895,7 +886,7 @@ function startSkipScheduleCheckingForStartSponsors() {
// For highlight category
const poiSegments = sponsorTimes
.filter((time) => time.segment[1] > video.currentTime && getCategoryActionType(time.category) === CategoryActionType.POI)
.filter((time) => time.segment[1] > video.currentTime && time.actionType === ActionType.Poi)
.sort((a, b) => b.segment[0] - a.segment[0]);
for (const time of poiSegments) {
const skipOption = utils.getCategorySelection(time.category)?.option;
@@ -946,6 +937,8 @@ async function getVideoInfo(): Promise<void> {
function getYouTubeVideoID(document: Document): string | boolean {
const url = document.URL;
// clips should never skip, going from clip to full video has no indications.
if (url.includes("youtube.com/clip/")) return false;
// skip to URL if matches youtube watch or invidious or matches youtube pattern
if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url);
// skip to document and don't hide if on /embed/
@@ -953,7 +946,7 @@ function getYouTubeVideoID(document: Document): string | boolean {
// skip to document if matches pattern
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document);
// not sure, try URL then document
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document);
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document, false);
}
function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
@@ -999,8 +992,8 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
return id.length == 11 ? id : false;
} else if (urlObject.pathname.startsWith("/embed/") || urlObject.pathname.startsWith("/shorts/")) {
try {
const id = urlObject.pathname.split("/")[2];
if (id && id.length >= 11) return id.substr(0, 11);
const id = urlObject.pathname.split("/")[2]
if (id?.length >=11 ) return id.slice(0, 11);
} catch (e) {
console.error("[SB] Video ID not valid for " + url);
return false;
@@ -1038,7 +1031,7 @@ function updatePreviewBar(): void {
category: segment.category,
unsubmitted: false,
actionType: segment.actionType,
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
showLarger: segment.actionType === ActionType.Poi
});
});
}
@@ -1049,7 +1042,7 @@ function updatePreviewBar(): void {
category: segment.category,
unsubmitted: true,
actionType: segment.actionType,
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
showLarger: segment.actionType === ActionType.Poi
});
});
@@ -1072,7 +1065,8 @@ async function whitelistCheck() {
const getChannelID = () => videoInfo?.videoDetails?.channelId
?? document.querySelector(".ytd-channel-name a")?.getAttribute("href")?.replace(/\/.+\//, "") // YouTube
?? document.querySelector(".ytp-title-channel-logo")?.getAttribute("href")?.replace(/https:\/.+\//, "") // YouTube Embed
?? document.querySelector("a > .channel-profile")?.parentElement?.getAttribute("href")?.replace(/\/.+\//, ""); // Invidious
?? document.querySelector("a > .channel-profile")?.parentElement?.getAttribute("href")?.replace(/\/.+\//, "") // Invidious
?? document.querySelector("a.slim-owner-icon-and-title")?.getAttribute("href")?.replace(/\/.+\//, ""); // Mobile YouTube
try {
await utils.wait(() => !!getChannelID(), 6000, 20);
@@ -1222,7 +1216,7 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
|| (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
&& (!onlySkippableSponsors || shouldSkip(possibleTimes[i]))
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
&& getCategoryActionType(possibleTimes[i].category) === CategoryActionType.Skippable) {
&& possibleTimes[i].actionType !== ActionType.Poi) {
scheduledTimes.push(possibleTimes[i].scheduledTime);
includedTimes.push(possibleTimes[i]);
@@ -1276,6 +1270,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment))
&& v.currentTime !== skipTime[1]) {
switch(skippingSegments[0].actionType) {
case ActionType.Poi:
case ActionType.Skip: {
// Fix for looped videos not working when skipping to the end #426
// for some reason you also can't skip to 1 second before the end
@@ -1303,14 +1298,21 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
if (autoSkip && Config.config.audioNotificationOnSkip) {
const beep = new Audio(chrome.runtime.getURL("icons/beep.ogg"));
beep.volume = video.volume * 0.1;
const oldMetadata = navigator.mediaSession.metadata
beep.play();
beep.addEventListener("ended", () => {
navigator.mediaSession.metadata = null;
setTimeout(() =>
navigator.mediaSession.metadata = oldMetadata
);
})
}
if (!autoSkip
&& skippingSegments.length === 1
&& getCategoryActionType(skippingSegments[0].category) === CategoryActionType.POI) {
&& skippingSegments[0].actionType === ActionType.Poi) {
skipButtonControlBar.enable(skippingSegments[0]);
if (onMobileYouTube) skipButtonControlBar.setShowKeybindHint(false);
if (onMobileYouTube || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false);
activeSkipKeybindElement?.setShowKeybindHint(false);
activeSkipKeybindElement = skipButtonControlBar;
@@ -1319,7 +1321,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
//send out the message saying that a sponsor message was skipped
if (!Config.config.dontShowNotice || !autoSkip) {
const newSkipNotice = new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer, unskipTime);
if (onMobileYouTube) newSkipNotice.setShowKeybindHint(false);
if (onMobileYouTube || Config.config.skipKeybind == null) newSkipNotice.setShowKeybindHint(false);
skipNotices.push(newSkipNotice);
activeSkipKeybindElement?.setShowKeybindHint(false);
@@ -1399,12 +1401,13 @@ function createButton(baseID: string, title: string, callback: () => void, image
function shouldAutoSkip(segment: SponsorTime): boolean {
return utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip ||
(Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& getCategoryActionType(segment.category) === CategoryActionType.Skippable);
&& segment.actionType !== ActionType.Poi);
}
function shouldSkip(segment: SponsorTime): boolean {
return utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay ||
(Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
return (segment.actionType !== ActionType.Full
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
}
/** Creates any missing buttons on the YouTube player if possible. */
@@ -1525,7 +1528,8 @@ function startOrEndTimingNewSegment() {
}
// Save the newly created segment
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
// Make sure they know if someone has already submitted something it while they were watching
sponsorsLookup(sponsorVideoID);
@@ -1547,7 +1551,8 @@ function isSegmentCreationInProgress(): boolean {
function cancelCreatingSegment() {
if (isSegmentCreationInProgress()) {
sponsorTimesSubmitting.splice(sponsorTimesSubmitting.length - 1, 1);
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
if (sponsorTimesSubmitting.length <= 0) resetSponsorSubmissionNotice();
}
@@ -1557,7 +1562,7 @@ function cancelCreatingSegment() {
}
function updateSponsorTimesSubmitting(getFromConfig = true) {
const segmentTimes = Config.config.segmentTimes.get(sponsorVideoID);
const segmentTimes = Config.config.unsubmittedSegments[sponsorVideoID];
//see if this data should be saved in the sponsorTimesSubmitting variable
if (getFromConfig && segmentTimes != undefined) {
@@ -1582,6 +1587,8 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
if (submissionNotice !== null) {
submissionNotice.update();
}
checkForPreloadedSegment();
}
function openInfoMenu() {
@@ -1684,7 +1691,7 @@ function closeInfoMenuAnd<T>(func: () => T): T {
function clearSponsorTimes() {
const currentVideoID = sponsorVideoID;
const sponsorTimes = Config.config.segmentTimes.get(currentVideoID);
const sponsorTimes = Config.config.unsubmittedSegments[currentVideoID];
if (sponsorTimes != undefined && sponsorTimes.length > 0) {
const confirmMessage = chrome.i18n.getMessage("clearThis") + getSegmentsMessage(sponsorTimes)
@@ -1694,7 +1701,8 @@ function clearSponsorTimes() {
resetSponsorSubmissionNotice();
//clear the sponsor times
Config.config.segmentTimes.delete(currentVideoID);
delete Config.config.unsubmittedSegments[currentVideoID];
Config.forceSyncUpdate("unsubmittedSegments");
//clear sponsor times submitting
sponsorTimesSubmitting = [];
@@ -1705,7 +1713,7 @@ function clearSponsorTimes() {
}
//if skipNotice is null, it will not affect the UI
async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent): Promise<void> {
async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent): Promise<VoteResponse> {
if (skipNotice !== null && skipNotice !== undefined) {
//add loading info
skipNotice.addVoteButtonInfo.bind(skipNotice)(chrome.i18n.getMessage("Loading"))
@@ -1733,6 +1741,8 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo
}
}
}
return response;
}
async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): Promise<VoteResponse> {
@@ -1762,7 +1772,29 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
type: type,
UUID: UUID,
category: category
}, resolve);
}, (response) => {
if (response.successType === 1) {
// Change the sponsor locally
const segment = utils.getSponsorTimeFromUUID(sponsorTimes, UUID);
if (segment) {
if (type === 0) {
segment.hidden = SponsorHideType.Downvoted;
} else if (category) {
segment.category = category;
} else if (type === 1) {
segment.hidden = SponsorHideType.Visible;
}
if (!category) {
utils.addHiddenSegment(sponsorVideoID, segment.UUID, segment.hidden);
}
updatePreviewBar();
}
}
resolve(response);
});
});
}
@@ -1803,6 +1835,12 @@ function submitSponsorTimes() {
//send the message to the background js
//called after all the checks have been made that it's okay to do so
async function sendSubmitMessage() {
// Block if submitting on a running livestream or premiere
if (isVisible(document.querySelector(".ytp-live-badge"))) {
alert(chrome.i18n.getMessage("liveOrPremiere"));
return;
}
// Add loading animation
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker.svg");
const stopAnimation = AnimationUtils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer());
@@ -1815,13 +1853,14 @@ async function sendSubmitMessage() {
}
//update sponsorTimes
Config.config.segmentTimes.set(sponsorVideoID, sponsorTimesSubmitting);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
// Check to see if any of the submissions are below the minimum duration set
if (Config.config.minDuration > 0) {
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
if (sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0] < Config.config.minDuration
&& getCategoryActionType(sponsorTimesSubmitting[i].category) !== CategoryActionType.POI) {
const duration = sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0];
if (duration > 0 && duration < Config.config.minDuration) {
const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" +
getSegmentsMessage(sponsorTimesSubmitting);
@@ -1842,7 +1881,8 @@ async function sendSubmitMessage() {
stopAnimation();
// Remove segments from storage since they've already been submitted
Config.config.segmentTimes.delete(sponsorVideoID);
delete Config.config.unsubmittedSegments[sponsorVideoID];
Config.forceSyncUpdate("unsubmittedSegments");
const newSegments = sponsorTimesSubmitting;
try {
@@ -1891,7 +1931,7 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
let timeMessage = utils.getFormattedTime(sponsorTimes[i].segment[s]);
//if this is an end time
if (s == 1) {
timeMessage = " to " + timeMessage;
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
} else if (i > 0) {
//add commas if necessary
timeMessage = ", " + timeMessage;
@@ -1916,30 +1956,46 @@ function addPageListeners(): void {
function addHotkeyListener(): void {
document.addEventListener("keydown", hotkeyListener);
document.addEventListener("keyup", (e) => pressedKeys.delete(e.key));
}
function hotkeyListener(e: KeyboardEvent): void {
if (["textarea", "input"].includes(document.activeElement?.tagName?.toLowerCase())
|| document.activeElement?.id?.toLowerCase()?.includes("editable")) return;
const key = e.key;
if (["Alt", "Control", "Shift", "AltGraph"].includes(e.key)) {
pressedKeys.add(e.key);
return;
}
const key:Keybind = {key: e.key, code: e.code, alt: pressedKeys.has("Alt"), ctrl: pressedKeys.has("Control"), shift: pressedKeys.has("Shift")};
const skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
switch (key) {
case skipKey:
if (activeSkipKeybindElement) {
if (!pressedKeys.has("AltGraph")) {
if (keybindEquals(key, skipKey)) {
if (activeSkipKeybindElement)
activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
}
break;
case startSponsorKey:
return;
} else if (keybindEquals(key, startSponsorKey)) {
startOrEndTimingNewSegment();
break;
case submitKey:
return;
} else if (keybindEquals(key, submitKey)) {
submitSponsorTimes();
return;
}
}
//legacy - to preserve keybinds for skipKey, startSponsorKey and submitKey for people who set it before the update. (shouldn't be changed for future keybind options)
if (key.key == skipKey?.key && skipKey.code == null && !keybindEquals(Config.syncDefaults.skipKeybind, skipKey)) {
if (activeSkipKeybindElement)
activeSkipKeybindElement.toggleSkip.call(activeSkipKeybindElement);
} else if (key.key == startSponsorKey?.key && startSponsorKey.code == null && !keybindEquals(Config.syncDefaults.startSponsorKeybind, startSponsorKey)) {
startOrEndTimingNewSegment();
} else if (key.key == submitKey?.key && submitKey.code == null && !keybindEquals(Config.syncDefaults.submitKeybind, submitKey)) {
submitSponsorTimes();
break;
}
}
@@ -1989,7 +2045,6 @@ function sendRequestToCustomServer(type, fullAddress, callback) {
function updateAdFlag(): void {
const wasAdPlaying = isAdPlaying;
isAdPlaying = document.getElementsByClassName('ad-showing').length > 0;
if(wasAdPlaying != isAdPlaying) {
updatePreviewBar();
updateVisibilityOfPlayerControlsButton();
@@ -2023,3 +2078,35 @@ function showTimeWithoutSkips(skippedDuration: number): void {
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
}
function checkForPreloadedSegment() {
if (loadedPreloadedSegment) return;
loadedPreloadedSegment = true;
const hashParams = getHashParams();
let pushed = false;
const segments = hashParams.segments;
if (Array.isArray(segments)) {
for (const segment of segments) {
if (Array.isArray(segment.segment)) {
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
sponsorTimesSubmitting.push({
segment: segment.segment,
UUID: utils.generateUserID() as SegmentUUID,
category: segment.category ? segment.category : Config.config.defaultCategory,
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
source: SponsorSourceType.Local
});
pushed = true;
}
}
}
}
if (pushed) {
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
}
}

View File

@@ -11,6 +11,10 @@ async function init() {
await utils.wait(() => Config.config !== null);
if (!Config.config.darkMode) {
document.documentElement.setAttribute("data-theme", "light");
}
if (!showDonationLink()) {
document.getElementById("sbDonate").style.display = "none";
}

View File

@@ -1,6 +1,7 @@
import Config from "../config";
import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils";
import { keybindToString } from "../utils/configUtils";
import Utils from "../utils";
import { AnimationUtils } from "../utils/animationUtils";
@@ -180,7 +181,7 @@ export class SkipButtonControlBar {
}
private getTitle(): string {
return getSkippingText([this.segment], false) + (this.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "");
return getSkippingText([this.segment], false) + (this.showKeybindHint ? " (" + keybindToString(Config.config.skipKeybind) + ")" : "");
}
private getChapterPrefix(): HTMLElement {

View File

@@ -2,7 +2,7 @@
// Message and Response Types
//
import { SponsorTime } from "./types";
import { SegmentUUID, SponsorHideType, SponsorTime } from "./types";
interface BaseMessage {
from?: string;
@@ -29,7 +29,19 @@ interface IsInfoFoundMessage {
updating: boolean;
}
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage);
interface SubmitVoteMessage {
message: "submitVote";
type: number;
UUID: SegmentUUID;
}
interface HideSegmentMessage {
message: "hideSegment";
type: SponsorHideType;
UUID: SegmentUUID;
}
export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SubmitVoteMessage | HideSegmentMessage);
export interface IsInfoFoundMessageResponse {
found: boolean;
@@ -59,7 +71,8 @@ export type MessageResponse =
| GetChannelIDResponse
| SponsorStartResponse
| IsChannelWhitelistedResponse
| Record<string, never>;
| Record<string, never>
| VoteResponse;
export interface VoteResponse {
successType: number;

View File

@@ -1,3 +1,6 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import Config from "./config";
import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json";
@@ -7,31 +10,54 @@ window.SB = Config;
import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
import KeybindComponent from "./components/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
const utils = new Utils();
let embed = false;
window.addEventListener('DOMContentLoaded', init);
async function init() {
utils.localizeHtmlPage();
// selected tab
if (location.hash != "") {
const substr = location.hash.slice(1);
let menuItem = document.querySelector(`[data-for='${substr}']`);
if (menuItem == null)
menuItem = document.querySelector(`[data-for='behavior']`);
menuItem.classList.add("selected");
} else {
document.querySelector(`[data-for='behavior']`).classList.add("selected");
}
document.getElementById("version").innerText = "v. " + chrome.runtime.getManifest().version;
// Remove header if needed
if (window.location.hash === "#embed") {
embed = true;
for (const element of document.getElementsByClassName("titleBar")) {
element.classList.add("hidden");
}
document.getElementById("options").classList.add("embed");
createStickyHeader();
}
if (!Config.configListeners.includes(optionsConfigUpdateListener)) {
Config.configListeners.push(optionsConfigUpdateListener);
if (!Config.configSyncListeners.includes(optionsConfigUpdateListener)) {
Config.configSyncListeners.push(optionsConfigUpdateListener);
}
await utils.wait(() => Config.config !== null);
if (!Config.config.darkMode) {
document.documentElement.setAttribute("data-theme", "light");
}
const donate = document.getElementById("sbDonate");
donate.addEventListener("click", () => Config.config.donateClicked = Config.config.donateClicked + 1);
if (!showDonationLink()) {
document.getElementById("sbDonate").style.visibility = "hidden";
donate.classList.add("hidden");
}
// Set all of the toggle options to the correct option
@@ -39,31 +65,31 @@ async function init() {
const optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) {
if ((optionsElements[i].getAttribute("private-mode-only") === "true" && !(await isIncognitoAllowed()))
|| (optionsElements[i].getAttribute("no-safari") === "true" && navigator.vendor === "Apple Computer, Inc.")
|| (optionsElements[i].getAttribute("if-false") && Config.config[optionsElements[i].getAttribute("if-false")])) {
optionsElements[i].classList.add("hidden");
const dependentOnName = optionsElements[i].getAttribute("data-dependent-on");
const dependentOn = optionsContainer.querySelector(`[data-sync='${dependentOnName}']`);
let isDependentOnReversed = false;
if (dependentOn)
isDependentOnReversed = dependentOn.getAttribute("data-toggle-type") === "reverse" || optionsElements[i].getAttribute("data-dependent-on-inverted") === "true";
if (await shouldHideOption(optionsElements[i]) || (dependentOn && (isDependentOnReversed ? Config.config[dependentOnName] : !Config.config[dependentOnName]))) {
optionsElements[i].classList.add("hidden", "hiding");
if (!dependentOn)
continue;
}
const option = optionsElements[i].getAttribute("sync-option");
const option = optionsElements[i].getAttribute("data-sync");
switch (optionsElements[i].getAttribute("option-type")) {
switch (optionsElements[i].getAttribute("data-type")) {
case "toggle": {
const optionResult = Config.config[option];
const checkbox = optionsElements[i].querySelector("input");
const reverse = optionsElements[i].getAttribute("toggle-type") === "reverse";
const reverse = optionsElements[i].getAttribute("data-toggle-type") === "reverse";
const confirmMessage = optionsElements[i].getAttribute("confirm-message");
const confirmMessage = optionsElements[i].getAttribute("data-confirm-message");
if (optionResult != undefined) {
checkbox.checked = optionResult;
if (reverse) {
optionsElements[i].querySelector("input").checked = !optionResult;
}
}
if (optionResult != undefined)
checkbox.checked = reverse ? !optionResult : optionResult;
// See if anything extra should be run first time
switch (option) {
@@ -73,7 +99,7 @@ async function init() {
}
// Add click listener
checkbox.addEventListener("click", () => {
checkbox.addEventListener("click", async () => {
// Confirm if required
if (checkbox.checked && confirmMessage && !confirm(chrome.i18n.getMessage(confirmMessage))){
checkbox.checked = false;
@@ -92,11 +118,36 @@ async function init() {
// Enable the notice
Config.config["dontShowNotice"] = false;
const showNoticeSwitch = <HTMLInputElement> document.querySelector("[sync-option='dontShowNotice'] > label > label > input");
const showNoticeSwitch = <HTMLInputElement> document.querySelector("[data-sync='dontShowNotice'] > div > label > input");
showNoticeSwitch.checked = true;
}
break;
case "showDonationLink":
if (checkbox.checked)
document.getElementById("sbDonate").classList.add("hidden");
else
document.getElementById("sbDonate").classList.remove("hidden");
break;
case "darkMode":
if (checkbox.checked) {
document.documentElement.setAttribute("data-theme", "dark");
} else {
document.documentElement.setAttribute("data-theme", "light");
}
break;
}
// If other options depend on this, hide/show them
const dependents = optionsContainer.querySelectorAll(`[data-dependent-on='${option}']`);
for (let j = 0; j < dependents.length; j++) {
const disableWhenChecked = dependents[j].getAttribute("data-dependent-on-inverted") === "true";
if (!await shouldHideOption(dependents[j]) && (!disableWhenChecked && checkbox.checked || disableWhenChecked && !checkbox.checked)) {
dependents[j].classList.remove("hidden");
setTimeout(() => dependents[j].classList.remove("hiding"), 1);
} else {
dependents[j].classList.add("hiding");
setTimeout(() => dependents[j].classList.add("hidden"), 400);
}
}
});
break;
@@ -144,7 +195,7 @@ async function init() {
textChangeResetButton.addEventListener("click", () => {
if (!confirm(chrome.i18n.getMessage("areYouSureReset"))) return;
Config.config[option] = Config.defaults[option];
Config.config[option] = Config.syncDefaults[option];
textChangeInput.value = Config.config[option];
});
@@ -155,7 +206,15 @@ async function init() {
const button = optionsElements[i].querySelector(".trigger-button");
button.addEventListener("click", () => activatePrivateTextChange(<HTMLElement> optionsElements[i]));
const privateTextChangeOption = optionsElements[i].getAttribute("sync-option");
if (option == "*") {
const downloadButton = optionsElements[i].querySelector(".download-button");
downloadButton.addEventListener("click", downloadConfig);
const uploadButton = optionsElements[i].querySelector(".upload-button");
uploadButton.addEventListener("change", (e) => uploadConfig(e));
}
const privateTextChangeOption = optionsElements[i].getAttribute("data-sync");
// See if anything extra must be done
switch (privateTextChangeOption) {
case "invidiousInstances":
@@ -167,7 +226,7 @@ async function init() {
case "button-press": {
const actionButton = optionsElements[i].querySelector(".trigger-button");
switch(optionsElements[i].getAttribute("sync-option")) {
switch(optionsElements[i].getAttribute("data-sync")) {
case "copyDebugInformation":
actionButton.addEventListener("click", copyDebugOutputToClipboard);
break;
@@ -176,9 +235,7 @@ async function init() {
break;
}
case "keybind-change": {
const keybindButton = optionsElements[i].querySelector(".trigger-button");
keybindButton.addEventListener("click", () => activateKeybindChange(<HTMLElement> optionsElements[i]));
ReactDOM.render(React.createElement(KeybindComponent, {option: option}), optionsElements[i].querySelector("div"));
break;
}
case "display": {
@@ -190,7 +247,7 @@ async function init() {
const numberInput = optionsElements[i].querySelector("input");
if (isNaN(configValue) || configValue < 0) {
numberInput.value = Config.defaults[option];
numberInput.value = Config.syncDefaults[option];
} else {
numberInput.value = configValue;
}
@@ -220,10 +277,57 @@ async function init() {
}
}
optionsContainer.classList.remove("hidden");
// Tab interaction
const tabElements = document.getElementsByClassName("tab-heading");
for (let i = 0; i < tabElements.length; i++) {
const tabFor = tabElements[i].getAttribute("data-for");
if (tabElements[i].classList.contains("selected"))
document.getElementById(tabFor).classList.remove("hidden");
tabElements[i].addEventListener("click", () => {
if (!embed) location.hash = tabFor;
createStickyHeader();
document.querySelectorAll(".tab-heading").forEach(element => { element.classList.remove("selected"); });
optionsContainer.querySelectorAll(".option-group").forEach(element => { element.classList.add("hidden"); });
tabElements[i].classList.add("selected");
document.getElementById(tabFor).classList.remove("hidden");
});
}
window.addEventListener("scroll", () => createStickyHeader());
optionsContainer.classList.add("animated");
}
function createStickyHeader() {
const container = document.getElementById("options-container");
const options = document.getElementById("options");
if (!embed && window.pageYOffset > 90 && (window.innerHeight <= 770 || window.innerWidth <= 1200)) {
if (!container.classList.contains("sticky")) {
options.style.marginTop = options.offsetTop.toString()+"px";
container.classList.add("sticky");
}
} else {
options.style.marginTop = "unset";
container.classList.remove("sticky");
}
}
/**
* Handle special cases where an option shouldn't show
*
* @param {String} element
*/
async function shouldHideOption(element: Element): Promise<boolean> {
return (element.getAttribute("data-private-only") === "true" && !(await isIncognitoAllowed()))
|| (element.getAttribute("data-no-safari") === "true" && navigator.vendor === "Apple Computer, Inc.");
}
/**
* Called when the config is updated
*
@@ -234,7 +338,7 @@ function optionsConfigUpdateListener() {
const optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) {
switch (optionsElements[i].getAttribute("option-type")) {
switch (optionsElements[i].getAttribute("data-type")) {
case "display":
updateDisplayElement(<HTMLElement> optionsElements[i])
}
@@ -247,17 +351,27 @@ function optionsConfigUpdateListener() {
* @param element
*/
function updateDisplayElement(element: HTMLElement) {
const displayOption = element.getAttribute("sync-option")
const displayOption = element.getAttribute("data-sync")
const displayText = Config.config[displayOption];
element.innerText = displayText;
// See if anything extra must be run
switch (displayOption) {
case "invidiousInstances":
case "invidiousInstances": {
element.innerText = displayText.join(', ');
let allEquals = displayText.length == invidiousList.length;
for (let i = 0; i < invidiousList.length && allEquals; i++) {
if (displayText[i] != invidiousList[i])
allEquals = false;
}
if (!allEquals) {
const resetButton = element.parentElement.querySelector(".invidious-instance-reset");
resetButton.classList.remove("hidden");
}
break;
}
}
}
/**
* Initializes the option to add Invidious instances
@@ -270,6 +384,8 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
const button = element.querySelector(".trigger-button");
const setButton = element.querySelector(".text-change-set");
const cancelButton = element.querySelector(".text-change-reset");
const resetButton = element.querySelector(".invidious-instance-reset");
setButton.addEventListener("click", async function() {
if (textBox.value == "" || textBox.value.includes("/") || textBox.value.includes("http")) {
alert(chrome.i18n.getMessage("addInvidiousInstanceError"));
@@ -287,19 +403,26 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
invidiousOnClick(checkbox, "supportInvidious");
textBox.value = "";
resetButton.classList.remove("hidden");
// Hide this section again
textBox.value = "";
element.querySelector(".option-hidden-section").classList.add("hidden");
button.classList.remove("disabled");
}
});
const resetButton = element.querySelector(".invidious-instance-reset");
cancelButton.addEventListener("click", async function() {
textBox.value = "";
element.querySelector(".option-hidden-section").classList.add("hidden");
button.classList.remove("disabled");
});
resetButton.addEventListener("click", function() {
if (confirm(chrome.i18n.getMessage("resetInvidiousInstanceAlert"))) {
// Set to CI populated list
Config.config[option] = invidiousList;
resetButton.classList.add("hidden");
}
});
}
@@ -351,91 +474,6 @@ async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Pro
});
}
/**
* Will trigger the container to ask the user for a keybind.
*
* @param element
*/
function activateKeybindChange(element: HTMLElement) {
const button = element.querySelector(".trigger-button");
if (button.classList.contains("disabled")) return;
button.classList.add("disabled");
const option = element.getAttribute("sync-option");
const currentlySet = Config.config[option] !== null ? chrome.i18n.getMessage("keybindCurrentlySet") : "";
const status = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status");
status.innerText = chrome.i18n.getMessage("keybindDescription") + currentlySet;
if (Config.config[option] !== null) {
const statusKey = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status-key");
statusKey.innerText = Config.config[option];
}
element.querySelector(".option-hidden-section").classList.remove("hidden");
document.addEventListener("keydown", (e) => keybindKeyPressed(element, e), {once: true});
}
/**
* Called when a key is pressed in an activiated keybind change option.
*
* @param element
* @param e
*/
function keybindKeyPressed(element: HTMLElement, e: KeyboardEvent) {
const key = e.key;
if (["Shift", "Control", "Meta", "Alt", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab"].indexOf(key) !== -1) {
// Wait for more
document.addEventListener("keydown", (e) => keybindKeyPressed(element, e), {once: true});
} else {
const button: HTMLElement = element.querySelector(".trigger-button");
const option = element.getAttribute("sync-option");
// Make sure keybind isn't used by the other listener
// TODO: If other keybindings are going to be added, we need a better way to find the other keys used.
const otherKeybind = (option === "startSponsorKeybind") ? Config.config['submitKeybind'] : Config.config['startSponsorKeybind'];
if (key === otherKeybind) {
closeKeybindOption(element, button);
alert(chrome.i18n.getMessage("theKey") + " " + key + " " + chrome.i18n.getMessage("keyAlreadyUsed"));
return;
}
// cancel setting a keybind
if (key === "Escape") {
closeKeybindOption(element, button);
return;
}
Config.config[option] = key;
const status = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status");
status.innerText = chrome.i18n.getMessage("keybindDescriptionComplete");
const statusKey = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status-key");
statusKey.innerText = key;
button.classList.remove("disabled");
}
}
/**
* Closes the menu for editing the keybind
*
* @param element
* @param button
*/
function closeKeybindOption(element: HTMLElement, button: HTMLElement) {
element.querySelector(".option-hidden-section").classList.add("hidden");
button.classList.remove("disabled");
}
/**
* Will trigger the textbox to appear to be able to change an option's text.
*
@@ -448,7 +486,7 @@ function activatePrivateTextChange(element: HTMLElement) {
button.classList.add("disabled");
const textBox = <HTMLInputElement> element.querySelector(".option-text-box");
const option = element.getAttribute("sync-option");
const option = element.getAttribute("data-sync");
// See if anything extra must be done
switch (option) {
@@ -458,16 +496,10 @@ function activatePrivateTextChange(element: HTMLElement) {
}
let result = Config.config[option];
// See if anything extra must be done
switch (option) {
case "*": {
const jsonData = JSON.parse(JSON.stringify(Config.localConfig));
// Fix segmentTimes data as it is destroyed from the JSON stringify
jsonData.segmentTimes = Config.encodeStoredItem(Config.localConfig.segmentTimes);
result = JSON.stringify(jsonData);
result = JSON.stringify(Config.cachedSyncConfig);
break;
}
}
@@ -476,38 +508,7 @@ function activatePrivateTextChange(element: HTMLElement) {
const setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async () => {
const confirmMessage = element.getAttribute("confirm-message");
if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
// See if anything extra must be done
switch (option) {
case "*":
try {
const newConfig = JSON.parse(textBox.value);
for (const key in newConfig) {
Config.config[key] = newConfig[key];
}
Config.convertJSON();
if (newConfig.supportInvidious) {
const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > label > label > input");
checkbox.checked = true;
await invidiousOnClick(checkbox, "supportInvidious");
}
window.location.reload();
} catch (e) {
alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
}
break;
default:
Config.config[option] = textBox.value;
}
}
setTextOption(option, element, textBox.value);
});
// See if anything extra must be done
@@ -531,6 +532,75 @@ function activatePrivateTextChange(element: HTMLElement) {
element.querySelector(".option-hidden-section").classList.remove("hidden");
}
/**
* Function to run when a textbox change is submitted
*
* @param option data-sync value
* @param element main container div
* @param value new text
* @param callbackOnError function to run if confirmMessage was denied
*/
async function setTextOption(option: string, element: HTMLElement, value: string, callbackOnError?: () => void) {
const confirmMessage = element.getAttribute("data-confirm-message");
if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
// See if anything extra must be done
switch (option) {
case "*":
try {
const newConfig = JSON.parse(value);
for (const key in newConfig) {
Config.config[key] = newConfig[key];
}
if (newConfig.supportInvidious) {
const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > div > label > input");
checkbox.checked = true;
await invidiousOnClick(checkbox, "supportInvidious");
}
window.location.reload();
} catch (e) {
alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
}
break;
default:
Config.config[option] = value;
}
} else {
if (typeof callbackOnError == "function")
callbackOnError();
}
}
function downloadConfig() {
const file = document.createElement("a");
const jsonData = JSON.parse(JSON.stringify(Config.cachedSyncConfig));
file.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData)));
file.setAttribute("download", "SponsorBlockConfig.json");
document.body.append(file);
file.click();
file.remove();
}
function uploadConfig(e) {
if (e.target.files.length == 1) {
const file = e.target.files[0];
const reader = new FileReader();
const element = document.querySelector("[data-sync='*']") as HTMLElement;
reader.onload = function(ev) {
setTextOption("*", element, ev.target.result as string, () => {
e.target.value = null;
});
};
reader.readAsText(file);
}
}
/**
* Validates the value used for the database server address.
* Returns null and alerts the user if there is an issue.
@@ -563,12 +633,9 @@ function copyDebugOutputToClipboard() {
language: navigator.language,
extensionVersion: chrome.runtime.getManifest().version
},
config: JSON.parse(JSON.stringify(Config.localConfig)) // Deep clone config object
config: JSON.parse(JSON.stringify(Config.cachedSyncConfig)) // Deep clone config object
};
// Fix segmentTimes data as it is destroyed from the JSON stringify
output.config.segmentTimes = Config.encodeStoredItem(Config.localConfig.segmentTimes);
// Sanitise sensitive user config values
delete output.config.userID;
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)

View File

@@ -1,10 +1,9 @@
import Config from "./config";
import Utils from "./utils";
import { SponsorTime, SponsorHideType, CategoryActionType, ActionType } from "./types";
import { SponsorTime, SponsorHideType, ActionType } from "./types";
import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { getCategoryActionType } from "./utils/categoryUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
const utils = new Utils();
@@ -109,13 +108,17 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//"downloadedSponsorMessageTimes",
"refreshSegmentsButton",
"whitelistButton",
"sbDonate"
"sbDonate",
"sponsorTimesDonateContainer",
"sbConsiderDonateLink",
"sbCloseDonate"
].forEach(id => PageElements[id] = document.getElementById(id));
// Hide donate button if wanted (Safari, or user choice)
if (!showDonationLink()) {
PageElements.sbDonate.style.display = "none";
}
PageElements.sbDonate.addEventListener("click", () => Config.config.donateClicked = Config.config.donateClicked + 1);
//setup click listeners
PageElements.sponsorStart.addEventListener("click", sendSponsorStartMessage);
@@ -126,7 +129,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
unwhitelistChannel();
}
});
PageElements.whitelistForceCheck.addEventListener("click", openOptions);
PageElements.whitelistForceCheck.addEventListener("click", () => {openOptionsAt("behavior")});
PageElements.toggleSwitch.addEventListener("change", function () {
toggleSkipping(!this.checked);
});
@@ -193,6 +196,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.sponsorTimesViewsDisplay.innerText = viewCount.toLocaleString();
PageElements.sponsorTimesViewsContainer.style.display = "unset";
}
showDonateWidget(viewCount);
}
});
@@ -242,6 +247,23 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
getSegmentsFromContentScript(false);
function showDonateWidget(viewCount: number) {
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) {
PageElements.sponsorTimesDonateContainer.style.display = "flex";
PageElements.sbConsiderDonateLink.addEventListener("click", () => {
Config.config.donateClicked = Config.config.donateClicked + 1;
});
PageElements.sbCloseDonate.addEventListener("click", () => {
PageElements.sponsorTimesDonateContainer.style.display = "none";
Config.config.showPopupDonationCount = 100;
});
Config.config.showPopupDonationCount = Config.config.showPopupDonationCount + 1;
}
}
function onTabs(tabs, updating: boolean): void {
messageHandler.sendMessage(tabs[0].id, { message: 'getVideoID' }, function (result) {
if (result !== undefined && result.videoID) {
@@ -263,7 +285,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
return;
}
sponsorTimes = Config.config.segmentTimes.get(currentVideoID) ?? [];
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
updateSegmentEditingUI();
messageHandler.sendMessage(
@@ -338,8 +360,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
// Perform a second update after the config changes take effect as a workaround for a race condition
const removeListener = (listener: typeof lateUpdate) => {
const index = Config.configListeners.indexOf(listener);
if (index !== -1) Config.configListeners.splice(index, 1);
const index = Config.configSyncListeners.indexOf(listener);
if (index !== -1) Config.configSyncListeners.splice(index, 1);
};
const lateUpdate = () => {
@@ -347,7 +369,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
removeListener(lateUpdate);
};
Config.configListeners.push(lateUpdate);
Config.configSyncListeners.push(lateUpdate);
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
setTimeout(() => removeListener(lateUpdate), 200);
@@ -361,7 +383,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
// Only update the segments after a segment was created
if (!creatingSegment) {
sponsorTimes = Config.config.segmentTimes.get(currentVideoID) || [];
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] || [];
}
// Update the UI
@@ -403,6 +425,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
} else if (segmentTimes[i].hidden === SponsorHideType.MinimumDuration) {
//this one is too short
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
} else if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
}
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo);
@@ -411,7 +435,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
} else {
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
(getCategoryActionType(segmentTimes[i].category) !== CategoryActionType.POI
(segmentTimes[i].actionType !== ActionType.Poi
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
: "");
}
@@ -443,8 +467,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg");
downvoteButton.addEventListener("click", () => vote(0, UUID));
//uuid button
const uuidButton = document.createElement("img");
uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
uuidButton.className = "voteButton";
@@ -455,10 +477,49 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
stopAnimation();
});
const hideButton = document.createElement("img");
hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
hideButton.className = "voteButton";
if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
} else {
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
}
hideButton.addEventListener("click", () => {
const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
stopAnimation();
if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
segmentTimes[i].hidden = SponsorHideType.Visible;
} else {
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
segmentTimes[i].hidden = SponsorHideType.Hidden;
}
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{
message: "hideSegment",
type: segmentTimes[i].hidden,
UUID: UUID
}
);
});
});
//add thumbs up, thumbs down and uuid copy buttons to the container
voteButtonsContainer.appendChild(upvoteButton);
voteButtonsContainer.appendChild(downvoteButton);
voteButtonsContainer.appendChild(uuidButton);
if (segmentTimes[i].actionType === ActionType.Skip
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segmentTimes[i].hidden)) {
voteButtonsContainer.appendChild(hideButton);
}
//add click listener to open up vote panel
sponsorTimeButton.addEventListener("click", function () {
@@ -517,6 +578,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
chrome.runtime.sendMessage({ "message": "openConfig" });
}
function openOptionsAt(location) {
chrome.runtime.sendMessage({ "message": "openConfig", "hash": location });
}
function openHelp() {
chrome.runtime.sendMessage({ "message": "openHelp" });
}
@@ -586,8 +651,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//add loading info
addVoteMessage(chrome.i18n.getMessage("Loading"), UUID);
//send the vote message to the tab
chrome.runtime.sendMessage({
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{
message: "submitVote",
type: type,
UUID: UUID
@@ -601,6 +671,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
}
}
);
});
}

View File

@@ -9,6 +9,9 @@ export interface TooltipProps {
bottomOffset?: string
timeout?: number;
opacity?: number;
displayTriangle?: boolean;
showLogo?: boolean;
showGotIt?: boolean;
}
export class Tooltip {
@@ -20,6 +23,9 @@ export class Tooltip {
constructor(props: TooltipProps) {
props.bottomOffset ??= "70px";
props.opacity ??= 0.7;
props.displayTriangle ??= true;
props.showLogo ??= true;
props.showGotIt ??= true;
this.text = props.text;
this.container = document.createElement('div');
@@ -40,11 +46,13 @@ export class Tooltip {
ReactDOM.render(
<div style={{bottom: props.bottomOffset, backgroundColor}}
className="sponsorBlockTooltip" >
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "")} >
<div>
{props.showLogo ?
<img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img>
: null}
<span className="sponsorSkipObject">
{this.text + (props.link ? ". " : "")}
{props.link ?
@@ -57,12 +65,14 @@ export class Tooltip {
: null}
</span>
</div>
{props.showGotIt ?
<button className="sponsorSkipObject sponsorSkipNoticeButton"
style ={{float: "right" }}
onClick={() => this.close()}>
{chrome.i18n.getMessage("GotIt")}
</button>
: null}
</div>,
this.container
)

View File

@@ -31,6 +31,8 @@ export interface FetchResponse {
ok: boolean
}
export type HashedValue = string & { __hashBrand: unknown };
export interface VideoDurationResponse {
duration: number;
}
@@ -49,18 +51,15 @@ export interface CategorySelection {
export enum SponsorHideType {
Visible = undefined,
Downvoted = 1,
MinimumDuration
}
export enum CategoryActionType {
Skippable = "", // Strings are used to find proper language configs
POI = "_POI"
MinimumDuration,
Hidden,
}
export enum ActionType {
Skip = "skip",
Mute = "mute",
Full = "full"
Full = "full",
Poi = "poi"
}
export const ActionTypes = [ActionType.Skip, ActionType.Mute];
@@ -224,3 +223,11 @@ export enum NoticeVisbilityMode {
FadedForAutoSkip = 3,
FadedForAll = 4
}
export type Keybind = {
key: string,
code?: string,
ctrl?: boolean,
alt?: boolean,
shift?: boolean
}

View File

@@ -1,5 +1,5 @@
import Config from "./config";
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration } from "./types";
import Config, { VideoDownvotes } from "./config";
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types";
import * as CompileConfig from "../config.json";
import { findValidElementFromSelector } from "./utils/pageUtils";
@@ -21,6 +21,10 @@ export default class Utils {
"popup.css"
];
/* Used for waitForElement */
waitingMutationObserver:MutationObserver = null;
waitingElements: { selector: string, callback: (element: Element) => void }[] = [];
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
this.backgroundScriptContainer = backgroundScriptContainer;
}
@@ -29,6 +33,41 @@ export default class Utils {
return GenericUtils.wait(condition, timeout, check);
}
/* Uses a mutation observer to wait asynchronously */
async waitForElement(selector: string): Promise<Element> {
return await new Promise((resolve) => {
this.waitingElements.push({
selector,
callback: resolve
});
if (!this.waitingMutationObserver) {
this.waitingMutationObserver = new MutationObserver(() => {
const foundSelectors = [];
for (const { selector, callback } of this.waitingElements) {
const element = document.querySelector(selector);
if (element) {
callback(element);
foundSelectors.push(selector);
}
}
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
if (this.waitingElements.length === 0) {
this.waitingMutationObserver.disconnect();
this.waitingMutationObserver = null;
}
});
this.waitingMutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
});
}
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
return new Promise((resolve) => {
chrome.permissions.contains(permissions, resolve)
@@ -219,6 +258,8 @@ export default class Utils {
localizeHtmlPage(): void {
//Localize by replacing __MSG_***__ meta tags
const localizedMessage = this.getLocalizedMessage(document.title);
if (localizedMessage) document.title = localizedMessage;
const objects = document.getElementsByClassName("sponsorBlockPageBody")[0].children;
for (let j = 0; j < objects.length; j++) {
const obj = objects[j];
@@ -331,9 +372,9 @@ export default class Utils {
findReferenceNode(): HTMLElement {
const selectors = [
"#player-container-id",
"#movie_player",
"#c4-player", // Channel Trailer
"#player-container", // Preview on hover
"#main-panel.ytmusic-player-page", // YouTube music
"#player-container .video-js", // Invidious
".main-video-section > .video-container" // Cloudtube
@@ -343,15 +384,17 @@ export default class Utils {
//for embeds
const player = document.getElementById("player");
referenceNode = player.firstChild as HTMLElement;
if (referenceNode) {
let index = 1;
//find the child that is the video player (sometimes it is not the first)
while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) {
while (index < player.children.length && (!referenceNode.classList?.contains("html5-video-player") || !referenceNode.classList?.contains("ytp-embed"))) {
referenceNode = player.children[index] as HTMLElement;
index++;
}
}
}
return referenceNode;
}
@@ -432,10 +475,10 @@ export default class Utils {
return typeof(browser) !== "undefined";
}
async getHash(value: string, times = 5000): Promise<string> {
if (times <= 0) return "";
async getHash<T extends string>(value: T, times = 5000): Promise<T & HashedValue> {
if (times <= 0) return "" as T & HashedValue;
let hashHex = value;
let hashHex: string = value;
for (let i = 0; i < times; i++) {
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(hashHex).buffer);
@@ -443,6 +486,47 @@ export default class Utils {
hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
return hashHex;
return hashHex as T & HashedValue;
}
async addHiddenSegment(videoID: VideoID, segmentUUID: string, hidden: SponsorHideType) {
if (chrome.extension.inIncognitoContext) return;
const hashedVideoID = (await this.getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const UUIDHash = await this.getHash(segmentUUID, 1);
const allDownvotes = Config.local.downvotedSegments;
const currentVideoData = allDownvotes[hashedVideoID] || { segments: [], lastAccess: 0 };
currentVideoData.lastAccess = Date.now();
const existingData = currentVideoData.segments.find((segment) => segment.uuid === UUIDHash);
if (hidden === SponsorHideType.Visible) {
delete allDownvotes[hashedVideoID];
} else {
if (existingData) {
existingData.hidden = hidden;
} else {
currentVideoData.segments.push({
uuid: UUIDHash,
hidden
});
}
allDownvotes[hashedVideoID] = currentVideoData;
}
const entries = Object.entries(allDownvotes);
if (entries.length > 10000) {
let min: [string, VideoDownvotes] = null;
for (let i = 0; i < entries[0].length; i++) {
if (min === null || entries[i][1].lastAccess < min[1].lastAccess) {
min = entries[i];
}
}
delete allDownvotes[min[0]];
}
Config.forceLocalUpdate("downvotedSegments");
}
}

View File

@@ -1,11 +1,10 @@
import { ActionType, Category, CategoryActionType, SponsorTime } from "../types";
import { ActionType, Category, SponsorTime } from "../types";
export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): string {
const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments"
: "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category);
if (autoSkip) {
let messageId = "";
if (getCategoryActionType(segments[0].category) === CategoryActionType.Skippable) {
switch (segments[0].actionType) {
case ActionType.Skip:
messageId = "skipped";
@@ -13,15 +12,14 @@ export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): str
case ActionType.Mute:
messageId = "muted";
break;
}
} else {
case ActionType.Poi:
messageId = "skipped_to_category";
break;
}
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
} else {
let messageId = "";
if (getCategoryActionType(segments[0].category) === CategoryActionType.Skippable) {
switch (segments[0].actionType) {
case ActionType.Skip:
messageId = "skip_category";
@@ -29,23 +27,15 @@ export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): str
case ActionType.Mute:
messageId = "mute_category";
break;
}
} else {
case ActionType.Poi:
messageId = "skip_to_category";
break;
}
return chrome.i18n.getMessage(messageId).replace("{0}", categoryName);
}
}
export function getCategoryActionType(category: Category): CategoryActionType {
if (category.startsWith("poi_")) {
return CategoryActionType.POI;
} else {
return CategoryActionType.Skippable;
}
}
export function getCategorySuffix(category: Category): string {
if (category.startsWith("poi_")) {
return "_POI";

View File

@@ -1,5 +1,44 @@
import Config from "../config";
import { Keybind } from "../types";
export function showDonationLink(): boolean {
return navigator.vendor !== "Apple Computer, Inc." && Config.config.showDonationLink;
}
export function keybindEquals(first: Keybind, second: Keybind): boolean {
if (first == null || second == null ||
Boolean(first.alt) != Boolean(second.alt) || Boolean(first.ctrl) != Boolean(second.ctrl) || Boolean(first.shift) != Boolean(second.shift) ||
first.key == null && first.code == null || second.key == null && second.code == null)
return false;
if (first.code != null && second.code != null)
return first.code === second.code;
if (first.key != null && second.key != null)
return first.key.toUpperCase() === second.key.toUpperCase();
return false;
}
export function formatKey(key: string): string {
if (key == null)
return "";
else if (key == " ")
return "Space";
else if (key.length == 1)
return key.toUpperCase();
else
return key;
}
export function keybindToString(keybind: Keybind): string {
if (keybind == null || keybind.key == null)
return "";
let ret = "";
if (keybind.ctrl)
ret += "Ctrl+";
if (keybind.alt)
ret += "Alt+";
if (keybind.shift)
ret += "Shift+";
return ret += formatKey(keybind.key);
}

View File

@@ -44,7 +44,30 @@ function getErrorMessage(statusCode: number, responseText: string): string {
return errorMessage + postFix;
}
/* Gets percieved luminance of a color */
function getLuminance(color: string): number {
const {r, g, b} = hexToRgb(color);
return Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b));
}
/* From https://stackoverflow.com/a/5624139 */
function hexToRgb(hex: string): {r: number, g: number, b: number} {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
export const GenericUtils = {
wait,
getErrorMessage
getErrorMessage,
getLuminance
}

View File

@@ -41,3 +41,24 @@ function findValidElementFromGenerator<T>(objects: T[] | NodeListOf<HTMLElement>
return null;
}
export function getHashParams(): Record<string, unknown> {
const windowHash = window.location.hash.slice(1);
if (windowHash) {
const params: Record<string, unknown> = windowHash.split('&').reduce((acc, param) => {
const [key, value] = param.split('=');
const decoded = decodeURIComponent(value);
try {
acc[key] = decoded?.match(/{|\[/) ? JSON.parse(decoded) : value;
} catch (e) {
console.error(`Failed to parse hash parameter ${key}: ${value}`);
}
return acc;
}, {});
return params;
}
return {};
}

View File

@@ -1,8 +1,27 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const webpack = require("webpack");
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const BuildManifest = require('./webpack.manifest');
const srcDir = '../src/';
const fs = require("fs");
const edgeLanguages = [
"de",
"en",
"es",
"fr",
"pl",
"pt_BR",
"ro",
"ru",
"sk",
"sv",
"tr",
"uk",
"zh_CN"
]
module.exports = env => ({
entry: {
@@ -15,7 +34,6 @@ module.exports = env => ({
},
output: {
path: path.join(__dirname, '../dist/js'),
filename: '[name].js'
},
optimization: {
splitChunks: {
@@ -46,6 +64,41 @@ module.exports = env => ({
ignore: ['manifest.json'],
},
context: './public',
filter: async (path) => {
if (path.match(/\/_locales\/.+/)) {
if (env.browser.toLowerCase() === "edge"
&& !edgeLanguages.includes(path.match(/(?<=\/_locales\/)[^/]+(?=\/[^/]+$)/)[0])) {
return false;
}
const data = await fs.promises.readFile(path);
const parsed = JSON.parse(data.toString());
return parsed.fullName && parsed.Description;
} else {
return true;
}
},
transform(content, path) {
if (path.match(/\/_locales\/.+/)) {
const parsed = JSON.parse(content.toString());
if (env.browser.toLowerCase() === "safari") {
parsed.fullName.message = parsed.fullName.message.match(/^.+(?= -)/)?.[0] || parsed.fullName.message;
if (parsed.fullName.message.length > 50) {
parsed.fullName.message = parsed.fullName.message.slice(0, 47) + "...";
}
parsed.Description.message = parsed.Description.message.match(/^.+(?=\. )/)?.[0] || parsed.Description.message;
if (parsed.Description.message.length > 80) {
parsed.Description.message = parsed.Description.message.slice(0, 77) + "...";
}
}
return Buffer.from(JSON.stringify(parsed));
}
return content;
}
}
]
}),

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

View File

@@ -1,13 +1,15 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const webpack = require("webpack");
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const validateOptions = require('schema-utils');
const { validate } = require('schema-utils');
const fs = require('fs');
const manifest = require("../manifest/manifest.json");
const firefoxManifestExtra = require("../manifest/firefox-manifest-extra.json");
const chromeManifestExtra = require("../manifest/chrome-manifest-extra.json");
const safariManifestExtra = require("../manifest/safari-manifest-extra.json");
const betaManifestExtra = require("../manifest/beta-manifest-extra.json");
const firefoxBetaManifestExtra = require("../manifest/firefox-beta-manifest-extra.json");
@@ -29,20 +31,24 @@ const schema = {
class BuildManifest {
constructor (options = {}) {
validateOptions(schema, options, "Build Manifest Plugin");
validate(schema, options, "Build Manifest Plugin");
this.options = options;
}
apply(compiler) {
apply() {
const distFolder = path.resolve(__dirname, "../dist/");
const distManifestFile = path.resolve(distFolder, "manifest.json");
// Add missing manifest elements
if (this.options.browser.toLowerCase() === "firefox") {
mergeObjects(manifest, firefoxManifestExtra);
} else if (this.options.browser.toLowerCase() === "chrome" || this.options.browser.toLowerCase() === "chromium") {
} else if (this.options.browser.toLowerCase() === "chrome"
|| this.options.browser.toLowerCase() === "chromium"
|| this.options.browser.toLowerCase() === "edge") {
mergeObjects(manifest, chromeManifestExtra);
} else if (this.options.browser.toLowerCase() === "safari") {
mergeObjects(manifest, safariManifestExtra);
}
if (this.options.stream === "beta") {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const merge = require('webpack-merge');
const common = require('./webpack.common.js');