Compare commits

...

91 Commits
5.0.7 ... 5.1.7

Author SHA1 Message Date
Ajay Ramachandran
ec12c1d324 bump version 2022-11-22 17:29:46 -05:00
Ajay
4447ff3142 Fix issues with mutes ending when highlights start 2022-11-09 12:45:11 -05:00
Ajay
00ef3856ca Fix race condition causing full video label not to display 2022-11-08 13:50:56 -05:00
Ajay
cd7d74fb33 Fix category pill text on new YouTube layout 2022-11-08 13:38:19 -05:00
Ajay
ce3f77ed20 Better key moments check by not importing when no 0 second chapter 2022-11-07 21:15:04 -05:00
Ajay Ramachandran
d347f4e575 bump version 2022-11-06 13:19:56 -05:00
Ajay Ramachandran
1c5802a5e2 New Crowdin updates (#1574) 2022-11-06 13:19:35 -05:00
Ajay Ramachandran
4bc7ce7cd0 Merge pull request #1577 from caneleex/patch/outro-end-time
Don't overwrite endcards time upon submitting
2022-11-06 13:18:14 -05:00
caneleex
3c6989f67d Don't overwrite endcards time upon submitting 2022-11-06 19:15:59 +01:00
Ajay Ramachandran
b40749717f Merge pull request #1550 from mchangrh/stricter-ts
add noFallThrough, inplicitReturn, update packages
2022-11-04 17:06:36 -04:00
Ajay
4596f3ab0d bump version 2022-11-04 17:04:35 -04:00
mini-bomba
89e87cd74d Don't update the whole segment list on time update (#1569)
Update segment element classes instead.
This probably is more efficient than what we're doing currently.
Also, this seems to fix a bug where the vote confirmation/error msg is removed immediately
2022-11-04 17:02:54 -04:00
Ajay Ramachandran
311c4caf2b New Crowdin updates (#1544) 2022-11-04 17:02:16 -04:00
Ajay
0e0ae9165e Fix segments really close to eachother not skipping
Also combine segments that are closer than skip buffer

Fix #1510
2022-11-04 17:01:23 -04:00
Ajay
ff2cec699f adjust skip to highlight button behavior to attempt to fix issues 2022-11-04 16:18:15 -04:00
Ajay
1775835392 Add hiding skip to highlight
Fix #1530
2022-11-04 16:05:09 -04:00
Ajay
2c5db670a4 Safety on getChapterButton
Fix #1573
2022-11-04 15:44:14 -04:00
Ajay Ramachandran
4b4743f1f7 Merge pull request #1531 from mchangrh/music-progressbar
add YT Music progress bar selector
2022-11-04 15:41:51 -04:00
Ajay Ramachandran
7719ca7eca Merge pull request #1561 from mchangrh/gha-update
bump actions to node 16
2022-11-04 15:40:05 -04:00
Ajay Ramachandran
f4f7df9f2b Merge pull request #1564 from mini-bomba/userinfo-publicID
Use publicID instead of privateID for /userInfo calls
2022-11-04 15:39:44 -04:00
Ajay
95551de09a Fix precise seeking notice offset with hover preview 2022-11-01 14:41:43 -04:00
Ajay Ramachandran
8653059b13 bump version 2022-10-30 22:00:35 -04:00
Ajay
b3afd0403e Add configuration for segment failed to fetch warning 2022-10-30 20:38:48 -04:00
Ajay
6db498ccb1 Fix key moments check not working when multiple videos present 2022-10-30 20:36:05 -04:00
Ajay
ef8c5f58c5 Fix scrubbing bar missing when chapter bar using % widths 2022-10-30 14:49:19 -04:00
Ajay
71998831ee bump version 2022-10-30 13:23:12 -04:00
Ajay
8f19d3e83c Fix segment failed to fetch warning appearing for 404 2022-10-30 13:23:02 -04:00
mini-bomba
ea73a92fb7 Use publicID instead of privateID for /userInfo calls
This should reduce the load on the server a bit, as it will no longer have to compute the publicID for each sponsorblock user.
This also reduces the list of actions that leak the privateID to the server.
2022-10-29 13:20:51 +02:00
Ajay
d68c3659be bump version 2022-10-27 21:47:00 -04:00
Ajay
715bcb6bd3 Added error when segments haven't loaded and improved popup message
Resolves #1553
2022-10-27 21:46:47 -04:00
Ajay
fea8a9a37e Disable virtual time in firefox again 2022-10-27 21:39:06 -04:00
Michael C
01eeb28b60 bump actions to node 16 2022-10-27 03:26:08 -04:00
Michael C
44d4dd54aa return undefined instead of resolving void 2022-10-21 02:41:01 -04:00
Michael C
27bb6045bc make tab open listeners non-async 2022-10-21 02:37:48 -04:00
Michael M. Chang
0610eea53d Update src/popup.ts
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2022-10-21 01:01:25 -04:00
Michael C
1fcfea9fd5 add noFallThrough, inplicitReturn, update packages 2022-10-20 22:13:51 -04:00
Ajay
aec5845bce Show import button for errors 2022-10-17 09:25:11 -04:00
Ajay
5209c0ea04 Import chapters as chooseACategory if chapters is disabled 2022-10-17 09:23:59 -04:00
Ajay
b52132e311 Open submission men after importing 2022-10-17 08:52:55 -04:00
Ajay
c2e731ef89 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-17 08:50:56 -04:00
Ajay
a48269f254 Import short category names too 2022-10-17 08:50:55 -04:00
Ajay Ramachandran
562adb9d55 bump version 2022-10-16 22:41:46 -04:00
Ajay Ramachandran
851ceb553d New translations messages.json (Korean) (#1529) 2022-10-16 22:41:31 -04:00
Ajay
4bd7e9ab34 Fix progress bar with 0px chapters 2022-10-14 18:23:40 -04:00
Michael C
784c1db0c1 add YT Music progress bar selector 2022-10-13 00:55:55 -04:00
Ajay
97fc8174b9 Add flag to disable virtual time 2022-10-12 23:48:20 -04:00
Ajay
9849c34ed3 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-12 23:46:02 -04:00
Ajay
56be762686 Add back virtual time to firefox and fix it with playback speed 2022-10-12 23:46:00 -04:00
Ajay Ramachandran
1648e104e1 bump version 2022-10-11 20:57:54 -04:00
Ajay Ramachandran
c4d7c4511e New Crowdin updates (#1511) 2022-10-11 20:57:36 -04:00
Ajay Ramachandran
cd78c46ef8 Merge pull request #1521 from mini-bomba/popup
Update popup on segment updates + some code cleanup
2022-10-11 20:57:21 -04:00
Ajay
758b6f18db update buttons when single time segment is edited 2022-10-11 18:30:53 -04:00
Ajay
6d05b2a849 Set endtime by default for outro 2022-10-11 18:24:52 -04:00
Ajay
4729268083 Reimport chapters if they are found to have changed 2022-10-11 17:59:46 -04:00
mini-bomba
b7a574fc16 Clear segment list & show loading animation in popup on video change
also removed the creatingSegment variable - results in "Start/End Segment Now" correctly updating when using buttons on the controls panel instead
also the "refreshSegments" message no longer sends a response, as we send an update manually now
2022-10-11 18:38:00 +02:00
mini-bomba
c8cbd893f7 Don't hide the popup on video change
also hide the info button if the popup was open when setting up buttons
2022-10-11 16:08:07 +02:00
mini-bomba
11b01fd3dd revert reordering of imports 2022-10-11 15:41:19 +02:00
Ajay
85e3d3bc18 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-11 00:08:16 -04:00
Ajay
47ea8cd017 Fix unused import 2022-10-11 00:08:14 -04:00
Ajay Ramachandran
61e78eb668 Merge pull request #1524 from caneleex/patch/full-type-skip
don't append skip button for full video labels
2022-10-11 00:07:38 -04:00
Ajay
5ebd44c0c7 update category pill for react 18 2022-10-11 00:04:02 -04:00
Ajay
9888dcc323 Fix chapter import not working with 0 time 2022-10-10 23:37:48 -04:00
Ajay
49a166a6b2 Fix key moments importing as chapters 2022-10-10 23:34:34 -04:00
caneleex
2bd1271575 don't append skip button for full video labels 2022-10-10 21:21:57 +02:00
Ajay
5d62b11a6d Enable chapters if not enabled after redeem successful 2022-10-10 00:38:09 -04:00
Ajay
f4cac58322 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-09 16:44:55 -04:00
Ajay
4a532e751c Add class for other extensions when displaying non chapter title 2022-10-09 16:44:53 -04:00
mini-bomba
4a3b33cb85 Dedupe & clean up popup -> content script communication code 2022-10-08 19:07:58 +02:00
mini-bomba
78e9f41854 Update popup when new segments are fetched 2022-10-08 18:34:20 +02:00
mini-bomba
48cfee57b7 Return null on fails in getYouTubeVideoID and add type annotations to videoIDChange 2022-10-08 16:40:48 +02:00
Ajay Ramachandran
b2ef9e5d6e Merge pull request #1518 from mchangrh/interface-delimiter
Interface delimiter
2022-10-08 10:07:58 -04:00
Ajay Ramachandran
c42ebce6e3 Merge pull request #1520 from mini-bomba/fix/empty-unsubmitted-video
Remove the unsubmittedSegments entry when removing the last segment
2022-10-08 10:07:14 -04:00
mini-bomba
bc1d6006eb Remove the unsubmittedSegments entry when removing the last segment 2022-10-08 13:10:25 +02:00
Ajay Ramachandran
727d925879 Merge pull request #1519 from ajayyy/ci/oss_attribution
Update OSS Attribution
2022-10-07 22:56:46 -04:00
github-actions[bot]
5d48d9ac74 Update OSS Attribution 2022-10-08 02:55:47 +00:00
Ajay Ramachandran
83ea183f58 Merge pull request #1517 from mchangrh/react-18
React 18
2022-10-07 22:54:51 -04:00
Michael C
a098858035 force delimiters to follow semi 2022-10-07 20:51:58 -04:00
Michael C
81e85c19ae fix typings and revert spacing 2022-10-07 20:06:03 -04:00
Michael C
fda4a03541 fix more components 2022-10-07 19:51:05 -04:00
Michael C
55c84662c0 add some changes that are rendering properly 2022-10-07 19:28:29 -04:00
Ajay Ramachandran
5c9e06468e Merge pull request #1515 from mchangrh/dependency-update
update dependencies (skips react (again))
2022-10-07 17:24:58 -04:00
Ajay
a3d38c57d7 Fix chapter icon with ytp big mode 2022-10-07 17:20:19 -04:00
Michael C
7ec09dd385 update dependencies (skips react) 2022-10-07 17:19:36 -04:00
Ajay
bb7f069254 Only check import duplicates against unsubmitted segments 2022-10-07 12:22:39 -04:00
Ajay
61fc1d2ed3 Fix zero start time breaking 2022-10-07 12:19:42 -04:00
Ajay
dabc63af73 Deletew all custom chapter bars we don't know about when clearing preview bar 2022-10-07 12:02:14 -04:00
Ajay
08181c1d5f Clear bote buttons when clearing preview bar 2022-10-07 11:59:56 -04:00
Ajay
03cd1b535b Count skips for chapters when viewed 2022-10-05 15:39:51 -04:00
Ajay
8cc3843ada Run chapters clear when preview bar cleared 2022-10-05 02:19:32 -04:00
Ajay
c4701092f4 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock 2022-10-05 02:15:32 -04:00
Ajay
63ef9b44c7 Make sure chapters are invalid if regex fails 2022-10-05 02:15:31 -04:00
83 changed files with 5514 additions and 3243 deletions

View File

@@ -23,7 +23,8 @@
"@typescript-eslint/no-unused-vars": "error",
"no-self-assign": "off",
"@typescript-eslint/no-empty-interface": "off",
"react/prop-types": [2, { "ignore": ["children"] }]
"react/prop-types": [2, { "ignore": ["children"] }],
"@typescript-eslint/member-delimiter-style": "warn"
},
"settings": {
"react": {

View File

@@ -10,8 +10,8 @@ jobs:
steps:
# Initialization
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
@@ -25,7 +25,7 @@ jobs:
# Create Chrome artifacts
- name: Create Chrome artifacts
run: npm run build:chrome
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: ChromeExtension
path: dist
@@ -37,7 +37,7 @@ jobs:
# Create Firefox artifacts
- name: Create Firefox artifacts
run: npm run build:firefox
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: FirefoxExtension
path: dist
@@ -48,7 +48,7 @@ jobs:
# Create Beta artifacts (Builds with the name changed to beta)
- name: Create Chrome Beta artifacts
run: npm run build:chrome -- --env stream=beta
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: ChromeExtensionBeta
path: dist
@@ -58,7 +58,7 @@ jobs:
- name: Create Firefox Beta artifacts
run: npm run build:firefox -- --env stream=beta
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: FirefoxExtensionBeta
path: dist

View File

@@ -12,8 +12,8 @@ jobs:
steps:
# Initialization
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
@@ -23,7 +23,7 @@ jobs:
# Create Chrome artifacts
- name: Create Chrome artifacts
run: npm run build:chrome
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: ChromeExtension
path: dist
@@ -34,7 +34,7 @@ jobs:
# Create Firefox artifacts
- name: Create Firefox artifacts
run: npm run build:firefox
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: FirefoxExtension
path: dist
@@ -44,7 +44,7 @@ jobs:
# Create Beta artifacts (Builds with the name changed to beta)
- name: Create Chrome Beta artifacts
run: npm run build:chrome -- --env stream=beta
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: ChromeExtensionBeta
path: dist
@@ -55,7 +55,7 @@ jobs:
# Create Safari artifacts
- name: Create Safari artifacts
run: npm run build:safari
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: SafariExtension
path: dist
@@ -66,7 +66,7 @@ jobs:
run: rm -rf ./dist
- name: Create Edge artifacts
run: npm run build:edge
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: EdgeExtension
path: dist
@@ -75,35 +75,35 @@ jobs:
# Upload each release asset
- name: Upload ChromeExtension to release
uses: Shopify/upload-to-release@master
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
with:
args: builds/ChromeExtension.zip
name: ChromeExtension.zip
path: ./builds/ChromeExtension.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload ChromeExtensionBeta to release
uses: Shopify/upload-to-release@master
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
with:
args: builds/ChromeExtensionBeta.zip
name: ChromeExtensionBeta.zip
path: ./builds/ChromeExtensionBeta.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload FirefoxExtension to release
uses: Shopify/upload-to-release@master
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
with:
args: builds/FirefoxExtension.zip
name: FirefoxExtension.zip
path: ./builds/FirefoxExtension.zip
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SafariExtension to release
uses: Shopify/upload-to-release@master
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
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
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
with:
args: builds/EdgeExtension.zip
name: EdgeExtension.zip
@@ -113,7 +113,7 @@ jobs:
# Firefox Beta
- name: Create Firefox Beta artifacts
run: npm run build:firefox -- --env stream=beta
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: FirefoxExtensionBeta
path: dist
@@ -130,13 +130,13 @@ jobs:
run: sudo apt-get install rename
- name: Rename signed file
run: cd ./web-ext-artifacts ; rename 's/.*/FirefoxSignedInstaller.xpi/' *
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: FirefoxExtensionSigned.xpi
path: ./web-ext-artifacts/FirefoxSignedInstaller.xpi
- name: Upload FirefoxSignedInstaller.xpi to release
uses: Shopify/upload-to-release@master
uses: Shopify/upload-to-release@07611424e04f1475ddf550e1c0dd650b867d5467
with:
args: web-ext-artifacts/FirefoxSignedInstaller.xpi
name: FirefoxSignedInstaller.xpi

View File

@@ -9,8 +9,8 @@ jobs:
steps:
# Initialization
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci

View File

@@ -12,9 +12,8 @@ jobs:
update-oss:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install and generate attribution
@@ -25,7 +24,8 @@ jobs:
mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt
- name: Create pull request to update list
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
uses: peter-evans/create-pull-request@b4d51739f96fca8047ad065eccef63442d8e99f7
# v4.2.0
with:
commit-message: Update OSS Attribution
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

View File

@@ -8,8 +8,7 @@ jobs:
check-list:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Download instance list
run: |
wget https://api.invidious.io/instances.json -O ci/data.json
@@ -19,7 +18,8 @@ jobs:
run: npm run ci:invidious
- name: Create pull request to update list
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
uses: peter-evans/create-pull-request@b4d51739f96fca8047ad065eccef63442d8e99f7
# v4.2.0
with:
commit-message: Update Invidious List
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

View File

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

File diff suppressed because one or more lines are too long

4882
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,36 +4,36 @@
"description": "",
"main": "background.js",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/chrome": "^0.0.193",
"@types/chrome": "^0.0.199",
"@types/firefox-webext-browser": "^94.0.1",
"@types/jest": "^28.1.6",
"@types/react": "^17.0.47",
"@types/react-dom": "^17.0.17",
"@types/selenium-webdriver": "^4.1.2",
"@types/wicg-mediasession": "^1.1.3",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"chromedriver": "^103.0.0",
"concurrently": "^7.3.0",
"@types/jest": "^29.2.0",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/selenium-webdriver": "^4.1.6",
"@types/wicg-mediasession": "^1.1.4",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"chromedriver": "^106.0.1",
"concurrently": "^7.4.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.20.0",
"eslint-plugin-react": "^7.30.1",
"eslint": "^8.25.0",
"eslint-plugin-react": "^7.31.10",
"fork-ts-checker-webpack-plugin": "^7.2.13",
"jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.0",
"jest": "^29.2.1",
"jest-environment-jsdom": "^29.2.1",
"rimraf": "^3.0.2",
"schema-utils": "^4.0.0",
"selenium-webdriver": "^4.3.1",
"selenium-webdriver": "^4.5.0",
"speed-measure-webpack-plugin": "^1.5.0",
"ts-jest": "^28.0.7",
"ts-loader": "^9.3.1",
"ts-jest": "^29.0.3",
"ts-loader": "^9.4.1",
"ts-node": "^10.9.1",
"typescript": "4.7",
"web-ext": "^7.1.1",
"typescript": "4.8",
"web-ext": "^7.3.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-merge": "^5.8.0"

View File

@@ -25,6 +25,9 @@
"Segments": {
"message": "أجزاء"
},
"Chapters": {
"message": "الفصول"
},
"upvoteButtonInfo": {
"message": "التصويت على هذا الإرسال"
},

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Възникна грешка с връзката. Код на грешката: "
},
"segmentsStillLoading": {
"message": "Сегментите все още се зареждат..."
},
"clearTimes": {
"message": "Изчистване на сегментите"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Споделяне като URL"
},
"segmentFetchFailureWarning": {
"message": "Внимание: сървърът все още не е отговорил със сегменти. В действителност може да има вече изпратени сегменти в този видеоклип, но просто не сте ги получили поради проблеми със сървъра."
}
}

View File

@@ -35,6 +35,9 @@
"message": "অংশসমূহকে অধ্যায়সমূহতে পরিণত করুন",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "ভিডিও সময়ের পাশে বর্তমান সেগমেন্ট দেখান"
},
"upvoteButtonInfo": {
"message": "এই জমাটিকে সমর্থন করুন"
},
@@ -113,6 +116,9 @@
"connectionError": {
"message": "একটি সংযোগ ত্রুটি ঘটেছে. ভুল সংকেত: "
},
"segmentsStillLoading": {
"message": "সেগমেন্ট এখনো লোড হচ্ছে..."
},
"clearTimes": {
"message": "বিভাগগুলি পরিষ্কার করুন"
},
@@ -122,6 +128,9 @@
"closePopup": {
"message": "পপআপ বন্ধ করুন"
},
"closeIcon": {
"message": "বন্ধ করার চিহ্ন"
},
"SubmitTimes": {
"message": "সেগমেন্ট জমা দিন"
},
@@ -414,9 +423,18 @@
"statusReminder": {
"message": "সার্ভারের স্ট্যাটাস এর জন্য status.sponsor.ajay.app দেখুন করুন।"
},
"changeUserID": {
"message": "‌আপনার ব্যক্তিগত ব্যবহারকারী আইডি ইম্পোর্ট/এক্সপোর্ট করুন"
},
"whatChangeUserID": {
"message": "এটি ব্যক্তিগত রাখা উচিত। এটি একটি পাসওয়ার্ডের মতো এবং কারও সাথে ভাগ করা উচিত নয়। কারও যদি এটি থাকে তবে তারা আপনার ছদ্মবেশ ধারণ করতে পারে। আপনি যদি আপনার পাবলিক ইউজারআইডি খুঁজছেন তবে পপআপে ক্লিপবোর্ড আইকনটি ক্লিক করুন।"
},
"setUserID": {
"message": "ব্যক্তিগত ব্যবহারকারী আইডি সেট করুন"
},
"userIDChangeWarning": {
"message": "সতর্কতা: ব্যক্তিগত ব্যবহারকারী আইডি পরিবর্তন চিরস্থায়ী। আপনি কি নিশ্চিত যে আপনি এটি করত এ চান? এক্ষেত্রে আপনার পুরাতন আইডির ব্যাকআপ নিশ্চিত করুন। "
},
"createdBy": {
"message": "সৃষ্টি করেছেন"
},
@@ -563,12 +581,19 @@
"message": "থেকে",
"description": "Used between segments. Example: 1:20 to 1:30"
},
"CopiedExclamation": {
"message": "কপি হয়েছে!",
"description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
},
"category_sponsor": {
"message": "স্পন্সর"
},
"category_sponsor_description": {
"message": "পেইড প্রমোশন, পেইড রেফারেল এবং সরাসরি বিজ্ঞাপন। নিজের পছন্দসই কারণ/স্রষ্টা/ওয়েবসাইট/পণ্যগুলিতে স্ব-প্রচার বা বিনামূল্যে প্রচারের জন্য নয়।"
},
"category_sponsor_guideline1": {
"message": "পেইড প্রচারণা"
},
"category_selfpromo": {
"message": "বিনা অর্থপ্রাপ্ত/স্ব-প্রচার"
},
@@ -603,6 +628,9 @@
"category_intro_short": {
"message": "ইন্টারমিশন"
},
"category_intro_guideline1": {
"message": "মূল কনটেন্ট ব্যতীত ব্যবধান"
},
"category_outro": {
"message": "এন্ডকার্ডস/ক্রেডিট"
},
@@ -612,6 +640,9 @@
"category_preview": {
"message": "প্রিভিউ/রিক্যাপ"
},
"category_preview_guideline2": {
"message": "আগের ভিডিওর রিক্যাপ"
},
"category_filler": {
"message": "ফিলার ট্যানজেন্ট/জোকস"
},
@@ -633,6 +664,9 @@
"category_poi_highlight_description": {
"message": "ভিডিওর অংশটি যা বেশিরভাগ লোকেরা খুঁজছেন। \"ভিডিওটি x এ শুরু হয়\" মন্তব্যের মতো।"
},
"category_chapter": {
"message": "অধ্যায়"
},
"category_livestream_messages": {
"message": "লাইভস্ট্রিম: অনুদান/বার্তা পাঠ"
},
@@ -663,6 +697,9 @@
"showOverlay_full": {
"message": "লেবেল দেখান"
},
"showOverlay_chapter": {
"message": "অধ্যায়গুলো দেখান"
},
"autoSkipOnMusicVideos": {
"message": "যখন অ-সংগীত বিভাগ থাকে তখন স্বয়ংক্রিয়ভাবে সমস্ত বিভাগগুলি এড়িয়ে যান"
},
@@ -718,6 +755,10 @@
"bracketEnd": {
"message": "(শেষ)"
},
"End": {
"message": "সমাপ্ত",
"description": "Button that skips to the end of a segment"
},
"hiddenDueToDownvote": {
"message": "লুক্কায়িতঃ ডাউনভোট"
},
@@ -755,6 +796,13 @@
"downvoteDescription": {
"message": "সময় ভুল দেওয়া হয়েছে"
},
"incorrectVote": {
"message": "সঠিক নয়"
},
"harmfulVote": {
"message": "ক্ষতিকর",
"description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
},
"incorrectCategory": {
"message": "বিভাগ পরিবর্তন করুন"
},
@@ -797,6 +845,12 @@
"hideForever": {
"message": "চিরকালের জন্য এই বিষয়বস্তু লুকান"
},
"questionButton": {
"message": "আমার একটি প্রশ্ন আছে"
},
"warningConfirmButton": {
"message": "আমি কারণ বুঝেছি"
},
"Donate": {
"message": "অনুদান"
},
@@ -869,6 +923,9 @@
"LearnMore": {
"message": "আরও জানুন"
},
"FullDetails": {
"message": "সম্পূর্ণ বিস্তারিত"
},
"CopyDownvoteButtonInfo": {
"message": "ডাউনভোট করে আপনার জন্য পুনরায় জমা দেওয়ার জন্য একটি স্থানীয় অনুলিপি তৈরি করে"
},
@@ -893,6 +950,12 @@
"hideSegment": {
"message": "অংশ আড়াল করুন"
},
"skipSegment": {
"message": "সেগমেন্ট এড়িয়ে যান"
},
"playChapter": {
"message": "অধ্যায় চালু করুন"
},
"SponsorTimeEditScrollNewFeature": {
"message": "দ্রুত সময়টি পরিবর্কতন করতে সম্পাদনা বাক্সে ঘুরে দেখার সময় আপনার মাউস এর হুইয়িলটি ব্যবহার করুন। কন্ট্রল বা শিফট কী এর সংমিশ্রণগুলি পরিবর্তনগুলি আরো নিখুতভাবে টিউন করতে ব্যবহার করা যেতে পারে।"
},
@@ -949,5 +1012,33 @@
},
"openOptionsPage": {
"message": "বিকল্প পাতা খুলুন"
},
"resetToDefault": {
"message": "সেটিংস ডিফল্টে ফিরিয়ে নিন"
},
"exportSegments": {
"message": "সেগমেন্ট রপ্তানি করুন"
},
"importSegments": {
"message": "সেগমেন্ট আমদানী করুন"
},
"Import": {
"message": "আমদানী",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"chooseACountry": {
"message": "দেশ নির্বাচন করুন"
},
"videosSingular": {
"message": "ভিডিও",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "ভিডিওগুলি",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "সকল সেগমেন্ট পরিস্কার করুন",
"description": "Label for a button in settings"
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Došlo k chybě připojení. Kód chyby: "
},
"segmentsStillLoading": {
"message": "Segmenty se stále načítají..."
},
"clearTimes": {
"message": "Vymazat segmenty"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Sdílet jako URL"
},
"segmentFetchFailureWarning": {
"message": "Varování: Server zatím neodpověděl se segmenty. U tohoto videa se již mohou nacházet odeslané segmenty, ale vy jste je neobdrželi kvůli chybě na serveru."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Der opstod en forbindelsesfejl. Fejlkode: "
},
"segmentsStillLoading": {
"message": "Segmenter indlæses stadig..."
},
"clearTimes": {
"message": "Ryd Segmenter"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Del som URL"
},
"segmentFetchFailureWarning": {
"message": "Advarsel: Serveren har endnu ikke svaret med segmenter. Der kan faktisk være segmenter på denne video, der allerede er indsendt, men du har bare ikke modtaget dem på grund af problemer med serveren."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Ein Verbindungsfehler ist aufgetreten. Fehlermeldung: "
},
"segmentsStillLoading": {
"message": "Segmente werden noch geladen..."
},
"clearTimes": {
"message": "Alle Segmente löschen"
},
@@ -369,7 +372,7 @@
"description": "Example: Sponsor Skipped"
},
"muted": {
"message": "{0} stumm geschaltet",
"message": "{0} stummgeschalten",
"description": "Example: Sponsor Muted"
},
"skipped_to_category": {
@@ -1214,7 +1217,7 @@
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Alle Segmente löschen",
"message": "Lösche alle Segmente",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Als URL teilen"
},
"segmentFetchFailureWarning": {
"message": "Warnung: Der Server hat noch nicht mit Segmenten geantwortet. Möglicherweise gibt es bereits Segmente zu diesem Video, aber du hast sie aufgrund von Problemen mit dem Server noch nicht erhalten."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "A connection error has occurred. Error code: "
},
"segmentsStillLoading": {
"message": "Segments still loading..."
},
"clearTimes": {
"message": "Clear Segments"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Share as URL"
},
"segmentFetchFailureWarning": {
"message": "Warning: The server hasn't responded with segments yet. There might actually be segments on this video already submitted but you just haven't recieved them due to issues with the server."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Ha ocurrido un error de conexión. Código de error: "
},
"segmentsStillLoading": {
"message": "Los segmentos aún se están cargando..."
},
"clearTimes": {
"message": "Borrar Segmentos"
},
@@ -1135,7 +1138,7 @@
"message": "¡Canje exitoso!"
},
"redeemFailed": {
"message": "La clave de la licencia no es válida"
"message": "La clave de la licencia es inválida"
},
"hideUpsells": {
"message": "Ocultar opciones no disponibles sin pago extra"
@@ -1153,18 +1156,18 @@
"message": "Seleccione su país"
},
"alreadyDonated": {
"message": "Si has donado cualquier cantidad antes del presente, puedes canjear el acceso gratuito vía correo electrónico:",
"message": "Si has donado cualquier cantidad antes de ahora, puedes canjear el acceso gratuito vía correo electrónico:",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Si no puedes permitirte comprar una licencia, haz clic en {here} para ver si eres elegible para un descuento",
"message": "Si no puedes permitirte comprar una licencia, haz clic {aquí} para ver si eres elegible para un descuento",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Iniciar sesión con Patreon"
},
"redeem": {
"message": "Canje"
"message": "Canjear"
},
"joinOnPatreon": {
"message": "Danos tu apoyo en Patreon"
@@ -1194,7 +1197,7 @@
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "Actualmente no tiene segmentos no enviados",
"message": "Actualmente no tiene segmentos sin enviar",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Compartir como URL"
},
"segmentFetchFailureWarning": {
"message": "Advertencia: El servidor aún no ha respondido con segmentos. Es posible que haya segmentos en este video ya enviados, pero no los ha recibido debido a problemas con el servidor."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Ühendusega esines tõrge. Veakood: "
},
"segmentsStillLoading": {
"message": "Segmendid on veel laadimas..."
},
"clearTimes": {
"message": "Tühjenda segmendid"
},
@@ -706,6 +709,9 @@
"category_chapter": {
"message": "Peatükk"
},
"category_chapter_guideline1": {
"message": "Ära maini sponsorite brändi nimesid"
},
"category_livestream_messages": {
"message": "Otseülekanne: annetuste ja sõnumite lugemine"
},
@@ -914,6 +920,9 @@
"helpPageReviewOptions": {
"message": "Palun vaata allolevad valikud üle"
},
"helpPageFeatureDisclaimer": {
"message": "Paljud funktsioonid on vaikimisi välja lülitatud. Kui sa soovid jätta vahele vaheaejad, kasutada Invidioust jms, lülita need allpool sisse. Sa saad ka peita/kuvada kasutajaliidese elemente."
},
"helpPageHowSkippingWorks": {
"message": "Kuidas vahelejätmine töötab"
},
@@ -1140,5 +1149,8 @@
},
"exportSegmentsAsURL": {
"message": "Jaga URLina"
},
"segmentFetchFailureWarning": {
"message": "Hoiatus: Server ei ole veel segmentidega vastanud. Sellel videol võivad juba olla saadetud segmendid, aga sa ei ole veel neid serveri vea tõttu saanud."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Yhteysvirhe on tapahtunut. Virhekoodi: "
},
"segmentsStillLoading": {
"message": "Osioita ladataan yhä..."
},
"clearTimes": {
"message": "Tyhjennä osiot"
},
@@ -238,7 +241,7 @@
"message": "Negatiivisesti äänestämäsi osiot pysyvät piilotettuina myös päivityksen jälkeen"
},
"trackDownvotesWarning": {
"message": "Varoitus: Tämän käytöstä poisto poistaa kaikki aiemmin tallennetut negatiiviset äänet"
"message": "Varoitus: Tämän poistaminen käytöstä poistaa kaikki aiemmin tallennetut alaäänet"
},
"enableQueryByHashPrefix": {
"message": "Kysely tiiviste-etuliittellä"
@@ -1242,6 +1245,9 @@
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Jaa web-osoitteena"
"message": "Jaa URL-osoitteena"
},
"segmentFetchFailureWarning": {
"message": "Varoitus: Palvelin ei ole vielä toimittanut osiotietoja. Video saattaa sisältää jo lähetettyjä osioita, mutta et vain ole vielä vastaanottanut niitä palvelinongelmien takia."
}
}

