Make tabs part of the navbar on web (#1318)

This commit is contained in:
Zoe Roux 2026-02-16 12:54:21 +01:00 committed by GitHub
commit 104f1fc599
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 379 additions and 1584 deletions

View File

@ -13,28 +13,28 @@
"@logtape/otel": "2.0.2",
"@logtape/redaction": "2.0.2",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.211.0",
"@opentelemetry/core": "^2.5.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.211.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.211.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.211.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.211.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.211.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.211.0",
"@opentelemetry/resources": "^2.5.0",
"@opentelemetry/sdk-logs": "^0.211.0",
"@opentelemetry/sdk-metrics": "^2.5.0",
"@opentelemetry/sdk-trace-base": "^2.5.0",
"@opentelemetry/sdk-trace-node": "^2.5.0",
"@opentelemetry/api-logs": "^0.212.0",
"@opentelemetry/core": "^2.5.1",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.212.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.212.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.212.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.212.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.212.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.212.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.212.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.212.0",
"@opentelemetry/resources": "^2.5.1",
"@opentelemetry/sdk-logs": "^0.212.0",
"@opentelemetry/sdk-metrics": "^2.5.1",
"@opentelemetry/sdk-trace-base": "^2.5.1",
"@opentelemetry/sdk-trace-node": "^2.5.1",
"@opentelemetry/semantic-conventions": "^1.39.0",
"@types/bun": "^1.3.6",
"@types/bun": "^1.3.9",
"blurhash": "^2.0.5",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "0.44.7",
"elysia": "^1.4.23",
"elysia": "^1.4.25",
"jose": "^6.1.3",
"node-addon-api": "^8.5.0",
"parjs": "^1.3.9",
@ -42,7 +42,7 @@
"sharp": "^0.34.5",
},
"devDependencies": {
"@biomejs/biome": "2.3.14",
"@biomejs/biome": "2.4.0",
"@types/pg": "^8.16.0",
},
},
@ -51,23 +51,23 @@
"drizzle-orm@0.44.7": "patches/drizzle-orm@0.44.7.patch",
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@2.3.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="],
"@biomejs/biome": ["@biomejs/biome@2.4.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.0", "@biomejs/cli-darwin-x64": "2.4.0", "@biomejs/cli-linux-arm64": "2.4.0", "@biomejs/cli-linux-arm64-musl": "2.4.0", "@biomejs/cli-linux-x64": "2.4.0", "@biomejs/cli-linux-x64-musl": "2.4.0", "@biomejs/cli-win32-arm64": "2.4.0", "@biomejs/cli-win32-x64": "2.4.0" }, "bin": { "biome": "bin/biome" } }, "sha512-iluT61cORUDIC5i/y42ljyQraCemmmcgbMLLCnYO+yh+2hjTmcMFcwY8G0zTzWCsPb3t3AyKc+0t/VuhPZULUg=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJGPpvWJMkLxSRtpCAKfKh41Q4JJXisvxZL8ChN1eNW3m/WlPFJ6EFDCE7YfUb4XS8ZFi3C1dFpxUJ0Ety5n+A=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L+YpOtPSuU0etomfvFTPWRsa7+8ejaJL3yaROEoT/96HDJbR6OsvZQk0C8JUYou+XFdP+JcGxqZknkp4n934RA=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-PNkLNQG6RLo8lG7QoWe/hhnMxJIt1tEimoXpGQjwS/dkdNiKBLPv4RpeQl8o3s1OKI3ZOR5XPiYtmbGGHAOnLA=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Aq+S7ffpb5ynTyLgtnEjG+W6xuTd2F7FdC7J6ShpvRhZwJhjzwITGF9vrqoOnw0sv1XWkt2Q1Rpg+hleg/Xg7Q=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-KT67FKfzIw6DNnUNdYlBg+eU24Go3n75GWK6NwU4+yJmDYFe9i/MjiI+U/iEzKvo0g7G7MZqoyrhIYuND2w8QQ=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-u2p54IhvNAWB+h7+rxCZe3reNfQYFK+ppDw+q0yegrGclFYnDPZAntv/PqgUacpC3uxTeuWFgWW7RFe3lHuxOA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-LInRbXhYujtL3sH2TMCH/UBwJZsoGwfQjBrMfl84CD4hL/41C/EU5mldqf1yoFpsI0iPWuU83U+nB2TUUypWeg=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1rhDUq8sf7xX3tg7vbnU3WVfanKCKi40OXc4VleBMzRStmQHdeBY46aFP6VdwEomcVjyNiu+Zcr3LZtAdrZrjQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ZsZzQsl9U+wxFrGGS4f6UxREUlgHwmEfu1IrXlgNFrNnd5Th6lIJr8KmSzu/+meSa9f4rzFrbEW9LBBA6ScoMA=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WVFOhsnzhrbMGOSIcB9yFdRV2oG2KkRRhIZiunI9gJqSU3ax9ErdnTxRfJUxZUI9NbzVxC60OCXNcu+mXfF/Tw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-KQU7EkbBBuHPW3/rAcoiVmhlPtDSGOGRPv9js7qJVpYTzjQmVR+C9Rfcz+ti8YCH+zT1J52tuBybtP4IodjxZQ=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Omo0xhl63z47X+CrE5viEWKJhejJyndl577VoXg763U/aoATrK3r5+8DPh02GokWPeODX1Hek00OtjjooGan9w=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-+IKYkj/pUBbnRf1G1+RlyA3LWiDgra1xpS7H2g4BuOzzRbRB+hmlw0yFsLprHhbbt7jUzbzAbAjK/Pn0FDnh1A=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-aqRwW0LJLV1v1NzyLvLWQhdLmDSAV1vUh+OBdYJaa8f28XBn5BZavo+WTfqgEzALZxlNfBmu6NGO6Al3MbCULw=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oizCjdyQ3WJEswpb3Chdngeat56rIdSYK12JI3iI11Mt5T5EXcZ7WLuowzEaFPNJ3zmOQFliMN8QY1Pi+qsfdQ=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-g47s+V+OqsGxbSZN3lpav6WYOk0PIc3aCBAq+p6dwSynL3K5MA6Cg6nkzDOlu28GEHwbakW+BllzHCJCxnfK5Q=="],
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
@ -203,31 +203,31 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="],
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="],
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw=="],
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.5.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw=="],
"@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="],
"@opentelemetry/core": ["@opentelemetry/core@2.5.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA=="],
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.211.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/sdk-logs": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ=="],
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.212.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-grpc-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/sdk-logs": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-/0bk6fQG+eSFZ4L6NlckGTgUous/ib5+OVdg0x4OdwYeHzV3lTEo3it1HgnPY6UKpmX7ki+hJvxjsOql8rCeZA=="],
"@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/sdk-logs": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA=="],
"@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/sdk-logs": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JidJasLwG/7M9RTxV/64xotDKmFAUSBc9SNlxI32QYuUMK5rVKhHNWMPDzC7E0pCAL3cu+FyiKvsTwLi2KqPYw=="],
"@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-logs": "0.211.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kMvfKMtY5vJDXeLnwhrZMEwhZ2PN8sROXmzacFU/Fnl4Z79CMrOaL7OE+5X3SObRYlDUa7zVqaXp9ZetYCxfDQ=="],
"@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-logs": "0.212.0", "@opentelemetry/sdk-trace-base": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RpKB5UVfxc7c6Ta1UaCrxXDTQ0OD7BCGT66a97Q5zR1x3+9fw4dSaiqMXT/6FAWj2HyFbem6Rcu1UzPZikGTWQ=="],
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.211.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.5.0", "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-metrics": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ=="],
"@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.212.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.5.1", "@opentelemetry/exporter-metrics-otlp-http": "0.212.0", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-grpc-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-metrics": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-/6Gqf9wpBq22XsomR1i0iPGnbQtCq2Vwnrq5oiDPjYSqveBdK1jtQbhGfmpK2mLLxk4cPDtD1ZEYdIou5K8EaA=="],
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-metrics": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA=="],
"@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-metrics": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8hgBw3aTTRpSTkU4b9MLf/2YVLnfWp+hfnLq/1Fa2cky+vx6HqTodo+Zv1GTIrAKMOOwgysOjufy0gTxngqeBg=="],
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/exporter-metrics-otlp-http": "0.211.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-metrics": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-61iNbffEpyZv/abHaz3BQM3zUtA2kVIDBM+0dS9RK68ML0QFLRGYa50xVMn2PYMToyfszEPEgFC3ypGae2z8FA=="],
"@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/exporter-metrics-otlp-http": "0.212.0", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-metrics": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-C7I4WN+ghn3g7SnxXm2RK3/sRD0k/BYcXaK6lGU3yPjiM7a1M25MLuM6zY3PeVPPzzTZPfuS7+wgn/tHk768Xw=="],
"@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.200.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-metrics": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw=="],
"@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.211.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-grpc-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw=="],
"@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.212.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-grpc-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-trace-base": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9xTuYWp8ClBhljDGAoa0NSsJcsxJsC9zCFKMSZJp1Osb9pjXCMRdA6fwXtlubyqe7w8FH16EWtQNKx/FWi+Ghw=="],
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw=="],
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-trace-base": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-v/0wMozNoiEPRolzC4YoPo4rAT0q8r7aqdnRw3Nu7IDN0CGFzNQazkfAlBJ6N5y0FYJkban7Aw5WnN73//6YlA=="],
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DkjXwbPiqpcPlycUojzG2RmR0/SIK8Gi9qWO9znNvSqgzrnAIE9x2n6yPfpZ+kWHZGafvsvA1lVXucTyyQa5Kg=="],
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-trace-base": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-d1ivqPT0V+i0IVOOdzGaLqonjtlk5jYrW7ItutWzXL/Mk+PiYb59dymy/i2reot9dDnBFWfrsvxyqdutGF5Vig=="],
"@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg=="],
@ -235,25 +235,25 @@
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.208.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA=="],
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.211.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-exporter-base": "0.211.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA=="],
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.212.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-exporter-base": "0.212.0", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-YidOSlzpsun9uw0iyIWrQp6HxpMtBlECE3tiHGAsnpEqJWbAUWcMnIffvIuvTtTQ1OyRtwwaE79dWSQ8+eiB7g=="],
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-logs": "0.211.0", "@opentelemetry/sdk-metrics": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0", "protobufjs": "8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA=="],
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1", "@opentelemetry/sdk-logs": "0.212.0", "@opentelemetry/sdk-metrics": "2.5.1", "@opentelemetry/sdk-trace-base": "2.5.1", "protobufjs": "8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bj7zYFOg6Db7NUwsRZQ/WoVXpAf41WY2gsd3kShSfdpZQDRKHWJiRZIg7A8HvWsf97wb05rMFzPbmSHyjEl9tw=="],
"@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA=="],
"@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA=="],
"@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="],
"@opentelemetry/resources": ["@opentelemetry/resources@2.5.1", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ=="],
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA=="],
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-qglb5cqTf0mOC1sDdZ7nfrPjgmAqs2OxkzOPIf2+Rqx8yKBK0pS7wRtB1xH30rqahBIut9QJDbDePyvtyqvH/Q=="],
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA=="],
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.5.1", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A=="],
"@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@opentelemetry/core": "2.0.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.200.0", "@opentelemetry/exporter-logs-otlp-http": "0.200.0", "@opentelemetry/exporter-logs-otlp-proto": "0.200.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.200.0", "@opentelemetry/exporter-metrics-otlp-http": "0.200.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.200.0", "@opentelemetry/exporter-prometheus": "0.200.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.200.0", "@opentelemetry/exporter-trace-otlp-http": "0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "0.200.0", "@opentelemetry/exporter-zipkin": "2.0.0", "@opentelemetry/instrumentation": "0.200.0", "@opentelemetry/propagator-b3": "2.0.0", "@opentelemetry/propagator-jaeger": "2.0.0", "@opentelemetry/resources": "2.0.0", "@opentelemetry/sdk-logs": "0.200.0", "@opentelemetry/sdk-metrics": "2.0.0", "@opentelemetry/sdk-trace-base": "2.0.0", "@opentelemetry/sdk-trace-node": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A=="],
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="],
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.1", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw=="],
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.5.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.5.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow=="],
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.5.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.5.1", "@opentelemetry/core": "2.5.1", "@opentelemetry/sdk-trace-base": "2.5.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9lopQ6ZoElETOEN0csgmtEV5/9C7BMfA7VtF4Jape3i954b6sTY2k3Xw3CxUTKreDck/vpAuJM+EDo4zheUw+A=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="],
@ -289,7 +289,7 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/node": ["@types/node@25.2.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ=="],
@ -311,7 +311,7 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"char-info": ["char-info@0.3.6", "", { "dependencies": { "node-interval-tree": "^1.3.3" } }, "sha512-JskE1gqij+E0bHxs7hZt4h1xG3mvFntZGPn+jFWqImn2y/g53ugy/4J7/thcOCriiZFcLDFph4w+A0xbe2NFbg=="],
@ -333,7 +333,7 @@
"drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
"elysia": ["elysia@1.4.23", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-mFIT/hEnNfrfbjGRUqunLNcURJfSXpKY4j+EWr4vP6Eoulf7feqs0WQLZwlgFZCxhdyfu0mrypIZ4mNJcEVVlQ=="],
"elysia": ["elysia@1.4.25", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-liKjavH99Gpzrv9cDil6uYWmPuqESfPFV1FIaFSd3iNqo3y7e29sN43VxFIK8tWWnyi6eDAmi2SZk8hNAMQMyg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@ -485,19 +485,21 @@
"@logtape/otel/@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ=="],
"@logtape/otel/@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="],
"@logtape/otel/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="],
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-prometheus/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
@ -505,11 +507,11 @@
"@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
@ -523,7 +525,7 @@
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="],
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.212.0", "", { "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/otlp-transformer": "0.212.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw=="],
"@opentelemetry/propagator-b3/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
@ -625,6 +627,8 @@
"@logtape/otel/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
"@logtape/otel/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="],
"@logtape/otel/@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
"@logtape/otel/@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],

