Merge pull request #1494 from Materialious/update/1.16.6

Update/1.16.6
This commit is contained in:
Ward
2026-02-24 14:55:27 +13:00
committed by GitHub
60 changed files with 1370 additions and 904 deletions
+1 -1
View File
@@ -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.
-34
View File
@@ -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
View File
@@ -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.
-4
View File
@@ -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
+2 -2
View File
@@ -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">
+173 -133
View File
@@ -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"
},
+3 -4
View File
@@ -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,
}
+127 -108
View File
@@ -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"
+4 -4
View File
@@ -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",
-4
View File
@@ -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' {} +
-287
View File
@@ -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)
);
}
+159
View File
@@ -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
})
});
}
@@ -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);
});
+112
View File
@@ -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);
}
+2 -63
View File
@@ -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
})
);
}
+12 -9
View File
@@ -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">
+4 -10
View File
@@ -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}
+13 -11
View File
@@ -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);
+39 -4
View File
@@ -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",
+39 -4
View File
@@ -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"
}
}
+4 -2
View File
@@ -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 {
+10 -1
View File
@@ -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());
});
+59 -2
View File
@@ -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
};
}
+6 -12
View File
@@ -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();
+7
View File
@@ -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
View File
@@ -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")