Compare commits

..

222 Commits
4.6.3 ... 5.0.2

Author SHA1 Message Date
Ajay
b7d85ca3c7 Add action type to preview bar test 2022-09-16 02:02:48 -04:00
Ajay
56611598b2 Fix filtered chapter group generation 2022-09-16 01:49:50 -04:00
Ajay
23e0666569 Improved behavior of next chapter keybind with overlap 2022-09-16 01:38:37 -04:00
Ajay
6571bba218 bump version 2022-09-16 01:00:53 -04:00
Ajay
51fc6fde22 Improve next chapter and previous chapter keybind 2022-09-16 00:57:43 -04:00
Ajay
b8d6d4a0b3 Handle preview bar hover without js 2022-09-15 23:37:11 -04:00
Ajay
6381f36a90 Fix hover for first chapter 2022-09-15 23:33:48 -04:00
Ajay
b9ef35dbbe Fix left over from merge conflict causing some skips to be ignored 2022-09-15 23:28:04 -04:00
Ajay
b43e3dab71 Fix doubling up segments in multi segment skip notice 2022-09-15 21:54:03 -04:00
Ajay
901dbb1ecf Fix info button animation 2022-09-15 15:35:38 -04:00
Ajay
68e01fbcc0 Add more checks to prevent double seek bar or no seek bar 2022-09-15 12:46:19 -04:00
Ajay
43d4b7ef18 Fix segments not available when hover preview -> click on same video 2022-09-15 12:10:39 -04:00
Ajay
4a00f3398e Fix last imported chapter not displaying sometimes 2022-09-14 03:40:24 -04:00
Ajay
8054e3d8f2 Fix chapters getting offset when small chapters filtered out 2022-09-14 03:18:41 -04:00
Ajay
b0e1d5e7fa Fix seek bar sometimes becoming empty when one seek section is completely filled 2022-09-14 02:58:22 -04:00
Ajay
d9e723b265 Sync official chapter margin 2022-09-14 02:27:51 -04:00
Ajay
9bb8a0986f Fix preview bar size offset with big mode 2022-09-13 23:59:00 -04:00
Ajay
6418d09039 Fix last preview bar being off 2022-09-13 23:40:16 -04:00
Ajay
afab681a60 Fix too many hover text tooltips 2022-09-13 11:56:23 -04:00
Ajay
8db077887d import segments instead of chapters 2022-09-13 11:52:09 -04:00
Ajay
c06b7857f8 Move to controls to make info button visible in selenium test 2022-09-05 00:32:04 -04:00
Ajay
e798cfdfe3 clarify what needs to be translated 2022-09-05 00:18:28 -04:00
Ajay
0e76342b04 fix typo 2022-09-05 00:17:44 -04:00
Ajay
d91e38fec9 bump version 2022-09-05 00:16:37 -04:00
Ajay
3316072f5d Fix votes appearing for unsubmitted segments 2022-09-05 00:14:23 -04:00
Ajay
4c568212ac Hide custom chapter bar while generating 2022-09-05 00:03:57 -04:00
Ajay
eaa119f152 Make sure original chapter bar that is used is always the right one 2022-09-05 00:01:11 -04:00
Ajay
e7deabe8d9 Properly handle hover previews for chapters and clear old unused ones 2022-09-04 23:57:10 -04:00
Ajay
6d47700ebd Safer document script 2022-09-04 23:14:15 -04:00
Ajay
93c616de23 Prevent some event bubbling issues 2022-09-04 22:04:48 -04:00
Ajay
ee25b41d7e Don't carry over incorrect/harmful vote menu between videos 2022-09-04 21:58:56 -04:00
Ajay
00f134029a Prevent creating multiple chapter vote containers 2022-09-04 21:52:14 -04:00
Ajay
00d625013b Add option to manual skip when a full video segment exists 2022-09-03 23:16:18 -04:00
Ajay
e81ff66dd3 Fix chapter -> full -> chapter not saving times 2022-09-03 21:28:02 -04:00
Ajay
97af12416e Fix copy tooltip 2022-09-03 01:58:22 -04:00
Ajay
bf191dab92 fix react errors about using inherit 2022-09-03 01:09:16 -04:00
Ajay
f8c61b7848 Don't use video before it is set 2022-09-03 00:36:28 -04:00
Ajay
5b136f2da8 Fix crashes on invidious 2022-09-03 00:32:20 -04:00
Ajay
8b80b33810 Don't show empty chapter bar for youtube chapters in popup 2022-09-03 00:27:18 -04:00
Ajay
e3c36ae6e2 Fix the freezing on firefox due to hover preview text 2022-09-03 00:22:03 -04:00
Ajay
533b15f44b Add support for hours in import segments 2022-09-02 21:42:36 -04:00
Ajay Ramachandran
4f0f8655f4 Merge pull request #1425 from mchangrh/contentScriptRebase
rebase document script out of videoInfo
2022-09-02 15:20:40 -04:00
Ajay Ramachandran
668f6856d1 bump version 2022-09-02 15:20:25 -04:00
Ajay
c8e2bb0c13 Auto update hidden categories when redeemed 2022-09-02 14:38:49 -04:00
Ajay
39ed7ea83c Fix license code box 2022-09-02 14:26:41 -04:00
Ajay
f1b2ff801a Fix redirect uri 2022-09-02 13:50:29 -04:00
Ajay Ramachandran
1d9c3a8b80 fix typo 2022-09-02 04:55:17 -04:00
Ajay
29c6151fe3 Ensure channel id is defined before declaring it found 2022-09-02 01:30:13 -04:00
Ajay
1377be9915 Use events for channel id and fallback to current system
Also fix formatting
2022-09-02 01:30:12 -04:00
Michael C
c479a601cd rebase document script out of videoInfo #1312 2022-09-02 01:30:09 -04:00
Ajay Ramachandran
f66a4d25bf Merge pull request #1446 from mini-bomba/clearUnsubmittedSegments
Add a section in options for unsubmitted segments
2022-09-02 00:17:57 -04:00
Ajay
9c7d153f15 Move segment export to backup page and improve margins 2022-09-02 00:15:05 -04:00
mini-bomba
bbea534781 Add "Export segments as URL" option the unsubmitted videos section 2022-09-01 23:03:25 -04:00
mini-bomba
df2586e76d Load segment description from hashparams 2022-09-01 23:03:25 -04:00
mini-bomba
59093cdf21 Move new react components to components/options/, following latest changes 2022-09-01 23:03:25 -04:00
mini-bomba
5f6307041a Add an Export Segments button to the unsubmitted segments list 2022-09-01 23:03:25 -04:00
mini-bomba
26f2143247 Don't force-sync unsubmitted segments when clear confirm prompt is cancelled 2022-09-01 23:03:25 -04:00
mini-bomba
bd292ff886 Split unsubmittedSegmentCounts string into many to account for singular/plural forms of nouns
hopefully with enough context for translators to properly translate...
2022-09-01 23:03:25 -04:00
mini-bomba
9915d46ad4 Add a section in options for unsubmitted segments 2022-09-01 23:03:25 -04:00
Ajay
2b5a02e068 Make required segments thicker in preview bar 2022-09-01 21:35:58 -04:00
Ajay
1f68f512fa Fix linting error 2022-09-01 20:04:53 -04:00
Ajay
d18f7c6195 Make limited width option better 2022-09-01 17:10:01 -04:00
Ajay
015ac7d46e Fix test breaking due to chrome.* api 2022-09-01 16:52:10 -04:00
Ajay
6631dfdea3 Also check license status for submitting chapter 2022-09-01 16:44:02 -04:00
Ajay
212fbb83fe Add tooltip to sort segments 2022-09-01 16:33:34 -04:00
Ajay
9e08d6012c Fix export/import not appearing without segments and without chapter enabled 2022-09-01 16:32:23 -04:00
Ajay
69c0fe1caf Make importer import non chapters too 2022-09-01 16:25:43 -04:00
Ajay
fcecd1163d Improve locked category display 2022-09-01 16:10:57 -04:00
Ajay
29ea112b4f Move hyphen so it is not treated as a range 2022-09-01 16:07:29 -04:00
Ajay Ramachandran
2b96fd5f57 Merge pull request #1001 from ajayyy/chapters
Chapters
2022-09-01 15:24:35 -04:00
Ajay
3e40745621 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-09-01 15:21:23 -04:00
Ajay
c6e30236e9 Add license requirement 2022-09-01 15:15:30 -04:00
Ajay
34c4ecf940 Don't include chapters in time without skips 2022-08-28 23:47:27 -04:00
Ajay
3550c168e2 Fix active segment sometimes disapearing 2022-08-28 23:45:02 -04:00
Ajay
901d6e6c92 Add voting for chapters 2022-08-28 23:38:40 -04:00
Ajay Ramachandran
f05d081cd6 Merge pull request #1459 from mchangrh/no-html-error
catch all html in error messages
2022-08-22 23:21:39 -04:00
Michael C
aadc1be56c catch all html in error messages 2022-08-22 20:44:23 -04:00
Ajay Ramachandran
19e230ea6a Merge pull request #1453 from AlecRust/fix-width-when-embedded
Fix popup width when embedded in page
2022-08-21 22:23:52 -04:00
Alec Rust
bc1263c341 Fix popup width when embedded in page 2022-08-21 11:56:31 +01:00
Ajay
42d76cf257 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-08-19 23:16:45 -04:00
Ajay
d06b7411dc Move options associated with specific categories into their div 2022-08-19 01:26:45 -04:00
Ajay
b14d766ffb Don't show hidden segments in active segment box 2022-08-18 02:44:49 -04:00
Ajay
32ff8db241 Fix buffering sometimes not rendering all the way 2022-08-18 02:39:56 -04:00
Ajay
ea87c8ca24 Fix seek bar progress offsets with custom chapter sections 2022-08-18 02:24:54 -04:00
Michael M. Chang
780ea4a9d0 update wording of Preview to be inline with wiki (#1441)
Co-authored-by: Ajay Ramachandran <dev@ajay.app>
2022-08-17 23:20:16 -04:00
Ajay
6ce4797772 Fix preview bar when video duration innacurate 2022-08-17 01:28:19 -04:00
Ajay
8e738a6097 Fix preview bars rendering incorrectly when native chapters are displayed 2022-08-17 01:21:06 -04:00
Ajay
7d3f86ded1 Fix skipping after paused at zero sometimes not working
Affects some autoplay blocking

Resolves #1437
2022-08-16 16:42:47 -04:00
Ajay
faeb5dede0 Add page for refreshing invidious permissions if it was revoked
Fixes #1354
2022-08-16 16:00:34 -04:00
Ajay Ramachandran
eae8485713 Merge pull request #1422 from mchangrh/localDisableMute
allow disabling mute segments locally
2022-08-14 00:23:29 -04:00
Ajay Ramachandran
87ca0a8a50 Merge pull request #1427 from mini-bomba/theming_fixes
Fix issues with DR & Invidious themes
2022-08-14 00:18:48 -04:00
Ajay
99c5375c6a Handle permission userinfo using new logic 2022-08-14 00:14:59 -04:00
mini-bomba
a62f6ca696 Fix issues with DR & Invidious themes 2022-07-30 00:35:55 +02:00
Michael C
6eb1d5d954 allow disabling mute segments locally 2022-07-29 00:37:02 -04:00
Ajay Ramachandran
81b01ac5cc Merge pull request #1417 from mchangrh/update-dependencies
Update dependencies
2022-07-25 17:18:13 -04:00
Michael C
6f47b66837 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into update-dependencies 2022-07-25 16:11:30 -04:00
Michael C
cf43e04d47 update dependnecies 2022-07-25 16:10:10 -04:00
Ajay
cda57e2d2b Make title link selector more specific
Co-authored-by: Michael C <michael@mchang.name>
2022-07-25 16:03:08 -04:00
Ajay
a9186a35e5 Fix hover previews on load and channel trailers 2022-07-25 02:40:48 -04:00
Ajay
1a6e6279c8 Fix autoskipping when skips load before video 2022-07-24 21:45:25 -04:00
Ajay Ramachandran
522a04eecb bump version 2022-07-21 23:40:26 -04:00
Ajay
d8dfbef1a7 Fix issue with navigator triggering events before document.url updates 2022-07-21 15:14:57 -04:00
Ajay
60ea7190f9 Pin github actions 2022-07-21 00:13:32 -04:00
Ajay
804870f18a Fix new warning ui 2022-07-20 21:07:57 -04:00
Ajay Ramachandran
7c302af207 bump version 2022-07-20 21:03:20 -04:00
Ajay
2cc1dcc6fd Add better UI for warnings allowing you to accept without chatting 2022-07-20 18:48:53 -04:00
Ajay
31cc4b4960 Fix running dev broken 2022-07-20 16:49:23 -04:00
Ajay
af86534992 Fix categories not being able to be disabled 2022-07-19 23:37:42 -04:00
Ajay
0f9122aa1c Fix skipping two segments at the same time for auto skip on music videos 2022-07-19 23:27:32 -04:00
Ajay
d0e35032a5 Include overlap when unmuting if about to autoskip 2022-07-14 17:39:08 -04:00
Ajay Ramachandran
acf26d3127 Merge pull request #1400 from mchangrh/update-deps
update dependencies
2022-07-13 12:25:37 -04:00
Ajay
d352c6efb4 bump version 2022-07-13 12:16:11 -04:00
Ajay
5ff9b10f21 Fix double skip issue 2022-07-13 12:15:56 -04:00
Michael C
80c67d8340 update dependencies 2022-07-12 23:47:22 -04:00
Ajay
fdbcf47149 make skip to next chapter go to next endpoint and fix reskip stackoverflow 2022-07-10 21:51:56 -04:00
Ajay
b1ef8a5d47 Don't draw chapters bar when no custom segments 2022-07-10 02:06:26 -04:00
Ajay
4cb6baaff0 Fix chapter import for no segments 2022-07-10 02:02:12 -04:00
Ajay
6cb4fac041 Add hotkeys for skipping to next and previous chapter 2022-07-10 01:58:39 -04:00
Ajay
d7176a9c97 Import chapters with no segments as well 2022-07-10 01:13:53 -04:00
Ajay
2eb0a34858 Always import chapters when segments 2022-07-10 00:56:31 -04:00
Ajay
cf86e91988 Added guideline reminders for 2022-07-09 23:42:52 -04:00
Ajay
058c41dd7e Rename chapter option to show chapter 2022-07-07 17:05:27 -04:00
Ajay
969b303c59 Limit chapter in UI to those who can submit 2022-07-05 13:34:30 -04:00
Ajay
8114e0dcf7 Fix some chapter and mute not skipping from popup 2022-07-04 01:21:36 -04:00
Ajay
561b3a2263 Fix double click skip in popup 2022-07-04 01:21:11 -04:00
Ajay
e0edb63501 Put chapter option higher up 2022-07-04 01:10:41 -04:00
Ajay
70ef867ec5 Don't count skip time for chapter 2022-07-04 01:05:14 -04:00
Ajay
23336fa65b Don't send message if tab not found 2022-07-04 00:49:32 -04:00
Ajay
fea90d024e Made render segments as chapters only affect non chapter segments 2022-07-04 00:43:55 -04:00
Ajay
de85d93602 Hide chapter chevron when it won't do anything 2022-07-03 23:50:18 -04:00
Ajay
7badfd9b32 Fix existing chapters opening skip notice when chapters disabled 2022-07-03 18:33:32 -04:00
Ajay
d0497d60e8 Add indicator where current player is for segments in popup 2022-07-03 17:53:40 -04:00
Ajay
cfecb9f94a Better import deduplication 2022-06-23 00:14:21 -04:00
Ajay
fc81e02026 Fix exporter test 2022-06-22 18:58:49 -04:00
Ajay
e12d5ff10a Better category name detection 2022-06-22 18:40:36 -04:00
Ajay
355572ba04 Add warning when chapter name similar to category 2022-06-22 18:10:07 -04:00
Ajay
70731e42a5 Only show import when chapter enabled 2022-06-22 14:03:58 -04:00
Ajay
023b875b0f Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-06-22 13:34:15 -04:00
Ajay
82b027159e Add UI for importing segments 2022-06-22 13:21:44 -04:00
Ajay
c6405fc0c1 Fix rendering chapters in specific overlapping cases 2022-06-22 13:02:04 -04:00
Ajay
1f6b8f6c53 Add end button for chapters 2022-06-19 15:47:08 -04:00
Ajay
caafba5f53 remove extra line from export 2022-06-06 22:32:02 -04:00
Ajay
32052c17f1 Add notice showing that copy happened 2022-06-06 20:41:15 -04:00
Ajay
5545a516be Added export button 2022-06-04 02:01:12 -04:00
Ajay
0fb2d8df79 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-06-04 01:57:27 -04:00
Ajay
b28d881a1b Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-06-03 15:12:42 -04:00
Ajay
b8cbdb55d5 Fix test error 2022-06-03 15:12:14 -04:00
Ajay
94fa649a17 Fix lint error 2022-06-03 13:34:41 -04:00
Ajay
c3cb450e92 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-06-03 02:20:30 -04:00
Ajay
621e28c7e7 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-05-17 14:44:54 -04:00
Ajay
9f02bf4ce2 Show both chapter names when small chapter in big chapter 2022-04-21 14:54:57 -04:00
Ajay
6325d3539c Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-04-11 01:26:39 -04:00
Ajay Ramachandran
9477ad425c Merge branch 'master' into chapters 2022-03-12 16:56:53 -05:00
Ajay
f81cfbecfe Add functions for importing/exporting segments 2022-02-26 01:07:29 -05:00
Ajay
eb35f5c543 Don't display existing chapters twice 2022-02-24 21:53:45 -05:00
Ajay
7d8188d575 Add importing chapters for cases with unsubmitted segments 2022-02-24 02:21:22 -05:00
Ajay
00ab317a3e Fix active segments when no real segments 2022-02-24 02:06:00 -05:00
Ajay
0536d419e5 Hide YouTube chapters from popup 2022-02-23 01:39:53 -05:00
Ajay
4d5c9005ae Fix stackoverflow 2022-02-23 01:37:19 -05:00
Ajay
0513a36a9a Fix chapters getting imported multiple times 2022-02-23 01:30:07 -05:00
Ajay
1ec184048c Fix chapter sorting 2022-02-23 01:24:15 -05:00
Ajay
2b811c5ab4 Include unsubmitted in active segment label 2022-02-23 01:12:19 -05:00
Ajay
ea91701430 Improve chapter generation performance by reusing elements
Also ensure large segments don't break chapter bar
2022-02-23 01:06:08 -05:00
Ajay
9654fabc3c remove old todo 2022-02-22 21:34:27 -05:00
Ajay
2ebc5489cd Load existing chapters 2022-02-22 21:22:30 -05:00
Ajay
cf3b3c5c48 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-02-22 01:34:12 -05:00
Ajay
930bc113fe Add sort segments button 2022-02-21 11:17:58 -05:00
Ajay
7aaa28b5c2 Fix transition issue 2022-02-21 01:02:23 -05:00
Ajay
bd3976e4c6 Add option to not render as chapters 2022-02-21 00:29:13 -05:00
Ajay
4e5a883d2e Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-02-20 18:37:18 -05:00
Ajay
e1de84dce3 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-01-21 20:20:27 -05:00
Ajay
9ce714fd36 Merge branch 'chapters' of https://github.com/ajayyy/SponsorBlock into chapters 2022-01-21 20:18:43 -05:00
Ajay
46983bec24 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-01-21 20:18:42 -05:00
Ajay
9d65df84be Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-01-21 20:17:34 -05:00
Ajay
e2d56d32fe Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2022-01-16 15:58:09 -05:00
Ajay
7895b9d2c1 Fix crash from preview bar not being defined yet 2022-01-02 22:44:15 -05:00
Ajay
63f6702f86 Fix chapter bar breaking when adding new unsubmitted segments 2021-12-30 01:08:54 -05:00
Ajay
02bc554b0e Fix progress not starting at full 2021-12-30 01:05:59 -05:00
Ajay
c3933a4eee reduce returned variables 2021-12-30 00:39:03 -05:00
Ajay
68c1f780d5 Fix sometimes not rendering chapters when no existing chapters 2021-12-30 00:35:39 -05:00
Ajay
496ef87a28 Reduce issues in rendering over existing chapters by replacing walking method with direct loop 2021-12-30 00:22:13 -05:00
Ajay
22e85f715d Add initial code to support drawing when there are existing chapters 2021-12-29 02:16:49 -05:00
Ajay
1a6a07744e Don't break chapter bar when existing chapters are there 2021-12-26 01:18:55 -05:00
Ajay
4a19fececf Always set segment source 2021-12-26 00:17:49 -05:00
Ajay
322a1483df Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2021-12-25 22:14:34 -05:00
Ajay
ab1520c560 Show both categories and chapter names in hover text 2021-12-25 01:09:34 -05:00
Ajay
e4f4a10965 Fix seek bar chapter name not updating and schedule end times 2021-12-24 21:52:33 -05:00
Ajay
05153a152d Add skip button to popup for segments and chapters 2021-12-24 20:35:36 -05:00
Ajay
798fd8b3f3 Add tabs for chapters and other segments 2021-12-24 02:13:25 -05:00
Ajay Ramachandran
c38cc07e0a Fix stack overflow issue with unfinished preview segments 2021-11-07 16:09:01 -05:00
Ajay Ramachandran
af547ce745 Show category description in popup 2021-11-07 15:38:41 -05:00
Ajay Ramachandran
0d0459a3a3 Make tests have config.json 2021-11-07 15:31:08 -05:00
Ajay Ramachandran
7dfee81188 Add chapter sorting method to show small chapters in the middle of large ones 2021-11-07 15:26:00 -05:00
Ajay Ramachandran
3a2d9c0e0e Update options page for chapters 2021-11-07 01:12:01 -05:00
Ajay Ramachandran
8e022bfb28 Fix offset on popup for chapters 2021-11-07 01:06:34 -05:00
Ajay Ramachandran
a69c19581d Fix chapter bar showing as empty sometimes 2021-11-07 01:28:23 -04:00
Ajay Ramachandran
a3e67b6cde Add chapter name autocomplete 2021-11-07 01:05:32 -04:00
Ajay Ramachandran
9e6e3b023d Fix small time differences between segments causing issues 2021-11-06 21:20:36 -04:00
Ajay Ramachandran
33cfe3f5d3 Show description in hover bar 2021-11-06 19:57:46 -04:00
Ajay Ramachandran
4a2ebe4b03 Fix undefined error when making segment 2021-11-04 00:34:21 -04:00
Ajay Ramachandran
374f0992ff Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2021-11-04 00:31:29 -04:00
Ajay Ramachandran
7814493974 Fix preview bar width and remove chapter refresh check 2021-11-04 00:25:58 -04:00
Ajay Ramachandran
f5fa758ac1 Don't shrink preview bar if segment not a chapter 2021-11-02 23:33:19 -04:00
Ajay Ramachandran
517e53a2e3 Fix small segments breaking chapters 2021-11-01 21:44:39 -04:00
Ajay Ramachandran
fb3635cdf8 Fix animation when chapter bar is recreated 2021-11-01 21:18:03 -04:00
Ajay Ramachandran
a804da06f5 Update chapter bar progress right after clone 2021-11-01 21:10:43 -04:00
Ajay Ramachandran
9ed9f9b873 Only create one chapter bar, support videos with no segments and preview segments 2021-11-01 20:59:04 -04:00
Ajay Ramachandran
b4a2f31520 Render preview bar behind scrubber 2021-10-30 21:48:52 -04:00
Ajay Ramachandran
c7acb902a4 Fix issues with hover failing after hovering scrubber 2021-10-30 21:32:54 -04:00
Ajay Ramachandran
37ac5c8cbd Listen for class changes for ytp-hover-progress-light and fix left 2021-10-30 15:11:22 -04:00
Ajay Ramachandran
bf4eb8fafc Fix z-index not applying 2021-10-29 00:02:46 -04:00
Ajay Ramachandran
7c4a0628b7 Add growing chapter on hover 2021-10-28 23:57:53 -04:00
Ajay Ramachandran
2d3e293d83 Support left style changes for chapters bar and fix negative size 2021-10-28 00:38:46 -04:00
Ajay Ramachandran
6dee56dc95 Add mutation listener to update progress indicators 2021-10-27 23:21:22 -04:00
Ajay Ramachandran
4c9548b303 Add basic chapter rendering for segments 2021-10-26 20:18:08 -04:00
Ajay Ramachandran
fd69e91880 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2021-10-17 19:25:16 -04:00
Ajay Ramachandran
0f4eeb4fe9 Add chapter name option when submitting 2021-10-16 01:36:44 -04:00
Ajay Ramachandran
9a24b906f9 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2021-10-15 19:57:18 -04:00
Ajay Ramachandran
496528be65 Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters 2021-10-14 21:58:37 -04:00
Ajay Ramachandran
f2c1ee4894 Use default action type from config 2021-10-14 00:03:48 -04:00
Ajay Ramachandran
acd2720372 Add messages for chapter 2021-10-13 23:50:17 -04:00
Ajay Ramachandran
e20b60979c Add initial chapter name rendering 2021-10-12 23:33:41 -04:00
73 changed files with 10048 additions and 11202 deletions

View File

@@ -30,7 +30,7 @@ jobs:
name: ChromeExtension
path: dist
- run: mkdir ./builds
- uses: montudor/action-zip@v1
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
with:
args: zip -qq -r ./builds/ChromeExtension.zip ./dist
@@ -41,7 +41,7 @@ jobs:
with:
name: FirefoxExtension
path: dist
- uses: montudor/action-zip@v1
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
with:
args: zip -qq -r ./builds/FirefoxExtension.zip ./dist
@@ -52,7 +52,7 @@ jobs:
with:
name: ChromeExtensionBeta
path: dist
- uses: montudor/action-zip@v1
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
with:
args: zip -qq -r ./builds/ChromeExtensionBeta.zip ./dist
@@ -62,7 +62,7 @@ jobs:
with:
name: FirefoxExtensionBeta
path: dist
- uses: montudor/action-zip@v1
- uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28
with:
args: zip -qq -r ./builds/FirefoxExtensionBeta.zip ./dist

View File

@@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: take the issue
uses: bdougie/take-action@main
uses: bdougie/take-action@28b86cd8d25593f037406ecbf96082db2836e928
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@@ -25,7 +25,7 @@ jobs:
mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt
- name: Create pull request to update list
uses: peter-evans/create-pull-request@v3
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
with:
commit-message: Update OSS Attribution
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

View File

@@ -19,7 +19,7 @@ jobs:
run: npm run ci:invidious
- name: Create pull request to update list
uses: peter-evans/create-pull-request@v3
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
with:
commit-message: Update Invidious List
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "__MSG_fullName__",
"short_name": "SponsorBlock",
"version": "4.6.3",
"version": "5.0.2",
"default_locale": "en",
"description": "__MSG_Description__",
"homepage_url": "https://sponsor.ajay.app",
@@ -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,10 +64,13 @@
"icons/bolt.svg",
"icons/stopwatch.svg",
"icons/music-note.svg",
"icons/import.svg",
"icons/export.svg",
"icons/PlayerInfoIconSponsorBlocker.svg",
"icons/PlayerDeleteIconSponsorBlocker.svg",
"popup.html",
"content.css"
"content.css",
"js/document.js"
],
"permissions": [
"storage",

14744
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,33 +8,34 @@
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/chrome": "^0.0.188",
"@types/chrome": "^0.0.193",
"@types/firefox-webext-browser": "^94.0.1",
"@types/jest": "^27.5.1",
"@types/react": "^17.0.43",
"@types/react-dom": "^17.0.14",
"@types/selenium-webdriver": "^4.1.1",
"@types/jest": "^28.1.6",
"@types/react": "^17.0.47",
"@types/react-dom": "^17.0.17",
"@types/selenium-webdriver": "^4.1.2",
"@types/wicg-mediasession": "^1.1.3",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
"chromedriver": "^101.0.0",
"concurrently": "^7.2.1",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"chromedriver": "^103.0.0",
"concurrently": "^7.3.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.16.0",
"eslint-plugin-react": "^7.30.0",
"fork-ts-checker-webpack-plugin": "^7.2.11",
"jest": "^28.1.0",
"eslint": "^8.20.0",
"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.2.0",
"selenium-webdriver": "^4.3.1",
"speed-measure-webpack-plugin": "^1.5.0",
"ts-jest": "^28.0.3",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.0",
"ts-jest": "^28.0.7",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typescript": "4.7",
"web-ext": "^6.8.0",
"webpack": "^5.72.1",
"webpack-cli": "^4.9.2",
"web-ext": "^7.1.1",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-merge": "^5.8.0"
},
"scripts": {

View File

@@ -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"
},
@@ -115,6 +125,9 @@
"SubmitTimes": {
"message": "Submit Segments"
},
"sortSegments": {
"message": "Sort Segments"
},
"submitCheck": {
"message": "Are you sure you want to submit this?"
},
@@ -289,6 +302,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."
},
@@ -442,6 +463,12 @@
"minDurationDescription": {
"message": "Segments shorter than the set value will not be skipped or show in the player."
},
"enableManualSkipOnFullVideo": {
"message": "Use manual skip when a full video label exists"
},
"whatManualSkipOnFullVideo": {
"message": "For people who want to watch the video uninterrupted if it is fully sponsored or self promotion."
},
"skipNoticeDuration": {
"message": "Skip notice duration (seconds):"
},
@@ -545,6 +572,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"
},
@@ -637,7 +668,7 @@
"message": "Preview/Recap"
},
"category_preview_description": {
"message": "Quick recap of previous episodes, or a preview of what's coming up later in the current video. Meant for edited together clips, not for spoken summaries."
"message": "Collection of clips that show what is coming up in in this video or other videos in a series where all information is repeated later in the video."
},
"category_preview_guideline1": {
"message": "Clips that appear later, or in a future video"
@@ -696,6 +727,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 +772,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 +830,10 @@
"bracketEnd": {
"message": "(End)"
},
"End": {
"message": "End",
"description": "Button that skips to the end of a segment"
},
"hiddenDueToDownvote": {
"message": "hidden: downvote"
},
@@ -794,11 +847,8 @@
"description": "This error appears in an alert when they try to whitelist a channel and the extension is unable to determine what channel they are looking at.",
"message": "Channel ID is not loaded yet. If you are using an embedded video, try using the YouTube homepage instead. This could also be caused by changes in the YouTube layout, if you think so, make a comment here:"
},
"videoInfoFetchFailed": {
"message": "It seems that something is blocking SponsorBlock's ability to get video data. Please see https://github.com/ajayyy/SponsorBlock/issues/741 for more info."
},
"youtubePermissionRequest": {
"message": "It seems that SponsorBlock is unable to reach the YouTube API. To fix this, accept the permission prompt that will appear next, wait a few seconds, and then reload the page."
"invidiousPermissionRefresh": {
"message": "The browser has revoked the permission needed to function on Invidious and other 3rd-party sites. Please click the button below to reactivate this permission."
},
"acceptPermission": {
"message": "Accept permission"
@@ -824,6 +874,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"
},
@@ -859,6 +916,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."
@@ -867,11 +927,19 @@
"message": "Hide forever"
},
"warningChatInfo": {
"message": "You got a warning and cannot submit segments temporarily. This means that we noticed you were making some common mistakes that are not malicious, please just confirm that you understand the rules and we will remove the warning. You can also join this chat using discord.gg/SponsorBlock or matrix.to/#/#sponsor:ajay.app"
"message": "We noticed you were making some common mistakes that are not malicious"
},
"voteRejectedWarning": {
"message": "Vote rejected due to a warning. Click to open a chat to resolve it, or come back later when you have time.",
"description": "This is an integrated chat panel that will appearing allowing them to talk to the Discord/Matrix chat without leaving their browser."
"warningTitle": {
"message": "You got a warning"
},
"questionButton": {
"message": "I have a question"
},
"warningConfirmButton": {
"message": "I understand the reason"
},
"warningError": {
"message": "Error when trying to acknowledge warning:"
},
"Donate": {
"message": "Donate"
@@ -1034,5 +1102,117 @@
},
"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 segments"
},
"Import": {
"message": "Import",
"description": "Button to initiate importing segments. Appears under the textbox where they paste in the data"
},
"redeemSuccess": {
"message": "Redeem 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. The word 'here' should be translated as well."
},
"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"
},
"unsubmittedSegmentCounts": {
"message": "You currently have {0} on {1}",
"description": "Example: You currently have 12 unsubmitted segments on 5 videos"
},
"unsubmittedSegmentCountsZero": {
"message": "You currently have no unsubmitted segments",
"description": "Replaces 'unsubmittedSegmentCounts' string when there are no unsubmitted segments"
},
"unsubmittedSegmentsSingular": {
"message": "unsubmitted segment",
"description": "Example: You currently have 1 *unsubmitted segment* on 1 video"
},
"unsubmittedSegmentsPlural": {
"message": "unsubmitted segments",
"description": "Example: You currently have 12 *unsubmitted segments* on 5 videos"
},
"videosSingular": {
"message": "video",
"description": "Example: You currently have 3 unsubmitted segments on 1 *video*"
},
"videosPlural": {
"message": "videos",
"description": "Example: You currently have 12 unsubmitted segments on 5 *videos*"
},
"clearUnsubmittedSegments": {
"message": "Clear all segments",
"description": "Label for a button in settings"
},
"clearUnsubmittedSegmentsConfirm": {
"message": "Are you sure you want to clear all your unsubmitted segments?",
"description": "Confirmation message for the Clear unsubmitted segments button"
},
"showUnsubmittedSegments": {
"message": "Show segments",
"description": "Show/hide button for the unsubmitted segments list"
},
"hideUnsubmittedSegments": {
"message": "Hide segments",
"description": "Show/hide button for the unsubmitted segments list"
},
"videoID": {
"message": "Video ID",
"description": "Header of the unsubmitted segments list"
},
"segmentCount": {
"message": "Segment Count",
"description": "Header of the unsubmitted segments list"
},
"actions": {
"message": "Actions",
"description": "Header of the unsubmitted segments list"
},
"exportSegmentsAsURL": {
"message": "Share as URL"
}
}

