wip: player

This commit is contained in:
ThaUnknown 2025-02-17 23:54:18 +01:00
parent a8a73ee9cc
commit 0fd3e29ec9
No known key found for this signature in database
42 changed files with 1573 additions and 163 deletions

View file

@ -1,13 +1,15 @@
import config from 'eslint-config-standard-universal'
import tseslint from 'typescript-eslint'
import svelteConfig from './svelte.config.js'
export default tseslint.config(
...config(),
{
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname
tsconfigRootDir: import.meta.dirname,
svelteConfig
}
},
}
}
)

View file

@ -22,15 +22,14 @@
"@types/events": "^3.0.3",
"@urql/introspection": "^1.1.0",
"autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"bits-ui": "^0.22.0",
"cmdk-sv": "^0.0.18",
"eslint-config-standard-universal": "^1.0.1",
"eslint-config-standard-universal": "github:thaunknown/eslint-config-standard-universal",
"globals": "^15.11.0",
"gql.tada": "^1.8.10",
"hayase-extensions": "github:hayase-app/extensions",
"svelte": "^4.2.7",
"svelte": "^4.2.19",
"svelte-check": "^4.0.5",
"svelte-eslint-parser": "^0.41.1",
"svelte-radix": "^1.1.1",
"svelte-sonner": "^0.3.28",
"tailwindcss": "^3.4.13",
@ -53,6 +52,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"debug": "^4.3.7",
"events": "^3.3.0",
"idb-keyval": "^6.2.1",
"js-levenshtein": "^1.1.6",
"lucide-svelte": "^0.452.0",

View file

