diff options
Diffstat (limited to 'frontend/src/components/modals/filepicker')
5 files changed, 372 insertions, 0 deletions
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(); + }; +} |
