Compare commits

...

237 commits

Author SHA1 Message Date
github-actions[bot]
60735f9507
docs(README): update contributors 2023-10-12 20:22:54 +00:00
kodxana
8c26ae0872
Merge pull request #139 from DeepDoge/master
Little clean up and fixes
2022-10-06 19:01:02 +02:00
Shiba
6ba1a373a9
🍣 max-width on popup removed
was causing some issues with exotic browsers
2022-10-01 12:18:34 +03:00
Shiba
c1b53d90a1
Merge branch 'LBRYFoundation:master' into master 2022-09-28 14:39:34 +03:00
Shiba
529931c43b 🍙 removed console.log s 2022-09-28 14:38:37 +03:00
kodxana
351ca5c6c3
Up Version 2022-09-02 13:19:07 +02:00
Shiba
ca65a0288d 🍣 some little fixes in the code
- nothing changed, just some little fixes
2022-08-10 10:18:04 +00:00
kodxana
309806605f
Merge pull request #134 from DeepDoge/master
Major changes
2022-08-10 11:50:01 +02:00
Shiba
7fa57b9e88 🍙 updated YTtoLBRY video 2022-08-10 09:38:57 +00:00
Shiba
21f6ffc85b 🍤 popup width made larger 2022-08-10 08:17:55 +00:00
Shiba
1ee722b6ff 🍣 improved logic of redrect and opening a new tab 2022-08-10 08:12:04 +00:00
Shiba
28ef256c40 🍙 on redirect pause the video directly only 2022-08-10 07:56:24 +00:00
Shiba
a41b51e454 🍣 handle unexpected location.replace behavior 2022-08-09 22:44:40 +00:00
Shiba
5c9910dff1 Merge branch 'master' of https://github.com/DeepDoge/Watch-on-LBRY 2022-08-09 22:22:34 +00:00
Shiba
59a5e0dee2 🍙 on redirect use replace if tab has no history 2022-08-09 22:22:25 +00:00
Shiba
e4fdc4e831
Merge branch 'LBRYFoundation:master' into master 2022-08-10 00:17:05 +03:00
Shiba
9c26d553f5 🍣 major changes
- modes are removed
- added more options for redirect
- extension badge removed
2022-08-09 21:16:05 +00:00
kodxana
f9c70dd90d
Merge pull request #133 from DeepDoge/master
Bug fixes again
2022-08-09 23:15:47 +02:00
Shiba
9ed962df96 Merge branch 'master' of https://github.com/DeepDoge/Watch-on-LBRY 2022-08-09 19:10:43 +00:00
Shiba
a95b46bf0c 🍙 made url resovler timeout longer 2022-08-09 19:10:37 +00:00
Shiba
913282290e
Merge branch 'LBRYFoundation:master' into master 2022-08-09 22:00:41 +03:00
Shiba
6af354de0c 🍙 more bug fixes 2022-08-09 17:49:16 +00:00
Shiba
15f03b3bcc 🍙 removed debug logs 2022-08-09 17:06:41 +00:00
kodxana
64570b83bf
Merge pull request #132 from DeepDoge/master
Opening new tabs handled by the background now
2022-08-09 18:58:29 +02:00
Shiba
4295dbe1fe 🍣 bug fixes 2022-08-09 16:49:45 +00:00
Shiba
619d66d6aa Merge branch 'master' of https://github.com/DeepDoge/Watch-on-LBRY 2022-08-09 16:42:58 +00:00
Shiba
b26d540a4a 🍣 bug fixes 2022-08-09 16:42:51 +00:00
Shiba
32290e5f63
Merge branch 'LBRYFoundation:master' into master 2022-08-09 13:32:27 +03:00
Shiba
c089de3f0f Merge branch 'master' of https://github.com/DeepDoge/Watch-on-LBRY 2022-08-09 10:31:55 +00:00
Shiba
fcdc5cb77a 🍙 opening new tabs handled by the background now
- because firefox seem to block content script from openning new tabs in some cases
2022-08-09 10:31:48 +00:00
kodxana
e0d514916e
Merge pull request #131 from DeepDoge/master
Added option for where to show the buttons
2022-08-08 00:00:17 +02:00
Shiba
376c7b185a
Merge branch 'LBRYFoundation:master' into master 2022-08-07 13:30:46 +03:00
Shiba
73147fdbcd Merge branch 'master' of https://github.com/DeepDoge/Watch-on-LBRY 2022-08-07 10:30:14 +00:00
Shiba
9edd318a35 🍙 added option for where to show the buttons 2022-08-07 10:30:06 +00:00
kodxana
f5c0fc450b
Merge pull request #130 from DeepDoge/master
Removed `tabs` permission
2022-08-06 22:24:43 +02:00
Shiba
259582cd3e
Merge branch 'LBRYFoundation:master' into master 2022-08-06 21:05:33 +03:00
Shiba
38f2d42404 removed tabs permission 2022-08-06 18:04:45 +00:00
kodxana
7b585b27a9
Merge pull request #129 from DeepDoge/master
Added timeout to resolver API request
2022-07-28 14:08:01 +02:00
Shiba
6721966e5b 🍙 added timeout to resolver api request 2022-07-27 06:43:58 +00:00
kodxana
f087d0d1e5
Merge pull request #128 from DeepDoge/master
Button changes
2022-07-26 10:59:34 +02:00
Shiba
de43b990f2 🍣 button changes 2022-07-25 23:25:33 +00:00
Shiba
8f88dbebe7 🍙 little bug fix 2022-07-25 21:44:14 +00:00
kodxana
0aa64bb8b2
Merge pull request #127 from DeepDoge/master
Watch on LBRY button also added to the Video player + Some bug fixes
2022-07-25 22:47:40 +02:00
Shiba
f4dd0306fc
Merge branch 'LBRYFoundation:master' into master 2022-07-25 10:41:07 +03:00
Shiba
29c9b2829a 🍣 New button on the video player 2022-07-24 23:18:34 +00:00
Shiba
a6472799a1 🍙 Bug fixes, and other fixes 2022-07-24 20:41:51 +00:00
kodxana
1f29fc7c29
Merge pull request #125 from DeepDoge/master
Made yt video pause, since we use new tab now
2022-07-08 23:24:19 +02:00
Shiba
ca4c486ea7 Merge branch 'master' of https://github.com/DeepDoge/Watch-on-LBRY 2022-07-07 18:00:38 +00:00
Shiba
4102177212 🍣 Some more bug fixes 2022-07-07 18:00:32 +00:00
Shiba
da8dee83f9
Merge branch 'LBRYFoundation:master' into master 2022-07-07 20:43:33 +03:00
Shiba
ce11d4fdf3 🍣 little bug fix 2022-07-07 17:37:57 +00:00
Shiba
1eb21eb51f 🍙 made yt video pause, since we use new tab now 2022-07-06 13:10:32 +00:00
kodxana
a0577b4530
Merge pull request #123 from DeepDoge/master
New stuff and some changes
2022-07-02 17:33:17 +02:00
Shiba
c895d53253 🍙 new stuff and some changes
- channel buttons and redirect
- in button mode if there is no target try to find lbry url in the description
- used a loop in content script instead of events and stuff to make it less confusing
2022-07-02 15:15:36 +00:00
kodxana
66cb8ccccf
Merge pull request #122 from DeepDoge/master
Some changes
2022-07-01 23:13:02 +02:00
Shiba
f072c95051 🍤 Build for manifest v2 and v3 at once
- and zip them
2022-07-01 20:39:13 +00:00
Shiba
0ba4efd43e 🍙 New button style with gradient 2022-07-01 20:37:03 +00:00
Shiba
066c400cce 🍣 Bug fixes and small changes 2022-07-01 20:34:53 +00:00
kodxana
63a251c0ae
Merge pull request #120 from DeepDoge/master
💩 Upgraded to manifest v3
2022-05-02 17:11:36 +02:00
Shiba
be310e6cb1 🧀 added manifest v2 support for firefox 2022-05-02 13:17:58 +00:00
Shiba
cebdf480fe 💩 upgraded to manifest v3 2022-05-02 12:12:11 +00:00
kodxana
7076a657fc
Update main.tsx 2022-05-02 11:49:00 +02:00
kodxana
dfa1fd03a8
Merge pull request #118 from DeepDoge/master
Bugfixes
2022-05-02 10:01:13 +02:00
Shiba
cddd415e22 🍣 more bugfixes 2022-05-02 01:09:11 +00:00
Shiba
6aa507c53d 🍙 custom dialogs added 2022-05-01 21:20:59 +00:00
Shiba
0bcf69e99b 🍜 bugfixes 2022-05-01 16:55:13 +00:00
kodxana
ccf6d2d990
Update README.md 2022-05-01 18:10:21 +02:00
kodxana
c4231c29d6
Update README.md 2022-05-01 18:09:02 +02:00
kodxana
cf85b39d6a
Merge pull request #116 from DeepDoge/master
Organized Files, Remade YTtoLBRY page, And some refactoring
2022-05-01 17:51:53 +02:00
Shiba
5d9d0416a3 🔥 Organized Files
- And some refactoring
2022-05-01 15:49:55 +00:00
Shiba
e2e7426b5b 🍘 YTtoLBRY tool has been remade 2022-05-01 14:38:54 +00:00
kodxana
6b8f193bfd
Merge pull request #115 from DeepDoge/master
Bug Fixes
2022-05-01 16:38:50 +02:00
Shiba
c2f894ec61
🍙 bugfix 2022-05-01 12:25:54 +03:00
Shiba
ce40824a0c 🍘 bugfix, filereader but on firefox 2022-04-30 23:33:19 +00:00
Shiba
f86affc093 🍣 crypto changes 2022-04-30 23:25:30 +00:00
Shiba
deed10423a 🍙 added back button to import/generate profile view 2022-04-30 18:35:00 +00:00
Shiba
caa0d196ed 🍣 bug fixes 2022-04-30 17:48:11 +00:00
kodxana
a27d5a285b
Merge pull request #114 from DeepDoge/master
Refactoring and Fixes
2022-04-30 18:17:58 +02:00
Shiba
e8d7a349f4 🍙 refactoring and fixes 2022-04-30 16:14:52 +00:00
kodxana
fe5c38bfed
Update manifest.json 2022-04-30 15:24:41 +02:00
kodxana
53eb4a0c8e
Merge pull request #113 from DeepDoge/master
New UI and Madiator Finder Features
2022-04-30 15:23:51 +02:00
Shiba
f1f4c335e9 🔥 New UI and Madiator Finder Features 2022-04-30 12:49:14 +00:00
Shiba
3ee7e530d6 🔥 New UI and Madiator Finder Features 2022-04-30 12:23:03 +00:00
Shiba
81f1742289 🍙 updating UI 2022-04-22 20:42:57 +00:00
Shiba
a0f66bc062 🍣 implementing finder login 2022-04-22 19:08:03 +00:00
Shiba
450ca8cef6 🔥 bug fixes 2022-04-15 13:37:37 +00:00
Shiba
651ca5b4dc 🔥 debug code removed 2022-04-15 10:30:49 +00:00
Shiba
2a3771e45a 🍙 signature fixes 2022-04-15 09:50:25 +00:00
Shiba
365173d316 🍙 auth added 2022-04-15 09:37:39 +00:00
Shiba
3e60ed295f 🍣 removed scrap and simplified the urlResolver 2022-04-14 20:58:47 +00:00
kodxana
8c5e2d68e0
Merge pull request #107 from DeepDoge/patch-2
Changed QUERY_CHUNK_SIZE
2022-03-14 16:24:04 +01:00
Shiba
222a71948b
🍙 changed QUERY_CHUNK_SIZE
- odysee api started to give 520 response with the previous size
2022-03-14 18:10:53 +03:00
kodxana
bd33aa833f
API CORS fix + Up Version 2022-03-04 19:43:05 +01:00
kodxana
3edc78c36a
Up version 2022-02-02 23:01:03 +01:00
kodxana
27207e0f9c
Merge pull request #101 from DeepDoge/master
Button wasn't being updated when we change target platform
2022-01-27 21:10:14 +01:00
Shiba
3692ccba89 ... 2022-01-27 11:24:35 +00:00
Shiba
bb0dc3c4f4 🍘 bug fixes
- lbry-url parser disabled because it was returning empty array and it just works without it
- content script wasnt starting to work
- background error on content script was being handled wrong
- background lbryPathnameFromVideoId wasnt ending well on error
2022-01-27 11:17:00 +00:00
Shiba
b17f2f4e2e 🍙 updater wasn't working on start 2022-01-26 18:22:43 +00:00
kodxana
4a23670e4d
Merge pull request #100 from DeepDoge/master
Browser's uri dialog was causing confusion sometimes
2022-01-25 23:10:31 +01:00
Shiba
92fbdd727d 🍘 updater wasn't working on settings change 2022-01-25 16:42:19 +00:00
Shiba
6ef5459d7a 🍙 browser's uri dialog was causing confusion sometimes 2022-01-25 16:24:19 +00:00
kodxana
e501a8b828
Merge pull request #99 from DeepDoge/master
On error background returns undefined but it should throw
2022-01-25 17:07:39 +01:00
Shiba
324d3800de 🍱 On error background returns undefined but it should throw 2022-01-24 08:34:41 +00:00
kodxana
e01a3990c8
Merge pull request #93 from DeepDoge/master
Madiator scrap API version changed to 2
2022-01-15 17:27:18 +01:00
Shiba
79ba91a1f1 🥡 set madiator scrap api version to 2 2022-01-14 18:36:18 +00:00
kodxana
7f3f8919f0
Merge pull request #91 from DeepDoge/master
Added .devcontainer
2022-01-11 21:53:36 +01:00
kodxana
c94ec17fa3
Merge pull request #92 from DeepDoge/patch-1
Changed cache time
2022-01-11 21:53:24 +01:00
Shiba
585a232021 🍘 changed cache time for lbrypathnames 2022-01-11 22:29:28 +03:00
Shiba
2b38c809ca 🥡 added .devcontainer
- to make building the project easier in vscode
- and consistent file formatting in vscode
2022-01-11 13:45:24 +00:00
kodxana
093deaa8fd
Merge pull request #90 from DeepDoge/1.7.6
Added support for new invidious instances
2022-01-10 18:13:36 +01:00
Shiba
fde3d7b897 🍣 added support for new invidious nodes
- vid.puffyan.us
- invidio.xamh.de
- invidious.kavin.rocks
2022-01-10 17:05:02 +00:00
kodxana
bccbce8db1
Merge pull request #89 from DeepDoge/1.7.6
Clear cache button added and caching improved
2022-01-10 17:55:16 +01:00
github-actions[bot]
766b6cf990
docs(README): update contributors 2022-01-10 16:37:14 +00:00
kodxana
889c64c0a6
Merge pull request #88 from DeepDoge/1.7.6
Refactor and bug fixes
2022-01-10 17:36:37 +01:00
Shiba
89ce1aeb31 🍙 clear cache button added 2022-01-10 16:34:01 +00:00
Shiba
3a07d7f82b 🍙 not checking for cache expiry only at start now 2022-01-10 16:33:41 +00:00
Shiba
4653f77be4 🍙 Redirect bug fix
- location.replace was running for the app too
2022-01-10 15:33:07 +00:00
Shiba
94cda17b99 🍙 Renamed LBRY Inc. to Odysee 2022-01-10 15:29:50 +00:00
Shiba
2c75082af9 🍣 Formatted Files, Organized Imports 2022-01-10 12:36:29 +00:00
Shiba
8f75c67601 🥡 Refactor 2022-01-10 12:32:32 +00:00
Shiba
5bcd33890d 🍘 made video pause on all redirects not only app 2022-01-10 00:39:28 +00:00
Shiba
d32958852a 🍙 bugfix redirect to lbryapp 2022-01-10 00:22:20 +00:00
Shiba
5396936586 🍣 typo fixes 2022-01-10 00:09:15 +00:00
Shiba
06524954e7 🥞 redirect bugfix 2022-01-10 00:08:10 +00:00
Shiba
86879183b2 🍱 Refactor bug fix 2022-01-09 23:17:20 +00:00
kodxana
30b65454ff
Merge pull request #87 from DeepDoge/1.7.6 2022-01-10 00:15:18 +01:00
Shiba
30f077ba38 🍙 Refactor and bugfix 2022-01-09 23:01:04 +00:00
Shiba
610b47d1e4 🍘 Refactor bugfix 2022-01-09 22:03:31 +00:00
Shiba
2b91436900 🥡 Refactor 2022-01-09 21:11:58 +00:00
Shiba
4f8e807a65 🍱 Refactor 2022-01-09 21:04:03 +00:00
Shiba
cb4b4f4b2e 🥞 Refactor 2022-01-09 19:10:27 +00:00
Shiba
75cb9cf01d 🍘 Refactor 2022-01-09 19:01:01 +00:00
Shiba
7727d04157 🥡 Refactor 2022-01-09 18:59:41 +00:00
Shiba
719ff06caf 🍣 Refactor 2022-01-09 18:54:28 +00:00
Shiba
4cdcc4c9a4 🍱 Refactor, removed not used backend code. bugfixes 2022-01-09 18:43:46 +00:00
Shiba
205a8fd151 🍙 Refactor removed pick from getExtensionSettings 2022-01-09 18:42:35 +00:00
Shiba
1e7293826a 🍙 Refactor, moved many backend stuff to content 2022-01-09 18:41:30 +00:00
Shiba
b750c86b88 🍘 ctxFromURL runs once for the same url at a time 2022-01-09 13:12:05 +00:00
Shiba
475c38ba0c 🍙 URL resolver caching improvements
- Updated types for url because it can also be null
- Cached 404 as null
2022-01-09 12:46:18 +00:00
Shiba
ff83dfc62d 🍱 Removed logs 2022-01-09 12:20:24 +00:00
Shiba
685f9615ca 🍱 URL resolver api caching handled by the client
- Told browser to not to cache the api request
- Removed manual memory cache
- Implimented caching using indexedDB
- Cache time is 1 day
2022-01-09 11:54:53 +00:00
Shiba
73508ff532 🍣 added back memory cache temporarily
- added back manual memory cache temporarily
- and this time its working as expected
- also disabled use of browser cache for now
- thought we are still saying browser to cache it for the future version
2022-01-08 23:32:49 +00:00
kodxana
70ad28ba22
Up version 2022-01-07 23:29:40 +01:00
kodxana
a0b6182a96
Merge pull request #86 from LBRYFoundation/1.7.6
1.7.6
2022-01-07 23:09:09 +01:00
kodxana
6597acd0a5
Merge pull request #85 from DeepDoge/1.7.6
Removed memory cache for ctxFromURL
2022-01-07 23:08:30 +01:00
Shiba
c2aacaf307 🍘 removed memory cache for ctxFromURL
- we already tell browser to cache the request
- and we dont need the response in the same call stack
2022-01-07 22:01:30 +00:00
kodxana
c24fd17013
Merge pull request #84 from LBRYFoundation/1.7.6
1.7.6
2022-01-07 22:01:18 +01:00
kodxana
a7b66660ea
Merge pull request #83 from DeepDoge/1.7.6
[Feature Update] Timestamp feature made more smart
2022-01-07 21:58:18 +01:00
Shiba
7c50daf7fc 🥞 timestamp feature made more smart
- if time is smaller than 3 dont add timestamp
- if the video ended dont add timestamp
2022-01-07 20:56:56 +00:00
kodxana
a671df51a0
Merge pull request #82 from LBRYFoundation/1.7.6
1.7.6
2022-01-07 21:55:12 +01:00
kodxana
668018d89d
Merge pull request #81 from DeepDoge/1.7.6
[BUG FIX]  ctxFromUrl pathname check fixed
2022-01-07 21:54:43 +01:00
Shiba
2b3d43e0dc 🥡bug fix
- ctxFromUrl pathname check fixed
2022-01-07 20:40:36 +00:00
kodxana
19967c5ecc
Merge pull request #80 from LBRYFoundation/1.7.6
1.7.6
2022-01-07 21:14:15 +01:00
github-actions[bot]
2352237bf5
docs(README): update contributors 2022-01-07 20:13:21 +00:00
kodxana
00f2bb82f8
Merge pull request #79 from DeepDoge/1.7.6
URL resolver changes
2022-01-07 21:13:04 +01:00
kodxana
77d95ab61c
Merge pull request #78 from LBRYFoundation/1.7.6
1.7.6
2022-01-07 21:12:53 +01:00
Shiba
75274005ee 🍙 url resolver changes
- made it send more than one request at once
2022-01-07 20:09:05 +00:00
kodxana
682af767c9
Merge pull request #77 from DeepDoge/patch
[Feature] Madiator.com URL resolver added
2022-01-07 20:11:49 +01:00
Shiba
9b68487ecc 🍣 madiator url resolver added
- multiple resolver feature added
- added progress bar to sub converter
- csv sub converter bug fix
2022-01-07 19:00:19 +00:00
kodxana
336e3050f0
Merge pull request #76 from DeepDoge/patch
Updated variable name
2021-12-28 18:34:19 +01:00
Shiba
6b4a377058 Updated variable name
- source/target platfrom setthing variable and the type had the same name
2021-12-27 22:36:17 +00:00
kodxana
279f7faff7
Merge pull request #75 from DeepDoge/yewtube-added
[Feature] Yewtube support added
2021-12-19 14:36:28 +01:00
Shiba
8f30825edd Updated Watch On LBRY button style 2021-12-19 11:59:36 +00:00
Shiba
4ab8fdce5e Updated Watch On LBRY button 2021-12-19 11:46:33 +00:00
Shiba
58e6c119f4 Updated youtube button mount point query 2021-12-19 11:12:44 +00:00
Shiba
ba5beca60a Changed button mount point for yewtube 2021-12-19 01:58:45 +03:00
Shiba
dd16ce1041 Variable name typo fixed 2021-12-19 01:58:14 +03:00
Shiba
63d91a2ee1 Updated styles to use em instead of px 2021-12-19 01:57:29 +03:00
Shiba
4220ea11c7 Platform name 'App changed to 'LBRY App' 2021-12-19 01:57:02 +03:00
Shiba
47d27b0671 Added yewtu.be support. And name changes
- Added settings for source platform
- Renamed platform as target platform
2021-12-19 01:56:40 +03:00
github-actions[bot]
b17a7020fd
docs(README): update contributors 2021-12-14 08:22:37 +00:00
kodxana
3bab9d15ee
Update contributors.yml 2021-12-14 09:22:08 +01:00
kodxana
adab9addf5
Update README.md 2021-12-14 09:21:23 +01:00
kodxana
f3cf3526f5
Create contributors.yml 2021-12-14 09:18:46 +01:00
kodxana
3b4bc7910f
Create CHANGELOG.md 2021-12-12 10:12:50 +01:00
kodxana
bff1a849fc
Up version to 1.7.5 2021-12-12 09:58:07 +01:00
kodxana
27b6c5f422
Merge pull request #71 from DeepDoge/patch-1
Madiator icon added
2021-12-12 09:16:02 +01:00
Shiba
ac911e6816 Madiator icon added 2021-12-11 22:39:04 +00:00
kodxana
6ac58a5bf8
Merge pull request #70 from DeepDoge/patch-1
Update ytContent.tsx
2021-12-11 23:00:58 +01:00
Shiba
dfdfe6778a Added CSV support for subscribtion converter 2021-12-11 21:38:47 +00:00
Shiba
6cc149a5c6 parsing yt time from url fixed 2021-12-11 20:05:30 +00:00
Shiba
5c61db3ea0 types added, names changed, rewrote most of ytContent.ts
- Added more types, so when there is an error it's more visible.
- Default setting was using `lbry.tv` which doesn't exists anymore, so i made it odysee.
- Changed `redirect` value name in the `LbrySettings` to `platform` which makes more sense to this version.
- Changed `url` in UpdateContext to `pathname`. Using `url` for the full URL.
- Rewrote most of the `ytContent.tsx` so the timestamp feature doesnt look like a patch.
2021-12-11 19:28:24 +00:00
Shiba
8c4f3e60e0
Update ytContent.tsx
Turns out YouTube doesn't destroy the `HTMLVideoElement` once its created, so no need to check if its destroyed
2021-12-11 04:23:36 +03:00
Shiba
c7a839573a
Update ytContent.tsx 2021-12-11 03:38:45 +03:00
kodxana
285d46bcc1
Merge pull request #68 from LBRYFoundation/dependabot/npm_and_yarn/tmpl-1.0.5
Bump tmpl from 1.0.4 to 1.0.5
2021-10-30 18:35:15 +02:00
dependabot[bot]
091c0e29b4
Bump tmpl from 1.0.4 to 1.0.5
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-22 05:15:05 +00:00
kodxana
9a9b51bbba
Merge pull request #67 from LBRYFoundation/bug_fix
Bug fix
2021-08-28 22:05:25 +02:00
kodxana
972f5e7415
Update ytContent.tsx 2021-08-28 22:00:01 +02:00
kodxana
014ad21fd6
Update tabOnUpdated.ts 2021-08-28 21:59:18 +02:00
kodxana
9532d45b15
Update settings.ts 2021-08-28 21:58:10 +02:00
kodxana
11334c7689
Update yt.ts 2021-08-28 21:56:57 +02:00
kodxana
e2f46d2d08
Update manifest.json 2021-08-28 21:56:20 +02:00
kodxana
d23fda84d5
Update manifest.json 2021-08-28 21:56:06 +02:00
kodxana
0f6ba8a4f9
Merge pull request #66 from LBRYFoundation/dependabot/npm_and_yarn/color-string-1.6.0
Bump color-string from 1.5.4 to 1.6.0
2021-08-28 21:49:33 +02:00
kodxana
666ec9db7b
Merge pull request #65 from LBRYFoundation/dependabot/npm_and_yarn/ws-5.2.3
Bump ws from 5.2.2 to 5.2.3
2021-08-28 21:49:22 +02:00
kodxana
10c843e7b8
Merge pull request #63 from LBRYFoundation/dependabot/npm_and_yarn/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7
2021-08-28 21:49:09 +02:00
dependabot[bot]
606288e809
Bump color-string from 1.5.4 to 1.6.0
Bumps [color-string](https://github.com/Qix-/color-string) from 1.5.4 to 1.6.0.
- [Release notes](https://github.com/Qix-/color-string/releases)
- [Changelog](https://github.com/Qix-/color-string/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Qix-/color-string/compare/1.5.4...1.6.0)

---
updated-dependencies:
- dependency-name: color-string
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-28 19:48:51 +00:00
dependabot[bot]
729deb8be4
Bump ws from 5.2.2 to 5.2.3
Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-28 19:48:50 +00:00
kodxana
7eba8ebea5
Merge pull request #62 from LBRYFoundation/dependabot/npm_and_yarn/browserslist-4.16.6
Bump browserslist from 4.16.1 to 4.16.6
2021-08-28 21:48:31 +02:00
kodxana
820208e94b
Merge pull request #61 from LBRYFoundation/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.20 to 4.17.21
2021-08-28 21:48:22 +02:00
kodxana
1d6e02d006
Merge pull request #60 from LBRYFoundation/dependabot/npm_and_yarn/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9
2021-08-28 21:48:14 +02:00
dependabot[bot]
fdbc0bf93c
Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-13 00:03:01 +00:00
dependabot[bot]
862e52030d
Bump browserslist from 4.16.1 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.1 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.1...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-27 00:41:29 +00:00
dependabot[bot]
8b0176a3a2
Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 23:58:49 +00:00
dependabot[bot]
366028f49b
Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 23:26:00 +00:00
kodxana
170fab60c0
Merge pull request #59 from LBRYFoundation/dependabot/npm_and_yarn/elliptic-6.5.4
Bump elliptic from 6.5.3 to 6.5.4
2021-04-30 19:04:50 +02:00
dependabot[bot]
32a1a01676
Bump elliptic from 6.5.3 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-28 14:27:11 +00:00
kodxana
eec8092a4a
Merge pull request #54 from LBRYFoundation/dependabot/npm_and_yarn/marked-2.0.0
Bump marked from 1.2.7 to 2.0.0
2021-04-28 16:27:01 +02:00
kodxana
5789397032
Merge pull request #58 from Aenigma/hotfix/cors-workaround
Fix CORS errors in content script
2021-04-28 16:26:23 +02:00
Kevin Raoofi
94af57b809 Fix CORS errors in content script
Move fetch requests to background page. Include a dirty cache so that
same requests don't get made over and over again as settings change.
2021-04-20 23:22:19 -04:00
kodxana
2afbdcef9b
Update README.md 2021-03-10 16:13:17 +01:00
kodxana
9a9acf3c29
Update README.md 2021-02-27 20:31:12 +01:00
dependabot[bot]
9bb7e5fd5c
Bump marked from 1.2.7 to 2.0.0
Bumps [marked](https://github.com/markedjs/marked) from 1.2.7 to 2.0.0.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/release.config.js)
- [Commits](https://github.com/markedjs/marked/compare/v1.2.7...v2.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-08 22:43:14 +00:00
kodxana
2203768d9b
Merge pull request #52 from LBRYFoundation/up_version
Update version to 1.7.2
2021-01-21 23:19:57 +01:00
kodxana
0ea63c60b1
Update manifest.json 2021-01-21 23:19:15 +01:00
kodxana
ab707bf533
Merge pull request #51 from Aenigma/feature/odysee-button-hotfix
Hotfixes for odysee button
2021-01-21 23:11:35 +01:00
kodxana
e77490ef19
Merge pull request #50 from LBRYFoundation/deps_update
Updated build process
2021-01-21 23:10:53 +01:00
Kevin Raoofi
410a87710b Fix redirect
The odysee button change came with passing around the secondary part of
the URL instead of the whole thing. Unfortunately, this change requires
functions to be a little smarter and resolve with the domain lookup.

This was missed for redirects.
2021-01-21 17:01:35 -05:00
Kevin Raoofi
a0963db2b4 build glob parity with watch
It seems like I only cared about the watch command and missed updating
`build` in the past. This makes the CI build broken packages.
2021-01-21 17:00:09 -05:00
kodxana
47d5292740
Update extension-build.js.yml 2021-01-21 22:54:03 +01:00
kodxana
e0434192ff
Updated build process 2021-01-21 22:53:06 +01:00
kodxana
4f835f0c24
Merge pull request #49 from LBRYFoundation/dev_test
Update extension-build.js.yml
2021-01-21 22:48:08 +01:00
kodxana
f04d0b7c1f
Update extension-build.js.yml 2021-01-21 22:47:36 +01:00
kodxana
2be251fbd2
Merge pull request #48 from Aenigma/feature/gh-actions
Github Action to Build
2021-01-21 22:30:57 +01:00
Kevin Raoofi
1aab3f67e1 Update docs 2021-01-21 16:25:56 -05:00
Kevin Raoofi
570d278f1f Create github actions
Builds the web extension and archives it as an artifact
2021-01-21 16:11:11 -05:00
kodxana
3db9a692c8
Merge pull request #47 from Aenigma/feature/yarn
Use Yarn Instead of npm
2021-01-21 21:48:43 +01:00
Kevin Raoofi
c0cd29abd2 Use yarn v1 instead of npm 2021-01-21 15:38:31 -05:00
kodxana
218f023600
Merge pull request #46 from Aenigma/feature/logo-update
Odysee Logo + Local Badges
2021-01-11 16:04:04 +01:00
Kevin Raoofi
e408b9f3e8 Update README install badges
* Badges included in repo so URL changes won't break them
* Link the repo license text, instead
2020-12-29 12:17:50 -05:00
Kevin Raoofi
c5c8f40ddf Add WoL logos
* Adds the new Watch on LBRY logos
* Use logo relative to repo for README

They were added into a new folder to make a distinction between
external and Watch on LBRY icons.

 adasd
2020-12-29 12:17:32 -05:00
Kevin Raoofi
76830c907e Odysee logo support
This change includes a settings object which describes how the button
should be displayed keyed by the redirect setting.
2020-12-29 09:54:28 -05:00
Kevin Raoofi
951acee9da Ignore DS_Store 2020-12-29 09:54:28 -05:00
kodxana
6b43680b5e
Update README.md 2020-12-18 16:39:25 +01:00
kodxana
c990b6920c
Create FUNDING.yml 2020-12-18 16:34:50 +01:00
kodxana
a03a5df765
Update README.md 2020-12-18 16:07:24 +01:00
kodxana
1996373b54
Added logo 2020-12-18 15:44:21 +01:00
73 changed files with 16105 additions and 3770 deletions

16
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,16 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/typescript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-buster
ARG VARIANT="16-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
# [Optional] Uncomment if you want to install more global node packages
# RUN su node -c "npm install -g <your-package-list -here>"

View file

@ -0,0 +1,36 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/typescript-node
{
"name": "Node.js & TypeScript",
"build": {
"dockerfile": "Dockerfile",
// Update 'VARIANT' to pick a Node version: 16, 14, 12.
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local on arm64/Apple Silicon.
"args": {
"VARIANT": "14-bullseye"
}
},
// Set *default* container specific settings.json values on container create.
"settings": {
"javascript.format.placeOpenBraceOnNewLineForControlBlocks": false,
"javascript.format.placeOpenBraceOnNewLineForFunctions": false,
"typescript.format.placeOpenBraceOnNewLineForControlBlocks": false,
"typescript.format.placeOpenBraceOnNewLineForFunctions": false
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"bierner.folder-source-actions",
"jbockle.jbockle-format-files",
"eamodio.gitlens"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "npm install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {
"python": "latest"
}
}

4
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,4 @@
# These are supported funding model platforms
liberapay: Madiator2011
custom: cointr.ee/madiator2011

23
.github/workflows/contributors.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: Add contributors
on:
schedule:
- cron: '20 20 * * *'
push:
branches:
- master
jobs:
add-contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: BobAnkh/add-contributors@master
with:
CONTRIBUTOR: '## Contributors'
COLUMN_PER_ROW: '6'
ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}
IMG_WIDTH: '100'
FONT_SIZE: '14'
PATH: '/README.md'
COMMIT_MESSAGE: 'docs(README): update contributors'
AVATAR_SHAPE: 'round'

View file

@ -0,0 +1,37 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [15.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: yarn
- run: npm run build
- run: npm run
- run: npm run build:webext
- name: Archive extension directory
uses: actions/upload-artifact@v2
with:
name: extension
path: |
dist
web-ext-artifacts

4
.gitignore vendored
View file

@ -1,4 +1,8 @@
.cache
dist
build
node_modules
web-ext-artifacts
yarn-error.log
.DS_Store

15
CHANGELOG.md Normal file
View file

@ -0,0 +1,15 @@
# Changelog
## [1.7.5](https://github.com/LBRYFoundation/Watch-on-LBRY/releases/tag/1.7.5) (2021-12-12)
**Closed issues:**
- Subscription Converter feature doesn't seem to work [\#64](https://github.com/LBRYFoundation/Watch-on-LBRY/issues/64)
- Redirect with timestamp [\#57](https://github.com/LBRYFoundation/Watch-on-LBRY/issues/57)
- Should default to odysee.com instead of lbry.tv [\#53](https://github.com/LBRYFoundation/Watch-on-LBRY/issues/53)
- Instead of redirect. Show a popup from the extension icon [\#38](https://github.com/LBRYFoundation/Watch-on-LBRY/issues/38)
**Merged pull requests:**
- Update ytContent.tsx [\#70](https://github.com/LBRYFoundation/Watch-on-LBRY/pull/70) ([Shiba](https://github.com/DeepDoge))
- Madiator icon added [\#71](https://github.com/LBRYFoundation/Watch-on-LBRY/pull/71) ([Shiba](https://github.com/DeepDoge))

View file

@ -1,11 +1,17 @@
## Looking for contributors :)
![Logo](src/assets/icons/wol/default-transparent.svg)
# Watch on LBRY
A plugin for web browsers that automatically checks whether a YouTube video or channel is on LBRY. If it is, it redirects you to LBRY to watch it there.
A plugin for web browsers that brings more utility for LBRY Protocol by allowing you to find people you watch on YouTube that are availible on LBRY.tv/Odysee/Desktop App and other LBRY Protocol based apps/websites, allows you to easly check your subscribtion list and much more!
# Privacy
This plugin is using LBRY Inc YouTube Sync API to check if video fot synchronized with LBRY Platform. For more informations read LBRY Inc Privacy Policy at [here](https://lbry.com/privacypolicy)
## Installation
[![Get on Firefox](https://addons.cdn.mozilla.net/static/img/addons-buttons/AMO-button_1.png)](https://addons.mozilla.org/en/firefox/addon/watch-on-lbry/?src=search) [![Get on Chrome](https://developer.chrome.com/webstore/images/ChromeWebStore_BadgeWBorder_v2_206x58.png)](https://chrome.google.com/webstore/detail/watch-on-lbry/jjmbbhopnjdjnpceiecihldbhibchgek)
[![Get it on Firefox](doc/img/AMO-button_1.png)](https://addons.mozilla.org/en/firefox/addon/watch-on-lbry/?src=search)
[![Get it on Chrome](doc/img/chrome-small-border.png)](https://chrome.google.com/webstore/detail/watch-on-lbry/jjmbbhopnjdjnpceiecihldbhibchgek)
## Build
@ -15,7 +21,7 @@ For Production
```bash
$ npm install
$ npm run build
$ npx web-ext build --source-dir ./dist # optional, to create the zip file from the dist directory
$ npm run build:webext # optional, to create the zip file from the dist directory
```
For Development
@ -56,8 +62,87 @@ Pull requests are welcome. For major changes, please open an issue first to disc
Please make sure to update tests as appropriate.
## Contributors
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kodxana>
<img src=https://avatars.githubusercontent.com/u/16674412?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=kodxana/>
<br />
<sub style="font-size:14px"><b>kodxana</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/DeepDoge>
<img src=https://avatars.githubusercontent.com/u/44804845?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Shiba/>
<br />
<sub style="font-size:14px"><b>Shiba</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Aenigma>
<img src=https://avatars.githubusercontent.com/u/409173?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kevin Raoofi/>
<br />
<sub style="font-size:14px"><b>Kevin Raoofi</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Yamboy1>
<img src=https://avatars.githubusercontent.com/u/37413895?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Yamboy1/>
<br />
<sub style="font-size:14px"><b>Yamboy1</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/clay53>
<img src=https://avatars.githubusercontent.com/u/16981283?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Clayton Hickey/>
<br />
<sub style="font-size:14px"><b>Clayton Hickey</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/adam-dorin>
<img src=https://avatars.githubusercontent.com/u/1072815?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Adam/>
<br />
<sub style="font-size:14px"><b>Adam</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kbakdev>
<img src=https://avatars.githubusercontent.com/u/56700396?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kacper Bąk/>
<br />
<sub style="font-size:14px"><b>Kacper Bąk</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/eggplantbren>
<img src=https://avatars.githubusercontent.com/u/1578298?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Brendon J. Brewer/>
<br />
<sub style="font-size:14px"><b>Brendon J. Brewer</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/FireMasterK>
<img src=https://avatars.githubusercontent.com/u/20838718?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Kavin/>
<br />
<sub style="font-size:14px"><b>Kavin</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/kauffj>
<img src=https://avatars.githubusercontent.com/u/530774?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jeremy Kauffman/>
<br />
<sub style="font-size:14px"><b>Jeremy Kauffman</b></sub>
</a>
</td>
</tr>
</table>
## License
[MIT](https://choosealicense.com/licenses/mit/)
[GPL-3.0 License](LICENSE)
## Support

BIN
doc/img/AMO-button_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

9
global.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
declare module '*.md' {
var _: string
export default _
}
declare namespace chrome
{
export const action = chrome.browserAction
}

56
manifest.v2.json Normal file
View file

@ -0,0 +1,56 @@
{
"manifest_version": 2,
"name": "Watch on LBRY",
"version": "2.0.1",
"icons": {
"16": "assets/icons/wol/icon16.png",
"48": "assets/icons/wol/icon48.png",
"128": "assets/icons/wol/icon128.png"
},
"permissions": [
"https://www.youtube.com/",
"https://yewtu.be/",
"https://vid.puffyan.us/",
"https://invidio.xamh.de/",
"https://invidious.kavin.rocks/",
"https://api.odysee.com/",
"https://lbry.tv/",
"https://odysee.com/",
"https://madiator.com/",
"https://finder.madiator.com/",
"tabs",
"storage"
],
"web_accessible_resources": [
"pages/popup/index.html",
"pages/YTtoLBRY/index.html",
"pages/import/index.html",
"assets/icons/lbry/lbry-logo.svg",
"assets/icons/lbry/odysee-logo.svg",
"assets/icons/lbry/madiator-logo.svg"
],
"browser_action": {
"default_title": "Watch on LBRY",
"default_popup": "pages/popup/index.html"
},
"content_scripts": [
{
"matches": [
"https://www.youtube.com/*",
"https://yewtu.be/*",
"https://vid.puffyan.us/*",
"https://invidio.xamh.de/*",
"https://invidious.kavin.rocks/*"
],
"js": [
"scripts/ytContent.js"
]
}
],
"background": {
"scripts": [
"service-worker-entry-point.js"
],
"persistent": true
}
}

59
manifest.v3.json Normal file
View file

@ -0,0 +1,59 @@
{
"manifest_version": 3,
"name": "Watch on LBRY",
"version": "2.0.1",
"icons": {
"16": "assets/icons/wol/icon16.png",
"48": "assets/icons/wol/icon48.png",
"128": "assets/icons/wol/icon128.png"
},
"permissions": [
"storage",
"tabs"
],
"host_permissions": [
"https://www.youtube.com/",
"https://yewtu.be/",
"https://vid.puffyan.us/",
"https://invidio.xamh.de/",
"https://invidious.kavin.rocks/",
"https://api.odysee.com/",
"https://lbry.tv/",
"https://odysee.com/",
"https://madiator.com/",
"https://finder.madiator.com/"
],
"web_accessible_resources": [{
"resources": [
"pages/popup/index.html",
"pages/YTtoLBRY/index.html",
"pages/import/index.html",
"assets/icons/lbry/lbry-logo.svg",
"assets/icons/lbry/odysee-logo.svg",
"assets/icons/lbry/madiator-logo.svg"
],
"matches": ["<all_urls>"],
"extension_ids": []
}],
"action": {
"default_title": "Watch on LBRY",
"default_popup": "pages/popup/index.html"
},
"content_scripts": [
{
"matches": [
"https://www.youtube.com/*",
"https://yewtu.be/*",
"https://vid.puffyan.us/*",
"https://invidio.xamh.de/*",
"https://invidious.kavin.rocks/*"
],
"js": [
"scripts/ytContent.js"
]
}
],
"background": {
"service_worker": "service-worker-entry-point.js"
}
}

6363
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,21 @@
"version": "1.5.4",
"license": "GPL-3.0",
"scripts": {
"build:assets": "cpx \"src/**/*.{json,png}\" dist/",
"build:assets": "cpx \"src/**/*.{json,png,svg}\" dist/",
"watch:assets": "cpx --watch -v \"src/**/*.{json,png,svg}\" dist/",
"build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps --no-minify \"src/scripts/*.{js,ts}\" \"src/**/*.html\"",
"watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/scripts/*.{js,jsx,ts,tsx}\" \"src/**/*.html\"",
"build": "npm-run-all -l -p build:parcel build:assets",
"watch": "npm-run-all -l -p watch:parcel watch:assets",
"build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps --no-minify \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"",
"watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"",
"build:webext": "web-ext build --source-dir ./dist --overwrite-dest",
"clear:dist": "rm -r ./dist ; mkdir ./dist",
"build:base": "npm-run-all -l -p build:parcel build:assets",
"pick:manifest:v2": "cp -b ./manifest.v2.json ./dist/manifest.json",
"pick:manifest:v3": "cp -b ./manifest.v3.json ./dist/manifest.json",
"build:v2": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v2",
"build:v3": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v3",
"build": "rm -r ./build ; mkdir ./build && npm run build:v2 && zip -r ./build/manifest-v2.zip ./dist && npm run build:v3 && zip -r ./build/manifest-v3.zip ./dist",
"watch:v2": "npm run clear:dist ; npm run pick:manifest:v2 && npm-run-all -l -p watch:parcel watch:assets",
"watch:v3": "npm run clear:dist ; npm run pick:manifest:v3 && npm-run-all -l -p watch:parcel watch:assets",
"watch": "npm run watch:v3",
"start:chrome": "web-ext run -t chromium --source-dir ./dist",
"start:firefox": "web-ext run --source-dir ./dist",
"test": "jest"
@ -28,9 +37,9 @@
"cpx": "^1.5.0",
"cross-env": "^7.0.2",
"jest": "^26.5.3",
"lodash": "^4.17.20",
"marked": "^1.2.5",
"node-forge": ">=0.10.0",
"lodash": "^4.17.21",
"marked": "^2.0.0",
"node-forge": "^0.10.0",
"node-sass": "^4.14.1",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.4",

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,38 @@
<svg width="525" height="136" viewBox="0 0 525 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M69.23 19.13L84.46 101.34H72.25L64.3 55.76L42.24 102.8L20.29 55.76L12.34 101.34H0.130005L15.36 19.13L42.24 75.69L69.23 19.13Z" fill="white"/>
<path d="M119.13 42.2C123.23 42.2 127.08 42.98 130.7 44.55C134.32 46.12 137.48 48.25 140.2 50.93C142.92 53.62 145.06 56.79 146.63 60.45C148.19 64.11 148.98 67.99 148.98 72.1V101.11H137.44V95.68C134.9 97.6 132.1 99.12 129.04 100.23C125.98 101.34 122.69 101.89 119.18 101.89C115.07 101.89 111.21 101.11 107.59 99.54C103.97 97.97 100.81 95.84 98.13 93.16C95.44 90.47 93.31 87.32 91.75 83.7C90.18 80.08 89.4 76.22 89.4 72.11C89.4 68 90.18 64.12 91.75 60.46C93.31 56.8 95.44 53.63 98.12 50.94C100.8 48.25 103.95 46.12 107.57 44.56C111.17 42.98 115.03 42.2 119.13 42.2ZM119.18 90.36C121.72 90.36 124.09 89.87 126.29 88.9C128.49 87.92 130.41 86.61 132.06 84.96C133.7 83.31 135.01 81.37 135.98 79.16C136.95 76.94 137.44 74.6 137.44 72.12C137.44 69.57 136.95 67.18 135.98 64.97C135.01 62.75 133.7 60.82 132.06 59.17C130.42 57.52 128.49 56.2 126.29 55.23C124.09 54.25 121.72 53.77 119.18 53.77C116.64 53.77 114.27 54.26 112.07 55.23C109.87 56.21 107.94 57.52 106.3 59.17C104.66 60.82 103.35 62.76 102.38 64.97C101.41 67.19 100.92 69.57 100.92 72.12C100.92 74.6 101.4 76.95 102.38 79.16C103.35 81.37 104.66 83.31 106.3 84.96C107.94 86.61 109.86 87.93 112.07 88.9C114.27 89.87 116.64 90.36 119.18 90.36Z" fill="white"/>
<path d="M214.72 71.77C214.72 75.88 213.95 79.72 212.42 83.31C210.89 86.89 208.78 90.03 206.09 92.72C203.4 95.41 200.27 97.52 196.68 99.05C193.1 100.58 189.25 101.35 185.14 101.35C181.03 101.35 177.19 100.59 173.6 99.05C170.02 97.52 166.88 95.41 164.19 92.72C161.5 90.03 159.39 86.9 157.86 83.31C156.33 79.73 155.56 75.88 155.56 71.77C155.56 67.66 156.32 63.82 157.86 60.23C159.39 56.65 161.5 53.51 164.19 50.82C166.88 48.13 170.01 46.02 173.6 44.49C177.18 42.96 181.03 42.19 185.14 42.19C188.57 42.19 191.8 42.73 194.83 43.81C197.85 44.89 200.64 46.41 203.17 48.35V2.64999H214.71V71.64V71.77H214.72ZM185.21 89.8C187.67 89.8 189.99 89.33 192.19 88.4C194.39 87.47 196.3 86.18 197.94 84.54C199.58 82.9 200.86 80.98 201.79 78.77C202.72 76.57 203.19 74.23 203.19 71.77V71.66C203.19 69.2 202.72 66.88 201.79 64.72C200.86 62.56 199.58 60.65 197.94 59.01C196.3 57.37 194.39 56.08 192.19 55.15C189.99 54.22 187.67 53.75 185.21 53.75C182.75 53.75 180.43 54.22 178.23 55.15C176.03 56.08 174.12 57.37 172.48 59.01C170.84 60.65 169.54 62.58 168.57 64.78C167.6 66.98 167.12 69.32 167.12 71.78C167.12 74.24 167.6 76.58 168.57 78.78C169.54 80.98 170.84 82.91 172.48 84.55C174.12 86.19 176.03 87.48 178.23 88.41C180.42 89.33 182.75 89.8 185.21 89.8Z" fill="white"/>
<path d="M223 36.04V24.5H234.54V36.04H223ZM234.54 42.09V101.23H223V42.09H234.54Z" fill="white"/>
<path d="M270.88 42.2C274.98 42.2 278.83 42.98 282.45 44.55C286.07 46.12 289.23 48.25 291.95 50.93C294.67 53.61 296.81 56.79 298.38 60.45C299.94 64.11 300.73 67.99 300.73 72.1V101.11H289.2V95.68C286.66 97.6 283.86 99.12 280.8 100.23C277.74 101.34 274.45 101.89 270.94 101.89C266.83 101.89 262.97 101.11 259.35 99.54C255.73 97.97 252.57 95.84 249.89 93.16C247.2 90.47 245.07 87.32 243.51 83.7C241.94 80.08 241.16 76.22 241.16 72.11C241.16 68 241.94 64.12 243.51 60.46C245.07 56.8 247.2 53.63 249.88 50.94C252.56 48.25 255.71 46.12 259.33 44.56C262.93 42.98 266.79 42.2 270.88 42.2ZM270.94 90.36C273.48 90.36 275.85 89.87 278.05 88.9C280.25 87.92 282.17 86.61 283.82 84.96C285.46 83.31 286.77 81.37 287.74 79.16C288.71 76.94 289.2 74.6 289.2 72.12C289.2 69.57 288.71 67.18 287.74 64.97C286.77 62.75 285.46 60.82 283.82 59.17C282.18 57.52 280.25 56.2 278.05 55.23C275.85 54.25 273.48 53.77 270.94 53.77C268.4 53.77 266.03 54.26 263.83 55.23C261.63 56.21 259.7 57.52 258.06 59.17C256.42 60.82 255.11 62.76 254.14 64.97C253.17 67.19 252.68 69.57 252.68 72.12C252.68 74.6 253.16 76.95 254.14 79.16C255.11 81.37 256.42 83.31 258.06 84.96C259.7 86.61 261.62 87.93 263.83 88.9C266.03 89.87 268.4 90.36 270.94 90.36Z" fill="white"/>
<path d="M340.49 37.27H329.74V101.33H318.2V37.27H307.34V25.73H318.2V3.78H329.74V25.73H340.49V37.27Z" fill="white"/>
<path d="M376.55 42.09C380.66 42.09 384.5 42.86 388.09 44.39C391.67 45.92 394.81 48.03 397.5 50.72C400.19 53.41 402.3 56.54 403.83 60.13C405.36 63.71 406.13 67.56 406.13 71.67C406.13 75.78 405.36 79.62 403.83 83.21C402.3 86.79 400.19 89.93 397.5 92.62C394.81 95.31 391.68 97.42 388.09 98.95C384.51 100.48 380.66 101.25 376.55 101.25C372.44 101.25 368.6 100.49 365.01 98.95C361.43 97.42 358.29 95.31 355.6 92.62C352.91 89.93 350.8 86.8 349.27 83.21C347.74 79.63 346.97 75.78 346.97 71.67C346.97 67.56 347.73 63.72 349.27 60.13C350.8 56.55 352.91 53.41 355.6 50.72C358.29 48.03 361.42 45.92 365.01 44.39C368.6 42.85 372.45 42.09 376.55 42.09ZM376.61 89.69C379.07 89.69 381.39 89.22 383.59 88.29C385.79 87.36 387.7 86.07 389.34 84.43C390.98 82.79 392.26 80.86 393.19 78.66C394.12 76.46 394.59 74.12 394.59 71.66C394.59 69.2 394.12 66.86 393.19 64.66C392.26 62.46 390.98 60.54 389.34 58.89C387.7 57.25 385.79 55.96 383.59 55.03C381.39 54.1 379.07 53.63 376.61 53.63C374.15 53.63 371.83 54.1 369.63 55.03C367.43 55.96 365.52 57.25 363.88 58.89C362.24 60.53 360.94 62.46 359.97 64.66C359 66.86 358.52 69.2 358.52 71.66C358.52 74.12 359 76.46 359.97 78.66C360.94 80.86 362.24 82.79 363.88 84.43C365.52 86.07 367.43 87.36 369.63 88.29C371.83 89.22 374.15 89.69 376.61 89.69Z" fill="white"/>
<path d="M452.38 46.5C454.92 48.16 457.08 50.23 458.88 52.72L452.38 57.88L449.92 59.9C448.65 58.03 446.99 56.54 444.94 55.42C442.89 54.3 440.66 53.74 438.28 53.74C436.34 53.74 434.51 54.11 432.79 54.86C431.07 55.61 429.58 56.62 428.31 57.88C427.04 59.15 426.03 60.64 425.29 62.36C424.54 64.08 424.17 65.91 424.17 67.85V89.13V101.11H412.63V67.78V42.2H424.17V46.5C426.19 45.14 428.39 44.09 430.78 43.33C433.17 42.58 435.67 42.2 438.28 42.2C440.89 42.2 443.39 42.58 445.78 43.33C448.16 44.08 450.36 45.14 452.38 46.5Z" fill="white"/>
<path d="M348.69 23.33C349.43 23.33 350.07 23.59 350.59 24.12C351.11 24.64 351.38 25.28 351.38 26.02C351.38 26.76 351.12 27.4 350.59 27.92C350.07 28.44 349.43 28.71 348.69 28.71C347.95 28.71 347.31 28.45 346.79 27.92C346.27 27.4 346 26.76 346 26.02C346 25.28 346.26 24.64 346.79 24.12C347.31 23.59 347.94 23.33 348.69 23.33Z" fill="white"/>
<path d="M368.7 23.48C370.02 23.48 371.23 23.19 372.35 22.62C373.47 22.05 374.39 21.29 375.14 20.34L379.24 23.53C378.02 25.1 376.5 26.35 374.68 27.29C372.86 28.23 370.86 28.7 368.7 28.7C366.88 28.7 365.15 28.35 363.53 27.66C361.91 26.97 360.48 26.01 359.26 24.8C358.03 23.58 357.07 22.16 356.37 20.54C355.67 18.92 355.32 17.18 355.32 15.32C355.32 13.5 355.67 11.77 356.37 10.15C357.07 8.52999 358.03 7.11001 359.26 5.89001C360.49 4.67001 361.91 3.72 363.53 3.03C365.15 2.34 366.87 1.98999 368.7 1.98999C370.86 1.98999 372.86 2.45 374.68 3.37C376.5 4.29 378.02 5.55 379.24 7.16L375.14 10.35C374.4 9.37001 373.47 8.60001 372.35 8.04001C371.24 7.48001 370.02 7.20001 368.7 7.20001C367.59 7.20001 366.53 7.40999 365.53 7.82999C364.53 8.24999 363.66 8.83001 362.92 9.57001C362.18 10.31 361.59 11.17 361.17 12.16C360.75 13.15 360.54 14.2 360.54 15.31C360.54 16.42 360.75 17.47 361.17 18.46C361.59 19.45 362.17 20.32 362.92 21.05C363.66 21.79 364.53 22.38 365.53 22.81C366.53 23.26 367.59 23.48 368.7 23.48Z" fill="white"/>
<path d="M396.52 1.94C398.38 1.94 400.12 2.29001 401.74 2.98001C403.36 3.67001 404.78 4.63 406 5.84C407.22 7.06 408.17 8.48001 408.86 10.1C409.55 11.72 409.9 13.46 409.9 15.32C409.9 17.18 409.55 18.92 408.86 20.54C408.17 22.16 407.21 23.58 406 24.8C404.78 26.02 403.37 26.97 401.74 27.66C400.12 28.35 398.38 28.7 396.52 28.7C394.66 28.7 392.92 28.35 391.3 27.66C389.68 26.97 388.26 26.01 387.04 24.8C385.82 23.59 384.87 22.16 384.18 20.54C383.49 18.92 383.14 17.18 383.14 15.32C383.14 13.46 383.49 11.72 384.18 10.1C384.87 8.48001 385.83 7.06 387.04 5.84C388.25 4.62 389.67 3.67001 391.3 2.98001C392.92 2.29001 394.66 1.94 396.52 1.94ZM396.55 23.48C397.66 23.48 398.71 23.27 399.71 22.85C400.7 22.43 401.57 21.85 402.31 21.1C403.05 20.36 403.63 19.49 404.05 18.49C404.47 17.49 404.68 16.44 404.68 15.32C404.68 14.2 404.47 13.15 404.05 12.15C403.63 11.15 403.05 10.28 402.31 9.54001C401.57 8.80001 400.7 8.21001 399.71 7.79001C398.72 7.37001 397.66 7.16 396.55 7.16C395.44 7.16 394.39 7.37001 393.39 7.79001C392.4 8.21001 391.53 8.80001 390.79 9.54001C390.05 10.28 389.46 11.15 389.02 12.15C388.58 13.15 388.36 14.2 388.36 15.32C388.36 16.44 388.58 17.49 389.02 18.49C389.46 19.49 390.05 20.36 390.79 21.1C391.53 21.84 392.4 22.43 393.39 22.85C394.38 23.27 395.44 23.48 396.55 23.48Z" fill="white"/>
<path d="M450.14 3.76999C451.73 4.81999 452.99 6.18999 453.94 7.89999C454.89 9.60999 455.36 11.47 455.36 13.5V28.75H450.14V23.28V13.5C450.14 12.62 449.97 11.79 449.63 11.02C449.29 10.24 448.83 9.56 448.24 8.97C447.65 8.38 446.96 7.90999 446.19 7.57999C445.41 7.23999 444.59 7.07001 443.71 7.07001C442.83 7.07001 441.99 7.23999 441.2 7.57999C440.41 7.91999 439.71 8.38 439.12 8.97C438.53 9.56 438.06 10.25 437.73 11.02C437.39 11.8 437.22 12.62 437.22 13.5V23.28V28.75H432V23.28V13.5C432 12.62 431.83 11.79 431.49 11.02C431.15 10.24 430.69 9.56 430.1 8.97C429.51 8.38 428.82 7.90999 428.05 7.57999C427.27 7.23999 426.44 7.07001 425.57 7.07001C424.69 7.07001 423.86 7.23999 423.06 7.57999C422.27 7.91999 421.57 8.38 420.98 8.97C420.39 9.56 419.92 10.25 419.59 11.02C419.25 11.8 419.08 12.62 419.08 13.5V23.28V28.75H413.86V13.5V1.84H419.08V3.76999C420.03 3.15999 421.05 2.69001 422.15 2.35001C423.25 2.01001 424.39 1.84 425.57 1.84C426.75 1.84 427.89 2.01001 428.99 2.35001C430.09 2.69001 431.09 3.15999 432 3.76999C432.98 4.40999 433.86 5.22001 434.63 6.20001C435.37 5.25001 436.23 4.43999 437.21 3.76999C438.16 3.15999 439.18 2.69001 440.28 2.35001C441.38 2.01001 442.52 1.84 443.7 1.84C444.88 1.84 446.02 2.01001 447.12 2.35001C448.22 2.69001 449.22 3.15999 450.14 3.76999Z" fill="white"/>
<path d="M17.89 119.39C17.91 119.51 17.93 119.63 17.94 119.75C17.95 119.87 17.95 120 17.95 120.14V120.17V120.2C17.95 120.34 17.95 120.47 17.94 120.59C17.93 120.71 17.91 120.83 17.89 120.95V120.98L17.8 121.7C17.78 121.72 17.77 121.76 17.77 121.82C17.75 121.92 17.73 122.01 17.71 122.09C17.69 122.17 17.66 122.26 17.62 122.36C17.34 123.28 16.92 124.1 16.37 124.82C15.82 125.54 15.15 126.15 14.37 126.65C13.71 127.05 12.98 127.36 12.18 127.59C11.38 127.82 10.53 127.93 9.63 127.93H5.7V134.44H2.5V112.4H9.64C11.44 112.4 13.02 112.83 14.38 113.69C15.16 114.19 15.82 114.8 16.38 115.52C16.93 116.24 17.34 117.05 17.63 117.95C17.71 118.17 17.76 118.36 17.78 118.52C17.78 118.58 17.79 118.62 17.81 118.64C17.83 118.76 17.84 118.88 17.85 119C17.86 119.12 17.87 119.23 17.89 119.33V119.39ZM10.03 125.12C10.69 125.12 11.29 124.98 11.84 124.7C12.39 124.42 12.87 124.05 13.28 123.59C13.69 123.13 14.01 122.61 14.24 122.01C14.47 121.42 14.58 120.81 14.58 120.17C14.58 119.53 14.46 118.92 14.24 118.33C14.01 117.74 13.69 117.21 13.28 116.75C12.87 116.29 12.39 115.92 11.84 115.64C11.29 115.36 10.69 115.22 10.03 115.22H5.74V125.12H10.03Z" fill="white"/>
<path d="M35.56 118.85C35.56 119.79 35.41 120.59 35.12 121.24C34.83 121.89 34.4 122.57 33.85 123.26L27.19 131.75H35.41V134.42H21.49L28.39 125.3C29.01 124.5 29.57 123.77 30.07 123.11C30.27 122.83 30.47 122.55 30.68 122.28C30.89 122.01 31.07 121.76 31.24 121.53C31.4 121.3 31.53 121.11 31.64 120.97C31.75 120.83 31.81 120.74 31.84 120.7C32.18 120.12 32.35 119.5 32.35 118.84C32.35 118.32 32.25 117.83 32.05 117.37C31.85 116.91 31.57 116.51 31.22 116.17C30.87 115.83 30.46 115.56 30 115.36C29.54 115.16 29.05 115.06 28.52 115.06C27.99 115.06 27.51 115.16 27.04 115.36C26.58 115.56 26.18 115.83 25.84 116.17C25.5 116.51 25.23 116.91 25.03 117.37C24.83 117.83 24.73 118.32 24.73 118.84H21.64C21.64 117.96 21.82 117.13 22.2 116.34C22.57 115.55 23.07 114.86 23.7 114.27C24.33 113.68 25.06 113.22 25.9 112.87C26.74 112.53 27.64 112.36 28.6 112.36C29.56 112.36 30.46 112.53 31.3 112.87C32.14 113.21 32.87 113.68 33.5 114.27C34.13 114.86 34.63 115.55 35 116.34C35.37 117.13 35.56 117.97 35.56 118.85Z" fill="white"/>
<path d="M54.49 119.39C54.51 119.51 54.53 119.63 54.54 119.75C54.55 119.87 54.55 120 54.55 120.14V120.17V120.2C54.55 120.34 54.55 120.47 54.54 120.59C54.53 120.71 54.51 120.83 54.49 120.95V120.98L54.4 121.7C54.38 121.72 54.37 121.76 54.37 121.82C54.35 121.92 54.33 122.01 54.31 122.09C54.29 122.17 54.26 122.26 54.22 122.36C53.94 123.28 53.52 124.1 52.97 124.82C52.42 125.54 51.75 126.15 50.97 126.65C50.31 127.05 49.58 127.36 48.78 127.59C47.98 127.82 47.13 127.93 46.23 127.93H42.3V134.44H39.1V112.4H46.24C48.04 112.4 49.62 112.83 50.98 113.69C51.76 114.19 52.42 114.8 52.98 115.52C53.53 116.24 53.94 117.05 54.23 117.95C54.31 118.17 54.36 118.36 54.38 118.52C54.38 118.58 54.39 118.62 54.41 118.64C54.43 118.76 54.44 118.88 54.45 119C54.46 119.12 54.47 119.23 54.49 119.33V119.39V119.39ZM46.63 125.12C47.29 125.12 47.89 124.98 48.44 124.7C48.99 124.42 49.47 124.05 49.88 123.59C50.29 123.13 50.61 122.61 50.84 122.01C51.07 121.42 51.18 120.81 51.18 120.17C51.18 119.53 51.06 118.92 50.84 118.33C50.61 117.74 50.29 117.21 49.88 116.75C49.47 116.29 48.99 115.92 48.44 115.64C47.89 115.36 47.29 115.22 46.63 115.22H42.34V125.12H46.63Z" fill="white"/>
<path d="M87.28 119.39C87.3 119.51 87.32 119.63 87.33 119.75C87.34 119.87 87.34 120 87.34 120.14V120.17V120.2C87.34 120.34 87.34 120.47 87.33 120.59C87.32 120.71 87.3 120.83 87.28 120.95V120.98L87.19 121.7C87.17 121.72 87.16 121.76 87.16 121.82C87.14 121.92 87.12 122.01 87.1 122.09C87.08 122.17 87.05 122.26 87.01 122.36C86.73 123.28 86.31 124.1 85.76 124.82C85.21 125.54 84.54 126.15 83.76 126.65C83.1 127.05 82.37 127.36 81.57 127.59C80.77 127.82 79.92 127.93 79.02 127.93H75.09V134.44H71.88V112.4H79.02C80.82 112.4 82.4 112.83 83.76 113.69C84.54 114.19 85.2 114.8 85.76 115.52C86.31 116.24 86.72 117.05 87.01 117.95C87.09 118.17 87.14 118.36 87.16 118.52C87.16 118.58 87.17 118.62 87.19 118.64C87.21 118.76 87.22 118.88 87.23 119C87.24 119.12 87.25 119.23 87.27 119.33V119.39H87.28ZM79.42 125.12C80.08 125.12 80.68 124.98 81.23 124.7C81.78 124.42 82.26 124.05 82.67 123.59C83.08 123.13 83.4 122.61 83.63 122.01C83.86 121.42 83.97 120.81 83.97 120.17C83.97 119.53 83.85 118.92 83.63 118.33C83.4 117.74 83.08 117.21 82.67 116.75C82.26 116.29 81.78 115.92 81.23 115.64C80.68 115.36 80.08 115.22 79.42 115.22H75.13V125.12H79.42Z" fill="white"/>
<path d="M98.8 118.58C99.9 118.58 100.93 118.79 101.89 119.2C102.85 119.61 103.69 120.17 104.41 120.89C105.13 121.61 105.7 122.45 106.11 123.41C106.52 124.37 106.72 125.4 106.72 126.5C106.72 127.6 106.51 128.63 106.11 129.59C105.7 130.55 105.13 131.39 104.41 132.11C103.69 132.83 102.85 133.4 101.89 133.8C100.93 134.21 99.9 134.42 98.8 134.42C97.7 134.42 96.67 134.21 95.71 133.8C94.75 133.39 93.91 132.83 93.19 132.11C92.47 131.39 91.9 130.55 91.49 129.59C91.08 128.63 90.88 127.6 90.88 126.5C90.88 125.4 91.08 124.37 91.49 123.41C91.9 122.45 92.47 121.61 93.19 120.89C93.91 120.17 94.75 119.61 95.71 119.2C96.67 118.79 97.7 118.58 98.8 118.58ZM98.81 131.33C99.47 131.33 100.09 131.21 100.68 130.95C101.27 130.69 101.78 130.36 102.22 129.91C102.66 129.47 103 128.95 103.25 128.37C103.5 127.78 103.62 127.16 103.62 126.49C103.62 125.82 103.5 125.2 103.25 124.61C103 124.02 102.66 123.51 102.22 123.07C101.78 122.63 101.27 122.28 100.68 122.03C100.09 121.78 99.47 121.65 98.81 121.65C98.15 121.65 97.53 121.77 96.94 122.03C96.35 122.28 95.84 122.63 95.4 123.07C94.96 123.51 94.61 124.03 94.35 124.61C94.09 125.19 93.96 125.82 93.96 126.49C93.96 127.16 94.09 127.78 94.35 128.37C94.61 128.96 94.96 129.48 95.4 129.91C95.84 130.35 96.35 130.7 96.94 130.95C97.53 131.21 98.15 131.33 98.81 131.33Z" fill="white"/>
<path d="M131.2 118.55L125.92 130.64L124.24 134.51L122.53 130.64L120.73 126.53L118.93 130.64L117.25 134.51L115.57 130.64L110.26 118.55H113.65L117.25 126.77L119.05 122.66L120.73 118.79L122.44 122.66L124.24 126.77L127.84 118.55H131.2Z" fill="white"/>
<path d="M143.95 127.55H137.92C138.04 128.09 138.25 128.6 138.54 129.06C138.83 129.53 139.19 129.94 139.6 130.28C140.01 130.62 140.49 130.88 141.01 131.07C141.53 131.26 142.08 131.36 142.66 131.36C143.36 131.36 144.01 131.22 144.62 130.94C145.23 130.66 145.77 130.27 146.22 129.77H146.79H149.85C149.73 130.07 149.58 130.35 149.41 130.63C149.24 130.9 149.05 131.17 148.85 131.42C148.13 132.34 147.23 133.08 146.15 133.63C145.07 134.18 143.9 134.46 142.64 134.46C141.54 134.46 140.51 134.25 139.55 133.84C138.59 133.43 137.75 132.87 137.03 132.14C136.31 131.42 135.74 130.58 135.33 129.62C134.92 128.66 134.72 127.63 134.72 126.53C134.72 125.43 134.92 124.4 135.33 123.44C135.74 122.48 136.31 121.64 137.03 120.92C137.75 120.2 138.59 119.63 139.55 119.22C140.51 118.81 141.54 118.61 142.64 118.61C143.9 118.61 145.07 118.88 146.15 119.43C147.23 119.98 148.13 120.73 148.85 121.67C149.51 122.47 149.99 123.41 150.29 124.49C150.47 125.13 150.56 125.81 150.56 126.53C150.56 126.89 150.53 127.23 150.47 127.55H147.35H143.95V127.55ZM142.66 121.7C141.68 121.7 140.8 121.95 140.04 122.46C139.27 122.96 138.69 123.63 138.29 124.46H145.28H147.05C146.95 124.3 146.86 124.14 146.76 123.99C146.67 123.83 146.57 123.68 146.48 123.54C146.02 122.97 145.46 122.52 144.8 122.19C144.14 121.86 143.42 121.7 142.66 121.7Z" fill="white"/>
<path d="M164.77 119.76C165.45 120.2 166.03 120.76 166.51 121.43L164.77 122.81L164.11 123.35C163.77 122.85 163.32 122.45 162.78 122.15C162.23 121.85 161.63 121.7 161 121.7C160.48 121.7 159.99 121.8 159.53 122C159.07 122.2 158.67 122.47 158.33 122.81C157.99 123.15 157.72 123.55 157.52 124.01C157.32 124.47 157.22 124.96 157.22 125.48V131.18V134.39H154.13V125.46V118.61H157.22V119.76C157.76 119.4 158.35 119.11 158.99 118.91C159.63 118.71 160.3 118.61 161 118.61C161.7 118.61 162.37 118.71 163.01 118.91C163.63 119.12 164.23 119.4 164.77 119.76Z" fill="white"/>
<path d="M177.55 127.55H171.52C171.64 128.09 171.85 128.6 172.14 129.06C172.43 129.53 172.79 129.94 173.2 130.28C173.61 130.62 174.09 130.88 174.61 131.07C175.13 131.26 175.68 131.36 176.26 131.36C176.96 131.36 177.61 131.22 178.22 130.94C178.83 130.66 179.37 130.27 179.82 129.77H180.39H183.45C183.33 130.07 183.18 130.35 183.01 130.63C182.84 130.9 182.65 131.17 182.45 131.42C181.73 132.34 180.83 133.08 179.75 133.63C178.67 134.18 177.5 134.46 176.24 134.46C175.14 134.46 174.11 134.25 173.15 133.84C172.19 133.43 171.35 132.87 170.63 132.14C169.91 131.42 169.34 130.58 168.93 129.62C168.52 128.66 168.32 127.63 168.32 126.53C168.32 125.43 168.52 124.4 168.93 123.44C169.34 122.48 169.91 121.64 170.63 120.92C171.35 120.2 172.19 119.63 173.15 119.22C174.11 118.81 175.14 118.61 176.24 118.61C177.5 118.61 178.67 118.88 179.75 119.43C180.83 119.98 181.73 120.73 182.45 121.67C183.11 122.47 183.59 123.41 183.89 124.49C184.07 125.13 184.16 125.81 184.16 126.53C184.16 126.89 184.13 127.23 184.07 127.55H180.95H177.55V127.55ZM176.26 121.7C175.28 121.7 174.4 121.95 173.64 122.46C172.87 122.96 172.29 123.63 171.89 124.46H178.88H180.65C180.55 124.3 180.46 124.14 180.36 123.99C180.27 123.83 180.17 123.68 180.08 123.54C179.62 122.97 179.06 122.52 178.4 122.19C177.74 121.86 177.02 121.7 176.26 121.7Z" fill="white"/>
<path d="M203.55 126.53C203.55 127.63 203.34 128.66 202.94 129.62C202.53 130.58 201.96 131.42 201.24 132.14C200.52 132.86 199.68 133.43 198.72 133.84C197.76 134.25 196.73 134.46 195.63 134.46C194.53 134.46 193.5 134.25 192.54 133.84C191.58 133.43 190.74 132.87 190.02 132.14C189.3 131.42 188.73 130.58 188.32 129.62C187.91 128.66 187.71 127.63 187.71 126.53C187.71 125.43 187.91 124.4 188.32 123.44C188.73 122.48 189.3 121.64 190.02 120.92C190.74 120.2 191.58 119.63 192.54 119.22C193.5 118.81 194.53 118.61 195.63 118.61C196.55 118.61 197.42 118.75 198.22 119.04C199.03 119.33 199.78 119.74 200.46 120.25V108.01H203.55V126.49V126.53ZM195.65 131.36C196.31 131.36 196.93 131.24 197.52 130.98C198.11 130.72 198.62 130.38 199.06 129.94C199.5 129.5 199.84 128.98 200.09 128.4C200.34 127.81 200.46 127.18 200.46 126.52V126.49C200.46 125.83 200.34 125.21 200.09 124.63C199.84 124.05 199.5 123.54 199.06 123.1C198.62 122.66 198.11 122.31 197.52 122.06C196.93 121.81 196.31 121.68 195.65 121.68C194.99 121.68 194.37 121.8 193.78 122.06C193.19 122.31 192.68 122.65 192.24 123.1C191.8 123.54 191.45 124.06 191.19 124.64C190.93 125.22 190.8 125.85 190.8 126.52C190.8 127.18 190.93 127.81 191.19 128.4C191.45 128.99 191.8 129.5 192.24 129.94C192.68 130.38 193.19 130.73 193.78 130.98C194.37 131.24 194.99 131.36 195.65 131.36Z" fill="white"/>
<path d="M224.1 131.3H233.07V134.45H220.89V112.43H224.1V131.3Z" fill="white"/>
<path d="M248.67 122.78C249.55 123.26 250.25 123.95 250.78 124.86C251.31 125.77 251.57 126.81 251.57 127.96C251.57 128.84 251.4 129.67 251.06 130.45C250.72 131.23 250.26 131.91 249.68 132.5C249.1 133.09 248.42 133.56 247.64 133.9C246.86 134.24 246.03 134.41 245.15 134.41H236.6V112.42H245C245.8 112.42 246.56 112.58 247.28 112.88C248 113.19 248.63 113.62 249.17 114.16C249.71 114.7 250.13 115.33 250.44 116.05C250.75 116.77 250.91 117.54 250.91 118.36C250.91 118.86 250.84 119.32 250.71 119.75C250.58 120.18 250.41 120.58 250.2 120.94C249.99 121.3 249.75 121.64 249.48 121.94C249.23 122.27 248.95 122.54 248.67 122.78ZM239.82 121.52H244.47C244.91 121.52 245.32 121.44 245.7 121.28C246.08 121.12 246.41 120.9 246.69 120.6C246.97 120.31 247.19 119.98 247.35 119.6C247.51 119.22 247.59 118.82 247.59 118.4C247.59 117.5 247.29 116.75 246.69 116.15C246.09 115.55 245.35 115.25 244.47 115.25H239.82V121.52V121.52ZM244.62 131.6C245.12 131.6 245.59 131.5 246.03 131.3C246.47 131.1 246.86 130.84 247.18 130.51C247.51 130.18 247.77 129.8 247.96 129.35C248.15 128.91 248.24 128.45 248.24 127.97C248.24 127.47 248.15 127 247.96 126.56C247.77 126.12 247.51 125.74 247.18 125.41C246.85 125.08 246.46 124.82 246.03 124.63C245.59 124.44 245.12 124.34 244.62 124.34H239.82V131.6H244.62V131.6Z" fill="white"/>
<path d="M270.24 122.39C269.96 123.29 269.54 124.1 268.99 124.82C268.44 125.54 267.76 126.15 266.96 126.65C266.92 126.69 266.87 126.72 266.81 126.74C266.75 126.76 266.7 126.79 266.66 126.83L270.53 134.45H267.14L263.75 127.82C263.51 127.86 263.27 127.89 263.03 127.91C262.79 127.93 262.53 127.94 262.25 127.94H258.32V134.45H255.11V112.4H262.25C263.15 112.4 263.99 112.52 264.78 112.74C265.57 112.96 266.29 113.28 266.95 113.68C268.57 114.7 269.66 116.13 270.22 117.97C270.24 118.07 270.26 118.16 270.28 118.24C270.3 118.32 270.33 118.41 270.37 118.51V118.66C270.41 118.76 270.44 118.87 270.45 118.99C270.46 119.11 270.48 119.23 270.49 119.35V119.38C270.51 119.5 270.52 119.62 270.52 119.74C270.52 119.86 270.52 119.99 270.52 120.13V120.16V120.22C270.52 120.34 270.52 120.46 270.52 120.58C270.52 120.7 270.51 120.82 270.49 120.94V121C270.47 121.12 270.45 121.24 270.45 121.34C270.44 121.45 270.41 121.57 270.37 121.69V121.81C270.31 122.04 270.26 122.23 270.24 122.39ZM262.65 125.12C263.31 125.12 263.91 124.98 264.46 124.7C265.01 124.42 265.49 124.05 265.89 123.59C266.29 123.13 266.61 122.6 266.85 122.01C267.09 121.42 267.21 120.8 267.21 120.16C267.21 119.54 267.09 118.93 266.85 118.32C266.61 117.72 266.29 117.19 265.89 116.74C265.49 116.29 265.01 115.92 264.46 115.64C263.91 115.36 263.31 115.22 262.65 115.22H258.36V125.12H262.65V125.12Z" fill="white"/>
<path d="M290.91 112.4V112.43L284.1 127.25V134.45V134.48H280.89V134.45V127.25L274.08 112.43H274.11L274.08 112.4H277.65L282.51 124.16L287.37 112.4H290.91Z" fill="white"/>
<path d="M308.25 112.43H311.46V134.42H308.25V112.43Z" fill="white"/>
<path d="M325.75 119.63C326.7 120.27 327.45 121.1 328.01 122.1C328.56 123.11 328.84 124.22 328.84 125.42V134.45H325.75V131.18V125.42C325.75 124.9 325.65 124.41 325.45 123.93C325.25 123.46 324.97 123.05 324.62 122.7C324.27 122.35 323.86 122.08 323.4 121.87C322.94 121.67 322.45 121.57 321.92 121.57C321.39 121.57 320.9 121.67 320.43 121.87C319.96 122.07 319.55 122.35 319.21 122.7C318.87 123.05 318.6 123.46 318.4 123.93C318.2 124.4 318.1 124.9 318.1 125.42V131.18V134.45H315V125.42V118.49H318.09L318.11 119.63C318.65 119.27 319.25 118.99 319.9 118.79C320.55 118.59 321.23 118.49 321.93 118.49C322.63 118.49 323.31 118.59 323.96 118.79C324.61 118.99 325.2 119.27 325.75 119.63Z" fill="white"/>
<path d="M341.13 127.1C341.41 127.5 341.63 127.94 341.79 128.41C341.95 128.88 342.02 129.37 342 129.89C341.98 130.55 341.83 131.16 341.55 131.72C341.27 132.28 340.9 132.77 340.44 133.19C339.98 133.61 339.44 133.94 338.82 134.17C338.2 134.4 337.55 134.5 336.87 134.48C335.91 134.44 335.04 134.19 334.28 133.73C333.51 133.27 332.92 132.66 332.53 131.9L335.11 130.1C335.21 130.46 335.43 130.77 335.77 131.02C336.11 131.27 336.51 131.4 336.97 131.4C337.49 131.42 337.95 131.28 338.34 130.97C338.73 130.66 338.92 130.27 338.92 129.81C338.92 129.67 338.91 129.57 338.89 129.51V129.45C338.87 129.41 338.86 129.38 338.86 129.35C338.86 129.32 338.85 129.29 338.83 129.24C338.69 128.9 338.43 128.62 338.05 128.4C337.99 128.36 337.9 128.32 337.78 128.28L337.81 128.25C337.53 128.09 337.22 127.93 336.88 127.79C336.54 127.64 336.19 127.48 335.84 127.31C335.49 127.14 335.14 126.96 334.8 126.77C334.46 126.58 334.14 126.35 333.84 126.1V126.07L333.75 125.98C333.33 125.58 333 125.11 332.76 124.58C332.52 124.05 332.41 123.49 332.43 122.89C332.45 122.29 332.59 121.73 332.85 121.21C333.11 120.69 333.45 120.24 333.89 119.86C334.32 119.48 334.82 119.18 335.4 118.96C335.98 118.74 336.6 118.64 337.26 118.66C338.12 118.7 338.89 118.91 339.58 119.29C340.27 119.67 340.82 120.18 341.22 120.82L338.7 122.56C338.58 122.34 338.39 122.15 338.12 121.99C337.85 121.83 337.54 121.75 337.2 121.75C336.74 121.73 336.35 121.84 336.02 122.08C335.69 122.32 335.52 122.61 335.52 122.95C335.52 123.05 335.53 123.12 335.55 123.16C335.59 123.38 335.69 123.57 335.85 123.73C336.01 123.89 336.21 124.02 336.45 124.12C336.83 124.34 337.26 124.57 337.75 124.8C338.24 125.03 338.74 125.28 339.24 125.56C339.6 125.72 339.96 125.95 340.32 126.25L340.47 126.4C340.67 126.56 340.89 126.8 341.13 127.1Z" fill="white"/>
<path d="M354.45 117.29H351.57V134.45H348.48V117.29H345.57V114.2H348.48V108.32H351.57V114.2H354.45V117.29Z" fill="white"/>
<path d="M365.54 118.61C366.64 118.61 367.67 118.82 368.64 119.24C369.61 119.66 370.46 120.23 371.19 120.95C371.92 121.67 372.49 122.52 372.91 123.5C373.33 124.48 373.54 125.52 373.54 126.62V134.39H370.45V132.94C369.77 133.46 369.02 133.86 368.2 134.16C367.38 134.46 366.5 134.61 365.56 134.61C364.46 134.61 363.43 134.4 362.46 133.98C361.49 133.56 360.64 132.99 359.92 132.27C359.2 131.55 358.63 130.71 358.21 129.73C357.79 128.76 357.58 127.73 357.58 126.63C357.58 125.53 357.79 124.49 358.21 123.51C358.63 122.53 359.2 121.68 359.92 120.96C360.64 120.24 361.48 119.67 362.45 119.25C363.41 118.82 364.44 118.61 365.54 118.61ZM365.55 131.51C366.23 131.51 366.87 131.38 367.46 131.12C368.05 130.86 368.56 130.51 369 130.06C369.44 129.62 369.79 129.1 370.05 128.51C370.31 127.92 370.44 127.29 370.44 126.62C370.44 125.94 370.31 125.3 370.05 124.7C369.79 124.11 369.44 123.59 369 123.14C368.56 122.7 368.04 122.35 367.46 122.08C366.87 121.82 366.23 121.69 365.55 121.69C364.87 121.69 364.23 121.82 363.65 122.08C363.06 122.34 362.54 122.69 362.11 123.14C361.67 123.58 361.32 124.1 361.06 124.7C360.8 125.29 360.67 125.93 360.67 126.62C360.67 127.28 360.8 127.91 361.06 128.51C361.32 129.1 361.67 129.62 362.11 130.06C362.55 130.5 363.06 130.86 363.65 131.12C364.24 131.38 364.87 131.51 365.55 131.51Z" fill="white"/>
<path d="M387.84 119.63C388.79 120.27 389.54 121.1 390.1 122.1C390.65 123.11 390.93 124.22 390.93 125.42V134.45H387.84V131.18V125.42C387.84 124.9 387.74 124.41 387.54 123.93C387.34 123.46 387.06 123.05 386.71 122.7C386.36 122.35 385.95 122.08 385.49 121.87C385.03 121.67 384.54 121.57 384.01 121.57C383.48 121.57 382.99 121.67 382.52 121.87C382.05 122.07 381.64 122.35 381.3 122.7C380.96 123.05 380.69 123.46 380.49 123.93C380.29 124.4 380.19 124.9 380.19 125.42V131.18V134.45H377.1V125.42V118.49H380.19L380.21 119.63C380.75 119.27 381.35 118.99 382 118.79C382.65 118.59 383.33 118.49 384.03 118.49C384.73 118.49 385.41 118.59 386.06 118.79C386.71 118.99 387.3 119.27 387.84 119.63Z" fill="white"/>
<path d="M402.42 131.33C403.2 131.33 403.92 131.16 404.58 130.82C405.24 130.48 405.79 130.03 406.23 129.47L408.66 131.36C407.94 132.29 407.04 133.03 405.96 133.59C404.88 134.15 403.7 134.42 402.42 134.42C401.34 134.42 400.32 134.21 399.36 133.8C398.4 133.39 397.56 132.83 396.83 132.11C396.1 131.39 395.53 130.55 395.12 129.59C394.71 128.63 394.5 127.6 394.5 126.5C394.5 125.42 394.71 124.4 395.12 123.44C395.53 122.48 396.1 121.64 396.83 120.92C397.56 120.2 398.4 119.63 399.36 119.22C400.32 118.81 401.34 118.61 402.42 118.61C403.7 118.61 404.88 118.88 405.96 119.43C407.04 119.98 407.94 120.72 408.66 121.67L406.23 123.56C405.79 122.98 405.24 122.53 404.58 122.2C403.92 121.87 403.2 121.7 402.42 121.7C401.76 121.7 401.13 121.82 400.54 122.07C399.95 122.32 399.43 122.66 399 123.1C398.56 123.54 398.21 124.05 397.96 124.64C397.71 125.23 397.58 125.85 397.58 126.5C397.58 127.15 397.7 127.78 397.96 128.36C398.21 128.95 398.55 129.46 399 129.9C399.44 130.34 399.96 130.69 400.54 130.94C401.12 131.19 401.76 131.33 402.42 131.33Z" fill="white"/>
<path d="M421.38 127.55H415.35C415.47 128.09 415.68 128.6 415.97 129.06C416.26 129.53 416.61 129.94 417.04 130.28C417.46 130.62 417.93 130.88 418.45 131.07C418.97 131.26 419.52 131.36 420.1 131.36C420.8 131.36 421.45 131.22 422.06 130.94C422.67 130.66 423.2 130.27 423.67 129.77H424.24H427.3C427.18 130.07 427.03 130.35 426.86 130.63C426.69 130.9 426.5 131.17 426.31 131.42C425.59 132.34 424.69 133.08 423.61 133.63C422.53 134.18 421.36 134.46 420.1 134.46C419 134.46 417.97 134.25 417.01 133.84C416.05 133.43 415.21 132.87 414.49 132.14C413.77 131.42 413.2 130.58 412.8 129.62C412.39 128.66 412.18 127.63 412.18 126.53C412.18 125.43 412.39 124.4 412.8 123.44C413.21 122.48 413.77 121.64 414.49 120.92C415.21 120.2 416.05 119.63 417.01 119.22C417.97 118.81 419 118.61 420.1 118.61C421.36 118.61 422.53 118.88 423.61 119.43C424.69 119.98 425.59 120.73 426.31 121.67C426.97 122.47 427.45 123.41 427.75 124.49C427.93 125.13 428.02 125.81 428.02 126.53C428.02 126.89 427.99 127.23 427.93 127.55H424.81H421.38V127.55ZM420.09 121.7C419.11 121.7 418.23 121.95 417.47 122.46C416.7 122.96 416.11 123.63 415.72 124.46H422.71H424.48C424.38 124.3 424.28 124.14 424.19 123.99C424.1 123.84 424.01 123.68 423.9 123.54C423.44 122.97 422.88 122.52 422.22 122.19C421.56 121.86 420.85 121.7 420.09 121.7Z" fill="white"/>
<path d="M479.01 135.92C476.25 135.92 474.01 133.68 474.01 130.92C474.01 129.91 474.01 127.21 514.54 4.35002C515.41 1.73002 518.24 0.300023 520.85 1.17002C523.47 2.03002 524.9 4.86002 524.03 7.48002C508.55 54.39 485.23 125.78 483.97 131.52C483.68 134 481.57 135.92 479.01 135.92Z" fill="white"/>
<path d="M455.35 135.92C452.59 135.92 450.35 133.68 450.35 130.92C450.35 129.91 450.35 127.21 490.88 4.35002C491.75 1.73002 494.57 0.300023 497.19 1.17002C499.81 2.04002 501.24 4.86002 500.37 7.48002C484.9 54.39 461.58 125.78 460.31 131.52C460.02 134 457.91 135.92 455.35 135.92Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="Layer_1"
data-name="Layer 1"
viewBox="0 0 103.3 103.3"
version="1.1"
sodipodi:docname="Logo_Textless_Vector.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
width="103.3"
height="103.3">
<metadata
id="metadata340">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>odysee_</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1025"
id="namedview338"
showgrid="false"
showguides="false"
inkscape:zoom="3.18"
inkscape:cx="191"
inkscape:cy="51.650002"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<defs
id="defs293">
<style
id="style278">.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#fff;}.cls-3{fill:none;}.cls-4{fill:#f9f9f9;}</style>
<linearGradient
id="linear-gradient"
x1="37.900002"
y1="5.54"
x2="110.84"
y2="180.14999"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-9,-8.3499985)">
<stop
offset="0"
stop-color="#ef1970"
id="stop280" />
<stop
offset="0.14"
stop-color="#f23b5c"
id="stop282" />
<stop
offset="0.44"
stop-color="#f77d35"
id="stop284" />
<stop
offset="0.7"
stop-color="#fcad18"
id="stop286" />
<stop
offset="0.89"
stop-color="#fecb07"
id="stop288" />
<stop
offset="1"
stop-color="#ffd600"
id="stop290" />
</linearGradient>
</defs>
<title
id="title295">odysee_</title>
<circle
class="cls-1"
cx="51.650002"
cy="51.650002"
r="51.650002"
id="circle297"
style="fill:url(#linear-gradient)" />
<path
class="cls-2"
d="m 11.92,38.220002 a 0.95,0.95 0 1 0 -0.3,1.31 0.95,0.95 0 0 0 0.3,-1.31"
id="path299" />
<path
class="cls-2"
d="m 67.44,13.370002 a 0.95,0.95 0 1 0 -0.3,1.31 0.95,0.95 0 0 0 0.3,-1.31"
id="path301" />
<path
class="cls-2"
d="m 78.91,50.650002 a 1.11,1.11 0 1 0 1.33,-0.84 1.11,1.11 0 0 0 -1.33,0.84"
id="path303" />
<path
class="cls-2"
d="m 62.35,87.650002 a 0.86,0.86 0 1 0 1,-0.65 0.86,0.86 0 0 0 -1,0.65"
id="path305" />
<path
class="cls-2"
d="m 19.18,21.160002 a 0.52,0.52 0 1 0 0.63,-0.39 0.52,0.52 0 0 0 -0.63,0.39"
id="path307" />
<path
class="cls-2"
d="m 21.86,69.960002 a 0.73,0.73 0 1 0 -0.59,0.85 0.73,0.73 0 0 0 0.59,-0.85"
id="path309" />
<path
class="cls-3"
d="m 43.75,10.140002 c 0,0 -8.16,2.24 -7.53,10.89 0.56,7.67 4.65,11.85 13.14,8.65 8.49,-3.2 9.93,-5.45 7.85,-11.85 -2.08,-6.4 -4.49,-10.7300005 -13.46,-7.69 z"
id="path311" />
<path
class="cls-2"
d="m 91.45,83.650002 c -0.32,-0.6 -6.45,-10 -7.21,-17.9 -0.56,-5.47 -7.71,-11.54 -12,-14.73 a 3.11,3.11 0 0 1 -0.24,-4.75 c 4.23,-4 11.69,-11.8 14.05,-15.92 a 31.3,31.3 0 0 0 3.44,-13.89 51.89,51.89 0 0 0 -9.82,-8.2200005 c -3.48,1.72 -4.42,7.0700005 -5.95,13.3000005 -2.08,8.49 -7,7.53 -9,7.53 -2,0 -0.8,-3 -5.45,-16.34 C 54.62,-0.60999847 42.53,2.7300015 33.34,8.2300015 21.66,15.230002 26.87,30.160002 29.76,39.780002 c -1.64,1.58 -7.81,2.81 -13.42,5.83 -6.95,3.74 -14.06,9.75 -15.91,12.51 a 51.33,51.33 0 0 0 2.62,11 5.89,5.89 0 0 0 1.38,0.95 c 3.29,1.53 8.13,-1.09 12.71,-5.84 a 23.33,23.33 0 0 1 4.57,-3.53 48.94,48.94 0 0 1 11.77,-5.53 c 0,0 4.49,6.89 8.65,15.06 4.16,8.17 -4.49,10.89 -5.45,10.89 -0.96,0 -14.59,-1.27 -11.55,10.26 3.04,11.529998 19.7,7.37 28.19,1.76 8.49,-5.61 6.41,-23.87 6.41,-23.87 8.33,-1.28 10.89,7.53 11.69,12 0.8,4.47 -1,12.33 7.37,12.5 a 10.48,10.48 0 0 0 3.47,-0.54 51.94,51.94 0 0 0 8.74,-8.17 2.88,2.88 0 0 0 0.45,-1.41 z m -42.09,-54 c -8.49,3.2 -12.58,-1 -13.14,-8.65 -0.63,-8.65 7.53,-10.89 7.53,-10.89 9,-3.0000005 11.37,1.28 13.46,7.69 2.09,6.41 0.64,8.68 -7.85,11.85 z"
id="path313" />
<polygon
class="cls-2"
points="97.44,50.39 96.27,48.07 93.72,47.54 96.04,46.37 96.56,43.82 97.74,46.14 100.29,46.66 97.97,47.84 "
id="polygon315"
transform="translate(-9,-8.3499985)" />
<path
class="cls-4"
d="m 54.25,19.360002 a 5.41,5.41 0 0 1 0.38,3.6"
id="path329" />
<path
class="cls-2"
d="m 54.63,24.060002 h -0.21 a 1.09,1.09 0 0 1 -0.86,-1.27 4.36,4.36 0 0 0 -0.31,-3 1.09,1.09 0 0 1 2,-0.84 6.46,6.46 0 0 1 0.44,4.23 1.09,1.09 0 0 1 -1.06,0.88 z"
id="path331" />
<path
class="cls-4"
d="m 51.56,13.330002 a 6.14,6.14 0 0 1 0.81,1.24"
id="path333" />
<path
class="cls-2"
d="m 52.36,15.650002 a 1.09,1.09 0 0 1 -1,-0.56 6.71,6.71 0 0 0 -0.64,-1 1.1,1.1 0 0 1 0,-1.52 1.07,1.07 0 0 1 1.49,0 6.8,6.8 0 0 1 1,1.49 1.09,1.09 0 0 1 -0.85,1.59 z"
id="path335" />
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -0,0 +1 @@
<svg data-v-423bf9ae="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 422.10743801652893 60" class="font"><!----><!----><!----><g data-v-423bf9ae="" id="c68be61a-7a4c-407d-aedb-150928d6ca02" fill="black" transform="matrix(6.347107410430908,0,0,6.347107410430908,0.2727353870868683,0.0743802934885025)" style="--darkreader-inline-fill:#000000;" data-darkreader-inline-fill=""><path d="M4.02 0.41C3.98 0.18 3.75 0 3.53 0L3.22 0C3.00 0 2.77 0.18 2.73 0.41L2.00 4.66L1.36 0.42C1.32 0.18 1.09 0 0.85 0L0.45 0C0.13 0-0.08 0.27-0.04 0.57L1.33 9.02C1.37 9.24 1.58 9.44 1.82 9.44L2.13 9.44C2.35 9.44 2.58 9.25 2.62 9.03L3.37 4.56L4.12 9.03C4.16 9.25 4.38 9.44 4.61 9.44L4.93 9.44C5.17 9.44 5.39 9.24 5.43 9.02L6.78 0.57C6.82 0.27 6.58 0 6.27 0L5.87 0C5.63 0 5.39 0.18 5.36 0.42L4.73 4.63ZM11.66 9.04C11.72 9.25 11.94 9.44 12.15 9.44L12.56 9.44C12.89 9.44 13.12 9.14 13.05 8.82L11.12 0.39C11.07 0.18 10.84 0 10.63 0L10.32 0C10.11 0 9.88 0.18 9.83 0.39L8.55 5.87L7.88 8.83C7.81 9.16 8.04 9.44 8.37 9.44L8.78 9.44C8.99 9.44 9.21 9.25 9.27 9.04L9.76 6.86L11.19 6.86ZM10.47 3.23C10.74 4.42 10.71 4.80 10.86 5.47L10.08 5.47ZM12.60 0.88C12.60 1.16 12.82 1.39 13.10 1.39L14.24 1.39L14.24 8.92C14.24 9.20 14.46 9.42 14.74 9.42L15.12 9.42C15.40 9.42 15.62 9.20 15.62 8.92L15.62 1.39L16.77 1.39C17.05 1.39 17.28 1.16 17.28 0.88L17.28 0.49C17.28 0.21 17.05-0.01 16.77-0.01L13.10-0.01C12.82-0.01 12.60 0.21 12.60 0.49ZM17.58 6.69C17.58 7.80 17.89 8.44 18.33 8.88C18.79 9.32 19.36 9.44 19.89 9.44C20.40 9.44 20.99 9.32 21.45 8.88C21.88 8.44 22.18 7.80 22.18 6.69L22.18 6.50C22.18 6.22 21.95 5.99 21.69 5.99L21.28 5.99C21 5.99 20.78 6.22 20.78 6.50L20.78 6.69C20.78 7.20 20.71 7.57 20.54 7.80C20.41 7.94 20.16 8.05 19.89 8.05C19.29 8.05 19.00 7.63 19.00 6.69L19.00 2.73C19.00 1.79 19.31 1.39 19.89 1.39C20.47 1.39 20.78 1.79 20.78 2.73L20.78 2.93C20.78 3.21 21 3.43 21.28 3.43L21.69 3.43C21.95 3.43 22.18 3.21 22.18 2.93L22.18 2.73C22.18 1.67 21.88 0.97 21.43 0.55C20.97 0.13 20.41 0 19.89 0C19.36 0 18.80 0.13 18.34 0.55C17.89 0.97 17.58 1.67 17.58 2.73ZM24.53 4.03L24.53 0.50C24.53 0.22 24.30 0 24.02 0L23.63 0C23.35 0 23.13 0.22 23.13 0.50L23.13 8.93C23.13 9.21 23.35 9.44 23.63 9.44L24.02 9.44C24.30 9.44 24.53 9.21 24.53 8.93L24.53 5.42L26.08 5.42L26.08 8.93C26.08 9.21 26.31 9.44 26.59 9.44L26.98 9.44C27.26 9.44 27.48 9.21 27.48 8.93L27.48 0.50C27.48 0.22 27.26 0 26.98 0L26.59 0C26.31 0 26.08 0.22 26.08 0.50L26.08 4.03ZM31.58 6.69C31.58 7.80 31.89 8.44 32.33 8.88C32.79 9.32 33.38 9.44 33.88 9.44C34.40 9.44 34.99 9.32 35.45 8.88C35.88 8.44 36.18 7.80 36.18 6.69L36.18 2.73C36.18 0.95 35.42 0 33.88 0C32.35 0 31.58 0.97 31.58 2.73ZM32.98 2.73C32.98 1.82 33.26 1.39 33.88 1.39C34.48 1.39 34.78 1.79 34.78 2.73L34.78 6.69C34.78 7.64 34.47 8.05 33.88 8.05C33.28 8.05 32.98 7.62 32.98 6.69ZM41.40 9.44C41.68 9.44 41.90 9.21 41.90 8.93L41.90 0.50C41.90 0.22 41.68 0 41.40 0L41.01 0C40.73 0 40.50 0.22 40.50 0.50L40.50 5.12L38.56 0.31C38.50 0.15 38.28 0 38.09 0L37.76 0C37.49 0 37.25 0.22 37.25 0.50L37.25 8.93C37.25 9.21 37.49 9.44 37.76 9.44L38.16 9.44C38.43 9.44 38.67 9.21 38.67 8.93L38.67 4.31L40.61 9.13C40.67 9.28 40.91 9.44 41.08 9.44ZM49.84 8.55C49.84 8.27 49.62 8.05 49.34 8.05L47.25 8.05L47.25 0.50C47.25 0.22 47.03 0 46.76 0L46.35 0C46.07 0 45.85 0.22 45.85 0.50L45.85 8.93C45.85 9.21 46.07 9.44 46.35 9.44L49.34 9.44C49.62 9.44 49.84 9.21 49.84 8.93ZM52.61 9.44C54.25 9.44 54.98 8.54 54.98 6.73C54.98 5.88 54.74 5.17 54.38 4.72L54.38 4.72C54.38 4.72 54.98 4.00 54.98 2.70C54.98 0.90 54.25 0 52.61 0L51.23 0C50.95 0 50.72 0.22 50.72 0.50L50.72 8.93C50.72 9.21 50.95 9.44 51.23 9.44ZM52.61 1.39C53.30 1.39 53.58 1.76 53.58 2.70C53.58 3.64 53.30 4.02 52.61 4.02L52.12 4.02L52.12 1.39ZM52.61 5.40C53.30 5.40 53.58 5.78 53.58 6.73C53.58 7.67 53.30 8.05 52.61 8.05L52.12 8.05L52.12 5.40ZM60.55 6.58C60.55 5.85 60.35 5.28 59.95 4.84C59.99 4.80 60.02 4.77 60.06 4.73C60.49 4.30 60.65 3.60 60.65 2.73C60.65 0.85 60.05-0.01 58.32-0.01L56.56-0.01C56.28-0.01 56.06 0.21 56.06 0.49L56.06 8.92C56.06 9.20 56.28 9.42 56.56 9.42L56.95 9.42C57.22 9.42 57.44 9.20 57.44 8.92L57.44 5.49L58.07 5.49C58.76 5.49 59.15 5.88 59.15 6.58L59.15 8.92C59.15 9.20 59.37 9.42 59.65 9.42L60.05 9.42C60.33 9.42 60.55 9.20 60.55 8.92ZM57.44 1.39L58.32 1.39C59.05 1.39 59.26 1.78 59.26 2.73C59.26 3.68 58.86 4.07 58.07 4.07L57.44 4.07ZM62.78 0.29C62.72 0.15 62.48 0 62.31 0L61.87 0C61.47 0 61.25 0.35 61.40 0.70C62.01 2.10 62.62 3.49 63.22 4.89L63.22 8.93C63.22 9.21 63.45 9.44 63.71 9.44L64.11 9.44C64.39 9.44 64.61 9.21 64.61 8.93L64.61 4.90L66.42 0.70C66.56 0.35 66.33 0 65.95 0L65.49 0C65.34 0 65.10 0.15 65.03 0.29L63.91 2.87Z"></path></g><!----><!----></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1 @@
<svg data-v-423bf9ae="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 422.10743801652893 60" class="font"><!----><!----><!----><g data-v-423bf9ae="" id="7ded7196-cb33-41a9-a3d2-8b6b37ca65cf" fill="white" transform="matrix(6.347107410430908,0,0,6.347107410430908,0.2727353870868683,0.0743802934885025)" style="--darkreader-inline-fill:#181a1b;" data-darkreader-inline-fill=""><path d="M4.02 0.41C3.98 0.18 3.75 0 3.53 0L3.22 0C3.00 0 2.77 0.18 2.73 0.41L2.00 4.66L1.36 0.42C1.32 0.18 1.09 0 0.85 0L0.45 0C0.13 0-0.08 0.27-0.04 0.57L1.33 9.02C1.37 9.24 1.58 9.44 1.82 9.44L2.13 9.44C2.35 9.44 2.58 9.25 2.62 9.03L3.37 4.56L4.12 9.03C4.16 9.25 4.38 9.44 4.61 9.44L4.93 9.44C5.17 9.44 5.39 9.24 5.43 9.02L6.78 0.57C6.82 0.27 6.58 0 6.27 0L5.87 0C5.63 0 5.39 0.18 5.36 0.42L4.73 4.63ZM11.66 9.04C11.72 9.25 11.94 9.44 12.15 9.44L12.56 9.44C12.89 9.44 13.12 9.14 13.05 8.82L11.12 0.39C11.07 0.18 10.84 0 10.63 0L10.32 0C10.11 0 9.88 0.18 9.83 0.39L8.55 5.87L7.88 8.83C7.81 9.16 8.04 9.44 8.37 9.44L8.78 9.44C8.99 9.44 9.21 9.25 9.27 9.04L9.76 6.86L11.19 6.86ZM10.47 3.23C10.74 4.42 10.71 4.80 10.86 5.47L10.08 5.47ZM12.60 0.88C12.60 1.16 12.82 1.39 13.10 1.39L14.24 1.39L14.24 8.92C14.24 9.20 14.46 9.42 14.74 9.42L15.12 9.42C15.40 9.42 15.62 9.20 15.62 8.92L15.62 1.39L16.77 1.39C17.05 1.39 17.28 1.16 17.28 0.88L17.28 0.49C17.28 0.21 17.05-0.01 16.77-0.01L13.10-0.01C12.82-0.01 12.60 0.21 12.60 0.49ZM17.58 6.69C17.58 7.80 17.89 8.44 18.33 8.88C18.79 9.32 19.36 9.44 19.89 9.44C20.40 9.44 20.99 9.32 21.45 8.88C21.88 8.44 22.18 7.80 22.18 6.69L22.18 6.50C22.18 6.22 21.95 5.99 21.69 5.99L21.28 5.99C21 5.99 20.78 6.22 20.78 6.50L20.78 6.69C20.78 7.20 20.71 7.57 20.54 7.80C20.41 7.94 20.16 8.05 19.89 8.05C19.29 8.05 19.00 7.63 19.00 6.69L19.00 2.73C19.00 1.79 19.31 1.39 19.89 1.39C20.47 1.39 20.78 1.79 20.78 2.73L20.78 2.93C20.78 3.21 21 3.43 21.28 3.43L21.69 3.43C21.95 3.43 22.18 3.21 22.18 2.93L22.18 2.73C22.18 1.67 21.88 0.97 21.43 0.55C20.97 0.13 20.41 0 19.89 0C19.36 0 18.80 0.13 18.34 0.55C17.89 0.97 17.58 1.67 17.58 2.73ZM24.53 4.03L24.53 0.50C24.53 0.22 24.30 0 24.02 0L23.63 0C23.35 0 23.13 0.22 23.13 0.50L23.13 8.93C23.13 9.21 23.35 9.44 23.63 9.44L24.02 9.44C24.30 9.44 24.53 9.21 24.53 8.93L24.53 5.42L26.08 5.42L26.08 8.93C26.08 9.21 26.31 9.44 26.59 9.44L26.98 9.44C27.26 9.44 27.48 9.21 27.48 8.93L27.48 0.50C27.48 0.22 27.26 0 26.98 0L26.59 0C26.31 0 26.08 0.22 26.08 0.50L26.08 4.03ZM31.58 6.69C31.58 7.80 31.89 8.44 32.33 8.88C32.79 9.32 33.38 9.44 33.88 9.44C34.40 9.44 34.99 9.32 35.45 8.88C35.88 8.44 36.18 7.80 36.18 6.69L36.18 2.73C36.18 0.95 35.42 0 33.88 0C32.35 0 31.58 0.97 31.58 2.73ZM32.98 2.73C32.98 1.82 33.26 1.39 33.88 1.39C34.48 1.39 34.78 1.79 34.78 2.73L34.78 6.69C34.78 7.64 34.47 8.05 33.88 8.05C33.28 8.05 32.98 7.62 32.98 6.69ZM41.40 9.44C41.68 9.44 41.90 9.21 41.90 8.93L41.90 0.50C41.90 0.22 41.68 0 41.40 0L41.01 0C40.73 0 40.50 0.22 40.50 0.50L40.50 5.12L38.56 0.31C38.50 0.15 38.28 0 38.09 0L37.76 0C37.49 0 37.25 0.22 37.25 0.50L37.25 8.93C37.25 9.21 37.49 9.44 37.76 9.44L38.16 9.44C38.43 9.44 38.67 9.21 38.67 8.93L38.67 4.31L40.61 9.13C40.67 9.28 40.91 9.44 41.08 9.44ZM49.84 8.55C49.84 8.27 49.62 8.05 49.34 8.05L47.25 8.05L47.25 0.50C47.25 0.22 47.03 0 46.76 0L46.35 0C46.07 0 45.85 0.22 45.85 0.50L45.85 8.93C45.85 9.21 46.07 9.44 46.35 9.44L49.34 9.44C49.62 9.44 49.84 9.21 49.84 8.93ZM52.61 9.44C54.25 9.44 54.98 8.54 54.98 6.73C54.98 5.88 54.74 5.17 54.38 4.72L54.38 4.72C54.38 4.72 54.98 4.00 54.98 2.70C54.98 0.90 54.25 0 52.61 0L51.23 0C50.95 0 50.72 0.22 50.72 0.50L50.72 8.93C50.72 9.21 50.95 9.44 51.23 9.44ZM52.61 1.39C53.30 1.39 53.58 1.76 53.58 2.70C53.58 3.64 53.30 4.02 52.61 4.02L52.12 4.02L52.12 1.39ZM52.61 5.40C53.30 5.40 53.58 5.78 53.58 6.73C53.58 7.67 53.30 8.05 52.61 8.05L52.12 8.05L52.12 5.40ZM60.55 6.58C60.55 5.85 60.35 5.28 59.95 4.84C59.99 4.80 60.02 4.77 60.06 4.73C60.49 4.30 60.65 3.60 60.65 2.73C60.65 0.85 60.05-0.01 58.32-0.01L56.56-0.01C56.28-0.01 56.06 0.21 56.06 0.49L56.06 8.92C56.06 9.20 56.28 9.42 56.56 9.42L56.95 9.42C57.22 9.42 57.44 9.20 57.44 8.92L57.44 5.49L58.07 5.49C58.76 5.49 59.15 5.88 59.15 6.58L59.15 8.92C59.15 9.20 59.37 9.42 59.65 9.42L60.05 9.42C60.33 9.42 60.55 9.20 60.55 8.92ZM57.44 1.39L58.32 1.39C59.05 1.39 59.26 1.78 59.26 2.73C59.26 3.68 58.86 4.07 58.07 4.07L57.44 4.07ZM62.78 0.29C62.72 0.15 62.48 0 62.31 0L61.87 0C61.47 0 61.25 0.35 61.40 0.70C62.01 2.10 62.62 3.49 63.22 4.89L63.22 8.93C63.22 9.21 63.45 9.44 63.71 9.44L64.11 9.44C64.39 9.44 64.61 9.21 64.61 8.93L64.61 4.90L66.42 0.70C66.56 0.35 66.33 0 65.95 0L65.49 0C65.34 0 65.10 0.15 65.03 0.29L63.91 2.87Z"></path></g><!----><!----></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,158 @@
:root {
--color-master: #499375;
--color-slave: #43889d;
--color-error: rgb(245, 81, 69);
--color-gradient-0: linear-gradient(130deg, var(--color-master), var(--color-slave));
--color-gradient-1: linear-gradient(130deg, #ff7a18, #af002d 41.07%, #319197 76.05%);
--color-dark: #0e1117;
--color-light: rgb(235, 237, 241);
--gradient-animation: gradient-animation 5s linear infinite alternate;
}
body {
font-size: .75rem;
font-family: Arial, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, sans-serif;
letter-spacing: .2ch;
}
body#page {
--root-smallest-font-size: .95rem;
--root-font-size-relative-to-screen: 1;
font-size: max(var(--root-smallest-font-size), calc(min(1vw, 2vh) * var(--root-font-size-relative-to-screen)));
}
body {
background: linear-gradient(to left top, var(--color-master), var(--color-slave));
background-attachment: fixed;
color: var(--color-light);
padding: 0;
margin: 0;
}
body::before {
content: "";
position: fixed;
inset: 0;
background: rgba(19, 19, 19, 0.75);
}
*,
*::before,
*::after {
box-sizing: border-box;
position: relative;
}
a {
cursor: pointer;
}
p,
ul,
ol,
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}
pre {
margin: 0;
white-space: break-spaces;
}
.options {
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fit, minmax(3em, 1fr));
justify-content: center;
gap: .25em;
}
.button {
display: inline-flex;
place-items: center;
place-content: center;
text-align: center;
padding: .5em 1em;
background: var(--color-dark);
color: var(--color-light);
border-radius: .5em;
border: unset;
font: inherit;
cursor: pointer;
}
.button.active {
background: var(--color-gradient-0);
}
.button.active::before {
content: "";
position: absolute;
inset: 0;
background: var(--color-gradient-0);
filter: blur(.5em);
z-index: -1;
border-radius: .5em;
}
.button.disabled {
filter: saturate(0);
pointer-events: none;
}
.filled {
background: var(--color-gradient-0);
background-clip: text;
-webkit-background-clip: text;
font-weight: bold;
color: transparent;
}
.error {
display: grid;
grid-auto-flow: column;
gap: .5em;
align-items: center;
justify-content: start;
color: var(--color-error);
}
.error::before {
content: "!";
width: 2em;
aspect-ratio: 1/1;
display: grid;
place-items: center;
letter-spacing: 0;
line-height: 0;
border: .1em solid currentColor;
border-radius: 100000vw;
font-weight: bold;
}
.overlay {
display: grid;
place-items: center;
position: fixed;
inset: 0;
font-size: 2em;
font-weight: bold;
}
.overlay::before {
content: '';
position: absolute;
inset: 0;
background-color: #0e1117;
opacity: .75;
}

View file

@ -1,16 +0,0 @@
@import '../style'
.ButtonRadio
display: flex
justify-content: center
.radio-button
@extend .button
margin: 6px
.radio-button.checked
@extend .button.active
input[type="radio"]
opacity: 0
position: absolute

View file

@ -1,31 +0,0 @@
import { h } from 'preact';
import classnames from 'classnames';
import './ButtonRadio.sass';
export interface SelectionOption {
value: string
display: string
}
export interface ButtonRadioProps<T extends string | SelectionOption = string> {
name?: string;
onChange(redirect: string): void;
value: T extends SelectionOption ? T['value'] : T;
options: T[];
}
const getAttr = (x: string | SelectionOption, key: keyof SelectionOption): string => typeof x === 'string' ? x : x[key];
export default function ButtonRadio<T extends string | SelectionOption = string>({ name = 'buttonRadio', onChange, options, value }: ButtonRadioProps<T>) {
/** If it's a string, return the string, if it's a SelectionOption get the selection option property */
return <div className='ButtonRadio'>
{options.map(o => ({ o: getAttr(o, 'value'), display: getAttr(o, 'display') })).map(({ o, display }) =>
<div key={o} className={classnames('radio-button', { 'checked': value === o })}
onClick={() => o !== value && onChange(o)}>
<input name={name} value={o} type='radio' checked={value === o} />
<label>{display}</label>
</div>
)}
</div>;
}

View file

@ -1,29 +0,0 @@
import { appRedirectUrl, parseProtocolUrl } from './lbry-url';
describe('web url parsing', () => {
const testCases: [string, string | undefined][] = [
['https://lbry.tv/@test:7/foo-123:7', 'lbry://@test:7/foo-123:7'],
['https://lbry.tv/@test1:c/foo:8', 'lbry://@test1:c/foo:8'],
['https://lbry.tv/@test1:0/foo-bar-2-baz-7:e#adasasddasdas123', 'lbry://@test1:0/foo-bar-2-baz-7:e'],
['https://lbry.tv/@test:7', 'lbry://@test:7'],
['https://lbry.tv/@test:c', 'lbry://@test:c'],
['https://lbry.tv/$/discover?t=foo%20bar', undefined],
['https://lbry.tv/$/signup?redirect=/@test1:0/foo-bar-2-baz-7:e#adasasddasdas123', undefined],
];
test.each(testCases)('redirect %s', (url, expected) => {
expect(appRedirectUrl(url)).toEqual(expected);
});
});
describe('app url parsing', () => {
const testCases: Array<[string, string[]]> = [
['test', ['test']],
['@test', ['@test']],
['lbry://@test$1/stuff', ['@test$1', 'stuff']],
];
test.each(testCases)('redirect %s', (url, expected) => {
expect(parseProtocolUrl(url)).toEqual(expected);
});
});

View file

@ -1,142 +0,0 @@
// Port of https://github.com/lbryio/lbry-sdk/blob/master/lbry/schema/url.py
interface UrlOptions {
/**
* Whether or not to encodeURIComponent the path segments.
* Doing so is a workaround such that browsers interpret it as a valid URL in a way that the desktop app understands.
*/
encode?: boolean
}
const invalidNamesRegex = /[^=&#:$@%*?;\"/\\<>%{}|^~`\[\]\u0000-\u0020\uD800-\uDFFF\uFFFE-\uFFFF]+/.source;
/** Creates a named regex group */
const named = (name: string, regex: string) => `(?<${name}>${regex})`;
/** Creates a non-capturing group */
const group = (regex: string) => `(?:${regex})`;
/** Allows for one of the patterns */
const oneOf = (...choices: string[]) => group(choices.join('|'));
/** Create an lbry url claim */
const claim = (name: string, prefix = '') => group(
named(`${name}_name`, prefix + invalidNamesRegex)
+ oneOf(
group(':' + named(`${name}_claim_id`, '[0-9a-f]{1,40}')),
group('\\*' + named(`${name}_sequence`, '[1-9][0-9]*')),
group('\\$' + named(`${name}_amount_order`, '[1-9][0-9]*'))
) + '?'
);
/** Create an lbry url claim, but use the old pattern for claims */
const legacyClaim = (name: string, prefix = '') => group(
named(`${name}_name`, prefix + invalidNamesRegex)
+ oneOf(
group('#' + named(`${name}_claim_id`, '[0-9a-f]{1,40}')),
group(':' + named(`${name}_sequence`, '[1-9][0-9]*')),
group('\\$' + named(`${name}_amount_order`, '[1-9][0-9]*'))
) + '?');
export const builder = { named, group, oneOf, claim, legacyClaim, invalidNamesRegex };
/** Creates a pattern to parse lbry protocol URLs. Unused, but I left it here. */
function createProtocolUrlRegex(legacy = false) {
const claim = legacy ? builder.legacyClaim : builder.claim;
return new RegExp('^' + named('scheme', 'lbry://') + '?' + oneOf(
group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')),
claim('channel', '@'),
claim('stream'),
) + '$');
}
/** Creates a pattern to match lbry.tv style sites by their pathname */
function createWebUrlRegex(legacy = false) {
const claim = legacy ? builder.legacyClaim : builder.claim;
return new RegExp('^/' + oneOf(
group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')),
claim('channel', '@'),
claim('stream'),
) + '$');
}
/** Pattern for lbry.tv style sites */
export const URL_REGEX = createWebUrlRegex();
export const PROTOCOL_URL_REGEX = createProtocolUrlRegex();
const PROTOCOL_URL_REGEX_LEGACY = createProtocolUrlRegex(true);
/**
* Encapsulates a lbry url path segment.
* Handles `StreamClaimNameAndModifier' and `ChannelClaimNameAndModifier`
*/
export class PathSegment {
constructor(public name: string,
public claimID?: string,
public sequence?: number,
public amountOrder?: number) { }
static fromMatchGroup(segment: string, groups: Record<string, string>) {
return new PathSegment(
groups[`${segment}_name`],
groups[`${segment}_claim_id`],
parseInt(groups[`${segment}_sequence`]),
parseInt(groups[`${segment}_amount_order`])
);
}
/** Prints the segment */
toString() {
if (this.claimID) return `${this.name}:${this.claimID}`;
if (this.sequence) return `${this.name}*${this.sequence}`;
if (this.amountOrder) return `${this.name}$${this.amountOrder}`;
return this.name;
}
}
/**
* Utility function
*
* @param ptn pattern to use; specific to the patterns defined in this file
* @param url the url to try to parse
* @returns an array of path segments; if invalid, will return an empty array
*/
function patternSegmenter(ptn: RegExp, url: string, options: UrlOptions = { encode: false }): string[] {
const match = url.match(ptn)?.groups;
if (!match) return [];
const segments = match['channel_name'] ? ['channel']
: match['channel_with_stream_name'] ? ['channel_with_stream', 'stream_in_channel']
: match['stream_name'] ? ['stream']
: null;
if (!segments) throw new Error(`${url} matched the overall pattern, but could not determine type`);
return segments.map(s => PathSegment.fromMatchGroup(s, match).toString())
.map(s => options.encode ? encodeURIComponent(s) : s);
}
/**
* Produces the lbry protocl URL from the frontend URL
*
* @param url lbry frontend URL
* @param options options for the redirect
*/
export function appRedirectUrl(url: string, options?: UrlOptions): string | undefined {
const segments = patternSegmenter(URL_REGEX, new URL(url).pathname, options);
if (segments.length === 0) return;
const path = segments.join('/');
return `lbry://${path}`;
}
/**
* Parses a lbry protocol and returns its constituent path segments. Attempts the spec compliant and then the old URL schemes.
*
* @param url the lbry url
* @returns an array of path segments; if invalid, will return an empty array
*/
export function parseProtocolUrl(url: string, options: UrlOptions = { encode: false }): string[] {
for (const ptn of [PROTOCOL_URL_REGEX, PROTOCOL_URL_REGEX_LEGACY]) {
const segments = patternSegmenter(ptn, url, options);
if (segments.length === 0) continue;
return segments;
}
return [];
}

View file

@ -1,16 +0,0 @@
export interface LbrySettings {
enabled: boolean
redirect: keyof typeof redirectDomains
}
export const DEFAULT_SETTINGS: LbrySettings = { enabled: true, redirect: 'lbry.tv' };
export const redirectDomains = {
'lbry.tv': { prefix: 'https://lbry.tv/', display: 'lbry.tv' },
odysee: { prefix: 'https://odysee.com/', display: 'odysee' },
app: { prefix: 'lbry://', display: 'App' },
};
export function getSettingsAsync<K extends Array<keyof LbrySettings>>(...keys: K): Promise<Pick<LbrySettings, K[number]>> {
return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any)));
}

View file

@ -1,34 +0,0 @@
$background-color: #191a1c !default
$text-color: whitesmoke !default
$btn-color: #075656 !default
$btn-select: teal !default
body
width: 400px
text-align: center
background-color: $background-color
color: $text-color
font-family: sans-serif
.container
display: block
text-align: center
margin: 0 32px
margin-bottom: 15px
.button
border-radius: 5px
background-color: $btn-color
border: 4px solid $btn-color
color: $text-color
font-size: 0.8rem
font-weight: 400
padding: 4px 15px
text-align: center
&.active
border: 4px solid $btn-select
&:focus
outline: none

View file

@ -1,30 +0,0 @@
import { useReducer, useEffect } from 'preact/hooks';
import { DEFAULT_SETTINGS } from './settings';
/**
* A hook to read the settings from local storage
*
* @param initial the default value. Must have all relevant keys present and should not change
*/
export function useSettings<T extends object>(initial: T) {
const [state, dispatch] = useReducer((state, nstate: Partial<T>) => ({ ...state, ...nstate }), initial);
// register change listeners, gets current values, and cleans up the listeners on unload
useEffect(() => {
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, areaName: string) => {
if (areaName !== 'local') return;
const changeSet = Object.keys(changes)
.filter(k => Object.keys(initial).includes(k))
.map(k => [k, changes[k].newValue]);
if (changeSet.length === 0) return; // no changes; no use dispatching
dispatch(Object.fromEntries(changeSet));
};
chrome.storage.onChanged.addListener(changeListener);
chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial<T>));
return () => chrome.storage.onChanged.removeListener(changeListener);
}, []);
return state;
}
/** A hook to read watch on lbry settings from local storage */
export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS);

View file

@ -1,126 +0,0 @@
import chunk from 'lodash/chunk';
import groupBy from 'lodash/groupBy';
import pickBy from 'lodash/pickBy';
const LBRY_API_HOST = 'https://api.lbry.com';
const QUERY_CHUNK_SIZE = 300;
interface YtResolverResponse {
success: boolean;
error: object | null;
data: {
videos?: Record<string, string>;
channels?: Record<string, string>;
};
}
interface YtSubscription {
id: string;
etag: string;
title: string;
snippet: {
description: string;
resourceId: {
channelId: string;
};
};
}
/**
* @param file to load
* @returns a promise with the file as a string
*/
export function getFileContent(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', event => resolve(event.target?.result as string || ''));
reader.addEventListener('error', () => {
reader.abort();
reject(new DOMException(`Could not read ${file.name}`));
});
reader.readAsText(file);
});
}
export interface YTDescriptor {
id: string
type: 'channel' | 'video'
}
export const ytService = {
/**
* Reads the array of YT channels from an OPML file
*
* @param opmlContents an opml file as as tring
* @returns the channel IDs
*/
readOpml(opmlContents: string): string[] {
const opml = new DOMParser().parseFromString(opmlContents, 'application/xml');
return Array.from(opml.querySelectorAll('outline > outline'))
.map(outline => outline.getAttribute('xmlUrl'))
.filter((url): url is string => !!url)
.map(url => ytService.getChannelId(url))
.filter((url): url is string => !!url); // we don't want it if it's empty
},
/**
* Reads an array of YT channel IDs from the YT subscriptions JSON file
*
* @param jsonContents a JSON file as a string
* @returns the channel IDs
*/
readJson(jsonContents: string): string[] {
const subscriptions: YtSubscription[] = JSON.parse(jsonContents);
return subscriptions.map(sub => sub.snippet.resourceId.channelId);
},
/**
* Extracts the channelID from a YT URL.
*
* Handles these two types of YT URLs:
* * /feeds/videos.xml?channel_id=*
* * /channel/*
*/
getChannelId(channelURL: string) {
const match = channelURL.match(/channel\/([^\s?]*)/);
return match ? match[1] : new URL(channelURL).searchParams.get('channel_id');
},
/** Extracts the video ID from a YT URL */
getVideoId(url: string) {
const regex = /watch\/?\?.*v=([^\s&]*)/;
const match = url.match(regex);
return match ? match[1] : null; // match[1] is the videoId
},
getId(url: string): YTDescriptor | null {
const videoId = ytService.getVideoId(url);
if (videoId) return { id: videoId, type: 'video' };
const channelId = ytService.getChannelId(url);
if (channelId) return { id: channelId, type: 'channel' };
return null;
},
/**
* @param descriptors YT resource IDs to check
* @returns a promise with the list of channels that were found on lbry
*/
async resolveById(...descriptors: YTDescriptor[]): Promise<string[]> {
const descChunks = chunk(descriptors, QUERY_CHUNK_SIZE);
const responses: (YtResolverResponse | null)[] = await Promise.all(descChunks.map(descChunk => {
const groups = groupBy(descChunk, d => d.type);
const params = new URLSearchParams(pickBy({
video_ids: groups['video']?.map(s => s.id).join(','),
channel_ids: groups['channel']?.map(s => s.id).join(','),
}));
return fetch(`${LBRY_API_HOST}/yt/resolve?${params}`, { cache: 'force-cache' })
.then(rsp => rsp.ok ? rsp.json() : null);
}));
return responses.filter((rsp): rsp is YtResolverResponse => !!rsp)
.flatMap(rsp => [...Object.values(rsp.data.videos || {}), ...Object.values(rsp.data.channels || {})]) // flatten the results into a 1D array
.filter(s => s);
},
};

142
src/components/dialogs.tsx Normal file
View file

@ -0,0 +1,142 @@
import { h } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
type Message = string
type Alert = { type: 'alert' | 'prompt' | 'confirm', message: Message, resolve: (data: string | boolean | null) => void }
export type DialogManager = ReturnType<typeof createDialogManager>
export function createDialogManager() {
const [alerts, setAlerts] = useState({} as Record<string, Alert>)
const id = crypto.randomUUID()
function add(alert: Alert) {
setAlerts({ ...alerts, alert })
}
function remove() {
delete alerts[id]
setAlerts({ ...alerts })
}
return {
useAlerts() { return alerts },
async alert(message: Message) {
return await new Promise<void>((resolve) => add({
message, type: 'alert', resolve: () => {
resolve()
remove()
}
}))
},
async prompt(message: Message) {
return await new Promise<string | null>((resolve) => add({
message, type: 'prompt', resolve: (data) => {
resolve(data?.toString() ?? null)
remove()
}
}))
},
async confirm(message: Message) {
return await new Promise<boolean>((resolve) => add({
message, type: 'confirm', resolve: (data) => {
resolve(!!data)
remove()
}
}))
}
}
}
export function Dialogs(params: { manager: ReturnType<typeof createDialogManager> }) {
const alerts = params.manager.useAlerts()
let currentAlert = Object.values(alerts)[0]
if (!currentAlert) return <noscript></noscript>
const [value, setValue] = useState(null as Parameters<typeof currentAlert['resolve']>[0])
let cancelled = false
const dialog = useRef(null as any as HTMLDialogElement)
useEffect(() => {
if (!dialog.current) return
if (!dialog.current.open) dialog.current.showModal()
const onClose = () => currentAlert.resolve(null)
dialog.current.addEventListener('close', onClose)
return dialog.current.removeEventListener('close', onClose)
})
return <dialog class="alert-dialog" ref={dialog}>
<style>
{`
.alert-dialog
{
position: fixed;
border: none;
background: var(--color-dark);
color: var(--color-light);
margin-bottom: 0;
width: 100%;
max-width: unset;
padding: 1.5em;
}
.alert-dialog::before {
content: "";
display: block;
background: var(--color-gradient-0);
height: 0.1em;
width: 100%;
position: absolute;
left: 0;
top: 0;
}
.alert-dialog form {
display: grid;
gap: 2em
}
.alert-dialog form .fields {
display: grid;
gap: .5em
}
.alert-dialog form .fields pre {
font: inherit;
}
.alert-dialog form .actions {
display: flex;
gap: .5em;
font-size: 1.1em;
font-weight: bold;
}
.alert-dialog form .actions::before {
content: "";
flex-grow: 1111111111111;
}`}
</style>
<form method='dialog' onSubmit={(event) => {
event.preventDefault()
currentAlert.resolve(cancelled ? null : currentAlert.type === 'confirm' ? true : value)
}}>
<div class="fields">
<pre>{currentAlert.message}</pre>
{currentAlert.type === 'prompt' && <input type='text' onInput={(event) => setValue(event.currentTarget.value)} />}
</div>
<div class="actions">
{/* This is here to capture, return key */}
<button style="position:0;opacity:0;pointer-events:none"></button>
{currentAlert.type !== 'alert' && <button className='button' onClick={() => cancelled = true}>Cancel</button>}
<button className='button active'>
{
currentAlert.type === 'alert' ? 'Ok'
: currentAlert.type === 'confirm' ? 'Confirm'
: currentAlert.type === 'prompt' ? 'Apply'
: 'Ok'
}
</button>
</div>
</form>
</dialog>
}

4
src/global.d.ts vendored
View file

@ -1,4 +0,0 @@
declare module '*.md' {
var _: string;
export default _;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -1,45 +0,0 @@
{
"name": "Watch on LBRY",
"version": "1.7.0",
"permissions": [
"https://www.youtube.com/",
"https://invidio.us/channel/*",
"https://invidio.us/watch?v=*",
"https://api.lbry.com/*",
"https://lbry.tv/*",
"tabs",
"storage"
],
"content_scripts": [
{
"matches": [
"https://www.youtube.com/*"
],
"js": [
"scripts/ytContent.js"
]
}
],
"background": {
"scripts": [
"scripts/storageSetup.js",
"scripts/tabOnUpdated.js"
],
"persistent": false
},
"browser_action": {
"default_title": "Watch on LBRY",
"default_popup": "popup/popup.html"
},
"web_accessible_resources": [
"popup.html",
"tools/YTtoLBRY.html",
"icons/lbry-logo.svg"
],
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"manifest_version": 2
}

200
src/modules/crypto/index.ts Normal file
View file

@ -0,0 +1,200 @@
import path from 'path'
import type { DialogManager } from '../../components/dialogs'
import { getExtensionSettingsAsync, setExtensionSetting, ytUrlResolversSettings } from "../../settings"
import { getFileContent } from '../file'
async function generateKeys() {
const keys = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
// Consider using a 4096-bit key for systems that require long-term security
modulusLength: 384,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-1",
},
true,
["sign", "verify"]
)
return {
publicKey: await exportPublicKey(keys.publicKey),
privateKey: await exportPrivateKey(keys.privateKey)
}
}
async function exportPrivateKey(key: CryptoKey) {
const exported = await crypto.subtle.exportKey(
"pkcs8",
key
)
return Buffer.from(exported).toString('base64')
}
const publicKeyPrefix = `MEwwDQYJKoZIhvcNAQEBBQADOwAwOAIxA`
const publicKeySuffix = `IDAQAB` //`wIDAQAB` `WIDAQAB`
const publicKeyLength = 65
async function exportPublicKey(key: CryptoKey) {
const exported = await crypto.subtle.exportKey(
"spki",
key
)
const publicKey = Buffer.from(exported).toString('base64')
return publicKey.substring(publicKeyPrefix.length, publicKeyPrefix.length + publicKeyLength)
}
function importPrivateKey(base64: string) {
return crypto.subtle.importKey(
"pkcs8",
Buffer.from(base64, 'base64'),
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-1",
},
true,
["sign"]
)
}
export async function sign(data: string, privateKey: string) {
return Buffer.from(await crypto.subtle.sign(
{ name: "RSASSA-PKCS1-v1_5" },
await importPrivateKey(privateKey),
await crypto.subtle.digest({ name: 'SHA-1' }, Buffer.from(data))
)).toString('base64')
}
export function resetProfileSettings() {
setExtensionSetting('publicKey', null)
setExtensionSetting('privateKey', null)
}
async function apiRequest<T extends object>(method: 'GET' | 'POST', pathname: string, data: T) {
const settings = await getExtensionSettingsAsync()
const url = new URL(ytUrlResolversSettings.madiatorFinder.href)
url.pathname = path.join(url.pathname, pathname)
url.searchParams.set('data', JSON.stringify(data))
if (!settings.privateKey || !settings.publicKey)
throw new Error('There is no profile.')
url.searchParams.set('keys', JSON.stringify({
signature: await sign(url.searchParams.toString(), settings.privateKey!),
publicKey: settings.publicKey
}))
const respond = await fetch(url.href, { method })
if (respond.ok) return respond.json()
throw new Error((await respond.json()).message)
}
export async function generateProfileAndSetNickname(dialogManager: DialogManager, overwrite = false) {
let { publicKey, privateKey } = await getExtensionSettingsAsync()
let nickname
while (true) {
nickname = await dialogManager.prompt("Pick a nickname")
if (nickname) break
if (nickname === null) return
await dialogManager.alert("Invalid nickname")
}
try {
if (overwrite || !privateKey || !publicKey) {
resetProfileSettings()
await generateKeys().then((keys) => {
publicKey = keys.publicKey
privateKey = keys.privateKey
})
setExtensionSetting('publicKey', publicKey)
setExtensionSetting('privateKey', privateKey)
}
await apiRequest('POST', '/profile', { nickname })
await dialogManager.alert(`Your nickname has been set to ${nickname}`)
} catch (error: any) {
resetProfileSettings()
await dialogManager.alert(error.message)
}
}
export async function purgeProfile(dialogManager: DialogManager) {
try {
if (!await dialogManager.confirm("This will purge all of your online and offline profile data.\nStill wanna continue?")) return
await apiRequest('POST', '/profile/purge', {})
resetProfileSettings()
await dialogManager.alert(`Your profile has been purged`)
} catch (error: any) {
await dialogManager.alert(error.message)
}
}
export async function getProfile() {
let { publicKey, privateKey } = await getExtensionSettingsAsync()
return (await apiRequest('GET', '/profile', { publicKey })) as { nickname: string, score: number, publickKey: string }
}
export function friendlyPublicKey(publicKey: string | null) {
// This is copy paste of Madiator Finder's friendly public key
return `${publicKey?.substring(0, 32)}...`
}
function download(data: string, filename: string, type: string) {
const file = new Blob([data], { type: type })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
setTimeout(() => {
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
})
}
// Using callback here because there is no good solution for detecting cancel event
export function inputKeyFile(callback: (file: File | null) => void) {
const input = document.createElement("input")
input.type = 'file'
input.accept = '.wol-keys.json'
input.click()
input.addEventListener("change", () => callback(input.files?.[0] ?? null))
}
interface ExportedProfileKeysFile {
publicKey: string
privateKey: string
}
export async function exportProfileKeysAsFile() {
const { publicKey, privateKey } = await getExtensionSettingsAsync()
const json = JSON.stringify({
publicKey,
privateKey
})
download(json, `watch-on-lbry-profile-export-${friendlyPublicKey(publicKey)}.wol-keys.json`, 'application/json')
}
export async function importProfileKeysFromFile(dialogManager: DialogManager, file: File) {
try {
let settings = await getExtensionSettingsAsync()
if (settings.publicKey && !await dialogManager.confirm(
"This will overwrite your old keypair." +
"\nStill wanna continue?\n\n" +
"NOTE: Without keypair you can't purge your data online.\n" +
"So if you wish to purge, please use purging instead."
)) return false
const json = await getFileContent(file)
if (!json) return false
const { publicKey, privateKey } = JSON.parse(json) as ExportedProfileKeysFile
setExtensionSetting('publicKey', publicKey)
setExtensionSetting('privateKey', privateKey)
return true
} catch (error: any) {
await dialogManager.alert(error.message)
return false
}
}

15
src/modules/file/index.ts Normal file
View file

@ -0,0 +1,15 @@
/**
* @param file to load
* @returns a promise with the file as a string
*/
export function getFileContent(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.addEventListener('load', event => resolve(event.target?.result as string || ''))
reader.addEventListener('error', () => {
reader.abort()
reject(new DOMException(`Could not read ${file.name}`))
})
reader.readAsText(file)
})
}

83
src/modules/yt/index.ts Normal file
View file

@ -0,0 +1,83 @@
interface YtExportedJsonSubscription {
id: string
etag: string
title: string
snippet: {
description: string
resourceId: {
channelId: string
}
}
}
/**
* Reads the array of YT channels from an OPML file
*
* @param opmlContents an opml file as as tring
* @returns the channel IDs
*/
export function getSubsFromOpml(opmlContents: string): string[] {
const opml = new DOMParser().parseFromString(opmlContents, 'application/xml')
opmlContents = ''
return Array.from(opml.querySelectorAll('outline > outline'))
.map(outline => outline.getAttribute('xmlUrl'))
.filter((url): url is string => !!url)
.map(url => getChannelId(url))
.filter((url): url is string => !!url) // we don't want it if it's empty
}
/**
* Reads an array of YT channel IDs from the YT subscriptions JSON file
*
* @param jsonContents a JSON file as a string
* @returns the channel IDs
*/
export function getSubsFromJson(jsonContents: string): string[] {
const subscriptions: YtExportedJsonSubscription[] = JSON.parse(jsonContents)
jsonContents = ''
return subscriptions.map(sub => sub.snippet.resourceId.channelId)
}
/**
* Reads an array of YT channel IDs from the YT subscriptions CSV file
*
* @param csvContent a CSV file as a string
* @returns the channel IDs
*/
export function getSubsFromCsv(csvContent: string): string[] {
const rows = csvContent.split('\n')
csvContent = ''
return rows.slice(1).map((row) => row.substring(0, row.indexOf(',')))
}
/**
* Extracts the channelID from a YT URL.
*
* Handles these two types of YT URLs:
* * /feeds/videos.xml?channel_id=*
* * /channel/*
*/
export function getChannelId(channelURL: string) {
const match = channelURL.match(/channel\/([^\s?]*)/)
return match ? match[1] : new URL(channelURL).searchParams.get('channel_id')
}
export function parseYouTubeURLTimeString(timeString: string) {
const signs = timeString.replace(/[0-9]/g, '')
if (signs.length === 0) return null
const numbers = timeString.replace(/[^0-9]/g, '-').split('-')
let total = 0
for (let i = 0; i < signs.length; i++) {
let t = parseInt(numbers[i])
switch (signs[i]) {
case 'd': t *= 24
case 'h': t *= 60
case 'm': t *= 60
case 's': break
default: return null
}
total += t
}
return total
}

View file

@ -0,0 +1,82 @@
// This should only work in background
if (typeof chrome.extension === 'undefined') throw new Error("YT urlCache can only be accessed from extension windows and service-workers.")
let db = new Promise<IDBDatabase>((resolve, reject) => {
if (typeof self.indexedDB !== 'undefined') {
const openRequest = indexedDB.open("yt-url-resolver-cache")
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
openRequest.addEventListener('success', () => {
resolve(openRequest.result)
clearExpired()
}, { once: true })
}
else reject(`IndexedDB not supported`)
})
async function clearExpired() {
return new Promise<void>(async (resolve, reject) => {
const transaction = (await db).transaction("store", "readwrite")
const range = IDBKeyRange.upperBound(new Date())
const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range)
expireAtCursorRequest.addEventListener('error', () => reject(expireAtCursorRequest.error))
expireAtCursorRequest.addEventListener('success', () => {
try {
const expireCursor = expireAtCursorRequest.result
if (!expireCursor) return
expireCursor.delete()
expireCursor.continue()
resolve()
}
catch (ex) {
reject(ex)
}
})
})
}
async function clearAll() {
return await new Promise<void>(async (resolve, reject) => {
const store = (await db).transaction("store", "readwrite").objectStore("store")
if (!store) return resolve()
const request = store.clear()
request.addEventListener('success', () => resolve())
request.addEventListener('error', () => reject(request.error))
})
}
async function put(url: string | null, id: string): Promise<void> {
return await new Promise(async (resolve, reject) => {
const store = (await db).transaction("store", "readwrite").objectStore("store")
if (!store) return resolve()
const expireAt = !url ? new Date(Date.now() + 1 * 60 * 60 * 1000) : new Date(Date.now() + 15 * 24 * 60 * 60 * 1000)
const request = store.put({ value: url, expireAt }, id)
console.log('caching', id, url, 'until:', expireAt)
request.addEventListener('success', () => resolve())
request.addEventListener('error', () => reject(request.error))
})
}
// string means there is cache of lbrypathname
// null means there is cache of that id has no lbrypathname
// undefined means there is no cache
async function get(id: string): Promise<string | null | undefined> {
const response = (await new Promise(async (resolve, reject) => {
const store = (await db).transaction("store", "readonly").objectStore("store")
if (!store) return reject(`Can't find object store.`)
const request = store.get(id)
request.addEventListener('success', () => resolve(request.result))
request.addEventListener('error', () => reject(request.error))
}) as { value: string | null, expireAt: Date } | undefined)
if (response === undefined) return undefined
if (response.expireAt <= new Date()) {
await clearExpired()
return undefined
}
return response.value
}
export const lbryUrlCache = { put, get, clearAll }

View file

@ -0,0 +1,88 @@
import { chunk } from "lodash"
import path from "path"
import { getExtensionSettingsAsync, SourcePlatform, ytUrlResolversSettings } from "../../settings"
import { sign } from "../crypto"
import { lbryUrlCache } from "./urlCache"
const QUERY_CHUNK_SIZE = 100
export type ResolveUrlTypes = 'video' | 'channel'
export type YtUrlResolveItem = { type: ResolveUrlTypes, id: string }
type Results = Record<string, YtUrlResolveItem>
type Paramaters = YtUrlResolveItem[]
interface ApiResponse {
data: {
channels?: Record<string, string>
videos?: Record<string, string>
}
}
export async function resolveById(params: Paramaters, progressCallback?: (progress: number) => void): Promise<Results> {
const { urlResolver: urlResolverSettingName, privateKey, publicKey } = await getExtensionSettingsAsync()
const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName]
async function requestChunk(params: Paramaters) {
const results: Results = {}
// Check for cache first, add them to the results if there are any cache
// And remove them from the params, so we dont request for them
params = (await Promise.all(params.map(async (item) => {
const cachedLbryUrl = await lbryUrlCache.get(item.id)
// Cache can be null, if there is no lbry url yet
if (cachedLbryUrl !== undefined) {
// Null values shouldn't be in the results
if (cachedLbryUrl !== null) results[item.id] = { id: cachedLbryUrl, type: item.type }
return null
}
// No cache found
return item
}))).filter((o) => o) as Paramaters
if (params.length === 0) return results
const url = new URL(`${urlResolverSetting.href}`)
url.pathname = path.join(url.pathname, '/resolve')
url.searchParams.set('video_ids', params.filter((item) => item.type === 'video').map((item) => item.id).join(','))
url.searchParams.set('channel_ids', params.filter((item) => item.type === 'channel').map((item) => item.id).join(','))
if (urlResolverSetting.signRequest && publicKey && privateKey)
url.searchParams.set('keys', JSON.stringify({
signature: await sign(url.searchParams.toString(), privateKey),
publicKey
}))
const controller = new AbortController()
// 5 second timeout:
const timeoutId = setTimeout(() => controller.abort(), 15000)
const apiResponse = await fetch(url.toString(), { cache: 'no-store', signal: controller.signal })
clearTimeout(timeoutId)
if (apiResponse.ok) {
const response: ApiResponse = await apiResponse.json()
for (const item of params) {
const lbryUrl = (item.type === 'channel' ? response.data.channels : response.data.videos)?.[item.id]?.replaceAll('#', ':') ?? null
// we cache it no matter if its null or not
await lbryUrlCache.put(lbryUrl, item.id)
if (lbryUrl) results[item.id] = { id: lbryUrl, type: item.type }
}
}
return results
}
const results: Results = {}
const chunks = chunk(params, QUERY_CHUNK_SIZE)
let i = 0
if (progressCallback) progressCallback(0)
for (const chunk of chunks) {
if (progressCallback) progressCallback(++i / (chunks.length + 1))
Object.assign(results, await requestChunk(chunk))
}
if (progressCallback) progressCallback(1)
return results
}

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Subscription Converter</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="../../assets/styles/common.css" />
<script src="main.tsx" charset="utf-8" defer></script>
</head>
<body id="page">
<div id="root" />
</body>
</html>

