feat: Implemented public plugin system accessible from the store.
feat: Implemented external ryujinx integration plugin refactor: moved sdk types and schemas to own workspace package fix: Fixed emulator launch with no game
This commit is contained in:
parent
9051834ace
commit
38cb752552
124 changed files with 1918 additions and 1067 deletions
128
bun.lock
128
bun.lock
|
|
@ -7,53 +7,54 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
"@auth/core": "^0.34.3",
|
"@auth/core": "^0.34.3",
|
||||||
"@elysiajs/cors": "^1.4.1",
|
"@elysiajs/cors": "^1.4.2",
|
||||||
"@elysiajs/eden": "^1.4.6",
|
"@elysiajs/eden": "^1.4.9",
|
||||||
"@jimp/wasm-webp": "^1.6.0",
|
"@jimp/wasm-webp": "^1.6.1",
|
||||||
"@phalcode/ts-igdb-client": "^1.0.26",
|
"@phalcode/ts-igdb-client": "^1.0.26",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"conf": "^15.0.2",
|
"conf": "^15.1.0",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.2",
|
||||||
"elysia": "^1.4.22",
|
"elysia": "^1.4.28",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.5",
|
||||||
"get-folder-size": "^5.0.0",
|
"get-folder-size": "^5.0.0",
|
||||||
"ini": "^6.0.0",
|
"ini": "^6.0.0",
|
||||||
"jimp": "^1.6.0",
|
"jimp": "^1.6.1",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"node-7z": "^3.0.0",
|
"node-7z": "^3.0.0",
|
||||||
"node-disk-info": "^1.3.0",
|
"node-disk-info": "^1.3.0",
|
||||||
"node-downloader-helper": "^2.1.10",
|
"node-downloader-helper": "^2.1.11",
|
||||||
"node-stream-zip": "^1.15.0",
|
"node-stream-zip": "^1.15.0",
|
||||||
"node-unrar-js": "^2.0.2",
|
"node-unrar-js": "^2.0.2",
|
||||||
|
"npm-check-updates": "^22.1.1",
|
||||||
"open": "^11.0.0",
|
"open": "^11.0.0",
|
||||||
"p-queue": "^9.1.2",
|
"p-queue": "^9.2.0",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"slugify": "^1.6.9",
|
"slugify": "^1.6.9",
|
||||||
"smol-toml": "^1.6.1",
|
"smol-toml": "^1.6.1",
|
||||||
"systeminformation": "^5.31.5",
|
"systeminformation": "^5.31.5",
|
||||||
"tapable": "^2.3.0",
|
"tapable": "^2.3.3",
|
||||||
"tough-cookie": "^6.0.0",
|
"tough-cookie": "^6.0.1",
|
||||||
"tough-cookie-file-store": "^3.3.0",
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
"unzip-stream": "^0.3.4",
|
"unzip-stream": "^0.3.4",
|
||||||
"webview-bun": "^2.4.0",
|
"webview-bun": "^2.4.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.4.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ap0nia/eden": "^1.0.0-next.22",
|
"@ap0nia/eden": "^1.6.1",
|
||||||
"@ap0nia/eden-tanstack-query": "^1.0.0-next.22",
|
"@ap0nia/eden-tanstack-query": "^1.0.0-next.22",
|
||||||
"@emulatorjs/emulatorjs": "^4.2.3",
|
"@emulatorjs/emulatorjs": "^4.2.3",
|
||||||
"@hey-api/openapi-ts": "^0.91.0",
|
"@hey-api/openapi-ts": "^0.91.1",
|
||||||
"@noriginmedia/norigin-spatial-navigation": "^3.1.0",
|
"@noriginmedia/norigin-spatial-navigation": "^3.1.0",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@tanstack/react-form": "^1.28.0",
|
"@tanstack/react-form": "^1.29.1",
|
||||||
"@tanstack/react-query": "^5.90.20",
|
"@tanstack/react-query": "^5.100.9",
|
||||||
"@tanstack/react-query-devtools": "^5.91.3",
|
"@tanstack/react-query-devtools": "^5.100.9",
|
||||||
"@tanstack/react-router": "^1.157.16",
|
"@tanstack/react-router": "^1.169.2",
|
||||||
"@tanstack/react-router-devtools": "^1.154.12",
|
"@tanstack/react-router-devtools": "^1.166.13",
|
||||||
"@tanstack/react-router-ssr-query": "^1.157.17",
|
"@tanstack/react-router-ssr-query": "^1.166.12",
|
||||||
"@tanstack/router-plugin": "^1.157.16",
|
"@tanstack/router-plugin": "^1.167.35",
|
||||||
"@tanstack/zod-adapter": "^1.162.4",
|
"@tanstack/zod-adapter": "^1.166.9",
|
||||||
"@types/adm-zip": "^0.5.8",
|
"@types/adm-zip": "^0.5.8",
|
||||||
"@types/audiosprite": "^0.7.3",
|
"@types/audiosprite": "^0.7.3",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
|
@ -64,11 +65,11 @@
|
||||||
"@types/mustache": "^4.2.6",
|
"@types/mustache": "^4.2.6",
|
||||||
"@types/node-7z": "^2.1.11",
|
"@types/node-7z": "^2.1.11",
|
||||||
"@types/rclone.js": "^0.6.3",
|
"@types/rclone.js": "^0.6.3",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/unzip-stream": "^0.3.4",
|
"@types/unzip-stream": "^0.3.4",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.17",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"app-builder-bin": "^5.0.0-alpha.13",
|
"app-builder-bin": "^5.0.0-alpha.13",
|
||||||
"audiosprite": "^0.7.2",
|
"audiosprite": "^0.7.2",
|
||||||
|
|
@ -76,32 +77,71 @@
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"daisyui": "^5.5.14",
|
"daisyui": "^5.5.19",
|
||||||
"drizzle-kit": "^0.31.9",
|
"drizzle-kit": "^0.31.10",
|
||||||
"dts-bundle-generator": "^9.5.1",
|
|
||||||
"eden-tanstack-query": "^0.0.9",
|
"eden-tanstack-query": "^0.0.9",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
"pretty-ms": "^9.3.0",
|
"pretty-ms": "^9.3.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.6",
|
||||||
"react-error-boundary": "^6.1.0",
|
"react-error-boundary": "^6.1.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-qr-code": "^2.0.18",
|
"react-qr-code": "^2.0.21",
|
||||||
"sass-embedded": "^1.97.3",
|
"sass-embedded": "^1.99.0",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.2.4",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.3",
|
||||||
"vite-plugin-svg-icons-ng": "^1.5.2",
|
"vite-plugin-svg-icons-ng": "^1.9.0",
|
||||||
"vite-static-assets-plugin": "^1.2.2",
|
"vite-static-assets-plugin": "^1.2.2",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"zod-to-ts": "^2.0.0",
|
},
|
||||||
|
},
|
||||||
|
"src/packages/gameflow-sdk": {
|
||||||
|
"name": "@simeonradivoev/gameflow-sdk",
|
||||||
|
"version": "1.5.3",
|
||||||
|
"bin": {
|
||||||
|
"gameflow-build": "build.ts",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"7zip-bin": "^5.2.0",
|
||||||
|
"@auth/core": "^0.34.3",
|
||||||
|
"@elysiajs/cors": "^1.4.2",
|
||||||
|
"@elysiajs/eden": "^1.4.9",
|
||||||
|
"@jimp/wasm-webp": "^1.6.1",
|
||||||
|
"@phalcode/ts-igdb-client": "^1.0.26",
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
|
"conf": "^15.1.0",
|
||||||
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"elysia": "^1.4.28",
|
||||||
|
"fs-extra": "^11.3.5",
|
||||||
|
"get-folder-size": "^5.0.0",
|
||||||
|
"ini": "^6.0.0",
|
||||||
|
"jimp": "^1.6.1",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
|
"node-7z": "^3.0.0",
|
||||||
|
"node-disk-info": "^1.3.0",
|
||||||
|
"node-downloader-helper": "^2.1.11",
|
||||||
|
"node-stream-zip": "^1.15.0",
|
||||||
|
"node-unrar-js": "^2.0.2",
|
||||||
|
"open": "^11.0.0",
|
||||||
|
"p-queue": "^9.2.0",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"slugify": "^1.6.9",
|
||||||
|
"smol-toml": "^1.6.1",
|
||||||
|
"systeminformation": "^5.31.5",
|
||||||
|
"tapable": "^2.3.3",
|
||||||
|
"tough-cookie": "^6.0.1",
|
||||||
|
"tough-cookie-file-store": "^3.3.0",
|
||||||
|
"unzip-stream": "^0.3.4",
|
||||||
|
"webview-bun": "^2.4.0",
|
||||||
|
"zod": "^4.4.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -520,6 +560,8 @@
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="],
|
||||||
|
|
||||||
|
"@simeonradivoev/gameflow-sdk": ["@simeonradivoev/gameflow-sdk@workspace:src/packages/gameflow-sdk"],
|
||||||
|
|
||||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
||||||
|
|
||||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
@ -948,8 +990,6 @@
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "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-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
|
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "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-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
|
||||||
|
|
||||||
"dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="],
|
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
||||||
|
|
@ -1394,6 +1434,8 @@
|
||||||
|
|
||||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
|
"npm-check-updates": ["npm-check-updates@22.1.1", "", { "bin": { "npm-check-updates": "build/cli.js", "ncu": "build/cli.js" } }, "sha512-uWSxJW25dy5ZM4SdLsi0VBgPSJlo7u+jARQ6Xql+85YYCoqXU2ZaympAZ6237/oybCq/I4nXddE9S9BTwBfBXA=="],
|
||||||
|
|
||||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
||||||
"nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="],
|
"nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="],
|
||||||
|
|
@ -1884,8 +1926,6 @@
|
||||||
|
|
||||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
"zod-to-ts": ["zod-to-ts@2.0.0", "", { "peerDependencies": { "typescript": "^5.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-aHsUgIl+CQutKAxtRNeZslLCLXoeuSq+j5HU7q3kvi/c2KIAo6q4YjT7/lwFfACxLB923ELHYMkHmlxiqFy4lw=="],
|
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
"@ap0nia/eden/elysia": ["elysia@1.2.15", "", { "dependencies": { "@sinclair/typebox": "^0.34.15", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-/oUSNb83jIWAGi6uSmbQ7Uy0RSJ9NimbVToSLnYS8jjsGId3zgdHqprsdf4rIMInOmEM8skjsFhZ4x8C5AB6+w=="],
|
"@ap0nia/eden/elysia": ["elysia@1.2.15", "", { "dependencies": { "@sinclair/typebox": "^0.34.15", "cookie": "^1.0.2", "memoirist": "^0.3.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-/oUSNb83jIWAGi6uSmbQ7Uy0RSJ9NimbVToSLnYS8jjsGId3zgdHqprsdf4rIMInOmEM8skjsFhZ4x8C5AB6+w=="],
|
||||||
|
|
|
||||||
14
package.json
14
package.json
|
|
@ -18,6 +18,9 @@
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.3.9",
|
"packageManager": "bun@1.3.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"workspaces": [
|
||||||
|
"./src/packages/gameflow-sdk"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development bun run build:vite && conc 'bun run ./scripts/dev.ts'",
|
"dev": "NODE_ENV=development bun run build:vite && conc 'bun run ./scripts/dev.ts'",
|
||||||
"dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run ./scripts/dev.ts'",
|
"dev:hmr": "PUBLIC_ACCESS=true conc -k 'bun run hmr' 'bun run ./scripts/dev.ts'",
|
||||||
|
|
@ -27,6 +30,7 @@
|
||||||
"build:prod:vite": "NODE_ENV=production bun run build:vite",
|
"build:prod:vite": "NODE_ENV=production bun run build:vite",
|
||||||
"build:dev:vite": "NODE_ENV=development bun run build:vite",
|
"build:dev:vite": "NODE_ENV=development bun run build:vite",
|
||||||
"build": "bun run build:vite && bun run ./scripts/package-bun.ts",
|
"build": "bun run build:vite && bun run ./scripts/package-bun.ts",
|
||||||
|
"build:non-compiled": "bun run build:vite && NON_COMPILED=true bun run ./scripts/package-bun.ts",
|
||||||
"build:prod": "NODE_ENV=production bun run build",
|
"build:prod": "NODE_ENV=production bun run build",
|
||||||
"build:linux": "TARGET=bun-linux-x64 bun run build",
|
"build:linux": "TARGET=bun-linux-x64 bun run build",
|
||||||
"openapi-ts": "bun run ./scripts/romm/openapi-ts.ts",
|
"openapi-ts": "bun run ./scripts/romm/openapi-ts.ts",
|
||||||
|
|
@ -49,8 +53,7 @@
|
||||||
"download:nwjs": "bun scripts/download-nw.ts",
|
"download:nwjs": "bun scripts/download-nw.ts",
|
||||||
"build:audiosprites": "bun ./scripts/generate-audio-sprites.ts",
|
"build:audiosprites": "bun ./scripts/generate-audio-sprites.ts",
|
||||||
"tsc": "tsc --noEmit",
|
"tsc": "tsc --noEmit",
|
||||||
"build:sdk": "bun ./scripts/build-sdk.ts",
|
"publish:sdk": "bun publish --cwd ./src/packages/gameflow-sdk/ --access public"
|
||||||
"publish:sdk": "bun build:sdk && bun publish --cwd ./dist-sdk/ --access public"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
|
|
@ -73,6 +76,7 @@
|
||||||
"node-downloader-helper": "^2.1.11",
|
"node-downloader-helper": "^2.1.11",
|
||||||
"node-stream-zip": "^1.15.0",
|
"node-stream-zip": "^1.15.0",
|
||||||
"node-unrar-js": "^2.0.2",
|
"node-unrar-js": "^2.0.2",
|
||||||
|
"npm-check-updates": "^22.1.1",
|
||||||
"open": "^11.0.0",
|
"open": "^11.0.0",
|
||||||
"p-queue": "^9.2.0",
|
"p-queue": "^9.2.0",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
|
|
@ -126,7 +130,6 @@
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"dts-bundle-generator": "^9.5.1",
|
|
||||||
"eden-tanstack-query": "^0.0.9",
|
"eden-tanstack-query": "^0.0.9",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
|
|
@ -148,7 +151,6 @@
|
||||||
"vite": "^7.3.3",
|
"vite": "^7.3.3",
|
||||||
"vite-plugin-svg-icons-ng": "^1.9.0",
|
"vite-plugin-svg-icons-ng": "^1.9.0",
|
||||||
"vite-static-assets-plugin": "^1.2.2",
|
"vite-static-assets-plugin": "^1.2.2",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1"
|
||||||
"zod-to-ts": "^2.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import path from 'node:path';
|
|
||||||
import appPkg from '../package.json';
|
|
||||||
import sdkTsConfig from './sdk/sdk.tsconfig.json';
|
|
||||||
import sdkPackage from './sdk/package.json';
|
|
||||||
import { emptyDir } from 'fs-extra';
|
|
||||||
import { generateDtsBundle } from 'dts-bundle-generator';
|
|
||||||
import { zodToTs, createAuxiliaryTypeStore, printNode } from 'zod-to-ts';
|
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
|
|
||||||
import * as types from './sdk/sdk';
|
|
||||||
|
|
||||||
const zodTypeRegex = /z\.infer<typeof? ([\w\d]+)>/gm;
|
|
||||||
|
|
||||||
async function generateApiDeclarations ()
|
|
||||||
{
|
|
||||||
const tmpConfigPath = "./scripts/sdk/sdk.tsconfig.json";
|
|
||||||
const outDir = path.join(path.dirname(tmpConfigPath), sdkTsConfig.compilerOptions.outDir);
|
|
||||||
await emptyDir(outDir);
|
|
||||||
|
|
||||||
const results = generateDtsBundle([{
|
|
||||||
filePath: './scripts/sdk/sdk.ts',
|
|
||||||
output: {
|
|
||||||
inlineDeclareGlobals: true,
|
|
||||||
sortNodes: true,
|
|
||||||
}
|
|
||||||
},], { preferredConfigPath: './scripts/sdk/sdk.tsconfig.json' });
|
|
||||||
|
|
||||||
const auxiliaryTypeStore = createAuxiliaryTypeStore();
|
|
||||||
|
|
||||||
await Bun.write('./dist-sdk/index.d.ts', results.map(r =>
|
|
||||||
{
|
|
||||||
const result = r;
|
|
||||||
return result.replaceAll(zodTypeRegex, (e, name) =>
|
|
||||||
{
|
|
||||||
const schema = types[name as keyof typeof types];
|
|
||||||
if (schema)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const { node } = zodToTs(schema as any, { auxiliaryTypeStore, unrepresentable: 'any' });
|
|
||||||
return printNode(node);
|
|
||||||
} catch (error)
|
|
||||||
{
|
|
||||||
console.error(error);
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
const pkg = {
|
|
||||||
...sdkPackage,
|
|
||||||
license: appPkg.license,
|
|
||||||
version: appPkg.version,
|
|
||||||
repository: appPkg.repository,
|
|
||||||
author: appPkg.author,
|
|
||||||
peerDependencies: appPkg.dependencies
|
|
||||||
};
|
|
||||||
await Bun.write(path.join(outDir, 'package.json'), JSON.stringify(pkg, null, 3));
|
|
||||||
await fs.cp('./scripts/sdk/README.md', path.join(outDir, 'README.md'));
|
|
||||||
}
|
|
||||||
|
|
||||||
await generateApiDeclarations();
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@simeonradivoev/gameflow-sdk",
|
|
||||||
"types": "index.d.ts",
|
|
||||||
"description": "plugin SDK for the Gameflow Deck Launcher",
|
|
||||||
"keywords": [
|
|
||||||
"gameflow",
|
|
||||||
"sdk"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { SettingsType } from '@/shared/constants';
|
|
||||||
import Conf from 'conf';
|
|
||||||
import { AppEventMap } from '../../src/bun/types/types';
|
|
||||||
import EventEmitter from "node:events";
|
|
||||||
import { TaskQueue } from '@/bun/api/task-queue';
|
|
||||||
|
|
||||||
export * from '../../src/bun/types/types.schema';
|
|
||||||
export * from '../../src/bun/types/types';
|
|
||||||
export * from '../../src/bun/api/hooks/app';
|
|
||||||
export * from '../../src/shared/constants';
|
|
||||||
export * from '../../src/shared/types';
|
|
||||||
export * from '../../src/shared/utils';
|
|
||||||
|
|
||||||
export declare const config: Conf<SettingsType>;
|
|
||||||
export declare let events: EventEmitter<AppEventMap>;
|
|
||||||
export declare let taskQueue: TaskQueue;
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import { TaskQueue } from "./task-queue";
|
import { TaskQueue, AppEventMap } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
import { CookieJar } from 'tough-cookie';
|
import { CookieJar } from 'tough-cookie';
|
||||||
import FileCookieStore from 'tough-cookie-file-store';
|
import FileCookieStore from 'tough-cookie-file-store';
|
||||||
|
|
@ -8,7 +8,7 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
|
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
import Conf from "conf";
|
import Conf from "conf";
|
||||||
import projectPackage from '~/package.json';
|
import projectPackage from '~/package.json';
|
||||||
import { SettingsSchema, SettingsType } from "@shared/constants";
|
import { SettingsType, SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { client } from "@clients/romm/client.gen";
|
import { client } from "@clients/romm/client.gen";
|
||||||
import * as schema from "@schema/app";
|
import * as schema from "@schema/app";
|
||||||
import cacheSchema from "@schema/cache";
|
import cacheSchema from "@schema/cache";
|
||||||
|
|
@ -24,7 +24,6 @@ import controls from './controls/controls';
|
||||||
import { RunAPIServer } from "./rpc";
|
import { RunAPIServer } from "./rpc";
|
||||||
import { RunBunServer } from "../server";
|
import { RunBunServer } from "../server";
|
||||||
import ReloadPluginsJob from "./jobs/reload-plugins-job";
|
import ReloadPluginsJob from "./jobs/reload-plugins-job";
|
||||||
import { AppEventMap } from "../types/types";
|
|
||||||
|
|
||||||
export let config: Conf<SettingsType>;
|
export let config: Conf<SettingsType>;
|
||||||
export let customEmulators: Conf<Record<string, string>>;
|
export let customEmulators: Conf<Record<string, string>>;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { cache } from "./app";
|
import { cache } from "./app";
|
||||||
import cacheSchema from "@schema/cache";
|
import cacheSchema from "@schema/cache";
|
||||||
import { GithubReleaseSchema } from "@/shared/constants";
|
import { GithubReleaseSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
|
@ -11,7 +11,8 @@ export const CACHE_KEYS = {
|
||||||
STORE_GAME_MANIFEST: 'store-game-manifest'
|
STORE_GAME_MANIFEST: 'store-game-manifest'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const githubRequestQueue = new PQueue({ intervalCap: 10, interval: 1000 * 60 * 10, strict: true });
|
// we aggressively cache github data so burst of calls is fine.
|
||||||
|
export const githubRequestQueue = new PQueue({ intervalCap: 60, interval: 1000 * 60 * 60, strict: true });
|
||||||
|
|
||||||
export async function getOrCached<T> (key: string, getter: (lastValue: T | undefined) => Promise<T>, options?: { expireMs?: number; force?: boolean; }): Promise<T>
|
export async function getOrCached<T> (key: string, getter: (lastValue: T | undefined) => Promise<T>, options?: { expireMs?: number; force?: boolean; }): Promise<T>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import { Drive } from '@/shared/types';
|
import { Drive } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
|
||||||
async function getAccess (path: string)
|
async function getAccess (path: string)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import z from "zod";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { config, events, plugins } from "../app";
|
import { config, events, plugins } from "../app";
|
||||||
import { getLocalGame, updateLocalLastPlayed } from "../games/services/statusService";
|
import { getLocalGame, updateLocalLastPlayed } from "../games/services/statusService";
|
||||||
import { SaveFileChange } from "@/shared/types";
|
import { SaveFileChange } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
// TODO: use the retroarch cores based on ES-DE
|
// TODO: use the retroarch cores based on ES-DE
|
||||||
export const cores: Record<string, string> = {
|
export const cores: Record<string, string> = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Elysia, { status } from "elysia";
|
import Elysia, { status } from "elysia";
|
||||||
import { plugins } from "../app";
|
import { plugins } from "../app";
|
||||||
import { FrontEndCollection } from "@/shared/types";
|
import { FrontEndCollection } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default new Elysia()
|
export default new Elysia()
|
||||||
.get('/collections', async () =>
|
.get('/collections', async () =>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import { and, desc, eq, getTableColumns, inArray, like, sql } from "drizzle-orm"
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import * as schema from "@schema/app";
|
import * as schema from "@schema/app";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { GameListFilterSchema, SERVER_URL } from "@shared/constants";
|
import { SERVER_URL } from "@shared/constants";
|
||||||
|
import { GameListFilterSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { InstallJob } from "../jobs/install-job";
|
import { InstallJob } from "../jobs/install-job";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { convertLocalToFrontend, getLocalGameMatch, getSourceGameDetailed } from "./services/utils";
|
import { convertLocalToFrontend, getLocalGameMatch, getSourceGameDetailed } from "./services/utils";
|
||||||
|
|
@ -22,7 +23,7 @@ import { LaunchGameJob } from "../jobs/launch-game-job";
|
||||||
import { cores } from "../emulatorjs/emulatorjs";
|
import { cores } from "../emulatorjs/emulatorjs";
|
||||||
import { findEmulatorPluginIntegration } from "../store/services/emulatorsService";
|
import { findEmulatorPluginIntegration } from "../store/services/emulatorsService";
|
||||||
import { ImportJob } from "../jobs/import-job";
|
import { ImportJob } from "../jobs/import-job";
|
||||||
import { EmulatorSourceEntryType, EmulatorSystem, FrontEndFilterLists, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailedEmulator, FrontEndGameTypeWithIds, FrontEndId, GameLookup } from "@/shared/types";
|
import { EmulatorSourceEntryType, EmulatorSystem, FrontEndFilterLists, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailedEmulator, FrontEndGameTypeWithIds, FrontEndId, GameLookup } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
// A custom jimp that supports webp
|
// A custom jimp that supports webp
|
||||||
const Jimp = createJimp({
|
const Jimp = createJimp({
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { and, count, eq, getTableColumns, not, notExists, or } from "drizzle-orm
|
||||||
import { config, db, plugins } from "../app";
|
import { config, db, plugins } from "../app";
|
||||||
import * as schema from "@schema/app";
|
import * as schema from "@schema/app";
|
||||||
import { findPlatform } from "./services/utils";
|
import { findPlatform } from "./services/utils";
|
||||||
import { FrontEndPlatformType } from "@/shared/types";
|
import { FrontEndPlatformType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default new Elysia()
|
export default new Elysia()
|
||||||
.get('/platforms', async () =>
|
.get('/platforms', async () =>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { config, taskQueue } from '../../app';
|
||||||
import { LaunchGameJob } from '../../jobs/launch-game-job';
|
import { LaunchGameJob } from '../../jobs/launch-game-job';
|
||||||
import { getStoreEmulatorPackage } from '../../store/services/gamesService';
|
import { getStoreEmulatorPackage } from '../../store/services/gamesService';
|
||||||
import { getOrCachedScoopPackage } from '../../store/services/emulatorsService';
|
import { getOrCachedScoopPackage } from '../../store/services/emulatorsService';
|
||||||
import { CommandEntry, EmulatorSourceEntryType, FrontEndId } from '@/shared/types';
|
import { CommandEntry, EmulatorSourceEntryType, FrontEndId } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
|
||||||
export async function launchCommand (validCommand: CommandEntry, id: FrontEndId, source?: string, sourceId?: string)
|
export async function launchCommand (validCommand: CommandEntry, id: FrontEndId, source?: string, sourceId?: string)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@ import z from "zod";
|
||||||
import { InstallJob, InstallJobStates } from "../../jobs/install-job";
|
import { InstallJob, InstallJobStates } from "../../jobs/install-job";
|
||||||
import { LaunchGameJob } from "../../jobs/launch-game-job";
|
import { LaunchGameJob } from "../../jobs/launch-game-job";
|
||||||
import * as appSchema from "@schema/app";
|
import * as appSchema from "@schema/app";
|
||||||
import { DownloadSourceSchema, RPC_URL } from "@/shared/constants";
|
import { RPC_URL } from "@/shared/constants";
|
||||||
|
import { DownloadSourceSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { host } from "@/bun/utils/host";
|
import { host } from "@/bun/utils/host";
|
||||||
import { CommandEntry, FrontEndId, GameLookup, GameStatusType, LocalDownloadFileEntry } from "@/shared/types";
|
import { CommandEntry, FrontEndId, GameLookup, GameStatusType, LocalDownloadFileEntry } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export class CommandSearchError extends Error
|
export class CommandSearchError extends Error
|
||||||
{
|
{
|
||||||
|
|
@ -115,11 +116,15 @@ export async function update (source: string, id: string)
|
||||||
const paths_screenshots: string[] = [...sourceGame.paths_screenshots.map(s => `${RPC_URL(host)}${s}`)];
|
const paths_screenshots: string[] = [...sourceGame.paths_screenshots.map(s => `${RPC_URL(host)}${s}`)];
|
||||||
if (paths_screenshots.length <= 0 && sourceGame.igdb_id)
|
if (paths_screenshots.length <= 0 && sourceGame.igdb_id)
|
||||||
{
|
{
|
||||||
const matches: GameLookup[] = [];
|
const matches = new Map<string, GameLookup[]>();
|
||||||
await plugins.hooks.games.gameLookup.promise({ source: 'igdb', id: String(sourceGame.igdb_id), matches });
|
await plugins.hooks.games.gameLookup.promise(matches, { source: 'igdb', id: String(sourceGame.igdb_id) });
|
||||||
if (matches.length > 0)
|
if (matches.size > 0)
|
||||||
{
|
{
|
||||||
paths_screenshots.push(...matches[0].screenshotUrls);
|
const firstMatches = matches.values().next().value;
|
||||||
|
if (firstMatches && firstMatches.length > 0)
|
||||||
|
{
|
||||||
|
paths_screenshots.push(...firstMatches[0].screenshotUrls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,7 +249,31 @@ export async function getValidLaunchCommandsForGame (source: string, id: string)
|
||||||
commands: commands.filter(c => c.valid),
|
commands: commands.filter(c => c.valid),
|
||||||
gameId: { id: String(localGame.id), source: 'local' },
|
gameId: { id: String(localGame.id), source: 'local' },
|
||||||
source: localGame.source ?? source,
|
source: localGame.source ?? source,
|
||||||
sourceId: String(localGame.source_id) ?? id,
|
sourceId: localGame.source_id ? String(localGame.source_id) : id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${Array.from(new Set(commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator))).join(', ')}`);
|
||||||
|
}
|
||||||
|
} else if (source === 'emulator')
|
||||||
|
{
|
||||||
|
const commands = await plugins.hooks.games.buildLaunchCommands.promise({
|
||||||
|
source,
|
||||||
|
sourceId: id,
|
||||||
|
id: { source: source, id: id },
|
||||||
|
systemSlug: "",
|
||||||
|
gamePath: null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (commands instanceof Error || !commands) return commands;
|
||||||
|
|
||||||
|
const validCommand = commands.find(c => c.valid);
|
||||||
|
if (validCommand)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
commands: commands.filter(c => c.valid),
|
||||||
|
gameId: { id, source }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { RPC_URL } from "@shared/constants";
|
||||||
import { hashFile } from "@/bun/utils";
|
import { hashFile } from "@/bun/utils";
|
||||||
import { host } from "@/bun/utils/host";
|
import { host } from "@/bun/utils/host";
|
||||||
import * as emulatorSchema from "@schema/emulators";
|
import * as emulatorSchema from "@schema/emulators";
|
||||||
import { DownloadFileEntry, FrontEndGameType, FrontEndGameTypeDetailed, GameLookup, LocalDownloadFileEntry, LocalGameMetadata } from "@/shared/types";
|
import { DownloadFileEntry, FrontEndGameType, FrontEndGameTypeDetailed, GameLookup, LocalDownloadFileEntry, LocalGameMetadata } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export async function calculateSize (installPath: string | null)
|
export async function calculateSize (installPath: string | null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "../../../packages/gameflow-sdk/task-queue";
|
||||||
import { config, plugins } from "../app";
|
import { config, plugins } from "../app";
|
||||||
import { simulateProgress } from "@/bun/utils";
|
import { simulateProgress } from "@/bun/utils";
|
||||||
import { Downloader } from "@/bun/utils/downloader";
|
import { Downloader } from "@/bun/utils/downloader";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { EmulatorPackageType } from "@/shared/constants";
|
import { EmulatorPackageType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { getStoreEmulatorPackage } from "../store/services/gamesService";
|
import { getStoreEmulatorPackage } from "../store/services/gamesService";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "../../../packages/gameflow-sdk/task-queue";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { config, plugins } from "../app";
|
import { config, plugins } from "../app";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
@ -12,7 +12,7 @@ import { simulateProgress } from "@/bun/utils";
|
||||||
import { path7za } from "7zip-bin";
|
import { path7za } from "7zip-bin";
|
||||||
import { getEmulatorDownload, getEmulatorPath } from "../store/services/emulatorsService";
|
import { getEmulatorDownload, getEmulatorPath } from "../store/services/emulatorsService";
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import { EmulatorSourceEntryType } from "@/shared/types";
|
import { EmulatorSourceEntryType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
type EmulatorDownloadStates = "download" | "extract";
|
type EmulatorDownloadStates = "download" | "extract";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { eq, or } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
import { db, plugins } from "../app";
|
import { db, plugins } from "../app";
|
||||||
import { createLocalGame } from "../games/services/utils";
|
import { createLocalGame } from "../games/services/utils";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "../../../packages/gameflow-sdk/task-queue";
|
||||||
import * as schema from "@schema/app";
|
import * as schema from "@schema/app";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { GameLookup } from "@/shared/types";
|
import { GameLookup } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export class ImportJob implements IJob<z.infer<typeof ImportJob.dataSchema>, string>
|
export class ImportJob implements IJob<z.infer<typeof ImportJob.dataSchema>, string>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "../../../packages/gameflow-sdk/task-queue";
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { config, events, plugins } from "../app";
|
import { config, events, plugins } from "../app";
|
||||||
|
|
@ -11,7 +11,7 @@ import { ensureDir, move } from "fs-extra";
|
||||||
import { path7za } from "7zip-bin";
|
import { path7za } from "7zip-bin";
|
||||||
import StreamZip from 'node-stream-zip';
|
import StreamZip from 'node-stream-zip';
|
||||||
import { which } from "bun";
|
import { which } from "bun";
|
||||||
import { DownloadInfo } from "@/shared/types";
|
import { DownloadInfo } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
interface JobConfig
|
interface JobConfig
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import TwitchLoginJob from "./twitch-login-job";
|
||||||
import UpdateStoreJob from "./update-store";
|
import UpdateStoreJob from "./update-store";
|
||||||
import { EmulatorDownloadJob } from "./emulator-download-job";
|
import { EmulatorDownloadJob } from "./emulator-download-job";
|
||||||
import { getErrorMessage } from "@/bun/utils";
|
import { getErrorMessage } from "@/bun/utils";
|
||||||
import { IJob } from "../task-queue";
|
import { IJob } from "../../../packages/gameflow-sdk/task-queue";
|
||||||
import { LaunchGameJob } from "./launch-game-job";
|
import { LaunchGameJob } from "./launch-game-job";
|
||||||
import { BiosDownloadJob } from "./bios-download-job";
|
import { BiosDownloadJob } from "./bios-download-job";
|
||||||
import { InstallJob } from "./install-job";
|
import { InstallJob } from "./install-job";
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "../../../packages/gameflow-sdk/task-queue";
|
||||||
import { ActiveGameSchema, ActiveGameType } from "@/bun/types/types.schema";
|
import { ActiveGameSchema, ActiveGameType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { config, db, events, plugins } from "../app";
|
import { config, db, events, plugins } from "../app";
|
||||||
import * as appSchema from "@schema/app";
|
import * as appSchema from "@schema/app";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { updateLocalLastPlayed } from "../games/services/statusService";
|
import { updateLocalLastPlayed } from "../games/services/statusService";
|
||||||
import { getErrorMessage } from "@/bun/utils";
|
import { getErrorMessage } from "@/bun/utils";
|
||||||
import { CommandEntry, FrontEndId, SaveSlots } from "@/shared/types";
|
import { CommandEntry, FrontEndId, SaveSlots } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, string>
|
export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSchema>, string>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Elysia, { status } from "elysia";
|
import Elysia, { status } from "elysia";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "../../../packages/gameflow-sdk/task-queue";
|
||||||
import { LOGIN_PORT, SERVER_URL } from "@/shared/constants";
|
import { LOGIN_PORT, SERVER_URL } from "@/shared/constants";
|
||||||
import { host, localIp } from "@/bun/utils/host";
|
import { host, localIp } from "@/bun/utils/host";
|
||||||
import cors from "@elysiajs/cors";
|
import cors from "@elysiajs/cors";
|
||||||
|
|
|
||||||
62
src/bun/api/jobs/plugin-operation-job.ts
Normal file
62
src/bun/api/jobs/plugin-operation-job.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import z from "zod";
|
||||||
|
import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk";
|
||||||
|
import { plugins } from "../app";
|
||||||
|
import { canUninstall, runBunPackageCommand } from "../plugins/services";
|
||||||
|
import { getPlugin, registerPlugin, unregisterPlugin } from "../plugins/register-plugins";
|
||||||
|
import { PluginRegistry } from "@/shared/constants";
|
||||||
|
|
||||||
|
export default class PluginOperationJob implements IJob<never, string>
|
||||||
|
{
|
||||||
|
static id = "plugin-operation-job" as const;
|
||||||
|
static dataSchema = z.never();
|
||||||
|
group = "plugin-operations";
|
||||||
|
operation: "add" | "update" | "remove";
|
||||||
|
plugin: string;
|
||||||
|
|
||||||
|
constructor(operation: "add" | "update" | "remove", plugin: string)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.operation = operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start (context: JobContext<IJob<never, string>, never, string>)
|
||||||
|
{
|
||||||
|
switch (this.operation)
|
||||||
|
{
|
||||||
|
case "add":
|
||||||
|
//TODO: find the latest compatible version with the current sdk version
|
||||||
|
const addResponse = await runBunPackageCommand(["add", this.plugin, '--omit', 'peer', "--registry", PluginRegistry]);
|
||||||
|
console.log(addResponse);
|
||||||
|
const addPlugin = await getPlugin(this.plugin, plugins);
|
||||||
|
if (!addPlugin) throw new Error(`${this.plugin} Not Found`);
|
||||||
|
await registerPlugin(addPlugin, 'store', plugins);
|
||||||
|
break;
|
||||||
|
case "update":
|
||||||
|
const existingPlugin = plugins.plugins[this.plugin];
|
||||||
|
if (!existingPlugin) throw new Error(`${this.plugin} Not Found`);
|
||||||
|
if (!existingPlugin.update?.new) throw new Error(`No Update Found`);
|
||||||
|
let updatePlugin = await getPlugin(this.plugin, plugins);
|
||||||
|
if (!updatePlugin) throw new Error(`${this.plugin} Not Found`);
|
||||||
|
await unregisterPlugin(this.plugin, plugins);
|
||||||
|
const updateResponse = await runBunPackageCommand(["update", `${this.plugin}@${existingPlugin.update?.new}`, '--omit', 'peer', "--registry", PluginRegistry, '--latest']);
|
||||||
|
console.log(updateResponse);
|
||||||
|
updatePlugin = await getPlugin(this.plugin, plugins);
|
||||||
|
if (!updatePlugin) throw new Error(`Something Went Wrong during update. Missing Plugin: ${this.plugin}`);
|
||||||
|
await registerPlugin(updatePlugin, existingPlugin.source, plugins);
|
||||||
|
break;
|
||||||
|
case "remove":
|
||||||
|
const removePlugin = plugins.plugins[this.plugin];
|
||||||
|
if (!removePlugin) throw new Error(`${this.plugin} Not Found`);
|
||||||
|
if (!canUninstall(removePlugin.description, removePlugin.source))
|
||||||
|
{
|
||||||
|
throw new Error("Uninstall Not Allowed");
|
||||||
|
}
|
||||||
|
const response = await runBunPackageCommand(['remove', this.plugin, "--registry", PluginRegistry, '--omit', 'peer']);
|
||||||
|
console.log(response);
|
||||||
|
await unregisterPlugin(this.plugin, plugins);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { plugins } from "../app";
|
import { plugins } from "../app";
|
||||||
|
|
||||||
export default class ReloadPluginsJob implements IJob<never, string>
|
export default class ReloadPluginsJob implements IJob<never, string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { events } from "../app";
|
import { events } from "../app";
|
||||||
import { Downloader } from "@/bun/utils/downloader";
|
import { Downloader } from "@/bun/utils/downloader";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk";
|
||||||
import secrets from "../secrets";
|
import secrets from "../secrets";
|
||||||
import open from "open";
|
import open from "open";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,57 @@
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
import { IJob, JobContext } from "../task-queue";
|
import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { getStoreRootFolder } from "../store/services/gamesService";
|
import { getStoreRootFolder } from "../store/services/gamesService";
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import { runBunPackageCommand } from "../plugins/services";
|
||||||
|
import { PluginRegistry } from "@/shared/constants";
|
||||||
|
import path from "node:path";
|
||||||
|
import sdkPkg from '@simeonradivoev/gameflow-sdk/package.json';
|
||||||
|
|
||||||
export default class UpdateStoreJob implements IJob<never, never>
|
export default class UpdateStoreJob implements IJob<never, string>
|
||||||
{
|
{
|
||||||
static id = "update-store" as const;
|
static id = "update-store" as const;
|
||||||
static dataSchema = z.never();
|
static dataSchema = z.never();
|
||||||
packageName: string;
|
packageName: string;
|
||||||
registry: URL;
|
|
||||||
storeVersion: string;
|
storeVersion: string;
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
this.packageName = process.env.STORE_PACKAGE_NAME ?? "@simeonradivoev/gameflow-store";
|
this.packageName = process.env.STORE_PACKAGE_NAME ?? "@simeonradivoev/gameflow-store";
|
||||||
this.registry = new URL(process.env.STORE_REGISTRY ?? "https://registry.npmjs.org");
|
|
||||||
this.storeVersion = process.env.STORE_VERSION ?? "^0.1.0";
|
this.storeVersion = process.env.STORE_VERSION ?? "^0.1.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCommand (commands: string[])
|
async start (context: JobContext<UpdateStoreJob, never, string>)
|
||||||
{
|
{
|
||||||
const tempCache = path.join(tmpdir(), "gameflow-bun-cache");
|
|
||||||
const storeFolder = getStoreRootFolder();
|
|
||||||
|
|
||||||
let proc = Bun.spawn([process.execPath, ...commands, "--registry", this.registry.href, '--json'], {
|
|
||||||
cwd: storeFolder,
|
|
||||||
stdout: 'pipe',
|
|
||||||
stderr: 'pipe',
|
|
||||||
env: {
|
|
||||||
BUN_BE_BUN: "1",
|
|
||||||
BUN_INSTALL_CACHE_DIR: tempCache
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let stdout = await new Response(proc.stdout).text();
|
|
||||||
console.log(stdout);
|
|
||||||
let stderr = await new Response(proc.stderr).text();
|
|
||||||
if (stderr)
|
|
||||||
console.error(stderr);
|
|
||||||
await proc.exited;
|
|
||||||
}
|
|
||||||
|
|
||||||
async start (context: JobContext<UpdateStoreJob, never, never>)
|
|
||||||
{
|
|
||||||
if (process.env.CUSTOM_STORE_PATH) return;
|
|
||||||
|
|
||||||
const storeFolder = getStoreRootFolder();
|
const storeFolder = getStoreRootFolder();
|
||||||
await ensureDir(storeFolder);
|
await ensureDir(storeFolder);
|
||||||
|
const storePackageFile = Bun.file(path.join(storeFolder, "package.json"));
|
||||||
|
if (!await storePackageFile.exists())
|
||||||
|
{
|
||||||
|
await storePackageFile.write(JSON.stringify({ dependencies: {} }, null, 3));
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Adding Store Package");
|
const storePackage = await Bun.file(path.join(storeFolder, "package.json")).json();
|
||||||
await this.runCommand(["add", `${this.packageName}@${this.storeVersion}`]);
|
|
||||||
|
|
||||||
console.log("Updating Store Package");
|
if (!storePackage.dependencies?.[sdkPkg.name] || storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version)
|
||||||
await this.runCommand(["update", `${this.packageName}@${this.storeVersion}`]);
|
{
|
||||||
|
let response = await runBunPackageCommand(["add", `${sdkPkg.name}@${sdkPkg.version}`, "--registry", PluginRegistry, '--omit', 'peer']);
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// probably just means we couldn't find a version of the sdk, just install latest
|
||||||
|
if (storePackage.dependencies?.[sdkPkg.name] !== sdkPkg.version)
|
||||||
|
{
|
||||||
|
let response = await runBunPackageCommand(["add", '--exact', `${sdkPkg.name}@latest`, "--registry", PluginRegistry, '--omit', 'peer']);
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CUSTOM_STORE_PATH) return;
|
||||||
|
|
||||||
|
if (!storePackage.dependencies?.['@simeonradivoev/gameflow-store'])
|
||||||
|
{
|
||||||
|
context.setProgress(0.5, "Adding Store");
|
||||||
|
let response = await runBunPackageCommand(["add", `${this.packageName}@${this.storeVersion}`, "--registry", PluginRegistry, '--omit', 'peer']);
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import { FrontendNotification } from '@/shared/types';
|
import { FrontendNotification } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { events } from './app';
|
import { events } from './app';
|
||||||
|
|
||||||
export default function buildNotificationsStream ()
|
export default function buildNotificationsStream ()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { config } from "@/bun/api/app";
|
import { config } from "@/bun/api/app";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import { config } from "@/bun/api/app";
|
import { config } from "@/bun/api/app";
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
|
|
||||||
import { config } from "@/bun/api/app";
|
import { config } from "@/bun/api/app";
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import defaultConfig from './PCSX2.ini' with { type: 'file' };
|
import defaultConfig from './PCSX2.ini' with { type: 'file' };
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import ini from 'ini';
|
import ini from 'ini';
|
||||||
import { EmulatorCapabilities } from "@/shared/types";
|
import { EmulatorCapabilities } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default class PCSX2Integration implements PluginType
|
export default class PCSX2Integration implements PluginType
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import { config } from "@/bun/api/app";
|
import { config } from "@/bun/api/app";
|
||||||
import configFilePathWin32 from './win32/ppsspp.ini' with { type: 'file' };
|
import configFilePathWin32 from './win32/ppsspp.ini' with { type: 'file' };
|
||||||
|
|
@ -11,7 +11,7 @@ import { ensureDir } from "fs-extra";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import ini from 'ini';
|
import ini from 'ini';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { EmulatorCapabilities } from "@/shared/types";
|
import { EmulatorCapabilities } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default class PPSSPPIntegration implements PluginType
|
export default class PPSSPPIntegration implements PluginType
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import { config } from "@/bun/api/app";
|
import { config } from "@/bun/api/app";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import GameflowHooks from "@/bun/api/hooks/app";
|
import { GameflowHooks } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { config } from "@/bun/api/app";
|
import { config } from "@/bun/api/app";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import { config, customEmulators, db, emulatorsDb } from "@/bun/api/app";
|
import { config, customEmulators, db, emulatorsDb } from "@/bun/api/app";
|
||||||
import * as emulatorSchema from '@schema/emulators';
|
import * as emulatorSchema from '@schema/emulators';
|
||||||
|
|
@ -13,7 +13,7 @@ import { findStoreEmulatorExec } from "@/bun/api/games/services/launchGameServic
|
||||||
import { which } from "bun";
|
import { which } from "bun";
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { getLocalGameMatch } from "@/bun/api/games/services/utils";
|
import { getLocalGameMatch } from "@/bun/api/games/services/utils";
|
||||||
import { CommandEntry, EmulatorSourceEntryType } from "@/shared/types";
|
import { CommandEntry, EmulatorSourceEntryType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default class IgdbIntegration implements PluginType
|
export default class IgdbIntegration implements PluginType
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import { config, db, events } from "@/bun/api/app";
|
import { config, db, events } from "@/bun/api/app";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import secrets from "@/bun/api/secrets";
|
import secrets from "@/bun/api/secrets";
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import * as igdb from '@phalcode/ts-igdb-client';
|
import * as igdb from '@phalcode/ts-igdb-client';
|
||||||
import { checkLoginAndRefreshTwitch } from "@/bun/api/auth";
|
import { checkLoginAndRefreshTwitch } from "@/bun/api/auth";
|
||||||
import { GameLookup } from "@/shared/types";
|
import { GameLookup } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default class IgdbIntegration implements PluginType
|
export default class IgdbIntegration implements PluginType
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
|
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomByMetadataProviderApiRomsByMetadataProviderGet, getRomContentApiRomsIdContentFileNameGet, getRomFiltersApiRomsFiltersGet, getRomsApiRomsGet, getSavesSummaryApiSavesSummaryGet, PlatformSchema, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm";
|
import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomByMetadataProviderApiRomsByMetadataProviderGet, getRomContentApiRomsIdContentFileNameGet, getRomFiltersApiRomsFiltersGet, getRomsApiRomsGet, getSavesSummaryApiSavesSummaryGet, PlatformSchema, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm";
|
||||||
import { config, events } from "@/bun/api/app";
|
import { config, events } from "@/bun/api/app";
|
||||||
|
|
@ -14,7 +14,7 @@ import { client } from "@/clients/romm/client.gen";
|
||||||
import { validateGameSource } from "@/bun/api/games/services/statusService";
|
import { validateGameSource } from "@/bun/api/games/services/statusService";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { checkLoginAndRefreshRomm } from "@/bun/api/auth";
|
import { checkLoginAndRefreshRomm } from "@/bun/api/auth";
|
||||||
import { DownloadFileEntry, DownloadInfo, FrontEndCollection, FrontEndGameType, FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement, FrontEndGameTypeWithIds, FrontEndPlatformType } from "@/shared/types";
|
import { DownloadFileEntry, DownloadInfo, FrontEndCollection, FrontEndGameType, FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement, FrontEndGameTypeWithIds, FrontEndPlatformType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
import Conf from "conf";
|
import Conf from "conf";
|
||||||
|
|
||||||
const SettingsSchema = z.object({
|
const SettingsSchema = z.object({
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "com.simeonradivoev.gameflow.store",
|
"name": "com.simeonradivoev.gameflow.store",
|
||||||
"displayName": "Gameflow Store",
|
"displayName": "Gameflow Store Integration",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "The internal gameflow store",
|
"description": "The internal gameflow store integration. This is the logic of the store that uses the data only store package",
|
||||||
"main": "./store.ts",
|
"main": "./store.ts",
|
||||||
"category": "sources",
|
"category": "sources",
|
||||||
"canDisable": false,
|
"canDisable": false,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { getStoreFolder } from "@/bun/api/store/services/gamesService";
|
import { getStoreFolder } from "@/bun/api/store/services/gamesService";
|
||||||
import { EmulatorDownloadInfoSchema, EmulatorDownloadInfoType, EmulatorPackageType, StoreDownloadType, StoreGameSchema, StoreGameType } from "@/shared/constants";
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import * as appSchema from '@schema/app';
|
import * as appSchema from '@schema/app';
|
||||||
|
|
@ -12,7 +11,7 @@ import { shuffleInPlace } from "@/bun/utils";
|
||||||
import mustache from "mustache";
|
import mustache from "mustache";
|
||||||
import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService";
|
import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { CommandEntry, EmulatorSourceEntryType, EmulatorSystem, FrontEndEmulator, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailed, SaveFileChange } from "@/shared/types";
|
import { CommandEntry, EmulatorSourceEntryType, EmulatorSystem, FrontEndEmulator, FrontEndFilterSets, FrontEndGameType, FrontEndGameTypeDetailed, SaveFileChange, EmulatorDownloadInfoType, StoreDownloadType, StoreGameType, EmulatorPackageType, EmulatorDownloadInfoSchema, StoreGameSchema } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export async function getStoreGames (gamesManifest: any[], filter?: { limit?: number; offset?: number; })
|
export async function getStoreGames (gamesManifest: any[], filter?: { limit?: number; offset?: number; })
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PluginLoadingContextType, PluginType } from "@/bun/types/types.schema";
|
import { PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import desc from './package.json';
|
import desc from './package.json';
|
||||||
import path, { } from 'node:path';
|
import path, { } from 'node:path';
|
||||||
import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService";
|
import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService";
|
||||||
|
|
@ -12,7 +12,7 @@ import { getSourceGameDetailed } from "@/bun/api/games/services/utils";
|
||||||
import UpdateStoreJob from "@/bun/api/jobs/update-store";
|
import UpdateStoreJob from "@/bun/api/jobs/update-store";
|
||||||
import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService";
|
import { getEmulatorDownload, getEmulatorPath } from "@/bun/api/store/services/emulatorsService";
|
||||||
import { buildFilters, buildLaunchCommand, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services";
|
import { buildFilters, buildLaunchCommand, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services";
|
||||||
import { DownloadInfo, FrontEndEmulatorDetailed, FrontEndGameTypeWithIds } from "@/shared/types";
|
import { DownloadInfo, FrontEndEmulatorDetailed, FrontEndGameTypeWithIds } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default class RommIntegration implements PluginType
|
export default class RommIntegration implements PluginType
|
||||||
{
|
{
|
||||||
|
|
@ -151,7 +151,8 @@ export default class RommIntegration implements PluginType
|
||||||
if (!validDownload || !validDownload.bin) return;
|
if (!validDownload || !validDownload.bin) return;
|
||||||
const glob = new Glob(validDownload.bin);
|
const glob = new Glob(validDownload.bin);
|
||||||
const files = await Array.fromAsync(glob.scan({ cwd: emulatorPath }));
|
const files = await Array.fromAsync(glob.scan({ cwd: emulatorPath }));
|
||||||
if (files.length > 0)
|
// es-de also searches for store executables so there might be duplicates, check first.
|
||||||
|
if (files.length > 0 && !sources.find(s => s.type === 'store'))
|
||||||
{
|
{
|
||||||
sources.push({ binPath: path.join(emulatorPath, files[0]), exists: true, rootPath: emulatorPath, type: 'store' });
|
sources.push({ binPath: path.join(emulatorPath, files[0]), exists: true, rootPath: emulatorPath, type: 'store' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import GameflowHooks from "../hooks/app";
|
import { GameflowHooks } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { PluginDescriptionType, PluginLoadingContextType, PluginType } from "../../types/types.schema";
|
import { PluginDescriptionType, PluginLoadingContextType, PluginType } from "@simeonradivoev/gameflow-sdk";
|
||||||
import { config } from "../app";
|
import { config, events, taskQueue } from "../app";
|
||||||
import Conf from "conf";
|
import Conf from "conf";
|
||||||
import projectPackage from '~/package.json';
|
import projectPackage from '~/package.json';
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { PluginSourceType } from "@/shared/types";
|
import { PluginSourceType, PluginUpdateCheck } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
import { getUpdates } from "./services";
|
||||||
|
import sdkPkg from '@simeonradivoev/gameflow-sdk/package.json';
|
||||||
|
import { semver } from "bun";
|
||||||
|
|
||||||
export const pluginZodRegistry = z.registry<{
|
export const pluginZodRegistry = z.registry<{
|
||||||
requiresRestart?: boolean;
|
requiresRestart?: boolean;
|
||||||
|
|
@ -21,9 +24,19 @@ export class PluginManager
|
||||||
description: PluginDescriptionType,
|
description: PluginDescriptionType,
|
||||||
source: PluginSourceType;
|
source: PluginSourceType;
|
||||||
config?: Conf;
|
config?: Conf;
|
||||||
|
update?: PluginUpdateCheck;
|
||||||
|
incompatible?: boolean;
|
||||||
|
|
||||||
}> = {};
|
}> = {};
|
||||||
|
|
||||||
|
unregister (id: string)
|
||||||
|
{
|
||||||
|
if (!this.plugins[id]) return false;
|
||||||
|
delete this.plugins[id];
|
||||||
|
console.log("Plugin", id, "unregistered");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
register (plugin: PluginType, description: PluginDescriptionType, source: PluginSourceType)
|
register (plugin: PluginType, description: PluginDescriptionType, source: PluginSourceType)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -68,16 +81,33 @@ export class PluginManager
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reload (name: string, reloadCtx: { setProgress: (progress: number, state: string) => void; })
|
checkValidity (plugin: PluginDescriptionType)
|
||||||
|
{
|
||||||
|
const sdkDep = plugin.peerDependencies?.[sdkPkg.name];
|
||||||
|
if (sdkDep)
|
||||||
|
{
|
||||||
|
return semver.satisfies(sdkPkg.version, sdkDep);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reload (name: string, reloadCtx: { setProgress: (progress: number, state: string) => void; }, update: string | undefined)
|
||||||
{
|
{
|
||||||
const plugin = this.plugins[name];
|
const plugin = this.plugins[name];
|
||||||
if (plugin)
|
if (plugin)
|
||||||
{
|
{
|
||||||
|
plugin.update = update && !semver.satisfies(plugin.description.version, update) ? { current: plugin.description.version, new: update } : undefined;
|
||||||
|
|
||||||
const ctx: PluginLoadingContextType = {
|
const ctx: PluginLoadingContextType = {
|
||||||
hooks: this.hooks,
|
hooks: this.hooks,
|
||||||
setProgress: reloadCtx.setProgress.bind(reloadCtx),
|
setProgress: reloadCtx.setProgress.bind(reloadCtx),
|
||||||
config: plugin.config as any,
|
config: plugin.config as any,
|
||||||
zodRegistry: pluginZodRegistry
|
zodRegistry: pluginZodRegistry,
|
||||||
|
app: {
|
||||||
|
config,
|
||||||
|
events,
|
||||||
|
taskQueue
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (plugin.loaded)
|
if (plugin.loaded)
|
||||||
|
|
@ -88,7 +118,14 @@ export class PluginManager
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (plugin.enabled || plugin.description.canDisable === false)
|
plugin.incompatible = !this.checkValidity(plugin.description);
|
||||||
|
if (plugin.incompatible)
|
||||||
|
{
|
||||||
|
console.error(plugin.description.name, "Incompatible sdk verison");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.enabled || plugin.description.canDisable === false || plugin.description.name === '@simeonradivoev/gameflow-store')
|
||||||
{
|
{
|
||||||
console.log("Loading Plugin", plugin.description.name);
|
console.log("Loading Plugin", plugin.description.name);
|
||||||
await plugin.plugin.load(ctx);
|
await plugin.plugin.load(ctx);
|
||||||
|
|
@ -106,10 +143,13 @@ export class PluginManager
|
||||||
async reloadAll (ctx: { setProgress: (progress: number, state: string) => void; })
|
async reloadAll (ctx: { setProgress: (progress: number, state: string) => void; })
|
||||||
{
|
{
|
||||||
this.hooks = new GameflowHooks();
|
this.hooks = new GameflowHooks();
|
||||||
|
|
||||||
|
const outdated = await getUpdates();
|
||||||
|
|
||||||
for await (const id of Object.keys(this.plugins))
|
for await (const id of Object.keys(this.plugins))
|
||||||
{
|
{
|
||||||
ctx.setProgress(0, `Loading ${id}`);
|
ctx.setProgress(0, `Loading ${id}`);
|
||||||
await this.reload(id, ctx);
|
await this.reload(id, ctx, outdated?.[id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import { plugins, taskQueue } from "../app";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { toggleElementInConfig } from "@/bun/utils";
|
import { toggleElementInConfig } from "@/bun/utils";
|
||||||
import ReloadPluginsJob from "../jobs/reload-plugins-job";
|
import ReloadPluginsJob from "../jobs/reload-plugins-job";
|
||||||
import { FrontendPlugin } from "@/shared/types";
|
import { FrontendPlugin } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
import { canDisable, canUninstall } from "./services";
|
||||||
|
import PluginOperationJob from "../jobs/plugin-operation-job";
|
||||||
|
|
||||||
export default new Elysia({ prefix: '/plugins' })
|
export default new Elysia({ prefix: '/plugins' })
|
||||||
.get('/', async () =>
|
.get('/', async () =>
|
||||||
|
|
@ -17,25 +19,27 @@ export default new Elysia({ prefix: '/plugins' })
|
||||||
description: p.description.description,
|
description: p.description.description,
|
||||||
source: p.source,
|
source: p.source,
|
||||||
version: p.description.version,
|
version: p.description.version,
|
||||||
canDisable: p.description.canDisable ?? true,
|
canDisable: canDisable(p.description),
|
||||||
icon: p.description.icon,
|
icon: p.description.icon,
|
||||||
category: p.description.category,
|
category: p.description.category,
|
||||||
hasSettings: !!p.config || !!p.plugin.eventsNames
|
hasSettings: !!p.config || !!p.plugin.eventsNames,
|
||||||
|
canUninstall: canUninstall(p.description, p.source),
|
||||||
|
update: p.update
|
||||||
};
|
};
|
||||||
return plugin;
|
return plugin;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.get('/:id', async ({ params: { id } }) =>
|
.get('/:id', async ({ params: { id } }) =>
|
||||||
{
|
{
|
||||||
const plugin = plugins.plugins[id];
|
const plugin = plugins.plugins[decodeURIComponent(id)];
|
||||||
return plugin.description;
|
return { ...plugin.description, update: plugin.update };
|
||||||
})
|
})
|
||||||
.post('/:id', async ({ params: { id }, body: { enabled } }) =>
|
.post('/:id', async ({ params: { id }, body: { enabled } }) =>
|
||||||
{
|
{
|
||||||
const plugin = plugins.plugins[id];
|
const plugin = plugins.plugins[decodeURIComponent(id)];
|
||||||
if (plugin)
|
if (plugin)
|
||||||
{
|
{
|
||||||
if (plugin.description.canDisable === false)
|
if (!canDisable(plugin.description))
|
||||||
{
|
{
|
||||||
return status("Forbidden");
|
return status("Forbidden");
|
||||||
}
|
}
|
||||||
|
|
@ -48,4 +52,26 @@ export default new Elysia({ prefix: '/plugins' })
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
body: z.object({ enabled: z.boolean() })
|
body: z.object({ enabled: z.boolean() })
|
||||||
|
}).post('/install', async ({ body: { id } }) =>
|
||||||
|
{
|
||||||
|
if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return;
|
||||||
|
await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("add", id));
|
||||||
|
await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob());
|
||||||
|
}, {
|
||||||
|
body: z.object({ id: z.string() })
|
||||||
|
}).post('/update', async ({ body: { id } }) =>
|
||||||
|
{
|
||||||
|
if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return;
|
||||||
|
await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("update", id));
|
||||||
|
await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob());
|
||||||
|
}, {
|
||||||
|
body: z.object({ id: z.string() })
|
||||||
|
})
|
||||||
|
.post('/uninstall', async ({ body: { id } }) =>
|
||||||
|
{
|
||||||
|
if (taskQueue.hasActiveOfType(PluginOperationJob) || taskQueue.hasActiveOfType(ReloadPluginsJob)) return;
|
||||||
|
await taskQueue.enqueue(PluginOperationJob.id, new PluginOperationJob("remove", id));
|
||||||
|
await taskQueue.enqueue(ReloadPluginsJob.id, new ReloadPluginsJob());
|
||||||
|
}, {
|
||||||
|
body: z.object({ id: z.string() })
|
||||||
});
|
});
|
||||||
|
|
@ -11,12 +11,78 @@ import igdb from './builtin/sources/com.simeonradivoev.gameflow.igdb/package.jso
|
||||||
import store from './builtin/sources/com.simeonradivoev.gameflow.store/package.json';
|
import store from './builtin/sources/com.simeonradivoev.gameflow.store/package.json';
|
||||||
import es from './builtin/launchers/com.simeonradivoev.gameflow.es/package.json';
|
import es from './builtin/launchers/com.simeonradivoev.gameflow.es/package.json';
|
||||||
import rclone from './builtin/other/com.simeonradivoev.gameflow.rclone/package.json';
|
import rclone from './builtin/other/com.simeonradivoev.gameflow.rclone/package.json';
|
||||||
import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@/bun/types/types.schema";
|
import { PluginDescriptionSchema, PluginDescriptionType, PluginSchema } from "@simeonradivoev/gameflow-sdk";
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { getStoreRootFolder } from "../store/services/gamesService";
|
import { getStoreRootFolder } from "../store/services/gamesService";
|
||||||
|
import { getUpdates } from "./services";
|
||||||
|
import { PluginSourceType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
import { taskQueue } from "../app";
|
||||||
|
import UpdateStoreJob from "../jobs/update-store";
|
||||||
|
|
||||||
type PluginEntry = PluginDescriptionType & { load: () => Promise<any>; };
|
type PluginEntry = PluginDescriptionType & { load: () => Promise<any>; };
|
||||||
|
|
||||||
|
const blacklist = new Set(['@simeonradivoev/gameflow-sdk']);
|
||||||
|
|
||||||
|
export async function getPlugin (id: string, pluginManager: PluginManager)
|
||||||
|
{
|
||||||
|
const pluginPath = path.join(getStoreRootFolder(), 'node_modules', id);
|
||||||
|
const pluginPackageFile = Bun.file(path.join(pluginPath, 'package.json'));
|
||||||
|
if (await pluginPackageFile.exists())
|
||||||
|
{
|
||||||
|
const pluginPackage = await PluginDescriptionSchema.safeParseAsync(await pluginPackageFile.json());
|
||||||
|
if (pluginPackage.success)
|
||||||
|
{
|
||||||
|
const mainPath = path.join(pluginPath, pluginPackage.data.main);
|
||||||
|
if (await Bun.file(mainPath).exists())
|
||||||
|
{
|
||||||
|
const entry: PluginEntry = { ...pluginPackage.data, load: () => import(mainPath) };
|
||||||
|
return entry;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
console.error("Main file for", id, "does not exist");
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
console.error("Invalid Package for", id, pluginPackage.error.message);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
console.error("Package for", id, "does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unregisterPlugin (id: string, pluginManager: PluginManager)
|
||||||
|
{
|
||||||
|
return pluginManager.unregister(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerPlugin (plugin: PluginEntry, source: PluginSourceType, pluginManager: PluginManager)
|
||||||
|
{
|
||||||
|
if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(plugin.name))
|
||||||
|
{
|
||||||
|
console.log("Skipping", plugin.name, "missing in whitelist");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(plugin.name))
|
||||||
|
{
|
||||||
|
console.log("Skipping", plugin.name, "found in whitelist");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await plugin.load();
|
||||||
|
if (file.default && typeof file.default === 'function')
|
||||||
|
{
|
||||||
|
const pluginInstance = new file.default();
|
||||||
|
await PluginSchema.parseAsync(pluginInstance);
|
||||||
|
const description = await PluginDescriptionSchema.parseAsync(plugin);
|
||||||
|
pluginManager.register(pluginInstance, description, source);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
console.log("Skipping", plugin.name, "invalid main. Has to be class with load method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default async function register (pluginManager: PluginManager)
|
export default async function register (pluginManager: PluginManager)
|
||||||
{
|
{
|
||||||
const plugins: PluginEntry[] = [
|
const plugins: PluginEntry[] = [
|
||||||
|
|
@ -33,53 +99,41 @@ export default async function register (pluginManager: PluginManager)
|
||||||
{ ...rclone, load: () => import('./builtin/other/com.simeonradivoev.gameflow.rclone/rclone') },
|
{ ...rclone, load: () => import('./builtin/other/com.simeonradivoev.gameflow.rclone/rclone') },
|
||||||
];
|
];
|
||||||
|
|
||||||
const storePackageFile = path.join(getStoreRootFolder(), 'package.json');
|
await Promise.all(plugins.map(p => registerPlugin(p, 'builtin', pluginManager)));
|
||||||
const storePackage = await Bun.file(storePackageFile).json();
|
|
||||||
|
|
||||||
if (storePackage.dependencies)
|
const storePackageFilePath = path.join(getStoreRootFolder(), 'package.json');
|
||||||
|
if (!await Bun.file(storePackageFilePath).exists())
|
||||||
{
|
{
|
||||||
const storePlugins = await Promise.all(Object.keys(storePackage.dependencies).map(async p =>
|
console.log("Store is missing. Updating it.");
|
||||||
|
await taskQueue.enqueue(UpdateStoreJob.id, new UpdateStoreJob());
|
||||||
|
console.log("Store Updated");
|
||||||
|
}
|
||||||
|
const storePackage = await Bun.file(storePackageFilePath).json();
|
||||||
|
|
||||||
|
if (storePackage?.dependencies)
|
||||||
|
{
|
||||||
|
const storePlugins = await Promise.all(Object.keys(storePackage.dependencies).filter(p => !blacklist.has(p)).map(async p =>
|
||||||
{
|
{
|
||||||
const pluginPath = path.join(getStoreRootFolder(), 'node_modules', p);
|
return getPlugin(p, pluginManager);
|
||||||
const pluginPackageFile = Bun.file(path.join(pluginPath, 'package.json'));
|
|
||||||
if (await pluginPackageFile.exists())
|
|
||||||
{
|
|
||||||
const pluginPackage = await PluginDescriptionSchema.safeParseAsync(await pluginPackageFile.json());
|
|
||||||
if (pluginPackage.success)
|
|
||||||
{
|
|
||||||
const mainPath = path.join(pluginPath, pluginPackage.data.main);
|
|
||||||
if (await Bun.file(mainPath).exists())
|
|
||||||
{
|
|
||||||
const entry: PluginEntry = { ...pluginPackage.data, load: () => import(mainPath) };
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
plugins.push(...storePlugins.filter(p => !!p));
|
console.log("Checking for outdated packages");
|
||||||
}
|
const outdated = await getUpdates();
|
||||||
|
|
||||||
await Promise.all(plugins.filter(p =>
|
const validPlugins = storePlugins.filter(p => !!p);
|
||||||
{
|
|
||||||
if (process.env.PLUGIN_WHITELIST && !process.env.PLUGIN_WHITELIST.includes(p.name))
|
if (outdated)
|
||||||
{
|
{
|
||||||
return false;
|
validPlugins.forEach(p =>
|
||||||
|
{
|
||||||
|
const newVersion = outdated[p.name];
|
||||||
|
if (newVersion)
|
||||||
|
{
|
||||||
|
console.log("Plugin", p.name, "has update", p.version, "=>", newVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (process.env.PLUGIN_BLACKLIST && process.env.PLUGIN_BLACKLIST.includes(p.name))
|
|
||||||
{
|
await Promise.all(validPlugins.map(p => registerPlugin(p, 'store', pluginManager)));
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).map(async (pluginPackage) =>
|
|
||||||
{
|
|
||||||
const file = await pluginPackage.load();
|
|
||||||
if (file.default && typeof file.default === 'function')
|
|
||||||
{
|
|
||||||
const pluginInstance = new file.default();
|
|
||||||
await PluginSchema.parseAsync(pluginInstance);
|
|
||||||
const description = await PluginDescriptionSchema.parseAsync(pluginPackage);
|
|
||||||
pluginManager.register(pluginInstance, description, 'builtin');
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
62
src/bun/api/plugins/services.ts
Normal file
62
src/bun/api/plugins/services.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import path from 'node:path';
|
||||||
|
import os from 'node:os';
|
||||||
|
import { getStoreRootFolder } from '../store/services/gamesService';
|
||||||
|
import { PluginDescriptionType } from '@simeonradivoev/gameflow-sdk';
|
||||||
|
import { run } from 'npm-check-updates';
|
||||||
|
|
||||||
|
export function canDisable (description: PluginDescriptionType)
|
||||||
|
{
|
||||||
|
if (description.name === '@simeonradivoev/gameflow-store')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return description.canDisable ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUpdates ()
|
||||||
|
{
|
||||||
|
const updated = await run({ packageManager: 'bun', peer: true, cwd: getStoreRootFolder(), jsonUpgraded: true, reject: ['@simeonradivoev/gameflow-sdk'] });
|
||||||
|
return updated as Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canUninstall (description: PluginDescriptionType, source: string)
|
||||||
|
{
|
||||||
|
if (description.name === '@simeonradivoev/gameflow-store')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return source !== 'builtin';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runBunPackageCommand (commands: string[])
|
||||||
|
{
|
||||||
|
const tempCache = path.join(os.tmpdir(), "gameflow-bun-cache");
|
||||||
|
const storeFolder = getStoreRootFolder();
|
||||||
|
|
||||||
|
let proc = Bun.spawn([process.execPath, ...commands, '--json'], {
|
||||||
|
cwd: storeFolder,
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
env: {
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
BUN_INSTALL_CACHE_DIR: tempCache
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = await new Response(proc.stdout).text();
|
||||||
|
let stderr = await new Response(proc.stderr).text();
|
||||||
|
if (stderr)
|
||||||
|
console.error(stderr);
|
||||||
|
await proc.exited;
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasPackage (id: string)
|
||||||
|
{
|
||||||
|
const storeFolder = getStoreRootFolder();
|
||||||
|
const packagePath = path.join(storeFolder, 'package.json');
|
||||||
|
const packageFile = Bun.file(packagePath);
|
||||||
|
if (!await packageFile.exists()) return false;
|
||||||
|
const pkg = await packageFile.json();
|
||||||
|
return !!pkg.dependencies?.[id];
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { LocalGameMetadata } from "@/shared/types";
|
|
||||||
|
import { LocalGameMetadata } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
import { sql, relations } from "drizzle-orm";
|
import { sql, relations } from "drizzle-orm";
|
||||||
import { integer, text, sqliteTable, blob } from "drizzle-orm/sqlite-core";
|
import { integer, text, sqliteTable, blob } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { cores } from '../emulatorjs/emulatorjs';
|
||||||
import { SERVER_URL } from '@/shared/constants';
|
import { SERVER_URL } from '@/shared/constants';
|
||||||
import { host } from '@/bun/utils/host';
|
import { host } from '@/bun/utils/host';
|
||||||
import { findEmulatorPluginIntegration } from '../store/services/emulatorsService';
|
import { findEmulatorPluginIntegration } from '../store/services/emulatorsService';
|
||||||
import { EmulatorSourceEntryType, FrontEndEmulator } from '@/shared/types';
|
import { EmulatorSourceEntryType, FrontEndEmulator } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get emulators based on local games. Only the ones we probably need.
|
* Get emulators based on local games. Only the ones we probably need.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { SettingsSchema } from "@shared/constants";
|
import { SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import Elysia, { status } from "elysia";
|
import Elysia, { status } from "elysia";
|
||||||
import { config, customEmulators, plugins, taskQueue } from "../app";
|
import { config, customEmulators, plugins, taskQueue } from "../app";
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
|
|
@ -96,27 +96,27 @@ export const settings = new Elysia({ prefix: '/api/settings' })
|
||||||
})
|
})
|
||||||
.get('/definitions/:source', async ({ params: { source } }) =>
|
.get('/definitions/:source', async ({ params: { source } }) =>
|
||||||
{
|
{
|
||||||
return plugins.plugins[source].plugin.settingsSchema?.toJSONSchema() as JSONSchema7;
|
return plugins.plugins[decodeURIComponent(source)].plugin.settingsSchema?.toJSONSchema() as JSONSchema7;
|
||||||
})
|
})
|
||||||
.get('/actions/:source', async ({ params: { source } }) =>
|
.get('/actions/:source', async ({ params: { source } }) =>
|
||||||
{
|
{
|
||||||
const plugin = plugins.plugins[source]?.plugin;
|
const plugin = plugins.plugins[decodeURIComponent(source)]?.plugin;
|
||||||
if (!plugin.eventsNames) return [];
|
if (!plugin.eventsNames) return [];
|
||||||
return plugin.eventsNames;
|
return plugin.eventsNames;
|
||||||
})
|
})
|
||||||
.post('/actions/:source/:id', async ({ params: { source, id } }) =>
|
.post('/actions/:source/:id', async ({ params: { source, id } }) =>
|
||||||
{
|
{
|
||||||
return await plugins.plugins[source]?.plugin.onEvent?.(id);
|
return await plugins.plugins[decodeURIComponent(source)]?.plugin.onEvent?.(decodeURIComponent(id));
|
||||||
})
|
})
|
||||||
.get('/:source/:id', async ({ params: { source, id } }) =>
|
.get('/:source/:id', async ({ params: { source, id } }) =>
|
||||||
{
|
{
|
||||||
return { value: plugins.plugins[source].config?.get(id) };
|
return { value: plugins.plugins[decodeURIComponent(source)].config?.get(decodeURIComponent(id)) };
|
||||||
})
|
})
|
||||||
.put('/:source/:id', async ({ params: { source, id }, body: { value } }) =>
|
.put('/:source/:id', async ({ params: { source, id }, body: { value } }) =>
|
||||||
{
|
{
|
||||||
const plugin = plugins.plugins[source];
|
const plugin = plugins.plugins[decodeURIComponent(source)];
|
||||||
if (!plugin.config) return status("Not Found", "Plugin has no config");
|
if (!plugin.config) return status("Not Found", "Plugin has no config");
|
||||||
const settingSchema = plugin.plugin.settingsSchema?.shape[id] as z.ZodObject;
|
const settingSchema = plugin.plugin.settingsSchema?.shape[decodeURIComponent(id)] as z.ZodObject;
|
||||||
if (!settingSchema) return status("Not Found", "Could not find setting");
|
if (!settingSchema) return status("Not Found", "Could not find setting");
|
||||||
const meta = pluginZodRegistry.get(settingSchema);
|
const meta = pluginZodRegistry.get(settingSchema);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { EmulatorDownloadInfoType, EmulatorPackageType, ScoopPackageSchema } from "@/shared/constants";
|
|
||||||
import { config, plugins } from "../../app";
|
import { config, plugins } from "../../app";
|
||||||
import { getOrCached, getOrCachedGithubRelease } from "../../cache";
|
import { getOrCached, getOrCachedGithubRelease } from "../../cache";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { EmulatorSourceEntryType, EmulatorSupport } from "@/shared/types";
|
import { EmulatorSourceEntryType, EmulatorSupport, ScoopPackageSchema, EmulatorPackageType, EmulatorDownloadInfoType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export function findEmulatorPluginIntegration (name: string, validSources: (EmulatorSourceEntryType | undefined)[]): EmulatorSupport[]
|
export function findEmulatorPluginIntegration (name: string, validSources: (EmulatorSourceEntryType | undefined)[]): EmulatorSupport[]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { EmulatorPackageSchema, EmulatorPackageType } from "@/shared/constants";
|
|
||||||
import { and, eq, or } from "drizzle-orm";
|
import { and, eq, or } from "drizzle-orm";
|
||||||
import { config, emulatorsDb } from '../../app';
|
import { config, emulatorsDb } from '../../app';
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import * as emulatorSchema from '@schema/emulators';
|
import * as emulatorSchema from '@schema/emulators';
|
||||||
import { EmulatorSystem } from "@/shared/types";
|
import { EmulatorSystem, EmulatorPackageType, EmulatorPackageSchema } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export function getStoreRootFolder ()
|
export function getStoreRootFolder ()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import Elysia, { status } from "elysia";
|
||||||
import { config, db, plugins, taskQueue } from "../app";
|
import { config, db, plugins, taskQueue } from "../app";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { EmulatorDownloadInfoSchema } from "@/shared/constants";
|
|
||||||
import * as appSchema from '@schema/app';
|
import * as appSchema from '@schema/app';
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { convertLocalToFrontendDetailed, getLocalGameMatch } from "../games/services/utils";
|
import { convertLocalToFrontendDetailed, getLocalGameMatch } from "../games/services/utils";
|
||||||
|
|
@ -13,7 +12,17 @@ import { getStoreFolder } from "./services/gamesService";
|
||||||
import { EmulatorDownloadJob } from "../jobs/emulator-download-job";
|
import { EmulatorDownloadJob } from "../jobs/emulator-download-job";
|
||||||
import { BiosDownloadJob } from "../jobs/bios-download-job";
|
import { BiosDownloadJob } from "../jobs/bios-download-job";
|
||||||
import { findEmulatorPluginIntegration, getEmulatorPath } from "./services/emulatorsService";
|
import { findEmulatorPluginIntegration, getEmulatorPath } from "./services/emulatorsService";
|
||||||
import { EmulatorSourceEntryType, FrontEndEmulator, FrontEndGameTypeDetailed } from "@/shared/types";
|
import { EmulatorSourceEntryType, FrontEndEmulator, FrontEndGameTypeDetailed, PluginBunDetailsSchema, PluginEntrySchema, EmulatorDownloadInfoSchema } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
import PQueue from "p-queue";
|
||||||
|
import { hasPackage, runBunPackageCommand } from "../plugins/services";
|
||||||
|
import { semver } from "bun";
|
||||||
|
|
||||||
|
const npmQueue = new PQueue({ intervalCap: 60, interval: 1000 * 60, strict: true });
|
||||||
|
const pluginsResponseSchema = z.object({
|
||||||
|
objects: z.array(PluginEntrySchema),
|
||||||
|
total: z.number(),
|
||||||
|
time: z.coerce.date()
|
||||||
|
});
|
||||||
|
|
||||||
export const store = new Elysia({ prefix: '/api/store' })
|
export const store = new Elysia({ prefix: '/api/store' })
|
||||||
.get('/emulators', async ({ query }) =>
|
.get('/emulators', async ({ query }) =>
|
||||||
|
|
@ -109,6 +118,49 @@ export const store = new Elysia({ prefix: '/api/store' })
|
||||||
gameCount
|
gameCount
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
.get('/plugin', async ({ query: { plugin } }) =>
|
||||||
|
{
|
||||||
|
const pluginsRes = await runBunPackageCommand(['info', plugin]);
|
||||||
|
const pluginData = await PluginBunDetailsSchema.parseAsync(JSON.parse(pluginsRes));
|
||||||
|
const existingVersion = plugins.plugins[plugin]?.description.version;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pluginData,
|
||||||
|
installed: !!plugins.plugins[plugin] || await hasPackage(plugin),
|
||||||
|
update: existingVersion && semver.order(pluginData.version, existingVersion) > 0 ? { from: existingVersion } : undefined
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
query: z.object({ plugin: z.string() })
|
||||||
|
})
|
||||||
|
.get('/plugins', async ({ query: { search } }) =>
|
||||||
|
{
|
||||||
|
//TODO: Find a better way to search keywords and a search term at the same time
|
||||||
|
const pluginsRes = await npmQueue.add(() => fetch(`https://registry.npmjs.com/-/v1/search?text=keywords:gameflow-plugin`));
|
||||||
|
if (!pluginsRes.ok) return status(pluginsRes.status, pluginsRes.statusText);
|
||||||
|
const data: z.infer<typeof pluginsResponseSchema> = await pluginsRes.json();
|
||||||
|
if (search)
|
||||||
|
{
|
||||||
|
data.objects = data.objects.filter(o =>
|
||||||
|
{
|
||||||
|
if (o.package.description && o.package.description.includes(search)) return true;
|
||||||
|
if (o.package.name.includes(search)) return true;
|
||||||
|
if (o.package.keywords.includes(search)) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
data.total = data.objects.length;
|
||||||
|
}
|
||||||
|
await Promise.all(data.objects.map(async o =>
|
||||||
|
{
|
||||||
|
const existingVersion = plugins.plugins[o.package.name]?.description.version;
|
||||||
|
o.installed = !!plugins.plugins[o.package.name] || await hasPackage(o.package.name);
|
||||||
|
o.update = existingVersion && semver.order(o.package.version, existingVersion) > 0 ? { from: existingVersion } : undefined;
|
||||||
|
}));
|
||||||
|
return data as any;
|
||||||
|
}, {
|
||||||
|
query: z.object({ search: z.string().optional() }),
|
||||||
|
response: pluginsResponseSchema
|
||||||
|
})
|
||||||
.get('/media/*', async ({ params }) =>
|
.get('/media/*', async ({ params }) =>
|
||||||
{
|
{
|
||||||
return Bun.file(path.join(getStoreFolder(), params["*"]));
|
return Bun.file(path.join(getStoreFolder(), params["*"]));
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { getAppVersion, isSteamDeck, openExternal } from "../utils";
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import buildNotificationsStream from "./notifications";
|
import buildNotificationsStream from "./notifications";
|
||||||
import path, { dirname } from "node:path";
|
import path, { dirname } from "node:path";
|
||||||
import { DirSchema, SystemInfoSchema } from "@/shared/constants";
|
import { SystemInfoSchema, DirSchema, DownloadsDrive } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { getDevices, getDevicesCurated } from "./drives";
|
import { getDevices, getDevicesCurated } from "./drives";
|
||||||
import getFolderSize from "get-folder-size";
|
import getFolderSize from "get-folder-size";
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
|
|
@ -16,7 +16,6 @@ import ReloadPluginsJob from "./jobs/reload-plugins-job";
|
||||||
import { semver } from "bun";
|
import { semver } from "bun";
|
||||||
import { getOrCachedGithubRelease } from "./cache";
|
import { getOrCachedGithubRelease } from "./cache";
|
||||||
import SelfUpdateJob from "./jobs/self-update-job";
|
import SelfUpdateJob from "./jobs/self-update-job";
|
||||||
import { DownloadsDrive } from "@/shared/types";
|
|
||||||
|
|
||||||
async function checkUpdate (force?: boolean)
|
async function checkUpdate (force?: boolean)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { EmulatorDownloadInfoType, EmulatorPackageType } from "@/shared/constants";
|
|
||||||
import { FrontendNotification } from "@/shared/types";
|
|
||||||
|
|
||||||
export interface AppEventMap
|
|
||||||
{
|
|
||||||
exitapp: [];
|
|
||||||
notification: [FrontendNotification];
|
|
||||||
focus: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmulatorPostInstallContext
|
|
||||||
{
|
|
||||||
emulator: string;
|
|
||||||
emulatorPackage?: EmulatorPackageType;
|
|
||||||
path: string;
|
|
||||||
update: boolean;
|
|
||||||
info: EmulatorDownloadInfoType;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { $, sleep } from 'bun';
|
import { $, sleep } from 'bun';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { SettingsType } from '@/shared/constants';
|
import { SettingsType, KeysWithValueAssignableTo } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { config } from './api/app';
|
import { config } from './api/app';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import packageDef from '~/package.json';
|
import packageDef from '~/package.json';
|
||||||
import { KeysWithValueAssignableTo } from '@/shared/types';
|
|
||||||
|
|
||||||
export function checkRunning (pid: number)
|
export function checkRunning (pid: number)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
|
||||||
import { createWriteStream } from "node:fs";
|
import { createWriteStream } from "node:fs";
|
||||||
import { config, jar } from "../api/app";
|
import { config, jar } from "../api/app";
|
||||||
import { moveAllFiles } from "../utils";
|
import { moveAllFiles } from "../utils";
|
||||||
import { DownloadFileEntry } from "@/shared/types";
|
import { DownloadFileEntry } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export interface ProgressStats
|
export interface ProgressStats
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { SystemInfoContext } from "../scripts/contexts";
|
import { SystemInfoContext } from "../scripts/contexts";
|
||||||
import { systemApi } from "../scripts/clientApi";
|
import { systemApi } from "../scripts/clientApi";
|
||||||
import { SystemInfoType } from "@/shared/constants";
|
import { SystemInfoType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import LoadingScreen from "./LoadingScreen";
|
import LoadingScreen from "./LoadingScreen";
|
||||||
import { GamepadKeyboard } from "./GamepadKeyboard";
|
import { GamepadKeyboard } from "./GamepadKeyboard";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { JSX, Suspense } from 'react';
|
||||||
import { FloatingShortcuts } from './Shortcuts';
|
import { FloatingShortcuts } from './Shortcuts';
|
||||||
import { AutoFocus } from './AutoFocus';
|
import { AutoFocus } from './AutoFocus';
|
||||||
import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts';
|
import { GamePadButtonCode, useShortcuts } from '../scripts/shortcuts';
|
||||||
import { GameListFilterType } from '@/shared/constants';
|
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { HandleGoBack } from '../scripts/utils';
|
import { HandleGoBack } from '../scripts/utils';
|
||||||
import LoadingCardList from './LoadingCardList';
|
import LoadingCardList from './LoadingCardList';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { FocusEventHandler, useContext, useRef, useState } from "react";
|
||||||
import path from "pathe";
|
import path from "pathe";
|
||||||
import { Check, File, FileInput, Folder, FolderInput, FolderOutput, FolderPlus, HardDrive, Usb, X } from "lucide-react";
|
import { Check, File, FileInput, Folder, FolderInput, FolderOutput, FolderPlus, HardDrive, Usb, X } from "lucide-react";
|
||||||
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { FocusContext, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { DirType } from "@/shared/constants";
|
import { DirType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
import { GamePadButtonCode, Shortcut, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { FileQuestion, HardDrive, Store } from "lucide-react";
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { FOCUS_KEYS } from "../scripts/types";
|
import { FOCUS_KEYS } from "../scripts/types";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { FrontEndGameType, FrontEndId } from "@/shared/types";
|
import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default function FrontEndGameCard (data: { index: number, game: FrontEndGameType; showSource?: boolean; } & FocusParams & InteractParams)
|
export default function FrontEndGameCard (data: { index: number, game: FrontEndGameType; showSource?: boolean; } & FocusParams & InteractParams)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { GameMetaExtra, CardList } from "./CardList";
|
import { GameMetaExtra, CardList } from "./CardList";
|
||||||
import { DefaultRommStaleTime, GameListFilterType, RPC_URL } from "@shared/constants";
|
import { DefaultRommStaleTime, RPC_URL } from "@shared/constants";
|
||||||
|
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { HardDrive } from "lucide-react";
|
import { HardDrive } from "lucide-react";
|
||||||
import { JSX, useContext } from "react";
|
import { JSX, useContext } from "react";
|
||||||
import { useLocalSetting } from "../scripts/utils";
|
import { useLocalSetting } from "../scripts/utils";
|
||||||
import { AnimatedBackgroundContext } from "../scripts/contexts";
|
import { AnimatedBackgroundContext } from "../scripts/contexts";
|
||||||
import { allGamesQuery } from "@queries/romm";
|
import { allGamesQuery } from "@queries/romm";
|
||||||
import { FrontEndGameType, FrontEndId } from "@/shared/types";
|
import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export interface GameListParams extends FocusParams
|
export interface GameListParams extends FocusParams
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function buildWheel (side: 0 | 1, shift: boolean, characters: boolean)
|
||||||
const elements: JSX.Element[] = [];
|
const elements: JSX.Element[] = [];
|
||||||
const refs: RefObject<HTMLSpanElement | null>[] = [];
|
const refs: RefObject<HTMLSpanElement | null>[] = [];
|
||||||
const positions: { left: string; top: string; }[] = [];
|
const positions: { left: string; top: string; }[] = [];
|
||||||
const W = 258, C = 129, R2 = 107, R1 = 42, n = GetKeys(characters)[side].length, GAP = 0.028;
|
const n = GetKeys(characters)[side].length, GAP = 0.028;
|
||||||
|
|
||||||
for (let i = 0; i < n; i++)
|
for (let i = 0; i < n; i++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { oneShot } from "../scripts/audio/audio";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import { RoundButton } from "./RoundButton";
|
import { RoundButton } from "./RoundButton";
|
||||||
import { useEventListener } from "usehooks-ts";
|
import { useEventListener } from "usehooks-ts";
|
||||||
import useActiveControl from "../scripts/gamepads";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
function SearchInput (data: {
|
function SearchInput (data: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { setFocus, useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { FOCUS_KEYS } from "../scripts/types";
|
import { FOCUS_KEYS } from "../scripts/types";
|
||||||
import { useIntersectionObserver } from "usehooks-ts";
|
import { useIntersectionObserver } from "usehooks-ts";
|
||||||
|
import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default function LoadMoreButton (data: { isFetching: boolean; hidden?: boolean, lastId?: FrontEndId; } & FocusParams & InteractParams)
|
export default function LoadMoreButton (data: { isFetching: boolean; hidden?: boolean, lastId?: FrontEndId; } & FocusParams & InteractParams)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { RPC_URL } from "@/shared/constants";
|
import { RPC_URL } from "@/shared/constants";
|
||||||
import { FrontendNotification } from "@/shared/types";
|
import { FrontendNotification } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
import { Clock, CloudUpload, Save } from "lucide-react";
|
import { Clock, CloudUpload, Save } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import toast, { ToastOptions } from "react-hot-toast";
|
import toast, { ToastOptions } from "react-hot-toast";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { GameListFilterType } from "@/shared/constants";
|
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { RoundButton } from "./RoundButton";
|
import { RoundButton } from "./RoundButton";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
|
||||||
|
|
@ -6,7 +6,7 @@ import { useFocusable, FocusContext } from "@noriginmedia/norigin-spatial-naviga
|
||||||
import { ArrowDownAz, ClockArrowDown, CalendarArrowDown, Rocket, HardDrive, SortDesc, User, Drama, FunnelX, Store } from "lucide-react";
|
import { ArrowDownAz, ClockArrowDown, CalendarArrowDown, Rocket, HardDrive, SortDesc, User, Drama, FunnelX, Store } from "lucide-react";
|
||||||
import { sourceIconMap } from "./Constants";
|
import { sourceIconMap } from "./Constants";
|
||||||
import { useContextDialog, ContextList, DialogEntry } from "./ContextDialog";
|
import { useContextDialog, ContextList, DialogEntry } from "./ContextDialog";
|
||||||
import { FrontEndFilterLists } from "@/shared/types";
|
import { FrontEndFilterLists } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
function FilterButton (data: {
|
function FilterButton (data: {
|
||||||
id: string,
|
id: string,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import { FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement } from "@/shared/types";
|
import { FrontEndGameTypeDetailed, FrontEndGameTypeDetailedAchievement } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
import { useFocusable } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { Medal } from "lucide-react";
|
import { Medal } from "lucide-react";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import ActionButton from "./ActionButton";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
import FocusTooltip from "../FocusTooltip";
|
import FocusTooltip from "../FocusTooltip";
|
||||||
import { useBlocker, useNavigate, useRouter } from "@tanstack/react-router";
|
import { useBlocker, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
import { FrontEndGameTypeDetailed } from "@/shared/types";
|
import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractParams)
|
function AchievementsInfo (data: { game: FrontEndGameTypeDetailed; } & InteractParams)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import prettyMilliseconds from 'pretty-ms';
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { validateSourceQuery } from "@/mainview/scripts/queries/romm";
|
import { validateSourceQuery } from "@/mainview/scripts/queries/romm";
|
||||||
import { sourceIconMap } from "../Constants";
|
import { sourceIconMap } from "../Constants";
|
||||||
import { FrontEndGameTypeDetailed } from "@/shared/types";
|
import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export function DetailElement (data: { icon: JSX.Element; tooltip?: string | null, children?: any | any[]; })
|
export function DetailElement (data: { icon: JSX.Element; tooltip?: string | null, children?: any | any[]; })
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import HeaderSearchField from "../HeaderSearchField";
|
||||||
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
import { scrollIntoViewHandler } from "@/mainview/scripts/utils";
|
import { scrollIntoViewHandler } from "@/mainview/scripts/utils";
|
||||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
import { FrontEndId, GameLookup } from "@/shared/types";
|
import { FrontEndId, GameLookup } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
import { gameLookupQuery } from "@/mainview/scripts/queries/romm";
|
import { gameLookupQuery } from "@/mainview/scripts/queries/romm";
|
||||||
import { Button } from "../options/Button";
|
import { Button } from "../options/Button";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ import { Clock, Crosshair, Download, EllipsisVertical, Import, PackageOpen, Play
|
||||||
import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm";
|
import { gameInvalidationQuery, installMutation, playMutation } from "@/mainview/scripts/queries/romm";
|
||||||
import ActionButton from "./ActionButton";
|
import ActionButton from "./ActionButton";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { DownloadSourceType } from "@/shared/constants";
|
|
||||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
|
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
import { CommandEntry, FrontEndGameTypeDetailed } from "@/shared/types";
|
import { CommandEntry, FrontEndGameTypeDetailed, DownloadSourceType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
|
export default function MainActions (data: { game?: FrontEndGameTypeDetailed, source: string, id: string; })
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ import { useState } from "react";
|
||||||
import { PathSettingsOptionBase, PathSettingsOptionParams } from "./PathSettingsOption";
|
import { PathSettingsOptionBase, PathSettingsOptionParams } from "./PathSettingsOption";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { changeDownloadsMutation, getSettingQuery } from "@queries/settings";
|
import { changeDownloadsMutation, getSettingQuery } from "@queries/settings";
|
||||||
import { SettingsType } from "@/shared/constants";
|
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
|
||||||
|
|
||||||
export default function DownloadDirectoryOption (data: PathSettingsOptionParams & { id: KeysWithValueAssignableTo<SettingsType, string>; })
|
export default function DownloadDirectoryOption (data: PathSettingsOptionParams & { id: KeysWithValueAssignableTo<SettingsType, string>; })
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { JSX } from "react";
|
import { JSX } from "react";
|
||||||
import { LocalSettingsSchema, LocalSettingsType } from "@shared/constants";
|
import { LocalSettingsSchema, LocalSettingsType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { OptionSpace } from "./OptionSpace";
|
import { OptionSpace } from "./OptionSpace";
|
||||||
import { OptionInput } from "./OptionInput";
|
import { OptionInput } from "./OptionInput";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { HTMLInputTypeAttribute, JSX, useEffect, useState } from "react";
|
import { HTMLInputTypeAttribute, JSX, useEffect, useState } from "react";
|
||||||
import { SettingsType } from "../../../shared/constants";
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { OptionSpace } from "./OptionSpace";
|
import { OptionSpace } from "./OptionSpace";
|
||||||
import { OptionInput } from "./OptionInput";
|
import { OptionInput } from "./OptionInput";
|
||||||
|
|
@ -9,7 +8,7 @@ import { ContextDialog } from "../ContextDialog";
|
||||||
import FilePicker from "../FilePicker";
|
import FilePicker from "../FilePicker";
|
||||||
import { setFocus } from "@noriginmedia/norigin-spatial-navigation";
|
import { setFocus } from "@noriginmedia/norigin-spatial-navigation";
|
||||||
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
||||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export interface PathSettingsOptionParams
|
export interface PathSettingsOptionParams
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { JSX, useCallback, useEffect, useState } from "react";
|
import { JSX, useCallback, useEffect, useState } from "react";
|
||||||
import { SettingsType } from "../../../shared/constants";
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { OptionSpace } from "./OptionSpace";
|
import { OptionSpace } from "./OptionSpace";
|
||||||
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
||||||
import { OptionDropdown } from "./OptionDropdown";
|
import { OptionDropdown } from "./OptionDropdown";
|
||||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export function SettingsDropdown (data: {
|
export function SettingsDropdown (data: {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { HTMLInputTypeAttribute, JSX, useCallback, useEffect, useState } from "react";
|
import { HTMLInputTypeAttribute, JSX, useCallback, useEffect, useState } from "react";
|
||||||
import { SettingsType } from "../../../shared/constants";
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { OptionSpace } from "./OptionSpace";
|
import { OptionSpace } from "./OptionSpace";
|
||||||
import { OptionInput } from "./OptionInput";
|
import { OptionInput } from "./OptionInput";
|
||||||
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
import { getSettingQuery, setSettingMutation } from "@queries/settings";
|
||||||
import { KeysWithValueAssignableTo } from "@/shared/types";
|
import { KeysWithValueAssignableTo, SettingsType } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export function SettingsOption (data: {
|
export function SettingsOption (data: {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { StoreEmulatorCard } from "./StoreEmulatorCard";
|
||||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
import Carousel from "../Carousel";
|
import Carousel from "../Carousel";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { FrontEndEmulator } from "@/shared/types";
|
import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant?: boolean; }) => void; })
|
function SeeAllCard (data: { id: string; onAction: () => void; onFocus?: (details: { node: HTMLElement, instant?: boolean; }) => void; })
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import FrontEndGameCard from "../FrontEndGameCard";
|
||||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
import Carousel from "../Carousel";
|
import Carousel from "../Carousel";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { FrontEndGameType, FrontEndId } from "@/shared/types";
|
import { FrontEndGameType, FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export function GamesSection (data: {
|
export function GamesSection (data: {
|
||||||
games?: FrontEndGameType[];
|
games?: FrontEndGameType[];
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { GamePadButtonCode, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||||
import { RPC_URL } from "@/shared/constants";
|
import { RPC_URL } from "@/shared/constants";
|
||||||
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
import { FOCUS_KEYS } from "@/mainview/scripts/types";
|
||||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||||
import { FrontEndEmulator } from "@/shared/types";
|
import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
// ── Single missing-emulator card ───────────────────────────────────────────
|
// ── Single missing-emulator card ───────────────────────────────────────────
|
||||||
interface MissingCardProps
|
interface MissingCardProps
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { JSX } from "react";
|
||||||
import { oneShot } from "@/mainview/scripts/audio/audio";
|
import { oneShot } from "@/mainview/scripts/audio/audio";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getUpdateInfoForEmulator } from "@/mainview/scripts/queries/store";
|
import { getUpdateInfoForEmulator } from "@/mainview/scripts/queries/store";
|
||||||
import { FrontEndEmulator } from "@/shared/types";
|
import { FrontEndEmulator } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export const emulatorStatusIcons: Record<string, JSX.Element> = {
|
export const emulatorStatusIcons: Record<string, JSX.Element> = {
|
||||||
store: <Store />,
|
store: <Store />,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { Route as SettingsAboutRouteImport } from './../routes/settings/about'
|
||||||
import { Route as GameAddRouteImport } from './../routes/game/add'
|
import { Route as GameAddRouteImport } from './../routes/game/add'
|
||||||
import { Route as StoreTabRouteRouteImport } from './../routes/store/tab/route'
|
import { Route as StoreTabRouteRouteImport } from './../routes/store/tab/route'
|
||||||
import { Route as StoreTabIndexRouteImport } from './../routes/store/tab/index'
|
import { Route as StoreTabIndexRouteImport } from './../routes/store/tab/index'
|
||||||
|
import { Route as StoreTabPluginsRouteImport } from './../routes/store/tab/plugins'
|
||||||
import { Route as StoreTabGamesRouteImport } from './../routes/store/tab/games'
|
import { Route as StoreTabGamesRouteImport } from './../routes/store/tab/games'
|
||||||
import { Route as StoreTabEmulatorsRouteImport } from './../routes/store/tab/emulators'
|
import { Route as StoreTabEmulatorsRouteImport } from './../routes/store/tab/emulators'
|
||||||
import { Route as SettingsPluginSourceRouteImport } from './../routes/settings/plugin.$source'
|
import { Route as SettingsPluginSourceRouteImport } from './../routes/settings/plugin.$source'
|
||||||
|
|
@ -30,6 +31,7 @@ import { Route as LauncherSourceIdRouteImport } from './../routes/launcher.$sour
|
||||||
import { Route as GameSourceIdRouteImport } from './../routes/game/$source.$id'
|
import { Route as GameSourceIdRouteImport } from './../routes/game/$source.$id'
|
||||||
import { Route as EmbeddedSourceIdRouteImport } from './../routes/embedded.$source.$id'
|
import { Route as EmbeddedSourceIdRouteImport } from './../routes/embedded.$source.$id'
|
||||||
import { Route as CollectionSourceIdRouteImport } from './../routes/collection.$source.$id'
|
import { Route as CollectionSourceIdRouteImport } from './../routes/collection.$source.$id'
|
||||||
|
import { Route as StoreDetailsPluginIdRouteImport } from './../routes/store/details.plugin.$id'
|
||||||
import { Route as StoreDetailsEmulatorIdRouteImport } from './../routes/store/details.emulator.$id'
|
import { Route as StoreDetailsEmulatorIdRouteImport } from './../routes/store/details.emulator.$id'
|
||||||
import { Route as GameUpdateSourceIdRouteImport } from './../routes/game/update.$source.$id'
|
import { Route as GameUpdateSourceIdRouteImport } from './../routes/game/update.$source.$id'
|
||||||
|
|
||||||
|
|
@ -98,6 +100,11 @@ const StoreTabIndexRoute = StoreTabIndexRouteImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => StoreTabRouteRoute,
|
getParentRoute: () => StoreTabRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const StoreTabPluginsRoute = StoreTabPluginsRouteImport.update({
|
||||||
|
id: '/plugins',
|
||||||
|
path: '/plugins',
|
||||||
|
getParentRoute: () => StoreTabRouteRoute,
|
||||||
|
} as any)
|
||||||
const StoreTabGamesRoute = StoreTabGamesRouteImport.update({
|
const StoreTabGamesRoute = StoreTabGamesRouteImport.update({
|
||||||
id: '/games',
|
id: '/games',
|
||||||
path: '/games',
|
path: '/games',
|
||||||
|
|
@ -138,6 +145,11 @@ const CollectionSourceIdRoute = CollectionSourceIdRouteImport.update({
|
||||||
path: '/collection/$source/$id',
|
path: '/collection/$source/$id',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const StoreDetailsPluginIdRoute = StoreDetailsPluginIdRouteImport.update({
|
||||||
|
id: '/store/details/plugin/$id',
|
||||||
|
path: '/store/details/plugin/$id',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const StoreDetailsEmulatorIdRoute = StoreDetailsEmulatorIdRouteImport.update({
|
const StoreDetailsEmulatorIdRoute = StoreDetailsEmulatorIdRouteImport.update({
|
||||||
id: '/store/details/emulator/$id',
|
id: '/store/details/emulator/$id',
|
||||||
path: '/store/details/emulator/$id',
|
path: '/store/details/emulator/$id',
|
||||||
|
|
@ -170,9 +182,11 @@ export interface FileRoutesByFullPath {
|
||||||
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
||||||
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
||||||
'/store/tab/games': typeof StoreTabGamesRoute
|
'/store/tab/games': typeof StoreTabGamesRoute
|
||||||
|
'/store/tab/plugins': typeof StoreTabPluginsRoute
|
||||||
'/store/tab/': typeof StoreTabIndexRoute
|
'/store/tab/': typeof StoreTabIndexRoute
|
||||||
'/game/update/$source/$id': typeof GameUpdateSourceIdRoute
|
'/game/update/$source/$id': typeof GameUpdateSourceIdRoute
|
||||||
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
||||||
|
'/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
|
@ -194,9 +208,11 @@ export interface FileRoutesByTo {
|
||||||
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
||||||
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
||||||
'/store/tab/games': typeof StoreTabGamesRoute
|
'/store/tab/games': typeof StoreTabGamesRoute
|
||||||
|
'/store/tab/plugins': typeof StoreTabPluginsRoute
|
||||||
'/store/tab': typeof StoreTabIndexRoute
|
'/store/tab': typeof StoreTabIndexRoute
|
||||||
'/game/update/$source/$id': typeof GameUpdateSourceIdRoute
|
'/game/update/$source/$id': typeof GameUpdateSourceIdRoute
|
||||||
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
||||||
|
'/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
|
|
@ -220,9 +236,11 @@ export interface FileRoutesById {
|
||||||
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
'/settings/plugin/$source': typeof SettingsPluginSourceRoute
|
||||||
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
'/store/tab/emulators': typeof StoreTabEmulatorsRoute
|
||||||
'/store/tab/games': typeof StoreTabGamesRoute
|
'/store/tab/games': typeof StoreTabGamesRoute
|
||||||
|
'/store/tab/plugins': typeof StoreTabPluginsRoute
|
||||||
'/store/tab/': typeof StoreTabIndexRoute
|
'/store/tab/': typeof StoreTabIndexRoute
|
||||||
'/game/update/$source/$id': typeof GameUpdateSourceIdRoute
|
'/game/update/$source/$id': typeof GameUpdateSourceIdRoute
|
||||||
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
'/store/details/emulator/$id': typeof StoreDetailsEmulatorIdRoute
|
||||||
|
'/store/details/plugin/$id': typeof StoreDetailsPluginIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
|
@ -247,9 +265,11 @@ export interface FileRouteTypes {
|
||||||
| '/settings/plugin/$source'
|
| '/settings/plugin/$source'
|
||||||
| '/store/tab/emulators'
|
| '/store/tab/emulators'
|
||||||
| '/store/tab/games'
|
| '/store/tab/games'
|
||||||
|
| '/store/tab/plugins'
|
||||||
| '/store/tab/'
|
| '/store/tab/'
|
||||||
| '/game/update/$source/$id'
|
| '/game/update/$source/$id'
|
||||||
| '/store/details/emulator/$id'
|
| '/store/details/emulator/$id'
|
||||||
|
| '/store/details/plugin/$id'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
|
|
@ -271,9 +291,11 @@ export interface FileRouteTypes {
|
||||||
| '/settings/plugin/$source'
|
| '/settings/plugin/$source'
|
||||||
| '/store/tab/emulators'
|
| '/store/tab/emulators'
|
||||||
| '/store/tab/games'
|
| '/store/tab/games'
|
||||||
|
| '/store/tab/plugins'
|
||||||
| '/store/tab'
|
| '/store/tab'
|
||||||
| '/game/update/$source/$id'
|
| '/game/update/$source/$id'
|
||||||
| '/store/details/emulator/$id'
|
| '/store/details/emulator/$id'
|
||||||
|
| '/store/details/plugin/$id'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
|
@ -296,9 +318,11 @@ export interface FileRouteTypes {
|
||||||
| '/settings/plugin/$source'
|
| '/settings/plugin/$source'
|
||||||
| '/store/tab/emulators'
|
| '/store/tab/emulators'
|
||||||
| '/store/tab/games'
|
| '/store/tab/games'
|
||||||
|
| '/store/tab/plugins'
|
||||||
| '/store/tab/'
|
| '/store/tab/'
|
||||||
| '/game/update/$source/$id'
|
| '/game/update/$source/$id'
|
||||||
| '/store/details/emulator/$id'
|
| '/store/details/emulator/$id'
|
||||||
|
| '/store/details/plugin/$id'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
|
|
@ -314,6 +338,7 @@ export interface RootRouteChildren {
|
||||||
PlatformSourceIdRoute: typeof PlatformSourceIdRoute
|
PlatformSourceIdRoute: typeof PlatformSourceIdRoute
|
||||||
GameUpdateSourceIdRoute: typeof GameUpdateSourceIdRoute
|
GameUpdateSourceIdRoute: typeof GameUpdateSourceIdRoute
|
||||||
StoreDetailsEmulatorIdRoute: typeof StoreDetailsEmulatorIdRoute
|
StoreDetailsEmulatorIdRoute: typeof StoreDetailsEmulatorIdRoute
|
||||||
|
StoreDetailsPluginIdRoute: typeof StoreDetailsPluginIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
|
|
@ -409,6 +434,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof StoreTabIndexRouteImport
|
preLoaderRoute: typeof StoreTabIndexRouteImport
|
||||||
parentRoute: typeof StoreTabRouteRoute
|
parentRoute: typeof StoreTabRouteRoute
|
||||||
}
|
}
|
||||||
|
'/store/tab/plugins': {
|
||||||
|
id: '/store/tab/plugins'
|
||||||
|
path: '/plugins'
|
||||||
|
fullPath: '/store/tab/plugins'
|
||||||
|
preLoaderRoute: typeof StoreTabPluginsRouteImport
|
||||||
|
parentRoute: typeof StoreTabRouteRoute
|
||||||
|
}
|
||||||
'/store/tab/games': {
|
'/store/tab/games': {
|
||||||
id: '/store/tab/games'
|
id: '/store/tab/games'
|
||||||
path: '/games'
|
path: '/games'
|
||||||
|
|
@ -465,6 +497,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof CollectionSourceIdRouteImport
|
preLoaderRoute: typeof CollectionSourceIdRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/store/details/plugin/$id': {
|
||||||
|
id: '/store/details/plugin/$id'
|
||||||
|
path: '/store/details/plugin/$id'
|
||||||
|
fullPath: '/store/details/plugin/$id'
|
||||||
|
preLoaderRoute: typeof StoreDetailsPluginIdRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/store/details/emulator/$id': {
|
'/store/details/emulator/$id': {
|
||||||
id: '/store/details/emulator/$id'
|
id: '/store/details/emulator/$id'
|
||||||
path: '/store/details/emulator/$id'
|
path: '/store/details/emulator/$id'
|
||||||
|
|
@ -511,12 +550,14 @@ const SettingsRouteRouteWithChildren = SettingsRouteRoute._addFileChildren(
|
||||||
interface StoreTabRouteRouteChildren {
|
interface StoreTabRouteRouteChildren {
|
||||||
StoreTabEmulatorsRoute: typeof StoreTabEmulatorsRoute
|
StoreTabEmulatorsRoute: typeof StoreTabEmulatorsRoute
|
||||||
StoreTabGamesRoute: typeof StoreTabGamesRoute
|
StoreTabGamesRoute: typeof StoreTabGamesRoute
|
||||||
|
StoreTabPluginsRoute: typeof StoreTabPluginsRoute
|
||||||
StoreTabIndexRoute: typeof StoreTabIndexRoute
|
StoreTabIndexRoute: typeof StoreTabIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const StoreTabRouteRouteChildren: StoreTabRouteRouteChildren = {
|
const StoreTabRouteRouteChildren: StoreTabRouteRouteChildren = {
|
||||||
StoreTabEmulatorsRoute: StoreTabEmulatorsRoute,
|
StoreTabEmulatorsRoute: StoreTabEmulatorsRoute,
|
||||||
StoreTabGamesRoute: StoreTabGamesRoute,
|
StoreTabGamesRoute: StoreTabGamesRoute,
|
||||||
|
StoreTabPluginsRoute: StoreTabPluginsRoute,
|
||||||
StoreTabIndexRoute: StoreTabIndexRoute,
|
StoreTabIndexRoute: StoreTabIndexRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,6 +578,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
PlatformSourceIdRoute: PlatformSourceIdRoute,
|
PlatformSourceIdRoute: PlatformSourceIdRoute,
|
||||||
GameUpdateSourceIdRoute: GameUpdateSourceIdRoute,
|
GameUpdateSourceIdRoute: GameUpdateSourceIdRoute,
|
||||||
StoreDetailsEmulatorIdRoute: StoreDetailsEmulatorIdRoute,
|
StoreDetailsEmulatorIdRoute: StoreDetailsEmulatorIdRoute,
|
||||||
|
StoreDetailsPluginIdRoute: StoreDetailsPluginIdRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
|
||||||
import { getRomApiRomsIdGetOptions, getRomsApiRomsGetOptions } from "../clients/romm/@tanstack/react-query.gen";
|
import { getRomApiRomsIdGetOptions, getRomsApiRomsGetOptions } from "../clients/romm/@tanstack/react-query.gen";
|
||||||
import { DefaultRommStaleTime, GameListFilterType } from "../shared/constants";
|
import { DefaultRommStaleTime } from "../shared/constants";
|
||||||
|
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
|
||||||
export function gamesQueryOptions (filter?: GameListFilterType)
|
export function gamesQueryOptions (filter?: GameListFilterType)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { AnimatedBackgroundContext } from '../scripts/contexts';
|
||||||
import { getCollectionQuery } from '@queries/romm';
|
import { getCollectionQuery } from '@queries/romm';
|
||||||
import { zodValidator } from '@tanstack/zod-adapter';
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { GameListFilterType } from '@/shared/constants';
|
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { useLocalStorage } from 'usehooks-ts';
|
import { useLocalStorage } from 'usehooks-ts';
|
||||||
|
|
||||||
export const Route = createFileRoute('/collection/$source/$id')({
|
export const Route = createFileRoute('/collection/$source/$id')({
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import Details from "@/mainview/components/game/Details";
|
||||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||||
import SelectMenu from "@/mainview/components/SelectMenu";
|
import SelectMenu from "@/mainview/components/SelectMenu";
|
||||||
import { IGDBIcon } from "@/mainview/scripts/brandIcons";
|
import { IGDBIcon } from "@/mainview/scripts/brandIcons";
|
||||||
import { FrontEndGameTypeDetailed } from "@/shared/types";
|
import { FrontEndGameTypeDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export const Route = createFileRoute("/game/$source/$id")({
|
export const Route = createFileRoute("/game/$source/$id")({
|
||||||
loader: async ({ params, context }) =>
|
loader: async ({ params, context }) =>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
import { AnimatedBackground } from '@/mainview/components/AnimatedBackground';
|
||||||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||||
import GameLookupElement from '@/mainview/components/game/GameLookup';
|
import GameLookupElement from '@/mainview/components/game/GameLookup';
|
||||||
import { HeaderUI, StickyHeaderUI } from '@/mainview/components/Header';
|
import { HeaderUI } from '@/mainview/components/Header';
|
||||||
import { FloatingShortcuts } from '@/mainview/components/Shortcuts';
|
import { FloatingShortcuts } from '@/mainview/components/Shortcuts';
|
||||||
import { customUpdateMutation, gameInvalidationQuery, gameQuery } from '@/mainview/scripts/queries/romm';
|
import { customUpdateMutation, gameInvalidationQuery, gameQuery } from '@/mainview/scripts/queries/romm';
|
||||||
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||||
import { HandleGoBack } from '@/mainview/scripts/utils';
|
import { HandleGoBack } from '@/mainview/scripts/utils';
|
||||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, useNavigate, useRouter } from '@tanstack/react-router';
|
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export const Route = createFileRoute('/game/update/$source/$id')({
|
export const Route = createFileRoute('/game/update/$source/$id')({
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
import { CollectionsDetail } from '../components/CollectionsDetail';
|
import { CollectionsDetail } from '../components/CollectionsDetail';
|
||||||
import { zodValidator } from '@tanstack/zod-adapter';
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { GameListFilterType } from '@/shared/constants';
|
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { useSessionStorage } from 'usehooks-ts';
|
import { useSessionStorage } from 'usehooks-ts';
|
||||||
import HeaderSearchField from '../components/HeaderSearchField';
|
import HeaderSearchField from '../components/HeaderSearchField';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ import SelectMenu from "../components/SelectMenu";
|
||||||
import HeaderSearchField from "../components/HeaderSearchField";
|
import HeaderSearchField from "../components/HeaderSearchField";
|
||||||
import CardElement from "../components/CardElement";
|
import CardElement from "../components/CardElement";
|
||||||
import { Router } from "..";
|
import { Router } from "..";
|
||||||
import { FrontEndId } from "@/shared/types";
|
import { FrontEndId } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: ConsoleHomeUI,
|
component: ConsoleHomeUI,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
import { createFileRoute, useRouter } from "@tanstack/react-router";
|
||||||
import { CollectionsDetail } from "../components/CollectionsDetail";
|
import { CollectionsDetail } from "../components/CollectionsDetail";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { GameListFilterType, RPC_URL } from "../../shared/constants";
|
import { RPC_URL } from "../../shared/constants";
|
||||||
|
import { GameListFilterType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { deletePlatformMutation, localPlatformFilter, platformQuery, updatePlatformMutation } from "@queries/romm";
|
import { deletePlatformMutation, localPlatformFilter, platformQuery, updatePlatformMutation } from "@queries/romm";
|
||||||
import { zodValidator } from "@tanstack/zod-adapter";
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ import
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { RommLoginDataSchema, RPC_URL } from "@shared/constants";
|
import { RPC_URL } from "@shared/constants";
|
||||||
|
import { RommLoginDataSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { OptionSpace } from "../../components/options/OptionSpace";
|
import { OptionSpace } from "../../components/options/OptionSpace";
|
||||||
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
import { useSettingsForm, useSettingsFormContext } from "../../components/options/SettingsAppForm";
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,13 @@ import { systemApi } from '@/mainview/scripts/clientApi';
|
||||||
import useActiveControl from '@/mainview/scripts/gamepads';
|
import useActiveControl from '@/mainview/scripts/gamepads';
|
||||||
import { changeDownloadsMutation } from '@queries/settings';
|
import { changeDownloadsMutation } from '@queries/settings';
|
||||||
import { downloadDrivesQuery } from '@/mainview/scripts/queries/system';
|
import { downloadDrivesQuery } from '@/mainview/scripts/queries/system';
|
||||||
import { DownloadsDrive } from '@/shared/types';
|
import { DownloadsDrive } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/directories')({
|
export const Route = createFileRoute('/settings/directories')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; refetchDrives: () => void; })
|
function DriveComponent (data: { drive: DownloadsDrive; downloadsSize: number; refetchDrives: () => void; })
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ import { Check, ChevronDown, FolderSearch, HardDrive, Plug, SearchAlert, Store,
|
||||||
import { ContextDialog, ContextList, DialogEntry, OptionElement } from '../../components/ContextDialog';
|
import { ContextDialog, ContextList, DialogEntry, OptionElement } from '../../components/ContextDialog';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { RPC_URL, SettingsSchema } from '../../../shared/constants';
|
import { RPC_URL } from '../../../shared/constants';
|
||||||
|
import { SettingsSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import emulators from '@emulators';
|
import emulators from '@emulators';
|
||||||
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, setFocus, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||||
|
|
@ -20,11 +21,14 @@ import { FOCUS_KEYS } from '@/mainview/scripts/types';
|
||||||
import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils';
|
import { scrollIntoNearestParent, scrollIntoViewHandler, useDragScroll } from '@/mainview/scripts/utils';
|
||||||
import { SettingsOption } from '@/mainview/components/options/SettingsOption';
|
import { SettingsOption } from '@/mainview/components/options/SettingsOption';
|
||||||
import { SettingsDropdown } from '@/mainview/components/options/SettingsDropdown';
|
import { SettingsDropdown } from '@/mainview/components/options/SettingsDropdown';
|
||||||
import { FrontEndEmulator } from '@/shared/types';
|
import { FrontEndEmulator } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/emulators')({
|
export const Route = createFileRoute('/settings/emulators')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
pendingComponent: EmulatorsPending,
|
pendingComponent: EmulatorsPending,
|
||||||
|
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function EmulatorsPending ()
|
function EmulatorsPending ()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { LocalOption } from '@/mainview/components/options/LocalOption';
|
import { LocalOption } from '@/mainview/components/options/LocalOption';
|
||||||
import { LocalSettingsSchema, settingRegistry } from '@/shared/constants';
|
import { settingRegistry } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
import { LocalSettingsSchema } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { Terminal } from 'lucide-react';
|
import { Terminal } from 'lucide-react';
|
||||||
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/interface')({
|
export const Route = createFileRoute('/settings/interface')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
validateSearch: zodValidator(z.object({ focus: z.string().optional() }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent ()
|
function RouteComponent ()
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,25 @@ import { OptionDropdown } from '@/mainview/components/options/OptionDropdown';
|
||||||
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
||||||
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
||||||
import { RoundButton } from '@/mainview/components/RoundButton';
|
import { RoundButton } from '@/mainview/components/RoundButton';
|
||||||
import { getPluginDetailsQuery } from '@/mainview/scripts/queries/plugins';
|
import { allPluginsFilter, getPluginDetailsQuery, updatePluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||||
import { getPluginActionsQuery, getPluginSettingQuery, getPluginSettingsDefinitionQuery, pluginActionMutation, setPluginSettingMutation } from '@/mainview/scripts/queries/settings';
|
import { getPluginActionsQuery, getPluginSettingQuery, getPluginSettingsDefinitionQuery, pluginActionMutation, setPluginSettingMutation } from '@/mainview/scripts/queries/settings';
|
||||||
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||||
import { scrollIntoViewHandler } from '@/mainview/scripts/utils';
|
import { scrollIntoViewHandler } from '@/mainview/scripts/utils';
|
||||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
|
import { PluginUpdateCheck } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
import { JSONSchema7 } from 'json-schema';
|
import { JSONSchema7 } from 'json-schema';
|
||||||
import { ArrowLeft, CirclePlay, Settings2 } from 'lucide-react';
|
import { ArrowLeft, ArrowRight, CircleFadingArrowUp, CirclePlay, Settings2 } from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
export const Route = createFileRoute('/settings/plugin/$source')({
|
export const Route = createFileRoute('/settings/plugin/$source')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
pendingComponent: Loading,
|
pendingComponent: Loading,
|
||||||
async loader (ctx)
|
async loader (ctx)
|
||||||
{
|
{
|
||||||
const definitions = await ctx.context.queryClient.fetchQuery(getPluginSettingsDefinitionQuery(ctx.params.source));
|
const source = decodeURIComponent(ctx.params.source);
|
||||||
const actions = await ctx.context.queryClient.fetchQuery(getPluginActionsQuery(ctx.params.source));
|
const definitions = await ctx.context.queryClient.fetchQuery(getPluginSettingsDefinitionQuery(source));
|
||||||
|
const actions = await ctx.context.queryClient.fetchQuery(getPluginActionsQuery(source));
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
return { definitions, actions };
|
return { definitions, actions };
|
||||||
},
|
},
|
||||||
|
|
@ -38,7 +40,8 @@ function Loading ()
|
||||||
|
|
||||||
function PluginAction (data: { id: string, title: string | undefined, description: string | undefined; action: string; reload: () => void; })
|
function PluginAction (data: { id: string, title: string | undefined, description: string | undefined; action: string; reload: () => void; })
|
||||||
{
|
{
|
||||||
const { source } = Route.useParams();
|
const { source: sourceRaw } = Route.useParams();
|
||||||
|
const source = decodeURIComponent(sourceRaw);
|
||||||
const action = useMutation({
|
const action = useMutation({
|
||||||
...pluginActionMutation(source, data.id),
|
...pluginActionMutation(source, data.id),
|
||||||
onSuccess (acitonData, variables, onMutateResult, context)
|
onSuccess (acitonData, variables, onMutateResult, context)
|
||||||
|
|
@ -67,7 +70,8 @@ function PluginAction (data: { id: string, title: string | undefined, descriptio
|
||||||
|
|
||||||
function PluginOption (data: { name: string, title?: string, prop: JSONSchema7; })
|
function PluginOption (data: { name: string, title?: string, prop: JSONSchema7; })
|
||||||
{
|
{
|
||||||
const { source } = Route.useParams();
|
const { source: sourceRaw } = Route.useParams();
|
||||||
|
const source = decodeURIComponent(sourceRaw);
|
||||||
const { data: value, refetch: refetchValue } = useQuery(getPluginSettingQuery(source, data.name));
|
const { data: value, refetch: refetchValue } = useQuery(getPluginSettingQuery(source, data.name));
|
||||||
const setValue = useMutation({
|
const setValue = useMutation({
|
||||||
...setPluginSettingMutation(source, data.name),
|
...setPluginSettingMutation(source, data.name),
|
||||||
|
|
@ -108,12 +112,21 @@ function PluginOption (data: { name: string, title?: string, prop: JSONSchema7;
|
||||||
</OptionSpace>;
|
</OptionSpace>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Settings ()
|
function Settings (data: { update: PluginUpdateCheck | undefined; })
|
||||||
{
|
{
|
||||||
const { definitions, actions } = Route.useLoaderData();
|
const { definitions, actions } = Route.useLoaderData();
|
||||||
const { source } = Route.useParams();
|
const { source: sourceRaw } = Route.useParams();
|
||||||
|
const source = decodeURIComponent(sourceRaw);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const update = useMutation({
|
||||||
|
...updatePluginMutation(source),
|
||||||
|
onSuccess (data, variables, onMutateResult, context)
|
||||||
|
{
|
||||||
|
context.client.invalidateQueries(allPluginsFilter);
|
||||||
|
navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(source) }, replace: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
const handleReload = () =>
|
const handleReload = () =>
|
||||||
{
|
{
|
||||||
queryClient.refetchQueries(getPluginSettingsDefinitionQuery(source));
|
queryClient.refetchQueries(getPluginSettingsDefinitionQuery(source));
|
||||||
|
|
@ -121,7 +134,7 @@ function Settings ()
|
||||||
};
|
};
|
||||||
const { ref, focusKey } = useFocusable({
|
const { ref, focusKey } = useFocusable({
|
||||||
focusKey: 'plugin-settings',
|
focusKey: 'plugin-settings',
|
||||||
focusable: (definitions?.properties && Object.keys(definitions?.properties).length > 0) || actions.length > 0
|
focusable: (definitions?.properties && Object.keys(definitions?.properties).length > 0) || actions.length > 0 || !!data.update
|
||||||
});
|
});
|
||||||
return <div ref={ref}>
|
return <div ref={ref}>
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
|
|
@ -148,6 +161,15 @@ function Settings ()
|
||||||
|
|
||||||
})}
|
})}
|
||||||
<div className="divider"><CirclePlay className='size-14' /> Actions</div>
|
<div className="divider"><CirclePlay className='size-14' /> Actions</div>
|
||||||
|
{!!data.update && <OptionSpace
|
||||||
|
id="update-option-space"
|
||||||
|
label={
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<div>Update</div>
|
||||||
|
<div className='flex gap-2 text-sm text-base-content/40 text-wrap'>{data?.update?.current} {'>'} {data?.update?.new}</div>
|
||||||
|
</div>}>
|
||||||
|
<Button style='warning' id='update-plugin-btn' onAction={e => update.mutate()} >{update.isPending ? <span className="loading loading-spinner loading-lg"></span> : <CircleFadingArrowUp />}Update</Button>
|
||||||
|
</OptionSpace>}
|
||||||
{actions?.map(a => <PluginAction key={a.id} id={a.id} title={a.title} description={a.description} action={a.action} reload={handleReload} />)}
|
{actions?.map(a => <PluginAction key={a.id} id={a.id} title={a.title} description={a.description} action={a.action} reload={handleReload} />)}
|
||||||
</FocusContext>
|
</FocusContext>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
@ -155,7 +177,8 @@ function Settings ()
|
||||||
|
|
||||||
function RouteComponent ()
|
function RouteComponent ()
|
||||||
{
|
{
|
||||||
const { source } = Route.useParams();
|
const { source: sourceRaw } = Route.useParams();
|
||||||
|
const source = decodeURIComponent(sourceRaw);
|
||||||
|
|
||||||
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugins' });
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugins' });
|
||||||
const { data } = useQuery(getPluginDetailsQuery(source));
|
const { data } = useQuery(getPluginDetailsQuery(source));
|
||||||
|
|
@ -167,17 +190,17 @@ function RouteComponent ()
|
||||||
<FocusContext value={focusKey}>
|
<FocusContext value={focusKey}>
|
||||||
|
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
<div className='flex text-2xl font-bold gap-2 grow items-center justify-center'>
|
<div className='flex gap-2 grow items-center justify-center'>
|
||||||
<RoundButton onFocus={scrollIntoViewHandler({ inline: 'end' })} id='return-to-plugins' onAction={handleReturn}><ArrowLeft /></RoundButton>
|
<RoundButton onFocus={scrollIntoViewHandler({ inline: 'end' })} id='return-to-plugins' onAction={handleReturn}><ArrowLeft /></RoundButton>
|
||||||
<img className='h-12' src={data?.icon}></img>
|
<img className='h-12' src={data?.icon}></img>
|
||||||
{data?.displayName}
|
<div className='text-2xl font-bold'>{data?.displayName}</div>
|
||||||
|
<div className='px-3 bg-base-300 rounded-full font-semibold'>{data?.version}</div>
|
||||||
|
{!!data?.update && <div className='flex gap-2'> <ArrowRight /><div className='px-3 bg-warning text-warning-content rounded-full font-semibold'>{data?.update.new}</div></div>}
|
||||||
</div>
|
</div>
|
||||||
<ul className='flex gap-2 justify-center'>{data?.keywords?.map((k, i) => <li key={i} className='bg-base-200 rounded-full p-2 px-4'>{k}</li>)}</ul>
|
<ul className='flex gap-2 justify-center'>{data?.keywords?.map((k, i) => <li key={i} className='bg-base-200 rounded-full p-2 px-4'>{k}</li>)}</ul>
|
||||||
<div className='bg-base-200 p-4 rounded-2xl'>{data?.description}</div>
|
<div className='bg-base-200 p-4 rounded-2xl'>{data?.description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Settings update={data?.update} />
|
||||||
<Settings />
|
|
||||||
|
|
||||||
</FocusContext>
|
</FocusContext>
|
||||||
<AutoFocus focus={focusSelf} />
|
<AutoFocus focus={focusSelf} />
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import { pluginCategoryIcons, pluginCategoryPriorities } from '@/mainview/compon
|
||||||
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
import { OptionInput } from '@/mainview/components/options/OptionInput';
|
||||||
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
import { OptionSpace } from '@/mainview/components/options/OptionSpace';
|
||||||
import { RoundButton } from '@/mainview/components/RoundButton';
|
import { RoundButton } from '@/mainview/components/RoundButton';
|
||||||
import { enablePluginMutation, getAllPluginsQuery } from '@/mainview/scripts/queries/plugins';
|
import { enablePluginMutation, getAllPluginsQuery, uninstallPluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||||
import { GamePadButtonCode, Shortcut } from '@/mainview/scripts/shortcuts';
|
import { GamePadButtonCode, Shortcut } from '@/mainview/scripts/shortcuts';
|
||||||
import { FrontendPlugin } from '@/shared/types';
|
import { FrontendPlugin } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
import { Eye, Puzzle, Search, Settings2 } from 'lucide-react';
|
import { CircleFadingArrowUp, Eye, Puzzle, Settings2, Trash } from 'lucide-react';
|
||||||
|
|
||||||
export const Route = createFileRoute('/settings/plugins')({
|
export const Route = createFileRoute('/settings/plugins')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
@ -33,7 +33,9 @@ function Plugin (data: {
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const handleDetails = () => navigate({ to: '/settings/plugin/$source', params: { source: data.plugin.name }, replace: true, viewTransition: { types: ['slide-up'] } });
|
const uninstall = useMutation(uninstallPluginMutation(data.plugin.name));
|
||||||
|
const handleUninstall = () => uninstall.mutate();
|
||||||
|
const handleDetails = () => navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(data.plugin.name) }, replace: true, viewTransition: { types: ['slide-up'] } });
|
||||||
|
|
||||||
return <OptionSpace
|
return <OptionSpace
|
||||||
label={
|
label={
|
||||||
|
|
@ -42,10 +44,13 @@ function Plugin (data: {
|
||||||
{data.plugin.icon ? <img src={data.plugin.icon}></img> : <Puzzle />}
|
{data.plugin.icon ? <img src={data.plugin.icon}></img> : <Puzzle />}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div>{data.plugin.displayName}</div>
|
<div>{data.plugin.displayName ?? data.plugin.name}</div>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<div className=' text-sm text-base-content/40'>{data.plugin.name} ({data.plugin.version})</div>
|
<div className=' text-sm text-base-content/40'>{data.plugin.name} ({data.plugin.version})</div>
|
||||||
{data.plugin.hasSettings && <Settings2 className='bg-base-300 rounded-full p-1 size-6' />}
|
{data.plugin.hasSettings && <Settings2 className='bg-base-300 rounded-full p-1 size-6' />}
|
||||||
|
{data.plugin.update && <div className={data.plugin.update.new} data-tip="hello">
|
||||||
|
<CircleFadingArrowUp className='bg-warning text-warning-content rounded-full p-1 size-6' />
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -55,6 +60,7 @@ function Plugin (data: {
|
||||||
>
|
>
|
||||||
<div className='flex gap-4'>
|
<div className='flex gap-4'>
|
||||||
<RoundButton className='size-12 p-1' onAction={handleDetails} id={`${data.plugin.name}-details`} >{data.plugin.hasSettings ? <Settings2 /> : <Eye />}</RoundButton>
|
<RoundButton className='size-12 p-1' onAction={handleDetails} id={`${data.plugin.name}-details`} >{data.plugin.hasSettings ? <Settings2 /> : <Eye />}</RoundButton>
|
||||||
|
{data.plugin.canUninstall && <RoundButton className='size-12 p-1' onAction={handleUninstall} id={`${data.plugin.name}-uninstall`} >{uninstall.isPending ? <span className="loading loading-spinner loading-lg"></span> : <Trash />}</RoundButton>}
|
||||||
{data.plugin.canDisable && <OptionInput compact onChange={v => data.setEnabled(!!v)} value={data.plugin.enabled} name={data.plugin.name} type="checkbox" />}
|
{data.plugin.canDisable && <OptionInput compact onChange={v => data.setEnabled(!!v)} value={data.plugin.enabled} name={data.plugin.name} type="checkbox" />}
|
||||||
</div>
|
</div>
|
||||||
</OptionSpace>;
|
</OptionSpace>;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import DotsLoading from '@/mainview/components/backgrounds/dots';
|
||||||
import { Button } from '@/mainview/components/options/Button';
|
import { Button } from '@/mainview/components/options/Button';
|
||||||
import { checkUpdateMutation, hasUpdateQuery, updateMutation } from '@/mainview/scripts/queries/system';
|
import { checkUpdateMutation, hasUpdateQuery, updateMutation } from '@/mainview/scripts/queries/system';
|
||||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
import { CircleFadingArrowUp, RefreshCcw } from 'lucide-react';
|
import { CircleFadingArrowUp, RefreshCcw } from 'lucide-react';
|
||||||
import { MarkdownAsync } from 'react-markdown';
|
import { MarkdownAsync } from 'react-markdown';
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import FocusTooltip from "@/mainview/components/FocusTooltip";
|
||||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||||
import { FilterUI } from "@/mainview/components/Filters";
|
import { FilterUI } from "@/mainview/components/Filters";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { FrontEndEmulatorDetailed } from "@/shared/types";
|
import { FrontEndEmulatorDetailed } from "@simeonradivoev/gameflow-sdk/shared";
|
||||||
|
|
||||||
export const Route = createFileRoute('/store/details/emulator/$id')({
|
export const Route = createFileRoute('/store/details/emulator/$id')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
|
||||||
161
src/mainview/routes/store/details.plugin.$id.tsx
Normal file
161
src/mainview/routes/store/details.plugin.$id.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||||
|
import DotsLoading from '@/mainview/components/backgrounds/dots';
|
||||||
|
import { StickyHeaderUI } from '@/mainview/components/Header';
|
||||||
|
import LoadingScreen from '@/mainview/components/LoadingScreen';
|
||||||
|
import { Button } from '@/mainview/components/options/Button';
|
||||||
|
import { FloatingShortcuts } from '@/mainview/components/Shortcuts';
|
||||||
|
import StatList, { StatEntry } from '@/mainview/components/StatList';
|
||||||
|
import { installPluginMutation, pluginFilter, uninstallPluginMutation, updatePluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||||
|
import { pluginDetailsQuery } from '@/mainview/scripts/queries/store';
|
||||||
|
import { GamePadButtonCode, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||||
|
import { HandleGoBack } from '@/mainview/scripts/utils';
|
||||||
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
|
import { QueryClient, useMutation } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, useNavigate, useRouter } from '@tanstack/react-router';
|
||||||
|
import { ArrowRight, CircleFadingArrowUp, Download, Settings, Trash } from 'lucide-react';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/store/details/plugin/$id')({
|
||||||
|
component: RouteComponent,
|
||||||
|
pendingComponent: Loading,
|
||||||
|
async loader (ctx)
|
||||||
|
{
|
||||||
|
const id = decodeURIComponent(ctx.params.id);
|
||||||
|
const data = await ctx.context.queryClient.fetchQuery(pluginDetailsQuery(id));
|
||||||
|
return { data };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function Loading ()
|
||||||
|
{
|
||||||
|
const { ref, focusSelf } = useFocusable({ focusKey: 'plugin-details' });
|
||||||
|
return <>
|
||||||
|
<DotsLoading ref={ref} />
|
||||||
|
<AutoFocus focus={focusSelf} />
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Details ()
|
||||||
|
{
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const plugin = decodeURIComponent(id);
|
||||||
|
const { data } = Route.useLoaderData();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handleRefresh = (client: QueryClient) =>
|
||||||
|
{
|
||||||
|
client.invalidateQueries(pluginFilter(plugin));
|
||||||
|
navigate({ to: '/store/details/plugin/$id', params: { id: encodeURIComponent(id) }, replace: true });
|
||||||
|
};
|
||||||
|
const update = useMutation({
|
||||||
|
...updatePluginMutation(plugin),
|
||||||
|
onSuccess (data, variables, onMutateResult, context)
|
||||||
|
{
|
||||||
|
handleRefresh(context.client);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const install = useMutation({
|
||||||
|
...installPluginMutation(plugin),
|
||||||
|
onSuccess (data, variables, onMutateResult, context)
|
||||||
|
{
|
||||||
|
handleRefresh(context.client);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const uninstall = useMutation({
|
||||||
|
...uninstallPluginMutation(plugin),
|
||||||
|
onSuccess (data, variables, onMutateResult, context)
|
||||||
|
{
|
||||||
|
handleRefresh(context.client);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats: StatEntry[] = [];
|
||||||
|
if (data.devDependencies)
|
||||||
|
{
|
||||||
|
stats.push({ content: Object.keys(data.devDependencies), label: "Dev Dependecies" });
|
||||||
|
}
|
||||||
|
if (data.dependencies)
|
||||||
|
{
|
||||||
|
stats.push({ content: Object.keys(data.dependencies), label: "Dependecies" });
|
||||||
|
}
|
||||||
|
if (data.maintainers)
|
||||||
|
{
|
||||||
|
stats.push({ content: data.maintainers.map(m => m.name), label: "Maintainers" });
|
||||||
|
}
|
||||||
|
if (data.dist)
|
||||||
|
{
|
||||||
|
stats.push({ content: prettyBytes(data.dist.unpackedSize), label: "Size" });
|
||||||
|
}
|
||||||
|
if (data.license)
|
||||||
|
{
|
||||||
|
stats.push({ content: data.license, label: "License" });
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
|
||||||
|
<div className='flex justify-between p-8'>
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
<div className='text-3xl font-bold'>{data.name}</div>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
{data.update ? <>
|
||||||
|
<div className='bg-base-300 px-2 rounded-full'>{data.update.from}</div>
|
||||||
|
<ArrowRight />
|
||||||
|
<div className='bg-warning text-warning-content px-2 rounded-full'>{data.version}</div>
|
||||||
|
</> :
|
||||||
|
<div className='bg-base-300 px-2 rounded-full'>{data.version}</div>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
by {data.author?.name ?? data._npmUser?.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
{data.installed && <>
|
||||||
|
{!!data.update && <Button onAction={e => update.mutate()} className='gap-2' style='warning' id='install-btn' >
|
||||||
|
{update.isPending ? <span className="loading loading-spinner loading-lg"></span> : <CircleFadingArrowUp />} Update
|
||||||
|
</Button>}
|
||||||
|
<Button onAction={e => uninstall.mutate()} className='gap-2' style='accent' id='install-btn' >
|
||||||
|
{uninstall.isPending ? <span className="loading loading-spinner loading-lg"></span> : <Trash />} Uninstall
|
||||||
|
</Button>
|
||||||
|
<Button external onAction={e => { navigate({ to: '/settings/plugin/$source', params: { source: encodeURIComponent(plugin) } }); }} className='gap-2' style='info' id='install-btn' >
|
||||||
|
<Settings /> Settings
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</>}
|
||||||
|
{!data.installed && <Button onAction={e => install.mutate()} className='gap-2' style='accent' id='install-btn' >
|
||||||
|
{install.isPending ? <span className="loading loading-spinner loading-lg"></span> : <Download />} Install
|
||||||
|
</Button>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divider">Details</div>
|
||||||
|
<div className='px-8'>
|
||||||
|
<div className='p-4 bg-base-200 rounded-2xl'>{data.description}</div>
|
||||||
|
<StatList id={'plugin-stats'} stats={stats} />
|
||||||
|
</div>
|
||||||
|
<div className="divider">Keywords</div>
|
||||||
|
<div className='flex gap-2 px-8'>
|
||||||
|
{data.keywords.map(k => <li className='flex px-2 bg-base-300 rounded-full'>{k}</li>)}
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RouteComponent ()
|
||||||
|
{
|
||||||
|
const router = useRouter();
|
||||||
|
const { ref, focusKey, focusSelf } = useFocusable({ focusKey: 'plugin-details' });
|
||||||
|
useShortcuts(focusKey, () => [{
|
||||||
|
label: "Return", button: GamePadButtonCode.B, action (e)
|
||||||
|
{
|
||||||
|
HandleGoBack(router, e);
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
return <div ref={ref} className='absolute w-full h-full overflow-y-scroll overflow-x-hidden'>
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<StickyHeaderUI ref={ref} />
|
||||||
|
<Suspense fallback={<LoadingScreen />}>
|
||||||
|
<Details />
|
||||||
|
</Suspense>
|
||||||
|
<FloatingShortcuts />
|
||||||
|
</FocusContext>
|
||||||
|
<AutoFocus focus={focusSelf} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,13 @@ import LoadMoreButton from '@/mainview/components/LoadMoreButton';
|
||||||
import { storeGamesInfiniteQuery } from '@queries/store';
|
import { storeGamesInfiniteQuery } from '@queries/store';
|
||||||
import InvalidStoreError from '@/mainview/components/store/InvalidStoreError';
|
import InvalidStoreError from '@/mainview/components/store/InvalidStoreError';
|
||||||
import { CardList, GameMetaExtra } from '@/mainview/components/CardList';
|
import { CardList, GameMetaExtra } from '@/mainview/components/CardList';
|
||||||
import { GameListFilterType, RPC_URL } from '@/shared/constants';
|
import { RPC_URL } from '@/shared/constants';
|
||||||
|
import { GameListFilterType, FrontEndGameType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
import { useSessionStorage } from 'usehooks-ts';
|
import { useSessionStorage } from 'usehooks-ts';
|
||||||
import { zodValidator } from '@tanstack/zod-adapter';
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import SideFilters from '@/mainview/components/SideFilters';
|
import SideFilters from '@/mainview/components/SideFilters';
|
||||||
import { gameFiltersQuery } from '@/mainview/scripts/queries/romm';
|
import { gameFiltersQuery } from '@/mainview/scripts/queries/romm';
|
||||||
import { FrontEndGameType } from '@/shared/types';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/store/tab/games')({
|
export const Route = createFileRoute('/store/tab/games')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import { autoEmulatorsQuery } from '@queries/settings';
|
import { autoEmulatorsQuery } from '@queries/settings';
|
||||||
import { storeEmulatorsRecommendedQuery, storeFeaturedGamesQuery } from '@queries/store';
|
import { storeEmulatorsRecommendedQuery, storeFeaturedGamesQuery } from '@queries/store';
|
||||||
import ImageWithFallbacks from '@/mainview/components/ImageWithFallbacks';
|
import ImageWithFallbacks from '@/mainview/components/ImageWithFallbacks';
|
||||||
import { FrontEndGameTypeDetailed } from '@/shared/types';
|
import { FrontEndGameTypeDetailed } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
|
||||||
export const Route = createFileRoute('/store/tab/')({
|
export const Route = createFileRoute('/store/tab/')({
|
||||||
component: RouteComponent
|
component: RouteComponent
|
||||||
|
|
|
||||||
151
src/mainview/routes/store/tab/plugins.tsx
Normal file
151
src/mainview/routes/store/tab/plugins.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { allPluginsFilter, installPluginMutation, uninstallPluginMutation, updatePluginMutation } from '@/mainview/scripts/queries/plugins';
|
||||||
|
import { pluginsQuery } from '@/mainview/scripts/queries/store';
|
||||||
|
import { GamePadButtonCode, Shortcut, useShortcuts } from '@/mainview/scripts/shortcuts';
|
||||||
|
import { FOCUS_KEYS } from '@/mainview/scripts/types';
|
||||||
|
import { PluginEntryType } from '@simeonradivoev/gameflow-sdk/shared';
|
||||||
|
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||||
|
import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
|
import { CircleFadingArrowUp, Dot, Download, HardDrive, Puzzle } from 'lucide-react';
|
||||||
|
import prettyMilliseconds from 'pretty-ms';
|
||||||
|
import { useSessionStorage } from 'usehooks-ts';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/store/tab/plugins')({
|
||||||
|
component: RouteComponent,
|
||||||
|
validateSearch: zodValidator(z.object({
|
||||||
|
search: z.string().optional()
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
function PluginCard (data: { plugin: PluginEntryType; })
|
||||||
|
{
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const onAction = () =>
|
||||||
|
{
|
||||||
|
navigate({ to: '/store/details/plugin/$id', params: { id: decodeURIComponent(data.plugin.package.name) } });
|
||||||
|
};
|
||||||
|
const { ref, focusKey } = useFocusable({ focusKey: FOCUS_KEYS.PLUGIN_ENTRY(data.plugin.package.sanitized_name), onEnterPress: onAction });
|
||||||
|
const handleRefresh = (client: QueryClient) =>
|
||||||
|
{
|
||||||
|
client.invalidateQueries(allPluginsFilter);
|
||||||
|
navigate({ to: '/store/tab/plugins', replace: true });
|
||||||
|
};
|
||||||
|
const update = useMutation({
|
||||||
|
...updatePluginMutation(data.plugin.package.name),
|
||||||
|
onSuccess (data, variables, onMutateResult, context)
|
||||||
|
{
|
||||||
|
handleRefresh(context.client);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const install = useMutation({
|
||||||
|
...installPluginMutation(data.plugin.package.name),
|
||||||
|
onSuccess (f, variables, onMutateResult, context)
|
||||||
|
{
|
||||||
|
handleRefresh(context.client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const uninstall = useMutation({
|
||||||
|
...uninstallPluginMutation(data.plugin.package.name),
|
||||||
|
onSuccess (f, variables, onMutateResult, context)
|
||||||
|
{
|
||||||
|
handleRefresh(context.client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
useShortcuts(focusKey, () =>
|
||||||
|
{
|
||||||
|
const shortcuts: Shortcut[] = [{
|
||||||
|
label: "Details", button: GamePadButtonCode.A, action (e)
|
||||||
|
{
|
||||||
|
onAction();
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (data.plugin.installed)
|
||||||
|
{
|
||||||
|
shortcuts.push({
|
||||||
|
label: "Uninstall",
|
||||||
|
button: GamePadButtonCode.X,
|
||||||
|
action (e)
|
||||||
|
{
|
||||||
|
uninstall.mutate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.plugin.update)
|
||||||
|
{
|
||||||
|
shortcuts.push({
|
||||||
|
label: "Update",
|
||||||
|
button: GamePadButtonCode.Y,
|
||||||
|
action (e)
|
||||||
|
{
|
||||||
|
update.mutate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
shortcuts.push({
|
||||||
|
label: "Install",
|
||||||
|
button: GamePadButtonCode.X,
|
||||||
|
action (e)
|
||||||
|
{
|
||||||
|
install.mutate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return shortcuts;
|
||||||
|
}, [data.plugin.installed, install.isPending, uninstall.isPending]);
|
||||||
|
return <div ref={ref} onClick={onAction} data-installed={data.plugin.installed} className='flex flex-wrap bg-base-100 p-4 rounded-2xl focusable focusable-secondary focusable-hover justify-between cursor-pointer'>
|
||||||
|
<div className='flex flex-col gap-1'>
|
||||||
|
<div className='flex gap-2 font-bold text-xl in-data-[installed=true]:text-info'>
|
||||||
|
{data.plugin.installed && <HardDrive className='p-1 bg-base-300 rounded-full size-8 text-base-content' />}
|
||||||
|
{data.plugin.update && <CircleFadingArrowUp className='p-1 bg-warning text-warning-content rounded-full size-8' />}
|
||||||
|
{data.plugin.package.name}
|
||||||
|
{(install.isPending || uninstall.isPending) && <span className="loading loading-spinner loading-lg"></span>}
|
||||||
|
</div>
|
||||||
|
<div className='text-base-content/40'>{data.plugin.package.description}</div>
|
||||||
|
<ul className='flex flex-wrap gap-2'>{data.plugin.package.keywords.concat(...data.plugin.installed ? ["installed"] : []).map(k => <li className='bg-base-300 px-2 rounded-full'>{k}</li>)}</ul>
|
||||||
|
<ul className='flex flex-wrap gap-2'>
|
||||||
|
<li>{data.plugin.package.publisher.username}</li>
|
||||||
|
<Dot />
|
||||||
|
<li>{data.plugin.package.version}</li>
|
||||||
|
<Dot />
|
||||||
|
<li>{prettyMilliseconds(new Date().getTime() - data.plugin.package.date.getTime(), { hideSeconds: true })}</li>
|
||||||
|
<Dot />
|
||||||
|
<li>{data.plugin.package.license}</li>
|
||||||
|
{install.isPending && <>
|
||||||
|
<Dot />
|
||||||
|
<li><span className="loading loading-spinner loading-md"></span>installing</li>
|
||||||
|
</>}
|
||||||
|
{uninstall.isPending && <>
|
||||||
|
<Dot />
|
||||||
|
<li><span className="loading loading-spinner loading-md"></span>uninstalling</li>
|
||||||
|
</>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center items-center'>
|
||||||
|
<div className='flex gap-2 bg-base-300 rounded-3xl px-3 py-2'>
|
||||||
|
<Download />
|
||||||
|
{data.plugin.downloads.monthly}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RouteComponent ()
|
||||||
|
{
|
||||||
|
const [search] = useSessionStorage<string | undefined>(`${Route.to}-search`, undefined);
|
||||||
|
const { data: plugins } = useQuery(pluginsQuery(search));
|
||||||
|
const { ref, focusKey } = useFocusable({ focusKey: "plugins-store" });
|
||||||
|
return <div ref={ref}>
|
||||||
|
<FocusContext value={focusKey}>
|
||||||
|
<div className="divider"><Puzzle className='size-12' /> {plugins?.total} Plugins</div>
|
||||||
|
<div className='flex flex-col gap-2 p-8'>
|
||||||
|
{plugins?.objects.map((p, i) => <PluginCard key={i} plugin={p} />)}
|
||||||
|
</div>
|
||||||
|
</FocusContext>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useMatchRoute, useRouter } from '@tanstack/react-router';
|
import { useMatchRoute, useRouter } from '@tanstack/react-router';
|
||||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||||
import { zodValidator } from '@tanstack/zod-adapter';
|
import { zodValidator } from '@tanstack/zod-adapter';
|
||||||
|
import { Gamepad2, Home, Joystick, Puzzle } from 'lucide-react';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useSessionStorage } from 'usehooks-ts';
|
import { useSessionStorage } from 'usehooks-ts';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
@ -93,9 +94,10 @@ function RouteComponent ()
|
||||||
const headerRef = useRef(null);
|
const headerRef = useRef(null);
|
||||||
const sentinelRef = useRef(null);
|
const sentinelRef = useRef(null);
|
||||||
const filters: Record<string, FilterOption> = {
|
const filters: Record<string, FilterOption> = {
|
||||||
home: { label: "Home", selected: useIsSettings(''), },
|
home: { label: "Home", icon: <Home />, selected: useIsSettings(''), },
|
||||||
emulators: { label: "Emulators", selected: useIsSettings('emulators') },
|
emulators: { label: "Emulators", icon: <Joystick />, selected: useIsSettings('emulators') },
|
||||||
games: { label: "Games", selected: useIsSettings('games') }
|
games: { label: "Games", icon: <Gamepad2 />, selected: useIsSettings('games') },
|
||||||
|
plugins: { label: "Plugins", icon: <Puzzle />, selected: useIsSettings('plugins') }
|
||||||
};
|
};
|
||||||
const [search, setSearch] = useSessionStorage<string | undefined>(`${router.history.location.pathname}-search`, undefined);
|
const [search, setSearch] = useSessionStorage<string | undefined>(`${router.history.location.pathname}-search`, undefined);
|
||||||
const [, setGamesSearch] = useSessionStorage<string | undefined>(`/store/tab/games-search`, undefined);
|
const [, setGamesSearch] = useSessionStorage<string | undefined>(`/store/tab/games-search`, undefined);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue