@@ -18,6 +18,7 @@
|
||||
# Features
|
||||
- Internal account system (Optional).
|
||||
* Subscriptions are end-to-end encrypted.
|
||||
* History & watch progress syncing.
|
||||
* [Proof-of-work Captcha](https://github.com/altcha-org/altcha).
|
||||
- Subscription importing/exporting
|
||||
* Automatically import/export Invidious/Materialious.
|
||||
@@ -25,7 +26,6 @@
|
||||
* Enjoy Materialious without a Invidious instance via our YouTube backend.
|
||||
* Local video fallback if Invidious fails loading videos.
|
||||
- Invidious companion support.
|
||||
- [Invidious API extended integration.](https://github.com/Materialious/api-extended)
|
||||
- Android TV support.
|
||||
- Support for disabling certificate validation for homelab users.
|
||||
- Sync your watch progress between Invidious sessions.
|
||||
|
||||
@@ -136,37 +136,3 @@ ryd-proxy:
|
||||
```
|
||||
|
||||
Modify/add `VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE` for Materialious to be the reverse proxied URL of RYD-Proxy.
|
||||
|
||||
## Step 3 (Optional, but recommended): Self-host Invidious API extended
|
||||
```yaml
|
||||
services:
|
||||
api_extended:
|
||||
image: wardpearce/invidious_api_extended:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 3004:80
|
||||
environment:
|
||||
api_extended_postgre: '{"host": "invidious-db", "port": 5432, "database": "invidious", "user": "kemal", "password": "kemal"}'
|
||||
api_extended_allowed_origins: '["https://materialious.example.com"]'
|
||||
api_extended_debug: false
|
||||
|
||||
# No trailing backslashes!
|
||||
api_extended_invidious_instance: "https://invidious.example.com"
|
||||
api_extended_production_instance: "https://syncious.example.com"
|
||||
```
|
||||
|
||||
Add these additional environment variables to Materialious.
|
||||
```yaml
|
||||
VITE_DEFAULT_API_EXTENDED_INSTANCE: "https://syncious.example.com"
|
||||
```
|
||||
|
||||
## Step 4 (Optional): Self-host PeerJS
|
||||
[Read the official guide.](https://github.com/peers/peerjs-server?tab=readme-ov-file#docker)
|
||||
|
||||
Add these additional environment variables to Materialious.
|
||||
```yaml
|
||||
# Will differ depending on how you self-host peerjs.
|
||||
VITE_DEFAULT_PEERJS_HOST: "peerjs.example.com"
|
||||
VITE_DEFAULT_PEERJS_PATH: "/"
|
||||
VITE_DEFAULT_PEERJS_PORT: 443
|
||||
```
|
||||
|
||||
+1
-28
@@ -278,34 +278,7 @@ http:
|
||||
### Step 3:
|
||||
Modify/add `VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE` for Materialious to be the reverse proxied URL of RYD-Proxy.
|
||||
|
||||
## Step 5 (Optional, but recommended): Self-host Invidious API extended
|
||||
|
||||
### Step 1: Docker compose
|
||||
Add the following to your docker compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api_extended:
|
||||
image: wardpearce/invidious_api_extended:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 3004:80
|
||||
environment:
|
||||
api_extended_postgre: '{"host": "invidious-db", "port": 5432, "database": "invidious", "user": "kemal", "password": "kemal"}'
|
||||
api_extended_allowed_origins: '["https://materialious.example.com"]'
|
||||
api_extended_debug: false
|
||||
|
||||
# No trailing backslashes!
|
||||
api_extended_invidious_instance: "https://invidious.example.com"
|
||||
api_extended_production_instance: "https://syncious.example.com"
|
||||
```
|
||||
|
||||
Add these additional environment variables to Materialious.
|
||||
```yaml
|
||||
VITE_DEFAULT_API_EXTENDED_INSTANCE: "https://syncious.example.com"
|
||||
```
|
||||
|
||||
## Step 6 (Optional): Self-host PeerJS
|
||||
## Step 5 (Optional): Self-host PeerJS
|
||||
[Read the official guide.](https://github.com/peers/peerjs-server?tab=readme-ov-file#docker)
|
||||
|
||||
Add these additional environment variables to Materialious.
|
||||
|
||||
@@ -20,10 +20,6 @@ RUN echo "VITE_DEFAULT_INVIDIOUS_INSTANCE=VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEH
|
||||
echo "VITE_DEFAULT_PEERJS_HOST=VITE_DEFAULT_PEERJS_HOST_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_PEERJS_PATH=VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_PEERJS_PORT=VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_DOWNLOAD_ENABLED=VITE_DEFAULT_DOWNLOAD_ENABLED_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_SYNCIOUS_INSTANCE=VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_API_EXTENDED_INSTANCE=VITE_DEFAULT_API_EXTENDED_INSTANCE_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_DASH_BITRATE=VITE_DEFAULT_DASH_BITRATE_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_SETTINGS='\"VITE_DEFAULT_SETTINGS_PLACEHOLDER\"'" >> .env && \
|
||||
echo "VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE=VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE_PLACEHOLDER" >> .env
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 223
|
||||
versionName "1.16.5"
|
||||
versionCode 224
|
||||
versionName "1.16.6"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -88,7 +88,11 @@
|
||||
|
||||
|
||||
|
||||
<release version="1.16.5" date="2026-2-23">
|
||||
|
||||
<release version="1.16.6" date="2026-2-23">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.16.6</url>
|
||||
</release>
|
||||
<release version="1.16.5" date="2026-2-23">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.16.5</url>
|
||||
</release>
|
||||
<release version="1.16.4" date="2026-2-22">
|
||||
|
||||
Generated
+173
-133
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.16.1",
|
||||
"version": "1.16.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.16.1",
|
||||
"version": "1.16.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
@@ -16,14 +16,14 @@
|
||||
"electron-is-dev": "~2.0.0",
|
||||
"electron-serve": "~3.0.0",
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"electron-updater": "^6.8.3",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"jsdom": "^28.0.0",
|
||||
"jsonfile": "^6.2.0",
|
||||
"youtubei.js": "^16.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^40.0.0",
|
||||
"electron": "^40.6.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"typescript": "^5.9.3"
|
||||
@@ -227,7 +227,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz",
|
||||
"integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -316,7 +315,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -355,7 +353,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -418,9 +415,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/asar/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -660,6 +657,29 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/cacache": {
|
||||
"version": "19.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz",
|
||||
@@ -730,13 +750,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/isexe": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
|
||||
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
|
||||
"integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/lru-cache": {
|
||||
@@ -770,13 +790,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
|
||||
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -786,11 +806,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@@ -930,9 +950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/rebuild/node_modules/tar": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
|
||||
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -1017,14 +1037,37 @@
|
||||
"node": ">=16.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
|
||||
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -1040,6 +1083,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1217,27 +1261,6 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
|
||||
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -1727,12 +1750,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -1786,9 +1808,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/app-builder-lib": {
|
||||
"version": "26.7.0",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.7.0.tgz",
|
||||
"integrity": "sha512-/UgCD8VrO79Wv8aBNpjMfsS1pIUfIPURoRn0Ik6tMe5avdZF+vQgl/juJgipcMmH3YS0BD573lCdCHyoi84USg==",
|
||||
"version": "26.8.1",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz",
|
||||
"integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1803,7 +1825,7 @@
|
||||
"@malept/flatpak-bundler": "^0.4.0",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"builder-util": "26.4.1",
|
||||
"builder-util": "26.8.1",
|
||||
"builder-util-runtime": "9.5.1",
|
||||
"chromium-pickle-js": "^0.2.0",
|
||||
"ci-info": "4.3.1",
|
||||
@@ -1811,7 +1833,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-expand": "^11.0.6",
|
||||
"ejs": "^3.1.8",
|
||||
"electron-publish": "26.6.0",
|
||||
"electron-publish": "26.8.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"hosted-git-info": "^4.1.0",
|
||||
"isbinaryfile": "^5.0.0",
|
||||
@@ -1833,8 +1855,8 @@
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"dmg-builder": "26.7.0",
|
||||
"electron-builder-squirrel-windows": "26.7.0"
|
||||
"dmg-builder": "26.8.1",
|
||||
"electron-builder-squirrel-windows": "26.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/@electron/get": {
|
||||
@@ -1956,21 +1978,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/isexe": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
|
||||
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz",
|
||||
"integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@@ -1989,9 +2011,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/app-builder-lib/node_modules/tar": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
|
||||
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
@@ -2249,9 +2271,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/builder-util": {
|
||||
"version": "26.4.1",
|
||||
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz",
|
||||
"integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==",
|
||||
"version": "26.8.1",
|
||||
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz",
|
||||
"integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2770,7 +2792,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
@@ -3015,9 +3038,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dir-compare/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -3028,15 +3051,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dmg-builder": {
|
||||
"version": "26.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.7.0.tgz",
|
||||
"integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==",
|
||||
"version": "26.8.1",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz",
|
||||
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.7.0",
|
||||
"builder-util": "26.4.1",
|
||||
"app-builder-lib": "26.8.1",
|
||||
"builder-util": "26.8.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"js-yaml": "^4.1.0"
|
||||
@@ -3154,9 +3176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "40.4.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-40.4.1.tgz",
|
||||
"integrity": "sha512-N1ZXybQZL8kYemO8vAeh9nrk4mSvqlAO8xs0QCHkXIvRnuB/7VGwEehjvQbsU5/f4bmTKpG+2GQERe/zmKpudQ==",
|
||||
"version": "40.6.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-40.6.0.tgz",
|
||||
"integrity": "sha512-ett8W+yOFGDuM0vhJMamYSkrbV3LoaffzJd9GfjI96zRAxyrNqUSKqBpf/WGbQCweDxX2pkUCUfrv4wwKpsFZA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -3173,18 +3195,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder": {
|
||||
"version": "26.7.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.7.0.tgz",
|
||||
"integrity": "sha512-LoXbCvSFxLesPneQ/fM7FB4OheIDA2tjqCdUkKlObV5ZKGhYgi5VHPHO/6UUOUodAlg7SrkPx7BZJPby+Vrtbg==",
|
||||
"version": "26.8.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz",
|
||||
"integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.7.0",
|
||||
"builder-util": "26.4.1",
|
||||
"app-builder-lib": "26.8.1",
|
||||
"builder-util": "26.8.1",
|
||||
"builder-util-runtime": "9.5.1",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^4.2.0",
|
||||
"dmg-builder": "26.7.0",
|
||||
"dmg-builder": "26.8.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"lazy-val": "^1.0.5",
|
||||
"simple-update-notifier": "2.0.0",
|
||||
@@ -3199,15 +3221,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder-squirrel-windows": {
|
||||
"version": "26.7.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.7.0.tgz",
|
||||
"integrity": "sha512-3EqkQK+q0kGshdPSKEPb2p5F75TENMKu6Fe5aTdeaPfdzFK4Yjp5L0d6S7K8iyvqIsGQ/ei4bnpyX9wt+kVCKQ==",
|
||||
"version": "26.8.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz",
|
||||
"integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.7.0",
|
||||
"builder-util": "26.4.1",
|
||||
"app-builder-lib": "26.8.1",
|
||||
"builder-util": "26.8.1",
|
||||
"electron-winstaller": "5.4.0"
|
||||
}
|
||||
},
|
||||
@@ -3236,14 +3258,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-publish": {
|
||||
"version": "26.6.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz",
|
||||
"integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==",
|
||||
"version": "26.8.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz",
|
||||
"integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"builder-util": "26.4.1",
|
||||
"builder-util": "26.8.1",
|
||||
"builder-util-runtime": "9.5.1",
|
||||
"chalk": "^4.1.2",
|
||||
"form-data": "^4.0.5",
|
||||
@@ -3361,9 +3383,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater": {
|
||||
"version": "6.7.3",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz",
|
||||
"integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==",
|
||||
"version": "6.8.3",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
|
||||
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"builder-util-runtime": "9.5.1",
|
||||
@@ -3419,6 +3441,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -3439,6 +3462,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -3454,6 +3478,7 @@
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
@@ -3464,6 +3489,7 @@
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
@@ -3721,26 +3747,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz",
|
||||
"integrity": "sha512-ct/ckWBV/9Dg3MlvCXsLcSUyoWwv9mCKqlhLNB2DAuXR/NZolSXlQqP5dyy6guWlPXBhodZyZ5lGPQcbQDxrEQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"minimatch": "^10.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
@@ -4876,20 +4892,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch/node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -5361,11 +5398,11 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@@ -5404,7 +5441,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5433,6 +5469,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -5450,6 +5487,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -6216,6 +6254,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -6257,6 +6296,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.16.5",
|
||||
"version": "1.16.6",
|
||||
"description": "Modern material design for YouTube and Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
@@ -24,19 +24,18 @@
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz",
|
||||
"chokidar": "~5.0.0",
|
||||
"electron-is-dev": "~2.0.0",
|
||||
"electron-serve": "~3.0.0",
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"electron-updater": "^6.8.3",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"jsdom": "^28.0.0",
|
||||
"jsonfile": "^6.2.0",
|
||||
"youtubei.js": "^16.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^40.0.0",
|
||||
"electron": "^40.6.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const CapacitorNodejs = require('../../../node_modules/capacitor-nodejs/electron/dist/plugin.js');
|
||||
|
||||
module.exports = {
|
||||
CapacitorNodejs,
|
||||
}
|
||||
Generated
+127
-108
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.16.4",
|
||||
"version": "1.16.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.16.4",
|
||||
"version": "1.16.6",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor/android": "^8.0.0",
|
||||
"@capacitor/app": "^8.0.0",
|
||||
"@capacitor/browser": "^8.0.0",
|
||||
"@capacitor/browser": "^8.0.1",
|
||||
"@capacitor/clipboard": "^8.0.0",
|
||||
"@capacitor/core": "^8.0.0",
|
||||
"@capacitor/network": "^8.0.0",
|
||||
"@capacitor/preferences": "^8.0.0",
|
||||
"@capacitor/preferences": "^8.0.1",
|
||||
"@capacitor/screen-orientation": "^8.0.0",
|
||||
"@capacitor/share": "^8.0.0",
|
||||
"@macfja/serializer": "^1.1.4",
|
||||
"@macfja/svelte-persistent-store": "^2.4.2",
|
||||
"altcha": "^2.3.0",
|
||||
"altcha-lib": "^1.4.1",
|
||||
"beercss": "^4.0.2",
|
||||
"beercss": "^4.0.15",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"capacitor-music-controls-plugin": "^6.1.0",
|
||||
@@ -2106,9 +2106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/browser": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-8.0.0.tgz",
|
||||
"integrity": "sha512-xey7maszGABKuStvXDV4vXN+EzIyz0o7zlyzw4JKG6o/1GzqeqHqVuE+8Ux+Hks3DZji4LoriWZVgVF6mR6RGg==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-8.0.1.tgz",
|
||||
"integrity": "sha512-cCEYK9DGJvamfCKOh95QP+VafApblySDR/eodOcl1Qt+d/Y+NNPzc8NYm/dztQlRf3aMLJ7ne1HrRCfu2UOMnw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
@@ -2266,9 +2266,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/preferences": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.0.tgz",
|
||||
"integrity": "sha512-NsE7Srk9Zr0SxiVelHGiAJR7M238eyCD6dI/sDhu3ckKwFrXn8/GRyGr+SZcnGLlQKy948li8Pfcfr0dqxNf1g==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz",
|
||||
"integrity": "sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
@@ -2968,20 +2968,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz",
|
||||
"integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"ajv": "^6.14.0",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^10.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^3.1.3",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4740,17 +4740,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
||||
"integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||
"integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/type-utils": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/type-utils": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
@@ -4763,7 +4763,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -4779,16 +4779,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
|
||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4829,14 +4829,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz",
|
||||
"integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
|
||||
"integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.56.0",
|
||||
"@typescript-eslint/types": "^8.56.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.56.1",
|
||||
"@typescript-eslint/types": "^8.56.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4876,14 +4876,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz",
|
||||
"integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
|
||||
"integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0"
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4894,9 +4894,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz",
|
||||
"integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4911,15 +4911,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz",
|
||||
"integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
@@ -4961,9 +4961,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz",
|
||||
"integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
|
||||
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4975,18 +4975,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz",
|
||||
"integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
|
||||
"integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.56.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/visitor-keys": "8.56.0",
|
||||
"@typescript-eslint/project-service": "8.56.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^9.0.5",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
@@ -5003,26 +5003,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
|
||||
"integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
|
||||
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/debug": {
|
||||
@@ -5044,16 +5044,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
|
||||
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -5067,16 +5067,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz",
|
||||
"integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
|
||||
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0"
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -5091,13 +5091,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz",
|
||||
"integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
|
||||
"integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5748,9 +5748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/beercss": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/beercss/-/beercss-4.0.2.tgz",
|
||||
"integrity": "sha512-pkkm1zOH8UqVMLUwnMRi3ueT+397KNqWDwJEejVBxGQmvSnpmSVf4mnVXjidQI0BvN5uklVdMu8UjcW2c28ZgQ==",
|
||||
"version": "4.0.15",
|
||||
"resolved": "https://registry.npmjs.org/beercss/-/beercss-4.0.15.tgz",
|
||||
"integrity": "sha512-SpxOOkZReQJCQOTldKxLaYo62uK6aC2RgZhSVM6R1Rbdsuu2XR0yMpbiEHk/iuVh2miaVTyarOg7C/fFwbNteQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"material-dynamic-colors": "^1.1.4"
|
||||
@@ -8101,36 +8101,55 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz",
|
||||
"integrity": "sha512-ct/ckWBV/9Dg3MlvCXsLcSUyoWwv9mCKqlhLNB2DAuXR/NZolSXlQqP5dyy6guWlPXBhodZyZ5lGPQcbQDxrEQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
"minimatch": "^10.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.7",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.7.tgz",
|
||||
"integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==",
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
@@ -15200,16 +15219,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz",
|
||||
"integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==",
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
|
||||
"integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.56.0",
|
||||
"@typescript-eslint/parser": "8.56.0",
|
||||
"@typescript-eslint/typescript-estree": "8.56.0",
|
||||
"@typescript-eslint/utils": "8.56.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@typescript-eslint/parser": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.16.5",
|
||||
"version": "1.16.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run patch:github && vite dev",
|
||||
@@ -48,18 +48,18 @@
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor/android": "^8.0.0",
|
||||
"@capacitor/app": "^8.0.0",
|
||||
"@capacitor/browser": "^8.0.0",
|
||||
"@capacitor/browser": "^8.0.1",
|
||||
"@capacitor/clipboard": "^8.0.0",
|
||||
"@capacitor/core": "^8.0.0",
|
||||
"@capacitor/network": "^8.0.0",
|
||||
"@capacitor/preferences": "^8.0.0",
|
||||
"@capacitor/preferences": "^8.0.1",
|
||||
"@capacitor/screen-orientation": "^8.0.0",
|
||||
"@capacitor/share": "^8.0.0",
|
||||
"@macfja/serializer": "^1.1.4",
|
||||
"@macfja/svelte-persistent-store": "^2.4.2",
|
||||
"altcha": "^2.3.0",
|
||||
"altcha-lib": "^1.4.1",
|
||||
"beercss": "^4.0.2",
|
||||
"beercss": "^4.0.15",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"capacitor-music-controls-plugin": "^6.1.0",
|
||||
|
||||
@@ -11,8 +11,4 @@ find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VI
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_HOST_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_HOST"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PATH"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PORT"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_DOWNLOAD_ENABLED_PLACEHOLDER|'"$VITE_DEFAULT_DOWNLOAD_ENABLED"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_SYNCIOUS_INSTANCE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_DASH_BITRATE_PLACEHOLDER|'"$VITE_DEFAULT_DASH_BITRATE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_API_EXTENDED_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_API_EXTENDED_INSTANCE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SETTINGS_PLACEHOLDER|'"$VITE_DEFAULT_SETTINGS"'|g' {} +
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { rawMasterKeyStore } from '../store';
|
||||
import { get } from 'svelte/store';
|
||||
import { parseChannelRSS } from './youtubejs/subscriptions';
|
||||
import { getChannelYTjs } from './youtubejs/channel';
|
||||
import type { ChannelSubscriptions } from '$lib/dexie';
|
||||
|
||||
async function getInternalAuthorId(authorId: string, rawKey: Uint8Array): Promise<string> {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(
|
||||
sodium.crypto_generichash(sodium.crypto_generichash_BYTES, authorId, rawKey)
|
||||
);
|
||||
}
|
||||
|
||||
async function getRawKey(): Promise<Uint8Array | undefined> {
|
||||
const rawMasterKey = get(rawMasterKeyStore);
|
||||
if (!rawMasterKey) return;
|
||||
|
||||
await sodium.ready;
|
||||
|
||||
return sodium.from_base64(rawMasterKey);
|
||||
}
|
||||
|
||||
export async function getSubscriptionsBackend(): Promise<ChannelSubscriptions[]> {
|
||||
const resp = await fetch(`/api/user/subscriptions`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return [];
|
||||
|
||||
const subscriptions: ChannelSubscriptions[] = [];
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
for (const sub of respJson.subscriptions) {
|
||||
subscriptions.push({
|
||||
channelName: (await decryptWithMasterKey(sub.channelNameNonce, sub.channelNameCipher)) ?? '',
|
||||
channelId: (await decryptWithMasterKey(sub.channelIdNonce, sub.channelIdCipher)) ?? '',
|
||||
lastRSSFetch: new Date(sub.lastRSSFetch)
|
||||
});
|
||||
}
|
||||
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
export async function updateRSSLastUpdated(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getInternalAuthorId(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function amSubscribedBackend(authorId: string): Promise<boolean> {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getInternalAuthorId(authorId, rawKey);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!resp.ok) return false;
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
return respJson.amSubscribed;
|
||||
}
|
||||
|
||||
export async function deleteUnsubscribeBackend(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getInternalAuthorId(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function postSubscribeBackend(
|
||||
authorId: string,
|
||||
authorName: string | undefined = undefined
|
||||
) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const internalAuthorId = await getInternalAuthorId(authorId, rawKey);
|
||||
|
||||
if (!authorName) {
|
||||
const channel = await getChannelYTjs(authorId);
|
||||
authorName = channel.author;
|
||||
}
|
||||
|
||||
const channelId = await encryptWithMasterKey(authorId);
|
||||
const channelName = await encryptWithMasterKey(authorName);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
channelIdCipher: channelId?.cipher,
|
||||
channelIdNonce: channelId?.nonce,
|
||||
channelNameCipher: channelName?.cipher,
|
||||
channelNameNonce: channelName?.nonce
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (resp.ok) parseChannelRSS(authorId);
|
||||
}
|
||||
|
||||
export type DerivePassword = (rawPassword: string, passwordSalt: Uint8Array) => Promise<Uint8Array>;
|
||||
|
||||
export async function createUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
|
||||
const loginHash = await derivePassword(rawPassword, passwordSalt);
|
||||
|
||||
const decryptionKeySalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
decryptionKeySalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES);
|
||||
const decryptionMasterKeyNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
|
||||
const masterKeyCipher = sodium.crypto_secretbox_easy(
|
||||
rawDecryptionMasterKey,
|
||||
decryptionMasterKeyNonce,
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
const userCreateResp = await fetch('/api/user/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: {
|
||||
hash: sodium.to_base64(loginHash),
|
||||
salt: sodium.to_base64(passwordSalt)
|
||||
},
|
||||
decryptionKeySalt: sodium.to_base64(decryptionKeySalt),
|
||||
masterKey: {
|
||||
cipher: sodium.to_base64(masterKeyCipher),
|
||||
nonce: sodium.to_base64(decryptionMasterKeyNonce)
|
||||
},
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!userCreateResp.ok) return false;
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function loginUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSaltsResp = await fetch(`/api/user/${username}/public`);
|
||||
if (!passwordSaltsResp.ok) return false;
|
||||
|
||||
const passwordSalts = await passwordSaltsResp.json();
|
||||
|
||||
const loginHash = await derivePassword(
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.passwordSalt)
|
||||
);
|
||||
|
||||
const loginResp = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
passwordHash: sodium.to_base64(loginHash),
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!loginResp.ok) return false;
|
||||
|
||||
const loginJson = await loginResp.json();
|
||||
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.decryptionKeySalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.crypto_secretbox_open_easy(
|
||||
sodium.from_base64(loginJson.masterKeyCipher),
|
||||
sodium.from_base64(loginJson.masterKeyNonce),
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function addOrUpdateKeyValue(key: string, value: string) {
|
||||
const valueEncrypted = await encryptWithMasterKey(value);
|
||||
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
valueCipher: valueEncrypted?.cipher,
|
||||
valueNonce: valueEncrypted?.nonce
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKeyValue(key: string) {
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export type KeyValue = boolean | number | string[] | string | object | undefined;
|
||||
|
||||
export async function getKeyValue(key: string): Promise<KeyValue | null> {
|
||||
const resp = await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return null;
|
||||
|
||||
const respJson = await resp.json();
|
||||
return (await decryptWithMasterKey(respJson.valueNonce, respJson.valueCipher)) ?? null;
|
||||
}
|
||||
|
||||
async function encryptWithMasterKey(
|
||||
text: string
|
||||
): Promise<{ nonce: string; cipher: string } | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const cipher = sodium.crypto_secretbox_easy(new TextEncoder().encode(text), nonce, rawKey);
|
||||
|
||||
return {
|
||||
nonce: sodium.to_base64(nonce),
|
||||
cipher: sodium.to_base64(cipher)
|
||||
};
|
||||
}
|
||||
|
||||
async function decryptWithMasterKey(nonce: string, cipher: string): Promise<string | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
return new TextDecoder().decode(
|
||||
sodium.crypto_secretbox_open_easy(sodium.from_base64(cipher), sodium.from_base64(nonce), rawKey)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { rawMasterKeyStore } from '$lib/store';
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function getSecureHash(toHash: string, rawKey: Uint8Array): Promise<string> {
|
||||
await sodium.ready;
|
||||
return sodium.to_base64(
|
||||
sodium.crypto_generichash(sodium.crypto_generichash_BYTES, toHash, rawKey)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRawKey(): Promise<Uint8Array | undefined> {
|
||||
const rawMasterKey = get(rawMasterKeyStore);
|
||||
if (!rawMasterKey) return;
|
||||
|
||||
await sodium.ready;
|
||||
|
||||
return sodium.from_base64(rawMasterKey);
|
||||
}
|
||||
|
||||
export async function encryptWithMasterKey(
|
||||
text: string
|
||||
): Promise<{ nonce: string; cipher: string } | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const cipher = sodium.crypto_secretbox_easy(new TextEncoder().encode(text), nonce, rawKey);
|
||||
|
||||
return {
|
||||
nonce: sodium.to_base64(nonce),
|
||||
cipher: sodium.to_base64(cipher)
|
||||
};
|
||||
}
|
||||
|
||||
export async function decryptWithMasterKey(
|
||||
nonce: string,
|
||||
cipher: string
|
||||
): Promise<string | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
return new TextDecoder().decode(
|
||||
sodium.crypto_secretbox_open_easy(sodium.from_base64(cipher), sodium.from_base64(nonce), rawKey)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { decryptWithMasterKey, encryptWithMasterKey, getRawKey, getSecureHash } from './encryption';
|
||||
import type { VideoPlay, VideoWatchHistory } from '../model';
|
||||
import { getBestThumbnail } from '$lib/images';
|
||||
import { get } from 'svelte/store';
|
||||
import { watchHistoryEnabledStore } from '$lib/store';
|
||||
|
||||
export async function updateWatchHistory(videoId: string, progress: number) {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const videoHash = await getSecureHash(videoId, rawKey);
|
||||
|
||||
await fetch(`/api/user/history/${videoHash}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
watched: new Date(),
|
||||
progress
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function decryptWatchHistory(
|
||||
history: Record<string, string | number>
|
||||
): Promise<VideoWatchHistory> {
|
||||
const author = (await decryptWithMasterKey(
|
||||
history.authorNonce as string,
|
||||
history.authorCipher as string
|
||||
)) as string;
|
||||
const title = (await decryptWithMasterKey(
|
||||
history.titleNonce as string,
|
||||
history.titleCipher as string
|
||||
)) as string;
|
||||
const thumbnail = (await decryptWithMasterKey(
|
||||
history.thumbnailNonce as string,
|
||||
history.thumbnailCipher as string
|
||||
)) as string;
|
||||
const videoId = (await decryptWithMasterKey(
|
||||
history.videoIdNonce as string,
|
||||
history.videoIdCipher as string
|
||||
)) as string;
|
||||
|
||||
return {
|
||||
author,
|
||||
title,
|
||||
thumbnail,
|
||||
videoId,
|
||||
watched: new Date(history.watched),
|
||||
lengthSeconds: history.lengthSeconds as number,
|
||||
id: history.id as string,
|
||||
progress: history.progress as number,
|
||||
type: 'historyVideo'
|
||||
};
|
||||
}
|
||||
|
||||
export async function getVideoWatchHistory(
|
||||
videoId: string
|
||||
): Promise<VideoWatchHistory | undefined> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const videoHash = await getSecureHash(videoId, rawKey);
|
||||
|
||||
const resp = await fetch(`/api/user/history/${videoHash}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return;
|
||||
|
||||
return await decryptWatchHistory(await resp.json());
|
||||
}
|
||||
|
||||
export async function getWatchHistory(
|
||||
options: { page?: number; videoIds?: string[] } = { page: undefined, videoIds: undefined }
|
||||
): Promise<VideoWatchHistory[]> {
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return [];
|
||||
|
||||
const videoHashes: string[] = [];
|
||||
if (options.videoIds) {
|
||||
for (const videoId of options.videoIds) {
|
||||
videoHashes.push(await getSecureHash(videoId, rawKey));
|
||||
}
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options.page) {
|
||||
params.set('page', options.page.toString());
|
||||
}
|
||||
|
||||
if (options.videoIds && options.videoIds.length > 0) {
|
||||
params.set('videoHashes', videoHashes.join(','));
|
||||
}
|
||||
|
||||
const resp = await fetch(`/api/user/history?${params.toString()}`);
|
||||
if (!resp.ok) return [];
|
||||
|
||||
const rawHistory = await resp.json();
|
||||
|
||||
const history: VideoWatchHistory[] = [];
|
||||
|
||||
for (const item of rawHistory) {
|
||||
history.push(await decryptWatchHistory(item));
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
export async function deleteWatchHistory() {
|
||||
await fetch('/api/user/history', { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function saveWatchHistory(video: VideoPlay, progress: number = 0) {
|
||||
if (!get(watchHistoryEnabledStore)) return;
|
||||
|
||||
await sodium.ready;
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const videoHash = await getSecureHash(video.videoId, rawKey);
|
||||
|
||||
const title = await encryptWithMasterKey(video.title);
|
||||
const author = await encryptWithMasterKey(video.author);
|
||||
const thumbnail = await encryptWithMasterKey(getBestThumbnail(video.videoThumbnails));
|
||||
const videoId = await encryptWithMasterKey(video.videoId);
|
||||
|
||||
await fetch('/api/user/history', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
id: videoHash,
|
||||
watched: new Date(),
|
||||
progress: progress,
|
||||
title: {
|
||||
cipher: title?.cipher,
|
||||
nonce: title?.nonce
|
||||
},
|
||||
author: {
|
||||
cipher: author?.cipher,
|
||||
nonce: author?.nonce
|
||||
},
|
||||
thumbnail: {
|
||||
cipher: thumbnail?.cipher,
|
||||
nonce: thumbnail?.nonce
|
||||
},
|
||||
videoId: {
|
||||
cipher: videoId?.cipher,
|
||||
nonce: videoId?.nonce
|
||||
},
|
||||
lengthSeconds: video.lengthSeconds
|
||||
})
|
||||
});
|
||||
}
|
||||
+8
-10
@@ -1,8 +1,8 @@
|
||||
import type { ApiExntendedProgressModel } from './model';
|
||||
import { getVideoProgress } from '.';
|
||||
import type { VideoWatchHistory } from '../model';
|
||||
import { getWatchHistory } from './history';
|
||||
|
||||
const videoIds: string[] = [];
|
||||
const pendingResolves = new Map<string, (result: ApiExntendedProgressModel | undefined) => void>();
|
||||
const pendingResolves = new Map<string, (result: VideoWatchHistory | undefined) => void>();
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
const DEBOUNCE_MS = 1000;
|
||||
@@ -15,15 +15,15 @@ async function processBatches(): Promise<void> {
|
||||
batches.push(videoIds.splice(0, BATCH_SIZE));
|
||||
}
|
||||
|
||||
const results: ApiExntendedProgressModel[] = [];
|
||||
const results: VideoWatchHistory[] = [];
|
||||
|
||||
for (const batch of batches) {
|
||||
const res: ApiExntendedProgressModel[] = await getVideoProgress(batch.join(','));
|
||||
const res: VideoWatchHistory[] = await getWatchHistory({ videoIds: batch });
|
||||
results.push(...res);
|
||||
|
||||
// Resolve pending promises for this batch
|
||||
for (const videoId of batch) {
|
||||
const match = res.find((item) => item.video_id === videoId);
|
||||
const match = res.find((item) => item.videoId === videoId);
|
||||
const resolve = pendingResolves.get(videoId);
|
||||
if (resolve) {
|
||||
resolve(match);
|
||||
@@ -33,12 +33,10 @@ async function processBatches(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export function queueGetWatchProgress(
|
||||
videoId: string
|
||||
): Promise<ApiExntendedProgressModel | undefined> {
|
||||
export function queueGetWatchHistory(videoId: string): Promise<VideoWatchHistory | undefined> {
|
||||
videoIds.push(videoId);
|
||||
|
||||
const promise = new Promise<ApiExntendedProgressModel | undefined>((resolve) => {
|
||||
const promise = new Promise<VideoWatchHistory | undefined>((resolve) => {
|
||||
pendingResolves.set(videoId, resolve);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import sodium from 'libsodium-wrappers-sumo';
|
||||
import { rawMasterKeyStore } from '$lib/store';
|
||||
|
||||
export type DerivePassword = (rawPassword: string, passwordSalt: Uint8Array) => Promise<Uint8Array>;
|
||||
|
||||
export async function createUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
|
||||
const loginHash = await derivePassword(rawPassword, passwordSalt);
|
||||
|
||||
const decryptionKeySalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
32,
|
||||
rawPassword,
|
||||
decryptionKeySalt,
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.randombytes_buf(sodium.crypto_secretbox_KEYBYTES);
|
||||
const decryptionMasterKeyNonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
|
||||
const masterKeyCipher = sodium.crypto_secretbox_easy(
|
||||
rawDecryptionMasterKey,
|
||||
decryptionMasterKeyNonce,
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
const userCreateResp = await fetch('/api/user/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: {
|
||||
hash: sodium.to_base64(loginHash),
|
||||
salt: sodium.to_base64(passwordSalt)
|
||||
},
|
||||
decryptionKeySalt: sodium.to_base64(decryptionKeySalt),
|
||||
masterKey: {
|
||||
cipher: sodium.to_base64(masterKeyCipher),
|
||||
nonce: sodium.to_base64(decryptionMasterKeyNonce)
|
||||
},
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!userCreateResp.ok) return false;
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function loginUserBackend(
|
||||
username: string,
|
||||
rawPassword: string,
|
||||
captchaPayload: string,
|
||||
derivePassword: DerivePassword
|
||||
): Promise<boolean> {
|
||||
await sodium.ready;
|
||||
|
||||
const passwordSaltsResp = await fetch(`/api/user/${username}/public`);
|
||||
if (!passwordSaltsResp.ok) return false;
|
||||
|
||||
const passwordSalts = await passwordSaltsResp.json();
|
||||
|
||||
const loginHash = await derivePassword(
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.passwordSalt)
|
||||
);
|
||||
|
||||
const loginResp = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
passwordHash: sodium.to_base64(loginHash),
|
||||
captchaPayload
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!loginResp.ok) return false;
|
||||
|
||||
const loginJson = await loginResp.json();
|
||||
|
||||
const rawDecryptionKey = sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
rawPassword,
|
||||
sodium.from_base64(passwordSalts.decryptionKeySalt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT
|
||||
);
|
||||
|
||||
const rawDecryptionMasterKey = sodium.crypto_secretbox_open_easy(
|
||||
sodium.from_base64(loginJson.masterKeyCipher),
|
||||
sodium.from_base64(loginJson.masterKeyNonce),
|
||||
rawDecryptionKey
|
||||
);
|
||||
|
||||
rawMasterKeyStore.set(sodium.to_base64(rawDecryptionMasterKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { decryptWithMasterKey, encryptWithMasterKey } from './encryption';
|
||||
|
||||
export async function addOrUpdateKeyValue(key: string, value: string) {
|
||||
const valueEncrypted = await encryptWithMasterKey(value);
|
||||
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
valueCipher: valueEncrypted?.cipher,
|
||||
valueNonce: valueEncrypted?.nonce
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKeyValue(key: string) {
|
||||
await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export type KeyValue = boolean | number | string[] | string | object | undefined;
|
||||
|
||||
export async function getKeyValue(key: string): Promise<KeyValue | null> {
|
||||
const resp = await fetch(`/api/user/keyValue/${key}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return null;
|
||||
|
||||
const respJson = await resp.json();
|
||||
return (await decryptWithMasterKey(respJson.valueNonce, respJson.valueCipher)) ?? null;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { parseChannelRSS } from '$lib/api/youtubejs/subscriptions';
|
||||
import { getChannelYTjs } from '$lib/api/youtubejs/channel';
|
||||
import type { ChannelSubscriptions } from '$lib/dexie';
|
||||
import { decryptWithMasterKey, encryptWithMasterKey, getRawKey, getSecureHash } from './encryption';
|
||||
|
||||
export async function getSubscriptionsBackend(): Promise<ChannelSubscriptions[]> {
|
||||
const resp = await fetch(`/api/user/subscriptions`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!resp.ok) return [];
|
||||
|
||||
const subscriptions: ChannelSubscriptions[] = [];
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
for (const sub of respJson.subscriptions) {
|
||||
subscriptions.push({
|
||||
channelName: (await decryptWithMasterKey(sub.channelNameNonce, sub.channelNameCipher)) ?? '',
|
||||
channelId: (await decryptWithMasterKey(sub.channelIdNonce, sub.channelIdCipher)) ?? '',
|
||||
lastRSSFetch: new Date(sub.lastRSSFetch)
|
||||
});
|
||||
}
|
||||
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
export async function updateRSSLastUpdated(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function amSubscribedBackend(authorId: string): Promise<boolean> {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!resp.ok) return false;
|
||||
|
||||
const respJson = await resp.json();
|
||||
|
||||
return respJson.amSubscribed;
|
||||
}
|
||||
|
||||
export async function deleteUnsubscribeBackend(authorId: string) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return false;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
}
|
||||
|
||||
export async function postSubscribeBackend(
|
||||
authorId: string,
|
||||
authorName: string | undefined = undefined
|
||||
) {
|
||||
const rawKey = await getRawKey();
|
||||
if (!rawKey) return;
|
||||
|
||||
const internalAuthorId = await getSecureHash(authorId, rawKey);
|
||||
|
||||
if (!authorName) {
|
||||
const channel = await getChannelYTjs(authorId);
|
||||
authorName = channel.author;
|
||||
}
|
||||
|
||||
const channelId = await encryptWithMasterKey(authorId);
|
||||
const channelName = await encryptWithMasterKey(authorName);
|
||||
|
||||
const resp = await fetch(`/api/user/subscriptions/${internalAuthorId}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
channelIdCipher: channelId?.cipher,
|
||||
channelIdNonce: channelId?.nonce,
|
||||
channelNameCipher: channelName?.cipher,
|
||||
channelNameNonce: channelName?.nonce
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (resp.ok) parseChannelRSS(authorId);
|
||||
}
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
playerYouTubeJsAlways,
|
||||
playerYouTubeJsFallback,
|
||||
rawMasterKeyStore,
|
||||
returnYTDislikesInstanceStore,
|
||||
synciousInstanceStore
|
||||
returnYTDislikesInstanceStore
|
||||
} from '../store';
|
||||
import type {
|
||||
ChannelPage,
|
||||
@@ -22,7 +21,6 @@ import type {
|
||||
ReturnYTDislikes,
|
||||
SearchSuggestion,
|
||||
Subscription,
|
||||
ApiExntendedProgressModel,
|
||||
Video,
|
||||
VideoPlay,
|
||||
SearchOptions,
|
||||
@@ -52,7 +50,7 @@ import {
|
||||
deleteUnsubscribeBackend,
|
||||
getSubscriptionsBackend,
|
||||
postSubscribeBackend
|
||||
} from './backend';
|
||||
} from './backend/subscriptions';
|
||||
import { getUserLocale } from '$lib/i18n';
|
||||
|
||||
export function buildPath(path: string): URL {
|
||||
@@ -597,62 +595,3 @@ export async function getThumbnail(
|
||||
);
|
||||
return URL.createObjectURL(await resp.blob());
|
||||
}
|
||||
|
||||
function buildApiExtendedAuthHeaders(): Record<string, Record<string, string>> {
|
||||
const authToken = get(invidiousAuthStore)?.token ?? '';
|
||||
return { headers: { Authorization: `Bearer ${authToken.replace('SID=', '')}` } };
|
||||
}
|
||||
|
||||
export async function getVideoProgress(
|
||||
videoId: string,
|
||||
fetchOptions: RequestInit = {}
|
||||
): Promise<ApiExntendedProgressModel[]> {
|
||||
const resp = await fetchErrorHandle(
|
||||
await fetch(`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`, {
|
||||
...buildApiExtendedAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
export async function saveVideoProgress(
|
||||
videoId: string,
|
||||
time: number,
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
const headers: Record<string, Record<string, string>> = buildApiExtendedAuthHeaders();
|
||||
headers['headers']['Content-type'] = 'application/json';
|
||||
|
||||
await fetchErrorHandle(
|
||||
await fetch(`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`, {
|
||||
...headers,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
time: time
|
||||
}),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteVideoProgress(videoId: string, fetchOptions: RequestInit = {}) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(`${get(synciousInstanceStore)}/video/${videoId}`, {
|
||||
method: 'DELETE',
|
||||
...buildApiExtendedAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteAllVideoProgress(fetchOptions: RequestInit = {}) {
|
||||
await fetchErrorHandle(
|
||||
await fetch(`${get(synciousInstanceStore)}/videos`, {
|
||||
method: 'DELETE',
|
||||
...buildApiExtendedAuthHeaders(),
|
||||
...fetchOptions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -302,15 +302,6 @@ export interface DeArrow {
|
||||
videoDuration: number;
|
||||
}
|
||||
|
||||
export interface ApiExntendedProgressModel {
|
||||
time: number;
|
||||
video_id: string;
|
||||
}
|
||||
|
||||
export interface SynciousSaveProgressModel {
|
||||
time: number;
|
||||
}
|
||||
|
||||
export type SearchResults = (Channel | Video | Playlist | HashTag)[] & {
|
||||
getContinuation?: () => Promise<SearchResults>;
|
||||
};
|
||||
@@ -330,3 +321,15 @@ export type ChannelOptions = {
|
||||
continuation?: string;
|
||||
sortBy?: ChannelSortBy;
|
||||
};
|
||||
|
||||
export interface VideoWatchHistory {
|
||||
author: string;
|
||||
watched: Date;
|
||||
lengthSeconds: number;
|
||||
progress: number;
|
||||
id: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
videoId: string;
|
||||
type: 'historyVideo';
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
engineMaxConcurrentChannelsStore,
|
||||
rawMasterKeyStore
|
||||
} from '$lib/store';
|
||||
import { getSubscriptionsBackend } from '../backend';
|
||||
import { getSubscriptionsBackend } from '../backend/subscriptions';
|
||||
|
||||
export async function getSubscriptionsYTjs(): Promise<Subscription[]> {
|
||||
const subscriptions: Subscription[] = [];
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
class:primary={!favoritedChannel}
|
||||
class:surface-container-highest={favoritedChannel}
|
||||
onclick={toggleFavourited}
|
||||
class="square"
|
||||
>
|
||||
<i>star</i>
|
||||
<div class="tooltip">
|
||||
|
||||
@@ -123,18 +123,12 @@
|
||||
}}
|
||||
role="presentation"
|
||||
>
|
||||
<div class="field prefix fill no-margin rounded search">
|
||||
<div class="field prefix fill no-margin search round">
|
||||
<i class="front" tabindex="-1">search</i>
|
||||
<input
|
||||
tabindex="0"
|
||||
id="search-box"
|
||||
placeholder={$_('searchPlaceholder')}
|
||||
bind:value={search}
|
||||
class="rounded"
|
||||
/>
|
||||
<input tabindex="0" id="search-box" placeholder={$_('searchPlaceholder')} bind:value={search} />
|
||||
{#if showSearchBox}
|
||||
<menu class="min rounded suggestions-container">
|
||||
<div class="field large prefix suffix no-margin fixed">
|
||||
<menu class="min suggestions-container fill">
|
||||
<div class="field large prefix fill suffix no-margin fixed round">
|
||||
<i class="front" role="presentation" onclick={() => dispatch('searchCancelled')}
|
||||
>arrow_back</i
|
||||
>
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
onclick={async () => {
|
||||
await shareURL(`${syncOrigin}${resolve(`/?sync=${createShareBase64()}`, {})}`);
|
||||
}}
|
||||
class="square round"
|
||||
class="round"
|
||||
>
|
||||
<i>content_copy</i>
|
||||
</button>
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if item.type === 'video' || item.type === 'shortVideo' || item.type === 'stream'}
|
||||
{#if item.type === 'video' || item.type === 'shortVideo' || item.type === 'stream' || item.type === 'historyVideo'}
|
||||
{#key item.videoId}
|
||||
<Thumbnail video={item} {playlistId} />
|
||||
{/key}
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { Slider } from 'melt/builders';
|
||||
import { deleteVideoProgress, getVideoProgress, saveVideoProgress } from '$lib/api';
|
||||
import type { VideoPlay } from '$lib/api/model';
|
||||
import {
|
||||
invidiousAuthStore,
|
||||
isAndroidTvStore,
|
||||
playerAlwaysLoopStore,
|
||||
playerAndroidLockOrientation,
|
||||
@@ -31,12 +29,11 @@
|
||||
playerState,
|
||||
playertheatreModeIsActive,
|
||||
playerYouTubeJsFallback,
|
||||
rawMasterKeyStore,
|
||||
sponsorBlockCategoriesStore,
|
||||
sponsorBlockDisplayToastStore,
|
||||
sponsorBlockStore,
|
||||
sponsorBlockUrlStore,
|
||||
synciousInstanceStore,
|
||||
synciousStore
|
||||
sponsorBlockUrlStore
|
||||
} from '$lib/store';
|
||||
import { setStatusBarColor } from '$lib/theme';
|
||||
import { getVideoYTjs } from '$lib/api/youtubejs/video';
|
||||
@@ -65,6 +62,7 @@
|
||||
import { Network, type ConnectionStatus } from '@capacitor/network';
|
||||
import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation';
|
||||
import ClosedCaptions from './ClosedCaptions.svelte';
|
||||
import { getVideoWatchHistory, updateWatchHistory } from '$lib/api/backend/history';
|
||||
|
||||
interface Props {
|
||||
data: { video: VideoPlay; content: ParsedDescription; playlistId: string | null };
|
||||
@@ -257,7 +255,7 @@
|
||||
clearInterval(watchProgressInterval);
|
||||
}
|
||||
// Auto save watch progress every minute.
|
||||
watchProgressInterval = setInterval(() => savePlayerPos(), 60000);
|
||||
watchProgressInterval = setInterval(() => savePlayerbackHistory(), 60000);
|
||||
|
||||
let dashUrl: string;
|
||||
|
||||
@@ -284,9 +282,9 @@
|
||||
!data.video.fallbackPatch
|
||||
) {
|
||||
const manifest = await manifestDomainInclusion(dashUrl);
|
||||
await player.load(manifest, await getLastPlayPos());
|
||||
await player.load(manifest, await getPlaybackHistory());
|
||||
} else {
|
||||
await player.load(dashUrl, await getLastPlayPos());
|
||||
await player.load(dashUrl, await getPlaybackHistory());
|
||||
}
|
||||
|
||||
if (data.content.timestamps) {
|
||||
@@ -675,7 +673,7 @@
|
||||
playerElement?.addEventListener('pause', async () => {
|
||||
playerCurrentPlaybackState = false;
|
||||
playerIsBuffering = false;
|
||||
savePlayerPos();
|
||||
savePlayerbackHistory();
|
||||
showPlayerUI();
|
||||
});
|
||||
|
||||
@@ -739,7 +737,7 @@
|
||||
updateVideoPlayerHeight();
|
||||
});
|
||||
|
||||
async function getLastPlayPos(): Promise<number> {
|
||||
async function getPlaybackHistory(): Promise<number> {
|
||||
if (loadTimeFromUrl($page) || !$playerSavePlaybackPositionStore) return 0;
|
||||
|
||||
let toSetTime = 0;
|
||||
@@ -753,47 +751,33 @@
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
if ($synciousStore && $synciousInstanceStore && $invidiousAuthStore) {
|
||||
try {
|
||||
toSetTime = (await getVideoProgress(data.video.videoId))[0].time;
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
const watchHistory = await getVideoWatchHistory(data.video.videoId);
|
||||
if (watchHistory) toSetTime = watchHistory.progress;
|
||||
}
|
||||
|
||||
return toSetTime;
|
||||
}
|
||||
|
||||
function savePlayerPos() {
|
||||
if (data.video.liveNow) return;
|
||||
function savePlayerbackHistory() {
|
||||
if (data.video.liveNow || !$playerSavePlaybackPositionStore || !playerElement) return;
|
||||
|
||||
const synciousEnabled = $synciousStore && $synciousInstanceStore && $invidiousAuthStore;
|
||||
|
||||
if ($playerSavePlaybackPositionStore && playerElement) {
|
||||
if (
|
||||
playerElement.currentTime < playerElement.duration - 10 &&
|
||||
playerElement.currentTime > 10
|
||||
) {
|
||||
try {
|
||||
localStorage.setItem(`v_${data.video.videoId}`, playerElement.currentTime.toString());
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
if (synciousEnabled) {
|
||||
saveVideoProgress(data.video.videoId, playerElement.currentTime);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
localStorage.removeItem(`v_${data.video.videoId}`);
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
if (synciousEnabled) {
|
||||
deleteVideoProgress(data.video.videoId);
|
||||
}
|
||||
if (playerElement.currentTime < playerElement.duration - 10 && playerElement.currentTime > 10) {
|
||||
try {
|
||||
localStorage.setItem(`v_${data.video.videoId}`, playerElement.currentTime.toString());
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
localStorage.removeItem(`v_${data.video.videoId}`);
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
}
|
||||
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
updateWatchHistory(data.video.videoId, playerElement.currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,7 +798,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
savePlayerPos();
|
||||
savePlayerbackHistory();
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@
|
||||
display: block;
|
||||
margin-bottom: 0.1rem;
|
||||
height: 100px;
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 1rem 1rem 0 0;
|
||||
}
|
||||
|
||||
.timeline.tooltip {
|
||||
@@ -332,6 +332,7 @@
|
||||
will-change: transform;
|
||||
padding: 0;
|
||||
display: block;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.tooltip.thumb {
|
||||
@@ -349,8 +350,8 @@
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
border-top-right-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
border-top-right-radius: var(--player-border-radius);
|
||||
border-bottom-right-radius: var(--player-border-radius);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -361,7 +362,7 @@
|
||||
left: 0;
|
||||
height: var(--player-timeline-height);
|
||||
background-color: var(--secondary);
|
||||
border-radius: 0.25rem;
|
||||
border-radius: var(--player-border-radius);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { ensureNoTrailingSlash } from '../../misc';
|
||||
import { synciousInstanceStore, synciousStore } from '../../store';
|
||||
|
||||
let synciousInstance = $state(get(synciousInstanceStore));
|
||||
</script>
|
||||
|
||||
<form
|
||||
onsubmit={preventDefault(() =>
|
||||
synciousInstanceStore.set(ensureNoTrailingSlash(synciousInstance))
|
||||
)}
|
||||
>
|
||||
<nav>
|
||||
<div class="field prefix label surface-container-highest max">
|
||||
<i>link</i>
|
||||
<input tabindex="0" bind:value={synciousInstance} name="syncious-instance" type="text" />
|
||||
<label tabindex="-1" for="syncious-instance">{$_('layout.instanceUrl')}</label>
|
||||
</div>
|
||||
<button class="square">
|
||||
<i>done</i>
|
||||
</button>
|
||||
</nav>
|
||||
</form>
|
||||
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<p>{$_('enabled')}</p>
|
||||
</div>
|
||||
<label class="switch" tabindex="0">
|
||||
<input
|
||||
bind:checked={$synciousStore}
|
||||
onclick={() => synciousStore.set(!$synciousStore)}
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
@@ -22,7 +22,7 @@
|
||||
<input bind:value={deArrowUrl} name="dearrow-instance" type="text" />
|
||||
<label for="dearrow-instance">{$_('layout.instanceUrl')}</label>
|
||||
</div>
|
||||
<button class="square">
|
||||
<button>
|
||||
<i>done</i>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -35,7 +35,7 @@
|
||||
<input bind:value={deArrowThumbnailUrl} name="dearrow-thumbnail-instance" type="text" />
|
||||
<label for="dearrow-thumbnail-instance">{$_('layout.deArrow.thumbnailInstanceUrl')}</label>
|
||||
</div>
|
||||
<button class="square">
|
||||
<button>
|
||||
<i>done</i>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { postSubscribeYTjs } from '$lib/api/youtubejs/subscriptions';
|
||||
import { addToast } from '../Toast.svelte';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { postSubscribeBackend } from '$lib/api/backend';
|
||||
import { postSubscribeBackend } from '$lib/api/backend/subscriptions';
|
||||
|
||||
const engineFallbacks: EngineFallback[] = [
|
||||
'Channel',
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="square">
|
||||
<button>
|
||||
<i>done</i>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { _ } from '$lib/i18n';
|
||||
import { materialiousLogout } from '$lib/misc';
|
||||
import { watchHistoryEnabledStore } from '$lib/store';
|
||||
|
||||
let clickCount = $state(0);
|
||||
const clicksToDelte = 3;
|
||||
@@ -21,8 +22,27 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<h6>Delete account</h6>
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.historyEnabled')}</div>
|
||||
</div>
|
||||
<label class="switch" tabindex="0">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$watchHistoryEnabledStore}
|
||||
onclick={() => watchHistoryEnabledStore.set(!$watchHistoryEnabledStore)}
|
||||
role="switch"
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="space"></div>
|
||||
<div class="divider"></div>
|
||||
<div class="space"></div>
|
||||
|
||||
<button class="tertiary" onclick={deleteAccount}>
|
||||
<i>warning</i>
|
||||
<span>{$_('layout.deleteAccount')}</span>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<input tabindex="0" bind:value={returnYTInstance} name="returnyt-instance" type="text" />
|
||||
<label tabindex="-1" for="returnyt-instance">{$_('layout.instanceUrl')}</label>
|
||||
</div>
|
||||
<button class="square">
|
||||
<button>
|
||||
<i>done</i>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount, type Component } from 'svelte';
|
||||
import { _ } from '$lib/i18n';
|
||||
import ApiExtended from './ApiExtended.svelte';
|
||||
import DeArrow from './DeArrow.svelte';
|
||||
import Interface from './Interface.svelte';
|
||||
import Player from './Player.svelte';
|
||||
@@ -23,7 +22,6 @@
|
||||
{ id: 'interface', label: $_('layout.interface'), icon: 'grid_view', component: Interface },
|
||||
{ id: 'player', label: $_('layout.player.title'), icon: 'smart_display', component: Player },
|
||||
{ id: 'ryd', label: 'Return YT Dislike', icon: 'thumb_down', component: Ryd },
|
||||
{ id: 'api extended', label: 'API Extended', icon: 'sync', component: ApiExtended },
|
||||
{ id: 'sponsorblock', label: 'Sponsorblock', icon: 'block', component: SponsorBlock },
|
||||
{
|
||||
id: 'dearrow',
|
||||
@@ -244,5 +242,10 @@
|
||||
.settings {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tab-menu {
|
||||
height: fit-content !important;
|
||||
max-height: fit-content !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
/>
|
||||
<label tabindex="-1" for="sponsorblock-instance">{$_('layout.instanceUrl')}</label>
|
||||
</div>
|
||||
<button class="square">
|
||||
<button>
|
||||
<i>done</i>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -7,26 +7,32 @@
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { getDeArrow, getThumbnail } from '$lib/api';
|
||||
import type { Notification, PlaylistPageVideo, Video, VideoBase } from '$lib/api/model';
|
||||
import type {
|
||||
Notification,
|
||||
PlaylistPageVideo,
|
||||
Video,
|
||||
VideoBase,
|
||||
VideoWatchHistory
|
||||
} from '$lib/api/model';
|
||||
import { createVideoUrl, insecureRequestImageHandler } from '$lib/misc';
|
||||
import type { PlayerEvents } from '$lib/player';
|
||||
import {
|
||||
invidiousAuthStore,
|
||||
deArrowEnabledStore,
|
||||
interfaceLowBandwidthMode,
|
||||
isAndroidTvStore,
|
||||
playerSavePlaybackPositionStore,
|
||||
playerState,
|
||||
rawMasterKeyStore,
|
||||
syncPartyConnectionsStore,
|
||||
syncPartyPeerStore,
|
||||
synciousInstanceStore,
|
||||
synciousStore
|
||||
syncPartyPeerStore
|
||||
} from '$lib/store';
|
||||
import { queueGetWatchProgress } from '$lib/api/apiExtended';
|
||||
import { relativeTimestamp } from '$lib/time';
|
||||
import { queueGetWatchHistory } from '$lib/api/backend/historyPool';
|
||||
import { page } from '$app/state';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
|
||||
interface Props {
|
||||
video: VideoBase | Video | Notification | PlaylistPageVideo;
|
||||
video: VideoBase | Video | Notification | PlaylistPageVideo | VideoWatchHistory;
|
||||
playlistId?: string;
|
||||
sideways?: boolean;
|
||||
}
|
||||
@@ -37,6 +43,8 @@
|
||||
|
||||
let watchUrl = createVideoUrl(video.videoId, playlistId);
|
||||
|
||||
let beenWatched: boolean = $state(false);
|
||||
|
||||
syncPartyPeerStore.subscribe((peer) => {
|
||||
if (peer) {
|
||||
watchUrl.searchParams.set('sync', peer.id);
|
||||
@@ -65,6 +73,10 @@
|
||||
} else sideways = true;
|
||||
}
|
||||
|
||||
function checkIfWatched() {
|
||||
beenWatched = !!(progress && !page.url.pathname.endsWith('/history'));
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
calcThumbnailPlaceholderHeight();
|
||||
|
||||
@@ -76,9 +88,24 @@
|
||||
calcThumbnailPlaceholderHeight();
|
||||
});
|
||||
|
||||
checkIfWatched();
|
||||
|
||||
if (
|
||||
!page.url.pathname.endsWith('/history') &&
|
||||
isOwnBackend()?.internalAuth &&
|
||||
get(rawMasterKeyStore)
|
||||
)
|
||||
queueGetWatchHistory(video.videoId).then((watchHistory) => {
|
||||
if (watchHistory) {
|
||||
progress = watchHistory.progress.toString();
|
||||
checkIfWatched();
|
||||
}
|
||||
});
|
||||
|
||||
if (get(interfaceLowBandwidthMode)) return;
|
||||
|
||||
let imageSrc = getBestThumbnail(video.videoThumbnails) as string;
|
||||
let imageSrc =
|
||||
'thumbnail' in video ? video.thumbnail : (getBestThumbnail(video.videoThumbnails) as string);
|
||||
|
||||
if (get(deArrowEnabledStore)) {
|
||||
try {
|
||||
@@ -107,14 +134,6 @@
|
||||
img.onload = () => {
|
||||
thumbnail = img;
|
||||
};
|
||||
|
||||
if (get(synciousStore) && get(synciousInstanceStore) && get(invidiousAuthStore)) {
|
||||
try {
|
||||
progress = (await queueGetWatchProgress(video.videoId))?.time?.toString() ?? undefined;
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -173,13 +192,30 @@
|
||||
onclick={onVideoSelected}
|
||||
>
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
{#if !thumbnail}
|
||||
<div class="secondary-container" style="width: 100%;height: {placeholderHeight}px;"></div>
|
||||
{:else}
|
||||
<div class:crop={thumbnail.height > 300}>
|
||||
<img class="responsive" loading="lazy" src={thumbnail.src} alt="Thumbnail for video" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="thumbnail-image">
|
||||
{#if !thumbnail}
|
||||
<div
|
||||
class="secondary-container"
|
||||
style="width: 100%;height: {placeholderHeight}px;"
|
||||
></div>
|
||||
{:else}
|
||||
<div class:crop={thumbnail.height > 300}>
|
||||
<img
|
||||
class="responsive"
|
||||
class:watched={beenWatched}
|
||||
loading="lazy"
|
||||
src={thumbnail.src}
|
||||
alt="Thumbnail for video"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if beenWatched}
|
||||
<div class="chip surface-container-highest">
|
||||
<i>check</i>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if progress}
|
||||
<progress
|
||||
@@ -225,7 +261,7 @@
|
||||
</a>
|
||||
|
||||
<div>
|
||||
{#if video.authorId}
|
||||
{#if 'authorId' in video && video.authorId}
|
||||
<a
|
||||
tabindex="-1"
|
||||
class:author={!sideways}
|
||||
@@ -237,7 +273,7 @@
|
||||
<p>{video.author}</p>
|
||||
{/if}
|
||||
|
||||
{#if video.promotedBy === 'favourited'}
|
||||
{#if 'promotedBy' in video && video.promotedBy === 'favourited'}
|
||||
<i>star</i>
|
||||
{/if}
|
||||
|
||||
@@ -331,6 +367,20 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.thumbnail-image {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thumbnail-image .chip {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.watched {
|
||||
filter: brightness(40%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1499px) {
|
||||
.sideways-root .thumbnail {
|
||||
width: 100%;
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
{#if transcriptCues.length > 0}
|
||||
<div class="space"></div>
|
||||
<div class="max field suffix prefix small no-margin surface-container-highest">
|
||||
<div class="max field suffix prefix small round no-margin surface-container-highest">
|
||||
<i class="front">search</i><input
|
||||
bind:value={search}
|
||||
oninput={searchTranscript}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
:root {
|
||||
--video-player-height: 0px;
|
||||
--safe-area-inset-top: 0px;
|
||||
--border-radius: 0.5rem;
|
||||
--player-border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -37,11 +39,6 @@ article {
|
||||
border: 0.5px solid var(--primary);
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
|
||||
.thumbnail-corner {
|
||||
padding: 0.1em 0.4em;
|
||||
}
|
||||
@@ -122,13 +119,18 @@ menu {
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
background-color: var(--surface) !important;
|
||||
border: 0.1px solid var(--primary) !important;
|
||||
background-color: var(--surface-container-highest) !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0.25rem !important;
|
||||
color: var(--on-surface) !important;
|
||||
}
|
||||
|
||||
button,
|
||||
.button,
|
||||
.round,
|
||||
menu {
|
||||
border-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
||||
.altcha {
|
||||
border: none !important;
|
||||
}
|
||||
@@ -142,7 +144,7 @@ menu.player-settings {
|
||||
.player-slider {
|
||||
height: var(--player-timeline-height);
|
||||
margin: 0 auto;
|
||||
border-radius: 0.25rem;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.player-slider.full-width {
|
||||
@@ -157,7 +159,7 @@ menu.player-settings {
|
||||
background: var(--secondary-container);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-radius: 0.25rem;
|
||||
border-radius: var(--player-border-radius);
|
||||
}
|
||||
|
||||
.player-slider .range {
|
||||
@@ -165,7 +167,7 @@ menu.player-settings {
|
||||
background: var(--inverse-primary);
|
||||
inset: 0;
|
||||
right: var(--percentage-inv);
|
||||
border-radius: 0.25rem;
|
||||
border-radius: var(--player-border-radius);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
||||
import { persistedStores } from './settings';
|
||||
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { addOrUpdateKeyValue, getKeyValue } from '$lib/api/backend';
|
||||
import { addOrUpdateKeyValue, getKeyValue } from '$lib/api/backend/keyvalue';
|
||||
import { rawMasterKeyStore } from '$lib/store';
|
||||
import { getPublicEnv } from '$lib/misc';
|
||||
|
||||
@@ -16,6 +16,8 @@ export async function syncSettingsToBackend() {
|
||||
|
||||
await Promise.all(
|
||||
persistedStores.map(async (store) => {
|
||||
if (store.excludeFromBackendSync) return;
|
||||
|
||||
getKeyValue(store.name).then((currentKeyValue) => {
|
||||
if (currentKeyValue !== null || allowNullOverwrite.includes(store.name)) {
|
||||
const currentKeyValueParsed = parseWithSchema(store.schema, currentKeyValue);
|
||||
|
||||
@@ -30,8 +30,6 @@ import {
|
||||
sponsorBlockDisplayToastStore,
|
||||
sponsorBlockStore,
|
||||
sponsorBlockUrlStore,
|
||||
synciousInstanceStore,
|
||||
synciousStore,
|
||||
themeColorStore,
|
||||
interfaceAutoExpandChapters,
|
||||
playerDefaultPlaybackSpeed,
|
||||
@@ -53,7 +51,8 @@ import {
|
||||
playerYouTubeJsFallback,
|
||||
playerYouTubeJsAlways,
|
||||
interfaceSearchHistoryEnabled,
|
||||
playerPreferredVolumeStore
|
||||
playerPreferredVolumeStore,
|
||||
watchHistoryEnabledStore
|
||||
} from '$lib/store';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
|
||||
@@ -63,6 +62,7 @@ type PersistedStore<T> = {
|
||||
schema: z.ZodType<T>;
|
||||
serialize?: (value: T) => string;
|
||||
excludeFromBookmarklet?: boolean; // Won't be include in bookmarklet
|
||||
excludeFromBackendSync?: boolean;
|
||||
};
|
||||
|
||||
const zBoolean = z.coerce.boolean();
|
||||
@@ -163,16 +163,6 @@ export const persistedStores: PersistedStore<any>[] = [
|
||||
store: deArrowThumbnailInstanceStore,
|
||||
schema: zString
|
||||
},
|
||||
{
|
||||
name: 'syncious',
|
||||
store: synciousStore,
|
||||
schema: zBoolean
|
||||
},
|
||||
{
|
||||
name: 'synciousInstance',
|
||||
store: synciousInstanceStore,
|
||||
schema: zString
|
||||
},
|
||||
{
|
||||
name: 'region',
|
||||
store: interfaceRegionStore,
|
||||
@@ -261,7 +251,8 @@ export const persistedStores: PersistedStore<any>[] = [
|
||||
{
|
||||
name: 'preferredVolume',
|
||||
store: playerPreferredVolumeStore,
|
||||
schema: zFloat
|
||||
schema: zFloat,
|
||||
excludeFromBackendSync: true
|
||||
},
|
||||
{
|
||||
name: 'sponsorBlockCategoriesv2',
|
||||
@@ -350,6 +341,11 @@ if (isOwnBackend()) {
|
||||
store: playerYouTubeJsAlways,
|
||||
schema: zBoolean
|
||||
});
|
||||
persistedStores.push({
|
||||
name: 'watchHistoryEnabled',
|
||||
store: watchHistoryEnabledStore,
|
||||
schema: zBoolean
|
||||
});
|
||||
}
|
||||
|
||||
export const persistedStoreKeys = persistedStores.map((store) => store.name);
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"invidiousRedirect": "Invidious-Weiterleitungslink kopieren",
|
||||
"youtubeLink": "YouTube-Link kopieren",
|
||||
"includeTimestamp": "Mit Zeitstempel",
|
||||
"copiedSuccess": "In Zwischenablage kopiert"
|
||||
"copiedSuccess": "In Zwischenablage kopiert",
|
||||
"copyXLink": "{{linkType}}-Link kopieren"
|
||||
},
|
||||
"download": "Herunterladen",
|
||||
"downloadSteps": {
|
||||
@@ -197,14 +198,21 @@
|
||||
"about": "Über",
|
||||
"androidNativeShare": "Natives Teilen nutzen",
|
||||
"joinSyncParty": "Synchronisationsgruppen-URL eingeben",
|
||||
"shareURL": "URL teilen"
|
||||
"shareURL": "URL teilen",
|
||||
"engine": "Backend-Engine",
|
||||
"materialiousAccount": "Konto",
|
||||
"deleteAccount": "Konto löschen",
|
||||
"clickXmoreTimesToDelete": "Klicke {{clicksTillDelete}} weitere(s) Mal(e) zum Löschen",
|
||||
"backendEngine": {
|
||||
"warning": "Erweiterte Einstellungen! Nur für erfahrene Nutzer. Ändere sie nicht, ausser du weisst, was du tust."
|
||||
}
|
||||
},
|
||||
"subscribe": "Abonnieren",
|
||||
"recommendedVideos": "Vorgeschlagene Videos",
|
||||
"playlistVideos": "Wiedergabelisten Videos",
|
||||
"defaultPage": "Standardseite",
|
||||
"disabled": "Deaktiviert",
|
||||
"invidiousLogin": "Bitte melde dich mit deinem Invidious-Konto an",
|
||||
"invidiousLogin": "Bitte melde dich mit deinem Invidious-Konto an.",
|
||||
"invalidInstance": "Bitte prüfe die URL. Wenn sie korrekt ist, ist die Instanz möglicherweise außer Betrieb oder unterstützt keine Clients von Drittanbietern.",
|
||||
"login": "Anmeldung",
|
||||
"premium": "YouTube-Premium-Inhalte können auf Materialious nicht angeschaut werden.",
|
||||
@@ -226,5 +234,32 @@
|
||||
"favouriteChannel": "Kanal favorisieren",
|
||||
"unfavouriteChannel": "Kanal entfavorisieren",
|
||||
"skip": "Überspringen",
|
||||
"upcomingSegment": "Kommend "
|
||||
"upcomingSegment": "Kommend ",
|
||||
"copy": "JSON kopieren",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"captionStyle": "Untertitel-Stil",
|
||||
"invalidPassword": "Ungültiges Passwort",
|
||||
"usernameTaken": "Benutzername vergeben",
|
||||
"linkInvidious": "Invidious-Konto verknüpfen",
|
||||
"unlinkInvidious": "Invidious-Konto entknüpfen",
|
||||
"createAccount": "Konto erstellen",
|
||||
"needRegister": "Ich muss mich registrieren",
|
||||
"registrationDisabled": "Registrierung aktuell deaktiviert, kontaktiere den Instanzinhaber.",
|
||||
"needLogin": "Ich muss mich anmelden",
|
||||
"materialiousLogin": "Bitte melde dich mit deinem Materialious-Konto an.",
|
||||
"materialiousCreate": "Bitte erstelle dein Materialious-Konto.",
|
||||
"backend": "Backend",
|
||||
"initalSetup": {
|
||||
"useInvidious": "Ich möchte Invidious nutzen",
|
||||
"useLocalFallback": "Ich möchte den lokalen Video-Fallback nutzen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"unsure": "Unsicher",
|
||||
"required": "Einrichtung erforderlich",
|
||||
"invidiousInfo": "Invidious ist eine selbständig hostbare YouTube-Benutzeroberfläche.",
|
||||
"localFallbackInfo": "Wenn das Laden eines Videos von Invidious fehlschlägt, lädt Materialious es lokal.",
|
||||
"configureInstance": "Instanz konfigurieren",
|
||||
"done": "Fertig"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"companionUrl": "Companion URL",
|
||||
"deleteAccount": "Delete account",
|
||||
"clickXmoreTimesToDelete": "Click {{clicksTillDelete}} more time(s) to delete",
|
||||
"historyEnabled": "Save watch history",
|
||||
"theme": {
|
||||
"theme": "Theme",
|
||||
"darkMode": "Dark mode",
|
||||
|
||||
@@ -95,7 +95,14 @@
|
||||
"about": "Über",
|
||||
"androidNativeShare": "Nativs Teile bruche",
|
||||
"shareURL": "URL teile",
|
||||
"joinSyncParty": "Synchronistationsgruppe-URL iigeh"
|
||||
"joinSyncParty": "Synchronistationsgruppe-URL iigeh",
|
||||
"engine": "Backend-Engine",
|
||||
"materialiousAccount": "Konto",
|
||||
"deleteAccount": "Konto lösche",
|
||||
"clickXmoreTimesToDelete": "Klick {{clicksTillDelete}} wiiteri Mal zum Lösche",
|
||||
"backendEngine": {
|
||||
"warning": "Erwiterti Iistellige! Nur für erfahreni Nutzer. Ändere sie nöd, usser du weisch, was du machsch."
|
||||
}
|
||||
},
|
||||
"videos": "Videos",
|
||||
"cancel": "Abbreche",
|
||||
@@ -169,7 +176,8 @@
|
||||
"youtubeLink": "YouTube-Link kopiere",
|
||||
"invidiousRedirect": "Invidious-Wiiterleitigslink kopiere",
|
||||
"includeTimestamp": "Mit Ziitstempel",
|
||||
"copiedSuccess": "I d Zwüscheablaag kopiert"
|
||||
"copiedSuccess": "I d Zwüscheablaag kopiert",
|
||||
"copyXLink": "{{linkType}}-Link kopiere"
|
||||
},
|
||||
"download": "Abelade",
|
||||
"downloadSteps": {
|
||||
@@ -204,7 +212,7 @@
|
||||
}
|
||||
},
|
||||
"subscribe": "Abonniere",
|
||||
"invidiousLogin": "Bitte meld dich mit dim Invidious-Konto aa",
|
||||
"invidiousLogin": "Bitte meld dich mit dim Invidious-Konto aa.",
|
||||
"invalidInstance": "Bitte prüef d URL. Wenn si korrekt isch, isch d Instanz möglicherwiis usser Betriib oder unterstützt kei Clients vo Drittaabüüter.",
|
||||
"loginRequired": "Aameldig erforderlich",
|
||||
"isUpcoming": "Video isch nonig imene aaluegbare Zuestand.",
|
||||
@@ -226,5 +234,32 @@
|
||||
"favouriteChannel": "Kanal favorisiere",
|
||||
"unfavouriteChannel": "Kanal entfavorisiere",
|
||||
"skip": "Überspringe",
|
||||
"upcomingSegment": "Kommend "
|
||||
"upcomingSegment": "Kommend ",
|
||||
"copy": "JSON kopiere",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"captionStyle": "Untertitel-Stil",
|
||||
"invalidPassword": "Ungültigs Passwort",
|
||||
"usernameTaken": "Benutzername vergeh",
|
||||
"linkInvidious": "Invidious-Konto verchnüpfe",
|
||||
"unlinkInvidious": "Invidious-Konto entchnüpfe",
|
||||
"createAccount": "Konto erstelle",
|
||||
"needRegister": "Ich muess mich registriere",
|
||||
"registrationDisabled": "Registrierig isch aktuell deaktiviert, kontaktier de Instanzinhaber.",
|
||||
"needLogin": "Ich muess mich aamelde",
|
||||
"materialiousLogin": "Bitte meld dich mit dim Materialious-Konto aa.",
|
||||
"materialiousCreate": "Bite erstell dis Materialious-Konto.",
|
||||
"backend": "Backend",
|
||||
"initalSetup": {
|
||||
"useInvidious": "Ich möcht Invidious nutze",
|
||||
"useLocalFallback": "Ich möcht de lokali Video-Fallback nutze",
|
||||
"yes": "Ja",
|
||||
"no": "Nei",
|
||||
"unsure": "Unsicher",
|
||||
"required": "Iirichtig erforderlich",
|
||||
"invidiousInfo": "Invidious isch e selbständig hostbari YouTube-Benutzeroberflächi.",
|
||||
"localFallbackInfo": "Wenn s Lade vomene Video vo Invidious fehlschlaat, ladet Materialious es lokal.",
|
||||
"configureInstance": "Instanz konfiguriere",
|
||||
"done": "Fertig"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ import type {
|
||||
PlaylistPage,
|
||||
PlaylistPageVideo,
|
||||
Video,
|
||||
VideoBase
|
||||
VideoBase,
|
||||
VideoWatchHistory
|
||||
} from './api/model';
|
||||
import { page } from '$app/state';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
@@ -155,7 +156,8 @@ export type feedItem =
|
||||
| Video
|
||||
| Playlist
|
||||
| HashTag
|
||||
| PlaylistPage;
|
||||
| PlaylistPage
|
||||
| VideoWatchHistory;
|
||||
export type feedItems = feedItem[];
|
||||
|
||||
export function extractUniqueId(item: feedItem): string {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { isYTBackend } from './misc';
|
||||
import { invidiousAuthStore } from './store';
|
||||
import { invidiousAuthStore, rawMasterKeyStore } from './store';
|
||||
import { isOwnBackend } from './shared';
|
||||
|
||||
export type Pages = { icon: string; href: string; name: string; requiresAuth: boolean }[];
|
||||
|
||||
@@ -28,6 +29,14 @@ export function getPages(): Pages {
|
||||
}
|
||||
];
|
||||
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore))
|
||||
pages.push({
|
||||
icon: 'history',
|
||||
href: '/history',
|
||||
name: get(_)('pages.history'),
|
||||
requiresAuth: false
|
||||
});
|
||||
|
||||
pages = pages.filter((page) => {
|
||||
return !page.requiresAuth || (get(invidiousAuthStore) && !isYTBackend());
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ let UserTable: ModelCtor<Model<any, any>>;
|
||||
let ChannelSubscriptionTable: ModelCtor<Model<any, any>>;
|
||||
let CaptchaTable: ModelCtor<Model<any, any>>;
|
||||
let UserKeyValueTable: ModelCtor<Model<any, any>>;
|
||||
let UserHistoryTable: ModelCtor<Model<any, any>>;
|
||||
|
||||
let sequelizeInstance: Sequelize | null = null;
|
||||
|
||||
@@ -15,6 +16,7 @@ export function getSequelize(): {
|
||||
ChannelSubscriptionTable: ModelCtor<Model<any, any>>;
|
||||
CaptchaTable: ModelCtor<Model<any, any>>;
|
||||
UserKeyValueTable: ModelCtor<Model<any, any>>;
|
||||
UserHistoryTable: ModelCtor<Model<any, any>>;
|
||||
} {
|
||||
if (sequelizeInstance) {
|
||||
return {
|
||||
@@ -22,7 +24,8 @@ export function getSequelize(): {
|
||||
UserTable,
|
||||
ChannelSubscriptionTable,
|
||||
CaptchaTable,
|
||||
UserKeyValueTable
|
||||
UserKeyValueTable,
|
||||
UserHistoryTable
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,6 +110,58 @@ export function getSequelize(): {
|
||||
}
|
||||
);
|
||||
|
||||
UserHistoryTable = sequelizeInstance.define('UserHistory', {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
primaryKey: true
|
||||
},
|
||||
watched: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
lengthSeconds: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
progress: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
videoIdCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
videoIdNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
titleCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
titleNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
authorCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
authorNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
thumbnailCipher: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
thumbnailNonce: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
ChannelSubscriptionTable = sequelizeInstance.define('Subscriptions', {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
@@ -149,13 +204,15 @@ export function getSequelize(): {
|
||||
|
||||
UserTable.hasMany(ChannelSubscriptionTable);
|
||||
UserTable.hasMany(UserKeyValueTable);
|
||||
UserTable.hasMany(UserHistoryTable);
|
||||
|
||||
return {
|
||||
sequelize: sequelizeInstance,
|
||||
UserTable,
|
||||
ChannelSubscriptionTable,
|
||||
CaptchaTable,
|
||||
UserKeyValueTable
|
||||
UserKeyValueTable,
|
||||
UserHistoryTable
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -203,18 +203,6 @@ export const returnYTDislikesInstanceStore: Writable<string | null | undefined>
|
||||
'returnYTDislikesInstance'
|
||||
);
|
||||
|
||||
export const synciousStore = persist(writable(false), createStorage(), 'syncious');
|
||||
export const synciousInstanceStore: Writable<string | null | undefined> = persist(
|
||||
writable(
|
||||
ifNotWebDefault(
|
||||
getPublicEnv('DEFAULT_SYNCIOUS_INSTANCE') || getPublicEnv('DEFAULT_API_EXTENDED_INSTANCE'),
|
||||
'https://extended-api.materialio.us'
|
||||
)
|
||||
),
|
||||
createStorage(),
|
||||
'synciousInstance'
|
||||
);
|
||||
|
||||
export const interfaceRegionStore: Writable<string> = persist(
|
||||
writable('US'),
|
||||
createStorage(),
|
||||
@@ -341,6 +329,12 @@ export const rawMasterKeyStore: Writable<string | undefined> = persist(
|
||||
'rawMasterKey'
|
||||
);
|
||||
|
||||
export const watchHistoryEnabledStore: Writable<boolean> = persist(
|
||||
writable(true),
|
||||
createStorage(),
|
||||
'watchHistoryEnabled'
|
||||
);
|
||||
|
||||
export const syncPartyPeerStore: Writable<Peer | null> = writable(null);
|
||||
export const syncPartyConnectionsStore: Writable<DataConnection[] | null> = writable();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
invidiousAuthStore,
|
||||
playerProxyVideosStore,
|
||||
playerState,
|
||||
rawMasterKeyStore,
|
||||
returnYTDislikesInstanceStore,
|
||||
returnYtDislikesStore
|
||||
} from '$lib/store';
|
||||
@@ -17,6 +18,8 @@ import { parseDescription } from '$lib/description';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
import { _ } from './i18n';
|
||||
import { isOwnBackend } from './shared';
|
||||
import { saveWatchHistory } from './api/backend/history';
|
||||
|
||||
export async function getWatchDetails(videoId: string, url: URL) {
|
||||
const playerStateRetrieved = get(playerState);
|
||||
@@ -45,6 +48,10 @@ export async function getWatchDetails(videoId: string, url: URL) {
|
||||
personalPlaylists = null;
|
||||
}
|
||||
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
saveWatchHistory(video);
|
||||
}
|
||||
|
||||
let comments;
|
||||
try {
|
||||
comments = video.liveNow ? null : getComments(videoId, { sort_by: 'top' }, { priority: 'low' });
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { deleteWatchHistory, getWatchHistory } from '$lib/api/backend/history';
|
||||
import ItemsList from '$lib/components/layout/ItemsList.svelte';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
import { _ } from '$lib/i18n';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let currentPage = 1;
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
currentPage++;
|
||||
|
||||
const history = await getWatchHistory({ page: currentPage });
|
||||
if (history.length === 0) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
data.videos = [...data.videos, ...history];
|
||||
event.detail.loaded();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHistory() {
|
||||
await deleteWatchHistory();
|
||||
data.videos = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if data.videos.length > 0}
|
||||
<nav class="right-align">
|
||||
<button onclick={deleteHistory} class="button surface-container-highest">
|
||||
<i>clear_all</i>
|
||||
<span>{$_('deleteAllHistory')}</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="space"></div>
|
||||
{/if}
|
||||
|
||||
<ItemsList items={data.videos} />
|
||||
<InfiniteLoading on:infinite={loadMore} />
|
||||
@@ -0,0 +1,13 @@
|
||||
import { resolve } from '$app/paths';
|
||||
import { getWatchHistory } from '$lib/api/backend/history';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { rawMasterKeyStore } from '$lib/store';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export async function load() {
|
||||
if (!isOwnBackend()?.internalAuth || !get(rawMasterKeyStore))
|
||||
throw redirect(302, resolve('/', {}));
|
||||
|
||||
return { videos: await getWatchHistory() };
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
<nav class="right-align padding">
|
||||
<button
|
||||
onclick={async () => await removePlaylistItem(playlist.playlistId)}
|
||||
class="tertiary square round"
|
||||
class="tertiary"
|
||||
>
|
||||
<i>delete</i>
|
||||
</button>
|
||||
@@ -61,7 +61,7 @@
|
||||
{/each}
|
||||
<ContentColumn>
|
||||
<article style="height: 100%;display: flex;align-items: center;justify-content: center;">
|
||||
<button onclick={() => ui('#create-playlist')} class="round extra">
|
||||
<button onclick={() => ui('#create-playlist')} class="extra">
|
||||
<i>add_circle</i>
|
||||
<span>{$_('playlist.createPlaylist')}</span>
|
||||
</button>
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
</h6></a
|
||||
>
|
||||
<div class="max"></div>
|
||||
<button onclick={async () => unsubscribe(sub.authorId)} class="border">Unsubscribe</button
|
||||
<button onclick={async () => unsubscribe(sub.authorId)} class="surface-container-highest"
|
||||
>Unsubscribe</button
|
||||
>
|
||||
</nav>
|
||||
</article>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
import { page } from '$app/state';
|
||||
import Share from '$lib/components/Share.svelte';
|
||||
import Playlist from '$lib/components/watch/Playlist.svelte';
|
||||
import type { PlayerEvents } from '$lib/player/index.js';
|
||||
import type { PlayerEvents } from '$lib/player/index';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
@@ -397,10 +397,10 @@
|
||||
<h5 class="no-margin">{letterCase(data.video.title)}</h5>
|
||||
|
||||
<div class="grid no-padding">
|
||||
<div class="s12 m12 l5" style="height: 100%;display: flex;align-items: center;">
|
||||
<div class="s12 m12 l7" style="height: 100%;display: flex;align-items: center;">
|
||||
<Author channel={data.video} />
|
||||
</div>
|
||||
<div class="s12 m12 l7 video-actions">
|
||||
<div class="s12 m12 l5 video-actions">
|
||||
<div>
|
||||
<LikesDislikes video={data.video} returnYTDislikes={data.streamed.returnYTDislikes} />
|
||||
|
||||
@@ -451,25 +451,25 @@
|
||||
},
|
||||
{
|
||||
type: 'invidious',
|
||||
path: `/watch?=${data.video.videoId}`,
|
||||
path: `/watch?v=${data.video.videoId}`,
|
||||
param: {
|
||||
key: 'v',
|
||||
key: 't',
|
||||
value: () => Math.round(playerCurrentTime)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'invidious redirect',
|
||||
path: `/watch?=${data.video.videoId}`,
|
||||
path: `/watch?v=${data.video.videoId}`,
|
||||
param: {
|
||||
key: 'v',
|
||||
key: 't',
|
||||
value: () => Math.round(playerCurrentTime)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'youtube',
|
||||
path: `/watch?=${data.video.videoId}`,
|
||||
path: `/watch?v=${data.video.videoId}`,
|
||||
param: {
|
||||
key: 'v',
|
||||
key: 't',
|
||||
value: () => Math.round(playerCurrentTime)
|
||||
}
|
||||
}
|
||||
@@ -695,7 +695,7 @@
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
@media screen and (max-width: 1200px) {
|
||||
.video-actions {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -32,7 +32,7 @@ const allowedBaseDomains: string[] = [
|
||||
'youtube.com',
|
||||
'ytimg.com',
|
||||
'googlevideo.com',
|
||||
'returnyoutubedislike.com',
|
||||
'returnyoutubedislikeapi.com',
|
||||
'ajay.app'
|
||||
];
|
||||
|
||||
@@ -48,7 +48,6 @@ const dynamicAllowDomainsEnvVars = [
|
||||
env.PUBLIC_DEFAULT_INVIDIOUS_INSTANCE,
|
||||
env.PUBLIC_DEFAULT_RETURNYTDISLIKES_INSTANCE,
|
||||
env.PUBLIC_DEFAULT_API_EXTENDED_INSTANCE,
|
||||
env.PUBLIC_DEFAULT_SYNCIOUS_INSTANCE,
|
||||
env.PUBLIC_DEFAULT_COMPANION_INSTANCE
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { getSequelize } from '$lib/server/database';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import z from 'zod';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
const zUserHistory = z.object({
|
||||
id: z.string().max(255),
|
||||
watched: z.coerce.date(),
|
||||
progress: z.number().max(115200),
|
||||
lengthSeconds: z.number().max(115200),
|
||||
title: z.object({
|
||||
cipher: z.string().max(255),
|
||||
nonce: z.string().max(255)
|
||||
}),
|
||||
author: z.object({
|
||||
cipher: z.string().max(255),
|
||||
nonce: z.string().max(255)
|
||||
}),
|
||||
thumbnail: z.object({
|
||||
cipher: z.string().max(1000),
|
||||
nonce: z.string().max(255)
|
||||
}),
|
||||
videoId: z.object({
|
||||
cipher: z.string().max(255),
|
||||
nonce: z.string().max(255)
|
||||
})
|
||||
});
|
||||
|
||||
export async function POST({ locals, request }) {
|
||||
if (!locals.userId) throw error(401, 'Unauthorized');
|
||||
|
||||
const data = zUserHistory.safeParse(await request.json());
|
||||
|
||||
if (!data.success) throw error(400, data.error.message);
|
||||
|
||||
const history = await getSequelize().UserHistoryTable.findOne({
|
||||
where: {
|
||||
UserId: locals.userId,
|
||||
id: data.data.id
|
||||
}
|
||||
});
|
||||
|
||||
const toStore = {
|
||||
watched: data.data.watched,
|
||||
lengthSeconds: data.data.lengthSeconds,
|
||||
titleCipher: data.data.title.cipher,
|
||||
titleNonce: data.data.title.nonce,
|
||||
authorCipher: data.data.author.cipher,
|
||||
authorNonce: data.data.author.nonce,
|
||||
thumbnailCipher: data.data.thumbnail.cipher,
|
||||
thumbnailNonce: data.data.thumbnail.nonce,
|
||||
videoIdCipher: data.data.videoId.cipher,
|
||||
videoIdNonce: data.data.videoId.nonce
|
||||
};
|
||||
|
||||
if (history) {
|
||||
await history.update(toStore);
|
||||
} else {
|
||||
await getSequelize().UserHistoryTable.create({
|
||||
UserId: locals.userId,
|
||||
id: data.data.id,
|
||||
progress: data.data.progress,
|
||||
...toStore
|
||||
});
|
||||
}
|
||||
|
||||
// Cull any history older then 3 months.
|
||||
const threeMonthsAgo = new Date();
|
||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
||||
getSequelize().UserHistoryTable.destroy({
|
||||
where: {
|
||||
UserId: locals.userId,
|
||||
watched: {
|
||||
[Op.lt]: threeMonthsAgo
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
export async function GET({ locals, url }) {
|
||||
if (!locals.userId) throw error(401, 'Unauthorized');
|
||||
|
||||
const limit = 100;
|
||||
const page = Number(url.searchParams.get('page') ?? 0);
|
||||
|
||||
const videoHashes = url.searchParams.get('videoHashes');
|
||||
let videoHashesList: string[] = [];
|
||||
|
||||
if (videoHashes) {
|
||||
videoHashesList = videoHashes.split(',');
|
||||
}
|
||||
|
||||
const whereClause: any = {
|
||||
UserId: locals.userId
|
||||
};
|
||||
|
||||
if (videoHashesList.length > 0) {
|
||||
whereClause.id = { [Op.in]: videoHashesList };
|
||||
}
|
||||
|
||||
const history = await getSequelize().UserHistoryTable.findAll({
|
||||
where: whereClause,
|
||||
limit,
|
||||
offset: page > 0 ? limit * page : undefined,
|
||||
order: [['watched', 'DESC']]
|
||||
});
|
||||
|
||||
return json(history);
|
||||
}
|
||||
|
||||
export async function DELETE({ locals }) {
|
||||
await getSequelize().UserHistoryTable.destroy({
|
||||
where: {
|
||||
UserId: locals.userId
|
||||
}
|
||||
});
|
||||
|
||||
return new Response();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getSequelize } from '$lib/server/database';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import z from 'zod';
|
||||
|
||||
const zUserHistoryUpdate = z
|
||||
.object({
|
||||
watched: z.coerce.date().optional(),
|
||||
progress: z.number().max(115200).optional()
|
||||
})
|
||||
.transform((data) => {
|
||||
return Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
|
||||
});
|
||||
|
||||
export async function POST({ locals, request, params }) {
|
||||
if (!locals.userId) throw error(401, 'Unauthorized');
|
||||
|
||||
const data = zUserHistoryUpdate.safeParse(await request.json());
|
||||
if (!data.success) throw error(400, data.error.message);
|
||||
|
||||
await getSequelize().UserHistoryTable.update(data.data, {
|
||||
where: { UserId: locals.userId, id: params.videoHash }
|
||||
});
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
export async function DELETE({ locals, params }) {
|
||||
if (!locals.userId) throw error(401, 'Unauthorized');
|
||||
|
||||
await getSequelize().UserHistoryTable.destroy({
|
||||
where: { UserId: locals.userId, id: params.videoHash }
|
||||
});
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
export async function GET({ locals, params }) {
|
||||
if (!locals.userId) throw error(401, 'Unauthorized');
|
||||
|
||||
const history = await getSequelize().UserHistoryTable.findOne({
|
||||
where: { UserId: locals.userId, id: params.videoHash }
|
||||
});
|
||||
|
||||
if (!history) throw error(404);
|
||||
|
||||
return json(history);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { persistedStoreKeys } from '$lib/externalSettings/settings.js';
|
||||
import { getUser } from '$lib/server/user.js';
|
||||
import { persistedStoreKeys } from '$lib/externalSettings/settings';
|
||||
import { getUser } from '$lib/server/user';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import z from 'zod';
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.16.5"
|
||||
LATEST_VERSION = "1.16.6"
|
||||
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
|
||||
|
||||
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")
|
||||
|
||||
Reference in New Issue
Block a user