summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorMarco Rodolfi <marco.rodolfi@tuta.io>2023-06-19 15:23:27 +0200
committerGitHub <noreply@github.com>2023-06-19 06:23:27 -0700
commit57f4555350c669e4cb098b48691975be79838468 (patch)
treef840889c0c344ed5ae56a0f06398a97ca5982d2b /frontend/src/components
parentbd87cc852b935369bba04130801467b8dbeb6d1c (diff)
downloaddecky-loader-57f4555350c669e4cb098b48691975be79838468.tar.gz
decky-loader-57f4555350c669e4cb098b48691975be79838468.zip
[Feature] File picker improvements (#454)
* First iteration for internationalization of the loader * First iteration for internationalization of the loader * Cleanup node mess * Cleanup node mess pt2 * Additional touches * Latest decky changed merged into i18n and updated translation. * Styling fixes * Initial backend hosting implementation * Added correct url path of the loopback server. * Added correct url path of the loopback server. * Some better namespaced text. * Added whitelist for locales path. * Refactor languages and fix hooks logic bugs. * Small typo in language translation structure. * Working backend, automatically swtich languages with steam and language fixes. * Fix to languages * Key fixes * Additional language fixes. * Additional json changes * Final text revision and added a vscode tasks to automatically extract text from code. * Typo in the middleware * Remove unused imports * Cleanup whitespaces. * Import changes * Revert "Import changes" This reverts commit 8e8231950fd7cc6cece87040e326d0a72ba79567. * Update index.d.ts * Clean up unused imports * Delete pnpm-lock.yaml * Update rollup.config.js * Update PluginInstallModal.tsx * Update index.tsx * Update plugin-loader.tsx * Update plugin-loader.tsx * Revert "Delete pnpm-lock.yaml" This reverts commit 3a39f36f2193cc976d36ffe07338239e363d5b04. * Additional strings reworks. * Fixes for issues coming from github merge. * Fixes for master * Styling fixes * Styling pt2 * Missed a few strings in master, * Styling fixes * Additional master merge fixes. * Final cleanup and adaptation to master. * Final empty language cleanup and few string added * Small changes to italian translation * Disabled translation on a few components inside plugin-loader for missing react hooks. * Fixed passing tag to translation. * Disable debug output for reducing console spam. * Return correct content type * Small italian language change * Added support for country code * Fixed missing translation for uninstall popup. * Fix class name shenanigans for toast notification * Update dependencies * Fixed github workflow to include the new locales folder * Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up * Missed a file name change * Updated dev dependencies to latest version * Missed a few dev dependencies * Revert "Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up" Messed up merge with a different main branch * Messed up deletion of rollup config. * Fix broken pnpm lock file * Missed a localized string during the merge * Fixed a parameter mistake in the uninstall text parameter * Fix pnpm random issues * Small italian language tweaks * Fix wrong parameter passed to the uninstall function call * Another fix on a wrong function parameter * Additional translation text on the store and branch selection channels * Changed the default type passed to map to being able to index the two arrays. * Reverted and reworked the last changes * Distinguish events in UI for installing vs reinstalling plugins * Additional fixes for reinstall prompt * Revert the use of intevalPlural since the parser doesn't seem to support that. * Missed a routing path in the backend * Small bugfixes * Small fixes * Correctly adding the parameter to the request headers. * Refactoring of the UI popup modal * Fix pnpm shenanigans * Final fixes for the install UI localization * Clean up unnedeed backend code * Small rework on text selection. * Cleaned up parser configuration * Removed extracttext dependency to pnpmsetup * Merged translation and cleaned up parser * Fixed JSON structure after manual merge. * Added translation to the file picker * First iteration for merging the new filepicker. * Revert changes to PluginInstallModal * Reworked the text modal for the final time * Missed the proper linted text * Missed the backend change * Final branch cleanup * First iteration for porting the new file picker * Hotfix for i18n where the detector was overriding localStorage * Please, pnpm, cooperate * Small fix regarding the backend getting hammered when switching to not supported languages plus a small english typo * Initial working upstream iteration for file picker * Typo on translation variable * File picker final improvements * Stylistic fixes and fix on wrong bool passed to fp * Fixup merge from main * Other merge errors fixed * Minor cleanups * Fixed missing padding under text label extension * Implement pagination backend side * First draft for filtering backend side * Implemented matching on file names. * Fix for unable to order per size on folders. * Hard checking a return value * Added a missing import. * Implemented show more as a frontend button * Whoops, python typo * Fixed python backend * Rendering bug fix and small qol improvement * Added missing parameter to openFilePicker call * Fixed path on windows and unknown error on wrong path * Small backend fixes * Extension fix * Simplified extension logic * Less string conversions. * Optimize backend code and removed additional components. * Take correctly into account the max value The button will now respect the actual maximum desired number of entries. * Bugfix for ordering logic and ignore cases during sorting * Regex call was missing an argument * Fixed issues with filtering extensions * Rollback testing changes * Minor cleanup and attempt at fixing the not updating multimodal. * Cleanup variable types. * Mantains the same api format from the original source code. * Removing hardcoded paths in the code * Additional fixes for resolving the user path * Cleanup useless modifications * Final fixes for avoid path hardcoding * Update lockfile and i18next version
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/DeckyState.tsx13
-rw-r--r--frontend/src/components/modals/DropdownMultiselect.tsx121
-rw-r--r--frontend/src/components/modals/filepicker/FilePickerError.tsx51
-rw-r--r--frontend/src/components/modals/filepicker/i18n/TSortOptions.tsx46
-rw-r--r--frontend/src/components/modals/filepicker/iconCustomizations.ts2
-rw-r--r--frontend/src/components/modals/filepicker/index.tsx393
-rw-r--r--frontend/src/components/settings/pages/developer/index.tsx26
7 files changed, 536 insertions, 116 deletions
diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx
index 53ef6d2d..920985b3 100644
--- a/frontend/src/components/DeckyState.tsx
+++ b/frontend/src/components/DeckyState.tsx
@@ -13,6 +13,12 @@ interface PublicDeckyState {
hasLoaderUpdate?: boolean;
isLoaderUpdating: boolean;
versionInfo: VerInfo | null;
+ userInfo: UserInfo | null;
+}
+
+export interface UserInfo {
+ username: string;
+ path: string;
}
export class DeckyState {
@@ -24,6 +30,7 @@ export class DeckyState {
private _hasLoaderUpdate: boolean = false;
private _isLoaderUpdating: boolean = false;
private _versionInfo: VerInfo | null = null;
+ private _userInfo: UserInfo | null = null;
public eventBus = new EventTarget();
@@ -37,6 +44,7 @@ export class DeckyState {
hasLoaderUpdate: this._hasLoaderUpdate,
isLoaderUpdating: this._isLoaderUpdating,
versionInfo: this._versionInfo,
+ userInfo: this._userInfo,
};
}
@@ -85,6 +93,11 @@ export class DeckyState {
this.notifyUpdate();
}
+ setUserInfo(userInfo: UserInfo) {
+ this._userInfo = userInfo;
+ this.notifyUpdate();
+ }
+
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
diff --git a/frontend/src/components/modals/DropdownMultiselect.tsx b/frontend/src/components/modals/DropdownMultiselect.tsx
new file mode 100644
index 00000000..5defbfa4
--- /dev/null
+++ b/frontend/src/components/modals/DropdownMultiselect.tsx
@@ -0,0 +1,121 @@
+import {
+ DialogButton,
+ DialogCheckbox,
+ DialogCheckboxProps,
+ Marquee,
+ Menu,
+ MenuItem,
+ findModuleChild,
+ showContextMenu,
+} from 'decky-frontend-lib';
+import { FC, useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaChevronDown } from 'react-icons/fa';
+
+const dropDownControlButtonClass = findModuleChild((m) => {
+ if (typeof m !== 'object') return undefined;
+ for (const prop in m) {
+ if (m[prop]?.toString()?.includes('gamepaddropdown_DropDownControlButton')) {
+ return m[prop];
+ }
+ }
+});
+
+const DropdownMultiselectItem: FC<
+ {
+ value: any;
+ onSelect: (checked: boolean, value: any) => void;
+ checked: boolean;
+ } & DialogCheckboxProps
+> = ({ value, onSelect, checked: defaultChecked, ...rest }) => {
+ const [checked, setChecked] = useState(defaultChecked);
+
+ useEffect(() => {
+ onSelect?.(checked, value);
+ }, [checked, onSelect, value]);
+
+ return (
+ <MenuItem bInteractableItem onClick={() => setChecked((x) => !x)}>
+ <DialogCheckbox
+ style={{ marginBottom: 0, padding: 0 }}
+ className="decky_DropdownMultiselectItem_DialogCheckbox"
+ bottomSeparator="none"
+ {...rest}
+ onClick={() => setChecked((x) => !x)}
+ onChange={(checked) => setChecked(checked)}
+ controlled
+ checked={checked}
+ />
+ </MenuItem>
+ );
+};
+
+const DropdownMultiselect: FC<{
+ items: {
+ label: string;
+ value: string;
+ }[];
+ selected: string[];
+ onSelect: (selected: any[]) => void;
+ label: string;
+}> = ({ label, items, selected, onSelect }) => {
+ const [itemsSelected, setItemsSelected] = useState<any>(selected);
+ const { t } = useTranslation();
+
+ const handleItemSelect = useCallback((checked, value) => {
+ setItemsSelected((x: any) =>
+ checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
+ );
+ }, []);
+
+ useEffect(() => {
+ onSelect(itemsSelected);
+ }, [itemsSelected, onSelect]);
+
+ return (
+ <DialogButton
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ maxWidth: '100%',
+ }}
+ className={dropDownControlButtonClass}
+ onClick={(evt) => {
+ evt.preventDefault();
+ showContextMenu(
+ <Menu label={label} cancelText={t('DropdownMultiselect.button.back') as string}>
+ <style>
+ {`
+ /* Inherit color from ".basiccontextmenu" */
+ .decky_DropdownMultiselectItem_DialogCheckbox > .DialogToggle_Label {
+ color: inherit;
+ }
+ `}
+ </style>
+ <div style={{ marginTop: '10px' }}>{/*FIXME: Hack for missing padding under label menu*/}</div>
+ {items.map((x) => (
+ <DropdownMultiselectItem
+ key={x.value}
+ label={x.label}
+ value={x.value}
+ checked={itemsSelected.includes(x.value)}
+ onSelect={handleItemSelect}
+ />
+ ))}
+ </Menu>,
+ evt.currentTarget ?? window,
+ );
+ }}
+ >
+ <Marquee>
+ {selected.length > 0
+ ? selected.map((x: any) => items[items.findIndex((v) => v.value === x)].label).join(', ')
+ : '…'}
+ </Marquee>
+ <div style={{ flexGrow: 1, minWidth: '1ch' }} />
+ <FaChevronDown style={{ height: '1em', flex: '0 0 1em' }} />
+ </DialogButton>
+ );
+};
+
+export default DropdownMultiselect;
diff --git a/frontend/src/components/modals/filepicker/FilePickerError.tsx b/frontend/src/components/modals/filepicker/FilePickerError.tsx
new file mode 100644
index 00000000..bf75afae
--- /dev/null
+++ b/frontend/src/components/modals/filepicker/FilePickerError.tsx
@@ -0,0 +1,51 @@
+import { FC, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { IconContext } from 'react-icons';
+import { FaExclamationTriangle, FaQuestionCircle } from 'react-icons/fa';
+
+export enum FileErrorTypes {
+ FileNotFound,
+ Unknown,
+ None,
+}
+
+interface FilePickerErrorProps {
+ error: FileErrorTypes;
+ rawError?: string;
+}
+
+const FilePickerError: FC<FilePickerErrorProps> = ({ error, rawError = null }) => {
+ const [icon, setIcon] = useState<JSX.Element>(<FaQuestionCircle />);
+ const [text, setText] = useState<string | null>(null);
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ switch (error) {
+ case FileErrorTypes.FileNotFound:
+ setText(t('FilePickerError.errors.file_not_found'));
+ setIcon(<FaExclamationTriangle />);
+ break;
+ case FileErrorTypes.Unknown:
+ setText(t('FilePickerError.errors.unknown', { raw_error: rawError }));
+ setIcon(<FaQuestionCircle />);
+ break;
+ case FileErrorTypes.None:
+ setText(null);
+ setIcon(<div></div>);
+ break;
+ }
+ }, [error]);
+
+ return (
+ <>
+ <div style={{ paddingTop: '50px', textAlign: 'center', height: '100%' }}>
+ <IconContext.Provider value={{ className: 'fileError', size: '128px' }}>
+ <div style={{ alignSelf: 'center', alignContent: 'center' }}>{icon}</div>
+ </IconContext.Provider>
+ <p style={{ height: '32px', paddingTop: '25px', alignSelf: 'flex-start', textAlign: 'center' }}>{text}</p>
+ </div>
+ </>
+ );
+};
+
+export default FilePickerError;
diff --git a/frontend/src/components/modals/filepicker/i18n/TSortOptions.tsx b/frontend/src/components/modals/filepicker/i18n/TSortOptions.tsx
new file mode 100644
index 00000000..5d861d18
--- /dev/null
+++ b/frontend/src/components/modals/filepicker/i18n/TSortOptions.tsx
@@ -0,0 +1,46 @@
+import { FC } from 'react';
+import { Translation } from 'react-i18next';
+
+export enum SortOptions {
+ name_desc = 'name_desc',
+ name_asc = 'name_asc',
+ modified_desc = 'modified_desc',
+ modified_asc = 'modified_asc',
+ created_desc = 'created_desc',
+ created_asc = 'created_asc',
+ size_desc = 'size_desc',
+ size_asc = 'size_asc',
+}
+
+interface TSortOptionsProps {
+ trans_part: SortOptions;
+}
+
+const TSortOptions: FC<TSortOptionsProps> = ({ trans_part }) => {
+ return (
+ <Translation>
+ {(t, {}) => {
+ switch (trans_part) {
+ case SortOptions.name_desc:
+ return t('FilePickerIndex.filter.name_desc');
+ case SortOptions.name_asc:
+ return t('FilePickerIndex.filter.name_asce');
+ case SortOptions.modified_desc:
+ return t('FilePickerIndex.filter.modified_desc');
+ case SortOptions.modified_asc:
+ return t('FilePickerIndex.filter.modified_asce');
+ case SortOptions.created_desc:
+ return t('FilePickerIndex.filter.created_desc');
+ case SortOptions.created_asc:
+ return t('FilePickerIndex.filter.created_asce');
+ case SortOptions.size_desc:
+ return t('FilePickerIndex.filter.size_desc');
+ case SortOptions.size_asc:
+ return t('FilePickerIndex.filter.size_asce');
+ }
+ }}
+ </Translation>
+ );
+};
+
+export default TSortOptions;
diff --git a/frontend/src/components/modals/filepicker/iconCustomizations.ts b/frontend/src/components/modals/filepicker/iconCustomizations.ts
index e09c9e67..69e1652d 100644
--- a/frontend/src/components/modals/filepicker/iconCustomizations.ts
+++ b/frontend/src/components/modals/filepicker/iconCustomizations.ts
@@ -38,7 +38,7 @@ const imageStyle = {
color: '#d18f00',
};
-const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff'];
+const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff', 'apng', 'tga'];
styleDef.push([imageStyle, imageExtList]);
diff --git a/frontend/src/components/modals/filepicker/index.tsx b/frontend/src/components/modals/filepicker/index.tsx
index 629f4ec5..7aada5a5 100644
--- a/frontend/src/components/modals/filepicker/index.tsx
+++ b/frontend/src/components/modals/filepicker/index.tsx
@@ -1,11 +1,26 @@
-import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend-lib';
-import { useEffect } from 'react';
-import { FunctionComponent, useState } from 'react';
+import {
+ ControlsList,
+ DialogBody,
+ DialogButton,
+ DialogControlsSection,
+ DialogFooter,
+ Dropdown,
+ Focusable,
+ Marquee,
+ SteamSpinner,
+ TextField,
+ ToggleField,
+} from 'decky-frontend-lib';
+import { filesize } from 'filesize';
+import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
import { FileIcon, defaultStyles } from 'react-file-icon';
import { useTranslation } from 'react-i18next';
import { FaArrowUp, FaFolder } from 'react-icons/fa';
import Logger from '../../../logger';
+import DropdownMultiselect from '../DropdownMultiselect';
+import FilePickerError, { FileErrorTypes } from './FilePickerError';
+import TSortOption, { SortOptions } from './i18n/TSortOptions';
import { styleDefObj } from './iconCustomizations';
const logger = new Logger('FilePicker');
@@ -13,27 +28,89 @@ const logger = new Logger('FilePicker');
export interface FilePickerProps {
startPath: string;
includeFiles?: boolean;
- regex?: RegExp;
+ includeFolders?: boolean;
+ filter?: RegExp | ((file: File) => boolean);
+ validFileExtensions?: string[];
+ allowAllFiles?: boolean;
+ defaultHidden?: boolean;
+ max?: number;
onSubmit: (val: { path: string; realpath: string }) => void;
closeModal?: () => void;
}
-interface File {
+export interface File {
isdir: boolean;
+ ishidden: boolean;
name: string;
realpath: string;
+ size: number;
+ modified: number;
+ created: number;
}
interface FileListing {
realpath: string;
files: File[];
+ total: number;
}
+const sortOptions = [
+ {
+ data: SortOptions.name_desc,
+ label: <TSortOption trans_part={SortOptions.name_desc} />,
+ },
+ {
+ data: SortOptions.name_asc,
+ label: <TSortOption trans_part={SortOptions.name_asc} />,
+ },
+ {
+ data: SortOptions.modified_desc,
+ label: <TSortOption trans_part={SortOptions.modified_desc} />,
+ },
+ {
+ data: SortOptions.modified_asc,
+ label: <TSortOption trans_part={SortOptions.modified_asc} />,
+ },
+ {
+ data: SortOptions.created_desc,
+ label: <TSortOption trans_part={SortOptions.created_desc} />,
+ },
+ {
+ data: SortOptions.created_asc,
+ label: <TSortOption trans_part={SortOptions.created_asc} />,
+ },
+ {
+ data: SortOptions.size_desc,
+ label: <TSortOption trans_part={SortOptions.size_desc} />,
+ },
+ {
+ data: SortOptions.size_asc,
+ label: <TSortOption trans_part={SortOptions.size_asc} />,
+ },
+];
+
function getList(
path: string,
- includeFiles: boolean = true,
+ includeFiles: boolean,
+ includeFolders: boolean = true,
+ includeExt: string[] | null = null,
+ includeHidden: boolean = false,
+ orderBy: SortOptions = SortOptions.name_desc,
+ filterFor: RegExp | ((file: File) => boolean) | null = null,
+ pageNumber: number = 1,
+ max: number = 1000,
): Promise<{ result: FileListing | string; success: boolean }> {
- return window.DeckyPluginLoader.callServerMethod('filepicker_ls', { path, include_files: includeFiles });
+ return window.DeckyPluginLoader.callServerMethod('filepicker_ls', {
+ path,
+ include_files: includeFiles,
+ include_folders: includeFolders,
+ include_ext: includeExt ? includeExt : [],
+ include_hidden: includeHidden,
+ order_by: orderBy,
+ filter_for: filterFor,
+ page: pageNumber,
+ max: max,
+ });
}
const iconStyles = {
@@ -44,126 +121,240 @@ const iconStyles = {
const FilePicker: FunctionComponent<FilePickerProps> = ({
startPath,
includeFiles = true,
- regex,
+ filter = undefined,
+ includeFolders = true,
+ validFileExtensions = undefined,
+ allowAllFiles = true,
+ defaultHidden = false, // false by default makes sense for most users
+ max = 1000,
onSubmit,
closeModal,
}) => {
const { t } = useTranslation();
- if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
+
+ if (startPath !== '/' && 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 [listing, setListing] = useState<FileListing>({ files: [], realpath: path, total: 0 });
+ const [files, setFiles] = useState<File[]>([]);
+ const [error, setError] = useState<FileErrorTypes>(FileErrorTypes.None);
+ const [rawError, setRawError] = useState<string | null>(null);
+ const [page, setPage] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(true);
+ const [showHidden, setShowHidden] = useState<boolean>(defaultHidden);
+ const [sort, setSort] = useState<SortOptions>(SortOptions.name_desc);
+ const [selectedExts, setSelectedExts] = useState<string[] | undefined>(validFileExtensions);
+
+ const validExtsOptions = useMemo(() => {
+ let validExt: { label: string; value: string }[] = [];
+ if (validFileExtensions) {
+ if (allowAllFiles) {
+ validExt.push({ label: t('FilePickerIndex.files.all_files'), value: 'all_files' });
+ }
+ validExt.push(...validFileExtensions.map((x) => ({ label: x, value: x })));
+ }
+ return validExt;
+ }, [validFileExtensions, allowAllFiles]);
+
+ function isSelectionValid(validExts: string[], selection: string[]) {
+ if (validExts.some((el) => selection.includes(el))) return true;
+ return false;
+ }
+
+ const handleExtsSelect = useCallback((val: any) => {
+ // unselect other options if "All Files" is checked
+ if (allowAllFiles && val.includes('all_files')) {
+ setSelectedExts(['all_files']);
+ } else if (validFileExtensions && isSelectionValid(validFileExtensions, val)) {
+ // If at least one extension is still selected, then assign this selection to the selected values
+ setSelectedExts(val);
+ } else {
+ // Else do nothing
+ setSelectedExts(selectedExts);
+ }
+ }, []);
useEffect(() => {
(async () => {
- if (error) setError(null);
setLoading(true);
- const listing = await getList(path, includeFiles);
+ const listing = await getList(
+ path,
+ includeFiles,
+ includeFolders,
+ selectedExts,
+ showHidden,
+ sort,
+ filter,
+ page,
+ max,
+ );
if (!listing.success) {
- setListing({ files: [], realpath: path });
+ setListing({ files: [], realpath: path, total: 0 });
setLoading(false);
- setError(listing.result as string);
- logger.error(listing.result);
+ const theError = listing.result as string;
+ switch (theError) {
+ case theError.match(/\[Errno\s2.*/i)?.input:
+ case theError.match(/\[WinError\s3.*/i)?.input:
+ setError(FileErrorTypes.FileNotFound);
+ break;
+ default:
+ setRawError(theError);
+ setError(FileErrorTypes.Unknown);
+ break;
+ }
+ logger.debug(theError);
return;
+ } else {
+ setRawError(null);
+ setError(FileErrorTypes.None);
+ setFiles((listing.result as FileListing).files);
}
setLoading(false);
setListing(listing.result as FileListing);
logger.log('reloaded', path, listing);
})();
- }, [path]);
+ }, [error, path, includeFiles, includeFolders, showHidden, sort, selectedExts, page]);
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();
- let newPath = newPathArr.join('/');
- if (newPath == '') newPath = '/';
- 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}${path.endsWith('/') ? '' : '/'}${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>
- )}
- <span
+ <>
+ <DialogBody className="deckyFilePicker">
+ <DialogControlsSection>
+ <Focusable flow-children="right" style={{ display: 'flex', marginBottom: '1em' }}>
+ <DialogButton
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ minWidth: 'unset',
+ width: '40px',
+ borderRadius: 'unset',
+ margin: '0',
+ padding: '10px',
+ }}
+ onClick={() => {
+ const newPathArr = path.split('/');
+ const lastPath = newPathArr.pop();
+ //If I have a single / with spaces, pop the array twice
+ if (lastPath?.match(/^\/\s*$/) != null) newPathArr.pop();
+ let newPath = newPathArr.join('/');
+ if (newPath == '') newPath = '/';
+ setPath(newPath);
+ }}
+ >
+ <FaArrowUp />
+ </DialogButton>
+ <div style={{ width: '100%' }}>
+ <TextField
+ value={path}
+ onChange={(e) => {
+ e.target.value && setPath(e.target.value);
+ }}
+ style={{ height: '100%' }}
+ />
+ </div>
+ </Focusable>
+ <ControlsList alignItems="center" spacing="standard">
+ <ToggleField
+ highlightOnFocus={false}
+ label={t('FilePickerIndex.files.show_hidden')}
+ bottomSeparator="none"
+ checked={showHidden}
+ onChange={() => setShowHidden((x) => !x)}
+ />
+ <Dropdown rgOptions={sortOptions} selectedOption={sort} onChange={(x) => setSort(x.data)} />
+ {validFileExtensions && (
+ <DropdownMultiselect
+ label={t('FilePickerIndex.files.file_type')}
+ items={validExtsOptions}
+ selected={selectedExts ? selectedExts : []}
+ onSelect={handleExtsSelect}
+ />
+ )}
+ </ControlsList>
+ </DialogControlsSection>
+ <DialogControlsSection style={{ marginTop: '1em' }}>
+ <Focusable
+ style={{ display: 'flex', gap: '.25em', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}
+ >
+ {loading && error === FileErrorTypes.None && <SteamSpinner style={{ height: '100%' }} />}
+ {!loading &&
+ error === FileErrorTypes.None &&
+ files.map((file) => {
+ const extension = file.realpath.split('.').pop() as string;
+ return (
+ <DialogButton
+ key={`${file.realpath}${file.name}`}
+ style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
+ onClick={() => {
+ const fullPath = `${path}${path.endsWith('/') ? '' : '/'}${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>
+ )}
+ <Marquee>{file.name}</Marquee>
+ </div>
+ <div
style={{
- textOverflow: 'ellipsis',
- overflow: 'hidden',
+ display: 'flex',
+ opacity: 0.5,
+ fontSize: '.6em',
textAlign: 'left',
+ lineHeight: 1,
+ marginTop: '.5em',
}}
>
- {file.name}
- </span>
- </div>
- </DialogButton>
- );
- })}
- {error}
- </Focusable>
+ {file.isdir ? t('FilePickerIndex.folder.label') : filesize(file.size, { standard: 'iec' })}
+ <span style={{ marginLeft: 'auto' }}>{new Date(file.modified * 1000).toLocaleString()}</span>
+ </div>
+ </DialogButton>
+ );
+ })}
+ {error !== FileErrorTypes.None && <FilePickerError error={error} rawError={rawError ? rawError : ''} />}
+ </Focusable>
+ </DialogControlsSection>
+ </DialogBody>
{!loading && !error && !includeFiles && (
- <DialogButton
- className="Primary"
- style={{ marginTop: '10px', alignSelf: 'flex-end' }}
- onClick={() => {
- onSubmit({ path, realpath: listing.realpath });
- closeModal?.();
- }}
- >
- {t('FilePickerIndex.folder.select')}
- </DialogButton>
+ <DialogFooter>
+ <DialogButton
+ className="Primary"
+ style={{ marginTop: '10px', alignSelf: 'flex-end' }}
+ onClick={() => {
+ onSubmit({ path, realpath: listing.realpath });
+ closeModal?.();
+ }}
+ >
+ {t('FilePickerIndex.folder.select')}
+ </DialogButton>
+ </DialogFooter>
+ )}
+ {page * max < listing.total && (
+ <DialogFooter>
+ <DialogButton
+ className="Primary"
+ style={{ marginTop: '10px', alignSelf: 'flex-end' }}
+ onClick={() => {
+ setPage(page + 1);
+ }}
+ >
+ {t('FilePickerIndex.folder.show_more')}
+ </DialogButton>
+ </DialogFooter>
)}
- </div>
+ </>
);
};
diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx
index 200f13ab..3e6db2f7 100644
--- a/frontend/src/components/settings/pages/developer/index.tsx
+++ b/frontend/src/components/settings/pages/developer/index.tsx
@@ -13,26 +13,24 @@ import { useTranslation } from 'react-i18next';
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react-icons/fa';
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
+import Logger from '../../../../logger';
import { installFromURL } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
+import { getSetting } from '../../../../utils/settings';
import RemoteDebuggingSettings from '../general/RemoteDebugging';
-const installFromZip = () => {
- window.DeckyPluginLoader.openFilePicker('/home/deck', true).then((val) => {
+const logger = new Logger('DeveloperIndex');
+
+const installFromZip = async () => {
+ const path = await getSetting<string>('user_info.user_home', '');
+ if (path === '') {
+ logger.error('The default path has not been found!');
+ return;
+ }
+ window.DeckyPluginLoader.openFilePicker(path, true, undefined, true, ['zip', 'rar'], false, true).then((val) => {
const url = `file://${val.path}`;
console.log(`Installing plugin locally from ${url}`);
-
- if (url.endsWith('.zip')) {
- installFromURL(url);
- } else {
- window.DeckyPluginLoader.toaster.toast({
- //title: t('SettingsDeveloperIndex.toast_zip.title'),
- title: 'Decky',
- //body: t('SettingsDeveloperIndex.toast_zip.body'),
- body: 'Installation failed! Only ZIP files are supported.',
- onClick: installFromZip,
- });
- }
+ installFromURL(url);
});
};