View file

@ -0,0 +1,86 @@
import { h, render } from 'preact'
import { useState } from 'preact/hooks'
import { getFileContent } from '../../modules/file'
import { getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from '../../modules/yt'
import { resolveById } from '../../modules/yt/urlResolve'
import { targetPlatformSettings, useExtensionSettings } from '../../settings'
async function getSubscribedChannelIdsFromFile(file: File) {
const ext = file.name.split('.').pop()?.toLowerCase()
const content = await getFileContent(file)
switch (ext) {
case 'xml':
case 'opml':
return getSubsFromOpml(content)
case 'csv':
return getSubsFromCsv(content)
default:
return getSubsFromJson(content)
}
}
async function findChannels(channelIds: string[], progressCallback: Parameters<typeof resolveById>['1']) {
const resultItems = await resolveById(channelIds.map((channelId) => ({ id: channelId, type: 'channel' })), progressCallback)
return Object.values(resultItems).map((item) => item.id)
}
function Conversion() {
const [file, setFile] = useState(null as File | null)
const [progress, setProgress] = useState(0)
const [lbryChannelIds, setLbryChannels] = useState([] as Awaited<ReturnType<typeof findChannels>>)
const settings = useExtensionSettings()
let loading = progress > 0 && progress !== 1
return <div className='conversion'>
<form onSubmit={async (event) => {
event.preventDefault()
if (file) setLbryChannels(await findChannels(await getSubscribedChannelIdsFromFile(file), (progress) => setProgress(progress)))
}}
>
<div class="fields">
<label for="conversion-file">Select YouTube Subscriptions</label>
<input id="conversion-file" type='file' onChange={event => setFile(event.currentTarget.files?.length ? event.currentTarget.files[0] : null)} />
</div>
<div class="actions">
<button className={`button ${!file || progress > 0 ? '' : 'active'}`} disabled={!file || loading}>
{loading ? `${(progress * 100).toFixed(1)}%` : 'Start Conversion!'}
</button>
</div>
</form>
{
progress === 1 &&
<div class="results">
<b>Results:</b>
{
lbryChannelIds.length > 0
? lbryChannelIds.map((lbryChannelId) =>
<article class="result-item">
<a href={`${targetPlatformSettings[settings.targetPlatform].domainPrefix}${lbryChannelId}`}>{lbryChannelId}</a>
</article>)
: <span class="error">No Result</span>
}
</div>
}
</div>
}
function YTtoLBRY() {
return <main>
<Conversion />
<aside class="help">
<iframe allowFullScreen
src='https://odysee.com/$/embed/convert-subscriptions-from-YouTube-to-LBRY/36f3a010295afe1c55e91b63bcb2eabc028ec86c?r=8bgP4hEdbd9jwBJmhEaqP3dD75LzsUob' />
<section><h1 id="getting-your-subscription-data">Getting your subscription data</h1>
<ol>
<li>Go to <a href="https://takeout.google.com/settings/takeout" target='_blank'>https://takeout.google.com/settings/takeout</a></li>
<li>Deselect everything except <code>YouTube and YouTube Music</code> and within that only select <code>subscriptions</code></li>
<li>Go through the process and create the export</li>
<li>Once it's exported, open the archive and find <code>YouTube and YouTube Music/subscriptions/subscriptions.(json/csv/opml)</code> and upload it to the extension</li>
</ol>
</section>
</aside>
</main>
}
render(<YTtoLBRY />, document.getElementById('root')!)

View file

@ -0,0 +1,86 @@
main {
display: flex;
flex-wrap: wrap-reverse;
min-height: 100vh;
gap: 2em;
padding: 1em;
overflow-wrap: break-word;
align-items: start;
}
.conversion,
.help {
flex: 1;
min-width: 40em;
display: grid;
gap: 1em;
}
.conversion>*,
.help>* {
background-color: rgba(0, 0, 0, 0.5);
border-radius: .5em;
padding: .5em;
}
.help iframe {
width: 100%;
max-width: 100%;
aspect-ratio: 16/9;
max-height: 50vh;
border: none;
}
.conversion {
height: 100%;
}
.conversion form {
display: grid;
gap: 1em;
justify-items: center;
}
.conversion form .fields {
display: grid;
gap: .5em;
}
.conversion form .actions {
display: flex;
flex-wrap: wrap;
gap: .5em;
}
.conversion form .actions::after {
content: "";
flex-grow: 100000000000000000;
}
.conversion form button {
cursor: pointer;
}
.conversion form button:disabled {
cursor: not-allowed;
}
.conversion .results {
display: grid;
gap: .5em;
}
.help a,
.conversion .results a {
background: var(--color-gradient-0);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-weight: bold;
}
.conversion .results .result-item {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="../../assets/styles/common.css" />
<link rel="stylesheet" href="style.css" />
<script src="main.tsx" defer></script>
</head>
<body>
<div id="root" />
</body>
</html>

62
src/pages/import/main.tsx Normal file
View file

@ -0,0 +1,62 @@
import { h, render } from 'preact'
import { useState } from 'preact/hooks'
import { createDialogManager, Dialogs } from '../../components/dialogs'
import { importProfileKeysFromFile, inputKeyFile } from '../../modules/crypto'
export async function openImportPopup() {
const importPopupWindow = open(
'/pages/import/index.html',
'Import Profile',
[
`height=${Math.max(document.body.clientHeight, screen.height * .5)}`,
`width=${document.body.clientWidth}`,
`toolbar=0,menubar=0,location=0`,
`top=${screenY}`,
`left=${screenX}`
].join(','))
close()
importPopupWindow?.focus()
}
function ImportPage() {
const [loading, updateLoading] = useState(() => false)
async function loads<T>(operation: Promise<T>) {
try {
updateLoading(true)
await operation
} catch (error) {
console.error(error)
}
finally {
updateLoading(false)
}
}
function importProfile() {
inputKeyFile(async (file) => file && await loads(
importProfileKeysFromFile(dialogManager, file)
.then((success) => success && (location.pathname = '/pages/popup/index.html'))
))
}
const dialogManager = createDialogManager()
return <div id='popup'>
<Dialogs manager={dialogManager} />
<main>
<section>
<label>Import your profile</label>
<p>Import your unique keypair.</p>
<div className='options'>
<a onClick={() => importProfile()} className={`button`}>Import</a>
</div>
</section>
</main>
{loading && <div class="overlay">
<span>Loading...</span>
</div>}
</div>
}
render(<ImportPage />, document.getElementById('root')!)

View file

@ -0,0 +1,35 @@
header {
display: grid;
gap: .5em;
padding: .75em;
position: sticky;
top: 0;
background: rgba(19, 19, 19, 0.5);
justify-items: center;
}
main {
display: grid;
gap: 2em;
padding: 1.5em 0.5em;
}
section {
display: grid;
justify-items: center;
text-align: center;
gap: .75em;
}
section label {
font-size: 1.75em;
font-weight: bold;
text-align: center;
}
#popup {
width: 35em;
max-width: 100%;
overflow: hidden;
margin: auto;
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="../../assets/styles/common.css" />
<link rel="stylesheet" href="style.css" />
<script src="main.tsx" defer></script>
</head>
<body>
<div id="root" />
</body>
</html>

236
src/pages/popup/main.tsx Normal file
View file

@ -0,0 +1,236 @@
import { h, render } from 'preact'
import { useState } from 'preact/hooks'
import { createDialogManager, Dialogs } from '../../components/dialogs'
import { exportProfileKeysAsFile, friendlyPublicKey, generateProfileAndSetNickname, getProfile, purgeProfile, resetProfileSettings } from '../../modules/crypto'
import { lbryUrlCache } from '../../modules/yt/urlCache'
import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, setExtensionSetting, useExtensionSettings } from '../../settings'
import { openImportPopup } from '../import/main'
/** Gets all the options for redirect destinations as selection options */
const targetPlatforms = getTargetPlatfromSettingsEntiries()
const ytUrlResolverOptions = getYtUrlResolversSettingsEntiries()
function WatchOnLbryPopup(params: { profile: Awaited<ReturnType<typeof getProfile>> | null }) {
const { targetPlatform, urlResolver, redirectChannel, redirectVideo, redirectVideoPlaylist, buttonVideoSub, buttonChannelSub, buttonVideoPlayer, privateKey, publicKey } = useExtensionSettings()
let [loading, updateLoading] = useState(() => false)
let [route, updateRoute] = useState<string>(() => '')
const dialogManager = createDialogManager()
const nickname = params.profile ? params.profile.nickname ?? 'No Nickname' : '...'
async function loads<T>(operation: Promise<T>) {
try {
updateLoading(true)
await operation
} catch (error) {
console.error(error)
}
finally {
updateLoading(false)
}
}
return <div id='popup'>
<Dialogs manager={dialogManager} />
{
<header>
{
publicKey &&
<section>
<label>{nickname}</label>
<p>{friendlyPublicKey(publicKey)}</p>
<span><b>Score: {params.profile?.score ?? '...'}</b> - <a target='_blank' href="https://finder.madiator.com/leaderboard" class="filled">🔗Leaderboard</a></span>
{urlResolver !== 'madiatorFinder' && <span class="error">You need to use Madiator Finder API for scoring to work</span>}
</section>
}
{
route !== ''
?
<section>
<a onClick={() => updateRoute('')} className="filled"> Back</a>
</section>
:
<section>
<a className='filled' onClick={() => updateRoute('profile')}>Profile Settings</a>
</section>
}
</header>
}
{
route === 'profile' ?
publicKey ?
<main>
<section>
<div className='options'>
<a onClick={() => loads(generateProfileAndSetNickname(dialogManager)).then(() => renderPopup())} className={`button active`}>
Change Nickname
</a>
<a onClick={async () => {
if (!await dialogManager.confirm("This will delete your keypair from this device."
+ "\nStill wanna continue?"
+ "\n\nNOTE: Without keypair you can't purge your data online."
+ "\nSo if you wish to purge, please use purging instead.")) return
resetProfileSettings()
renderPopup()
}}
className={`button`}
>
Forget/Logout
</a>
</div>
</section>
<section>
<label>Backup your account</label>
<p>Import and export your unique keypair.</p>
<div className='options'>
<a onClick={() => exportProfileKeysAsFile()} className={`button active`}>
Export
</a>
<a onClick={() => openImportPopup()}
className={`button`}
>
Import
</a>
</div>
</section>
<section>
<label>Purge your profile and data!</label>
<p>Purge your profile data online and offline.</p>
<div className='options'>
<div className="center">
<span className='filled'>(°° </span>
</div>
<a onClick={() => loads(purgeProfile(dialogManager)).then(() => renderPopup())} className={`button`}>
Purge Everything!!
</a>
</div>
</section>
<section>
<label>Generate new profile</label>
<p>Generate a new keypair.</p>
<div className='options'>
<a onClick={async () => await dialogManager.confirm("This will overwrite your old keypair.\nStill wanna continue?\n\nNOTE: Without keypair you can't purge your data online.\nSo if you wish to purge, please use purging instead.")
&& loads(generateProfileAndSetNickname(dialogManager, true)).then(() => renderPopup())
}
className={`button`}
>
Generate New Account
</a>
</div>
</section>
</main>
:
<main>
<section>
<label>You don't have a profile.</label>
<p>You can either import keypair for an existing profile or generate a new profile keypair.</p>
<div className='options'>
<a onClick={() => openImportPopup()} className={`button`}>
Import
</a>
<a onClick={() => loads(generateProfileAndSetNickname(dialogManager)).then(() => renderPopup())} className={`button active`}>
Generate
</a>
</div>
</section>
</main>
:
route === 'advanced' ?
<main>
<section>
<label>Which platform you would like to redirect to?</label>
<div className='options'>
{targetPlatforms.map(([name, value]) =>
<a onClick={() => setExtensionSetting('targetPlatform', name)} className={`button ${targetPlatform === name ? 'active' : ''}`}>
{value.displayName}
</a>
)}
</div>
</section>
<section>
<label>Which resolver API you want to use?</label>
<div className='options'>
{ytUrlResolverOptions.map(([name, value]) =>
<a onClick={() => setExtensionSetting('urlResolver', name)} className={`button ${urlResolver === name ? 'active' : ''}`}>
{value.name}
</a>
)}
</div>
<a onClick={() => loads(lbryUrlCache.clearAll().then(() => dialogManager.alert("Cleared Cache!")))} className={`button active`}>
Clear Resolver Cache
</a>
</section>
</main>
:
<main>
<section>
<label>Auto redirect when:</label>
<div className='options'>
<div class="toggle-option">
<span>Playing a video</span>
<a onClick={() => setExtensionSetting('redirectVideo', !redirectVideo)} className={`button ${redirectVideo ? 'active' : ''}`}>
{redirectVideo ? 'Active' : 'Deactive'}
</a>
</div>
<div class="toggle-option">
<span>Playing a playlist</span>
<a onClick={() => setExtensionSetting('redirectVideoPlaylist', !redirectVideoPlaylist)} className={`button ${redirectVideoPlaylist ? 'active' : ''}`}>
{redirectVideoPlaylist ? 'Active' : 'Deactive'}
</a>
</div>
<div class="toggle-option">
<span>Viewing a channel</span>
<a onClick={() => setExtensionSetting('redirectChannel', !redirectChannel)} className={`button ${redirectChannel ? 'active' : ''}`}>
{redirectChannel ? 'Active' : 'Deactive'}
</a>
</div>
</div>
</section>
<section>
<label>Show redirect button on:</label>
<div className='options'>
<div className="toggle-option">
<span>Video Page</span>
<a onClick={() => setExtensionSetting('buttonVideoSub', !buttonVideoSub)} className={`button ${buttonVideoSub ? 'active' : ''}`}>
{buttonVideoSub ? 'Active' : 'Deactive'}
</a>
</div>
<div className="toggle-option">
<span>Channel Page</span>
<a onClick={() => setExtensionSetting('buttonChannelSub', !buttonChannelSub)} className={`button ${buttonChannelSub ? 'active' : ''}`}>
{buttonChannelSub ? 'Active' : 'Deactive'}
</a>
</div>
<div className="toggle-option">
<span>Video Player</span>
<a onClick={() => setExtensionSetting('buttonVideoPlayer', !buttonVideoPlayer)} className={`button ${buttonVideoPlayer ? 'active' : ''}`}>
{buttonVideoPlayer ? 'Active' : 'Deactive'}
</a>
</div>
</div>
</section>
<section>
<label>Tools</label>
<a target='_blank' href='/pages/YTtoLBRY/index.html' className={`filled`}>
Subscription Converter
</a>
<a className='filled' onClick={() => updateRoute('advanced')}>Advanced Settings</a>
</section>
</main>
}
{loading && <div class="overlay">
<span>Loading...</span>
</div>}
</div>
}
function renderPopup() {
render(<WatchOnLbryPopup profile={null} />, document.getElementById('root')!)
getProfile().then((profile) => render(<WatchOnLbryPopup profile={profile} />, document.getElementById('root')!))
}
renderPopup()

55
src/pages/popup/style.css Normal file
View file

@ -0,0 +1,55 @@
#popup {
width: 40em;
overflow: hidden;
margin: auto;
}
header {
display: grid;
gap: .5em;
padding: .75em;
position: sticky;
top: 0;
background: rgba(19, 19, 19, 0.5);
justify-items: center;
}
main {
display: grid;
gap: 2em;
padding: 1.5em 0.5em;
}
section {
display: grid;
justify-items: center;
text-align: center;
gap: .75em;
}
label {
font-size: 1.75em;
font-weight: bold;
text-align: center;
}
section>* {
padding: 0 1rem;
}
.center {
display: grid;
place-items: center;
}
.left {
display: grid;
justify-items: start;
align-items: center;
text-align: left;
}
.toggle-option {
display: grid;
gap: .5em;
}

View file

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="popup.tsx" defer></script>
</head>
<body>
<div class="container">
<div id="root" />
</div>
</body>
</html>

View file

@ -1,4 +0,0 @@
.radio-label
font-size: 1.1rem
margin: 15px auto
display: block

View file

@ -1,33 +0,0 @@
import { h, render } from 'preact';
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio';
import { redirectDomains } from '../common/settings';
import { useLbrySettings } from '../common/useSettings';
import './popup.sass';
/** Utilty to set a setting in the browser */
const setSetting = (setting: string, value: any) => chrome.storage.local.set({ [setting]: value });
/** Gets all the options for redirect destinations as selection options */
const redirectOptions: SelectionOption[] = Object.entries(redirectDomains)
.map(([value, { display }]) => ({ value, display }));
function WatchOnLbryPopup() {
const { enabled, redirect } = useLbrySettings();
return <div className='container'>
<label className='radio-label'>Enable Redirection:</label>
<ButtonRadio value={enabled ? 'YES' : 'NO'} options={['YES', 'NO']}
onChange={enabled => setSetting('enabled', enabled.toLowerCase() === 'yes')} />
<label className='radio-label'>Where would you like to redirect?</label>
<ButtonRadio value={redirect as string} options={redirectOptions}
onChange={redirect => setSetting('redirect', redirect)} />
<label className='radio-label'>Other useful tools:</label>
<a href='/tools/YTtoLBRY.html' target='_blank'>
<button type='button' className='btn1 button is-primary'>Subscriptions Converter</button>
</a>
</div>;
}
render(<WatchOnLbryPopup />, document.getElementById('root')!);

36
src/scripts/background.ts Normal file
View file

@ -0,0 +1,36 @@
import { resolveById } from "../modules/yt/urlResolve"
const onGoingLbryPathnameRequest: Record<string, ReturnType<typeof resolveById>> = {}
chrome.runtime.onMessage.addListener(({ method, data }, sender, sendResponse) => {
function resolve(result: Awaited<ReturnType<typeof resolveById>>) {
sendResponse(JSON.stringify(result))
}
(async () => {
switch (method) {
case 'openTab':
{
const { href }: { href: string } = JSON.parse(data)
chrome.tabs.create({ url: href, active: sender.tab?.active, index: sender.tab ? sender.tab.index + 1 : undefined })
}
break
case 'resolveUrl':
try {
const params: Parameters<typeof resolveById> = JSON.parse(data)
// Don't create a new Promise for same ID until on going one is over.
const promise = onGoingLbryPathnameRequest[data] ?? (onGoingLbryPathnameRequest[data] = resolveById(...params))
resolve(await promise)
} catch (error) {
sendResponse(`error: ${(error as any).toString()}`)
console.error(error)
}
finally {
delete onGoingLbryPathnameRequest[data]
}
break
}
})()
return true
})

View file

@ -1,28 +0,0 @@
import { DEFAULT_SETTINGS, LbrySettings, getSettingsAsync } from '../common/settings';
/** Reset settings to default value and update the browser badge text */
async function initSettings() {
const settings = await getSettingsAsync(...Object.keys(DEFAULT_SETTINGS) as Array<keyof LbrySettings>);
// get all the values that aren't set and use them as a change set
const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof LbrySettings, LbrySettings[keyof LbrySettings]]>)
.filter(([k]) => settings[k] === null || settings[k] === undefined);
// fix our local var and set it in storage for later
if (invalidEntries.length > 0) {
const changeSet = Object.fromEntries(invalidEntries);
Object.assign(settings, changeSet);
chrome.storage.local.set(changeSet);
}
chrome.browserAction.setBadgeText({ text: settings.enabled ? 'ON' : 'OFF' });
}
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local' || !changes.enabled) return;
chrome.browserAction.setBadgeText({ text: changes.enabled.newValue ? 'ON' : 'OFF' });
});
chrome.runtime.onStartup.addListener(initSettings);
chrome.runtime.onInstalled.addListener(initSettings);

