Compare commits
254 commits
Author | SHA1 | Date | |
---|---|---|---|
|
60735f9507 | ||
|
8c26ae0872 | ||
|
6ba1a373a9 | ||
|
c1b53d90a1 | ||
|
529931c43b | ||
|
351ca5c6c3 | ||
|
ca65a0288d | ||
|
309806605f | ||
|
7fa57b9e88 | ||
|
21f6ffc85b | ||
|
1ee722b6ff | ||
|
28ef256c40 | ||
|
a41b51e454 | ||
|
5c9910dff1 | ||
|
59a5e0dee2 | ||
|
e4fdc4e831 | ||
|
9c26d553f5 | ||
|
f9c70dd90d | ||
|
9ed962df96 | ||
|
a95b46bf0c | ||
|
913282290e | ||
|
6af354de0c | ||
|
15f03b3bcc | ||
|
64570b83bf | ||
|
4295dbe1fe | ||
|
619d66d6aa | ||
|
b26d540a4a | ||
|
32290e5f63 | ||
|
c089de3f0f | ||
|
fcdc5cb77a | ||
|
e0d514916e | ||
|
376c7b185a | ||
|
73147fdbcd | ||
|
9edd318a35 | ||
|
f5c0fc450b | ||
|
259582cd3e | ||
|
38f2d42404 | ||
|
7b585b27a9 | ||
|
6721966e5b | ||
|
f087d0d1e5 | ||
|
de43b990f2 | ||
|
8f88dbebe7 | ||
|
0aa64bb8b2 | ||
|
f4dd0306fc | ||
|
29c9b2829a | ||
|
a6472799a1 | ||
|
1f29fc7c29 | ||
|
ca4c486ea7 | ||
|
4102177212 | ||
|
da8dee83f9 | ||
|
ce11d4fdf3 | ||
|
1eb21eb51f | ||
|
a0577b4530 | ||
|
c895d53253 | ||
|
66cb8ccccf | ||
|
f072c95051 | ||
|
0ba4efd43e | ||
|
066c400cce | ||
|
63a251c0ae | ||
|
be310e6cb1 | ||
|
cebdf480fe | ||
|
7076a657fc | ||
|
dfa1fd03a8 | ||
|
cddd415e22 | ||
|
6aa507c53d | ||
|
0bcf69e99b | ||
|
ccf6d2d990 | ||
|
c4231c29d6 | ||
|
cf85b39d6a | ||
|
5d9d0416a3 | ||
|
e2e7426b5b | ||
|
6b8f193bfd | ||
|
c2f894ec61 | ||
|
ce40824a0c | ||
|
f86affc093 | ||
|
deed10423a | ||
|
caa0d196ed | ||
|
a27d5a285b | ||
|
e8d7a349f4 | ||
|
fe5c38bfed | ||
|
53eb4a0c8e | ||
|
f1f4c335e9 | ||
|
3ee7e530d6 | ||
|
81f1742289 | ||
|
a0f66bc062 | ||
|
450ca8cef6 | ||
|
651ca5b4dc | ||
|
2a3771e45a | ||
|
365173d316 | ||
|
3e60ed295f | ||
|
8c5e2d68e0 | ||
|
222a71948b | ||
|
bd33aa833f | ||
|
3edc78c36a | ||
|
27207e0f9c | ||
|
3692ccba89 | ||
|
bb0dc3c4f4 | ||
|
b17f2f4e2e | ||
|
4a23670e4d | ||
|
92fbdd727d | ||
|
6ef5459d7a | ||
|
e501a8b828 | ||
|
324d3800de | ||
|
e01a3990c8 | ||
|
79ba91a1f1 | ||
|
7f3f8919f0 | ||
|
c94ec17fa3 | ||
|
585a232021 | ||
|
2b38c809ca | ||
|
093deaa8fd | ||
|
fde3d7b897 | ||
|
bccbce8db1 | ||
|
766b6cf990 | ||
|
889c64c0a6 | ||
|
89ce1aeb31 | ||
|
3a07d7f82b | ||
|
4653f77be4 | ||
|
94cda17b99 | ||
|
2c75082af9 | ||
|
8f75c67601 | ||
|
5bcd33890d | ||
|
d32958852a | ||
|
5396936586 | ||
|
06524954e7 | ||
|
86879183b2 | ||
|
30b65454ff | ||
|
30f077ba38 | ||
|
610b47d1e4 | ||
|
2b91436900 | ||
|
4f8e807a65 | ||
|
cb4b4f4b2e | ||
|
75cb9cf01d | ||
|
7727d04157 | ||
|
719ff06caf | ||
|
4cdcc4c9a4 | ||
|
205a8fd151 | ||
|
1e7293826a | ||
|
b750c86b88 | ||
|
475c38ba0c | ||
|
ff83dfc62d | ||
|
685f9615ca | ||
|
73508ff532 | ||
|
70ad28ba22 | ||
|
a0b6182a96 | ||
|
6597acd0a5 | ||
|
c2aacaf307 | ||
|
c24fd17013 | ||
|
a7b66660ea | ||
|
7c50daf7fc | ||
|
a671df51a0 | ||
|
668018d89d | ||
|
2b3d43e0dc | ||
|
19967c5ecc | ||
|
2352237bf5 | ||
|
00f2bb82f8 | ||
|
77d95ab61c | ||
|
75274005ee | ||
|
682af767c9 | ||
|
9b68487ecc | ||
|
336e3050f0 | ||
|
6b4a377058 | ||
|
279f7faff7 | ||
|
8f30825edd | ||
|
4ab8fdce5e | ||
|
58e6c119f4 | ||
|
ba5beca60a | ||
|
dd16ce1041 | ||
|
63d91a2ee1 | ||
|
4220ea11c7 | ||
|
47d27b0671 | ||
|
b17a7020fd | ||
|
3bab9d15ee | ||
|
adab9addf5 | ||
|
f3cf3526f5 | ||
|
3b4bc7910f | ||
|
bff1a849fc | ||
|
27b6c5f422 | ||
|
ac911e6816 | ||
|
6ac58a5bf8 | ||
|
dfdfe6778a | ||
|
6cc149a5c6 | ||
|
5c61db3ea0 | ||
|
8c4f3e60e0 | ||
|
c7a839573a | ||
|
285d46bcc1 | ||
|
091c0e29b4 | ||
|
9a9b51bbba | ||
|
972f5e7415 | ||
|
014ad21fd6 | ||
|
9532d45b15 | ||
|
11334c7689 | ||
|
e2f46d2d08 | ||
|
d23fda84d5 | ||
|
0f6ba8a4f9 | ||
|
666ec9db7b | ||
|
10c843e7b8 | ||
|
606288e809 | ||
|
729deb8be4 | ||
|
7eba8ebea5 | ||
|
820208e94b | ||
|
1d6e02d006 | ||
|
fdbc0bf93c | ||
|
862e52030d | ||
|
8b0176a3a2 | ||
|
366028f49b | ||
|
170fab60c0 | ||
|
32a1a01676 | ||
|
eec8092a4a | ||
|
5789397032 | ||
|
94af57b809 | ||
|
2afbdcef9b | ||
|
9a9acf3c29 | ||
|
9bb7e5fd5c | ||
|
2203768d9b | ||
|
0ea63c60b1 | ||
|
ab707bf533 | ||
|
e77490ef19 | ||
|
410a87710b | ||
|
a0963db2b4 | ||
|
47d5292740 | ||
|
e0434192ff | ||
|
4f835f0c24 | ||
|
f04d0b7c1f | ||
|
2be251fbd2 | ||
|
1aab3f67e1 | ||
|
570d278f1f | ||
|
3db9a692c8 | ||
|
c0cd29abd2 | ||
|
218f023600 | ||
|
e408b9f3e8 | ||
|
c5c8f40ddf | ||
|
76830c907e | ||
|
951acee9da | ||
|
6b43680b5e | ||
|
c990b6920c | ||
|
a03a5df765 | ||
|
1996373b54 | ||
|
25bc0399a8 | ||
|
f1a3a38116 | ||
|
40a3a7fee0 | ||
|
a51be792ab | ||
|
4d3a6d6e97 | ||
|
7e049858b5 | ||
|
8341291497 | ||
|
30c93c2c4f | ||
|
bd5adf6652 | ||
|
991a37c367 | ||
|
08842c4ba8 | ||
|
2e58a04333 | ||
|
7954c29482 | ||
|
40036013c2 | ||
|
f198929922 | ||
|
0401c6f3d1 | ||
|
dca9b984e8 |
16
.devcontainer/Dockerfile
Normal 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>"
|
36
.devcontainer/devcontainer.json
Normal 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
|
@ -0,0 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
liberapay: Madiator2011
|
||||
custom: cointr.ee/madiator2011
|
23
.github/workflows/contributors.yml
vendored
Normal 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'
|
37
.github/workflows/extension-build.js.yml
vendored
Normal 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
|
@ -1,4 +1,8 @@
|
|||
.cache
|
||||
dist
|
||||
build
|
||||
node_modules
|
||||
web-ext-artifacts
|
||||
yarn-error.log
|
||||
|
||||
.DS_Store
|
||||
|
|
15
CHANGELOG.md
Normal 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))
|
101
README.md
|
@ -1,16 +1,30 @@
|
|||
|
||||
## Looking for contributors :)
|
||||

