mirror of
https://github.com/movie-web/movie-web.git
synced 2025-09-13 15:53:23 +00:00
Compare commits
27 Commits
4.6.1
...
settings-m
Author | SHA1 | Date | |
---|---|---|---|
|
51e9c4d758 | ||
|
bcfadc8f60 | ||
|
a642abc783 | ||
|
558c6431fd | ||
|
2a0e46a97d | ||
|
227defd713 | ||
|
f1a8ff4bf8 | ||
|
162da3b22b | ||
|
35113ed522 | ||
|
94a003bce6 | ||
|
e0fec7ffa3 | ||
|
6f3c700dcb | ||
|
c3fec6c522 | ||
|
2ac0f2304f | ||
|
524e3f7358 | ||
|
135feab14c | ||
|
41949b0ab3 | ||
|
9f025bd12b | ||
|
810a12a097 | ||
|
c1f9382f50 | ||
|
8ccca76573 | ||
|
fc76a84bc8 | ||
|
ee047327a1 | ||
|
8e73751f48 | ||
|
8420bedb84 | ||
|
ba2f3fd359 | ||
|
852e6ff324 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "movie-web",
|
||||
"version": "4.6.1",
|
||||
"version": "4.6.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/movie-web/movie-web",
|
||||
"scripts": {
|
||||
@@ -29,7 +29,7 @@
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@ladjs/country-language": "^1.0.3",
|
||||
"@movie-web/providers": "^2.2.2",
|
||||
"@movie-web/providers": "^2.2.3",
|
||||
"@noble/hashes": "^1.3.3",
|
||||
"@plasmohq/messaging": "^0.6.1",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
@@ -44,7 +44,7 @@
|
||||
"focus-trap-react": "^10.2.3",
|
||||
"fscreen": "^1.2.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"hls.js": "^1.4.14",
|
||||
"hls.js": "^1.5.7",
|
||||
"i18next": "^23.7.11",
|
||||
"immer": "^10.0.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
@@ -68,6 +68,7 @@
|
||||
"semver": "^7.5.4",
|
||||
"slugify": "^1.6.6",
|
||||
"subsrt-ts": "^2.1.2",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
73
pnpm-lock.yaml
generated
73
pnpm-lock.yaml
generated
@@ -22,8 +22,8 @@ dependencies:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@movie-web/providers':
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
'@noble/hashes':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
@@ -67,8 +67,8 @@ dependencies:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
hls.js:
|
||||
specifier: ^1.4.14
|
||||
version: 1.4.14
|
||||
specifier: ^1.5.7
|
||||
version: 1.5.7
|
||||
i18next:
|
||||
specifier: ^23.7.11
|
||||
version: 23.7.11
|
||||
@@ -138,6 +138,9 @@ dependencies:
|
||||
subsrt-ts:
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2
|
||||
zod:
|
||||
specifier: ^3.22.4
|
||||
version: 3.22.4
|
||||
zustand:
|
||||
specifier: ^4.4.7
|
||||
version: 4.4.7(@types/react@18.2.45)(immer@10.0.3)(react@18.2.0)
|
||||
@@ -274,7 +277,7 @@ devDependencies:
|
||||
version: 0.5.9(prettier@3.1.1)
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^5.11.0
|
||||
version: 5.11.0(@rollup/wasm-node@4.12.1)
|
||||
version: 5.11.0(@rollup/wasm-node@4.13.0)
|
||||
tailwind-scrollbar:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5(tailwindcss@3.4.0)
|
||||
@@ -1942,8 +1945,8 @@ packages:
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/@movie-web/providers@2.2.2:
|
||||
resolution: {integrity: sha512-pTlErE5bdu+b68mUW2YAKOJKz2hwSx63auGAfTkGQ+0SHDMlCV9QQ8S8O9IoSsvdXps7/YlWJWOMX8pmKuYbPQ==}
|
||||
/@movie-web/providers@2.2.3:
|
||||
resolution: {integrity: sha512-0axy02Zzlk7Tvtalc/Ebv9u5vzUPVWmQm0Ts5+6l6KPU41JUdLnFgmOl0yf0lbNeHRNSTx5SDlvWcYNL8rgpyA==}
|
||||
dependencies:
|
||||
cheerio: 1.0.0-rc.12
|
||||
cookie: 0.6.0
|
||||
@@ -2068,7 +2071,7 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: false
|
||||
|
||||
/@rollup/plugin-babel@5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.12.1):
|
||||
/@rollup/plugin-babel@5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.13.0):
|
||||
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
peerDependencies:
|
||||
@@ -2081,36 +2084,36 @@ packages:
|
||||
dependencies:
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/helper-module-imports': 7.22.15
|
||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.12.1)
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.13.0)
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
dev: true
|
||||
|
||||
/@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.12.1):
|
||||
/@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.13.0):
|
||||
resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
peerDependencies:
|
||||
rollup: npm:@rollup/wasm-node
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.12.1)
|
||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.13.0)
|
||||
'@types/resolve': 1.17.1
|
||||
builtin-modules: 3.3.0
|
||||
deepmerge: 4.3.1
|
||||
is-module: 1.0.0
|
||||
resolve: 1.22.4
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
dev: true
|
||||
|
||||
/@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.12.1):
|
||||
/@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.13.0):
|
||||
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
|
||||
peerDependencies:
|
||||
rollup: npm:@rollup/wasm-node
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.12.1)
|
||||
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.13.0)
|
||||
magic-string: 0.25.9
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
dev: true
|
||||
|
||||
/@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.12.1):
|
||||
/@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.13.0):
|
||||
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
peerDependencies:
|
||||
@@ -2119,11 +2122,11 @@ packages:
|
||||
'@types/estree': 0.0.39
|
||||
estree-walker: 1.0.1
|
||||
picomatch: 2.3.1
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
dev: true
|
||||
|
||||
/@rollup/wasm-node@4.12.1:
|
||||
resolution: {integrity: sha512-5j3BVQEccCzCb8fkl++IbDgAsnlsKBPz049C4C//j5s3pFKxKGlybl63QApdJKl1fNLr7HIwQEJcBImQtA3ZHg==}
|
||||
/@rollup/wasm-node@4.13.0:
|
||||
resolution: {integrity: sha512-oFX11wzU7RTaiW06WBtRpzIVN/oaG0I3XkevNO0brBklYnY9zpLhTfksN4b+TdBt6CfXV/KdVhdWLbb0fQIR7A==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
@@ -4388,8 +4391,8 @@ packages:
|
||||
function-bind: 1.1.2
|
||||
dev: true
|
||||
|
||||
/hls.js@1.4.14:
|
||||
resolution: {integrity: sha512-UppQjyvPVclg+6t2KY/Rv03h0+bA5u6zwqVoz4LAC/L0fgYmIaCD7ZCrwe8WI1Gv01be1XL0QFsRbSdIHV/Wbw==}
|
||||
/hls.js@1.5.7:
|
||||
resolution: {integrity: sha512-Hnyf7ojTBtXHeOW1/t6wCBJSiK1WpoKF9yg7juxldDx8u3iswrkPt2wbOA/1NiwU4j27DSIVoIEJRAhcdMef/A==}
|
||||
dev: false
|
||||
|
||||
/hoist-non-react-statics@3.3.2:
|
||||
@@ -5112,7 +5115,7 @@ packages:
|
||||
'@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.6)
|
||||
'@babel/types': 7.23.6
|
||||
kleur: 4.1.5
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
unplugin: 1.5.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -6040,7 +6043,7 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: true
|
||||
|
||||
/rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.12.1):
|
||||
/rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.13.0):
|
||||
resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
|
||||
deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser
|
||||
peerDependencies:
|
||||
@@ -6048,12 +6051,12 @@ packages:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.23.5
|
||||
jest-worker: 26.6.2
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
serialize-javascript: 4.0.0
|
||||
terser: 5.19.3
|
||||
dev: true
|
||||
|
||||
/rollup-plugin-visualizer@5.11.0(@rollup/wasm-node@4.12.1):
|
||||
/rollup-plugin-visualizer@5.11.0(@rollup/wasm-node@4.13.0):
|
||||
resolution: {integrity: sha512-exM0Ms2SN3AgTzMeW7y46neZQcyLY7eKwWAop1ZoRTCZwyrIRdMMJ6JjToAJbML77X/9N8ZEpmXG4Z/Clb9k8g==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
@@ -6065,7 +6068,7 @@ packages:
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 2.3.1
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
source-map: 0.7.4
|
||||
yargs: 17.7.2
|
||||
dev: true
|
||||
@@ -7051,7 +7054,7 @@ packages:
|
||||
'@types/node': 20.10.5
|
||||
esbuild: 0.19.10
|
||||
postcss: 8.4.32
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
@@ -7313,9 +7316,9 @@ packages:
|
||||
'@babel/core': 7.23.6
|
||||
'@babel/preset-env': 7.23.6(@babel/core@7.23.6)
|
||||
'@babel/runtime': 7.23.6
|
||||
'@rollup/plugin-babel': 5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.12.1)
|
||||
'@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.12.1)
|
||||
'@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.12.1)
|
||||
'@rollup/plugin-babel': 5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.13.0)
|
||||
'@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.13.0)
|
||||
'@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.13.0)
|
||||
'@surma/rollup-plugin-off-main-thread': 2.2.3
|
||||
ajv: 8.12.0
|
||||
common-tags: 1.8.2
|
||||
@@ -7324,8 +7327,8 @@ packages:
|
||||
glob: 7.2.3
|
||||
lodash: 4.17.21
|
||||
pretty-bytes: 5.6.0
|
||||
rollup: /@rollup/wasm-node@4.12.1
|
||||
rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.12.1)
|
||||
rollup: /@rollup/wasm-node@4.13.0
|
||||
rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.13.0)
|
||||
source-map: 0.8.0-beta.0
|
||||
stringify-object: 3.3.0
|
||||
strip-comments: 2.0.1
|
||||
@@ -7526,6 +7529,10 @@ packages:
|
||||
engines: {node: '>=12.20'}
|
||||
dev: true
|
||||
|
||||
/zod@3.22.4:
|
||||
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||
dev: false
|
||||
|
||||
/zustand@4.4.7(@types/react@18.2.45)(immer@10.0.3)(react@18.2.0):
|
||||
resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
|
@@ -82,7 +82,7 @@ export const locales = {
|
||||
ko,
|
||||
sl,
|
||||
ta,
|
||||
"zh-HANT": zhhant,
|
||||
"zh-Hant": zhhant,
|
||||
is,
|
||||
ru,
|
||||
gl,
|
||||
|
@@ -100,7 +100,8 @@
|
||||
"onboarding": "Setup",
|
||||
"pagetitle": "{{title}} - movie-web",
|
||||
"register": "Register",
|
||||
"settings": "Settings"
|
||||
"settings": "Settings",
|
||||
"migration": "Migration"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
@@ -153,6 +154,27 @@
|
||||
"show": "Show"
|
||||
}
|
||||
},
|
||||
"migration": {
|
||||
"start": {
|
||||
"title": "Migrate your data",
|
||||
"explainer": "If you wish to migrate or backup your data, you can do so using the options below. This will allow you to keep your data when you switch backend servers.",
|
||||
"options": {
|
||||
"or": "or",
|
||||
"direct": {
|
||||
"description": "This will directly migrate your data to the new server. This is the fastest option. <br /><br />This option allows you to keep your passphrase the same!",
|
||||
"title": "Direct migration",
|
||||
"quality": "Easiest and fastest",
|
||||
"action": "Transfer data"
|
||||
},
|
||||
"download": {
|
||||
"description": "This will download your data to your device. You can then upload it to the new server or just keep it for safekeeping.",
|
||||
"title": "Download data",
|
||||
"quality": "More technical",
|
||||
"action": "Download data"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"banner": {
|
||||
"offline": "Check your internet connection"
|
||||
@@ -216,6 +238,7 @@
|
||||
"start": {
|
||||
"explainer": "To get the best streams possible, you will need to choose which streaming method you want to use.",
|
||||
"options": {
|
||||
"or": "or",
|
||||
"default": {
|
||||
"text": "I don't want good quality streams,<0 /> <1>use the default setup</1>"
|
||||
},
|
||||
|
@@ -117,8 +117,7 @@
|
||||
"loading": "Wczytywanie...",
|
||||
"noResults": "Nie mogliśmy niczego znaleźć!",
|
||||
"placeholder": {
|
||||
"default": "Co chciałbyś obejrzeć?",
|
||||
"extra": []
|
||||
"default": "Co chciałbyś obejrzeć?"
|
||||
},
|
||||
"sectionTitle": "Wyniki wyszukiwania"
|
||||
},
|
||||
@@ -131,11 +130,15 @@
|
||||
},
|
||||
"morning": {
|
||||
"default": "Co chciałbyś obejrzeć dziś rano?",
|
||||
"extra": ["Słyszałem że „Przed wschodem słońca” jest dobre"]
|
||||
"extra": [
|
||||
"Słyszałem że „Przed wschodem słońca” jest dobre"
|
||||
]
|
||||
},
|
||||
"night": {
|
||||
"default": "Co chciałbyś obejrzeć dziś wieczorem?",
|
||||
"extra": ["Zmęczony? Słyszałem że „Egzorcysta” jest dobry."]
|
||||
"extra": [
|
||||
"Zmęczony? Słyszałem że „Egzorcysta” jest dobry."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -176,7 +179,7 @@
|
||||
"back": "Wstecz",
|
||||
"explainer": "Korzystając z rozszerzenia przeglądarki, możesz uzyskać najlepsze strumienie. Wystarczy prosta instalacja.",
|
||||
"explainerIos": "Niestety, rozszerzenie przeglądarki nie jest obsługiwane w systemie iOS, naciśnij <bold>Wstecz</bold>, aby wybrać inną opcję.",
|
||||
"extensionHelp": "Jeżeli zainstalowałeś rozszerzenie, ale nie zostało ono wykryte. <bold>Otwórz rozszerzenie za pomocą menu rozszerzeń przeglądarki</bold> i postępuj zgodnie z instrukcjami wyświetlanymi na ekranie.",
|
||||
"extensionHelp": "Jeżeli zainstalowałeś rozszerzenie, ale nie zostało ono wykryte, <bold>otwórz rozszerzenie za pomocą menu rozszerzeń przeglądarki</bold> i postępuj zgodnie z instrukcjami wyświetlanymi na ekranie.",
|
||||
"linkChrome": "Zainstaluj rozszerzenie na Chrome",
|
||||
"linkFirefox": "Zainstaluj rozszerzenie na Firefox",
|
||||
"notDetecting": "Zainstalowano na Chrome, ale się nie wyświetla? Spróbuj odświeżyć stronę!",
|
||||
@@ -207,7 +210,7 @@
|
||||
"title": "Stwórzmy nowe proxy"
|
||||
},
|
||||
"start": {
|
||||
"explainer": "Aby uzyskać najlepsze transmisje strumieniowe. Będziesz musiał wybrać metodę strumieniowania, której chcesz użyć.",
|
||||
"explainer": "Aby uzyskać najlepsze transmisje strumieniowe, będziesz musiał wybrać metodę strumieniowania której chcesz użyć.",
|
||||
"options": {
|
||||
"default": {
|
||||
"text": "Nie chcę dobrej jakości strumieni, <0 /> <1>użyj domyślnej konfiguracji</1>"
|
||||
@@ -524,8 +527,8 @@
|
||||
}
|
||||
},
|
||||
"subtitles": {
|
||||
"backgroundLabel": "Krycie tła",
|
||||
"backgroundBlurLabel": "Rozmycie tła",
|
||||
"backgroundLabel": "Krycie tła",
|
||||
"colorLabel": "Kolor",
|
||||
"previewQuote": "Nie wolno mi się bać. Strach zabija myślenie.",
|
||||
"textSizeLabel": "Rozmiar czcionki",
|
||||
|
@@ -57,6 +57,8 @@
|
||||
},
|
||||
"host": "lawa ilo sina li <0>{{hostname}}</0> - ona li pona tawa sina la sina ken pali e lipu open",
|
||||
"no": "o weka",
|
||||
"noHost": "lawa ilo ni li open ala li nasin ala la, sina ken ala pali e lipu open",
|
||||
"noHostTitle": "lawa ilo li open ala a!",
|
||||
"title": "lawa ilo ni li pona tawa sina anu seme?",
|
||||
"yes": "lawa ilo ni li pona"
|
||||
},
|
||||
@@ -79,7 +81,8 @@
|
||||
},
|
||||
"footer": {
|
||||
"legal": {
|
||||
"disclaimer": "o sona e ni:"
|
||||
"disclaimer": "o sona e ni:",
|
||||
"disclaimerText": "ilo Muwi-We li mama ala e ijo sitelen. ona li toki taso tawa ilo ante. utala nasin li lon la o toki tawa ona pi ilo ante. sitelen ale li tan ala ilo Muwi-We"
|
||||
},
|
||||
"links": {
|
||||
"discord": "kulupu Siko",
|
||||
@@ -117,22 +120,33 @@
|
||||
"noResults": "ijo li lon ala a!",
|
||||
"placeholder": {
|
||||
"default": "sina wile lukin e seme?",
|
||||
"extra": []
|
||||
"extra": [
|
||||
"sina wile alasa e seme?",
|
||||
"sina wile lukin e seme?",
|
||||
"sitelen nanpa wan sina li seme?",
|
||||
"sitelen nanpa wan sina li seme?"
|
||||
]
|
||||
},
|
||||
"sectionTitle": "mi lukin e ni:"
|
||||
},
|
||||
"titles": {
|
||||
"day": {
|
||||
"default": "tenpo suno ni la sina wile lukin e seme?",
|
||||
"extra": ["sina pilin alasa la o lukin e sitelen Jurassic Park"]
|
||||
"extra": [
|
||||
"sina pilin alasa la o lukin e sitelen Jurassic Park"
|
||||
]
|
||||
},
|
||||
"morning": {
|
||||
"default": "tenpo sin ni la sina wile lukin e seme?",
|
||||
"extra": ["ken la sitelen Before Sunrise li pona"]
|
||||
"extra": [
|
||||
"ken la sitelen Before Sunrise li pona"
|
||||
]
|
||||
},
|
||||
"night": {
|
||||
"default": "tenpo pimeja ni la sina wile lukin e seme?",
|
||||
"extra": ["sina pilin lape anu seme? o alasa lukin e sitelen Exorcist"]
|
||||
"extra": [
|
||||
"sina pilin lape anu seme? o alasa lukin e sitelen Exorcist"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -163,6 +177,9 @@
|
||||
"title": "mi ken ala lukin e lipu ona"
|
||||
},
|
||||
"onboarding": {
|
||||
"defaultConfirm": {
|
||||
"cancel": "ala"
|
||||
},
|
||||
"start": {
|
||||
"title": "o open e ilo Muwi-We"
|
||||
}
|
||||
|
@@ -21,9 +21,7 @@ export function verifyValidMnemonic(mnemonic: string) {
|
||||
return validateMnemonic(mnemonic, wordlist);
|
||||
}
|
||||
|
||||
export async function keysFromMnemonic(mnemonic: string): Promise<Keys> {
|
||||
const seed = await seedFromMnemonic(mnemonic);
|
||||
|
||||
export async function keysFromSeed(seed: Uint8Array): Promise<Keys> {
|
||||
const { privateKey, publicKey } = forge.pki.ed25519.generateKeyPair({
|
||||
seed,
|
||||
});
|
||||
@@ -35,6 +33,12 @@ export async function keysFromMnemonic(mnemonic: string): Promise<Keys> {
|
||||
};
|
||||
}
|
||||
|
||||
export async function keysFromMnemonic(mnemonic: string): Promise<Keys> {
|
||||
const seed = await seedFromMnemonic(mnemonic);
|
||||
|
||||
return keysFromSeed(seed);
|
||||
}
|
||||
|
||||
export function genMnemonic(): string {
|
||||
return generateMnemonic(wordlist);
|
||||
}
|
||||
|
@@ -51,3 +51,15 @@ export async function downloadCaption(
|
||||
downloadCache.set(caption.url, output, expirySeconds);
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the WebVTT content. No different than a simple
|
||||
* get request with a cache.
|
||||
*/
|
||||
export async function downloadWebVTT(url: string): Promise<string> {
|
||||
const cached = downloadCache.get(url);
|
||||
if (cached) return cached;
|
||||
|
||||
const data = await fetch(url).then((v) => v.text());
|
||||
return data;
|
||||
}
|
||||
|
@@ -64,6 +64,8 @@ export enum Icons {
|
||||
DONATION = "donation",
|
||||
CIRCLE_QUESTION = "circle_question",
|
||||
BRUSH = "brush",
|
||||
CLOUD_ARROW_UP = "cloud_arrow_up",
|
||||
FILE_ARROW_DOWN = "file_arrow_down",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
@@ -134,6 +136,8 @@ const iconList: Record<Icons, string> = {
|
||||
donation: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M163.9 136.9c-29.4-29.8-29.4-78.2 0-108s77-29.8 106.4 0l17.7 18 17.7-18c29.4-29.8 77-29.8 106.4 0s29.4 78.2 0 108L310.5 240.1c-6.2 6.3-14.3 9.4-22.5 9.4s-16.3-3.1-22.5-9.4L163.9 136.9zM568.2 336.3c13.1 17.8 9.3 42.8-8.5 55.9L433.1 485.5c-23.4 17.2-51.6 26.5-80.7 26.5H192 32c-17.7 0-32-14.3-32-32V416c0-17.7 14.3-32 32-32H68.8l44.9-36c22.7-18.2 50.9-28 80-28H272h16 64c17.7 0 32 14.3 32 32s-14.3 32-32 32H288 272c-8.8 0-16 7.2-16 16s7.2 16 16 16H392.6l119.7-88.2c17.8-13.1 42.8-9.3 55.9 8.5zM193.6 384l0 0-.9 0c.3 0 .6 0 .9 0z"/></svg>`,
|
||||
circle_question: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
|
||||
brush: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M162.4 6c-1.5-3.6-5-6-8.9-6h-19c-3.9 0-7.5 2.4-8.9 6L104.9 57.7c-3.2 8-14.6 8-17.8 0L66.4 6c-1.5-3.6-5-6-8.9-6H48C21.5 0 0 21.5 0 48V224v22.4V256H9.6 374.4 384v-9.6V224 48c0-26.5-21.5-48-48-48H230.5c-3.9 0-7.5 2.4-8.9 6L200.9 57.7c-3.2 8-14.6 8-17.8 0L162.4 6zM0 288v32c0 35.3 28.7 64 64 64h64v64c0 35.3 28.7 64 64 64s64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V288H0zM192 432a16 16 0 1 1 0 32 16 16 0 1 1 0-32z" fill="currentColor"/></svg>`,
|
||||
cloud_arrow_up: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128H144zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39V392c0 13.3 10.7 24 24 24s24-10.7 24-24V257.9l39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z" fill="currentColor"/></svg>`,
|
||||
file_arrow_down: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM216 232V334.1l31-31c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-72 72c-9.4 9.4-24.6 9.4-33.9 0l-72-72c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l31 31V232c0-13.3 10.7-24 24-24s24 10.7 24 24z" fill="currentColor"/></svg>`,
|
||||
};
|
||||
|
||||
function ChromeCastButton() {
|
||||
|
9
src/components/layout/VerticalLine.tsx
Normal file
9
src/components/layout/VerticalLine.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
export function VerticalLine(props: { className?: string }) {
|
||||
return (
|
||||
<div className={classNames("w-full grid justify-center", props.className)}>
|
||||
<div className="w-px h-10 bg-onboarding-divider" />
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -122,9 +122,16 @@ export function CaptionsView({ id }: { id: string }) {
|
||||
>(null);
|
||||
const { selectCaptionById, disable } = useCaptions();
|
||||
const captionList = usePlayerStore((s) => s.captionList);
|
||||
const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList);
|
||||
|
||||
const captions = useMemo(
|
||||
() =>
|
||||
captionList.length !== 0 ? captionList : getHlsCaptionList?.() ?? [],
|
||||
[captionList, getHlsCaptionList],
|
||||
);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const subtitleList = useSubtitleList(captionList, searchQuery);
|
||||
const subtitleList = useSubtitleList(captions, searchQuery);
|
||||
|
||||
const [downloadReq, startDownload] = useAsyncFn(
|
||||
async (captionId: string) => {
|
||||
|
@@ -67,6 +67,11 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
let preferenceQuality: SourceQuality | null = null;
|
||||
let lastVolume = 1;
|
||||
|
||||
const languagePromises = new Map<
|
||||
string,
|
||||
(value: void | PromiseLike<void>) => void
|
||||
>();
|
||||
|
||||
function reportLevels() {
|
||||
if (!hls) return;
|
||||
const levels = hls.levels;
|
||||
@@ -133,6 +138,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
},
|
||||
},
|
||||
},
|
||||
renderTextTracksNatively: false,
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
console.error("HLS error", data);
|
||||
@@ -173,6 +179,16 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]);
|
||||
emit("changedquality", quality);
|
||||
});
|
||||
hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, () => {
|
||||
for (const [lang, resolve] of languagePromises) {
|
||||
const track = hls?.subtitleTracks.find((t) => t.lang === lang);
|
||||
if (track) {
|
||||
resolve();
|
||||
languagePromises.delete(lang);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hls.attachMedia(vid);
|
||||
@@ -413,5 +429,40 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||
setPlaybackRate(rate) {
|
||||
if (videoElement) videoElement.playbackRate = rate;
|
||||
},
|
||||
getCaptionList() {
|
||||
return (
|
||||
hls?.subtitleTracks.map((track) => {
|
||||
return {
|
||||
id: track.id.toString(),
|
||||
language: track.lang ?? "unknown",
|
||||
url: track.url,
|
||||
needsProxy: false,
|
||||
hls: true,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
},
|
||||
getSubtitleTracks() {
|
||||
return hls?.subtitleTracks ?? [];
|
||||
},
|
||||
async setSubtitlePreference(lang) {
|
||||
// default subtitles are already loaded by hls.js
|
||||
const track = hls?.subtitleTracks.find((t) => t.lang === lang);
|
||||
if (track?.details !== undefined) return Promise.resolve();
|
||||
|
||||
// need to wait a moment before hls loads the subtitles
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
languagePromises.set(lang, resolve);
|
||||
|
||||
// reject after some time, if hls.js fails to load the subtitles
|
||||
// for any reason
|
||||
setTimeout(() => {
|
||||
reject();
|
||||
languagePromises.delete(lang);
|
||||
}, 5000);
|
||||
});
|
||||
hls?.setSubtitleOption({ lang });
|
||||
return promise;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -274,5 +274,14 @@ export function makeChromecastDisplayInterface(
|
||||
playbackRate = rate;
|
||||
setSource();
|
||||
},
|
||||
getCaptionList() {
|
||||
return [];
|
||||
},
|
||||
getSubtitleTracks() {
|
||||
return [];
|
||||
},
|
||||
async setSubtitlePreference() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { MediaPlaylist } from "hls.js";
|
||||
|
||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||
import { CaptionListItem } from "@/stores/player/slices/source";
|
||||
import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities";
|
||||
import { Listener } from "@/utils/events";
|
||||
|
||||
@@ -70,4 +73,7 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
||||
setMeta(meta: DisplayMeta): void;
|
||||
setCaption(caption: DisplayCaption | null): void;
|
||||
getType(): DisplayType;
|
||||
getCaptionList(): CaptionListItem[];
|
||||
getSubtitleTracks(): MediaPlaylist[];
|
||||
setSubtitlePreference(lang: string): Promise<void>;
|
||||
}
|
||||
|
@@ -1,9 +1,16 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import subsrt from "subsrt-ts";
|
||||
|
||||
import { downloadCaption } from "@/backend/helpers/subs";
|
||||
import { downloadCaption, downloadWebVTT } from "@/backend/helpers/subs";
|
||||
import { Caption } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
|
||||
import {
|
||||
filterDuplicateCaptionCues,
|
||||
parseVttSubtitles,
|
||||
} from "../utils/captions";
|
||||
|
||||
export function useCaptions() {
|
||||
const setLanguage = useSubtitleStore((s) => s.setLanguage);
|
||||
const enabled = useSubtitleStore((s) => s.enabled);
|
||||
@@ -12,32 +19,85 @@ export function useCaptions() {
|
||||
);
|
||||
const setCaption = usePlayerStore((s) => s.setCaption);
|
||||
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
|
||||
|
||||
const captionList = usePlayerStore((s) => s.captionList);
|
||||
const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList);
|
||||
|
||||
const getSubtitleTracks = usePlayerStore((s) => s.display?.getSubtitleTracks);
|
||||
const setSubtitlePreference = usePlayerStore(
|
||||
(s) => s.display?.setSubtitlePreference,
|
||||
);
|
||||
|
||||
const captions = useMemo(
|
||||
() =>
|
||||
captionList.length !== 0 ? captionList : getHlsCaptionList?.() ?? [],
|
||||
[captionList, getHlsCaptionList],
|
||||
);
|
||||
|
||||
const selectCaptionById = useCallback(
|
||||
async (captionId: string) => {
|
||||
const caption = captionList.find((v) => v.id === captionId);
|
||||
const caption = captions.find((v) => v.id === captionId);
|
||||
if (!caption) return;
|
||||
const srtData = await downloadCaption(caption);
|
||||
setCaption({
|
||||
|
||||
const captionToSet: Caption = {
|
||||
id: caption.id,
|
||||
language: caption.language,
|
||||
srtData,
|
||||
url: caption.url,
|
||||
});
|
||||
srtData: "",
|
||||
};
|
||||
|
||||
if (!caption.hls) {
|
||||
const srtData = await downloadCaption(caption);
|
||||
captionToSet.srtData = srtData;
|
||||
} else {
|
||||
// request a language change to hls, so it can load the subtitles
|
||||
await setSubtitlePreference?.(caption.language);
|
||||
const track = getSubtitleTracks?.().find(
|
||||
(t) => t.id.toString() === caption.id && t.details !== undefined,
|
||||
);
|
||||
if (!track) return;
|
||||
|
||||
const fragments =
|
||||
track.details?.fragments?.filter(
|
||||
(frag) => frag !== null && frag.url !== null,
|
||||
) ?? [];
|
||||
|
||||
const vttCaptions = (
|
||||
await Promise.all(
|
||||
fragments.map(async (frag) => {
|
||||
const vtt = await downloadWebVTT(frag.url);
|
||||
return parseVttSubtitles(vtt);
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
|
||||
const filtered = filterDuplicateCaptionCues(vttCaptions);
|
||||
|
||||
const srtData = subsrt.build(filtered, { format: "srt" });
|
||||
captionToSet.srtData = srtData;
|
||||
}
|
||||
|
||||
setCaption(captionToSet);
|
||||
resetSubtitleSpecificSettings();
|
||||
setLanguage(caption.language);
|
||||
},
|
||||
[setLanguage, captionList, setCaption, resetSubtitleSpecificSettings],
|
||||
[
|
||||
setLanguage,
|
||||
captions,
|
||||
setCaption,
|
||||
resetSubtitleSpecificSettings,
|
||||
getSubtitleTracks,
|
||||
setSubtitlePreference,
|
||||
],
|
||||
);
|
||||
|
||||
const selectLanguage = useCallback(
|
||||
async (language: string) => {
|
||||
const caption = captionList.find((v) => v.language === language);
|
||||
const caption = captions.find((v) => v.language === language);
|
||||
if (!caption) return;
|
||||
return selectCaptionById(caption.id);
|
||||
},
|
||||
[captionList, selectCaptionById],
|
||||
[captions, selectCaptionById],
|
||||
);
|
||||
|
||||
const disable = useCallback(async () => {
|
||||
|
@@ -32,6 +32,9 @@ export function MediaSession() {
|
||||
|
||||
const updatePositionState = useCallback(
|
||||
(position: number) => {
|
||||
// If the browser doesn't support setPositionState, return
|
||||
if (typeof navigator.mediaSession.setPositionState !== "function") return;
|
||||
|
||||
// If the updated position needs to be buffered, queue an update
|
||||
if (position > data.progress.buffered) {
|
||||
shouldUpdatePositionState.current = true;
|
||||
|
@@ -50,12 +50,30 @@ export function convertSubtitlesToSrt(text: string): string {
|
||||
return srt;
|
||||
}
|
||||
|
||||
export function filterDuplicateCaptionCues(cues: ContentCaption[]) {
|
||||
return cues.reduce((acc: ContentCaption[], cap: ContentCaption) => {
|
||||
const lastCap = acc[acc.length - 1];
|
||||
const isSameAsLast =
|
||||
lastCap?.start === cap.start &&
|
||||
lastCap?.end === cap.end &&
|
||||
lastCap?.content === cap.content;
|
||||
if (lastCap === undefined || !isSameAsLast) {
|
||||
acc.push(cap);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function parseVttSubtitles(vtt: string) {
|
||||
return parse(vtt).filter((cue) => cue.type === "caption") as CaptionCueType[];
|
||||
}
|
||||
|
||||
export function parseSubtitles(
|
||||
text: string,
|
||||
_language?: string,
|
||||
): CaptionCueType[] {
|
||||
const vtt = convertSubtitlesToVtt(text);
|
||||
return parse(vtt).filter((cue) => cue.type === "caption") as CaptionCueType[];
|
||||
return parseVttSubtitles(vtt);
|
||||
}
|
||||
|
||||
function stringToBase64(input: string): string {
|
||||
|
125
src/hooks/auth/useMigration.ts
Normal file
125
src/hooks/auth/useMigration.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { SessionResponse } from "@/backend/accounts/auth";
|
||||
import { bookmarkMediaToInput } from "@/backend/accounts/bookmarks";
|
||||
import {
|
||||
base64ToBuffer,
|
||||
bytesToBase64,
|
||||
bytesToBase64Url,
|
||||
encryptData,
|
||||
keysFromMnemonic,
|
||||
keysFromSeed,
|
||||
signChallenge,
|
||||
} from "@/backend/accounts/crypto";
|
||||
import { importBookmarks, importProgress } from "@/backend/accounts/import";
|
||||
import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login";
|
||||
import { progressMediaItemToInputs } from "@/backend/accounts/progress";
|
||||
import {
|
||||
getRegisterChallengeToken,
|
||||
registerAccount,
|
||||
} from "@/backend/accounts/register";
|
||||
import { removeSession } from "@/backend/accounts/sessions";
|
||||
import { getSettings } from "@/backend/accounts/settings";
|
||||
import {
|
||||
UserResponse,
|
||||
getBookmarks,
|
||||
getProgress,
|
||||
getUser,
|
||||
} from "@/backend/accounts/user";
|
||||
import { useAuthData } from "@/hooks/auth/useAuthData";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import { AccountWithToken, useAuthStore } from "@/stores/auth";
|
||||
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
|
||||
|
||||
export interface RegistrationData {
|
||||
recaptchaToken?: string;
|
||||
mnemonic: string;
|
||||
userData: {
|
||||
device: string;
|
||||
profile: {
|
||||
colorA: string;
|
||||
colorB: string;
|
||||
icon: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface LoginData {
|
||||
mnemonic: string;
|
||||
userData: {
|
||||
device: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useMigration() {
|
||||
const currentAccount = useAuthStore((s) => s.account);
|
||||
const progress = useProgressStore((s) => s.items);
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
const { login: userDataLogin } = useAuthData();
|
||||
|
||||
const importData = async (
|
||||
backendUrl: string,
|
||||
account: AccountWithToken,
|
||||
progressItems: Record<string, ProgressMediaItem>,
|
||||
bookmarkItems: Record<string, BookmarkMediaItem>,
|
||||
) => {
|
||||
if (
|
||||
Object.keys(progressItems).length === 0 &&
|
||||
Object.keys(bookmarkItems).length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progressInputs = Object.entries(progressItems).flatMap(
|
||||
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
|
||||
);
|
||||
|
||||
const bookmarkInputs = Object.entries(bookmarkItems).map(([tmdbId, item]) =>
|
||||
bookmarkMediaToInput(tmdbId, item),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
importProgress(backendUrl, account, progressInputs),
|
||||
importBookmarks(backendUrl, account, bookmarkInputs),
|
||||
]);
|
||||
};
|
||||
|
||||
const migrate = useCallback(
|
||||
async (backendUrl: string, recaptchaToken: string) => {
|
||||
if (!currentAccount) return;
|
||||
|
||||
const { challenge } = await getRegisterChallengeToken(
|
||||
backendUrl,
|
||||
recaptchaToken,
|
||||
);
|
||||
const keys = await keysFromSeed(base64ToBuffer(currentAccount.seed));
|
||||
const signature = await signChallenge(keys, challenge);
|
||||
const registerResult = await registerAccount(backendUrl, {
|
||||
challenge: {
|
||||
code: challenge,
|
||||
signature,
|
||||
},
|
||||
publicKey: bytesToBase64Url(keys.publicKey),
|
||||
device: await encryptData(currentAccount.deviceName, keys.seed),
|
||||
profile: currentAccount.profile,
|
||||
});
|
||||
|
||||
const account = await userDataLogin(
|
||||
registerResult,
|
||||
registerResult.user,
|
||||
registerResult.session,
|
||||
bytesToBase64(keys.seed),
|
||||
);
|
||||
|
||||
await importData(backendUrl, account, progress, bookmarks);
|
||||
|
||||
return account;
|
||||
},
|
||||
[currentAccount, userDataLogin, bookmarks, progress],
|
||||
);
|
||||
|
||||
return {
|
||||
migrate,
|
||||
};
|
||||
}
|
103
src/hooks/useSettingsExport.ts
Normal file
103
src/hooks/useSettingsExport.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { Settings } from "@/hooks/useSettingsImport";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useLanguageStore } from "@/stores/language";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { useQualityStore } from "@/stores/quality";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { useVolumeStore } from "@/stores/volume";
|
||||
|
||||
export function useSettingsExport() {
|
||||
const authStore = useAuthStore();
|
||||
const bookmarksStore = useBookmarkStore();
|
||||
const languageStore = useLanguageStore();
|
||||
const preferencesStore = usePreferencesStore();
|
||||
const progressStore = useProgressStore();
|
||||
const qualityStore = useQualityStore();
|
||||
const subtitleStore = useSubtitleStore();
|
||||
const themeStore = useThemeStore();
|
||||
const volumeStore = useVolumeStore();
|
||||
|
||||
const collect = useCallback(
|
||||
(includeAuth: boolean): Settings => {
|
||||
return {
|
||||
auth: {
|
||||
account: includeAuth ? authStore.account : undefined,
|
||||
backendUrl: authStore.backendUrl,
|
||||
proxySet: authStore.proxySet,
|
||||
},
|
||||
bookmarks: {
|
||||
bookmarks: bookmarksStore.bookmarks,
|
||||
},
|
||||
language: {
|
||||
language: languageStore.language,
|
||||
},
|
||||
preferences: {
|
||||
enableThumbnails: preferencesStore.enableThumbnails,
|
||||
},
|
||||
progress: {
|
||||
items: progressStore.items,
|
||||
},
|
||||
quality: {
|
||||
quality: {
|
||||
automaticQuality: qualityStore.quality.automaticQuality,
|
||||
lastChosenQuality: qualityStore.quality.lastChosenQuality,
|
||||
},
|
||||
},
|
||||
subtitles: {
|
||||
lastSelectedLanguage: subtitleStore.lastSelectedLanguage,
|
||||
styling: {
|
||||
backgroundBlur: subtitleStore.styling.backgroundBlur,
|
||||
backgroundOpacity: subtitleStore.styling.backgroundOpacity,
|
||||
color: subtitleStore.styling.color,
|
||||
size: subtitleStore.styling.size,
|
||||
},
|
||||
overrideCasing: subtitleStore.overrideCasing,
|
||||
delay: subtitleStore.delay,
|
||||
},
|
||||
theme: {
|
||||
theme: themeStore.theme,
|
||||
},
|
||||
volume: {
|
||||
volume: volumeStore.volume,
|
||||
},
|
||||
};
|
||||
},
|
||||
[
|
||||
authStore,
|
||||
bookmarksStore,
|
||||
languageStore,
|
||||
preferencesStore,
|
||||
progressStore,
|
||||
qualityStore,
|
||||
subtitleStore,
|
||||
themeStore,
|
||||
volumeStore,
|
||||
],
|
||||
);
|
||||
|
||||
const exportSettings = useCallback(
|
||||
(includeAuth: boolean) => {
|
||||
const output = JSON.stringify(collect(includeAuth), null, 2);
|
||||
|
||||
const blob = new Blob([output], { type: "application/json" });
|
||||
const elem = window.document.createElement("a");
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
|
||||
const date = new Date();
|
||||
elem.download = `movie-web settings - ${
|
||||
date.toISOString().split("T")[0]
|
||||
}.json`;
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
},
|
||||
[collect],
|
||||
);
|
||||
|
||||
return exportSettings;
|
||||
}
|
234
src/hooks/useSettingsImport.ts
Normal file
234
src/hooks/useSettingsImport.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useCallback } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useLanguageStore } from "@/stores/language";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { useQualityStore } from "@/stores/quality";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { useVolumeStore } from "@/stores/volume";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
auth: z.object({
|
||||
account: z
|
||||
.object({
|
||||
profile: z.object({
|
||||
colorA: z.string(),
|
||||
colorB: z.string(),
|
||||
icon: z.string(),
|
||||
}),
|
||||
sessionId: z.string(),
|
||||
userId: z.string(),
|
||||
token: z.string(),
|
||||
seed: z.string(),
|
||||
deviceName: z.string(),
|
||||
})
|
||||
.nullish(),
|
||||
backendUrl: z.string().nullable(),
|
||||
proxySet: z.array(z.string()).nullable(),
|
||||
}),
|
||||
bookmarks: z.object({
|
||||
bookmarks: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
year: z.number().optional(),
|
||||
poster: z.string().optional(),
|
||||
type: z.enum(["show", "movie"]),
|
||||
updatedAt: z.number(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
language: z.object({
|
||||
language: z.string(),
|
||||
}),
|
||||
preferences: z.object({
|
||||
enableThumbnails: z.boolean(),
|
||||
}),
|
||||
progress: z.object({
|
||||
items: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
year: z.number().optional(),
|
||||
poster: z.string().optional(),
|
||||
type: z.enum(["show", "movie"]),
|
||||
updatedAt: z.number(),
|
||||
progress: z
|
||||
.object({
|
||||
watched: z.number(),
|
||||
duration: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
seasons: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
number: z.number(),
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
episodes: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
number: z.number(),
|
||||
id: z.string(),
|
||||
seasonId: z.string(),
|
||||
updatedAt: z.number(),
|
||||
progress: z.object({
|
||||
watched: z.number(),
|
||||
duration: z.number(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
quality: z.object({
|
||||
quality: z.object({
|
||||
automaticQuality: z.boolean(),
|
||||
lastChosenQuality: z
|
||||
.enum(["unknown", "360", "480", "720", "1080", "4k"])
|
||||
.nullable(),
|
||||
}),
|
||||
}),
|
||||
subtitles: z.object({
|
||||
lastSelectedLanguage: z.string().nullable(),
|
||||
styling: z.object({
|
||||
backgroundBlur: z.number(),
|
||||
backgroundOpacity: z.number(),
|
||||
color: z.string(),
|
||||
size: z.number(),
|
||||
}),
|
||||
overrideCasing: z.boolean(),
|
||||
delay: z.number(),
|
||||
}),
|
||||
theme: z.object({
|
||||
theme: z.string().nullable(),
|
||||
}),
|
||||
volume: z.object({
|
||||
volume: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
const settingsPartialSchema = settingsSchema.partial();
|
||||
|
||||
export type Settings = z.infer<typeof settingsSchema>;
|
||||
|
||||
export function useSettingsImport() {
|
||||
const authStore = useAuthStore();
|
||||
const bookmarksStore = useBookmarkStore();
|
||||
const languageStore = useLanguageStore();
|
||||
const preferencesStore = usePreferencesStore();
|
||||
const progressStore = useProgressStore();
|
||||
const qualityStore = useQualityStore();
|
||||
const subtitleStore = useSubtitleStore();
|
||||
const themeStore = useThemeStore();
|
||||
const volumeStore = useVolumeStore();
|
||||
|
||||
const importSettings = useCallback(
|
||||
async (file: File) => {
|
||||
const text = await file.text();
|
||||
|
||||
const data = settingsPartialSchema.parse(JSON.parse(text));
|
||||
if (data.auth?.account) authStore.setAccount(data.auth.account);
|
||||
if (data.auth?.backendUrl) authStore.setBackendUrl(data.auth.backendUrl);
|
||||
if (data.auth?.proxySet) authStore.setProxySet(data.auth.proxySet);
|
||||
if (data.bookmarks) {
|
||||
for (const [id, item] of Object.entries(data.bookmarks.bookmarks)) {
|
||||
bookmarksStore.setBookmark(id, {
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
year: item.year,
|
||||
poster: item.poster,
|
||||
updatedAt: item.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (data.language) languageStore.setLanguage(data.language.language);
|
||||
if (data.preferences) {
|
||||
preferencesStore.setEnableThumbnails(data.preferences.enableThumbnails);
|
||||
}
|
||||
if (data.quality) {
|
||||
qualityStore.setAutomaticQuality(data.quality.quality.automaticQuality);
|
||||
qualityStore.setLastChosenQuality(
|
||||
data.quality.quality.lastChosenQuality,
|
||||
);
|
||||
}
|
||||
if (data.subtitles) {
|
||||
subtitleStore.setLanguage(data.subtitles.lastSelectedLanguage);
|
||||
subtitleStore.updateStyling(data.subtitles.styling);
|
||||
subtitleStore.setOverrideCasing(data.subtitles.overrideCasing);
|
||||
subtitleStore.setDelay(data.subtitles.delay);
|
||||
}
|
||||
if (data.theme) themeStore.setTheme(data.theme.theme);
|
||||
if (data.volume) volumeStore.setVolume(data.volume.volume);
|
||||
|
||||
if (data.progress) {
|
||||
for (const [id, item] of Object.entries(data.progress.items)) {
|
||||
if (!progressStore.items[id]) {
|
||||
progressStore.setItem(id, item);
|
||||
}
|
||||
|
||||
// We want to preserve existing progress so we take the max of the updatedAt and the progress
|
||||
const storeItem = progressStore.items[id];
|
||||
storeItem.updatedAt = Math.max(storeItem.updatedAt, item.updatedAt);
|
||||
storeItem.title = item.title;
|
||||
storeItem.year = item.year;
|
||||
storeItem.poster = item.poster;
|
||||
storeItem.type = item.type;
|
||||
storeItem.progress = item.progress
|
||||
? {
|
||||
duration: item.progress.duration,
|
||||
watched: Math.max(
|
||||
storeItem.progress?.watched ?? 0,
|
||||
item.progress.watched,
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
for (const [seasonId, season] of Object.entries(item.seasons)) {
|
||||
storeItem.seasons[seasonId] = season;
|
||||
}
|
||||
|
||||
for (const [episodeId, episode] of Object.entries(item.episodes)) {
|
||||
if (!storeItem.episodes[episodeId]) {
|
||||
storeItem.episodes[episodeId] = episode;
|
||||
}
|
||||
|
||||
const storeEpisode = storeItem.episodes[episodeId];
|
||||
storeEpisode.updatedAt = Math.max(
|
||||
storeEpisode.updatedAt,
|
||||
episode.updatedAt,
|
||||
);
|
||||
storeEpisode.title = episode.title;
|
||||
storeEpisode.number = episode.number;
|
||||
storeEpisode.seasonId = episode.seasonId;
|
||||
storeEpisode.progress = {
|
||||
duration: episode.progress.duration,
|
||||
watched: Math.max(
|
||||
storeEpisode.progress.watched,
|
||||
episode.progress.watched,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
progressStore.setItem(id, storeItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
authStore,
|
||||
bookmarksStore,
|
||||
languageStore,
|
||||
preferencesStore,
|
||||
progressStore,
|
||||
qualityStore,
|
||||
subtitleStore,
|
||||
themeStore,
|
||||
volumeStore,
|
||||
],
|
||||
);
|
||||
|
||||
return importSettings;
|
||||
}
|
65
src/pages/migration/Migration.tsx
Normal file
65
src/pages/migration/Migration.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { Stepper } from "@/components/layout/Stepper";
|
||||
import { CenterContainer } from "@/components/layout/ThinContainer";
|
||||
import { VerticalLine } from "@/components/layout/VerticalLine";
|
||||
import { Heading2, Paragraph } from "@/components/utils/Text";
|
||||
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
|
||||
import { Card, CardContent, Link } from "@/pages/migration/utils";
|
||||
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
||||
|
||||
export function MigrationPage() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MinimalPageLayout>
|
||||
<PageTitle subpage k="global.pages.migration" />
|
||||
<CenterContainer>
|
||||
<Stepper steps={2} current={1} className="mb-12" />
|
||||
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
|
||||
{t("migration.start.title")}
|
||||
</Heading2>
|
||||
<Paragraph className="max-w-[320px]">
|
||||
{t("migration.start.explainer")}
|
||||
</Paragraph>
|
||||
|
||||
<div className="w-full flex flex-col md:flex-row gap-3">
|
||||
<Card onClick={() => navigate("/migration/direct")}>
|
||||
<CardContent
|
||||
colorClass="!text-onboarding-best"
|
||||
title={t("migration.start.options.direct.title")}
|
||||
subtitle={t("migration.start.options.direct.quality")}
|
||||
description={
|
||||
<Trans i18nKey="migration.start.options.direct.description" />
|
||||
}
|
||||
icon={Icons.CLOUD_ARROW_UP}
|
||||
>
|
||||
<Link>{t("migration.start.options.direct.action")}</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="hidden md:grid grid-rows-[1fr,auto,1fr] justify-center gap-4">
|
||||
<VerticalLine className="items-end" />
|
||||
<span className="text-xs uppercase font-bold">
|
||||
{t("migration.start.options.or")}
|
||||
</span>
|
||||
<VerticalLine />
|
||||
</div>
|
||||
<Card onClick={() => navigate("/migration/download")}>
|
||||
<CardContent
|
||||
colorClass="!text-migration-good"
|
||||
title={t("migration.start.options.download.title")}
|
||||
subtitle={t("migration.start.options.download.quality")}
|
||||
description={t("migration.start.options.download.description")}
|
||||
icon={Icons.FILE_ARROW_DOWN}
|
||||
>
|
||||
<Link>{t("migration.start.options.download.action")}</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CenterContainer>
|
||||
</MinimalPageLayout>
|
||||
);
|
||||
}
|
26
src/pages/migration/MigrationDirect.tsx
Normal file
26
src/pages/migration/MigrationDirect.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { CenterContainer } from "@/components/layout/ThinContainer";
|
||||
import { useSettingsExport } from "@/hooks/useSettingsExport";
|
||||
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
|
||||
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
||||
|
||||
export function MigrationDirectPage() {
|
||||
const exportSettings = useSettingsExport();
|
||||
|
||||
const doDownload = useCallback(() => {
|
||||
const data = exportSettings(false);
|
||||
console.log(data);
|
||||
}, [exportSettings]);
|
||||
|
||||
return (
|
||||
<MinimalPageLayout>
|
||||
<PageTitle subpage k="global.pages.migration" />
|
||||
<CenterContainer>
|
||||
<button onClick={doDownload} type="button">
|
||||
Hello
|
||||
</button>
|
||||
</CenterContainer>
|
||||
</MinimalPageLayout>
|
||||
);
|
||||
}
|
92
src/pages/migration/utils.tsx
Normal file
92
src/pages/migration/utils.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import classNames from "classnames";
|
||||
import { ReactNode } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Heading2, Heading3, Paragraph } from "@/components/utils/Text";
|
||||
|
||||
export function Card(props: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
"bg-onboarding-card duration-300 border border-onboarding-border rounded-lg p-7":
|
||||
true,
|
||||
"hover:bg-onboarding-cardHover transition-colors cursor-pointer":
|
||||
!!props.onClick,
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardContent(props: {
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
subtitle: ReactNode;
|
||||
colorClass: string;
|
||||
children?: React.ReactNode;
|
||||
icon: Icons;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-rows-[1fr,auto] h-full">
|
||||
<div>
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
className={classNames("text-4xl mb-8 block", props.colorClass)}
|
||||
/>
|
||||
<Heading3
|
||||
className={classNames(
|
||||
"!mt-0 !mb-0 !text-xs uppercase",
|
||||
props.colorClass,
|
||||
)}
|
||||
>
|
||||
{props.subtitle}
|
||||
</Heading3>
|
||||
<Heading2 className="!mb-0 !mt-1 !text-base">{props.title}</Heading2>
|
||||
<Paragraph className="max-w-[320px] !my-4">
|
||||
{props.description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Link(props: {
|
||||
children?: React.ReactNode;
|
||||
to?: string;
|
||||
href?: string;
|
||||
className?: string;
|
||||
target?: "_blank";
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
if (props.to) navigate(props.to);
|
||||
}}
|
||||
href={props.href}
|
||||
target={props.target}
|
||||
className={classNames(
|
||||
"text-onboarding-link cursor-pointer inline-flex gap-2 items-center group hover:opacity-75 transition-opacity",
|
||||
props.className,
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{props.children}
|
||||
<Icon
|
||||
icon={Icons.ARROW_RIGHT}
|
||||
className="group-hover:translate-x-0.5 transition-transform text-xl group-active:translate-x-0"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
import classNames from "classnames";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Stepper } from "@/components/layout/Stepper";
|
||||
import { CenterContainer } from "@/components/layout/ThinContainer";
|
||||
import { VerticalLine } from "@/components/layout/VerticalLine";
|
||||
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
||||
import { Heading1, Heading2, Paragraph } from "@/components/utils/Text";
|
||||
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
|
||||
@@ -15,14 +15,6 @@ import { Card, CardContent, Link } from "@/pages/onboarding/utils";
|
||||
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
||||
import { getProxyUrls } from "@/utils/proxyUrls";
|
||||
|
||||
function VerticalLine(props: { className?: string }) {
|
||||
return (
|
||||
<div className={classNames("w-full grid justify-center", props.className)}>
|
||||
<div className="w-px h-10 bg-onboarding-divider" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OnboardingPage() {
|
||||
const navigate = useNavigateOnboarding();
|
||||
const skipModal = useModal("skip");
|
||||
@@ -73,7 +65,9 @@ export function OnboardingPage() {
|
||||
</Card>
|
||||
<div className="hidden md:grid grid-rows-[1fr,auto,1fr] justify-center gap-4">
|
||||
<VerticalLine className="items-end" />
|
||||
<span className="text-xs uppercase font-bold">or</span>
|
||||
<span className="text-xs uppercase font-bold">
|
||||
{t("onboarding.start.options.or")}
|
||||
</span>
|
||||
<VerticalLine />
|
||||
</div>
|
||||
<Card onClick={() => navigate("/onboarding/proxy")}>
|
||||
|
@@ -19,6 +19,8 @@ import { DmcaPage, shouldHaveDmcaPage } from "@/pages/Dmca";
|
||||
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
||||
import { HomePage } from "@/pages/HomePage";
|
||||
import { LoginPage } from "@/pages/Login";
|
||||
import { MigrationPage } from "@/pages/migration/Migration";
|
||||
import { MigrationDirectPage } from "@/pages/migration/MigrationDirect";
|
||||
import { OnboardingPage } from "@/pages/onboarding/Onboarding";
|
||||
import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension";
|
||||
import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy";
|
||||
@@ -129,6 +131,9 @@ function App() {
|
||||
/>
|
||||
<Route path="/onboarding/proxy" element={<OnboardingProxyPage />} />
|
||||
|
||||
<Route path="/migration" element={<MigrationPage />} />
|
||||
<Route path="/migration/direct" element={<MigrationDirectPage />} />
|
||||
|
||||
{shouldHaveDmcaPage() ? (
|
||||
<Route path="/dmca" element={<DmcaPage />} />
|
||||
) : null}
|
||||
|
@@ -94,9 +94,11 @@ export function conf(): RuntimeConfig {
|
||||
DMCA_EMAIL: getKey("DMCA_EMAIL"),
|
||||
ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: getKey(
|
||||
"ONBOARDING_CHROME_EXTENSION_INSTALL_LINK",
|
||||
"https://chromewebstore.google.com/detail/movie-web-extension/hoffoikpiofojilgpofjhnkkamfnnhmm",
|
||||
),
|
||||
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: getKey(
|
||||
"ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK",
|
||||
"https://addons.mozilla.org/en-GB/firefox/addon/movie-web-extension",
|
||||
),
|
||||
ONBOARDING_PROXY_INSTALL_LINK: getKey("ONBOARDING_PROXY_INSTALL_LINK"),
|
||||
BACKEND_URL: getKey("BACKEND_URL", BACKEND_URL),
|
||||
@@ -106,7 +108,7 @@ export function conf(): RuntimeConfig {
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v.length > 0),
|
||||
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
|
||||
HAS_ONBOARDING: getKey("HAS_ONBOARDING", "false") === "true",
|
||||
HAS_ONBOARDING: getKey("HAS_ONBOARDING", "true") === "true",
|
||||
TURNSTILE_KEY: getKey("TURNSTILE_KEY"),
|
||||
DISALLOWED_IDS: getKey("DISALLOWED_IDS", "")
|
||||
.split(",")
|
||||
|
@@ -26,6 +26,7 @@ export interface BookmarkStore {
|
||||
bookmarks: Record<string, BookmarkMediaItem>;
|
||||
updateQueue: BookmarkUpdateItem[];
|
||||
addBookmark(meta: PlayerMeta): void;
|
||||
setBookmark(id: string, item: BookmarkMediaItem): void;
|
||||
removeBookmark(id: string): void;
|
||||
replaceBookmarks(items: Record<string, BookmarkMediaItem>): void;
|
||||
clear(): void;
|
||||
@@ -94,6 +95,11 @@ export const useBookmarkStore = create(
|
||||
s.updateQueue = [...s.updateQueue.filter((v) => v.id !== id)];
|
||||
});
|
||||
},
|
||||
setBookmark(id, item) {
|
||||
set((s) => {
|
||||
s.bookmarks[id] = item;
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: "__MW::bookmarks",
|
||||
|
@@ -53,6 +53,7 @@ export interface CaptionListItem {
|
||||
language: string;
|
||||
url: string;
|
||||
needsProxy: boolean;
|
||||
hls?: boolean;
|
||||
}
|
||||
|
||||
export interface SourceSlice {
|
||||
|
@@ -64,6 +64,7 @@ export interface ProgressStore {
|
||||
clear(): void;
|
||||
clearUpdateQueue(): void;
|
||||
removeUpdateItem(id: string): void;
|
||||
setItem(id: string, item: ProgressMediaItem): void;
|
||||
}
|
||||
|
||||
let updateId = 0;
|
||||
@@ -173,6 +174,11 @@ export const useProgressStore = create(
|
||||
s.updateQueue = [...s.updateQueue.filter((v) => v.id !== id)];
|
||||
});
|
||||
},
|
||||
setItem(id, item) {
|
||||
set((s) => {
|
||||
s.items[id] = item;
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: "__MW::progress",
|
||||
|
48
vercel.json
48
vercel.json
@@ -1,3 +1,49 @@
|
||||
{
|
||||
"routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }]
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "X-Content-Type-Options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "DENY"
|
||||
},
|
||||
{
|
||||
"key": "X-XSS-Protection",
|
||||
"value": "1; mode=block"
|
||||
},
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=0, s-maxage=0, must-revalidate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/manifest.webmanifest",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/manifest+json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/assets/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, s-maxage=31536000, immutable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user