View file

@ -1,29 +0,0 @@
import { appRedirectUrl } from '../common/lbry-url';
import { getSettingsAsync } from '../common/settings';
// handles lbry.tv -> lbry app redirect
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, { url: tabUrl }) => {
const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect');
if (!enabled || redirect !== 'app' || !changeInfo.url || !tabUrl?.startsWith('https://lbry.tv/')) return;
const url = appRedirectUrl(tabUrl, { encode: true });
if (!url) return;
chrome.tabs.update(tabId, { url });
alert('Opened link in LBRY App!'); // Better for UX since sometimes LBRY App doesn't take focus, if that is fixed, this can be removed
chrome.tabs.executeScript(tabId, {
code: `if (window.history.length === 1) {
window.close();
} else {
window.history.back();
}
document.querySelectorAll('video').forEach(v => v.pause());
`
});
});
// relay youtube link changes to the content script
chrome.tabs.onUpdated.addListener((tabId, changeInfo, { url }) => {
if (!changeInfo.url || !url ||
!(url.startsWith('https://www.youtube.com/watch?v=') || url.startsWith('https://www.youtube.com/channel/'))) return;
chrome.tabs.sendMessage(tabId, { url });
});

View file

@ -1,118 +1,363 @@
import { h, render } from 'preact';
import { h, render } from 'preact'
import { parseYouTubeURLTimeString } from '../modules/yt'
import type { resolveById, ResolveUrlTypes } from '../modules/yt/urlResolve'
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTargetPlatfromSettingsEntiries, SourcePlatform, sourcePlatfromSettings, TargetPlatform, targetPlatformSettings } from '../settings';
import { parseProtocolUrl } from '../common/lbry-url';
import { getSettingsAsync, LbrySettings, redirectDomains } from '../common/settings';
import { YTDescriptor, ytService } from '../common/yt';
(async () => {
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
interface UpdaterOptions {
/** invoked if a redirect should be performed */
onRedirect?(ctx: UpdateContext): void
/** invoked if a URL is found */
onURL?(ctx: UpdateContext): void
}
interface UpdateContext {
descriptor: YTDescriptor
url: string
enabled: boolean
redirect: LbrySettings['redirect']
}
function pauseVideo() { document.querySelectorAll<HTMLVideoElement>('video').forEach(v => v.pause()); }
function openApp(url: string) {
pauseVideo();
location.assign(url);
}
async function resolveYT(descriptor: YTDescriptor) {
const lbryProtocolUrl: string | null = await ytService.resolveById(descriptor).then(a => a[0]);
const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true });
if (segments.length === 0) return;
return segments.join('/');
}
/** Compute the URL and determine whether or not a redirect should be performed. Delegates the redirect to callbacks. */
async function handleURLChange(url: URL | Location, { onRedirect, onURL }: UpdaterOptions): Promise<void> {
const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect');
const urlPrefix = redirectDomains[redirect].prefix;
const descriptor = ytService.getId(url.href);
if (!descriptor) return; // couldn't get the ID, so we're done
const res = await resolveYT(descriptor);
if (!res) return; // couldn't find it on lbry, so we're done
const ctx = { descriptor, url: urlPrefix + res, enabled, redirect };
if (onURL) onURL(ctx);
if (enabled && onRedirect) onRedirect(ctx);
}
/** Returns a mount point for the button */
async function findMountPoint(): Promise<HTMLDivElement | void> {
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));
let ownerBar = document.querySelector('ytd-video-owner-renderer');
for (let i = 0; !ownerBar && i < 50; i++) {
await sleep(200);
ownerBar = document.querySelector('ytd-video-owner-renderer');
interface Target {
platform: TargetPlatform
lbryPathname: string
type: ResolveUrlTypes
time: number | null
}
if (!ownerBar) return;
const div = document.createElement('div');
div.style.display = 'flex';
ownerBar.insertAdjacentElement('afterend', div);
return div;
}
interface Source {
platform: SourcePlatform
id: string
type: ResolveUrlTypes
url: URL
time: number | null
}
function WatchOnLbryButton({ url }: { url?: string }) {
if (!url) return null;
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
<a href={url} onClick={pauseVideo} role='button'
children={<div>
<img src={chrome.runtime.getURL('icons/lbry-logo.svg')} height={10} width={14}
style={{ marginRight: 12, transform: 'scale(1.75)' }} />
Watch on LBRY
</div>}
const targetPlatforms = getTargetPlatfromSettingsEntiries()
const settings = await getExtensionSettingsAsync()
// Listen Settings Change
chrome.storage.onChanged.addListener(async (changes, areaName) => {
if (areaName !== 'local') return
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
})
const buttonMountPoint = document.createElement('div')
buttonMountPoint.style.display = 'inline-flex'
const playerButtonMountPoint = document.createElement('div')
playerButtonMountPoint.style.display = 'inline-flex'
function WatchOnLbryButton({ source, target }: { source?: Source, target?: Target }) {
if (!target || !source) return null
const url = getLbryUrlByTarget(target)
return <div
style={{
borderRadius: '2px',
backgroundColor: '#075656',
border: '0',
color: 'whitesmoke',
padding: '10px 16px',
marginRight: '5px',
fontSize: '14px',
textDecoration: 'none',
}} />
</div>
}
display: 'grid',
gridTemplateRows: '36px',
gridAutoColumns: 'auto',
alignContent: 'center'
}}
>
<a href={`${url.href}`} target={target.platform === targetPlatformSettings.app ? '' : '_blank'} role='button'
style={{
display: 'flex',
alignItems: 'center',
gap: '7px',
borderRadius: '2px',
padding: '0 16px',
margin: '0 4px',
fontWeight: 'bold',
border: '0',
color: 'whitesmoke',
fontSize: '14px',
textDecoration: 'none',
backgroundColor: target.platform.theme,
backgroundImage: target.platform.theme,
...target.platform.button.style?.button,
}}
onClick={() => findVideoElementAwait(source).then((videoElement) => {
videoElement.pause()
})}
>
<img src={target.platform.button.icon} height={24} style={{ ...target.platform.button.style?.icon }} />
<span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
</a>
</div>
}
function WatchOnLbryPlayerButton({ source, target }: { source?: Source, target?: Target }) {
if (!target || !source) return null
const url = getLbryUrlByTarget(target)
return <div
style={{
display: 'grid',
gridTemplateRows: '36px',
gridAutoColumns: 'auto',
alignContent: 'center'
}}
>
<a href={`${url.href}`} target={target.platform === targetPlatformSettings.app ? '' : '_blank'} role='button'
style={{
display: 'flex',
alignItems: 'center',
gap: '7px',
borderRadius: '2px',
paddingRight: '10px',
fontWeight: 'bold',
border: '0',
color: 'whitesmoke',
fontSize: '14px',
textDecoration: 'none',
...target.platform.button.style?.button,
}}
onClick={() => findVideoElementAwait(source).then((videoElement) => {
videoElement.pause()
})}
>
<img src={target.platform.button.icon} height={24} style={{ ...target.platform.button.style?.icon }} />
<span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
</a>
</div>
}
function updateButtons(params: { source: Source, target: Target } | null): void {
if (!params) {
render(<WatchOnLbryButton />, buttonMountPoint)
render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
return
}
{
const mountPlayerButtonBefore = settings.buttonVideoPlayer ?
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountPlayerButtonBefore) :
null
if (!mountPlayerButtonBefore) render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
else {
if (playerButtonMountPoint.getAttribute('data-id') !== params.source.id) {
mountPlayerButtonBefore.parentElement?.insertBefore(playerButtonMountPoint, mountPlayerButtonBefore)
playerButtonMountPoint.setAttribute('data-id', params.source.id)
}
render(<WatchOnLbryPlayerButton target={params.target} source={params.source} />, playerButtonMountPoint)
}
}
{
const mountButtonBefore = settings[(`button${params.source.type[0].toUpperCase() + params.source.type.substring(1)}Sub`) as 'buttonVideoSub' | 'buttonChannelSub'] ?
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountButtonBefore[params.source.type]) :
null
if (!mountButtonBefore) render(<WatchOnLbryButton />, buttonMountPoint)
else {
if (buttonMountPoint.getAttribute('data-id') !== params.source.id) {
mountButtonBefore.parentElement?.insertBefore(buttonMountPoint, mountButtonBefore)
buttonMountPoint.setAttribute('data-id', params.source.id)
}
render(<WatchOnLbryButton target={params.target} source={params.source} />, buttonMountPoint)
}
}
}
async function findVideoElementAwait(source: Source) {
let videoElement: HTMLVideoElement | null = null
while (!(videoElement = document.querySelector(source.platform.htmlQueries.videoPlayer))) await sleep(200)
return videoElement
}
async function getSourceByUrl(url: URL): Promise<Source | null> {
const platform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!platform) return null
if (url.pathname === '/watch' && url.searchParams.has('v')) {
return {
id: url.searchParams.get('v')!,
platform,
time: url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null,
type: 'video',
url
}
}
else if (url.pathname.startsWith('/channel/')) {
return {
id: url.pathname.substring("/channel/".length),
platform,
time: null,
type: 'channel',
url
}
}
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) {
// We have to download the page content again because these parts of the page are not responsive
// yt front end sucks anyway
const content = await (await fetch(location.href)).text()
const prefix = `https://www.youtube.com/feeds/videos.xml?channel_id=`
const suffix = `"`
const startsAt = content.indexOf(prefix) + prefix.length
const endsAt = content.indexOf(suffix, startsAt)
const id = content.substring(startsAt, endsAt)
return {
id,
platform,
time: null,
type: 'channel',
url
}
}
return null
}
async function getTargetsBySources(...sources: Source[]) {
const params: Parameters<typeof requestResolveById>[0] = sources.map((source) => ({ id: source.id, type: source.type }))
const platform = targetPlatformSettings[settings.targetPlatform]
const results = await requestResolveById(params) ?? []
const targets: Record<string, Target | null> = Object.fromEntries(
sources.map((source) => {
const result = results[source.id]
if (!result) return [
source.id,
null
]
return [
source.id,
{
type: result.type,
lbryPathname: result.id,
platform,
time: source.time
}
]
})
)
return targets
}
// We should get this from background, so the caching works and we don't get errors in the future if yt decides to impliment CORS
async function requestResolveById(...params: Parameters<typeof resolveById>): ReturnType<typeof resolveById> {
const response = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ method: 'resolveUrl', data: JSON.stringify(params) }, resolve))
if (response?.startsWith('error:')) {
console.error("Background error on:", params)
throw new Error(`Background error. ${response ?? ''}`)
}
return response ? JSON.parse(response) : null
}
// Request new tab
async function openNewTab(url: URL) {
chrome.runtime.sendMessage({ method: 'openTab', data: JSON.stringify({ href: url.href }) })
}
function findTargetFromSourcePage(source: Source): Target | null {
const linksContainer =
source.type === 'video' ?
document.querySelector(source.platform.htmlQueries.videoDescription) :
source.platform.htmlQueries.channelLinks ? document.querySelector(source.platform.htmlQueries.channelLinks) : null
if (linksContainer) {
const anchors = Array.from(linksContainer.querySelectorAll<HTMLAnchorElement>('a'))
for (const anchor of anchors) {
if (!anchor.href) continue
const url = new URL(anchor.href)
let lbryURL: URL | null = null
// Extract real link from youtube's redirect link
if (source.platform === sourcePlatfromSettings['youtube.com']) {
if (!targetPlatforms.some(([key, platform]) => url.searchParams.get('q')?.startsWith(platform.domainPrefix))) continue
lbryURL = new URL(url.searchParams.get('q')!)
}
// Just directly use the link itself on other platforms
else {
if (!targetPlatforms.some(([key, platform]) => url.href.startsWith(platform.domainPrefix))) continue
lbryURL = new URL(url.href)
}
if (lbryURL) {
return {
lbryPathname: lbryURL.pathname.substring(1),
time: null,
type: lbryURL.pathname.substring(1).includes('/') ? 'video' : 'channel',
platform: targetPlatformSettings[settings.targetPlatform]
}
}
}
}
return null
}
function getLbryUrlByTarget(target: Target) {
const url = new URL(`${target.platform.domainPrefix}${target.lbryPathname}`)
if (target.time) url.searchParams.set('t', target.time.toFixed(0))
return url
}
const mountPointPromise = findMountPoint();
// Master Loop
for (
let url = new URL(location.href),
urlHrefCache: string | null = null;
;
urlHrefCache = url.href,
url = new URL(location.href)
) {
await sleep(500)
try {
const source = await getSourceByUrl(new URL(location.href))
if (!source) {
updateButtons(null)
continue
}
const target = (await getTargetsBySources(source))[source.id] ?? findTargetFromSourcePage(source)
if (!target) {
updateButtons(null)
continue
}
const handle = (url: URL | Location) => handleURLChange(url, {
async onURL({ descriptor: { type }, url }) {
const mountPoint = await mountPointPromise;
if (type !== 'video' || !mountPoint) return;
render(<WatchOnLbryButton url={url} />, mountPoint)
},
onRedirect({ redirect, url }) {
if (redirect === 'app') return openApp(url);
location.replace(url);
},
});
// Update Buttons
if (urlHrefCache !== url.href) updateButtons(null)
// If target is a video target add timestampt to it
if (target.type === 'video') {
const videoElement = document.querySelector<HTMLVideoElement>(source.platform.htmlQueries.videoPlayer)
if (videoElement) target.time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
}
updateButtons({ target, source })
// handle the location on load of the page
handle(location);
// Redirect
if (
source.type === target.type &&
(
(
settings.redirectVideo &&
source.type === 'video' && !source.url.searchParams.has('list')
) ||
(
settings.redirectVideoPlaylist &&
source.type === 'video' && source.url.searchParams.has('list')
) ||
(
settings.redirectChannel &&
source.type === 'channel'
)
)
) {
if (url.href === urlHrefCache) continue
/*
* Gets messages from background script which relays tab update events. This is because there's no sensible way to detect
* history.pushState changes from a content script
*/
chrome.runtime.onMessage.addListener(async (req: { url: string }) => {
mountPointPromise.then(mountPoint => mountPoint && render(<WatchOnLbryButton />, mountPoint))
if (!req.url) return;
handle(new URL(req.url));
});
const lbryURL = getLbryUrlByTarget(target)
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local' || !changes.redirect) return;
handle(new URL(location.href))
});
if (source.type === 'video') {
findVideoElementAwait(source).then((videoElement) => videoElement.pause())
}
if (target.platform === targetPlatformSettings.app) {
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
// Replace is being used so browser doesnt start an empty window
// Its not gonna be able to replace anyway, since its a LBRY Uri
location.replace(lbryURL)
}
else {
openNewTab(lbryURL)
if (window.history.length === 1)
window.close()
else
window.history.back()
}
}
} catch (error) {
console.error(error)
}
}
})()