View File

@@ -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,16 +21,20 @@
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);
}
.ytp-big-mode #previewbar {
transform: scaleY(0.625) translateY(-30%) translateY(1.5px);
}
.progress-bar-line > #previewbar {
height: 3px;
}
#previewbar.hovered {
div:hover > #previewbar.sbNotInvidious {
transform: scaleY(1)
}
@@ -30,6 +43,10 @@
height: 100%;
}
.previewbar.requiredSegment {
transform: scaleY(3)
}
/* Make sure settings are upfront */
.ytp-settings-menu {
z-index: 6000 !important;
@@ -45,23 +62,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 +130,20 @@
vertical-align: top;
}
.playerButton.hidden:not(.autoHiding) {
display: none !important;
}
/* 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 +169,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 +201,7 @@
position: absolute;
right: 5px;
bottom: 100px;
right: 10px;
right: var(--skip-notice-right);
}
.sponsorSkipNoticeParent {
@@ -173,6 +229,7 @@
}
.sponsorSkipNoticeTableContainer {
color: white;
background-color: rgba(28, 28, 28, 0.9);
border-radius: 5px;
min-width: 100%;
@@ -351,6 +408,7 @@
.sponsorTimesInfoMessage {
font-size: 13.3333px;
color: rgb(235, 235, 235);
overflow-wrap: anywhere;
}
.sb-guidelines-notice .sponsorTimesInfoMessage td {
@@ -523,12 +581,56 @@ 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;
}
.sponsorTimeEditSelector > option {
background-color: rgba(28, 28, 28, 0.9);
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;
@@ -543,17 +645,6 @@ input::-webkit-inner-spin-button {
opacity: 0.8;
}
.sbChatNotice iframe {
height: 32px;
cursor: pointer;
height: 100%;
}
.sbChatClose {
height: 14px;
cursor: pointer;
}
.skipButtonControlBarContainer {
cursor: pointer;
display: flex;
@@ -627,6 +718,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;
}

106
public/icons/export.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 2.7 KiB

91
public/icons/import.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 2.3 KiB

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>

After

Width:  |  Height:  |  Size: 196 B

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/></svg>

After

Width:  |  Height:  |  Size: 201 B

View File

@@ -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,14 @@ input[type='number'] {
color: grey;
}
.disabled .slider {
cursor: default;
}
tr.disabled {
opacity: 0.3;
}
#options {
height: 100vh;
flex-basis: 80%;
@@ -346,6 +362,10 @@ input[type='number'] {
padding: 4px;
}
.switch-label {
width: inherit;
}
.switch {
position: relative;
display: inline-block;
@@ -670,4 +690,9 @@ svg {
#options > div {
max-width: 100%;
}
}
.upsellButton {
cursor: pointer;
vertical-align: middle;
}

View File

@@ -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">
@@ -110,6 +98,20 @@
<div class="small-description">__MSG_minDurationDescription__</div>
</div>
<div data-type="toggle" data-sync="manualSkipOnFullVideo">
<div class="switch-container">
<label class="switch">
<input id="manualSkipOnFullVideo" type="checkbox" checked>
<span class="slider round"></span>
</label>
<label class="switch-label" for="manualSkipOnFullVideo">
__MSG_enableManualSkipOnFullVideo__
</label>
</div>
<div class="small-description">__MSG_whatManualSkipOnFullVideo__</div>
</div>
<div data-type="toggle" data-sync="forceChannelCheck">
<div class="switch-container">
@@ -314,6 +316,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 +347,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">
@@ -352,6 +376,8 @@
</div>
</div>
</div>
<div data-type="react-UnsubmittedVideosComponent"></div>
<div data-type="private-text-change" data-sync="*" data-confirm-message="exportOptionsWarning">
<h2>__MSG_exportOptions__</h2>
@@ -480,7 +506,7 @@
<div class="small-description">__MSG_copyDebugInformationOptions__</div>
</div>
<div data-type="toggle" data-sync="testingServer" data-confirm-message="testingServerWarning" data-no-safari="true">
<div class="switch-container">
<label class="switch">

View File

@@ -19,6 +19,12 @@
<br/>
<div class="center">
__MSG_invidiousPermissionRefresh__
</div>
<br/>
<div class="center">
<div id="acceptPermissionButton" class="option-button inline">
__MSG_acceptPermission__

View File

@@ -100,6 +100,10 @@
margin-bottom: 16px;
}
#sponsorBlockPopupContainer iframe {
width: 100%;
}
/*
* Disable popup max height when displayed in-page (content.ts)
*/
@@ -110,7 +114,7 @@
/*
* Disable fixed popup width when displayed in-page (content.ts)
*/
#sponsorBlockPopupContainer #sponsorBlockPopupBody {
#sponsorBlockPopupBody.is-embedded {
width: auto;
}
@@ -148,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
*/
@@ -195,6 +223,15 @@
.segmentSummary > div {
text-align: left;
}
.segmentActive {
color: #bdfffb;
}
.segmentPassed {
color: #adadad;
}
/*
* Category dot in segment
*/
@@ -207,7 +244,7 @@
border-radius: 50%;
display: inline-block;
}
/*
/*
* Category name in segment
*/
.summaryLabel {
@@ -511,7 +548,7 @@
background: var(--sb-grey-bg-color);
}
/*
/*
* Submissions
*/
#sponsorTimesContributionsContainer {
@@ -556,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);
}

