Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into pr/PickleNik/1300

This commit is contained in:
Ajay 2022-06-02 21:48:05 -04:00
commit 20b95887af
52 changed files with 5340 additions and 3133 deletions

View file

@ -1,34 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true,
jasmine: true,
},
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["react", "@typescript-eslint"],
rules: {
// TODO: Remove warn rules when not needed anymore
"no-self-assign": "off",
"@typescript-eslint/no-empty-interface": "off",
"react/prop-types": [2, { ignore: ['children'] }]
},
settings: {
react: {
version: "detect",
},
},
};

33
.eslintrc.json Normal file
View file

@ -0,0 +1,33 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"no-self-assign": "off",
"@typescript-eslint/no-empty-interface": "off",
"react/prop-types": [2, { "ignore": ["children"] }]
},
"settings": {
"react": {
"version": "detect"
}
}
}

View file

@ -13,7 +13,7 @@
<a href="https://addons.mozilla.org/addon/sponsorblock/?src=external-github">Firefox</a> |
<a href="https://github.com/ajayyy/SponsorBlock/wiki/Android">Android</a> |
<a href="https://github.com/ajayyy/SponsorBlock/wiki/Edge">Edge</a> |
<a href="https://github.com/ajayyy/SponsorBlock/wiki/Safari">Safari for MacOS</a> |
<a href="https://github.com/ajayyy/SponsorBlock/wiki/Safari">Safari for MacOS and iOS</a> |
<a href="https://sponsor.ajay.app">Website</a> |
<a href="https://sponsor.ajay.app/stats">Stats</a>
</p>

View file

@ -5,4 +5,5 @@ module.exports = {
"transform": {
"^.+\\.ts$": "ts-jest"
},
"reporters": ["github-actions"]
};

View file