View file

@ -0,0 +1,2 @@
import './settings/background'
import './scripts/background'

View file

@ -0,0 +1,33 @@
import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync, setExtensionSetting, targetPlatformSettings, ytUrlResolversSettings } from '../settings'
// This is for manifest v2 and v3
const chromeAction = chrome.action ?? chrome.browserAction
/** Reset settings to default value and update the browser badge text */
async function initSettings() {
let settings = await getExtensionSettingsAsync()
// get all the values that aren't set and use them as a change set
const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>)
.filter(([k]) => settings[k] === undefined || settings[k] === null)
// fix our local var and set it in storage for later
if (invalidEntries.length > 0) {
const changeSet = Object.fromEntries(invalidEntries)
chrome.storage.local.set(changeSet)
settings = await getExtensionSettingsAsync()
}
if (!Object.keys(targetPlatformSettings).includes(settings.targetPlatform)) setExtensionSetting('targetPlatform', DEFAULT_SETTINGS.targetPlatform)
if (!Object.keys(ytUrlResolversSettings).includes(settings.urlResolver)) setExtensionSetting('urlResolver', DEFAULT_SETTINGS.urlResolver)
// chromeAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' })
}
/* chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local' || !changes.redirect) return
chromeAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' })
}) */
chrome.runtime.onStartup.addListener(initSettings)
chrome.runtime.onInstalled.addListener(initSettings)