View File

@@ -35,6 +35,9 @@
"message": "Affiche les segments en tant que chapitre",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Afficher le segment actuel à côté de du temps de la vidéo"
},
"upvoteButtonInfo": {
"message": "Voter pour cette entrée"
},
@@ -113,6 +116,9 @@
"connectionError": {
"message": "Erreur de connexion. Code d'erreur : "
},
"segmentsStillLoading": {
"message": "Segments toujours en cours de chargement..."
},
"clearTimes": {
"message": "Effacer les segments"
},
@@ -122,6 +128,9 @@
"closePopup": {
"message": "Fermer le Popup"
},
"closeIcon": {
"message": "Icône \"Fermer\""
},
"SubmitTimes": {
"message": "Soumettre des segments"
},
@@ -246,6 +255,12 @@
"whatRefetchWhenNotFound": {
"message": "Si la vidéo vient de sortir et qu'aucun segment n'a été encore trouvé, SponsorBlock en cherchera durant la lecture de la vidéo."
},
"enableShowCategoryWithoutPermission": {
"message": "Afficher les catégories dans le menu des contributions, même celles sans autorisation"
},
"whatShowCategoryWithoutPermission": {
"message": "Certaines catégories nécessitent une autorisation en raison d'exigences minimales de réputation"
},
"showNotice": {
"message": "Afficher la notification"
},
@@ -408,9 +423,18 @@
"statusReminder": {
"message": "Vérifiez status.sponsor.ajay.app pour le status du serveur."
},
"changeUserID": {
"message": "Importer/Exporter Votre ID privé d'utilisateur"
},
"whatChangeUserID": {
"message": "Il doit rester privé. 0 l'instar d'un mot de passe, il ne doit être partagé avec personne. Si une autre personne est en possession de cette information, elle peut usurper votre identité SponsorBlock. Si vous cherchez votre UserID public, cliquez sur l'icône du presse-papiers dans l'encart."
},
"setUserID": {
"message": "Définir l'ID privé d'utilisateur"
},
"userIDChangeWarning": {
"message": "Avertissement : La modification de l'ID privé d'utilisateur est permanente. Êtes-vous sûr de vouloir faire ça ? Assurez-vous de sauvegarder l'ancien au cas où."
},
"createdBy": {
"message": "Créé par"
},
@@ -508,6 +532,9 @@
"exportOptionsUpload": {
"message": "Charger à partir du fichier"
},
"whatExportOptions": {
"message": "Contient toute votre configuration au format JSON. Inclut votre ID privé d'utilisateur, donc ne le partagez pas."
},
"setOptions": {
"message": "Définir les options"
},
@@ -670,6 +697,9 @@
"category_filler": {
"message": "Digressions/Blagues"
},
"category_filler_description": {
"message": "Digressions ajoutées uniquement dans un but de remplissage ou de l'humour non requis pour comprendre le sujet principal de la vidéo. Ne dois pas inclure des segments fournissant du contexte ou des détails de fond. Il s'agit d'une catégorie très agressive destinée aux moments qui ne sont pas \"pour le fun\"."
},
"category_filler_short": {
"message": "Remplissage"
},
@@ -1025,6 +1055,12 @@
"hideSegment": {
"message": "Cacher le segment"
},
"skipSegment": {
"message": "Passer le segment"
},
"playChapter": {
"message": "Jouer le chapitre"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Utilisez la molette de votre souris en survolant la boîte d'édition pour ajuster rapidement le minutage. Les combinaisons de touches ctrl ou maj peuvent être utilisées pour affiner les modifications."
},
@@ -1145,10 +1181,17 @@
"chaptersPage1": {
"message": "La fonctionnalité participative des chapitres de SponsorBlock n'est disponible que pour les personnes qui achètent une licence, ou pour les personnes qui en ont l'accès gratuitement grâce à leurs contributions passées"
},
"chaptersPage2": {
"message": "Remarque : l'autorisation d'envoyer des chapitres est toujours basée sur la réputation calculée. L'achat d'une licence vous permet uniquement de voir les chapitres envoyés par d'autres",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Nouvelle fonctionnalité : Chapitres personnalisés crowd-sourcés. Ce sont des sections dans les vidéos avec un nom personnalisé qui s'améliore au fur et à mesure. Achetez une licence pour voir les chapitres soumis sur cette vidéo tels que: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Nouvelle fonctionnalité : Chapitres personnalisés crowd-sourcés. Ce sont des sections dans les vidéos avec un nom personnalisé qui peuvent s'additionner pour être de plus en plus précis. Vous y avez accès gratuitement, activez la dans les options."
},
"unsubmittedSegmentCounts": {
"message": "Vous avez actuellement {0} sur {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
@@ -1203,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Partager un URL"
},
"segmentFetchFailureWarning": {
"message": "Attention : Le serveur n'a pas encore répondu avec les segments. Il se peut qu'il y ait des segments sur cette vidéo déjà soumise, mais vous ne les avez tout simplement pas reçus en raison de problèmes avec le serveur."
}
}

View File

@@ -25,6 +25,12 @@
"Segments": {
"message": "מקטעים"
},
"SegmentsCap": {
"message": "מקטעים"
},
"Chapters": {
"message": "פרקים"
},
"upvoteButtonInfo": {
"message": "הצבע לדיווח הזה"
},
@@ -211,6 +217,14 @@
"message": "קוד מקור",
"description": "Used on Firefox Store Page"
},
"nextChapterKeybind": {
"message": "הפרק הבא",
"description": "Keybind label"
},
"yourWork": {
"message": "העבודה שלך",
"description": "Used to describe the section that will show you the statistics from your submissions."
},
"errorCode": {
"message": "קוד שגיאה: "
},

View File

@@ -25,24 +25,143 @@
"Segments": {
"message": "खंडों"
},
"SegmentsCap": {
"message": "खंड"
},
"Chapters": {
"message": "चैप्टर"
},
"renderAsChapters": {
"message": "खंडो को चैप्टर के रूप में दिखाएँ",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "वीडियो समय के अलावा वर्तमान खंड दिखाएँ"
},
"upvoteButtonInfo": {
"message": "इस प्रस्तुतीकरण को वॉट दें"
},
"reportButtonTitle": {
"message": "रिपोर्ट"
},
"reportButtonInfo": {
"message": "इस प्रस्तुतीकरण को गलत रिपोर्ट करें"
},
"Dismiss": {
"message": "रद्द करें"
},
"Loading": {
"message": "लोड कर रहा है..."
},
"Hide": {
"message": "कभी नही दिखाएँ"
},
"hitGoBack": {
"message": "जहाँ से आप आये थी वहां पर जाने के लिए अनस्किप दबाएं।"
},
"unskip": {
"message": "अनस्किप"
},
"reskip": {
"message": "रिस्किप"
},
"unmute": {
"message": "अनम्यूट"
},
"paused": {
"message": "रुका हुआ"
},
"manualPaused": {
"message": "टाइम रुका"
},
"confirmMSG": {
"message": "अलग-अलग मानों को संपादित करने या हटाने के लिए, जानकारी बटन पर क्लिक करें या ऊपरी दाएं कोने में एक्सटेंशन आइकन पर क्लिक करके एक्सटेंशन पॉपअप खोलें।"
},
"clearThis": {
"message": "क्या आप वाकई इसे साफ़ करना चाहते हैं?\n\n"
},
"Unknown": {
"message": "आपका प्रायोजक समय सबमिट करने में एक त्रुटि हुई, कृपया बाद में पुन: प्रयास करें।"
},
"sponsorFound": {
"message": "इस वीडियो के खंड डेटाबेस में हैं!"
},
"sponsor404": {
"message": "कोई खंड नहीं मिले"
},
"sponsorStart": {
"message": "खंड अभी शुरू करें"
},
"sponsorEnd": {
"message": "खंड अब समाप्त होता है"
},
"sponsorCancel": {
"message": "खंड बनाना रद्द करें"
},
"noVideoID": {
"message": "कोई YouTube वीडियो नही मिली।\nअगर यह गलत है, तो इस तब को रिफ्रेश कीजिये।"
},
"refreshSegments": {
"message": "खंड रिफ्रेश करें"
},
"success": {
"message": "सफल!"
},
"voted": {
"message": "वॉट किया!"
},
"serverDown": {
"message": "ऐसा लगता है की सर्वर डाउन है। कृपया डेवलपर से तुरंत संपर्क करें।"
},
"connectionError": {
"message": "एक संपर्क गलती हो गयी है। गलती कूट: "
},
"clearTimes": {
"message": "खंड हटाएँ"
},
"openPopup": {
"message": "SponsorBlock पॉपअप खोलें"
},
"closePopup": {
"message": "पॉपअप बंद करें"
},
"closeIcon": {
"message": "आइकॉन बंद करें"
},
"SubmitTimes": {
"message": "खंड दर्ज करें"
},
"sortSegments": {
"message": "खंड छांटें"
},
"submitCheck": {
"message": "क्या आप वाकई इसे दर्ज करना चाहते हैं?"
},
"whitelistChannel": {
"message": "चैनल व्हाइटलिस्ट करें"
},
"removeFromWhitelist": {
"message": "चैनल को व्हाइटलिस्ट से हटाएँ"
},
"voteOnTime": {
"message": "एक खंड को वॉट करें"
},
"Submissions": {
"message": "प्रविष्टियां"
},
"savedPeopleFrom": {
"message": "आपने लोगो को बचाया है "
},
"viewLeaderboard": {
"message": "लीडरबोर्ड"
},
"recordTimesDescription": {
"message": "दर्ज करें"
},
"submissionEditHint": {
"message": "सबमिट पर क्लिक करने के बाद अनुभाग सम्पादन दिखाई देगा",
"description": "Appears in the popup to inform them that editing has been moved to the video player."
},
"Options": {
"message": "विकल्प"
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Kapcsolódási probléma merült fel. Hibakód: "
},
"segmentsStillLoading": {
"message": "Szegmensek betöltése folyamatban..."
},
"clearTimes": {
"message": "Szegmensek törlése"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Megosztás linkként"
},
"segmentFetchFailureWarning": {
"message": "Figyelem: A szerver még nem válaszolt szegmensekkel. Elképzelhető, hogy már vannak beküldött szegmensek ezen a videón, csak még nem töltődtek be szerver-problémák miatt."
}
}

View File

@@ -36,7 +36,7 @@
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Mostra il Segmento Corrente Affianco al Tempo del Video"
"message": "Mostra il Segmento Corrente Affianco alla Durata del Video"
},
"upvoteButtonInfo": {
"message": "Vota questo contributo"
@@ -116,6 +116,9 @@
"connectionError": {
"message": "Si è verificato un errore durante la connessione. Codice errore: "
},
"segmentsStillLoading": {
"message": "Segmenti in caricamento..."
},
"clearTimes": {
"message": "Rimuovi i segmenti"
},
@@ -132,7 +135,7 @@
"message": "Invia i segmenti"
},
"sortSegments": {
"message": "Ordina segmenti"
"message": "Ordina Segmenti"
},
"submitCheck": {
"message": "Sei sicuro di volerlo inviare?"
@@ -235,7 +238,7 @@
"message": "Memorizza i voti negativi del segmento"
},
"whatTrackDownvotes": {
"message": "Segmenti votati negativamente rimarranno nascosti anche dopo aver ricaricato la pagina"
"message": "I segmenti votati negativamente rimarranno nascosti anche dopo aver ricaricato la pagina"
},
"trackDownvotesWarning": {
"message": "Attenzione: Disabilitarlo eliminerà tutti i voti negativi precedentemente memorizzati"
@@ -256,7 +259,7 @@
"message": "Mostra le categorie nel menu d'invio anche senza l'autorizzazione all'invio"
},
"whatShowCategoryWithoutPermission": {
"message": "Alcune categorie richiedono l'autorizzazione all'invio a causa dei requisiti di reputazione minimi"
"message": "Alcune categorie richiedono l'autorizzazione all'invio a causa dei requisiti sulla reputazione minima"
},
"showNotice": {
"message": "Mostra di nuovo l'avviso"
@@ -307,11 +310,11 @@
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Inizia/Finisci segmento",
"message": "Inizio/Fine segmento",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Invia i segmenti",
"message": "Invia segmenti",
"description": "Keybind label"
},
"nextChapterKeybind": {
@@ -323,7 +326,7 @@
"description": "Keybind label"
},
"keybindDescription": {
"message": "Seleziona un tasto digitandolo e scegli qualsiasi tasto modificatore che desideri utilizzare."
"message": "Seleziona un tasto digitandolo e spunta qualsiasi tasto modificatore che desideri utilizzare."
},
"0": {
"message": "Timeout della connessione. Controlla la tua connessione a Internet. Se internet sta funzionando, il server è probabilmente sovraccarico oppure giù."
@@ -479,7 +482,7 @@
"message": "Usa il salto manuale quando esiste un'etichetta del video completo"
},
"whatManualSkipOnFullVideo": {
"message": "Per le persone che vogliono guardare il video senza interruzioni se contiene sponsorizzazioni o auto-promozioni."
"message": "Per le persone che vogliono guardare il video senza interruzioni se contiene sponsorizzazioni o autopromozioni."
},
"skipNoticeDuration": {
"message": "Salta durata avviso (secondi):"
@@ -578,7 +581,7 @@
"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."
},
"keyAlreadyUsed": {
"message": "Questa scorciatoia è associata ad un'altra azione. Selezionane una diversa."
"message": "Questa scorciatoia è associata a un'altra azione. Selezionane una diversa."
},
"to": {
"message": "a",
@@ -592,7 +595,7 @@
"message": "Includi transizioni"
},
"generic_guideline2": {
"message": "Riproduci come se nulla fosse stato saltato"
"message": "Riprodotto come se non ci fossero salti"
},
"category_sponsor": {
"message": "Sponsorizzazione"
@@ -613,22 +616,22 @@
"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_selfpromo_guideline1": {
"message": "Donazioni, abbonamenti e merce personalizzata"
"message": "Donazioni, abbonamenti e merchandise personalizzato"
},
"category_selfpromo_guideline2": {
"message": "Shoutout non pagati che non aggiungono nulla al video"
},
"category_selfpromo_guideline3": {
"message": "Non per prodotti progettati da aziende e merce"
"message": "Non per prodotti aziendali e merchandise"
},
"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."
"message": "Solo per etichettare interi video. Usato quando un video mostra un prodotto, un servizio o un luogo che è stato ricevuto gratuitamente o a cui si ha 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",
"message": "Questo video mostra un prodotto, un servizio o un luogo che è stato ricevuto gratuitamente o a cui si ha avuto un accesso sovvenzionato",
"description": "Short description for this category"
},
"category_exclusive_access_guideline1": {
@@ -641,13 +644,13 @@
"message": "Quando nel punto centrale del contenuto è presente un breve promemoria per like, iscrizione o follow. Se dovesse risultare esteso o riguardante qualcosa di specifico, potrebbe essere auto-promozione."
},
"category_interaction_guideline1": {
"message": "Brevi promemoria per mi piace, iscrizioni o follow"
"message": "Brevi inviti a mettere mi piace, follow o iscriversi"
},
"category_interaction_guideline2": {
"message": "Include promemoria indiretti al commento"
"message": "Include inviti indiretti a commentare"
},
"category_interaction_guideline3": {
"message": "Non per promozione generale, solo chiamata all'azione"
"message": "Non per promozione generale, solo inviti all'interazione"
},
"category_interaction_short": {
"message": "Promemoria d'Interazione"
@@ -662,7 +665,7 @@
"message": "Intermezzo"
},
"category_intro_guideline1": {
"message": "Intervallo senza contenuto effettivo"
"message": "Sezione senza contenuto effettivo"
},
"category_intro_guideline2": {
"message": "Non per transizioni con informazioni"
@@ -680,13 +683,13 @@
"message": "Anteprima/Riepilogo"
},
"category_preview_description": {
"message": "Raccolta di clip che mostra cosa succederà in questo video o altri video in una serie, in cui tutte le informazioni sono ripetute in seguito nel video."
"message": "Raccolta di clip che mostra cosa succederà in questo video o altri video in una serie, tutte le informazioni sono ripetute in seguito nel video."
},
"category_preview_guideline1": {
"message": "Clip che appaiono più tardi in questo video, oppure in un video futuro"
"message": "Clip che appaiono in seguito in questo video, oppure in un video futuro"
},
"category_preview_guideline2": {
"message": "Riepilogo o riassunto di un video precedente"
"message": "Riepilogo di un video precedente"
},
"category_preview_guideline3": {
"message": "Non per sezioni che aggiungono contenuti in più"
@@ -695,16 +698,16 @@
"message": "Riempitivi irrilevanti/Battute"
},
"category_filler_description": {
"message": "Scene tangenziali aggiunte solo come riempimento o per umorismo, non necessarie alla comprensione del contenuto principale del video. Questo non dovrebbe includere i segmenti che forniscono contesto o dettagli di background. Questa è una categoria molto aggressiva, pensata per quanto non hai voglia di \"divertirti\"."
"message": "Scene tangenziali aggiunte solo come riempimento o per umorismo, non necessarie alla comprensione del contenuto principale del video. Non dovrebbe includere i segmenti che forniscono contesto o dettagli di background. Questa è una categoria molto aggressiva, pensata per quanto non si è in un mood \"divertente\"."
},
"category_filler_short": {
"message": "Filler"
},
"category_filler_guideline1": {
"message": "Scene non correlate usate solo per filler o umorismo"
"message": "Scene non correlate usate solo come filler o umorismo"
},
"category_filler_guideline2": {
"message": "Distrazioni, blooper, replay"
"message": "Distrazioni, bloopers, replay"
},
"category_filler_guideline3": {
"message": "Non per scene necessarie a capire l'argomento"
@@ -737,7 +740,7 @@
"message": "Può ignorare il contesto"
},
"category_poi_highlight_guideline3": {
"message": "Può portare al titolo o alla miniatura del video"
"message": "Può saltare al contenuto del titolo o dell'anteprima"
},
"category_chapter": {
"message": "Capitolo"
@@ -752,7 +755,7 @@
"message": "Usa capitoli più grandi per le sezioni generali"
},
"category_chapter_guideline3": {
"message": "I capitoli più piccoli possono essere collocati all'interno di quelli più grandi"
"message": "Dei capitoli più piccoli possono essere collocati all'interno di quelli più grandi"
},
"category_livestream_messages": {
"message": "Livestream: Donazione/Letture dei Messaggi"
@@ -794,7 +797,7 @@
"message": "Silenzia i segmenti invece di saltarli, quando possibile"
},
"fullVideoSegments": {
"message": "Mostra un'icona quando un video è interamente una pubblicità",
"message": "Mostra un'icona quando un video è interamente promozionale",
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
},
"previewColor": {
@@ -853,7 +856,7 @@
"message": "nascosto: troppo corto"
},
"manuallyHidden": {
"message": "nascosta manualmente"
"message": "nascosto manualmente"
},
"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.",
@@ -878,7 +881,7 @@
"message": "Forza controllo canale prima di andare avanti"
},
"whatForceChannelCheck": {
"message": "Di default, verranno saltati i segmenti subito, anche prima che si sappia il canale. Di default, alcuni segmenti all'inizio del video potrebbero essere saltati sui canali nella whitelist. L'attivazione di questa opzione eviterà che ciò accada, ma ogni salto avrà un leggero ritardo in quanto ottenere l'ID del canale può richiedere un certo tempo. Questo ritardo potrebbe essere impercettibile se si dispone di una connessione internet veloce."
"message": "Di default, verranno saltati i segmenti immediatamente, anche prima di verificare se il canale è in whitelist. Di default, alcuni segmenti all'inizio del video potrebbero essere saltati anche sui canali in whitelist. L'attivazione di questa opzione eviterà che ciò accada, ma ogni salto avrà un leggero ritardo in quanto ottenere l'ID del canale può richiedere un certo tempo. Questo ritardo potrebbe essere impercettibile con una connessione internet veloce."
},
"forceChannelCheckPopup": {
"message": "Considera l'Attivazione dell'opzione \"Forza la Verifica del Canale Prima del Salto\""
@@ -887,10 +890,10 @@
"message": "Tempo Non Corretto/Errato"
},
"incorrectVote": {
"message": "Non corretto"
"message": "Errato"
},
"harmfulVote": {
"message": "Dannoso",
"message": "Offensivo",
"description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
},
"incorrectCategory": {
@@ -923,10 +926,10 @@
"description": "Used as the button to dismiss a tooltip"
},
"fullVideoTooltipWarning": {
"message": "Questo segmento è grande. Se l'intero video è su un argomento, passa da \"Salta\" a \"Video Completo\". Vedi le linee guida per ulteriori informazioni."
"message": "Questo segmento è grande. Se l'intero video è su un unico argomento, passa da \"Salta\" a \"Video Completo\". Vedi le linee guida per ulteriori informazioni."
},
"categoryPillTitleText": {
"message": "Questo intero video è etichettato come questa categoria ed è troppo integrato per poterlo separarare"
"message": "Questo intero video è etichettato con questa categoria ed è troppo integrato per poterlo separare"
},
"chapterNameTooltipWarning": {
"message": "Uno dei nomi dei tuoi capitoli è simile a una categoria. Quando possibile dovresti usare le categorie."
@@ -939,10 +942,10 @@
"message": "Nascondi per sempre"
},
"warningChatInfo": {
"message": "Abbiamo notato che stavi facendo alcuni errori comuni che non sono dannosi"
"message": "Abbiamo notato che stavi facendo alcuni errori comuni che non sono intenzionali"
},
"warningTitle": {
"message": "Hai ricevuto un avviso"
"message": "Hai ricevuto un avvertimento"
},
"questionButton": {
"message": "Ho una domanda"
@@ -957,7 +960,7 @@
"message": "Dona"
},
"considerDonating": {
"message": "Contribuisci allo sviluppo del fondo"
"message": "Aiuta a finanziare lo sviluppo"
},
"hideDonationLink": {
"message": "Nascondi Link di Donazione"
@@ -1093,7 +1096,7 @@
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "Salta l'aspetto del preavviso",
"message": "Aspetto dell'avviso di skip",
"description": "Option label"
},
"unbind": {
@@ -1119,13 +1122,13 @@
"message": "Ripristina le impostazioni predefinite"
},
"confirmResetToDefault": {
"message": "Sei sicuro di voler reimpostare tutte le impostazioni ai valori predefiniti? Questo non può essere annullato."
"message": "Sei sicuro di voler reimpostare tutte le impostazioni ai valori predefiniti? Questa azione è irreversibile."
},
"exportSegments": {
"message": "Esporta segmenti"
},
"importSegments": {
"message": "Importa i segmenti"
"message": "Importa segmenti"
},
"Import": {
"message": "Importa",
@@ -1144,7 +1147,7 @@
"message": "Scegli un paese"
},
"noDiscount": {
"message": "Non ti qualifichi per uno sconto"
"message": "Non hai diritto a uno sconto"
},
"discountLink": {
"message": "Link Sconto (Vedi il prezzo rosa)"
@@ -1153,11 +1156,11 @@
"message": "Seleziona il tuo paese"
},
"alreadyDonated": {
"message": "Se hai donato qualsiasi importo prima di ora, puoi riscattare l'accesso gratuito contattando:",
"message": "Se hai donato qualsiasi importo in passato, puoi riscattare l'accesso gratuito mandando una mail a:",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Se non puoi permetterti di acquistare una licenza, premi {here} per vedere se hai diritto a uno sconto",
"message": "Se non puoi permetterti di acquistare una licenza, premi {here} per verificare se hai diritto a uno sconto",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
@@ -1176,18 +1179,18 @@
"message": "Inserisci chiave di licenza"
},
"chaptersPage1": {
"message": "La funzionalità dei capitoli in crowd-sourcing di SponsorBlock è disponibile soltanto a coloro che acquistano una licenza o a coloro aventi l'accesso garantito gratuitamente grazie ai loro contributi passati"
"message": "La funzionalità dei capitoli in crowd-sourcing di SponsorBlock è disponibile soltanto a chi acquista una licenza o a chi viene premiato con l'accesso gratuito grazie ai contributi passati"
},
"chaptersPage2": {
"message": "Nota: L'autorizzazione a inviare i capitoli si basa ancora sulla reputazione calcolata. Acquistare una licenza ti consente di visualizzare soltanto i capitoli inviati dagli altri",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Nuova Funzionalità: Capitoli personalizzati in crowdsourcing. Sono sezioni dal nome personalizzato nei video, che possono esser impilate per essere sempre più precise. Acquista una licenza per visualizzare i capitoli inviati in questo video come: ",
"message": "Nuova Funzionalità: Capitoli personalizzati in crowdsourcing. Sono sezioni dal nome personalizzato che possono esser sovrapposte per essere sempre più precise. Acquista una licenza per visualizzare i capitoli inviati in questo video come ad esempio: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Nuova Funzionalità: Capitoli personalizzati in crowdsourcing. Sono sezioni dal nome personalizzato nei video, che possono esser impilate per essere sempre più precise. Hai accesso gratuitamente, abilitalo nelle opzioni."
"message": "Nuova Funzionalità: Capitoli personalizzati in crowdsourcing. Sono sezioni dal nome personalizzato, che possono esser sovrapposte per essere sempre più precise. Hai accesso gratuitamente, abilitalo nelle opzioni."
},
"unsubmittedSegmentCounts": {
"message": "Al momento hai {0} su {1}",
@@ -1214,7 +1217,7 @@
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Elimina tutti i segmenti",
"message": "Cancella tutti i segmenti",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Condividi URL"
},
"segmentFetchFailureWarning": {
"message": "Attenzione: il server non ha ancora fornito i segmenti. Questo video potrebbe avere dei segmenti inviati, ma forse non sono stati ricevuti a causa di problemi del server."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "接続エラーが発生しました。 エラーコード: "
},
"segmentsStillLoading": {
"message": "セグメントがまだ読み込み中です..."
},
"clearTimes": {
"message": "セグメントを消去"
},
@@ -150,7 +153,7 @@
"message": "提案数"
},
"savedPeopleFrom": {
"message": "みんなの時間をこれだけ節約しました: "
"message": "あなたが節約したみんなの時間: "
},
"viewLeaderboard": {
"message": "リーダーボード"
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "URLとして共有"
},
"segmentFetchFailureWarning": {
"message": "警告: サーバーがまだセグメントで応答していません。この動画に対するセグメントはすでに提出されているものの、サーバーの問題によりそれが受信できていないだけという可能性があります。"
}
}

View File