@ -1,7 +1,7 @@
{
"name": "__MSG_fullName__",
"short_name": "SponsorBlock",
"version": "4.3.1",
"version": "4.4.5",
"default_locale": "en",
"description": "__MSG_Description__",
"homepage_url": "https://sponsor.ajay.app",
@ -34,6 +34,7 @@
"icons/settings.svg",
"icons/pencil.svg",
"icons/check.svg",
"icons/check-smaller.svg",
"icons/upvote.png",
"icons/downvote.png",
"icons/thumbs_down.svg",
@ -50,6 +51,16 @@
"icons/heart.svg",
"icons/visible.svg",
"icons/not_visible.svg",
"icons/money.svg",
"icons/segway.png",
"icons/close-smaller.svg",
"icons/right-arrow.svg",
"icons/campaign.svg",
"icons/star.svg",
"icons/lightbulb.svg",
"icons/bolt.svg",
"icons/stopwatch.svg",
"icons/music-note.svg",
"icons/PlayerInfoIconSponsorBlocker.svg",
"icons/PlayerDeleteIconSponsorBlocker.svg",
"popup.html",
@ -64,7 +75,35 @@
],
"browser_action": {
"default_title": "SponsorBlock",
"default_popup": "popup.html"
"default_popup": "popup.html",
"default_icon": {
"16": "icons/IconSponsorBlocker16px.png",
"32": "icons/IconSponsorBlocker32px.png",
"64": "icons/LogoSponsorBlocker64px.png",
"128": "icons/LogoSponsorBlocker128px.png"
},
"theme_icons": [
{
"light": "icons/IconSponsorBlocker16px.png",
"dark": "icons/IconSponsorBlocker16px.png",
"size": 16
},
{
"light": "icons/IconSponsorBlocker32px.png",
"dark": "icons/IconSponsorBlocker32px.png",
"size": 32
},
{
"light": "icons/LogoSponsorBlocker64px.png",
"dark": "icons/LogoSponsorBlocker64px.png",
"size": 64
},
{
"light": "icons/LogoSponsorBlocker128px.png",
"dark": "icons/LogoSponsorBlocker128px.png",
"size": 128
}
]
},
"background": {
"scripts":[

6152
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,39 +3,40 @@
"version": "1.0.0",
"description": "",
"main": "background.js",
"type": "module",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/chrome": "^0.0.180",
"@types/chrome": "^0.0.188",
"@types/firefox-webext-browser": "^94.0.1",
"@types/jest": "^27.4.1",
"@types/jest": "^27.5.1",
"@types/react": "^17.0.43",
"@types/react-dom": "^17.0.14",
"@types/selenium-webdriver": "^4.0.18",
"@types/selenium-webdriver": "^4.1.0",
"@types/wicg-mediasession": "^1.1.3",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"chromedriver": "^100.0.0",
"concurrently": "^7.0.0",
"copy-webpack-plugin": "^10.2.4",
"eslint": "^8.12.0",
"eslint-plugin-react": "^7.29.4",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"jest": "^27.5.1",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
"chromedriver": "^101.0.0",
"concurrently": "^7.2.1",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.16.0",
"eslint-plugin-react": "^7.30.0",
"fork-ts-checker-webpack-plugin": "^7.2.11",
"jest": "^28.1.0",
"rimraf": "^3.0.2",
"schema-utils": "^4.0.0",
"selenium-webdriver": "^4.1.1",
"selenium-webdriver": "^4.1.2",
"speed-measure-webpack-plugin": "^1.5.0",
"ts-jest": "^27.1.4",
"ts-loader": "^9.2.8",
"ts-node": "^10.7.0",
"typescript": "4.6",
"ts-jest": "^28.0.3",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.0",
"typescript": "4.7",
"web-ext": "^6.8.0",
"webpack": "^5.64.4",
"webpack": "^5.72.1",
"webpack-cli": "^4.9.2",
"webpack-merge": "^4.2.2"
"webpack-merge": "^5.8.0"
},
"scripts": {
"web-run": "npm run web-run:chrome",
@ -65,7 +66,7 @@
"lint:fix": "eslint src --fix"
},
"engines": {
"node": ">=12.20.0"
"node": ">=16"
},
"funding": [
{

View file

@ -162,7 +162,10 @@
"message": "ব্যবহারকারীর নাম দিন"
},
"copyPublicID": {
"message": "পাবলিক ইউজারআইডি কপি করুন"
"message": "পাবলিক ইউজার আইডি কপি করুন"
},
"copySegmentID": {
"message": "অংশের আইডি কপি করুন"
},
"discordAdvert": {
"message": "পরামর্শ এবং প্রতিক্রিয়া জানাতে অফিসিয়াল ডিসকর্ড সার্ভারে যোগ দিন!"
@ -218,9 +221,15 @@
"trackDownvotesWarning": {
"message": "সতর্কীকরণ: এটি বন্ধ করলে পূর্বে সংরক্ষিত সব ডাউনভোট মুছে যাবে"
},
"enableQueryByHashPrefix": {
"message": "হ্যাশের প্রিফিক্স দিয়ে খুজুন "
},
"whatQueryByHashPrefix": {
"message": "সার্ভার থেকে videoID দিয়ে অংশ অনুরোধ করার পরিবর্তে videoID এর হ্যাশ এর প্রথম অক্ষর পাঠানো হয়। এই সার্ভার সমতুল্য হ্যাশ এর সকল ভিডিও এর তথ্য ফেরত পাঠাবে।"
},
"enableRefetchWhenNotFound": {
"message": "নতুন ভিডিওতে আবার অংশটি যোগার করুন"
},
"whatRefetchWhenNotFound": {
"message": "যদি ভিডিওটি নতুন হয়, এবং কোন অংশ পাওয়া না যায়, আপনার দেখার সময় কয়েক মিনিট পর পরই এটি তথ্য আনতে থাকবে।"
},
@ -230,6 +239,21 @@
"showSkipNotice": {
"message": "একটি অংশ এড়ানোর পরে নোটিস প্রদর্শন করুন"
},
"noticeVisibilityMode0": {
"message": "পূর্ণ আকারের স্কিপ নোটিস"
},
"noticeVisibilityMode1": {
"message": "স্বয়ংক্রিয় স্কিপের জন্য ক্ষুদ্র আকারের স্কিপ নোটিস"
},
"noticeVisibilityMode2": {
"message": "সব ক্ষুদ্র আকারের স্কিপ নোটিস"
},
"noticeVisibilityMode3": {
"message": "স্বয়ংক্রিয় স্কিপের জন্য অনুজ্বল স্কিপ নোটিস"
},
"noticeVisibilityMode4": {
"message": "সব অনুজ্বল স্কিপ নোটিস"
},
"longDescription": {
"message": "SponsorBlock আপনাকে YouTube ভিডিওসমূহের স্পন্সর বার্তা, সূচনাবার্তা, সমাপ্তিবার্তা, সাবস্ক্রাইব করার জন্য স্মরণ করানো, এবং অন্যান্য বিবিধ বিরক্তিকর অংশ এড়িয়ে যেতে সাহায্য করে। SponsorBlock জনসংগৃহীত তথ্যসম্বলিত একটি ব্রাউজার এক্সটেনশন যা যে কাউকে একটি ভিডিওর স্পন্সর বার্তা এবং অন্যান্য অংশের শুরু এবং শেষ সময় সাবমিট করতে দেয়। যখন কেউ একজন এই তথ্য সাবমিট করে, এই এক্সটেনশন ব্যবহারকারী সবাই ঐ স্পন্সর বার্তা সম্বলিত অংশ এড়িয়ে যাবে। মিউজিক ভিডিও এর মিউজিক বহির্ভুত অংশও আপনি এড়িয়ে যেতে পারেন।",
"description": "Full description of the extension on the store pages."
@ -246,6 +270,10 @@
"message": "নোটিসটি আপগ্রেড করা হয়েছে!",
"description": "The first line of the message displayed after the notice was upgraded."
},
"noticeUpdate2": {
"message": "আপনি যদি এখনও এটি পছন্দ না করেন তবে কখনই দেখাবে না বোতামটি চাপুন।",
"description": "The second line of the message displayed after the notice was upgraded."
},
"setSkipShortcut": {
"message": "সেগমেন্ট এড়িয়ে যান",
"description": "Keybind label"
@ -257,5 +285,667 @@
"setSubmitKeybind": {
"message": "সেগমেন্ট জমা দিন",
"description": "Keybind label"
},
"keybindDescription": {
"message": "এটি টাইপ করে একটি কী নির্বাচন করুন এবং আপনি যে কোনও সংশোধক কীগুলি ব্যবহার করতে চান তা চয়ন করুন।"
},
"0": {
"message": "সংযোগের সময়সীমা অতিক্রান্ত। আপনার ইন্টারনেট সংযোগ যাচাই করুন. যদি আপনার ইন্টারনেট কাজ করে থাকে তবে সার্ভারটি সম্ভবত ওভারলোডেড বা ডাউন।"
},
"disableSkipping": {
"message": "স্কিপিং চালু করা হয়েছে"
},
"enableSkipping": {
"message": "স্কিপিং বন্ধ করা হয়েছে"
},
"yourWork": {
"message": "আপনার কাজ",
"description": "Used to describe the section that will show you the statistics from your submissions."
},
"502": {
"message": "সার্ভারটি সম্ভবত ওভারলোডেড। কয়েক সেকেন্ডের মধ্যে আবার চেষ্টা করুন।"
},
"errorCode": {
"message": "ইরোর কোডঃ "
},
"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} এড়িয়ে যান?"
},
"mute_category": {
"message": "{0} নিঃশব্দ করুন?"
},
"skip_to_category": {
"message": "{0} তে চলে যান?",
"description": "Used for skipping to things (Skip to Highlight)"
},
"skipped": {
"message": "{0} এড়িয়ে যাওয়া হয়েছে",
"description": "Example: Sponsor Skipped"
},
"muted": {
"message": "{0} নিঃশব্দ করা হয়েছে",
"description": "Example: Sponsor Muted"
},
"skipped_to_category": {
"message": "{0} তে চলে যাওয়া হয়েছে",
"description": "Used for skipping to things (Skipped to Highlight)"
},
"disableAutoSkip": {
"message": "স্বয়ংক্রিয়ভাবে স্কিপ করা বন্ধ করুন"
},
"enableAutoSkip": {
"message": "স্বয়ংক্রিয়ভাবে স্কিপ করা চালু করুন"
},
"audioNotification": {
"message": "স্কিপের জন্য অডিও নোটিস"
},
"audioNotificationDescription": {
"message": "যখনই কোনও অংশ এড়িয়ে যাওয়া হয় তখন একটি শব্দ বাজাবে। যদি বন্ধ করা হয় (বা অটো স্কিপ বন্ধ থাকে) তবে কোনও শব্দ বাজানো হবে না।"
},
"showTimeWithSkips": {
"message": "স্কিপগুলি সরানো সহ সময় দেখান"
},
"showTimeWithSkipsDescription": {
"message": "এই সময়টি সময় বারের নীচে বর্তমান সময়ের পাশের বন্ধনীগুলিতে উপস্থিত হয়। এটি মোট ভিডিও সময়কাল যে কোনও বিভাগকে বিয়োগ করে তা দেখায়। এর মধ্যে কেবল \"সময় বার এ দেখান\" হিসাবে চিহ্নিত বিভাগগুলি অন্তর্ভুক্ত রয়েছে।"
},
"youHaveSkipped": {
"message": "আপনি এড়িয়েছেন "
},
"minLower": {
"message": "মিনিট"
},
"minsLower": {
"message": "মিনিট"
},
"hourLower": {
"message": "ঘন্টা"
},
"hoursLower": {
"message": "ঘন্টা"
},
"youHaveSavedTime": {
"message": "আপনি মানুষকে রক্ষা করেছেন",
"description": "You've saved people from 887,362 segments (236d 15h 5.3 minutes of their lives)."
},
"youHaveSavedTimeEnd": {
"message": "তাদের জীবন থেকে",
"description": "You've saved people from 887,362 segments (236d 15h 5.3 minutes of their lives)."
},
"statusReminder": {
"message": "সার্ভারের স্ট্যাটাস এর জন্য status.sponsor.ajay.app দেখুন করুন।"
},
"changeUserID": {
"message": "আপনার ইউজার আইডি ইম্পোর্ট/এক্সপোর্ট করুন"
},
"whatChangeUserID": {
"message": "এটি ব্যক্তিগত রাখা উচিত। এটি একটি পাসওয়ার্ডের মতো এবং কারও সাথে ভাগ করা উচিত নয়। কারও যদি এটি থাকে তবে তারা আপনার ছদ্মবেশ ধারণ করতে পারে। আপনি যদি আপনার পাবলিক ইউজারআইডি খুঁজছেন তবে পপআপে ক্লিপবোর্ড আইকনটি ক্লিক করুন।"
},
"setUserID": {
"message": "ইউজার আইডি দিন"
},
"userIDChangeWarning": {
"message": "সতর্কতা: ইউজারআইডি পরিবর্তন করা চিরস্থায়ী। আপনি কি নিশ্চিত যে আপনি এটি করতে চান? আপনার পুরানোটিকে সাবধানতার সার্থে ব্যাকআপ করার বিষয়টি নিশ্চিত করুন।"
},
"createdBy": {
"message": "সৃষ্টি করেছেন"
},
"supportOtherSites": {
"message": "এটি ৩য় পক্ষের ইউটইউব সাইট সাপোর্ট করে"
},
"supportOtherSitesDescription": {
"message": "তৃতীয় পক্ষের ইউটিউব ক্লায়েন্টদের সমর্থন করুন। সমর্থন সক্ষম করতে, আপনাকে অবশ্যই অতিরিক্ত অনুমতিগুলি গ্রহণ করতে হবে। এটি ক্রোম এবং অন্যান্য ক্রোমিয়াম ভেরিয়েন্টগুলিতে ছদ্মবেশে কাজ করে না।",
"description": "This replaces the 'supports Invidious' option because it now works on other YouTube sites such as Cloudtube"
},
"supportedSites": {
"message": "সমর্থিত সাইটসমুহ:"
},
"optionsInfo": {
"message": "Indivious সমর্থন সক্ষম করুন, অটোস্কিপ বন্ধ করুন, বোতামগুলি লুকান এবং আরও অনেক কিছু করুন।"
},
"addInvidiousInstance": {
"message": "তৃতীয় পক্ষের ক্লায়েন্ট যুক্ত করুন"
},
"addInvidiousInstanceDescription": {
"message": "একটি কাস্টম উদাহরণ যুক্ত করুন। এটি অবশ্যই ডোমেন দিয়ে ফর্ম্যাট করা উচিত। উদাহরণ: invidious.ajay.app"
},
"add": {
"message": "যোগ করুন"
},
"addInvidiousInstanceError": {
"message": "এটি একটি অবৈধ ডোমেন। এটিতে কেবল ডোমেন অংশ অন্তর্ভুক্ত করা উচিত। উদাহরণ: invidious.ajay.app"
},
"resetInvidiousInstance": {
"message": "Invidious Instance এর তালিকা পুনরায় সেট করুন"
},
"resetInvidiousInstanceAlert": {
"message": "আপনি এখন Invidious Instance এর তালিকা পুনরায় আগের মত করে দিবেন"
},
"currentInstances": {
"message": "বর্তমান Instance এর তালিকা"
},
"minDuration": {
"message": "সর্বনিম্ন দৈর্ঘ্য (সেকেন্ড):"
},
"minDurationDescription": {
"message": "সেট করা মান (সেকেন্ডে) থেকে ছোট সেগমেন্টগুলি প্লেয়ারে এড়িয়ে যাওয়া হবে বা দেখানো হবে না"
},
"skipNoticeDuration": {
"message": "নোটিশ প্রদর্শন করার দৈর্ঘ্য (সেকেন্ড):"
},
"skipNoticeDurationDescription": {
"message": "স্কিপ নোটিশটি কমপক্ষে এত সেকেন্ডের জন্য স্ক্রিনে থাকবে। নিজে এড়িয়ে যাওয়ার জন্য, এটি দীর্ঘকাল ধরে দৃশ্যমান হতে পারে।"
},
"shortCheck": {
"message": "নিম্নলিখিত সাবমিশনটি আপনার ন্যূনতম সময়কাল অপশনের চেয়ে কম। এর মানে এই হতে পারে যে এটি ইতিমধ্যে জমা দেওয়া হয়েছে, এবং এই অপশনের কারণে উপেক্ষা করা হচ্ছে। আপনি কি জমা দিতে চান?"
},
"liveOrPremiere": {
"message": "একটি সক্রিয় লাইভস্ট্রিম বা প্রিমিয়ারে জমা দেওয়ার অনুমতি নেই। এটি শেষ না হওয়া পর্যন্ত অপেক্ষা করুন, তারপরে পৃষ্ঠাটি রিফ্রেশ করুন এবং অংশগুলি এখনও বৈধ কিনা তা যাচাই করুন।"
},
"showUploadButton": {
"message": "আপলোড করার বোতামটি দেখান"
},
"customServerAddress": {
"message": "স্পনসরব্লক সার্ভার ঠিকানা"
},
"customServerAddressDescription": {
"message": "এ ঠিকানা SponsorBlock সার্ভারে সাথে যোগাযোগ করতে ব্যবহার করে।\nআপনার নিজের সার্ভার না থাকলে এটি পরিবর্তন করবেন না।"
},
"save": {
"message": "সংরক্ষণ করুন"
},
"reset": {
"message": "পুনরায় সেট করুন"
},
"customAddressError": {
"message": "এই ঠিকানাটি সঠিক আকারে নেই। এটিতে http: // বা https: // শুরুতে এবং কোনও পিছনের স্ল্যাশ নেই তা নিশ্চিত করুন।"
},
"areYouSureReset": {
"message": "আপনি কি নিশ্চিতভাবে এটি মুছে ফেলতে চান??"
},
"mobileUpdateInfo": {
"message": "m.youtube.com এখন সাপর্টেড"
},
"exportOptions": {
"message": "সব অপশন ইম্পোর্ট/এক্সপোর্ট করুন"
},
"exportOptionsCopy": {
"message": "সম্পাদন/কপি করুন"
},
"exportOptionsDownload": {
"message": "ফাইল এ সেভ করুন"
},
"exportOptionsUpload": {
"message": "ফাইল থেকে লোড করুন"
},
"whatExportOptions": {
"message": "এটি আপনার সম্পূর্ণ কনফিগারেশন এতে আপনার ইউজারআইডি অন্তর্ভুক্ত রয়েছে, তাই বিজ্ঞতার সাথে শেয়ার করতে ভুলবেন না।."
},
"setOptions": {
"message": "অপশন সেট করুন"
},
"exportOptionsWarning": {
"message": "সতর্কতা: অপশনগুলি পরিবর্তন করা চিরস্থায়ী এবং আপনার ইনস্টলটি ভাঙতে পারে। আপনি কি নিশ্চিত যে আপনি এটি করতে চান? আপনার পুরানোটিকে সাবধানতার সার্থে ব্যাকআপ করার বিষয়টি নিশ্চিত করুন।"
},
"incorrectlyFormattedOptions": {
"message": "এই JSON সঠিকভাবে ফর্ম্যাট করা হয় নাই। আপনার অপশনগুলি পরিবর্তন করা হয়নি।"
},
"confirmNoticeTitle": {
"message": "অংশ জমা দিন"
},
"submit": {
"message": "জমা দিন"
},
"cancel": {
"message": "বাতিল করুন"
},
"delete": {
"message": "মুছে ফেলুন"
},
"preview": {
"message": "প্রিভিউ দেখুন"
},
"unsubmitted": {
"message": "জমাকৃত নয়"
},
"inspect": {
"message": "পরিদর্শন করুন"
},
"edit": {
"message": "সম্পাদন করুন"
},
"copyDebugInformation": {
"message": "ডিবাগ তথ্য ক্লিপবোর্ডে কপি করুন"
},
"copyDebugInformationFailed": {
"message": "ক্লিপবোর্ডে কপি করা যায় নি"
},
"copyDebugInformationOptions": {
"message": " কোনও বাগ উত্থাপন করার সময় / যখন কোনও ডেভেলপার এটির জন্য অনুরোধ করে তখন ক্লিপবোর্ডে তথ্য কপি করে। সংবেদনশীল তথ্য যেমন আপনার ইউজার আইডি, সাদা তালিকাভুক্ত চ্যানেল এবং কাস্টম সার্ভারের ঠিকানা সরানো হয়েছে। তবে এটিতে আপনার ব্যবহারকারীর, ব্রাউজার, অপারেটিং সিস্টেম এবং এক্সটেনশন সংস্করণ নম্বরের মতো তথ্য রয়েছে।"
},
"copyDebugInformationComplete": {
"message": "ডিবাগের তথ্য ক্লিপ বোর্ডে কপি করা হয়েছে। আপনি কোনও তথ্য দিতে অনিচ্ছুক হলে তা নির্দ্বিধায় অপসারণ করতে পারেন। এটি একটি টেক্সট ফাইলে সংরক্ষণ করুন বা বাগ প্রতিবেদনে পেস্ট করুন।"
},
"keyAlreadyUsed": {
"message": "এই শর্টকাটটি অন্য ক্রিয়ায় আবদ্ধ। দয়া করে একটি আলাদা নির্বাচন করুন।"
},
"to": {
"message": "থেকে",
"description": "Used between segments. Example: 1:20 to 1:30"
},
"category_sponsor": {
"message": "স্পন্সর"
},
"category_sponsor_description": {
"message": "পেইড প্রমোশন, পেইড রেফারেল এবং সরাসরি বিজ্ঞাপন। নিজের পছন্দসই কারণ/স্রষ্টা/ওয়েবসাইট/পণ্যগুলিতে স্ব-প্রচার বা বিনামূল্যে প্রচারের জন্য নয়।"
},
"category_selfpromo": {
"message": "বিনা অর্থপ্রাপ্ত/স্ব-প্রচার"
},
"category_selfpromo_description": {
"message": "অবৈতনিক বা স্ব -প্রচার ব্যতীত \"স্পনসর\" এর মতো। এর মধ্যে পণ্যদ্রব্য, অনুদান বা তারা কার সাথে সহযোগিতা করেছে সে সম্পর্কে তথ্য অন্তর্ভুক্ত রয়েছে।"
},
"category_exclusive_access": {
"message": "এক্সক্লুসিভ অ্যাক্সেস"
},
"category_exclusive_access_description": {
"message": "শুধুমাত্র পুরো ভিডিও লেবেল করার জন্য। যখন কোনও ভিডিও কোনও পণ্য, পরিষেবা বা অবস্থান প্রদর্শন করে যা তারা নিখরচায় বা ভর্তুকিযুক্ত অ্যাক্সেস পেয়েছে।"
},
"category_exclusive_access_pill": {
"message": "এই ভিডিওটি এমন একটি পণ্য, পরিষেবা বা অবস্থান প্রদর্শন করে যা তারা নিখরচায় বা ভর্তুকিযুক্ত অ্যাক্সেস পেয়েছে",
"description": "Short description for this category"
},
"category_interaction": {
"message": "ইন্টারঅ্যাকশন রেমাইন্ডার (সাবস্ক্রাইব)"
},
"category_interaction_description": {
"message": "যখন সামগ্রীর মাঝখানে তাদেরকে লাইক, সাবস্ক্রাইব বা ফলো করার জন্য একটি সংক্ষিপ্ত অনুস্মারক থাকে। যদি এটি দীর্ঘ বা নির্দিষ্ট কিছু সম্পর্কে হয় তবে পরিবর্তে এটি স্ব -প্রচারের অধীনে থাকা উচিত।"
},
"category_interaction_short": {
"message": "ইন্টারঅ্যাকশন রেমাইন্ডার"
},
"category_intro": {
"message": "ইন্টারমিশন/ইন্ট্র অ্যানিমেশন"
},
"category_intro_description": {
"message": "প্রকৃত বিষয়বস্তু ছাড়াই একটি বিরতি। বিরতি, স্থির ফ্রেম, অ্যানিমেশন পুনরাবৃত্তি হতে পারে। এটি তথ্যযুক্ত ট্রানজিশনের জন্য ব্যবহার করা উচিত নয়।"
},
"category_intro_short": {
"message": "ইন্টারমিশন"
},
"category_outro": {
"message": "এন্ডকার্ডস/ক্রেডিট"
},
"category_outro_description": {
"message": "ক্রেডিট বা যখন ইউটিউব এন্ডকার্ডগুলি উপস্থিত হয়। তথ্য সহ সিদ্ধান্তের জন্য নয়।"
},
"category_preview": {
"message": "প্রিভিউ/রিক্যাপ"
},
"category_preview_description": {
"message": "পূর্ববর্তী পর্বগুলির দ্রুত পুনরুদ্ধার, বা বর্তমান ভিডিওতে পরে কী ঘটছে তার পূর্বরূপ। একসাথে সম্পাদিত ক্লিপ এর জন্য, কথ্য সংক্ষিপ্তসার এর জন্য নয়।"
},
"category_filler": {
"message": "ফিলার ট্যানজেন্ট/জোকস"
},
"category_filler_description": {
"message": "স্পর্শকাতর দৃশ্য যেগুলি কেবল ফিলার বা হাস্যরসের জন্য যুক্ত হয়েছে যা ভিডিওর মূল বিষয়বস্তু বোঝার জন্য প্রয়োজন হয় না। এর মধ্যে প্রসঙ্গ বা পটভূমির বিশদ সরবরাহকারী বিভাগগুলি অন্তর্ভুক্ত করা উচিত নয়।"
},
"category_filler_short": {
"message": "ফিলার"
},
"category_music_offtopic": {
"message": "সঙ্গীত: অসঙ্গীত বিভাগ"
},
"category_music_offtopic_description": {
"message": "শুধুমাত্র সঙ্গীত ভিডিওতে ব্যবহারের জন্য। এটি কেবলমাত্র সংগীত ভিডিওর সেসব বিভাগের জন্য ব্যবহার করা উচিত যা ইতিমধ্যে অন্য কোনও বিভাগ দ্বারা আচ্ছাদিত নয়।"
},
"category_music_offtopic_short": {
"message": "মিউসিক নয়"
},
"category_poi_highlight": {
"message": "গুরুত্বপূর্ণ"
},
"category_poi_highlight_description": {
"message": "ভিডিওর অংশটি যা বেশিরভাগ লোকেরা খুঁজছেন। \"ভিডিওটি x এ শুরু হয়\" মন্তব্যের মতো।"
},
"category_livestream_messages": {
"message": "লাইভস্ট্রিম: অনুদান/বার্তা পাঠ"
},
"category_livestream_messages_short": {
"message": "বার্তা পাঠ"
},
"autoSkip": {
"message": "স্বয়ংক্রিয়ভাবে এড়িয়ে যান"
},
"manualSkip": {
"message": "নিজে এড়িয়ে যান"
},
"showOverlay": {
"message": "সময় বার এ দেখান"
},
"disable": {
"message": "নিষ্ক্রিয় করুন"
},
"autoSkip_POI": {
"message": "স্বয়ংক্রিয় ভাবে শুরুতে স্কিপ করুন"
},
"manualSkip_POI": {
"message": "ভিডিও লোড হলে জিজ্ঞেস করুন"
},
"showOverlay_POI": {
"message": "সময় বার এ দেখান"
},
"showOverlay_full": {
"message": "লেবেল দেখান"
},
"autoSkipOnMusicVideos": {
"message": "যখন অ-সংগীত বিভাগ থাকে তখন স্বয়ংক্রিয়ভাবে সমস্ত বিভাগগুলি এড়িয়ে যান"
},
"muteSegments": {
"message": "স্কিপের পরিবর্তে অডিও নিঃশব্দ এমন বিভাগগুলিকে দেখানোর অনুমতি দিন"
},
"fullVideoSegments": {
"message": "যখন কোনও ভিডিও সম্পূর্ণ বিজ্ঞাপন হয় তখন একটি আইকন দেখান",
"description": "Referring to the category pill that is now shown on videos that are entirely sponsor or entirely selfpromo"
},
"previewColor": {
"message": "জমাকৃত নয় এমন অংশের রঙ",
"description": "Referring to submissions that have not been sent to the server yet."
},
"seekBarColor": {
"message": "সময় বারের রঙ"
},
"category": {
"message": "বিভাগ"
},
"skipOption": {
"message": "সেগমেন্ট এড়িয়ে যাওয়ার অপশন",
"description": "Used on the options page to describe the ways to skip the segment (auto skip, manual, etc.)"
},
"enableTestingServer": {
"message": "বিটা টেস্টিং প্রোগ্রামে যোগদান করুন"
},
"whatEnableTestingServer": {
"message": "আপনার জমাকৃত অংশ এবং ভোটগুলি মূল সার্ভারের হিসাবে গণনা করা হবে না। কেবল পরীক্ষার জন্য এটি ব্যবহার করুন।"
},
"testingServerWarning": {
"message": "সমস্ত জমাকৃত অংশ এবং ভোট টেস্ট সার্ভারের সাথে সংযোগ করার সময় মূল সার্ভারের হিসাবে গণনা করা হবেনা। আপনি যখন সত্যিকারের অংশ ও ভোট জমা দিতে চান তখন এটি বন্ধ করার বিষয়টি নিশ্চিত করুন।"
},
"bracketNow": {
"message": "(এখন)"
},
"moreCategories": {
"message": "আরো বিভাগ"
},
"chooseACategory": {
"message": "বিভাগ নির্বাচন করুন"
},
"enableThisCategoryFirst": {
"message": "\"{0}\" বিভাগের অন্তর্ভুক্ত অংশ জমা দিতে, আপনাকে এটি অপশন এ গিয়ে চালু করতে হবে। আপনাকে এখন অপশন এ পাঠানো হবে।",
"description": "Used when submitting segments to only let them select a certain category if they have it enabled in the options."
},
"poiOnlyOneSegment": {
"message": "সতর্কতা: এই ধরণের বিভাগে একসময়ে সর্বাধিক একবার সক্রিয় থাকতে পারে। একাধিক জমা দেওয়া হলে যেকোন একটি এলোমেলোভাবে বেছে সেটি দেখানো হবে।"
},
"youMustSelectACategory": {
"message": "আপনাকে প্রত্যেকটি অংশের জন্য কমপক্ষে একটি করে ক্যাটাগরি সিলেক্ট করতে হবে!"
},
"bracketEnd": {
"message": "(শেষ)"
},
"hiddenDueToDownvote": {
"message": "লুক্কায়িতঃ ডাউনভোট"
},
"hiddenDueToDuration": {
"message": "লুক্কায়িতঃ খুব ছোট"
},
"manuallyHidden": {
"message": "নিজে লুক্কায়িত"
},
"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": "চ্যানেল আইডি এখনও লোড হয় না। আপনি যদি এম্বেডে থাকা ভিডিও দেখছেন তবে পরিবর্তে ইউটিউব হোমপেজটি ব্যবহার করার চেষ্টা করুন। এটি ইউটিউব লেআউটে পরিবর্তনের কারণেও হতে পারে, আপনার যদি মনেহয় এটি লেআউটে পরিবর্তনের কারণে হয়েছে তাহলে এখানে একটি মন্তব্য করুনঃ "
},
"videoInfoFetchFailed": {
"message": "দেখে মনে হচ্ছে যে কোনও কিছু স্পনসরব্লকের ভিডিওর ডেটা পাওয়ার ক্ষমতা অবরুদ্ধ করছে। আরও তথ্যের জন্য দয়া করে https://github.com/ajayy/sponsorblock/issues/741 দেখুন।"
},
"youtubePermissionRequest": {
"message": "দেখে মনে হচ্ছে স্পনসরব্লক YouTube API তে পৌঁছাতে অক্ষম। এটি ঠিক করতে, এরপর উপস্থিত হবে সেই অনুমতি প্রম্পটটি গ্রহণ করবেন, তারপর কয়েক সেকেন্ড অপেক্ষা করে পৃষ্ঠাটি পুনরায় লোড করুন।"
},
"acceptPermission": {
"message": "অনুমতি একসেপ্ট করুন"
},
"permissionRequestSuccess": {
"message": "অনুমতির অনুরোধ সফল হয়েছে!"
},
"permissionRequestFailed": {
"message": "অনুমতির অনুরোধ ব্যর্থ হয়েছে, আপনি কি ডেনাই ক্লিক করেছেন?"
},
"adblockerIssueWhitelist": {
"message": "আপনি যদি এটি সমাধান করতে অক্ষম হন তবে সেটিংস এ গিয়ে 'এড়িয়ে যাওয়ার আগে চ্যানেল চেক করুন' বন্ধ করুন, কারণ স্পনসরব্লক এই ভিডিওটির জন্য চ্যানেলের তথ্য পুনরুদ্ধার করতে অক্ষম"
},
"forceChannelCheck": {
"message": "এড়িয়ে যাওয়ার আগে চ্যানেল চেক করুন"
},
"whatForceChannelCheck": {
"message": "সাধারণত, এটি চ্যানেলটি কী তা জানার আগেই এটি এখনই বিভাগগুলি এড়িয়ে যাবে। সাধারণত, ভিডিওর শুরুতে কিছু বিভাগগুলি সাদা তালিকাভুক্ত চ্যানেলগুলিতে এড়িয়ে যেতে পারে। এই বিকল্পটি সক্ষম করা এটিকে প্রতিরোধ করবে তবে চ্যানেলআইডি পেতে কিছুটা সময় নিতে পারে বলে সমস্ত এড়িয়ে যাওয়া কিছুটা বিলম্বিত করে। আপনার যদি দ্রুত ইন্টারনেট থাকে তবে এই বিলম্বটি অদৃশ্য হতে পারে।"
},
"forceChannelCheckPopup": {
"message": "\"এড়িয়ে যাওয়ার আগে চ্যানেল চেক করুন\" সেটিংস টি চালু করার বিষয়টি বিবেচনা করুন"
},
"downvoteDescription": {
"message": "সময় ভুল দেওয়া হয়েছে"
},
"incorrectCategory": {
"message": "বিভাগ পরিবর্তন করুন"
},
"nonMusicCategoryOnMusic": {
"message": "এই ভিডিওটি সংগীত হিসাবে শ্রেণীবদ্ধ করা হয়েছে। আপনি কি নিশ্চিত যে এটি একটি স্পনসর আছে? যদি এটি আসলে একটি \"অ-সংগীত বিভাগ\" হয় তবে এক্সটেনশন এর অপশন এ যান এবং এই বিভাগটি চালু করুন। তারপরে, আপনি এই বিভাগটিকে স্পনসর পরিবর্তে \"অ-সংগীত\" হিসাবে জমা দিতে পারেন। আপনি বিভ্রান্ত হলে দয়া করে নিয়মকানুনপড়ুন।"
},
"multipleSegments": {
"message": "অনেকগুলো অংশ"
},
"guidelines": {
"message": "নিয়মকানুন"
},
"readTheGuidelines": {
"message": "নিয়মকানুন পড়ুন!!",
"description": "Show the first time they submit or if they are \"high risk\""
},
"categoryUpdate1": {
"message": "বিভাগ ফিচার এসেছে!"
},
"categoryUpdate2": {
"message": "ইন্ট্রোস, আউট্রোস, মার্চ ইত্যাদি এড়িয়ে যাওয়ার জন্য অপশন খুলুন।"
},
"help": {
"message": "সাহায্য"
},
"GotIt": {
"message": "বুঝেছি",
"description": "Used as the button to dismiss a tooltip"
},
"fullVideoTooltipWarning": {
"message": "এই বিভাগটি বিশাল। যদি পুরো ভিডিওটি একটি বিষয় নিয়ে হয় তবে \"Skip\" থেকে \"Full Video\" এ পরিবর্তন করুন। আরও তথ্যের জন্য নিয়মকানুন দেখুন।"
},
"categoryPillTitleText": {
"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."
},
"hideForever": {
"message": "চিরকালের জন্য এই বিষয়বস্তু লুকান"
},
"warningChatInfo": {
"message": "আপনি একটি সতর্কতা পেয়েছেন এবং অস্থায়ীভাবে বিভাগগুলি জমা দিতে পারবেন না। এর অর্থ হ'ল আমরা লক্ষ্য করেছি যে আপনি কিছু সাধারণ ভুল করছেন যা দূষিত নয়, দয়া করে কেবল নিশ্চিত করুন যে আপনি নিয়মগুলি বুঝতে পেরেছেন এবং আমরা সতর্কতাটি সরিয়ে দেব। আপনি আমাদের চ্যাটটি discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app ব্যবহার করে যোগ দিতে পারেন"
},
"voteRejectedWarning": {
"message": "একটি সতর্কতার কারণে ভোট প্রত্যাখ্যান করা হয়েছে। এটি সমাধানের জন্য চ্যাট খুলতে ক্লিক করুন বা আপনার এখন সময় না থাকলে পরে আবার ফিরে আসুন।",
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
},
"Donate": {
"message": "অনুদান"
},
"considerDonating": {
"message": "অনুদানের মাধ্যমে ভবিষ্যতেে এর উন্নয়নের কাজে সাহায্য হবে"
},
"hideDonationLink": {
"message": "অনুদানের লিঙ্ক লুকান"
},
"darkModeOptionsPage": {
"message": "অপশন পেজে ডার্ক মোড "
},
"helpPageThanksForInstalling": {
"message": "স্পনসরব্লক ইনস্টল করার জন্য আপনাকে ধন্যবাদ."
},
"helpPageReviewOptions": {
"message": "নীচের অপশনগুলি পর্যালোচনা করুন"
},
"helpPageFeatureDisclaimer": {
"message": "অনেকগুলি বৈশিষ্ট্য সাধারণত অক্ষম থাকে। আপনি যদি ইন্ট্রোস, আউট্রোস এড়িয়ে যেতে চান, Invidious ইত্যাদি ব্যবহার করতে চান তবে সেগুলি নীচে সক্ষম করুন। আপনি UI উপাদানগুলিও লুকাতে/দেখাতে পারেন।"
},
"helpPageHowSkippingWorks": {
"message": "এটি কীভাবে কাজ করে"
},
"helpPageHowSkippingWorks1": {
"message": "ভিডিও এর অংশ ডাটাবেসে পাওয়া গেলে সেগুলি স্বয়ংক্রিয়ভাবে এড়িয়ে যাবে। তারা কীরকম তা পূর্বরূপ দেখতেআপনি এক্সটেনশন আইকনে ক্লিক করে পপআপটি খুলতে পারেন।"
},
"helpPageHowSkippingWorks2": {
"message": "আপনি যখনই কোনও বিভাগ এড়িয়ে যান, আপনি একটা নোটিস পাবেন। সময়টি যদি ভুল বলে মনে হয় তবে ডাউনভোটে ক্লিক করে ভোট দিন! আপনি পপআপ থেকেও ভোট দিতে পারেন।"
},
"Submitting": {
"message": "নতুন অংশ জমা দেওয়া"
},
"helpPageSubmitting1": {
"message": "জমা দেওয়া যায় পপআপে \"অংশ এখন শুরু হয়\" বোতামটি টিপ মেরে বা প্লেয়ারের বোতামগুলি সহ ভিডিও প্লেয়ারে টিপ মেরে করে।"
},
"helpPageSubmitting2": {
"message": "প্লে বোতামটি ক্লিক করা একটি বিভাগের শুরু নির্দেশ করে এবং স্টপ আইকনটি ক্লিক করা শেষটি নির্দেশ করে। আপনি জমা দেওয়ার আগে একাধিক স্পনসর প্রস্তুত করতে পারেন। আপলোড বোতামটি ক্লিক করা জমা দেওয়া হবে। আবর্জনায় ক্লিক করে বাদ দিতে পারেন এটি।"
},
"Editing": {
"message": "সম্পাদন করা"
},
"helpPageEditing1": {
"message": "যদি আপনি ভুল করে বসেন তবে আপনি উপড়ের তীর বোতামটি ক্লিক করার পরে আপনার বিভাগগুলি সম্পাদনা বা মুছতে পারেন।"
},
"helpPageTooSlow": {
"message": "গতি অত্যন্ত ধীর।"
},
"helpPageTooSlow1": {
"message": "আপনি যদি ব্যবহার করতে চান তাইলে হটকি রয়েছে। স্পনসর বিভাগের শুরু/শেষ নির্দেশ করতে সেমিকোলন কী টিপুন এবং জমা দেওয়ার জন্য অ্যাপোস্ট্রোফে ক্লিক করুন। এগুলি পরিবর্তন করা যেতে পারে। আপনি যদি QWERTY ব্যবহার না করেন তবে আপনার সম্ভবত কীবাইন্ডিং পরিবর্তন করা উচিত।"
},
"helpPageCopyOfDatabase": {
"message": "আমি কি ডাটাবেসের একটি অনুলিপি পেতে পারি? আপনি হারিয়ে গেলে হলে কি হবে?"
},
"helpPageCopyOfDatabase1": {
"message": "ডাটাবেসটি সবার জন্য প্রকাশিত এবং পাওয়া যাবে এখানেঃ "
},
"helpPageCopyOfDatabase2": {
"message": "এটির সোর্স কোড অবাধে উপলব্ধ। সুতরাং, যদি আমার কিছু ঘটেও যায় তবে আপনার জমাকৃত অংশগুলি হারিয়ে যাবে না।"
},
"helpPageNews": {
"message": "খবর এবং এটি কীভাবে তৈরি হয়"
},
"helpPageSourceCode": {
"message": "আমি সোর্স কোডটি কোথায় পেতে পারি?"
},
"Credits": {
"message": "কৃতিত্ব"
},
"LearnMore": {
"message": "আরও জানুন"
},
"CopyDownvoteButtonInfo": {
"message": "ডাউনভোট করে আপনার জন্য পুনরায় জমা দেওয়ার জন্য একটি স্থানীয় অনুলিপি তৈরি করে"
},
"OpenCategoryWikiPage": {
"message": "এই বিভাগের উইকি পাতা খুলুন."
},
"CopyAndDownvote": {
"message": "কপি এবং ডাউনভোট"
},
"ContinueVoting": {
"message": "ভোট করা চালিয়ে যান"
},
"ChangeCategoryTooltip": {
"message": "এটি তাৎক্ষনিকভাবে আপনার অংশে দেওয়া হবে"
},
"downvote": {
"message": "ডাউনভোট"
},
"upvote": {
"message": "আপভোট"
},
"hideSegment": {
"message": "অংশ আড়াল করুন"
},
"SponsorTimeEditScrollNewFeature": {
"message": "দ্রুত সময়টি পরিবর্কতন করতে সম্পাদনা বাক্সে ঘুরে দেখার সময় আপনার মাউস এর হুইয়িলটি ব্যবহার করুন। কন্ট্রল বা শিফট কী এর সংমিশ্রণগুলি পরিবর্তনগুলি আরো নিখুতভাবে টিউন করতে ব্যবহার করা যেতে পারে।"
},
"categoryPillNewFeature": {
"message": "নতুন! দেখুন যখন কোনও ভিডিও সম্পূর্ণ স্পনসর করা বা স্ব-প্রচার হয়"
},
"dayAbbreviation": {
"message": " দিন",
"description": "100d"
},
"hourAbbreviation": {
"message": " ঘণ্টা",
"description": "100h"
},
"optionsTabBehavior": {
"message": "আচরণ",
"description": "Appears in Options as a tab header for options related to categories and skipping behavior. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabInterface": {
"message": "ইন্টারফেস",
"description": "Appears in Options as a tab header for options related to GUI and sounds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabKeyBinds": {
"message": "কীবোর্ড শর্টকাট",
"description": "Appears in Options as a tab header for keybinds. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabBackup": {
"message": "ব্যাকআপ এবং পুনঃস্থাপন",
"description": "Appears in Options as a tab header for options related to saving/restoring your settings. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"optionsTabAdvanced": {
"message": "বিবিধ",
"description": "Appears in Options as a tab header for advanced/niche options. To fit inside the button, it should not be longer than ~20-25 characters (depending on their width)."
},
"noticeVisibilityLabel": {
"message": "নোটিশ প্রদর্শন করা বন্ধ করুন",
"description": "Option label"
},
"unbind": {
"message": "বাদ দিন",
"description": "Unbind keyboard shortcut"
},
"notSet": {
"message": "নির্ধারণ করা হয়নি"
},
"change": {
"message": "বদল করুন"
},
"youtubeKeybindWarning": {
"message": "এটি একটি অন্তর্নির্মিত ইউটিউব শর্টকাট। আপনি কি নিশ্চিত যে আপনি এটি ব্যবহার করতে চান?"
},
"betaServerWarning": {
"message": "বেটা সার্ভার চালু করা হয়েছে!"
},
"openOptionsPage": {
"message": "বিকল্প পাতা খুলুন"
}
}

View file

@ -101,7 +101,7 @@
"message": "It seems the server is down. Contact the dev immediately."
},
"connectionError": {
"message": "A connection error has occured. Error code: "
"message": "A connection error has occurred. Error code: "
},
"clearTimes": {
"message": "Clear Segments"
@ -239,6 +239,9 @@
"showSkipNotice": {
"message": "Show Notice After A Segment Is Skipped"
},
"showCategoryGuidelines": {
"message": "Show Category Help"
},
"noticeVisibilityMode0": {
"message": "Full Size Skip Notices"
},
@ -356,7 +359,7 @@
"message": "Show Time With Skips Removed"
},
"showTimeWithSkipsDescription": {
"message": "This time appears in brackets next to the current time on below the seekbar. This shows the total video duration minus any segments. This includes segments marked as only \"Show In Seekbar\"."
"message": "This time appears in brackets next to the current time on below the Seek Bar. This shows the total video duration minus any segments. This includes segments marked as only \"Show In Seek Bar\"."
},
"youHaveSkipped": {
"message": "You've skipped "
@ -410,7 +413,7 @@
"message": "Supported Sites: "
},
"optionsInfo": {
"message": "Enable Invidious support, disable autoskip, hide buttons and more."
"message": "Enable Invidious support, disable auto skip, hide buttons and more."
},
"addInvidiousInstance": {
"message": "Add 3rd-Party Client Instance"
@ -499,7 +502,7 @@
"incorrectlyFormattedOptions": {
"message": "This JSON is not formatted correctly. Your options have not been changed."
},
"confirmNoticeTitle" : {
"confirmNoticeTitle": {
"message": "Submit Segment"
},
"submit": {
@ -542,18 +545,39 @@
"message": "to",
"description": "Used between segments. Example: 1:20 to 1:30"
},
"generic_guideline1": {
"message": "Include segue transitions"
},
"generic_guideline2": {
"message": "Plays as if nothing was skipped"
},
"category_sponsor": {
"message": "Sponsor"
},
"category_sponsor_description": {
"message": "Paid promotion, paid referrals and direct advertisements. Not for self-promotion or free shoutouts to causes/creators/websites/products they like."
},
"category_sponsor_guideline1": {
"message": "Paid promotions"
},
"category_sponsor_guideline2": {
"message": "Not for donations or custom merch"
},
"category_selfpromo": {
"message": "Unpaid/Self Promotion"
},
"category_selfpromo_description": {
"message": "Similar to \"sponsor\" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated with."
},
"category_selfpromo_guideline1": {
"message": "Donations, memberships and custom merch"
},
"category_selfpromo_guideline2": {
"message": "Free shoutouts that don't add to the video"
},
"category_selfpromo_guideline3": {
"message": "Not for corporate designed products and merch"
},
"category_exclusive_access": {
"message": "Exclusive Access"
},
@ -564,12 +588,24 @@
"message": "This video showcases a product, service or location that they've received free or subsidized access to",
"description": "Short description for this category"
},
"category_exclusive_access_guideline1": {
"message": "Entire video showcases something with free or subsidized access"
},
"category_interaction": {
"message": "Interaction Reminder (Subscribe)"
},
"category_interaction_description": {
"message": "When there is a short reminder to like, subscribe or follow them in the middle of content. If it is long or about something specific, it should be under self promotion instead."
},
"category_interaction_guideline1": {
"message": "Short reminders to like, subscribe or follow"
},
"category_interaction_guideline2": {
"message": "Includes indirect reminders to comment"
},
"category_interaction_guideline3": {
"message": "Not for general promotion, only calls to action"
},
"category_interaction_short": {
"message": "Interaction Reminder"
},
@ -582,18 +618,36 @@
"category_intro_short": {
"message": "Intermission"
},
"category_intro_guideline1": {
"message": "Interval without actual content"
},
"category_intro_guideline2": {
"message": "Not for transitions with information"
},
"category_outro": {
"message": "Endcards/Credits"
},
"category_outro_description": {
"message": "Credits or when the YouTube endcards appear. Not for conclusions with information."
},
"category_outro_guideline1": {
"message": "Don't include content, even if endcards are on screen"
},
"category_preview": {
"message": "Preview/Recap"
},
"category_preview_description": {
"message": "Quick recap of previous episodes, or a preview of what's coming up later in the current video. Meant for edited together clips, not for spoken summaries."
},
"category_preview_guideline1": {
"message": "Clips that appear later, or in a future video"
},
"category_preview_guideline2": {
"message": "Recap of a previous video"
},
"category_preview_guideline3": {
"message": "Not for sections that add additional content"
},
"category_filler": {
"message": "Filler Tangent/Jokes"
},
@ -603,6 +657,15 @@
"category_filler_short": {
"message": "Filler"
},
"category_filler_guideline1": {
"message": "Tangential scenes only for filler or humor"
},
"category_filler_guideline2": {
"message": "Distractions, bloopers, replays"
},
"category_filler_guideline3": {
"message": "Not for scenes required to understand the topic"
},
"category_music_offtopic": {
"message": "Music: Non-Music Section"
},
@ -612,12 +675,27 @@
"category_music_offtopic_short": {
"message": "Non-Music"
},
"category_music_offtopic_guideline1": {
"message": "Sections not in official releases"
},
"category_music_offtopic_guideline2": {
"message": "Non-music in a live performance"
},
"category_poi_highlight": {
"message": "Highlight"
},
"category_poi_highlight_description": {
"message": "The part of the video that most people are looking for. Similar to \"Video starts at x\" comments."
},
"category_poi_highlight_guideline1": {
"message": "Section most people are looking for"
},
"category_poi_highlight_guideline2": {
"message": "Can skip context"
},
"category_poi_highlight_guideline3": {
"message": "Can skip to the title or thumbnail"
},
"category_livestream_messages": {
"message": "Livestream: Donation/Message Readings"
},
@ -867,7 +945,10 @@
"LearnMore": {
"message": "Learn More"
},
"CopyDownvoteButtonInfo": {
"FullDetails": {
"message": "Full Details"
},
"CopyDownvoteButtonInfo": {
"message": "Downvotes and creates a local copy for you to resubmit"
},
"OpenCategoryWikiPage": {
@ -947,5 +1028,11 @@
},
"openOptionsPage": {
"message": "Open options page"
},
"resetToDefault": {
"message": "Reset settings to default"
},
"confirmResetToDefault": {
"message": "Are you sure you want to reset all settings to their default values? This cannot be undone."
}
}

View file

@ -164,6 +164,9 @@
"copyPublicID": {
"message": "Copiar ID Pública de Usuário"
},
"copySegmentID": {
"message": "Copiar ID do segmento"
},
"discordAdvert": {
"message": "Junte-se ao servidor do discord oficial para dar dicas e sugestões!"
},
@ -879,6 +882,15 @@
"ChangeCategoryTooltip": {
"message": "Isto irá aplicar instantaneamente seus segmentos"
},
"downvote": {
"message": "Voto negativo"
},
"upvote": {
"message": "Voto positivo"
},
"hideSegment": {
"message": "Ocultar segmento"
},
"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."
},

View file

@ -186,7 +186,7 @@
"message": "Detta döljer knapparna på YouTube-spelaren som du kan skicka in segment med som ska hoppas över."
},
"showSkipButton": {
"message": "Behåll knappen hoppa till markerat på spelaren"
"message": "Behåll knappen hoppa till höjdpunkt på spelaren"
},
"showInfoButton": {
"message": "Visa Infoknapp På YouTube-spelaren"
@ -613,7 +613,7 @@
"message": "Icke-musik"
},
"category_poi_highlight": {
"message": "Markera"
"message": "Höjdpunkt"
},
"category_poi_highlight_description": {
"message": "Den del av videon som de flesta letar efter. Liknande kommentarer \"Video börjar på x\"."

View file

@ -17,6 +17,10 @@
transition: transform .1s cubic-bezier(0,0,0.2,1);
}
.ytm-progress-bar > #previewbar {
height: 3px;
}
#previewbar.hovered {
transform: scaleY(1)
}
@ -152,9 +156,6 @@
}
.sponsorSkipNoticeParent, .sponsorSkipNotice {
min-width: 350px;
max-width: 50%;
border-spacing: 5px 10px;
padding-left: 5px;
padding-right: 5px;
@ -162,6 +163,15 @@
border-collapse: unset;
}
.sponsorSkipNoticeParent {
min-width: 350px;
max-width: 50%;
}
.sponsorSkipNotice {
width: 100%;
}
.sponsorSkipNoticeTableContainer {
background-color: rgba(28, 28, 28, 0.9);
border-radius: 5px;
@ -173,7 +183,7 @@
}
.sponsorSkipNoticeLimitWidth {
min-width: calc(100% - 50px);
max-width: calc(100% - 50px);
}
.sponsorSkipNotice .hidden {
@ -343,6 +353,22 @@
color: rgb(235, 235, 235);
}
.sb-guidelines-notice .sponsorTimesInfoMessage td {
padding-left: 5px;
padding-top: 2px;
padding-bottom: 2px;
font-size: 15px;
display: flex;
align-items: center;
}
.sponsorTimesInfoIcon {
width: 30px;
padding-right: 10px;
padding-left: 10px;
}
.segmentSummary {
outline: none !important;
}
@ -649,4 +675,6 @@ input::-webkit-inner-spin-button {
#sponsorBlockDurationAfterSkips.ytm-time-display {
padding-left: 4px;
margin: 0px;
color: #fff;
opacity: .7;
}