201
src/settings/index.ts Normal file
View file

@ -0,0 +1,201 @@
import type { JSX } from "preact"
import { useEffect, useReducer } from "preact/hooks"
import type { ResolveUrlTypes } from "../modules/yt/urlResolve"
export interface ExtensionSettings extends Record<string, string | number | boolean | null | undefined>{
targetPlatform: TargetPlatformName
urlResolver: YTUrlResolverName,
redirectVideo: boolean,
redirectChannel: boolean,
redirectVideoPlaylist: boolean,
buttonVideoSub: boolean
buttonVideoPlayer: boolean
buttonChannelSub: boolean
publicKey: string | null,
privateKey: string | null
}
export const DEFAULT_SETTINGS: ExtensionSettings = {
targetPlatform: 'odysee',
urlResolver: 'odyseeApi',
redirectVideo: false,
redirectChannel: false,
redirectVideoPlaylist: false,
buttonVideoSub: true,
buttonVideoPlayer: true,
buttonChannelSub: true,
privateKey: null,
publicKey: null
}
export function getExtensionSettingsAsync(): Promise<ExtensionSettings> {
return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any)))
}
/** Utilty to set a setting in the browser */
export const setExtensionSetting = <K extends keyof ExtensionSettings>(setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value })
/**
* A hook to read the settings from local storage
*
* @param defaultSettings the default value. Must have all relevant keys present and should not change
*/
function useSettings(defaultSettings: ExtensionSettings) {
const [state, dispatch] = useReducer((state, nstate: Partial<ExtensionSettings>) => ({ ...state, ...nstate }), defaultSettings)
const settingsKeys = Object.keys(defaultSettings)
// register change listeners, gets current values, and cleans up the listeners on unload
useEffect(() => {
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, areaName: string) => {
if (areaName !== 'local') return
const changeEntries = Object.keys(changes).filter((key) => settingsKeys.includes(key)).map((key) => [key, changes[key].newValue])
if (changeEntries.length === 0) return // no changes; no use dispatching
dispatch(Object.fromEntries(changeEntries))
}
chrome.storage.onChanged.addListener(changeListener)
chrome.storage.local.get(settingsKeys, async (settings) => dispatch(settings))
return () => chrome.storage.onChanged.removeListener(changeListener)
}, [])
return state
}
/** A hook to read watch on lbry settings from local storage */
export const useExtensionSettings = () => useSettings(DEFAULT_SETTINGS)
const targetPlatform = (o: {
domainPrefix: string
displayName: string
theme: string
button: {
platformNameText: string,
icon: string
style?:
{
icon?: JSX.CSSProperties
button?: JSX.CSSProperties
}
}
}) => o
export type TargetPlatform = ReturnType<typeof targetPlatform>
export type TargetPlatformName = Extract<keyof typeof targetPlatformSettings, string>
export const getTargetPlatfromSettingsEntiries = () => {
return Object.entries(targetPlatformSettings) as any as [Extract<keyof typeof targetPlatformSettings, string>, TargetPlatform][]
}
export const targetPlatformSettings = {
'madiator.com': targetPlatform({
domainPrefix: 'https://madiator.com/',
displayName: 'Madiator.com',
theme: 'linear-gradient(130deg, #499375, #43889d)',
button: {
platformNameText: '',
icon: chrome.runtime.getURL('assets/icons/lbry/madiator-logo.svg'),
style: {
button: { flexDirection: 'row-reverse' },
icon: { }
}
}
}),
odysee: targetPlatform({
domainPrefix: 'https://odysee.com/',
displayName: 'Odysee',
theme: 'linear-gradient(130deg, #c63d59, #f77937)',
button: {
platformNameText: 'Odysee',
icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg')
}
}),
app: targetPlatform({
domainPrefix: 'lbry://',
displayName: 'LBRY App',
theme: 'linear-gradient(130deg, #499375, #43889d)',
button: {
platformNameText: 'LBRY',
icon: chrome.runtime.getURL('assets/icons/lbry/lbry-logo.svg')
}
}),
}
const sourcePlatform = (o: {
hostnames: string[]
htmlQueries: {
mountPoints: {
mountButtonBefore: Record<ResolveUrlTypes, string>,
mountPlayerButtonBefore: string,
}
videoPlayer: string,
videoDescription: string
channelLinks: string
}
}) => o
export type SourcePlatform = ReturnType<typeof sourcePlatform>
export type SourcePlatformName = Extract<keyof typeof sourcePlatfromSettings, string>
export function getSourcePlatfromSettingsFromHostname(hostname: string) {
const values = Object.values(sourcePlatfromSettings)
for (const settings of values)
if (settings.hostnames.includes(hostname)) return settings
return null
}
export const sourcePlatfromSettings = {
"youtube.com": sourcePlatform({
hostnames: ['www.youtube.com'],
htmlQueries: {
mountPoints: {
mountButtonBefore: {
video: 'ytd-video-owner-renderer~#subscribe-button',
channel: '#channel-header-container #buttons #subscribe-button'
},
mountPlayerButtonBefore: 'ytd-watch-flexy ytd-player .ytp-right-controls',
},
videoPlayer: '#ytd-player video',
videoDescription: 'ytd-video-secondary-info-renderer #description',
channelLinks: '#channel-header #links-holder'
}
}),
"yewtu.be": sourcePlatform({
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
htmlQueries: {
mountPoints: {
mountButtonBefore:
{
video: '#subscribe',
channel: '#subscribe'
},
mountPlayerButtonBefore: '#player-container ~ .h-box > h1 > a',
},
videoPlayer: '#player-container video',
videoDescription: '#descriptionWrapper',
channelLinks: '#descriptionWrapper'
}
})
}
const ytUrlResolver = (o: {
name: string
href: string
signRequest: boolean
}) => o
export type YTUrlResolver = ReturnType<typeof ytUrlResolver>
export type YTUrlResolverName = Extract<keyof typeof ytUrlResolversSettings, string>
export const getYtUrlResolversSettingsEntiries = () => Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
export const ytUrlResolversSettings = {
odyseeApi: ytUrlResolver({
name: "Odysee",
href: "https://api.odysee.com/yt",
signRequest: false
}),
madiatorFinder: ytUrlResolver({
name: "Madiator Finder",
href: "https://finder.madiator.com/api/v1",
signRequest: true
}),
/* madiatorFinderLocal: ytUrlResolver({
name: "Madiator Finder Local",
href: "http://127.0.0.1:3001/api/v1",
signRequest: true
}) */
}

