feat: implemented storage management
fix: Enabled fallback secrets feat: Made header stats actually work feat: Made steam deck keyboard auto open for some inputs fix: Made keybaord also work with shortcuts (no tooltips yet)
This commit is contained in:
parent
62f16cbcc1
commit
e4df8fb9fb
55 changed files with 1675 additions and 398 deletions
|
|
@ -5,22 +5,39 @@ import
|
|||
useFocusable,
|
||||
} from "@noriginmedia/norigin-spatial-navigation";
|
||||
import classNames from "classnames";
|
||||
import { GamePadButtonCode, Shortcut, useShortcuts } from "@/mainview/scripts/shortcuts";
|
||||
|
||||
export function Button (data: { id: string, children?: any, className?: string, disabled?: boolean, type: "reset" | "button" | "submit" | undefined; } & InteractParams & FocusParams)
|
||||
export function Button (data: {
|
||||
id: string,
|
||||
children?: any,
|
||||
className?: string,
|
||||
disabled?: boolean,
|
||||
type: "reset" | "button" | "submit" | undefined;
|
||||
shortcutLabel?: string;
|
||||
focusClassName?: string;
|
||||
} & InteractParams & FocusParams)
|
||||
{
|
||||
const { ref, focused } = useFocusable({
|
||||
const { ref, focused, focusKey } = useFocusable({
|
||||
focusKey: data.id,
|
||||
onEnterPress: data.onAction,
|
||||
onFocus: data.onFocus,
|
||||
focusable: !data.disabled
|
||||
});
|
||||
|
||||
if (data.shortcutLabel)
|
||||
{
|
||||
useShortcuts(focusKey, () => [{ label: data.shortcutLabel, action: data.onAction, button: GamePadButtonCode.A }], [data.shortcutLabel]);
|
||||
}
|
||||
|
||||
return <button
|
||||
ref={ref}
|
||||
onClick={data.onAction}
|
||||
disabled={data.disabled}
|
||||
className={twMerge("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg", classNames({
|
||||
"btn-accent": focused
|
||||
}, data.className))}
|
||||
className={twMerge("btn rounded-full focus:bg-base-content focus:text-base-300 md:text-lg",
|
||||
focused ? data.focusClassName : undefined,
|
||||
classNames({
|
||||
"btn-accent": focused,
|
||||
}, data.className))}
|
||||
type={data.type}
|
||||
>
|
||||
{data.children}
|
||||
|
|
|
|||
32
src/mainview/components/options/DownloadDirectoryOption.tsx
Normal file
32
src/mainview/components/options/DownloadDirectoryOption.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useState } from "react";
|
||||
import { PathSettingsOptionBase, PathSettingsOptionParams } from "./PathSettingsOption";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { changeDownloadsMutation } from "@/mainview/scripts/queries";
|
||||
|
||||
export default function DownloadDirectoryOption (data: PathSettingsOptionParams)
|
||||
{
|
||||
const [localValue, setLocalValue] = useState<string | undefined>();
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const setSettingMutation = useMutation({
|
||||
...changeDownloadsMutation,
|
||||
onSuccess: (d, v, r, cx) =>
|
||||
{
|
||||
setDirty(r !== localValue);
|
||||
}
|
||||
});
|
||||
|
||||
return <PathSettingsOptionBase
|
||||
isDirty={dirty}
|
||||
label={data.label}
|
||||
id={data.id}
|
||||
type={data.type}
|
||||
save={setSettingMutation.mutate}
|
||||
allowNewFolderCreation={data.allowNewFolderCreation}
|
||||
isDirectoryPicker={true}
|
||||
localValue={localValue}
|
||||
setLocalValue={(v) =>
|
||||
{
|
||||
setLocalValue(v);
|
||||
setDirty(true);
|
||||
}} />;
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ export function OptionInput (data: {
|
|||
focusKey: data.name, onEnterPress: () =>
|
||||
{
|
||||
inputRef.current?.focus();
|
||||
systemApi.api.system.show_keyboard.post();
|
||||
}
|
||||
});
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -32,6 +31,21 @@ export function OptionInput (data: {
|
|||
inputRef.current?.focus();
|
||||
},
|
||||
});
|
||||
const handleFocus = () =>
|
||||
{
|
||||
option.focus();
|
||||
if (inputRef.current)
|
||||
{
|
||||
var rect = inputRef.current?.getBoundingClientRect();
|
||||
systemApi.api.system.show_keyboard.post({
|
||||
XPosition: rect.x,
|
||||
YPosition: rect.y,
|
||||
Width: rect.width,
|
||||
Height: rect.height
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<label ref={ref} className={twMerge("flex items-center gap-3 rounded-full sm:flex-2 md:flex-1 divide-accent",
|
||||
|
|
@ -47,7 +61,7 @@ export function OptionInput (data: {
|
|||
defaultValue={data.defaultValue}
|
||||
type={data.type}
|
||||
autoComplete={data.autocomplete}
|
||||
onFocus={() => option.focus()}
|
||||
onFocus={handleFocus}
|
||||
placeholder={data.placeholder}
|
||||
onChange={data.onChange}
|
||||
onBlur={data.onBlur}
|
||||
|
|
|
|||
156
src/mainview/components/options/PathSettingsOption.tsx
Normal file
156
src/mainview/components/options/PathSettingsOption.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import { HTMLInputTypeAttribute, JSX, useCallback, useState } from "react";
|
||||
import { SettingsType } from "../../../shared/constants";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { OptionSpace } from "./OptionSpace";
|
||||
import { OptionInput } from "./OptionInput";
|
||||
import { settingsApi } from "../../scripts/clientApi";
|
||||
import { Button } from "./Button";
|
||||
import { FileSearchCorner, FolderSearch, Pen, Save } from "lucide-react";
|
||||
import { ContextDialog } from "../ContextDialog";
|
||||
import FilePicker from "../FilePicker";
|
||||
import { setFocus } from "@noriginmedia/norigin-spatial-navigation";
|
||||
|
||||
type KeysWithValueAssignableTo<T, Value> = {
|
||||
[K in keyof T]: Exclude<T[K], undefined> extends Value ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export interface PathSettingsOptionParams
|
||||
{
|
||||
label: string;
|
||||
id: KeysWithValueAssignableTo<SettingsType, string>;
|
||||
type: HTMLInputTypeAttribute;
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
children?: any;
|
||||
onBrowseAction?: (path: string | undefined) => void;
|
||||
requireConfirmation?: boolean;
|
||||
isDirectoryPicker?: boolean;
|
||||
allowNewFolderCreation?: boolean;
|
||||
}
|
||||
|
||||
export function PathSettingsOption (data: PathSettingsOptionParams)
|
||||
{
|
||||
const [localValue, setLocalValue] = useState<string | undefined>();
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const setSettingMutation = useMutation({
|
||||
mutationKey: ["setting", data.id],
|
||||
mutationFn: async (value: any) =>
|
||||
{
|
||||
const response = await settingsApi.api.settings({ id: data.id! }).post({ value });
|
||||
if (response.error) throw response.error;
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (d, v, r, cx) =>
|
||||
{
|
||||
setDirty(r !== localValue);
|
||||
}
|
||||
});
|
||||
|
||||
return <PathSettingsOptionBase
|
||||
isDirty={dirty}
|
||||
label={data.label}
|
||||
id={data.id}
|
||||
type={data.type}
|
||||
save={setSettingMutation.mutate}
|
||||
localValue={localValue}
|
||||
allowNewFolderCreation={data.allowNewFolderCreation}
|
||||
setLocalValue={(v) =>
|
||||
{
|
||||
setLocalValue(v);
|
||||
setDirty(true);
|
||||
}} />;
|
||||
}
|
||||
|
||||
export function PathSettingsOptionBase (data: PathSettingsOptionParams & {
|
||||
save: (value: string | undefined) => void;
|
||||
localValue: string | undefined;
|
||||
setLocalValue: (value: string | undefined) => void;
|
||||
isDirty: boolean;
|
||||
})
|
||||
{
|
||||
const [isBrowsing, setIsBrowsing] = useState(false);
|
||||
const { data: defaultValue } = useQuery({
|
||||
enabled: !!data.id,
|
||||
queryKey: ["setting", data.id],
|
||||
queryFn: async () =>
|
||||
{
|
||||
const { data: value, error } = await settingsApi.api.settings({ id: data.id! }).get();
|
||||
if (error) throw error;
|
||||
if (!data.isDirty)
|
||||
{
|
||||
data.setLocalValue(String(value.value));
|
||||
}
|
||||
return value.value;
|
||||
},
|
||||
});
|
||||
const changed = defaultValue !== data.localValue;
|
||||
|
||||
const handleSelectPath = (path: string) =>
|
||||
{
|
||||
data.setLocalValue(path);
|
||||
handleCloseSeatch();
|
||||
if (data.requireConfirmation !== true)
|
||||
{
|
||||
data.save(path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseSeatch = () =>
|
||||
{
|
||||
setIsBrowsing(false);
|
||||
setFocus(`${data.id}-browse`);
|
||||
};
|
||||
|
||||
const handleInputBlur = () =>
|
||||
{
|
||||
if (data.requireConfirmation !== true)
|
||||
{
|
||||
data.save(data.localValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<OptionSpace id={data.id} className="gap-2" label={<>{data.label}{changed && <Pen />}</>}>
|
||||
<OptionInput
|
||||
icon={data.icon}
|
||||
name={`${data.id}-input`}
|
||||
type={data.type}
|
||||
placeholder={data.placeholder}
|
||||
onBlur={handleInputBlur}
|
||||
onChange={(e) =>
|
||||
{
|
||||
data.setLocalValue(e.currentTarget.value);
|
||||
}}
|
||||
value={data.localValue}
|
||||
/>
|
||||
<Button id={`${data.id}-browse`} className="ring-accent-content" focusClassName="ring-7" onAction={() =>
|
||||
{
|
||||
setIsBrowsing(true);
|
||||
data.onBrowseAction?.(data.localValue);
|
||||
}} type="button">
|
||||
{data.isDirectoryPicker ? <FolderSearch /> : <FileSearchCorner />}
|
||||
</Button>
|
||||
{data.requireConfirmation === true && <Button
|
||||
disabled={defaultValue === data.localValue}
|
||||
id={`${data.id}-save`}
|
||||
onAction={() => data.save(data.localValue)}
|
||||
type="button">
|
||||
<Save />
|
||||
</Button>}
|
||||
|
||||
<ContextDialog className="h-[80vh] w-[60vw]" id={`file-picker-${data.id}`} open={isBrowsing} close={handleCloseSeatch} >
|
||||
{isBrowsing && <FilePicker
|
||||
isDirectoryPicker={data.isDirectoryPicker}
|
||||
onSelect={handleSelectPath}
|
||||
key={`download-path-${data.id}`}
|
||||
startingPath={data.localValue}
|
||||
id={`download-path-${data.id}`}
|
||||
cancel={handleCloseSeatch}
|
||||
allowNewFolderCreation={data.allowNewFolderCreation}
|
||||
/>
|
||||
}
|
||||
</ContextDialog>
|
||||
{data.children}
|
||||
</OptionSpace>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ export function SettingsOption (data: {
|
|||
type: HTMLInputTypeAttribute;
|
||||
placeholder?: string;
|
||||
icon?: JSX.Element;
|
||||
children?: any;
|
||||
})
|
||||
{
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
|
@ -67,6 +68,7 @@ export function SettingsOption (data: {
|
|||
}}
|
||||
value={localValue}
|
||||
/>
|
||||
{data.children}
|
||||
</OptionSpace>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue