From c99d6e4e6b4e1b1301146cf908c46f9e3925918b Mon Sep 17 00:00:00 2001 From: Dmytro Kondakov Date: Sun, 13 Apr 2025 22:48:26 +0200 Subject: [PATCH] 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. --- frontend/src/App.tsx | 64 +++------ frontend/src/ImagePage.tsx | 67 --------- frontend/src/Settings.tsx | 22 +++ frontend/src/components/FolderPicker.tsx | 12 +- .../src/components/search-card/SearchCard.tsx | 22 +++ .../search-card/SearchCardContact.tsx | 4 - .../search-card/SearchCardEvent.tsx | 18 +-- .../search-card/SearchCardLocation.tsx | 6 - .../components/search-card/SearchCardNote.tsx | 6 +- .../src/components/shortcuts/ShortcutItem.tsx | 78 ++++++++++ .../src/components/shortcuts/Shortcuts.tsx | 70 +++++++++ .../shortcuts/hooks/useShortcutEditor.ts | 134 ++++++++++++++++++ .../components/shortcuts/utils/formatKey.ts | 38 +++++ .../shortcuts/utils/isModifierKey.ts | 4 + .../shortcuts/utils/normalizeKey.ts | 33 +++++ .../components/shortcuts/utils/sortKeys.ts | 14 ++ frontend/src/index.tsx | 8 +- 17 files changed, 451 insertions(+), 149 deletions(-) delete mode 100644 frontend/src/ImagePage.tsx create mode 100644 frontend/src/Settings.tsx create mode 100644 frontend/src/components/search-card/SearchCard.tsx create mode 100644 frontend/src/components/shortcuts/ShortcutItem.tsx create mode 100644 frontend/src/components/shortcuts/Shortcuts.tsx create mode 100644 frontend/src/components/shortcuts/hooks/useShortcutEditor.ts create mode 100644 frontend/src/components/shortcuts/utils/formatKey.ts create mode 100644 frontend/src/components/shortcuts/utils/isModifierKey.ts create mode 100644 frontend/src/components/shortcuts/utils/normalizeKey.ts create mode 100644 frontend/src/components/shortcuts/utils/sortKeys.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6d3009b..9a796da 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ; - case "event": - return ; - case "note": - return ; - case "contact": - return ; - // case "Website": - // return ; - // case "Note": - // return ; - // case "Receipt": - // return ; - default: - return null; - } -}; - // How wonderfully functional const getAllValues = (object: object): Array => { const loop = (acc: Array, next: object): Array => { @@ -66,7 +47,7 @@ const getAllValues = (object: object): Array => { return loop([], object); }; -function App() { +export const App = () => { const [searchResults, setSearchResults] = createSignal([]); const [searchQuery, setSearchQuery] = createSignal(""); const [selectedItem, setSelectedItem] = createSignal( @@ -125,11 +106,9 @@ function App() { return ( <>
- login - -
+
-
+
+
@@ -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" > {item.data.Name} - {getCardComponent(item)} +
)}
- ) : searchQuery() !== "" ? ( + ) : (
No results found
- ) : null} + )}
@@ -198,6 +172,4 @@ function App() {
); -} - -export default App; +}; diff --git a/frontend/src/ImagePage.tsx b/frontend/src/ImagePage.tsx deleted file mode 100644 index 5d3bc5c..0000000 --- a/frontend/src/ImagePage.tsx +++ /dev/null @@ -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 ( - Loading...}> - Back -

{image()?.Image.ImageName}

- link -
-

Tags

- - {(tag) =>
{tag.Tag.Tag}
} -
- -

Locations

- - {(location) => ( -
    -
  • {location.Name}
  • - {location.Address &&
  • {location.Address}
  • } - {location.Coordinates && ( -
  • {location.Coordinates}
  • - )} - {location.Description && ( -
  • {location.Description}
  • - )} -
- )} -
- -

Events