@ -13,25 +13,25 @@ importers:
version: 5.1.0
'@prgm/sveltekit-progress-bar':
specifier: 2.0.0
version: 2.0.0(@sveltejs/kit@2.8.1)(svelte@4.2.19)
version: 2.0.0(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)
'@thaunknown/web-irc':
specifier: ^1.0.1
version: 1.0.1
'@urql/exchange-auth':
specifier: ^2.2.0
version: 2.2.0(@urql/core@5.1.0)
version: 2.2.0(@urql/core@5.1.0(graphql@16.10.0))
'@urql/exchange-graphcache':
specifier: ^7.2.1
version: 7.2.1(@urql/core@5.1.0)(graphql@16.10.0)
version: 7.2.1(@urql/core@5.1.0(graphql@16.10.0))(graphql@16.10.0)
'@urql/exchange-request-policy':
specifier: ^1.2.0
version: 1.2.0(@urql/core@5.1.0)
version: 1.2.0(@urql/core@5.1.0(graphql@16.10.0))
'@urql/exchange-retry':
specifier: ^1.3.0
version: 1.3.0(@urql/core@5.1.0)
version: 1.3.0(@urql/core@5.1.0(graphql@16.10.0))
'@urql/svelte':
specifier: ^4.2.1
version: 4.2.1(@urql/core@5.1.0)(svelte@4.2.19)
version: 4.2.1(@urql/core@5.1.0(graphql@16.10.0))(svelte@4.2.19)
abslink:
specifier: ^1.0.9
version: 1.0.9
@ -50,6 +50,9 @@ importers:
debug:
specifier: ^4.3.7
version: 4.3.7
events:
specifier: ^3.3.0
version: 3.3.0
idb-keyval:
specifier: ^6.2.1
version: 6.2.1
@ -79,20 +82,20 @@ importers:
version: 2.2.5
urql:
specifier: ^4.2.1
version: 4.2.1(@urql/core@5.1.0)(react@19.0.0)
version: 4.2.1(@urql/core@5.1.0(graphql@16.10.0))(react@19.0.0)
devDependencies:
'@gql.tada/svelte-support':
specifier: ^1.0.1
version: 1.0.1(svelte@4.2.19)(typescript@5.7.2)
'@sveltejs/adapter-auto':
specifier: ^3.2.5
version: 3.2.5(@sveltejs/kit@2.8.1)
version: 3.2.5(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11))
'@sveltejs/adapter-static':
specifier: ^3.0.5
version: 3.0.5(@sveltejs/kit@2.8.1)
version: 3.0.5(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11))
'@sveltejs/kit':
specifier: ^2.8.1
version: 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2)(svelte@4.2.19)(vite@5.4.11)
version: 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11)
'@sveltejs/vite-plugin-svelte':
specifier: ^3.1.2
version: 3.1.2(svelte@4.2.19)(vite@5.4.11)
@ -107,34 +110,31 @@ importers:
version: 1.1.0(graphql@16.10.0)
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.4.49)
version: 10.4.20(postcss@8.4.47)
bits-ui:
specifier: ^0.21.16
version: 0.21.16(svelte@4.2.19)
specifier: ^0.22.0
version: 0.22.0(svelte@4.2.19)
cmdk-sv:
specifier: ^0.0.18
version: 0.0.18(svelte@4.2.19)
eslint-config-standard-universal:
specifier: ^1.0.1
version: 1.0.1(@typescript-eslint/parser@8.18.0)(svelte@4.2.19)(typescript@5.7.2)
specifier: github:thaunknown/eslint-config-standard-universal
version: https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/d50760bd09eb47a2082cb7ec0310e0104ff4e799(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(jiti@1.21.6)
globals:
specifier: ^15.11.0
version: 15.11.0
gql.tada:
specifier: ^1.8.10
version: 1.8.10(@gql.tada/svelte-support@1.0.1)(graphql@16.10.0)(typescript@5.7.2)
version: 1.8.10(@gql.tada/svelte-support@1.0.1(svelte@4.2.19)(typescript@5.7.2))(graphql@16.10.0)(typescript@5.7.2)
hayase-extensions:
specifier: github:hayase-app/extensions
version: https://codeload.github.com/hayase-app/extensions/tar.gz/edf2e76fd25e9ed24cde1be03f82ce5703758e7a
svelte:
specifier: ^4.2.7
specifier: ^4.2.19
version: 4.2.19
svelte-check:
specifier: ^4.0.5
version: 4.0.5(svelte@4.2.19)(typescript@5.7.2)
svelte-eslint-parser:
specifier: ^0.41.1
version: 0.41.1(svelte@4.2.19)
version: 4.0.5(picomatch@4.0.2)(svelte@4.2.19)(typescript@5.7.2)
svelte-radix:
specifier: ^1.1.1
version: 1.1.1(svelte@4.2.19)
@ -561,8 +561,8 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@stylistic/eslint-plugin@2.12.1':
resolution: {integrity: sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==}
'@stylistic/eslint-plugin@3.1.0':
resolution: {integrity: sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: '>=8.40.0'
@ -735,7 +735,6 @@ packages:
anitomyscript@https://codeload.github.com/thaunknown/anitomyscript/tar.gz/21a751e60a9efca83adb502212a3298f0e9a31e6:
resolution: {tarball: https://codeload.github.com/thaunknown/anitomyscript/tar.gz/21a751e60a9efca83adb502212a3298f0e9a31e6}
name: anitomyscript
version: 2.0.7
ansi-regex@5.0.1:
@ -835,6 +834,11 @@ packages:
peerDependencies:
svelte: ^4.0.0 || ^5.0.0-next.118
bits-ui@0.22.0:
resolution: {integrity: sha512-r7Fw1HNgA4YxZBRcozl7oP0bheQ8EHh+kfMBZJgyFISix8t4p/nqDcHLmBgIiJ3T5XjYnJRorYDjIWaCfhb5fw==}
peerDependencies:
svelte: ^4.0.0 || ^5.0.0
bittorrent-peerid@1.3.6:
resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==}
@ -1095,8 +1099,9 @@ packages:
peerDependencies:
eslint: '>=6.0.0'
eslint-config-standard-universal@1.0.1:
resolution: {integrity: sha512-vNoLFO1HYht4Lcl/QNovcreQxoS1wPDNaoIchdakm21B0EQlMdyHyylHDrFIJdkQANjaZEQwBchw7WfFN7ndjg==}
eslint-config-standard-universal@https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/d50760bd09eb47a2082cb7ec0310e0104ff4e799:
resolution: {tarball: https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/d50760bd09eb47a2082cb7ec0310e0104ff4e799}
version: 1.0.4
eslint-import-resolver-node@0.3.9:
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
@ -1216,6 +1221,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@ -1388,7 +1397,6 @@ packages:
hayase-extensions@https://codeload.github.com/hayase-app/extensions/tar.gz/edf2e76fd25e9ed24cde1be03f82ce5703758e7a:
resolution: {tarball: https://codeload.github.com/hayase-app/extensions/tar.gz/edf2e76fd25e9ed24cde1be03f82ce5703758e7a}
name: hayase-extensions
version: 1.0.5
idb-keyval@6.2.1:
@ -2073,15 +2081,6 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0'
svelte-eslint-parser@0.41.1:
resolution: {integrity: sha512-08ndI6zTghzI8SuJAFpvMbA/haPSGn3xz19pjre19yYMw8Nw/wQJ2PrZBI/L8ijGTgtkWCQQiLLy+Z1tfaCwNA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.191
peerDependenciesMeta:
svelte:
optional: true
svelte-eslint-parser@0.43.0:
resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -2357,11 +2356,11 @@ packages:
snapshots:
'@0no-co/graphql.web@1.0.11(graphql@16.10.0)':
dependencies:
optionalDependencies:
graphql: 16.10.0
'@0no-co/graphql.web@1.0.12(graphql@16.10.0)':
dependencies:
optionalDependencies:
graphql: 16.10.0
'@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.7.2)':
@ -2446,9 +2445,9 @@ snapshots:
'@esbuild/win32-x64@0.21.5':
optional: true
'@eslint-community/eslint-utils@4.4.1(eslint@9.17.0)':
'@eslint-community/eslint-utils@4.4.1(eslint@9.17.0(jiti@1.21.6))':
dependencies:
eslint: 9.17.0
eslint: 9.17.0(jiti@1.21.6)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
@ -2500,13 +2499,14 @@ snapshots:
'@fontsource-variable/nunito@5.1.0': {}
'@gql.tada/cli-utils@1.6.3(@0no-co/graphqlsp@1.12.16)(@gql.tada/svelte-support@1.0.1)(graphql@16.10.0)(typescript@5.7.2)':
'@gql.tada/cli-utils@1.6.3(@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.7.2))(@gql.tada/svelte-support@1.0.1(svelte@4.2.19)(typescript@5.7.2))(graphql@16.10.0)(typescript@5.7.2)':
dependencies:
'@0no-co/graphqlsp': 1.12.16(graphql@16.10.0)(typescript@5.7.2)
'@gql.tada/internal': 1.0.8(graphql@16.10.0)(typescript@5.7.2)
'@gql.tada/svelte-support': 1.0.1(svelte@4.2.19)(typescript@5.7.2)
graphql: 16.10.0
typescript: 5.7.2
optionalDependencies:
'@gql.tada/svelte-support': 1.0.1(svelte@4.2.19)(typescript@5.7.2)
'@gql.tada/internal@1.0.8(graphql@16.10.0)(typescript@5.7.2)':
dependencies:
@ -2593,9 +2593,9 @@ snapshots:
'@polka/url@1.0.0-next.28': {}
'@prgm/sveltekit-progress-bar@2.0.0(@sveltejs/kit@2.8.1)(svelte@4.2.19)':
'@prgm/sveltekit-progress-bar@2.0.0(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)':
dependencies:
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2)(svelte@4.2.19)(vite@5.4.11)
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11)
svelte: 4.2.19
'@rollup/rollup-android-arm-eabi@4.27.2':
@ -2654,10 +2654,10 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@stylistic/eslint-plugin@2.12.1(eslint@9.17.0)(typescript@5.7.2)':
'@stylistic/eslint-plugin@3.1.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
eslint: 9.17.0
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.17.0(jiti@1.21.6)
eslint-visitor-keys: 4.2.0
espree: 10.3.0
estraverse: 5.3.0
@ -2666,16 +2666,16 @@ snapshots:
- supports-color
- typescript
'@sveltejs/adapter-auto@3.2.5(@sveltejs/kit@2.8.1)':
'@sveltejs/adapter-auto@3.2.5(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11))':
dependencies:
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2)(svelte@4.2.19)(vite@5.4.11)
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11)
import-meta-resolve: 4.1.0
'@sveltejs/adapter-static@3.0.5(@sveltejs/kit@2.8.1)':
'@sveltejs/adapter-static@3.0.5(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11))':
dependencies:
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2)(svelte@4.2.19)(vite@5.4.11)
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11)
'@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2)(svelte@4.2.19)(vite@5.4.11)':
'@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11)':
dependencies:
'@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.19)(vite@5.4.11)
'@types/cookie': 0.6.0
@ -2693,7 +2693,7 @@ snapshots:
tiny-glob: 0.2.9
vite: 5.4.11
'@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2)(svelte@4.2.19)(vite@5.4.11)':
'@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11)':
dependencies:
'@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.19)(vite@5.4.11)
debug: 4.3.7
@ -2704,7 +2704,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11)':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2)(svelte@4.2.19)(vite@5.4.11)
'@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.11))(svelte@4.2.19)(vite@5.4.11)
debug: 4.3.7
deepmerge: 4.3.1
kleur: 4.1.5
@ -2742,15 +2742,15 @@ snapshots:
'@types/ms@0.7.34': {}
'@typescript-eslint/eslint-plugin@8.18.0(@typescript-eslint/parser@8.18.0)(eslint@9.17.0)(typescript@5.7.2)':
'@typescript-eslint/eslint-plugin@8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/scope-manager': 8.18.0
'@typescript-eslint/type-utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
'@typescript-eslint/type-utils': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/visitor-keys': 8.18.0
eslint: 9.17.0
eslint: 9.17.0(jiti@1.21.6)
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@ -2759,14 +2759,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.18.0(eslint@9.17.0)(typescript@5.7.2)':
'@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@typescript-eslint/scope-manager': 8.18.0
'@typescript-eslint/types': 8.18.0
'@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2)
'@typescript-eslint/visitor-keys': 8.18.0
debug: 4.3.7
eslint: 9.17.0
eslint: 9.17.0(jiti@1.21.6)
typescript: 5.7.2
transitivePeerDependencies:
- supports-color
@ -2776,12 +2776,12 @@ snapshots:
'@typescript-eslint/types': 8.18.0
'@typescript-eslint/visitor-keys': 8.18.0
'@typescript-eslint/type-utils@8.18.0(eslint@9.17.0)(typescript@5.7.2)':
'@typescript-eslint/type-utils@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2)
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
debug: 4.3.7
eslint: 9.17.0
eslint: 9.17.0(jiti@1.21.6)
ts-api-utils: 1.3.0(typescript@5.7.2)
typescript: 5.7.2
transitivePeerDependencies:
@ -2803,13 +2803,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.18.0(eslint@9.17.0)(typescript@5.7.2)':
'@typescript-eslint/utils@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0)
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
'@typescript-eslint/scope-manager': 8.18.0
'@typescript-eslint/types': 8.18.0
'@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2)
eslint: 9.17.0
eslint: 9.17.0(jiti@1.21.6)
typescript: 5.7.2
transitivePeerDependencies:
- supports-color
@ -2826,12 +2826,12 @@ snapshots:
transitivePeerDependencies:
- graphql
'@urql/exchange-auth@2.2.0(@urql/core@5.1.0)':
'@urql/exchange-auth@2.2.0(@urql/core@5.1.0(graphql@16.10.0))':
dependencies:
'@urql/core': 5.1.0(graphql@16.10.0)
wonka: 6.3.4
'@urql/exchange-graphcache@7.2.1(@urql/core@5.1.0)(graphql@16.10.0)':
'@urql/exchange-graphcache@7.2.1(@urql/core@5.1.0(graphql@16.10.0))(graphql@16.10.0)':
dependencies:
'@0no-co/graphql.web': 1.0.11(graphql@16.10.0)
'@urql/core': 5.1.0(graphql@16.10.0)
@ -2839,12 +2839,12 @@ snapshots:
transitivePeerDependencies:
- graphql
'@urql/exchange-request-policy@1.2.0(@urql/core@5.1.0)':
'@urql/exchange-request-policy@1.2.0(@urql/core@5.1.0(graphql@16.10.0))':
dependencies:
'@urql/core': 5.1.0(graphql@16.10.0)
wonka: 6.3.4
'@urql/exchange-retry@1.3.0(@urql/core@5.1.0)':
'@urql/exchange-retry@1.3.0(@urql/core@5.1.0(graphql@16.10.0))':
dependencies:
'@urql/core': 5.1.0(graphql@16.10.0)
wonka: 6.3.4
@ -2853,7 +2853,7 @@ snapshots:
dependencies:
graphql: 16.10.0
'@urql/svelte@4.2.1(@urql/core@5.1.0)(svelte@4.2.19)':
'@urql/svelte@4.2.1(@urql/core@5.1.0(graphql@16.10.0))(svelte@4.2.19)':
dependencies:
'@urql/core': 5.1.0(graphql@16.10.0)
svelte: 4.2.19
@ -2955,14 +2955,14 @@ snapshots:
is-array-buffer: 3.0.4
is-shared-array-buffer: 1.0.3
autoprefixer@10.4.20(postcss@8.4.49):
autoprefixer@10.4.20(postcss@8.4.47):
dependencies:
browserslist: 4.24.0
caniuse-lite: 1.0.30001668
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.0
postcss: 8.4.49
postcss: 8.4.47
postcss-value-parser: 4.2.0
available-typed-arrays@1.0.7:
@ -2991,6 +2991,13 @@ snapshots:
nanoid: 5.0.7
svelte: 4.2.19
bits-ui@0.22.0(svelte@4.2.19):
dependencies:
'@internationalized/date': 3.5.6
'@melt-ui/svelte': 0.76.2(svelte@4.2.19)
nanoid: 5.0.7
svelte: 4.2.19
bittorrent-peerid@1.3.6: {}
bittorrent-tracker@9.19.0:
@ -3056,6 +3063,7 @@ snapshots:
bufferutil@4.0.9:
dependencies:
node-gyp-build: 4.8.2
optional: true
call-bind@1.0.7:
dependencies:
@ -3111,7 +3119,7 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
'@types/estree': 1.0.6
acorn: 8.12.1
acorn: 8.14.0
estree-walker: 3.0.3
periscopic: 3.1.0
@ -3337,31 +3345,31 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-compat-utils@0.5.1(eslint@9.17.0):
eslint-compat-utils@0.5.1(eslint@9.17.0(jiti@1.21.6)):
dependencies:
eslint: 9.17.0
eslint: 9.17.0(jiti@1.21.6)
semver: 7.6.3
eslint-config-standard-universal@1.0.1(@typescript-eslint/parser@8.18.0)(svelte@4.2.19)(typescript@5.7.2):
eslint-config-standard-universal@https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/d50760bd09eb47a2082cb7ec0310e0104ff4e799(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(jiti@1.21.6):
dependencies:
'@stylistic/eslint-plugin': 2.12.1(eslint@9.17.0)(typescript@5.7.2)
eslint: 9.17.0
'@stylistic/eslint-plugin': 3.1.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.17.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0)(eslint@9.17.0)
eslint-plugin-n: 17.15.0(eslint@9.17.0)
eslint-plugin-promise: 7.2.1(eslint@9.17.0)
eslint-plugin-svelte: 2.46.1(eslint@9.17.0)(svelte@4.2.19)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.6))
eslint-plugin-n: 17.15.0(eslint@9.17.0(jiti@1.21.6))
eslint-plugin-promise: 7.2.1(eslint@9.17.0(jiti@1.21.6))
eslint-plugin-svelte: 2.46.1(eslint@9.17.0(jiti@1.21.6))(svelte@4.2.19)
globals: 15.13.0
typescript-eslint: 8.18.0(eslint@9.17.0)(typescript@5.7.2)
svelte: 4.2.19
typescript: 5.7.2
typescript-eslint: 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- jiti
- supports-color
- svelte
- ts-node
- typescript
eslint-import-resolver-node@0.3.9:
dependencies:
@ -3371,35 +3379,35 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.0)(eslint-import-resolver-node@0.3.9)(eslint@9.17.0):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint@9.17.0(jiti@1.21.6)):
dependencies:
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
debug: 3.2.7
eslint: 9.17.0
optionalDependencies:
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.17.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
eslint-plugin-es-x@7.8.0(eslint@9.17.0):
eslint-plugin-es-x@7.8.0(eslint@9.17.0(jiti@1.21.6)):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0)
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
'@eslint-community/regexpp': 4.12.1
eslint: 9.17.0
eslint-compat-utils: 0.5.1(eslint@9.17.0)
eslint: 9.17.0(jiti@1.21.6)
eslint-compat-utils: 0.5.1(eslint@9.17.0(jiti@1.21.6))
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.0)(eslint@9.17.0):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.6)):
dependencies:
'@rtsao/scc': 1.1.0
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
array.prototype.flat: 1.3.2
array.prototype.flatmap: 1.3.2
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.17.0
eslint: 9.17.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.0)(eslint-import-resolver-node@0.3.9)(eslint@9.17.0)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint@9.17.0(jiti@1.21.6))
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@ -3410,34 +3418,36 @@ snapshots:
semver: 6.3.1
string.prototype.trimend: 1.0.8
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-n@17.15.0(eslint@9.17.0):
eslint-plugin-n@17.15.0(eslint@9.17.0(jiti@1.21.6)):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0)
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
enhanced-resolve: 5.17.1
eslint: 9.17.0
eslint-plugin-es-x: 7.8.0(eslint@9.17.0)
eslint: 9.17.0(jiti@1.21.6)
eslint-plugin-es-x: 7.8.0(eslint@9.17.0(jiti@1.21.6))
get-tsconfig: 4.8.1
globals: 15.13.0
ignore: 5.3.2
minimatch: 9.0.5
semver: 7.6.3
eslint-plugin-promise@7.2.1(eslint@9.17.0):
eslint-plugin-promise@7.2.1(eslint@9.17.0(jiti@1.21.6)):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0)
eslint: 9.17.0
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
eslint: 9.17.0(jiti@1.21.6)
eslint-plugin-svelte@2.46.1(eslint@9.17.0)(svelte@4.2.19):
eslint-plugin-svelte@2.46.1(eslint@9.17.0(jiti@1.21.6))(svelte@4.2.19):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0)
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
'@jridgewell/sourcemap-codec': 1.5.0
eslint: 9.17.0
eslint-compat-utils: 0.5.1(eslint@9.17.0)
eslint: 9.17.0(jiti@1.21.6)
eslint-compat-utils: 0.5.1(eslint@9.17.0(jiti@1.21.6))
esutils: 2.0.3
known-css-properties: 0.35.0
postcss: 8.4.49
@ -3445,8 +3455,9 @@ snapshots:
postcss-safe-parser: 6.0.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2
semver: 7.6.3
svelte: 4.2.19
svelte-eslint-parser: 0.43.0(svelte@4.2.19)
optionalDependencies:
svelte: 4.2.19
transitivePeerDependencies:
- ts-node
@ -3464,9 +3475,9 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint@9.17.0:
eslint@9.17.0(jiti@1.21.6):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0)
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.19.1
'@eslint/core': 0.9.1
@ -3500,6 +3511,8 @@ snapshots:
minimatch: 3.1.2
natural-compare: 1.4.0
optionator: 0.9.4
optionalDependencies:
jiti: 1.21.6
transitivePeerDependencies:
- supports-color
@ -3533,6 +3546,8 @@ snapshots:
esutils@2.0.3: {}
events@3.3.0: {}
fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
@ -3553,7 +3568,9 @@ snapshots:
dependencies:
reusify: 1.0.4
fdir@6.4.0: {}
fdir@6.4.0(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
file-entry-cache@8.0.0:
dependencies:
@ -3660,11 +3677,11 @@ snapshots:
dependencies:
get-intrinsic: 1.2.4
gql.tada@1.8.10(@gql.tada/svelte-support@1.0.1)(graphql@16.10.0)(typescript@5.7.2):
gql.tada@1.8.10(@gql.tada/svelte-support@1.0.1(svelte@4.2.19)(typescript@5.7.2))(graphql@16.10.0)(typescript@5.7.2):
dependencies:
'@0no-co/graphql.web': 1.0.11(graphql@16.10.0)
'@0no-co/graphqlsp': 1.12.16(graphql@16.10.0)(typescript@5.7.2)
'@gql.tada/cli-utils': 1.6.3(@0no-co/graphqlsp@1.12.16)(@gql.tada/svelte-support@1.0.1)(graphql@16.10.0)(typescript@5.7.2)
'@gql.tada/cli-utils': 1.6.3(@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.7.2))(@gql.tada/svelte-support@1.0.1(svelte@4.2.19)(typescript@5.7.2))(graphql@16.10.0)(typescript@5.7.2)
'@gql.tada/internal': 1.0.8(graphql@16.10.0)(typescript@5.7.2)
typescript: 5.7.2
transitivePeerDependencies:
@ -3932,7 +3949,8 @@ snapshots:
lower-case: 2.0.2
tslib: 2.7.0
node-gyp-build@4.8.2: {}
node-gyp-build@4.8.2:
optional: true
node-releases@2.0.18: {}
@ -4062,14 +4080,16 @@ snapshots:
postcss-load-config@3.1.4(postcss@8.4.49):
dependencies:
lilconfig: 2.1.0
postcss: 8.4.49
yaml: 1.10.2
optionalDependencies:
postcss: 8.4.49
postcss-load-config@4.0.2(postcss@8.4.47):
dependencies:
lilconfig: 3.1.2
postcss: 8.4.47
yaml: 2.6.0
optionalDependencies:
postcss: 8.4.47
postcss-nested@6.2.0(postcss@8.4.47):
dependencies:
@ -4080,10 +4100,6 @@ snapshots:
dependencies:
postcss: 8.4.49
postcss-scss@4.0.9(postcss@8.4.47):
dependencies:
postcss: 8.4.47
postcss-scss@4.0.9(postcss@8.4.49):
dependencies:
postcss: 8.4.49
@ -4378,11 +4394,11 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.0.5(svelte@4.2.19)(typescript@5.7.2):
svelte-check@4.0.5(picomatch@4.0.2)(svelte@4.2.19)(typescript@5.7.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 4.0.1
fdir: 6.4.0
fdir: 6.4.0(picomatch@4.0.2)
picocolors: 1.1.0
sade: 1.8.1
svelte: 4.2.19
@ -4390,15 +4406,6 @@ snapshots:
transitivePeerDependencies:
- picomatch
svelte-eslint-parser@0.41.1(svelte@4.2.19):
dependencies:
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1
postcss: 8.4.47
postcss-scss: 4.0.9(postcss@8.4.47)
svelte: 4.2.19
svelte-eslint-parser@0.43.0(svelte@4.2.19):
dependencies:
eslint-scope: 7.2.2
@ -4406,6 +4413,7 @@ snapshots:
espree: 9.6.1
postcss: 8.4.49
postcss-scss: 4.0.9(postcss@8.4.49)
optionalDependencies:
svelte: 4.2.19
svelte-hmr@0.16.0(svelte@4.2.19):
@ -4437,7 +4445,7 @@ snapshots:
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@types/estree': 1.0.6
acorn: 8.12.1
acorn: 8.14.0
aria-query: 5.3.2
axobject-query: 4.1.0
code-red: 1.0.4
@ -4558,12 +4566,12 @@ snapshots:
is-typed-array: 1.1.13
possible-typed-array-names: 1.0.0
typescript-eslint@8.18.0(eslint@9.17.0)(typescript@5.7.2):
typescript-eslint@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2):
dependencies:
'@typescript-eslint/eslint-plugin': 8.18.0(@typescript-eslint/parser@8.18.0)(eslint@9.17.0)(typescript@5.7.2)
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2)
eslint: 9.17.0
'@typescript-eslint/eslint-plugin': 8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/parser': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
'@typescript-eslint/utils': 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
eslint: 9.17.0(jiti@1.21.6)
typescript: 5.7.2
transitivePeerDependencies:
- supports-color
@ -4593,7 +4601,7 @@ snapshots:
dependencies:
punycode: 2.3.1
urql@4.2.1(@urql/core@5.1.0)(react@19.0.0):
urql@4.2.1(@urql/core@5.1.0(graphql@16.10.0))(react@19.0.0):
dependencies:
'@urql/core': 5.1.0(graphql@16.10.0)
react: 19.0.0
@ -4602,6 +4610,7 @@ snapshots:
utf-8-validate@5.0.10:
dependencies:
node-gyp-build: 4.8.2
optional: true
util-deprecate@1.0.2: {}
@ -4619,7 +4628,7 @@ snapshots:
fsevents: 2.3.3
vitefu@0.2.5(vite@5.4.11):
dependencies:
optionalDependencies:
vite: 5.4.11
vscode-languageserver-textdocument@1.0.12: {}
@ -4671,7 +4680,7 @@ snapshots:
optional: true
ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10):
dependencies:
optionalDependencies:
bufferutil: 4.0.9
utf-8-validate: 5.0.10