View file

@ -1,6 +0,0 @@
# Getting your subscription data
1. Go to https://takeout.google.com/settings/takeout
2. Deselect everything except `YouTube and YouTube Music` and within that only select `subscriptions`
3. Go through the process and create the export
4. Once it's exported, open the archive and find `YouTube and YouTube Music/subscriptions/subscriptions.json` and upload it to the extension

View file

@ -1,88 +0,0 @@
body {
--color-text: whitesmoke;
--color-backround: rgb(28, 31, 34);
--color-card: #2a2e32;
--color-primary: rgb(43, 187, 144);
background-color: var(--color-backround);
color: var(--color-text);
font-size: 1rem;
}
a {
color: var(--color-primary);
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 10px;
}
.container {
display: block;
text-align: center;
margin: auto;
width: 50%;
margin-bottom: 15px;
border: 3px;
padding: 10px;
}
.YTtoLBRY {
display: flex;
justify-content: space-around;
flex-direction: row;
}
.Conversion {
margin: 1rem;
width: 45em;
}
.ConversionHelp {
display: flex;
flex-direction: column;
align-items: center;
}
.ConversionCard {
box-shadow: 0 0 0 1px rgba(16,22,26,.1), 0 1px 1px rgba(16,22,26,.2), 0 2px 6px rgba(16,22,26,.2);
border-radius: 5px;
background-color: var(--color-card);
padding: 20px;
}
.btn {
color: var(--color-text);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
padding: 5px 10px;
border-radius: 3px;
font-size: 14px;
min-width: 30px;
min-height: 30px;
}
.btn.btn-primary {
background-color: var(--color-primary);
}
.btn:disabled {
background-color: rgba(200, 200, 200, .5);
color: rgba(90, 90, 90, .6);
cursor: not-allowed;
}
@media (max-width: 1400px) {
.YTtoLBRY {
flex-direction: column;
}
.Conversion {
width: auto;
}
}

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Subscription Converter</title>
<link rel="stylesheet" href="YTtoLBRY.css" />
<script src="YTtoLBRY.tsx" charset="utf-8" defer></script>
</head>
<body>
<div id="root" />
</body>
</html>