View File

@@ -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">
<div id="importExportButtons">
<button id="importSegmentsButton" title="__MSG_importSegments__">
<img src="/icons/import.svg" alt="Refresh icon" id="importSegments" />
</button>
<button id="exportSegmentsButton" class="hidden" 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>

View File

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

219
public/shared.css Normal file
View File

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

94
public/upsell/index.html Normal file
View File

@@ -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%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__ sponsorblock-free@ajay.app
</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>

387
public/upsell/styles.css Normal file
View File

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

View File

@@ -8,11 +8,14 @@ import { Registration } from "./types";
window.SB = Config;
import Utils from "./utils";
import { GenericUtils } from "./utils/genericUtils";
const utils = new Utils({
registerFirefoxContentScript,
unregisterFirefoxContentScript
});
const popupPort: Record<string, chrome.runtime.Port> = {};
// Used only on Firefox, which does not support non persistent background pages.
const contentScriptRegistrations = {};
@@ -52,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 : ''))});
@@ -99,14 +102,30 @@ 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.
// This is required on Firefox
setTimeout(function() {
setTimeout(async () => {
const userID = Config.config.userID;
// If there is no userID, then it is the first install.
@@ -115,13 +134,19 @@ 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;
// Don't show update notification
Config.config.categoryPillUpdate = true;
}
if (Config.config.supportInvidious) {
if (!(await utils.containsInvidiousPermission())) {
chrome.tabs.create({url: chrome.extension.getURL("/permissions/index.html")});
}
}
}, 1500);
});
@@ -158,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;
}
@@ -205,7 +230,7 @@ async function asyncRequestToServer(type: string, address: string, data = {}) {
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
// If GET, convert JSON to parameters
if (type.toLowerCase() === "get") {
url = utils.objectToURI(url, data, true);
url = GenericUtils.objectToURI(url, data, true);
data = null;
}

View File

@@ -0,0 +1,126 @@
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 {
if (this.tooltip && !this.state.show) {
this.tooltip.close();
this.tooltip = null;
}
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={null} height={null} />
</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={null}
height={null} />
</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;

View File

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

View File

@@ -36,12 +36,31 @@ class NoticeTextSelectionComponent extends React.Component<NoticeTextSelectionPr
: null}
<span>
{this.props.text}
{this.getTextElements(this.props.text)}
</span>
</td>
</tr>
);
}
private getTextElements(text: string): Array<string | React.ReactElement> {
const elements: Array<string | React.ReactElement> = [];
const textParts = text.split(/(?=\s+)/);
for (const textPart of textParts) {
if (textPart.match(/^\s*http/)) {
elements.push(
<a href={textPart} target="_blank" rel="noreferrer">
{textPart}
</a>
);
} else {
elements.push(textPart);
}
}
return elements;
}
}
export default NoticeTextSelectionComponent;