37
public/icons/bolt.svg Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="bolt.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M19.95 42 22 27.9H14.7Q14.15 27.9 13.9 27.4Q13.65 26.9 13.9 26.45L26.15 6H28.2L26.15 20.05H33.35Q33.9 20.05 34.175 20.55Q34.45 21.05 34.2 21.5L22 42Z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

37
public/icons/campaign.svg Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="campaign.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M36.5 25.5V22.5H44V25.5ZM39 40 32.95 35.5 34.75 33.1 40.8 37.6ZM34.9 14.85 33.1 12.45 39 8 40.8 10.4ZM10.5 38V30H7Q5.75 30 4.875 29.125Q4 28.25 4 27V21Q4 19.75 4.875 18.875Q5.75 18 7 18H16L26 12V36L16 30H13.5V38ZM28 30.7V17.3Q29.35 18.5 30.175 20.225Q31 21.95 31 24Q31 26.05 30.175 27.775Q29.35 29.5 28 30.7Z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
version="1.1"
id="svg4"
sodipodi:docname="check-smaller.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="26.25"
inkscape:cx="12.038095"
inkscape:cy="12"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
fill="#ffffff"
d="M 17.69347,4.9833775 9.9421192,12.940517 6.3065298,9.5107153 3.7684768,12.048769 9.9421192,18.016623 20.231523,7.5214304 Z"
id="path2"
style="stroke-width:0.68596" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="close-smaller.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-width="1366"
inkscape:window-height="731"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M12.45 37.65 10.35 35.55 21.9 24 10.35 12.45 12.45 10.35 24 21.9 35.55 10.35 37.65 12.45 26.1 24 37.65 35.55 35.55 37.65 24 26.1Z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="lightbulb.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M24 44Q22.3 44 21.125 42.825Q19.95 41.65 19.95 39.95H28.05Q28.05 41.65 26.875 42.825Q25.7 44 24 44ZM15.9 36.85V33.85H32.1V36.85ZM16.15 30.8Q12.85 28.65 10.925 25.425Q9 22.2 9 18.15Q9 12.05 13.45 7.6Q17.9 3.15 24 3.15Q30.1 3.15 34.55 7.6Q39 12.05 39 18.15Q39 22.2 37.1 25.425Q35.2 28.65 31.85 30.8Z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

