aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--config.json.example8
-rw-r--r--manifest/manifest.json5
-rw-r--r--package-lock.json1089
-rw-r--r--package.json1
-rw-r--r--public/_locales/en/messages.json111
-rw-r--r--public/content.css98
-rw-r--r--public/icons/export.svg106
-rw-r--r--public/icons/import.svg91
-rw-r--r--public/icons/skip.svg1
-rw-r--r--public/icons/sort.svg1
-rw-r--r--public/options/options.css17
-rw-r--r--public/options/options.html34
-rw-r--r--public/popup.css87
-rw-r--r--public/popup.html27
-rw-r--r--public/res/countries.json1
-rw-r--r--public/shared.css219
-rw-r--r--public/upsell/index.html94
-rw-r--r--public/upsell/styles.css387
-rw-r--r--src/background.ts24
-rw-r--r--src/components/ChapterVoteComponent.tsx121
-rw-r--r--src/components/NoticeComponent.tsx61
-rw-r--r--src/components/SelectorComponent.tsx55
-rw-r--r--src/components/SkipNoticeComponent.tsx3
-rw-r--r--src/components/SponsorTimeEditComponent.tsx208
-rw-r--r--src/components/SubmissionNoticeComponent.tsx19
-rw-r--r--src/components/options/CategoryChooserComponent.tsx (renamed from src/components/CategoryChooserComponent.tsx)4
-rw-r--r--src/components/options/CategorySkipOptionsComponent.tsx (renamed from src/components/CategorySkipOptionsComponent.tsx)101
-rw-r--r--src/components/options/KeybindComponent.tsx (renamed from src/components/KeybindComponent.tsx)6
-rw-r--r--src/components/options/KeybindDialogComponent.tsx (renamed from src/components/KeybindDialogComponent.tsx)6
-rw-r--r--src/components/options/ToggleOptionComponent.tsx56
-rw-r--r--src/config.ts32
-rw-r--r--src/content.ts254
-rw-r--r--src/js-components/previewBar.ts651
-rw-r--r--src/messageTypes.ts31
-rw-r--r--src/options.ts2
-rw-r--r--src/popup.ts478
-rw-r--r--src/render/CategoryChooser.tsx2
-rw-r--r--src/render/ChapterVote.tsx63
-rw-r--r--src/render/GenericNotice.tsx18
-rw-r--r--src/render/RectangleTooltip.tsx4
-rw-r--r--src/render/Tooltip.tsx71
-rw-r--r--src/svg-icons/lock_svg.tsx22
-rw-r--r--src/svg-icons/thumbs_down_svg.tsx10
-rw-r--r--src/svg-icons/thumbs_up_svg.tsx10
-rw-r--r--src/types.ts20
-rw-r--r--src/upsell.ts71
-rw-r--r--src/utils.ts68
-rw-r--r--src/utils/arrayUtils.ts6
-rw-r--r--src/utils/categoryUtils.ts6
-rw-r--r--src/utils/constants.ts11
-rw-r--r--src/utils/exporter.ts65
-rw-r--r--src/utils/genericUtils.ts69
-rw-r--r--src/utils/licenseKey.ts65
-rw-r--r--src/utils/pageUtils.ts45
-rw-r--r--test/exporter.test.ts241
-rw-r--r--test/previewBar.test.ts665
-rw-r--r--webpack/webpack.common.js3
57 files changed, 5456 insertions, 568 deletions
diff --git a/config.json.example b/config.json.example
index 1912aaee..c5e86444 100644
--- a/config.json.example
+++ b/config.json.example
@@ -2,7 +2,7 @@
"serverAddress": "https://sponsor.ajay.app",
"testingServerAddress": "https://sponsor.ajay.app/test",
"serverAddressComment": "This specifies the default SponsorBlock server to connect to",
- "categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "music_offtopic"],
+ "categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "chapter", "music_offtopic"],
"categorySupport": {
"sponsor": ["skip", "mute", "full"],
"selfpromo": ["skip", "mute", "full"],
@@ -13,7 +13,8 @@
"preview": ["skip", "mute"],
"filler": ["skip", "mute"],
"music_offtopic": ["skip"],
- "poi_highlight": ["poi"]
+ "poi_highlight": ["poi"],
+ "chapter": ["chapter"]
},
"wikiLinks": {
"sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor",
@@ -27,6 +28,7 @@
"music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section",
"poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight",
"guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines",
- "mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment"
+ "mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment",
+ "chapter": "https://wiki.sponsor.ajay.app/w/Chapter"
}
}
diff --git a/manifest/manifest.json b/manifest/manifest.json
index f5a603e2..4e6f97d5 100644
--- a/manifest/manifest.json
+++ b/manifest/manifest.json
@@ -18,6 +18,7 @@
],
"css": [
"content.css",
+ "shared.css",
"./libs/Source+Sans+Pro.css",
"popup.css"
]
@@ -48,9 +49,11 @@
"icons/beep.ogg",
"icons/pause.svg",
"icons/stop.svg",
+ "icons/skip.svg",
"icons/heart.svg",
"icons/visible.svg",
"icons/not_visible.svg",
+ "icons/sort.svg",
"icons/money.svg",
"icons/segway.png",
"icons/close-smaller.svg",
@@ -61,6 +64,8 @@
"icons/bolt.svg",
"icons/stopwatch.svg",
"icons/music-note.svg",
+ "icons/import.svg",
+ "icons/export.svg",
"icons/PlayerInfoIconSponsorBlocker.svg",
"icons/PlayerDeleteIconSponsorBlocker.svg",
"popup.html",
diff --git a/package-lock.json b/package-lock.json
index db45f657..e6a4ec01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -47,6 +47,7 @@
"eslint-plugin-react": "^7.30.1",
"fork-ts-checker-webpack-plugin": "^7.2.13",
"jest": "^28.1.3",
+ "jest-environment-jsdom": "^28.1.0",
"rimraf": "^3.0.2",
"schema-utils": "^4.0.0",
"selenium-webdriver": "^4.3.1",
@@ -1621,6 +1622,15 @@
"integrity": "sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw==",
"dev": true
},
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@tsconfig/node10": {
"version": "1.0.8",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
@@ -1803,6 +1813,17 @@
"pretty-format": "^28.0.0"
}
},
+ "node_modules/@types/jsdom": {
+ "version": "16.2.14",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz",
+ "integrity": "sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/parse5": "*",
+ "@types/tough-cookie": "*"
+ }
+ },
"node_modules/@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
@@ -1840,6 +1861,12 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "node_modules/@types/parse5": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
+ "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==",
+ "dev": true
+ },
"node_modules/@types/prettier": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
@@ -1900,6 +1927,12 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
+ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
+ "dev": true
+ },
"node_modules/@types/wicg-mediasession": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@types/wicg-mediasession/-/wicg-mediasession-1.1.3.tgz",
@@ -2415,6 +2448,12 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "dev": true
+ },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -2439,6 +2478,28 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-globals": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+ "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^7.1.1",
+ "acorn-walk": "^7.1.1"
+ }
+ },
+ "node_modules/acorn-globals/node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -2448,6 +2509,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/addons-linter": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/addons-linter/-/addons-linter-5.10.0.tgz",
@@ -2796,6 +2866,45 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
+ "node_modules/addons-linter/node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/addons-linter/node_modules/optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/addons-linter/node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/addons-linter/node_modules/semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@@ -2823,6 +2932,18 @@
"node": ">=8"
}
},
+ "node_modules/addons-linter/node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/addons-linter/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -3531,6 +3652,12 @@
"node": ">=8"
}
},
+ "node_modules/browser-process-hrtime": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
+ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
+ "dev": true
+ },
"node_modules/browserslist": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz",
@@ -4407,6 +4534,30 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ },
"node_modules/csstype": {
"version": "3.0.10",
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
@@ -4424,6 +4575,33 @@
"node": ">=0.10"
}
},
+ "node_modules/data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/date-fns": {
"version": "2.28.0",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
@@ -4470,6 +4648,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/decimal.js": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
+ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
+ "dev": true
+ },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -4680,6 +4864,18 @@
}
]
},
+ "node_modules/domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "dev": true,
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
@@ -4951,6 +5147,37 @@
"node": ">=0.8.0"
}
},
+ "node_modules/escodegen": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
+ "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
+ "dev": true,
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/eslint": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz",
@@ -5262,6 +5489,45 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/eslint/node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint/node_modules/optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint/node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/eslint/node_modules/supports-color": {
"version": "7.2.0",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
@@ -5273,6 +5539,18 @@
"node": ">=8"
}
},
+ "node_modules/eslint/node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/eslint/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -6296,6 +6574,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -6327,6 +6617,20 @@
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"dev": true
},
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -6795,6 +7099,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
"node_modules/is-regex": {
"version": "1.1.4",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
@@ -7621,6 +7931,25 @@
"node": ">=8"
}
},
+ "node_modules/jest-environment-jsdom": {
+ "version": "28.1.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-28.1.0.tgz",
+ "integrity": "sha512-8n6P4xiDjNVqTWv6W6vJPuQdLx+ZiA3dbYg7YJ+DPzR+9B61K6pMVJrSs2IxfGRG4J7pyAUA5shQ9G0KEun78w==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.0",
+ "@jest/fake-timers": "^28.1.0",
+ "@jest/types": "^28.1.0",
+ "@types/jsdom": "^16.2.4",
+ "@types/node": "*",
+ "jest-mock": "^28.1.0",
+ "jest-util": "^28.1.0",
+ "jsdom": "^19.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
"node_modules/jest-environment-node": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz",
@@ -8752,6 +9081,72 @@
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"dev": true
},
+ "node_modules/jsdom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz",
+ "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.5",
+ "acorn": "^8.5.0",
+ "acorn-globals": "^6.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.1",
+ "decimal.js": "^10.3.1",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.0",
+ "parse5": "6.0.1",
+ "saxes": "^5.0.1",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.0.0",
+ "w3c-hr-time": "^1.0.2",
+ "w3c-xmlserializer": "^3.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^10.0.0",
+ "ws": "^8.2.3",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/jsdom/node_modules/parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+ "dev": true
+ },
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -8986,13 +9381,13 @@
}
},
"node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
"dev": true,
"dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
},
"engines": {
"node": ">= 0.8.0"
@@ -9557,6 +9952,12 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/nwsapi": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
+ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
+ "dev": true
+ },
"node_modules/oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -9710,17 +10111,17 @@
}
},
"node_modules/optionator": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
- "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"dev": true,
"dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.3"
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
},
"engines": {
"node": ">= 0.8.0"
@@ -10111,9 +10512,9 @@
}
},
"node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
"dev": true,
"engines": {
"node": ">= 0.8.0"
@@ -10508,6 +10909,19 @@
"node": ">= 6"
}
},
+ "node_modules/request/node_modules/tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "dev": true,
+ "dependencies": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/request/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -10691,6 +11105,18 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
+ "node_modules/saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dev": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/scheduler": {
"version": "0.20.2",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
@@ -11346,6 +11772,12 @@
"node": ">=8"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -11553,16 +11985,38 @@
}
},
"node_modules/tough-cookie": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
- "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
+ "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
+ "dev": true,
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie/node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"dev": true,
"dependencies": {
- "psl": "^1.1.28",
"punycode": "^2.1.1"
},
"engines": {
- "node": ">=0.8"
+ "node": ">=12"
}
},
"node_modules/tree-kill": {
@@ -11821,12 +12275,12 @@
"dev": true
},
"node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
"dev": true,
"dependencies": {
- "prelude-ls": "^1.2.1"
+ "prelude-ls": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
@@ -12045,6 +12499,27 @@
"extsprintf": "^1.2.0"
}
},
+ "node_modules/w3c-hr-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+ "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+ "dev": true,
+ "dependencies": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz",
+ "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==",
+ "dev": true,
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -12195,6 +12670,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/webpack": {
"version": "5.74.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz",
@@ -12347,6 +12831,52 @@
"node": ">=10.13.0"
}
},
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz",
+ "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/when": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/when/-/when-3.7.7.tgz",
@@ -12565,6 +13095,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
@@ -12587,6 +13126,12 @@
"node": ">=4.0"
}
},
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -13870,6 +14415,12 @@
"integrity": "sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw==",
"dev": true
},
+ "@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true
+ },
"@tsconfig/node10": {
"version": "1.0.8",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
@@ -14052,6 +14603,17 @@
"pretty-format": "^28.0.0"
}
},
+ "@types/jsdom": {
+ "version": "16.2.14",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz",
+ "integrity": "sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/parse5": "*",
+ "@types/tough-cookie": "*"
+ }
+ },
"@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
@@ -14089,6 +14651,12 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "@types/parse5": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
+ "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==",
+ "dev": true
+ },
"@types/prettier": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
@@ -14149,6 +14717,12 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
+ "@types/tough-cookie": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
+ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
+ "dev": true
+ },
"@types/wicg-mediasession": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@types/wicg-mediasession/-/wicg-mediasession-1.1.3.tgz",
@@ -14526,6 +15100,12 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true
},
+ "abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "dev": true
+ },
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -14541,6 +15121,24 @@
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
"dev": true
},
+ "acorn-globals": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+ "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.1.1",
+ "acorn-walk": "^7.1.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true
+ }
+ }
+ },
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -14548,6 +15146,12 @@
"dev": true,
"requires": {}
},
+ "acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "dev": true
+ },
"addons-linter": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/addons-linter/-/addons-linter-5.10.0.tgz",
@@ -14815,6 +15419,36 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
"semver": {
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
@@ -14833,6 +15467,15 @@
"has-flag": "^4.0.0"
}
},
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -15344,6 +15987,12 @@
"fill-range": "^7.0.1"
}
},
+ "browser-process-hrtime": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
+ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
+ "dev": true
+ },
"browserslist": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz",
@@ -15988,6 +16637,29 @@
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"dev": true
},
+ "cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true
+ },
+ "cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "requires": {
+ "cssom": "~0.3.6"
+ },
+ "dependencies": {
+ "cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ }
+ }
+ },
"csstype": {
"version": "3.0.10",
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
@@ -16002,6 +16674,29 @@
"assert-plus": "^1.0.0"
}
},
+ "data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "dependencies": {
+ "whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
+ "requires": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ }
+ }
+ }
+ },
"date-fns": {
"version": "2.28.0",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
@@ -16027,6 +16722,12 @@
"integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==",
"dev": true
},
+ "decimal.js": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
+ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
+ "dev": true
+ },
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -16176,6 +16877,15 @@
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true
},
+ "domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "dev": true,
+ "requires": {
+ "webidl-conversions": "^7.0.0"
+ }
+ },
"domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
@@ -16380,6 +17090,27 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
+ "escodegen": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
+ "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
+ "dev": true,
+ "requires": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true
+ }
+ }
+ },
"eslint": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz",
@@ -16517,6 +17248,36 @@
"argparse": "^2.0.1"
}
},
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
"supports-color": {
"version": "7.2.0",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
@@ -16525,6 +17286,15 @@
"has-flag": "^4.0.0"
}
},
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -17355,6 +18125,15 @@
"integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==",
"dev": true
},
+ "html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "requires": {
+ "whatwg-encoding": "^2.0.0"
+ }
+ },
"html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -17379,6 +18158,17 @@
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"dev": true
},
+ "http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "requires": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ }
+ },
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -17691,6 +18481,12 @@
"isobject": "^3.0.1"
}
},
+ "is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
"is-regex": {
"version": "1.1.4",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
@@ -18290,6 +19086,22 @@
}
}
},
+ "jest-environment-jsdom": {
+ "version": "28.1.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-28.1.0.tgz",
+ "integrity": "sha512-8n6P4xiDjNVqTWv6W6vJPuQdLx+ZiA3dbYg7YJ+DPzR+9B61K6pMVJrSs2IxfGRG4J7pyAUA5shQ9G0KEun78w==",
+ "dev": true,
+ "requires": {
+ "@jest/environment": "^28.1.0",
+ "@jest/fake-timers": "^28.1.0",
+ "@jest/types": "^28.1.0",
+ "@types/jsdom": "^16.2.4",
+ "@types/node": "*",
+ "jest-mock": "^28.1.0",
+ "jest-util": "^28.1.0",
+ "jsdom": "^19.0.0"
+ }
+ },
"jest-environment-node": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz",
@@ -19142,6 +19954,60 @@
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"dev": true
},
+ "jsdom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz",
+ "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.5",
+ "acorn": "^8.5.0",
+ "acorn-globals": "^6.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.1",
+ "decimal.js": "^10.3.1",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.0",
+ "parse5": "6.0.1",
+ "saxes": "^5.0.1",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.0.0",
+ "w3c-hr-time": "^1.0.2",
+ "w3c-xmlserializer": "^3.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^10.0.0",
+ "ws": "^8.2.3",
+ "xml-name-validator": "^4.0.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+ "dev": true
+ }
+ }
+ },
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -19333,13 +20199,13 @@
"dev": true
},
"levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
"dev": true,
"requires": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
}
},
"lie": {
@@ -19795,6 +20661,12 @@
"boolbase": "^1.0.0"
}
},
+ "nwsapi": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
+ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
+ "dev": true
+ },
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@@ -19900,17 +20772,17 @@
}
},
"optionator": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
- "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
"dev": true,
"requires": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.3"
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
}
},
"os-locale": {
@@ -20189,9 +21061,9 @@
}
},
"prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
"dev": true
},
"pretty-format": {
@@ -20507,6 +21379,16 @@
"uuid": "^3.3.2"
},
"dependencies": {
+ "tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ },
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -20644,6 +21526,15 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
+ "saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dev": true,
+ "requires": {
+ "xmlchars": "^2.2.0"
+ }
+ },
"scheduler": {
"version": "0.20.2",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
@@ -21154,6 +22045,12 @@
}
}
},
+ "symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
"tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -21302,12 +22199,30 @@
"dev": true
},
"tough-cookie": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
- "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
+ "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.1.2"
+ },
+ "dependencies": {
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true
+ }
+ }
+ },
+ "tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"dev": true,
"requires": {
- "psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
@@ -21469,12 +22384,12 @@
"dev": true
},
"type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
"dev": true,
"requires": {
- "prelude-ls": "^1.2.1"
+ "prelude-ls": "~1.1.2"
}
},
"type-detect": {
@@ -21633,6 +22548,24 @@
"extsprintf": "^1.2.0"
}
},
+ "w3c-hr-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+ "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+ "dev": true,
+ "requires": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "w3c-xmlserializer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz",
+ "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==",
+ "dev": true,
+ "requires": {
+ "xml-name-validator": "^4.0.0"
+ }
+ },
"walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -21742,6 +22675,12 @@
}
}
},
+ "webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true
+ },
"webpack": {
"version": "5.74.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz",
@@ -21838,6 +22777,42 @@
"wildcard": "^2.0.0"
}
},
+ "whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "requires": {
+ "iconv-lite": "0.6.3"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ }
+ }
+ }
+ },
+ "whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz",
+ "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==",
+ "dev": true,
+ "requires": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ }
+ },
"when": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/when/-/when-3.7.7.tgz",
@@ -21989,6 +22964,12 @@
"integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==",
"dev": true
},
+ "xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true
+ },
"xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
@@ -22005,6 +22986,12 @@
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"dev": true
},
+ "xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 1465d561..775e412b 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"eslint-plugin-react": "^7.30.1",
"fork-ts-checker-webpack-plugin": "^7.2.13",
"jest": "^28.1.3",
+ "jest-environment-jsdom": "^28.1.0",
"rimraf": "^3.0.2",
"schema-utils": "^4.0.0",
"selenium-webdriver": "^4.3.1",
diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json
index fe3176f1..ae14284a 100644
--- a/public/_locales/en/messages.json
+++ b/public/_locales/en/messages.json
@@ -25,6 +25,16 @@
"Segments": {
"message": "segments"
},
+ "SegmentsCap": {
+ "message": "Segments"
+ },
+ "Chapters": {
+ "message": "Chapters"
+ },
+ "renderAsChapters": {
+ "message": "Render segments as chapters",
+ "description": "Refers to drawing segments on the YouTube seek bar as split up chapters, similar to the existing chapter system"
+ },
"upvoteButtonInfo": {
"message": "Upvote this submission"
},
@@ -289,6 +299,14 @@
"message": "Submit segments",
"description": "Keybind label"
},
+ "nextChapterKeybind": {
+ "message": "Next chapter",
+ "description": "Keybind label"
+ },
+ "previousChapterKeybind": {
+ "message": "Previous chapter",
+ "description": "Keybind label"
+ },
"keybindDescription": {
"message": "Select a key by typing it and choose any modifier keys you wish to use."
},
@@ -545,6 +563,10 @@
"message": "to",
"description": "Used between segments. Example: 1:20 to 1:30"
},
+ "CopiedExclamation": {
+ "message": "Copied!",
+ "description": "Used after something has been copied to the clipboard. Example: 'Copied!'"
+ },
"generic_guideline1": {
"message": "Include segue transitions"
},
@@ -696,6 +718,21 @@
"category_poi_highlight_guideline3": {
"message": "Can skip to the title or thumbnail"
},
+ "category_chapter": {
+ "message": "Chapter"
+ },
+ "category_chapter_description": {
+ "message": "Custom named chapters describing major sections of a video."
+ },
+ "category_chapter_guideline1": {
+ "message": "Don't mention sponsor brand names"
+ },
+ "category_chapter_guideline2": {
+ "message": "Use larger chapters for general sections"
+ },
+ "category_chapter_guideline3": {
+ "message": "Smaller chapters can be placed inside larger ones"
+ },
"category_livestream_messages": {
"message": "Livestream: Donation/Message Readings"
},
@@ -726,6 +763,9 @@
"showOverlay_full": {
"message": "Show Label"
},
+ "showOverlay_chapter": {
+ "message": "Show Chapters"
+ },
"autoSkipOnMusicVideos": {
"message": "Auto skip all segments when there is a non-music segment"
},
@@ -781,6 +821,10 @@
"bracketEnd": {
"message": "(End)"
},
+ "End": {
+ "message": "End",
+ "description": "Button that skips to the end of a segment"
+ },
"hiddenDueToDownvote": {
"message": "hidden: downvote"
},
@@ -821,6 +865,13 @@
"downvoteDescription": {
"message": "Incorrect/Wrong Timing"
},
+ "incorrectVote": {
+ "message": "Incorrect"
+ },
+ "harmfulVote": {
+ "message": "Harmful",
+ "description": "Used for chapter segments when the text is harmful/offensive to remove it faster"
+ },
"incorrectCategory": {
"message": "Change Category"
},
@@ -856,6 +907,9 @@
"categoryPillTitleText": {
"message": "This entire video is labeled as this category and is too tightly integrated to be able to separate"
},
+ "chapterNameTooltipWarning": {
+ "message": "One of your chapter names is similar to a category. You should use categories when possible instead."
+ },
"experiementOptOut": {
"message": "Opt-out of all future experiments",
"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."
@@ -1039,5 +1093,62 @@
},
"confirmResetToDefault": {
"message": "Are you sure you want to reset all settings to their default values? This cannot be undone."
+ },
+ "exportSegments": {
+ "message": "Export segments"
+ },
+ "importSegments": {
+ "message": "Import chapters"
+ },
+ "Import": {
+ "message": "Import",
+ "description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
+ },
+ "redeemSuccess": {
+ "message": "Reedem Successful!"
+ },
+ "redeemFailed": {
+ "message": "License key is invalid"
+ },
+ "hideUpsells": {
+ "message": "Hide options not available without extra payment"
+ },
+ "chooseACountry": {
+ "message": "Choose a country"
+ },
+ "noDiscount": {
+ "message": "You do not qualify for a discount"
+ },
+ "discountLink": {
+ "message": "Discount Link (See the pink price)"
+ },
+ "selectYourCountry": {
+ "message": "Select your country"
+ },
+ "alreadyDonated": {
+ "message": "If you've donated any amount before now, you may redeem free access by emailing:",
+ "description": "After the colon is an email address"
+ },
+ "cantAfford": {
+ "message": "If you can't afford to purchase a license, click {here} to see if you are eligible for a discount",
+ "description": "Keep the curly braces"
+ },
+ "patreonSignIn": {
+ "message": "Sign in with Patreon"
+ },
+ "redeem": {
+ "message": "Redeem"
+ },
+ "joinOnPatreon": {
+ "message": "Subscribe on Patreon"
+ },
+ "oneTimePurchase": {
+ "message": "One Time Purchase"
+ },
+ "enterLicenseKey": {
+ "message": "Enter License Key"
+ },
+ "chaptersPage1": {
+ "message": "SponsorBlock crowd-sourced chapters feature is only available to people who purchase a license, or for people who are granted access for free due their past contributions"
}
}
diff --git a/public/content.css b/public/content.css
index a4d2dfbd..da79f74d 100644
--- a/public/content.css
+++ b/public/content.css
@@ -1,3 +1,12 @@
+:root {
+ --skip-notice-right: 10px;
+ --skip-notice-padding: 5px;
+ --skip-notice-margin: 5px;
+ --skip-notice-border-horizontal: 5px;
+ --skip-notice-border-vertical: 10px;
+ --sb-dark-red-outline: rgb(130,0,0,0.9);
+}
+
.hidden {
display: none;
}
@@ -12,7 +21,7 @@
height: 100%;
transform: scaleY(0.6) translateY(-30%) translateY(1.5px);
- z-index: 40;
+ z-index: 42;
transition: transform .1s cubic-bezier(0,0,0.2,1);
}
@@ -45,23 +54,48 @@
transform: translateY(-1em) !important;
}
+.ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
+ transform: translateY(-2em) !important;
+}
+
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
transform: translateY(-2em) !important;
}
+.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips {
+ transform: translateY(-4em) !important;
+}
+
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
transform: translateY(1em) !important;
}
+#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper {
+ transform: translateY(2em) !important;
+}
+
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
transform: translateY(0.5em) !important;
}
+.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper {
+ transform: translateY(1em) !important;
+}
+
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
display: block !important;
transform: translateY(1em) !important;
}
+.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible.sponsorTwoTooltips > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
+ display: block !important;
+ transform: translateY(2em) !important;
+}
+
+div:hover > .sponsorBlockChapterBar {
+ z-index: 41 !important;
+}
+
/* */
.popup {
@@ -88,6 +122,16 @@
vertical-align: top;
}
+/* Removes auto width from being a ytp-player-button */
+.sbPlayerDownvote {
+ width: auto !important;
+}
+
+/* Adds back the padding */
+.sbPlayerDownvote svg {
+ padding-right: 3.6px;
+}
+
.autoHiding {
overflow: visible !important;
}
@@ -113,8 +157,8 @@
.sponsorSkipObject {
font-family: Roboto, Arial, Helvetica, sans-serif;
- margin-left: 2px;
- margin-right: 2px;
+ margin-left: var(--skip-notice-margin);
+ margin-right: var(--skip-notice-margin);
}
.sponsorSkipLogo {
@@ -145,7 +189,7 @@
position: absolute;
right: 5px;
bottom: 100px;
- right: 10px;
+ right: var(--skip-notice-right);
}
.sponsorSkipNoticeParent {
@@ -525,7 +569,7 @@ input::-webkit-inner-spin-button {
margin-bottom: 5px;
background-color: rgba(28, 28, 28, 0.9);
- border-color: rgb(130,0,0,0.9);
+ border-color: var(--sb-dark-red-outline);
color: white;
border-width: 3px;
padding: 3px;
@@ -536,6 +580,45 @@ input::-webkit-inner-spin-button {
color: white;
}
+/* Start SelectorComponent */
+
+.sbSelector {
+ position: absolute;
+ text-align: center;
+ width: calc(100% - var(--skip-notice-right) - var(--skip-notice-padding) * 2 - var(--skip-notice-margin) * 2 - var(--skip-notice-border-horizontal) * 2);
+
+ z-index: 1000;
+}
+
+.sbSelectorBackground {
+ text-align: center;
+
+ background-color: rgba(28, 28, 28, 0.9);
+ border-radius: 6px;
+ padding: 3px;
+ margin: auto;
+ width: 170px;
+}
+
+.sbSelectorOption {
+ cursor: pointer;
+ background-color: rgb(43, 43, 43);
+ padding: 5px;
+ margin: 5px;
+ color: white;
+ border-radius: 5px;
+ font-size: 14px;
+
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.sbSelectorOption:hover {
+ background-color: #3a0000;
+}
+
+/* End SelectorComponent */
+
.helpButton {
height: 25px;
cursor: pointer;
@@ -623,6 +706,11 @@ input::-webkit-inner-spin-button {
border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent;
}
+.sponsorBlockTooltip.sbTriangle.centeredSBTriangle::after {
+ left: 50%;
+ right: 50%;
+}
+
.sponsorBlockLockedColor {
color: #ffc83d;
}
diff --git a/public/icons/export.svg b/public/icons/export.svg
new file mode 100644
index 00000000..337823ab
--- /dev/null
+++ b/public/icons/export.svg
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ version="1.1"
+ id="Capa_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 67.671 67.671"
+ style="enable-background:new 0 0 67.671 67.671;"
+ xml:space="preserve"
+ sodipodi:docname="export.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="defs41" /><sodipodi:namedview
+ id="namedview39"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="9.309749"
+ inkscape:cx="33.835499"
+ inkscape:cy="16.649214"
+ inkscape:window-width="1366"
+ inkscape:window-height="731"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="Capa_1" />
+<g
+ id="g6"
+ style="fill:#ffffff">
+ <path
+ d="M 52.946,23.348 H 42.834 v 6 h 10.112 c 3.007,0 5.34,1.536 5.34,2.858 v 26.606 c 0,1.322 -2.333,2.858 -5.34,2.858 H 14.724 c -3.007,0 -5.34,-1.536 -5.34,-2.858 V 32.207 c 0,-1.322 2.333,-2.858 5.34,-2.858 h 10.11 v -6 h -10.11 c -6.359,0 -11.34,3.891 -11.34,8.858 v 26.606 c 0,4.968 4.981,8.858 11.34,8.858 h 38.223 c 6.358,0 11.34,-3.891 11.34,-8.858 V 32.207 C 64.286,27.239 59.305,23.348 52.946,23.348 Z"
+ id="path2"
+ style="fill:#ffffff" />
+ <path
+ d="m 24.957,14.955 c 0.768,0 1.535,-0.293 2.121,-0.879 l 3.756,-3.756 v 13.028 6 11.494 c 0,1.657 1.343,3 3,3 1.657,0 3,-1.343 3,-3 v -11.494 -6 -13.231 l 3.959,3.959 c 0.586,0.586 1.354,0.879 2.121,0.879 0.767,0 1.535,-0.293 2.121,-0.879 1.172,-1.171 1.172,-3.071 0,-4.242 L 36.078,0.877 C 35.492,0.291 34.725,0 33.958,0 33.95,0 33.943,0 33.935,0 33.927,0 33.92,0 33.912,0 33.145,0 32.378,0.291 31.792,0.877 l -8.957,8.957 c -1.172,1.171 -1.172,3.071 0,4.242 0.587,0.586 1.354,0.879 2.122,0.879 z"
+ id="path4"
+ style="fill:#ffffff" />
+</g>
+<g
+ id="g8"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g10"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g12"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g14"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g16"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g18"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g20"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g22"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g24"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g26"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g28"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g30"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g32"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g34"
+ style="fill:#ffffff">
+</g>
+<g
+ id="g36"
+ style="fill:#ffffff">
+</g>
+</svg>
diff --git a/public/icons/import.svg b/public/icons/import.svg
new file mode 100644
index 00000000..b1692f37
--- /dev/null
+++ b/public/icons/import.svg
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ version="1.1"
+ id="Capa_1"
+ x="0px"
+ y="0px"
+ viewBox="0 0 67.671 67.671"
+ style="enable-background:new 0 0 67.671 67.671;"
+ xml:space="preserve"
+ sodipodi:docname="import.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="defs41" /><sodipodi:namedview
+ id="namedview39"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="9.309749"
+ inkscape:cx="33.835499"
+ inkscape:cy="33.835499"
+ inkscape:window-width="1920"
+ inkscape:window-height="983"
+ inkscape:window-x="482"
+ inkscape:window-y="768"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g6" />
+<g
+ id="g6">
+ <path
+ d="M52.946,23.348H42.834v6h10.112c3.007,0,5.34,1.536,5.34,2.858v26.606c0,1.322-2.333,2.858-5.34,2.858H14.724 c-3.007,0-5.34-1.536-5.34-2.858V32.207c0-1.322,2.333-2.858,5.34-2.858h10.11v-6h-10.11c-6.359,0-11.34,3.891-11.34,8.858v26.606 c0,4.968,4.981,8.858,11.34,8.858h38.223c6.358,0,11.34-3.891,11.34-8.858V32.207C64.286,27.239,59.305,23.348,52.946,23.348z"
+ id="path2"
+ style="fill:#ffffff" />
+ <path
+ d="m 42.913,34.887 c -0.768,0 -1.370265,0.528017 -2.121,0.879 l -3.756,3.756 v -19.028 -6 V 3 c 0,-1.657 -1.343,-3 -3,-3 -1.657,0 -3,1.343 -3,3 v 11.494 12 13.231 l -3.959,-3.959 c -0.586,-0.586 -1.354,-0.879 -2.121,-0.879 -0.767,0 -1.535,0.293 -2.121,0.879 -1.172,1.171 -1.172,3.071 0,4.242 l 8.957,8.957 c 0.586,0.586 1.353,0.877 2.12,0.877 h 0.023 0.023 c 0.767,0 1.534,-0.291 2.12,-0.877 l 8.957,-8.957 c 1.172,-1.171 1.172,-3.071 0,-4.242 -0.587,-0.586 -1.354,-0.879 -2.122,-0.879 z"
+ id="path4"
+ sodipodi:nodetypes="sscccssscccssccsscssccs"
+ style="fill:#ffffff" />
+</g>
+<g
+ id="g8">
+</g>
+<g
+ id="g10">
+</g>
+<g
+ id="g12">
+</g>
+<g
+ id="g14">
+</g>
+<g
+ id="g16">
+</g>
+<g
+ id="g18">
+</g>
+<g
+ id="g20">
+</g>
+<g
+ id="g22">
+</g>
+<g
+ id="g24">
+</g>
+<g
+ id="g26">
+</g>
+<g
+ id="g28">
+</g>
+<g
+ id="g30">
+</g>
+<g
+ id="g32">
+</g>
+<g
+ id="g34">
+</g>
+<g
+ id="g36">
+</g>
+</svg>
diff --git a/public/icons/skip.svg b/public/icons/skip.svg
new file mode 100644
index 00000000..774e741c
--- /dev/null
+++ b/public/icons/skip.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg> \ No newline at end of file
diff --git a/public/icons/sort.svg b/public/icons/sort.svg
new file mode 100644
index 00000000..29c6d16f
--- /dev/null
+++ b/public/icons/sort.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/></svg> \ No newline at end of file
diff --git a/public/options/options.css b/public/options/options.css
index a2a829a1..2bd27161 100644
--- a/public/options/options.css
+++ b/public/options/options.css
@@ -123,6 +123,14 @@ html, body {
border-image: linear-gradient(to right, var(--border-color), #00000000 80%) 1;
}
+.categoryExtraOptions {
+ padding-bottom: 20px;
+}
+
+#music_offtopic_autoSkipOnMusicVideos {
+ padding-bottom: 0;
+}
+
.option-group > div:last-child, .option-group > #keybind-dialog {
border-bottom: inherit;
}
@@ -309,6 +317,10 @@ input[type='number'] {
color: grey;
}
+tr.disabled {
+ opacity: 0.3;
+}
+
#options {
height: 100vh;
flex-basis: 80%;
@@ -670,4 +682,9 @@ svg {
#options > div {
max-width: 100%;
}
+}
+
+.upsellButton {
+ cursor: pointer;
+ vertical-align: middle;
} \ No newline at end of file
diff --git a/public/options/options.html b/public/options/options.html
index d50f9c5c..765134ec 100644
--- a/public/options/options.html
+++ b/public/options/options.html
@@ -66,18 +66,6 @@
</div>
- <div data-type="toggle" data-sync="autoSkipOnMusicVideos">
- <div class="switch-container">
- <label class="switch">
- <input id="autoSkipOnMusicVideos" type="checkbox" checked>
- <span class="slider round"></span>
- </label>
- <label class="switch-label" for="autoSkipOnMusicVideos">
- __MSG_autoSkipOnMusicVideos__
- </label>
- </div>
- </div>
-
<div data-type="toggle" data-sync="muteSegments">
<div class="switch-container">
<label class="switch">
@@ -314,6 +302,18 @@
</div>
</div>
+ <div data-type="toggle" data-toggle-type="reverse" data-sync="showUpsells" data-no-safari="true">
+ <div class="switch-container">
+ <label class="switch">
+ <input id="showUpsell" type="checkbox" checked>
+ <span class="slider round"></span>
+ </label>
+ <label class="switch-label" for="showUpsells">
+ __MSG_hideUpsells__
+ </label>
+ </div>
+ </div>
+
</div>
<div id="keybinds" class="option-group hidden">
@@ -333,6 +333,16 @@
<div class="inline"></div>
</div>
+ <div data-type="keybind-change" data-sync="nextChapterKeybind">
+ <label class="optionLabel">__MSG_nextChapterKeybind__:</label>
+ <div class="inline"></div>
+ </div>
+
+ <div data-type="keybind-change" data-sync="previousChapterKeybind">
+ <label class="optionLabel">__MSG_previousChapterKeybind__:</label>
+ <div class="inline"></div>
+ </div>
+
</div>
<div id="import" class="option-group hidden">
diff --git a/public/popup.css b/public/popup.css
index f314ae03..e2494158 100644
--- a/public/popup.css
+++ b/public/popup.css
@@ -152,22 +152,46 @@
margin: 8px;
}
-/*
- * Refresh segments button
- */
#refreshSegmentsButton {
display: flex;
align-items: center;
+ padding: 5px;
+ margin: 5px auto;
+}
+
+#issueReporterImportExport {
+ position: relative;
+}
+
+#refreshSegmentsButton, #issueReporterImportExport button {
background: transparent;
border-radius: 50%;
- margin: 5px auto;
border: none;
- padding: 5px;
}
-#refreshSegmentsButton:hover {
+
+#refreshSegmentsButton:hover, #issueReporterImportExport button:hover {
background-color: var(--sb-grey-bg-color);
}
+#issueReporterImportExport button {
+ padding: 5px;
+ margin-right: 15px;
+ margin-left: 15px;
+}
+
+#issueReporterImportExport img {
+ width: 24px;
+ display: block;
+}
+
+#importSegmentsText {
+ margin-top: 7px;
+}
+
+#importSegmentsMenu button {
+ padding: 10px;
+}
+
/*
* <details> wrapper around each segment
*/
@@ -199,6 +223,15 @@
.segmentSummary > div {
text-align: left;
}
+
+.segmentActive {
+ color: #bdfffb;
+}
+
+.segmentPassed {
+ color: #adadad;
+}
+
/*
* Category dot in segment
*/
@@ -560,3 +593,45 @@
margin-bottom: 20px;
padding: 5px;
}
+
+#sponsorBlockPopupBody .u-mZ {
+ margin: 0 !important;
+}
+
+#sponsorBlockPopupBody .hidden {
+ display: none !important;
+}
+
+#issueReporterTabs {
+ margin: 5px;
+}
+
+#issueReporterTabs > span {
+ padding: 2px 4px;
+ margin: 0 3px;
+ cursor: pointer;
+ background-color: #444848;
+ border-radius: 10px;
+}
+
+#issueReporterTabs > span > span {
+ position: relative;
+ padding: 0.2em 0;
+}
+
+#issueReporterTabs > span > span::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 0.1em;
+ background-color: rgb(145, 0, 0);
+ transition: transform 300ms;
+ transform: scaleX(0);
+ transform-origin: center;
+}
+
+#issueReporterTabs > span.sbSelected > span::after {
+ transform: scaleX(0.8);
+} \ No newline at end of file
diff --git a/public/popup.html b/public/popup.html
index 48cdeea1..a31269c6 100644
--- a/public/popup.html
+++ b/public/popup.html
@@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link id="sponsorBlockPopupFont" href="/libs/Source+Sans+Pro.css" rel="stylesheet">
<link id="sponsorBlockStyleSheet" href="popup.css" rel="stylesheet">
+ <link id="sponsorBlockStyleSheet" href="shared.css" rel="stylesheet">
</head>
<body id="sponsorBlockPopupBody" style="visibility: hidden">
@@ -34,7 +35,33 @@
</button>
<!-- Video Segments -->
<div id="issueReporterContainer">
+ <div id="issueReporterTabs" class="hidden">
+ <span id="issueReporterTabSegments" class="sbSelected">
+ <span>__MSG_SegmentsCap__</span>
+ </span>
+ <span id="issueReporterTabChapters">
+ <span>__MSG_Chapters__</span>
+ </span>
+ </div>
<div id="issueReporterTimeButtons"></div>
+ <div id="issueReporterImportExport" class="hidden">
+ <div id="importExportButtons">
+ <button id="importSegmentsButton" title="__MSG_importSegments__" class="hidden">
+ <img src="/icons/import.svg" alt="Refresh icon" id="importSegments" />
+ </button>
+ <button id="exportSegmentsButton" title="__MSG_exportSegments__">
+ <img src="/icons/export.svg" alt="Export icon" id="exportSegments" />
+ </button>
+ </div>
+
+ <span id="importSegmentsMenu" class="hidden">
+ <textarea id="importSegmentsText" rows="5" style="width:80%"></textarea>
+
+ <button id="importSegmentsSubmit" title="__MSG_importSegments__">
+ __MSG_Import__
+ </button>
+ </span>
+ </div>
</div>
</div>
diff --git a/public/res/countries.json b/public/res/countries.json
new file mode 100644
index 00000000..33839f29
--- /dev/null
+++ b/public/res/countries.json
@@ -0,0 +1 @@
+{"Albania":{"allowed":true},"Algeria":{"allowed":true},"Angola":{"allowed":true},"Argentina":{"allowed":true},"Armenia":{"allowed":true},"Australia":{"allowed":false},"Austria":{"allowed":false},"Azerbaijan":{"allowed":true},"Bangladesh":{"allowed":true},"Belarus":{"allowed":true},"Belgium":{"allowed":false},"Belize":{"allowed":true},"Benin":{"allowed":true},"Bhutan":{"allowed":true},"Bolivia":{"allowed":true},"Bosnia and Herzegovina":{"allowed":true},"Botswana":{"allowed":true},"Brazil":{"allowed":true},"Bulgaria":{"allowed":true},"Burkina Faso":{"allowed":true},"Burundi":{"allowed":true},"Cameroon":{"allowed":true},"Canada":{"allowed":false},"Central African Republic":{"allowed":true},"Chad":{"allowed":true},"Chile":{"allowed":true},"China":{"allowed":true},"Colombia":{"allowed":true},"Comoros":{"allowed":true},"Costa Rica":{"allowed":true},"Croatia":{"allowed":true},"Cyprus":{"allowed":false},"Czech Republic":{"allowed":false},"Denmark":{"allowed":false},"Djibouti":{"allowed":true},"Dominican Republic":{"allowed":true},"DR Congo":{"allowed":true},"Ecuador":{"allowed":true},"Egypt":{"allowed":true},"El Salvador":{"allowed":true},"Estonia":{"allowed":false},"Eswatini":{"allowed":true},"Ethiopia":{"allowed":true},"Fiji":{"allowed":true},"Finland":{"allowed":false},"France":{"allowed":false},"Gabon":{"allowed":true},"Gambia":{"allowed":true},"Georgia":{"allowed":true},"Germany":{"allowed":false},"Ghana":{"allowed":true},"Greece":{"allowed":true},"Guatemala":{"allowed":true},"Guinea":{"allowed":true},"Guinea-Bissau":{"allowed":true},"Guyana":{"allowed":true},"Haiti":{"allowed":true},"Honduras":{"allowed":true},"Hungary":{"allowed":true},"Iceland":{"allowed":false},"India":{"allowed":true},"Iran":{"allowed":true},"Iraq":{"allowed":true},"Ireland":{"allowed":false},"Israel":{"allowed":false},"Italy":{"allowed":false},"Ivory Coast":{"allowed":true},"Jamaica":{"allowed":true},"Japan":{"allowed":false},"Jordan":{"allowed":true},"Kazakhstan":{"allowed":true},"Kenya":{"allowed":true},"Kiribati":{"allowed":true},"Kyrgyzstan":{"allowed":true},"Laos":{"allowed":true},"Latvia":{"allowed":true},"Lebanon":{"allowed":true},"Lesotho":{"allowed":true},"Liberia":{"allowed":true},"Lithuania":{"allowed":true},"Luxembourg":{"allowed":false},"Madagascar":{"allowed":true},"Malawi":{"allowed":true},"Malaysia":{"allowed":true},"Maldives":{"allowed":true},"Mali":{"allowed":true},"Malta":{"allowed":false},"Mauritania":{"allowed":true},"Mauritius":{"allowed":true},"Mexico":{"allowed":true},"Micronesia":{"allowed":true},"Moldova":{"allowed":true},"Mongolia":{"allowed":true},"Montenegro":{"allowed":true},"Morocco":{"allowed":true},"Mozambique":{"allowed":true},"Myanmar":{"allowed":true},"Namibia":{"allowed":true},"Nepal":{"allowed":true},"Netherlands":{"allowed":false},"Nicaragua":{"allowed":true},"Niger":{"allowed":true},"Nigeria":{"allowed":true},"North Macedonia":{"allowed":true},"Norway":{"allowed":false},"Pakistan":{"allowed":true},"Panama":{"allowed":true},"Papua New Guinea":{"allowed":true},"Paraguay":{"allowed":true},"Peru":{"allowed":true},"Philippines":{"allowed":true},"Poland":{"allowed":true},"Portugal":{"allowed":true},"Republic of the Congo":{"allowed":true},"Romania":{"allowed":true},"Russia":{"allowed":true},"Rwanda":{"allowed":true},"Saint Lucia":{"allowed":true},"Samoa":{"allowed":true},"Sao Tome and Principe":{"allowed":true},"Senegal":{"allowed":true},"Serbia":{"allowed":true},"Seychelles":{"allowed":true},"Sierra Leone":{"allowed":true},"Slovakia":{"allowed":true},"Slovenia":{"allowed":false},"Solomon Islands":{"allowed":true},"South Africa":{"allowed":true},"South Korea":{"allowed":false},"South Sudan":{"allowed":true},"Spain":{"allowed":false},"Sri Lanka":{"allowed":true},"Sudan":{"allowed":true},"Suriname":{"allowed":true},"Sweden":{"allowed":false},"Switzerland":{"allowed":false},"Syria":{"allowed":true},"Taiwan":{"allowed":false},"Tajikistan":{"allowed":true},"Tanzania":{"allowed":true},"Thailand":{"allowed":true},"Timor-Leste":{"allowed":true},"Togo":{"allowed":true},"Tonga":{"allowed":true},"Trinidad and Tobago":{"allowed":true},"Tunisia":{"allowed":true},"Turkey":{"allowed":true},"Turkmenistan":{"allowed":true},"Tuvalu":{"allowed":true},"Uganda":{"allowed":true},"Ukraine":{"allowed":true},"United Arab Emirates":{"allowed":false},"United Kingdom":{"allowed":false},"United States":{"allowed":false},"Uruguay":{"allowed":true},"Uzbekistan":{"allowed":true},"Vanuatu":{"allowed":true},"Venezuela":{"allowed":true},"Vietnam":{"allowed":true},"Yemen":{"allowed":true},"Zambia":{"allowed":true},"Zimbabwe":{"allowed":true}} \ No newline at end of file
diff --git a/public/shared.css b/public/shared.css
new file mode 100644
index 00000000..5cb53f88
--- /dev/null
+++ b/public/shared.css
@@ -0,0 +1,219 @@
+.sponsorSkipNoticeParent {
+ position: absolute;
+
+ bottom: 100px;
+ right: var(--skip-notice-right);
+}
+
+.sponsorSkipNoticeParent, .sponsorSkipNotice {
+ border-spacing: var(--skip-notice-border-horizontal) var(--skip-notice-border-vertical);
+ padding-left: var(--skip-notice-padding);
+ padding-right: var(--skip-notice-padding);
+
+ 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;
+ min-width: 100%;
+}
+
+.sponsorSkipNotice {
+ transition: all 0.1s ease-out;
+}
+
+.sponsorSkipNoticeLimitWidth {
+ max-width: calc(100% - 50px);
+}
+
+.sponsorSkipNotice .hidden {
+ display: none;
+}
+
+/* For Cloudtube */
+.sponsorSkipNotice td, .sponsorSkipNotice table, .sponsorSkipNotice th {
+ border: none;
+}
+
+.sponsorSkipNoticeFadeIn {
+ animation: fadeIn 0.5s ease-out;
+}
+
+.sponsorSkipNoticeFaded {
+ opacity: 0.5;
+}
+
+.sponsorSkipNoticeFadeOut {
+ transition: opacity 3s cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ opacity: 0 !important;
+ animation: none !important;
+}
+
+.sponsorSkipNotice .sponsorSkipNoticeTimeLeft {
+ color: #eeeeee;
+
+ border-radius: 4px;
+ padding: 2px 5px;
+ font-size: 12px;
+
+ display: flex;
+ align-items: center;
+
+ border: 1px solid #eeeeee;
+}
+
+.sponsorSkipNoticeTimeLeft img {
+ vertical-align: middle;
+ height: 13px;
+
+ padding-top: 7.8%;
+ padding-bottom: 7.8%;
+}
+
+/* if two are very close to eachother */
+.secondSkipNotice {
+ bottom: 290px;
+}
+
+.noticeLeftIcon {
+ display: flex;
+ align-items: center;
+}
+
+.sponsorSkipNotice .sponsorSkipNoticeUnskipSection {
+ float: left;
+
+ border-left: 1px solid rgb(150, 150, 150);
+}
+
+.sponsorSkipNoticeButton {
+ background: none;
+ color: rgb(235, 235, 235);
+ border: none;
+ display: inline-block;
+ font-size: 13.3333px !important;
+
+ cursor: pointer;
+
+ margin-right: 10px;
+
+ padding: 2px 5px;
+}
+
+.sponsorSkipNoticeButton:hover {
+ background-color: rgba(235, 235, 235,0.2);
+ border-radius: 4px;
+
+ transition: background-color 0.4s;
+}
+
+.sponsorSkipNoticeFirstRow .sponsorSkipNoticeButton.sponsorSkipSmallButton {
+ height: 1.3em;
+ padding: 0;
+}
+
+.sponsorTimesVoteButtonsContainer {
+ float: left;
+ vertical-align:middle;
+ padding: 2px 5px;
+
+ margin-right: 4px;
+}
+
+.sponsorTimesVoteButtonsContainer div{
+ display: inline-block;
+}
+
+.sponsorSkipNoticeRightSection {
+ right: 0;
+ position: absolute;
+
+ float: right;
+
+ margin-right: 10px;
+ display: flex;
+ align-items: center;
+}
+
+.sponsorSkipNoticeRightButton {
+ margin-right: 0;
+}
+
+.sponsorSkipNoticeCloseButton {
+ height: 10px;
+ width: 10px;
+ box-sizing: unset;
+
+ padding: 2px 5px;
+
+ margin-left: 2px;
+ float: right;
+}
+
+.sponsorSkipNoticeCloseButton.biggerCloseButton {
+ padding: 20px;
+}
+
+.sponsorSkipMessage {
+ font-size: 14px;
+ font-weight: bold;
+ color: rgb(235, 235, 235);
+
+ margin-top: auto;
+ display: inline-block;
+ margin-right: 10px;
+ margin-bottom: auto;
+}
+
+.sponsorSkipInfo {
+ font-size: 10px;
+ color: #000000;
+ text-align: center;
+ margin-top: 0px;
+}
+
+#sponsorTimesThanksForVotingText {
+ font-size: 20px;
+ font-weight: bold;
+ color: #000000;
+ text-align: center;
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
+#sponsorTimesThanksForVotingInfoText {
+ font-size: 12px;
+ font-weight: bold;
+ color: #000000;
+ text-align: center;
+ margin-top: 0px;
+}
+
+.sponsorTimesVoteButtonMessage {
+ float: left;
+}
+
+.sponsorTimesInfoMessage {
+ font-size: 13.3333px;
+ 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;
+} \ No newline at end of file
diff --git a/public/upsell/index.html b/public/upsell/index.html
new file mode 100644
index 00000000..1dbc21af
--- /dev/null
+++ b/public/upsell/index.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+
+<head>
+ <title>Upsell - SponsorBlock</title>
+ <meta charset="utf-8">
+
+ <link href="styles.css" rel="stylesheet" />
+
+ <script src="../js/vendor.js"></script>
+ <script src="../js/upsell.js"></script>
+</head>
+
+<body class="sponsorBlockPageBody">
+
+ <div id="title" class="titleBar">
+ <img src="../icons/LogoSponsorBlocker256px.png" height="80" class="profilepic" />
+ SponsorBlock
+ </div>
+
+ <br />
+
+ <div class="center">
+ <p>
+ __MSG_chaptersPage1__
+ </p>
+ </div>
+
+ <div class="center">
+ <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/H_mP7bpbA_c?modestbranding=1&rel=0" title="Demo Video"
+ frameborder="0" allow="autoplay; clipboard-write; encrypted-media; picture-in-picture"
+ allowfullscreen>
+ </iframe>
+ </div>
+
+
+
+ <br />
+
+ <div class="center row-item">
+ <a href="https://buy.ajay.app/l/sponsorblock" class="option-link side-by-side" target="_blank" rel="noreferrer">
+ <div id="oneTimePurchase" class="option-button inline">
+ __MSG_oneTimePurchase__
+ </div>
+ </a>
+
+ <a href="https://www.patreon.com/ajayyy" class="option-link side-by-side" target="_blank" rel="noreferrer">
+ <div class="option-button side-by-side inline">
+ __MSG_joinOnPatreon__
+ </div>
+ </a>
+ </div>
+
+ <div class="center row-item">
+ <input id="redeemCodeInput" class="option-text-box" type="text" placeholder="__MSG_enterLicenseKey__">
+ <div id="redeemButton" class="option-button inline">
+ __MSG_redeem__
+ </div>
+ </div>
+
+ <div class="center row-item">
+ <a href="https://www.patreon.com/oauth2/authorize?response_type=code&client_id=-W7ib8J-LB3jowb1fqE07A7RDUovy45_pOoWcjby6yr5upo6At8Jlg2BPhWDXO2k&redirect_uri=https%3A%2F%2Fsponsor.ajay.app%3A3000%2Fapi%2FgenerateToken%2Fpatreon"
+ class="option-link" target="_blank" rel="noreferrer">
+ <div class="option-button inline">
+ __MSG_patreonSignIn__
+ </div>
+ </a>
+ </div>
+
+ <div id="cantAfford" class="center">
+
+ </div>
+
+ <div class="center">
+ __MSG_alreadyDonated__ [email protected]
+ </div>
+
+ <div id="subsidizedPrice" class="center hidden">
+ __MSG_selectYourCountry__
+ </div>
+
+ <div id="subsidizedLink" class="center hidden">
+ <a href="https://buy.ajay.app/l/sponsorblock/purchasing-power" class="option-link" target="_blank"
+ rel="noreferrer">
+ <div class="option-button inline">
+ __MSG_discountLink__
+ </div>
+ </a>
+ </div>
+
+ <div id="noSubsidizedLink" class="center hidden">
+ __MSG_noDiscount__
+ </div>
+
+</body> \ No newline at end of file
diff --git a/public/upsell/styles.css b/public/upsell/styles.css
new file mode 100644
index 00000000..0c0d6b6b
--- /dev/null
+++ b/public/upsell/styles.css
@@ -0,0 +1,387 @@
+/* Based on options page CSS */
+html {
+ color-scheme: dark;
+}
+
+body {
+ font-family: sans-serif;
+}
+
+.center {
+ text-align: center;
+}
+
+.center p {
+ margin: auto;
+}
+
+.inline {
+ display: inline-block;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.row-item {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.keybind-status {
+ display: inline;
+}
+
+.small-description {
+ color: white;
+ font-size: 13px;
+}
+
+.medium-description {
+ color: white;
+ font-size: 15px;
+}
+
+.option-text-box {
+ width: 300px;
+}
+
+.option-button {
+ cursor: pointer;
+
+ background-color: #c00000;
+ padding: 10px;
+ color: white;
+ border-radius: 5px;
+ font-size: 14px;
+
+ width: max-content;
+}
+
+.option-link {
+ text-decoration: none;
+}
+
+.option-link.side-by-side {
+ padding: 50px;
+}
+
+.option-button:hover {
+ background-color: #fc0303;
+}
+
+.option-button.disabled {
+ cursor: default;
+
+ background-color: #520000;
+ color: grey;
+}
+
+#options {
+ max-width: 60%;
+ text-align: left;
+ display: inline-block;
+}
+
+.switch-container:after {
+ content: attr(label-name);
+ position: absolute;
+ padding: 4px;
+ width: max-content;
+
+ font-size: 14px;
+ color: white;
+}
+
+.text-label-container {
+ font-size: 14px;
+ color: white;
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 40px;
+ height: 24px;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #707070;
+}
+
+.animated * {
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 16px;
+ width: 16px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+}
+
+.animated .slider:before {
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+input:checked + .slider {
+ background-color: #fc0303;
+}
+
+input:checked + .slider:before {
+ -webkit-transform: translateX(16px);
+ -ms-transform: translateX(16px);
+ transform: translateX(16px);
+}
+
+/* Rounded sliders */
+.slider.round {
+ border-radius: 34px;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
+
+
+/* Boilerplate CSS from https://ajay.app */
+
+body {
+ background-color: #333333;
+}
+
+.projectPreview {
+ position: relative;
+}
+
+.projectPreviewImage {
+ position: absolute;
+ left: -90px;
+ width: 80px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.projectPreviewImageLarge {
+ position: absolute;
+ left: -210px;
+ width: 200px;
+ top: 50%;
+ transform: translateY(-20%);
+}
+
+.projectPreviewImageLargeRight {
+ position: absolute;
+ right: -210px;
+ width: 200px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.createdBy {
+ font-size: 14px;
+ text-align: center;
+ padding-top: 0px;
+ padding-bottom: 0px;
+
+ display: inline-block;
+}
+
+#title {
+ background-color: #636363;
+
+ text-align: center;
+ vertical-align: middle;
+
+ font-size: 50px;
+ color: #212121;
+
+ padding: 20px;
+
+ text-decoration: none;
+
+ transition: font-size 1s;
+}
+
+.subtitle {
+ font-size: 40px;
+ color: #dad8d8;
+
+ padding-top: 10px;
+
+ transition: font-size 0.4s;
+}
+
+.subtitle:hover {
+ font-size: 45px;
+
+ transition: font-size 0.4s;
+}
+
+.profilepic {
+ background-color: #636363 !important;
+ vertical-align: middle;
+}
+
+.profilepiccircle {
+ vertical-align: middle;
+ overflow: hidden;
+ border-radius: 50%;
+}
+
+a {
+ text-decoration: underline;
+ color: inherit;
+}
+
+.link {
+ padding: 20px;
+
+ height: 80px;
+
+ transition: height 0.2s;
+}
+
+.link:hover {
+ height: 95px;
+
+ transition: height 0.2s;
+}
+
+#contact,.smalllink {
+ font-size: 25px;
+ color: #e8e8e8;
+
+ text-align: center;
+
+ padding: 10px;
+}
+
+#contact {
+ text-decoration: none;
+}
+
+p,li {
+ font-size: 20px;
+ color: #c4c4c4;
+
+ padding: 10px;
+}
+
+p,li,code,a {
+ max-width: 60%;
+ text-align: left;
+ overflow-wrap: break-word;
+}
+
+@media screen and (orientation:portrait) {
+ p,li,code,a {
+ max-width: 100%;
+ }
+
+ .projectPreviewImage {
+ position: unset;
+ width: 130px;
+ display: block;
+ margin: auto;
+ transform: none;
+ }
+}
+
+.previewImage {
+ max-height: 200px;
+}
+
+img {
+ max-width: 100%;
+
+ text-align: center;
+}
+
+#recentPostTitle {
+ font-size: 30px;
+ color: #dad8d8;
+}
+
+#recentPostDate {
+ font-size: 15px;
+ color: #dad8d8;
+}
+
+h1,h2,h3,h4,h5,h6 {
+ color: #dad8d8;
+}
+
+svg {
+ text-decoration: none;
+}
+
+.number-container:before {
+ content: attr(label-name);
+ padding-right: 4px;
+ width: max-content;
+
+ font-size: 14px;
+ color: white;
+}
+
+/* React styles */
+
+.categoryTableElement {
+ font-size: 16px;
+
+ color: white;
+}
+
+.categoryTableElement > * {
+ padding-right: 15px;
+ padding-bottom: 15px;
+}
+
+.optionsSelector {
+ background-color: #c00000;
+ color: white;
+
+ border: none;
+ font-size: 14px;
+ padding: 5px;
+ border-radius: 5px;
+}
+
+.categoryColorTextBox {
+ width: 60px;
+
+ background: none;
+ border: none;
+}
+
+#subsidizedPrice {
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+#discountButton {
+ text-decoration: underline;
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/src/background.ts b/src/background.ts
index 6c92a10d..80f0cd8d 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -14,6 +14,8 @@ const utils = new Utils({
unregisterFirefoxContentScript
});
+const popupPort: Record<string, chrome.runtime.Port> = {};
+
// Used only on Firefox, which does not support non persistent background pages.
const contentScriptRegistrations = {};
@@ -53,7 +55,7 @@ if (!Config.configSyncListeners.includes(onNavigationApiAvailableChange)) {
Config.configSyncListeners.push(onNavigationApiAvailableChange);
}
-chrome.runtime.onMessage.addListener(function (request, _, callback) {
+chrome.runtime.onMessage.addListener(function (request, sender, callback) {
switch(request.message) {
case "openConfig":
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
@@ -100,9 +102,25 @@ chrome.runtime.onMessage.addListener(function (request, _, callback) {
});
return true;
}
+ case "time":
+ if (sender.tab) {
+ popupPort[sender.tab.id]?.postMessage(request);
+ }
+ return false;
}
});
+chrome.runtime.onConnect.addListener((port) => {
+ if (port.name === "popup") {
+ chrome.tabs.query({
+ active: true,
+ currentWindow: true
+ }, tabs => {
+ popupPort[tabs[0].id] = port;
+ });
+ }
+});
+
//add help page on install
chrome.runtime.onInstalled.addListener(function () {
// This let's the config sync to run fully before checking.
@@ -116,7 +134,7 @@ chrome.runtime.onInstalled.addListener(function () {
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
//generate a userID
- const newUserID = utils.generateUserID();
+ const newUserID = GenericUtils.generateUserID();
//save this UUID
Config.config.userID = newUserID;
@@ -165,7 +183,7 @@ async function submitVote(type: number, UUID: string, category: string) {
if (userID == undefined || userID === "undefined") {
//generate one
- userID = utils.generateUserID();
+ userID = GenericUtils.generateUserID();
Config.config.userID = userID;
}
diff --git a/src/components/ChapterVoteComponent.tsx b/src/components/ChapterVoteComponent.tsx
new file mode 100644
index 00000000..b1f590a7
--- /dev/null
+++ b/src/components/ChapterVoteComponent.tsx
@@ -0,0 +1,121 @@
+import * as React from "react";
+import Config from "../config";
+import { Category, SegmentUUID, SponsorTime } from "../types";
+
+import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
+import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
+import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
+import { VoteResponse } from "../messageTypes";
+import { AnimationUtils } from "../utils/animationUtils";
+import { GenericUtils } from "../utils/genericUtils";
+import { Tooltip } from "../render/Tooltip";
+
+export interface ChapterVoteProps {
+ vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
+}
+
+export interface ChapterVoteState {
+ segment?: SponsorTime;
+ show: boolean;
+}
+
+class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVoteState> {
+ tooltip?: Tooltip;
+
+ constructor(props: ChapterVoteProps) {
+ super(props);
+
+ this.state = {
+ segment: null,
+ show: false
+ };
+ }
+
+ render(): React.ReactElement {
+ return (
+ <>
+ {/* Upvote Button */}
+ <button id={"sponsorTimesDownvoteButtonsContainerUpvoteChapter"}
+ className={"playerButton sbPlayerUpvote ytp-button " + (!this.state.show ? "hidden" : "")}
+ draggable="false"
+ title={chrome.i18n.getMessage("upvoteButtonInfo")}
+ onClick={(e) => this.vote(e, 1)}>
+ <ThumbsUpSvg className="playerButtonImage"
+ fill={Config.config.colorPalette.white}
+ width={"inherit"} height={"inherit"} />
+ </button>
+
+ {/* Downvote Button */}
+ <button id={"sponsorTimesDownvoteButtonsContainerDownvoteChapter"}
+ className={"playerButton sbPlayerDownvote ytp-button " + (!this.state.show ? "hidden" : "")}
+ draggable="false"
+ title={chrome.i18n.getMessage("reportButtonInfo")}
+ onClick={(e) => {
+ const chapterNode = document.querySelector(".ytp-chapter-container") as HTMLElement;
+
+ if (this.tooltip) {
+ this.tooltip.close();
+ this.tooltip = null;
+ } else {
+ const referenceNode = chapterNode?.parentElement?.parentElement;
+ if (referenceNode) {
+ const outerBounding = referenceNode.getBoundingClientRect();
+ const buttonBounding = (e.target as HTMLElement)?.parentElement?.getBoundingClientRect();
+
+ this.tooltip = new Tooltip({
+ referenceNode: chapterNode?.parentElement?.parentElement,
+ prependElement: chapterNode?.parentElement,
+ showLogo: false,
+ showGotIt: false,
+ bottomOffset: `${outerBounding.height + 25}px`,
+ leftOffset: `${buttonBounding.x - outerBounding.x}px`,
+ extraClass: "centeredSBTriangle",
+ buttons: [
+ {
+ name: chrome.i18n.getMessage("incorrectVote"),
+ listener: (event) => this.vote(event, 0, e.target as HTMLElement).then(() => {
+ this.tooltip?.close();
+ this.tooltip = null;
+ })
+ }, {
+ name: chrome.i18n.getMessage("harmfulVote"),
+ listener: (event) => this.vote(event, 30, e.target as HTMLElement).then(() => {
+ this.tooltip?.close();
+ this.tooltip = null;
+ })
+ }
+ ]
+ });
+ }
+ }
+ }}>
+ <ThumbsDownSvg
+ className="playerButtonImage"
+ fill={downvoteButtonColor(this.state.segment ? [this.state.segment] : null, SkipNoticeAction.Downvote, SkipNoticeAction.Downvote)}
+ width={"inherit"}
+ height={"inherit"} />
+ </button>
+ </>
+ );
+ }
+
+ private async vote(event: React.MouseEvent, type: number, element?: HTMLElement): Promise<void> {
+ event.stopPropagation();
+ if (this.state.segment) {
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(element ?? event.currentTarget as HTMLElement, 0.3);
+
+ const response = await this.props.vote(type, this.state.segment.UUID);
+ await stopAnimation();
+
+ if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
+ this.setState({
+ show: type === 1
+ });
+ } else if (response.statusCode !== 403) {
+ alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
+ }
+ }
+ }
+}
+
+export default ChapterVoteComponent;
diff --git a/src/components/NoticeComponent.tsx b/src/components/NoticeComponent.tsx
index 83540875..cf108671 100644
--- a/src/components/NoticeComponent.tsx
+++ b/src/components/NoticeComponent.tsx
@@ -11,6 +11,7 @@ export interface NoticeProps {
noticeTitle: string,
maxCountdownTime?: () => number,
+ dontPauseCountdown?: boolean,
amountOfPreviousNotices?: number,
showInSecondSlot?: boolean,
timed?: boolean,
@@ -25,6 +26,8 @@ export interface NoticeProps {
smaller?: boolean,
limitWidth?: boolean,
extraClass?: string,
+ hideLogo?: boolean,
+ hideRightInfo?: boolean,
// Callback for when this is closed
closeListener: () => void,
@@ -117,13 +120,15 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
{/* Left column */}
<td className="noticeLeftIcon">
{/* Logo */}
- <img id={"sponsorSkipLogo" + this.idSuffix}
- className="sponsorSkipLogo sponsorSkipObject"
- src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
- </img>
+ {!this.props.hideLogo &&
+ <img id={"sponsorSkipLogo" + this.idSuffix}
+ className="sponsorSkipLogo sponsorSkipObject"
+ src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
+ </img>
+ }
<span id={"sponsorSkipMessage" + this.idSuffix}
- style={{float: "left"}}
+ style={{float: "left", marginRight: this.props.hideLogo ? "0px" : null}}
className="sponsorSkipMessage sponsorSkipObject">
{this.props.noticeTitle}
@@ -135,28 +140,30 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
{this.props.firstRow}
{/* Right column */}
- <td className="sponsorSkipNoticeRightSection"
- style={{top: "9.32px"}}>
+ {!this.props.hideRightInfo &&
+ <td className="sponsorSkipNoticeRightSection"
+ style={{top: "9.32px"}}>
+
+ {/* Time left */}
+ {this.props.timed ? (
+ <span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
+ onClick={() => this.toggleManualPause()}
+ className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
+
+ {this.getCountdownElements()}
+
+ </span>
+ ) : ""}
- {/* Time left */}
- {this.props.timed ? (
- <span id={"sponsorSkipNoticeTimeLeft" + this.idSuffix}
- onClick={() => this.toggleManualPause()}
- className="sponsorSkipObject sponsorSkipNoticeTimeLeft">
-
- {this.getCountdownElements()}
-
- </span>
- ) : ""}
-
-
- {/* Close button */}
- <img src={chrome.extension.getURL("icons/close.png")}
- className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
- + (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
- onClick={() => this.close()}>
- </img>
- </td>
+
+ {/* Close button */}
+ <img src={chrome.extension.getURL("icons/close.png")}
+ className={"sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton"
+ + (this.props.biggerCloseButton ? " biggerCloseButton" : "")}
+ onClick={() => this.close()}>
+ </img>
+ </td>
+ }
</tr>
{this.props.children}
@@ -289,7 +296,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
}
pauseCountdown(): void {
- if (!this.props.timed) return;
+ if (!this.props.timed || this.props.dontPauseCountdown) return;
//remove setInterval
if (this.countdownInterval) clearInterval(this.countdownInterval);
diff --git a/src/components/SelectorComponent.tsx b/src/components/SelectorComponent.tsx
new file mode 100644
index 00000000..018d78a6
--- /dev/null
+++ b/src/components/SelectorComponent.tsx
@@ -0,0 +1,55 @@
+import * as React from "react";
+
+export interface SelectorOption {
+ label: string;
+}
+
+export interface SelectorProps {
+ id: string;
+ options: SelectorOption[];
+ onChange: (value: string) => void;
+}
+
+export interface SelectorState {
+
+}
+
+class SelectorComponent extends React.Component<SelectorProps, SelectorState> {
+
+ constructor(props: SelectorProps) {
+ super(props);
+
+ // Setup state
+ this.state = {
+
+ }
+ }
+
+ render(): React.ReactElement {
+ return (
+ <div id={this.props.id}
+ className="sbSelector">
+ <div className="sbSelectorBackground">
+ {this.getOptions()}
+ </div>
+ </div>
+ );
+ }
+
+ getOptions(): React.ReactElement[] {
+ const result: React.ReactElement[] = [];
+ for (const option of this.props.options) {
+ result.push(
+ <div className="sbSelectorOption"
+ onClick={() => this.props.onChange(option.label)}
+ key={option.label}>
+ {option.label}
+ </div>
+ );
+ }
+
+ return result;
+ }
+}
+
+export default SelectorComponent; \ No newline at end of file
diff --git a/src/components/SkipNoticeComponent.tsx b/src/components/SkipNoticeComponent.tsx
index 7de0599d..f42c0396 100644
--- a/src/components/SkipNoticeComponent.tsx
+++ b/src/components/SkipNoticeComponent.tsx
@@ -13,6 +13,7 @@ import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
import PencilSvg from "../svg-icons/pencil_svg";
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
+import { GenericUtils } from "../utils/genericUtils";
enum SkipButtonState {
Undo, // Unskip
@@ -540,7 +541,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
const sponsorTimesSubmitting : SponsorTime = {
segment: this.segments[index].segment,
- UUID: utils.generateUserID() as SegmentUUID,
+ UUID: GenericUtils.generateUserID() as SegmentUUID,
category: this.segments[index].category,
actionType: this.segments[index].actionType,
source: SponsorSourceType.Local
diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx
index 42a8d070..1515644a 100644
--- a/src/components/SponsorTimeEditComponent.tsx
+++ b/src/components/SponsorTimeEditComponent.tsx
@@ -1,10 +1,12 @@
import * as React from "react";
import * as CompileConfig from "../../config.json";
import Config from "../config";
-import { ActionType, Category, ContentContainer, SponsorTime } from "../types";
+import { ActionType, Category, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
import Utils from "../utils";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
import { RectangleTooltip } from "../render/RectangleTooltip";
+import SelectorComponent, { SelectorOption } from "./SelectorComponent";
+import { GenericUtils } from "../utils/genericUtils";
const utils = new Utils();
@@ -25,16 +27,23 @@ export interface SponsorTimeEditState {
editing: boolean;
sponsorTimeEdits: [string, string];
selectedCategory: Category;
+ description: string;
+ suggestedNames: SelectorOption[];
+ chapterNameSelectorOpen: boolean;
}
const DEFAULT_CATEGORY = "chooseACategory";
+const categoryNamesGrams: string[] = [].concat(...CompileConfig.categoryList.filter((name) => name !== "chapter")
+ .map((name) => chrome.i18n.getMessage("category_" + name).split(/\/|\s|-/)));
+
class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, SponsorTimeEditState> {
idSuffix: string;
categoryOptionRef: React.RefObject<HTMLSelectElement>;
actionTypeOptionRef: React.RefObject<HTMLSelectElement>;
+ descriptionOptionRef: React.RefObject<HTMLInputElement>;
configUpdateListener: () => void;
@@ -42,26 +51,35 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
// Used when selecting POI or Full
timesBeforeChanging: number[] = [];
fullVideoWarningShown = false;
+ categoryNameWarningShown = false;
+
+ // For description auto-complete
+ fetchingSuggestions: boolean;
constructor(props: SponsorTimeEditProps) {
super(props);
this.categoryOptionRef = React.createRef();
this.actionTypeOptionRef = React.createRef();
+ this.descriptionOptionRef = React.createRef();
this.idSuffix = this.props.idSuffix;
-
this.previousSkipType = ActionType.Skip;
+
+ const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
this.state = {
editing: false,
sponsorTimeEdits: [null, null],
- selectedCategory: DEFAULT_CATEGORY as Category
+ selectedCategory: DEFAULT_CATEGORY as Category,
+ description: sponsorTime.description || "",
+ suggestedNames: [],
+ chapterNameSelectorOpen: false
};
}
componentDidMount(): void {
// Prevent inputs from triggering key events
- document.getElementById("sponsorTimesContainer" + this.idSuffix).addEventListener('keydown', function (event) {
+ document.getElementById("sponsorTimeEditContainer" + this.idSuffix).addEventListener('keydown', function (event) {
event.stopPropagation();
});
@@ -87,6 +105,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
render(): React.ReactElement {
this.checkToShowFullVideoWarning();
+ this.checkToShowChapterWarning();
const style: React.CSSProperties = {
textAlign: "center"
@@ -118,8 +137,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
type="text"
style={{color: "inherit", backgroundColor: "inherit"}}
value={this.state.sponsorTimeEdits[0]}
- onChange={(e) => {this.handleOnChange(0, e, sponsorTime, e.target.value)}}
- onWheel={(e) => {this.changeTimesWhenScrolling(0, e, sponsorTime)}}>
+ onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
+ onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
</input>
{sponsorTime.actionType !== ActionType.Poi ? (
@@ -133,8 +152,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
type="text"
style={{color: "inherit", backgroundColor: "inherit"}}
value={this.state.sponsorTimeEdits[1]}
- onChange={(e) => {this.handleOnChange(1, e, sponsorTime, e.target.value)}}
- onWheel={(e) => {this.changeTimesWhenScrolling(1, e, sponsorTime)}}>
+ onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)}
+ onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}>
</input>
<span id={"nowButton1" + this.idSuffix}
@@ -159,15 +178,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
style={timeDisplayStyle}
className="sponsorTimeDisplay"
onClick={this.toggleEditTime.bind(this)}>
- {utils.getFormattedTime(segment[0], true) +
+ {GenericUtils.getFormattedTime(segment[0], true) +
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi)
- ? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segment[1], true) : "")}
+ ? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(segment[1], true) : "")}
</div>
);
}
return (
- <div style={style}>
+ <div id={"sponsorTimeEditContainer" + this.idSuffix} style={style}>
{timeDisplay}
@@ -178,7 +197,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
defaultValue={sponsorTime.category}
ref={this.categoryOptionRef}
style={{color: "inherit", backgroundColor: "inherit"}}
- onChange={this.categorySelectionChange.bind(this)}>
+ onChange={(event) => this.categorySelectionChange(event)}>
{this.getCategoryOptions()}
</select>
@@ -209,6 +228,27 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
</div>
): ""}
+ {/* Chapter Name */}
+ {sponsorTime.actionType === ActionType.Chapter ? (
+ <div onMouseLeave={() => this.setState({chapterNameSelectorOpen: false})}>
+ <input id={"chapterName" + this.idSuffix}
+ className="sponsorTimeEdit"
+ ref={this.descriptionOptionRef}
+ type="text"
+ value={this.state.description}
+ onChange={(e) => this.descriptionUpdate(e.target.value)}
+ onFocus={() => this.setState({chapterNameSelectorOpen: true})}>
+ </input>
+ {this.state.chapterNameSelectorOpen && this.state.description &&
+ <SelectorComponent
+ id={"chapterNameSelector" + this.idSuffix}
+ options={this.state.suggestedNames}
+ onChange={(v) => this.descriptionUpdate(v)}
+ />
+ }
+ </div>
+ ): ""}
+
<br/>
{/* Editing Tools */}
@@ -223,7 +263,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
<span id={"sponsorTimePreviewButton" + this.idSuffix}
className="sponsorTimeEditButton"
onClick={(e) => this.previewTime(e.ctrlKey, e.shiftKey)}>
- {chrome.i18n.getMessage("preview")}
+ {sponsorTime.actionType !== ActionType.Chapter ? chrome.i18n.getMessage("preview")
+ : chrome.i18n.getMessage("End")}
</span>
): ""}
@@ -250,16 +291,15 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const sponsorTimeEdits = this.state.sponsorTimeEdits;
// check if change is small engough to show tooltip
- const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
- const after = utils.getFormattedTimeToSeconds(targetValue);
+ const before = GenericUtils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
+ const after = GenericUtils.getFormattedTimeToSeconds(targetValue);
const difference = Math.abs(before - after);
- if (0 < difference && difference< 0.5) this.showScrollToEditToolTip();
+ if (0 < difference && difference < 0.5) this.showScrollToEditToolTip();
sponsorTimeEdits[index] = targetValue;
if (index === 0 && sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = targetValue;
- this.setState({sponsorTimeEdits});
- this.saveEditTimes();
+ this.setState({sponsorTimeEdits}, () => this.saveEditTimes());
}
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void {
@@ -275,7 +315,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
const sponsorTimeEdits = this.state.sponsorTimeEdits;
- let timeAsNumber = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
+ let timeAsNumber = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
if (timeAsNumber !== null && e.deltaY != 0) {
if (e.deltaY < 0) {
timeAsNumber += step;
@@ -284,7 +324,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
} else {
timeAsNumber = 0;
}
- sponsorTimeEdits[index] = utils.getFormattedTime(timeAsNumber, true);
+
+ sponsorTimeEdits[index] = GenericUtils.getFormattedTime(timeAsNumber, true);
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
this.setState({sponsorTimeEdits});
@@ -294,26 +335,29 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
showScrollToEditToolTip(): void {
if (!Config.config.scrollToEditTimeUpdate && document.getElementById("sponsorRectangleTooltip" + "sponsorTimesContainer" + this.idSuffix) === null) {
- this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), () => { Config.config.scrollToEditTimeUpdate = true });
+ this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), "scrollToEdit", () => { Config.config.scrollToEditTimeUpdate = true });
}
}
- showToolTip(text: string, buttonFunction?: () => void): boolean {
+ showToolTip(text: string, id: string, buttonFunction?: () => void): boolean {
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
- if (element) {
- new RectangleTooltip({
- text,
- referenceNode: element.parentElement,
- prependElement: element,
- timeout: 15,
- bottomOffset: 0 + "px",
- leftOffset: -318 + "px",
- backgroundColor: "rgba(28, 28, 28, 1.0)",
- htmlId: "sponsorTimesContainer" + this.idSuffix,
- buttonFunction,
- fontSize: "14px",
- maxHeight: "200px"
- });
+ if (element) {
+ const htmlId = `sponsorRectangleTooltip${id + this.idSuffix}`;
+ if (!document.getElementById(htmlId)) {
+ new RectangleTooltip({
+ text,
+ referenceNode: element.parentElement,
+ prependElement: element,
+ timeout: 15,
+ bottomOffset: 0 + "px",
+ leftOffset: -318 + "px",
+ backgroundColor: "rgba(28, 28, 28, 1.0)",
+ htmlId,
+ buttonFunction,
+ fontSize: "14px",
+ maxHeight: "200px"
+ });
+ }
return true;
} else {
@@ -328,12 +372,25 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
if (videoPercentage > 0.6 && !this.fullVideoWarningShown
&& (sponsorTime.category === "sponsor" || sponsorTime.category === "selfpromo" || sponsorTime.category === "chooseACategory")) {
- if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"))) {
+ if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"), "fullVideoWarning")) {
this.fullVideoWarningShown = true;
}
}
}
+ checkToShowChapterWarning(): void {
+ const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
+
+ if (sponsorTime.actionType === ActionType.Chapter && sponsorTime.description
+ && !this.categoryNameWarningShown
+ && categoryNamesGrams.some(
+ (category) => sponsorTime.description.toLowerCase().includes(category.toLowerCase()))) {
+ if (this.showToolTip(chrome.i18n.getMessage("chapterNameTooltipWarning"), "chapterWarning")) {
+ this.categoryNameWarningShown = true;
+ }
+ }
+ }
+
getCategoryOptions(): React.ReactElement[] {
const elements = [(
<option value={DEFAULT_CATEGORY}
@@ -343,6 +400,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
)];
for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) {
+ // If permission not loaded, treat it like we have permission except chapter
+ const defaultBlockCategories = ["chapter"];
+ const permission = Config.config.permissions[category as Category];
+ if ((defaultBlockCategories.includes(category) || permission !== undefined) && !permission) continue;
+
elements.push(
<option value={category}
key={category}
@@ -363,7 +425,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
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)) {
+ if (chosenCategory !== DEFAULT_CATEGORY && !Config.config.categorySelections.some((category) => category.name === chosenCategory)) {
event.target.value = DEFAULT_CATEGORY;
// Alert that they have to enable this category first
@@ -464,7 +526,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.setState({
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
- }, this.saveEditTimes);
+ }, () => this.saveEditTimes());
}
toggleEditTime(): void {
@@ -487,16 +549,16 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
/** Returns an array in the sponsorTimeEdits form (formatted time string) from a normal seconds sponsor time */
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
- return [utils.getFormattedTime(sponsorTime.segment[0], true),
- utils.getFormattedTime(sponsorTime.segment[1], true)];
+ return [GenericUtils.getFormattedTime(sponsorTime.segment[0], true),
+ GenericUtils.getFormattedTime(sponsorTime.segment[1], true)];
}
saveEditTimes(): void {
const sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
if (this.state.editing) {
- const startTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
- const endTime = utils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
+ const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
+ const endTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
// Change segment time only if the format was correct
if (startTime !== null && endTime !== null) {
@@ -507,8 +569,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const category = this.categoryOptionRef.current.value as Category
sponsorTimesSubmitting[this.props.index].category = category;
- const inputActionType = this.actionTypeOptionRef?.current?.value as ActionType;
- sponsorTimesSubmitting[this.props.index].actionType = this.getNextActionType(category, inputActionType);
+ const actionType = this.getNextActionType(category, this.actionTypeOptionRef?.current?.value as ActionType);
+ sponsorTimesSubmitting[this.props.index].actionType = actionType;
+
+ const description = actionType === ActionType.Chapter ? this.descriptionOptionRef?.current?.value : "";
+ sponsorTimesSubmitting[this.props.index].description = description;
Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
Config.forceSyncUpdate("unsubmittedSegments");
@@ -530,19 +595,19 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
previewTime(ctrlPressed = false, shiftPressed = false): void {
const sponsorTimes = this.props.contentContainer().sponsorTimesSubmitting;
const index = this.props.index;
-
- const skipTime = sponsorTimes[index].segment[0];
- // If segment starts at 0:00, start playback at the end of the segment
- if (skipTime === 0) {
- this.props.contentContainer().previewTime(sponsorTimes[index].segment[1]);
- return;
- }
-
let seekTime = 2;
if (ctrlPressed) seekTime = 0.5;
if (shiftPressed) seekTime = 0.25;
- this.props.contentContainer().previewTime(skipTime - (seekTime * this.props.contentContainer().v.playbackRate));
+ const startTime = sponsorTimes[index].segment[0];
+ const endTime = sponsorTimes[index].segment[1];
+ const isChapter = sponsorTimes[index].actionType === ActionType.Chapter;
+
+ // If segment starts at 0:00, start playback at the end of the segment
+ const skipToEndTime = startTime === 0 || isChapter;
+ const skipTime = skipToEndTime ? endTime : (startTime - (seekTime * this.props.contentContainer().v.playbackRate));
+
+ this.props.contentContainer().previewTime(skipTime, !isChapter);
}
inspectTime(): void {
@@ -586,6 +651,41 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
}
+ descriptionUpdate(description: string): void {
+ this.setState({
+ description
+ });
+
+ if (!this.fetchingSuggestions) {
+ this.fetchSuggestions(description);
+ }
+
+ this.saveEditTimes();
+ }
+
+ async fetchSuggestions(description: string): Promise<void> {
+ if (this.props.contentContainer().channelIDInfo.status !== ChannelIDStatus.Found) return;
+
+ this.fetchingSuggestions = true;
+ const result = await utils.asyncRequestToServer("GET", "/api/chapterNames", {
+ description,
+ channelID: this.props.contentContainer().channelIDInfo.id
+ });
+
+ if (result.ok) {
+ try {
+ const names = JSON.parse(result.responseText) as {description: string}[];
+ this.setState({
+ suggestedNames: names.map(n => ({
+ label: n.description
+ }))
+ });
+ } catch (e) {} //eslint-disable-line no-empty
+ }
+
+ this.fetchingSuggestions = false;
+ }
+
configUpdate(): void {
this.forceUpdate();
}
diff --git a/src/components/SubmissionNoticeComponent.tsx b/src/components/SubmissionNoticeComponent.tsx
index fea0c762..7997303b 100644
--- a/src/components/SubmissionNoticeComponent.tsx
+++ b/src/components/SubmissionNoticeComponent.tsx
@@ -73,12 +73,19 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
}
render(): React.ReactElement {
+ const sortButton =
+ <img id={"sponsorSkipSortButton" + this.state.idSuffix}
+ className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipSmallButton"
+ onClick={() => this.sortSegments()}
+ src={chrome.extension.getURL("icons/sort.svg")}>
+ </img>;
return (
<NoticeComponent noticeTitle={this.state.noticeTitle}
idSuffix={this.state.idSuffix}
ref={this.noticeRef}
closeListener={this.cancel.bind(this)}
- zIndex={5000}>
+ zIndex={5000}
+ firstColumn={sortButton}>
{/* Text Boxes */}
{this.getMessageBoxes()}
@@ -198,6 +205,16 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
this.cancel();
}
+ sortSegments(): void {
+ let sponsorTimesSubmitting = this.props.contentContainer().sponsorTimesSubmitting;
+ sponsorTimesSubmitting = sponsorTimesSubmitting.sort((a, b) => a.segment[0] - b.segment[0]);
+
+ Config.config.unsubmittedSegments[this.props.contentContainer().sponsorVideoID] = sponsorTimesSubmitting;
+ Config.forceSyncUpdate("unsubmittedSegments");
+
+ this.forceUpdate();
+ }
+
categoryChangeListener(index: number, category: Category): void {
const dialogWidth = this.noticeRef?.current?.getElement()?.current?.offsetWidth;
if (category !== "chooseACategory" && Config.config.showCategoryGuidelines
diff --git a/src/components/CategoryChooserComponent.tsx b/src/components/options/CategoryChooserComponent.tsx
index 763254b9..1da4f641 100644
--- a/src/components/CategoryChooserComponent.tsx
+++ b/src/components/options/CategoryChooserComponent.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
-import * as CompileConfig from "../../config.json";
-import { Category } from "../types";
+import * as CompileConfig from "../../../config.json";
+import { Category } from "../../types";
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";
export interface CategoryChooserProps {
diff --git a/src/components/CategorySkipOptionsComponent.tsx b/src/components/options/CategorySkipOptionsComponent.tsx
index 4c68e957..338435a8 100644
--- a/src/components/CategorySkipOptionsComponent.tsx
+++ b/src/components/options/CategorySkipOptionsComponent.tsx
@@ -1,10 +1,13 @@
import * as React from "react";
-import Config from "../config"
-import * as CompileConfig from "../../config.json";
-import { Category, CategorySkipOption } from "../types";
+import Config from "../../config"
+import * as CompileConfig from "../../../config.json";
+import { Category, CategorySkipOption } from "../../types";
-import { getCategorySuffix } from "../utils/categoryUtils";
+import { getCategorySuffix } from "../../utils/categoryUtils";
+import ToggleOptionComponent, { ToggleOptionProps } from "./ToggleOptionComponent";
+import { fetchingChaptersAllowed } from "../../utils/licenseKey";
+import LockSvg from "../../svg-icons/lock_svg";
export interface CategorySkipOptionsProps {
category: Category;
@@ -15,6 +18,7 @@ export interface CategorySkipOptionsProps {
export interface CategorySkipOptionsState {
color: string;
previewColor: string;
+ hideChapter: boolean;
}
class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsProps, CategorySkipOptionsState> {
@@ -27,7 +31,14 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
this.state = {
color: props.defaultColor || Config.config.barTypes[this.props.category]?.color,
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color,
- }
+ hideChapter: true
+ };
+
+ fetchingChaptersAllowed().then((allowed) => {
+ this.setState({
+ hideChapter: !allowed
+ });
+ })
}
render(): React.ReactElement {
@@ -51,12 +62,25 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
}
}
+ let extraClasses = "";
+ const disabled = this.props.category === "chapter" && this.state.hideChapter;
+ if (disabled) {
+ extraClasses += " disabled";
+
+ if (!Config.config.showUpsells) {
+ return <></>;
+ }
+ }
+
return (
<>
<tr id={this.props.category + "OptionsRow"}
- className="categoryTableElement">
+ className={`categoryTableElement${extraClasses}`} >
<td id={this.props.category + "OptionName"}
className="categoryTableLabel">
+ {disabled &&
+ <LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
+ }
{chrome.i18n.getMessage("category_" + this.props.category)}
</td>
@@ -65,21 +89,25 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
<select
className="optionsSelector"
defaultValue={defaultOption}
+ disabled={disabled}
onChange={this.skipOptionSelected.bind(this)}>
{this.getCategorySkipOptions()}
</select>
</td>
- <td id={this.props.category + "ColorOption"}
- className="colorOption">
- <input
- className="categoryColorTextBox option-text-box"
- type="color"
- onChange={(event) => this.setColorState(event, false)}
- value={this.state.color} />
- </td>
+ {this.props.category !== "chapter" &&
+ <td id={this.props.category + "ColorOption"}
+ className="colorOption">
+ <input
+ className="categoryColorTextBox option-text-box"
+ type="color"
+ disabled={disabled}
+ onChange={(event) => this.setColorState(event, false)}
+ value={this.state.color} />
+ </td>
+ }
- {this.props.category !== "exclusive_access" &&
+ {!["chapter", "exclusive_access"].includes(this.props.category) &&
<td id={this.props.category + "PreviewColorOption"}
className="previewColorOption">
<input
@@ -93,7 +121,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</tr>
<tr id={this.props.category + "DescriptionRow"}
- className="small-description categoryTableDescription">
+ className={`small-description categoryTableDescription${extraClasses}`}>
<td
colSpan={2}>
{chrome.i18n.getMessage("category_" + this.props.category + "_description")}
@@ -103,6 +131,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</a>
</td>
</tr>
+
+ {this.getExtraOptionComponents(this.props.category, extraClasses, disabled)}
</>
);
@@ -147,7 +177,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
const elements: JSX.Element[] = [];
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
- if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
+ if (this.props.category === "chapter") optionNames = ["disable", "showOverlay"]
+ else if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
for (const optionName of optionNames) {
elements.push(
@@ -184,6 +215,42 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
Config.config.barTypes = Config.config.barTypes;
}, 50);
}
+
+ getExtraOptionComponents(category: string, extraClasses: string, disabled: boolean): JSX.Element[] {
+ const result = [];
+ for (const option of this.getExtraOptions(category)) {
+ result.push(
+ <tr key={option.configKey} className={extraClasses}>
+ <td id={`${category}_${option.configKey}`} className="categoryExtraOptions">
+ <ToggleOptionComponent
+ configKey={option.configKey}
+ label={option.label}
+ disabled={disabled}
+ />
+ </td>
+ </tr>
+ )
+ }
+
+ return result;
+ }
+
+ getExtraOptions(category: string): ToggleOptionProps[] {
+ switch (category) {
+ case "chapter":
+ return [{
+ configKey: "renderSegmentsAsChapters",
+ label: chrome.i18n.getMessage("renderAsChapters"),
+ }];
+ case "music_offtopic":
+ return [{
+ configKey: "autoSkipOnMusicVideos",
+ label: chrome.i18n.getMessage("autoSkipOnMusicVideos"),
+ }];
+ default:
+ return [];
+ }
+ }
}
export default CategorySkipOptionsComponent; \ No newline at end of file
diff --git a/src/components/KeybindComponent.tsx b/src/components/options/KeybindComponent.tsx
index 17aefcbb..34345301 100644
--- a/src/components/KeybindComponent.tsx
+++ b/src/components/options/KeybindComponent.tsx
@@ -1,9 +1,9 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
-import Config from "../config";
-import { Keybind } from "../types";
+import Config from "../../config";
+import { Keybind } from "../../types";
import KeybindDialogComponent from "./KeybindDialogComponent";
-import { keybindEquals, keybindToString, formatKey } from "../utils/configUtils";
+import { keybindEquals, keybindToString, formatKey } from "../../utils/configUtils";
export interface KeybindProps {
option: string;
diff --git a/src/components/KeybindDialogComponent.tsx b/src/components/options/KeybindDialogComponent.tsx
index c4e7cf48..7c8684a7 100644
--- a/src/components/KeybindDialogComponent.tsx
+++ b/src/components/options/KeybindDialogComponent.tsx
@@ -1,8 +1,8 @@
import * as React from "react";
import { ChangeEvent } from "react";
-import Config from "../config";
-import { Keybind } from "../types";
-import { keybindEquals, formatKey } from "../utils/configUtils";
+import Config from "../../config";
+import { Keybind } from "../../types";
+import { keybindEquals, formatKey } from "../../utils/configUtils";
export interface KeybindDialogProps {
option: string;
diff --git a/src/components/options/ToggleOptionComponent.tsx b/src/components/options/ToggleOptionComponent.tsx
new file mode 100644
index 00000000..c6ea698d
--- /dev/null
+++ b/src/components/options/ToggleOptionComponent.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+
+import Config from "../../config";
+
+export interface ToggleOptionProps {
+ configKey: string;
+ label: string;
+ disabled?: boolean;
+}
+
+export interface ToggleOptionState {
+ enabled: boolean;
+}
+
+class ToggleOptionComponent extends React.Component<ToggleOptionProps, ToggleOptionState> {
+
+ constructor(props: ToggleOptionProps) {
+ super(props);
+
+ // Setup state
+ this.state = {
+ enabled: Config.config[props.configKey]
+ }
+ }
+
+ render(): React.ReactElement {
+ return (
+ <div>
+ <div className="switch-container">
+ <label className="switch">
+ <input id={this.props.configKey}
+ type="checkbox"
+ checked={this.state.enabled}
+ disabled={this.props.disabled}
+ onChange={(e) => this.clicked(e)}/>
+ <span className="slider round"></span>
+ </label>
+ <label className="switch-label" htmlFor={this.props.configKey}>
+ {this.props.label}
+ </label>
+ </div>
+ </div>
+ );
+ }
+
+ clicked(event: React.ChangeEvent<HTMLInputElement>): void {
+ Config.config[this.props.configKey] = event.target.checked;
+
+ this.setState({
+ enabled: event.target.checked
+ });
+ }
+
+}
+
+export default ToggleOptionComponent; \ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index 10baf5b7..2fc48814 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -3,12 +3,18 @@ import * as invidiousList from "../ci/invidiouslist.json";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
import { keybindEquals } from "./utils/configUtils";
+export interface Permission {
+ canSubmit: boolean;
+}
+
interface SBConfig {
userID: string,
isVip: boolean,
+ permissions: Record<Category, Permission>,
/* Contains unsubmitted segments that the user has created. */
unsubmittedSegments: Record<string, SponsorTime[]>,
defaultCategory: Category,
+ renderSegmentsAsChapters: boolean,
whitelistedChannels: string[],
forceChannelCheck: boolean,
minutesSaved: number,
@@ -44,6 +50,7 @@ interface SBConfig {
allowExpirements: boolean,
showDonationLink: boolean,
showPopupDonationCount: number,
+ showUpsells: boolean,
donateClicked: number,
autoHideInfoButton: boolean,
autoSkipOnMusicVideos: boolean,
@@ -56,6 +63,7 @@ interface SBConfig {
categoryPillUpdate: boolean,
darkMode: boolean,
showCategoryGuidelines: boolean,
+ chaptersAvailable: boolean,
// Used to cache calculated text color info
categoryPillColors: {
@@ -68,10 +76,19 @@ interface SBConfig {
skipKeybind: Keybind,
startSponsorKeybind: Keybind,
submitKeybind: Keybind,
+ nextChapterKeybind: Keybind,
+ previousChapterKeybind: Keybind,
// What categories should be skipped
categorySelections: CategorySelection[],
+ payments: {
+ licenseKey: string,
+ lastCheck: number,
+ freeAccess: boolean,
+ chaptersAllowed: boolean
+ }
+
// Preview bar
barTypes: {
"preview-chooseACategory": PreviewBarOption,
@@ -128,8 +145,10 @@ const Config: SBObject = {
syncDefaults: {
userID: null,
isVip: false,
+ permissions: {},
unsubmittedSegments: {},
defaultCategory: "chooseACategory" as Category,
+ renderSegmentsAsChapters: false,
whitelistedChannels: [],
forceChannelCheck: false,
minutesSaved: 0,
@@ -165,6 +184,7 @@ const Config: SBObject = {
allowExpirements: true,
showDonationLink: true,
showPopupDonationCount: 0,
+ showUpsells: true,
donateClicked: 0,
autoHideInfoButton: true,
autoSkipOnMusicVideos: false,
@@ -172,6 +192,7 @@ const Config: SBObject = {
categoryPillUpdate: false,
darkMode: true,
showCategoryGuidelines: true,
+ chaptersAvailable: true,
categoryPillColors: {},
@@ -185,6 +206,8 @@ const Config: SBObject = {
skipKeybind: {key: "Enter"},
startSponsorKeybind: {key: ";"},
submitKeybind: {key: "'"},
+ nextChapterKeybind: {key: "]"},
+ previousChapterKeybind: {key: "["},
categorySelections: [{
name: "sponsor" as Category,
@@ -197,6 +220,13 @@ const Config: SBObject = {
option: CategorySkipOption.ShowOverlay
}],
+ payments: {
+ licenseKey: null,
+ lastCheck: 0,
+ freeAccess: false,
+ chaptersAllowed: false
+ },
+
colorPalette: {
red: "#780303",
white: "#ffffff",
@@ -516,6 +546,8 @@ function migrateOldSyncFormats(config: SBConfig) {
}
async function setupConfig() {
+ if (typeof(chrome) === "undefined") return;
+
await fetchConfig();
addDefaults();
const config = configProxy();
diff --git a/src/content.ts b/src/content.ts
index e0af1f34..a1f22fbc 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -12,12 +12,14 @@ import SubmissionNotice from "./render/SubmissionNotice";
import { Message, MessageResponse, VoteResponse } from "./messageTypes";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
import { getStartTimeFromUrl } from "./utils/urlParser";
-import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
+import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils";
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";
+import { importTimes } from "./utils/exporter";
+import { ChapterVote } from "./render/ChapterVote";
import { openWarningDialog } from "./utils/warnings";
// Hack to get the CSS loaded on permission-based sites (Invidious)
@@ -26,7 +28,8 @@ utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
//was sponsor data found when doing SponsorsLookup
let sponsorDataFound = false;
//the actual sponsorTimes if loaded and UUIDs associated with them
-let sponsorTimes: SponsorTime[] = null;
+let sponsorTimes: SponsorTime[] = [];
+let existingChaptersImported = false;
//what video id are these sponsors for
let sponsorVideoID: VideoID = null;
// List of open skip notices
@@ -138,7 +141,8 @@ const skipNoticeContentContainer: ContentContainer = () => ({
previewTime,
videoInfo,
getRealCurrentTime: getRealCurrentTime,
- lockedCategories
+ lockedCategories,
+ channelIDInfo
});
// value determining when to count segment as skipped and send telemetry to server (percent based)
@@ -167,6 +171,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
found: sponsorDataFound,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
+ time: video.currentTime,
onMobileYouTube
});
@@ -212,10 +217,17 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
found: sponsorDataFound,
status: lastResponseStatus,
sponsorTimes: sponsorTimes,
+ time: video.currentTime,
onMobileYouTube
}));
return true;
+ case "unskip":
+ unskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), null, true);
+ break;
+ case "reskip":
+ reskipSponsorTime(sponsorTimes.find((segment) => segment.UUID === request.UUID), true);
+ break;
case "submitVote":
vote(request.type, request.UUID).then((response) => sendResponse(response));
return true;
@@ -230,6 +242,31 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
case "copyToClipboard":
navigator.clipboard.writeText(request.text);
break;
+ case "importSegments": {
+ const importedSegments = importTimes(request.data, video.duration);
+ let addedSegments = false;
+ for (const segment of importedSegments) {
+ if (!sponsorTimesSubmitting.concat(sponsorTimes ?? []).some(
+ (s) => Math.abs(s.segment[0] - segment.segment[0]) < 1
+ && Math.abs(s.segment[1] - segment.segment[1]) < 1)) {
+ sponsorTimesSubmitting.push(segment);
+ addedSegments = true;
+ }
+ }
+
+ if (addedSegments) {
+ Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting;
+ Config.forceSyncUpdate("unsubmittedSegments");
+
+ updateEditButtonsOnPlayer();
+ updateSponsorTimesSubmitting(false);
+ }
+
+ sendResponse({
+ importedSegments
+ });
+ break;
+ }
case "keydown":
document.dispatchEvent(new KeyboardEvent('keydown', {
key: request.key,
@@ -249,8 +286,6 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
/**
* Called when the config is updated
- *
- * @param {String} changes
*/
function contentConfigUpdateListener(changes: StorageChangesObject) {
for (const key in changes) {
@@ -276,8 +311,8 @@ function resetValues() {
lastCheckVideoTime = -1;
retryCount = 0;
- //reset sponsor times
- sponsorTimes = null;
+ sponsorTimes = [];
+ existingChaptersImported = false;
sponsorSkipped = [];
videoInfo = null;
@@ -423,7 +458,7 @@ function createPreviewBar(): void {
isVisibleCheck: true
}, {
// For Desktop YouTube
- selector: ".ytp-progress-bar-container",
+ selector: ".ytp-progress-bar",
isVisibleCheck: true
}, {
// For Desktop YouTube
@@ -441,7 +476,8 @@ function createPreviewBar(): void {
const el = option.isVisibleCheck ? findValidElement(allElements) : allElements[0];
if (el) {
- previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
+ const chapterVote = new ChapterVote(voteAsync);
+ previewBar = new PreviewBar(el, onMobileYouTube, onInvidious, chapterVote);
updatePreviewBar();
@@ -507,13 +543,15 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
}
logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`);
-
- if (!video || video.paused) return;
+ if (!video) return;
if (currentTime === undefined || currentTime === null) {
currentTime = getVirtualTime();
}
lastTimeFromWaitingEvent = null;
+ updateActiveSegment(currentTime);
+
+ if (video.paused) return;
const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments);
const currentSkip = skipInfo.array[skipInfo.index];
@@ -568,35 +606,41 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?:
if (incorrectVideoCheck(videoID, currentSkip)) return;
forceVideoTime ||= Math.max(video.currentTime, getVirtualTime());
- if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) {
- skipToTime({
- v: video,
- skipTime,
- skippingSegments,
- openNotice: skipInfo.openNotice
- });
+ if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))) {
+ if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) {
+ skipToTime({
+ v: video,
+ skipTime,
+ skippingSegments,
+ openNotice: skipInfo.openNotice
+ });
- // These are segments that start at the exact same time but need seperate notices
- for (const extra of skipInfo.extraIndexes) {
- const extraSkip = skipInfo.array[extra];
- if (shouldSkip(extraSkip)) {
- skipToTime({
- v: video,
- skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]],
- skippingSegments: [extraSkip],
- openNotice: skipInfo.openNotice
- });
+ // These are segments that start at the exact same time but need seperate notices
+ for (const extra of skipInfo.extraIndexes) {
+ const extraSkip = skipInfo.array[extra];
+ if (shouldSkip(extraSkip)) {
+ skipToTime({
+ v: video,
+ skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]],
+ skippingSegments: [extraSkip],
+ openNotice: skipInfo.openNotice
+ });
+ }
+ }
+
+ if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
+ || currentSkip.actionType === ActionType.Mute) {
+ forcedSkipTime = skipTime[0] + 0.001;
+ } else {
+ forcedSkipTime = skipTime[1];
+ forcedIncludeIntersectingSegments = true;
+ forcedIncludeNonIntersectingSegments = false;
}
- }
-
- if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip
- || currentSkip.actionType === ActionType.Mute) {
- forcedSkipTime = skipTime[0] + 0.001;
} else {
- forcedSkipTime = skipTime[1];
- forcedIncludeIntersectingSegments = true;
- forcedIncludeNonIntersectingSegments = false;
+ forcedSkipTime = forceVideoTime + 0.001;
}
+ } else {
+ forcedSkipTime = forceVideoTime + 0.001;
}
startSponsorSchedule(forcedIncludeIntersectingSegments, forcedSkipTime, forcedIncludeNonIntersectingSegments);
@@ -793,8 +837,12 @@ function setupVideoListeners() {
lastTimeFromWaitingEvent = null;
startSponsorSchedule();
- } else if (video.currentTime === 0) {
- lastPausedAtZero = true;
+ } else {
+ updateActiveSegment(video.currentTime);
+
+ if (video.currentTime === 0) {
+ lastPausedAtZero = true;
+ }
}
});
video.addEventListener('ratechange', () => startSponsorSchedule());
@@ -888,7 +936,12 @@ async function sponsorsLookup(keepOldSubmissions = true) {
if (response?.ok) {
const recievedSegments: SponsorTime[] = JSON.parse(response.responseText)
?.filter((video) => video.videoID === sponsorVideoID)
- ?.map((video) => video.segments)[0];
+ ?.map((video) => video.segments)?.[0]
+ ?.map((segment) => ({
+ ...segment,
+ source: SponsorSourceType.Server
+ }))
+ ?.sort((a, b) => a.segment[0] - b.segment[0]);
if (!recievedSegments || !recievedSegments.length) {
// return if no video found
retryFetch(404);
@@ -909,6 +962,7 @@ async function sponsorsLookup(keepOldSubmissions = true) {
const oldSegments = sponsorTimes || [];
sponsorTimes = recievedSegments;
+ existingChaptersImported = false;
// Hide all submissions smaller than the minimum duration
if (Config.config.minDuration !== 0) {
@@ -956,13 +1010,28 @@ async function sponsorsLookup(keepOldSubmissions = true) {
retryFetch(lastResponseStatus);
}
+ importExistingChapters(true);
+
if (Config.config.isVip) {
lockedCategoriesLookup();
}
}
+function importExistingChapters(wait: boolean) {
+ if (!existingChaptersImported) {
+ GenericUtils.wait(() => video && getExistingChapters(sponsorVideoID, video.duration),
+ wait ? 5000 : 0, 100, (c) => c?.length > 0).then((chapters) => {
+ if (!existingChaptersImported && chapters?.length > 0) {
+ sponsorTimes = (sponsorTimes ?? []).concat(...chapters).sort((a, b) => a.segment[0] - b.segment[0]);
+ existingChaptersImported = true;
+ updatePreviewBar();
+ }
+ }).catch(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
+ }
+}
+
function getEnabledActionTypes(): ActionType[] {
- const actionTypes = [ActionType.Skip, ActionType.Poi];
+ const actionTypes = [ActionType.Skip, ActionType.Poi, ActionType.Chapter];
if (Config.config.muteSegments) {
actionTypes.push(ActionType.Mute);
}
@@ -1000,7 +1069,8 @@ function retryFetch(errorCode: number): void {
const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000);
setTimeout(() => {
- if (sponsorVideoID && sponsorTimes?.length === 0) {
+ if (sponsorVideoID && sponsorTimes?.length === 0
+ || sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) {
sponsorsLookup();
}
}, delay);
@@ -1164,9 +1234,11 @@ function updatePreviewBar(): void {
previewBarSegments.push({
segment: segment.segment as [number, number],
category: segment.category,
- unsubmitted: false,
actionType: segment.actionType,
- showLarger: segment.actionType === ActionType.Poi
+ unsubmitted: false,
+ showLarger: segment.actionType === ActionType.Poi,
+ description: segment.description,
+ source: segment.source,
});
});
}
@@ -1175,16 +1247,21 @@ function updatePreviewBar(): void {
previewBarSegments.push({
segment: segment.segment as [number, number],
category: segment.category,
- unsubmitted: true,
actionType: segment.actionType,
- showLarger: segment.actionType === ActionType.Poi
+ unsubmitted: true,
+ showLarger: segment.actionType === ActionType.Poi,
+ description: segment.description,
+ source: segment.source
});
});
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
+ updateActiveSegment(video.currentTime);
if (Config.config.showTimeWithSkips) {
- const skippedDuration = utils.getTimestampsDuration(previewBarSegments.map(({segment}) => segment));
+ const skippedDuration = utils.getTimestampsDuration(previewBarSegments
+ .filter(({actionType}) => actionType !== ActionType.Chapter)
+ .map(({segment}) => segment));
showTimeWithoutSkips(skippedDuration);
}
@@ -1248,7 +1325,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } =
getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments);
- const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true);
+ const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true);
// This is an array in-case multiple segments have the exact same start time
const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime));
@@ -1263,7 +1340,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool
const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } =
getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments);
- const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false);
+ const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false);
const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime));
const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex);
@@ -1344,7 +1421,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH
* the current time, but end after
*/
function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean,
- minimum?: number, onlySkippableSponsors = false, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
+ minimum?: number, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], scheduledTimes: number[]} {
if (!sponsorTimes) return {includedTimes: [], scheduledTimes: []};
const includedTimes: ScheduledTime[] = [];
@@ -1355,9 +1432,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
scheduledTime: sponsorTime.segment[0]
}));
- // Schedule at the end time to know when to unmute
- sponsorTimes.filter(sponsorTime => sponsorTime.actionType === ActionType.Mute)
- .forEach(sponsorTime => {
+ // Schedule at the end time to know when to unmute and remove title from seek bar
+ sponsorTimes.forEach(sponsorTime => {
if (!possibleTimes.some((time) => sponsorTime.segment[1] === time.scheduledTime)) {
possibleTimes.push({
...sponsorTime,
@@ -1369,9 +1445,9 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments:
for (let i = 0; i < possibleTimes.length; i++) {
if ((minimum === undefined
|| ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum)
- || (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
- && (!onlySkippableSponsors || shouldSkip(possibleTimes[i]))
+ || (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum)))
&& (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible)
+ && possibleTimes[i].segment.length === 2
&& possibleTimes[i].actionType !== ActionType.Poi) {
scheduledTimes.push(possibleTimes[i].scheduledTime);
@@ -1535,7 +1611,7 @@ function reskipSponsorTime(segment: SponsorTime, forceSeek = false) {
const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount;
video.currentTime = segment.segment[1];
- sendTelemetryAndCount([segment], skippedTime, fullSkip);
+ sendTelemetryAndCount([segment], segment.actionType !== ActionType.Chapter ? skippedTime : 0, fullSkip);
startSponsorSchedule(true, segment.segment[1], false);
}
}
@@ -1586,6 +1662,7 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
function shouldSkip(segment: SponsorTime): boolean {
return (segment.actionType !== ActionType.Full
+ && segment.source !== SponsorSourceType.YouTube
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
}
@@ -1692,7 +1769,7 @@ function startOrEndTimingNewSegment() {
if (!isSegmentCreationInProgress()) {
sponsorTimesSubmitting.push({
segment: [roundedTime],
- UUID: utils.generateUserID() as SegmentUUID,
+ UUID: GenericUtils.generateUserID() as SegmentUUID,
category: Config.config.defaultCategory,
actionType: ActionType.Skip,
source: SponsorSourceType.Local
@@ -1716,6 +1793,8 @@ function startOrEndTimingNewSegment() {
updateEditButtonsOnPlayer();
updateSponsorTimesSubmitting(false);
+
+ importExistingChapters(false);
}
function getIncompleteSegment(): SponsorTime {
@@ -1754,9 +1833,14 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
UUID: segmentTime.UUID,
category: segmentTime.category,
actionType: segmentTime.actionType,
+ description: segmentTime.description,
source: segmentTime.source
});
}
+
+ if (sponsorTimesSubmitting.length > 0) {
+ importExistingChapters(true);
+ }
}
updatePreviewBar();
@@ -1878,7 +1962,7 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category):
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
// Don't vote for preview sponsors
- if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source === SponsorSourceType.Local) return;
+ if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source !== SponsorSourceType.Server) return;
// See if the local time saved count and skip count should be saved
if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) {
@@ -2025,7 +2109,7 @@ async function sendSubmitMessage() {
} catch(e) {} // eslint-disable-line no-empty
// Add submissions to current sponsors list
- sponsorTimes = (sponsorTimes || []).concat(newSegments);
+ sponsorTimes = (sponsorTimes || []).concat(newSegments).sort((a, b) => a.segment[0] - b.segment[0]);
// Increase contribution count
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimesSubmitting.length;
@@ -2062,7 +2146,7 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
for (let i = 0; i < sponsorTimes.length; i++) {
for (let s = 0; s < sponsorTimes[i].segment.length; s++) {
- let timeMessage = utils.getFormattedTime(sponsorTimes[i].segment[s]);
+ let timeMessage = GenericUtils.getFormattedTime(sponsorTimes[i].segment[s]);
//if this is an end time
if (s == 1) {
timeMessage = " " + chrome.i18n.getMessage("to") + " " + timeMessage;
@@ -2078,6 +2162,44 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
return sponsorTimesMessage;
}
+function updateActiveSegment(currentTime: number): void {
+ previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime);
+ chrome.runtime.sendMessage({
+ message: "time",
+ time: currentTime
+ });
+}
+
+function nextChapter(): void {
+ const chapters = sponsorTimes.filter((time) => time.actionType === ActionType.Chapter)
+ .sort((a, b) => a.segment[1] - b.segment[1]);
+ if (chapters.length <= 0) return;
+
+ const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
+ && time.segment[1] > video.currentTime);
+ if (nextChapter !== -1) {
+ reskipSponsorTime(chapters[nextChapter], true);
+ } else {
+ video.currentTime = video.duration;
+ }
+}
+
+function previousChapter(): void {
+ const chapters = sponsorTimes.filter((time) => time.actionType === ActionType.Chapter);
+ if (chapters.length <= 0) return;
+
+ // subtract 5 seconds to allow skipping back to the previous chapter if close to start of
+ // the current one
+ const nextChapter = chapters.findIndex((time) => time.actionType === ActionType.Chapter
+ && time.segment[0] > video.currentTime - Math.min(5, time.segment[1] - time.segment[0]));
+ const previousChapter = nextChapter !== -1 ? (nextChapter - 1) : (chapters.length - 1);
+ if (previousChapter !== -1) {
+ unskipSponsorTime(chapters[previousChapter], null, true);
+ } else {
+ video.currentTime = 0;
+ }
+}
+
function addPageListeners(): void {
const refreshListners = () => {
if (!isVisible(video)) {
@@ -2107,6 +2229,8 @@ function hotkeyListener(e: KeyboardEvent): void {
const skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
+ const nextChapterKey = Config.config.nextChapterKeybind;
+ const previousChapterKey = Config.config.previousChapterKeybind;
if (keybindEquals(key, skipKey)) {
if (activeSkipKeybindElement)
@@ -2118,6 +2242,12 @@ function hotkeyListener(e: KeyboardEvent): void {
} else if (keybindEquals(key, submitKey)) {
submitSponsorTimes();
return;
+ } else if (keybindEquals(key, nextChapterKey)) {
+ nextChapter();
+ return;
+ } else if (keybindEquals(key, previousChapterKey)) {
+ previousChapter();
+ return;
}
//legacy - to preserve keybinds for skipKey, startSponsorKey and submitKey for people who set it before the update. (shouldn't be changed for future keybind options)
@@ -2187,8 +2317,8 @@ function showTimeWithoutSkips(skippedDuration: number): void {
display.appendChild(duration);
}
-
- const durationAfterSkips = utils.getFormattedTime(video?.duration - skippedDuration)
+
+ const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration);
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
}
@@ -2207,7 +2337,7 @@ function checkForPreloadedSegment() {
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
sponsorTimesSubmitting.push({
segment: segment.segment,
- UUID: utils.generateUserID() as SegmentUUID,
+ UUID: GenericUtils.generateUserID() as SegmentUUID,
category: segment.category ? segment.category : Config.config.defaultCategory,
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
source: SponsorSourceType.Local
diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts
index 16098b7d..9779913f 100644
--- a/src/js-components/previewBar.ts
+++ b/src/js-components/previewBar.ts
@@ -6,41 +6,63 @@ https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd
'use strict';
import Config from "../config";
-import { ActionType } from "../types";
-import Utils from "../utils";
-const utils = new Utils();
+import { ChapterVote } from "../render/ChapterVote";
+import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceType, SponsorTime } from "../types";
+import { partition } from "../utils/arrayUtils";
+import { shortCategoryName } from "../utils/categoryUtils";
+import { GenericUtils } from "../utils/genericUtils";
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
+const MIN_CHAPTER_SIZE = 0.003;
export interface PreviewBarSegment {
segment: [number, number];
- category: string;
- unsubmitted: boolean;
+ category: Category;
actionType: ActionType;
+ unsubmitted: boolean;
showLarger: boolean;
+ description: string;
+ source: SponsorSourceType;
+}
+
+interface ChapterGroup extends SegmentContainer {
+ originalDuration: number
}
class PreviewBar {
container: HTMLUListElement;
categoryTooltip?: HTMLDivElement;
- tooltipContainer?: HTMLElement;
+ categoryTooltipContainer?: HTMLElement;
+ chapterTooltip?: HTMLDivElement;
parent: HTMLElement;
onMobileYouTube: boolean;
onInvidious: boolean;
segments: PreviewBarSegment[] = [];
+ existingChapters: PreviewBarSegment[] = [];
videoDuration = 0;
- constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
+ // For chapter bar
+ hoveredSection: HTMLElement;
+ customChaptersBar: HTMLElement;
+ chaptersBarSegments: PreviewBarSegment[];
+ chapterVote: ChapterVote;
+ originalChapterBar: HTMLElement;
+ originalChapterBarBlocks: NodeListOf<HTMLElement>;
+
+ constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean, chapterVote: ChapterVote, test=false) {
+ if (test) return;
this.container = document.createElement('ul');
this.container.id = 'previewbar';
this.parent = parent;
this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious;
+ this.chapterVote = chapterVote;
this.createElement(parent);
+ this.createChapterMutationObservers();
this.setupHoverText();
}
@@ -51,16 +73,19 @@ class PreviewBar {
// Create label placeholder
this.categoryTooltip = document.createElement("div");
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
+ this.chapterTooltip = document.createElement("div");
+ this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
- this.tooltipContainer = tooltipTextWrapper.parentElement;
+ this.categoryTooltipContainer = tooltipTextWrapper.parentElement;
const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title");
- if (!this.tooltipContainer || !titleTooltip) return;
+ if (!this.categoryTooltipContainer || !titleTooltip) return;
tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
+ tooltipTextWrapper.insertBefore(this.chapterTooltip, titleTooltip.nextSibling);
const seekBar = document.querySelector(".ytp-progress-bar-container");
if (!seekBar) return;
@@ -76,7 +101,7 @@ class PreviewBar {
});
const observer = new MutationObserver((mutations) => {
- if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return;
+ if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer) return;
// If the mutation observed is only for our tooltip text, ignore
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
@@ -93,7 +118,7 @@ class PreviewBar {
const tooltipText = tooltipTextElement.textContent;
if (tooltipText === null || tooltipText.length === 0) continue;
- timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText);
+ timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText);
if (timeInSeconds !== null) break;
}
@@ -101,36 +126,32 @@ class PreviewBar {
if (timeInSeconds === null) return;
// Find the segment at that location, using the shortest if multiple found
- let segment: PreviewBarSegment | null = null;
- let currentSegmentLength = Infinity;
-
- for (const seg of this.segments) {//
- const segmentLength = seg.segment[1] - seg.segment[0];
- const minSize = this.getMinimumSize(seg.showLarger);
-
- const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]);
- const endTime = segmentLength > minSize ? seg.segment[1] : Math.ceil(seg.segment[0] + minSize);
- if (startTime <= timeInSeconds && endTime >= timeInSeconds) {
- if (segmentLength < currentSegmentLength) {
- currentSegmentLength = segmentLength;
- segment = seg;
- }
- }
+ const [normalSegments, chapterSegments] =
+ partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
+ (segment) => segment.actionType !== ActionType.Chapter);
+ let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments);
+ let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments);
+ if (mainSegment === null && secondarySegment !== null) {
+ mainSegment = secondarySegment;
+ secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
}
- if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
- this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
- } else if (segment !== null) {
- this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
-
- if (segment.unsubmitted) {
- this.categoryTooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + utils.shortCategoryName(segment.category);
+ if (mainSegment === null && secondarySegment === null) {
+ this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
+ } else {
+ this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
+ if (mainSegment !== null && secondarySegment !== null) {
+ this.categoryTooltipContainer.classList.add("sponsorTwoTooltips");
} else {
- this.categoryTooltip.textContent = utils.shortCategoryName(segment.category);
+ this.categoryTooltipContainer.classList.remove("sponsorTwoTooltips");
}
- // Use the class if the timestamp text uses it to prevent overlapping
+ this.setTooltipTitle(mainSegment, this.categoryTooltip);
+ this.setTooltipTitle(secondarySegment, this.chapterTooltip);
+
+ // Used to prevent overlapping
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
+ this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
}
});
@@ -140,6 +161,21 @@ class PreviewBar {
});
}
+ private setTooltipTitle(segment: PreviewBarSegment, tooltip: HTMLElement): void {
+ if (segment) {
+ const name = segment.description || shortCategoryName(segment.category);
+ if (segment.unsubmitted) {
+ tooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + name;
+ } else {
+ tooltip.textContent = name;
+ }
+
+ tooltip.style.removeProperty("display");
+ } else {
+ tooltip.style.display = "none";
+ }
+ }
+
createElement(parent: HTMLElement): void {
this.parent = parent;
@@ -148,7 +184,7 @@ class PreviewBar {
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
parent.style.opacity = "1";
}
-
+
this.container.style.transform = "none";
} else if (!this.onInvidious) {
// Hover listener
@@ -157,39 +193,62 @@ class PreviewBar {
this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered"));
}
-
-
// On the seek bar
this.parent.prepend(this.container);
}
clear(): void {
- this.videoDuration = 0;
- this.segments = [];
-
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
}
set(segments: PreviewBarSegment[], videoDuration: number): void {
+ this.segments = segments ?? [];
+ this.videoDuration = videoDuration ?? 0;
+
+ const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
+ // Sometimes video duration is inaccurate, pull from accessibility info
+ const ariaDuration = parseInt(progressBar?.getAttribute('aria-valuemax')) ?? 0;
+ if (ariaDuration && Math.abs(ariaDuration - this.videoDuration) > 3) {
+ this.videoDuration = ariaDuration;
+ }
+
+ this.update();
+ }
+
+ private update(): void {
this.clear();
- if (!segments) return;
+ if (!this.segments) return;
- this.segments = segments;
- this.videoDuration = videoDuration;
+ this.originalChapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
+ this.originalChapterBarBlocks = this.originalChapterBar.querySelectorAll(":scope > div") as NodeListOf<HTMLElement>
+ this.existingChapters = this.segments.filter((s) => s.source === SponsorSourceType.YouTube).sort((a, b) => a.segment[0] - b.segment[0])
- this.segments.sort(({segment: a}, {segment: b}) => {
+ const sortedSegments = this.segments.sort(({ segment: a }, { segment: b }) => {
// Sort longer segments before short segments to make shorter segments render later
return (b[1] - b[0]) - (a[1] - a[0]);
- }).forEach((segment) => {
+ });
+ for (const segment of sortedSegments) {
const bar = this.createBar(segment);
this.container.appendChild(bar);
- });
+ }
+
+ this.createChaptersBar(this.segments.sort((a, b) => a.segment[0] - b.segment[0]));
+
+ const chapterChevron = this.getChapterChevron();
+ if (this.segments.some((segment) => segment.actionType !== ActionType.Chapter
+ && segment.source === SponsorSourceType.YouTube)) {
+ chapterChevron.style.removeProperty("display");
+ } else {
+ chapterChevron.style.display = "none";
+ }
}
- createBar({category, unsubmitted, segment, showLarger}: PreviewBarSegment): HTMLLIElement {
+ createBar(barSegment: PreviewBarSegment): HTMLLIElement {
+ const { category, unsubmitted, segment, showLarger } = barSegment;
+
const bar = document.createElement('li');
bar.classList.add('previewbar');
bar.innerHTML = showLarger ? '&nbsp;&nbsp;' : '&nbsp;';
@@ -202,7 +261,9 @@ class PreviewBar {
bar.style.position = "absolute";
const duration = Math.min(segment[1], this.videoDuration) - segment[0];
- if (duration > 0) bar.style.width = this.timeToPercentage(duration);
+ if (duration > 0) {
+ bar.style.width = `calc(${this.intervalToPercentage(segment[0], segment[1])}${this.chapterFilter(barSegment) ? ' - 2px' : ''})`;
+ }
const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
bar.style.left = this.timeToPercentage(time);
@@ -210,6 +271,413 @@ class PreviewBar {
return bar;
}
+ createChaptersBar(segments: PreviewBarSegment[]): void {
+ const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
+ if (!progressBar || !this.originalChapterBar || this.originalChapterBar.childElementCount <= 0) return;
+
+ if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
+ || (!Config.config.renderSegmentsAsChapters
+ && segments.every((segment) => segment.actionType !== ActionType.Chapter
+ || segment.source === SponsorSourceType.YouTube))) {
+ if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
+ this.originalChapterBar.style.removeProperty("display");
+ return;
+ }
+
+ // Merge overlapping chapters
+ const filteredSegments = segments?.filter((segment) => this.chapterFilter(segment));
+ const chaptersToRender = this.createChapterRenderGroups(filteredSegments).filter((segment) => this.chapterGroupFilter(segment));
+
+ if (chaptersToRender?.length <= 0) {
+ if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
+ this.originalChapterBar.style.removeProperty("display");
+ return;
+ }
+
+ // Create it from cloning
+ let createFromScratch = false;
+ if (!this.customChaptersBar) {
+ createFromScratch = true;
+ this.customChaptersBar = this.originalChapterBar.cloneNode(true) as HTMLElement;
+ this.customChaptersBar.classList.add("sponsorBlockChapterBar");
+ }
+ this.customChaptersBar.style.removeProperty("display");
+ const originalSections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container");
+ const originalSection = originalSections[0];
+
+ this.customChaptersBar = this.customChaptersBar;
+
+ // For switching to a video with less chapters
+ if (originalSections.length > chaptersToRender.length) {
+ for (let i = originalSections.length - 1; i >= chaptersToRender.length; i--) {
+ this.customChaptersBar.removeChild(originalSections[i]);
+ }
+ }
+
+ // Modify it to have sections for each segment
+ for (let i = 0; i < chaptersToRender.length; i++) {
+ const chapter = chaptersToRender[i].segment;
+ let newSection = originalSections[i] as HTMLElement;
+ if (!newSection) {
+ newSection = originalSection.cloneNode(true) as HTMLElement;
+
+ this.firstTimeSetupChapterSection(newSection);
+ this.customChaptersBar.appendChild(newSection);
+ }
+
+ this.setupChapterSection(newSection, chapter[0], chapter[1], i !== chaptersToRender.length - 1);
+ }
+
+ // Hide old bar
+ this.originalChapterBar.style.display = "none";
+
+ if (createFromScratch) {
+ if (this.container?.parentElement === progressBar) {
+ progressBar.insertBefore(this.customChaptersBar, this.container.nextSibling);
+ } else {
+ progressBar.prepend(this.customChaptersBar);
+ }
+ }
+
+ this.updateChapterAllMutation(this.originalChapterBar, progressBar, true);
+ }
+
+ createChapterRenderGroups(segments: PreviewBarSegment[]): ChapterGroup[] {
+ const result: ChapterGroup[] = [];
+
+ segments?.forEach((segment, index) => {
+ const latestChapter = result[result.length - 1];
+ if (latestChapter && latestChapter.segment[1] > segment.segment[0]) {
+ const segmentDuration = segment.segment[1] - segment.segment[0];
+ if (segment.segment[0] < latestChapter.segment[0]
+ || segmentDuration < latestChapter.originalDuration) {
+ // Remove latest if it starts too late
+ let latestValidChapter = latestChapter;
+ const chaptersToAddBack: ChapterGroup[] = []
+ while (latestValidChapter?.segment[0] >= segment.segment[0]) {
+ const invalidChapter = result.pop();
+ if (invalidChapter.segment[1] > segment.segment[1]) {
+ if (invalidChapter.segment[0] === segment.segment[0]) {
+ invalidChapter.segment[0] = segment.segment[1];
+ }
+
+ chaptersToAddBack.push(invalidChapter);
+ }
+ latestValidChapter = result[result.length - 1];
+ }
+
+ // Split the latest chapter if smaller
+ result.push({
+ segment: [segment.segment[0], segment.segment[1]],
+ originalDuration: segmentDuration,
+ });
+ if (latestValidChapter?.segment[1] > segment.segment[1]) {
+ result.push({
+ segment: [segment.segment[1], latestValidChapter.segment[1]],
+ originalDuration: latestValidChapter.originalDuration
+ });
+ }
+
+ chaptersToAddBack.reverse();
+ let lastChapterChecked: number[] = segment.segment;
+ for (const chapter of chaptersToAddBack) {
+ if (chapter.segment[0] < lastChapterChecked[1]) {
+ chapter.segment[0] = lastChapterChecked[1];
+ }
+
+ lastChapterChecked = chapter.segment;
+ }
+ result.push(...chaptersToAddBack);
+ if (latestValidChapter) latestValidChapter.segment[1] = segment.segment[0];
+ } else {
+ // Start at end of old one otherwise
+ result.push({
+ segment: [latestChapter.segment[1], segment.segment[1]],
+ originalDuration: segmentDuration
+ });
+ }
+ } else {
+ // Add empty buffer before segment if needed
+ const lastTime = latestChapter?.segment[1] || 0;
+ if (segment.segment[0] > lastTime) {
+ result.push({
+ segment: [lastTime, segment.segment[0]],
+ originalDuration: 0
+ });
+ }
+
+ // Normal case
+ const endTime = Math.min(segment.segment[1], this.videoDuration);
+ result.push({
+ segment: [segment.segment[0], endTime],
+ originalDuration: endTime - segment.segment[0]
+ });
+ }
+
+ // Add empty buffer after segment if needed
+ if (index === segments.length - 1) {
+ const nextSegment = segments[index + 1];
+ const nextTime = nextSegment ? nextSegment.segment[0] : this.videoDuration;
+ const lastTime = result[result.length - 1]?.segment[1] || segment.segment[1];
+ if (this.intervalToDecimal(lastTime, nextTime) > MIN_CHAPTER_SIZE) {
+ result.push({
+ segment: [lastTime, nextTime],
+ originalDuration: 0
+ });
+ }
+ }
+ });
+
+ return result;
+ }
+
+ private setupChapterSection(section: HTMLElement, startTime: number, endTime: number, addMargin: boolean): void {
+ const sizePercent = this.intervalToPercentage(startTime, endTime);
+ if (addMargin) {
+ section.style.marginRight = "2px";
+ section.style.width = `calc(${sizePercent} - 2px)`;
+ } else {
+ section.style.marginRight = "0";
+ section.style.width = sizePercent;
+ }
+
+ section.setAttribute("decimal-width", String(this.intervalToDecimal(startTime, endTime)));
+ }
+
+ private firstTimeSetupChapterSection(section: HTMLElement): void {
+ section.addEventListener("mouseenter", () => {
+ this.hoveredSection?.classList.remove("ytp-exp-chapter-hover-effect");
+ section.classList.add("ytp-exp-chapter-hover-effect");
+ this.hoveredSection = section;
+ });
+ }
+
+ private createChapterMutationObservers(): void {
+ const progressBar = document.querySelector('.ytp-progress-bar') as HTMLElement;
+ const chapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
+ if (!progressBar || !chapterBar) return;
+
+ const attributeObserver = new MutationObserver((mutations) => {
+ const changes: Record<string, HTMLElement> = {};
+ for (const mutation of mutations) {
+ const currentElement = mutation.target as HTMLElement;
+ if (mutation.type === "attributes"
+ && currentElement.parentElement?.classList.contains("ytp-progress-list")) {
+ changes[currentElement.classList[0]] = mutation.target as HTMLElement;
+ }
+ }
+
+ this.updateChapterMutation(changes, progressBar);
+ });
+
+ attributeObserver.observe(chapterBar, {
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["style", "class"]
+ });
+
+ const childListObserver = new MutationObserver((mutations) => {
+ const changes: Record<string, HTMLElement> = {};
+ for (const mutation of mutations) {
+ if (mutation.type === "childList") {
+ this.update();
+ }
+ }
+
+ this.updateChapterMutation(changes, progressBar);
+ });
+
+ // Only direct children, no subtree
+ childListObserver.observe(chapterBar, {
+ childList: true
+ });
+ }
+
+ private updateChapterAllMutation(originalChapterBar: HTMLElement, progressBar: HTMLElement, firstUpdate = false): void {
+ const elements = originalChapterBar.querySelectorAll(".ytp-progress-list > *");
+ const changes: Record<string, HTMLElement> = {};
+ for (const element of elements) {
+ changes[element.classList[0]] = element as HTMLElement;
+ }
+
+ this.updateChapterMutation(changes, progressBar, firstUpdate);
+ }
+
+ private updateChapterMutation(changes: Record<string, HTMLElement>, progressBar: HTMLElement, firstUpdate = false): void {
+ // Go through each newly generated chapter bar and update the width based on changes array
+ if (this.customChaptersBar) {
+ // Width reached so far in decimal percent
+ let cursor = 0;
+
+ const sections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container") as NodeListOf<HTMLElement>;
+ for (let i = 0; i < sections.length; i++) {
+ const section = sections[i];
+
+ const sectionWidthDecimal = parseFloat(section.getAttribute("decimal-width"));
+ const sectionWidthDecimalNoMargin = sectionWidthDecimal - 2 / progressBar.clientWidth;
+
+ for (const className in changes) {
+ const selector = `.${className}`
+ const customChangedElement = section.querySelector(selector) as HTMLElement;
+ if (customChangedElement) {
+ const fullSectionWidth = i === sections.length - 1 ? sectionWidthDecimal : sectionWidthDecimalNoMargin;
+ const changedElement = changes[className];
+ const changedData = this.findLeftAndScale(selector, changedElement, progressBar);
+
+ const left = (changedData.left) / progressBar.clientWidth;
+ const calculatedLeft = Math.max(0, Math.min(1, (left - cursor) / fullSectionWidth));
+ if (!isNaN(left) && !isNaN(calculatedLeft)) {
+ customChangedElement.style.left = `${calculatedLeft * 100}%`;
+ customChangedElement.style.removeProperty("display");
+ }
+
+ if (changedData.scale !== null) {
+ const transformScale = (changedData.scale) / progressBar.clientWidth;
+
+ customChangedElement.style.transform =
+ `scaleX(${Math.max(0, Math.min(1 - calculatedLeft, (transformScale - cursor) / fullSectionWidth - calculatedLeft))}`;
+ if (firstUpdate) {
+ customChangedElement.style.transition = "none";
+ setTimeout(() => customChangedElement.style.removeProperty("transition"), 50);
+ }
+ }
+
+ if (customChangedElement.className !== changedElement.className) {
+ customChangedElement.className = changedElement.className;
+ }
+ }
+ }
+
+ cursor += sectionWidthDecimal;
+ }
+ }
+ }
+
+ private findLeftAndScale(selector: string, currentElement: HTMLElement, progressBar: HTMLElement):
+ { left: number, scale: number } {
+ const sections = currentElement.parentElement.parentElement.parentElement.children;
+ let currentWidth = 0;
+
+ let left = 0;
+ let leftPosition = 0;
+
+ let scale = null;
+ let scalePosition = 0;
+ let scaleWidth = 0;
+
+ for (let i = 0; i < sections.length; i++) {
+ const section = sections[i] as HTMLElement;
+ const checkElement = section.querySelector(selector) as HTMLElement;
+ const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") || progressBar.clientWidth;
+ const currentSectionWidth = currentSectionWidthNoMargin
+ + this.getPartialChapterSectionStyle(section, "marginRight");
+
+ // First check for left
+ const checkLeft = parseFloat(checkElement.style.left.replace("px", ""));
+ if (checkLeft !== 0) {
+ left = checkLeft;
+ leftPosition = currentWidth;
+ }
+
+ // Then check for scale
+ const transformMatch = checkElement.style.transform.match(/scaleX\(([0-9.]+?)\)/);
+ if (transformMatch) {
+ const transformScale = parseFloat(transformMatch[1]);
+ if (i === sections.length - 1 || (transformScale < 1 && transformScale + checkLeft / currentSectionWidthNoMargin < 0.99999)) {
+ scale = transformScale;
+ scaleWidth = currentSectionWidthNoMargin;
+
+ if (transformScale > 0) {
+ // reached the end of this section for sure, since the scale is now between 0 and 1
+ // if the scale is always zero, then it will go through all sections but still return 0
+
+ scalePosition = currentWidth;
+ if (checkLeft !== 0) {
+ scalePosition += left;
+ }
+ break;
+ }
+ }
+ }
+
+ currentWidth += currentSectionWidth;
+ }
+
+ return {
+ left: left + leftPosition,
+ scale: scale !== null ? scale * scaleWidth + scalePosition : null
+ };
+ }
+
+ private getPartialChapterSectionStyle(element: HTMLElement, param: string): number {
+ const data = element.style[param];
+ if (data?.includes("100%")) {
+ return 0;
+ } else {
+ return parseInt(element.style[param].match(/\d+/g)?.[0]) || 0;
+ }
+ }
+
+ updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): void {
+ if (!segments && submittingSegments?.length <= 0) return;
+
+ segments ??= [];
+ if (submittingSegments?.length > 0) segments = segments.concat(submittingSegments);
+ const activeSegments = segments.filter((segment) => {
+ return segment.hidden === SponsorHideType.Visible
+ && segment.segment[0] <= currentTime && segment.segment[1] > currentTime;
+ });
+
+ this.setActiveSegments(activeSegments);
+ }
+
+ /**
+ * Adds the text to the chapters slot if not filled by default
+ */
+ private setActiveSegments(segments: SponsorTime[]): void {
+ const chaptersContainer = document.querySelector(".ytp-chapter-container") as HTMLDivElement;
+
+ if (chaptersContainer) {
+ // TODO: Check if existing chapters exist (if big chapters menu is available?)
+
+ if (segments.length > 0) {
+ chaptersContainer.style.removeProperty("display");
+
+ const chosenSegment = segments.sort((a, b) => {
+ if (a.actionType === ActionType.Chapter && b.actionType !== ActionType.Chapter) {
+ return -1;
+ } else if (a.actionType !== ActionType.Chapter && b.actionType === ActionType.Chapter) {
+ return 1;
+ } else {
+ return (b.segment[0] - a.segment[0]);
+ }
+ })[0];
+
+ const chapterButton = chaptersContainer.querySelector("button.ytp-chapter-title") as HTMLButtonElement;
+ chapterButton.classList.remove("ytp-chapter-container-disabled");
+ chapterButton.disabled = false;
+
+ const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
+ chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
+
+ const chapterVoteContainer = this.chapterVote.getContainer();
+ if (chosenSegment.source === SponsorSourceType.Server) {
+ if (!chapterButton.contains(chapterVoteContainer)) {
+ chapterButton.insertBefore(chapterVoteContainer, this.getChapterChevron());
+ }
+
+ this.chapterVote.setVisibility(true);
+ this.chapterVote.setSegment(chosenSegment);
+ } else {
+ this.chapterVote.setVisibility(false);
+ }
+ } else {
+ // Hide chapters menu again
+ chaptersContainer.style.display = "none";
+ }
+ }
+ }
+
remove(): void {
this.container.remove();
@@ -218,14 +686,66 @@ class PreviewBar {
this.categoryTooltip = undefined;
}
- if (this.tooltipContainer) {
- this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
- this.tooltipContainer = undefined;
+ if (this.categoryTooltipContainer) {
+ this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
+ this.categoryTooltipContainer = undefined;
}
}
+ private chapterFilter(segment: PreviewBarSegment): boolean {
+ return (Config.config.renderSegmentsAsChapters || segment.actionType === ActionType.Chapter)
+ && segment.actionType !== ActionType.Poi
+ && this.chapterGroupFilter(segment);
+ }
+
+ private chapterGroupFilter(segment: SegmentContainer): boolean {
+ return segment.segment.length === 2 && this.intervalToDecimal(segment.segment[0], segment.segment[1]) > MIN_CHAPTER_SIZE;
+ }
+
+ intervalToPercentage(startTime: number, endTime: number) {
+ return `${this.intervalToDecimal(startTime, endTime) * 100}%`;
+ }
+
+ intervalToDecimal(startTime: number, endTime: number) {
+ return (this.timeToDecimal(endTime) - this.timeToDecimal(startTime));
+ }
+
timeToPercentage(time: number): string {
- return Math.min(100, time / this.videoDuration * 100) + '%';
+ return `${this.timeToDecimal(time) * 100}%`
+ }
+
+ timeToDecimal(time: number): number {
+ if (this.originalChapterBarBlocks?.length > 1 && this.existingChapters.length === this.originalChapterBarBlocks?.length) {
+ // Parent element to still work when display: none
+ const totalPixels = this.originalChapterBar.parentElement.clientWidth;
+ let pixelOffset = 0;
+ let lastCheckedChapter = -1;
+ for (let i = 0; i < this.originalChapterBarBlocks.length; i++) {
+ const chapterElement = this.originalChapterBarBlocks[i];
+ const widthPixels = parseFloat(chapterElement.style.width.replace("px", ""));
+
+ if (time >= this.existingChapters[i].segment[1]) {
+ const marginPixels = chapterElement.style.marginRight ? parseFloat(chapterElement.style.marginRight.replace("px", "")) : 0;
+ pixelOffset += widthPixels + marginPixels;
+ lastCheckedChapter = i;
+ } else {
+ break;
+ }
+ }
+
+ // The next chapter is the one we are currently inside of
+ const latestChapter = this.existingChapters[lastCheckedChapter + 1];
+ if (latestChapter) {
+ const latestWidth = parseFloat(this.originalChapterBarBlocks[lastCheckedChapter + 1].style.width.replace("px", ""));
+ const latestChapterDuration = latestChapter.segment[1] - latestChapter.segment[0];
+
+ const percentageInCurrentChapter = (time - latestChapter.segment[0]) / latestChapterDuration;
+ const sizeOfCurrentChapter = latestWidth / totalPixels;
+ return Math.min(1, ((pixelOffset / totalPixels) + (percentageInCurrentChapter * sizeOfCurrentChapter)));
+ }
+ }
+
+ return Math.min(1, time / this.videoDuration);
}
/*
@@ -234,6 +754,31 @@ class PreviewBar {
getMinimumSize(showLarger = false): number {
return this.videoDuration * (showLarger ? 0.006 : 0.003);
}
+
+ private getSmallestSegment(timeInSeconds: number, segments: PreviewBarSegment[]): PreviewBarSegment | null {
+ let segment: PreviewBarSegment | null = null;
+ let currentSegmentLength = Infinity;
+
+ for (const seg of segments) { //
+ const segmentLength = seg.segment[1] - seg.segment[0];
+ const minSize = this.getMinimumSize(seg.showLarger);
+
+ const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]);
+ const endTime = segmentLength > minSize ? seg.segment[1] : Math.ceil(seg.segment[0] + minSize);
+ if (startTime <= timeInSeconds && endTime >= timeInSeconds) {
+ if (segmentLength < currentSegmentLength) {
+ currentSegmentLength = segmentLength;
+ segment = seg;
+ }
+ }
+ }
+
+ return segment;
+ }
+
+ private getChapterChevron(): HTMLElement {
+ return document.querySelector(".ytp-chapter-title-chevron");
+ }
}
export default PreviewBar;
diff --git a/src/messageTypes.ts b/src/messageTypes.ts
index 764d2dc4..c48aba34 100644
--- a/src/messageTypes.ts
+++ b/src/messageTypes.ts
@@ -30,6 +30,11 @@ interface IsInfoFoundMessage {
updating: boolean;
}
+interface SkipMessage {
+ message: "unskip" | "reskip";
+ UUID: SegmentUUID;
+}
+
interface SubmitVoteMessage {
message: "submitVote";
type: number;
@@ -47,6 +52,11 @@ interface CopyToClipboardMessage {
text: string;
}
+interface ImportSegmentsMessage {
+ message: "importSegments";
+ data: string;
+}
+
interface KeyDownMessage {
message: "keydown";
key: string;
@@ -59,12 +69,13 @@ interface KeyDownMessage {
metaKey: boolean;
}
-export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | KeyDownMessage);
+export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage);
export interface IsInfoFoundMessageResponse {
found: boolean;
status: number;
sponsorTimes: SponsorTime[];
+ time: number;
onMobileYouTube: boolean;
}
@@ -90,11 +101,23 @@ export type MessageResponse =
| GetChannelIDResponse
| SponsorStartResponse
| IsChannelWhitelistedResponse
- | Record<never, never> // empty object response {}
- | VoteResponse;
+ | Record<string, never> // empty object response {}
+ | VoteResponse
+ | ImportSegmentsResponse;
export interface VoteResponse {
successType: number;
statusCode: number;
responseText: string;
-} \ No newline at end of file
+}
+
+export interface ImportSegmentsResponse {
+ importedSegments: SponsorTime[];
+}
+
+export interface TimeUpdateMessage {
+ message: "time";
+ time: number;
+}
+
+export type PopupMessage = TimeUpdateMessage;
diff --git a/src/options.ts b/src/options.ts
index 9d456936..1a522c9b 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -10,7 +10,7 @@ window.SB = Config;
import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
-import KeybindComponent from "./components/KeybindComponent";
+import KeybindComponent from "./components/options/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
const utils = new Utils();
diff --git a/src/popup.ts b/src/popup.ts
index 73d33f18..e357034c 100644
--- a/src/popup.ts
+++ b/src/popup.ts
@@ -1,12 +1,15 @@
import Config from "./config";
import Utils from "./utils";
-import { SponsorTime, SponsorHideType, ActionType, StorageChangesObject } from "./types";
-import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes";
+import { SponsorTime, SponsorHideType, ActionType, SegmentUUID, SponsorSourceType, StorageChangesObject, CategorySkipOption } from "./types";
+import { Message, MessageResponse, IsInfoFoundMessageResponse, ImportSegmentsResponse, PopupMessage } from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
+import { shortCategoryName } from "./utils/categoryUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
+import { exportTimes } from "./utils/exporter";
+import GenericNotice from "./render/GenericNotice";
const utils = new Utils();
interface MessageListener {
@@ -68,10 +71,18 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//the start and end time pairs (2d)
let sponsorTimes: SponsorTime[] = [];
+ let downloadedTimes: SponsorTime[] = [];
//current video ID of this tab
let currentVideoID = null;
+ enum SegmentTab {
+ Segments,
+ Chapters
+ }
+ let segmentTab = SegmentTab.Segments;
+ let port: chrome.runtime.Port = null;
+
const PageElements: PageElements = {};
[
@@ -124,11 +135,21 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
"refreshSegmentsButton",
"whitelistButton",
"sbDonate",
+ "issueReporterTabs",
+ "issueReporterTabSegments",
+ "issueReporterTabChapters",
"sponsorTimesDonateContainer",
"sbConsiderDonateLink",
"sbCloseDonate",
"sbBetaServerWarning",
- "sbCloseButton"
+ "sbCloseButton",
+ "issueReporterImportExport",
+ "importSegmentsButton",
+ "exportSegmentsButton",
+ "importSegmentsMenu",
+ "importSegmentsText",
+ "importSegmentsSubmit"
+
].forEach(id => PageElements[id] = document.getElementById(id));
getSegmentsFromContentScript(false);
@@ -162,7 +183,11 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
});
}
- //setup click listeners
+ PageElements.exportSegmentsButton.addEventListener("click", exportSegments);
+ PageElements.importSegmentsButton.addEventListener("click",
+ () => PageElements.importSegmentsMenu.classList.toggle("hidden"));
+ PageElements.importSegmentsSubmit.addEventListener("click", importSegments);
+
PageElements.sponsorStart.addEventListener("click", sendSponsorStartMessage);
PageElements.whitelistToggle.addEventListener("change", function () {
if (this.checked) {
@@ -215,6 +240,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
});
}
+ setupComPort();
+
//show proper disable skipping button
const disableSkipping = Config.config.disableSkipping;
if (disableSkipping != undefined && disableSkipping) {
@@ -230,7 +257,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.showNoticeAgain.style.display = "unset";
}
- utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&userID=" + Config.config.userID, (res) => {
+ utils.sendRequestToServer("GET", "/api/userInfo?value=userName&value=viewCount&value=minutesSaved&value=vip&value=permissions&value=freeChaptersAccess&userID="
+ + Config.config.userID, (res) => {
if (res.status === 200) {
const userInfo = JSON.parse(res.responseText);
PageElements.usernameValue.innerText = userInfo.userName;
@@ -259,6 +287,14 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
Config.config.isVip = userInfo.vip;
+ Config.config.permissions = userInfo.permissions;
+
+ if (userInfo.freeChaptersAccess) {
+ Config.config.payments.chaptersAllowed = userInfo.freeChaptersAccess;
+ Config.config.payments.freeAccess = userInfo.freeChaptersAccess;
+ Config.config.payments.lastCheck = Date.now();
+ Config.forceSyncUpdate("payments");
+ }
}
});
@@ -294,6 +330,22 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
// Must be delayed so it only happens once loaded
setTimeout(() => PageElements.sponsorblockPopup.classList.remove("preload"), 250);
+ PageElements.issueReporterTabSegments.addEventListener("click", () => {
+ PageElements.issueReporterTabSegments.classList.add("sbSelected");
+ PageElements.issueReporterTabChapters.classList.remove("sbSelected");
+
+ segmentTab = SegmentTab.Segments;
+ getSegmentsFromContentScript(true);
+ });
+
+ PageElements.issueReporterTabChapters.addEventListener("click", () => {
+ PageElements.issueReporterTabSegments.classList.remove("sbSelected");
+ PageElements.issueReporterTabChapters.classList.add("sbSelected");
+
+ segmentTab = SegmentTab.Chapters;
+ getSegmentsFromContentScript(true);
+ });
+
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) {
@@ -365,10 +417,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.whitelistButton.classList.remove("hidden");
PageElements.loadingIndicator.style.display = "none";
+ downloadedTimes = request.sponsorTimes ?? [];
if (request.found) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
- displayDownloadedSponsorTimes(request);
+ if (request.sponsorTimes) {
+ displayDownloadedSponsorTimes(request.sponsorTimes, request.time);
+ }
} else if (request.status == 404 || request.status == 200) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
} else {
@@ -441,165 +496,208 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
//display the video times from the array at the top, in a different section
- function displayDownloadedSponsorTimes(request: { found: boolean, sponsorTimes: SponsorTime[] }) {
- if (request.sponsorTimes != undefined) {
- // Sort list by start time
- const segmentTimes = request.sponsorTimes
- .sort((a, b) => a.segment[1] - b.segment[1])
- .sort((a, b) => a.segment[0] - b.segment[0]);
-
- //add them as buttons to the issue reporting container
- const container = document.getElementById("issueReporterTimeButtons");
- while (container.firstChild) {
- container.removeChild(container.firstChild);
- }
+ function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
+ let currentSegmentTab = segmentTab;
+ if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter)) {
+ PageElements.issueReporterTabs.classList.add("hidden");
+ currentSegmentTab = SegmentTab.Segments;
+ } else {
+ PageElements.issueReporterTabs.classList.remove("hidden");
+ }
- const isVip = Config.config.isVip;
- for (let i = 0; i < segmentTimes.length; i++) {
- const UUID = segmentTimes[i].UUID;
- const locked = segmentTimes[i].locked;
-
- const segmentSummary = document.createElement("summary");
- segmentSummary.className = "segmentSummary";
-
- const categoryColorCircle = document.createElement("span");
- categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
- categoryColorCircle.style.backgroundColor = Config.config.barTypes[segmentTimes[i].category]?.color;
- categoryColorCircle.classList.add("dot");
- categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
-
- let extraInfo = "";
- if (segmentTimes[i].hidden === SponsorHideType.Downvoted) {
- //this one is downvoted
- extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
- } else if (segmentTimes[i].hidden === SponsorHideType.MinimumDuration) {
- //this one is too short
- extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
- } else if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
- extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
+ // Sort list by start time
+ const downloadedTimes = sponsorTimes
+ .filter((segment) => {
+ if (currentSegmentTab === SegmentTab.Segments) {
+ return segment.actionType !== ActionType.Chapter;
+ } else if (currentSegmentTab === SegmentTab.Chapters) {
+ return segment.actionType === ActionType.Chapter
+ && segment.source !== SponsorSourceType.YouTube;
+ } else {
+ return true;
}
+ })
+ .sort((a, b) => a.segment[1] - b.segment[1])
+ .sort((a, b) => a.segment[0] - b.segment[0]);
+
+ //add them as buttons to the issue reporting container
+ const container = document.getElementById("issueReporterTimeButtons");
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+
+ if (downloadedTimes.length > 0) {
+ PageElements.issueReporterImportExport.classList.remove("hidden");
+ if (utils.getCategorySelection("chapter")?.option === CategorySkipOption.ShowOverlay) {
+ PageElements.importSegmentsButton.classList.remove("hidden");
+ }
+ } else {
+ PageElements.issueReporterImportExport.classList.add("hidden");
+ }
- const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + extraInfo);
- const segmentTimeFromToNode = document.createElement("div");
- if (segmentTimes[i].actionType === ActionType.Full) {
- segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
+ const isVip = Config.config.isVip;
+ for (let i = 0; i < downloadedTimes.length; i++) {
+ const UUID = downloadedTimes[i].UUID;
+ const locked = downloadedTimes[i].locked;
+ const category = downloadedTimes[i].category;
+ const actionType = downloadedTimes[i].actionType;
+
+ const segmentSummary = document.createElement("summary");
+ segmentSummary.classList.add("segmentSummary");
+ if (time >= downloadedTimes[i].segment[0]) {
+ if (time < downloadedTimes[i].segment[1]) {
+ segmentSummary.classList.add("segmentActive");
} else {
- segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
- (segmentTimes[i].actionType !== ActionType.Poi
- ? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
- : "");
+ segmentSummary.classList.add("segmentPassed");
}
+ }
- segmentTimeFromToNode.style.margin = "5px";
-
- // for inline-styling purposes
- const labelContainer = document.createElement("div");
- labelContainer.appendChild(categoryColorCircle);
-
- const span = document.createElement('span');
- span.className = "summaryLabel";
- span.appendChild(textNode);
- labelContainer.appendChild(span);
- // for inline-styling purposes
-
- segmentSummary.appendChild(labelContainer);
- segmentSummary.appendChild(segmentTimeFromToNode);
-
- const votingButtons = document.createElement("details");
- votingButtons.classList.add("votingButtons");
-
- //thumbs up and down buttons
- const voteButtonsContainer = document.createElement("div");
- voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
- voteButtonsContainer.classList.add("sbVoteButtonsContainer");
-
- const upvoteButton = document.createElement("img");
- upvoteButton.id = "sponsorTimesUpvoteButtonsContainer" + UUID;
- upvoteButton.className = "voteButton";
- upvoteButton.title = chrome.i18n.getMessage("upvote");
- upvoteButton.src = chrome.runtime.getURL("icons/thumbs_up.svg");
- upvoteButton.addEventListener("click", () => vote(1, UUID));
-
- const downvoteButton = document.createElement("img");
- downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
- downvoteButton.className = "voteButton";
- downvoteButton.title = chrome.i18n.getMessage("downvote");
- downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg");
- downvoteButton.addEventListener("click", () => vote(0, UUID));
-
- const uuidButton = document.createElement("img");
- uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
- uuidButton.className = "voteButton";
- uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
- uuidButton.title = chrome.i18n.getMessage("copySegmentID");
- uuidButton.addEventListener("click", () => {
- copyToClipboard(UUID);
- const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
- stopAnimation();
- });
+ const categoryColorCircle = document.createElement("span");
+ categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
+ categoryColorCircle.style.backgroundColor = Config.config.barTypes[category]?.color;
+ categoryColorCircle.classList.add("dot");
+ categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
+
+ let extraInfo = "";
+ if (downloadedTimes[i].hidden === SponsorHideType.Downvoted) {
+ //this one is downvoted
+ extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
+ } else if (downloadedTimes[i].hidden === SponsorHideType.MinimumDuration) {
+ //this one is too short
+ extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
+ } else if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
+ extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
+ }
- const hideButton = document.createElement("img");
- hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
- hideButton.className = "voteButton";
- hideButton.title = chrome.i18n.getMessage("hideSegment");
- if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
- hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
- } else {
+ const name = downloadedTimes[i].description || shortCategoryName(category);
+ const textNode = document.createTextNode(name + extraInfo);
+ const segmentTimeFromToNode = document.createElement("div");
+ if (downloadedTimes[i].actionType === ActionType.Full) {
+ segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
+ } else {
+ segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
+ (actionType !== ActionType.Poi
+ ? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(downloadedTimes[i].segment[1], true)
+ : "");
+ }
+
+ segmentTimeFromToNode.style.margin = "5px";
+
+ // for inline-styling purposes
+ const labelContainer = document.createElement("div");
+ if (actionType !== ActionType.Chapter) labelContainer.appendChild(categoryColorCircle);
+
+ const span = document.createElement('span');
+ span.className = "summaryLabel";
+ span.appendChild(textNode);
+ labelContainer.appendChild(span);
+
+ segmentSummary.appendChild(labelContainer);
+ segmentSummary.appendChild(segmentTimeFromToNode);
+
+ const votingButtons = document.createElement("details");
+ votingButtons.classList.add("votingButtons");
+
+ //thumbs up and down buttons
+ const voteButtonsContainer = document.createElement("div");
+ voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
+ voteButtonsContainer.classList.add("sbVoteButtonsContainer");
+
+ const upvoteButton = document.createElement("img");
+ upvoteButton.id = "sponsorTimesUpvoteButtonsContainer" + UUID;
+ upvoteButton.className = "voteButton";
+ upvoteButton.title = chrome.i18n.getMessage("upvote");
+ upvoteButton.src = chrome.runtime.getURL("icons/thumbs_up.svg");
+ upvoteButton.addEventListener("click", () => vote(1, UUID));
+
+ const downvoteButton = document.createElement("img");
+ downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
+ downvoteButton.className = "voteButton";
+ downvoteButton.title = chrome.i18n.getMessage("downvote");
+ downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg");
+ downvoteButton.addEventListener("click", () => vote(0, UUID));
+
+ const uuidButton = document.createElement("img");
+ uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
+ uuidButton.className = "voteButton";
+ uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
+ uuidButton.title = chrome.i18n.getMessage("copySegmentID");
+ uuidButton.addEventListener("click", () => {
+ copyToClipboard(UUID);
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
+ stopAnimation();
+ });
+
+ const hideButton = document.createElement("img");
+ hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
+ hideButton.className = "voteButton";
+ hideButton.title = chrome.i18n.getMessage("hideSegment");
+ if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
+ hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
+ } else {
+ hideButton.src = chrome.runtime.getURL("icons/visible.svg");
+ }
+ hideButton.addEventListener("click", () => {
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
+ stopAnimation();
+
+ if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
+ downloadedTimes[i].hidden = SponsorHideType.Visible;
+ } else {
+ hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
+ downloadedTimes[i].hidden = SponsorHideType.Hidden;
}
- hideButton.addEventListener("click", () => {
- const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
- stopAnimation();
- if (segmentTimes[i].hidden === SponsorHideType.Hidden) {
- hideButton.src = chrome.runtime.getURL("icons/visible.svg");
- segmentTimes[i].hidden = SponsorHideType.Visible;
- } else {
- hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
- segmentTimes[i].hidden = SponsorHideType.Hidden;
- }
-
- messageHandler.query({
- active: true,
- currentWindow: true
- }, tabs => {
- messageHandler.sendMessage(
- tabs[0].id,
- {
- message: "hideSegment",
- type: segmentTimes[i].hidden,
- UUID: UUID
- }
- );
- });
+ messageHandler.query({
+ active: true,
+ currentWindow: true
+ }, tabs => {
+ messageHandler.sendMessage(
+ tabs[0].id,
+ {
+ message: "hideSegment",
+ type: downloadedTimes[i].hidden,
+ UUID: UUID
+ }
+ );
});
+ });
- //add thumbs up, thumbs down and uuid copy buttons to the container
- voteButtonsContainer.appendChild(upvoteButton);
- voteButtonsContainer.appendChild(downvoteButton);
- voteButtonsContainer.appendChild(uuidButton);
- if ((segmentTimes[i].actionType === ActionType.Skip || segmentTimes[i].actionType === ActionType.Mute)
- && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(segmentTimes[i].hidden)) {
- voteButtonsContainer.appendChild(hideButton);
- }
+ const skipButton = document.createElement("img");
+ skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
+ skipButton.className = "voteButton";
+ skipButton.src = chrome.runtime.getURL("icons/skip.svg");
+ skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
+ votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
+
+ //add thumbs up, thumbs down and uuid copy buttons to the container
+ voteButtonsContainer.appendChild(upvoteButton);
+ voteButtonsContainer.appendChild(downvoteButton);
+ voteButtonsContainer.appendChild(uuidButton);
+ if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
+ && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
+ voteButtonsContainer.appendChild(hideButton);
+ }
+ voteButtonsContainer.appendChild(skipButton);
- // Will contain request status
- const voteStatusContainer = document.createElement("div");
- voteStatusContainer.id = "sponsorTimesVoteStatusContainer" + UUID;
- voteStatusContainer.classList.add("sponsorTimesVoteStatusContainer");
- voteStatusContainer.style.display = "none";
- const thanksForVotingText = document.createElement("div");
- thanksForVotingText.id = "sponsorTimesThanksForVotingText" + UUID;
- thanksForVotingText.classList.add("sponsorTimesThanksForVotingText");
- voteStatusContainer.appendChild(thanksForVotingText);
+ // Will contain request status
+ const voteStatusContainer = document.createElement("div");
+ voteStatusContainer.id = "sponsorTimesVoteStatusContainer" + UUID;
+ voteStatusContainer.classList.add("sponsorTimesVoteStatusContainer");
+ voteStatusContainer.style.display = "none";
- votingButtons.append(segmentSummary);
- votingButtons.append(voteButtonsContainer);
- votingButtons.append(voteStatusContainer);
+ const thanksForVotingText = document.createElement("div");
+ thanksForVotingText.id = "sponsorTimesThanksForVotingText" + UUID;
+ thanksForVotingText.classList.add("sponsorTimesThanksForVotingText");
+ voteStatusContainer.appendChild(thanksForVotingText);
- container.appendChild(votingButtons);
- }
+ votingButtons.append(segmentSummary);
+ votingButtons.append(voteButtonsContainer);
+ votingButtons.append(voteStatusContainer);
+
+ container.appendChild(votingButtons);
}
}
@@ -708,6 +806,8 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//this is not a YouTube video page
function displayNoVideo() {
document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID");
+
+ PageElements.issueReporterTabs.classList.add("hidden");
}
function addVoteMessage(message, UUID) {
@@ -881,6 +981,37 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
);
}
+ function skipSegment(actionType: ActionType, UUID: SegmentUUID, element?: HTMLElement): void {
+ if (actionType === ActionType.Chapter) {
+ sendMessage({
+ message: "unskip",
+ UUID: UUID
+ });
+ } else {
+ sendMessage({
+ message: "reskip",
+ UUID: UUID
+ });
+ }
+
+ if (element) {
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3);
+ stopAnimation();
+ }
+ }
+
+ function sendMessage(request: Message): void {
+ messageHandler.query({
+ active: true,
+ currentWindow: true
+ }, tabs => {
+ messageHandler.sendMessage(
+ tabs[0].id,
+ request
+ );
+ });
+ }
+
/**
* Should skipping be disabled (visuals stay)
*/
@@ -910,6 +1041,41 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
}
+ async function importSegments() {
+ const text = (PageElements.importSegmentsText as HTMLInputElement).value;
+
+ await sendTabMessage({
+ message: "importSegments",
+ data: text
+ }) as ImportSegmentsResponse;
+
+ PageElements.importSegmentsMenu.classList.add("hidden");
+ }
+
+ function exportSegments() {
+ copyToClipboard(exportTimes(downloadedTimes));
+
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.exportSegmentsButton, 0.3);
+ stopAnimation();
+ new GenericNotice(null, "exportCopied", {
+ title: chrome.i18n.getMessage(`CopiedExclamation`),
+ timed: true,
+ maxCountdownTime: () => 0.6,
+ referenceNode: PageElements.exportSegmentsButton.parentElement,
+ dontPauseCountdown: true,
+ style: {
+ top: 0,
+ bottom: 0,
+ minWidth: 0,
+ right: "30px",
+ margin: "auto",
+ height: "max-content"
+ },
+ hideLogo: true,
+ hideRightInfo: true
+ });
+ }
+
/**
* Converts time in minutes to 2d 5h 25.1
* If less than 1 hour, just returns minutes
@@ -934,6 +1100,20 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
}
}
+
+ function setupComPort(): void {
+ port = chrome.runtime.connect({ name: "popup" });
+ port.onDisconnect.addListener(() => setupComPort());
+ port.onMessage.addListener((msg) => onMessage(msg));
+ }
+
+ function onMessage(msg: PopupMessage) {
+ switch (msg.message) {
+ case "time":
+ displayDownloadedSponsorTimes(downloadedTimes, msg.time);
+ break;
+ }
+ }
}
runThePopup();
diff --git a/src/render/CategoryChooser.tsx b/src/render/CategoryChooser.tsx
index eab9edf6..97c33cf9 100644
--- a/src/render/CategoryChooser.tsx
+++ b/src/render/CategoryChooser.tsx
@@ -1,6 +1,6 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
-import CategoryChooserComponent from "../components/CategoryChooserComponent";
+import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
class CategoryChooser {
diff --git a/src/render/ChapterVote.tsx b/src/render/ChapterVote.tsx
new file mode 100644
index 00000000..b78a5ce8
--- /dev/null
+++ b/src/render/ChapterVote.tsx
@@ -0,0 +1,63 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent";
+import { VoteResponse } from "../messageTypes";
+import { Category, SegmentUUID, SponsorTime } from "../types";
+
+export class ChapterVote {
+ container: HTMLElement;
+ ref: React.RefObject<ChapterVoteComponent>;
+
+ unsavedState: ChapterVoteState;
+
+ mutationObserver?: MutationObserver;
+
+ constructor(vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>) {
+ this.ref = React.createRef();
+
+ this.container = document.createElement('span');
+ this.container.id = "chapterVote";
+ this.container.style.height = "100%";
+
+ ReactDOM.render(
+ <ChapterVoteComponent ref={this.ref} vote={vote} />,
+ this.container
+ );
+ }
+
+ getContainer(): HTMLElement {
+ return this.container;
+ }
+
+ close(): void {
+ ReactDOM.unmountComponentAtNode(this.container);
+ this.container.remove();
+ }
+
+ setVisibility(show: boolean): void {
+ const newState = {
+ show,
+ };
+
+ if (this.ref.current) {
+ this.ref.current?.setState(newState);
+ } else {
+ this.unsavedState = newState;
+ }
+ }
+
+ async setSegment(segment: SponsorTime): Promise<void> {
+ if (this.ref.current?.state?.segment !== segment) {
+ const newState = {
+ segment,
+ show: true
+ };
+
+ if (this.ref.current) {
+ this.ref.current?.setState(newState);
+ } else {
+ this.unsavedState = newState;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/render/GenericNotice.tsx b/src/render/GenericNotice.tsx
index 60b69835..1b691488 100644
--- a/src/render/GenericNotice.tsx
+++ b/src/render/GenericNotice.tsx
@@ -5,14 +5,9 @@ import NoticeComponent from "../components/NoticeComponent";
import Utils from "../utils";
const utils = new Utils();
-import { ContentContainer } from "../types";
+import { ButtonListener, ContentContainer } from "../types";
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
-export interface ButtonListener {
- name: string,
- listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
-}
-
export interface TextBox {
icon: string,
text: string
@@ -20,12 +15,17 @@ export interface TextBox {
export interface NoticeOptions {
title: string,
+ referenceNode?: HTMLElement,
textBoxes?: TextBox[],
buttons?: ButtonListener[],
fadeIn?: boolean,
timed?: boolean
style?: React.CSSProperties;
extraClass?: string;
+ maxCountdownTime?: () => number;
+ dontPauseCountdown?: boolean;
+ hideLogo?: boolean;
+ hideRightInfo?: boolean;
}
export default class GenericNotice {
@@ -42,7 +42,7 @@ export default class GenericNotice {
this.contentContainer = contentContainer;
- const referenceNode = utils.findReferenceNode();
+ const referenceNode = options.referenceNode ?? utils.findReferenceNode();
this.noticeElement = document.createElement("div");
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
@@ -62,6 +62,10 @@ export default class GenericNotice {
ref={this.noticeRef}
style={options.style}
extraClass={options.extraClass}
+ maxCountdownTime={options.maxCountdownTime}
+ dontPauseCountdown={options.dontPauseCountdown}
+ hideLogo={options.hideLogo}
+ hideRightInfo={options.hideRightInfo}
closeListener={() => this.close()} >
<tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}
diff --git a/src/render/RectangleTooltip.tsx b/src/render/RectangleTooltip.tsx
index 1887cbbc..ea019db7 100644
--- a/src/render/RectangleTooltip.tsx
+++ b/src/render/RectangleTooltip.tsx
@@ -33,8 +33,8 @@ export class RectangleTooltip {
props.fontSize ??= "10px";
this.container = document.createElement('div');
- props.htmlId ??= props.text;
- this.container.id = "sponsorRectangleTooltip" + props.htmlId;
+ props.htmlId ??= "sponsorRectangleTooltip" + props.text;
+ this.container.id = props.htmlId;
this.container.style.display = "relative";
if (props.prependElement) {
diff --git a/src/render/Tooltip.tsx b/src/render/Tooltip.tsx
index 66e581d5..d59728fe 100644
--- a/src/render/Tooltip.tsx
+++ b/src/render/Tooltip.tsx
@@ -1,29 +1,37 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
+import { ButtonListener } from "../types";
export interface TooltipProps {
- text: string,
- link?: string,
- referenceNode: HTMLElement,
- prependElement?: HTMLElement, // Element to append before
- bottomOffset?: string
+ text?: string;
+ link?: string;
+ referenceNode: HTMLElement;
+ prependElement?: HTMLElement; // Element to append before
+ bottomOffset?: string;
+ leftOffset?: string;
+ rightOffset?: string;
timeout?: number;
opacity?: number;
displayTriangle?: boolean;
+ extraClass?: string;
showLogo?: boolean;
showGotIt?: boolean;
+ buttons?: ButtonListener[];
}
export class Tooltip {
- text: string;
+ text?: string;
container: HTMLDivElement;
timer: NodeJS.Timeout;
constructor(props: TooltipProps) {
props.bottomOffset ??= "70px";
+ props.leftOffset ??= "inherit";
+ props.rightOffset ??= "inherit";
props.opacity ??= 0.7;
props.displayTriangle ??= true;
+ props.extraClass ??= "";
props.showLogo ??= true;
props.showGotIt ??= true;
this.text = props.text;
@@ -45,25 +53,29 @@ export class Tooltip {
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
ReactDOM.render(
- <div style={{bottom: props.bottomOffset, backgroundColor}}
- className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "")} >
+ <div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
+ className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
<div>
{props.showLogo ?
<img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img>
: null}
- <span className="sponsorSkipObject">
- {this.text + (props.link ? ". " : "")}
- {props.link ?
- <a style={{textDecoration: "underline"}}
- target="_blank"
- rel="noopener noreferrer"
- href={props.link}>
- {chrome.i18n.getMessage("LearnMore")}
- </a>
- : null}
- </span>
+ {this.text ?
+ <span className="sponsorSkipObject">
+ {this.text + (props.link ? ". " : "")}
+ {props.link ?
+ <a style={{textDecoration: "underline"}}
+ target="_blank"
+ rel="noopener noreferrer"
+ href={props.link}>
+ {chrome.i18n.getMessage("LearnMore")}
+ </a>
+ : null}
+ </span>
+ : null}
+
+ {this.getButtons(props.buttons)}
</div>
{props.showGotIt ?
<button className="sponsorSkipObject sponsorSkipNoticeButton"
@@ -78,6 +90,27 @@ export class Tooltip {
)
}
+ getButtons(buttons?: ButtonListener[]): JSX.Element[] {
+ if (buttons) {
+ const result: JSX.Element[] = [];
+
+ for (const button of buttons) {
+ result.push(
+ <button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
+ key={button.name}
+ onClick={(e) => button.listener(e)}>
+
+ {button.name}
+ </button>
+ )
+ }
+
+ return result;
+ } else {
+ return null;
+ }
+ }
+
close(): void {
ReactDOM.unmountComponentAtNode(this.container);
this.container.remove();
diff --git a/src/svg-icons/lock_svg.tsx b/src/svg-icons/lock_svg.tsx
new file mode 100644
index 00000000..ef077541
--- /dev/null
+++ b/src/svg-icons/lock_svg.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+
+const lockSvg = ({
+ fill = "#fcba03",
+ className = "",
+ width = "20",
+ height = "20",
+ onClick
+}): JSX.Element => (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ height={width}
+ width={height}
+ className={className}
+ fill={fill}
+ onClick={onClick} >
+ <path
+ d="M5.5 18q-.625 0-1.062-.438Q4 17.125 4 16.5v-8q0-.625.438-1.062Q4.875 7 5.5 7H6V5q0-1.667 1.167-2.833Q8.333 1 10 1q1.667 0 2.833 1.167Q14 3.333 14 5v2h.5q.625 0 1.062.438Q16 7.875 16 8.5v8q0 .625-.438 1.062Q15.125 18 14.5 18Zm4.5-4q.625 0 1.062-.438.438-.437.438-1.062t-.438-1.062Q10.625 11 10 11t-1.062.438Q8.5 11.875 8.5 12.5t.438 1.062Q9.375 14 10 14ZM7.5 7h5V5q0-1.042-.729-1.771Q11.042 2.5 10 2.5q-1.042 0-1.771.729Q7.5 3.958 7.5 5Z"/>
+ </svg>
+);
+
+export default lockSvg;
diff --git a/src/svg-icons/thumbs_down_svg.tsx b/src/svg-icons/thumbs_down_svg.tsx
index ce61db5a..a1161c10 100644
--- a/src/svg-icons/thumbs_down_svg.tsx
+++ b/src/svg-icons/thumbs_down_svg.tsx
@@ -1,13 +1,17 @@
import * as React from "react";
const thumbsDownSvg = ({
- fill = "#ffffff"
+ fill = "#ffffff",
+ className = "",
+ width = "18",
+ height = "18"
}): JSX.Element => (
<svg
xmlns="http://www.w3.org/2000/svg"
- width="18"
- height="18"
+ width={width}
+ height={height}
fill={fill}
+ className={className}
viewBox="0 0 24 24"
>
<path
diff --git a/src/svg-icons/thumbs_up_svg.tsx b/src/svg-icons/thumbs_up_svg.tsx
index 10c95d94..08afec11 100644
--- a/src/svg-icons/thumbs_up_svg.tsx
+++ b/src/svg-icons/thumbs_up_svg.tsx
@@ -1,13 +1,17 @@
import * as React from "react";
const thumbsUpSvg = ({
- fill = "#ffffff"
+ fill = "#ffffff",
+ className = "",
+ width = "18",
+ height = "18"
}): JSX.Element => (
<svg
xmlns="http://www.w3.org/2000/svg"
- width="18"
- height="18"
fill={fill}
+ width={width}
+ height={height}
+ className={className}
viewBox="0 0 24 24"
>
<path
diff --git a/src/types.ts b/src/types.ts
index 48dfb7a8..6b8f9969 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -21,7 +21,8 @@ export interface ContentContainer {
previewTime: (time: number, unpause?: boolean) => void,
videoInfo: VideoInfo,
getRealCurrentTime: () => number,
- lockedCategories: string[]
+ lockedCategories: string[],
+ channelIDInfo: ChannelIDInfo
}
}
@@ -58,6 +59,7 @@ export enum SponsorHideType {
export enum ActionType {
Skip = "skip",
Mute = "mute",
+ Chapter = "chapter",
Full = "full",
Poi = "poi"
}
@@ -69,19 +71,24 @@ export type Category = string & { __categoryBrand: unknown };
export enum SponsorSourceType {
Server = undefined,
- Local = 1
+ Local = 1,
+ YouTube = 2
}
-export interface SponsorTime {
+export interface SegmentContainer {
segment: [number] | [number, number];
+}
+
+export interface SponsorTime extends SegmentContainer {
UUID: SegmentUUID;
locked?: number;
category: Category;
actionType: ActionType;
+ description?: string;
hidden?: SponsorHideType;
- source?: SponsorSourceType;
+ source: SponsorSourceType;
videoDuration?: number;
}
@@ -230,4 +237,9 @@ export type Keybind = {
ctrl?: boolean,
alt?: boolean,
shift?: boolean
+}
+
+export interface ButtonListener {
+ name: string,
+ listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
} \ No newline at end of file
diff --git a/src/upsell.ts b/src/upsell.ts
new file mode 100644
index 00000000..faa94a1d
--- /dev/null
+++ b/src/upsell.ts
@@ -0,0 +1,71 @@
+import Config from "./config";
+import { checkLicenseKey } from "./utils/licenseKey";
+import { localizeHtmlPage } from "./utils/pageUtils";
+
+import * as countries from "../public/res/countries.json";
+
+// This is needed, if Config is not imported before Utils, things break.
+// Probably due to cyclic dependencies
+Config.config;
+
+window.addEventListener('DOMContentLoaded', init);
+
+async function init() {
+ localizeHtmlPage();
+
+ const cantAfford = document.getElementById("cantAfford");
+ const cantAffordTexts = chrome.i18n.getMessage("cantAfford").split(/{|}/);
+ cantAfford.appendChild(document.createTextNode(cantAffordTexts[0]));
+ const discountButton = document.createElement("span");
+ discountButton.id = "discountButton";
+ discountButton.innerText = cantAffordTexts[1];
+ cantAfford.appendChild(discountButton);
+ cantAfford.appendChild(document.createTextNode(cantAffordTexts[2]));
+
+ const redeemButton = document.getElementById("redeemButton") as HTMLInputElement;
+ redeemButton.addEventListener("click", async () => {
+ const licenseKey = redeemButton.value;
+
+ if (await checkLicenseKey(licenseKey)) {
+ Config.config.payments.licenseKey = licenseKey;
+ Config.forceSyncUpdate("payments");
+
+ alert(chrome.i18n.getMessage("redeemSuccess"));
+ } else {
+ alert(chrome.i18n.getMessage("redeemFailed"));
+ }
+ });
+
+ discountButton.addEventListener("click", async () => {
+ const subsidizedSection = document.getElementById("subsidizedPrice");
+ subsidizedSection.classList.remove("hidden");
+
+ const oldSelector = document.getElementById("countrySelector");
+ if (oldSelector) oldSelector.remove();
+ const countrySelector = document.createElement("select");
+ countrySelector.id = "countrySelector";
+ countrySelector.className = "optionsSelector";
+ const defaultOption = document.createElement("option");
+ defaultOption.innerText = chrome.i18n.getMessage("chooseACountry");
+ countrySelector.appendChild(defaultOption);
+
+ for (const country of Object.keys(countries)) {
+ const option = document.createElement("option");
+ option.value = country;
+ option.innerText = country;
+ countrySelector.appendChild(option);
+ }
+
+ countrySelector.addEventListener("change", () => {
+ if (countries[countrySelector.value]?.allowed) {
+ document.getElementById("subsidizedLink").classList.remove("hidden");
+ document.getElementById("noSubsidizedLink").classList.add("hidden");
+ } else {
+ document.getElementById("subsidizedLink").classList.add("hidden");
+ document.getElementById("noSubsidizedLink").classList.remove("hidden");
+ }
+ });
+
+ subsidizedSection.appendChild(countrySelector);
+ });
+} \ No newline at end of file
diff --git a/src/utils.ts b/src/utils.ts
index ae390d78..081e014e 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -30,7 +30,7 @@ export default class Utils {
this.backgroundScriptContainer = backgroundScriptContainer;
}
- async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
+ async wait<T>(condition: () => T, timeout = 5000, check = 100): Promise<T> {
return GenericUtils.wait(condition, timeout, check);
}
@@ -331,24 +331,6 @@ export default class Utils {
return permissionRegex;
}
- generateUserID(length = 36): string {
- const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- let result = "";
- if (window.crypto && window.crypto.getRandomValues) {
- const values = new Uint32Array(length);
- window.crypto.getRandomValues(values);
- for (let i = 0; i < length; i++) {
- result += charset[values[i] % charset.length];
- }
- return result;
- } else {
- for (let i = 0; i < length; i++) {
- result += charset[Math.floor(Math.random() * charset.length)];
- }
- return result;
- }
- }
-
/**
* Sends a request to a custom server
*
@@ -434,54 +416,6 @@ export default class Utils {
return referenceNode;
}
- getFormattedTime(seconds: number, precise?: boolean): string {
- seconds = Math.max(seconds, 0);
-
- const hours = Math.floor(seconds / 60 / 60);
- const minutes = Math.floor(seconds / 60) % 60;
- let minutesDisplay = String(minutes);
- let secondsNum = seconds % 60;
- if (!precise) {
- secondsNum = Math.floor(secondsNum);
- }
-
- let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
-
- if (secondsNum < 10) {
- //add a zero
- secondsDisplay = "0" + secondsDisplay;
- }
- if (hours && minutes < 10) {
- //add a zero
- minutesDisplay = "0" + minutesDisplay;
- }
- if (isNaN(hours) || isNaN(minutes)) {
- return null;
- }
-
- const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
-
- return formatted;
- }
-
- getFormattedTimeToSeconds(formatted: string): number | null {
- const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
-
- if (fragments === null) {
- return null;
- }
-
- const hours = fragments[1] ? parseInt(fragments[1]) : 0;
- const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
- const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
-
- return hours * 3600 + minutes * 60 + seconds;
- }
-
- shortCategoryName(categoryName: string): string {
- return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
- }
-
isContentScript(): boolean {
return window.location.protocol === "http:" || window.location.protocol === "https:";
}
diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts
new file mode 100644
index 00000000..b19b376d
--- /dev/null
+++ b/src/utils/arrayUtils.ts
@@ -0,0 +1,6 @@
+export function partition<T>(array: T[], filter: (element: T) => boolean): [T[], T[]] {
+ const pass = [], fail = [];
+ array.forEach((element) => (filter(element) ? pass : fail).push(element));
+
+ return [pass, fail];
+} \ No newline at end of file
diff --git a/src/utils/categoryUtils.ts b/src/utils/categoryUtils.ts
index c00a81b8..e6bc175e 100644
--- a/src/utils/categoryUtils.ts
+++ b/src/utils/categoryUtils.ts
@@ -41,7 +41,13 @@ export function getCategorySuffix(category: Category): string {
return "_POI";
} else if (category === "exclusive_access") {
return "_full";
+ } else if (category === "chapter") {
+ return "_chapter";
} else {
return "";
}
+}
+
+export function shortCategoryName(categoryName: string): string {
+ return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName);
} \ No newline at end of file
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 3530f234..74f0da71 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -137,5 +137,16 @@ export function getGuidelineInfo(category: Category): TextBox[] {
icon: "icons/bolt.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}];
+ case "chapter":
+ return [{
+ icon: "icons/close-smaller.svg",
+ text: chrome.i18n.getMessage(`category_${category}_guideline1`)
+ }, {
+ icon: "icons/check-smaller.svg",
+ text: chrome.i18n.getMessage(`category_${category}_guideline2`)
+ }, {
+ icon: "icons/check-smaller.svg",
+ text: chrome.i18n.getMessage(`category_${category}_guideline3`)
+ }];
}
} \ No newline at end of file
diff --git a/src/utils/exporter.ts b/src/utils/exporter.ts
new file mode 100644
index 00000000..2620a40f
--- /dev/null
+++ b/src/utils/exporter.ts
@@ -0,0 +1,65 @@
+import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
+import { shortCategoryName } from "./categoryUtils";
+import { GenericUtils } from "./genericUtils";
+
+export function exportTimes(segments: SponsorTime[]): string {
+ let result = "";
+ for (const segment of segments) {
+ if (![ActionType.Full, ActionType.Mute].includes(segment.actionType)
+ && segment.source !== SponsorSourceType.YouTube) {
+ result += exportTime(segment) + "\n";
+ }
+ }
+
+ return result.replace(/\n$/, "");
+}
+
+function exportTime(segment: SponsorTime): string {
+ const name = segment.description || shortCategoryName(segment.category);
+
+ return `${GenericUtils.getFormattedTime(segment.segment[0], true)}${
+ segment.segment[1] && segment.segment[0] !== segment.segment[1]
+ ? ` - ${GenericUtils.getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
+}
+
+export function importTimes(data: string, videoDuration: number): SponsorTime[] {
+ const lines = data.split("\n");
+ const result: SponsorTime[] = [];
+ for (const line of lines) {
+ const match = line.match(/(?:(\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
+ if (match) {
+ const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
+ if (startTime) {
+ const specialCharsMatcher = /^(?:\s+seconds?)?[:()-\s]*|(?:\s+at)?[:()-\s]+$/g
+ const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
+ let titleRight = null;
+ const split2 = line.split(match[1] || match[0]);
+ titleRight = split2[split2.length - 1].replace(specialCharsMatcher, "");
+
+ const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
+ if (title) {
+ const segment: SponsorTime = {
+ segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
+ category: "chapter" as Category,
+ actionType: ActionType.Chapter,
+ description: title,
+ source: SponsorSourceType.Local,
+ UUID: GenericUtils.generateUserID() as SegmentUUID
+ };
+
+ if (result.length > 0 && result[result.length - 1].segment[1] === null) {
+ result[result.length - 1].segment[1] = segment.segment[0];
+ }
+
+ result.push(segment);
+ }
+ }
+ }
+ }
+
+ if (result.length > 0 && result[result.length - 1].segment[1] === null) {
+ result[result.length - 1].segment[1] = videoDuration;
+ }
+
+ return result;
+} \ No newline at end of file
diff --git a/src/utils/genericUtils.ts b/src/utils/genericUtils.ts
index 2d912a66..8b07e699 100644
--- a/src/utils/genericUtils.ts
+++ b/src/utils/genericUtils.ts
@@ -1,5 +1,5 @@
/** Function that can be used to wait for a condition before returning. */
-async function wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
+async function wait<T>(condition: () => T, timeout = 5000, check = 100, predicate?: (obj: T) => boolean): Promise<T> {
return await new Promise((resolve, reject) => {
setTimeout(() => {
clearInterval(interval);
@@ -8,7 +8,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
const intervalCheck = () => {
const result = condition();
- if (result) {
+ if (predicate ? predicate(result) : result) {
resolve(result);
clearInterval(interval);
}
@@ -21,6 +21,50 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
});
}
+function getFormattedTimeToSeconds(formatted: string): number | null {
+ const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
+
+ if (fragments === null) {
+ return null;
+ }
+
+ const hours = fragments[1] ? parseInt(fragments[1]) : 0;
+ const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
+ const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
+
+ return hours * 3600 + minutes * 60 + seconds;
+}
+
+function getFormattedTime(seconds: number, precise?: boolean): string {
+ seconds = Math.max(seconds, 0);
+
+ const hours = Math.floor(seconds / 60 / 60);
+ const minutes = Math.floor(seconds / 60) % 60;
+ let minutesDisplay = String(minutes);
+ let secondsNum = seconds % 60;
+ if (!precise) {
+ secondsNum = Math.floor(secondsNum);
+ }
+
+ let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
+
+ if (secondsNum < 10) {
+ //add a zero
+ secondsDisplay = "0" + secondsDisplay;
+ }
+ if (hours && minutes < 10) {
+ //add a zero
+ minutesDisplay = "0" + minutesDisplay;
+ }
+ if (isNaN(hours) || isNaN(minutes)) {
+ return null;
+ }
+
+ const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
+
+ return formatted;
+}
+
/**
* Gets the error message in a nice string
*
@@ -85,10 +129,31 @@ function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): str
return url;
}
+function generateUserID(length = 36): string {
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ let result = "";
+ if (window.crypto && window.crypto.getRandomValues) {
+ const values = new Uint32Array(length);
+ window.crypto.getRandomValues(values);
+ for (let i = 0; i < length; i++) {
+ result += charset[values[i] % charset.length];
+ }
+ return result;
+ } else {
+ for (let i = 0; i < length; i++) {
+ result += charset[Math.floor(Math.random() * charset.length)];
+ }
+ return result;
+ }
+}
+
export const GenericUtils = {
wait,
+ getFormattedTime,
+ getFormattedTimeToSeconds,
getErrorMessage,
getLuminance,
+ generateUserID,
indexesOf,
objectToURI
} \ No newline at end of file
diff --git a/src/utils/licenseKey.ts b/src/utils/licenseKey.ts
new file mode 100644
index 00000000..53c54c76
--- /dev/null
+++ b/src/utils/licenseKey.ts
@@ -0,0 +1,65 @@
+import Config from "../config";
+import Utils from "../utils";
+import * as CompileConfig from "../../config.json";
+
+const utils = new Utils();
+
+export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
+ const result = await utils.asyncRequestToServer("GET", "/api/verifyToken", {
+ licenseKey
+ });
+
+ try {
+ if (result.ok && JSON.parse(result.responseText).allowed) {
+ Config.config.payments.chaptersAllowed = true;
+ Config.config.payments.lastCheck = Date.now();
+ Config.forceSyncUpdate("payments");
+
+ return true;
+ }
+ } catch (e) { } //eslint-disable-line no-empty
+
+ return false
+}
+
+export async function fetchingChaptersAllowed(): Promise<boolean> {
+ if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) {
+ return true;
+ }
+
+ //more than 14 days
+ if (Config.config.payments.licenseKey && Date.now() - Config.config.payments.lastCheck > 14 * 24 * 60 * 60 * 1000) {
+ const licensePromise = checkLicenseKey(Config.config.payments.licenseKey);
+
+ if (!Config.config.payments.chaptersAllowed) {
+ return licensePromise;
+ }
+ }
+
+ if (Config.config.payments.chaptersAllowed) return true;
+
+ if (Config.config.payments.lastCheck === 0) {
+ // Check for free access if no license key, and it is the first time
+ const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
+ value: "freeChaptersAccess",
+ userID: Config.config.userID
+ });
+
+ try {
+ if (result.ok) {
+ const userInfo = JSON.parse(result.responseText);
+
+ Config.config.payments.lastCheck = Date.now();
+ if (userInfo.freeChaptersAccess) {
+ Config.config.payments.freeAccess = true;
+ Config.config.payments.chaptersAllowed = true;
+ Config.forceSyncUpdate("payments");
+
+ return true;
+ }
+ }
+ } catch (e) { } //eslint-disable-line no-empty
+ }
+
+ return false;
+} \ No newline at end of file
diff --git a/src/utils/pageUtils.ts b/src/utils/pageUtils.ts
index de4d8308..9ab31328 100644
--- a/src/utils/pageUtils.ts
+++ b/src/utils/pageUtils.ts
@@ -1,4 +1,7 @@
-export function getControls(): HTMLElement | false {
+import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
+import { GenericUtils } from "./genericUtils";
+
+export function getControls(): HTMLElement {
const controlsSelectors = [
// YouTube
".ytp-right-controls",
@@ -16,7 +19,7 @@ export function getControls(): HTMLElement | false {
}
}
- return false;
+ return null;
}
export function isVisible(element: HTMLElement): boolean {
@@ -63,6 +66,44 @@ export function getHashParams(): Record<string, unknown> {
return {};
}
+export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] {
+ const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
+
+ const chapters: SponsorTime[] = [];
+ if (chaptersBox) {
+ let lastSegment: SponsorTime = null;
+ const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
+ for (const link of links) {
+ const timeElement = link.querySelector("#time") as HTMLElement;
+ const description = link.querySelector("#details h4") as HTMLElement;
+ if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) {
+ const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText);
+
+ if (lastSegment) {
+ lastSegment.segment[1] = time;
+ chapters.push(lastSegment);
+ }
+
+ lastSegment = {
+ segment: [time, null],
+ category: "chapter" as Category,
+ actionType: ActionType.Chapter,
+ description: description.innerText,
+ source: SponsorSourceType.YouTube,
+ UUID: null
+ };
+ }
+ }
+
+ if (lastSegment) {
+ lastSegment.segment[1] = duration;
+ chapters.push(lastSegment);
+ }
+ }
+
+ return chapters;
+}
+
export function localizeHtmlPage(): void {
//Localize by replacing __MSG_***__ meta tags
const localizedTitle = getLocalizedMessage(document.title);
diff --git a/test/exporter.test.ts b/test/exporter.test.ts
new file mode 100644
index 00000000..dd589db1
--- /dev/null
+++ b/test/exporter.test.ts
@@ -0,0 +1,241 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../src/types";
+import { exportTimes, importTimes } from "../src/utils/exporter";
+
+describe("Export segments", () => {
+ it("Some segments", () => {
+ const segments: SponsorTime[] = [{
+ segment: [0, 10],
+ category: "chapter" as Category,
+ actionType: ActionType.Chapter,
+ description: "Chapter 1",
+ source: SponsorSourceType.Server,
+ UUID: "1" as SegmentUUID
+ }, {
+ segment: [20, 20],
+ category: "poi_highlight" as Category,
+ actionType: ActionType.Poi,
+ description: "Highlight",
+ source: SponsorSourceType.Server,
+ UUID: "2" as SegmentUUID
+ }, {
+ segment: [30, 40],
+ category: "sponsor" as Category,
+ actionType: ActionType.Skip,
+ description: "Sponsor", // Force a description since chrome is not defined
+ source: SponsorSourceType.Server,
+ UUID: "3" as SegmentUUID
+ }, {
+ segment: [50, 60],
+ category: "selfpromo" as Category,
+ actionType: ActionType.Mute,
+ description: "Selfpromo",
+ source: SponsorSourceType.Server,
+ UUID: "4" as SegmentUUID
+ }, {
+ segment: [0, 0],
+ category: "selfpromo" as Category,
+ actionType: ActionType.Full,
+ description: "Selfpromo",
+ source: SponsorSourceType.Server,
+ UUID: "5" as SegmentUUID
+ }, {
+ segment: [80, 90],
+ category: "interaction" as Category,
+ actionType: ActionType.Skip,
+ description: "Interaction",
+ source: SponsorSourceType.YouTube,
+ UUID: "6" as SegmentUUID
+ }];
+
+ const result = exportTimes(segments);
+
+ expect(result).toBe(
+ "0:00.000 - 0:10.000 Chapter 1\n" +
+ "0:20.000 Highlight\n" +
+ "0:30.000 - 0:40.000 Sponsor"
+ );
+ });
+
+});
+
+describe("Import segments", () => {
+ it("1:20 to 1:21 thing", () => {
+ const input = ` 1:20 to 1:21 thing
+ 1:25 to 1:28 another thing`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 81],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 88],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("thing 1:20 to 1:21", () => {
+ const input = ` thing 1:20 to 1:21
+ another thing 1:25 to 1:28 ext`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 81],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 88],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("1:20 - 1:21 thing", () => {
+ const input = ` 1:20 - 1:21 thing
+ 1:25 - 1:28 another thing`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 81],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 88],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("1:20 1:21 thing", () => {
+ const input = ` 1:20 1:21 thing
+ 1:25 1:28 another thing`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 81],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 88],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("1:20 thing", () => {
+ const input = ` 1:20 thing
+ 1:25 another thing`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 85],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 120],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("1:20: thing", () => {
+ const input = ` 1:20: thing
+ 1:25: another thing`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 85],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 120],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("1:20 (thing)", () => {
+ const input = ` 1:20 (thing)
+ 1:25 (another thing)`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 85],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 120],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("thing 1:20", () => {
+ const input = ` thing 1:20
+ another thing 1:25`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 85],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 120],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("thing at 1:20", () => {
+ const input = ` thing at 1:20
+ another thing at 1:25`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [80, 85],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [85, 120],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("thing at 1s", () => {
+ const input = ` thing at 1s
+ another thing at 5s`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [1, 5],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [5, 120],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+
+ it("thing at 1 second", () => {
+ const input = ` thing at 1 second
+ another thing at 5 seconds`;
+
+ const result = importTimes(input, 120);
+ expect(result).toMatchObject([{
+ segment: [1, 5],
+ description: "thing",
+ category: "chapter" as Category
+ }, {
+ segment: [5, 120],
+ description: "another thing",
+ category: "chapter" as Category
+ }]);
+ });
+}); \ No newline at end of file
diff --git a/test/previewBar.test.ts b/test/previewBar.test.ts
new file mode 100644
index 00000000..b599b3a3
--- /dev/null
+++ b/test/previewBar.test.ts
@@ -0,0 +1,665 @@
+import PreviewBar, { PreviewBarSegment } from "../src/js-components/previewBar";
+
+describe("createChapterRenderGroups", () => {
+ let previewBar: PreviewBar;
+ beforeEach(() => {
+ previewBar = new PreviewBar(null, null, null, null, true);
+ })
+
+ it("Two unrelated times", () => {
+ previewBar.videoDuration = 315;
+ const groups = previewBar.createChapterRenderGroups([{
+ segment: [2, 30],
+ category: "sponsor",
+ unsubmitted: false,
+ showLarger: false,
+ description: ""
+ }, {
+ segment: [50, 80],
+ category: "sponsor",
+ unsubmitted: false,
+ showLarger: false,
+ description: ""
+ }] as PreviewBarSegment[]);
+
+ expect(groups).toStrictEqual([{
+ segment: [0, 2],
+ originalDuration: 0
+ }, {
+ segment: [2, 30],
+ originalDuration: 30 - 2
+ }, {
+ segment: [30, 50],
+ originalDuration: 0
+ }, {
+ segment: [50, 80],
+ originalDuration: 80 - 50
+ }, {
+ segment: [80, 315],
+ originalDuration: 0
+ }]);
+ });
+
+ it("Small time in bigger time", () => {
+ previewBar.videoDuration = 315;
+ const groups = previewBar.createChapterRenderGroups([{
+ segment: [2.52, 30],
+ category: "sponsor",
+ unsubmitted: false,
+ showLarger: false,
+ description: ""
+ }, {
+ segment: [20, 25],
+ category: "sponsor",
+ unsubmitted: false,
+ showLarger: false,
+ description: ""
+ }] as PreviewBarSegment[]);
+
+ expect(groups).toStrictEqual([{
+ segment: [0, 2.52],
+ originalDuration: 0
+ }, {
+ segment: [2.52, 20],
+ originalDuration: 30 - 2.52
+ }, {
+ segment: [20, 25],
+ originalDuration: 25 - 20
+ }, {
+ segment: [25, 30],
+ originalDuration: 30 - 2.52
+ }, {
+ segment: [30, 315],
+ originalDuration: 0
+ }]);
+ });
+
+ it("Same start time", () => {
+ previewBar.videoDuration = 315;
+ const groups = previewBar.createChapterRenderGroups([{
+ segment: [2.52, 30],
+ category: "sponsor",
+ unsubmitted: false,
+ showLarger: false,
+ description: ""
+ }, {
+ segment: [2.52, 40],
+ category: "sponsor",
+ unsubmitted: false,
+ showLarger: false,
+ description: ""
+ }] as PreviewBarSegment[]);
+
+ expect(groups).toStrictEqual([{
+ segment: [0, 2.52],
+ originalDuration: 0
+ }, {
+ segment: [2.52, 30],
+ originalDuration: 30 - 2.52
+ }, {
+ segment: [30, 40],
+ originalDuration: 40 - 2.52
+ }, {
+ segment: [40, 315],
+ originalDuration: 0
+ }]);
+ });
+
+ it("Lots of overlapping segments", () => {
+ previewBar.videoDuration = 315.061;
+ const groups = previewBar.createChapterRenderGroups([
+ {
+ "category": "chapter",
+ "segment": [
+ 0,
+ 49.977
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
+ "description": "Start of video"
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 2.926,
+ 5
+ ],
+ "locked": 1,
+ "votes": 2,
+ "videoDuration": 316,
+ "userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
+ "description": ""
+ },
+ {
+ "category": "chapter",
+ "segment": [
+ 14.487,
+ 37.133
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
+ "description": "Subset of start"
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 23.450537,
+ 34.486084
+ ],
+ "locked": 0,
+ "votes": -1,
+ "videoDuration": 315.061,
+ "userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
+ "description": ""
+ },
+ {
+ "category": "interaction",
+ "segment": [
+ 50.015343,
+ 56.775314
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "b2a85e8cdfbf21dd504babbcaca7f751b55a5a2df8179c1a83a121d0f5d56c0e",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 62.51888,
+ 74.33331
+ ],
+ "locked": 0,
+ "votes": -1,
+ "videoDuration": 316,
+ "userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 88.71328,
+ 96.05933
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "6c08c092db2b7a31210717cc1f2652e7e97d032e03c82b029a27c81cead1f90c",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 101.50703,
+ 115.088326
+ ],
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "2db207ad4b7a535a548fab293f4567bf97373997e67aadb47df8f91b673f6e53",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 122.211845,
+ 137.42178
+ ],
+ "locked": 0,
+ "votes": 1,
+ "videoDuration": 0,
+ "userID": "0312cbfa514d9d2dfb737816dc45f52aba7c23f0a3f0911273a6993b2cb57fcc",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 144.08913,
+ 160.14084
+ ],
+ "locked": 0,
+ "votes": -1,
+ "videoDuration": 316,
+ "userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 164.22084,
+ 170.98082
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "845c4377060d5801f5324f8e1be1e8373bfd9addcf6c68fc5a3c038111b506a3",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 180.56674,
+ 189.16516
+ ],
+ "locked": 0,
+ "votes": -1,
+ "videoDuration": 315.061,
+ "userID": "7c6b015687db7800c05756a0fd226fd8d101f5a1e1bfb1e5d97c440331fd6cb7",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 204.10468,
+ 211.87865
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "3472e8ee00b5da957377ae32d59ddd3095c2b634c2c0c970dfabfb81d143699f",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 214.92064,
+ 222.0186
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 0,
+ "userID": "62a00dffb344d27de7adf8ea32801c2fc0580087dc8d282837923e4bda6a1745",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 233.0754,
+ 244.56734
+ ],
+ "locked": 0,
+ "votes": -1,
+ "videoDuration": 315,
+ "userID": "dcf7fb0a6c071d5a93273ebcbecaee566e0ff98181057a36ed855e9b92bf25ea",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 260.64053,
+ 269.35938
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 288.686,
+ 301.96
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
+ "description": ""
+ },
+ {
+ "category": "sponsor",
+ "segment": [
+ 288.686,
+ 295
+ ],
+ "locked": 0,
+ "votes": 0,
+ "videoDuration": 315.061,
+ "userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
+ "description": ""
+ }] as unknown as PreviewBarSegment[]);
+
+ expect(groups).toStrictEqual([
+ {
+ "segment": [
+ 0,
+ 2.926
+ ],
+ "originalDuration": 49.977
+ },
+ {
+ "segment": [
+ 2.926,
+ 5
+ ],
+ "originalDuration": 2.074
+ },
+ {
+ "segment": [
+ 5,
+ 14.487
+ ],
+ "originalDuration": 49.977
+ },
+ {
+ "segment": [
+ 14.487,
+ 23.450537
+ ],
+ "originalDuration": 22.646
+ },
+ {
+ "segment": [
+ 23.450537,
+ 34.486084
+ ],
+ "originalDuration": 11.035546999999998
+ },
+ {
+ "segment": [
+ 34.486084,
+ 37.133
+ ],
+ "originalDuration": 22.646
+ },
+ {
+ "segment": [
+ 37.133,
+ 49.977
+ ],
+ "originalDuration": 49.977
+ },
+ {
+ "segment": [
+ 49.977,
+ 50.015343
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 50.015343,
+ 56.775314
+ ],
+ "originalDuration": 6.759971
+ },
+ {
+ "segment": [
+ 56.775314,
+ 62.51888
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 62.51888,
+ 74.33331
+ ],
+ "originalDuration": 11.814429999999994
+ },
+ {
+ "segment": [
+ 74.33331,
+ 88.71328
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 88.71328,
+ 96.05933
+ ],
+ "originalDuration": 7.346050000000005
+ },
+ {
+ "segment": [
+ 96.05933,
+ 101.50703
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 101.50703,
+ 115.088326
+ ],
+ "originalDuration": 13.581295999999995
+ },
+ {
+ "segment": [
+ 115.088326,
+ 122.211845
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 122.211845,
+ 137.42178
+ ],
+ "originalDuration": 15.209935000000016
+ },
+ {
+ "segment": [
+ 137.42178,
+ 144.08913
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 144.08913,
+ 160.14084
+ ],
+ "originalDuration": 16.051709999999986
+ },
+ {
+ "segment": [
+ 160.14084,
+ 164.22084
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 164.22084,
+ 170.98082
+ ],
+ "originalDuration": 6.759979999999985
+ },
+ {
+ "segment": [
+ 170.98082,
+ 180.56674
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 180.56674,
+ 189.16516
+ ],
+ "originalDuration": 8.598419999999976
+ },
+ {
+ "segment": [
+ 189.16516,
+ 204.10468
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 204.10468,
+ 211.87865
+ ],
+ "originalDuration": 7.773969999999991
+ },
+ {
+ "segment": [
+ 211.87865,
+ 214.92064
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 214.92064,
+ 222.0186
+ ],
+ "originalDuration": 7.0979600000000005
+ },
+ {
+ "segment": [
+ 222.0186,
+ 233.0754
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 233.0754,
+ 244.56734
+ ],
+ "originalDuration": 11.49194
+ },
+ {
+ "segment": [
+ 244.56734,
+ 260.64053
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 260.64053,
+ 269.35938
+ ],
+ "originalDuration": 8.718849999999975
+ },
+ {
+ "segment": [
+ 269.35938,
+ 288.686
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 288.686,
+ 295
+ ],
+ "originalDuration": 6.314000000000021
+ },
+ {
+ "segment": [
+ 295,
+ 301.96
+ ],
+ "originalDuration": 13.274000000000001
+ },
+ {
+ "segment": [
+ 301.96,
+ 315.061
+ ],
+ "originalDuration": 0
+ }
+ ]);
+ })
+
+ it("Multiple overlapping", () => {
+ previewBar.videoDuration = 3615.161;
+ const groups = previewBar.createChapterRenderGroups([{
+ "segment": [
+ 160,
+ 2797.323
+ ],
+ "category": "chooseACategory",
+ "unsubmitted": true,
+ "showLarger": false,
+ },{
+ "segment": [
+ 169,
+ 3432.255
+ ],
+ "category": "chooseACategory",
+ "unsubmitted": true,
+ "showLarger": false,
+ },{
+ "segment": [
+ 169,
+ 3412.413
+ ],
+ "category": "chooseACategory",
+ "unsubmitted": true,
+ "showLarger": false,
+ },{
+ "segment": [
+ 1594.92,
+ 1674.286
+ ],
+ "category": "sponsor",
+ "unsubmitted": false,
+ "showLarger": false,
+ }
+ ] as unknown as PreviewBarSegment[]);
+
+ expect(groups).toStrictEqual([
+ {
+ "segment": [
+ 0,
+ 160
+ ],
+ "originalDuration": 0
+ },
+ {
+ "segment": [
+ 160,
+ 169
+ ],
+ "originalDuration": 2637.323
+ },
+ {
+ "segment": [
+ 169,
+ 1594.92
+ ],
+ "originalDuration": 3243.413
+ },
+ {
+ "segment": [
+ 1594.92,
+ 1674.286
+ ],
+ "originalDuration": 79.36599999999999
+ },
+ {
+ "segment": [
+ 1674.286,
+ 3412.413
+ ],
+ "originalDuration": 3243.413
+ },
+ {
+ "segment": [
+ 3412.413,
+ 3432.255
+ ],
+ "originalDuration": 3263.255
+ },
+ {
+ "segment": [
+ 3432.255,
+ 3615.161
+ ],
+ "originalDuration": 0
+ }
+ ]);
+ });
+}) \ No newline at end of file
diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js
index 78015cbf..73c17807 100644
--- a/webpack/webpack.common.js
+++ b/webpack/webpack.common.js
@@ -31,7 +31,8 @@ module.exports = env => ({
content: path.join(__dirname, srcDir + 'content.ts'),
options: path.join(__dirname, srcDir + 'options.ts'),
help: path.join(__dirname, srcDir + 'help.ts'),
- permissions: path.join(__dirname, srcDir + 'permissions.ts')
+ permissions: path.join(__dirname, srcDir + 'permissions.ts'),
+ upsell: path.join(__dirname, srcDir + 'upsell.ts')
},
output: {
path: path.join(__dirname, '../dist/js'),