View File

@@ -0,0 +1,58 @@
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={(e) => {
e.stopPropagation();
this.props.onChange(option.label);
}}
key={option.label}>
{option.label}
</div>
);
}
return result;
}
}
export default SelectorComponent;

View File

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

View File

@@ -1,10 +1,13 @@
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";
import { noRefreshFetchingChaptersAllowed } from "../utils/licenseKey";
const utils = new Utils();
@@ -25,16 +28,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 +52,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 +106,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
render(): React.ReactElement {
this.checkToShowFullVideoWarning();
this.checkToShowChapterWarning();
const style: React.CSSProperties = {
textAlign: "center"
@@ -96,14 +116,6 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
style.marginTop = "15px";
}
// This method is required to get !important
// https://stackoverflow.com/a/45669262/1985387
const oldYouTubeDarkStyles = (node) => {
if (node) {
node.style.setProperty("color", "black", "important");
node.style.setProperty("text-shadow", "none", "important");
}
};
// Create time display
let timeDisplay: JSX.Element;
const timeDisplayStyle: React.CSSProperties = {};
@@ -123,11 +135,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
</span>
<input id={"submittingTime0" + this.idSuffix}
className="sponsorTimeEdit sponsorTimeEditInput"
ref={oldYouTubeDarkStyles}
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 ? (
@@ -138,11 +150,11 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
<input id={"submittingTime1" + this.idSuffix}
className="sponsorTimeEdit sponsorTimeEditInput"
ref={oldYouTubeDarkStyles}
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}
@@ -167,15 +179,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}
@@ -185,7 +197,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
className="sponsorTimeEditSelector sponsorTimeCategories"
defaultValue={sponsorTime.category}
ref={this.categoryOptionRef}
onChange={this.categorySelectionChange.bind(this)}>
style={{color: "inherit", backgroundColor: "inherit"}}
onChange={(event) => this.categorySelectionChange(event)}>
{this.getCategoryOptions()}
</select>
@@ -208,6 +221,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
<select id={"sponsorTimeActionTypes" + this.idSuffix}
className="sponsorTimeEditSelector sponsorTimeActionTypes"
defaultValue={sponsorTime.actionType}
style={{color: "inherit", backgroundColor: "inherit"}}
ref={this.actionTypeOptionRef}
onChange={(e) => this.actionTypeSelectionChange(e)}>
{this.getActionTypeOptions(sponsorTime)}
@@ -215,6 +229,28 @@ 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}
onContextMenu={(e) => e.stopPropagation()}
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 */}
@@ -229,7 +265,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>
): ""}
@@ -256,16 +293,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 {
@@ -281,7 +317,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;
@@ -290,7 +326,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});
@@ -300,26 +337,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 {
@@ -334,12 +374,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}
@@ -349,6 +402,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] && (category !== "chapter" || noRefreshFetchingChaptersAllowed());
if ((defaultBlockCategories.includes(category) || permission !== undefined) && !permission) continue;
elements.push(
<option value={category}
key={category}
@@ -369,7 +427,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
@@ -420,7 +478,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
this.previousSkipType = ActionType.Full;
} else if ((category === "chooseACategory" || (CompileConfig.categorySupport[category]?.includes(ActionType.Skip)
} else if ((category === "chooseACategory" || ((CompileConfig.categorySupport[category]?.includes(ActionType.Skip)
|| CompileConfig.categorySupport[category]?.includes(ActionType.Chapter))
&& ![ActionType.Poi, ActionType.Full].includes(this.getNextActionType(category, actionType))))
&& this.previousSkipType !== ActionType.Skip) {
if (this.timesBeforeChanging[0]) {
@@ -470,7 +529,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.setState({
sponsorTimeEdits: this.getFormattedSponsorTimesEdits(sponsorTime)
}, this.saveEditTimes);
}, () => this.saveEditTimes());
}
toggleEditTime(): void {
@@ -493,16 +552,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) {
@@ -513,8 +572,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");
@@ -536,19 +598,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 {
@@ -592,6 +654,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();
}

View File

@@ -73,12 +73,20 @@ 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()}
title={chrome.i18n.getMessage("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 +206,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

View File

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

View File

@@ -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,10 +31,28 @@ 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 {
if (this.state.hideChapter) {
// Ensure force update refreshes this
fetchingChaptersAllowed().then((allowed) => {
if (allowed) {
this.setState({
hideChapter: !allowed
});
}
});
}
let defaultOption = "disable";
// Set the default opton properly
for (const categorySelection of Config.config.categorySelections) {
@@ -51,10 +73,20 @@ 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">
{chrome.i18n.getMessage("category_" + this.props.category)}
@@ -65,21 +97,29 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
<select
className="optionsSelector"
defaultValue={defaultOption}
disabled={disabled}
onChange={this.skipOptionSelected.bind(this)}>
{this.getCategorySkipOptions()}
</select>
{disabled &&
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
}
</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 +133,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 +143,8 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</a>
</td>
</tr>
{this.getExtraOptionComponents(this.props.category, extraClasses, disabled)}
</>
);
@@ -112,7 +154,9 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
let option: CategorySkipOption;
switch (event.target.value) {
case "disable":
case "disable":
Config.config.categorySelections = Config.config.categorySelections.filter(
categorySelection => categorySelection.name !== this.props.category);
return;
case "showOverlay":
option = CategorySkipOption.ShowOverlay;
@@ -145,7 +189,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(
@@ -182,6 +227,43 @@ 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}
style={{width: "inherit"}}
/>
</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;

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
import * as React from "react";
import Config from "../../config";
export interface ToggleOptionProps {
configKey: string;
label: string;
disabled?: boolean;
style?: React.CSSProperties;
}
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" style={this.props.style}>
<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;

View File

@@ -0,0 +1,72 @@
import * as React from "react";
import Config from "../../config";
import UnsubmittedVideoListItem from "./UnsubmittedVideoListItem";
export interface UnsubmittedVideoListProps {
}
export interface UnsubmittedVideoListState {
}
class UnsubmittedVideoListComponent extends React.Component<UnsubmittedVideoListProps, UnsubmittedVideoListState> {
constructor(props: UnsubmittedVideoListProps) {
super(props);
// Setup state
this.state = {
};
}
render(): React.ReactElement {
// Render nothing if there are no unsubmitted segments
if (Object.keys(Config.config.unsubmittedSegments).length == 0)
return <></>;
return (
<table id="unsubmittedVideosList"
className="categoryChooserTable"
style={{marginTop: "10px"}} >
<tbody>
{/* Headers */}
<tr id="UnsubmittedVideosListHeader"
className="categoryTableElement categoryTableHeader">
<th id="UnsubmittedVideoID">
{chrome.i18n.getMessage("videoID")}
</th>
<th id="UnsubmittedSegmentCount">
{chrome.i18n.getMessage("segmentCount")}
</th>
<th id="UnsubmittedVideoActions">
{chrome.i18n.getMessage("actions")}
</th>
</tr>
{this.getUnsubmittedVideos()}
</tbody>
</table>
);
}
getUnsubmittedVideos(): JSX.Element[] {
const elements: JSX.Element[] = [];
for (const videoID of Object.keys(Config.config.unsubmittedSegments)) {
elements.push(
<UnsubmittedVideoListItem videoID={videoID} key={videoID}>
</UnsubmittedVideoListItem>
);
}
return elements;
}
}
export default UnsubmittedVideoListComponent;

View File

@@ -0,0 +1,95 @@
import * as React from "react";
import Config from "../../config";
import { exportTimes, exportTimesAsHashParam } from "../../utils/exporter";
export interface UnsubmittedVideosListItemProps {
videoID: string;
}
export interface UnsubmittedVideosListItemState {
}
class UnsubmittedVideoListItem extends React.Component<UnsubmittedVideosListItemProps, UnsubmittedVideosListItemState> {
constructor(props: UnsubmittedVideosListItemProps) {
super(props);
// Setup state
this.state = {
};
}
render(): React.ReactElement {
const segmentCount = Config.config.unsubmittedSegments[this.props.videoID]?.length ?? 0;
return (
<>
<tr id={this.props.videoID + "UnsubmittedSegmentsRow"}
className="categoryTableElement">
<td id={this.props.videoID + "UnsubmittedVideoID"}
className="categoryTableLabel">
<a href={`https://youtu.be/${this.props.videoID}`}
target="_blank" rel="noreferrer">
{this.props.videoID}
</a>
</td>
<td id={this.props.videoID + "UnsubmittedSegmentCount"}>
{segmentCount}
</td>
<td id={this.props.videoID + "UnsubmittedVideoActions"}>
<div id={this.props.videoID + "ExportSegmentsAction"}
className="option-button inline low-profile"
onClick={this.exportSegments.bind(this)}>
{chrome.i18n.getMessage("exportSegments")}
</div>
{" "}
<div id={this.props.videoID + "ExportSegmentsAsURLAction"}
className="option-button inline low-profile"
onClick={this.exportSegmentsAsURL.bind(this)}>
{chrome.i18n.getMessage("exportSegmentsAsURL")}
</div>
{" "}
<div id={this.props.videoID + "ClearSegmentsAction"}
className="option-button inline low-profile"
onClick={this.clearSegments.bind(this)}>
{chrome.i18n.getMessage("clearTimes")}
</div>
</td>
</tr>
</>
);
}
clearSegments(): void {
if (confirm(chrome.i18n.getMessage("clearThis"))) {
delete Config.config.unsubmittedSegments[this.props.videoID];
Config.forceSyncUpdate("unsubmittedSegments");
}
}
exportSegments(): void {
this.copyToClipboard(exportTimes(Config.config.unsubmittedSegments[this.props.videoID]));
}
exportSegmentsAsURL(): void {
this.copyToClipboard(`https://youtube.com/watch?v=${this.props.videoID}${exportTimesAsHashParam(Config.config.unsubmittedSegments[this.props.videoID])}`)
}
copyToClipboard(text: string): void {
navigator.clipboard.writeText(text)
.then(() => {
alert(chrome.i18n.getMessage("CopiedExclamation"));
})
.catch(() => {
alert(chrome.i18n.getMessage("copyDebugInformationFailed"));
});
}
}
export default UnsubmittedVideoListItem;

View File

@@ -0,0 +1,55 @@
import * as React from "react";
import Config from "../../config";
import UnsubmittedVideoListComponent from "./UnsubmittedVideoListComponent";
export interface UnsubmittedVideosProps {
}
export interface UnsubmittedVideosState {
tableVisible: boolean,
}
class UnsubmittedVideosComponent extends React.Component<UnsubmittedVideosProps, UnsubmittedVideosState> {
constructor(props: UnsubmittedVideosProps) {
super(props);
this.state = {
tableVisible: false,
};
}
render(): React.ReactElement {
const videoCount = Object.keys(Config.config.unsubmittedSegments).length;
const segmentCount = Object.values(Config.config.unsubmittedSegments).reduce((acc: number, vid: Array<unknown>) => acc + vid.length, 0);
return <>
<div style={{marginBottom: "10px"}}>
{segmentCount == 0 ?
chrome.i18n.getMessage("unsubmittedSegmentCountsZero") :
chrome.i18n.getMessage("unsubmittedSegmentCounts")
.replace("{0}", `${segmentCount} ${chrome.i18n.getMessage("unsubmittedSegments" + (segmentCount == 1 ? "Singular" : "Plural"))}`)
.replace("{1}", `${videoCount} ${chrome.i18n.getMessage("videos" + (videoCount == 1 ? "Singular" : "Plural"))}`)
}
</div>
{videoCount > 0 && <div className="option-button inline" onClick={() => this.setState({tableVisible: !this.state.tableVisible})}>
{chrome.i18n.getMessage(this.state.tableVisible ? "hideUnsubmittedSegments" : "showUnsubmittedSegments")}
</div>}
{" "}
<div className="option-button inline" onClick={this.clearAllSegments}>
{chrome.i18n.getMessage("clearUnsubmittedSegments")}
</div>
{this.state.tableVisible && <UnsubmittedVideoListComponent/>}
</>;
}
clearAllSegments(): void {
if (confirm(chrome.i18n.getMessage("clearUnsubmittedSegmentsConfirm")))
Config.config.unsubmittedSegments = {};
}
}
export default UnsubmittedVideosComponent;

View File

@@ -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,
@@ -19,6 +25,7 @@ interface SBConfig {
disableSkipping: boolean,
muteSegments: boolean,
fullVideoSegments: boolean,
manualSkipOnFullVideo: boolean,
trackViewCount: boolean,
trackViewCountInPrivate: boolean,
trackDownvotes: boolean,
@@ -44,6 +51,7 @@ interface SBConfig {
allowExpirements: boolean,
showDonationLink: boolean,
showPopupDonationCount: number,
showUpsells: boolean,
donateClicked: number,
autoHideInfoButton: boolean,
autoSkipOnMusicVideos: boolean,
@@ -56,6 +64,7 @@ interface SBConfig {
categoryPillUpdate: boolean,
darkMode: boolean,
showCategoryGuidelines: boolean,
chaptersAvailable: boolean,
// Used to cache calculated text color info
categoryPillColors: {
@@ -68,10 +77,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 +146,10 @@ const Config: SBObject = {
syncDefaults: {
userID: null,
isVip: false,
permissions: {},
unsubmittedSegments: {},
defaultCategory: "chooseACategory" as Category,
renderSegmentsAsChapters: false,
whitelistedChannels: [],
forceChannelCheck: false,
minutesSaved: 0,
@@ -140,6 +160,7 @@ const Config: SBObject = {
disableSkipping: false,
muteSegments: true,
fullVideoSegments: true,
manualSkipOnFullVideo: false,
trackViewCount: true,
trackViewCountInPrivate: true,
trackDownvotes: true,
@@ -165,6 +186,7 @@ const Config: SBObject = {
allowExpirements: true,
showDonationLink: true,
showPopupDonationCount: 0,
showUpsells: true,
donateClicked: 0,
autoHideInfoButton: true,
autoSkipOnMusicVideos: false,
@@ -172,6 +194,7 @@ const Config: SBObject = {
categoryPillUpdate: false,
darkMode: true,
showCategoryGuidelines: true,
chaptersAvailable: true,
categoryPillColors: {},
@@ -185,6 +208,8 @@ const Config: SBObject = {
skipKeybind: {key: "Enter"},
startSponsorKeybind: {key: ";"},
submitKeybind: {key: "'"},
nextChapterKeybind: {key: "]"},
previousChapterKeybind: {key: "["},
categorySelections: [{
name: "sponsor" as Category,
@@ -197,6 +222,13 @@ const Config: SBObject = {
option: CategorySkipOption.ShowOverlay
}],
payments: {
licenseKey: null,
lastCheck: 0,
freeAccess: false,
chaptersAllowed: false
},
colorPalette: {
red: "#780303",
white: "#ffffff",
@@ -516,6 +548,8 @@ function migrateOldSyncFormats(config: SBConfig) {
}
async function setupConfig() {
if (typeof(chrome) === "undefined") return;
await fetchConfig();
addDefaults();
const config = configProxy();

File diff suppressed because it is too large Load Diff

93
src/document.ts Normal file
View File

@@ -0,0 +1,93 @@
/*
Content script are run in an isolated DOM so it is not possible to access some key details that are sanitized when passed cross-dom
This script is used to get the details from the page and make them available for the content script by being injected directly into the page
*/
import { PageType } from "./types";
interface StartMessage {
type: "navigation",
pageType: PageType
videoID: string | null,
}
interface FinishMessage extends StartMessage {
channelID: string,
channelTitle: string
}
interface AdMessage {
type: "ad",
playing: boolean
}
interface VideoData {
type: "data",
videoID: string,
isLive: boolean,
isPremiere: boolean
}
type WindowMessage = StartMessage | FinishMessage | AdMessage | VideoData;
// global playerClient - too difficult to type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let playerClient: any;
const sendMessage = (message: WindowMessage): void => {
window.postMessage({ source: "sponsorblock", ...message }, "/");
}
function setupPlayerClient(e: CustomEvent): void {
if (playerClient) return; // early exit if already defined
playerClient = e.detail;
sendVideoData(); // send playerData after setup
e.detail.addEventListener('onAdStart', () => sendMessage({ type: "ad", playing: true } as AdMessage));
e.detail.addEventListener('onAdFinish', () => sendMessage({ type: "ad", playing: false } as AdMessage));
}
document.addEventListener("yt-player-updated", setupPlayerClient);
document.addEventListener("yt-navigate-start", navigationStartSend);
document.addEventListener("yt-navigate-finish", navigateFinishSend);
function navigationParser(event: CustomEvent): StartMessage {
const pageType: PageType = event.detail.pageType;
if (pageType) {
const result: StartMessage = { type: "navigation", pageType, videoID: null };
if (pageType === "shorts" || pageType === "watch") {
const endpoint = event.detail.endpoint
if (!endpoint) return null;
result.videoID = (pageType === "shorts" ? endpoint.reelWatchEndpoint : endpoint.watchEndpoint).videoId;
}
return result;
} else {
return null;
}
}
function navigationStartSend(event: CustomEvent): void {
const message = navigationParser(event) as StartMessage;
if (message) {
sendMessage(message);
}
}
function navigateFinishSend(event: CustomEvent): void {
sendVideoData(); // arrived at new video, send video data
const videoDetails = event.detail?.response?.playerResponse?.videoDetails;
if (videoDetails) {
sendMessage({ channelID: videoDetails.channelId, channelTitle: videoDetails.author, ...navigationParser(event) } as FinishMessage);
}
}
function sendVideoData(): void {
if (!playerClient) return;
const videoData = playerClient.getVideoData();
if (videoData) {
sendMessage({ type: "data", videoID: videoData.video_id, isLive: videoData.isLive, isPremiere: videoData.isPremiere } as VideoData);
}
}

View File

@@ -1,47 +0,0 @@
import Config from "../config";
import Utils from "../utils";
const utils = new Utils();
export interface ChatConfig {
displayName: string,
composerInitialValue?: string,
customDescription?: string
}
export function openChat(config: ChatConfig): void {
const chat = document.createElement("div");
chat.classList.add("sbChatNotice");
chat.style.zIndex = "2000";
const iframe= document.createElement("iframe");
iframe.src = "https://chat.sponsor.ajay.app/#" + utils.objectToURI("", config, false);
chat.appendChild(iframe);
const closeButton = document.createElement("img");
closeButton.classList.add("sbChatClose");
closeButton.src = chrome.extension.getURL("icons/close.png");
closeButton.addEventListener("click", () => {
chat.remove();
closeButton.remove();
});
chat.appendChild(closeButton);
const referenceNode = utils.findReferenceNode();
referenceNode.prepend(chat);
}
export async function openWarningChat(warningMessage: string): Promise<void> {
const warningReasonMatch = warningMessage.match(/Warning reason: '(.+)'/);
alert(chrome.i18n.getMessage("warningChatInfo") + `\n\n${warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``}`);
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
const publicUserID = await utils.getHash(Config.config.userID);
openChat({
displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`,
composerInitialValue: `I got a warning and confirm I [REMOVE THIS CAPITAL TEXT TO CONFIRM] reread the guidelines.` +
warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``,
customDescription: chrome.i18n.getMessage("warningChatInfo")
});
}

View File

@@ -6,41 +6,71 @@ 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";
import { findValidElement } from "../utils/pageUtils";
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;
requiredSegment?: boolean;
}
interface ChapterGroup extends SegmentContainer {
originalDuration: number;
actionType: ActionType;
}
class PreviewBar {
container: HTMLUListElement;
categoryTooltip?: HTMLDivElement;
tooltipContainer?: HTMLElement;
categoryTooltipContainer?: HTMLElement;
chapterTooltip?: HTMLDivElement;
parent: HTMLElement;
onMobileYouTube: boolean;
onInvidious: boolean;
progressBar: HTMLElement;
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>;
chapterMargin: number;
unfilteredChapterGroups: ChapterGroup[];
chapterGroups: ChapterGroup[];
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.updatePageElements();
this.createElement(parent);
this.createChapterMutationObservers();
this.setupHoverText();
}
@@ -48,19 +78,26 @@ class PreviewBar {
setupHoverText(): void {
if (this.onMobileYouTube || this.onInvidious) return;
// delete old ones
document.querySelectorAll(`.sponsorCategoryTooltip`)
.forEach((e) => e.remove());
// 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,10 +113,10 @@ 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")) {
if (mutations.some((mutation) => (mutation.target as HTMLElement).classList.contains("sponsorCategoryTooltip"))) {
return;
}
@@ -93,7 +130,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 +138,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 +173,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,50 +196,94 @@ 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
this.parent.addEventListener("mouseenter", () => this.container.classList.add("hovered"));
this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered"));
this.container.classList.add("sbNotInvidious");
}
// 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;
this.updatePageElements();
// Sometimes video duration is inaccurate, pull from accessibility info
const ariaDuration = parseInt(this.progressBar?.getAttribute('aria-valuemax')) ?? 0;
if (ariaDuration && Math.abs(ariaDuration - this.videoDuration) > 3) {
this.videoDuration = ariaDuration;
}
this.update();
}
private updatePageElements(): void {
const allProgressBars = document.querySelectorAll('.ytp-progress-bar') as NodeListOf<HTMLElement>;
this.progressBar = findValidElement(allProgressBars) ?? allProgressBars?.[0];
const newChapterBar = this.progressBar.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement;
if (this.originalChapterBar !== newChapterBar) {
// Make sure changes are undone on old bar
this.originalChapterBar?.style?.removeProperty("display");
this.originalChapterBar = newChapterBar;
}
}
private update(): void {
this.clear();
if (!segments) return;
if (!this.segments) return;
this.segments = segments;
this.videoDuration = videoDuration;
this.chapterMargin = 2;
if (this.originalChapterBar) {
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]);
if (this.existingChapters?.length > 0) {
const margin = parseFloat(this.originalChapterBarBlocks?.[0]?.style?.marginRight?.replace("px", ""));
if (margin) this.chapterMargin = margin;
}
}
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 (chapterChevron) {
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');
if (barSegment.requiredSegment) bar.classList.add("requiredSegment");
bar.innerHTML = showLarger ? '&nbsp;&nbsp;' : '&nbsp;';
const fullCategoryName = (unsubmitted ? 'preview-' : '') + category;
@@ -202,7 +294,10 @@ 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) && segment[1] < this.videoDuration ? ` - ${this.chapterMargin}px` : ''})`;
}
const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
bar.style.left = this.timeToPercentage(time);
@@ -210,6 +305,464 @@ class PreviewBar {
return bar;
}
createChaptersBar(segments: PreviewBarSegment[]): void {
if (!this.progressBar || !this.originalChapterBar || this.originalChapterBar.childElementCount <= 0) {
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
return;
}
// Merge overlapping chapters
this.unfilteredChapterGroups = this.createChapterRenderGroups(segments);
if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|| (!Config.config.renderSegmentsAsChapters
&& segments.every((segment) => segment.actionType !== ActionType.Chapter
|| segment.source === SponsorSourceType.YouTube))
|| this.chapterGroups?.length <= 0) {
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
this.originalChapterBar.style.removeProperty("display");
return;
}
const filteredSegments = segments?.filter((segment) => this.chapterFilter(segment));
if (filteredSegments) {
let groups = this.unfilteredChapterGroups;
if (filteredSegments.length !== segments.length) {
groups = this.createChapterRenderGroups(filteredSegments);
}
this.chapterGroups = groups.filter((segment) => this.chapterGroupFilter(segment));
if (groups.length !== this.chapterGroups.length) {
// Fix missing sections due to filtered segments
for (let i = 1; i < this.chapterGroups.length; i++) {
if (this.chapterGroups[i].segment[0] !== this.chapterGroups[i - 1].segment[1]) {
this.chapterGroups[i - 1].segment[1] = this.chapterGroups[i].segment[0]
}
}
}
} else {
this.chapterGroups = this.unfilteredChapterGroups;
}
// Create it from cloning
let createFromScratch = false;
if (!this.customChaptersBar || !this.progressBar.contains(this.customChaptersBar)) {
// Clear anything remaining
document.querySelectorAll(".sponsorBlockChapterBar").forEach((element) => element.remove());
createFromScratch = true;
this.customChaptersBar = this.originalChapterBar.cloneNode(true) as HTMLElement;
this.customChaptersBar.classList.add("sponsorBlockChapterBar");
}
this.customChaptersBar.style.display = "none";
const originalSections = this.customChaptersBar.querySelectorAll(".ytp-chapter-hover-container");
const originalSection = originalSections[0];
// For switching to a video with less chapters
if (originalSections.length > this.chapterGroups.length) {
for (let i = originalSections.length - 1; i >= this.chapterGroups.length; i--) {
this.customChaptersBar.removeChild(originalSections[i]);
}
}
// Modify it to have sections for each segment
for (let i = 0; i < this.chapterGroups.length; i++) {
const chapter = this.chapterGroups[i].segment;
let newSection = originalSections[i] as HTMLElement;
if (!newSection) {
newSection = originalSection.cloneNode(true) as HTMLElement;
this.firstTimeSetupChapterSection(newSection);
this.customChaptersBar.appendChild(newSection);
} else if (createFromScratch) {
this.firstTimeSetupChapterSection(newSection);
}
this.setupChapterSection(newSection, chapter[0], chapter[1], i !== this.chapterGroups.length - 1);
}
// Hide old bar
this.originalChapterBar.style.display = "none";
this.customChaptersBar.style.removeProperty("display");
if (createFromScratch) {
if (this.container?.parentElement === this.progressBar) {
this.progressBar.insertBefore(this.customChaptersBar, this.container.nextSibling);
} else {
this.progressBar.prepend(this.customChaptersBar);
}
}
this.updateChapterAllMutation(this.originalChapterBar, this.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];
}
const priorityActionType = this.getActionTypePrioritized([segment.actionType, latestChapter?.actionType]);
// Split the latest chapter if smaller
result.push({
segment: [segment.segment[0], segment.segment[1]],
originalDuration: segmentDuration,
actionType: priorityActionType
});
if (latestValidChapter?.segment[1] > segment.segment[1]) {
result.push({
segment: [segment.segment[1], latestValidChapter.segment[1]],
originalDuration: latestValidChapter.originalDuration,
actionType: latestValidChapter.actionType
});
}
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,
actionType: segment.actionType
});
}
} 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,
actionType: null
});
}
// Normal case
const endTime = Math.min(segment.segment[1], this.videoDuration);
result.push({
segment: [segment.segment[0], endTime],
originalDuration: endTime - segment.segment[0],
actionType: segment.actionType
});
}
// 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,
actionType: null
});
}
}
});
return result;
}
private getActionTypePrioritized(actionTypes: ActionType[]): ActionType {
if (actionTypes.includes(ActionType.Skip)) {
return ActionType.Skip;
} else if (actionTypes.includes(ActionType.Mute)) {
return ActionType.Mute;
} else {
return actionTypes.find(a => a) ?? actionTypes[0];
}
}
private setupChapterSection(section: HTMLElement, startTime: number, endTime: number, addMargin: boolean): void {
const sizePercent = this.intervalToPercentage(startTime, endTime);
if (addMargin) {
section.style.marginRight = `${this.chapterMargin}px`;
section.style.width = `calc(${sizePercent} - ${this.chapterMargin}px)`;
} 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 {
if (!this.progressBar || !this.originalChapterBar) 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, this.progressBar);
});
attributeObserver.observe(this.originalChapterBar, {
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, this.progressBar);
});
// Only direct children, no subtree
childListObserver.observe(this.originalChapterBar, {
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 - this.chapterMargin / 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 lastWidth = 0;
let left = 0;
let leftPosition = 0;
let scale = null;
let scalePosition = 0;
let scaleWidth = 0;
let lastScalePosition = 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]);
const endPosition = transformScale + checkLeft / currentSectionWidthNoMargin;
if (lastScalePosition > 0.99999 && endPosition === 0) {
// Last one was an end section that was fully filled
scalePosition = currentWidth - lastWidth;
break;
}
lastScalePosition = endPosition;
scale = transformScale;
scaleWidth = currentSectionWidthNoMargin;
if ((i === sections.length - 1 || endPosition < 0.99999) && endPosition > 0) {
// reached the end of this section for sure
// 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;
}
}
lastWidth = currentSectionWidth;
currentWidth += lastWidth;
}
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) {
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)) {
const oldVoteContainers = document.querySelectorAll("#chapterVote");
if (oldVoteContainers.length > 0) {
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
}
chapterButton.insertBefore(chapterVoteContainer, this.getChapterChevron());
}
this.chapterVote.setVisibility(true);
this.chapterVote.setSegment(chosenSegment);
} else {
this.chapterVote.setVisibility(false);
}
} else {
chaptersContainer.style.display = "none";
this.chapterVote.setVisibility(false);
}
}
}
remove(): void {
this.container.remove();
@@ -218,14 +771,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 +839,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;

View File

@@ -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;
}
}
export interface ImportSegmentsResponse {
importedSegments: SponsorTime[];
}
export interface TimeUpdateMessage {
message: "time";
time: number;
}
export type PopupMessage = TimeUpdateMessage;