View File

@ -17,28 +17,28 @@
"@logtape/otel": "2.0.2",
"@logtape/redaction": "2.0.2",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.211.0",
"@opentelemetry/core": "^2.5.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.211.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.211.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.211.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.211.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.211.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.211.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.211.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.211.0",
"@opentelemetry/resources": "^2.5.0",
"@opentelemetry/sdk-logs": "^0.211.0",
"@opentelemetry/sdk-metrics": "^2.5.0",
"@opentelemetry/sdk-trace-base": "^2.5.0",
"@opentelemetry/sdk-trace-node": "^2.5.0",
"@opentelemetry/api-logs": "^0.212.0",
"@opentelemetry/core": "^2.5.1",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.212.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.212.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.212.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.212.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.212.0",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.212.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.212.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.212.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.212.0",
"@opentelemetry/resources": "^2.5.1",
"@opentelemetry/sdk-logs": "^0.212.0",
"@opentelemetry/sdk-metrics": "^2.5.1",
"@opentelemetry/sdk-trace-base": "^2.5.1",
"@opentelemetry/sdk-trace-node": "^2.5.1",
"@opentelemetry/semantic-conventions": "^1.39.0",
"@types/bun": "^1.3.6",
"@types/bun": "^1.3.9",
"blurhash": "^2.0.5",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "0.44.7",
"elysia": "^1.4.23",
"elysia": "^1.4.25",
"jose": "^6.1.3",
"node-addon-api": "^8.5.0",
"parjs": "^1.3.9",
@ -46,7 +46,7 @@
"sharp": "^0.34.5"
},
"devDependencies": {
"@biomejs/biome": "2.3.14",
"@biomejs/biome": "2.4.0",
"@types/pg": "^8.16.0"
},
"module": "src/index.js",

View File

@ -135,7 +135,7 @@ const newsSort: Sort = {
],
};
const entryRelations = {
export const entryRelations = {
translations: () => {
const { pk, language, ...trans } = getColumns(entryTranslations);
return db

View File

@ -6,6 +6,7 @@ import { entries } from "~/db/schema";
import { watchlist } from "~/db/schema/watchlist";
import { getColumns } from "~/db/utils";
import { Entry } from "~/models/entry";
import { Show } from "~/models/show";
import {
AcceptLanguage,
createPage,
@ -21,6 +22,7 @@ import { desc } from "~/models/utils/descriptions";
import {
entryFilters,
entryProgressQ,
entryRelations,
entryVideosQ,
getEntryTransQ,
mapProgress,
@ -71,7 +73,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
query: { sort, filter, query, limit, after },
headers: { "accept-language": languages, ...headers },
request: { url },
jwt: { sub },
jwt: { sub, settings },
}) => {
const langs = processLanguages(languages);
const transQ = getEntryTransQ(langs);
@ -102,6 +104,11 @@ export const nextup = new Elysia({ tags: ["profiles"] })
seasonNumber: sql<number>`${seasonNumber}`,
episodeNumber: sql<number>`${episodeNumber}`,
name: sql<string>`${transQ.name}`,
show: sql`${entryRelations.show({
languages: langs,
preferOriginal: settings.preferOriginal,
})}`,
})
.from(entries)
.innerJoin(watchlist, eq(watchlist.nextEntry, entries.pk))
@ -134,7 +141,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
"accept-language": AcceptLanguage({ autoFallback: true }),
}),
response: {
200: Page(Entry),
200: Page(t.Intersect([Entry, t.Object({ show: Show })])),
},
},
);

View File

@ -1,9 +1,6 @@
{
"extends": "//",
"files": {
"includes": [
"src/**",
"scripts/**"
]
"includes": ["**", "!public", "!.expo"]
}
}

View File