37
public/icons/money.svg Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="money.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="6.5625"
inkscape:cx="37.942857"
inkscape:cy="29.714286"
inkscape:window-width="1366"
inkscape:window-height="731"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M 22.070204,47.757552 V 42.214123 Q 18.308592,41.554191 15.89984,39.343419 13.491088,37.132647 12.435197,33.766994 l 3.695619,-1.517844 q 1.121884,3.167674 3.233667,4.718514 2.111782,1.55084 5.081476,1.55084 3.167674,0 5.213463,-1.583837 2.045789,-1.583837 2.045789,-4.355551 0,-2.903701 -1.814813,-4.487538 -1.814813,-1.583836 -6.830296,-3.233666 -4.75151,-1.517844 -7.094269,-4.025586 -2.342759,-2.507741 -2.342759,-6.269354 0,-3.629626 2.342759,-6.0713741 2.342759,-2.4417484 6.104371,-2.7717144 V 0.24244792 h 3.959592 V 5.7198835 q 2.969694,0.329966 5.114473,1.9467994 2.144779,1.6168335 3.266663,4.1245751 l -3.695619,1.583837 q -0.923905,-2.111783 -2.474745,-3.068684 -1.55084,-0.9569014 -4.058582,-0.9569014 -3.035687,0 -4.817503,1.3858574 -1.781817,1.385857 -1.781817,3.761612 0,2.507742 1.979796,4.058582 1.979796,1.55084 7.325246,3.20067 4.487537,1.385857 6.632316,3.992589 2.144779,2.606731 2.144779,6.566323 0,4.157572 -2.441748,6.69831 -2.441749,2.540738 -7.193259,3.266663 v 5.477436 z"
id="path2"
style="fill:#ffffff;stroke-width:1.31986" />
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="music-note.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M19.65 42Q16.5 42 14.325 39.825Q12.15 37.65 12.15 34.5Q12.15 31.35 14.325 29.175Q16.5 27 19.65 27Q21.05 27 22.175 27.4Q23.3 27.8 24.15 28.5V6H35.85V12.75H27.15V34.5Q27.15 37.65 24.975 39.825Q22.8 42 19.65 42Z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="right-arrow.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24.07619"
inkscape:cy="24"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M 17.039265,39.62264 14.838095,37.382164 28.320259,23.9 14.838095,10.417836 17.039265,8.1773601 32.761905,23.9 Z"
id="path2"
style="fill:#ffffff;stroke-width:0.786132" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/icons/segway.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