View file

@ -3,7 +3,6 @@
@tailwind utilities;
:root {
/* opt out of auto-dark-mode plugins, which break discord css, such as force-dark, dark-reader etc */
color-scheme: only light;
}

32
src/app.d.ts vendored
View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// See https://kit.svelte.dev/docs/types#app
import type { SessionMetadata } from '$lib/components/ui/player/util'
import type { Search } from '$lib/modules/anilist/queries'
import type { VariablesOf } from 'gql.tada'
@ -11,6 +11,15 @@ export interface AuthResponse {
token_type: 'Bearer'
}
export interface Track {
selected: boolean
enabled: boolean
id: string
kind: string
label: string
language: string
}
export interface Native {
authAL: (url: string) => Promise<AuthResponse>
restart: () => Promise<void>
@ -23,11 +32,15 @@ export interface Native {
selectDownload: () => Promise<string>
setAngle: (angle: string) => Promise<void>
getLogs: () => Promise<string>
getDeviceInfo: () => Promise<any>
getDeviceInfo: () => Promise<unknown>
openUIDevtools: () => Promise<void>
openTorrentDevtools: () => Promise<void>
checkUpdate: () => Promise<void>
toggleDiscordDetails: (enabled: boolean) => Promise<void>
setMediaSession: (metadata: SessionMetadata) => Promise<void>
setPositionState: (state?: MediaPositionState) => Promise<void>
setPlayBackState: (paused: 'none' | 'paused' | 'playing') => Promise<void>
setActionHandler: Navigator['mediaSession']['setActionHandler']
}
declare global {
@ -51,13 +64,26 @@ declare global {
function selectDownload (): Promise<string>
function setAngle (angle: string): Promise<void>
function getLogs (): Promise<string>
function getDeviceInfo (): Promise<any>
function getDeviceInfo (): Promise<unknown>
function openUIDevtools (): Promise<void>
function openTorrentDevtools (): Promise<void>
function checkUpdate (): Promise<void>
function toggleDiscordDetails (enabled: boolean): Promise<void>
function setMediaSession (metadata: SessionMetadata): Promise<void>
function setPositionState (state?: MediaPositionState): Promise<void>
function setPlayBackState (paused: 'none' | 'paused' | 'playing'): Promise<void>
function setActionHandler (...args: Parameters<Navigator['mediaSession']['setActionHandler']>): ReturnType<Navigator['mediaSession']['setActionHandler']>
function setTimeout (handler: TimerHandler, timeout?: number): number & { unref?: () => void }
interface HTMLMediaElement {
videoTracks?: Track[]
audioTracks?: Track[]
}
interface ScreenOrientation {
lock: (orientation: 'any' | 'natural' | 'landscape' | 'portrait' | 'portrait-primary' | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary') => Promise<void>
}
}
export {}

View file

@ -0,0 +1,9 @@
<script lang='ts'>
import { Icon, type Attrs } from 'lucide-svelte'
import type { SvelteHTMLElements } from 'svelte/elements'
const iconNode: Array<[elementName: keyof SvelteHTMLElements, attrs: Attrs]> = [['path', { d: 'M8 4.5v5H3m-1-6 6 6m13 0v-3c0-1.16-.84-2-2-2h-7m-9 9v2c0 1.05.95 2 2 2h3' }], ['rect', { width: '10', height: '7', x: '12', y: '13', rx: '2', fill: 'currentColor' }]]
</script>
<Icon name='picture-in-picture' {...$$props} {iconNode}>
<slot />
</Icon>

View file

@ -0,0 +1,9 @@
<script lang='ts'>
import { Icon, type Attrs } from 'lucide-svelte'
import type { SvelteHTMLElements } from 'svelte/elements'
const iconNode: Array<[elementName: keyof SvelteHTMLElements, attrs: Attrs]> = [['path', { d: 'M21 9V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h4' }], ['rect', { width: '10', height: '7', x: '12', y: '13', rx: '2', fill: 'currentColor' }]]
</script>
<Icon name='picture-in-picture-exit' {...$$props} {iconNode}>
<slot />
</Icon>

View file

@ -0,0 +1,12 @@
<script lang='ts'>
import { Icon, type Attrs } from 'lucide-svelte'
import type { SvelteHTMLElements } from 'svelte/elements'
const iconNode: Array<[elementName: keyof SvelteHTMLElements, attrs: Attrs]> = [
['path', { 'fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M22.99 11.7773C22.9219 10.8388 22.4205 9.92777 21.4859 9.39398L7.48782 1.39936C5.48785 0.25713 3 1.70127 3 4.00443V19.9937C3 22.2968 5.48785 23.741 7.48781 22.5988L21.4859 14.6041C22.4205 14.0703 22.9219 13.1593 22.99 12.2208C23.0226 12.0751 23.0226 11.9231 22.99 11.7773Z' }]
]
</script>
<Icon name='play' {...$$props} {iconNode} viewBox='0 0 24 24' strokeWidth='0'>
<slot />
</Icon>

View file

@ -0,0 +1,13 @@
<script lang='ts'>
import { Icon, type Attrs } from 'lucide-svelte'
import type { SvelteHTMLElements } from 'svelte/elements'
const iconNode: Array<[elementName: keyof SvelteHTMLElements, attrs: Attrs]> = [
['path', { fill: 'none', d: 'M0 0h24v24H0z' }],
['path', { d: 'M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM4 12h4v2H4v-2zm10 6H4v-2h10v2zm6 0h-4v-2h4v2zm0-4H10v-2h10v2z' }]
]
</script>
<Icon name='subtitles' {...$$props} {iconNode} viewBox='0 0 24 24' strokeWidth='0'>
<slot />
</Icon>

View file

@ -21,7 +21,7 @@
{#await src then src}
<div class={cn('object-cover w-screen absolute top-0 left-0 h-full overflow-hidden pointer-events-none bg-black', className)}>
{#if src}
<div class='min-w-[100vw] w-screen h-[30rem] bg-url bg-center bg-cover opacity-90 transition-opacity duration-500 border-gradient-to-t' style:--bg='url({src})' class:!opacity-15={$hideBanner} />
<div class='min-w-[100vw] w-screen h-[30rem] bg-url bg-center bg-cover opacity-100 transition-opacity duration-500 border-gradient-to-t' style:--bg='url({src})' class:!opacity-45={$hideBanner} />
{/if}
</div>
{/await}

View file

@ -13,7 +13,7 @@ const buttonVariants = tv({
destructive: 'bg-destructive text-destructive-foreground select:bg-destructive/90 shadow-sm',
outline: 'border-input bg-background select:bg-accent select:text-accent-foreground border shadow-sm',
secondary: 'bg-secondary text-secondary-foreground select:bg-secondary/80 shadow-sm',
ghost: 'select:bg-accent select:text-accent-foreground',
ghost: 'select:bg-secondary-foreground/10 select:text-accent-foreground',
link: 'text-primary underline-offset-4 select:underline'
},
size: {

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import Check from "svelte-radix/Check.svelte";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.CheckboxItemProps;
type $$Events = ContextMenuPrimitive.CheckboxItemEvents;
let className: $$Props["class"] = undefined;
export { className as class };
export let checked: $$Props["checked"] = undefined;
</script>
<ContextMenuPrimitive.CheckboxItem
bind:checked
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.CheckboxIndicator>
<Check class="h-4 w-4" />
</ContextMenuPrimitive.CheckboxIndicator>
</span>
<slot />
</ContextMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none",
className
)}
{...$$restProps}
on:keydown
>
<slot />
</ContextMenuPrimitive.Content>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.ItemProps & {
inset?: boolean;
};
type $$Events = ContextMenuPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Item
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<slot />
</ContextMenuPrimitive.Item>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.LabelProps & {
inset?: boolean;
};
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Label
class={cn("text-foreground px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...$$restProps}
>
<slot />
</ContextMenuPrimitive.Label>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
type $$Props = ContextMenuPrimitive.RadioGroupProps;
export let value: $$Props["value"] = undefined;
</script>
<ContextMenuPrimitive.RadioGroup {...$$restProps} bind:value>
<slot />
</ContextMenuPrimitive.RadioGroup>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import DotFilled from "svelte-radix/DotFilled.svelte";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.RadioItemProps;
type $$Events = ContextMenuPrimitive.RadioItemEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<ContextMenuPrimitive.RadioItem
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{value}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerdown
on:pointerleave
on:pointermove
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.RadioIndicator>
<DotFilled class="h-4 w-4 fill-current" />
</ContextMenuPrimitive.RadioIndicator>
</span>
<slot />
</ContextMenuPrimitive.RadioItem>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.SeparatorProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.Separator
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...$$restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...$$restProps}
>
<slot />
</span>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.SubContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.SubContent
{transition}
{transitionConfig}
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...$$restProps}
on:keydown
on:focusout
on:pointermove
>
<slot />
</ContextMenuPrimitive.SubContent>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import ChevronRight from "svelte-radix/ChevronRight.svelte";
import { cn } from "$lib/utils.js";
type $$Props = ContextMenuPrimitive.SubTriggerProps & {
inset?: boolean;
};
type $$Events = ContextMenuPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script>
<ContextMenuPrimitive.SubTrigger
class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
inset && "pl-8",
className
)}
{...$$restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,49 @@
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import Item from "./context-menu-item.svelte";
import Label from "./context-menu-label.svelte";
import Content from "./context-menu-content.svelte";
import Shortcut from "./context-menu-shortcut.svelte";
import RadioItem from "./context-menu-radio-item.svelte";
import Separator from "./context-menu-separator.svelte";
import RadioGroup from "./context-menu-radio-group.svelte";
import SubContent from "./context-menu-sub-content.svelte";
import SubTrigger from "./context-menu-sub-trigger.svelte";
import CheckboxItem from "./context-menu-checkbox-item.svelte";
const Sub = ContextMenuPrimitive.Sub;
const Root = ContextMenuPrimitive.Root;
const Trigger = ContextMenuPrimitive.Trigger;
const Group = ContextMenuPrimitive.Group;
export {
Sub,
Root,
Item,
Label,
Group,
Trigger,
Content,
Shortcut,
Separator,
RadioItem,
SubContent,
SubTrigger,
RadioGroup,
CheckboxItem,
//
Root as ContextMenu,
Sub as ContextMenuSub,
Item as ContextMenuItem,
Label as ContextMenuLabel,
Group as ContextMenuGroup,
Content as ContextMenuContent,
Trigger as ContextMenuTrigger,
Shortcut as ContextMenuShortcut,
RadioItem as ContextMenuRadioItem,
Separator as ContextMenuSeparator,
RadioGroup as ContextMenuRadioGroup,
SubContent as ContextMenuSubContent,
SubTrigger as ContextMenuSubTrigger,
CheckboxItem as ContextMenuCheckboxItem,
};

View file

@ -16,6 +16,6 @@
<DialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn('custom-bg fixed inset-0 z-50 backdrop-blur-sm', className)}
class={cn('custom-bg absolute inset-0 z-50 backdrop-blur-sm', className)}
{...$$restProps}
/>

View file

@ -0,0 +1 @@
export { default as Player } from './player.svelte'

View file

@ -0,0 +1,385 @@
<script lang='ts'>
import * as Dialog from '$lib/components/ui/dialog'
import PictureInPictureExit from '$lib/components/icons/PictureInPictureExit.svelte'
import PictureInPicture from '$lib/components/icons/PictureInPicture.svelte'
import Subtitles from '$lib/components/icons/Subtitles.svelte'
import Play from '$lib/components/icons/Play.svelte'
import { Button } from '$lib/components/ui/button'
import { settings } from '$lib/modules/settings'
import { bindPiP, toTS } from '$lib/utils'
import { Cast, EllipsisVertical, FastForward, Maximize, Minimize, Pause, Rewind, SkipBack, SkipForward } from 'lucide-svelte'
import { writable, type Writable } from 'simple-store-svelte'
import { persisted } from 'svelte-persisted-store'
import { toast } from 'svelte-sonner'
import Seekbar from './seekbar.svelte'
import type { SvelteMediaTimeRange } from 'svelte/elements'
import { fade } from 'svelte/transition'
import { getChapterTitle, normalizeTracks, sanitizeChapters, type MediaInfo } from './util'
import Thumbnailer from './thumbnailer'
import { onMount, tick } from 'svelte'
import native from '$lib/modules/native'
import { click } from '$lib/modules/navigate'
import { goto } from '$app/navigation'
import * as Tree from '$lib/components/ui/tree'
export let mediaInfo: MediaInfo
// bindings
// values
let videoHeight = 9
let videoWidth = 16
let currentTime = 0
let seekPercent = 0
let duration = 1
let playbackRate = 1
let buffered: SvelteMediaTimeRange[] = []
$: buffer = Math.max(...buffered.map(({ end }) => end))
let readyState = 0
$: safeduration = isFinite(duration) ? duration : currentTime
const volume = persisted('volume', 0)
// state
let seeking = false
let ended = false
let paused = true
const cast = false
// elements
let fullscreenElement: HTMLElement | null = null
const pictureInPictureElement: Writable<HTMLVideoElement | null> = writable(null)
let video: HTMLVideoElement
let wrapper: HTMLDivElement
// functions
function playPause () {
playAnimation(paused ? 'play' : 'pause')
return paused ? video.play() : video.pause()
}
function fullscreen () {
return fullscreenElement ? document.exitFullscreen() : wrapper.requestFullscreen()
}
function pip () {
return $pictureInPictureElement ? document.exitPictureInPicture() : video.requestPictureInPicture()
}
$: fullscreenElement ? screen.orientation.lock('landscape') : screen.orientation.unlock()
function checkAudio () {
if ('audioTracks' in HTMLVideoElement.prototype) {
if (!video.audioTracks!.length) {
toast.error('Audio Codec Unsupported', {
description: "This torrent's audio codec is not supported, try a different release by disabling Autoplay Torrents in RSS settings."
})
} else if (video.audioTracks!.length > 1) {
const preferredTrack = [...video.audioTracks!].find(({ language }) => language === $settings.audioLanguage)
if (preferredTrack) return selectAudio(preferredTrack.id)
const japaneseTrack = [...video.audioTracks!].find(({ language }) => language === 'jpn')
if (japaneseTrack) return selectAudio(japaneseTrack.id)
}
}
}
function selectAudio (id: string) {
if (id) {
for (const track of video.audioTracks ?? []) {
track.enabled = track.id === id
}
seek(-0.2) // stupid fix because video freezes up when chaging tracks
}
open = false
}
function selectVideo (id: string) {
if (id) {
for (const track of video.videoTracks!) {
track.selected = track.id === id
}
}
open = false
}
function prev () {
// TODO
}
function next () {
// TODO
}
function seek (time: number) {
video.currentTime = currentTime = currentTime + time
playAnimation(time > 0 ? 'seekforw' : 'seekback')
}
function seekTo (time: number) {
playAnimation(time > currentTime ? 'seekforw' : 'seekback')
video.currentTime = currentTime = time
}
let wasPaused = false
function startSeek () {
wasPaused = paused
if (!paused) video.pause()
}
function finishSeek () {
seekTo(seekPercent * safeduration / 100)
if (!wasPaused) video.play()
}
// animations
function playAnimation (type: 'play' | 'pause' | 'seekforw' | 'seekback') {
animations.push({ type, id: crypto.randomUUID() })
// eslint-disable-next-line no-self-assign
animations = animations
}
function endAnimation (id: string) {
const index = animations.findIndex(animation => animation.id === id)
if (index !== -1) animations.splice(index, 1)
// eslint-disable-next-line no-self-assign
animations = animations
}
interface Animation {
type: 'play' | 'pause' | 'seekforw' | 'seekback'
id: string
}
let animations: Animation[] = []
const thumbnailer = new Thumbnailer(mediaInfo.url)
$: thumbnailer.updateSource(mediaInfo.url)
onMount(() => {
thumbnailer.setVideo(video)
})
// other
$: chapters = sanitizeChapters([
{ start: 5, end: 15, text: 'Chapter 0' },
{ start: 1.0 * 60, end: 1.2 * 60, text: 'Chapter 1' },
{ start: 1.4 * 60, end: 88, text: 'Chapter 2 ' }
], safeduration)
$: seekIndex = Math.max(0, Math.floor(seekPercent * safeduration / 100 / thumbnailer.interval))
$: native.setMediaSession(mediaInfo.session)
$: native.setPositionState({ duration: safeduration, position: Math.max(0, currentTime), playbackRate })
$: native.setPlayBackState(readyState === 0 ? 'none' : paused ? 'paused' : 'playing')
native.setActionHandler('play', playPause)
native.setActionHandler('pause', playPause)
native.setActionHandler('seekto', ({ seekTime }) => seekTo(seekTime ?? 0))
native.setActionHandler('seekbackward', () => seek(-2))
native.setActionHandler('seekforward', () => seek(2))
native.setActionHandler('previoustrack', prev)
native.setActionHandler('nexttrack', next)
let open = false
let treeState: Writable<string[]>
async function openSubs () {
open = true
await tick()
treeState.set(['subs'])
}
</script>
<svelte:document bind:fullscreenElement use:bindPiP={pictureInPictureElement} />
<div style:aspect-ratio='{videoWidth} / {videoHeight}' class='max-w-full max-h-full min-w-[clamp(0%,700px,100%)] relative content-center fullscreen:bg-black fullscreen:rounded-none rounded-xl overflow-clip' bind:this={wrapper}>
<video
class='w-full max-h-full grow bg-black'
preload='auto'
loop
src={mediaInfo.url}
bind:videoHeight
bind:videoWidth
bind:currentTime
bind:duration
bind:ended
bind:paused
bind:readyState
bind:buffered
bind:playbackRate
bind:volume={$volume}
bind:this={video}
on:click={playPause}
on:loadeddata={checkAudio}
/>
<div class='absolute w-full h-full flex items-center justify-center top-0 pointer-events-none'>
{#if seeking}
{#await thumbnailer.getThumbnail(seekIndex) then src}
<img {src} alt='thumbnail' class='w-full h-full bg-black absolute top-0 right-0' />
{/await}
{/if}
<Button class='mobile:inline-flex hidden p-3 w-12 h-12 absolute top-10 right-10 backdrop-blur-lg border-white/15 border bg-black/20 pointer-events-auto' variant='ghost'>
<EllipsisVertical size='24px' class='p-[1px]' />
</Button>
<div class='mobile:flex hidden gap-4 absolute items-center'>
<Button class='p-3 w-16 h-16 pointer-events-auto rounded-[50%] backdrop-blur-lg border-white/15 border bg-black/20' variant='ghost'>
<SkipBack size='24px' fill='currentColor' strokeWidth='1' />
</Button>
<Button class='p-3 w-24 h-24 pointer-events-auto rounded-[50%] backdrop-blur-lg border-white/15 border bg-black/20' variant='ghost' on:click={playPause}>
{#if paused}
<Play size='42px' fill='currentColor' class='p-0.5' />
{:else}
<Pause size='42px' fill='currentColor' strokeWidth='1' />
{/if}
</Button>
<Button class='p-3 w-16 h-16 pointer-events-auto rounded-[50%] backdrop-blur-lg border-white/15 border bg-black/20' variant='ghost'>
<SkipForward size='24px' fill='currentColor' strokeWidth='1' />
</Button>
</div>
{#if readyState < 3}
<div in:fade={{ duration: 200, delay: 500 }} out:fade={{ duration: 200 }}>
<div class='border-[3px] rounded-[50%] w-10 h-10 drop-shadow-lg border-transparent border-t-white animate-spin' />
</div>
{/if}
{#each animations as { type, id } (id)}
<div class='absolute animate-pulse-once' on:animationend={() => endAnimation(id)}>
{#if type === 'play'}
<Play size='64px' fill='white' />
{:else if type === 'pause'}
<Pause size='64px' fill='white' />
{:else if type === 'seekforw'}
<FastForward size='64px' fill='white' />
{:else if type === 'seekback'}
<Rewind size='64px' fill='white' />
{/if}
</div>
{/each}
</div>
<div class='absolute w-full bottom-0 flex flex-col gradient px-10 py-4'>
<div class='flex justify-between gap-12'>
<div class='flex flex-col gap-2 text-left group/mediainfo cursor-pointer' use:click={() => goto(`/anime/${mediaInfo.media.id}`)}>
<div class='text-white text-lg font-normal leading-none line-clamp-1 group-hover/mediainfo:text-neutral-200'>{mediaInfo.session.title}</div>
<div class='text-[rgba(217,217,217,0.6)] group-hover/mediainfo:text-neutral-400 text-sm leading-none font-light line-clamp-1'>{mediaInfo.session.description}</div>
</div>
<div class='flex flex-col gap-2 grow-0 items-end self-end'>
<div class='text-[rgba(217,217,217,0.6)] text-sm leading-none font-light line-clamp-1'>{getChapterTitle(seeking ? seekPercent * safeduration / 100 : currentTime, chapters) || ''}</div>
<div class='ml-auto self-end text-sm leading-none font-light text-nowrap'>{toTS(seeking ? seekPercent * safeduration / 100 : currentTime)} / {toTS(safeduration)}</div>
</div>
</div>
<Seekbar {duration} {currentTime} buffer={buffer / duration * 100} {chapters} bind:seeking bind:seek={seekPercent} on:seeked={finishSeek} on:seeking={startSeek} {thumbnailer} />
<div class='flex justify-between gap-2 mobile:hidden'>
<div class='flex text-white gap-2'>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={playPause}>
{#if paused}
<Play size='24px' fill='currentColor' class='p-0.5' />
{:else}
<Pause size='24px' fill='currentColor' strokeWidth='1' />
{/if}
</Button>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={prev}>
<SkipBack size='24px' fill='currentColor' strokeWidth='1' />
</Button>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={next}>
<SkipForward size='24px' fill='currentColor' strokeWidth='1' />
</Button>
</div>
<div class='flex gap-2'>
<Dialog.Root portal={wrapper} bind:open>
<Dialog.Trigger asChild let:builder>
<Button class='p-3 w-12 h-12' variant='ghost' builders={[builder]}>
<EllipsisVertical size='24px' class='p-[1px]' />
</Button>
</Dialog.Trigger>
<Dialog.Content class='absolute bg-transparent border-none p-0 shadow-none h-full w-full overflow-hidden'>
<div on:pointerdown|self={() => { open = false }} class='h-full flex w-full justify-center items-center overflow-y-scroll'>
<Tree.Root bind:state={treeState}>
<Tree.Item>
<span slot='trigger'>Audio</span>
<Tree.Sub>
{#each Object.entries(normalizeTracks(video.audioTracks ?? [])) as [lang, tracks] (lang)}
<Tree.Item>
<span slot='trigger' class='capitalize'>{lang}</span>
<Tree.Sub>
{#each tracks as track (track.id)}
<Tree.Item active={track.enabled} on:click={() => selectAudio(track.id)}>
<span>{track.label}</span>
</Tree.Item>
{/each}
</Tree.Sub>
</Tree.Item>
{/each}
</Tree.Sub>
</Tree.Item>
<Tree.Item>
<span slot='trigger'>Video</span>
<Tree.Sub>
{#each Object.entries(normalizeTracks(video.videoTracks ?? [])) as [lang, tracks] (lang)}
<Tree.Item>
<span slot='trigger' class='capitalize'>{lang}</span>
<Tree.Sub>
{#each tracks as track (track.id)}
<Tree.Item active={track.enabled} on:click={() => selectVideo(track.id)}>
<span>{track.label}</span>
</Tree.Item>
{/each}
</Tree.Sub>
</Tree.Item>
{/each}
</Tree.Sub>
</Tree.Item>
<Tree.Item id='subs'>
<span slot='trigger'>Subtitles</span>
<Tree.Sub>
<Tree.Item>
<span>Consulting</span>
</Tree.Item>
<Tree.Item>
<span>Support</span>
</Tree.Item>
</Tree.Sub>
</Tree.Item>
</Tree.Root>\
</div>
</Dialog.Content>
</Dialog.Root>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={openSubs}>
<Subtitles size='24px' fill='currentColor' strokeWidth='0' />
</Button>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={pip}>
{#if $pictureInPictureElement}
<PictureInPictureExit size='24px' strokeWidth='2' />
{:else}
<PictureInPicture size='24px' strokeWidth='2' />
{/if}
</Button>
{#if false}
<Button class='p-3 w-12 h-12' variant='ghost'>
{#if cast}
<Cast size='24px' fill='white' strokeWidth='2' />
{:else}
<Cast size='24px' strokeWidth='2' />
{/if}
</Button>
{/if}
<Button class='p-3 w-12 h-12' variant='ghost' on:click={fullscreen}>
{#if fullscreenElement}
<Minimize size='24px' class='p-0.5' strokeWidth='2.5' />
{:else}
<Maximize size='24px' class='p-0.5' strokeWidth='2.5' />
{/if}
</Button>
</div>
</div>
</div>
</div>
<style>
.gradient {
background: linear-gradient(to top, oklab(0 0 0 / 0.85) 0%, oklab(0 0 0 / 0.7) 35%, oklab(0 0 0 / 0) 100%);
}
.animate-pulse-once {
animation: pulse-once .4s linear;
}
@keyframes pulse-once {
0% {
opacity: 1;
scale: 1;
}
100% {
opacity: 0;
scale: 1.2;
}
}
</style>

View file

@ -0,0 +1,158 @@
<script context='module' lang='ts'>
type percent = number
interface BarSegment {
size: percent
text: string
offset: percent
scale: percent
}
interface Chapter {
start: number
end: number
text: string
}
function clamp (value: percent) {
return Math.min(Math.max(value, 0), 100)
}
function skewclamp (value: percent) {
const clamp = Math.min(Math.max(value, 0), 100)
return clamp === 0 ? -5 : clamp
}
</script>
<script lang='ts'>
import { toTS } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { getChapterTitle } from './util'
import type Thumbnailer from './thumbnailer'
const dispatch = createEventDispatcher<{
seeking: null
seeked: null
}>()
// state
export let chapters: Chapter[] = []
export let currentTime = 0
export let duration: number
export let buffer = 0
$: progress = clamp(currentTime / duration * 100)
export let seek = 0
$: segments = makeSegments(chapters, duration)
function makeSegments (chapters: Chapter[], length: number): BarSegment[] {
const barSegments: BarSegment[] = []
let offset = 0
for (const chapter of chapters) {
const chapterDuration = chapter.end - chapter.start
if (chapterDuration > 0) { // Still necessary to handle empty "missing" segments.
const size = (chapterDuration / length) * 100
barSegments.push({
size,
text: chapter.text,
offset,
scale: 100 / size
})
offset += size
}
}
return barSegments
}
// seeking
let seekbar: HTMLDivElement
export let seeking = false
function calculatePositionProgress ({ pageX, currentTarget }: PointerEvent) {
const target = currentTarget as HTMLDivElement
const percent = clamp((pageX - target.getBoundingClientRect().left) / target.clientWidth * 100)
if (seeking) {
progress = percent
}
seek = percent
}
function endHover () {
seek = 0
}
function startSeeking (e: PointerEvent) {
seeking = true
calculatePositionProgress(e)
if (e.pointerId) seekbar.setPointerCapture(e.pointerId)
dispatch('seeking')
}
function endSeeking ({ pointerId }: PointerEvent) {
seeking = false
if (pointerId) seekbar.releasePointerCapture(pointerId)
dispatch('seeked')
}
// function checkThumbActive (progress: number, seek: number) {
// for (const { offset, size } of segments) {
// if (offset + size > progress) return offset + size > seek && offset < seek
// }
// return false
// }
$: seekTime = seek * duration / 100
$: title = getChapterTitle(seekTime, chapters)
export let thumbnailer: Thumbnailer
$: seekIndex = Math.max(0, Math.floor(seekTime / thumbnailer.interval))
</script>
<div class='w-full flex cursor-pointer relative group/seekbar touch-none' class:!cursor-grab={seeking}
bind:this={seekbar}
on:pointerdown={startSeeking}
on:pointerup={endSeeking}
on:pointermove={calculatePositionProgress}
on:pointerleave={endHover}>
{#each segments as chapter, i (chapter)}
{@const { size, scale, offset } = chapter}
{@const active = seek && seek > offset && seek < offset + size}
<div class='w-full py-4 shrink-0 flex items-center justify-center' style:width='{size}%'>
<div class='relative w-full h-1 flex items-center justify-center overflow-hidden rounded-[2px]' class:ml-0.5={!!i}>
<div class='bg-[rgba(217,217,217,0.4)] absolute left-0 w-full h-0.5 transition-[height] duration-75' class:h-1={active} />
<div class='bg-[rgba(217,217,217,0.4)] absolute left-0 w-full h-0.5 transition-[height] duration-75 transform-gpu' class:h-1={active} style:--tw-translate-x='{skewclamp(scale * (buffer - offset)) - 100}%' />
<div class='bg-[rgba(217,217,217,0.4)] absolute left-0 w-full h-0.5 transition-[height] duration-75 transform-gpu' class:h-1={active} style:--tw-translate-x='{skewclamp(scale * (seek - offset)) - 100}%' />
<div class='bg-white absolute w-full left-0 h-0.5 transition-[height] duration-75 transform-gpu' class:h-1={active} style:--tw-translate-x='{skewclamp(scale * (progress - offset)) - 100}%' />
</div>
</div>
{/each}
{#if !seeking}
<div class='absolute w-full transform-gpu flex pointer-events-none group-hover/seekbar:opacity-100 opacity-0 bottom-9' style:--tw-translate-x='clamp(64px, {clamp(seek)}%, calc(100% - 64px))'>
<div class='-translate-x-1/2 text-sm leading-none text-nowrap flex flex-col justify-center items-center gap-1 rounded-lg bg-neutral-200 border-white border py-2 px-3 has-[img]:p-0 text-zinc-900 shadow-lg'>
{#await thumbnailer.getThumbnail(seekIndex)}
{#if title}
<div class='max-w-24 text-ellipsis overflow-hidden'>{title}</div>
{/if}
<div>{toTS(seekTime)}</div>
{:then src}
<img {src} alt='thumbnail' class='w-40 rounded-lg min-h-10' />
{#if title}
<div class='max-w-24 text-ellipsis overflow-hidden absolute top-0 bg-white py-1 px-2 rounded-b-lg'>{title}</div>
{/if}
<div class='absolute bottom-0 bg-white py-1 px-2 rounded-t-lg'>{toTS(seekTime)}</div>
{/await}
</div>
</div>
{/if}
<!-- <div class='absolute w-full transform-gpu flex pointer-events-none top-[18px]' style:--tw-translate-x='{clamp(seeking ? seek : progress)}%'>
<div class='transform-gpu -translate-x-1/2 -translate-y-1/2 rounded-[50%] w-3 h-3 bg-white scale-0 transition-transform group-hover/seekbar:scale-{checkThumbActive(progress, seek) ? 100 : 75}' />
</div> -->
</div>

View file

@ -0,0 +1,101 @@
interface RenderItem {
index: number
run: () => void
promise: Promise<string>
}
export default class Thumbnailer {
video = document.createElement('video')
canvas = new OffscreenCanvas(0, 0)
ctx = this.canvas.getContext('2d')!
thumbnails: string[] = []
size = 800
interval = 10
currentTask: RenderItem | undefined
nextTask: RenderItem | undefined
src
constructor (src: string) {
this.video.preload = 'none'
this.video.src = this.src = src
this.video.load()
this.video.playbackRate = 0
this.video.muted = true
}
setVideo (currentVideo: HTMLVideoElement) {
currentVideo.addEventListener('timeupdate', () => {
const index = Math.floor(currentVideo.currentTime / this.interval)
const thumbnail = this.thumbnails[index]
if (!thumbnail) this._paintThumbnail(currentVideo, index)
})
}
_createTask (index: number): RenderItem {
const { promise, resolve } = Promise.withResolvers<string>()
const run = () => {
this.video.requestVideoFrameCallback((_now, meta) => {
resolve(this._paintThumbnail(this.video, index, meta.width, meta.height))
this.video.currentTime = 0
this.currentTask = undefined
if (this.nextTask) {
this.currentTask = this.nextTask
this.nextTask = undefined
this.currentTask.run()
}
})
this.video.currentTime = index * this.interval
}
return { index, run, promise }
}
// get a task or create one to create a thumbnail
// don't touch currently running task, overwrite next task
_createThumbnail (index: number) {
if (!this.currentTask) {
this.currentTask = this._createTask(index)
this.currentTask.run()
return this.currentTask.promise
}
if (index === this.currentTask.index) return this.currentTask.promise
if (!this.nextTask) {
this.nextTask = this._createTask(index)
return this.nextTask.promise
}
if (index === this.nextTask.index) return this.nextTask.promise
this.nextTask = this._createTask(index)
return this.nextTask.promise
}
// generate and store the thumbnail
async _paintThumbnail (video: HTMLVideoElement, index: number, width = video.videoWidth, height = video.videoHeight) {
this.canvas.width = this.size
this.canvas.height = height / width * this.size
this.ctx.drawImage(video, 0, 0, this.canvas.width, this.canvas.height)
this.thumbnails[index] = URL.createObjectURL(await this.canvas.convertToBlob({ type: 'image/webp', quality: 0.6 }))
return this.thumbnails[index]
}
async getThumbnail (index: number): Promise<string> {
const thumbnail = this.thumbnails[index]
if (thumbnail) return thumbnail
return await this._createThumbnail(index)
}
updateSource (src: string) {
if (src === this.src) return
for (const thumbnail of this.thumbnails) URL.revokeObjectURL(thumbnail)
this.thumbnails = []
this.currentTask = undefined
this.nextTask = undefined
this.video.src = this.src = src
this.video.load()
}
}

View file

@ -0,0 +1,167 @@
import type { Media } from '$lib/modules/anilist'
import type { Track } from '../../../../app'
export interface Chapter {
start: number
end: number
text: string
}
export interface SessionMetadata {
title: string
description: string
image: string
}
export interface MediaInfo {
url: string
media: Media
episode: number
forced: boolean
session: SessionMetadata
}
export function getChapterTitle (time: number, chapters: Chapter[]): string | false {
for (const { start, end, text } of chapters) {
if (end > time) return start <= time && text
}
return false
}
interface Interval {
startTime: number
endTime: number
}
interface Result {
interval: Interval
skipType: string
skipId: string
episodeLength: number
}
interface AniSkip {
found: boolean
results: Result[]
message: string
statusCode: number
}
export async function getChaptersAniSkip (idMal: number, episode: number, duration: number) {
const resAccurate = await fetch(`https://api.aniskip.com/v2/skip-times/${idMal}/${episode}/?episodeLength=${duration}&types=op&types=ed&types=recap`)
const jsonAccurate = await resAccurate.json() as AniSkip
const resRough = await fetch(`https://api.aniskip.com/v2/skip-times/${idMal}/${episode}/?episodeLength=0&types=op&types=ed&types=recap`)
const jsonRough = await resRough.json() as AniSkip
const map: Record<string, Result> = {}
for (const result of [...jsonAccurate.results, ...jsonRough.results]) {
if (!(result.skipType in map)) map[result.skipType] = result
}
const results = Object.values(map)
if (!results.length) return []
const chapters = results.map(result => {
const diff = duration - result.episodeLength
return {
start: (result.interval.startTime + diff) * 1000,
end: (result.interval.endTime + diff) * 1000,
text: result.skipType.toUpperCase()
}
})
const ed = chapters.find(({ text }) => text === 'ED')
const recap = chapters.find(({ text }) => text === 'RECAP')
if (recap) recap.text = 'Recap'
chapters.sort((a, b) => a.start - b.start)
if ((chapters[0]!.start | 0) !== 0) {
chapters.unshift({ start: 0, end: chapters[0]!.start, text: chapters[0]!.text === 'OP' ? 'Intro' : 'Episode' })
}
if (ed) {
if ((ed.end | 0) + 5000 - duration * 1000 < 0) {
chapters.push({ start: ed.end, end: duration * 1000, text: 'Preview' })
}
} else if ((chapters[chapters.length - 1]!.end | 0) + 5000 - duration * 1000 < 0) {
chapters.push({
start: chapters[chapters.length - 1]!.end,
end: duration * 1000,
text: 'Episode'
})
}
for (let i = 0, len = chapters.length - 2; i <= len; ++i) {
const current = chapters[i]
const next = chapters[i + 1]
if ((current!.end | 0) !== (next!.start | 0)) {
chapters.push({
start: current!.end,
end: next!.start,
text: 'Episode'
})
}
}
chapters.sort((a, b) => a.start - b.start)
return chapters
}
export function sanitizeChapters (chapters: Chapter[], length: number): Chapter[] {
if (length <= 0) {
return []
}
const sanitizedChapters: Chapter[] = []
let currentTime = 0
const sortedChapters = chapters.map(chapter => {
const end = Math.min(length, chapter.end)
const start = Math.min(Math.max(0, chapter.start), end)
return { start, end, text: chapter.text }
}).sort((a, b) => a.start - b.start)
for (const chapter of sortedChapters) {
// Handle Missing Segment Before Chapter
if (chapter.start > currentTime) {
sanitizedChapters.push({
start: currentTime,
end: chapter.start,
text: sanitizedChapters.length === 0 ? '' : 'Episode'
})
}
sanitizedChapters.push(chapter)
currentTime = chapter.end
}
// Handle Missing Segment After Last Chapter
if (currentTime < length) {
sanitizedChapters.push({
start: currentTime,
end: length,
text: ''
})
}
return sanitizedChapters
}
export function normalizeTracks (_tracks: Track[]) {
const tracks = [..._tracks]
const hasEng = tracks.some(track => track.language === 'eng' || track.language === 'en')
const lang = tracks.map(({ id, language, label, enabled, selected }) => {
return {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
enabled: enabled ?? selected,
id,
language: language || !hasEng ? 'eng' : 'unk',
label
}
})
return lang.reduce<Record<string, typeof lang>>((acc, track) => {
if (!acc[track.language]) acc[track.language] = []
acc[track.language]!.push(track)
return acc
}, {})
}

View file

@ -0,0 +1,28 @@
import { getContext, setContext } from 'svelte'
import { writable, get, type Writable } from 'svelte/store'
const MENU_CONTEXT = 'MENU_CONTEXT'
const LEVEL_CONTEXT = 'LEVEL_CONTEXT'
export function createRootContext () {
const state: Writable<string[]> = writable([])
setContext(LEVEL_CONTEXT, writable(-1))
return setContext(MENU_CONTEXT, {
state,
setActive: (id: string, level: number) => state.update(s => s.slice(0, level).concat(id)),
setInactive: (level: number) => state.update(s => s.slice(0, level))
})
}
export function getMenuContext () {
return getContext<ReturnType<typeof createRootContext>>(MENU_CONTEXT)
}
export function getLevelContext () {
return getContext<Writable<number>>(LEVEL_CONTEXT)
}
export function createChildLevel () {
return setContext(LEVEL_CONTEXT, writable(get(getLevelContext()) + 1))
}

View file

@ -0,0 +1,3 @@
export { default as Root } from './root.svelte'
export { default as Sub } from './sub.svelte'
export { default as Item } from './item.svelte'

View file

@ -0,0 +1,47 @@
<script lang='ts'>
import { ChevronRight } from 'svelte-radix'
import { getMenuContext, getLevelContext } from './context.ts'
const { state, setActive, setInactive } = getMenuContext()
const levelStore = getLevelContext()
export let id: string = crypto.randomUUID()
export let active = false
$: level = $levelStore
$: isActive = $state[level] === id
$: showSubmenu = isActive
$: activeSibling = $state[level]
$: hasSub = $$slots.trigger
function handleClick () {
if (hasSub) {
isActive ? setInactive(level) : setActive(id, level)
} else {
setInactive(level)
}
}
</script>
<div class='relative'>
<button class='w-full hover:bg-accent hover:text-accent-foreground flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none pl-8 cursor-pointer'
on:click={handleClick}
on:click
class:bg-accent={isActive || active} class:text-accent-foreground={isActive || active}
class:opacity-30={activeSibling}
data-open={isActive}>
{#if hasSub}
<slot name='trigger' />
<ChevronRight class='ml-auto h-4 w-4' />
{:else}
<slot />
{/if}
</button>
{#if showSubmenu && hasSub}
<slot />
{/if}
</div>

View file

@ -0,0 +1,13 @@
<script lang='ts'>
import { cn } from '$lib/utils'
import type { HTMLAttributes } from 'svelte/elements'
type $$Props = HTMLAttributes<HTMLDivElement>
let className: $$Props['class'] = ''
export { className as class }
</script>
<div class={cn('relative w-64 bg-black text-popover-foreground rounded-md z-50 border p-1 shadow-md focus:outline-none has-[>[data-open=true]]:bg-black/30', className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,16 @@
<script lang='ts'>
import { createRootContext, createChildLevel } from './context.ts'
import Menu from './menu.svelte'
export const { state } = createRootContext()
createChildLevel()
function close (fn: () => void) {
fn()
return () => state.set([])
}
</script>
<Menu style='margin-left: -{$state.length * 528}px' class='transition-[margin-left]'>
<slot {close} />
</Menu>

View file

@ -0,0 +1,10 @@
<script lang='ts'>
import { createChildLevel } from './context.ts'
import Menu from './menu.svelte'
createChildLevel()
</script>
<Menu class='absolute right-[calc(-100%-1.25rem)] top-[calc(-1px-0.25rem)]'>
<slot />
</Menu>

View file

@ -11,7 +11,7 @@ export default expose({
async load (code: string): Promise<TorrentSource> {
// WARN: unsafe eval
const url = URL.createObjectURL(new Blob([code], { type: 'application/javascript' }))
const module = await import(url)
const module = await import(/* @vite-ignore */url)
URL.revokeObjectURL(url)
return module.default
},

View file

@ -20,9 +20,7 @@ globalThis.authAL ??= (url: string) => {
}
globalThis.restart ??= async () => location.reload()
globalThis.openURL ??= async (url: string) => {
open(url)
}
globalThis.openURL ??= async (url: string) => { open(url) }
globalThis.selectPlayer ??= async () => 'mpv'
globalThis.selectDownload ??= async () => '/tmp/webtorrent'
globalThis.share ??= (...args) => navigator.share(...args)
@ -36,6 +34,11 @@ globalThis.maximise ??= async () => undefined
globalThis.close ??= async () => undefined
globalThis.checkUpdate ??= async () => undefined
globalThis.toggleDiscordDetails ??= async () => undefined
// TODO: chapter info?
globalThis.setMediaSession ??= async (metadata) => { navigator.mediaSession.metadata = new MediaMetadata({ title: metadata.title, artist: metadata.description, artwork: [{ src: metadata.image }] }) }
globalThis.setPositionState ??= async e => navigator.mediaSession.setPositionState(e)
globalThis.setPlayBackState ??= async e => { navigator.mediaSession.playbackState = e }
globalThis.setActionHandler ??= async (...args) => navigator.mediaSession.setActionHandler(...args)
const native = globalThis as Native

View file

@ -2,7 +2,7 @@ import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { cubicOut } from 'svelte/easing'
import type { TransitionConfig } from 'svelte/transition'
import { readable } from 'simple-store-svelte'
import { readable, type Writable } from 'simple-store-svelte'
export function cn (...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@ -122,3 +122,35 @@ export function fastPrettyBytes (num: number) {
const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), units.length - 1)
return Number((num / Math.pow(1000, exponent)).toFixed(2)) + units[exponent]!
}
export function toTS (sec: number, full?: number) {
if (isNaN(sec) || sec < 0) {
switch (full) {
case 1:
return '0:00:00.00'
case 2:
return '0:00:00'
case 3:
return '00:00'
default:
return '0:00'
}
}
const hours = Math.floor(sec / 3600)
let minutes: string | number = Math.floor(sec / 60) - hours * 60
let seconds: string | number = full === 1 ? (sec % 60).toFixed(2) : Math.floor(sec % 60)
if (minutes < 10 && (hours > 0 || full)) minutes = '0' + minutes
if (Number(seconds) < 10) seconds = '0' + seconds
return (hours > 0 || full === 1 || full === 2) ? hours + ':' + minutes + ':' + seconds : minutes + ':' + seconds
}
export function bindPiP (doc: Document, store: Writable<HTMLVideoElement | null>) {
const signal = new AbortController()
doc.addEventListener('enterpictureinpicture', (e) => {
store.set(e.target as HTMLVideoElement)
}, { signal: signal.signal })
doc.addEventListener('leavepictureinpicture', () => {
store.set(null)
}, { signal: signal.signal })
return { destroy: () => signal.abort() }
}

View file

@ -0,0 +1,46 @@
<script lang='ts'>
import { bannerSrc, hideBanner } from '$lib/components/ui/banner'
import { Player } from '$lib/components/ui/player'
import type { MediaInfo } from '$lib/components/ui/player/util'
import { banner, client, title } from '$lib/modules/anilist'
import { IDMedia } from '$lib/modules/anilist/queries'
import { onMount, tick } from 'svelte'
onMount(async () => {
await tick()
hideBanner.value = true
})
const mediaInfo: PromiseLike<MediaInfo> = client.client.query(IDMedia, { id: 176642 }, { requestPolicy: 'cache-first' }).then(v => {
const media = v.data!.Media!
bannerSrc.value = media
return {
url: '/video.mkv',
episode: 6,
media,
forced: false,
session: {
title: title(media),
description: 'Episode 6 - Fierce Blazing Finale',
image: banner(media) ?? ''
}
}
})
</script>
<div class='px-3 w-full h-full py-12 gap-4 content-center text-webkit-center'>
{#await mediaInfo then mediaInfo}
<Player {mediaInfo} />
{/await}
<!-- <div class='w-60 shrink-0'>
Episode list
</div> -->
</div>
<style>
.text-webkit-center {
text-align: -webkit-center;
}
</style>

View file

@ -3,6 +3,10 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import('@sveltejs/kit').Config} */
const config = {
onwarn: (warning, handler) => {
if (warning.code === 'a11y-media-has-caption') return
handler(warning)
},
preprocess: vitePreprocess({}),
kit: {
adapter: adapter({ fallback: 'index.html' }),

View file

@ -4,9 +4,32 @@ import plugin from 'tailwindcss/plugin'
const config: Config = {
plugins: [
plugin (({ addVariant }) => {
plugin(({ addVariant, matchVariant }) => {
addVariant('select', ['&:hover', '&:focus-visible', '&:active'])
addVariant('group-select', [':merge(.group):hover &', ':merge(.group):focus-visible &', ':merge(.group):active &'])
addVariant('fullscreen', '&:fullscreen')
addVariant('group-fullscreen', ':merge(.group):fullscreen &')
matchVariant(
'group-fullscreen',
(value, { modifier }) => [
':merge(.group):fullscreen &',
`:merge(.group\\/${modifier}):fullscreen &`
],
{ values: { DEFAULT: undefined } }
)
matchVariant(
'group-select',
(value, { modifier }) => [
':merge(.group):hover &',
`:merge(.group\\/${modifier}):hover &`,
':merge(.group):focus-visible &',
`:merge(.group\\/${modifier}):focus-visible &`,
':merge(.group):active &',
`:merge(.group\\/${modifier}):active &`
],
{ values: { DEFAULT: undefined } }
)
addVariant('mobile', '@media (pointer: none), (pointer: coarse)')
})
],
darkMode: ['class'],

View file

@ -24,7 +24,6 @@
]
},
"include": [
"tailwind.config.ts",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.svelte"

View file

@ -4,8 +4,11 @@ import { defineConfig } from 'vite'
export default defineConfig({
plugins: [sveltekit()],
server: { port: 7344 },
build: {
target: 'esnext'
},
ssr: {
target: "webworker"
target: 'webworker'
},
optimizeDeps: {
exclude: ['anitomyscript']