@@ -223,13 +223,13 @@
"message": "YouTube 탐색 바에서 삭제 버튼 표시"
},
"enableViewTracking": {
"message": "건너뛴 횟수 추적 활성화"
"message": "건너뛴 횟수 추적 사용"
},
"whatViewTracking": {
"message": "이 기능으로 건너뛴 구간을 추적해서 사용자가 제출한 내용이 다른 분께 얼마나 도움이 되는지 알려주고 잘못된 구간이 데이터베이스에 들어가지 않도록 추천과 함께 분석에 사용해요. 이 확장 프로그램이 구간을 건너뛸 때마다 서버에 메시지를 보낼 거예요. 조회수가 정확하기 위해서는 이 설정을 변경하지 않기를 바라요. :)"
},
"enableViewTrackingInPrivate": {
"message": "시크릿/사생활 보호 탭에서 건너뛴 횟수 추적 활성화"
"message": "시크릿/사생활 보호 탭에서 건너뛴 횟수 추적 사용"
},
"enableTrackDownvotes": {
"message": "비추천한 구간 저장"
@@ -238,7 +238,7 @@
"message": "비추천한 구간을 새로고침 이후에도 계속 숨겨요"
},
"trackDownvotesWarning": {
"message": "경고: 비활성화하면 이전에 저장된 비추천 구간이 삭제돼요"
"message": "경고: 사용하지 않으면 이전에 저장된 비추천 구간이 삭제돼요"
},
"enableQueryByHashPrefix": {
"message": "해시 접두사로 요청 전송"
@@ -377,22 +377,22 @@
"description": "Used for skipping to things (Skipped to Highlight)"
},
"disableAutoSkip": {
"message": "자동 건너뛰기 비활성화"
"message": "자동 건너뛰기 사용 안 함"
},
"enableAutoSkip": {
"message": "자동 건너뛰기 활성화"
"message": "자동 건너뛰기 사용"
},
"audioNotification": {
"message": "건너뛸 때 소리 재생"
},
"audioNotificationDescription": {
"message": "구간을 건너뛸 때마다 소리를 재생해요. 자동 건너뛰기가 비활성화된 경우, 아무 소리도 재생되지 않아요."
"message": "구간을 건너뛸 때마다 소리를 재생해요. 자동 건너뛰기를 사용하지 않는 경우, 아무 소리도 재생되지 않아요."
},
"showTimeWithSkips": {
"message": "건너뛰기로 제외된 시간 표시"
},
"showTimeWithSkipsDescription": {
"message": "탐색 바 아래에 있는 동영상 시간 옆 괄호에 시간이 표시돼요. 건너뛸 구간을 제외할 실제로 재생하게 될 동영상의 길이를 보여줘요. \"탐색 바에 표시\"로만 지정된 구간도 포함해요."
"message": "이 시간은 탐색 바 아래에 있고 현재 시간 옆에 있는 대괄호로 표시돼요. 건너뛸 구간을 제외할 실제로 재생하게 될 동영상의 길이를 보여줘요. \"탐색 바에 표시\"로만 지정된 구간도 포함해요."
},
"youHaveSkipped": {
"message": "건너뛴 구간: "
@@ -424,7 +424,7 @@
"message": "비공개 사용자 ID 가져오기/내보내기"
},
"whatChangeUserID": {
"message": "이 정보를 다른 분께 공개하지 마세요. 비밀번호처럼 알려주면 위험한 정보랍니다. 다른 분이 이 정보를 가지고 나를 사칭할 수도 있어요. 공개 사용자 ID를 찾고 있다면, 팝업 내 클립보드 아이콘을 눌러주세요."
"message": "이 정보를 다른 분께 공개하지 마세요. 이건 비밀번호와 같으며 누구와도 공유해서는 안 되는 정보랍니다. 다른 분이 이 정보를 습득한다면, 나를 사칭할 수도 있어요. 공개 사용자 ID를 찾고 있다면, 팝업 내 클립보드 아이콘을 눌러주세요."
},
"setUserID": {
"message": "비공개 사용자 ID 설정"
@@ -446,7 +446,7 @@
"message": "지원되는 사이트: "
},
"optionsInfo": {
"message": "Invidious 지원 활성화, 자동 건너뛰기 비활성화, 버튼 숨기기 등이 있어요."
"message": "Invidious 지원 활성화, 자동 건너뛰기 사용 안 함, 버튼 숨기기 등이 있어요."
},
"addInvidiousInstance": {
"message": "제3자 클라이언트 인스턴스 추가"
@@ -458,7 +458,7 @@
"message": "추가"
},
"addInvidiousInstanceError": {
"message": "잘못된 도메인이에요. 도메인 부분만 포함해야 해요. 예: invious.ajay.app"
"message": "잘못된 도메인이에요. 도메인 부분만 포함해야 해요. 예: invious.ajay.app"
},
"resetInvidiousInstance": {
"message": "Invidious 인스턴스 목록 초기화"
@@ -830,7 +830,7 @@
"message": "카테고리 선택"
},
"enableThisCategoryFirst": {
"message": "\"{0}\" 카테고리의 구간을 제출하려면, 설정에서 활성화를 해주셔야 해요. 바로 설정 창으로 이동하실 거예요.",
"message": "\"{0}\" 카테고리의 구간을 제출하려면, 설정에서 사용을 해주셔야 해요. 바로 설정 창으로 이동하실 거예요.",
"description": "Used when submitting segments to only let them select a certain category if they have it enabled in the options."
},
"poiOnlyOneSegment": {
@@ -872,7 +872,7 @@
"message": "권한 요청에 실패했어요. 거부를 누르셨나요?"
},
"adblockerIssueWhitelist": {
"message": "이 문제를 해결할 수 없는 경우 SponsorBlock이 이 동영상의 채널 정보를 찾을 수 없는 것일 수 있으니, '건너뛰기 전 채널 강제 확인' 설정을 비활성화해주세요."
"message": "이 문제를 해결할 수 없는 경우 SponsorBlock이 이 동영상의 채널 정보를 찾을 수 없는 것일 수 있으니, '건너뛰기 전 채널 강제 확인' 설정을 사용하지 않아야 해요"
},
"forceChannelCheck": {
"message": "건너뛰기 전 채널 강제 확인"
@@ -897,7 +897,7 @@
"message": "카테고리 변경"
},
"nonMusicCategoryOnMusic": {
"message": "이 동영상은 음악 동영상으로 분류되어 있어요. 동영상에 스폰서 광고 구간이 있나요? \"음악이 아닌 구간\"으로 지정된 카테고리인 경우, 확장 프로그램 설정을 열어 이 카테고리를 활성화하세요. 그리고, 이 구간을 \"스폰서 광고 구간\" 대신 \"음악이 아닌 구간\"으로 지정하세요. 혼동된다면 가이드라인을 읽어주세요."
"message": "이 동영상은 음악 동영상으로 분류되어 있어요. 동영상에 스폰서 광고 구간이 있나요? \"음악이 아닌 구간\"으로 지정된 카테고리인 경우, 확장 프로그램 설정을 열어 이 카테고리를 사용하세요. 그리고, 이 구간을 \"스폰서 광고 구간\" 대신 \"음악이 아닌 구간\"으로 지정하세요. 혼동된다면 가이드라인을 읽어주세요."
},
"multipleSegments": {
"message": "여러 구간"
@@ -972,7 +972,7 @@
"message": "아래 설정을 확인해 보세요"
},
"helpPageFeatureDisclaimer": {
"message": "기본값으로 많은 기능이 비활성화되어 있어요. 인트로, 아웃트로 같은 부분을 건너뛰고 싶으시다면 아래 설정을 야 해요. 또한 UI 요소를 숨기거나 표시할 수 있답니다."
"message": "기본값으로 많은 기능이 사용되지 않아요. 인트로, 아웃트로 같은 부분을 건너뛰고 싶으시다면 아래 설정을 사용해야 해요. 또한 UI 요소를 숨기거나 표시할 수 있답니다."
},
"helpPageHowSkippingWorks": {
"message": "건너뛰기가 작동하는 방법"
@@ -1002,7 +1002,7 @@
"message": "이건 너무 느린 거 같아요"
},
"helpPageTooSlow1": {
"message": "원하는 경우 단축키를 용할 수 있어요. 쌍반점(세미콜론) 키를 눌러 스폰서 광고 구간의 시점/종점을 설정할 수 있으며 작은따옴표 키를 눌러 구간을 제출할 수 있답니다. 언제든지 설정에서 변경할 수 있어요. QWERTY 자판을 사용하지 않는 경우, 단축키를 변경해야 할 수도 있어요."
"message": "원하는 경우 단축키를 용할 수 있어요. 쌍반점(세미콜론) 키를 눌러 스폰서 광고 구간의 시점/종점을 설정할 수 있으며 작은따옴표 키를 눌러 구간을 제출할 수 있답니다. 언제든지 설정에서 변경할 수 있어요. QWERTY 자판을 사용하지 않는 경우, 단축키를 변경해야 할 수도 있어요."
},
"helpPageCopyOfDatabase": {
"message": "데이터베이스의 복사본을 구할 수 있나요? 개발자분께 무슨 일이 생기면 어떻게 되는 거죠?"
@@ -1011,7 +1011,7 @@
"message": "데이터베이스는 여기에서 확인하실 수 있어요:"
},
"helpPageCopyOfDatabase2": {
"message": "또한 소스 코드는 자유롭게 용할 수 있어요. 따라서 데이터베이스에 무슨 일이 생기더라도, 제출된 구간이 사라지는 일은 없을 거예요."
"message": "또한 소스 코드는 자유롭게 용할 수 있어요. 따라서 데이터베이스에 무슨 일이 생기더라도, 제출된 구간이 사라지는 일은 없을 거예요."
},
"helpPageNews": {
"message": "새로운 변경 사항은 어디에서 확인하나요?"
@@ -1138,7 +1138,7 @@
"message": "라이선스 키가 유효하지 않아요"
},
"hideUpsells": {
"message": "추가 결제 없이는 숨김 설정을 이용하실 수 없어요"
"message": "(추가 결제 없이는 숨김 설정을 사용할 수 없어요)"
},
"chooseACountry": {
"message": "국가 선택"
@@ -1176,10 +1176,10 @@
"message": "라이선스 키를 입력하세요"
},
"chaptersPage1": {
"message": "SponsorBlock 사용자 참여 챕터 기능은 라이선스 결제 사용자나, 이전 기여를 통해 접근을 허가받은 사용자만 용할 수 있어요"
"message": "SponsorBlock 사용자 참여 챕터 기능은 라이선스 결제 사용자나, 이전 기여를 통해 접근을 허가받은 사용자만 용할 수 있어요"
},
"chaptersPage2": {
"message": "참고: 여전히 챕터 제출 권한은 산정된 평판만을 바탕으로 부여돼요. 라이선스를 결제하면 다른 분이 제출한 챕터를 확인하는 기능만 추가로 용할 수 있어요",
"message": "참고: 여전히 챕터 제출 권한은 산정된 평판만을 바탕으로 부여돼요. 라이선스를 결제하면 다른 분이 제출한 챕터를 확인하는 기능만 추가로 용할 수 있어요",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
@@ -1187,7 +1187,7 @@
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "새로운 기능: 사용자 참여 챕터 기능. 챕터는 사용자가 직접 이름을 지정할 수 있고 중첩시킬 수 있어 더욱 더 정확해요. 설정에서 활성화해서 무료로 사용할 수 있어요."
"message": "새로운 기능: 사용자 참여 챕터 기능. 챕터는 사용자가 직접 이름을 지정할 수 있고 중첩시킬 수 있어 더욱 더 정확해요. 설정에서 사용해서 무료로 사용할 수 있어요."
},
"unsubmittedSegmentCounts": {
"message": "You currently have {0} on {1}",

View File

@@ -25,6 +25,19 @@
"Segments": {
"message": "segmen"
},
"SegmentsCap": {
"message": "Segmen"
},
"Chapters": {
"message": "Bab"
},
"renderAsChapters": {
"message": "Render segmen sebagai bab",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Tunjukkan Segmen Semasa Di Sebelah Masa Video"
},
"upvoteButtonInfo": {
"message": "Sokong penyerahan ini"
},
@@ -52,6 +65,9 @@
"reskip": {
"message": "Reskip"
},
"unmute": {
"message": "Nyahredam"
},
"paused": {
"message": "Dijeda"
},
@@ -79,9 +95,15 @@
"sponsorEnd": {
"message": "Segmen Berakhir Sekarang"
},
"sponsorCancel": {
"message": "Batalkan Membuat Segmen"
},
"noVideoID": {
"message": "Tiada video YouTube dijumpai.\nSekiranya ini tidak betul, muat semula tab."
},
"refreshSegments": {
"message": "Muat semula segmen"
},
"success": {
"message": "Berjaya!"
},
@@ -94,6 +116,9 @@
"connectionError": {
"message": "Ralat sambungan telah berlaku. Kod salah: "
},
"segmentsStillLoading": {
"message": "Segmen masih dimuatkan..."
},
"clearTimes": {
"message": "Kosongkan Segmen"
},
@@ -103,9 +128,15 @@
"closePopup": {
"message": "Tutup Pop timbul"
},
"closeIcon": {
"message": "Tutup Ikon"
},
"SubmitTimes": {
"message": "Hantar Segmen"
},
"sortSegments": {
"message": "Susun Segmen"
},
"submitCheck": {
"message": "Adakah anda pasti mahu menghantarnya?"
},
@@ -152,6 +183,12 @@
"setUsername": {
"message": "Tetapkan Nama Pengguna"
},
"copyPublicID": {
"message": "Salin PenggunaID Awam"
},
"copySegmentID": {
"message": "Salin ID Segmen"
},
"discordAdvert": {
"message": "Mari sertai pelayan perselisihan rasmi untuk memberi cadangan dan maklum balas!"
},
@@ -170,12 +207,18 @@
"hideButtonsDescription": {
"message": "Ini menyembunyikan butang yang muncul di pemain YouTube untuk menyerahkan segmen langkau."
},
"showSkipButton": {
"message": "Simpan Butang Langkau Ke Sorotan Di Pemain"
},
"showInfoButton": {
"message": "Tunjukkan Butang Maklumat Pada Pemain YouTube"
},
"hideInfoButton": {
"message": "Sembunyikan Butang Maklumat Pada Pemain YouTube"
},
"autoHideInfoButton": {
"message": "Auto-sembunyikan Butang Maklumat"
},
"hideDeleteButton": {
"message": "Sembunyikan Butang Padam Pada Pemain YouTube"
},
@@ -188,6 +231,18 @@
"whatViewTracking": {
"message": "Fungsi ini mengesan segmen mana yang telah anda lewati untuk memberi tahu pengguna seberapa banyak penyerahan mereka telah membantu orang lain dan digunakan sebagai metrik bersama dengan suara positif untuk memastikan bahawa spam tidak masuk ke dalam pangkalan data. Sambungan tersebut menghantar mesej ke pelayan setiap kali anda melewatkan segmen. Mudah-mudahan kebanyakan orang tidak mengubah tetapan ini supaya nombor paparan tepat. :)"
},
"enableViewTrackingInPrivate": {
"message": "Bolehkan Penjejakan kiraan Langkau Di Dalam Tab Persendirian/Inkognito"
},
"enableTrackDownvotes": {
"message": "Simpan segmen undi turun"
},
"whatTrackDownvotes": {
"message": "Segmen yang anda undi turun akan kekal tersembunyi walaupun selepas memuat semulakan"
},
"trackDownvotesWarning": {
"message": "Amaran: Melumpuhkan ini akan padam semua undi turun yang tersimpan sebelum ini"
},
"enableQueryByHashPrefix": {
"message": "Pertanyaan Oleh Awalan Hash"
},
@@ -200,12 +255,36 @@
"whatRefetchWhenNotFound": {
"message": "Sekiranya videonya baru, dan tidak ada segmen yang dijumpai, video akan terus diambil setiap beberapa minit semasa anda menonton."
},
"enableShowCategoryWithoutPermission": {
"message": "Tunjuk kategori dalam menu penghantaran walaupun tanpa kebenaran penghantaran"
},
"whatShowCategoryWithoutPermission": {
"message": "Beberapa kategori memerlukan kebenaran untuk hantar disebabkan keperluan reputasi minimum"
},
"showNotice": {
"message": "Tunjukkan Notis Lagi"
},
"showSkipNotice": {
"message": "Tunjukkan Makluman Setelah Segmen Dilangkau"
},
"showCategoryGuidelines": {
"message": "Tunjukkan Bantuan Kategori"
},
"noticeVisibilityMode0": {
"message": "Saiz Penuh Langkau Notis"
},
"noticeVisibilityMode1": {
"message": "Notis Langkau Kecil untuk Auto Langkau"
},
"noticeVisibilityMode2": {
"message": "Semua Kecil Notis Langkau"
},
"noticeVisibilityMode3": {
"message": "Notis Langkau Pudar untuk Auto Langkau"
},
"noticeVisibilityMode4": {
"message": "Semua Pudar Notis Langkau"
},
"longDescription": {
"message": "SponsorBlock membolehkan anda melewati penaja, intro, outro, peringatan langganan, dan bahagian lain dari video YouTube yang menjengkelkan. SponsorBlock adalah pelanjutan penyemak imbas yang banyak untuk membolehkan sesiapa sahaja menghantar segmen tajaan dan masa tayangan dari segmen video YouTube yang lain. Setelah satu orang menyerahkan maklumat ini, semua orang yang mempunyai pelanjutan ini akan melangkau segmen yang ditaja. Anda juga boleh melangkau bahagian muzik video muzik bukan.",
"description": "Full description of the extension on the store pages."
@@ -226,6 +305,29 @@
"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": "Langkau segmen",
"description": "Keybind label"
},
"setStartSponsorShortcut": {
"message": "Mula/berhenti segmen",
"description": "Keybind label"
},
"setSubmitKeybind": {
"message": "Hantar segmen",
"description": "Keybind label"
},
"nextChapterKeybind": {
"message": "Bab seterusnya",
"description": "Keybind label"
},
"previousChapterKeybind": {
"message": "Bab sebelumnya",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Pilih kekunci dengan menaipnya dan pilih mana-mana kekunci pengubah suai yang anda ingin gunakan."
},
"0": {
"message": "Masa sambungan telah tamat. Periksa sambungan internet anda. Sekiranya internet anda berfungsi, pelayan mungkin berlebihan atau tidak berfungsi."
},
@@ -248,9 +350,35 @@
"skip": {
"message": "Langkau"
},
"mute": {
"message": "Redam"
},
"full": {
"message": "Seluruh Video",
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
},
"skip_category": {
"message": "Langkau {0}?"
},
"mute_category": {
"message": "Redam {0}?"
},
"skip_to_category": {
"message": "Langkau kepada {0}?",
"description": "Used for skipping to things (Skip to Highlight)"
},
"skipped": {
"message": "{0} Melangkau",
"description": "Example: Sponsor Skipped"
},
"muted": {
"message": "{0} Diredam",
"description": "Example: Sponsor Muted"
},
"skipped_to_category": {
"message": "Melangkau kepada {0}",
"description": "Used for skipping to things (Skipped to Highlight)"
},
"disableAutoSkip": {
"message": "Lumpuhkan Langkau Auto"
},
@@ -295,12 +423,40 @@
"statusReminder": {
"message": "Periksa status.sponsor.ajay.app untuk status pelayan."
},
"changeUserID": {
"message": "Import/Eksport PenggunaID Persendirian Anda"
},
"whatChangeUserID": {
"message": "Ini patut dirahsiakan. Ini seperti kata laluan dan tidak patut untuk dikongsi dengan sesiapa. Jika seseorang mempunyai ini, mereka boleh menyamar sebagai anda. Jika anda sedang mencari untuk penggunaID awam anda, tekan ikon papan keratan di dalam pop timbul."
},
"setUserID": {
"message": "Tetapkan PenggunaanID Peribadi"
},
"userIDChangeWarning": {
"message": "Amaran: Mengubah PenggunaanID Peribadi adalah kekal. Adakah anda pasti anda mahu melakukan ini? Pastikan untuk membuat sandaran anda yang lama untuk berjaga-jaga."
},
"createdBy": {
"message": "Dicipta oleh"
},
"supportOtherSites": {
"message": "Sokong Pihak Ke-3 Laman YouTube"
},
"supportOtherSitesDescription": {
"message": "Sokong klien pihak ketiga YouTube. Untuk membolehkan sokong, anda perlu menerima kebenaran tambahan. Ini TIDAK berfungsi dalam inkognito pada Chrome dan varian Chromium lain.",
"description": "This replaces the 'supports Invidious' option because it now works on other YouTube sites such as Cloudtube"
},
"supportedSites": {
"message": "Laman Disokong: "
},
"optionsInfo": {
"message": "Dayakan sokongan Invidious, lumpuhkan autoskip, sembunyikan butang dan banyak lagi."
},
"addInvidiousInstance": {
"message": "Tambah Pihak Ke-3 Contoh Klien"
},
"addInvidiousInstanceDescription": {
"message": "Tambah contoh tersuai. Ini mesti diformat HANYA dengan domain. Contoh: invidious.ajay.app"
},
"add": {
"message": "Tambah"
},
@@ -322,9 +478,24 @@
"minDurationDescription": {
"message": "Segmen yang lebih pendek daripada nilai yang ditetapkan tidak akan dilangkau atau ditunjukkan dalam pemain."
},
"enableManualSkipOnFullVideo": {
"message": "Gunakan langkau manual apabila label video penuh wujud"
},
"whatManualSkipOnFullVideo": {
"message": "Untuk orang yang ingin menonton video penuh tanpa gangguan jika ia adalah ditaja sepenuhnya atau promosi diri."
},
"skipNoticeDuration": {
"message": "Tenpoh masa notis langkau (saat):"
},
"skipNoticeDurationDescription": {
"message": "Notis langkau akan kekal di skrin untuk sekurang-kurangnya beberapa saat ini. Untuk melangkau secara manual, ia mungkin dilihat lebih lama."
},
"shortCheck": {
"message": "Penyerahan berikut lebih pendek daripada pilihan tempoh minimum anda. Ini mungkin bermaksud ini sudah dihantar, dan hanya diabaikan kerana pilihan ini. Adakah anda pasti mahu menghantar?"
},
"liveOrPremiere": {
"message": "Menghantar pada siaran langsung aktif atau tayangan perdana tidak dibenarkan. Sila tunggu sehingga ia selesai, kemudian muat semula halaman dan mengesahkan yang segmen masih sah."
},
"showUploadButton": {
"message": "Tunjukkan Butang Muat Naik"
},
@@ -352,6 +523,18 @@
"exportOptions": {
"message": "Import / Eksport Semua Pilihan"
},
"exportOptionsCopy": {
"message": "Sunting/salin"
},
"exportOptionsDownload": {
"message": "Simpan ke fail"
},
"exportOptionsUpload": {
"message": "Muatkan dari fail"
},
"whatExportOptions": {
"message": "Ini adalah keseluruhan konfigurasi anda dalam JSON. Ini termasuk PenggunaanID Persendirian anda, jadi pastikan untuk kongsi ini dengan bijak."
},
"setOptions": {
"message": "Tetapkan Pilihan"
},
@@ -376,6 +559,9 @@
"preview": {
"message": "Pratonton"
},
"unsubmitted": {
"message": "Belum Diserahkan"
},
"inspect": {
"message": "Periksa"
},
@@ -394,28 +580,78 @@
"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."
},
"keyAlreadyUsed": {
"message": "Pintasan ini terikat kepada tindakan lain. Sila pilih yang lain."
},
"to": {
"message": "ke",
"description": "Used between segments. Example: 1:20 to 1:30"
},
"CopiedExclamation": {
"message": "Disalin!",
"description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
},
"generic_guideline1": {
"message": "Termasuk peralihan segue"
},
"generic_guideline2": {
"message": "Main seperti seolah-olahnya tiada apa-apa yang melangkau"
},
"category_sponsor": {
"message": "Penaja"
},
"category_sponsor_description": {
"message": "Promosi berbayar, rujukan berbayar dan iklan langsung. Bukan untuk promosi diri atau sapaan percuma kepada penyebab / pencipta / laman web / produk yang mereka suka."
},
"category_sponsor_guideline1": {
"message": "Promosi dibayar"
},
"category_sponsor_guideline2": {
"message": "Bukan untuk derma atau barangan tersuai"
},
"category_selfpromo": {
"message": "Promosi Tanpa Bayaran / Diri"
},
"category_selfpromo_description": {
"message": "Sama dengan \"penaja\" kecuali untuk promosi tanpa gaji atau diri. Ini merangkumi bahagian mengenai barang dagangan, sumbangan, atau maklumat tentang siapa mereka bekerjasama."
},
"category_selfpromo_guideline1": {
"message": "Dermaan, keahlian dan barangan tersuai"
},
"category_selfpromo_guideline2": {
"message": "Shoutout percuma yang tidak menambah kepada video"
},
"category_selfpromo_guideline3": {
"message": "Bukan untuk produk reka bentuk korporat dan barang dagangan"
},
"category_exclusive_access": {
"message": "Akses Eksklusif"
},
"category_exclusive_access_description": {
"message": "Hanya untuk melabelkan keseluruhan video. Digunakan apabila video menunjukkan sesuatu produk, servis atau lokasi yang mereka menerima dengan percuma atau akses bersubsidi kepada."
},
"category_exclusive_access_pill": {
"message": "Video ini menunjukkan sesuatu produk, servis atau lokasi yang mereka menerima dengan percuma atau akses bersubsidi kepada",
"description": "Short description for this category"
},
"category_exclusive_access_guideline1": {
"message": "Keseluruhan video menunjukkan benda yang percuma atau akses bersubsidi"
},
"category_interaction": {
"message": "Peringatan Interaksi (Langgan)"
},
"category_interaction_description": {
"message": "Apabila ada peringatan pendek untuk menyukai, melanggan atau mengikutinya di tengah-tengah kandungan. Sekiranya ia panjang atau mengenai sesuatu yang spesifik, ia harus dipromosikan sendiri."
},
"category_interaction_guideline1": {
"message": "Peringatan pendek untuk suka, melanggan atau ikut"
},
"category_interaction_guideline2": {
"message": "Termasuk peringatan tidak langsung untuk komen"
},
"category_interaction_guideline3": {
"message": "Bukan untuk promosi umum, hanya seruan untuk bertindak"
},
"category_interaction_short": {
"message": "Peringatan Interaksi"
},
@@ -428,12 +664,54 @@
"category_intro_short": {
"message": "Selang"
},
"category_intro_guideline1": {
"message": "Selang waktu tanpa kandungan sebenar"
},
"category_intro_guideline2": {
"message": "Bukan untuk transisi dengan maklumat"
},
"category_outro": {
"message": "Kad Akhir / Kredit"
},
"category_outro_description": {
"message": "Kredit atau ketika kad akhir YouTube muncul. Bukan untuk kesimpulan dengan maklumat."
},
"category_outro_guideline1": {
"message": "Jangan termasuk kandungan, walaupun kad akhir pada skrin"
},
"category_preview": {
"message": "Pratonton/Imbas Kembali"
},
"category_preview_description": {
"message": "Koleksi klip yang menunjukkan apa yang akan datang di dalam video ini atau video lain di dalam siri di mana semua maklumat diulang kemudian di dalam video."
},
"category_preview_guideline1": {
"message": "Klip yang muncul kemudian, atau di dalam video masa depan"
},
"category_preview_guideline2": {
"message": "Imbas kembali video sebelum ini"
},
"category_preview_guideline3": {
"message": "Bukan untuk bahagian yang menambah kandungan tambahan"
},
"category_filler": {
"message": "Tangen Pengisi/Lawak"
},
"category_filler_description": {
"message": "Adegan tangensial ditambah hanya untuk pengisi atau jenaka yang tidak diperlukan untuk memahami kandungan utama daripada video tersebut. Ini tidak patutnya termasuk segmen yang menyediakan konteks atau butiran latar belakang. Ini kategori yang sangat aggresif dimaksudkan apabila kamu tiada mood untuk \"seronok\"."
},
"category_filler_short": {
"message": "Pengisi"
},
"category_filler_guideline1": {
"message": "Adegan tangensial hanya untuk pengisi atau humor"
},
"category_filler_guideline2": {
"message": "Gangguan, blooper, ulang tayang"
},
"category_filler_guideline3": {
"message": "Bukan untuk adegan diperlukan untuk memahami topik"
},
"category_music_offtopic": {
"message": "Muzik: Bahagian Bukan Muzik"
},
@@ -443,6 +721,42 @@
"category_music_offtopic_short": {
"message": "Bukan Muzik"
},
"category_music_offtopic_guideline1": {
"message": "Bahagian bukan dalam keluaran rasmi"
},
"category_music_offtopic_guideline2": {
"message": "Bukan muzik di dalam persembahan secara langsung"
},
"category_poi_highlight": {
"message": "Sorotan"
},
"category_poi_highlight_description": {
"message": "Bahagian video yang kebanyakan orang sedang mencari. Sama seperti komen \"Video bermula pada x\"."
},
"category_poi_highlight_guideline1": {
"message": "Bahagian kebanyakan orang sedang mencari"
},
"category_poi_highlight_guideline2": {
"message": "Boleh langkau konteks"
},
"category_poi_highlight_guideline3": {
"message": "Boleh langkau ke tajuk atau thumbnail"
},
"category_chapter": {
"message": "Bab"
},
"category_chapter_description": {
"message": "Nama bab tersuai menerangkan bahagian utama video."
},
"category_chapter_guideline1": {
"message": "Jangan menyebut nama jenama penaja"
},
"category_chapter_guideline2": {
"message": "Gunakan bab lebih besar untuk bahagian umum"
},
"category_chapter_guideline3": {
"message": "Kandungan yang lebih kecil boleh diletakkan di dalam yang lebih besar"
},
"category_livestream_messages": {
"message": "Strim Langsung: Bacaan Derma / Mesej"
},
@@ -461,6 +775,35 @@
"disable": {
"message": "Nyahaktifkan"
},
"autoSkip_POI": {
"message": "Langkau ke permulaan secara automatik"
},
"manualSkip_POI": {
"message": "Tanya apabila video dimuatkan"
},
"showOverlay_POI": {
"message": "Tunjuk Dalam Bar Mencari"
},
"showOverlay_full": {
"message": "Tunjuk Label"
},
"showOverlay_chapter": {
"message": "Tunjukkan bab"
},
"autoSkipOnMusicVideos": {
"message": "Auto langkau semua segmen apabila terdapat segmen bukan-muzik"
},
"muteSegments": {
"message": "Benarkan segmen yang meredamkan audio sebaliknya langkau"
},
"fullVideoSegments": {
"message": "Tunjukkan ikon apabila video ialah iklan sepenuhnya",
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
},
"previewColor": {
"message": "Warna Belum Diserahkan",
"description": "Referring to submissions that have not been sent to the server yet."
},
"seekBarColor": {
"message": "Cari Warna Bar"
},
@@ -493,18 +836,47 @@
"message": "Untuk menghantar segmen dengan kategori \"{0}\", anda mesti mengaktifkannya dalam pilihan. Anda akan diarahkan ke pilihan sekarang.",
"description": "Used when submitting segments to only let them select a certain category if they have it enabled in the options."
},
"poiOnlyOneSegment": {
"message": "Amaran: Segmen jenis ini hanya boleh mempunyai maksimum satu aktif pada satu masa. Menghantar pelbagai lagi akan menyebabkan satu ditunjukkan secara rawak."
},
"youMustSelectACategory": {
"message": "Anda mesti memilih kategori untuk semua segmen yang anda kirimkan!"
},
"bracketEnd": {
"message": "(Tamat)"
},
"End": {
"message": "Tamat",
"description": "Button that skips to the end of a segment"
},
"hiddenDueToDownvote": {
"message": "tersembunyi: undi"
},
"hiddenDueToDuration": {
"message": "tersembunyi: terlalu pendek"
},
"manuallyHidden": {
"message": "disembunyikan secara manual"
},
"channelDataNotFound": {
"description": "This error appears in an alert when they try to whitelist a channel and the extension is unable to determine what channel they are looking at.",
"message": "ID saluran tidak dimuatkan lagi. Jika anda menggunakan video terbenam, cuba gunakan laman utama YouTube sebaliknya. Ini juga boleh disebabkan perubahan di dalam susun atur YouTube, jika anda fikir begitu, buat komen di sini:"
},
"invidiousPermissionRefresh": {
"message": "Pelayar telah membatalkan kebenaran diperlukan untuk berfungsi di Invidious dan laman pihak ke-3 yang lain. Sila tekan butang di bawah untuk aktifkan semula kebenaran ini."
},
"acceptPermission": {
"message": "Terima kebenaran"
},
"permissionRequestSuccess": {
"message": "Permintaan kebenaran berjaya!"
},
"permissionRequestFailed": {
"message": "Permintaan kebenaran gagal, adakah anda menekan menafikan?"
},
"adblockerIssueWhitelist": {
"message": "Jika anda tidak dapat menyelesaikan perkara ini, kemudian lumpuhkan tetapan 'Paksa Semak Saluran Sebelum Langkau', kerana SponsorBlock tidak dapat mengambil semula maklumat saluran untuk video ini"
},
"forceChannelCheck": {
"message": "Pakai Pemeriksaan Saluran Sebelum Melangkau"
},
@@ -517,6 +889,16 @@
"downvoteDescription": {
"message": "Pemasaan Tidak Betul / Salah"
},
"incorrectVote": {
"message": "Salah"
},
"harmfulVote": {
"message": "Memudaratkan",
"description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
},
"incorrectCategory": {
"message": "Tukar Kategori"
},
"nonMusicCategoryOnMusic": {
"message": "Video ini dikategorikan sebagai muzik. Adakah anda pasti ini mempunyai penaja? Sekiranya ini sebenarnya adalah \"Segmen Bukan Muzik\", buka pilihan peluasan dan aktifkan kategori ini. Kemudian, anda boleh menghantar segmen ini sebagai \"Bukan Muzik\" dan bukannya penaja. Sila baca panduan sekiranya anda keliru."
},
@@ -535,5 +917,337 @@
},
"categoryUpdate2": {
"message": "Buka pilihan untuk melangkau perkenalan, pengeluaran luar, barang dagangan, dll."
},
"help": {
"message": "Bantuan"
},
"GotIt": {
"message": "Dah dapat",
"description": "Used as the button to dismiss a tooltip"
},
"fullVideoTooltipWarning": {
"message": "Segmen ini besar. Jika seluruh video adalah mengenai satu topik, kemudian tukar daripada \"Langkau\" ke \"Seluruh Video\". Lihat garis panduan untuk maklumat lanjut."
},
"categoryPillTitleText": {
"message": "Keseluruhan video ini dilabel sebagai kategori ini dan terlalu bersepadu erat untuk dapat dipisah"
},
"chapterNameTooltipWarning": {
"message": "Sebuah nama bab anda ialah serupa dengan kategori. Anda patut guna kategori apabila mungkin sebaliknya."
},
"experiementOptOut": {
"message": "Menarik diri daripada semua eksperimen masa depan",
"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."
},
"hideForever": {
"message": "Sembunyikan selamanya"
},
"warningChatInfo": {
"message": "Kami perasaan yang anda telah membuat beberapa kesilapan biasa yang tidak berniat jahat"
},
"warningTitle": {
"message": "Anda mendapat amaran"
},
"questionButton": {
"message": "Saya ada soalan"
},
"warningConfirmButton": {
"message": "Saya memahami sebabnya"
},
"warningError": {
"message": "Kesilapan apabila mencuba untuk mengakui amaran:"
},
"Donate": {
"message": "Derma"
},
"considerDonating": {
"message": "Bantu pembangunan dana"
},
"hideDonationLink": {
"message": "Sembunyikan Pautan Derma"
},
"darkModeOptionsPage": {
"message": "Mod Gelap Di Halaman Pilihan"
},
"helpPageThanksForInstalling": {
"message": "Terima kasih kerana memasang SponsorBlock."
},
"helpPageReviewOptions": {
"message": "Tolong semak pilihan di bawah"
},
"helpPageFeatureDisclaimer": {
"message": "Banyak ciri dilumpuhkan secara lalai. Jika anda ingin langkau intro, outro, gunakan Invidious, dll. Bolehkan mereka di bawah. Anda boleh juga sembunyi/tunjuk elemen UI."
},
"helpPageHowSkippingWorks": {
"message": "Bagaimana melangkau berfungsi"
},
"helpPageHowSkippingWorks1": {
"message": "Segmen video akan dilangkau secara automatik jika mereka dijumpa di dalam pangkalan data. Anda boleh membuka pop timbul dengan menekan ikon sambungan untuk mendapat pratonton daripada apa yang mereka ada."
},
"helpPageHowSkippingWorks2": {
"message": "Apabila anda melangkau sebuah segmen, anda akan dapat notis. Jika masa nampak salah mengundi turun dengan menekan undi turun! Anda boleh juga undi di dalam pop timbul."
},
"Submitting": {
"message": "Menghantar"
},
"helpPageSubmitting1": {
"message": "Menghantar boleh dilakukan sama ada di dalam pop timbul dengan menekan butang \"Segmen Bermula Sekarang\" atau di dalam pemain video dengan menggunakan butang di pemain."
},
"helpPageSubmitting2": {
"message": "Menekan butang main menunjukkan permulaan segmen dan menekan ikon berhenti menunjukkan tamat. Anda boleh sediakan berbilang sponsor sebelum menekan hantar. Menekan butang muat turun akan hantar. Menekan tong sampah akan padam."
},
"Editing": {
"message": "Menyunting"
},
"helpPageEditing1": {
"message": "Jika anda tersilap, anda boleh sunting atau padam segmen anda selepas menekan butang anak panah atas."
},
"helpPageTooSlow": {
"message": "Ini terlalu lambat"
},
"helpPageTooSlow1": {
"message": "Terdapat kekunci panas jika anda ingin menggunakannya. Tekan kekunci koma bertitik untuk menunjukkan mula/akhir daripada sesuatu segmen penaja. Ini boleh diubah di dalam pilihan. Jika anda tidak menggunakan QWERTY, anda mungkin patut ubah ikatan kekunci."
},
"helpPageCopyOfDatabase": {
"message": "Bolehkah saya mendapat salinan pangkalan data? Apakah akan terjadi jika anda hilang?"
},
"helpPageCopyOfDatabase1": {
"message": "Pangkalan data ialah awam dan boleh didapati di"
},
"helpPageCopyOfDatabase2": {
"message": "Kod sumber tersedia secara percuma. Jadi, walaupun sesuatu berlaku pada saya, penghantaran anda tidak hilang."
},
"helpPageNews": {
"message": "Berita dan bagaimana ia dibuat"
},
"helpPageSourceCode": {
"message": "Dimana saya boleh mendapat kod sumber?"
},
"Credits": {
"message": "Penghargaan"
},
"LearnMore": {
"message": "Ketahui Lebih Lanjut"
},
"FullDetails": {
"message": "Perincian Penuh"
},
"CopyDownvoteButtonInfo": {
"message": "Undi turun dan membuat salinan tempatan untuk anda hantar semula"
},
"OpenCategoryWikiPage": {
"message": "Buka halaman wiki kategori ini."
},
"CopyAndDownvote": {
"message": "Salin dan undi turun"
},
"ContinueVoting": {
"message": "Teruskan Undian"
},
"ChangeCategoryTooltip": {
"message": "Ini akan serta merta dikenakan pada segmen anda"
},
"downvote": {
"message": "Undi turun"
},
"upvote": {
"message": "Undi naik"
},
"hideSegment": {
"message": "Sembunyikan segmen"
},
"skipSegment": {
"message": "Langkau segmen"
},
"playChapter": {
"message": "Main bab"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Gunakan roda tetikus anda sambil di atas kotak edit untuk melaraskan masa dengan cepat. Gabungan kekunci ctrl atau shift boleh digunakan untuk membaiki perubahan."
},
"categoryPillNewFeature": {
"message": "Baharu! Tengok apabila video ialah penaja atau promosi diri sepenuhnya"
},
"dayAbbreviation": {
"message": "h",
"description": "100d"
},
"hourAbbreviation": {
"message": "j",
"description": "100h"
},
"optionsTabBehavior": {
"message": "Tingkah Laku",
"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": "Antaramuka",
"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": "Pintasan papan kekunci",
"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": "Sandaran/Memulihkan",
"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": "Lain-Lain",
"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": "Langkau notis penampilan",
"description": "Option label"
},
"unbind": {
"message": "Buka Ikatan",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Tidak ditetapkan"
},
"change": {
"message": "Tukar"
},
"youtubeKeybindWarning": {
"message": "Ini adalah dibina dalam pintasan YouTube. Adakah anda pasti anda mahu menggunakannya?"
},
"betaServerWarning": {
"message": "Pelayan BETA ialah didayakan!"
},
"openOptionsPage": {
"message": "Buka halaman pilihan"
},
"resetToDefault": {
"message": "Set semula tetapan kepada asal"
},
"confirmResetToDefault": {
"message": "Adakah anda pasti anda mahu menetapkan semula semua tetapan kepada nilai asalnya? Perkara ini tidak boleh diubah."
},
"exportSegments": {
"message": "Eksport segmen"
},
"importSegments": {
"message": "Import segmen"
},
"Import": {
"message": "Import",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"redeemSuccess": {
"message": "Tebusan Berjaya!"
},
"redeemFailed": {
"message": "Kunci lesen tidak sah"
},
"hideUpsells": {
"message": "Sembunyikan pilihan tidak tersedia tanpa pembayaran tambahan"
},
"chooseACountry": {
"message": "Pilih negara"
},
"noDiscount": {
"message": "Anda tidak layak untuk diskaun"
},
"discountLink": {
"message": "Pautan Diskaun (Lihat harga pink)"
},
"selectYourCountry": {
"message": "Pilih negara anda"
},
"alreadyDonated": {
"message": "Jika anda telah menderma apa-apa jumlah sebelum ini, anda boleh tebus akses percuma dengan menghantar emel:",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Jika anda tidak mampu untuk membeli lesen, tekan {sini} untuk melihat sama ada anda layak untuk diskaun",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Daftar masuk dengan Patreon"
},
"redeem": {
"message": "Tebus"
},
"joinOnPatreon": {
"message": "Langgan di Patreon"
},
"oneTimePurchase": {
"message": "Pembelian Sekali"
},
"enterLicenseKey": {
"message": "Masukkan Kunci Lesen"
},
"chaptersPage1": {
"message": "Fitur kandungan sumber awam SponsorBlock hanya tersedia untuk orang yang membeli lesen atau untuk orang yang diberikan akses secara percuma kerana sumbangan lepas"
},
"chaptersPage2": {
"message": "Nota: Kebenaran untuk hantar kandungan masih berdasarkan oleh reputasi yang dikira. Membeli lesen hanya membenarkan anda untuk melihat kandungan yang dihantar oleh orang lain",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Fitur Baharu: Kandungan sumber awam tersuai. Ini adalah bahagian dinamakan tersuai dalam video yang boleh bertindan untuk mendapatkan lebih dan lebih tepat. Beli lesen untuk melihat kandungan dihantar pada video seperti ini: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Fitur Baharu: Kandungan sumber awam tersuai. Ini adalah bahagian dinamakan tersuai dalam video yang boleh bertindan untuk mendapatkan lebih dan lebih tepat. Anda mempunyai akses secara percuma, membolehkannya di pilihan."
},
"unsubmittedSegmentCounts": {
"message": "Anda pada masa ini mempunyai {0} di {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "Anda pada masa ini tiada segmen belum dihantar",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "segmen belum dihantar",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "segmen belum dihantar",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "video",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "video",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Kosongkan semua segmen",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Adakah anda pasti anda mahu kosongkan semua segmen belum dihantar anda?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Tunjuk segmen",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Sembunyikan segmen",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "ID Video",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Kiraan Segmen",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "Tindakan",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Kongsi sebagai URL"
},
"segmentFetchFailureWarning": {
"message": "Amaran: Pelayan belum membalas dengan segmen lagi. Mungkin sebenarnya ada segmen di video ini sudah dihantar tetapi anda hanya belum menerimanya kerana isu dengan pelayan."
}
}

