summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/utilities.py30
-rw-r--r--frontend/package.json5
-rw-r--r--frontend/pnpm-lock.yaml269
-rw-r--r--frontend/rollup.config.js2
-rw-r--r--frontend/src/components/WithSuspense.tsx32
-rw-r--r--frontend/src/components/modals/filepicker/iconCustomizations.ts170
-rw-r--r--frontend/src/components/modals/filepicker/index.tsx159
-rw-r--r--frontend/src/components/modals/filepicker/patches/README.md1
-rw-r--r--frontend/src/components/modals/filepicker/patches/index.ts10
-rw-r--r--frontend/src/components/modals/filepicker/patches/library.ts32
-rw-r--r--frontend/src/logger.ts6
-rw-r--r--frontend/src/plugin-loader.tsx83
-rw-r--r--frontend/src/store.tsx6
13 files changed, 758 insertions, 47 deletions
diff --git a/backend/utilities.py b/backend/utilities.py
index b3431cb6..853f60d2 100644
--- a/backend/utilities.py
+++ b/backend/utilities.py
@@ -1,4 +1,5 @@
import uuid
+import os
from json.decoder import JSONDecodeError
from aiohttp import ClientSession, web
@@ -24,7 +25,8 @@ class Utilities:
"allow_remote_debugging": self.allow_remote_debugging,
"disallow_remote_debugging": self.disallow_remote_debugging,
"set_setting": self.set_setting,
- "get_setting": self.get_setting
+ "get_setting": self.get_setting,
+ "filepicker_ls": self.filepicker_ls
}
if context:
@@ -166,3 +168,29 @@ class Utilities:
async def disallow_remote_debugging(self):
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
return True
+
+ async def filepicker_ls(self, path, include_files=True):
+ # def sorter(file): # Modification time
+ # if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
+ # return os.path.getmtime(os.path.join(path, file))
+ # return 0
+ # file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
+ file_names = sorted(os.listdir(path)) # Alphabetical
+
+ files = []
+
+ for file in file_names:
+ full_path = os.path.join(path, file)
+ is_dir = os.path.isdir(full_path)
+
+ if is_dir or include_files:
+ files.append({
+ "isdir": is_dir,
+ "name": file,
+ "realpath": os.path.realpath(full_path)
+ })
+
+ return {
+ "realpath": os.path.realpath(path),
+ "files": files
+ }
diff --git a/frontend/package.json b/frontend/package.json
index 5ce04122..0b8e774d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,7 @@
"@rollup/plugin-replace": "^4.0.0",
"@rollup/plugin-typescript": "^8.3.3",
"@types/react": "16.14.0",
+ "@types/react-file-icon": "^1.0.1",
"@types/react-router": "5.1.18",
"@types/webpack": "^5.28.0",
"husky": "^8.0.1",
@@ -27,6 +28,7 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.76.0",
+ "rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-polyfill-node": "^0.10.2",
"tslib": "^2.4.0",
@@ -39,7 +41,8 @@
}
},
"dependencies": {
- "decky-frontend-lib": "^2.0.0",
+ "decky-frontend-lib": "^3.0.0",
+ "react-file-icon": "^1.2.0",
"react-icons": "^4.4.0",
"react-markdown": "^8.0.3",
"remark-gfm": "^3.0.1"
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index a66dd98e..365573ef 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -7,9 +7,10 @@ specifiers:
'@rollup/plugin-replace': ^4.0.0
'@rollup/plugin-typescript': ^8.3.3
'@types/react': 16.14.0
+ '@types/react-file-icon': ^1.0.1
'@types/react-router': 5.1.18
'@types/webpack': ^5.28.0
- decky-frontend-lib: ^2.0.0
+ decky-frontend-lib: ^3.0.0
husky: ^8.0.1
import-sort-style-module: ^6.0.0
inquirer: ^8.2.4
@@ -17,17 +18,20 @@ specifiers:
prettier-plugin-import-sort: ^0.0.7
react: 16.14.0
react-dom: 16.14.0
+ react-file-icon: ^1.2.0
react-icons: ^4.4.0
react-markdown: ^8.0.3
remark-gfm: ^3.0.1
rollup: ^2.76.0
+ rollup-plugin-delete: ^2.0.0
rollup-plugin-external-globals: ^0.6.1
rollup-plugin-polyfill-node: ^0.10.2
tslib: ^2.4.0
typescript: ^4.7.4
dependencies:
- decky-frontend-lib: 2.0.0
+ decky-frontend-lib: 3.0.0
+ react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
react-icons: 4.4.0_react@16.14.0
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
remark-gfm: 3.0.1
@@ -39,6 +43,7 @@ devDependencies:
'@rollup/plugin-replace': 4.0.0_rollup@2.76.0
'@rollup/plugin-typescript': 8.3.3_mrkdcqv53wzt2ybukxlrvz47fu
'@types/react': 16.14.0
+ '@types/react-file-icon': 1.0.1
'@types/react-router': 5.1.18
'@types/webpack': 5.28.0
husky: 8.0.1
@@ -49,6 +54,7 @@ devDependencies:
react: 16.14.0
react-dom: 16.14.0_react@16.14.0
rollup: 2.76.0
+ rollup-plugin-delete: 2.0.0
rollup-plugin-external-globals: 0.6.1_rollup@2.76.0
rollup-plugin-polyfill-node: 0.10.2_rollup@2.76.0
tslib: 2.4.0
@@ -296,6 +302,27 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
+ /@nodelib/fs.scandir/2.1.5:
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+ dev: true
+
+ /@nodelib/fs.stat/2.0.5:
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+ dev: true
+
+ /@nodelib/fs.walk/1.2.8:
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.13.0
+ dev: true
+
/@rollup/plugin-commonjs/21.1.0_rollup@2.76.0:
resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==}
engines: {node: '>= 8.0.0'}
@@ -427,6 +454,13 @@ packages:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
+ /@types/glob/7.2.0:
+ resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
+ dependencies:
+ '@types/minimatch': 5.1.2
+ '@types/node': 18.0.4
+ dev: true
+
/@types/hast/2.3.4:
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
dependencies:
@@ -451,6 +485,10 @@ packages:
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
dev: false
+ /@types/minimatch/5.1.2:
+ resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
+ dev: true
+
/@types/ms/0.7.31:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: false
@@ -462,6 +500,12 @@ packages:
/@types/prop-types/15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
+ /@types/react-file-icon/1.0.1:
+ resolution: {integrity: sha512-QTdYCkYXzh/PfKEIwcPxRdaPQkii5R4Ke7fcO+KB++IDPbYAG1jj+ulEcTA7pRf0gZ5jAvjWcTXBJJRtfYHjlw==}
+ dependencies:
+ '@types/react': 16.14.0
+ dev: true
+
/@types/react-router/5.1.18:
resolution: {integrity: sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==}
dependencies:
@@ -626,6 +670,14 @@ packages:
hasBin: true
dev: true
+ /aggregate-error/3.1.0:
+ resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
+ engines: {node: '>=8'}
+ dependencies:
+ clean-stack: 2.2.0
+ indent-string: 4.0.0
+ dev: true
+
/ajv-keywords/3.5.2_ajv@6.12.6:
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
peerDependencies:
@@ -675,6 +727,11 @@ packages:
sprintf-js: 1.0.3
dev: true
+ /array-union/2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+ dev: true
+
/bail/2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
dev: false
@@ -702,6 +759,13 @@ packages:
concat-map: 0.0.1
dev: true
+ /braces/3.0.2:
+ resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
+ engines: {node: '>=8'}
+ dependencies:
+ fill-range: 7.0.1
+ dev: true
+
/browserslist/4.21.2:
resolution: {integrity: sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -786,6 +850,11 @@ packages:
engines: {node: '>=6.0'}
dev: true
+ /clean-stack/2.2.0:
+ resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
+ engines: {node: '>=6'}
+ dev: true
+
/cli-cursor/3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@@ -875,8 +944,8 @@ packages:
dependencies:
ms: 2.1.2
- /decky-frontend-lib/2.0.0:
- resolution: {integrity: sha512-H7+JpKHlClECVpo+MCEwej7R9wDWk9M2uMSyTvuhTfLZe3RThsxWCiqY640Cjh/zIW2A7GyVRd4SjLtn6Isdeg==}
+ /decky-frontend-lib/3.0.0:
+ resolution: {integrity: sha512-ZqJ9ii7QoYWHFfkU8hV82IHi3+McZDmE4wS22duXpgRI8r5BBMiZItw6tYkc24ZtsJIVso83FFt7adcEBqBwJA==}
dependencies:
minimist: 1.2.6
dev: false
@@ -898,6 +967,20 @@ packages:
clone: 1.0.4
dev: true
+ /del/5.1.0:
+ resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==}
+ engines: {node: '>=8'}
+ dependencies:
+ globby: 10.0.2
+ graceful-fs: 4.2.10
+ is-glob: 4.0.3
+ is-path-cwd: 2.2.0
+ is-path-inside: 3.0.3
+ p-map: 3.0.0
+ rimraf: 3.0.2
+ slash: 3.0.0
+ dev: true
+
/dequal/2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -913,6 +996,13 @@ packages:
engines: {node: '>=0.3.1'}
dev: false
+ /dir-glob/3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+ dependencies:
+ path-type: 4.0.0
+ dev: true
+
/electron-to-chromium/1.4.189:
resolution: {integrity: sha512-dQ6Zn4ll2NofGtxPXaDfY2laIa6NyCQdqXYHdwH90GJQW0LpJJib0ZU/ERtbb0XkBEmUD2eJtagbOie3pdMiPg==}
dev: true
@@ -1015,10 +1105,27 @@ packages:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
+ /fast-glob/3.2.11:
+ resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
+ engines: {node: '>=8.6.0'}
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.5
+ dev: true
+
/fast-json-stable-stringify/2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
dev: true
+ /fastq/1.13.0:
+ resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
+ dependencies:
+ reusify: 1.0.4
+ dev: true
+
/figures/3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@@ -1026,6 +1133,13 @@ packages:
escape-string-regexp: 1.0.5
dev: true
+ /fill-range/7.0.1:
+ resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ to-regex-range: 5.0.1
+ dev: true
+
/find-line-column/0.5.2:
resolution: {integrity: sha512-eNhNkDt5RbxY4X++JwyDURP62FYhV1bh9LF4dfOiwpVCTk5vvfEANhnui5ypUEELGR02QZSrWFtaTgd4ulW5tw==}
dev: true
@@ -1055,6 +1169,13 @@ packages:
engines: {node: '>=6.9.0'}
dev: true
+ /glob-parent/5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+ dependencies:
+ is-glob: 4.0.3
+ dev: true
+
/glob-to-regexp/0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
dev: true
@@ -1075,6 +1196,20 @@ packages:
engines: {node: '>=4'}
dev: true
+ /globby/10.0.2:
+ resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==}
+ engines: {node: '>=8'}
+ dependencies:
+ '@types/glob': 7.2.0
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.2.11
+ glob: 7.2.3
+ ignore: 5.2.0
+ merge2: 1.4.1
+ slash: 3.0.0
+ dev: true
+
/graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
dev: true
@@ -1117,6 +1252,11 @@ packages:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true
+ /ignore/5.2.0:
+ resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
+ engines: {node: '>= 4'}
+ dev: true
+
/import-fresh/2.0.0:
resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
engines: {node: '>=4'}
@@ -1174,6 +1314,11 @@ packages:
resolve: 1.22.1
dev: true
+ /indent-string/4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+ dev: true
+
/inflight/1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
@@ -1237,11 +1382,23 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /is-extglob/2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/is-fullwidth-code-point/3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
dev: true
+ /is-glob/4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ is-extglob: 2.1.1
+ dev: true
+
/is-interactive/1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
@@ -1251,6 +1408,21 @@ packages:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
+ /is-number/7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+ dev: true
+
+ /is-path-cwd/2.2.0:
+ resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /is-path-inside/3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+ dev: true
+
/is-plain-obj/4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
@@ -1321,6 +1493,10 @@ packages:
engines: {node: '>=6.11.5'}
dev: true
+ /lodash.uniqueid/4.0.1:
+ resolution: {integrity: sha512-GQQWaIeGlL6DIIr06kj1j6sSmBxyNMwI8kaX9aKpHR/XsMTiaXDVPNPAkiboOTK9OJpTJF/dXT3xYoFQnj386Q==}
+ dev: false
+
/lodash/4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
@@ -1483,6 +1659,11 @@ packages:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
+ /merge2/1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+ dev: true
+
/micromark-core-commonmark/1.0.6:
resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==}
dependencies:
@@ -1732,6 +1913,14 @@ packages:
- supports-color
dev: false
+ /micromatch/4.0.5:
+ resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
+ engines: {node: '>=8.6'}
+ dependencies:
+ braces: 3.0.2
+ picomatch: 2.3.1
+ dev: true
+
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -1816,6 +2005,13 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /p-map/3.0.0:
+ resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==}
+ engines: {node: '>=8'}
+ dependencies:
+ aggregate-error: 3.1.0
+ dev: true
+
/parse-json/4.0.0:
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'}
@@ -1833,6 +2029,11 @@ packages:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
+ /path-type/4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+ dev: true
+
/picocolors/1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: true
@@ -1878,6 +2079,10 @@ packages:
engines: {node: '>=6'}
dev: true
+ /queue-microtask/1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ dev: true
+
/randombytes/2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
dependencies:
@@ -1894,7 +2099,19 @@ packages:
prop-types: 15.8.1
react: 16.14.0
scheduler: 0.19.1
- dev: true
+
+ /react-file-icon/1.2.0_wcqkhtmu7mswc6yz4uyexck3ty:
+ resolution: {integrity: sha512-BI8CTyZu/k8AmhjGJiGYOqgjfp2si2Lt5PUNF6kfF31c7BFYJeerpfHnZBfpPjrb2M/DAdW1qNub17Rt+xuefQ==}
+ peerDependencies:
+ react: ^18.0.0 || ^17.0.0 || ^16.2.0
+ react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0
+ dependencies:
+ lodash.uniqueid: 4.0.1
+ prop-types: 15.8.1
+ react: 16.14.0
+ react-dom: 16.14.0_react@16.14.0
+ tinycolor2: 1.4.2
+ dev: false
/react-icons/4.4.0_react@16.14.0:
resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==}
@@ -2012,6 +2229,25 @@ packages:
signal-exit: 3.0.7
dev: true
+ /reusify/1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+ dev: true
+
+ /rimraf/3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ hasBin: true
+ dependencies:
+ glob: 7.2.3
+ dev: true
+
+ /rollup-plugin-delete/2.0.0:
+ resolution: {integrity: sha512-/VpLMtDy+8wwRlDANuYmDa9ss/knGsAgrDhM+tEwB1npHwNu4DYNmDfUL55csse/GHs9Q+SMT/rw9uiaZ3pnzA==}
+ engines: {node: '>=10'}
+ dependencies:
+ del: 5.1.0
+ dev: true
+
/rollup-plugin-external-globals/0.6.1_rollup@2.76.0:
resolution: {integrity: sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==}
peerDependencies:
@@ -2046,6 +2282,12 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
+ /run-parallel/1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+ dependencies:
+ queue-microtask: 1.2.3
+ dev: true
+
/rxjs/7.5.6:
resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==}
dependencies:
@@ -2076,7 +2318,6 @@ packages:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
- dev: true
/schema-utils/3.1.1:
resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==}
@@ -2102,6 +2343,11 @@ packages:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
+ /slash/3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+ dev: true
+
/source-map-support/0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
dependencies:
@@ -2224,6 +2470,10 @@ packages:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true
+ /tinycolor2/1.4.2:
+ resolution: {integrity: sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==}
+ dev: false
+
/tmp/0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
@@ -2236,6 +2486,13 @@ packages:
engines: {node: '>=4'}
dev: true
+ /to-regex-range/5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+ dependencies:
+ is-number: 7.0.0
+ dev: true
+
/trim-lines/3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
dev: false
diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js
index f472b816..c4bcd0a2 100644
--- a/frontend/rollup.config.js
+++ b/frontend/rollup.config.js
@@ -2,6 +2,7 @@ import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import externalGlobals from "rollup-plugin-external-globals";
+import del from 'rollup-plugin-delete'
import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
@@ -9,6 +10,7 @@ import { defineConfig } from 'rollup';
export default defineConfig({
input: 'src/index.tsx',
plugins: [
+ del({ targets: "../backend/static/*", force: true }),
commonjs(),
nodeResolve(),
externalGlobals({
diff --git a/frontend/src/components/WithSuspense.tsx b/frontend/src/components/WithSuspense.tsx
new file mode 100644
index 00000000..7460aa3d
--- /dev/null
+++ b/frontend/src/components/WithSuspense.tsx
@@ -0,0 +1,32 @@
+import { SteamSpinner } from 'decky-frontend-lib';
+import { FunctionComponent, ReactElement, ReactNode, Suspense } from 'react';
+
+interface WithSuspenseProps {
+ children: ReactNode;
+}
+
+// Nice little wrapper around Suspense so we don't have to duplicate the styles and code for the loading spinner
+const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
+ const propsCopy = { ...props };
+ delete propsCopy.children;
+ (props.children as ReactElement)?.props && Object.assign((props.children as ReactElement).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
+ return (
+ <Suspense
+ fallback={
+ <div
+ style={{
+ marginTop: '40px',
+ height: 'calc( 100% - 40px )',
+ overflowY: 'scroll',
+ }}
+ >
+ <SteamSpinner />
+ </div>
+ }
+ >
+ {props.children}
+ </Suspense>
+ );
+};
+
+export default WithSuspense;
diff --git a/frontend/src/components/modals/filepicker/iconCustomizations.ts b/frontend/src/components/modals/filepicker/iconCustomizations.ts
new file mode 100644
index 00000000..e09c9e67
--- /dev/null
+++ b/frontend/src/components/modals/filepicker/iconCustomizations.ts
@@ -0,0 +1,170 @@
+// https://codesandbox.io/s/react-file-icon-colored-tmwut?file=/src/App.js
+import { FileIconProps } from 'react-file-icon';
+
+type T_FileExtList = string[];
+
+const styleDef: [FileIconProps, T_FileExtList][] = [];
+
+// video ////////////////////////////////////
+const videoStyle = {
+ color: '#f00f0f',
+};
+const videoExtList = [
+ 'avi',
+ '3g2',
+ '3gp',
+ 'aep',
+ 'asf',
+ 'flv',
+ 'm4v',
+ 'mkv',
+ 'mov',
+ 'mp4',
+ 'mpeg',
+ 'mpg',
+ 'ogv',
+ 'pr',
+ 'swfw',
+ 'webm',
+ 'wmv',
+ 'swf',
+ 'rm',
+];
+
+styleDef.push([videoStyle, videoExtList]);
+
+// image ////////////////////////////////////
+const imageStyle = {
+ color: '#d18f00',
+};
+
+const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff'];
+
+styleDef.push([imageStyle, imageExtList]);
+
+// zip ////////////////////////////////////
+const zipStyle = {
+ color: '#f7b500',
+ labelTextColor: '#000',
+ // glyphColor: "#de9400"
+};
+
+const zipExtList = ['zip', 'zipx', '7zip', 'tar', 'sitx', 'gz', 'rar'];
+
+styleDef.push([zipStyle, zipExtList]);
+
+// audio ////////////////////////////////////
+const audioStyle = {
+ color: '#f00f0f',
+};
+
+const audioExtList = ['aac', 'aif', 'aiff', 'flac', 'm4a', 'mid', 'mp3', 'ogg', 'wav'];
+
+styleDef.push([audioStyle, audioExtList]);
+
+// text ////////////////////////////////////
+const textStyle = {
+ color: '#ffffff',
+ glyphColor: '#787878',
+};
+
+const textExtList = ['cue', 'odt', 'md', 'rtf', 'txt', 'tex', 'wpd', 'wps', 'xlr', 'fodt'];
+
+styleDef.push([textStyle, textExtList]);
+
+// system ////////////////////////////////////
+const systemStyle = {
+ color: '#111',
+};
+
+const systemExtList = ['exe', 'ini', 'dll', 'plist', 'sys'];
+
+styleDef.push([systemStyle, systemExtList]);
+
+// srcCode ////////////////////////////////////
+const srcCodeStyle = {
+ glyphColor: '#787878',
+ color: '#ffffff',
+};
+
+const srcCodeExtList = [
+ 'asp',
+ 'aspx',
+ 'c',
+ 'cpp',
+ 'cs',
+ 'css',
+ 'scss',
+ 'py',
+ 'json',
+ 'htm',
+ 'html',
+ 'java',
+ 'yml',
+ 'php',
+ 'js',
+ 'ts',
+ 'rb',
+ 'jsx',
+ 'tsx',
+];
+
+styleDef.push([srcCodeStyle, srcCodeExtList]);
+
+// vector ////////////////////////////////////
+const vectorStyle = {
+ color: '#ffe600',
+};
+
+const vectorExtList = ['dwg', 'dxf', 'ps', 'svg', 'eps'];
+
+styleDef.push([vectorStyle, vectorExtList]);
+
+// font ////////////////////////////////////
+const fontStyle = {
+ color: '#555',
+};
+
+const fontExtList = ['fnt', 'ttf', 'otf', 'fon', 'eot', 'woff'];
+
+styleDef.push([fontStyle, fontExtList]);
+
+// objectModel ////////////////////////////////////
+const objectModelStyle = {
+ color: '#bf6a02',
+ glyphColor: '#bf6a02',
+};
+
+const objectModelExtList = ['3dm', '3ds', 'max', 'obj', 'pkg'];
+
+styleDef.push([objectModelStyle, objectModelExtList]);
+
+// sheet ////////////////////////////////////
+const sheetStyle = {
+ color: '#2a6e00',
+};
+
+const sheetExtList = ['csv', 'fods', 'ods', 'xlr'];
+
+styleDef.push([sheetStyle, sheetExtList]);
+
+// const defaultStyle: Record<string, FileIconProps> = {
+// pdf: {
+// glyphColor: "white",
+// color: "#D93831"
+// }
+// };
+
+//////////////////////////////////////////////////
+
+function createStyleObj(extList: T_FileExtList, styleObj: Partial<FileIconProps>) {
+ return Object.fromEntries(
+ extList.map((ext) => {
+ return [ext, { ...styleObj, glyphColor: 'white' }];
+ }),
+ );
+}
+
+export const styleDefObj = styleDef.reduce((acc, [fileStyle, fileExtList]) => {
+ return { ...acc, ...createStyleObj(fileExtList, fileStyle) };
+});
diff --git a/frontend/src/components/modals/filepicker/index.tsx b/frontend/src/components/modals/filepicker/index.tsx
new file mode 100644
index 00000000..0847bd14
--- /dev/null
+++ b/frontend/src/components/modals/filepicker/index.tsx
@@ -0,0 +1,159 @@
+import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend-lib';
+import { useEffect } from 'react';
+import { FunctionComponent, useState } from 'react';
+import { FileIcon, defaultStyles } from 'react-file-icon';
+import { FaArrowUp, FaFolder } from 'react-icons/fa';
+
+import Logger from '../../../logger';
+import { styleDefObj } from './iconCustomizations';
+
+const logger = new Logger('FilePicker');
+
+export interface FilePickerProps {
+ startPath: string;
+ includeFiles?: boolean;
+ regex?: RegExp;
+ onSubmit: (val: { path: string; realpath: string }) => void;
+ closeModal?: () => void;
+}
+
+interface File {
+ isdir: boolean;
+ name: string;
+ realpath: string;
+}
+
+interface FileListing {
+ realpath: string;
+ files: File[];
+}
+
+function getList(
+ path: string,
+ includeFiles: boolean = true,
+): Promise<{ result: FileListing | string; success: boolean }> {
+ return window.DeckyPluginLoader.callServerMethod('filepicker_ls', { path, include_files: includeFiles });
+}
+
+const iconStyles = {
+ paddingRight: '10px',
+ width: '1em',
+};
+
+const FilePicker: FunctionComponent<FilePickerProps> = ({
+ startPath,
+ includeFiles = true,
+ regex,
+ onSubmit,
+ closeModal,
+}) => {
+ if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
+ const [path, setPath] = useState<string>(startPath);
+ const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
+ const [error, setError] = useState<string | null>(null);
+ const [loading, setLoading] = useState<boolean>(true);
+
+ useEffect(() => {
+ (async () => {
+ if (error) setError(null);
+ setLoading(true);
+ const listing = await getList(path, includeFiles);
+ if (!listing.success) {
+ setListing({ files: [], realpath: path });
+ setLoading(false);
+ setError(listing.result as string);
+ logger.error(listing.result);
+ return;
+ }
+ setLoading(false);
+ setListing(listing.result as FileListing);
+ logger.log('reloaded', path, listing);
+ })();
+ }, [path]);
+
+ return (
+ <div className="deckyFilePicker">
+ <Focusable style={{ display: 'flex', flexDirection: 'row', paddingBottom: '10px' }}>
+ <DialogButton
+ style={{
+ minWidth: 'unset',
+ width: '40px',
+ flexGrow: '0',
+ borderRadius: 'unset',
+ margin: '0',
+ padding: '10px',
+ }}
+ onClick={() => {
+ const newPathArr = path.split('/');
+ newPathArr.pop();
+ const newPath = newPathArr.join('/');
+ setPath(newPath);
+ }}
+ >
+ <FaArrowUp />
+ </DialogButton>
+ <div style={{ flexGrow: '1', width: '100%' }}>
+ <TextField
+ value={path}
+ onChange={(e) => {
+ e.target.value && setPath(e.target.value);
+ }}
+ style={{ height: '100%' }}
+ />
+ </div>
+ </Focusable>
+ <Focusable style={{ display: 'flex', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}>
+ {loading && <SteamSpinner style={{ height: '100%' }} />}
+ {!loading &&
+ listing.files
+ .filter((file) => (includeFiles || file.isdir) && (!regex || regex.test(file.name)))
+ .map((file) => {
+ let extension = file.realpath.split('.').pop() as string;
+ return (
+ <DialogButton
+ style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
+ onClick={() => {
+ const fullPath = `${path}/${file.name}`;
+ if (file.isdir) setPath(fullPath);
+ else {
+ onSubmit({ path: fullPath, realpath: file.realpath });
+ closeModal?.();
+ }
+ }}
+ >
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start' }}>
+ {file.isdir ? (
+ <FaFolder style={iconStyles} />
+ ) : (
+ <div style={iconStyles}>
+ {file.realpath.includes('.') ? (
+ <FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
+ ) : (
+ <FileIcon />
+ )}
+ </div>
+ )}
+ {file.name}
+ </div>
+ </DialogButton>
+ );
+ })}
+ {error}
+ </Focusable>
+ {!loading && !error && !includeFiles && (
+ <DialogButton
+ className="Primary"
+ style={{ marginTop: '10px', alignSelf: 'flex-end' }}
+ onClick={() => {
+ onSubmit({ path, realpath: listing.realpath });
+ closeModal?.();
+ }}
+ >
+ Use this folder
+ </DialogButton>
+ )}
+ </div>
+ );
+};
+
+export default FilePicker;
diff --git a/frontend/src/components/modals/filepicker/patches/README.md b/frontend/src/components/modals/filepicker/patches/README.md
new file mode 100644
index 00000000..154914c5
--- /dev/null
+++ b/frontend/src/components/modals/filepicker/patches/README.md
@@ -0,0 +1 @@
+This directory contains patches that replace Valve's broken file picker with ours.
diff --git a/frontend/src/components/modals/filepicker/patches/index.ts b/frontend/src/components/modals/filepicker/patches/index.ts
new file mode 100644
index 00000000..310bfbf8
--- /dev/null
+++ b/frontend/src/components/modals/filepicker/patches/index.ts
@@ -0,0 +1,10 @@
+import library from './library';
+let patches: Function[] = [];
+
+export function deinitFilepickerPatches() {
+ patches.forEach((unpatch) => unpatch());
+}
+
+export async function initFilepickerPatches() {
+ patches.push(await library());
+}
diff --git a/frontend/src/components/modals/filepicker/patches/library.ts b/frontend/src/components/modals/filepicker/patches/library.ts
new file mode 100644
index 00000000..8792900d
--- /dev/null
+++ b/frontend/src/components/modals/filepicker/patches/library.ts
@@ -0,0 +1,32 @@
+import { replacePatch, sleep } from 'decky-frontend-lib';
+
+declare global {
+ interface Window {
+ SteamClient: any;
+ appDetailsStore: any;
+ }
+}
+
+export default async function libraryPatch() {
+ await sleep(10000); // If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so wait 10s
+ const patch = replacePatch(window.SteamClient.Apps, 'PromptToChangeShortcut', async ([appid]: number[]) => {
+ try {
+ const details = window.appDetailsStore.GetAppDetails(appid);
+ console.log(details);
+ // strShortcutStartDir
+ const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
+ console.log('user selected', file);
+ window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
+ const pathArr = file.path.split('/');
+ pathArr.pop();
+ const folder = pathArr.join('/');
+ window.SteamClient.Apps.SetShortcutStartDir(appid, JSON.stringify(folder));
+ } catch (e) {
+ console.error(e);
+ }
+ });
+
+ return () => {
+ patch.unpatch();
+ };
+}
diff --git a/frontend/src/logger.ts b/frontend/src/logger.ts
index 22036362..143bef16 100644
--- a/frontend/src/logger.ts
+++ b/frontend/src/logger.ts
@@ -19,7 +19,7 @@ export const debug = (name: string, ...args: any[]) => {
};
export const error = (name: string, ...args: any[]) => {
- console.log(
+ console.error(
`%c Decky %c ${name} %c`,
'background: #16a085; color: black;',
'background: #FF0000;',
@@ -40,6 +40,10 @@ class Logger {
debug(...args: any[]) {
debug(this.name, ...args);
}
+
+ error(...args: any[]) {
+ error(this.name, ...args);
+ }
}
export default Logger;
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index 4d3415c8..493e5935 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -1,13 +1,15 @@
-import { ModalRoot, QuickAccessTab, Router, SteamSpinner, showModal, sleep, staticClasses } from 'decky-frontend-lib';
-import { Suspense, lazy } from 'react';
+import { ConfirmModal, ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
+import { lazy } from 'react';
import { FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
+import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import PluginInstallModal from './components/modals/PluginInstallModal';
import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
import TitleView from './components/TitleView';
+import WithSuspense from './components/WithSuspense';
import Logger from './logger';
import { Plugin } from './plugin';
import RouterHook from './router-hook';
@@ -16,6 +18,11 @@ import TabsHook from './tabs-hook';
import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater';
+const StorePage = lazy(() => import('./components/store/Store'));
+const SettingsPage = lazy(() => import('./components/settings'));
+
+const FilePicker = lazy(() => import('./components/modals/filepicker'));
+
declare global {
interface Window {}
}
@@ -58,47 +65,22 @@ class PluginLoader extends Logger {
),
});
- const StorePage = lazy(() => import('./components/store/Store'));
- const SettingsPage = lazy(() => import('./components/settings'));
-
this.routerHook.addRoute('/decky/store', () => (
- <Suspense
- fallback={
- <div
- style={{
- marginTop: '40px',
- height: 'calc( 100% - 40px )',
- overflowY: 'scroll',
- }}
- >
- <SteamSpinner />
- </div>
- }
- >
+ <WithSuspense>
<StorePage />
- </Suspense>
+ </WithSuspense>
));
this.routerHook.addRoute('/decky/settings', () => {
return (
<DeckyStateContextProvider deckyState={this.deckyState}>
- <Suspense
- fallback={
- <div
- style={{
- marginTop: '40px',
- height: 'calc( 100% - 40px )',
- overflowY: 'scroll',
- }}
- >
- <SteamSpinner />
- </div>
- }
- >
+ <WithSuspense>
<SettingsPage />
- </Suspense>
+ </WithSuspense>
</DeckyStateContextProvider>
);
});
+
+ initFilepickerPatches();
}
public async notifyUpdates() {
@@ -147,7 +129,7 @@ class PluginLoader extends Logger {
public uninstallPlugin(name: string) {
showModal(
- <ModalRoot
+ <ConfirmModal
onOK={async () => {
await this.callServerMethod('uninstall_plugin', { name });
}}
@@ -158,7 +140,7 @@ class PluginLoader extends Logger {
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
Uninstall {name}?
</div>
- </ModalRoot>,
+ </ConfirmModal>,
);
}
@@ -176,6 +158,7 @@ class PluginLoader extends Logger {
public deinit() {
this.routerHook.removeRoute('/decky/store');
this.routerHook.removeRoute('/decky/settings');
+ deinitFilepickerPatches();
}
public unloadPlugin(name: string) {
@@ -257,11 +240,41 @@ class PluginLoader extends Logger {
return response.json();
}
+ openFilePicker(
+ startPath: string,
+ includeFiles?: boolean,
+ regex?: RegExp,
+ ): Promise<{ path: string; realpath: string }> {
+ return new Promise((resolve, reject) => {
+ const Content = ({ closeModal }: { closeModal?: () => void }) => (
+ // Purposely outside of the FilePicker component as lazy-loaded ModalRoots don't focus correctly
+ <ModalRoot
+ onCancel={() => {
+ reject('User canceled');
+ closeModal?.();
+ }}
+ >
+ <WithSuspense>
+ <FilePicker
+ startPath={startPath}
+ includeFiles={includeFiles}
+ regex={regex}
+ onSubmit={resolve}
+ closeModal={closeModal}
+ />
+ </WithSuspense>
+ </ModalRoot>
+ );
+ showModal(<Content />);
+ });
+ }
+
createPluginAPI(pluginName: string) {
return {
routerHook: this.routerHook,
toaster: this.toaster,
callServerMethod: this.callServerMethod,
+ openFilePicker: this.openFilePicker,
async callPluginMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
method: 'POST',
diff --git a/frontend/src/store.tsx b/frontend/src/store.tsx
index 12c8972d..bdaae6f2 100644
--- a/frontend/src/store.tsx
+++ b/frontend/src/store.tsx
@@ -1,4 +1,4 @@
-import { ModalRoot, showModal, staticClasses } from 'decky-frontend-lib';
+import { ConfirmModal, showModal, staticClasses } from 'decky-frontend-lib';
import { Plugin } from './plugin';
@@ -51,7 +51,7 @@ export async function installFromURL(url: string) {
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
showModal(
- <ModalRoot
+ <ConfirmModal
onOK={() => {
window.DeckyPluginLoader.callServerMethod('install_plugin', {
name: plugin.artifact,
@@ -70,7 +70,7 @@ export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVe
You are currently installing a <b>legacy</b> plugin. Legacy plugins are no longer supported and may have issues.
Legacy plugins do not support gamepad input. To interact with a legacy plugin, you will need to use the
touchscreen.
- </ModalRoot>,
+ </ConfirmModal>,
);
}