View File

@@ -10,12 +10,17 @@ window.SB = Config;
import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
import KeybindComponent from "./components/KeybindComponent";
import UnsubmittedVideos from "./render/UnsubmittedVideos";
import KeybindComponent from "./components/options/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
import { StorageChangesObject } from "./types";
const utils = new Utils();
let embed = false;
const categoryChoosers: CategoryChooser[] = [];
const unsubmittedVideos: UnsubmittedVideos[] = [];
window.addEventListener('DOMContentLoaded', init);
async function init() {
@@ -103,7 +108,7 @@ async function init() {
// Add click listener
checkbox.addEventListener("click", async () => {
// Confirm if required
if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked))
if (confirmMessage && ((confirmOnTrue && checkbox.checked) || (!confirmOnTrue && !checkbox.checked))
&& !confirm(chrome.i18n.getMessage(confirmMessage))){
checkbox.checked = !checkbox.checked;
return;
@@ -120,7 +125,7 @@ async function init() {
if (!checkbox.checked) {
// Enable the notice
Config.config["dontShowNotice"] = false;
const showNoticeSwitch = <HTMLInputElement> document.querySelector("[data-sync='dontShowNotice'] > div > label > input");
showNoticeSwitch.checked = true;
}
@@ -162,7 +167,7 @@ async function init() {
}
case "text-change": {
const textChangeInput = <HTMLInputElement> optionsElements[i].querySelector(".option-text-box");
const textChangeSetButton = <HTMLElement> optionsElements[i].querySelector(".text-change-set");
textChangeInput.value = Config.config[option];
@@ -290,7 +295,10 @@ async function init() {
break;
}
case "react-CategoryChooserComponent":
new CategoryChooser(optionsElements[i]);
categoryChoosers.push(new CategoryChooser(optionsElements[i]));
break;
case "react-UnsubmittedVideosComponent":
unsubmittedVideos.push(new UnsubmittedVideos(optionsElements[i]));
break;
}
}
@@ -338,8 +346,8 @@ function createStickyHeader() {
/**
* Handle special cases where an option shouldn't show
*
* @param {String} element
*
* @param {String} element
*/
async function shouldHideOption(element: Element): Promise<boolean> {
return (element.getAttribute("data-private-only") === "true" && !(await isIncognitoAllowed()))
@@ -348,10 +356,8 @@ async function shouldHideOption(element: Element): Promise<boolean> {
/**
* Called when the config is updated
*
* @param {String} element
*/
function optionsConfigUpdateListener() {
function optionsConfigUpdateListener(changes: StorageChangesObject) {
const optionsContainer = document.getElementById("options");
const optionsElements = optionsContainer.querySelectorAll("*");
@@ -359,14 +365,25 @@ function optionsConfigUpdateListener() {
switch (optionsElements[i].getAttribute("data-type")) {
case "display":
updateDisplayElement(<HTMLElement> optionsElements[i])
break;
}
}
if (changes.categorySelections || changes.payments) {
for (const chooser of categoryChoosers) {
chooser.update();
}
} else if (changes.unsubmittedSegments) {
for (const chooser of unsubmittedVideos) {
chooser.update();
}
}
}
/**
* Will set display elements to the proper text
*
* @param element
*
* @param element
*/
function updateDisplayElement(element: HTMLElement) {
const displayOption = element.getAttribute("data-sync")
@@ -393,9 +410,9 @@ function updateDisplayElement(element: HTMLElement) {
/**
* Initializes the option to add Invidious instances
*
* @param element
* @param option
*
* @param element
* @param option
*/
function invidiousInstanceAddInit(element: HTMLElement, option: string) {
const textBox = <HTMLInputElement> element.querySelector(".option-text-box");
@@ -447,18 +464,12 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
/**
* Run when the invidious button is being initialized
*
* @param checkbox
* @param option
*
* @param checkbox
* @param option
*/
function invidiousInit(checkbox: HTMLInputElement, option: string) {
let permissions = ["declarativeContent"];
if (utils.isFirefox()) permissions = [];
chrome.permissions.contains({
origins: utils.getPermissionRegex(),
permissions: permissions
}, function (result) {
utils.containsInvidiousPermission().then((result) => {
if (result != checkbox.checked) {
Config.config[option] = result;
@@ -469,33 +480,19 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) {
/**
* Run whenever the invidious checkbox is clicked
*
* @param checkbox
* @param option
*
* @param checkbox
* @param option
*/
async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> {
return new Promise((resolve) => {
if (checkbox.checked) {
utils.setupExtraSitePermissions(function (granted) {
if (!granted) {
Config.config[option] = false;
checkbox.checked = false;
} else {
checkbox.checked = true;
}
resolve();
});
} else {
utils.removeExtraSiteRegistration();
}
});
const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option);
checkbox.checked = enabled;
}
/**
* Will trigger the textbox to appear to be able to change an option's text.
*
* @param element
*
* @param element
*/
function activatePrivateTextChange(element: HTMLElement) {
const button = element.querySelector(".trigger-button");
@@ -512,7 +509,7 @@ function activatePrivateTextChange(element: HTMLElement) {
element.querySelector(".option-hidden-section").classList.remove("hidden");
return;
}
let result = Config.config[option];
// See if anything extra must be done
switch (option) {
@@ -523,7 +520,7 @@ function activatePrivateTextChange(element: HTMLElement) {
}
textBox.value = result;
const setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async () => {
setTextOption(option, element, textBox.value);
@@ -552,7 +549,7 @@ function activatePrivateTextChange(element: HTMLElement) {
/**
* Function to run when a textbox change is submitted
*
*
* @param option data-sync value
* @param element main container div
* @param value new text
@@ -562,7 +559,7 @@ async function setTextOption(option: string, element: HTMLElement, value: string
const confirmMessage = element.getAttribute("data-confirm-message");
if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
// See if anything extra must be done
switch (option) {
case "*":
@@ -574,13 +571,13 @@ async function setTextOption(option: string, element: HTMLElement, value: string
if (newConfig.supportInvidious) {
const checkbox = <HTMLInputElement> document.querySelector("#support-invidious > div > label > input");
checkbox.checked = true;
await invidiousOnClick(checkbox, "supportInvidious");
}
window.location.reload();
} catch (e) {
alert(chrome.i18n.getMessage("incorrectlyFormattedOptions"));
}
@@ -623,7 +620,7 @@ function uploadConfig(e) {
/**
* Validates the value used for the database server address.
* Returns null and alerts the user if there is an issue.
*
*
* @param input Input server address
*/
function validateServerAddress(input: string): string {
@@ -657,7 +654,7 @@ function copyDebugOutputToClipboard() {
// Sanitise sensitive user config values
delete output.config.userID;
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
? "Default server address" : "Custom server address";
output.config.invidiousInstances = output.config.invidiousInstances.length;
output.config.whitelistedChannels = output.config.whitelistedChannels.length;
@@ -674,4 +671,4 @@ function copyDebugOutputToClipboard() {
function isIncognitoAllowed(): Promise<boolean> {
return new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
}
}

View File

@@ -12,25 +12,17 @@ window.addEventListener('DOMContentLoaded', init);
async function init() {
localizeHtmlPage();
const domains = document.location.hash.replace("#", "").split(",");
const acceptButton = document.getElementById("acceptPermissionButton");
acceptButton.addEventListener("click", () => {
chrome.permissions.request({
origins: utils.getPermissionRegex(domains),
permissions: []
}, (granted) => {
if (granted) {
utils.applyInvidiousPermissions(Config.config.supportInvidious).then((enabled) => {
Config.config.supportInvidious = enabled;
if (enabled) {
alert(chrome.i18n.getMessage("permissionRequestSuccess"));
Config.config.ytInfoPermissionGranted = true;
chrome.tabs.getCurrent((tab) => {
chrome.tabs.remove(tab.id);
});
window.close();
} else {
alert(chrome.i18n.getMessage("permissionRequestFailed"));
}
});
})
});
}

View File

@@ -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 } 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,13 +71,22 @@ 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 = {};
[
"sponsorBlockPopupBody",
"sponsorblockPopup",
"sponsorStart",
// Top toggles
@@ -123,16 +135,26 @@ 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);
await utils.wait(() => Config.config !== null && allowPopup, 5000, 5);
document.querySelector("body").style.removeProperty("visibility");
PageElements.sponsorBlockPopupBody.style.removeProperty("visibility");
if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
Config.configSyncListeners.push(contentConfigUpdateListener);
}
@@ -145,6 +167,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (window !== window.top) {
PageElements.sbCloseButton.classList.remove("hidden");
PageElements.sponsorBlockPopupBody.classList.add("is-embedded");
}
// Hide donate button if wanted (Safari, or user choice)
@@ -160,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) {
@@ -187,7 +214,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (window !== window.top) {
document.addEventListener("keydown", (e) => {
const target = e.target as HTMLElement;
if (target.tagName === "INPUT"
if (target.tagName === "INPUT"
|| target.tagName === "TEXTAREA"
|| e.key === "ArrowUp"
|| e.key === "ArrowDown") {
@@ -213,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) {
@@ -228,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;
@@ -255,8 +285,16 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
PageElements.sponsorTimesOthersTimeSavedDisplay.innerText = getFormattedHours(minutesSaved);
}
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");
}
}
});
@@ -292,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) {
@@ -363,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 {
@@ -439,165 +496,205 @@ 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]);
function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
let currentSegmentTab = segmentTab;
if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter && segment.source !== SponsorSourceType.YouTube)) {
PageElements.issueReporterTabs.classList.add("hidden");
currentSegmentTab = SegmentTab.Segments;
} else {
PageElements.issueReporterTabs.classList.remove("hidden");
}
//add them as buttons to the issue reporting container
const container = document.getElementById("issueReporterTimeButtons");
while (container.firstChild) {
container.removeChild(container.firstChild);
// 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.exportSegmentsButton.classList.remove("hidden");
} else {
PageElements.exportSegmentsButton.classList.add("hidden");
}
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 {
segmentSummary.classList.add("segmentPassed");
}
}
const isVip = Config.config.isVip;
for (let i = 0; i < segmentTimes.length; i++) {
const UUID = segmentTimes[i].UUID;
const locked = segmentTimes[i].locked;
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");
const segmentSummary = document.createElement("summary");
segmentSummary.className = "segmentSummary";
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 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");
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)
: "");
}
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") + ")";
}
segmentTimeFromToNode.style.margin = "5px";
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");
} 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)
: "");
}
// for inline-styling purposes
const labelContainer = document.createElement("div");
if (actionType !== ActionType.Chapter) labelContainer.appendChild(categoryColorCircle);
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);
const span = document.createElement('span');
span.className = "summaryLabel";
span.appendChild(textNode);
labelContainer.appendChild(span);
// for inline-styling purposes
segmentSummary.appendChild(labelContainer);
segmentSummary.appendChild(segmentTimeFromToNode);
segmentSummary.appendChild(labelContainer);
segmentSummary.appendChild(segmentTimeFromToNode);
const votingButtons = document.createElement("details");
votingButtons.classList.add("votingButtons");
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");
//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 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 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 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();
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 {
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
&& [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));
// 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);
votingButtons.append(segmentSummary);
votingButtons.append(voteButtonsContainer);
votingButtons.append(voteStatusContainer);
container.appendChild(votingButtons);
//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);
votingButtons.append(segmentSummary);
votingButtons.append(voteButtonsContainer);
votingButtons.append(voteStatusContainer);
container.appendChild(votingButtons);
}
}
@@ -706,6 +803,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) {
@@ -879,6 +978,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)
*/
@@ -908,6 +1038,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
@@ -932,6 +1097,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();

View File

@@ -1,15 +1,23 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import CategoryChooserComponent from "../components/CategoryChooserComponent";
import CategoryChooserComponent from "../components/options/CategoryChooserComponent";
class CategoryChooser {
ref: React.RefObject<CategoryChooserComponent>;
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<CategoryChooserComponent/>,
<CategoryChooserComponent ref={this.ref} />,
element
);
}
update(): void {
this.ref.current?.forceUpdate();
}
}
export default CategoryChooser;

View File

@@ -0,0 +1,64 @@
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,
...(!show ? { segment: null } : {})
};
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;
}
}
}
}