37
public/icons/star.svg Normal file
View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="star.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M11.65 44 16.3 28.8 4 20H19.2L24 4L28.8 20H44L31.7 28.8L36.35 44L24 34.6Z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="48"
width="48"
version="1.1"
id="svg4"
sodipodi:docname="stopwatch.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="13.125"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-width="1920"
inkscape:window-height="983"
inkscape:window-x="482"
inkscape:window-y="768"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="M14.45 34Q16.3 35.95 18.8 36.975Q21.3 38 24 38Q29.85 38 33.925 33.925Q38 29.85 38 24Q38 18.15 33.925 14.075Q29.85 10 24 10V24ZM24 44Q19.75 44 16.1 42.475Q12.45 40.95 9.75 38.25Q7.05 35.55 5.525 31.9Q4 28.25 4 24Q4 19.8 5.525 16.15Q7.05 12.5 9.75 9.8Q12.45 7.1 16.1 5.55Q19.75 4 24 4Q28.2 4 31.85 5.55Q35.5 7.1 38.2 9.8Q40.9 12.5 42.45 16.15Q44 19.8 44 24Q44 28.25 42.45 31.9Q40.9 35.55 38.2 38.25Q35.5 40.95 31.85 42.475Q28.2 44 24 44Z"
id="path2"
style="fill:#ffffff" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -175,6 +175,18 @@
<option value="4">__MSG_noticeVisibilityMode4__</option>
</select>
</div>
<div data-type="toggle" data-sync="showCategoryGuidelines">
<div class="switch-container">
<label class="switch">
<input id="showCategoryGuidelines" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="showCategoryGuidelines">
__MSG_showCategoryGuidelines__
</label>
</div>
</div>
<div data-type="toggle" data-toggle-type="reverse" data-sync="hideVideoPlayerControls">
<div class="switch-container">
@ -368,6 +380,12 @@
</div>
</div>
<div data-type="button-press" data-sync="resetToDefault" data-confirm-message="confirmResetToDefault">
<div class="option-button trigger-button">
__MSG_resetToDefault__
</div>
</div>
</div>
<div id="advanced" class="option-group hidden">

View file

@ -8,9 +8,13 @@
<link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet">
</head>
<body id="sponsorBlockPopupBody">
<body id="sponsorBlockPopupBody" style="visibility: hidden">
<div id="sponsorblockPopup" class="sponsorBlockPageBody sb-preload">
<button id="sbCloseButton" title="__MSG_closePopup__" class="sbCloseButton hidden">
<img src="icons/close.png" width="15" height="15">
</button>
<div id="sbBetaServerWarning" class="hidden" title="__MSG_openOptionsPage__">
__MSG_betaServerWarning__
</div>

View file