View File

@@ -35,6 +35,9 @@
"message": "Segmenten weergeven als hoofdstukken",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Huidig segment weergeven naast videotijd"
},
"upvoteButtonInfo": {
"message": "Stemmen op deze inzending"
},
@@ -113,6 +116,9 @@
"connectionError": {
"message": "Er is een verbindingsfout opgetreden. Foutcode: "
},
"segmentsStillLoading": {
"message": "Segmenten worden nog steeds geladen..."
},
"clearTimes": {
"message": "Segmenten verwijderen"
},
@@ -122,6 +128,9 @@
"closePopup": {
"message": "Pop-up sluiten"
},
"closeIcon": {
"message": "Pictogram voor sluiten"
},
"SubmitTimes": {
"message": "Segmenten indienen"
},
@@ -1046,6 +1055,12 @@
"hideSegment": {
"message": "Segment verbergen"
},
"skipSegment": {
"message": "Segment overslaan"
},
"playChapter": {
"message": "Hoofdstuk afspelen"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Gebruik het muiswiel terwijl u over het invoerveld beweegt om de tijd snel aan te passen. Combinaties van de ctrl- of shift-toets kunnen worden gebruikt om de wijzigingen te verfijnen."
},
@@ -1231,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Delen als URL"
},
"segmentFetchFailureWarning": {
"message": "Waarschuwing: de server heeft nog niet gereageerd met segmenten. Misschien zijn er al segmenten van deze video ingediend, maar heeft u ze nog niet ontvangen door problemen met de server."
}
}

View File

@@ -35,6 +35,9 @@
"message": "Pokazuj segmenty jako rozdziały",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Pokaż bieżący segment poza czasem wideo"
},
"upvoteButtonInfo": {
"message": "Zagłosuj na ten segment"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Zamknij okno"
},
"closeIcon": {
"message": "Wyłącz ikonę"
},
"SubmitTimes": {
"message": "Prześlij segmenty"
},
@@ -246,6 +252,12 @@
"whatRefetchWhenNotFound": {
"message": "Jeśli film jest nowy i nie znaleziono żadnych segmentów, dane będą pobierane na nowo co kilka minut w czasie oglądania."
},
"enableShowCategoryWithoutPermission": {
"message": "Pokaż kategorie w menu zgłoszeń, nawet bez uprawnień do zgłaszania"
},
"whatShowCategoryWithoutPermission": {
"message": "Niektóre kategorie wymagają specjalnych uprawnień, by przesłać segment ze względu na minimalne wymogi reputacji"
},
"showNotice": {
"message": "Pokaż informacje ponownie"
},
@@ -408,9 +420,18 @@
"statusReminder": {
"message": "Sprawdź status serwera na status.sponsor.ajay.app"
},
"changeUserID": {
"message": "Importuj/Eksportuj swój prywatny UserID"
},
"whatChangeUserID": {
"message": "To powinno pozostać prywatne. Jest to niczym hasło i nie powinno zostać nikomu udostępnione. Przy jego użyciu ktoś może się pod ciebie podszywać. Jeśli szukasz publicznego ID użytkownika, kliknij ikonę schowka w wyskakującym oknie."
},
"setUserID": {
"message": "Ustaw prywatny UserID"
},
"userIDChangeWarning": {
"message": "Uwaga: Zmiana ID użytkownika jest trwała. Czy na pewno chcesz to zrobić? Na wszelki wypadek skopiuj swój poprzedni ID."
},
"createdBy": {
"message": "Stworzony przez"
},
@@ -454,6 +475,12 @@
"minDurationDescription": {
"message": "Segmenty krótsze niż ustawiona wartość nie będą pomijane ani pokazywane w odtwarzaczu."
},
"enableManualSkipOnFullVideo": {
"message": "Użyj ręcznego pomijania, gdy istnieje etykieta na całym filmie"
},
"whatManualSkipOnFullVideo": {
"message": "Dla osób, które chcą oglądać film nieprzerwanie, jeśli film jest w pełni sponsorowany lub autopromocyjny."
},
"skipNoticeDuration": {
"message": "Czas trwania powiadomienia pominięcia (sekundy):"
},
@@ -502,6 +529,9 @@
"exportOptionsUpload": {
"message": "Wczytaj z pliku"
},
"whatExportOptions": {
"message": "Jest to cała twoja konfiguracja w formacie JSON. Zawarty jest w niej twój prywatny UserID, więc uważaj, komu ją udostępniasz."
},
"setOptions": {
"message": "Zapisz ustawienia"
},
@@ -664,6 +694,9 @@
"category_filler": {
"message": "Wypełniacz nietematyczny/żart"
},
"category_filler_description": {
"message": "To jest bardzo agresywna kategoria, której możesz użyć, jeżeli nie masz ochoty, by widzieć scenki humorystyczne/wypełniacze."
},
"category_filler_short": {
"message": "Wypełniacz"
},
@@ -1019,6 +1052,12 @@
"hideSegment": {
"message": "Ukryj segment"
},
"skipSegment": {
"message": "Pomiń segment"
},
"playChapter": {
"message": "Odtwórz rozdział"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Użyj scroll'a myszy po najechaniu nad pole edycji, aby szybko dostosować czas. Kombinacje z ctrl'em i shift'em mogą być użyte, aby doszlifować zmiany."
},
@@ -1085,6 +1124,9 @@
"exportSegments": {
"message": "Eksportuj segmenty"
},
"importSegments": {
"message": "Importuj segmenty"
},
"Import": {
"message": "Importuj",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
@@ -1101,9 +1143,23 @@
"chooseACountry": {
"message": "Wybierz kraj"
},
"noDiscount": {
"message": "Nie kwalifikujesz się do przeceny"
},
"discountLink": {
"message": "Link rabatowy"
},
"selectYourCountry": {
"message": "Wybierz swój kraj"
},
"alreadyDonated": {
"message": "Jeśli do tej pory przekazałeś jakąkolwiek darowiznę, możesz odebrać darmowy dostęp poprzez wysyłanie maila do:",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Jeśli nie możesz sobie pozwolić na zakup licencji, kliknij {tutaj} aby sprawdzić, czy kwalifikujesz się do zniżki",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Zaloguj się za pomocą Patreon"
},
@@ -1119,10 +1175,20 @@
"enterLicenseKey": {
"message": "Wprowadź klucz licencyjny"
},
"chaptersPage1": {
"message": "Funkcja społecznościowych rozdziałów SponsorBlock jest dostępna tylko dla osób, które wykupią licencję, albo którym przyznano dostęp za darmo ze względu na swoje wcześniejszy wkład"
},
"chaptersPage2": {
"message": "Uwaga: Przesyłanie rozdziałów jest nadal oparte na skalkulowanej reputacji. Kupowanie licencji pozwala tylko przeglądać rozdziały przesłane przez innych",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Nowa funkcja: niestandardowe rozdziały ze źródeł społecznościowych. Są to sekcje niestandardowo nazwane w filmach, które mogą być ustawione w sposób bardziej precyzyjny. Kup licencję, aby wyświetlić rozdziały przedstawione na tym filmie, takie jak: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Nowa funkcja: niestandardowe rozdziały ze źródeł społecznościowych. Są to sekcje niestandardowo nazwane w filmach, które mogą być ustawione w sposób bardziej precyzyjny."
},
"unsubmittedSegmentCounts": {
"message": "Aktualnie masz {0} na {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"

View File

@@ -35,6 +35,9 @@
"message": "Renderizar segmentos como capítulos",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Mostrar Segmento Atual ao Lado do Tempo do Vídeo"
},
"upvoteButtonInfo": {
"message": "Votar nesse segmento positivamente"
},
@@ -122,6 +125,9 @@
"closePopup": {
"message": "Fechar Popup"
},
"closeIcon": {
"message": "Ícone de Fechar"
},
"SubmitTimes": {
"message": "Enviar Segmentos"
},
@@ -246,6 +252,12 @@
"whatRefetchWhenNotFound": {
"message": "Se o vídeo for novo e nenhum segmento for encontrado, continuaremos buscando enquanto você assiste."
},
"enableShowCategoryWithoutPermission": {
"message": "Mostrar categorias no menu de envios mesmo sem permissão de envio"
},
"whatShowCategoryWithoutPermission": {
"message": "Algumas categorias exigem autorização de envio devido a requisitos mínimos de reputação"
},
"showNotice": {
"message": "Mostrar notificação outra vez"
},
@@ -408,9 +420,18 @@
"statusReminder": {
"message": "Verifique status.sponsor.ajay.app para o status do servidor."
},
"changeUserID": {
"message": "Importar/Exportar Seu UserID Privado"
},
"whatChangeUserID": {
"message": "Esta informação deve se mantida privada. Ela é como uma senha e não deve ser compartilhada. Outras pessoas poderão se passar por você caso obtenham acesso. Se estiver procurando por sua ID Pública de Usuário, clique no ícone de prancheta no popup."
},
"setUserID": {
"message": "Definir UserID Privado"
},
"userIDChangeWarning": {
"message": "Aviso: A modificação do ID de usuário privado é permanente. Você tem certeza de que quer fazer isso? Certifique-se de fazer backup do anterior."
},
"createdBy": {
"message": "Criado por"
},
@@ -454,6 +475,12 @@
"minDurationDescription": {
"message": "Segmentos menores do que o valor definido não serão pulados ou mostrados no reprodutor."
},
"enableManualSkipOnFullVideo": {
"message": "Usar o pulo manual quando houver um rótulo de vídeo completo"
},
"whatManualSkipOnFullVideo": {
"message": "Para pessoas que desejam assistir ao vídeo sem interrupção se ele for totalmente patrocinado ou autopromoção."
},
"skipNoticeDuration": {
"message": "Duração do aviso prévio de pular (segundos):"
},
@@ -709,6 +736,12 @@
"category_chapter": {
"message": "Capítulo"
},
"category_chapter_description": {
"message": "Capítulos personalizados que descrevem as principais seções de um vídeo."
},
"category_chapter_guideline1": {
"message": "Não mencione os nomes dos patrocinadores"
},
"category_livestream_messages": {
"message": "Livestream: Leituras de Doação/Mensagem"
},
@@ -739,6 +772,9 @@
"showOverlay_full": {
"message": "Mostrar Rótulo"
},
"showOverlay_chapter": {
"message": "Mostrar Capítulos"
},
"autoSkipOnMusicVideos": {
"message": "Pular automaticamente todos os segmentos quando há um segmento que não é música"
},
@@ -866,6 +902,9 @@
"categoryPillTitleText": {
"message": "Este vídeo inteiro está rotulado como esta categoria e está muito integrado para poder ser separado"
},
"chapterNameTooltipWarning": {
"message": "Um de seus nomes de capítulo é semelhante a uma categoria. Sempre que possível, você deve usar categorias."
},
"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."
@@ -873,6 +912,9 @@
"hideForever": {
"message": "Ocultar para sempre"
},
"warningChatInfo": {
"message": "Percebemos que você estava cometendo alguns erros comuns que não são prejudiciais"
},
"Donate": {
"message": "Doar"
},
@@ -945,6 +987,9 @@
"LearnMore": {
"message": "Saiba mais"
},
"FullDetails": {
"message": "Ver Detalhes Completos"
},
"CopyDownvoteButtonInfo": {
"message": "Dá voto negativo e cria uma cópia local para você reenviar"
},
@@ -969,6 +1014,12 @@
"hideSegment": {
"message": "Ocultar segmento"
},
"skipSegment": {
"message": "Pular segmento"
},
"playChapter": {
"message": "Reproduzir capítulo"
},
"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."
},
@@ -1031,5 +1082,69 @@
},
"confirmResetToDefault": {
"message": "Tem certeza de que deseja redefinir todas as configurações para os valores padrão? Essa ação não poderá ser desfeita."
},
"exportSegments": {
"message": "Exportar segmentos"
},
"importSegments": {
"message": "Importar segmentos"
},
"Import": {
"message": "Importar",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"selectYourCountry": {
"message": "Selecione o seu país"
},
"unsubmittedSegmentCountsZero": {
"message": "No momento, você não tem segmentos não enviados",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "segmento não enviado",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "segmentos não enviados",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "vídeo",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "vídeos",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Excluir todos os segmentos",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Tem certeza de que deseja excluir todos os segmentos não enviados?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Mostrar segmentos",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Ocultar segmentos",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "ID do Vídeo",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Número de segmentos",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "Ações",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Compartilhar como URL"
}
}

View File