View File

@@ -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,9 +62,19 @@ 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()} >
{this.getMessageBox(this.idSuffix, options.textBoxes)}
<tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix}
className="sponsorTimeMessagesRow"
style={{maxHeight: this.contentContainer ? (this.contentContainer().v.offsetHeight - 200) + "px" : null}}>
<td style={{width: "100%"}}>
{this.getMessageBoxes(this.idSuffix, options.textBoxes)}
</td>
</tr>
<tr id={"sponsorSkipNoticeSpacer" + this.idSuffix}
className="sponsorBlockSpacer">
@@ -81,7 +91,7 @@ export default class GenericNotice {
);
}
getMessageBox(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
getMessageBoxes(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] {
if (textBoxes) {
const result = [];
for (let i = 0; i < textBoxes.length; i++) {

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import UnsubmittedVideosComponent from "../components/options/UnsubmittedVideosComponent";
class UnsubmittedVideos {
ref: React.RefObject<UnsubmittedVideosComponent>;
constructor(element: Element) {
this.ref = React.createRef();
ReactDOM.render(
<UnsubmittedVideosComponent ref={this.ref} />,
element
);
}
update(): void {
this.ref.current?.forceUpdate();
}
}
export default UnsubmittedVideos;

View File

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

View File

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

View File

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

View File

@@ -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,17 @@ export type Keybind = {
ctrl?: boolean,
alt?: boolean,
shift?: boolean
}
export enum PageType {
Shorts = "shorts",
Watch = "watch",
Search = "search",
Browse = "browse",
Channel = "channel"
}
export interface ButtonListener {
name: string,
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}

72
src/upsell.ts Normal file
View File

@@ -0,0 +1,72 @@
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;
const redeemInput = document.getElementById("redeemCodeInput") as HTMLInputElement;
redeemButton.addEventListener("click", async () => {
const licenseKey = redeemInput.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);
});
}

View File

@@ -2,7 +2,7 @@ import Config, { VideoDownvotes } from "./config";
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types";
import * as CompileConfig from "../config.json";
import { findValidElementFromSelector } from "./utils/pageUtils";
import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils";
import { GenericUtils } from "./utils/genericUtils";
export default class Utils {
@@ -22,52 +22,79 @@ export default class Utils {
];
/* Used for waitForElement */
waitingMutationObserver:MutationObserver = null;
waitingElements: { selector: string, callback: (element: Element) => void }[] = [];
creatingWaitingMutationObserver = false;
waitingMutationObserver: MutationObserver = null;
waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = [];
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
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);
}
/* Uses a mutation observer to wait asynchronously */
async waitForElement(selector: string): Promise<Element> {
async waitForElement(selector: string, visibleCheck = false): Promise<Element> {
return await new Promise((resolve) => {
const initialElement = this.getElement(selector, visibleCheck);
if (initialElement) {
resolve(initialElement);
return;
}
this.waitingElements.push({
selector,
visibleCheck,
callback: resolve
});
if (!this.waitingMutationObserver) {
this.waitingMutationObserver = new MutationObserver(() => {
const foundSelectors = [];
for (const { selector, callback } of this.waitingElements) {
const element = document.querySelector(selector);
if (element) {
callback(element);
foundSelectors.push(selector);
}
}
if (!this.creatingWaitingMutationObserver) {
this.creatingWaitingMutationObserver = true;
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
if (this.waitingElements.length === 0) {
this.waitingMutationObserver.disconnect();
this.waitingMutationObserver = null;
}
});
this.waitingMutationObserver.observe(document.body, {
childList: true,
subtree: true
});
if (document.body) {
this.setupWaitingMutationListener();
} else {
window.addEventListener("DOMContentLoaded", () => {
this.setupWaitingMutationListener();
});
}
}
});
}
private setupWaitingMutationListener(): void {
if (!this.waitingMutationObserver) {
this.waitingMutationObserver = new MutationObserver(() => {
const foundSelectors = [];
for (const { selector, visibleCheck, callback } of this.waitingElements) {
const element = this.getElement(selector, visibleCheck);
if (element) {
callback(element);
foundSelectors.push(selector);
}
}
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
if (this.waitingElements.length === 0) {
this.waitingMutationObserver.disconnect();
this.waitingMutationObserver = null;
this.creatingWaitingMutationObserver = false;
}
});
this.waitingMutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
private getElement(selector: string, visibleCheck: boolean) {
return visibleCheck ? findValidElement(document.querySelectorAll(selector)) : document.querySelector(selector);
}
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
return new Promise((resolve) => {
chrome.permissions.contains(permissions, resolve)
@@ -183,6 +210,37 @@ export default class Utils {
});
}
applyInvidiousPermissions(enable: boolean, option = "supportInvidious"): Promise<boolean> {
return new Promise((resolve) => {
if (enable) {
this.setupExtraSitePermissions((granted) => {
if (!granted) {
Config.config[option] = false;
}
resolve(granted);
});
} else {
this.removeExtraSiteRegistration();
resolve(false);
}
});
}
containsInvidiousPermission(): Promise<boolean> {
return new Promise((resolve) => {
let permissions = ["declarativeContent"];
if (this.isFirefox()) permissions = [];
chrome.permissions.contains({
origins: this.getPermissionRegex(),
permissions: permissions
}, function (result) {
resolve(result);
});
})
}
/**
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
*/
@@ -273,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
*
@@ -376,67 +416,6 @@ export default class Utils {
return referenceNode;
}
objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
let counter = 0;
for (const key in data) {
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
counter++;
}
return url;
}
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:";
}

6
src/utils/arrayUtils.ts Normal file
View File

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

View File

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

View File

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

87
src/utils/exporter.ts Normal file
View File

@@ -0,0 +1,87 @@
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
import { shortCategoryName } from "./categoryUtils";
import { GenericUtils } from "./genericUtils";
import * as CompileConfig from "../../config.json";
const inTest = typeof chrome === "undefined";
const chapterNames = CompileConfig.categoryList.filter((code) => code !== "chapter")
.map((code) => ({
code,
name: !inTest ? chrome.i18n.getMessage("category_" + code) : code
}));
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+)?)|(?:\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 determinedCategory = chapterNames.find(c => c.name === title)?.code as Category;
const segment: SponsorTime = {
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
category: determinedCategory ?? ("chapter" as Category),
actionType: determinedCategory ? ActionType.Skip : 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;
}
export function exportTimesAsHashParam(segments: SponsorTime[]): string {
const hashparamSegments = segments.map(segment => ({
actionType: segment.actionType,
category: segment.category,
segment: segment.segment,
...(segment.description ? {description: segment.description} : {}) // don't include the description param if empty
}));
return `#segments=${JSON.stringify(hashparamSegments)}`;
}

View File

@@ -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
*
@@ -28,7 +72,7 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
* @returns {string} errorMessage
*/
function getErrorMessage(statusCode: number, responseText: string): string {
const postFix = ((responseText && !responseText.includes(`cf-wrapper`)) ? "\n\n" + responseText : "");
const postFix = ((responseText && !(responseText.includes(`cf-wrapper`) || responseText.includes("<!DOCTYPE html>"))) ? "\n\n" + responseText : "");
// display response body for 4xx
if([400, 429, 409, 0].includes(statusCode)) {
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode + postFix;
@@ -72,9 +116,44 @@ function indexesOf<T>(array: T[], value: T): number[] {
return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1);
}
function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
let counter = 0;
for (const key in data) {
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
counter++;
}
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,
indexesOf
generateUserID,
indexesOf,
objectToURI
}

72
src/utils/licenseKey.ts Normal file
View File

@@ -0,0 +1,72 @@
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
}
/**
* The other one also tried refreshing, so returns a promise
*/
export function noRefreshFetchingChaptersAllowed(): boolean {
return Config.config.payments.chaptersAllowed || CompileConfig["freeChapterAccess"];
}
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;
}