@ -23,14 +23,37 @@ if (utils.isFirefox()) {
});
}
chrome.tabs.onUpdated.addListener(function(tabId) {
chrome.tabs.sendMessage(tabId, {
function onTabUpdatedListener(tabId: number) {
chrome.tabs.sendMessage(tabId, {
message: 'update',
}, () => void chrome.runtime.lastError ); // Suppress error on Firefox
}, () => void chrome.runtime.lastError ); // Suppress error on Firefox
}
function onNavigationApiAvailableChange(changes: {[key: string]: chrome.storage.StorageChange}) {
if (changes.navigationApiAvailable) {
if (changes.navigationApiAvailable.newValue) {
chrome.tabs.onUpdated.removeListener(onTabUpdatedListener);
} else {
chrome.tabs.onUpdated.addListener(onTabUpdatedListener);
}
}
}
// If Navigation API is not supported, then background has to inform content script about video change.
// This happens on Safari, Firefox, and Chromium 101 (inclusive) and below.
chrome.tabs.onUpdated.addListener(onTabUpdatedListener);
utils.wait(() => Config.local !== null).then(() => {
if (Config.local.navigationApiAvailable) {
chrome.tabs.onUpdated.removeListener(onTabUpdatedListener);
}
});
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
switch(request.message) {
if (!Config.configSyncListeners.includes(onNavigationApiAvailableChange)) {
Config.configSyncListeners.push(onNavigationApiAvailableChange);
}
chrome.runtime.onMessage.addListener(function (request, _, callback) {
switch(request.message) {
case "openConfig":
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
return;
@ -61,6 +84,19 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
case "unregisterContentScript":
unregisterFirefoxContentScript(request.id)
return false;
case "tabs":
chrome.tabs.query({
active: true,
currentWindow: true
}, tabs => {
chrome.tabs.sendMessage(
tabs[0].id,
request.data,
(response) => callback(response)
);
}
);
return true;
}
});

View file

@ -18,6 +18,7 @@ export interface CategorySkipOptionsState {
}
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
setBarColorTimeout: NodeJS.Timeout;
constructor(props: CategorySkipOptionsProps) {
super(props);
@ -172,6 +173,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
}
setColorState(event: React.FormEvent<HTMLInputElement>, preview: boolean): void {
clearTimeout(this.setBarColorTimeout);
if (preview) {
this.setState({
previewColor: event.currentTarget.value
@ -188,7 +191,9 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
}
// Make listener get called
Config.config.barTypes = Config.config.barTypes;
this.setBarColorTimeout = setTimeout(() => {
Config.config.barTypes = Config.config.barTypes;
}, 50);
}
}

View file

@ -24,6 +24,7 @@ export interface NoticeProps {
smaller?: boolean,
limitWidth?: boolean,
extraClass?: string,
// Callback for when this is closed
closeListener: () => void,
@ -35,8 +36,6 @@ export interface NoticeProps {
}
export interface NoticeState {
noticeTitle: string,
maxCountdownTime: () => number,
countdownTime: number,
@ -54,9 +53,13 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
amountOfPreviousNotices: number;
parentRef: React.RefObject<HTMLDivElement>;
constructor(props: NoticeProps) {
super(props);
this.parentRef = React.createRef();
const maxCountdownTime = () => {
if (this.props.maxCountdownTime) return this.props.maxCountdownTime();
else return Config.config.skipNoticeDuration;
@ -71,8 +74,6 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
// Setup state
this.state = {
noticeTitle: props.noticeTitle,
maxCountdownTime,
//the countdown until this notice closes
@ -97,9 +98,11 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
return (
<div id={"sponsorSkipNotice" + this.idSuffix}
className={"sponsorSkipObject sponsorSkipNoticeParent"
+ (this.props.showInSecondSlot ? " secondSkipNotice" : "")}
+ (this.props.showInSecondSlot ? " secondSkipNotice" : "")
+ (this.props.extraClass ? ` ${this.props.extraClass}` : "")}
onMouseEnter={(e) => this.onMouseEnter(e) }
onMouseLeave={() => this.timerMouseLeave()}
ref={this.parentRef}
style={noticeStyle} >
<div className={"sponsorSkipNoticeTableContainer"
+ (this.props.fadeIn ? " sponsorSkipNoticeFadeIn" : "")
@ -123,7 +126,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
style={{float: "left"}}
className="sponsorSkipMessage sponsorSkipObject">
{this.state.noticeTitle}
{this.props.noticeTitle}
</span>
{this.props.firstColumn}
@ -344,12 +347,6 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
if (!silent) this.props.closeListener();
}
changeNoticeTitle(title: string): void {
this.setState({
noticeTitle: title
});
}
addNoticeInfoMessage(message: string, message2 = ""): void {
//TODO: Replace
@ -384,6 +381,10 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
document.querySelector("#sponsorSkipNotice" + this.idSuffix + " > tbody").insertBefore(thanksForVotingText2, document.getElementById("sponsorSkipNoticeSpacer" + this.idSuffix));
}
}
getElement(): React.RefObject<HTMLDivElement> {
return this.parentRef;
}
}
export default NoticeComponent;

View file

@ -1,6 +1,7 @@
import * as React from "react";
export interface NoticeTextSelectionProps {
icon?: string,
text: string,
idSuffix: string,
onClick?: (event: React.MouseEvent) => unknown
@ -24,12 +25,21 @@ class NoticeTextSelectionComponent extends React.Component<NoticeTextSelectionPr
}
return (
<p id={"sponsorTimesInfoMessage" + this.props.idSuffix}
<tr id={"sponsorTimesInfoMessage" + this.props.idSuffix}
onClick={this.props.onClick}
style={style}
className="sponsorTimesInfoMessage">
{this.props.text}
</p>
<td>
{this.props.icon ?
<img src={chrome.runtime.getURL(this.props.icon)} className="sponsorTimesInfoIcon" />
: null}
<span>
{this.props.text}
</span>
</td>
</tr>
);
}
}

View file

@ -1,7 +1,7 @@
import * as React from "react";
import * as CompileConfig from "../../config.json";
import Config from "../config"
import { Category, ContentContainer, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
import { Category, ContentContainer, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import Utils from "../utils";
@ -72,7 +72,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
amountOfPreviousNotices: number;
showInSecondSlot: boolean;
idSuffix: string;
noticeRef: React.MutableRefObject<NoticeComponent>;
@ -105,7 +105,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
if (this.segments.length > 1) {
this.segments.sort((a, b) => a.segment[0] - b.segment[0]);
}
// This is the suffix added at the end of every id
for (const segment of this.segments) {
this.idSuffix += segment.UUID;
@ -168,7 +168,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
noticeStyle.transform = "scale(0.8) translate(10%, 10%)";
}
// If it started out as smaller, always keep the
// If it started out as smaller, always keep the
// skip button there
const showFirstSkipButton = this.props.smaller || this.segments[0].actionType === ActionType.Mute;
const firstColumn = showFirstSkipButton ? (
@ -181,7 +181,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
showInSecondSlot={this.showInSecondSlot}
idSuffix={this.idSuffix}
fadeIn={true}
startFaded={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAll
startFaded={Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAll
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAutoSkip && this.autoSkip)}
timed={true}
maxCountdownTime={this.state.maxCountdownTime}
@ -205,7 +205,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
key={0}>
{/* Vote Button Container */}
{!this.state.thanksForVotingText ?
{!this.state.thanksForVotingText ?
<td id={"sponsorTimesVoteButtonsContainer" + this.idSuffix}
className="sponsorTimesVoteButtonsContainer">
@ -268,7 +268,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
? this.getSkipButton(1) : null}
{/* Never show button */}
{!this.autoSkip || this.props.startReskip ? "" :
{!this.autoSkip || this.props.startReskip ? "" :
<td className="sponsorSkipNoticeRightSection"
key={1}>
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
@ -343,7 +343,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
}
getSkipButton(buttonIndex: number): JSX.Element {
if (this.state.showSkipButton[buttonIndex] && (this.segments.length > 1
if (this.state.showSkipButton[buttonIndex] && (this.segments.length > 1
|| this.segments[0].actionType !== ActionType.Poi
|| this.props.unskipTime)) {
@ -365,8 +365,8 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
className="sponsorSkipObject sponsorSkipNoticeButton"
style={style}
onClick={() => this.prepAction(buttonIndex === 1 ? SkipNoticeAction.Unskip1 : SkipNoticeAction.Unskip0)}>
{this.getSkipButtonText(buttonIndex, forceSeek ? ActionType.Skip : null)
+ (!forceSeek && this.state.showKeybindHint
{this.getSkipButtonText(buttonIndex, forceSeek ? ActionType.Skip : null)
+ (!forceSeek && this.state.showKeybindHint
? " (" + keybindToString(Config.config.skipKeybind) + ")" : "")}
</button>
</span>
@ -379,7 +379,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
for (let i = 0; i < this.segments.length; i++) {
elements.push(
<button className="sponsorSkipObject sponsorSkipNoticeButton"
style={{opacity: this.getSubmissionChooserOpacity(i),
style={{opacity: this.getSubmissionChooserOpacity(i),
color: this.getSubmissionChooserColor(i)}}
onClick={() => this.performAction(i)}
key={"submission" + i + this.segments[i].category + this.idSuffix}>
@ -404,7 +404,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
getSubmissionChooserColor(index: number): string {
const isDownvote = this.state.actionState == SkipNoticeAction.Downvote;
const isCopyDownvote = this.state.actionState == SkipNoticeAction.CopyDownvote;
const shouldWarnUser = Config.config.isVip && (isDownvote || isCopyDownvote)
const shouldWarnUser = Config.config.isVip && (isDownvote || isCopyDownvote)
&& this.segments[index].locked === 1;
return shouldWarnUser ? this.lockedColor : this.unselectedColor;
@ -480,8 +480,8 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
/**
* Performs the action from the current state
*
* @param index
*
* @param index
*/
performAction(index: number, action?: SkipNoticeAction): void {
switch (action ?? this.state.actionState) {
@ -617,11 +617,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
countdownTime: Config.config.skipNoticeDuration
};
// See if the title should be changed
if (!this.autoSkip) {
newState.noticeTitle = chrome.i18n.getMessage("noticeTitle");
}
//reset countdown
this.setState(newState, () => {
this.noticeRef.current.resetCountdown();
@ -723,7 +718,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
messages
});
}
addVoteButtonInfo(message: string): void {
this.setState({
thanksForVotingText: message
@ -786,7 +781,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
case ActionType.Mute: {
return chrome.i18n.getMessage("unmute");
}
case ActionType.Skip:
case ActionType.Skip:
default: {
return chrome.i18n.getMessage("unskip");
}
@ -799,7 +794,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
case ActionType.Mute: {
return chrome.i18n.getMessage("mute");
}
case ActionType.Skip:
case ActionType.Skip:
default: {
return chrome.i18n.getMessage("reskip");
}
@ -812,7 +807,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
case ActionType.Mute: {
return chrome.i18n.getMessage("mute");
}
case ActionType.Skip:
case ActionType.Skip:
default: {
return chrome.i18n.getMessage("skip");
}

View file

@ -18,6 +18,7 @@ export interface SponsorTimeEditProps {
submissionNotice: SubmissionNoticeComponent;
categoryList?: Category[];
categoryChangeListener?: (index: number, category: Category) => void;
}
export interface SponsorTimeEditState {
@ -365,9 +366,10 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
categorySelectionChange(event: React.ChangeEvent<HTMLSelectElement>): void {
const chosenCategory = event.target.value as Category;
// See if show more categories was pressed
if (event.target.value !== DEFAULT_CATEGORY && !Config.config.categorySelections.some((category) => category.name === event.target.value)) {
const chosenCategory = event.target.value;
event.target.value = DEFAULT_CATEGORY;
// Alert that they have to enable this category first
@ -381,8 +383,12 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
this.handleReplacingLostTimes(event.target.value as Category, sponsorTime.actionType, sponsorTime);
this.handleReplacingLostTimes(chosenCategory, sponsorTime.actionType, sponsorTime);
this.saveEditTimes();
if (this.props.categoryChangeListener) {
this.props.categoryChangeListener(this.props.index, chosenCategory);
}
}
actionTypeSelectionChange(event: React.ChangeEvent<HTMLSelectElement>): void {

View file

@ -1,10 +1,13 @@
import * as React from "react";
import Config from "../config"
import { ContentContainer } from "../types";
import GenericNotice from "../render/GenericNotice";
import { Category, ContentContainer } from "../types";
import * as CompileConfig from "../../config.json";
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import SponsorTimeEditComponent from "./SponsorTimeEditComponent";
import { getGuidelineInfo } from "../utils/constants";
export interface SubmissionNoticeProps {
// Contains functions and variables from the content script needed by the skip notice
@ -32,6 +35,8 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
videoObserver: MutationObserver;
guidelinesReminder: GenericNotice;
constructor(props: SubmissionNoticeProps) {
super(props);
this.noticeRef = React.createRef();
@ -128,6 +133,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
index={i}
contentContainer={this.props.contentContainer}
submissionNotice={this}
categoryChangeListener={this.categoryChangeListener.bind(this)}
ref={timeRef}>
</SponsorTimeEditComponent>
);
@ -154,6 +160,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
}
cancel(): void {
this.guidelinesReminder?.close();
this.noticeRef.current.close(true);
this.contentContainer().resetSponsorSubmissionNotice();
@ -190,6 +197,45 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
this.cancel();
}
categoryChangeListener(index: number, category: Category): void {
const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth;
if (category !== "chooseACategory" && Config.config.showCategoryGuidelines
&& this.contentContainer().v.offsetWidth > dialogWidth * 2) {
const options = {
title: chrome.i18n.getMessage(`category_${category}`),
textBoxes: getGuidelineInfo(category),
buttons: [{
name: chrome.i18n.getMessage("FullDetails"),
listener: () => window.open(CompileConfig.wikiLinks[category])
},
{
name: chrome.i18n.getMessage("Hide"),
listener: () => {
Config.config.showCategoryGuidelines = false;
this.guidelinesReminder?.close();
this.guidelinesReminder = null;
}
}],
timed: false,
style: {
right: `${dialogWidth + 10}px`,
},
extraClass: "sb-guidelines-notice"
};
if (options.textBoxes) {
if (this.guidelinesReminder) {
this.guidelinesReminder.update(options);
} else {
this.guidelinesReminder = new GenericNotice(null, "GuidelinesReminder", options);
}
} else {
this.guidelinesReminder?.close();
this.guidelinesReminder = null;
}
}
}
}
export default SubmissionNoticeComponent;

View file

@ -1,6 +1,4 @@
import * as React from "react";
import Config from "../config";
import { Category, SegmentUUID, SponsorTime } from "../types";
export interface TooltipProps {
text: string;

View file

@ -1,6 +1,6 @@
import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
import { keybindEquals } from "./utils/configUtils";
interface SBConfig {
@ -55,6 +55,7 @@ interface SBConfig {
scrollToEditTimeUpdate: boolean,
categoryPillUpdate: boolean,
darkMode: boolean,
showCategoryGuidelines: boolean,
// Used to cache calculated text color info
categoryPillColors: {
@ -101,9 +102,11 @@ export type VideoDownvotes = { segments: { uuid: HashedValue, hidden: SponsorHid
interface SBStorage {
/* VideoID prefixes to UUID prefixes */
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>,
navigationApiAvailable: boolean,
}
export interface SBObject {
configLocalListeners: Array<(changes: StorageChangesObject) => unknown>;
configSyncListeners: Array<(changes: StorageChangesObject) => unknown>;
syncDefaults: SBConfig;
localDefaults: SBStorage;
@ -113,12 +116,14 @@ export interface SBObject {
local: SBStorage;
forceSyncUpdate(prop: string): void;
forceLocalUpdate(prop: string): void;
resetToDefault(): void;
}
const Config: SBObject = {
/**
* Callback function when an option is updated
*/
configLocalListeners: [],
configSyncListeners: [],
syncDefaults: {
userID: null,
@ -166,6 +171,7 @@ const Config: SBObject = {
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
categoryPillUpdate: false,
darkMode: true,
showCategoryGuidelines: true,
categoryPillColors: {},
@ -282,14 +288,16 @@ const Config: SBObject = {
}
},
localDefaults: {
downvotedSegments: {}
downvotedSegments: {},
navigationApiAvailable: null
},
cachedSyncConfig: null,
cachedLocalStorage: null,
config: null,
local: null,
forceSyncUpdate,
forceLocalUpdate
forceLocalUpdate,
resetToDefault
};
// Function setup
@ -300,7 +308,7 @@ function configProxy(): { sync: SBConfig, local: SBStorage } {
for (const key in changes) {
Config.cachedSyncConfig[key] = changes[key].newValue;
}
for (const callback of Config.configSyncListeners) {
callback(changes);
}
@ -308,9 +316,13 @@ function configProxy(): { sync: SBConfig, local: SBStorage } {
for (const key in changes) {
Config.cachedLocalStorage[key] = changes[key].newValue;
}
for (const callback of Config.configLocalListeners) {
callback(changes);
}
}
});
const syncHandler: ProxyHandler<SBConfig> = {
set<K extends keyof SBConfig>(obj: SBConfig, prop: K, value: SBConfig[K]) {
Config.cachedSyncConfig[prop] = value;
@ -327,10 +339,10 @@ function configProxy(): { sync: SBConfig, local: SBStorage } {
return obj[prop] || data;
},
deleteProperty(obj: SBConfig, prop: keyof SBConfig) {
chrome.storage.sync.remove(<string> prop);
return true;
}
@ -352,10 +364,10 @@ function configProxy(): { sync: SBConfig, local: SBStorage } {
return obj[prop] || data;
},
deleteProperty(obj: SBStorage, prop: keyof SBStorage) {
chrome.storage.local.remove(<string> prop);
return true;
}
@ -368,8 +380,20 @@ function configProxy(): { sync: SBConfig, local: SBStorage } {
}
function forceSyncUpdate(prop: string): void {
const value = Config.cachedSyncConfig[prop];
if (prop === "unsubmittedSegments") {
// Early to be safe
if (JSON.stringify(value).length + prop.length > 8000) {
for (const key in value) {
if (!value[key] || value[key].length <= 0) {
delete value[key];
}
}
}
}
chrome.storage.sync.set({
[prop]: Config.cachedSyncConfig[prop]
[prop]: value
});
}
@ -379,7 +403,7 @@ function forceLocalUpdate(prop: string): void {
});
}
async function fetchConfig(): Promise<void> {
async function fetchConfig(): Promise<void> {
await Promise.all([new Promise<void>((resolve) => {
chrome.storage.sync.get(null, function(items) {
Config.cachedSyncConfig = <SBConfig> <unknown> items;
@ -387,7 +411,7 @@ async function fetchConfig(): Promise<void> {
});
}), new Promise<void>((resolve) => {
chrome.storage.local.get(null, function(items) {
Config.cachedLocalStorage = <SBStorage> <unknown> items;
Config.cachedLocalStorage = <SBStorage> <unknown> items;
resolve();
});
})]);
@ -431,9 +455,9 @@ function migrateOldSyncFormats(config: SBConfig) {
if (!config["autoSkipOnMusicVideosUpdate"]) {
config["autoSkipOnMusicVideosUpdate"] = true;
for (const selection of config.categorySelections) {
if (selection.name === "music_offtopic"
if (selection.name === "music_offtopic"
&& selection.option === CategorySkipOption.AutoSkip) {
config.autoSkipOnMusicVideos = true;
break;
}
@ -522,6 +546,16 @@ function addDefaults() {
}
}
function resetToDefault() {
chrome.storage.sync.set({
...Config.syncDefaults,
userID: Config.config.userID,
minutesSaved: Config.config.minutesSaved,
skipCount: Config.config.skipCount,
sponsorTimesContributed: Config.config.sponsorTimesContributed
});
}
// Sync config
setupConfig();

View file

@ -5,8 +5,6 @@ import { ContentContainer, Keybind } from "./types";
import Utils from "./utils";
const utils = new Utils();
import runThePopup from "./popup";
import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
@ -20,6 +18,7 @@ import { isSafari, keybindEquals } from "./utils/configUtils";
import { CategoryPill } from "./render/CategoryPill";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
import { logDebug } from "./utils/logger";
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@ -218,6 +217,9 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
utils.addHiddenSegment(sponsorVideoID, request.UUID, request.type);
updatePreviewBar();
break;
case "closePopup":
closeInfoMenu();
break;
}
}
@ -275,6 +277,7 @@ function resetValues() {
switchingVideos = false;
} else {
switchingVideos = true;
logDebug("Setting switching videos to true (reset data)");
}
firstEvent = true;
@ -292,7 +295,7 @@ function resetValues() {
async function videoIDChange(id) {
//if the id has not changed return unless the video element has changed
if (sponsorVideoID === id && isVisible(video)) return;
if (sponsorVideoID === id && (isVisible(video) || !video)) return;
//set the global videoID
sponsorVideoID = id;
@ -319,9 +322,6 @@ async function videoIDChange(id) {
}
}
// Get new video info
// getVideoInfo(); // Seems to have been replaced
// Update whitelist data when the video data is loaded
whitelistCheck();
@ -391,18 +391,32 @@ function handleMobileControlsMutations(): void {
function createPreviewBar(): void {
if (previewBar !== null) return;
const progressElementSelectors = [
// For mobile YouTube
".progress-bar-background",
// For YouTube
".ytp-progress-bar-container",
".no-model.cue-range-markers",
// For Invidious/VideoJS
".vjs-progress-holder"
const progressElementOptions = [{
// For mobile YouTube
selector: ".progress-bar-background",
isVisibleCheck: true
}, {
// For new mobile YouTube (#1287)
selector: ".ytm-progress-bar",
isVisibleCheck: true
}, {
// For Desktop YouTube
selector: ".ytp-progress-bar-container",
isVisibleCheck: true
}, {
// For Desktop YouTube
selector: ".no-model.cue-range-marker",
isVisibleCheck: true
}, {
// For Invidious/VideoJS
selector: ".vjs-progress-holder",
isVisibleCheck: false
}
];
for (const selector of progressElementSelectors) {
const el = findValidElement(document.querySelectorAll(selector));
for (const option of progressElementOptions) {
const allElements = document.querySelectorAll(option.selector) as NodeListOf<HTMLElement>;
const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0];
if (el) {
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
@ -434,6 +448,8 @@ function videoOnReadyListener(): void {
}
function cancelSponsorSchedule(): void {
logDebug("Pausing skipping");
if (currentSkipSchedule !== null) {
clearTimeout(currentSkipSchedule);
currentSkipSchedule = null;
@ -456,20 +472,16 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
// Reset lastCheckVideoTime
lastCheckVideoTime = -1;
lastCheckTime = 0;
logDebug("[SB] Ad playing, pausing skipping");
return;
}
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
if (!video || video.paused) return;
if (currentTime === undefined || currentTime === null) {
const virtualTime = lastTimeFromWaitingEvent ?? (lastKnownVideoTime.videoTime ?
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6){
currentTime = virtualTime;
} else {
currentTime = video.currentTime;
}
currentTime = getVirtualTime();
}
lastTimeFromWaitingEvent = null;
@ -491,6 +503,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`);
if (skipInfo.index === -1) return;
const currentSkip = skipInfo.array[skipInfo.index];
@ -511,6 +524,8 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
}
}
logDebug(`Next step in starting skipping: ${!shouldSkip(currentSkip)}, ${!sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)}`);
// Don't skip if this category should not be skipped
if (!shouldSkip(currentSkip) && !sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)) return;
@ -520,7 +535,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
let forcedIncludeNonIntersectingSegments = true;
if (incorrectVideoCheck(videoID, currentSkip)) return;
forceVideoTime ||= video.currentTime;
forceVideoTime ||= Math.max(video.currentTime, getVirtualTime());
if (forceVideoTime >= skipTime[0] && forceVideoTime < skipTime[1]) {
skipToTime({
@ -548,9 +563,11 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
} else {
const delayTime = timeUntilSponsor * 1000 * (1 / video.playbackRate);
if (delayTime < 300) {
// For Firefox, use interval instead of timeout near the end to combat imprecise video time
// Use interval instead of timeout near the end to combat imprecise video time
const startIntervalTime = performance.now();
const startVideoTime = Math.max(currentTime, video.currentTime);
logDebug(`Starting setInterval skipping ${video.currentTime} to skip at ${skipTime[0]}`);
currentSkipInterval = setInterval(() => {
const intervalDuration = performance.now() - startIntervalTime;
if (intervalDuration >= delayTime || video.currentTime >= skipTime[0]) {
@ -565,12 +582,26 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
}
}, 1);
} else {
logDebug(`Starting timeout to skip ${video.currentTime} to skip at ${skipTime[0]}`);
// Schedule for right before to be more precise than normal timeout
currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - 100));
currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - 150));
}
}
}
function getVirtualTime(): number {
const virtualTime = lastTimeFromWaitingEvent ?? (lastKnownVideoTime.videoTime ?
(performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null);
if ((lastTimeFromWaitingEvent || !utils.isFirefox())
&& !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6) {
return virtualTime;
} else {
return video.currentTime;
}
}
function inMuteSegment(currentTime: number): boolean {
const checkFunction = (segment) => segment.actionType === ActionType.Mute && segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction);
@ -641,6 +672,8 @@ function setupVideoListeners() {
if (!Config.config.disableSkipping) {
switchingVideos = false;
let startedWaiting = false;
video.addEventListener('play', () => {
// If it is not the first event, then the only way to get to 0 is if there is a seek event
// This check makes sure that changing the video resolution doesn't cause the extension to think it
@ -652,6 +685,8 @@ function setupVideoListeners() {
if (switchingVideos) {
switchingVideos = false;
logDebug("Setting switching videos to false");
// If already segments loaded before video, retry to skip starting segments
if (sponsorTimes) startSkipScheduleCheckingForStartSponsors();
}
@ -671,6 +706,12 @@ function setupVideoListeners() {
});
video.addEventListener('playing', () => {
updateVirtualTime();
if (startedWaiting) {
startedWaiting = false;
logDebug(`[SB] Playing event after buffering: ${Math.abs(lastCheckVideoTime - video.currentTime) > 0.3
|| (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)}`);
}
// Make sure it doesn't get double called with the play event
if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3
@ -709,8 +750,13 @@ function setupVideoListeners() {
cancelSponsorSchedule();
};
video.addEventListener('pause', paused);
video.addEventListener('waiting', paused);
video.addEventListener('pause', () => paused());
video.addEventListener('waiting', () => {
logDebug("[SB] Not skipping due to buffering");
startedWaiting = true;
paused();
});
startSponsorSchedule();
}
@ -899,12 +945,10 @@ function startSkipScheduleCheckingForStartSponsors() {
// See if there are any starting sponsors
let startingSegmentTime = getStartTimeFromUrl(document.URL) || -1;
let found = false;
let startingSegment: SponsorTime = null;
for (const time of sponsorTimes) {
if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime
&& time.actionType !== ActionType.Poi) {
startingSegmentTime = time.segment[0];
startingSegment = time;
found = true;
break;
}
@ -914,7 +958,6 @@ function startSkipScheduleCheckingForStartSponsors() {
if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime
&& time.actionType !== ActionType.Poi) {
startingSegmentTime = time.segment[0];
startingSegment = time;
found = true;
break;
}
@ -952,26 +995,6 @@ function startSkipScheduleCheckingForStartSponsors() {
}
}
/**
* Get the video info for the current tab from YouTube
*
* TODO: Replace
*/
async function getVideoInfo(): Promise<void> {
const result = await utils.asyncRequestToCustomServer("GET", "https://www.youtube.com/get_video_info?video_id=" + sponsorVideoID + "&html5=1&c=TVHTML5&cver=7.20190319");
if (result.ok) {
const decodedData = decodeURIComponent(result.responseText).match(/player_response=([^&]*)/)[1];
if (!decodedData) {
console.error("[SB] Failed at getting video info from YouTube.");
console.error("[SB] Data returned from YouTube: " + result.responseText);
return;
}
videoInfo = JSON.parse(decodedData);
}
}
function getYouTubeVideoID(document: Document): string | boolean {
const url = document.URL;
// clips should never skip, going from clip to full video has no indications.
@ -1106,7 +1129,7 @@ async function whitelistCheck() {
?? document.querySelector("a.ytp-title-channel-logo") // YouTube Embed
?? document.querySelector(".channel-profile #channel-name")?.parentElement.parentElement // Invidious
?? document.querySelector("a.slim-owner-icon-and-title")) // Mobile YouTube
?.getAttribute("href")?.match(/\/channel\/(UC[a-zA-Z0-9_-]{22})/)[1];
?.getAttribute("href")?.match(/\/(?:channel|c|user)\/(UC[a-zA-Z0-9_-]{22}|[a-zA-Z0-9_-]+)/)?.[1];
try {
await utils.wait(() => !!getChannelID(), 6000, 20);
@ -1114,14 +1137,12 @@ async function whitelistCheck() {
channelIDInfo = {
status: ChannelIDStatus.Found,
id: getChannelID().match(/^\/?([^\s/]+)/)[0]
}
};
} catch (e) {
channelIDInfo = {
status: ChannelIDStatus.Failed,
id: null
}
return;
};
}
//see if this is a whitelisted channel
@ -1666,71 +1687,30 @@ function openInfoMenu() {
//hide info button
if (playerButtons.info) playerButtons.info.button.style.display = "none";
sendRequestToCustomServer('GET', chrome.extension.getURL("popup.html"), function(xmlhttp) {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
const popup = document.createElement("div");
popup.id = "sponsorBlockPopupContainer";
const popup = document.createElement("div");
popup.id = "sponsorBlockPopupContainer";
const frame = document.createElement("iframe");
frame.width = "374";
frame.height = "500";
frame.addEventListener("load", () => frame.contentWindow.postMessage("", "*"));
frame.src = chrome.extension.getURL("popup.html");
popup.appendChild(frame);
let htmlData = xmlhttp.responseText;
// Hack to replace head data (title, favicon)
htmlData = htmlData.replace(/<head>[\S\s]*<\/head>/gi, "");
// Hack to replace body and html tag with div
htmlData = htmlData.replace(/<body/gi, "<div");
htmlData = htmlData.replace(/<\/body/gi, "</div");
htmlData = htmlData.replace(/<html/gi, "<div");
htmlData = htmlData.replace(/<\/html/gi, "</div");
popup.innerHTML = htmlData;
//close button
const closeButton = document.createElement("button");
const closeButtonIcon = document.createElement("img");
closeButtonIcon.src = chrome.extension.getURL("icons/close.png");
closeButtonIcon.width = 15;
closeButtonIcon.height = 15;
closeButton.appendChild(closeButtonIcon);
closeButton.setAttribute("title", chrome.i18n.getMessage("closePopup"));
closeButton.classList.add("sbCloseButton");
closeButton.addEventListener("click", closeInfoMenu);
//add the close button
popup.prepend(closeButton);
const parentNodes = document.querySelectorAll("#secondary");
let parentNode = null;
for (let i = 0; i < parentNodes.length; i++) {
if (parentNodes[i].firstElementChild !== null) {
parentNode = parentNodes[i];
}
}
if (parentNode == null) {
//old youtube theme
parentNode = document.getElementById("watch7-sidebar-contents");
}
//make the logo source not 404
//query selector must be used since getElementByID doesn't work on a node and this isn't added to the document yet
const logo = <HTMLImageElement> popup.querySelector("#sponsorBlockPopupLogo");
const edit = <HTMLImageElement> popup.querySelector("#sbPopupIconEdit");
const copy = <HTMLImageElement> popup.querySelector("#sbPopupIconCopyUserID");
const check = <HTMLImageElement> popup.querySelector("#sbPopupIconCheck");
const refreshSegments = <HTMLImageElement> popup.querySelector("#refreshSegments");
const heart = <HTMLImageElement> popup.querySelector(".sbHeart");
const close = <HTMLImageElement> popup.querySelector("#sbCloseDonate");
logo.src = chrome.extension.getURL("icons/IconSponsorBlocker256px.png");
edit.src = chrome.extension.getURL("icons/pencil.svg");
copy.src = chrome.extension.getURL("icons/clipboard.svg");
check.src = chrome.extension.getURL("icons/check.svg");
heart.src = chrome.extension.getURL("icons/heart.svg");
close.src = chrome.extension.getURL("icons/close.png");
refreshSegments.src = chrome.extension.getURL("icons/refresh.svg");
parentNode.insertBefore(popup, parentNode.firstChild);
//run the popup init script
runThePopup(messageListener);
const parentNodes = document.querySelectorAll("#secondary");
let parentNode = null;
for (let i = 0; i < parentNodes.length; i++) {
if (parentNodes[i].firstElementChild !== null) {
parentNode = parentNodes[i];
}
});
}
if (parentNode == null) {
//old youtube theme
parentNode = document.getElementById("watch7-sidebar-contents");
}
parentNode.insertBefore(popup, parentNode.firstChild);
}
function closeInfoMenu() {
@ -1909,8 +1889,6 @@ async function sendSubmitMessage() {
return;
}
sponsorsLookup();
// Add loading animation
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker.svg");
const stopAnimation = AnimationUtils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer());
@ -2093,25 +2071,6 @@ function addCSS() {
}
}
function sendRequestToCustomServer(type, fullAddress, callback) {
const xmlhttp = new XMLHttpRequest();
xmlhttp.open(type, fullAddress, true);
if (callback != undefined) {
xmlhttp.onreadystatechange = function () {
callback(xmlhttp, false);
};
xmlhttp.onerror = function() {
callback(xmlhttp, true);
};
}
//submit this request
xmlhttp.send();
}
/**
* Update the isAdPlaying flag and hide preview bar/controls if ad is playing
*/
@ -2184,3 +2143,18 @@ function checkForPreloadedSegment() {
Config.forceSyncUpdate("unsubmittedSegments");
}
}
// Register listener for URL change via Navigation API
const navigationApiAvailable = "navigation" in window;
if (navigationApiAvailable) {
// TODO: Remove type cast once type declarations are updated
(window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", () => videoIDChange(getYouTubeVideoID(document)));
}
// Record availability of Navigation API
utils.wait(() => Config.local !== null).then(() => {
if (Config.local.navigationApiAvailable !== navigationApiAvailable) {
Config.local.navigationApiAvailable = navigationApiAvailable;
Config.forceLocalUpdate("navigationApiAvailable");
}
});

View file

@ -1,15 +1,15 @@
import Config from "./config";
import { showDonationLink } from "./utils/configUtils";
import Utils from "./utils";
const utils = new Utils();
import { localizeHtmlPage } from "./utils/pageUtils";
import { GenericUtils } from "./utils/genericUtils";
window.addEventListener('DOMContentLoaded', init);
async function init() {
utils.localizeHtmlPage();
localizeHtmlPage();
await utils.wait(() => Config.config !== null);
await GenericUtils.wait(() => Config.config !== null);
if (!Config.config.darkMode) {
document.documentElement.setAttribute("data-theme", "light");

View file

@ -144,8 +144,10 @@ class PreviewBar {
this.parent = parent;
if (this.onMobileYouTube) {
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
parent.style.opacity = "1";
if (parent.classList.contains("progress-bar-background")) {
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
parent.style.opacity = "1";
}
this.container.style.transform = "none";
} else if (!this.onInvidious) {

View file

@ -2,10 +2,7 @@ import Config from "../config";
import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils";
import { keybindToString } from "../utils/configUtils";
import Utils from "../utils";
import { AnimationUtils } from "../utils/animationUtils";
const utils = new Utils();
export interface SkipButtonControlBarProps {
skip: (segment: SponsorTime) => void;
@ -53,7 +50,7 @@ export class SkipButtonControlBar {
this.skipIcon.id = "sbSkipIconControlBarImage";
this.textContainer = document.createElement("div");
this.container.appendChild(this.skipIcon);
this.container.appendChild(this.textContainer);
this.container.addEventListener("click", () => this.toggleSkip());
@ -73,7 +70,7 @@ export class SkipButtonControlBar {
attachToPage(): void {
const mountingContainer = this.getMountingContainer();
this.chapterText = document.querySelector(".ytp-chapter-container");
if (mountingContainer && !mountingContainer.contains(this.container)) {
if (this.onMobileYouTube) {
mountingContainer.appendChild(this.container);
@ -151,7 +148,7 @@ export class SkipButtonControlBar {
}
disableText(): void {
if (Config.config.hideVideoPlayerControls || Config.config.hideSkipButtonPlayerControls) {
if (Config.config.hideSkipButtonPlayerControls) {
this.disable();
return;
}
@ -172,10 +169,10 @@ export class SkipButtonControlBar {
const overlay = document.getElementById("player-control-overlay");
if (overlay && this.enabled) {
if (overlay?.classList?.contains("pointer-events-off")) {
this.hideButton();
} else {
if (overlay?.classList?.contains("fadein")) {
this.showButton();
} else {
this.hideButton();
}
}
}
@ -220,4 +217,3 @@ export class SkipButtonControlBar {
this.container.style.left = this.left + "px";
}
}

View file

@ -16,7 +16,8 @@ interface DefaultMessage {
| "getChannelID"
| "isChannelWhitelisted"
| "submitTimes"
| "refreshSegments";
| "refreshSegments"
| "closePopup";
}
interface BoolValueMessage {

View file

@ -12,13 +12,14 @@ import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
import KeybindComponent from "./components/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
const utils = new Utils();
let embed = false;
window.addEventListener('DOMContentLoaded', init);
async function init() {
utils.localizeHtmlPage();
localizeHtmlPage();
// selected tab
if (location.hash != "") {
@ -232,12 +233,22 @@ async function init() {
}
case "button-press": {
const actionButton = optionsElements[i].querySelector(".trigger-button");
const confirmMessage = optionsElements[i].getAttribute("data-confirm-message");
switch(optionsElements[i].getAttribute("data-sync")) {
case "copyDebugInformation":
actionButton.addEventListener("click", copyDebugOutputToClipboard);
break;
}
actionButton.addEventListener("click", () => {
if (confirmMessage !== null && !confirm(chrome.i18n.getMessage(confirmMessage))) {
return;
}
switch (optionsElements[i].getAttribute("data-sync")) {
case "copyDebugInformation":
copyDebugOutputToClipboard();
break;
case "resetToDefault":
Config.resetToDefault();
window.location.reload();
break;
}
});
break;
}

View file

@ -1,5 +1,6 @@
import Config from "./config";
import Utils from "./utils";
import { localizeHtmlPage } from "./utils/pageUtils";
const utils = new Utils();
// This is needed, if Config is not imported before Utils, things break.
@ -9,7 +10,7 @@ Config.config;
window.addEventListener('DOMContentLoaded', init);
async function init() {
utils.localizeHtmlPage();
localizeHtmlPage();
const domains = document.location.hash.replace("#", "").split(",");

View file

@ -6,6 +6,7 @@ import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageT
import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
const utils = new Utils();
interface MessageListener {
@ -22,13 +23,15 @@ class MessageHandler {
sendMessage(id: number, request: Message, callback?) {
if (this.messageListener) {
this.messageListener(request, null, callback);
} else {
} else if (chrome.tabs) {
chrome.tabs.sendMessage(id, request, callback);
} else {
chrome.runtime.sendMessage({ message: "tabs", data: request }, callback);
}
}
query(config, callback) {
if (this.messageListener) {
if (this.messageListener || !chrome.tabs) {
// Send back dummy info
callback([{
url: document.URL,
@ -41,15 +44,17 @@ class MessageHandler {
}
}
// To prevent clickjacking
let allowPopup = window === window.top;
window.addEventListener("message", async (e) => {
if (e.source !== window.parent) return;
if (e.origin.endsWith('.youtube.com')) return allowPopup = true;
});
//make this a function to allow this to run on the content page
async function runThePopup(messageListener?: MessageListener): Promise<void> {
const messageHandler = new MessageHandler(messageListener);
utils.localizeHtmlPage();
await utils.wait(() => Config.config !== null);
localizeHtmlPage();
type InputPageElements = {
whitelistToggle?: HTMLInputElement,
@ -58,6 +63,15 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
};
type PageElements = { [key: string]: HTMLElement } & InputPageElements
/** If true, the content script is in the process of creating a new segment. */
let creatingSegment = false;
//the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = [];
//current video ID of this tab
let currentVideoID = null;
const PageElements: PageElements = {};
[
@ -112,9 +126,24 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
"sponsorTimesDonateContainer",
"sbConsiderDonateLink",
"sbCloseDonate",
"sbBetaServerWarning"
"sbBetaServerWarning",
"sbCloseButton"
].forEach(id => PageElements[id] = document.getElementById(id));
getSegmentsFromContentScript(false);
await utils.wait(() => Config.config !== null && allowPopup, 5000, 5);
document.querySelector("body").style.removeProperty("visibility");
PageElements.sbCloseButton.addEventListener("click", () => {
sendTabMessage({
message: "closePopup"
});
});
if (window !== window.top) {
PageElements.sbCloseButton.classList.remove("hidden");
}
// Hide donate button if wanted (Safari, or user choice)
if (!showDonationLink()) {
PageElements.sbDonate.style.display = "none";
@ -151,15 +180,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.refreshSegmentsButton.addEventListener("click", refreshSegments);
PageElements.sbPopupIconCopyUserID.addEventListener("click", async () => navigator.clipboard.writeText(await utils.getHash(Config.config.userID)));
/** If true, the content script is in the process of creating a new segment. */
let creatingSegment = false;
//the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = [];
//current video ID of this tab
let currentVideoID = null;
//show proper disable skipping button
const disableSkipping = Config.config.disableSkipping;
if (disableSkipping != undefined && disableSkipping) {
@ -239,8 +259,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
// Must be delayed so it only happens once loaded
setTimeout(() => PageElements.sponsorblockPopup.classList.remove("preload"), 250);
getSegmentsFromContentScript(false);
function showDonateWidget(viewCount: number) {
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) {
@ -272,13 +290,14 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
});
}
function loadTabData(tabs, updating: boolean): void {
async function loadTabData(tabs, updating: boolean): Promise<void> {
if (!currentVideoID) {
//this isn't a YouTube video then
displayNoVideo();
return;
}
await utils.wait(() => Config.config !== null, 5000, 10);
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
updateSegmentEditingUI();
@ -588,6 +607,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)
);
}
);
});
}
//make the options username setting option visible
function setUsernameButton() {
PageElements.usernameInput.value = PageElements.usernameValue.innerText;
@ -832,9 +867,4 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//end of function
}
if (chrome.tabs != undefined) {
//this means it is actually opened in the popup
runThePopup();
}
export default runThePopup;
runThePopup();

View file

@ -13,12 +13,19 @@ export interface ButtonListener {
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}
export interface TextBox {
icon: string,
text: string
}
export interface NoticeOptions {
title: string,
textBoxes?: string[],
textBoxes?: TextBox[],
buttons?: ButtonListener[],
fadeIn?: boolean,
timed?: boolean
style?: React.CSSProperties;
extraClass?: string;
}
export default class GenericNotice {
@ -27,9 +34,11 @@ export default class GenericNotice {
noticeElement: HTMLDivElement;
noticeRef: React.MutableRefObject<NoticeComponent>;
idSuffix: string;
constructor(contentContainer: ContentContainer, idSuffix: string, options: NoticeOptions) {
this.noticeRef = React.createRef();
this.idSuffix = idSuffix;
this.contentContainer = contentContainer;
@ -40,39 +49,47 @@ export default class GenericNotice {
referenceNode.prepend(this.noticeElement);
this.update(options);
}
update(options: NoticeOptions): void {
ReactDOM.render(
<NoticeComponent
noticeTitle={options.title}
idSuffix={idSuffix}
idSuffix={this.idSuffix}
fadeIn={options.fadeIn ?? true}
timed={options.timed ?? true}
ref={this.noticeRef}
style={options.style}
extraClass={options.extraClass}
closeListener={() => this.close()} >
{this.getMessageBox(idSuffix, options.textBoxes)}
{this.getMessageBox(this.idSuffix, options.textBoxes)}
<tr id={"sponsorSkipNoticeSpacer" + idSuffix}
<tr id={"sponsorSkipNoticeSpacer" + this.idSuffix}
className="sponsorBlockSpacer">
</tr>
<div className="sponsorSkipNoticeRightSection"
<tr className="sponsorSkipNoticeRightSection"
style={{position: "relative"}}>
{this.getButtons(options.buttons)}
</div>
<td>
{this.getButtons(options.buttons)}
</td>
</tr>
</NoticeComponent>,
this.noticeElement
);
}
getMessageBox(idSuffix: string, textBoxes: string[]): JSX.Element[] {
getMessageBox(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
if (textBoxes) {
const result = [];
for (let i = 0; i < textBoxes.length; i++) {
result.push(
<NoticeTextSelectionComponent idSuffix={idSuffix}
key={i}
text={textBoxes[i]} />
icon={textBoxes[i].icon}
text={textBoxes[i].text} />
)
}

View file

@ -256,31 +256,6 @@ export default class Utils {
}
}
localizeHtmlPage(): void {
//Localize by replacing __MSG_***__ meta tags
const localizedMessage = this.getLocalizedMessage(document.title);
if (localizedMessage) document.title = localizedMessage;
const objects = document.getElementsByClassName("sponsorBlockPageBody")[0].children;
for (let j = 0; j < objects.length; j++) {
const obj = objects[j];
const localizedMessage = this.getLocalizedMessage(obj.innerHTML.toString());
if (localizedMessage) obj.innerHTML = localizedMessage;
}
}
getLocalizedMessage(text: string): string | false {
const valNewH = text.replace(/__MSG_(\w+)__/g, function(match, v1) {
return v1 ? chrome.i18n.getMessage(v1).replace(/</g, "&#60;")
.replace(/"/g, "&quot;").replace(/\n/g, "<br/>") : "";
});
if(valNewH != text) {
return valNewH;
} else {
return false;
}
}
/**
* @returns {String[]} Domains in regex form
*/

141
src/utils/constants.ts Normal file
View file

@ -0,0 +1,141 @@
import { TextBox } from "../render/GenericNotice";
import { Category } from "../types";
export function getGuidelineInfo(category: Category): TextBox[] {
switch (category) {
case "sponsor":
return [{
icon: "icons/money.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/close-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "selfpromo":
return [{
icon: "icons/money.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/campaign.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/close-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}, {
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "exclusive_access":
return [{
icon: "icons/money.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}];
case "interaction":
return [{
icon: "icons/lightbulb.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/lightbulb.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/close-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}, {
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "intro":
return [{
icon: "icons/check-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/close-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "outro":
return [{
icon: "icons/close-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "preview":
return [{
icon: "icons/check-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/check-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/close-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}, {
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "filler":
return [{
icon: "icons/stopwatch.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/stopwatch.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/close-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}, {
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "music_offtopic":
return [{
icon: "icons/music-note.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/music-note.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
case "poi_highlight":
return [{
icon: "icons/star.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline1`)
}, {
icon: "icons/bolt.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline2`)
}, {
icon: "icons/bolt.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}];
}
}

View file

@ -62,7 +62,7 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} {
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
}
export const GenericUtils = {
wait,

12
src/utils/logger.ts Normal file
View file

@ -0,0 +1,12 @@
window["SBLogs"] = {
debug: [],
warn: []
};
export function logDebug(message: string) {
window["SBLogs"].debug.push(`[${new Date().toISOString()}] ${message}`);
}
export function logWarn(message: string) {
window["SBLogs"].warn.push(`[${new Date().toISOString()}] ${message}`);
}

View file

@ -61,4 +61,29 @@ export function getHashParams(): Record<string, unknown> {
}
return {};
}
export function localizeHtmlPage(): void {
//Localize by replacing __MSG_***__ meta tags
const localizedMessage = getLocalizedMessage(document.title);
if (localizedMessage) document.title = localizedMessage;
const objects = document.getElementsByClassName("sponsorBlockPageBody")[0].children;
for (let j = 0; j < objects.length; j++) {
const obj = objects[j];
const localizedMessage = getLocalizedMessage(obj.innerHTML.toString());
if (localizedMessage) obj.innerHTML = localizedMessage;
}
}
export function getLocalizedMessage(text: string): string | false {
const valNewH = text.replace(/__MSG_(\w+)__/g, function(match, v1) {
return v1 ? chrome.i18n.getMessage(v1).replace(/</g, "&#60;")
.replace(/"/g, "&quot;").replace(/\n/g, "<br/>") : "";
});
if (valNewH != text) {
return valNewH;
} else {
return false;
}
}

View file

@ -1,12 +1,15 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const webpack = require("webpack");
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const BuildManifest = require('./webpack.manifest');
const srcDir = '../src/';
const fs = require("fs");
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import webpack from "webpack"
import path from "path"
import { fileURLToPath } from "url"
import CopyPlugin from "copy-webpack-plugin"
import BuildManifest from "./webpack.manifest.cjs";
const srcDir = "../src/";
import fs from "fs";
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const edgeLanguages = [
"de",
@ -24,7 +27,7 @@ const edgeLanguages = [
"zh_CN"
]
module.exports = env => ({
export default env => ({
entry: {
popup: path.join(__dirname, srcDir + 'popup.ts'),
background: path.join(__dirname, srcDir + 'background.ts'),

View file

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
import { merge } from "webpack-merge";
import common from './webpack.common.js';
module.exports = env => merge(common(env), {
export default env => merge(common(env), {
devtool: 'inline-source-map',
mode: 'development'
});

View file

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
import { merge } from "webpack-merge";
import common from './webpack.common.js';
module.exports = env => {
export default env => {
let mode = "production";
env.mode = mode;