- - {(event) => ( -
    -
  • {event.Name}
  • - {event.Location &&
  • {event.Location.Name}
  • } - {event.Description &&
  • {event.Description}
  • } -
- )} -
-
-
- ); -} diff --git a/frontend/src/Settings.tsx b/frontend/src/Settings.tsx new file mode 100644 index 0000000..b9269af --- /dev/null +++ b/frontend/src/Settings.tsx @@ -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 ( + <> +
+
+ +

Settings

+
+
+ + +
+
+ + ); +}; diff --git a/frontend/src/components/FolderPicker.tsx b/frontend/src/components/FolderPicker.tsx index 80ea73b..04521a3 100644 --- a/frontend/src/components/FolderPicker.tsx +++ b/frontend/src/components/FolderPicker.tsx @@ -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(""); - const [status, setStatus] = createSignal(""); 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() {

{selectedPath()}

)} - - {status() &&

{status()}

} ); } diff --git a/frontend/src/components/search-card/SearchCard.tsx b/frontend/src/components/search-card/SearchCard.tsx new file mode 100644 index 0000000..c361d3e --- /dev/null +++ b/frontend/src/components/search-card/SearchCard.tsx @@ -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 ; + case "event": + return ; + case "note": + return ; + case "contact": + return ; + default: + return null; + } +}; diff --git a/frontend/src/components/search-card/SearchCardContact.tsx b/frontend/src/components/search-card/SearchCardContact.tsx index 066fae3..817c06c 100644 --- a/frontend/src/components/search-card/SearchCardContact.tsx +++ b/frontend/src/components/search-card/SearchCardContact.tsx @@ -19,10 +19,6 @@ export const SearchCardContact = ({ item }: Props) => {

{data.PhoneNumber}

{data.Email}

- -

- {data.Description} -

); }; diff --git a/frontend/src/components/search-card/SearchCardEvent.tsx b/frontend/src/components/search-card/SearchCardEvent.tsx index b75015b..69c2665 100644 --- a/frontend/src/components/search-card/SearchCardEvent.tsx +++ b/frontend/src/components/search-card/SearchCardEvent.tsx @@ -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) => {

Organized by {data.Organizer?.Name ?? "unknown"} on{" "} - {new Date(data.StartDateTime).toLocaleDateString("en-US", { - month: "long", - day: "numeric", - year: "numeric", - })} -

- -

- {data.Description} + {data.StartDateTime + ? new Date(data.StartDateTime).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }) + : "unknown date"}

); diff --git a/frontend/src/components/search-card/SearchCardLocation.tsx b/frontend/src/components/search-card/SearchCardLocation.tsx index bae2e0d..faaebc3 100644 --- a/frontend/src/components/search-card/SearchCardLocation.tsx +++ b/frontend/src/components/search-card/SearchCardLocation.tsx @@ -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) => {

{data.Address}

- -

- {data.Description} -

); }; diff --git a/frontend/src/components/search-card/SearchCardNote.tsx b/frontend/src/components/search-card/SearchCardNote.tsx index 5b82097..7011c68 100644 --- a/frontend/src/components/search-card/SearchCardNote.tsx +++ b/frontend/src/components/search-card/SearchCardNote.tsx @@ -16,11 +16,7 @@ export const SearchCardNote = ({ item }: Props) => {

{data.Name}

-

Keywords TODO

- -

- {data.Content} -

+

Note

); }; diff --git a/frontend/src/components/shortcuts/ShortcutItem.tsx b/frontend/src/components/shortcuts/ShortcutItem.tsx new file mode 100644 index 0000000..2fc4a30 --- /dev/null +++ b/frontend/src/components/shortcuts/ShortcutItem.tsx @@ -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 = (props) => { + const renderKeys = (keys: string[]) => { + const sortedKeys = sortKeys(keys); + return ( + + {(key) => ( + + {formatKey(key)} + + )} + + ); + }; + + return ( +
+
+ {props.isEditing ? ( + <> +
+ {props.currentKeys.length > 0 ? ( + renderKeys(props.currentKeys) + ) : ( + + Press keys... + + )} +
+
+ + +
+ + ) : ( + <> +
+ {renderKeys(props.shortcut)} +
+ + + )} +
+
+ ); +}; diff --git a/frontend/src/components/shortcuts/Shortcuts.tsx b/frontend/src/components/shortcuts/Shortcuts.tsx new file mode 100644 index 0000000..5098b06 --- /dev/null +++ b/frontend/src/components/shortcuts/Shortcuts.tsx @@ -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([]); + + 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 ( + + ); +}; diff --git a/frontend/src/components/shortcuts/hooks/useShortcutEditor.ts b/frontend/src/components/shortcuts/hooks/useShortcutEditor.ts new file mode 100644 index 0000000..67218c8 --- /dev/null +++ b/frontend/src/components/shortcuts/hooks/useShortcutEditor.ts @@ -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([]); + const pressedKeys = new Set(); + + 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, + }; +} diff --git a/frontend/src/components/shortcuts/utils/formatKey.ts b/frontend/src/components/shortcuts/utils/formatKey.ts new file mode 100644 index 0000000..8134511 --- /dev/null +++ b/frontend/src/components/shortcuts/utils/formatKey.ts @@ -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; + } +}; diff --git a/frontend/src/components/shortcuts/utils/isModifierKey.ts b/frontend/src/components/shortcuts/utils/isModifierKey.ts new file mode 100644 index 0000000..b460727 --- /dev/null +++ b/frontend/src/components/shortcuts/utils/isModifierKey.ts @@ -0,0 +1,4 @@ +export const isModifierKey = (key: string): boolean => { + const modifiers = ["Control", "Shift", "Alt", "Meta", "Command"]; + return modifiers.includes(key); +}; diff --git a/frontend/src/components/shortcuts/utils/normalizeKey.ts b/frontend/src/components/shortcuts/utils/normalizeKey.ts new file mode 100644 index 0000000..fd04cb5 --- /dev/null +++ b/frontend/src/components/shortcuts/utils/normalizeKey.ts @@ -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 = { + 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; +}; diff --git a/frontend/src/components/shortcuts/utils/sortKeys.ts b/frontend/src/components/shortcuts/utils/sortKeys.ts new file mode 100644 index 0000000..dbc0325 --- /dev/null +++ b/frontend/src/components/shortcuts/utils/sortKeys.ts @@ -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; + }); +}; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index fb4943b..f5fb4fa 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -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( - + ),