@@ -25,6 +25,19 @@
"Segments": {
"message": "segmente"
},
"SegmentsCap": {
"message": "Segmente"
},
"Chapters": {
"message": "Capitolele"
},
"renderAsChapters": {
"message": "Randarea segmentelor ca, capitole",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Arată ora video din segmentul curent"
},
"upvoteButtonInfo": {
"message": "Votează această înregistrare"
},
@@ -112,9 +125,15 @@
"closePopup": {
"message": "Închide pop-up"
},
"closeIcon": {
"message": "Închide pictograma"
},
"SubmitTimes": {
"message": "Trimite segmente"
},
"sortSegments": {
"message": "Sortare Segmente"
},
"submitCheck": {
"message": "Sunteți sigur că doriți să trimiteți asta?"
},
@@ -233,6 +252,12 @@
"whatRefetchWhenNotFound": {
"message": "Dacă videoclipul este nou și nu sunt segmente găsite, va continua să se refeteze la fiecare câteva minute în timp ce vizionați."
},
"enableShowCategoryWithoutPermission": {
"message": "Arată categoriile în meniul de adăugare chiar fără permisiunea de adăugare"
},
"whatShowCategoryWithoutPermission": {
"message": "Unele categorii necesită permisiunea de a depune din cauza cerințelor minime de reputație"
},
"showNotice": {
"message": "Arată Notificarea Din Nou"
},
@@ -289,6 +314,14 @@
"message": "Predă segmentele",
"description": "Keybind label"
},
"nextChapterKeybind": {
"message": "Capitolul următor",
"description": "Keybind label"
},
"previousChapterKeybind": {
"message": "Capitolul anterior",
"description": "Keybind label"
},
"keybindDescription": {
"message": "Selectează o tastă prin a o tasta, și alege orice taste modificatoare pe care dorești să o folosești."
},
@@ -318,7 +351,7 @@
"message": "Mut"
},
"full": {
"message": "Întregul Videoclip",
"message": "Video complet",
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
},
"skip_category": {
@@ -387,9 +420,18 @@
"statusReminder": {
"message": "Verificați status.sponsor.ajay.app pentru starea serverului."
},
"changeUserID": {
"message": "Importă/Exportă UserID-ul tău Privat"
},
"whatChangeUserID": {
"message": "Aceste informații sunt private și nu ar trebui să fie dezvăluite nimănui. Dacă cineva are aceste informații, pot fi folosite pentru a vă imita. În cazul în care vă căutați ID-ul public de utilizator, dați click pe icoana de clipboard din popup."
},
"setUserID": {
"message": "Setare UserID privat"
},
"userIDChangeWarning": {
"message": "Avertisment: Modificarea Id-ului de utilizator privat este permanentă. Sunteți sigur că doriți să faceți acest lucru? Asigurați-vă că ați făcut o copie de rezervă pe cea veche, pentru orice eventualitate."
},
"createdBy": {
"message": "Creat De"
},
@@ -409,6 +451,9 @@
"addInvidiousInstance": {
"message": "Adaugă Instanță pentru clienți 3rd-party"
},
"addInvidiousInstanceDescription": {
"message": "Adăugați o instanță personalizată. Aceasta trebuie să fie formatată DOAR cu domeniul. Exemplu: invidious.ajay.app"
},
"add": {
"message": "Adaugă"
},
@@ -430,12 +475,24 @@
"minDurationDescription": {
"message": "Segmentele mai scurte decât valoarea setată nu vor fi omise sau afișate în player."
},
"enableManualSkipOnFullVideo": {
"message": "Utilizați omiterea manuală atunci când există o etichetă video completă"
},
"whatManualSkipOnFullVideo": {
"message": "Pentru persoanele care doresc să vizioneze videoclipul neîntrerupt dacă acesta este sponsorizat în totalitate sau dacă este autopromovat."
},
"skipNoticeDuration": {
"message": "Durata notificărilor de omitere (secunde):"
},
"skipNoticeDurationDescription": {
"message": "Notificarea sări peste va rămâne pe ecran cel puțin atâtea secunde. Pentru omiterea manuală, este posibil să fie vizibilă pentru mai mult timp."
},
"shortCheck": {
"message": "Următoarea înregistrare este mai mică decât opțiunea voastră de durată minimă. Acest lucru ar putea însemna că a fost deja trimisă și a fost ignorat datorită acestei opțiuni. Sunteți sigur că doriți să trimiteți?"
},
"liveOrPremiere": {
"message": "Nu este permisă adăugarea pe un live stream sau pe o primă. Vă rugăm să așteptați până se termină, apoi reîmprospătați pagina și verificați dacă segmentele sunt încă valide."
},
"showUploadButton": {
"message": "Arată Butonul De Încărcare"
},
@@ -472,6 +529,9 @@
"exportOptionsUpload": {
"message": "Încarcă din fișier"
},
"whatExportOptions": {
"message": "Aceasta este întreaga configurație în format JSON. Asta include si ID-ul tău privat de utilizator, așa că fi sigur să împărtășești asta cu înțelepciune."
},
"setOptions": {
"message": "Setează Opțiuni"
},
@@ -517,10 +577,23 @@
"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."
},
"keyAlreadyUsed": {
"message": "Această scurtătură este legată de o altă acțiune. Vă rugăm să selectați una diferită."
},
"to": {
"message": "până la",
"description": "Used between segments. Example: 1:20 to 1:30"
},
"CopiedExclamation": {
"message": "Copiat!",
"description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
},
"generic_guideline1": {
"message": "Include tranzițiile segmentului"
},
"generic_guideline2": {
"message": "Reda ca și cum nimic nu a fost omis"
},
"category_sponsor": {
"message": "Sponsor"
},
@@ -545,15 +618,37 @@
"category_selfpromo_guideline2": {
"message": "Mențiuni neplătite care nu ajută videoclipul"
},
"category_selfpromo_guideline3": {
"message": "Nu pentru produsele proiectate și merch"
},
"category_exclusive_access": {
"message": "Acces Exclusiv"
},
"category_exclusive_access_description": {
"message": "Doar pentru etichetarea videoclipurilor întregi. Folosit atunci când un video prezintă un produs, serviciu sau o locație la care au primit acces gratuit sau subvenționat."
},
"category_exclusive_access_pill": {
"message": "Acest videoclip prezintă un produs, serviciu sau o locație la care au primit acces gratuit sau subvenționat",
"description": "Short description for this category"
},
"category_exclusive_access_guideline1": {
"message": "Întreaga înregistrare video prezintă ceva cu acces liber sau subvenționat"
},
"category_interaction": {
"message": "Reamintire de Interactiune (Abonare)"
},
"category_interaction_description": {
"message": "Când există o scurtă remintire pentru like, abonare sau urmărire în mijlocul conținutului. Daca este mai lung sau despre ceva specific, folosiți autopromovarea."
},
"category_interaction_guideline1": {
"message": "Scurte memento-uri pentru a vă abona sau urmări"
},
"category_interaction_guideline2": {
"message": "Include memento-uri indirecte de comentat"
},
"category_interaction_guideline3": {
"message": "Nu pentru promovare generală, se face apel doar la acțiune"
},
"category_interaction_short": {
"message": "Reamintire de Interacțiune"
},
@@ -566,21 +661,99 @@
"category_intro_short": {
"message": "Pauză"
},
"category_intro_guideline1": {
"message": "Interval fără conținut real"
},
"category_intro_guideline2": {
"message": "Nu se efectuează tranziții cu informații"
},
"category_outro": {
"message": "Ecran De Final/Credite"
},
"category_outro_description": {
"message": "Credite sau atunci când apare ecranul de final YouTube. Nu pentru concluzii cu informații."
},
"category_outro_guideline1": {
"message": "Nu include conținutul, chiar dacă end card-urile sunt pe ecran"
},
"category_preview": {
"message": "Previzualizare/Recapitulare"
},
"category_preview_description": {
"message": "Colecția de clipuri care arată ce se află în acest videoclip sau alte videoclipuri într-o serie în care toate informațiile sunt repetate mai târziu în videoclip."
},
"category_preview_guideline1": {
"message": "Clipuri care apar mai târziu sau într-un video viitor"
},
"category_preview_guideline2": {
"message": "Recapitularea unui videoclip anterior"
},
"category_preview_guideline3": {
"message": "Nu pentru secțiunile care adaugă conținut suplimentar"
},
"category_filler": {
"message": "Glume de Umplutură/Tangențiale"
},
"category_filler_description": {
"message": "Scenele tactil adăugate numai pentru umplutură sau umor care nu sunt necesare pentru a înțelege conținutul principal al videoclipului. Aceasta nu ar trebui să includă segmente care să ofere detalii de context sau de fundal. Aceasta este o categorie foarte agresivă, pentru care nu ești în starea de spirit pentru \"distracție\"."
},
"category_filler_short": {
"message": "Materiale de umplutură"
},
"category_filler_guideline1": {
"message": "Scenele tactil numai pentru umplere sau umor"
},
"category_filler_guideline2": {
"message": "Distracții, blooper, reluări"
},
"category_filler_guideline3": {
"message": "Nu sunt necesare scene pentru a înțelege subiectul"
},
"category_music_offtopic": {
"message": "Muzică: Secţiune Non-Muzicală"
},
"category_music_offtopic_description": {
"message": "Doar pentru a fi folosit în videoclipuri muzicale. Acesta ar trebui să fie folosit doar pentru secțiunile de videoclipuri muzicale care nu sunt deja acoperite de o altă categorie."
},
"category_music_offtopic_short": {
"message": "Non-Muzical"
},
"category_music_offtopic_guideline1": {
"message": "Secțiuni care nu sunt în lansări oficiale"
},
"category_music_offtopic_guideline2": {
"message": "Fără muzica într-un spectacol in direct"
},
"category_poi_highlight": {
"message": "Momente importante"
},
"category_poi_highlight_description": {
"message": "Partea din videoclip pe care majoritatea oamenilor o caută. Similar cu \"Videoclipul începe de la x\"."
},
"category_poi_highlight_guideline1": {
"message": "Secțiunea pe care majoritatea persoanelor o caută"
},
"category_poi_highlight_guideline2": {
"message": "Poate sări peste context"
},
"category_poi_highlight_guideline3": {
"message": "Poți sări la titlu sau miniatură"
},
"category_chapter": {
"message": "Capitol"
},
"category_chapter_description": {
"message": "Capitolele cu nume personalizate care descriu secțiuni majore ale unui videoclip."
},
"category_chapter_guideline1": {
"message": "Nu menţiona numele de marcă sponsor"
},
"category_chapter_guideline2": {
"message": "Folosește capitole mai mari pentru secțiunile generale"
},
"category_chapter_guideline3": {
"message": "Capitolele mai mici pot fi plasate în interiorul capitolelor mai mari"
},
"category_livestream_messages": {
"message": "Transmisiune În Direct: Donație/Citirea Mesajelor"
},
@@ -599,6 +772,27 @@
"disable": {
"message": "Dezactivare"
},
"autoSkip_POI": {
"message": "Omitere automată la început"
},
"manualSkip_POI": {
"message": "Întreabă când se încarcă videoclipul"
},
"showOverlay_POI": {
"message": "Arată În Bara de Derulare"
},
"showOverlay_full": {
"message": "Arată eticheta"
},
"showOverlay_chapter": {
"message": "Afișează Capitolele"
},
"autoSkipOnMusicVideos": {
"message": "Omiteți automat toate segmentele atunci când există un segment non-muzical"
},
"muteSegments": {
"message": "Permite segmente care blochează sunetul în loc de a sări peste"
},
"fullVideoSegments": {
"message": "Arată o icoană atunci când un videoclip este în întregime o reclamă",
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
@@ -648,6 +842,10 @@
"bracketEnd": {
"message": "(Sfârșit)"
},
"End": {
"message": "Sfârșit",
"description": "Button that skips to the end of a segment"
},
"hiddenDueToDownvote": {
"message": "ascuns: downvote"
},
@@ -657,9 +855,25 @@
"manuallyHidden": {
"message": "ascuns manual"
},
"channelDataNotFound": {
"description": "This error appears in an alert when they try to whitelist a channel and the extension is unable to determine what channel they are looking at.",
"message": "ID-ul canalului nu este încă încărcat. Dacă utilizați un videoclip încorporat, încercați în schimb să utilizați pagina de pornire YouTube. Acest lucru ar putea fi cauzat și de modificările aduse aspectului YouTube, dacă credeți că faceți un comentariu aici:"
},
"invidiousPermissionRefresh": {
"message": "Browserul a revocat permisiunea necesară pentru a funcționa pe site-urile Invidioase și pe alte site-uri de 3 părți. Vă rugăm să faceți clic pe butonul de mai jos pentru a reactiva această permisiune."
},
"acceptPermission": {
"message": "Acceptă permisiunea"
},
"permissionRequestSuccess": {
"message": "Solicitarea permisiunii reușită!"
},
"permissionRequestFailed": {
"message": "Solicitarea permisiunii a eșuat, ai dat clic pe refuz?"
},
"adblockerIssueWhitelist": {
"message": "Dacă nu puteți rezolva acest lucru, apoi dezactivați setarea 'Forțați verificarea canalului înainte de sărit', deoarece SponsorBlock nu poate prelua informațiile canalului pentru acest videoclip"
},
"forceChannelCheck": {
"message": "Forțează verificarea canalului înainte de a sări"
},
@@ -672,6 +886,16 @@
"downvoteDescription": {
"message": "Timpi Incorecți/Greșiți"
},
"incorrectVote": {
"message": "Incorect"
},
"harmfulVote": {
"message": "Periculos",
"description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
},
"incorrectCategory": {
"message": "Schimbare categorie"
},
"nonMusicCategoryOnMusic": {
"message": "Acest videoclip este categorisit ca muzică. Ești sigur ca există un sponsor? Dacă acesta este defapt un segment non-muzical, deschideți opțiunile extensiei și activați această categorie. Apoi, puteți trimite acest segment ca non-muzical în loc de sponsol. Vă rugăm să citiți ghidul dacă sunteți confuz."
},
@@ -691,6 +915,110 @@
"categoryUpdate2": {
"message": "Deschide opțiunile pentru a sări peste intro-uri, outro-uri, merch, etc."
},
"help": {
"message": "Ajutor"
},
"GotIt": {
"message": "Am înțeles",
"description": "Used as the button to dismiss a tooltip"
},
"fullVideoTooltipWarning": {
"message": "Acest segment este mare. Dacă întregul videoclip este aproximativ un subiect, atunci schimbați de la \"Săriți\" la \"Videoclip întreg\". Vedeți instrucțiunile pentru mai multe informații."
},
"categoryPillTitleText": {
"message": "Acest întreg videoclip este etichetat ca această categorie și este prea strâns integrat pentru a putea separa"
},
"chapterNameTooltipWarning": {
"message": "Unul dintre numele capitolelor tale este similar cu o categorie. Ar trebui să folosești categorii atunci când este posibil."
},
"experiementOptOut": {
"message": "Renunțarea tuturor experimentelor viitoare",
"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."
},
"hideForever": {
"message": "Ascunde pentru totdeauna"
},
"warningChatInfo": {
"message": "Am observat că făceați niște greșeli comune care nu sunt răuvoitoare"
},
"warningTitle": {
"message": "Ai primit o avertizare"
},
"questionButton": {
"message": "Am o întrebare"
},
"warningConfirmButton": {
"message": "Înțeleg motivul"
},
"warningError": {
"message": "Eroare la încercarea de a confirma avertismentul:"
},
"Donate": {
"message": "Donează"
},
"considerDonating": {
"message": "Ajută la dezvoltarea fondului"
},
"hideDonationLink": {
"message": "Ascunde linkul de donare"
},
"darkModeOptionsPage": {
"message": "Modul întunecat pe pagina de opțiuni"
},
"helpPageThanksForInstalling": {
"message": "Vă mulțumim pentru instalarea SponsorBlock."
},
"helpPageReviewOptions": {
"message": "Vă rugăm să analizați opțiunile de mai jos"
},
"helpPageFeatureDisclaimer": {
"message": "Multe caracteristici sunt dezactivate în mod implicit. Dacă doriți să săriți peste introduceri, outros, utilizați Invidios, etc., activați-le mai jos. De asemenea, puteți ascunde/afișa elemente UI."
},
"helpPageHowSkippingWorks": {
"message": "Cum funcționează omiterea"
},
"helpPageHowSkippingWorks1": {
"message": "Segmentele video vor fi omise automat dacă sunt găsite în baza de date. Poți deschide fereastra pop-up făcând clic pe pictograma extensiei pentru a obține o previzualizare a ceea ce sunt."
},
"helpPageHowSkippingWorks2": {
"message": "Ori de câte ori săriți peste un segment, veți primi observație. Dacă sincronizarea pare greșită votând în jos făcând clic pe vot! De asemenea, poți vota în pop-up."
},
"Submitting": {
"message": "Se transmite"
},
"helpPageSubmitting1": {
"message": "Trimiterea poate fi făcută fie în fereastra pop-up prin apăsarea butonului \"Segmentul Incepe Acum\", fie în video player-ul cu butoanele playerului."
},
"helpPageSubmitting2": {
"message": "Făcând clic pe butonul de redare indică începutul unui segment și apăsând pe pictograma de stop indică sfârșitul. Poți pregăti mai mulți sponsori înainte de a apasă pe butonul de încărcare va fi trimis. Click-ul pe gunoi se va șterge."
},
"Editing": {
"message": "Editează"
},
"helpPageEditing1": {
"message": "Dacă ai greșit, poți edita sau șterge segmentele după ce dai click pe butonul săgeată în sus."
},
"helpPageTooSlow": {
"message": "Acest lucru este prea lent"
},
"helpPageTooSlow1": {
"message": "Exista scurtături daca dorești sa le folosești. Apăsați tasta cu punct și virgulă pentru a indica începutul/sfârșitul unui segment de sponsor și faceți clic pe apostrof pentru a trimite. Acestea pot fi schimbate în opțiuni. Dacă nu utilizați QWERTY, probabil ar trebui să schimbați scurtăturile."
},
"helpPageCopyOfDatabase": {
"message": "Pot obține o copie a bazei de date? Ce se întâmplă dacă dispari?"
},
"helpPageCopyOfDatabase1": {
"message": "Baza de date este publică și disponibilă la adresa"
},
"helpPageCopyOfDatabase2": {
"message": "Codul sursă este disponibil gratuit. Deci, chiar dacă ceva mi se întâmplă mie, trimiterile tale nu sunt pierdute."
},
"helpPageNews": {
"message": "Știri și modul în care este făcut"
},
"helpPageSourceCode": {
"message": "Unde pot obține codul sursă?"
},
"Credits": {
"message": "Contribuții"
},
@@ -700,15 +1028,42 @@
"FullDetails": {
"message": "Detalii Complete"
},
"CopyDownvoteButtonInfo": {
"message": "Voturi negative și creați o copie locală pentru a retrimite"
},
"OpenCategoryWikiPage": {
"message": "Deschide pagina wiki a acestei categorii."
},
"CopyAndDownvote": {
"message": "Copiază și dă downvote"
},
"ContinueVoting": {
"message": "Continuă votarea"
},
"ChangeCategoryTooltip": {
"message": "Acest lucru se va aplica instantaneu segmentelor dvs"
},
"downvote": {
"message": "Downvote"
},
"upvote": {
"message": "Upvote"
},
"hideSegment": {
"message": "Ascunde segmentul"
},
"skipSegment": {
"message": "Sari peste segment"
},
"playChapter": {
"message": "Reda capitolul"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Folosește rotița mouse-ului în timp ce ești deasupra casetei de editare pentru a ajusta rapid timpul. Combinațiile dintre ctrl sau Shift pot fi folosite pentru a ajusta modificările."
},
"categoryPillNewFeature": {
"message": "Nou! Vezi când un videoclip este sponsorizat în întregime sau auto-promovat"
},
"dayAbbreviation": {
"message": "zile",
"description": "100d"
@@ -741,10 +1096,152 @@
"message": "Aspectul notificării de omiteri",
"description": "Option label"
},
"unbind": {
"message": "Unbind",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "Nesetat"
},
"change": {
"message": "Schimbă"
},
"youtubeKeybindWarning": {
"message": "Aceasta este o scurtătură YouTube integrată. Ești sigur că vrei să o folosești?"
},
"betaServerWarning": {
"message": "Serverul BETA este activat!"
},
"openOptionsPage": {
"message": "Deschide pagina de opțiuni"
},
"resetToDefault": {
"message": "Resetați setările la valori implicite"
},
"confirmResetToDefault": {
"message": "Ești sigur că vrei sa resetezi toate setările la valorile lor implicite? Această acțiune nu poate fi anulată."
},
"exportSegments": {
"message": "Exportare segmente"
},
"importSegments": {
"message": "Importare segmente"
},
"Import": {
"message": "Importă",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"redeemSuccess": {
"message": "Revendicare cu succes!"
},
"redeemFailed": {
"message": "Cheia de licență nu este validă"
},
"hideUpsells": {
"message": "Ascunde opțiunile indisponibile fără o plată suplimentara"
},
"chooseACountry": {
"message": "Selectați țara"
},
"noDiscount": {
"message": "Nu te califici pentru o reducere"
},
"discountLink": {
"message": "Link de reducere (Vezi prețul roz)"
},
"selectYourCountry": {
"message": "Selectaţi ţara"
},
"alreadyDonated": {
"message": "Dacă ai donat orice sumă înainte, poți revendica acces gratuit prin e-mail:",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Dacă nu vă puteți permite să achiziționați o licență, faceți clic pe {aici} pentru a vedea dacă sunteți eligibil pentru o reducere",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Conectează-te cu Patreon"
},
"redeem": {
"message": "Folosește"
},
"joinOnPatreon": {
"message": "Abonează-te pe Patreon"
},
"oneTimePurchase": {
"message": "Achiziție unică"
},
"enterLicenseKey": {
"message": "Introdu cheia de licență"
},
"chaptersPage1": {
"message": "Funcția SponsorBlock crowd-sourted capitol este disponibilă doar pentru persoanele care achiziționează o licență, fie persoanelor cărora li se acordă acces gratuit la contribuţiile lor anterioare"
},
"chaptersPage2": {
"message": "Notă: Permisiunea de a trimite capitole este încă bazată pe reputația calculată. Achiziționarea unei licențe vă permite doar să vizualizați capitolele prezentate de alții",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Funcție nouă: Capitole personalizate din surse colective. Acestea sunt secțiuni personalizate în videoclipuri care pot fi stivuite pentru a deveni din ce în ce mai precise. Cumpără o licență pentru a vizualiza capitolele trimise pe acest videoclip, cum ar fi: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Funcție nouă: Capitole personalizate din surse colective. Acestea sunt secțiuni personalizate în videoclipuri care pot fi stivuite pentru a deveni din ce în ce mai precise. Ai acces gratuit, activeaz-o in opțiuni."
},
"unsubmittedSegmentCounts": {
"message": "Ai în prezent {0} pe {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "În prezent nu aveți segmente netrimise",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "segment netransmis",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "segmente netransmise",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "videoclip",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "videoclipuri",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Curăță toate segmentele",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Sigur doriți ștergerea tuturor segmentelor netransmise?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Arată segmentele",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Ascunde segmentele",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "ID videoclip",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Numărul de segmente",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "Acțiuni",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Distribuie ca URL"
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Ошибка соединения. Код ошибки: "
},
"segmentsStillLoading": {
"message": "Сегменты все еще загружаются..."
},
"clearTimes": {
"message": "Очистить сегменты"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Поделиться ссылкой"
},
"segmentFetchFailureWarning": {
"message": "Внимание: Сервер еще не прислал существующие сегменты. В этом видео уже могут быть отправленные сегменты, но вы их ещё не получили из-за проблем с сервером."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Nastala chyba pripojenia. Kód chyby: "
},
"segmentsStillLoading": {
"message": "Segmenty sa ešte nahrávajú..."
},
"clearTimes": {
"message": "Zmazať segmenty"
},
@@ -1060,5 +1063,8 @@
"chapterNewFeature": {
"message": "Nová funkcia: Crowd-sourcované vlastné kapitoly. Toto sú sekcie videa s vlastnými názvami, ktoré môžu byť pre väčšiu presnosť vyskladané. Ak si kúpite licenciu, uvidíte kapitoly v tomto videu, ako napr.: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"segmentFetchFailureWarning": {
"message": "Varovanie: Zo servera sa ešte nenahrali segmenty. V tomto videu už môžu byť vytvorené segmenty, ale vzhľadom na problémy so serverom ešte nemuseli byť nahraté."
}
}

View File

@@ -116,6 +116,9 @@
"connectionError": {
"message": "Anslutningsfel. Felkod: "
},
"segmentsStillLoading": {
"message": "Segmenten laddas fortfarande..."
},
"clearTimes": {
"message": "Rensa segmenten"
},
@@ -1243,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Dela som webbadress"
},
"segmentFetchFailureWarning": {
"message": "Varning: Servern har inte skickat ut några segment. Det kan faktiskt finnas segment på den här videon som redan skickats in, men du har inte mottagit dem ännu på grund av problem med servern."
}
}

View File

@@ -35,6 +35,9 @@
"message": "Показувати сегменти як розділи",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "Показати поточний сегмент поруч із часом відео"
},
"upvoteButtonInfo": {
"message": "Проголосувати за цей сегмент"
},
@@ -113,6 +116,9 @@
"connectionError": {
"message": "Помилка з'єднання. Код помилки: "
},
"segmentsStillLoading": {
"message": "Сегмент все ще завантажується..."
},
"clearTimes": {
"message": "Очистити сегменти"
},
@@ -122,6 +128,9 @@
"closePopup": {
"message": "Закрити вікно"
},
"closeIcon": {
"message": "Закрити іконку"
},
"SubmitTimes": {
"message": "Надіслати сегменти"
},
@@ -246,6 +255,12 @@
"whatRefetchWhenNotFound": {
"message": "Якщо відео нове і для нього не знайдено сегментів, то інформація про них буде оновлюватися кожні пару хвилин, поки ви дивитеся відео."
},
"enableShowCategoryWithoutPermission": {
"message": "Показувати категорії в меню подання навіть без дозволу на подання"
},
"whatShowCategoryWithoutPermission": {
"message": "Деякі категорії потребують дозволу на подання через мінімальні вимоги до репутації"
},
"showNotice": {
"message": "Показувати сповіщення знову"
},
@@ -408,9 +423,18 @@
"statusReminder": {
"message": "Дивіться стан сервера на status.sponsor.ajay.app."
},
"changeUserID": {
"message": "Імпорт/експорт вашого приватного UserID"
},
"whatChangeUserID": {
"message": "Тримайте його в таємниці. Ставтеся до нього як до паролю і не передавайте нікому. Якщо хтось їм заволодіє, то зможе видати себе за вас. Якщо ви шукаєте публічний ID користувача, натисніть значок буфера обміну у спливаючому вікні."
},
"setUserID": {
"message": "Встан. приватний UserID"
},
"userIDChangeWarning": {
"message": "Попередження: зміна приватного UserID є постійною. Ви впевнені, що хочете це зробити? Про всяк випадок обов’язково зробіть резервну копію свого старого."
},
"createdBy": {
"message": "Створено"
},
@@ -454,6 +478,12 @@
"minDurationDescription": {
"message": "Сегменти коротше цього значення не будуть пропускатися і не будуть показані в плеєрі."
},
"enableManualSkipOnFullVideo": {
"message": "Використовуйте ручний пропуск, якщо існує повна мітка відео"
},
"whatManualSkipOnFullVideo": {
"message": "Для людей, які хочуть дивитися відео без перерв, якщо воно повністю спонсороване або самореклама."
},
"skipNoticeDuration": {
"message": "Тривалість повідомлення пропуску (в секундах):"
},
@@ -502,6 +532,9 @@
"exportOptionsUpload": {
"message": "Завантажити з файлу"
},
"whatExportOptions": {
"message": "Вся ваша конфігурація в JSON. Це включає ваш приватний UserID, тому використовуйте з розумом."
},
"setOptions": {
"message": "Встановити параметри"
},
@@ -573,12 +606,24 @@
"category_sponsor_guideline1": {
"message": "Платні акції"
},
"category_sponsor_guideline2": {
"message": "Не для пожертвувань чи товарів на замовлення"
},
"category_selfpromo": {
"message": "Самореклама/рекомендація"
},
"category_selfpromo_description": {
"message": "Схоже на \"Спонсора\", але для безкоштовної реклами і самореклами. Включає себе вставки про мерчендайз, пожертвування або інформацію про тих, разом з ким було зроблено відео."
},
"category_selfpromo_guideline1": {
"message": "Пожертвування, членства та мерч"
},
"category_selfpromo_guideline2": {
"message": "Безкоштовні вітання, які не додають до відео"
},
"category_selfpromo_guideline3": {
"message": "Не для корпоративних продуктів і мерча"
},
"category_exclusive_access": {
"message": "Ексклюзивний доступ"
},
@@ -589,12 +634,24 @@
"message": "Це відео демонструє продукт, послугу або місцеположення, до яких автори отримали безоплатний або субсидований доступ",
"description": "Short description for this category"
},
"category_exclusive_access_guideline1": {
"message": "У відео демонструють щось із безкоштовним або субсидованим доступом"
},
"category_interaction": {
"message": "Нагадування про взаємодію (підписка)"
},
"category_interaction_description": {
"message": "Коли є коротке нагадування поставити лайк, підписатися на канал або в соцмережах в середині вмісту. Якщо ця вставка тривала або про щось конкретне, вона повинна класифікуватися як самореклама."
},
"category_interaction_guideline1": {
"message": "Короткі нагадування поставити лайк, підписатися"
},
"category_interaction_guideline2": {
"message": "Включає в себе непрямі нагадування про коментарі"
},
"category_interaction_guideline3": {
"message": "Не для загального просування, лише для закликів до дії"
},
"category_interaction_short": {
"message": "Нагадування про взаємодію"
},
@@ -607,24 +664,54 @@
"category_intro_short": {
"message": "Заставка"
},
"category_intro_guideline1": {
"message": "Інтервал без реального змісту"
},
"category_intro_guideline2": {
"message": "Не для переходів з інформацією"
},
"category_outro": {
"message": "Кінцева заставка/титри"
},
"category_outro_description": {
"message": "Титри або час появи кінцевих заставок YouTube. Не для підведення підсумків сказаного у відео."
},
"category_outro_guideline1": {
"message": "Не включайте вміст, навіть якщо на екрані є кінцеві картки"
},
"category_preview": {
"message": "Попередній перегляд/короткий зміст"
},
"category_preview_description": {
"message": "Колекція кліпів, які показують, що відбувається в цьому відео або інших відео в серії, де вся інформація повторюється пізніше у відео."
},
"category_preview_guideline1": {
"message": "Кліпи, які з’являться пізніше або в майбутньому відео"
},
"category_preview_guideline2": {
"message": "Повторення попереднього відео"
},
"category_preview_guideline3": {
"message": "Не для розділів, які додають додатковий вміст"
},
"category_filler": {
"message": "Дотичне наповнення/Жарти"
},
"category_filler_description": {
"message": "Дотичні сцени додані лише для наповнення або гумору, які не потрібні для розуміння основного вмісту відео. Це не повинно включати сегменти, що надають контекст або передісторію\". Це дуже агресивна категорія, призначена для випадків, коли ви не в настрої «розважатися»."
},
"category_filler_short": {
"message": "Наповнення"
},
"category_filler_guideline1": {
"message": "Дотичні сцени лише для наповнення чи гумору"
},
"category_filler_guideline2": {
"message": "Відволікання, ляпи, повтори"
},
"category_filler_guideline3": {
"message": "Не для сцен, які потрібні для розуміння теми"
},
"category_music_offtopic": {
"message": "Музика: Сегмент без музики"
},
@@ -634,6 +721,9 @@
"category_music_offtopic_short": {
"message": "Без музики"
},
"category_music_offtopic_guideline1": {
"message": "Розділів немає в офіційних випусках"
},
"category_music_offtopic_guideline2": {
"message": "Не музика під час виступу наживо"
},
@@ -643,6 +733,9 @@
"category_poi_highlight_description": {
"message": "Частина відео яку шукає більшість людей (Аналогічно коментарю \"Відео починається з Х:ХХ\")."
},
"category_poi_highlight_guideline1": {
"message": "Розділ, який шукають більшість людей"
},
"category_poi_highlight_guideline2": {
"message": "Може пропустити контекст"
},
@@ -652,6 +745,18 @@
"category_chapter": {
"message": "Розділ"
},
"category_chapter_description": {
"message": "Розділи з іменами користувача, що описують основні розділи відео."
},
"category_chapter_guideline1": {
"message": "Не згадуйте про торгові марки спонсорів"
},
"category_chapter_guideline2": {
"message": "Використовуйте більші розділи для загальних розділів"
},
"category_chapter_guideline3": {
"message": "Менші розділи можна розмістити всередині великих"
},
"category_livestream_messages": {
"message": "Прямі трансляції: пожертвування/читання повідомлення"
},
@@ -826,6 +931,9 @@
"categoryPillTitleText": {
"message": "Все відео позначене цією категорією, воно є її уособленням, тож категорію неможливо виокремити."
},
"chapterNameTooltipWarning": {
"message": "Одна з назв ваших розділів схожа на категорію. Ви повинні використовувати категорії, коли це можливо."
},
"experiementOptOut": {
"message": "Відмова від усіх майбутніх експериментів",
"description": "This is used in a popup about a new experiment to get a list of unlisted videos to back up since all unlisted videos uploaded before 2017 will be set to private."
@@ -947,6 +1055,12 @@
"hideSegment": {
"message": "Приховати сегмент"
},
"skipSegment": {
"message": "Пропустити сегмент"
},
"playChapter": {
"message": "Відтворити розділ"
},
"SponsorTimeEditScrollNewFeature": {
"message": "Навівши курсор на поле редагування, користуйтеся колесом прокрутки, щоб швидко відрегулювати час. Комбінації клавіш ctrl або shift можуть бути використані для точнішої настройки змін."
},
@@ -1013,22 +1127,48 @@
"exportSegments": {
"message": "Експортувати сегменти"
},
"importSegments": {
"message": "Імпорт сегмента"
},
"Import": {
"message": "Імпорт",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"redeemSuccess": {
"message": "Купон успішно!"
},
"redeemFailed": {
"message": "Ліцензійний ключ недійсний"
},
"hideUpsells": {
"message": "Опції приховування недоступні без додаткової оплати"
},
"chooseACountry": {
"message": "Виберіть країну"
},
"noDiscount": {
"message": "Ви не відповідаєте вимогам на знижку"
},
"discountLink": {
"message": "Посилання на знижку (див. рожеву ціну)"
},
"selectYourCountry": {
"message": "Виберіть вашу країну"
},
"alreadyDonated": {
"message": "Якщо ви вже пожертвували будь-яку суму, ви можете отримати безкоштовний доступ, надіславши електронний лист:\nВажливо:Після двокрапки вказується адреса електронної пошти",
"description": "After the colon is an email address"
},
"cantAfford": {
"message": "Якщо ви не можете дозволити собі придбати ліцензію, натисніть {тут}, щоб дізнатися, чи маєте ви право на знижку",
"description": "Keep the curly braces. The word 'here' should be translated as well."
},
"patreonSignIn": {
"message": "Вхід через Patreon"
},
"redeem": {
"message": "Купон"
},
"joinOnPatreon": {
"message": "Підписатися на Patreon"
},
@@ -1038,6 +1178,20 @@
"enterLicenseKey": {
"message": "Введіть ліцензійний ключ"
},
"chaptersPage1": {
"message": "SponsorBlock функція краудсорсингу розділів доступна лише для людей, які придбали ліцензію, або для людей, яким надано безкоштовний доступ через їхні попередні внески"
},
"chaptersPage2": {
"message": "Примітка: Дозвіл надсилати глави все ще базується на обчисленій репутації. Придбання ліцензії дозволяє лише переглядати розділи, надіслані іншими",
"description": "On the chapters page for getting access to the paid chapters feature"
},
"chapterNewFeature": {
"message": "Нова функція: краудсорсингові користувацькі розділи. Це розділи у відео зі спеціальними назвами, які можна складати, щоб отримати точніші результати. Придбайте ліцензію, щоб переглядати розділи цього відео, наприклад: ",
"description": "After the comma, a list of chapters for this video will appear"
},
"chapterNewFeature2": {
"message": "Нова функція: краудсорсингові користувацькі розділи. Це розділи у відео зі спеціальними назвами, які можна складати, щоб отримати точніші результати. Ви маєте безкоштовний доступ, увімкніть параметр."
},
"unsubmittedSegmentCounts": {
"message": "Наразі у вас є {0} на {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
@@ -1092,5 +1246,8 @@
},
"exportSegmentsAsURL": {
"message": "Поділитися посиланням"
},
"segmentFetchFailureWarning": {
"message": "Попередження: сервер ще не завантажив сегменти. Можливо, у цьому відео вже є сегменти, але ви просто не отримали їх через проблеми з сервером."
}
}

View File

@@ -25,6 +25,19 @@
"Segments": {
"message": "片段"
},
"SegmentsCap": {
"message": "片段"
},
"Chapters": {
"message": "章节"
},
"renderAsChapters": {
"message": "将片段显示为章节",
"description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
},
"showSegmentNameInChapterBar": {
"message": "在视频时间旁显示当前片段"
},
"upvoteButtonInfo": {
"message": "为这个提交点赞"
},
@@ -52,6 +65,9 @@
"reskip": {
"message": "继续跳过"
},
"unmute": {
"message": "取消静音"
},
"paused": {
"message": "已暂停"
},
@@ -85,6 +101,9 @@
"noVideoID": {
"message": "未找到 YouTube 视频。\n如果识别错误请刷新此页面。"
},
"refreshSegments": {
"message": "刷新片段"
},
"success": {
"message": "成功 "
},
@@ -106,6 +125,9 @@
"closePopup": {
"message": "关闭弹窗"
},
"closeIcon": {
"message": "关闭图标"
},
"SubmitTimes": {
"message": "提交片段"
},
@@ -155,6 +177,9 @@
"setUsername": {
"message": "设定用户名"
},
"copySegmentID": {
"message": "复制片段 ID"
},
"discordAdvert": {
"message": "快加入官方 Discord 服务器来提供建议与反馈!"
},
@@ -179,6 +204,9 @@
"hideInfoButton": {
"message": "在 Youtube 播放器上隐藏信息按钮"
},
"autoHideInfoButton": {
"message": "自动隐藏信息按钮"
},
"hideDeleteButton": {
"message": "在 Youtube 播放器上隐藏删除按钮"
},
@@ -251,6 +279,13 @@
"skip": {
"message": "跳过"
},
"mute": {
"message": "静音"
},
"full": {
"message": "整个视频",
"description": "Used for the name of the option to label an entire video as sponsor or self promotion."
},
"skip_category": {
"message": "跳过{0}"
},
@@ -304,6 +339,9 @@
"supportOtherSites": {
"message": "支持第三方 YouTube 网站"
},
"supportedSites": {
"message": "支持的站点: "
},
"optionsInfo": {
"message": "启用 Invidious 支持,禁用自动跳过,隐藏按钮等等。"
},
@@ -358,6 +396,15 @@
"exportOptions": {
"message": "导入/导出所有选项"
},
"exportOptionsCopy": {
"message": "编辑/复制"
},
"exportOptionsDownload": {
"message": "保存到文件"
},
"exportOptionsUpload": {
"message": "从文件加载"
},
"setOptions": {
"message": "设定选项"
},
@@ -382,6 +429,9 @@
"preview": {
"message": "预览"
},
"unsubmitted": {
"message": "未提交"
},
"inspect": {
"message": "检查"
},
@@ -400,22 +450,38 @@
"copyDebugInformationComplete": {
"message": "调试信息已复制到剪切板中。 您可以随意移除任何您不想分享的信息。请将其另存为 .txt 文件或粘贴到错误报告中。"
},
"keyAlreadyUsed": {
"message": "此快捷键已绑定到另一个动作。请选择其他快捷键。"
},
"to": {
"message": "到",
"description": "Used between segments. Example: 1:20 to 1:30"
},
"CopiedExclamation": {
"message": "复制成功!",
"description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
},
"category_sponsor": {
"message": "赞助商广告"
},
"category_sponsor_description": {
"message": "付费推广、付费推荐和直接广告。不应用于自我推广或免费提及、推荐他们喜欢的事物/创作者/网站/产品。"
},
"category_sponsor_guideline1": {
"message": "付费推广"
},
"category_sponsor_guideline2": {
"message": "捐赠或自制周边不属于此项"
},
"category_selfpromo": {
"message": "未收钱的/自我推销"
},
"category_selfpromo_description": {
"message": "类似于 “赞助商广告” ,但为无报酬或自我推广。包括有关商品、捐赠的部分或合作者的信息。"
},
"category_selfpromo_guideline1": {
"message": "捐赠、会员和自制周边"
},
"category_interaction": {
"message": "互动提醒(订阅)"
},

View File

@@ -30,6 +30,10 @@
transform: scaleY(0.625) translateY(-30%) translateY(1.5px);
}
.ytp-big-mode .sponsorTwoTooltips .sponsorCategoryTooltip {
top: 75px !important;
}
.progress-bar-line > #previewbar {
height: 3px;
}
@@ -62,6 +66,11 @@ div:hover > #previewbar.sbNotInvidious {
transform: translateY(-1em) !important;
}
/* Pull up for precise seeking */
.ytp-tooltip.sponsorCategoryTooltipVisible .ytp-tooltip-edu {
transform: translateY(-1em) !important;
}
.ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
transform: translateY(-2em) !important;
}
@@ -82,6 +91,11 @@ div:hover > #previewbar.sbNotInvidious {
transform: translateY(2em) !important;
}
/* Pull up for precise seeking */
.ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips .ytp-tooltip-edu {
transform: translateY(-2em) !important;
}
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
transform: translateY(0.5em) !important;
}