@ -67,8 +67,8 @@
"zod": "^4.3.5",
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
"@types/bun": "^1.3.6",
"@biomejs/biome": "2.4.0",
"@types/bun": "^1.3.9",
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7",
"typescript": "5.9.3",
@ -270,23 +270,23 @@
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
"@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
"@biomejs/biome": ["@biomejs/biome@2.4.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.0", "@biomejs/cli-darwin-x64": "2.4.0", "@biomejs/cli-linux-arm64": "2.4.0", "@biomejs/cli-linux-arm64-musl": "2.4.0", "@biomejs/cli-linux-x64": "2.4.0", "@biomejs/cli-linux-x64-musl": "2.4.0", "@biomejs/cli-win32-arm64": "2.4.0", "@biomejs/cli-win32-x64": "2.4.0" }, "bin": { "biome": "bin/biome" } }, "sha512-iluT61cORUDIC5i/y42ljyQraCemmmcgbMLLCnYO+yh+2hjTmcMFcwY8G0zTzWCsPb3t3AyKc+0t/VuhPZULUg=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L+YpOtPSuU0etomfvFTPWRsa7+8ejaJL3yaROEoT/96HDJbR6OsvZQk0C8JUYou+XFdP+JcGxqZknkp4n934RA=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Aq+S7ffpb5ynTyLgtnEjG+W6xuTd2F7FdC7J6ShpvRhZwJhjzwITGF9vrqoOnw0sv1XWkt2Q1Rpg+hleg/Xg7Q=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-u2p54IhvNAWB+h7+rxCZe3reNfQYFK+ppDw+q0yegrGclFYnDPZAntv/PqgUacpC3uxTeuWFgWW7RFe3lHuxOA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1rhDUq8sf7xX3tg7vbnU3WVfanKCKi40OXc4VleBMzRStmQHdeBY46aFP6VdwEomcVjyNiu+Zcr3LZtAdrZrjQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WVFOhsnzhrbMGOSIcB9yFdRV2oG2KkRRhIZiunI9gJqSU3ax9ErdnTxRfJUxZUI9NbzVxC60OCXNcu+mXfF/Tw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Omo0xhl63z47X+CrE5viEWKJhejJyndl577VoXg763U/aoATrK3r5+8DPh02GokWPeODX1Hek00OtjjooGan9w=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-aqRwW0LJLV1v1NzyLvLWQhdLmDSAV1vUh+OBdYJaa8f28XBn5BZavo+WTfqgEzALZxlNfBmu6NGO6Al3MbCULw=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-g47s+V+OqsGxbSZN3lpav6WYOk0PIc3aCBAq+p6dwSynL3K5MA6Cg6nkzDOlu28GEHwbakW+BllzHCJCxnfK5Q=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
@ -442,7 +442,7 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@legendapp/list": ["@legendapp/list@github:zoriya/legend-list#c36ff94", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "*" } }, "zoriya-legend-list-c36ff94"],
"@legendapp/list": ["@legendapp/list@github:zoriya/legend-list#a7465a6", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "*" } }, "zoriya-legend-list-a7465a6"],
"@material-symbols/svg-400": ["@material-symbols/svg-400@0.40.2", "", {}, "sha512-e2yEgZW/OveVT1sGaZW1kkRWTPVghjsJYWy+vIea3q08Fv2o7FCYv23PESMyr5D4AaAXdM5dKWkF1e6yIm4swA=="],
@ -532,7 +532,7 @@
"@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.81.5", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw=="],
"@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.10.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-4YPB3cAtt5hwNnR3cpU4c85g1CXd8BJ9Eop1D/hls0zC2rAwbFrTk/jMCSxCvXJzDrYam0cgvcN+jk03jLmkog=="],
"@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.13.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-qxxjRDpjhZ4vIZqG4rBU1Vx2jgOAO/ciUKc9sJqVlTM005E2E+aK1EaE3lGaBDyZxTpjonollAucZcqL7OTscQ=="],
"@react-navigation/core": ["@react-navigation/core@7.14.0", "", { "dependencies": { "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g=="],
@ -604,13 +604,13 @@
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.18", "", {}, "sha512-rbGx6bHgPNVzutP7BEr+53UPKohpckqlMAad+To9UxTbeaQ+kC/1SDRj+QzkwbQ7qhLT/1IKp34yS6thda6fzA=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.92.0", "", {}, "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.18", "", { "dependencies": { "@tanstack/query-core": "5.90.18" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-KqNZX0C5IFz4639zR1ilnQ288tQdJrMNLtzmlzyJ14xauBkhtLEy3mPU/V4KiHsr41eL1ILZbDP36TB12lYfCQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="],
"@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="],
@ -622,7 +622,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="],
@ -656,7 +656,7 @@
"@urql/exchange-retry": ["@urql/exchange-retry@1.3.2", "", { "dependencies": { "@urql/core": "^5.1.2", "wonka": "^6.3.2" } }, "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg=="],
"@videojs/http-streaming": ["@videojs/http-streaming@3.17.2", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.1.1", "aes-decrypter": "^4.0.2", "global": "^4.4.0", "m3u8-parser": "^7.2.0", "mpd-parser": "^1.3.1", "mux.js": "7.1.0", "video.js": "^7 || ^8" } }, "sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g=="],
"@videojs/http-streaming": ["@videojs/http-streaming@3.17.4", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^4.1.1", "aes-decrypter": "^4.0.2", "global": "^4.4.0", "m3u8-parser": "^7.2.0", "mpd-parser": "^1.3.1", "mux.js": "7.1.0", "video.js": "^7 || ^8" } }, "sha512-XAvdG2dolBuV2Fx8bu1kjmQ2D4TonGzZH68Pgv/O9xMSFWdZtITSMFismeQLEAtMmGwze8qNJp3RgV+jStrJqg=="],
"@videojs/vhs-utils": ["@videojs/vhs-utils@4.1.1", "", { "dependencies": { "@babel/runtime": "^7.12.5", "global": "^4.4.0" } }, "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA=="],
@ -752,7 +752,7 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@ -1034,7 +1034,7 @@
"i18next": ["i18next@25.7.4", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
"i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="],
@ -1360,7 +1360,7 @@
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
"react-i18next": ["react-i18next@16.5.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw=="],
"react-i18next": ["react-i18next@16.5.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g=="],
"react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="],
@ -1372,9 +1372,9 @@
"react-native-localization-settings": ["react-native-localization-settings@1.2.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-pxX/mfokqjwIdb1zINuN6DLL4PeVHTaIGz2Tk833tS94fmpsSuPoYnkCmtXsfvZjxhDOSsRceao/JutJbIlpIQ=="],
"react-native-mmkv": ["react-native-mmkv@4.1.1", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-nYFjM27l7zVhIiyAqWEFRagGASecb13JMIlzAuOeakRRz9GMJ49hCQntUBE2aeuZRE4u4ehSqTOomB0mTF56Ew=="],
"react-native-mmkv": ["react-native-mmkv@4.1.2", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-6LHb2DQBXuo96Aues13EugmlWw/HAYuh3KoJoQNrC4JsBwn3J3KiRYAg2mCm5Je0VYq2YsmbgZG7XJwX/WFYZA=="],
"react-native-nitro-modules": ["react-native-nitro-modules@0.33.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-ZlfOe6abODeHv/eZf8PxeSkrxIUhEKha6jaAAA9oXy7I6VPr7Ff4dUsAq3cyF3kX0L6qt2Dh9nzD2NdSsDwGpA=="],
"react-native-nitro-modules": ["react-native-nitro-modules@0.33.9", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-BM9C5mCGYYjrc8CDWZZ0anLWU/knH2xaEuFzvzogKTOW6fzgS6mmsCdM3ty+AhImJNSYwK19DLrHaqwnrrwEzw=="],
"react-native-reanimated": ["react-native-reanimated@4.1.6", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*", "react-native-worklets": ">=0.5.0" } }, "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ=="],
@ -1384,7 +1384,7 @@
"react-native-svg": ["react-native-svg@15.12.1", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g=="],
"react-native-svg-transformer": ["react-native-svg-transformer@1.5.2", "", { "dependencies": { "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "path-dirname": "^1.0.2" }, "peerDependencies": { "react-native": ">=0.59.0", "react-native-svg": ">=12.0.0" } }, "sha512-eW4hOtrd30s4SRdN4X1XYxTCu1czsxDGQKmfQ3RFbZMN5yw4ZmiKGGr+lXbQW4uDGZvSoGd9FHL1f+rgGoKg8Q=="],
"react-native-svg-transformer": ["react-native-svg-transformer@1.5.3", "", { "dependencies": { "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "path-dirname": "^1.0.2" }, "peerDependencies": { "react-native": ">=0.59.0", "react-native-svg": ">=12.0.0" } }, "sha512-M4uFg5pUt35OMgjD4rWWbwd6PmxV96W7r/gQTTa+iZA5B+jO6aURhzAZGLHSrg1Kb91cKG0Rildy9q1WJvYstg=="],
"react-native-video": ["react-native-video@github:zoriya/react-native-video#b8ce1c9", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": ">=0.27.2" } }, "zoriya-react-native-video-b8ce1c9"],
@ -1530,9 +1530,9 @@
"svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="],
"sweetalert2": ["sweetalert2@11.26.17", "", {}, "sha512-kkaySn1IRfwNlf9AkZVqDmBINDWw9NRR6Ij0O5dBRBOD1+mbtZJWxxR9/pA90nce9E5tIIkJ7SWij4rMWXtA1g=="],
"sweetalert2": ["sweetalert2@11.26.18", "", {}, "sha512-3O5feBqV+hTIOwCRKGuZGHosjiuBAKP/vpBl6vKFZeVYfCUGdXqXuuidn6YXHan3f6e62UdmnjwJBt8UtDVBhg=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
"tailwind-merge": ["tailwind-merge@3.4.1", "", {}, "sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
@ -1592,7 +1592,7 @@
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
"uniwind": ["uniwind@1.2.6", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "culori": "4.0.2", "lightningcss": "1.30.2" }, "peerDependencies": { "react": ">=19.0.0", "react-native": ">=0.81.0", "tailwindcss": ">=4" } }, "sha512-DWzKsIzhI6fDmwtpewm71T6y8Wxq/P95VzmfThFearPuwyLeoLaR82OAAW2fXM75olRyE96zRo6IWydKMxErug=="],
"uniwind": ["uniwind@1.3.1", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "culori": "4.0.2", "lightningcss": "1.30.2" }, "peerDependencies": { "react": ">=19.0.0", "react-native": ">=0.81.0", "tailwindcss": ">=4" } }, "sha512-rXhalY2VN9jvZzLiotPltpq4SNsSXpy6CzwhqdeglmgVWIFRSYzIgEa+UnVbyjzPtwy9stfSHwQ4kbR5DmjhUA=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@ -1616,7 +1616,7 @@
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
"video.js": ["video.js@8.23.4", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@videojs/http-streaming": "^3.17.2", "@videojs/vhs-utils": "^4.1.1", "@videojs/xhr": "2.7.0", "aes-decrypter": "^4.0.2", "global": "4.4.0", "m3u8-parser": "^7.2.0", "mpd-parser": "^1.3.1", "mux.js": "^7.0.1", "videojs-contrib-quality-levels": "4.1.0", "videojs-font": "4.2.0", "videojs-vtt.js": "0.15.5" } }, "sha512-qI0VTlYmKzEqRsz1Nppdfcaww4RSxZAq77z2oNSl3cNg2h6do5C8Ffl0KqWQ1OpD8desWXsCrde7tKJ9gGTEyQ=="],
"video.js": ["video.js@8.23.7", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@videojs/http-streaming": "^3.17.3", "@videojs/vhs-utils": "^4.1.1", "@videojs/xhr": "2.7.0", "aes-decrypter": "^4.0.2", "global": "4.4.0", "m3u8-parser": "^7.2.0", "mpd-parser": "^1.3.1", "mux.js": "^7.0.1", "videojs-contrib-quality-levels": "4.1.0", "videojs-font": "4.2.0", "videojs-vtt.js": "0.15.5" } }, "sha512-cG4HOygYt+Z8j6Sf5DuK6OgEOoM+g9oGP6vpqoZRaD13aHE4PMITbyjJUXZcIQbgB0wJEadBRaVm5lJIzo2jAA=="],
"videojs-contrib-quality-levels": ["videojs-contrib-quality-levels@4.1.0", "", { "dependencies": { "global": "^4.4.0" }, "peerDependencies": { "video.js": "^8" } }, "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA=="],
@ -1674,7 +1674,7 @@
"yoshiki": ["yoshiki@1.2.14", "", { "dependencies": { "@types/inline-style-prefixer": "^5.0.0", "@types/node": "18.x.x", "@types/react": "18.x.x", "inline-style-prefixer": "^7.0.0" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native", "react-native-web"] }, "sha512-TQoaB1C8/rUCuz/856eDv1BUxN+/OYL0f7RG+MwqIv260BEQQeUrsGGaMfN2GHdy86geNkn9wQArJqsjut/3Lg=="],
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@ -1752,6 +1752,8 @@
"expo-router/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="],
"expo-router/@react-navigation/bottom-tabs": ["@react-navigation/bottom-tabs@7.10.0", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-4YPB3cAtt5hwNnR3cpU4c85g1CXd8BJ9Eop1D/hls0zC2rAwbFrTk/jMCSxCvXJzDrYam0cgvcN+jk03jLmkog=="],
"expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"fbjs/cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],

View File

@ -77,8 +77,8 @@
"zod": "^4.3.5"
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
"@types/bun": "^1.3.6",
"@biomejs/biome": "2.4.0",
"@types/bun": "^1.3.9",
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7",
"typescript": "5.9.3"

View File

@ -1,40 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import type { QueryPage } from "@kyoo/models";
import { ts } from "@kyoo/primitives";
import { ScrollView } from "react-native";
import { DefaultLayout } from "../layout";
import { Scanner } from "./scanner";
import { UserList } from "./users";
export const AdminPage: QueryPage = () => {
return (
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
<UserList />
<Scanner />
</ScrollView>
);
};
AdminPage.getLayout = DefaultLayout;
AdminPage.requiredPermissions = ["admin.read"];
AdminPage.getFetchUrls = () => [UserList.query()];

View File

@ -1,102 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import {
type Issue,
IssueP,
type QueryIdentifier,
queryFn,
useFetch,
} from "@kyoo/models";
import { Button, Icon, P, Skeleton, tooltip, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { z } from "zod";
import { SettingsContainer } from "../settings/base";
import Info from "@material-symbols/svg-400/outlined/info.svg";
import Scan from "@material-symbols/svg-400/outlined/sensors.svg";
import { useMutation } from "@tanstack/react-query";
export const Scanner = () => {
const { css } = useYoshiki();
const { t } = useTranslation();
const { data, error } = useFetch(Scanner.query());
const metadataRefreshMutation = useMutation({
mutationFn: () =>
queryFn({
path: ["rescan"],
method: "POST",
}),
});
return (
<SettingsContainer
title={t("admin.scanner.label")}
extraTop={
<Button
licon={<Icon icon={Scan} {...css({ marginX: ts(1) })} />}
text={t("admin.scanner.scan")}
onPress={() => metadataRefreshMutation.mutate()}
{...css({ marginBottom: ts(2) })}
/>
}
>
<>
( (data ?? [...Array(3)])?.map((x, i) => (
<View
key={x?.cause ?? `${i}`}
{...css({
marginY: ts(1),
marginX: ts(3),
flexDirection: "row",
alignItems: "center",
flexGrow: 1,
})}
>
<Icon
icon={Info}
{...css({ flexShrink: 0, marginRight: ts(2) })}
{...tooltip(x?.cause)}
/>
<Skeleton>
{x && (
<P {...css({ flexGrow: 1, flexShrink: 1, flexWrap: "wrap" })}>
{x.reason}
</P>
)}
</Skeleton>
</View>
)) )}
{data != null && data.length === 0 && <P>{t("admin.scanner.empty")}</P>}
</>
</SettingsContainer>
);
};
Scanner.query = (): QueryIdentifier<Issue[]> => ({
parser: z.array(IssueP),
path: ["issues"],
params: {
filter: "domain eq scanner",
},
});

View File

@ -1,207 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { type QueryIdentifier, type User, UserP, queryFn } from "@kyoo/models";
import { Alert, Avatar, Icon, IconButton, Menu, P, Skeleton, tooltip, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { px, useYoshiki } from "yoshiki/native";
import type { Layout } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite";
import { SettingsContainer } from "../settings/base";
import UserI from "@material-symbols/svg-400/rounded/account_circle.svg";
import Delete from "@material-symbols/svg-400/rounded/delete.svg";
import Unverifed from "@material-symbols/svg-400/rounded/gpp_bad.svg";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
import Admin from "@material-symbols/svg-400/rounded/shield_person.svg";
import Verifed from "@material-symbols/svg-400/rounded/verified_user.svg";
import { useMutation, useQueryClient } from "@tanstack/react-query";
export const UserGrid = ({
id,
username,
avatar,
isAdmin,
isVerified,
...props
}: {
id: string;
username: string;
avatar: string;
isAdmin: boolean;
isVerified: boolean;
}) => {
const { css } = useYoshiki();
const { t } = useTranslation();
const queryClient = useQueryClient();
const { mutateAsync } = useMutation({
mutationFn: async (update: Partial<User>) =>
await queryFn({
path: ["users", id],
method: "PATCH",
body: update,
}),
onSettled: async () => await queryClient.invalidateQueries({ queryKey: ["users"] }),
});
return (
<View {...css({ alignItems: "center" }, props)}>
<Avatar src={avatar} alt={username} placeholder={username} size={UserGrid.layout.size} fill />
<View {...css({ flexDirection: "row", alignItems: "center" })}>
<Icon
icon={!isVerified ? Unverifed : isAdmin ? Admin : UserI}
{...css({
m: ts(1),
})}
{...tooltip(
t(
!isVerified
? "admin.users.unverifed"
: isAdmin
? "admin.users.adminUser"
: "admin.users.regularUser",
),
)}
/>
<P>{username}</P>
<Menu Trigger={IconButton} icon={MoreVert} {...tooltip(t("misc.more"))}>
{!isVerified && (
<Menu.Item
label={t("admin.users.verify")}
icon={Verifed}
onSelect={() =>
mutateAsync({
permissions: ["overall.read", "overall.play"],
})
}
/>
)}
<Menu.Sub label={t("admin.users.set-permissions")} icon={Admin}>
<Menu.Item
selected={!isAdmin}
label={t("admin.users.regularUser")}
onSelect={() =>
mutateAsync({
permissions: ["overall.read", "overall.play"],
})
}
/>
<Menu.Item
selected={isAdmin}
label={t("admin.users.adminUser")}
onSelect={() =>
mutateAsync({
permissions: [
"overall.read",
"overall.write",
"overall.create",
"overall.delete",
"overall.play",
"admin.read",
"admin.write",
"admin.create",
"admin.delete",
],
})
}
/>
</Menu.Sub>
<Menu.Item
label={t("admin.users.delete")}
icon={Delete}
onSelect={async () => {
Alert.alert(
t("admin.users.delete"),
t("login.delete-confirmation"),
[
{ text: t("misc.cancel"), style: "cancel" },
{
text: t("misc.delete"),
onPress: async () => {
await queryFn({ path: ["users", id], method: "DELETE" });
await queryClient.invalidateQueries({ queryKey: ["users"] });
},
style: "destructive",
},
],
{
cancelable: true,
icon: "warning",
},
);
}}
/>
</Menu>
</View>
</View>
);
};
UserGrid.Loader = (props: object) => {
const { css } = useYoshiki();
return (
<View {...css({ alignItems: "center" }, props)}>
<Avatar.Loader size={UserGrid.layout.size} />
<View {...css({ flexDirection: "row", alignItems: "center", flexShrink: 1, flexGrow: 1 })}>
<Icon icon={UserI} {...css({ m: ts(1) })} />
<Skeleton {...css({ flexGrow: 1, width: ts(8) })} />
<IconButton icon={MoreVert} disabled />
</View>
</View>
);
};
UserGrid.layout = {
size: px(150),
numColumns: { xs: 2, sm: 3, md: 5, lg: 6, xl: 7 },
gap: { xs: ts(1), sm: ts(2), md: ts(4) },
layout: "grid",
} satisfies Layout;
export const UserList = () => {
const { t } = useTranslation();
return (
<SettingsContainer title={t("admin.users.label")}>
<InfiniteFetch
query={UserList.query()}
layout={UserGrid.layout}
Render={({ item }) => (
<UserGrid
id={item.id}
username={item.username}
avatar={item.logo}
isAdmin={item.isAdmin}
isVerified={item.isVerified}
/>
)}
Loader={UserGrid.Loader}
/>
</SettingsContainer>
);
};
UserList.query = (): QueryIdentifier<User> => ({
parser: UserP,
path: ["users"],
infinite: true,
});

View File

@ -1,194 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import {
type Collection,
CollectionP,
type LibraryItem,
LibraryItemP,
type QueryIdentifier,
type QueryPage,
getDisplayDate,
} from "@kyoo/models";
import {
Container,
GradientImageBackground,
Head,
P,
Skeleton,
ts,
usePageStyle,
} from "@kyoo/primitives";
import { forwardRef } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View, type ViewProps } from "react-native";
import { percent, px, useYoshiki } from "yoshiki/native";
import { ItemGrid } from "../browse/grid";
import { Header as ShowHeader, TitleLine } from "../../../../src/ui/details/headeri/details/header";
import { SvgWave } from "../../../../src/ui/details/show/ui/details/show";
import { Fetch } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite";
import { ItemDetails } from "../home/recommended";
import { DefaultLayout } from "../layout";
const Header = ({ slug }: { slug: string }) => {
const { css } = useYoshiki();
const { t } = useTranslation();
return (
<Fetch query={Header.query(slug)}>
{({ isLoading, ...data }) => (
<>
<Head title={data?.name} description={data?.overview} image={data?.thumbnail?.high} />
<GradientImageBackground
src={data?.thumbnail}
quality="high"
alt=""
containerStyle={ShowHeader.containerStyle}
>
<TitleLine
isLoading={isLoading}
type={"collection"}
playHref={null}
name={data?.name}
tagline={null}
date={null}
rating={null}
runtime={null}
poster={data?.poster}
trailerUrl={null}
studio={null}
{...css(ShowHeader.childStyle)}
/>
</GradientImageBackground>
<Container
{...css({
paddingTop: ts(4),
marginBottom: ts(4),
})}
>
<Skeleton lines={4}>
{isLoading || (
<P {...css({ textAlign: "justify" })}>{data.overview ?? t("show.noOverview")}</P>
)}
</Skeleton>
</Container>
</>
)}
</Fetch>
);
};
Header.query = (slug: string): QueryIdentifier<Collection> => ({
parser: CollectionP,
path: ["collections", slug],
});
const CollectionHeader = forwardRef<View, ViewProps & { slug: string }>(function ShowHeader(
{ children, slug, ...props },
ref,
) {
const { css, theme } = useYoshiki();
return (
<View
ref={ref}
{...css(
[
{ bg: (theme) => theme.variant.background },
Platform.OS === "web" && {
flexGrow: 1,
flexShrink: 1,
// @ts-ignore Web only property
overflowY: "auto" as any,
},
],
props,
)}
>
<Header slug={slug} />
<SvgWave fill={theme.background} {...css({ flexShrink: 0, flexGrow: 1, display: "flex" })} />
<View {...css({ bg: theme.background, paddingTop: { xs: ts(8), md: 0 } })}>
<View
{...css({
width: percent(100),
maxWidth: { xs: percent(100), lg: px(1170) },
alignSelf: "center",
})}
>
{children}
</View>
</View>
</View>
);
});
const query = (slug: string): QueryIdentifier<LibraryItem> => ({
parser: LibraryItemP,
path: ["collections", slug, "items"],
infinite: true,
params: {
fields: ["firstEpisode", "episodesCount", "watchStatus"],
},
});
export const CollectionPage: QueryPage<{ slug: string }> = ({ slug }) => {
const { css } = useYoshiki();
const pageStyle = usePageStyle();
return (
<InfiniteFetch
query={query(slug)}
placeholderCount={2}
layout={{ ...ItemDetails.layout, numColumns: { xs: 1, md: 2 } }}
Header={CollectionHeader}
headerProps={{ slug }}
contentContainerStyle={{ padding: 0, paddingHorizontal: 0, ...pageStyle }}
Render={({ item }) => (
<ItemDetails
slug={item.slug}
type={item.kind}
name={item.name}
tagline={"tagline" in item ? item.tagline : null}
overview={item.overview}
poster={item.poster}
subtitle={item.kind !== "collection" ? getDisplayDate(item) : null}
genres={"genres" in item ? item.genres : null}
href={item.href}
playHref={item.kind !== "collection" ? item.playHref : null}
watchStatus={(item.kind !== "collection" && item.watchStatus?.status) || null}
unseenEpisodesCount={
item.kind === "show"
? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!)
: null
}
{...css({ marginX: ItemGrid.layout.gap })}
/>
)}
Loader={ItemDetails.Loader}
/>
);
};
CollectionPage.getLayout = { Layout: DefaultLayout, props: { transparent: true } };
CollectionPage.getFetchUrls = ({ slug }) => [query(slug), Header.query(slug)];

View File

@ -1,22 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
export { DownloadPage } from "./page";
export { DownloadProvider, useDownloader } from "./state";

View File

@ -1,52 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { type WatchInfo, getCurrentApiUrl, queryFn, toQueryKey } from "@kyoo/models";
import { getCurrentAccount } from "@kyoo/models/src/account-internal";
import type { ReactNode } from "react";
import { Player } from "../../../../src/ui/player../src/ui/player";
export const useDownloader = () => {
return async (type: "episode" | "movie", slug: string) => {
const account = getCurrentAccount();
const query = Player.infoQuery(type, slug);
const info: WatchInfo = await queryFn(
{
queryKey: toQueryKey(query),
signal: undefined as any,
meta: undefined,
apiUrl: account?.apiUrl,
},
query.parser,
account?.token.access_token,
);
// TODO: This methods does not work with auth.
const a = document.createElement("a");
a.style.display = "none";
a.href = `${getCurrentApiUrl()!}/${type}/${slug}/direct`;
a.download = `${slug}.${info.extension}`;
document.body.appendChild(a);
a.click();
};
};
export const DownloadPage = () => {};
export const DownloadProvider = ({ children }: { children: ReactNode }) => children;

View File

@ -1,233 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import type { KyooImage } from "@kyoo/models";
import {
Alert,
H6,
IconButton,
ImageBackground,
Menu,
P,
PressableFeedback,
SubP,
focusReset,
ts,
usePageStyle,
} from "@kyoo/primitives";
import DownloadForOffline from "@material-symbols/svg-400/rounded/download_for_offline.svg";
import Downloading from "@material-symbols/svg-400/rounded/downloading.svg";
import ErrorIcon from "@material-symbols/svg-400/rounded/error.svg";
import NotStarted from "@material-symbols/svg-400/rounded/not_started.svg";
import { FlashList } from "@shopify/flash-list";
import { useRouter } from "expo-router";
import { type Atom, useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { percent, useYoshiki } from "yoshiki/native";
import { EpisodeLine, displayRuntime, episodeDisplayNumber } from "../../../../src/ui/details/episode";
import { EmptyView } from "../fetch";
import { type State, downloadAtom } from "./state";
const DownloadedItem = ({
name,
statusAtom,
runtime,
kind,
image,
...props
}: {
name: string;
statusAtom: Atom<State>;
runtime: number | null;
kind: "episode" | "movie";
image: KyooImage | null;
}) => {
const { css } = useYoshiki();
const { t } = useTranslation();
const router = useRouter();
const { error, status, progress, pause, resume, remove, play, retry } = useAtomValue(statusAtom);
return (
<PressableFeedback
onPress={() => play?.(router)}
{...css(
{
alignItems: "center",
flexDirection: "row",
fover: {
self: focusReset,
title: {
textDecorationLine: "underline",
},
},
},
props,
)}
>
<ImageBackground
src={image}
quality="low"
alt=""
layout={{
width: percent(25),
aspectRatio: kind === "episode" ? 16 / 9 : 2 / 3,
}}
{...css({ flexShrink: 0, m: ts(1) })}
>
{/* {(watchedPercent || watchedStatus === WatchStatusV.Completed) && ( */}
{/* <> */}
{/* <View */}
{/* {...css({ */}
{/* backgroundColor: (theme) => theme.overlay0, */}
{/* width: percent(100), */}
{/* height: ts(0.5), */}
{/* position: "absolute", */}
{/* bottom: 0, */}
{/* })} */}
{/* /> */}
{/* <View */}
{/* {...css({ */}
{/* backgroundColor: (theme) => theme.accent, */}
{/* width: percent(watchedPercent ?? 100), */}
{/* height: ts(0.5), */}
{/* position: "absolute", */}
{/* bottom: 0, */}
{/* })} */}
{/* /> */}
{/* </> */}
{/* )} */}
</ImageBackground>
<View
{...css({
flexGrow: 1,
flexShrink: 1,
flexDirection: "row",
justifyContent: "space-between",
})}
>
<View {...css({ flexGrow: 1, flexShrink: 1 })}>
{/* biome-ignore lint/a11y/useValidAriaValues: use h6 for style only */}
<H6 aria-level={undefined} {...css([{ flexShrink: 1 }, "title"])}>
{name ?? t("show.episodeNoMetadata")}
</H6>
{status === "FAILED" && <P>{t("downloads.error", { error: error ?? "Unknow error" })}</P>}
{runtime && status === "DONE" && <SubP>{displayRuntime(runtime)}</SubP>}
{progress !== 100 && progress !== null && (
<View {...css({ flexDirection: "row", alignItems: "center" })}>
<SubP {...css({ flexShrink: 0 })}>{Math.round(progress)}%</SubP>
<View
{...css({
flexGrow: 1,
flexShrink: 1,
marginLeft: ts(0.5),
height: ts(0.5),
backgroundColor: (theme) => theme.user.overlay0,
})}
>
<View
{...css({
backgroundColor: (theme) => theme.user.accent,
width: percent(progress),
height: percent(100),
})}
/>
</View>
</View>
)}
</View>
<Menu Trigger={IconButton} icon={downloadIcon(status)}>
{status === "FAILED" && (
<Menu.Item label={t("downloads.retry")} onSelect={() => retry?.()} />
)}
{status === "DOWNLOADING" && (
<Menu.Item label={t("downloads.pause")} onSelect={() => pause?.()} />
)}
{status === "PAUSED" && (
<Menu.Item label={t("downloads.resume")} onSelect={() => resume?.()} />
)}
<Menu.Item
label={t("downloads.delete")}
onSelect={() => {
Alert.alert(
t("downloads.delete"),
t("downloads.deleteMessage"),
[
{ text: t("misc.cancel"), style: "cancel" },
{ text: t("misc.delete"), onPress: remove, style: "destructive" },
],
{
icon: "error",
},
);
}}
/>
</Menu>
</View>
</PressableFeedback>
);
};
const downloadIcon = (status: State["status"]) => {
switch (status) {
case "DONE":
return DownloadForOffline;
case "DOWNLOADING":
return Downloading;
case "FAILED":
return ErrorIcon;
case "PENDING":
case "PAUSED":
case "STOPPED":
return NotStarted;
}
};
export const DownloadPage = () => {
const pageStyle = usePageStyle();
const downloads = useAtomValue(downloadAtom);
const { t } = useTranslation();
if (downloads.length === 0) return <EmptyView message={t("downloads.empty")} />;
return (
<FlashList
data={downloads}
getItemType={(item) => item.data.kind}
renderItem={({ item }) => (
<DownloadedItem
name={
item.data.kind === "episode"
? `${episodeDisplayNumber(item.data)!}: ${item.data.name}`
: item.data.name
}
statusAtom={item.state}
runtime={item.data.runtime}
kind={item.data.kind}
image={item.data.kind === "episode" ? item.data.thumbnail : item.data.poster}
/>
)}
estimatedItemSize={EpisodeLine.layout.size}
keyExtractor={(x) => x.data.id}
numColumns={1}
contentContainerStyle={pageStyle}
/>
);
};

View File

@ -1,308 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import RNBackgroundDownloader, {
type DownloadTask,
} from "@kesha-antonov/react-native-background-downloader";
import {
type Account,
type Episode,
EpisodeP,
type Movie,
MovieP,
type QueryIdentifier,
type WatchInfo,
WatchInfoP,
queryFn,
toQueryKey,
} from "@kyoo/models";
import { getCurrentAccount, storage } from "@kyoo/models/src/account-internal";
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
import { deleteAsync } from "expo-file-system";
import type { useRouter } from "expo-router";
import { type PrimitiveAtom, atom, useSetAtom, useStore } from "jotai";
import { type ReactNode, useEffect } from "react";
import { ToastAndroid } from "react-native";
import { z } from "zod";
import { Player } from "../../../../src/ui/player";
type Router = ReturnType<typeof useRouter>;
export type State = {
status: "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED" | "PENDING";
progress: number | null;
size: number;
availableSize: number;
error?: string;
pause: (() => void) | null;
resume: (() => void) | null;
remove: () => void;
play: (router: Router) => void;
retry: (() => void) | null;
};
export const downloadAtom = atom<
{
data: Episode | Movie;
info: WatchInfo;
path: string;
state: PrimitiveAtom<State>;
}[]
>([]);
const query = <T,>(query: QueryIdentifier<T>, info: Account): Promise<T> =>
queryFn(
{
queryKey: toQueryKey(query),
signal: undefined as any,
meta: undefined,
apiUrl: info.apiUrl,
// use current user and current apiUrl to download this meta.
},
query.parser,
info.token.access_token,
);
const setupDownloadTask = (
state: { data: Episode | Movie; info: WatchInfo; path: string },
task: DownloadTask,
store: ReturnType<typeof useStore>,
queryClient: QueryClient,
stateAtom?: PrimitiveAtom<State>,
) => {
if (!stateAtom) stateAtom = atom({} as State);
store.set(stateAtom, {
status: task.state,
progress: task.bytesTotal ? (task.bytesDownloaded / task.bytesTotal) * 100 : null,
size: task.bytesTotal,
availableSize: task.bytesDownloaded,
pause: () => {
task.pause();
store.set(stateAtom!, (x) => ({ ...x, state: "PAUSED" }));
},
resume: () => {
task.resume();
store.set(stateAtom!, (x) => ({ ...x, state: "DOWNLOADING" }));
},
remove: () => {
task.stop();
store.set(downloadAtom, (x) => x.filter((y) => y.data.id !== task.id));
},
play: () => {
ToastAndroid.show("The file has not finished downloading", ToastAndroid.LONG);
},
retry: () => {
const [newTask, path] = download(
{
type: state.data.kind,
id: state.data.id,
slug: state.data.slug,
extension: state.info.extension,
},
getCurrentAccount()!,
);
setupDownloadTask({ ...state, path }, newTask, store, queryClient, stateAtom);
},
});
// we use the store instead of the onMount because we want to update the state to cache it even if it was not
// used during this launch of the app.
const update = updater(store, stateAtom);
task
.begin(({ expectedBytes }) => update((x) => ({ ...x, size: expectedBytes })))
.progress(({ bytesDownloaded, bytesTotal }) => {
update((x) => ({
...x,
progress: Math.round((bytesDownloaded / bytesTotal) * 100),
size: bytesTotal,
availableSize: bytesDownloaded,
status: "DOWNLOADING",
}));
})
.done(() => {
update((x) => ({ ...x, progress: 100, status: "DONE", play: playFn(state, queryClient) }));
RNBackgroundDownloader.completeHandler(task.id);
})
.error(({ error }) => {
update((x) => ({ ...x, status: "FAILED", error }));
console.error(`Error downloading ${state.data.slug}`, error);
ToastAndroid.show(`Error downloading ${state.data.slug}`, ToastAndroid.LONG);
});
return { data: state.data, info: state.info, path: state.path, state: stateAtom };
};
const updater = (
store: ReturnType<typeof useStore>,
atom: PrimitiveAtom<State>,
): ((f: (old: State) => State) => void) => {
return (f) => {
// if it lags, we could only store progress info on status change and not on every change.
store.set(atom, f);
const downloads = store.get(downloadAtom);
storage.set(
"downloads",
JSON.stringify(
downloads.map((d) => ({
data: d.data,
info: d.info,
path: d.path,
state: store.get(d.state),
})),
),
);
};
};
const download = (
{
type,
id,
slug,
extension,
}: { slug: string; id: string; type: "episode" | "movie"; extension: string },
account: Account,
) => {
// TODO: support custom paths
const path = `${RNBackgroundDownloader.directories.documents}/${slug}-${id}.${extension}`;
const task = RNBackgroundDownloader.download({
id: id,
// TODO: support variant qualities
url: `${account.apiUrl}/${type}/${slug}/direct`,
destination: path,
headers: {
Authorization: account.token.access_token,
},
isNotificationVisible: true,
// TODO: Implement only wifi
// network: Network.ALL,
});
console.log("Starting download", path);
return [task, path] as const;
};
export const useDownloader = () => {
const setDownloads = useSetAtom(downloadAtom);
const store = useStore();
const queryClient = useQueryClient();
return async (type: "episode" | "movie", slug: string) => {
try {
const account = getCurrentAccount()!;
const [data, info] = await Promise.all([
query(Player.query(type, slug), account),
query(Player.infoQuery(type, slug), account),
]);
if (store.get(downloadAtom).find((x) => x.data.id === data.id)) {
ToastAndroid.show(`${slug} is already downloaded, skipping`, ToastAndroid.LONG);
return;
}
const [task, path] = download(
{ type, slug, id: data.id, extension: info.extension },
account,
);
setDownloads((x) => [
...x,
setupDownloadTask({ data, info, path }, task, store, queryClient),
]);
} catch (e) {
console.error("download error", e);
ToastAndroid.show(`Error downloading ${slug}`, ToastAndroid.LONG);
}
};
};
const playFn =
(dl: { data: Episode | Movie; info: WatchInfo; path: string }, queryClient: QueryClient) =>
(router: Router) => {
dl.data.links.direct = dl.path;
dl.data.links.hls = null;
queryClient.setQueryData(toQueryKey(Player.query(dl.data.kind, dl.data.slug)), dl.data);
queryClient.setQueryData(toQueryKey(Player.infoQuery(dl.data.kind, dl.data.slug)), dl.info);
router.push(
dl.data.kind === "episode"
? { pathname: "/watch/[slug]", params: { slug: dl.data.slug } }
: { pathname: "/movie/[slug]/watch", params: { slug: dl.data.slug } },
);
};
export const DownloadProvider = ({ children }: { children: ReactNode }) => {
const store = useStore();
const queryClient = useQueryClient();
useEffect(() => {
async function run() {
if (store.get(downloadAtom).length) return;
const tasks = await RNBackgroundDownloader.checkForExistingDownloads();
const dls: { data: Episode | Movie; info: WatchInfo; path: string; state: State }[] =
JSON.parse(storage.getString("downloads") ?? "[]");
const downloads = dls.map((dl) => {
const t = tasks.find((x) => x.id === dl.data.id);
if (t) return setupDownloadTask(dl, t, store, queryClient);
const stateAtom = atom({
status: dl.state.status === "DONE" ? "DONE" : "FAILED",
progress: dl.state.progress,
size: dl.state.size,
availableSize: dl.state.availableSize,
pause: null,
resume: null,
play: playFn(dl, queryClient),
remove: () => {
deleteAsync(dl.path);
store.set(downloadAtom, (x) => x.filter((y) => y.data.id !== dl.data.id));
},
retry: () => {
const [newTask, path] = download(
{
type: dl.data.kind,
id: dl.data.id,
slug: dl.data.slug,
extension: dl.info.extension,
},
getCurrentAccount()!,
);
setupDownloadTask({ ...dl, path }, newTask, store, queryClient, stateAtom);
},
} as State);
return {
data: z.union([EpisodeP, MovieP]).parse(dl.data),
info: WatchInfoP.parse(dl.info),
path: dl.path,
state: stateAtom,
};
});
store.set(downloadAtom, downloads);
for (const t of tasks) {
if (!downloads.find((x) => x.data.id === t.id)) t.stop();
}
RNBackgroundDownloader.ensureDownloadsAreRunning();
}
run();
}, [store, queryClient]);
return children;
};

View File

@ -1,16 +0,0 @@
import intl from "langmap";
const seenNativeNames = new Set();
export const languageCodes = Object.keys(intl)
.filter((x) => {
const nativeName = intl[x]?.nativeName;
// Only include if nativeName is unique and defined
if (nativeName && !seenNativeNames.has(nativeName)) {
seenNativeNames.add(nativeName);
return true;
}
return false;
})
.filter((x) => !x.includes("@"));

View File

@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"sourceMap": true,
"noEmit": true,
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -1,14 +1,21 @@
import Browse from "@material-symbols/svg-400/rounded/browse-fill.svg";
// import Downloading from "@material-symbols/svg-400/rounded/downloading-fill.svg";
import Home from "@material-symbols/svg-400/rounded/home-fill.svg";
import { Tabs } from "expo-router";
import { Slot, Tabs } from "expo-router";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { Icon } from "~/primitives";
import { cn } from "~/utils";
export const unstable_settings = {
initialRouteName: "index",
};
export default function TabsLayout() {
const { t } = useTranslation();
if (Platform.OS === "web") return <Slot />;
return (
<Tabs
screenOptions={{

View File

@ -1,10 +1,14 @@
import { Stack } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useCSSVariable, useResolveClassNames } from "uniwind";
import { NavbarRight, NavbarTitle } from "~/ui/navbar";
import { NavbarLeft, NavbarRight } from "~/ui/navbar";
export { ErrorBoundary } from "~/ui/error-bondary";
export const unstable_settings = {
initialRouteName: "(tabs)",
};
export default function Layout() {
const insets = useSafeAreaInsets();
const accent = useCSSVariable("--color-accent");
@ -13,7 +17,7 @@ export default function Layout() {
return (
<Stack
screenOptions={{
headerTitle: () => <NavbarTitle />,
headerTitle: () => <NavbarLeft />,
headerRight: () => <NavbarRight />,
contentStyle: {
paddingLeft: insets.left,

View File

@ -3,10 +3,16 @@ import { Slot } from "expo-router";
import { Platform } from "react-native";
import { Providers } from "~/providers";
import "../global.css";
import { Tooltip } from "~/primitives";
import { Tooltip, useMobileHover } from "~/primitives";
import "~/fonts.web.css";
export const unstable_settings = {
initialRouteName: "(app)",
};
export default function Layout() {
useMobileHover();
return (
<Providers>
<Slot />

View File

@ -14,7 +14,7 @@ export const Container = <AsProps = ViewProps>({
return (
<As
className={cn(
"flex w-full flex-1 self-center px-4",
"flex w-full self-center px-4",
"sm:w-xl md:w-3xl lg:w-5xl xl:w-7xl",
className,
)}

View File

@ -5,11 +5,11 @@ import {
Platform,
Pressable,
type PressableProps,
Text,
type TextProps,
} from "react-native";
import { useResolveClassNames } from "uniwind";
import { cn } from "~/utils";
import { P } from "./text";
export function useLinkTo({
href,
@ -57,7 +57,7 @@ export const A = ({
const linkProps = useLinkTo({ href, replace });
return (
<Text
<P
{...linkProps}
className={cn(
"select-text text-accent hover:underline focus:underline",
@ -66,7 +66,7 @@ export const A = ({
{...props}
>
{children}
</Text>
</P>
);
};

View File

@ -3,7 +3,7 @@ import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fi
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
import * as RSelect from "@radix-ui/react-select";
import { forwardRef } from "react";
import { View } from "react-native";
import { Platform, View } from "react-native";
import { cn } from "~/utils";
import { Icon } from "./icons";
import { PressableFeedback } from "./links";
@ -27,7 +27,7 @@ export const Select = ({
<RSelect.Root value={value} onValueChange={onValueChange}>
<RSelect.Trigger aria-label={label} asChild>
<InternalTriger
Component={PressableFeedback}
Component={Platform.OS === "web" ? "div" : PressableFeedback}
className={cn(
"group flex-row items-center justify-center overflow-hidden rounded-4xl",
"border-2 border-accent p-1 outline-0 focus-within:bg-accent hover:bg-accent",

View File

@ -0,0 +1,42 @@
import { useEffect } from "react";
import { Platform } from "react-native";
let preventHover = false;
let hoverTimeout: NodeJS.Timeout | number;
export const useMobileHover = () => {
if (Platform.OS !== "web") return;
// biome-ignore lint/correctness/useHookAtTopLevel: const condition
useEffect(() => {
const enableHover = () => {
console.log("pc");
if (preventHover) return;
document.body.classList.remove("noHover");
};
const disableHover = () => {
console.log("mobile");
if (hoverTimeout) clearTimeout(hoverTimeout);
preventHover = true;
hoverTimeout = setTimeout(() => {
preventHover = false;
}, 1000);
document.body.classList.add("noHover");
};
document.addEventListener("touchstart", disableHover, true);
document.addEventListener("mousemove", enableHover, true);
return () => {
document.removeEventListener("touchstart", disableHover);
document.removeEventListener("mousemove", enableHover);
};
}, []);
};
export const useIsTouch = () => {
if (Platform.OS !== "web") return true;
if (typeof window === "undefined") return false;
// TODO: Subscribe to the change.
return document.body.classList.contains("noHover");
};

View File

@ -1,6 +1,5 @@
export * from "./breakpoint";
export * from "./capitalize";
export * from "./head";
export * from "./nojs";
export * from "./hover";
export * from "./spacing";
export * from "./touchonly";

View File

@ -1,17 +0,0 @@
import type { ViewProps } from "react-native";
export const hiddenIfNoJs: ViewProps = {
style: { $$css: true, noJs: "noJsHidden" } as any,
};
export const HiddenIfNoJs = () => (
<noscript>
<style>
{`
.noJsHidden {
display: none;
}
`}
</style>
</noscript>
);

View File

@ -1,17 +1,3 @@
import { Platform } from "react-native";
export const important = <T,>(value: T): T => {
return `${value} !important` as T;
};
export const ts = (spacing: number) => {
return spacing * 8;
};
export const focusReset: object =
Platform.OS === "web"
? {
boxShadow: "unset",
outline: "none",
}
: {};

View File

@ -1,34 +0,0 @@
import { Platform, type ViewProps } from "react-native";
export const TouchOnlyCss = () => {
return (
<style jsx global>{`
:where(body.noHover) .noTouch {
display: none;
}
:where(body:not(.noHover)) .touchOnly {
display: none;
}
`}</style>
);
};
export const touchOnly: ViewProps = {
style:
Platform.OS === "web"
? ({ $$css: true, touchOnly: "touchOnly" } as any)
: {},
};
export const noTouch: ViewProps = {
style:
Platform.OS === "web"
? ({ $$css: true, noTouch: "noTouch" } as any)
: { display: "none" },
};
export const useIsTouch = () => {
if (Platform.OS !== "web") return true;
if (typeof window === "undefined") return false;
// TODO: Subscribe to the change.
return document.body.classList.contains("noHover");
};

View File

@ -8,9 +8,9 @@ import { HeaderBackground, useScrollNavbar } from "../navbar";
import { GenreGrid } from "./genre";
import { Header } from "./header";
import { NewsList } from "./news";
import { NextupList } from "./nextup";
import { Recommended } from "./recommended";
import { VerticalRecommended } from "./vertical";
import { WatchlistList } from "./watchlist";
export const HomePage = () => {
const genres = shuffle(Object.values(Genre.enum));
@ -50,7 +50,7 @@ export const HomePage = () => {
)}
Loader={Header.Loader}
/>
<WatchlistList />
<NextupList />
<NewsList />
{genres
.filter((_, i) => i < 2)
@ -64,10 +64,11 @@ export const HomePage = () => {
<GenreGrid key={x} genre={x} />
))}
<VerticalRecommended />
{/*
TODO: Lazy load those items
{randomItems.filter((_, i) => i >= 6).map((x) => <GenreGrid key={x} genre={x} />)}
*/}
{genres
.filter((_, i) => i >= 6)
.map((x) => (
<GenreGrid key={x} genre={x} />
))}
</Animated.ScrollView>
</>
);
@ -75,7 +76,7 @@ export const HomePage = () => {
HomePage.queries = (randomItems: Genre[]) => [
Header.query(),
WatchlistList.query(),
NextupList.query(),
NewsList.query(),
...randomItems.filter((_, i) => i < 6).map((x) => GenreGrid.query(x)),
Recommended.query(),

View File

@ -15,20 +15,18 @@ export const NewsList = () => {
query={NewsList.query()}
layout={{ ...EntryBox.layout, layout: "horizontal" }}
Empty={<EmptyView message={t("home.none")} />}
Render={({ item }) => {
return (
<EntryBox
kind={item.kind}
slug={item.slug}
serieSlug={item.show!.slug}
name={`${item.show!.name} ${entryDisplayNumber(item)}`}
description={item.name}
thumbnail={item.thumbnail}
href={item.href ?? "#"}
watchedPercent={item.progress.percent}
/>
);
}}
Render={({ item }) => (
<EntryBox
kind={item.kind}
slug={item.slug}
serieSlug={item.show!.slug}
name={`${item.show!.name} ${entryDisplayNumber(item)}`}
description={item.name}
thumbnail={item.thumbnail ?? item.show!.thumbnail}
href={item.href ?? "#"}
watchedPercent={item.progress.percent}
/>
)}
Loader={EntryBox.Loader}
/>
</>

View File

@ -0,0 +1,66 @@
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { EntryBox, entryDisplayNumber } from "~/components/entries";
import { ItemGrid } from "~/components/items";
import { Entry } from "~/models";
import { Button, Link, P } from "~/primitives";
import { useAccount } from "~/providers/account-context";
import { InfiniteFetch, type QueryIdentifier } from "~/query";
import { EmptyView } from "~/ui/empty-view";
import { Header } from "./genre";
export const NextupList = () => {
const { t } = useTranslation();
const account = useAccount();
if (!account) {
return (
<>
<Header title={t("home.watchlist")} />
<View className="items-center justify-center">
<P>{t("home.watchlistLogin")}</P>
<Button
as={Link}
href={"/login"}
text={t("login.login")}
className="m-4 min-w-md"
/>
</View>
</>
);
}
return (
<>
<Header title={t("home.watchlist")} />
<InfiniteFetch
query={NextupList.query()}
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
Empty={<EmptyView message={t("home.none")} />}
Render={({ item }) => (
<EntryBox
kind={item.kind}
slug={item.slug}
serieSlug={item.show!.slug}
name={`${item.show!.name} ${entryDisplayNumber(item)}`}
description={item.name}
thumbnail={item.thumbnail ?? item.show!.thumbnail}
href={item.href ?? "#"}
watchedPercent={item.progress.percent}
/>
)}
Loader={EntryBox.Loader}
/>
</>
);
};
NextupList.query = (): QueryIdentifier<Entry> => ({
parser: Entry,
infinite: true,
path: ["api", "profiles", "me", "nextup"],
params: {
limit: 10,
with: ["nextEntry"],
},
});

View File

@ -1,80 +0,0 @@
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { EntryBox, entryDisplayNumber } from "~/components/entries";
import { ItemGrid, itemMap } from "~/components/items";
import { Show } from "~/models";
import { Button, Link, P } from "~/primitives";
import { useAccount } from "~/providers/account-context";
import { InfiniteFetch, type QueryIdentifier } from "~/query";
import { EmptyView } from "~/ui/empty-view";
import { Header } from "./genre";
export const WatchlistList = () => {
const { t } = useTranslation();
const account = useAccount();
if (!account) {
return (
<>
<Header title={t("home.watchlist")} />
<View className="items-center justify-center">
<P>{t("home.watchlistLogin")}</P>
<Button
as={Link}
href={"/login"}
text={t("login.login")}
className="m-4 min-w-md"
/>
</View>
</>
);
}
return (
<>
<Header title={t("home.watchlist")} />
<InfiniteFetch
query={WatchlistList.query()}
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
getItemType={(x, i) =>
(x?.kind === "serie" && x.nextEntry) || (!x && i % 2)
? "episode"
: "item"
}
getItemSizeMult={(_, __, kind) => (kind === "episode" ? 2 : 1)}
Empty={<EmptyView message={t("home.none")} />}
Render={({ item }) => {
const entry = item.kind === "serie" ? item.nextEntry : null;
if (entry) {
return (
<EntryBox
kind={entry.kind}
slug={entry.slug}
serieSlug={item.slug}
name={`${item.name} ${entryDisplayNumber(entry)}`}
description={entry.name}
thumbnail={entry.thumbnail ?? item.thumbnail}
href={entry.href ?? "#"}
watchedPercent={entry.progress.percent}
/>
);
}
return <ItemGrid {...itemMap(item)} horizontal />;
}}
Loader={({ index }) =>
index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader horizontal />
}
/>
</>
);
};
WatchlistList.query = (): QueryIdentifier<Show> => ({
parser: Show,
infinite: true,
path: ["api", "profiles", "me", "watchlist"],
params: {
limit: 10,
with: ["nextEntry"],
},
});

View File

@ -45,6 +45,24 @@ import { useAccount, useAccounts } from "~/providers/account-context";
import { logout } from "~/ui/login/logic";
import { cn } from "~/utils";
export const NavbarLeft = () => {
const { t } = useTranslation();
if (Platform.OS !== "web") return <NavbarTitle />;
return (
<View className="flex-row items-center gap-2">
<NavbarTitle />
<A
href="/browse"
className="font-headers text-lg text-slate-200 uppercase dark:text-slate-200"
>
{t("navbar.browse")}
</A>
</View>
);
};
export const NavbarTitle = ({
className,
...props
@ -64,68 +82,24 @@ export const NavbarTitle = ({
);
};
const getDisplayUrl = (url: string) => {
url = url.replace(/\/api$/, "");
url = url.replace(/https?:\/\//, "");
return url;
};
export const NavbarProfile = () => {
export const NavbarRight = () => {
const { t } = useTranslation();
const account = useAccount();
const accounts = useAccounts();
const isAdmin = false; //useHasPermission(AdminPage.requiredPermissions);
return (
<Menu
Trigger={Avatar<PressableProps>}
as={PressableFeedback}
src={account?.logo}
placeholder={account?.username}
alt={t("navbar.login")}
className="m-2"
{...tooltip(account?.username ?? t("navbar.login"))}
>
{accounts?.map((x) => (
<Menu.Item
key={x.id}
label={
Platform.OS === "web"
? x.username
: `${x.username} - ${getDisplayUrl(x.apiUrl)}`
}
left={
<Avatar placeholder={x.username} src={x.logo} className="mx-2" />
}
selected={x.selected}
onSelect={() => x.select()}
<View className="shrink flex-row items-center">
<SearchBar />
{isAdmin && (
<IconButton
icon={Admin}
as={Link}
href={"/admin"}
iconClassName="fill-slate-200 dark:fill-slate-200"
{...tooltip(t("navbar.admin"))}
/>
))}
{accounts.length > 0 && <HR />}
<Menu.Item label={t("misc.settings")} icon={Settings} href="/settings" />
{!account ? (
<>
<Menu.Item label={t("login.login")} icon={Login} href="/login" />
<Menu.Item
label={t("login.register")}
icon={Register}
href="/register"
/>
</>
) : (
<>
<Menu.Item
label={t("login.add-account")}
icon={Login}
href="/login"
/>
<Menu.Item
label={t("login.logout")}
icon={Logout}
onSelect={logout}
/>
</>
)}
</Menu>
<NavbarProfile />
</View>
);
};
@ -217,24 +191,68 @@ const SearchBar = () => {
);
};
export const NavbarRight = () => {
const getDisplayUrl = (url: string) => {
url = url.replace(/\/api$/, "");
url = url.replace(/https?:\/\//, "");
return url;
};
export const NavbarProfile = () => {
const { t } = useTranslation();
const isAdmin = false; //useHasPermission(AdminPage.requiredPermissions);
const account = useAccount();
const accounts = useAccounts();
return (
<View className="shrink flex-row items-center">
<SearchBar />
{isAdmin && (
<IconButton
icon={Admin}
as={Link}
href={"/admin"}
iconClassName="fill-slate-200 dark:fill-slate-200"
{...tooltip(t("navbar.admin"))}
<Menu
Trigger={Avatar<PressableProps>}
as={PressableFeedback}
src={account?.logo}
placeholder={account?.username}
alt={t("navbar.login")}
className="m-2"
{...tooltip(account?.username ?? t("navbar.login"))}
>
{accounts?.map((x) => (
<Menu.Item
key={x.id}
label={
Platform.OS === "web"
? x.username
: `${x.username} - ${getDisplayUrl(x.apiUrl)}`
}
left={
<Avatar placeholder={x.username} src={x.logo} className="mx-2" />
}
selected={x.selected}
onSelect={() => x.select()}
/>
))}
{accounts.length > 0 && <HR />}
<Menu.Item label={t("misc.settings")} icon={Settings} href="/settings" />
{!account ? (
<>
<Menu.Item label={t("login.login")} icon={Login} href="/login" />
<Menu.Item
label={t("login.register")}
icon={Register}
href="/register"
/>
</>
) : (
<>
<Menu.Item
label={t("login.add-account")}
icon={Login}
href="/login"
/>
<Menu.Item
label={t("login.logout")}
icon={Logout}
onSelect={logout}
/>
</>
)}
<NavbarProfile />
</View>
</Menu>
);
};

View File

@ -72,8 +72,6 @@ export const Controls = ({
previous={previous}
next={next}
setMenu={setMenu}
// Fixed is used because firefox android make the hover disappear under the navigation bar in absolute
// position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
className="absolute bottom-0 w-full bg-slate-900/50 px-safe pt-safe"
{...hoverControls}
/>

View File

@ -35,12 +35,12 @@ export const MiddleControls = ({
"mx-6 bg-gray-800/70",
!previous && "pointer-events-none opacity-0",
)}
iconClassName="h-16 w-16"
iconClassName="h-16 w-16 fill-slate-200 dark:fill-slate-200"
/>
<PlayButton
player={player}
className={cn("mx-6 bg-gray-800/50")}
iconClassName="h-24 w-24"
iconClassName="h-24 w-24 fill-slate-200 dark:fill-slate-200"
/>
<IconButton
icon={SkipNext}
@ -51,7 +51,7 @@ export const MiddleControls = ({
"mx-6 bg-gray-800/70",
!next && "pointer-events-none opacity-0",
)}
iconClassName="h-16 w-16"
iconClassName="h-16 w-16 fill-slate-200 dark:fill-slate-200"
/>
</View>
);

View File

@ -2,9 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": [
"./src/*"
]
"~/*": ["./src/*"]
},
"strict": true,
"rootDir": ".",
@ -17,21 +15,10 @@
"jsx": "react-jsx",
"forceConsistentCasingInFileNames": true,
"allowImportingTsExtensions": true,
"types": [
"node",
"react"
],
"lib": [
"dom",
"esnext"
]
"types": ["node", "react"],
"lib": ["dom", "esnext"]
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
],
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"],
"exclude": [
"node_modules",
".expo",