View file

@ -1,62 +0,0 @@
import { Fragment, h, JSX, render } from 'preact';
import { useState } from 'preact/hooks';
import { getSettingsAsync, redirectDomains } from '../common/settings';
import { getFileContent, ytService } from '../common/yt';
import readme from './README.md';
/**
* Parses the subscription file and queries the API for lbry channels
*
* @param file to read
* @returns a promise with the list of channels that were found on lbry
*/
async function lbryChannelsFromFile(file: File) {
const ext = file.name.split('.').pop()?.toLowerCase();
const content = await getFileContent(file);
const ids = new Set((ext === 'xml' || ext == 'opml' ? ytService.readOpml(content) : ytService.readJson(content)))
const lbryUrls = await ytService.resolveById(...Array.from(ids).map(id => ({ id, type: 'channel' } as const)));
const { redirect } = await getSettingsAsync('redirect');
const urlPrefix = redirectDomains[redirect].prefix;
return lbryUrls.map(channel => urlPrefix + channel);
}
function ConversionCard({ onSelect }: { onSelect(file: File): Promise<void> | void }) {
const [file, setFile] = useState(null as File | null);
const [isLoading, setLoading] = useState(false);
return <div className='ConversionCard'>
<h2>Select YouTube Subscriptions</h2>
<div style={{ marginBottom: 10 }}>
<input type='file' onChange={e => setFile(e.currentTarget.files?.length ? e.currentTarget.files[0] : null)} />
</div>
<button class='btn btn-primary' children='Start Conversion!' disabled={!file || isLoading} onClick={async () => {
if (!file) return;
setLoading(true);
await onSelect(file);
setLoading(false);
}} />
</div>
}
function YTtoLBRY() {
const [lbryChannels, setLbryChannels] = useState([] as string[]);
return <div className='YTtoLBRY'>
<div className='Conversion'>
<ConversionCard onSelect={async file => setLbryChannels(await lbryChannelsFromFile(file))} />
<ul>
{lbryChannels.map((x, i) => <li key={i} children={<a href={x} children={x} />} />)}
</ul>
</div>
<div className='ConversionHelp'>
<iframe width='712px' height='400px' allowFullScreen
src='https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e' />
<section dangerouslySetInnerHTML={{ __html: readme }} />
</div>
</div>
}
render(<YTtoLBRY />, document.getElementById('root')!);

View file

@ -41,6 +41,7 @@
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"importsNotUsedAsValues": "error", /* Import types always with `import type` */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
@ -53,7 +54,6 @@
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */

10083
yarn.lock Normal file

File diff suppressed because it is too large Load diff