|
||||
# 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
|
||||
|
||||
[](https://addons.mozilla.org/en/firefox/addon/watch-on-lbry/?src=search) [](https://chrome.google.com/webstore/detail/watch-on-lbry/jjmbbhopnjdjnpceiecihldbhibchgek)
|
||||
[](https://addons.mozilla.org/en/firefox/addon/watch-on-lbry/?src=search)
|
||||
[](https://chrome.google.com/webstore/detail/watch-on-lbry/jjmbbhopnjdjnpceiecihldbhibchgek)
|
||||
|
||||
## Build
|
||||
|
||||
From the root of the project
|
||||
|
||||
For Production
|
||||
```bash
|
||||
$ npm install
|
||||
$ npm run build
|
||||
$ npm run build:webext # optional, to create the zip file from the dist directory
|
||||
```
|
||||
|
||||
For Development
|
||||
```bash
|
||||
$ npm install
|
||||
$ npm run watch
|
||||
|
@ -48,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
After Width: | Height: | Size: 5.9 KiB |
BIN
doc/img/chrome-small-border.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
9
global.d.ts
vendored
Normal 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
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
9398
package-lock.json
generated
27
package.json
|
@ -3,12 +3,21 @@
|
|||
"version": "1.5.4",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"build:assets": "cpx \"src/**/*.{json,png}\" dist/",
|
||||
"watch:assets": "cpx --watch -v \"src/**/*.{json,png}\" dist/",
|
||||
"build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps \"src/scripts/*.{js,ts}\" \"src/**/*.html\"",
|
||||
"watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/scripts/*.{js,ts}\" \"src/**/*.html\"",
|
||||
"build": "npm-run-all -l -p build:parcel build:assets",
|
||||
"watch": "npm-run-all -l -p watch:parcel watch:assets",
|
||||
"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/**/*.{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"
|
||||
|
@ -17,7 +26,6 @@
|
|||
">1%",
|
||||
"not ie > 0"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@types/chrome": "0.0.124",
|
||||
|
@ -29,8 +37,9 @@
|
|||
"cpx": "^1.5.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"jest": "^26.5.3",
|
||||
"lodash": "^4.17.20",
|
||||
"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",
|
||||
|
|
79
src/assets/icons/lbry/lbry-logo.svg
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<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"
|
||||
width="80.207558mm"
|
||||
height="58.081333mm"
|
||||
viewBox="0 0 284.20001 205.8"
|
||||
id="svg3479"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="lbry-white-logo-only.svg">
|
||||
<defs
|
||||
id="defs3481" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-470.0429"
|
||||
inkscape:cy="-5.6714247"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata3484">
|
||||
<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></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-363.61433,-320.89078)">
|
||||
<g
|
||||
id="g3396"
|
||||
transform="translate(363.61433,320.89078)">
|
||||
<g
|
||||
id="g3398">
|
||||
<polygon
|
||||
style="fill:#ffffff"
|
||||
points="138.8,155.2 271,74 271,68.2 146.2,8 7,94.1 7,132.6 138.8,197.8 276.4,113.4 280.3,119.4 139.2,205.8 0,137 0,90.2 145.8,0 278,63.8 278,77.9 139.2,163.2 34.6,111.9 34.8,104 "
|
||||
id="polygon3400" />
|
||||
</g>
|
||||
<g
|
||||
id="g3402">
|
||||
<polygon
|
||||
style="fill:#ffffff"
|
||||
points="276.5,128.5 278.5,115.9 266.3,113.8 267.1,108.9 284.2,111.8 281.4,129.3 "
|
||||
id="polygon3404" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
38
src/assets/icons/lbry/madiator-logo.svg
Normal 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 |
151
src/assets/icons/lbry/odysee-logo.svg
Normal 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 |
1
src/assets/icons/wol/default-monochrome-black.svg
Normal file
After Width: | Height: | Size: 5.2 KiB |
1
src/assets/icons/wol/default-monochrome-white.svg
Normal file
After Width: | Height: | Size: 5.2 KiB |
1
src/assets/icons/wol/default-transparent.svg
Normal file
After Width: | Height: | Size: 5.7 KiB |
1
src/assets/icons/wol/default.svg
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/assets/icons/wol/icon128.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/icons/wol/icon16.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
src/assets/icons/wol/icon48.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
1
src/assets/icons/wol/isolated-layout.svg
Normal file
After Width: | Height: | Size: 5.2 KiB |
1
src/assets/icons/wol/isolated-monochrome-black.svg
Normal 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 |
1
src/assets/icons/wol/isolated-monochrome-white.svg
Normal 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 |
158
src/assets/styles/common.css
Normal 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;
|
||||
}
|
|
@ -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
|
|
@ -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>;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 [];
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
import { useEffect, useReducer } from 'preact/hooks'
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
|
@ -1,33 +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
|
||||
|
||||
.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
|
|
@ -1,98 +0,0 @@
|
|||
import { chunk, groupBy, pickBy } from 'lodash';
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 URLs
|
||||
*/
|
||||
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); // we don't want it if it's empty
|
||||
},
|
||||
|
||||
/**
|
||||
* 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}`)
|
||||
.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
|
@ -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>
|
||||
}
|
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 4.4 KiB |
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"name": "Watch on LBRY",
|
||||
"version": "1.6.0",
|
||||
"permissions": [
|
||||
"https://www.youtube.com/watch?v=*",
|
||||
"https://www.youtube.com/channel/*",
|
||||
"https://invidio.us/channel/*",
|
||||
"https://invidio.us/watch?v=*",
|
||||
"https://api.lbry.com/*",
|
||||
"https://lbry.tv/*",
|
||||
"tabs",
|
||||
"storage"
|
||||
],
|
||||
"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": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"manifest_version": 2
|
||||
}
|
200
src/modules/crypto/index.ts
Normal 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
|
@ -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
|
@ -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
|
||||
}
|
82
src/modules/yt/urlCache.ts
Normal 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 }
|
||||
|
88
src/modules/yt/urlResolve.ts
Normal 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
|
||||
}
|
17
src/pages/YTtoLBRY/index.html
Normal 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>
|
86
src/pages/YTtoLBRY/main.tsx
Normal 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')!)
|
86
src/pages/YTtoLBRY/style.css
Normal 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;
|
||||
}
|
14
src/pages/import/index.html
Normal 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
|
@ -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')!)
|
35
src/pages/import/style.css
Normal 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;
|
||||
}
|
14
src/pages/popup/index.html
Normal 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
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||
.radio-label
|
||||
font-size: 1.1rem
|
||||
margin: 15px auto
|
||||
display: block
|
|
@ -1,32 +0,0 @@
|
|||
import { h, render } from 'preact';
|
||||
|
||||
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio';
|
||||
import { redirectDomains, useLbrySettings } from '../common/settings';
|
||||
|
||||
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
|
@ -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
|
||||
})
|
|
@ -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);
|
|
@ -1,43 +0,0 @@
|
|||
import { appRedirectUrl, parseProtocolUrl } from '../common/lbry-url';
|
||||
import { getSettingsAsync, redirectDomains } from '../common/settings';
|
||||
import { ytService } from '../common/yt';
|
||||
|
||||
function openApp(tabId: number, url: string) {
|
||||
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
|
||||
// Close tab if it lacks history and go back if it does
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
code: `if (window.history.length === 1) {
|
||||
window.close();
|
||||
} else {
|
||||
window.history.back();
|
||||
}`
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveYT(ytUrl: string) {
|
||||
const descriptor = ytService.getId(ytUrl);
|
||||
if (!descriptor) return; // can't parse YT url; may not be one
|
||||
|
||||
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('/');
|
||||
}
|
||||
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, { url: tabUrl }) => {
|
||||
const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect');
|
||||
const urlPrefix = redirectDomains[redirect].prefix;
|
||||
|
||||
if (!enabled || !changeInfo.url || !tabUrl) return;
|
||||
|
||||
const url = redirect === 'app' && tabUrl.match(/\b(https:\/\/lbry.tv|lbry:\/\/)/g) ? appRedirectUrl(tabUrl, { encode: true })
|
||||
: await resolveYT(tabUrl);
|
||||
|
||||
if (!url) return;
|
||||
if (redirect === 'app') {
|
||||
openApp(tabId, urlPrefix + url);
|
||||
return;
|
||||
}
|
||||
chrome.tabs.executeScript(tabId, { code: `location.replace("${urlPrefix + url}")` });
|
||||
});
|
363
src/scripts/ytContent.tsx
Normal file
|
@ -0,0 +1,363 @@
|
|||
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';
|
||||
|
||||
(async () => {
|
||||
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
|
||||
|
||||
interface Target {
|
||||
platform: TargetPlatform
|
||||
lbryPathname: string
|
||||
type: ResolveUrlTypes
|
||||
time: number | null
|
||||
}
|
||||
|
||||
interface Source {
|
||||
platform: SourcePlatform
|
||||
id: string
|
||||
type: ResolveUrlTypes
|
||||
url: URL
|
||||
time: number | null
|
||||
}
|
||||
|
||||
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={{
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 })
|
||||
|
||||
// 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
|
||||
|
||||
const lbryURL = getLbryUrlByTarget(target)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
})()
|
2
src/service-worker-entry-point.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
import './settings/background'
|
||||
import './scripts/background'
|
33
src/settings/background.ts
Normal 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
|
@ -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
|
||||
}) */
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
.body {
|
||||
display: block;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
border: 3px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
margin-bottom: 15px;
|
||||
border: 3px;
|
||||
padding: 10px;
|
||||
|
||||
}
|
||||
|
||||
.goButton {
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 15px 30px;
|
||||
color: teal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
text-decoration: none;
|
||||
font-size: 24px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: transparent;
|
||||
font-weight: 400;
|
||||
padding: 4px 15px;
|
||||
margin-right: 25px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
transition: 0.2s;
|
||||
overflow: hidden;
|
||||
margin-top: 90px;
|
||||
}
|
||||
|
||||
.goButton:hover{
|
||||
color: #075656;
|
||||
background: teal;
|
||||
box-shadow: 0 0px 10px teal, 0 0px 40px teal, 0 0 80px teal;
|
||||
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.1rem;
|
||||
margin: 15px auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.selectYtSubscriptions{
|
||||
color: teal;
|
||||
font: 36px Tahoma, Helvetica, Arial, Sans-Serif;
|
||||
text-shadow: 0px 2px 3px rgb(0, 0, 0);
|
||||
text-align: center;
|
||||
margin: 90px;
|
||||
}
|
||||
|
||||
.selectYtSubscriptions:hover{
|
||||
text-shadow: 0px 2px 3px rgb(0, 255, 255);
|
||||
transition: 0.2s;
|
||||
}
|
|
@ -1,15 +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 style="background-color:#474747;">
|
||||
<div class="container">
|
||||
<div id='root' />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,47 +0,0 @@
|
|||
import { Fragment, h, render } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
import { getSettingsAsync, redirectDomains } from '../common/settings';
|
||||
import { YTDescriptor, getFileContent, ytService } from '../common/yt';
|
||||
|
||||
/**
|
||||
* Parses OPML 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 lbryChannelsFromOpml(file: File): Promise<string[]> {
|
||||
const lbryUrls = await ytService.resolveById(...ytService.readOpml(await getFileContent(file))
|
||||
.map(url => ytService.getId(url))
|
||||
.filter((id): id is YTDescriptor => !!id));
|
||||
|
||||
const { redirect } = await getSettingsAsync('redirect');
|
||||
const urlPrefix = redirectDomains[redirect].prefix;
|
||||
return lbryUrls.map(channel => urlPrefix + channel);
|
||||
}
|
||||
|
||||
function YTtoLBRY() {
|
||||
const [file, setFile] = useState(null as File | null);
|
||||
const [lbryChannels, setLbryChannels] = useState([] as string[]);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
return <>
|
||||
<iframe width="100%" height="400px" allowFullScreen
|
||||
src="https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e" />
|
||||
<div class="selectYtSubscriptions">Select Youtube Subscriptions</div>
|
||||
<hr />
|
||||
<input type="file" className="PickFile" onChange={e =>
|
||||
setFile(e.currentTarget.files?.length ? e.currentTarget.files[0] : null)} />
|
||||
<hr />
|
||||
<input type="button" value="Start Conversion!" class="goButton" disabled={!file || isLoading} onClick={async () => {
|
||||
if (!file) return;
|
||||
setLoading(true);
|
||||
setLbryChannels(await lbryChannelsFromOpml(file));
|
||||
setLoading(false);
|
||||
}} />
|
||||
<ul>
|
||||
{lbryChannels.map((x, i) => <li key={i} children={<a href={x} children={x} />} />)}
|
||||
</ul>
|
||||
</>;
|
||||
}
|
||||
|
||||
render(<YTtoLBRY />, document.getElementById('root')!);
|
|
@ -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. */
|
||||
|
|