View File

@@ -50,37 +50,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
******************************
object-assign
4.1.1 <https://github.com/sindresorhus/object-assign>
The MIT License (MIT)
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
******************************
react
17.0.2 <https://github.com/facebook/react>
18.2.0 <https://github.com/facebook/react>
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
@@ -107,7 +80,7 @@ SOFTWARE.
******************************
react-dom
17.0.2 <https://github.com/facebook/react>
18.2.0 <https://github.com/facebook/react>
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
@@ -134,7 +107,7 @@ SOFTWARE.
******************************
scheduler
0.20.2 <https://github.com/facebook/react>
0.23.0 <https://github.com/facebook/react>
MIT License
Copyright (c) Facebook, Inc. and its affiliates.

View File

@@ -24,7 +24,7 @@ if (utils.isFirefox()) {
utils.wait(() => Config.config !== null).then(function() {
if (Config.config.supportInvidious) utils.setupExtraSiteContentScripts();
});
}
}
function onTabUpdatedListener(tabId: number) {
chrome.tabs.sendMessage(tabId, {
@@ -59,16 +59,16 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
switch(request.message) {
case "openConfig":
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
return;
return false;
case "openHelp":
chrome.tabs.create({url: chrome.runtime.getURL('help/index.html')});
return;
return false;
case "openUpsell":
chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')});
return;
return false;
case "openPage":
chrome.tabs.create({url: chrome.runtime.getURL(request.url)});
return;
return false;
case "sendRequest":
sendRequestToCustomServer(request.type, request.url, request.data).then(async (response) => {
callback({
@@ -77,17 +77,17 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
ok: response.ok
});
});
return true;
case "submitVote":
submitVote(request.type, request.UUID, request.category).then(callback);
//this allows the callback to be called later
return true;
case "registerContentScript":
case "registerContentScript":
registerFirefoxContentScript(request);
return false;
case "unregisterContentScript":
case "unregisterContentScript":
unregisterFirefoxContentScript(request.id)
return false;
case "tabs": {
@@ -106,10 +106,14 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
return true;
}
case "time":
case "infoUpdated":
case "videoChanged":
if (sender.tab) {
popupPort[sender.tab.id]?.postMessage(request);
}
return false;
default:
return false;
}
});
@@ -156,8 +160,8 @@ chrome.runtime.onInstalled.addListener(function () {
/**
* Only works on Firefox.
* Firefox requires that it be applied after every extension restart.
*
* @param {JSON} options
*
* @param {JSON} options
*/
function registerFirefoxContentScript(options: Registration) {
const oldRegistration = contentScriptRegistrations[options.id];
@@ -174,7 +178,7 @@ function registerFirefoxContentScript(options: Registration) {
/**
* Only works on Firefox.
* Firefox requires that this is handled by the background script
*
*
*/
function unregisterFirefoxContentScript(id: string) {
contentScriptRegistrations[id].unregister();
@@ -225,10 +229,10 @@ async function asyncRequestToServer(type: string, address: string, data = {}) {
/**
* Sends a request to the specified url
*
*
* @param type The request type "GET", "POST", etc.
* @param address The address to add to the SponsorBlock server address
* @param callback
* @param callback
*/
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
// If GET, convert JSON to parameters
@@ -248,4 +252,4 @@ async function sendRequestToCustomServer(type: string, url: string, data = {}) {
});
return response;
}
}

View File

@@ -140,16 +140,17 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
}
private openTooltip(): void {
const tooltipMount = document.querySelector("ytd-video-primary-info-renderer > #container") as HTMLElement;
const tooltipMount = document.querySelector("#above-the-fold") as HTMLElement;
if (tooltipMount) {
this.tooltip = new Tooltip({
text: this.getTitleText(),
referenceNode: tooltipMount,
bottomOffset: "70px",
bottomOffset: "0px",
opacity: 0.95,
displayTriangle: false,
showLogo: false,
showGotIt: false
showGotIt: false,
prependElement: tooltipMount.firstElementChild as HTMLElement
});
}
}

View File

@@ -8,41 +8,42 @@ enum CountdownMode {
}
export interface NoticeProps {
noticeTitle: string,
noticeTitle: string;
maxCountdownTime?: () => number,
dontPauseCountdown?: boolean,
amountOfPreviousNotices?: number,
showInSecondSlot?: boolean,
timed?: boolean,
idSuffix?: string,
maxCountdownTime?: () => number;
dontPauseCountdown?: boolean;
amountOfPreviousNotices?: number;
showInSecondSlot?: boolean;
timed?: boolean;
idSuffix?: string;
fadeIn?: boolean,
startFaded?: boolean,
firstColumn?: React.ReactElement[] | React.ReactElement,
firstRow?: React.ReactElement,
bottomRow?: React.ReactElement[],
fadeIn?: boolean;
startFaded?: boolean;
firstColumn?: React.ReactElement[] | React.ReactElement;
firstRow?: React.ReactElement;
bottomRow?: React.ReactElement[];
smaller?: boolean,
limitWidth?: boolean,
extraClass?: string,
hideLogo?: boolean,
hideRightInfo?: boolean,
smaller?: boolean;
limitWidth?: boolean;
extraClass?: string;
hideLogo?: boolean;
hideRightInfo?: boolean;
// Callback for when this is closed
closeListener: () => void,
onMouseEnter?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void,
closeListener: () => void;
onMouseEnter?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
zIndex?: number,
style?: React.CSSProperties
zIndex?: number;
style?: React.CSSProperties;
biggerCloseButton?: boolean;
children?: React.ReactNode;
}
export interface NoticeState {
maxCountdownTime: () => number,
maxCountdownTime: () => number;
countdownTime: number,
countdownMode: CountdownMode,
countdownTime: number;
countdownMode: CountdownMode;
mouseHovering: boolean;

View File

@@ -1,10 +1,11 @@
import * as React from "react";
export interface NoticeTextSelectionProps {
icon?: string,
text: string,
idSuffix: string,
onClick?: (event: React.MouseEvent) => unknown
icon?: string;
text: string;
idSuffix: string;
onClick?: (event: React.MouseEvent) => unknown;
children?: React.ReactNode;
}
export interface NoticeTextSelectionState {

View File

@@ -373,6 +373,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
</span>
);
}
return null;
}
getSubmissionChooser(): JSX.Element[] {

View File

@@ -14,15 +14,16 @@ import { DEFAULT_CATEGORY } from "../utils/categoryUtils";
const utils = new Utils();
export interface SponsorTimeEditProps {
index: number,
index: number;
idSuffix: string,
idSuffix: string;
// Contains functions and variables from the content script needed by the skip notice
contentContainer: ContentContainer,
contentContainer: ContentContainer;
submissionNotice: SubmissionNoticeComponent;
categoryList?: Category[];
categoryChangeListener?: (index: number, category: Category) => void;
children?: React.ReactNode;
}
export interface SponsorTimeEditState {
@@ -540,9 +541,14 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
if (time === null) time = sponsorTime.segment[0];
const addedTime = sponsorTime.segment.length === 1;
sponsorTime.segment[index] = time;
if (sponsorTime.actionType === ActionType.Poi) sponsorTime.segment[1] = time;
if (addedTime) {
this.props.contentContainer().updateEditButtonsOnPlayer();
}
this.setState({
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
}, () => this.saveEditTimes());
@@ -574,6 +580,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
saveEditTimes(): void {
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
const category = this.categoryOptionRef.current.value as Category
if (this.state.editing) {
const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
@@ -581,11 +588,18 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
// Change segment time only if the format was correct
if (startTime !== null && endTime !== null) {
const addingTime = sponsorTimesSubmitting[this.props.index].segment.length === 1;
sponsorTimesSubmitting[this.props.index].segment = [startTime, endTime];
if (addingTime) {
this.props.contentContainer().updateEditButtonsOnPlayer();
}
}
} else if (this.state.sponsorTimeEdits[1] === null && category === "outro" && !sponsorTimesSubmitting[this.props.index].segment[1]) {
sponsorTimesSubmitting[this.props.index].segment[1] = this.props.contentContainer().v.duration;
this.props.contentContainer().updateEditButtonsOnPlayer();
}
const category = this.categoryOptionRef.current.value as Category
sponsorTimesSubmitting[this.props.index].category = category;
const actionType = this.getNextActionType(category, this.actionTypeOptionRef?.current?.value as ActionType);

View File

@@ -20,8 +20,8 @@ export interface SubmissionNoticeProps {
}
export interface SubmissionNoticeState {
noticeTitle: string,
messages: string[],
noticeTitle: string;
messages: string[];
idSuffix: string;
}

View File

@@ -13,6 +13,7 @@ export interface CategorySkipOptionsProps {
category: Category;
defaultColor?: string;
defaultPreviewColor?: string;
children?: React.ReactNode;
}
export interface CategorySkipOptionsState {

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import Config from "../../config";
import { Keybind } from "../../types";
import KeybindDialogComponent from "./KeybindDialogComponent";
@@ -14,6 +14,7 @@ export interface KeybindState {
}
let dialog;
let root: Root;
class KeybindComponent extends React.Component<KeybindProps, KeybindState> {
constructor(props: KeybindProps) {
@@ -56,11 +57,12 @@ class KeybindComponent extends React.Component<KeybindProps, KeybindState> {
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);
root = createRoot(dialog);
root.render(<KeybindDialogComponent option={this.props.option} closeListener={(updateWith) => this.closeEditDialog(updateWith)} />);
}
closeEditDialog(updateWith: Keybind): void {
ReactDOM.unmountComponentAtNode(dialog);
root.unmount();
dialog.remove();
if (updateWith != null)
this.setState({keybind: updateWith});

View File

@@ -5,6 +5,7 @@ import { exportTimes, exportTimesAsHashParam } from "../../utils/exporter";
export interface UnsubmittedVideosListItemProps {
videoID: string;
children?: React.ReactNode;
}
export interface UnsubmittedVideosListItemState {

View File

@@ -7,7 +7,7 @@ export interface UnsubmittedVideosProps {
}
export interface UnsubmittedVideosState {
tableVisible: boolean,
tableVisible: boolean;
}
class UnsubmittedVideosComponent extends React.Component<UnsubmittedVideosProps, UnsubmittedVideosState> {

View File

@@ -8,122 +8,124 @@ export interface Permission {
}
interface SBConfig {
userID: string,
isVip: boolean,
permissions: Record<Category, Permission>,
userID: string;
isVip: boolean;
permissions: Record<Category, Permission>;
/* Contains unsubmitted segments that the user has created. */
unsubmittedSegments: Record<string, SponsorTime[]>,
defaultCategory: Category,
renderSegmentsAsChapters: boolean,
whitelistedChannels: string[],
forceChannelCheck: boolean,
minutesSaved: number,
skipCount: number,
sponsorTimesContributed: number,
submissionCountSinceCategories: number, // New count used to show the "Read The Guidelines!!" message
showTimeWithSkips: boolean,
disableSkipping: boolean,
muteSegments: boolean,
fullVideoSegments: boolean,
manualSkipOnFullVideo: boolean,
trackViewCount: boolean,
trackViewCountInPrivate: boolean,
trackDownvotes: boolean,
dontShowNotice: boolean,
noticeVisibilityMode: NoticeVisbilityMode,
hideVideoPlayerControls: boolean,
hideInfoButtonPlayerControls: boolean,
hideDeleteButtonPlayerControls: boolean,
hideUploadButtonPlayerControls: boolean,
hideSkipButtonPlayerControls: boolean,
hideDiscordLaunches: number,
hideDiscordLink: boolean,
invidiousInstances: string[],
supportInvidious: boolean,
serverAddress: string,
minDuration: number,
skipNoticeDuration: number,
audioNotificationOnSkip: boolean,
checkForUnlistedVideos: boolean,
testingServer: boolean,
refetchWhenNotFound: boolean,
ytInfoPermissionGranted: boolean,
allowExpirements: boolean,
showDonationLink: boolean,
showPopupDonationCount: number,
showUpsells: boolean,
donateClicked: number,
autoHideInfoButton: boolean,
autoSkipOnMusicVideos: boolean,
unsubmittedSegments: Record<string, SponsorTime[]>;
defaultCategory: Category;
renderSegmentsAsChapters: boolean;
whitelistedChannels: string[];
forceChannelCheck: boolean;
minutesSaved: number;
skipCount: number;
sponsorTimesContributed: number;
submissionCountSinceCategories: number; // New count used to show the "Read The Guidelines!!" message
showTimeWithSkips: boolean;
disableSkipping: boolean;
muteSegments: boolean;
fullVideoSegments: boolean;
manualSkipOnFullVideo: boolean;
trackViewCount: boolean;
trackViewCountInPrivate: boolean;
trackDownvotes: boolean;
dontShowNotice: boolean;
noticeVisibilityMode: NoticeVisbilityMode;
hideVideoPlayerControls: boolean;
hideInfoButtonPlayerControls: boolean;
hideDeleteButtonPlayerControls: boolean;
hideUploadButtonPlayerControls: boolean;
hideSkipButtonPlayerControls: boolean;
hideDiscordLaunches: number;
hideDiscordLink: boolean;
invidiousInstances: string[];
supportInvidious: boolean;
serverAddress: string;
minDuration: number;
skipNoticeDuration: number;
audioNotificationOnSkip: boolean;
checkForUnlistedVideos: boolean;
testingServer: boolean;
refetchWhenNotFound: boolean;
ytInfoPermissionGranted: boolean;
allowExpirements: boolean;
showDonationLink: boolean;
showPopupDonationCount: number;
showUpsells: boolean;
donateClicked: number;
autoHideInfoButton: boolean;
autoSkipOnMusicVideos: boolean;
colorPalette: {
red: string,
white: string,
locked: string
},
scrollToEditTimeUpdate: boolean,
categoryPillUpdate: boolean,
showChapterInfoMessage: boolean,
darkMode: boolean,
showCategoryGuidelines: boolean,
showCategoryWithoutPermission: boolean,
showSegmentNameInChapterBar: boolean,
red: string;
white: string;
locked: string;
};
scrollToEditTimeUpdate: boolean;
categoryPillUpdate: boolean;
showChapterInfoMessage: boolean;
darkMode: boolean;
showCategoryGuidelines: boolean;
showCategoryWithoutPermission: boolean;
showSegmentNameInChapterBar: boolean;
useVirtualTime: boolean;
showSegmentFailedToFetchWarning: boolean;
// Used to cache calculated text color info
categoryPillColors: {
[key in Category]: {
lastColor: string,
textColor: string
lastColor: string;
textColor: string;
}
}
};
skipKeybind: Keybind,
startSponsorKeybind: Keybind,
submitKeybind: Keybind,
nextChapterKeybind: Keybind,
previousChapterKeybind: Keybind,
skipKeybind: Keybind;
startSponsorKeybind: Keybind;
submitKeybind: Keybind;
nextChapterKeybind: Keybind;
previousChapterKeybind: Keybind;
// What categories should be skipped
categorySelections: CategorySelection[],
categorySelections: CategorySelection[];
payments: {
licenseKey: string,
lastCheck: number,
lastFreeCheck: number,
freeAccess: boolean,
chaptersAllowed: boolean
}
licenseKey: string;
lastCheck: number;
lastFreeCheck: number;
freeAccess: boolean;
chaptersAllowed: boolean;
};
// Preview bar
barTypes: {
"preview-chooseACategory": PreviewBarOption,
"sponsor": PreviewBarOption,
"preview-sponsor": PreviewBarOption,
"selfpromo": PreviewBarOption,
"preview-selfpromo": PreviewBarOption,
"exclusive_access": PreviewBarOption,
"interaction": PreviewBarOption,
"preview-interaction": PreviewBarOption,
"intro": PreviewBarOption,
"preview-intro": PreviewBarOption,
"outro": PreviewBarOption,
"preview-outro": PreviewBarOption,
"preview": PreviewBarOption,
"preview-preview": PreviewBarOption,
"music_offtopic": PreviewBarOption,
"preview-music_offtopic": PreviewBarOption,
"poi_highlight": PreviewBarOption,
"preview-poi_highlight": PreviewBarOption,
"filler": PreviewBarOption,
"preview-filler": PreviewBarOption,
}
"preview-chooseACategory": PreviewBarOption;
"sponsor": PreviewBarOption;
"preview-sponsor": PreviewBarOption;
"selfpromo": PreviewBarOption;
"preview-selfpromo": PreviewBarOption;
"exclusive_access": PreviewBarOption;
"interaction": PreviewBarOption;
"preview-interaction": PreviewBarOption;
"intro": PreviewBarOption;
"preview-intro": PreviewBarOption;
"outro": PreviewBarOption;
"preview-outro": PreviewBarOption;
"preview": PreviewBarOption;
"preview-preview": PreviewBarOption;
"music_offtopic": PreviewBarOption;
"preview-music_offtopic": PreviewBarOption;
"poi_highlight": PreviewBarOption;
"preview-poi_highlight": PreviewBarOption;
"filler": PreviewBarOption;
"preview-filler": PreviewBarOption;
};
}
export type VideoDownvotes = { segments: { uuid: HashedValue, hidden: SponsorHideType }[] , lastAccess: number };
export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[] ; lastAccess: number };
interface SBStorage {
/* VideoID prefixes to UUID prefixes */
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>,
navigationApiAvailable: boolean,
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>;
navigationApiAvailable: boolean;
}
export interface SBObject {
@@ -200,6 +202,8 @@ const Config: SBObject = {
showCategoryGuidelines: true,
showCategoryWithoutPermission: false,
showSegmentNameInChapterBar: true,
useVirtualTime: true,
showSegmentFailedToFetchWarning: true,
categoryPillColors: {},
@@ -340,7 +344,7 @@ const Config: SBObject = {
// Function setup
function configProxy(): { sync: SBConfig, local: SBStorage } {
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) {

View File

@@ -8,6 +8,7 @@ import {
ContentContainer,
HashedValue,
Keybind,
PageType,
ScheduledTime,
SegmentUUID,
SkipToTimeParams,
@@ -18,7 +19,6 @@ import {
ToggleSkippable,
VideoID,
VideoInfo,
PageType
} from "./types";
import Utils from "./utils";
import PreviewBar, { PreviewBarSegment } from "./js-components/previewBar";
@@ -45,6 +45,8 @@ const utils = new Utils();
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
const skipBuffer = 0.003;
//was sponsor data found when doing SponsorsLookup
let sponsorDataFound = false;
//the actual sponsorTimes if loaded and UUIDs associated with them
@@ -56,6 +58,7 @@ let sponsorVideoID: VideoID = null;
const skipNotices: SkipNotice[] = [];
let activeSkipKeybindElement: ToggleSkippable = null;
let retryFetchTimeout: NodeJS.Timeout = null;
let shownSegmentFailedToFetchWarning = false;
// JSON video info
let videoInfo: VideoInfo = null;
@@ -68,7 +71,7 @@ let channelIDInfo: ChannelIDInfo;
// Locked Categories in this tab, like: ["sponsor","intro","outro"]
let lockedCategories: Category[] = [];
// Used to calculate a more precise "virtual" video time
let lastKnownVideoTime: { videoTime: number, preciseTime: number } = {
let lastKnownVideoTime: { videoTime: number; preciseTime: number } = {
videoTime: null,
preciseTime: null
};
@@ -125,7 +128,7 @@ let categoryPill: CategoryPill = null;
let controls: HTMLElement | null = null;
/** Contains buttons created by `createButton()`. */
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement, setupListener: boolean}> = {};
const playerButtons: Record<string, {button: HTMLButtonElement; image: HTMLImageElement; setupListener: boolean}> = {};
// Direct Links after the config is loaded
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
@@ -215,7 +218,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
case "getVideoID":
sendResponse({
videoID: sponsorVideoID,
creatingSegment: isSegmentCreationInProgress(),
});
break;
@@ -243,15 +245,9 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
// update video on refresh if videoID invalid
if (!sponsorVideoID) videoIDChange(getYouTubeVideoID(document));
// fetch segments
sponsorsLookup(false).then(() => sendResponse({
found: sponsorDataFound,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
}));
sponsorsLookup(false);
return true;
break;
case "unskip":
unskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), null, true);
break;
@@ -265,6 +261,11 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
utils.getSponsorTimeFromUUID(sponsorTimes, request.UUID).hidden = request.type;
utils.addHiddenSegment(sponsorVideoID, request.UUID, request.type);
updatePreviewBar();
if (skipButtonControlBar?.isEnabled()
&& sponsorTimesSubmitting.every((s) => s.hidden !== SponsorHideType.Visible || s.actionType !== ActionType.Poi)) {
skipButtonControlBar.disable();
}
break;
case "closePopup":
closeInfoMenu();
@@ -276,10 +277,15 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
const importedSegments = importTimes(request.data, video.duration);
let addedSegments = false;
for (const segment of importedSegments) {
if (!sponsorTimesSubmitting.concat(sponsorTimes ?? []).some(
if (!sponsorTimesSubmitting.some(
(s) => Math.abs(s.segment[0] - segment.segment[0]) < 1
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)
&& (segment.category !== "chapter" || utils.getCategorySelection("chapter"))) {
&& Math.abs(s.segment[1] - segment.segment[1]) < 1)) {
if (segment.category === "chapter" && !utils.getCategorySelection("chapter")) {
segment.category = "chooseACategory" as Category;
segment.actionType = ActionType.Skip;
segment.description = "";
}
sponsorTimesSubmitting.push(segment);
addedSegments = true;
}
@@ -291,6 +297,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false);
submitSponsorTimes();
}
sendResponse({
@@ -345,6 +352,8 @@ function resetValues() {
sponsorTimes = [];
existingChaptersImported = false;
sponsorSkipped = [];
lastResponseStatus = 0;
shownSegmentFailedToFetchWarning = false;
sponsorVideoID = null;
videoInfo = null;
@@ -376,15 +385,15 @@ function resetValues() {
// Reset advert playing flag
isAdPlaying = false;
skipButtonControlBar?.disable();
categoryPill?.setVisibility(false);
for (let i = 0; i < skipNotices.length; i++) {
skipNotices.pop()?.close();
}
skipButtonControlBar?.disable();
categoryPill?.setVisibility(false);
}
async function videoIDChange(id): Promise<void> {
async function videoIDChange(id: string): Promise<void> {
// don't switch to invalid value
if (!id && sponsorVideoID && !document?.URL?.includes("youtube.com/clip/")) return;
//if the id has not changed return unless the video element has changed
@@ -438,10 +447,14 @@ async function videoIDChange(id): Promise<void> {
}
}
//close popup
closeInfoMenu();
// Notify the popup about the video change
chrome.runtime.sendMessage({
message: "videoChanged",
videoID: sponsorVideoID,
whitelisted: channelWhitelisted
});
sponsorsLookup(id);
sponsorsLookup();
// Make sure all player buttons are properly added
updateVisibilityOfPlayerControlsButton();
@@ -502,6 +515,11 @@ function createPreviewBar(): void {
// For Invidious/VideoJS
selector: ".vjs-progress-holder",
isVisibleCheck: false
}, {
// For Youtube Music
// there are two sliders, one for volume and one for progress - both called #progressContainer
selector: "#progress-bar>#sliderContainer>div>#sliderBar>#progressContainer",
isVisibleCheck: false
}
];
@@ -511,7 +529,7 @@ function createPreviewBar(): void {
if (el) {
const chapterVote = new ChapterVote(voteAsync);
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote);
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote, () => importExistingChapters(false));
updatePreviewBar();
@@ -592,7 +610,6 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
const skipTime: number[] = [currentSkip?.scheduledTime, skipInfo.array[skipInfo.endIndex]?.segment[1]];
const timeUntilSponsor = skipTime?.[0] - currentTime;
const videoID = sponsorVideoID;
const skipBuffer = 0.003;
if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1
&& timeUntilSponsor < skipBuffer && shouldAutoSkip(currentSkip))) {
@@ -712,10 +729,10 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
function getVirtualTime(): number {
const virtualTime = lastTimeFromWaitingEvent ?? (lastKnownVideoTime.videoTime ?
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
(performance.now() - lastKnownVideoTime.preciseTime) * video.playbackRate / 1000 + lastKnownVideoTime.videoTime : null);
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) {
if (Config.config.useVirtualTime && !utils.isFirefox() && !isSafari() && virtualTime
&& Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) {
return virtualTime;
} else {
return video.currentTime;
@@ -877,9 +894,19 @@ function setupVideoListeners() {
}
}
});
video.addEventListener('ratechange', () => startSponsorSchedule());
video.addEventListener('ratechange', () => {
updateVirtualTime();
lastTimeFromWaitingEvent = null;
startSponsorSchedule();
});
// Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740)
video.addEventListener('videoSpeed_ratechange', () => startSponsorSchedule());
video.addEventListener('videoSpeed_ratechange', () => {
updateVirtualTime();
lastTimeFromWaitingEvent = null;
startSponsorSchedule();
});
const paused = () => {
// Reset lastCheckVideoTime
lastCheckVideoTime = -1;
@@ -948,13 +975,13 @@ async function sponsorsLookup(keepOldSubmissions = true) {
setupVideoMutationListener();
const showChapterMessage = Config.config.showUpsells
&& Config.config.payments.lastCheck !== 0
&& Config.config.payments.lastCheck !== 0
&& !noRefreshFetchingChaptersAllowed()
&& Config.config.showChapterInfoMessage
&& Config.config.skipCount > 200;
if (!showChapterMessage
&& Config.config.showChapterInfoMessage
if (!showChapterMessage
&& Config.config.showChapterInfoMessage
&& Config.config.payments.freeAccess) {
Config.config.showChapterInfoMessage = false;
@@ -1004,6 +1031,14 @@ async function sponsorsLookup(keepOldSubmissions = true) {
?.sort((a, b) => a.segment[0] - b.segment[0]);
if (!recievedSegments || !recievedSegments.length) {
// return if no video found
chrome.runtime.sendMessage({
message: "infoUpdated",
found: false,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
});
retryFetch(404);
return;
}
@@ -1093,6 +1128,16 @@ async function sponsorsLookup(keepOldSubmissions = true) {
importExistingChapters(true);
// notify popup of segment changes
chrome.runtime.sendMessage({
message: "infoUpdated",
found: sponsorDataFound,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
time: video.currentTime,
onMobileYouTube
});
if (Config.config.isVip) {
lockedCategoriesLookup();
}
@@ -1138,8 +1183,8 @@ async function lockedCategoriesLookup(): Promise<void> {
}
function retryFetch(errorCode: number): void {
if (!Config.config.refetchWhenNotFound) return;
sponsorDataFound = false;
if (!Config.config.refetchWhenNotFound) return;
if (retryFetchTimeout) clearTimeout(retryFetchTimeout);
if ((errorCode !== 404 && retryCount > 1) || (errorCode !== 404 && retryCount > 10)) {
@@ -1190,7 +1235,8 @@ function startSkipScheduleCheckingForStartSponsors() {
// For highlight category
const poiSegments = sponsorTimes
.filter((time) => time.segment[1] > video.currentTime && time.actionType === ActionType.Poi)
.filter((time) => time.segment[1] > video.currentTime
&& time.actionType === ActionType.Poi && time.hidden === SponsorHideType.Visible)
.sort((a, b) => b.segment[0] - a.segment[0]);
for (const time of poiSegments) {
const skipOption = utils.getCategorySelection(time.category)?.option;
@@ -1219,12 +1265,12 @@ function startSkipScheduleCheckingForStartSponsors() {
}
}
function getYouTubeVideoID(document: Document, url?: string): string | boolean {
function getYouTubeVideoID(document: Document, url?: string): string {
url ||= document.URL;
// pageType shortcut
if (pageType === PageType.Channel) return getYouTubeVideoIDFromDocument()
if (pageType === PageType.Channel) return getYouTubeVideoIDFromDocument();
// clips should never skip, going from clip to full video has no indications.
if (url.includes("youtube.com/clip/")) return false;
if (url.includes("youtube.com/clip/")) return null;
// skip to document and don't hide if on /embed/
if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false, PageType.Embed);
// skip to URL if matches youtube watch or invidious or matches youtube pattern
@@ -1235,10 +1281,10 @@ function getYouTubeVideoID(document: Document, url?: string): string | boolean {
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false);
}
function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watch): string | boolean {
function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watch): string {
const selector = "a.ytp-title-link[data-sessionlink='feature=player-title']";
// get ID from document (channel trailer / embedded playlist)
const element = pageHint === PageType.Embed ? document.querySelector(selector)
const element = pageHint === PageType.Embed ? document.querySelector(selector)
: video?.parentElement?.parentElement?.querySelector(selector);
const videoURL = element?.getAttribute("href");
if (videoURL) {
@@ -1247,11 +1293,11 @@ function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watc
pageType = pageHint;
return getYouTubeVideoIDFromURL(videoURL);
} else {
return false;
return null;
}
}
function getYouTubeVideoIDFromURL(url: string): string | boolean {
function getYouTubeVideoIDFromURL(url: string): string {
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
//Attempt to parse url
@@ -1260,7 +1306,7 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
urlObject = new URL(url);
} catch (e) {
console.error("[SB] Unable to parse URL: " + url);
return false;
return null;
}
// Check if valid hostname
@@ -1274,7 +1320,7 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoIDFromURL(url)));
}
return false;
return null;
} else {
onInvidious = false;
}
@@ -1282,17 +1328,17 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean {
//Get ID from searchParam
if (urlObject.searchParams.has("v") && ["/watch", "/watch/"].includes(urlObject.pathname) || urlObject.pathname.startsWith("/tv/watch")) {
const id = urlObject.searchParams.get("v");
return id.length == 11 ? id : false;
return id.length == 11 ? id : null;
} else if (urlObject.pathname.startsWith("/embed/") || urlObject.pathname.startsWith("/shorts/")) {
try {
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;
return null;
}
}
return false;
return null;
}
/**
@@ -1403,7 +1449,7 @@ async function whitelistCheck() {
* Returns info about the next upcoming sponsor skip
*/
function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean):
{array: ScheduledTime[], index: number, endIndex: number, extraIndexes: number[], openNotice: boolean} {
{array: ScheduledTime[]; index: number; endIndex: number; extraIndexes: number[]; openNotice: boolean} {
const autoSkipSorter = (segment: ScheduledTime) => {
const skipOption = utils.getCategorySelection(segment.category)?.option;
@@ -1487,7 +1533,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
const currentSegment = sponsorTimes[i].segment;
const latestEndTime = sponsorTimes[latestEndTimeIndex].segment[1];
if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime
if (currentSegment[0] - skipBuffer <= latestEndTime && currentSegment[1] > latestEndTime
&& (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible)
&& shouldAutoSkip(sponsorTimes[i])
&& sponsorTimes[i].actionType === ActionType.Skip) {
@@ -1515,12 +1561,19 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
* the current time, but end after
*/
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean,
minimum?: number, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
minimum?: number, hideHiddenSponsors = false): {includedTimes: ScheduledTime[]; scheduledTimes: number[]} {
if (!sponsorTimes) return {includedTimes: [], scheduledTimes: []};
const includedTimes: ScheduledTime[] = [];
const scheduledTimes: number[] = [];
const shouldIncludeTime = (segment: ScheduledTime ) => (minimum === undefined
|| ((includeNonIntersectingSegments && segment.scheduledTime >= minimum)
|| (includeIntersectingSegments && segment.scheduledTime < minimum && segment.segment[1] > minimum)))
&& (!hideHiddenSponsors || segment.hidden === SponsorHideType.Visible)
&& segment.segment.length === 2
&& segment.actionType !== ActionType.Poi;
const possibleTimes = sponsorTimes.map((sponsorTime) => ({
...sponsorTime,
scheduledTime: sponsorTime.segment[0]
@@ -1528,7 +1581,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
// Schedule at the end time to know when to unmute and remove title from seek bar
sponsorTimes.forEach(sponsorTime => {
if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime)) {
if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime && shouldIncludeTime(time))
&& (minimum === undefined || sponsorTime.segment[1] > minimum)) {
possibleTimes.push({
...sponsorTime,
scheduledTime: sponsorTime.segment[1]
@@ -1537,13 +1591,7 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
});
for (let i = 0; i < possibleTimes.length; i++) {
if ((minimum === undefined
|| ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum)
|| (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
&& possibleTimes[i].segment.length === 2
&& possibleTimes[i].actionType !== ActionType.Poi) {
if (shouldIncludeTime(possibleTimes[i])) {
scheduledTimes.push(possibleTimes[i].scheduledTime);
includedTimes.push(possibleTimes[i]);
}
@@ -1577,7 +1625,9 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped:
sponsorSkipped[index] = true;
if (!counted) {
Config.config.minutesSaved = Config.config.minutesSaved + secondsSkipped / 60;
Config.config.skipCount = Config.config.skipCount + 1;
if (segment.actionType !== ActionType.Chapter) {
Config.config.skipCount = Config.config.skipCount + 1;
}
counted = true;
}
@@ -1749,7 +1799,7 @@ function createButton(baseID: string, title: string, callback: () => void, image
}
function shouldAutoSkip(segment: SponsorTime): boolean {
return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
return (!Config.config.manualSkipOnFullVideo || !sponsorTimes?.some((s) => s.category === segment.category && s.actionType === ActionType.Full))
&& (utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip ||
(Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")
&& segment.actionType !== ActionType.Poi));
@@ -1792,7 +1842,8 @@ async function updateVisibilityOfPlayerControlsButton(): Promise<void> {
updateEditButtonsOnPlayer();
// Don't show the info button on embeds
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious) {
if (Config.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious
|| document.getElementById("sponsorBlockPopupContainer") != null) {
playerButtons.info.button.style.display = "none";
} else {
playerButtons.info.button.style.removeProperty("display");
@@ -1888,6 +1939,13 @@ function startOrEndTimingNewSegment() {
updateSponsorTimesSubmitting(false);
importExistingChapters(false);
if (lastResponseStatus !== 200 && lastResponseStatus !== 404
&& !shownSegmentFailedToFetchWarning && Config.config.showSegmentFailedToFetchWarning) {
alert(chrome.i18n.getMessage("segmentFetchFailureWarning"));
shownSegmentFailedToFetchWarning = true;
}
}
function getIncompleteSegment(): SponsorTime {
@@ -1902,11 +1960,15 @@ function isSegmentCreationInProgress(): boolean {
function cancelCreatingSegment() {
if (isSegmentCreationInProgress()) {
sponsorTimesSubmitting.splice(sponsorTimesSubmitting.length - 1, 1);
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
if (sponsorTimesSubmitting.length > 1) { // If there's more than one segment: remove last
sponsorTimesSubmitting.pop();
Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
} else { // Otherwise delete the video entry & close submission menu
resetSponsorSubmissionNotice();
sponsorTimesSubmitting = [];
delete Config.config.unsubmittedSegments[sponsorVideoID];
}
Config.forceSyncUpdate("unsubmittedSegments");
if (sponsorTimesSubmitting.length <= 0) resetSponsorSubmissionNotice();
}
updateEditButtonsOnPlayer();
@@ -2051,11 +2113,11 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo
return response;
}
async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): Promise<VoteResponse> {
async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): Promise<VoteResponse | undefined> {
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
// Don't vote for preview sponsors
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source !== SponsorSourceType.Server) return;
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source !== SponsorSourceType.Server) return Promise.resolve(undefined);
// See if the local time saved count and skip count should be saved
if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) {
@@ -2286,11 +2348,16 @@ function windowListenerHandler(event: MessageEvent): void {
}
function updateActiveSegment(currentTime: number): void {
previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
const activeSegments = previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
chrome.runtime.sendMessage({
message: "time",
time: currentTime
});
const chapterSegments = activeSegments?.filter((segment) => segment.actionType === ActionType.Chapter);
if (chapterSegments?.length > 0) {
sendTelemetryAndCount(chapterSegments, 0, true);
}
}
function nextChapter(): void {

View File

@@ -6,26 +6,26 @@
import { PageType } from "./types";
interface StartMessage {
type: "navigation",
pageType: PageType
videoID: string | null,
type: "navigation";
pageType: PageType;
videoID: string | null;
}
interface FinishMessage extends StartMessage {
channelID: string,
channelTitle: string
channelID: string;
channelTitle: string;
}
interface AdMessage {
type: "ad",
playing: boolean
type: "ad";
playing: boolean;
}
interface VideoData {
type: "data",
videoID: string,
isLive: boolean,
isPremiere: boolean
type: "data";
videoID: string;
isLive: boolean;
isPremiere: boolean;
}
type WindowMessage = StartMessage | FinishMessage | AdMessage | VideoData;

2
src/globals.d.ts vendored
View File

@@ -1,6 +1,6 @@
import { SBObject } from "./config";
declare global {
interface Window { SB: SBObject; }
interface Window { SB: SBObject }
// Remove this once the API becomes stable and types are shipped in @types/chrome
namespace chrome {
namespace declarativeContent {

View File

@@ -46,6 +46,8 @@ class PreviewBar {
segments: PreviewBarSegment[] = [];
existingChapters: PreviewBarSegment[] = [];
videoDuration = 0;
updateExistingChapters: () => void;
lastChapterUpdate = 0;
// For chapter bar
hoveredSection: HTMLElement;
@@ -58,7 +60,7 @@ class PreviewBar {
unfilteredChapterGroups: ChapterGroup[];
chapterGroups: ChapterGroup[];
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, test=false) {
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, updateExistingChapters: () => void, test=false) {
if (test) return;
this.container = document.createElement('ul');
this.container.id = 'previewbar';
@@ -67,6 +69,7 @@ class PreviewBar {
this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious;
this.chapterVote = chapterVote;
this.updateExistingChapters = updateExistingChapters;
this.updatePageElements();
this.createElement(parent);
@@ -214,6 +217,16 @@ class PreviewBar {
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
this.originalChapterBar?.style?.removeProperty("display");
this.chapterVote?.setVisibility(false);
document.querySelectorAll(`.sponsorBlockChapterBar`).forEach((e) => {
if (e !== this.customChaptersBar) {
e.remove();
}
});
}
set(segments: PreviewBarSegment[], videoDuration: number): void {
@@ -637,11 +650,17 @@ class PreviewBar {
cursor += sectionWidthDecimal;
}
if (sections.length !== 0 && sections.length !== this.existingChapters?.length
&& Date.now() - this.lastChapterUpdate > 3000) {
this.lastChapterUpdate = Date.now();
this.updateExistingChapters();
}
}
}
private findLeftAndScale(selector: string, currentElement: HTMLElement, progressBar: HTMLElement):
{ left: number, scale: number } {
{ left: number; scale: number } {
const sections = currentElement.parentElement.parentElement.parentElement.children;
let currentWidth = 0;
let lastWidth = 0;
@@ -657,7 +676,7 @@ class PreviewBar {
for (let i = 0; i < sections.length; i++) {
const section = sections[i] as HTMLElement;
const checkElement = section.querySelector(selector) as HTMLElement;
const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") || progressBar.clientWidth;
const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") ?? progressBar.clientWidth;
const currentSectionWidth = currentSectionWidthNoMargin
+ this.getPartialChapterSectionStyle(section, "marginRight");
@@ -709,23 +728,23 @@ class PreviewBar {
private getPartialChapterSectionStyle(element: HTMLElement, param: string): number {
const data = element.style[param];
if (data?.includes("100%")) {
return 0;
if (data?.includes("%")) {
return this.customChaptersBar.clientWidth * (parseFloat(data.replace("%", "")) / 100);
} else {
return parseInt(element.style[param].match(/\d+/g)?.[0]) || 0;
}
}
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): void {
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): SponsorTime[] {
if (!Config.config.showSegmentNameInChapterBar
|| ((!segments || segments.length <= 0) && submittingSegments?.length <= 0)) {
const chaptersContainer = this.getChaptersContainer();
const chapterButton = this.getChapterButton(chaptersContainer);
if (chapterButton.classList.contains("ytp-chapter-container-disabled")) {
if (chapterButton && chapterButton.classList.contains("ytp-chapter-container-disabled")) {
chaptersContainer.style.display = "none";
}
return;
return [];
}
segments ??= [];
@@ -737,6 +756,7 @@ class PreviewBar {
});
this.setActiveSegments(activeSegments);
return activeSegments;
}
/**
@@ -765,9 +785,14 @@ class PreviewBar {
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
if (chosenSegment.actionType !== ActionType.Chapter) {
chapterTitle.classList.add("sponsorBlock-segment-title");
} else {
chapterTitle.classList.remove("sponsorBlock-segment-title");
}
const chapterVoteContainer = this.chapterVote.getContainer();
if (chosenSegment.source === SponsorSourceType.Server) {
const chapterVoteContainer = this.chapterVote.getContainer();
if (!chapterButton.contains(chapterVoteContainer)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) {
@@ -795,7 +820,7 @@ class PreviewBar {
private getChapterButton(chaptersContainer: HTMLElement): HTMLButtonElement {
return (chaptersContainer ?? this.getChaptersContainer())
.querySelector("button.ytp-chapter-title") as HTMLButtonElement;
?.querySelector("button.ytp-chapter-title") as HTMLButtonElement;
}
remove(): void {

View File

@@ -102,6 +102,7 @@ export class SkipButtonControlBar {
this.enabled = true;
this.refreshText();
this.container?.classList?.remove("textDisabled");
this.textContainer?.classList?.remove("hidden");
AnimationUtils.disableAutoHideAnimation(this.skipIcon);
@@ -134,7 +135,6 @@ export class SkipButtonControlBar {
disable(): void {
this.container.classList.add("hidden");
this.textContainer?.classList?.remove("hidden");
this.chapterText?.classList?.remove("hidden");
this.getChapterPrefix()?.classList?.remove("hidden");
@@ -142,6 +142,10 @@ export class SkipButtonControlBar {
this.enabled = false;
}
isEnabled(): boolean {
return this.enabled;
}
toggleSkip(): void {
this.skip(this.segment);
this.disableText();

View File

@@ -9,7 +9,7 @@ interface BaseMessage {
}
interface DefaultMessage {
message:
message:
"update"
| "sponsorStart"
| "getVideoID"
@@ -83,19 +83,19 @@ interface GetVideoIdResponse {
videoID: string;
}
interface GetChannelIDResponse {
export interface GetChannelIDResponse {
channelID: string;
}
interface SponsorStartResponse {
export interface SponsorStartResponse {
creatingSegment: boolean;
}
interface IsChannelWhitelistedResponse {
export interface IsChannelWhitelistedResponse {
value: boolean;
}
export type MessageResponse =
export type MessageResponse =
IsInfoFoundMessageResponse
| GetVideoIdResponse
| GetChannelIDResponse
@@ -111,7 +111,7 @@ export interface VoteResponse {
responseText: string;
}
export interface ImportSegmentsResponse {
interface ImportSegmentsResponse {
importedSegments: SponsorTime[];
}
@@ -120,4 +120,14 @@ export interface TimeUpdateMessage {
time: number;
}
export type PopupMessage = TimeUpdateMessage;
export type InfoUpdatedMessage = IsInfoFoundMessageResponse & {
message: "infoUpdated";
}
export interface VideoChangedPopupMessage {
message: "videoChanged";
videoID: string;
whitelisted: boolean;
}
export type PopupMessage = TimeUpdateMessage | InfoUpdatedMessage | VideoChangedPopupMessage;

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot } from 'react-dom/client';
import Config from "./config";
import * as CompileConfig from "../config.json";
@@ -258,7 +258,8 @@ async function init() {
break;
}
case "keybind-change": {
ReactDOM.render(React.createElement(KeybindComponent, {option: option}), optionsElements[i].querySelector("div"));
const root = createRoot(optionsElements[i].querySelector("div"));
root.render(React.createElement(KeybindComponent, {option: option}));
break;
}
case "display": {
@@ -531,7 +532,7 @@ function activatePrivateTextChange(element: HTMLElement) {
case "userID":
if (Config.config[option]) {
utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config[option],
publicUserID: utils.getHash(Config.config[option]),
values: ["warnings", "banned"]
}).then((result) => {
const userInfo = JSON.parse(result.responseText);
@@ -671,4 +672,4 @@ function copyDebugOutputToClipboard() {
function isIncognitoAllowed(): Promise<boolean> {
return new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
}
}

View File

@@ -1,8 +1,24 @@
import Config from "./config";
import Utils from "./utils";
import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject } from "./types";
import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
import {
ActionType,
SegmentUUID,
SponsorHideType,
SponsorSourceType,
SponsorTime,
StorageChangesObject,
} from "./types";
import {
GetChannelIDResponse,
IsChannelWhitelistedResponse,
IsInfoFoundMessageResponse,
Message,
MessageResponse,
PopupMessage,
SponsorStartResponse,
VoteResponse,
} from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
@@ -11,6 +27,7 @@ import { localizeHtmlPage } from "./utils/pageUtils";
import { exportTimes } from "./utils/exporter";
import GenericNotice from "./render/GenericNotice";
import { noRefreshFetchingChaptersAllowed } from "./utils/licenseKey";
const utils = new Utils();
interface MessageListener {
@@ -50,9 +67,11 @@ class MessageHandler {
// To prevent clickjacking
let allowPopup = window === window.top;
window.addEventListener("message", async (e) => {
window.addEventListener("message", async (e): Promise<void> => {
if (e.source !== window.parent) return;
if (e.origin.endsWith('.youtube.com')) return allowPopup = true;
if (e.origin.endsWith('.youtube.com')) {
allowPopup = true;
}
});
//make this a function to allow this to run on the content page
@@ -61,14 +80,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
localizeHtmlPage();
type InputPageElements = {
whitelistToggle?: HTMLInputElement,
toggleSwitch?: HTMLInputElement,
usernameInput?: HTMLInputElement,
whitelistToggle?: HTMLInputElement;
toggleSwitch?: HTMLInputElement;
usernameInput?: HTMLInputElement;
};
type PageElements = { [key: string]: HTMLElement } & InputPageElements
/** If true, the content script is in the process of creating a new segment. */
let creatingSegment = false;
let stopLoadingAnimation = null;
//the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = [];
@@ -188,7 +206,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
PageElements.exportSegmentsButton.addEventListener("click", exportSegments);
PageElements.importSegmentsButton.addEventListener("click",
PageElements.importSegmentsButton.addEventListener("click",
() => PageElements.importSegmentsMenu.classList.toggle("hidden"));
PageElements.importSegmentsSubmit.addEventListener("click", importSegments);
@@ -260,12 +278,12 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (dontShowNotice != undefined && dontShowNotice) {
PageElements.showNoticeAgain.style.display = "unset";
}
const values = ["userName", "viewCount", "minutesSaved", "vip", "permissions"];
if (!Config.config.payments.freeAccess && !noRefreshFetchingChaptersAllowed()) values.push("freeChaptersAccess");
utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config.userID,
publicUserID: await utils.getHash(Config.config.userID),
values
}).then((res) => {
if (res.status === 200) {
@@ -376,7 +394,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
messageHandler.sendMessage(tabs[0].id, { message: 'getVideoID' }, function (result) {
if (result !== undefined && result.videoID) {
currentVideoID = result.videoID;
creatingSegment = result.creatingSegment;
loadTabData(tabs, updating);
} else if (result === undefined && chrome.runtime.lastError) {
@@ -411,7 +428,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}, (tabs) => onTabs(tabs, updating));
}
function infoFound(request: IsInfoFoundMessageResponse) {
async function infoFound(request: IsInfoFoundMessageResponse) {
// End any loading animation
if (stopLoadingAnimation != null) {
stopLoadingAnimation();
stopLoadingAnimation = null;
}
if (chrome.runtime.lastError) {
//This page doesn't have the injected content script, or at least not yet
displayNoVideo();
@@ -427,79 +450,59 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.loadingIndicator.style.display = "none";
downloadedTimes = request.sponsorTimes ?? [];
displayDownloadedSponsorTimes(downloadedTimes, request.time);
if (request.found) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
PageElements.issueReporterImportExport.classList.remove("hidden");
if (request.sponsorTimes) {
displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
}
} else if (request.status == 404 || request.status == 200) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
PageElements.issueReporterImportExport.classList.remove("hidden");
} else {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
PageElements.issueReporterImportExport.classList.add("hidden");
if (request.status) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
} else {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("segmentsStillLoading");
}
PageElements.issueReporterImportExport.classList.remove("hidden");
}
}
//see if whitelist button should be swapped
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'isChannelWhitelisted' },
function (response) {
if (response.value) {
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
PageElements.whitelistToggle.checked = true;
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
}
});
const response = await sendTabMessageAsync({ message: 'isChannelWhitelisted' }) as IsChannelWhitelistedResponse;
if (response.value) {
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
PageElements.whitelistToggle.checked = true;
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
}
);
}
function sendSponsorStartMessage() {
async function sendSponsorStartMessage() {
//the content script will get the message if a YouTube page is open
messageHandler.query({
active: true,
currentWindow: true,
}, (tabs) => {
messageHandler.sendMessage(
tabs[0].id,
{ from: 'popup', message: 'sponsorStart' },
async (response) => {
startSponsorCallback(response);
const response = await sendTabMessageAsync({ from: 'popup', message: 'sponsorStart' }) as SponsorStartResponse;
startSponsorCallback(response);
// 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.configSyncListeners.indexOf(listener);
if (index !== -1) Config.configSyncListeners.splice(index, 1);
};
// 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.configSyncListeners.indexOf(listener);
if (index !== -1) Config.configSyncListeners.splice(index, 1);
};
const lateUpdate = () => {
startSponsorCallback(response);
removeListener(lateUpdate);
};
const lateUpdate = () => {
startSponsorCallback(response);
removeListener(lateUpdate);
};
Config.configSyncListeners.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);
},
);
});
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
setTimeout(() => removeListener(lateUpdate), 200);
}
function startSponsorCallback(response: { creatingSegment: boolean }) {
creatingSegment = response.creatingSegment;
function startSponsorCallback(response: SponsorStartResponse) {
// Only update the segments after a segment was created
if (!creatingSegment) {
if (!response.creatingSegment) {
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] || [];
}
@@ -514,7 +517,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.issueReporterTabs.classList.add("hidden");
currentSegmentTab = SegmentTab.Segments;
} else {
if (currentSegmentTab === SegmentTab.Segments
if (currentSegmentTab === SegmentTab.Segments
&& sponsorTimes.every((segment) => segment.actionType === ActionType.Chapter)) {
PageElements.issueReporterTabs.classList.add("hidden");
currentSegmentTab = SegmentTab.Chapters;
@@ -529,7 +532,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (currentSegmentTab === SegmentTab.Segments) {
return segment.actionType !== ActionType.Chapter;
} else if (currentSegmentTab === SegmentTab.Chapters) {
return segment.actionType === ActionType.Chapter
return segment.actionType === ActionType.Chapter
&& segment.source !== SponsorSourceType.YouTube;
} else {
return true;
@@ -546,7 +549,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (downloadedTimes.length > 0) {
PageElements.exportSegmentsButton.classList.remove("hidden");
} else {
} else {
PageElements.exportSegmentsButton.classList.add("hidden");
}
@@ -590,7 +593,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (downloadedTimes[i].actionType === ActionType.Full) {
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
} else {
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
(actionType !== ActionType.Poi
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(downloadedTimes[i].segment[1], true)
: "");
@@ -613,6 +616,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
const votingButtons = document.createElement("details");
votingButtons.classList.add("votingButtons");
votingButtons.id = "votingButtons" + UUID;
votingButtons.setAttribute("data-uuid", UUID);
votingButtons.addEventListener("toggle", () => {
if (votingButtons.open) {
openedUUIDs.push(UUID);
@@ -676,26 +680,18 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
downloadedTimes[i].hidden = SponsorHideType.Hidden;
}
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{
message: "hideSegment",
type: downloadedTimes[i].hidden,
UUID: UUID
}
);
});
sendTabMessage({
message: "hideSegment",
type: downloadedTimes[i].hidden,
UUID: UUID
})
});
const skipButton = document.createElement("img");
skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
skipButton.className = "voteButton";
skipButton.src = chrome.runtime.getURL("icons/skip.svg");
skipButton.title = actionType === ActionType.Chapter ? chrome.i18n.getMessage("playChapter")
skipButton.title = actionType === ActionType.Chapter ? chrome.i18n.getMessage("playChapter")
: chrome.i18n.getMessage("skipSegment");
skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
@@ -705,11 +701,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
voteButtonsContainer.appendChild(downvoteButton);
voteButtonsContainer.appendChild(uuidButton);
if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
|| downloadedTimes[i].actionType === ActionType.Poi
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
voteButtonsContainer.appendChild(hideButton);
}
voteButtonsContainer.appendChild(skipButton);
if (downloadedTimes[i].actionType !== ActionType.Full) {
voteButtonsContainer.appendChild(skipButton);
}
// Will contain request status
const voteStatusContainer = document.createElement("div");
@@ -732,15 +730,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
function submitTimes() {
if (sponsorTimes.length > 0) {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'submitTimes' },
);
});
sendTabMessage({ message: 'submitTimes' })
}
}
@@ -750,9 +740,16 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.showNoticeAgain.style.display = "none";
}
function isCreatingSegment(): boolean {
const segments = Config.config.unsubmittedSegments[currentVideoID];
if (!segments) return false;
const lastSegment = segments[segments.length - 1];
return lastSegment && lastSegment?.segment?.length !== 2;
}
/** Updates any UI related to segment editing and submission according to the current state. */
function updateSegmentEditingUI() {
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(creatingSegment ? "sponsorEnd" : "sponsorStart");
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(isCreatingSegment() ? "sponsorEnd" : "sponsorStart");
PageElements.submitTimes.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
PageElements.submissionHint.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
@@ -771,20 +768,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
chrome.runtime.sendMessage({ "message": "openHelp" });
}
function sendTabMessage(data: Message): Promise<unknown> {
return new Promise((resolve) => {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
data,
(response) => resolve(response)
);
}
function sendTabMessage(data: Message, callback?) {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
data,
callback
);
});
}
);
}
function sendTabMessageAsync(data: Message): Promise<unknown> {
return new Promise((resolve) => sendTabMessage(data, (response) => resolve(response)))
}
//make the options username setting option visible
@@ -861,186 +860,122 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
thanksForVotingText.removeAttribute("innerText");
}
function vote(type, UUID) {
async function vote(type, UUID) {
//add loading info
addVoteMessage(chrome.i18n.getMessage("Loading"), UUID);
const response = await sendTabMessageAsync({
message: "submitVote",
type: type,
UUID: UUID
}) as VoteResponse;
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{
message: "submitVote",
type: type,
UUID: UUID
}, function (response) {
if (response != undefined) {
//see if it was a success or failure
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
//success (treat rate limits as a success)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
} else if (response.successType == -1) {
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
setTimeout(() => removeVoteMessage(UUID), 1500);
}
}
);
if (response != undefined) {
//see if it was a success or failure
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
//success (treat rate limits as a success)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
} else if (response.successType == -1) {
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
setTimeout(() => removeVoteMessage(UUID), 1500);
}
}
async function whitelistChannel() {
//get the channel url
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
if (!response.channelID) {
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
return;
}
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//add on this channel
whitelistedChannels.push(response.channelID);
//change button
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
//show 'consider force channel check' alert
if (!Config.config.forceChannelCheck) PageElements.whitelistForceCheck.classList.remove("hidden");
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//send a message to the client
sendTabMessage({
message: 'whitelistChange',
value: true
});
}
function whitelistChannel() {
async function unwhitelistChannel() {
//get the channel url
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'getChannelID' },
function (response) {
if (!response.channelID) {
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
return;
}
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//add on this channel
whitelistedChannels.push(response.channelID);
//remove this channel
const index = whitelistedChannels.indexOf(response.channelID);
whitelistedChannels.splice(index, 1);
//change button
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
//change button
PageElements.whitelistChannel.style.display = "unset";
PageElements.unwhitelistChannel.style.display = "none";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.remove("rotated");
//show 'consider force channel check' alert
if (!Config.config.forceChannelCheck) PageElements.whitelistForceCheck.classList.remove("hidden");
//hide 'consider force channel check' alert
PageElements.whitelistForceCheck.classList.add("hidden");
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//send a message to the client
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id, {
message: 'whitelistChange',
value: true
});
}
);
}
);
//send a message to the client
sendTabMessage({
message: 'whitelistChange',
value: false
});
}
function unwhitelistChannel() {
//get the channel url
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'getChannelID' },
function (response) {
//get whitelisted channels
let whitelistedChannels = Config.config.whitelistedChannels;
if (whitelistedChannels == undefined) {
whitelistedChannels = [];
}
//remove this channel
const index = whitelistedChannels.indexOf(response.channelID);
whitelistedChannels.splice(index, 1);
//change button
PageElements.whitelistChannel.style.display = "unset";
PageElements.unwhitelistChannel.style.display = "none";
document.querySelectorAll('.SBWhitelistIcon')[0].classList.remove("rotated");
//hide 'consider force channel check' alert
PageElements.whitelistForceCheck.classList.add("hidden");
//save this
Config.config.whitelistedChannels = whitelistedChannels;
//send a message to the client
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id, {
message: 'whitelistChange',
value: false
});
}
);
}
);
});
function startLoadingAnimation() {
stopLoadingAnimation = AnimationUtils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
}
function refreshSegments() {
const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
{ message: 'refreshSegments' },
(response) => {
infoFound(response);
stopAnimation();
}
)
}
);
startLoadingAnimation();
sendTabMessage({ message: 'refreshSegments' });
}
function skipSegment(actionType: ActionType, UUID: SegmentUUID, element?: HTMLElement): void {
if (actionType === ActionType.Chapter) {
sendMessage({
sendTabMessage({
message: "unskip",
UUID: UUID
});
} else {
sendMessage({
sendTabMessage({
message: "reskip",
UUID: UUID
});
}
if (element) {
const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3);
stopAnimation();
}
}
function sendMessage(request: Message): void {
messageHandler.query({
active: true,
currentWindow: true
}, tabs => {
messageHandler.sendMessage(
tabs[0].id,
request
);
});
}
/**
* Should skipping be disabled (visuals stay)
*/
@@ -1073,10 +1008,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
async function importSegments() {
const text = (PageElements.importSegmentsText as HTMLInputElement).value;
await sendTabMessage({
sendTabMessage({
message: "importSegments",
data: text
}) as ImportSegmentsResponse;
});
PageElements.importSegmentsMenu.classList.add("hidden");
}
@@ -1136,10 +1071,58 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
port.onMessage.addListener((msg) => onMessage(msg));
}
function updateCurrentTime(currentTime: number) {
// Create a map of segment UUID -> segment object for easy access
const segmentMap: Record<string, SponsorTime> = {};
for (const segment of downloadedTimes)
segmentMap[segment.UUID] = segment
// Iterate over segment elements and update their classes
const segmentList = document.getElementById("issueReporterTimeButtons");
for (const segmentElement of segmentList.children) {
const UUID = segmentElement.getAttribute("data-uuid");
if (UUID == null || segmentMap[UUID] == undefined) continue;
const summaryElement = segmentElement.querySelector("summary")
if (summaryElement == null) continue;
const segment = segmentMap[UUID]
summaryElement.classList.remove("segmentActive", "segmentPassed")
if (currentTime >= segment.segment[0]) {
if (currentTime < segment.segment[1]) {
summaryElement.classList.add("segmentActive");
} else {
summaryElement.classList.add("segmentPassed");
}
}
}
}
function onMessage(msg: PopupMessage) {
switch (msg.message) {
case "time":
displayDownloadedSponsorTimes(downloadedTimes, msg.time);
updateCurrentTime(msg.time);
break;
case "infoUpdated":
infoFound(msg);
break;
case "videoChanged":
currentVideoID = msg.videoID
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
updateSegmentEditingUI();
if (msg.whitelisted) {
PageElements.whitelistChannel.style.display = "none";
PageElements.unwhitelistChannel.style.display = "unset";
PageElements.whitelistToggle.checked = true;
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
}
// Clear segments list & start loading animation
// We'll get a ping once they're loaded
startLoadingAnimation();
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("Loading");
displayDownloadedSponsorTimes([], 0);
break;
}
}

View File

@@ -1,5 +1,6 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot } from 'react-dom/client';
import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
class CategoryChooser {
@@ -9,9 +10,9 @@ class CategoryChooser {
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<CategoryChooserComponent ref={this.ref} />,
element
const root = createRoot(element);
root.render(
<CategoryChooserComponent ref={this.ref} />
);
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from "react-dom/client";
import CategoryPillComponent, { CategoryPillState } from "../components/CategoryPillComponent";
import Config from "../config";
import { VoteResponse } from "../messageTypes";
@@ -10,6 +10,7 @@ import { Tooltip } from "./Tooltip";
export class CategoryPill {
container: HTMLElement;
ref: React.RefObject<CategoryPillComponent>;
root: Root;
unsavedState: CategoryPillState;
@@ -38,14 +39,14 @@ export class CategoryPill {
this.unsavedState = this.ref.current.state;
}
ReactDOM.render(
<CategoryPillComponent ref={this.ref} vote={vote} />,
this.container
);
this.root = createRoot(this.container);
this.root.render(<CategoryPillComponent ref={this.ref} vote={vote} />);
if (this.unsavedState) {
this.ref.current?.setState(this.unsavedState);
this.unsavedState = null;
GenericUtils.wait(() => this.ref.current).then(() => {
this.ref.current?.setState(this.unsavedState);
this.unsavedState = null;
});
}
if (onMobileYouTube) {
@@ -64,7 +65,7 @@ export class CategoryPill {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent";
import { VoteResponse } from "../messageTypes";
import { Category, SegmentUUID, SponsorTime } from "../types";
@@ -7,6 +7,7 @@ import { Category, SegmentUUID, SponsorTime } from "../types";
export class ChapterVote {
container: HTMLElement;
ref: React.RefObject<ChapterVoteComponent>;
root: Root;
unsavedState: ChapterVoteState;
@@ -19,10 +20,8 @@ export class ChapterVote {
this.container.id = "chapterVote";
this.container.style.height = "100%";
ReactDOM.render(
<ChapterVoteComponent ref={this.ref} vote={vote} />,
this.container
);
this.root = createRoot(this.container);
this.root.render(<ChapterVoteComponent ref={this.ref} vote={vote} />);
}
getContainer(): HTMLElement {
@@ -30,7 +29,7 @@ export class ChapterVote {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import NoticeComponent from "../components/NoticeComponent";
import Utils from "../utils";
@@ -9,17 +9,17 @@ import { ButtonListener, ContentContainer } from "../types";
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
export interface TextBox {
icon: string,
text: string
icon: string;
text: string;
}
export interface NoticeOptions {
title: string,
referenceNode?: HTMLElement,
textBoxes?: TextBox[],
buttons?: ButtonListener[],
fadeIn?: boolean,
timed?: boolean
title: string;
referenceNode?: HTMLElement;
textBoxes?: TextBox[];
buttons?: ButtonListener[];
fadeIn?: boolean;
timed?: boolean;
style?: React.CSSProperties;
extraClass?: string;
maxCountdownTime?: () => number;
@@ -35,6 +35,7 @@ export default class GenericNotice {
noticeElement: HTMLDivElement;
noticeRef: React.MutableRefObject<NoticeComponent>;
idSuffix: string;
root: Root;
constructor(contentContainer: ContentContainer, idSuffix: string, options: NoticeOptions) {
this.noticeRef = React.createRef();
@@ -49,11 +50,13 @@ export default class GenericNotice {
referenceNode.prepend(this.noticeElement);
this.update(options);
this.root = createRoot(this.noticeElement);
this.update(options);
}
update(options: NoticeOptions): void {
ReactDOM.render(
this.root.render(
<NoticeComponent
noticeTitle={options.title}
idSuffix={this.idSuffix}
@@ -92,8 +95,7 @@ export default class GenericNotice {
</>
: null}
</NoticeComponent>,
this.noticeElement
</NoticeComponent>
);
}
@@ -137,7 +139,7 @@ export default class GenericNotice {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.noticeElement);
this.root.unmount();
this.noticeElement.remove();
}

View File

@@ -1,26 +1,26 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
export interface RectangleTooltipProps {
text: string,
link?: string,
referenceNode: HTMLElement,
prependElement?: HTMLElement, // Element to append before
bottomOffset?: string,
leftOffset?: string,
timeout?: number,
htmlId?: string,
maxHeight?: string,
maxWidth?: string,
backgroundColor?: string,
fontSize?: string,
text: string;
link?: string;
referenceNode: HTMLElement;
prependElement?: HTMLElement; // Element to append before
bottomOffset?: string;
leftOffset?: string;
timeout?: number;
htmlId?: string;
maxHeight?: string;
maxWidth?: string;
backgroundColor?: string;
fontSize?: string;
buttonFunction?: () => void;
}
export class RectangleTooltip {
text: string;
container: HTMLDivElement;
root: Root;
timer: NodeJS.Timeout;
constructor(props: RectangleTooltipProps) {
@@ -47,7 +47,8 @@ export class RectangleTooltip {
this.timer = setTimeout(() => this.close(), props.timeout * 1000);
}
ReactDOM.render(
this.root = createRoot(this.container);
this.root.render(
<div style={{
bottom: props.bottomOffset,
left: props.leftOffset,
@@ -81,13 +82,12 @@ export class RectangleTooltip {
{chrome.i18n.getMessage("GotIt")}
</button>
</div>,
this.container
</div>
)
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
if (this.timer) clearTimeout(this.timer);

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import Utils from "../utils";
const utils = new Utils();
@@ -18,6 +18,7 @@ class SkipNotice {
noticeElement: HTMLDivElement;
skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
root: Root;
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, unskipTime: number = null, startReskip = false) {
this.skipNoticeRef = React.createRef();
@@ -41,7 +42,8 @@ class SkipNotice {
referenceNode.prepend(this.noticeElement);
ReactDOM.render(
this.root = createRoot(this.noticeElement);
this.root.render(
<SkipNoticeComponent segments={segments}
autoSkip={autoSkip}
startReskip={startReskip}
@@ -50,8 +52,7 @@ class SkipNotice {
closeListener={() => this.close()}
smaller={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAll
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.MiniForAutoSkip && autoSkip)}
unskipTime={unskipTime} />,
this.noticeElement
unskipTime={unskipTime} />
);
}
@@ -62,7 +63,7 @@ class SkipNotice {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.noticeElement);
this.root.unmount();
this.noticeElement.remove();

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import Utils from "../utils";
const utils = new Utils();
@@ -17,6 +17,8 @@ class SubmissionNotice {
noticeElement: HTMLDivElement;
root: Root;
constructor(contentContainer: ContentContainer, callback: () => unknown) {
this.noticeRef = React.createRef();
@@ -30,13 +32,13 @@ class SubmissionNotice {
referenceNode.prepend(this.noticeElement);
ReactDOM.render(
this.root = createRoot(this.noticeElement);
this.root.render(
<SubmissionNoticeComponent
contentContainer={contentContainer}
callback={callback}
ref={this.noticeRef}
closeListener={() => this.close(false)} />,
this.noticeElement
closeListener={() => this.close(false)} />
);
}
@@ -46,7 +48,7 @@ class SubmissionNotice {
close(callRef = true): void {
if (callRef) this.noticeRef.current.cancel();
ReactDOM.unmountComponentAtNode(this.noticeElement);
this.root.unmount();
this.noticeElement.remove();
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot, Root } from 'react-dom/client';
import { ButtonListener } from "../types";
export interface TooltipProps {
@@ -26,6 +26,7 @@ export class Tooltip {
container: HTMLDivElement;
timer: NodeJS.Timeout;
root: Root;
constructor(props: TooltipProps) {
props.bottomOffset ??= "70px";
@@ -54,8 +55,9 @@ export class Tooltip {
}
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
ReactDOM.render(
this.root = createRoot(this.container);
this.root.render(
<div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
<div>
@@ -93,8 +95,7 @@ export class Tooltip {
{chrome.i18n.getMessage("GotIt")}
</button>
: null}
</div>,
this.container
</div>
)
}
@@ -120,7 +121,7 @@ export class Tooltip {
}
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.root.unmount();
this.container.remove();
if (this.timer) clearTimeout(this.timer);

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createRoot } from 'react-dom/client';
import UnsubmittedVideosComponent from "../components/options/UnsubmittedVideosComponent";
class UnsubmittedVideos {
@@ -9,9 +9,9 @@ class UnsubmittedVideos {
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<UnsubmittedVideosComponent ref={this.ref} />,
element
const root = createRoot(element);
root.render(
<UnsubmittedVideosComponent ref={this.ref} />
);
}

View File

@@ -4,32 +4,32 @@ import SkipNotice from "./render/SkipNotice";
export interface ContentContainer {
(): {
vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void,
dontShowNoticeAgain: () => void,
unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean) => void,
sponsorTimes: SponsorTime[],
sponsorTimesSubmitting: SponsorTime[],
skipNotices: SkipNotice[],
v: HTMLVideoElement,
sponsorVideoID,
reskipSponsorTime: (segment: SponsorTime, forceSeek?: boolean) => void,
updatePreviewBar: () => void,
onMobileYouTube: boolean,
sponsorSubmissionNotice: SubmissionNotice,
resetSponsorSubmissionNotice: (callRef?: boolean) => void,
updateEditButtonsOnPlayer: () => void,
previewTime: (time: number, unpause?: boolean) => void,
videoInfo: VideoInfo,
getRealCurrentTime: () => number,
lockedCategories: string[],
channelIDInfo: ChannelIDInfo
}
vote: (type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) => void;
dontShowNoticeAgain: () => void;
unskipSponsorTime: (segment: SponsorTime, unskipTime: number, forceSeek?: boolean) => void;
sponsorTimes: SponsorTime[];
sponsorTimesSubmitting: SponsorTime[];
skipNotices: SkipNotice[];
v: HTMLVideoElement;
sponsorVideoID;
reskipSponsorTime: (segment: SponsorTime, forceSeek?: boolean) => void;
updatePreviewBar: () => void;
onMobileYouTube: boolean;
sponsorSubmissionNotice: SubmissionNotice;
resetSponsorSubmissionNotice: (callRef?: boolean) => void;
updateEditButtonsOnPlayer: () => void;
previewTime: (time: number, unpause?: boolean) => void;
videoInfo: VideoInfo;
getRealCurrentTime: () => number;
lockedCategories: string[];
channelIDInfo: ChannelIDInfo;
};
}
export interface FetchResponse {
responseText: string,
status: number,
ok: boolean
responseText: string;
status: number;
ok: boolean;
}
export type HashedValue = string & { __hashBrand: unknown };
@@ -46,7 +46,7 @@ export enum CategorySkipOption {
export interface CategorySelection {
name: Category;
option: CategorySkipOption
option: CategorySkipOption;
}
export enum SponsorHideType {
@@ -97,95 +97,95 @@ export interface ScheduledTime extends SponsorTime {
}
export interface PreviewBarOption {
color: string,
opacity: string
color: string;
opacity: string;
}
export interface Registration {
message: string,
id: string,
allFrames: boolean,
js: browser.extensionTypes.ExtensionFileOrCode[],
css: browser.extensionTypes.ExtensionFileOrCode[],
matches: string[]
message: string;
id: string;
allFrames: boolean;
js: browser.extensionTypes.ExtensionFileOrCode[];
css: browser.extensionTypes.ExtensionFileOrCode[];
matches: string[];
}
export interface BackgroundScriptContainer {
registerFirefoxContentScript: (opts: Registration) => void,
unregisterFirefoxContentScript: (id: string) => void
registerFirefoxContentScript: (opts: Registration) => void;
unregisterFirefoxContentScript: (id: string) => void;
}
export interface VideoInfo {
responseContext: {
serviceTrackingParams: Array<{service: string, params: Array<{key: string, value: string}>}>,
serviceTrackingParams: Array<{service: string; params: Array<{key: string; value: string}>}>;
webResponseContextExtensionData: {
hasDecorated: boolean
}
},
hasDecorated: boolean;
};
};
playabilityStatus: {
status: string,
playableInEmbed: boolean,
status: string;
playableInEmbed: boolean;
miniplayer: {
miniplayerRenderer: {
playbackMode: string
}
}
playbackMode: string;
};
};
};
streamingData: unknown;
playbackTracking: unknown;
videoDetails: {
videoId: string,
title: string,
lengthSeconds: string,
keywords: string[],
channelId: string,
isOwnerViewing: boolean,
shortDescription: string,
isCrawlable: boolean,
videoId: string;
title: string;
lengthSeconds: string;
keywords: string[];
channelId: string;
isOwnerViewing: boolean;
shortDescription: string;
isCrawlable: boolean;
thumbnail: {
thumbnails: Array<{url: string, width: number, height: number}>
},
averageRating: number,
allowRatings: boolean,
viewCount: string,
author: string,
isPrivate: boolean,
isUnpluggedCorpus: boolean,
isLiveContent: boolean,
thumbnails: Array<{url: string; width: number; height: number}>;
};
averageRating: number;
allowRatings: boolean;
viewCount: string;
author: string;
isPrivate: boolean;
isUnpluggedCorpus: boolean;
isLiveContent: boolean;
};
playerConfig: unknown;
storyboards: unknown;
microformat: {
playerMicroformatRenderer: {
thumbnail: {
thumbnails: Array<{url: string, width: number, height: number}>
},
thumbnails: Array<{url: string; width: number; height: number}>;
};
embed: {
iframeUrl: string,
flashUrl: string,
width: number,
height: number,
flashSecureUrl: string,
},
iframeUrl: string;
flashUrl: string;
width: number;
height: number;
flashSecureUrl: string;
};
title: {
simpleText: string,
},
simpleText: string;
};
description: {
simpleText: string,
},
lengthSeconds: string,
ownerProfileUrl: string,
externalChannelId: string,
availableCountries: string[],
isUnlisted: boolean,
hasYpcMetadata: boolean,
viewCount: string,
category: Category,
publishDate: string,
ownerChannelName: string,
uploadDate: string,
}
simpleText: string;
};
lengthSeconds: string;
ownerProfileUrl: string;
externalChannelId: string;
availableCountries: string[];
isUnlisted: boolean;
hasYpcMetadata: boolean;
viewCount: string;
category: Category;
publishDate: string;
ownerChannelName: string;
uploadDate: string;
};
};
trackingParams: string;
attestation: unknown;
@@ -205,17 +205,17 @@ export enum ChannelIDStatus {
}
export interface ChannelIDInfo {
id: string,
status: ChannelIDStatus
id: string;
status: ChannelIDStatus;
}
export interface SkipToTimeParams {
v: HTMLVideoElement,
skipTime: number[],
skippingSegments: SponsorTime[],
openNotice: boolean,
forceAutoSkip?: boolean,
unskipTime?: number
v: HTMLVideoElement;
skipTime: number[];
skippingSegments: SponsorTime[];
openNotice: boolean;
forceAutoSkip?: boolean;
unskipTime?: number;
}
export interface ToggleSkippable {
@@ -232,11 +232,11 @@ export enum NoticeVisbilityMode {
}
export type Keybind = {
key: string,
code?: string,
ctrl?: boolean,
alt?: boolean,
shift?: boolean
key: string;
code?: string;
ctrl?: boolean;
alt?: boolean;
shift?: boolean;
}
export enum PageType {
@@ -249,6 +249,6 @@ export enum PageType {
}
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
name: string;
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}

View File

@@ -3,11 +3,15 @@ import { checkLicenseKey } from "./utils/licenseKey";
import { localizeHtmlPage } from "./utils/pageUtils";
import * as countries from "../public/res/countries.json";
import Utils from "./utils";
import { Category, CategorySkipOption } from "./types";
// This is needed, if Config is not imported before Utils, things break.
// Probably due to cyclic dependencies
Config.config;
const utils = new Utils();
window.addEventListener('DOMContentLoaded', init);
async function init() {
@@ -31,6 +35,13 @@ async function init() {
Config.config.payments.licenseKey = licenseKey;
Config.forceSyncUpdate("payments");
if (!utils.getCategorySelection("chapter")) {
Config.config.categorySelections.push({
name: "chapter" as Category,
option: CategorySkipOption.ShowOverlay
});
}
alert(chrome.i18n.getMessage("redeemSuccess"));
} else {
alert(chrome.i18n.getMessage("redeemFailed"));

View File

@@ -24,7 +24,7 @@ export default class Utils {
/* Used for waitForElement */
creatingWaitingMutationObserver = false;
waitingMutationObserver: MutationObserver = null;
waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = [];
waitingElements: { selector: string; visibleCheck: boolean; callback: (element: Element) => void }[] = [];
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
this.backgroundScriptContainer = backgroundScriptContainer;
@@ -319,6 +319,7 @@ export default class Utils {
return selection;
}
}
return { name: "None", option: 0} as CategorySelection;
}
/**

View File

@@ -25,7 +25,7 @@ function applyLoadingAnimation(element: HTMLElement, time: number, callback?: ()
});
}
function setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void, show: () => void } {
function setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void; show: () => void } {
if (enabled) element.classList.add("autoHiding");
element.classList.add("hidden");
element.classList.add("animationDone");

View File

@@ -148,5 +148,13 @@ export function getGuidelineInfo(category: Category): TextBox[] {
icon: "icons/check-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}];
default:
return [{
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
}
}

View File

@@ -8,7 +8,7 @@ const inTest = typeof chrome === "undefined";
const chapterNames = CompileConfig.categoryList.filter((code) => code !== "chapter")
.map((code) => ({
code,
name: !inTest ? chrome.i18n.getMessage("category_" + code) : code
names: !inTest ? [chrome.i18n.getMessage("category_" + code), shortCategoryName(code)] : [code]
}));
export function exportTimes(segments: SponsorTime[]): string {
@@ -38,7 +38,7 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
const match = line.match(/(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
if (match) {
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
if (startTime) {
if (startTime !== null) {
const specialCharsMatcher = /^(?:\s+seconds?)?[-:()\s]*|(?:\s+at)?[-:()\s]+$/g
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
let titleRight = null;
@@ -47,7 +47,7 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
if (title) {
const determinedCategory = chapterNames.find(c => c.name === title)?.code as Category;
const determinedCategory = chapterNames.find(c => c.names.includes(title))?.code as Category;
const segment: SponsorTime = {
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],

View File

@@ -93,7 +93,7 @@ function getLuminance(color: string): number {
}
/* From https://stackoverflow.com/a/5624139 */
function hexToRgb(hex: string): {r: number, g: number, b: number} {
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) {

View File

@@ -15,7 +15,7 @@ export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
Config.config.showChapterInfoMessage = false;
Config.config.payments.lastCheck = Date.now();
Config.forceSyncUpdate("payments");
return true;
}
} catch (e) { } //eslint-disable-line no-empty
@@ -43,7 +43,7 @@ export async function fetchingChaptersAllowed(): Promise<boolean> {
return licensePromise;
}
}
if (Config.config.payments.chaptersAllowed) return true;
if (Config.config.payments.lastCheck === 0 && Date.now() - Config.config.payments.lastFreeCheck > 2 * 24 * 60 * 60 * 1000) {
@@ -53,7 +53,7 @@ export async function fetchingChaptersAllowed(): Promise<boolean> {
// Check for free access if no license key, and it is the first time
const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
value: "freeChaptersAccess",
userID: Config.config.userID
publicUserID: await utils.getHash(Config.config.userID)
});
try {
@@ -66,7 +66,7 @@ export async function fetchingChaptersAllowed(): Promise<boolean> {
Config.config.payments.chaptersAllowed = true;
Config.config.showChapterInfoMessage = false;
Config.forceSyncUpdate("payments");
return true;
}
}
@@ -74,4 +74,4 @@ export async function fetchingChaptersAllowed(): Promise<boolean> {
}
return false;
}
}

View File

@@ -70,6 +70,7 @@ export function getExistingChapters(currentVideoID: VideoID, duration: number):
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
const chapters: SponsorTime[] = [];
// .ytp-timed-markers-container indicates that key-moments are present, which should not be divided
if (chaptersBox) {
let lastSegment: SponsorTime = null;
const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
@@ -78,6 +79,7 @@ export function getExistingChapters(currentVideoID: VideoID, duration: number):
const description = link.querySelector("#details h4") as HTMLElement;
if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) {
const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText);
if (time === null) return [];
if (lastSegment) {
lastSegment.segment[1] = time;
@@ -101,6 +103,11 @@ export function getExistingChapters(currentVideoID: VideoID, duration: number):
}
}
if (chapters[0] && chapters[0].segment[0] !== 0) {
// This is key moments instead of chapters, don't import as they are not full sections
return [];
}
return chapters;
}

View File

@@ -22,5 +22,7 @@ export function urlTimeToSeconds(time: string): number {
return hours * 3600 + minutes * 60 + seconds;
} else if (/\d+/.test(time)) {
return parseInt(time, 10);
} else {
return 0;
}
}

View File

@@ -6,14 +6,14 @@ import { GenericUtils } from "./genericUtils";
const utils = new Utils();
export interface ChatConfig {
displayName: string,
composerInitialValue?: string,
customDescription?: string
displayName: string;
composerInitialValue?: string;
customDescription?: string;
}
export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {
const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config.userID,
publicUserID: await utils.getHash(Config.config.userID),
values: ["warningReason"]
});
@@ -63,4 +63,4 @@ export async function openWarningDialog(contentContainer: ContentContainer): Pro
export function openChat(config: ChatConfig): void {
window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false));
}
}

View File

@@ -254,4 +254,25 @@ describe("Import segments", () => {
category: "chapter" as Category
}]);
});
it ("00:00", () => {
const input = ` 00:00 Cap 1
00:10 Cap 2
00:12 Cap 3`;
const result = importTimes(input, 8000);
expect(result).toMatchObject([{
segment: [0, 10],
description: "Cap 1",
category: "chapter" as Category
}, {
segment: [10, 12],
description: "Cap 2",
category: "chapter" as Category
}, {
segment: [12, 8000],
description: "Cap 3",
category: "chapter" as Category
}]);
});
});

View File

@@ -3,7 +3,7 @@ import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
describe("createChapterRenderGroups", () => {
let previewBar: PreviewBar;
beforeEach(() => {
previewBar = new PreviewBar(null, null, null, null, true);
previewBar = new PreviewBar(null, null, null, null, null, true);
})
it("Two unrelated times", () => {

View File

@@ -3,6 +3,8 @@
"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": false,
"outDir": "dist/js",
"noEmitOnError": false,

View File

@@ -3,6 +3,8 @@
"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"outDir": "dist/js",
"noEmitOnError": false,