View File

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

66
src/utils/warnings.ts Normal file
View File

@@ -0,0 +1,66 @@
import Config from "../config";
import GenericNotice, { NoticeOptions } from "../render/GenericNotice";
import { ContentContainer } from "../types";
import Utils from "../utils";
import { GenericUtils } from "./genericUtils";
const utils = new Utils();
export interface ChatConfig {
displayName: string,
composerInitialValue?: string,
customDescription?: string
}
export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {
const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config.userID,
values: ["warningReason"]
});
if (userInfo.ok) {
const warningReason = JSON.parse(userInfo.responseText)?.warningReason;
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
const publicUserID = await utils.getHash(Config.config.userID);
let notice: GenericNotice = null;
const options: NoticeOptions = {
title: chrome.i18n.getMessage("warningTitle"),
textBoxes: [{
text: chrome.i18n.getMessage("warningChatInfo"),
icon: null
}, ...warningReason.split("\n").map((reason) => ({
text: reason,
icon: null
}))],
buttons: [{
name: chrome.i18n.getMessage("questionButton"),
listener: () => openChat({
displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`
})
},
{
name: chrome.i18n.getMessage("warningConfirmButton"),
listener: async () => {
const result = await utils.asyncRequestToServer("POST", "/api/warnUser", {
userID: Config.config.userID,
enabled: false
});
if (result.ok) {
notice?.close();
} else {
alert(`${chrome.i18n.getMessage("warningError")} ${result.status}`);
}
}
}],
timed: false
};
notice = new GenericNotice(contentContainer, "warningNotice", options);
}
}
export function openChat(config: ChatConfig): void {
window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false));
}

257
test/exporter.test.ts Normal file
View File

@@ -0,0 +1,257 @@
/**
* @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
}]);
});
it ("22. 2:04:22 some name", () => {
const input = ` 22. 2:04:22 some name
23. 2:04:22.23 some other name`;
const result = importTimes(input, 8000);
expect(result).toMatchObject([{
segment: [7462, 7462.23],
description: "some name",
category: "chapter" as Category
}, {
segment: [7462.23, 8000],
description: "some other name",
category: "chapter" as Category
}]);
});
});

749
test/previewBar.test.ts Normal file
View File

@@ -0,0 +1,749 @@
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",
actionType: "skip",
unsubmitted: false,
showLarger: false,
description: ""
}, {
segment: [50, 80],
category: "sponsor",
actionType: "skip",
unsubmitted: false,
showLarger: false,
description: ""
}] as PreviewBarSegment[]);
expect(groups).toStrictEqual([{
segment: [0, 2],
originalDuration: 0,
actionType: null
}, {
segment: [2, 30],
originalDuration: 30 - 2,
actionType: "skip"
}, {
segment: [30, 50],
originalDuration: 0,
actionType: null
}, {
segment: [50, 80],
originalDuration: 80 - 50,
actionType: "skip"
}, {
segment: [80, 315],
originalDuration: 0,
actionType: null
}]);
});
it("Small time in bigger time", () => {
previewBar.videoDuration = 315;
const groups = previewBar.createChapterRenderGroups([{
segment: [2.52, 30],
category: "sponsor",
actionType: "skip",
unsubmitted: false,
showLarger: false,
description: ""
}, {
segment: [20, 25],
category: "sponsor",
actionType: "skip",
unsubmitted: false,
showLarger: false,
description: ""
}] as PreviewBarSegment[]);
expect(groups).toStrictEqual([{
segment: [0, 2.52],
originalDuration: 0,
actionType: null
}, {
segment: [2.52, 20],
originalDuration: 30 - 2.52,
actionType: "skip"
}, {
segment: [20, 25],
originalDuration: 25 - 20,
actionType: "skip"
}, {
segment: [25, 30],
originalDuration: 30 - 2.52,
actionType: "skip"
}, {
segment: [30, 315],
originalDuration: 0,
actionType: null
}]);
});
it("Same start time", () => {
previewBar.videoDuration = 315;
const groups = previewBar.createChapterRenderGroups([{
segment: [2.52, 30],
category: "sponsor",
actionType: "skip",
unsubmitted: false,
showLarger: false,
description: ""
}, {
segment: [2.52, 40],
category: "sponsor",
actionType: "skip",
unsubmitted: false,
showLarger: false,
description: ""
}] as PreviewBarSegment[]);
expect(groups).toStrictEqual([{
segment: [0, 2.52],
originalDuration: 0,
actionType: null
}, {
segment: [2.52, 30],
originalDuration: 30 - 2.52,
actionType: "skip"
}, {
segment: [30, 40],
originalDuration: 40 - 2.52,
actionType: "skip"
}, {
segment: [40, 315],
originalDuration: 0,
actionType: null
}]);
});
it("Lots of overlapping segments", () => {
previewBar.videoDuration = 315.061;
const groups = previewBar.createChapterRenderGroups([
{
"category": "chapter",
"actionType": "chapter",
"segment": [
0,
49.977
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
"description": "Start of video"
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
2.926,
5
],
"locked": 1,
"votes": 2,
"videoDuration": 316,
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
"description": ""
},
{
"category": "chapter",
"actionType": "chapter",
"segment": [
14.487,
37.133
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "b1919787a85cd422af07136a913830eda1364d32e8a9e12104cf5e3bad8f6f45",
"description": "Subset of start"
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
23.450537,
34.486084
],
"locked": 0,
"votes": -1,
"videoDuration": 315.061,
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
"description": ""
},
{
"category": "interaction",
"actionType": "skip",
"segment": [
50.015343,
56.775314
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "b2a85e8cdfbf21dd504babbcaca7f751b55a5a2df8179c1a83a121d0f5d56c0e",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
62.51888,
74.33331
],
"locked": 0,
"votes": -1,
"videoDuration": 316,
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
88.71328,
96.05933
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "6c08c092db2b7a31210717cc1f2652e7e97d032e03c82b029a27c81cead1f90c",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
101.50703,
115.088326
],
"votes": 0,
"videoDuration": 315.061,
"userID": "2db207ad4b7a535a548fab293f4567bf97373997e67aadb47df8f91b673f6e53",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
122.211845,
137.42178
],
"locked": 0,
"votes": 1,
"videoDuration": 0,
"userID": "0312cbfa514d9d2dfb737816dc45f52aba7c23f0a3f0911273a6993b2cb57fcc",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
144.08913,
160.14084
],
"locked": 0,
"votes": -1,
"videoDuration": 316,
"userID": "938444fccfdb7272fd871b7f98c27ea9e5e806681db421bb69452e6a42730c20",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
164.22084,
170.98082
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "845c4377060d5801f5324f8e1be1e8373bfd9addcf6c68fc5a3c038111b506a3",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
180.56674,
189.16516
],
"locked": 0,
"votes": -1,
"videoDuration": 315.061,
"userID": "7c6b015687db7800c05756a0fd226fd8d101f5a1e1bfb1e5d97c440331fd6cb7",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
204.10468,
211.87865
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "3472e8ee00b5da957377ae32d59ddd3095c2b634c2c0c970dfabfb81d143699f",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
214.92064,
222.0186
],
"locked": 0,
"votes": 0,
"videoDuration": 0,
"userID": "62a00dffb344d27de7adf8ea32801c2fc0580087dc8d282837923e4bda6a1745",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
233.0754,
244.56734
],
"locked": 0,
"votes": -1,
"videoDuration": 315,
"userID": "dcf7fb0a6c071d5a93273ebcbecaee566e0ff98181057a36ed855e9b92bf25ea",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
260.64053,
269.35938
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"segment": [
288.686,
301.96
],
"locked": 0,
"votes": 0,
"videoDuration": 315.061,
"userID": "e0238059ae4e711567af5b08a3afecfe45601c995b0ea2f37ca43f15fca4e298",
"description": ""
},
{
"category": "sponsor",
"actionType": "skip",
"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,
"actionType": "chapter"
},
{
"segment": [
2.926,
5
],
"originalDuration": 2.074,
"actionType": "skip"
},
{
"segment": [
5,
14.487
],
"originalDuration": 49.977,
"actionType": "chapter"
},
{
"segment": [
14.487,
23.450537
],
"originalDuration": 22.646,
"actionType": "chapter"
},
{
"segment": [
23.450537,
34.486084
],
"originalDuration": 11.035546999999998,
"actionType": "skip"
},
{
"segment": [
34.486084,
37.133
],
"originalDuration": 22.646,
"actionType": "chapter"
},
{
"segment": [
37.133,
49.977
],
"originalDuration": 49.977,
"actionType": "chapter"
},
{
"segment": [
49.977,
50.015343
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
50.015343,
56.775314
],
"originalDuration": 6.759971,
"actionType": "skip"
},
{
"segment": [
56.775314,
62.51888
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
62.51888,
74.33331
],
"originalDuration": 11.814429999999994,
"actionType": "skip"
},
{
"segment": [
74.33331,
88.71328
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
88.71328,
96.05933
],
"originalDuration": 7.346050000000005,
"actionType": "skip"
},
{
"segment": [
96.05933,
101.50703
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
101.50703,
115.088326
],
"originalDuration": 13.581295999999995,
"actionType": "skip"
},
{
"segment": [
115.088326,
122.211845
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
122.211845,
137.42178
],
"originalDuration": 15.209935000000016,
"actionType": "skip"
},
{
"segment": [
137.42178,
144.08913
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
144.08913,
160.14084
],
"originalDuration": 16.051709999999986,
"actionType": "skip"
},
{
"segment": [
160.14084,
164.22084
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
164.22084,
170.98082
],
"originalDuration": 6.759979999999985,
"actionType": "skip"
},
{
"segment": [
170.98082,
180.56674
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
180.56674,
189.16516
],
"originalDuration": 8.598419999999976,
"actionType": "skip"
},
{
"segment": [
189.16516,
204.10468
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
204.10468,
211.87865
],
"originalDuration": 7.773969999999991,
"actionType": "skip"
},
{
"segment": [
211.87865,
214.92064
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
214.92064,
222.0186
],
"originalDuration": 7.0979600000000005,
"actionType": "skip"
},
{
"segment": [
222.0186,
233.0754
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
233.0754,
244.56734
],
"originalDuration": 11.49194,
"actionType": "skip"
},
{
"segment": [
244.56734,
260.64053
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
260.64053,
269.35938
],
"originalDuration": 8.718849999999975,
"actionType": "skip"
},
{
"segment": [
269.35938,
288.686
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
288.686,
295
],
"originalDuration": 6.314000000000021,
"actionType": "skip"
},
{
"segment": [
295,
301.96
],
"originalDuration": 13.274000000000001,
"actionType": "skip"
},
{
"segment": [
301.96,
315.061
],
"originalDuration": 0,
"actionType": null
}
]);
})
it("Multiple overlapping", () => {
previewBar.videoDuration = 3615.161;
const groups = previewBar.createChapterRenderGroups([{
"segment": [
160,
2797.323
],
"category": "chooseACategory",
"actionType": "skip",
"unsubmitted": true,
"showLarger": false,
},{
"segment": [
169,
3432.255
],
"category": "chooseACategory",
"actionType": "skip",
"unsubmitted": true,
"showLarger": false,
},{
"segment": [
169,
3412.413
],
"category": "chooseACategory",
"actionType": "skip",
"unsubmitted": true,
"showLarger": false,
},{
"segment": [
1594.92,
1674.286
],
"category": "sponsor",
"actionType": "skip",
"unsubmitted": false,
"showLarger": false,
}
] as unknown as PreviewBarSegment[]);
expect(groups).toStrictEqual([
{
"segment": [
0,
160
],
"originalDuration": 0,
"actionType": null
},
{
"segment": [
160,
169
],
"originalDuration": 2637.323,
"actionType": "skip"
},
{
"segment": [
169,
1594.92
],
"originalDuration": 3243.413,
"actionType": "skip"
},
{
"segment": [
1594.92,
1674.286
],
"originalDuration": 79.36599999999999,
"actionType": "skip"
},
{
"segment": [
1674.286,
3412.413
],
"originalDuration": 3243.413,
"actionType": "skip"
},
{
"segment": [
3412.413,
3432.255
],
"originalDuration": 3263.255,
"actionType": "skip"
},
{
"segment": [
3432.255,
3615.161
],
"originalDuration": 0,
"actionType": null
}
]);
});
})

View File

@@ -202,6 +202,8 @@ async function muteSkipSegment(driver: WebDriver, startTime: number, endTime: nu
async function toggleWhitelist(driver: WebDriver): Promise<void> {
const popupButton = await driver.findElement(By.id("infoButton"));
const rightControls = await driver.findElement(By.css(".ytp-right-controls"));
await driver.actions().move({ origin: rightControls }).perform();
if ((await popupButton.getCssValue("display")) !== "none") {
await driver.actions().move({ origin: popupButton }).perform();
await popupButton.click();

View File

@@ -29,9 +29,11 @@ module.exports = env => ({
popup: path.join(__dirname, srcDir + 'popup.ts'),
background: path.join(__dirname, srcDir + 'background.ts'),
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')
options: path.join(__dirname, srcDir + 'options.ts'),
help: path.join(__dirname, srcDir + 'help.ts'),
permissions: path.join(__dirname, srcDir + 'permissions.ts'),
document: path.join(__dirname, srcDir + 'document.ts'),
upsell: path.join(__dirname, srcDir + 'upsell.ts')
},
output: {
path: path.join(__dirname, '../dist/js'),

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const merge = require('webpack-merge');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = env => merge(common(env), {