feat(app): refactor App component and add Settings page
- Refactored the App component to utilize a new SearchCard component for rendering search results. - Introduced a Settings page with FolderPicker and Shortcuts components for user configuration. - Removed the ImagePage component as it was no longer needed. - Updated routing to include the new Settings page and adjusted imports accordingly. - Added a settings button to the main interface for easy access to the new settings functionality.
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { A } from "@solidjs/router";
|
||||
import { IconSearch } from "@tabler/icons-solidjs";
|
||||
import { IconSearch, IconSettings } from "@tabler/icons-solidjs";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import clsx from "clsx";
|
||||
import Fuse from "fuse.js";
|
||||
@ -11,6 +12,7 @@ import {
|
||||
onCleanup,
|
||||
} from "solid-js";
|
||||
import { ImageViewer } from "./components/ImageViewer";
|
||||
import { SearchCard } from "./components/search-card/SearchCard";
|
||||
import { SearchCardContact } from "./components/search-card/SearchCardContact";
|
||||
import { SearchCardEvent } from "./components/search-card/SearchCardEvent";
|
||||
import { SearchCardLocation } from "./components/search-card/SearchCardLocation";
|
||||
@ -18,27 +20,6 @@ import { SearchCardNote } from "./components/search-card/SearchCardNote";
|
||||
import { type UserImage, getUserImages } from "./network";
|
||||
import { getCardSize } from "./utils/getCardSize";
|
||||
|
||||
const getCardComponent = (item: UserImage) => {
|
||||
switch (item.type) {
|
||||
case "location":
|
||||
return <SearchCardLocation item={item} />;
|
||||
case "event":
|
||||
return <SearchCardEvent item={item} />;
|
||||
case "note":
|
||||
return <SearchCardNote item={item} />;
|
||||
case "contact":
|
||||
return <SearchCardContact item={item} />;
|
||||
// case "Website":
|
||||
// return <SearchCardWebsite item={item} />;
|
||||
// case "Note":
|
||||
// return <SearchCardNote item={item} />;
|
||||
// case "Receipt":
|
||||
// return <SearchCardReceipt item={item} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// How wonderfully functional
|
||||
const getAllValues = (object: object): Array<string> => {
|
||||
const loop = (acc: Array<string>, next: object): Array<string> => {
|
||||
@ -66,7 +47,7 @@ const getAllValues = (object: object): Array<string> => {
|
||||
return loop([], object);
|
||||
};
|
||||
|
||||
function App() {
|
||||
export const App = () => {
|
||||
const [searchResults, setSearchResults] = createSignal<UserImage[]>([]);
|
||||
const [searchQuery, setSearchQuery] = createSignal("");
|
||||
const [selectedItem, setSelectedItem] = createSignal<UserImage | null>(
|
||||
@ -125,11 +106,9 @@ function App() {
|
||||
return (
|
||||
<>
|
||||
<main class="container pt-2">
|
||||
<A href="login">login</A>
|
||||
<ImageViewer />
|
||||
<div class="px-4">
|
||||
<div class="px-4 flex items-center">
|
||||
<div class="inline-flex justify-between w-full rounded-xl text-base leading-none outline-none bg-white border border-neutral-200 text-neutral-900">
|
||||
<div class="appearance-none inline-flex justify-center items-center w-auto outline-none rounded-l-md px-2.5 text-gray-900">
|
||||
<div class="appearance-none inline-flex justify-center items-center w-auto outline-none rounded-md px-2.5 text-gray-900">
|
||||
<IconSearch
|
||||
size={20}
|
||||
class="m-auto size-5 text-neutral-600"
|
||||
@ -145,6 +124,13 @@ function App() {
|
||||
class="appearance-none inline-flex w-full min-h-[40px] text-base bg-transparent rounded-l-md outline-none placeholder:text-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
as="a"
|
||||
href="/settings"
|
||||
class="ml-2 p-2.5 bg-neutral-200 rounded-lg"
|
||||
>
|
||||
<IconSettings size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="px-4 mt-4 bg-white rounded-t-2xl">
|
||||
@ -162,33 +148,21 @@ function App() {
|
||||
setSelectedItem(item);
|
||||
}
|
||||
}}
|
||||
class={clsx(
|
||||
"h-[144px] border relative border-neutral-200 cursor-pointer overflow-hidden rounded-xl",
|
||||
{
|
||||
"col-span-3":
|
||||
getCardSize(
|
||||
item.type,
|
||||
) === "1/1",
|
||||
"col-span-6":
|
||||
getCardSize(
|
||||
item.type,
|
||||
) === "2/1",
|
||||
},
|
||||
)}
|
||||
class="h-[144px] border relative col-span-3 border-neutral-200 cursor-pointer overflow-hidden rounded-xl"
|
||||
>
|
||||
<span class="sr-only">
|
||||
{item.data.Name}
|
||||
</span>
|
||||
{getCardComponent(item)}
|
||||
<SearchCard item={item} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
) : searchQuery() !== "" ? (
|
||||
) : (
|
||||
<div class="text-center text-lg m-auto text-neutral-700">
|
||||
No results found
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -198,6 +172,4 @@ function App() {
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
};
|
||||
|
@ -1,67 +0,0 @@
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import { createEffect, createResource, For, Suspense } from "solid-js";
|
||||
import { getUserImages } from "./network";
|
||||
|
||||
export function ImagePage() {
|
||||
const { imageId } = useParams<{ imageId: string }>();
|
||||
|
||||
const [image] = createResource(async () => {
|
||||
const userImages = await getUserImages();
|
||||
|
||||
const currentImage = userImages.find((image) => image.ID === imageId);
|
||||
if (currentImage == null) {
|
||||
// TODO: this error handling.
|
||||
throw new Error("must be valid");
|
||||
}
|
||||
|
||||
return currentImage;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(image());
|
||||
});
|
||||
|
||||
return (
|
||||
<Suspense fallback={<>Loading...</>}>
|
||||
<A href="/">Back</A>
|
||||
<h1 class="text-2xl font-bold">{image()?.Image.ImageName}</h1>
|
||||
<img
|
||||
src={`http://localhost:3040/image/${image()?.ID}`}
|
||||
alt="link"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-xl font-bold">Tags</h2>
|
||||
<For each={image()?.Tags ?? []}>
|
||||
{(tag) => <div>{tag.Tag.Tag}</div>}
|
||||
</For>
|
||||
|
||||
<h2 class="text-xl font-bold">Locations</h2>
|
||||
<For each={image()?.Locations ?? []}>
|
||||
{(location) => (
|
||||
<ul>
|
||||
<li>{location.Name}</li>
|
||||
{location.Address && <li>{location.Address}</li>}
|
||||
{location.Coordinates && (
|
||||
<li>{location.Coordinates}</li>
|
||||
)}
|
||||
{location.Description && (
|
||||
<li>{location.Description}</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<h2 class="text-xl font-bold">Events</h2>
|
||||
<For each={image()?.Events ?? []}>
|
||||
{(event) => (
|
||||
<ul>
|
||||
<li>{event.Name}</li>
|
||||
{event.Location && <li>{event.Location.Name}</li>}
|
||||
{event.Description && <li>{event.Description}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
22
frontend/src/Settings.tsx
Normal file
22
frontend/src/Settings.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { FolderPicker } from "./components/FolderPicker";
|
||||
import { Shortcuts } from "./components/shortcuts/Shortcuts";
|
||||
|
||||
export const Settings = () => {
|
||||
return (
|
||||
<>
|
||||
<main class="container pt-2">
|
||||
<div class="flex px-4 gap-2 items-center">
|
||||
<Button as="a" href="/">
|
||||
Back to home
|
||||
</Button>
|
||||
<h1 class="text-2xl font-bold">Settings</h1>
|
||||
</div>
|
||||
<div class="flex flex-col px-4 gap-2">
|
||||
<FolderPicker />
|
||||
<Shortcuts />
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,10 +1,9 @@
|
||||
import { createSignal } from "solid-js";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { createSignal } from "solid-js";
|
||||
|
||||
export function FolderPicker() {
|
||||
const [selectedPath, setSelectedPath] = createSignal<string>("");
|
||||
const [status, setStatus] = createSignal<string>("");
|
||||
|
||||
const handleFolderSelect = async () => {
|
||||
try {
|
||||
@ -19,10 +18,11 @@ export function FolderPicker() {
|
||||
const response = await invoke("handle_selected_folder", {
|
||||
path: selected,
|
||||
});
|
||||
setStatus(`Folder processed: ${response}`);
|
||||
|
||||
console.log("DBG: ", response);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(`Error: ${error}`);
|
||||
console.error("DBG: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -42,8 +42,6 @@ export function FolderPicker() {
|
||||
<p class="text-sm break-all">{selectedPath()}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status() && <p class="text-sm text-gray-600">{status()}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
22
frontend/src/components/search-card/SearchCard.tsx
Normal file
22
frontend/src/components/search-card/SearchCard.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { UserImage } from "../../network";
|
||||
import { SearchCardContact } from "./SearchCardContact";
|
||||
import { SearchCardEvent } from "./SearchCardEvent";
|
||||
import { SearchCardLocation } from "./SearchCardLocation";
|
||||
import { SearchCardNote } from "./SearchCardNote";
|
||||
|
||||
export const SearchCard = (props: { item: UserImage }) => {
|
||||
const { item } = props;
|
||||
|
||||
switch (item.type) {
|
||||
case "location":
|
||||
return <SearchCardLocation item={item} />;
|
||||
case "event":
|
||||
return <SearchCardEvent item={item} />;
|
||||
case "note":
|
||||
return <SearchCardNote item={item} />;
|
||||
case "contact":
|
||||
return <SearchCardContact item={item} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
@ -19,10 +19,6 @@ export const SearchCardContact = ({ item }: Props) => {
|
||||
<p class="text-xs text-neutral-500">{data.PhoneNumber}</p>
|
||||
<Separator class="my-2" />
|
||||
<p class="text-xs text-neutral-500">{data.Email}</p>
|
||||
<Separator class="my-2" />
|
||||
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
|
||||
{data.Description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Separator } from "@kobalte/core/separator";
|
||||
|
||||
import { IconCalendar } from "@tabler/icons-solidjs";
|
||||
import type { UserImage } from "../../network";
|
||||
|
||||
@ -18,15 +16,13 @@ export const SearchCardEvent = ({ item }: Props) => {
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500">
|
||||
Organized by {data.Organizer?.Name ?? "unknown"} on{" "}
|
||||
{new Date(data.StartDateTime).toLocaleDateString("en-US", {
|
||||
{data.StartDateTime
|
||||
? new Date(data.StartDateTime).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
<Separator class="my-2" />
|
||||
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
|
||||
{data.Description}
|
||||
})
|
||||
: "unknown date"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Separator } from "@kobalte/core/separator";
|
||||
|
||||
import { IconMapPin } from "@tabler/icons-solidjs";
|
||||
import type { UserImage } from "../../network";
|
||||
|
||||
@ -17,10 +15,6 @@ export const SearchCardLocation = ({ item }: Props) => {
|
||||
<IconMapPin size={20} class="text-neutral-500 mt-1" />
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500">{data.Address}</p>
|
||||
<Separator class="my-2" />
|
||||
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
|
||||
{data.Description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,11 +16,7 @@ export const SearchCardNote = ({ item }: Props) => {
|
||||
<p class="text-sm text-neutral-900 font-bold">{data.Name}</p>
|
||||
<IconNote size={20} class="text-neutral-500 mt-1" />
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500">Keywords TODO</p>
|
||||
<Separator class="my-2" />
|
||||
<p class="text-xs text-neutral-500 line-clamp-2 overflow-hidden">
|
||||
{data.Content}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-500">Note</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
78
frontend/src/components/shortcuts/ShortcutItem.tsx
Normal file
78
frontend/src/components/shortcuts/ShortcutItem.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { IconX } from "@tabler/icons-solidjs";
|
||||
import { type Component, For } from "solid-js";
|
||||
import { formatKey } from "./utils/formatKey";
|
||||
import { sortKeys } from "./utils/sortKeys";
|
||||
|
||||
interface ShortcutItemProps {
|
||||
shortcut: string[];
|
||||
isEditing: boolean;
|
||||
currentKeys: string[];
|
||||
onEdit: () => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ShortcutItem: Component<ShortcutItemProps> = (props) => {
|
||||
const renderKeys = (keys: string[]) => {
|
||||
const sortedKeys = sortKeys(keys);
|
||||
return (
|
||||
<For each={sortedKeys}>
|
||||
{(key) => (
|
||||
<kbd class="px-2 py-1 text-sm font-semibold rounded shadow-sm bg-neutral-100 border-neutral-300 text-neutral-900 ">
|
||||
{formatKey(key)}
|
||||
</kbd>
|
||||
)}
|
||||
</For>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex items-center justify-between p-4 rounded-lg bg-neutral-5">
|
||||
<div class="flex items-center gap-4">
|
||||
{props.isEditing ? (
|
||||
<>
|
||||
<div class="flex gap-1 min-w-[120px] justify-end">
|
||||
{props.currentKeys.length > 0 ? (
|
||||
renderKeys(props.currentKeys)
|
||||
) : (
|
||||
<span class="italic text-neutral-500">
|
||||
Press keys...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onSave}
|
||||
disabled={props.currentKeys.length < 2}
|
||||
class="px-3 py-1 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onCancel}
|
||||
class="p-1 rounded text-neutral-500 hover:text-neutral-700 hover:bg-neutral-200 "
|
||||
>
|
||||
<IconX class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div class="flex gap-1">
|
||||
{renderKeys(props.shortcut)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onEdit}
|
||||
class="px-3 py-1 text-sm rounded bg-neutral-200 text-neutral-700 hover:bg-neutral-300 "
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
70
frontend/src/components/shortcuts/Shortcuts.tsx
Normal file
70
frontend/src/components/shortcuts/Shortcuts.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { createSignal, onMount } from "solid-js";
|
||||
|
||||
import { ShortcutItem } from "./ShortcutItem";
|
||||
import { type Shortcut, useShortcutEditor } from "./hooks/useShortcutEditor";
|
||||
|
||||
export const Shortcuts = () => {
|
||||
const [shortcut, setShortcut] = createSignal<Shortcut>([]);
|
||||
|
||||
async function getCurrentShortcut() {
|
||||
try {
|
||||
const res: string = await invoke("get_current_shortcut");
|
||||
console.log("DBG: ", res);
|
||||
setShortcut(res?.split("+"));
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch shortcut:", err);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getCurrentShortcut();
|
||||
});
|
||||
|
||||
const changeShortcut = (key: Shortcut) => {
|
||||
setShortcut(key);
|
||||
if (key.length === 0) return;
|
||||
invoke("change_shortcut", { key: key?.join("+") }).catch((err) => {
|
||||
console.error("Failed to save hotkey:", err);
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
isEditing,
|
||||
currentKeys,
|
||||
startEditing,
|
||||
saveShortcut,
|
||||
cancelEditing,
|
||||
} = useShortcutEditor(shortcut(), changeShortcut);
|
||||
|
||||
const onEditShortcut = async () => {
|
||||
startEditing();
|
||||
invoke("unregister_shortcut").catch((err) => {
|
||||
console.error("Failed to save hotkey:", err);
|
||||
});
|
||||
};
|
||||
|
||||
const onCancelShortcut = async () => {
|
||||
cancelEditing();
|
||||
invoke("change_shortcut", { key: shortcut()?.join("+") }).catch(
|
||||
(err) => {
|
||||
console.error("Failed to save hotkey:", err);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveShortcut = async () => {
|
||||
saveShortcut();
|
||||
};
|
||||
|
||||
return (
|
||||
<ShortcutItem
|
||||
shortcut={shortcut()}
|
||||
isEditing={isEditing()}
|
||||
currentKeys={currentKeys()}
|
||||
onEdit={onEditShortcut}
|
||||
onSave={onSaveShortcut}
|
||||
onCancel={onCancelShortcut}
|
||||
/>
|
||||
);
|
||||
};
|
134
frontend/src/components/shortcuts/hooks/useShortcutEditor.ts
Normal file
134
frontend/src/components/shortcuts/hooks/useShortcutEditor.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js";
|
||||
import { isModifierKey } from "../utils/isModifierKey";
|
||||
import { normalizeKey } from "../utils/normalizeKey";
|
||||
import { sortKeys } from "../utils/sortKeys";
|
||||
|
||||
export type Shortcut = string[];
|
||||
|
||||
const RESERVED_SHORTCUTS = [
|
||||
["Command", "C"],
|
||||
["Command", "V"],
|
||||
["Command", "X"],
|
||||
["Command", "A"],
|
||||
["Command", "Z"],
|
||||
["Command", "Q"],
|
||||
// Windows/Linux
|
||||
["Control", "C"],
|
||||
["Control", "V"],
|
||||
["Control", "X"],
|
||||
["Control", "A"],
|
||||
["Control", "Z"],
|
||||
];
|
||||
|
||||
export function useShortcutEditor(
|
||||
shortcut: Shortcut,
|
||||
onChange: (shortcut: Shortcut) => void,
|
||||
) {
|
||||
console.log("DBG: ", shortcut);
|
||||
|
||||
const [isEditing, setIsEditing] = createSignal(false);
|
||||
const [currentKeys, setCurrentKeys] = createSignal<string[]>([]);
|
||||
const pressedKeys = new Set<string>();
|
||||
|
||||
const startEditing = () => {
|
||||
setIsEditing(true);
|
||||
setCurrentKeys([]);
|
||||
};
|
||||
|
||||
const saveShortcut = async () => {
|
||||
if (!isEditing() || currentKeys().length < 2) return;
|
||||
|
||||
const hasModifier = currentKeys().some(isModifierKey);
|
||||
const hasNonModifier = currentKeys().some((key) => !isModifierKey(key));
|
||||
|
||||
if (!hasModifier || !hasNonModifier) return;
|
||||
|
||||
const isReserved = RESERVED_SHORTCUTS.some(
|
||||
(reserved) =>
|
||||
reserved.length === currentKeys().length &&
|
||||
reserved.every(
|
||||
(key, index) =>
|
||||
key.toLowerCase() ===
|
||||
currentKeys()[index].toLowerCase(),
|
||||
),
|
||||
);
|
||||
|
||||
if (isReserved) {
|
||||
console.error("This is a system reserved shortcut");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort keys to ensure consistent order (modifiers first)
|
||||
const sortedKeys = sortKeys(currentKeys());
|
||||
|
||||
onChange(sortedKeys);
|
||||
setIsEditing(false);
|
||||
setCurrentKeys([]);
|
||||
};
|
||||
|
||||
const cancelEditing = () => {
|
||||
setIsEditing(false);
|
||||
setCurrentKeys([]);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isEditing()) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const key = normalizeKey(e.code);
|
||||
|
||||
// Update pressed keys
|
||||
pressedKeys.add(key);
|
||||
|
||||
setCurrentKeys(() => {
|
||||
const keys = Array.from(pressedKeys);
|
||||
let modifiers = keys.filter(isModifierKey);
|
||||
let nonModifiers = keys.filter((k) => !isModifierKey(k));
|
||||
|
||||
if (modifiers.length > 2) {
|
||||
modifiers = modifiers.slice(0, 2);
|
||||
}
|
||||
|
||||
if (nonModifiers.length > 2) {
|
||||
nonModifiers = nonModifiers.slice(0, 2);
|
||||
}
|
||||
|
||||
// Combine modifiers and non-modifiers
|
||||
return [...modifiers, ...nonModifiers];
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (!isEditing()) return;
|
||||
const key = normalizeKey(e.code);
|
||||
pressedKeys.delete(key);
|
||||
};
|
||||
|
||||
// Use createEffect to handle event listeners based on editing state
|
||||
createEffect(() => {
|
||||
if (isEditing()) {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
} else {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
pressedKeys.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up event listeners on unmount
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
});
|
||||
|
||||
return {
|
||||
isEditing,
|
||||
currentKeys,
|
||||
startEditing,
|
||||
saveShortcut,
|
||||
cancelEditing,
|
||||
};
|
||||
}
|
38
frontend/src/components/shortcuts/utils/formatKey.ts
Normal file
38
frontend/src/components/shortcuts/utils/formatKey.ts
Normal file
@ -0,0 +1,38 @@
|
||||
export const formatKey = (key: string): string => {
|
||||
// Convert to uppercase for consistency
|
||||
const upperKey = key.toUpperCase();
|
||||
|
||||
// Handle special keys
|
||||
switch (upperKey) {
|
||||
case "CONTROL":
|
||||
return "Ctrl";
|
||||
case "META":
|
||||
return "⌘";
|
||||
case "ALT":
|
||||
return "⌥";
|
||||
case "SHIFT":
|
||||
return "⇧";
|
||||
case "ARROWUP":
|
||||
return "↑";
|
||||
case "ARROWDOWN":
|
||||
return "↓";
|
||||
case "ARROWLEFT":
|
||||
return "←";
|
||||
case "ARROWRIGHT":
|
||||
return "→";
|
||||
case "ESCAPE":
|
||||
return "Esc";
|
||||
case "ENTER":
|
||||
return "Enter";
|
||||
case "BACKSPACE":
|
||||
return "⌫";
|
||||
case "DELETE":
|
||||
return "⌦";
|
||||
case "TAB":
|
||||
return "⇥";
|
||||
case "CAPSLOCK":
|
||||
return "⇪";
|
||||
default:
|
||||
return upperKey;
|
||||
}
|
||||
};
|
4
frontend/src/components/shortcuts/utils/isModifierKey.ts
Normal file
4
frontend/src/components/shortcuts/utils/isModifierKey.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const isModifierKey = (key: string): boolean => {
|
||||
const modifiers = ["Control", "Shift", "Alt", "Meta", "Command"];
|
||||
return modifiers.includes(key);
|
||||
};
|
33
frontend/src/components/shortcuts/utils/normalizeKey.ts
Normal file
33
frontend/src/components/shortcuts/utils/normalizeKey.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export const normalizeKey = (code: string): string => {
|
||||
// Remove 'Key' prefix from letter keys
|
||||
if (code.startsWith("Key")) {
|
||||
return code.slice(3);
|
||||
}
|
||||
// Remove 'Digit' prefix from number keys
|
||||
if (code.startsWith("Digit")) {
|
||||
return code.slice(5);
|
||||
}
|
||||
// Handle special keys
|
||||
const specialKeys: Record<string, string> = {
|
||||
ControlLeft: "Control",
|
||||
ControlRight: "Control",
|
||||
ShiftLeft: "Shift",
|
||||
ShiftRight: "Shift",
|
||||
AltLeft: "Alt",
|
||||
AltRight: "Alt",
|
||||
MetaLeft: "Command",
|
||||
MetaRight: "Command",
|
||||
ArrowLeft: "ArrowLeft",
|
||||
ArrowRight: "ArrowRight",
|
||||
ArrowUp: "ArrowUp",
|
||||
ArrowDown: "ArrowDown",
|
||||
Enter: "Enter",
|
||||
Space: "Space",
|
||||
Escape: "Escape",
|
||||
Backspace: "Backspace",
|
||||
Delete: "Delete",
|
||||
Tab: "Tab",
|
||||
CapsLock: "CapsLock",
|
||||
};
|
||||
return specialKeys[code] || code;
|
||||
};
|
14
frontend/src/components/shortcuts/utils/sortKeys.ts
Normal file
14
frontend/src/components/shortcuts/utils/sortKeys.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const sortKeys = (keys: string[]): string[] => {
|
||||
const priority: { [key: string]: number } = {
|
||||
Control: 1,
|
||||
Meta: 2,
|
||||
Alt: 3,
|
||||
Shift: 4,
|
||||
};
|
||||
|
||||
return [...keys].sort((a, b) => {
|
||||
const aPriority = priority[a] || 999;
|
||||
const bPriority = priority[b] || 999;
|
||||
return aPriority - bPriority;
|
||||
});
|
||||
};
|
@ -1,11 +1,13 @@
|
||||
/* @refresh reload */
|
||||
import { render } from "solid-js/web";
|
||||
import App from "./App";
|
||||
|
||||
import "./index.css";
|
||||
import { Route, Router } from "@solidjs/router";
|
||||
import { ImagePage } from "./ImagePage";
|
||||
|
||||
import { App } from "./App";
|
||||
import { Login } from "./Login";
|
||||
import { ProtectedRoute } from "./ProtectedRoute";
|
||||
import { Settings } from "./Settings";
|
||||
|
||||
render(
|
||||
() => (
|
||||
@ -14,7 +16,7 @@ render(
|
||||
|
||||
<Route path="/" component={ProtectedRoute}>
|
||||
<Route path="/" component={App} />
|
||||
<Route path="/image/:imageId" component={ImagePage} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
</Route>
|
||||
</Router>
|
||||
),
|
||||
|
Reference in New Issue
Block a user