From dc638cb3cebf3f6675ff8c9c1b224bc21a56f225 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 11:58:34 +0100 Subject: [PATCH 01/19] fix: adjust padding and line-height in main styles for better layout consistency --- styles/index.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/styles/index.scss b/styles/index.scss index 20a154d..2608ce2 100644 --- a/styles/index.scss +++ b/styles/index.scss @@ -138,7 +138,6 @@ main { justify-content: space-between; padding: 8px 0; border-bottom: 1px solid $divider; - line-height: 1; &:last-child { border-bottom: none; @@ -146,7 +145,7 @@ main { } &:first-child { - padding-top: 22px; + padding-top: 14px; } p { From 5e669749d7df364f813b8fbe96205c11caebd253 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:33:00 +0100 Subject: [PATCH 02/19] feat: add get_app_info command with error handling and panic safety for improved app info retrieval --- src-tauri/src/main.rs | 3 +- src-tauri/src/utils/commands.rs | 56 ++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3629212..ff0d817 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -127,7 +127,8 @@ fn main() { db::history::read_image, db::settings::get_setting, db::settings::save_setting, - utils::commands::fetch_page_meta + utils::commands::fetch_page_meta, + utils::commands::get_app_info ] ) .run(tauri::generate_context!()) diff --git a/src-tauri/src/utils/commands.rs b/src-tauri/src/utils/commands.rs index ebf71b1..dca7528 100644 --- a/src-tauri/src/utils/commands.rs +++ b/src-tauri/src/utils/commands.rs @@ -36,31 +36,49 @@ pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) { } } +#[tauri::command] pub fn get_app_info() -> (String, Option) { println!("Getting app info"); let mut ctx = AppInfoContext::new(vec![]); println!("Created AppInfoContext"); - ctx.refresh_apps().unwrap(); + + if let Err(e) = ctx.refresh_apps() { + println!("Failed to refresh apps: {:?}", e); + return ("System".to_string(), None); + } + println!("Refreshed apps"); - match ctx.get_frontmost_application() { - Ok(window) => { - println!("Found frontmost application: {}", window.name); - let name = window.name.clone(); - let icon = window - .load_icon() - .ok() - .map(|i| { - println!("Loading icon for {}", name); - let png = i.to_png().unwrap(); - let encoded = STANDARD.encode(png.get_bytes()); - println!("Icon encoded successfully"); - encoded - }); - println!("Returning app info: {} with icon: {}", name, icon.is_some()); - (name, icon) + + let result = std::panic::catch_unwind(|| { + match ctx.get_frontmost_application() { + Ok(window) => { + println!("Found frontmost application: {}", window.name); + let name = window.name.clone(); + let icon = window + .load_icon() + .ok() + .and_then(|i| { + println!("Loading icon for {}", name); + i.to_png().ok().map(|png| { + let encoded = STANDARD.encode(png.get_bytes()); + println!("Icon encoded successfully"); + encoded + }) + }); + println!("Returning app info: {} with icon: {}", name, icon.is_some()); + (name, icon) + } + Err(e) => { + println!("Failed to get frontmost application: {:?}", e); + ("System".to_string(), None) + } } - Err(e) => { - println!("Failed to get frontmost application: {:?}", e); + }); + + match result { + Ok(info) => info, + Err(_) => { + println!("Panic occurred while getting app info"); ("System".to_string(), None) } } From af64b77b7417cfd44152150f03b0327d9d62d85e Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:33:22 +0100 Subject: [PATCH 03/19] refactor: move selectedResult logic to a new plugin for better organization and maintainability --- lib/selectedResult.ts | 54 ------------------------------- plugins/selectedResult.ts | 68 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 54 deletions(-) delete mode 100644 lib/selectedResult.ts create mode 100644 plugins/selectedResult.ts diff --git a/lib/selectedResult.ts b/lib/selectedResult.ts deleted file mode 100644 index e53411b..0000000 --- a/lib/selectedResult.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { HistoryItem } from '~/types/types' - -interface GroupedHistory { - label: string - items: HistoryItem[] -} - -export const selectedGroupIndex = ref(0) -export const selectedItemIndex = ref(0) -export const selectedElement = ref(null) - -export const useSelectedResult = (groupedHistory: Ref) => { - const selectedItem = computed(() => { - const group = groupedHistory.value[selectedGroupIndex.value] - return group?.items[selectedItemIndex.value] ?? null - }) - - const isSelected = (groupIndex: number, itemIndex: number): boolean => { - return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex - } - - const selectNext = (): void => { - const currentGroup = groupedHistory.value[selectedGroupIndex.value] - if (selectedItemIndex.value < currentGroup.items.length - 1) { - selectedItemIndex.value++ - } else if (selectedGroupIndex.value < groupedHistory.value.length - 1) { - selectedGroupIndex.value++ - selectedItemIndex.value = 0 - } - } - - const selectPrevious = (): void => { - if (selectedItemIndex.value > 0) { - selectedItemIndex.value-- - } else if (selectedGroupIndex.value > 0) { - selectedGroupIndex.value-- - selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1 - } - } - - const selectItem = (groupIndex: number, itemIndex: number): void => { - selectedGroupIndex.value = groupIndex - selectedItemIndex.value = itemIndex - } - - return { - selectedItem, - isSelected, - selectNext, - selectPrevious, - selectItem, - selectedElement - } -} \ No newline at end of file diff --git a/plugins/selectedResult.ts b/plugins/selectedResult.ts new file mode 100644 index 0000000..d03babd --- /dev/null +++ b/plugins/selectedResult.ts @@ -0,0 +1,68 @@ +import { ref, computed } from 'vue'; +import type { HistoryItem } from '~/types/types'; + +interface GroupedHistory { + label: string; + items: HistoryItem[]; +} + +const selectedGroupIndex = ref(0); +const selectedItemIndex = ref(0); +const selectedElement = ref(null); + +const useSelectedResult = (groupedHistory: Ref) => { + const selectedItem = computed(() => { + const group = groupedHistory.value[selectedGroupIndex.value]; + return group?.items[selectedItemIndex.value] ?? undefined; + }); + + const isSelected = (groupIndex: number, itemIndex: number): boolean => { + return selectedGroupIndex.value === groupIndex && selectedItemIndex.value === itemIndex; + }; + + const selectNext = (): void => { + const currentGroup = groupedHistory.value[selectedGroupIndex.value]; + if (selectedItemIndex.value < currentGroup.items.length - 1) { + selectedItemIndex.value++; + } else if (selectedGroupIndex.value < groupedHistory.value.length - 1) { + selectedGroupIndex.value++; + selectedItemIndex.value = 0; + } + }; + + const selectPrevious = (): void => { + if (selectedItemIndex.value > 0) { + selectedItemIndex.value--; + } else if (selectedGroupIndex.value > 0) { + selectedGroupIndex.value--; + selectedItemIndex.value = groupedHistory.value[selectedGroupIndex.value].items.length - 1; + } + }; + + const selectItem = (groupIndex: number, itemIndex: number): void => { + selectedGroupIndex.value = groupIndex; + selectedItemIndex.value = itemIndex; + }; + + return { + selectedItem, + isSelected, + selectNext, + selectPrevious, + selectItem, + selectedElement + }; +}; + +export default defineNuxtPlugin(() => { + return { + provide: { + selectedResult: { + selectedGroupIndex, + selectedItemIndex, + selectedElement, + useSelectedResult + } + } + }; +}); \ No newline at end of file From b946b6455f6c212e4eb5923a1d30de6fa2d869af Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:33:40 +0100 Subject: [PATCH 04/19] feat: implement keyboard context management and shortcuts for improved user interaction --- pages/index.vue | 122 +++++++++++----------- pages/settings.vue | 101 +++++++++++-------- plugins/keyboard.ts | 239 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 353 insertions(+), 109 deletions(-) create mode 100644 plugins/keyboard.ts diff --git a/pages/index.vue b/pages/index.vue index 0accd10..60eb990 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -49,9 +49,12 @@ onClick: pasteSelectedItem, }" :secondary-action="{ text: 'Actions', - icon: IconsK, + icon: IconsKey, + input: 'K', showModifier: true, + onClick: toggleActionsMenu, }" /> + @@ -73,22 +76,17 @@ import type { InfoColor, InfoCode, } from "~/types/types"; -import { Key, keyboard } from "wrdu-keyboard"; -import { - selectedGroupIndex, - selectedItemIndex, - selectedElement, - useSelectedResult, -} from "~/lib/selectedResult"; import IconsEnter from "~/components/Icons/Enter.vue"; -import IconsK from "~/components/Icons/K.vue"; +import IconsKey from "~/components/Icons/Key.vue"; +import ActionsMenu from "~/components/ActionsMenu.vue"; interface GroupedHistory { label: string; items: HistoryItem[]; } -const { $history } = useNuxtApp(); +const { $history, $keyboard, $selectedResult } = useNuxtApp(); +const { selectedGroupIndex, selectedItemIndex, selectedElement, useSelectedResult } = $selectedResult; const CHUNK_SIZE = 50; const SCROLL_THRESHOLD = 100; @@ -113,9 +111,24 @@ const imageLoadError = ref(false); const imageLoading = ref(false); const pageTitle = ref(""); const pageOgImage = ref(""); +const isActionsMenuVisible = ref(false); const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null); +const toggleActionsMenu = () => { + nextTick(() => { + isActionsMenuVisible.value = !isActionsMenuVisible.value; + if (isActionsMenuVisible.value) { + $keyboard.enableContext('actionsMenu'); + } + }); +}; + +const closeActionsMenu = () => { + isActionsMenuVisible.value = false; + $keyboard.disableContext('actionsMenu'); +}; + const isSameDay = (date1: Date, date2: Date): boolean => { return ( date1.getFullYear() === date2.getFullYear() && @@ -527,70 +540,47 @@ const setupEventListeners = async (): Promise => { } focusSearchInput(); - keyboard.clear(); - keyboard.prevent.down([Key.DownArrow], () => { - selectNext(); + $keyboard.clearAll(); + $keyboard.setupAppShortcuts({ + onNavigateDown: selectNext, + onNavigateUp: selectPrevious, + onSelect: pasteSelectedItem, + onEscape: () => { + if (isActionsMenuVisible.value) { + closeActionsMenu(); + } else { + hideApp(); + } + }, + onToggleActions: toggleActionsMenu, + contextName: 'main', + priority: $keyboard.PRIORITY.LOW }); - - keyboard.prevent.down([Key.UpArrow], () => { - selectPrevious(); - }); - - keyboard.prevent.down([Key.Enter], () => { - pasteSelectedItem(); - }); - - keyboard.prevent.down([Key.Escape], () => { - hideApp(); - }); - - switch (os.value) { - case "macos": - keyboard.prevent.down([Key.LeftMeta, Key.K], () => { }); - keyboard.prevent.down([Key.RightMeta, Key.K], () => { }); - break; - - case "linux": - case "windows": - keyboard.prevent.down([Key.LeftControl, Key.K], () => { }); - keyboard.prevent.down([Key.RightControl, Key.K], () => { }); - break; - } + $keyboard.enableContext('main'); }); await listen("tauri://blur", () => { searchInput.value?.blur(); - keyboard.clear(); + $keyboard.clearAll(); + $keyboard.disableContext('main'); }); - keyboard.prevent.down([Key.DownArrow], () => { - selectNext(); + $keyboard.setupAppShortcuts({ + onNavigateDown: selectNext, + onNavigateUp: selectPrevious, + onSelect: pasteSelectedItem, + onEscape: () => { + if (isActionsMenuVisible.value) { + closeActionsMenu(); + } else { + hideApp(); + } + }, + onToggleActions: toggleActionsMenu, + contextName: 'main', + priority: $keyboard.PRIORITY.LOW }); - - keyboard.prevent.down([Key.UpArrow], () => { - selectPrevious(); - }); - - keyboard.prevent.down([Key.Enter], () => { - pasteSelectedItem(); - }); - - keyboard.prevent.down([Key.Escape], () => { - hideApp(); - }); - - switch (os.value) { - case "macos": - keyboard.prevent.down([Key.LeftMeta, Key.K], () => { }); - keyboard.prevent.down([Key.RightMeta, Key.K], () => { }); - break; - - case "linux": - case "windows": - keyboard.prevent.down([Key.LeftControl, Key.K], () => { }); - keyboard.prevent.down([Key.RightControl, Key.K], () => { }); - break; - } + $keyboard.enableContext('main'); }; const hideApp = async (): Promise => { diff --git a/pages/settings.vue b/pages/settings.vue index f67a52b..f291861 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -79,7 +79,7 @@ import { platform } from "@tauri-apps/plugin-os"; import { useRouter } from "vue-router"; import { KeyValues, KeyLabels } from "../types/keys"; import { disable, enable } from "@tauri-apps/plugin-autostart"; -import { Key, keyboard } from "wrdu-keyboard"; +import { useNuxtApp } from "#app"; import BottomBar from "../components/BottomBar.vue"; import IconsEnter from "~/components/Icons/Enter.vue"; @@ -92,7 +92,7 @@ const os = ref(""); const router = useRouter(); const showEmptyKeybindError = ref(false); const autostart = ref(false); -const { $settings } = useNuxtApp(); +const { $settings, $keyboard } = useNuxtApp(); const modifierKeySet = new Set([ KeyValues.AltLeft, @@ -174,56 +174,71 @@ const toggleAutostart = async () => { os.value = platform(); onMounted(async () => { - keyboard.prevent.down([Key.All], (event: KeyboardEvent) => { - if (isKeybindInputFocused.value) { - onKeyDown(event); + $keyboard.setupKeybindCapture({ + onCapture: (key: string) => { + if (isKeybindInputFocused.value) { + const keyValue = key as KeyValues; + + if (isModifier(keyValue)) { + activeModifiers.add(keyValue); + } else if (!keybind.value.includes(keyValue)) { + keybind.value = keybind.value.filter((k) => isModifier(k)); + keybind.value.push(keyValue); + } + + updateKeybind(); + showEmptyKeybindError.value = false; + } + }, + onComplete: () => { + if (isKeybindInputFocused.value) { + keybindInput.value?.blur(); + } else { + router.push("/"); + } } }); - keyboard.prevent.down([Key.Escape], () => { - if (isKeybindInputFocused.value) { - keybindInput.value?.blur(); - } else { - router.push("/"); - } - }); - - switch (os.value) { - case "macos": - keyboard.prevent.down([Key.LeftMeta, Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }); - - keyboard.prevent.down([Key.RightMeta, Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }); - break; - - case "linux": - case "windows": - keyboard.prevent.down([Key.LeftControl, Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }); - - keyboard.prevent.down([Key.RightControl, Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }); - break; + if (os.value === "macos") { + $keyboard.on("settings", [$keyboard.Key.LeftMeta, $keyboard.Key.Enter], () => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }, { priority: $keyboard.PRIORITY.MEDIUM }); + + $keyboard.on("settings", [$keyboard.Key.RightMeta, $keyboard.Key.Enter], () => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }, { priority: $keyboard.PRIORITY.MEDIUM }); + } else { + $keyboard.on("settings", [$keyboard.Key.LeftControl, $keyboard.Key.Enter], () => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }, { priority: $keyboard.PRIORITY.MEDIUM }); + + $keyboard.on("settings", [$keyboard.Key.RightControl, $keyboard.Key.Enter], () => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }, { priority: $keyboard.PRIORITY.MEDIUM }); } + $keyboard.on("settings", [$keyboard.Key.Escape], () => { + if (!isKeybindInputFocused.value) { + router.push("/"); + } + }, { priority: $keyboard.PRIORITY.MEDIUM }); + + $keyboard.enableContext("settings"); + autostart.value = (await $settings.getSetting("autostart")) === "true"; }); onUnmounted(() => { - keyboard.clear(); + $keyboard.disableContext("settings"); + $keyboard.clearAll(); }); diff --git a/plugins/keyboard.ts b/plugins/keyboard.ts new file mode 100644 index 0000000..a1f3ab0 --- /dev/null +++ b/plugins/keyboard.ts @@ -0,0 +1,239 @@ +import { Key, keyboard } from "wrdu-keyboard"; +import { platform } from "@tauri-apps/plugin-os"; + +type KeyboardHandler = (event: KeyboardEvent) => void; + +const activeContexts = new Set(); +const handlersByContext: Record< + string, + Array<{ + keys: Key[]; + callback: KeyboardHandler; + prevent: boolean; + priority?: number; + }> +> = {}; + +const PRIORITY = { + HIGH: 100, + MEDIUM: 50, + LOW: 0, +}; + +let currentOS = "windows"; +const initOS = async () => { + currentOS = await platform(); +}; + +const useKeyboard = { + PRIORITY, + + registerContext: (contextName: string) => { + if (!handlersByContext[contextName]) { + handlersByContext[contextName] = []; + } + }, + + enableContext: (contextName: string) => { + if (!handlersByContext[contextName]) { + useKeyboard.registerContext(contextName); + } + activeContexts.add(contextName); + + initKeyboardHandlers(); + }, + + disableContext: (contextName: string) => { + activeContexts.delete(contextName); + + initKeyboardHandlers(); + }, + + on: ( + contextName: string, + keys: Key[], + callback: KeyboardHandler, + options: { prevent?: boolean; priority?: number } = {} + ) => { + if (!handlersByContext[contextName]) { + useKeyboard.registerContext(contextName); + } + handlersByContext[contextName].push({ + keys, + callback, + prevent: options.prevent ?? true, + priority: options.priority ?? PRIORITY.LOW, + }); + + if (activeContexts.has(contextName)) { + initKeyboardHandlers(); + } + }, + + clearAll: () => { + keyboard.clear(); + }, + + setupAppShortcuts: (options: { + onNavigateUp?: () => void; + onNavigateDown?: () => void; + onSelect?: () => void; + onEscape?: () => void; + onToggleActions?: () => void; + contextName?: string; + priority?: number; + }) => { + const { + onNavigateUp, + onNavigateDown, + onSelect, + onEscape, + onToggleActions, + contextName = "app", + priority = PRIORITY.LOW, + } = options; + + if (!handlersByContext[contextName]) { + useKeyboard.registerContext(contextName); + } + + if (onNavigateUp) { + useKeyboard.on(contextName, [Key.UpArrow], () => onNavigateUp(), { + priority, + }); + } + + if (onNavigateDown) { + useKeyboard.on(contextName, [Key.DownArrow], () => onNavigateDown(), { + priority, + }); + } + + if (onSelect) { + useKeyboard.on(contextName, [Key.Enter], () => onSelect(), { priority }); + } + + if (onEscape) { + useKeyboard.on(contextName, [Key.Escape], () => onEscape(), { priority }); + } + + if (onToggleActions) { + const togglePriority = PRIORITY.HIGH; + + if (currentOS === "macos") { + useKeyboard.on( + contextName, + [Key.LeftMeta, Key.K], + () => onToggleActions(), + { priority: togglePriority } + ); + useKeyboard.on( + contextName, + [Key.RightMeta, Key.K], + () => onToggleActions(), + { priority: togglePriority } + ); + } else { + useKeyboard.on( + contextName, + [Key.LeftControl, Key.K], + () => onToggleActions(), + { priority: togglePriority } + ); + useKeyboard.on( + contextName, + [Key.RightControl, Key.K], + () => onToggleActions(), + { priority: togglePriority } + ); + } + } + }, + + setupKeybindCapture: (options: { + onCapture: (key: string) => void; + onComplete: () => void; + }) => { + const { onCapture, onComplete } = options; + + keyboard.prevent.down([Key.All], (event: KeyboardEvent) => { + if (event.code === "Escape") { + onComplete(); + return; + } + onCapture(event.code); + }); + }, +}; + +const initKeyboardHandlers = () => { + keyboard.clear(); + + let allHandlers: Array<{ keys: Key[], callback: KeyboardHandler, prevent: boolean, priority: number, contextName: string }> = []; + + for (const contextName of activeContexts) { + const handlers = handlersByContext[contextName] || []; + allHandlers = [...allHandlers, ...handlers.map(handler => ({ + ...handler, + priority: handler.priority ?? PRIORITY.LOW, + contextName + }))]; + } + + allHandlers.sort((a, b) => b.priority - a.priority); + + const handlersByKeyCombination: Record> = {}; + + allHandlers.forEach(handler => { + const keyCombo = handler.keys.join('+'); + if (!handlersByKeyCombination[keyCombo]) { + handlersByKeyCombination[keyCombo] = []; + } + handlersByKeyCombination[keyCombo].push(handler); + }); + + Object.values(handlersByKeyCombination).forEach(handlers => { + const handler = handlers[0]; + + const wrappedCallback: KeyboardHandler = (event) => { + const isMetaCombo = handler.keys.length > 1 && + (handler.keys.includes(Key.LeftMeta) || + handler.keys.includes(Key.RightMeta) || + handler.keys.includes(Key.LeftControl) || + handler.keys.includes(Key.RightControl)); + + const isNavigationKey = event.key === 'ArrowUp' || + event.key === 'ArrowDown' || + event.key === 'Enter' || + event.key === 'Escape'; + + const isInInput = event.target instanceof HTMLInputElement || + event.target instanceof HTMLTextAreaElement; + + if (isMetaCombo || isNavigationKey || !isInInput) { + handler.callback(event); + } + }; + + if (handler.prevent) { + keyboard.prevent.down(handler.keys, wrappedCallback); + } else { + keyboard.down(handler.keys, wrappedCallback); + } + }); +}; + +export default defineNuxtPlugin(async () => { + await initOS(); + + initKeyboardHandlers(); + + return { + provide: { + keyboard: { + ...useKeyboard, + Key, + }, + }, + }; +}); From 7ba418f4cc17ee2d1dc0ea14d3a241b97e1fb5f4 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:33:51 +0100 Subject: [PATCH 05/19] refactor: update BottomBar component to improve action handling and add input support for secondary action --- components/BottomBar.vue | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/components/BottomBar.vue b/components/BottomBar.vue index 2ec32ed..30a91b3 100644 --- a/components/BottomBar.vue +++ b/components/BottomBar.vue @@ -5,17 +5,17 @@

Qopy

-
+

{{ primaryAction.text }}

-
+

{{ secondaryAction.text }}

- +
@@ -23,13 +23,17 @@ @@ -111,6 +129,12 @@ onMounted(() => { background-color: var(--border); } + .paste:active, + .actions:active { + background-color: var(--border-active, #444); + transform: scale(0.98); + } + &:hover .paste:hover ~ .divider, &:hover .divider:has(+ .actions:hover) { opacity: 0; From 2865f8749eb25295fc39841a29e35b9b1289b60d Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:34:01 +0100 Subject: [PATCH 06/19] feat: add ActionsMenu component for enhanced action management with search functionality and keyboard navigation --- components/ActionsMenu.vue | 702 +++++++++++++++++++++++++++++++++++++ components/Icons/K.vue | 29 -- components/Icons/Key.vue | 33 ++ components/Icons/Shift.vue | 12 + 4 files changed, 747 insertions(+), 29 deletions(-) create mode 100644 components/ActionsMenu.vue delete mode 100644 components/Icons/K.vue create mode 100644 components/Icons/Key.vue create mode 100644 components/Icons/Shift.vue diff --git a/components/ActionsMenu.vue b/components/ActionsMenu.vue new file mode 100644 index 0000000..4a681f5 --- /dev/null +++ b/components/ActionsMenu.vue @@ -0,0 +1,702 @@ + + + + + diff --git a/components/Icons/K.vue b/components/Icons/K.vue deleted file mode 100644 index 8bdc675..0000000 --- a/components/Icons/K.vue +++ /dev/null @@ -1,29 +0,0 @@ - diff --git a/components/Icons/Key.vue b/components/Icons/Key.vue new file mode 100644 index 0000000..f60d6fd --- /dev/null +++ b/components/Icons/Key.vue @@ -0,0 +1,33 @@ + + + diff --git a/components/Icons/Shift.vue b/components/Icons/Shift.vue new file mode 100644 index 0000000..5af5a9a --- /dev/null +++ b/components/Icons/Shift.vue @@ -0,0 +1,12 @@ + \ No newline at end of file From fce7eec1fc3b86a32f68d468e1bd4114ac4b0cf9 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:32:14 +0100 Subject: [PATCH 07/19] feat: add new icon components including Bin, Board, Brush, Cube, Expand, Gear, Globe, Open, Pen, Rotate, T, and Zip; remove unused Cmd, Ctrl, Enter, Key, and Shift icons for a cleaner icon set --- components/Icons/Bin.vue | 7 +++++++ components/Icons/Board.vue | 14 +++++++++++++ components/Icons/Brush.vue | 7 +++++++ components/Icons/Cmd.vue | 39 ----------------------------------- components/Icons/Ctrl.vue | 32 ----------------------------- components/Icons/Cube.vue | 7 +++++++ components/Icons/Enter.vue | 41 ------------------------------------- components/Icons/Expand.vue | 10 +++++++++ components/Icons/Gear.vue | 7 +++++++ components/Icons/Globe.vue | 7 +++++++ components/Icons/Key.vue | 33 ----------------------------- components/Icons/Open.vue | 7 +++++++ components/Icons/Pen.vue | 7 +++++++ components/Icons/Rotate.vue | 7 +++++++ components/Icons/Shift.vue | 12 ----------- components/Icons/T.vue | 7 +++++++ components/Icons/Zip.vue | 7 +++++++ 17 files changed, 94 insertions(+), 157 deletions(-) create mode 100644 components/Icons/Bin.vue create mode 100644 components/Icons/Board.vue create mode 100644 components/Icons/Brush.vue delete mode 100644 components/Icons/Cmd.vue delete mode 100644 components/Icons/Ctrl.vue create mode 100644 components/Icons/Cube.vue delete mode 100644 components/Icons/Enter.vue create mode 100644 components/Icons/Expand.vue create mode 100644 components/Icons/Gear.vue create mode 100644 components/Icons/Globe.vue delete mode 100644 components/Icons/Key.vue create mode 100644 components/Icons/Open.vue create mode 100644 components/Icons/Pen.vue create mode 100644 components/Icons/Rotate.vue delete mode 100644 components/Icons/Shift.vue create mode 100644 components/Icons/T.vue create mode 100644 components/Icons/Zip.vue diff --git a/components/Icons/Bin.vue b/components/Icons/Bin.vue new file mode 100644 index 0000000..d69f6a4 --- /dev/null +++ b/components/Icons/Bin.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Board.vue b/components/Icons/Board.vue new file mode 100644 index 0000000..7f45d86 --- /dev/null +++ b/components/Icons/Board.vue @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/components/Icons/Brush.vue b/components/Icons/Brush.vue new file mode 100644 index 0000000..d282b03 --- /dev/null +++ b/components/Icons/Brush.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Cmd.vue b/components/Icons/Cmd.vue deleted file mode 100644 index 7b18f3e..0000000 --- a/components/Icons/Cmd.vue +++ /dev/null @@ -1,39 +0,0 @@ - diff --git a/components/Icons/Ctrl.vue b/components/Icons/Ctrl.vue deleted file mode 100644 index 60163bd..0000000 --- a/components/Icons/Ctrl.vue +++ /dev/null @@ -1,32 +0,0 @@ - diff --git a/components/Icons/Cube.vue b/components/Icons/Cube.vue new file mode 100644 index 0000000..b987711 --- /dev/null +++ b/components/Icons/Cube.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Enter.vue b/components/Icons/Enter.vue deleted file mode 100644 index e6e50a0..0000000 --- a/components/Icons/Enter.vue +++ /dev/null @@ -1,41 +0,0 @@ - diff --git a/components/Icons/Expand.vue b/components/Icons/Expand.vue new file mode 100644 index 0000000..41e86ae --- /dev/null +++ b/components/Icons/Expand.vue @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/components/Icons/Gear.vue b/components/Icons/Gear.vue new file mode 100644 index 0000000..581378e --- /dev/null +++ b/components/Icons/Gear.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Globe.vue b/components/Icons/Globe.vue new file mode 100644 index 0000000..f3203ed --- /dev/null +++ b/components/Icons/Globe.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Key.vue b/components/Icons/Key.vue deleted file mode 100644 index f60d6fd..0000000 --- a/components/Icons/Key.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/components/Icons/Open.vue b/components/Icons/Open.vue new file mode 100644 index 0000000..f022013 --- /dev/null +++ b/components/Icons/Open.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Pen.vue b/components/Icons/Pen.vue new file mode 100644 index 0000000..5a5c7f8 --- /dev/null +++ b/components/Icons/Pen.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Rotate.vue b/components/Icons/Rotate.vue new file mode 100644 index 0000000..84f57d7 --- /dev/null +++ b/components/Icons/Rotate.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Shift.vue b/components/Icons/Shift.vue deleted file mode 100644 index 5af5a9a..0000000 --- a/components/Icons/Shift.vue +++ /dev/null @@ -1,12 +0,0 @@ - \ No newline at end of file diff --git a/components/Icons/T.vue b/components/Icons/T.vue new file mode 100644 index 0000000..61dcdbe --- /dev/null +++ b/components/Icons/T.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/components/Icons/Zip.vue b/components/Icons/Zip.vue new file mode 100644 index 0000000..e1bf671 --- /dev/null +++ b/components/Icons/Zip.vue @@ -0,0 +1,7 @@ + \ No newline at end of file From 843a1ea8b79d0f887c56b69300ca712dfe1cc70e Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:32:20 +0100 Subject: [PATCH 08/19] feat: add new key icon components for Cmd, Enter, Key, and Shift to enhance keyboard representation --- components/Keys/Cmd.vue | 39 +++++++++++++++++++++++++++++++++++++ components/Keys/Enter.vue | 41 +++++++++++++++++++++++++++++++++++++++ components/Keys/Key.vue | 30 ++++++++++++++++++++++++++++ components/Keys/Shift.vue | 12 ++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 components/Keys/Cmd.vue create mode 100644 components/Keys/Enter.vue create mode 100644 components/Keys/Key.vue create mode 100644 components/Keys/Shift.vue diff --git a/components/Keys/Cmd.vue b/components/Keys/Cmd.vue new file mode 100644 index 0000000..7b18f3e --- /dev/null +++ b/components/Keys/Cmd.vue @@ -0,0 +1,39 @@ + diff --git a/components/Keys/Enter.vue b/components/Keys/Enter.vue new file mode 100644 index 0000000..e6e50a0 --- /dev/null +++ b/components/Keys/Enter.vue @@ -0,0 +1,41 @@ + diff --git a/components/Keys/Key.vue b/components/Keys/Key.vue new file mode 100644 index 0000000..181ac17 --- /dev/null +++ b/components/Keys/Key.vue @@ -0,0 +1,30 @@ + + + diff --git a/components/Keys/Shift.vue b/components/Keys/Shift.vue new file mode 100644 index 0000000..5af5a9a --- /dev/null +++ b/components/Keys/Shift.vue @@ -0,0 +1,12 @@ + \ No newline at end of file From 409e10a8fdac5d00cdf01af51ddcf929fa5920db Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:32:27 +0100 Subject: [PATCH 09/19] refactor: update BottomBar component to use new Key component for keyboard modifier representation --- components/BottomBar.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/BottomBar.vue b/components/BottomBar.vue index 30a91b3..c978c99 100644 --- a/components/BottomBar.vue +++ b/components/BottomBar.vue @@ -13,7 +13,7 @@

{{ secondaryAction.text }}

- +
@@ -25,8 +25,8 @@ @@ -683,6 +719,10 @@ onUnmounted(() => { width: 14px; height: 14px; } + + .title { + color: inherit; + } } } diff --git a/composables/useActions.ts b/composables/useActions.ts new file mode 100644 index 0000000..78d2254 --- /dev/null +++ b/composables/useActions.ts @@ -0,0 +1,222 @@ +import { invoke } from "@tauri-apps/api/core"; +import { HistoryItem } from "../types/types"; + +const { $history } = useNuxtApp(); +const { hideApp } = useAppControl(); + +export function useActions() { + const isProcessing = ref(false); + + const handleAction = async (action: string, item?: HistoryItem) => { + if (!item && action !== "settings" && action !== "delete-all") return; + + isProcessing.value = true; + + try { + switch (action) { + case "paste-to-app": + await pasteToCurrentApp(item); + break; + case "copy": + // await copyToClipboard(item); + break; + case "delete": + await deleteEntry(item); + break; + case "delete-all": + // await deleteAllEntries(); + break; + case "settings": + openSettings(); + break; + case "paste-plain": + // await pasteAsPlainText(item); + break; + case "edit-text": + // openTextEditor(item); + break; + case "rotate-image": + // await rotateImage(item); + break; + case "resize-image": + // openImageResizer(item); + break; + case "compress-image": + // await compressImage(item); + break; + case "open-file": + // await openFile(item); + break; + case "compress-file": + // await compressFile(item); + break; + case "open-link": + // await openInBrowser(item); + break; + case "copy-hex": + // await copyColorFormat(item, "hex"); + break; + case "copy-rgba": + // await copyColorFormat(item, "rgba"); + break; + case "copy-hsla": + // await copyColorFormat(item, "hsla"); + break; + default: + console.warn(`Action ${action} not implemented`); + } + } catch (error) { + console.error(`Error executing action ${action}:`, error); + } finally { + isProcessing.value = false; + } + }; + + const pasteToCurrentApp = async (item?: HistoryItem) => { + if (!item) return; + + let content = item.content; + let contentType: string = item.content_type; + if (contentType === "image") { + try { + content = await $history.readImage({ filename: content }); + } catch (error) { + console.error("Error reading image file:", error); + return; + } + } + await hideApp(); + await $history.writeAndPaste({ content, contentType }); + }; + + // const copyToClipboard = async (item?: HistoryItem) => { + // if (!item) return; + + // try { + // switch (item.content_type) { + // case ContentType.Text: + // case ContentType.Link: + // case ContentType.Code: + // await writeText(item.content); + // break; + // case ContentType.Image: + // await invoke("copy_image_to_clipboard", { path: item.file_path }); + // break; + // case ContentType.File: + // await invoke("copy_file_reference", { path: item.file_path }); + // break; + // case ContentType.Color: + // await writeText(item.content); + // break; + // default: + // console.warn(`Copying type ${item.content_type} not implemented`); + // } + // } catch (error) { + // console.error("Failed to copy to clipboard:", error); + // } + // }; + + const deleteEntry = async (item?: HistoryItem) => { + if (!item) return; + try { + await invoke("delete_history_item", { id: item.id }); + } catch (error) { + console.error("Failed to delete entry:", error); + } + }; + + // const deleteAllEntries = async () => { + // try { + // await invoke('delete_all_history'); + // } catch (error) { + // console.error('Failed to delete all entries:', error); + // } + // }; + + const openSettings = () => { + navigateTo("/settings"); + }; + + // const pasteAsPlainText = async (item?: HistoryItem) => { + // if (!item) return; + // try { + // await invoke('paste_as_plain_text', { content: item.content }); + // } catch (error) { + // console.error('Failed to paste as plain text:', error); + // } + // }; + + // const openTextEditor = (item?: HistoryItem) => { + // if (!item) return; + // // Implement logic to open text editor with the content + // // This might use Nuxt router or a modal based on your app architecture + // }; + + // const rotateImage = async (item?: HistoryItem) => { + // if (!item || item.content_type !== ContentType.Image) return; + // try { + // await invoke('rotate_image', { path: item.file_path }); + // } catch (error) { + // console.error('Failed to rotate image:', error); + // } + // }; + + // const openImageResizer = (item?: HistoryItem) => { + // if (!item || item.content_type !== ContentType.Image) return; + // // Implement logic to open image resizer UI for this image + // }; + + // const compressImage = async (item?: HistoryItem) => { + // if (!item || item.content_type !== ContentType.Image) return; + // try { + // await invoke('compress_image', { path: item.file_path }); + // } catch (error) { + // console.error('Failed to compress image:', error); + // } + // }; + + // const openFile = async (item?: HistoryItem) => { + // if (!item || item.content_type !== ContentType.File) return; + // try { + // await invoke('open_file', { path: item.file_path }); + // } catch (error) { + // console.error('Failed to open file:', error); + // } + // }; + + // const compressFile = async (item?: HistoryItem) => { + // if (!item || item.content_type !== ContentType.File) return; + // try { + // await invoke('compress_file', { path: item.file_path }); + // } catch (error) { + // console.error('Failed to compress file:', error); + // } + // }; + + // const openInBrowser = async (item?: HistoryItem) => { + // if (!item || item.content_type !== ContentType.Link) return; + // try { + // await invoke('open_url', { url: item.content }); + // } catch (error) { + // console.error('Failed to open URL in browser:', error); + // } + // }; + + // const copyColorFormat = async (item?: HistoryItem, format: 'hex' | 'rgba' | 'hsla' = 'hex') => { + // if (!item || item.content_type !== ContentType.Color) return; + // try { + // const formattedColor = await invoke('get_color_format', { + // color: item.content, + // format + // }); + // await writeText(formattedColor as string); + // } catch (error) { + // console.error(`Failed to copy color as ${format}:`, error); + // } + // }; + + return { + handleAction, + isProcessing, + }; +} From be1718d9a59e4b996a25505fb19b2fe9da3af843 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:33:00 +0100 Subject: [PATCH 12/19] feat: add new color variable for red and update SQL query in history management to include additional fields --- app.vue | 2 ++ pages/settings.vue | 2 +- src-tauri/src/db/history.rs | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app.vue b/app.vue index 6b9f531..14363d4 100644 --- a/app.vue +++ b/app.vue @@ -64,6 +64,8 @@ onMounted(async () => { --accent: #feb453; --border: #ffffff0d; + --red: #F84E4E; + --text: #e5dfd5; --text-secondary: #ada9a1; --text-muted: #78756f; diff --git a/pages/settings.vue b/pages/settings.vue index f291861..d982118 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -81,7 +81,7 @@ import { KeyValues, KeyLabels } from "../types/keys"; import { disable, enable } from "@tauri-apps/plugin-autostart"; import { useNuxtApp } from "#app"; import BottomBar from "../components/BottomBar.vue"; -import IconsEnter from "~/components/Icons/Enter.vue"; +import IconsEnter from "~/components/Keys/Enter.vue"; const activeModifiers = reactive>(new Set()); const isKeybindInputFocused = ref(false); diff --git a/src-tauri/src/db/history.rs b/src-tauri/src/db/history.rs index 4f70310..bc0ab87 100644 --- a/src-tauri/src/db/history.rs +++ b/src-tauri/src/db/history.rs @@ -71,8 +71,12 @@ pub async fn add_history_item( Some(_) => { sqlx ::query( - "UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?" + "UPDATE history SET source = ?, source_icon = ?, timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now'), favicon = ?, language = ? WHERE content = ? AND content_type = ?" ) + .bind(&source) + .bind(&source_icon) + .bind(&favicon) + .bind(&language) .bind(&content) .bind(&content_type) .execute(&*pool).await From a79268d0f7690201d0c763a9c2ed303f3088b3f2 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:26:28 +0100 Subject: [PATCH 13/19] feat: enhance ActionsMenu functionality with improved keyboard context management and dynamic history updates --- pages/index.vue | 42 +++++++++++++++++++++++-------------- src-tauri/src/db/history.rs | 5 +++++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index 4290ba8..b6a8464 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -54,7 +54,7 @@ showModifier: true, onClick: toggleActionsMenu, }" /> - + @@ -116,17 +116,21 @@ const isActionsMenuVisible = ref(false); const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null); const toggleActionsMenu = () => { - nextTick(() => { - isActionsMenuVisible.value = !isActionsMenuVisible.value; - if (isActionsMenuVisible.value) { - $keyboard.enableContext('actionsMenu'); - } - }); + isActionsMenuVisible.value = !isActionsMenuVisible.value; + + if (isActionsMenuVisible.value) { + $keyboard.disableContext('main'); + $keyboard.enableContext('actionsMenu'); + } else { + $keyboard.disableContext('actionsMenu'); + $keyboard.enableContext('main'); + } }; const closeActionsMenu = () => { isActionsMenuVisible.value = false; $keyboard.disableContext('actionsMenu'); + $keyboard.enableContext('main'); }; const isSameDay = (date1: Date, date2: Date): boolean => { @@ -430,13 +434,13 @@ const getYoutubeThumbnail = (url: string): string => { }; const updateHistory = async (resetScroll: boolean = false): Promise => { - const results = await $history.loadHistoryChunk(0, CHUNK_SIZE); + offset = 0; + history.value = []; + + const results = await $history.loadHistoryChunk(offset, CHUNK_SIZE); if (results.length > 0) { - const existingIds = new Set(history.value.map((item) => item.id)); - const uniqueNewItems = results.filter((item) => !existingIds.has(item.id)); - - const processedNewItems = await Promise.all( - uniqueNewItems.map(async (item) => { + const processedItems = await Promise.all( + results.map(async (item) => { const historyItem = new HistoryItem( item.source, item.content_type, @@ -484,7 +488,8 @@ const updateHistory = async (resetScroll: boolean = false): Promise => { }) ); - history.value = [...processedNewItems, ...history.value]; + history.value = processedItems; + offset = results.length; if ( resetScroll && @@ -554,9 +559,14 @@ const setupEventListeners = async (): Promise => { }, onToggleActions: toggleActionsMenu, contextName: 'main', - priority: $keyboard.PRIORITY.LOW + priority: $keyboard.PRIORITY.HIGH }); - $keyboard.enableContext('main'); + + if (isActionsMenuVisible.value) { + $keyboard.enableContext('actionsMenu'); + } else { + $keyboard.enableContext('main'); + } }); await listen("tauri://blur", () => { diff --git a/src-tauri/src/db/history.rs b/src-tauri/src/db/history.rs index bc0ab87..a058eeb 100644 --- a/src-tauri/src/db/history.rs +++ b/src-tauri/src/db/history.rs @@ -5,6 +5,7 @@ use rand::distr::Alphanumeric; use sqlx::{ Row, SqlitePool }; use std::fs; use tauri_plugin_aptabase::EventTracker; +use tauri::Emitter; pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box> { let id: String = rng() @@ -106,6 +107,8 @@ pub async fn add_history_item( "content_type": item.content_type.to_string() })) ); + + let _ = app_handle.emit("clipboard-content-updated", ()); Ok(()) } @@ -195,6 +198,7 @@ pub async fn delete_history_item( .map_err(|e| e.to_string())?; let _ = app_handle.track_event("history_item_deleted", None); + let _ = app_handle.emit("clipboard-content-updated", ()); Ok(()) } @@ -210,6 +214,7 @@ pub async fn clear_history( .map_err(|e| e.to_string())?; let _ = app_handle.track_event("history_cleared", None); + let _ = app_handle.emit("clipboard-content-updated", ()); Ok(()) } From 554943d3499e2fcb3aa93d869ecb499ab96fc4db Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:26:53 +0100 Subject: [PATCH 14/19] feat: remove Cube icon component and enhance ActionsMenu with new keyboard shortcuts for toggling actions --- components/ActionsMenu.vue | 75 +++++++++++++++++++++++++++++++------- components/Icons/Cube.vue | 7 ---- 2 files changed, 61 insertions(+), 21 deletions(-) delete mode 100644 components/Icons/Cube.vue diff --git a/components/ActionsMenu.vue b/components/ActionsMenu.vue index 4654e27..57d8225 100644 --- a/components/ActionsMenu.vue +++ b/components/ActionsMenu.vue @@ -11,8 +11,8 @@
@@ -34,9 +34,9 @@ selected: isSelected && currentIndex === getActionIndex(index, 'top'), }" :ref="(el) => { - if (currentIndex === getActionIndex(index, 'top')) - setSelectedElement(el); - } + if (currentIndex === getActionIndex(index, 'top')) + setSelectedElement(el); + } ">
@@ -60,9 +60,9 @@ isSelected && currentIndex === getActionIndex(index, 'specific'), }" :ref="(el) => { - if (currentIndex === getActionIndex(index, 'specific')) - setSelectedElement(el); - } + if (currentIndex === getActionIndex(index, 'specific')) + setSelectedElement(el); + } ">
@@ -85,9 +85,9 @@ selected: isSelected && currentIndex === getActionIndex(index, 'bottom'), }" :ref="(el) => { - if (currentIndex === getActionIndex(index, 'bottom')) - setSelectedElement(el); - } + if (currentIndex === getActionIndex(index, 'bottom')) + setSelectedElement(el); + } " :style="action.color ? { color: action.color } : {}">
@@ -124,7 +124,6 @@ import Bin from "./Icons/Bin.vue"; import Pen from "./Icons/Pen.vue"; import T from "./Icons/T.vue"; import Board from "./Icons/Board.vue"; -import Cube from "./Icons/Cube.vue"; import Open from "./Icons/Open.vue"; import Globe from "./Icons/Globe.vue"; import Zip from "./Icons/Zip.vue"; @@ -149,7 +148,7 @@ const menuRef = ref(null); const scrollbarsRef = ref | null>(null); -const { handleAction, isProcessing } = useActions(); +const { handleAction } = useActions(); const SCROLL_PADDING = 8; @@ -177,6 +176,7 @@ const props = defineProps<{ const emit = defineEmits<{ (e: "close"): void; + (e: "toggle"): void; (e: "action", action: string, item?: HistoryItem): void; }>(); @@ -206,7 +206,7 @@ const topActions = computed((): ActionItem[] => [ } }); } - } : Cube, + } : undefined, }, { title: "Copy to Clipboard", @@ -502,6 +502,46 @@ const setupKeyboardHandlers = () => { }, { priority: $keyboard.PRIORITY.HIGH } ); + + $keyboard.on( + "actionsMenu", + [$keyboard.Key.LeftControl, $keyboard.Key.K], + (event) => { + event.preventDefault(); + emit("toggle"); + }, + { priority: $keyboard.PRIORITY.HIGH } + ); + + $keyboard.on( + "actionsMenu", + [$keyboard.Key.RightControl, $keyboard.Key.K], + (event) => { + event.preventDefault(); + emit("toggle"); + }, + { priority: $keyboard.PRIORITY.HIGH } + ); + + $keyboard.on( + "actionsMenu", + [$keyboard.Key.MetaLeft, $keyboard.Key.K], + (event) => { + event.preventDefault(); + emit("toggle"); + }, + { priority: $keyboard.PRIORITY.HIGH } + ); + + $keyboard.on( + "actionsMenu", + [$keyboard.Key.MetaRight, $keyboard.Key.K], + (event) => { + event.preventDefault(); + emit("toggle"); + }, + { priority: $keyboard.PRIORITY.HIGH } + ); }; const selectNext = () => { @@ -586,6 +626,13 @@ const handleSearchKeydown = (event: KeyboardEvent) => { ) { return; } + + if (event.key.toLowerCase() === "k" && (event.ctrlKey || event.metaKey)) { + event.preventDefault(); + event.stopPropagation(); + emit("toggle"); + return; + } event.stopPropagation(); }; diff --git a/components/Icons/Cube.vue b/components/Icons/Cube.vue deleted file mode 100644 index b987711..0000000 --- a/components/Icons/Cube.vue +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file From b8238d01cab23abedd054dd24ec67ac6b75f08a2 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:27:06 +0100 Subject: [PATCH 15/19] feat: remove unused clipboard content update emission in setup function --- nuxt.config.ts | 4 +++- src-tauri/src/api/clipboard.rs | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index b10de45..77635be 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -3,6 +3,7 @@ export default defineNuxtConfig({ devtools: { enabled: false }, compatibilityDate: "2024-07-04", ssr: false, + app: { head: { charset: "utf-8", @@ -10,6 +11,7 @@ export default defineNuxtConfig({ "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0", }, }, + vite: { css: { preprocessorOptions: { @@ -19,4 +21,4 @@ export default defineNuxtConfig({ }, }, }, -}); +}); \ No newline at end of file diff --git a/src-tauri/src/api/clipboard.rs b/src-tauri/src/api/clipboard.rs index f06881c..5a7d8d3 100644 --- a/src-tauri/src/api/clipboard.rs +++ b/src-tauri/src/api/clipboard.rs @@ -230,7 +230,6 @@ pub fn setup(app: &AppHandle) { } } - let _ = app_handle.emit("clipboard-content-updated", ()); let _ = app_handle.track_event( "clipboard_copied", Some( From 3a5e2cba7e79d2ae339649357f8cb57237141504 Mon Sep 17 00:00:00 2001 From: pandadev <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:00:48 +0100 Subject: [PATCH 16/19] feat: refactor ActionsMenu for improved accessibility and keyboard navigation, including focus management and enhanced keyboard shortcut handling --- components/ActionsMenu.vue | 205 +++++++++++++++++++------------------ pages/index.vue | 53 ++++++---- pages/settings.vue | 1 - plugins/keyboard.ts | 127 ++++++++++++++--------- 4 files changed, 220 insertions(+), 166 deletions(-) diff --git a/components/ActionsMenu.vue b/components/ActionsMenu.vue index 57d8225..37fbae8 100644 --- a/components/ActionsMenu.vue +++ b/components/ActionsMenu.vue @@ -1,110 +1,119 @@ diff --git a/pages/index.vue b/pages/index.vue index b6a8464..774b0a0 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -125,6 +125,14 @@ const toggleActionsMenu = () => { $keyboard.disableContext('actionsMenu'); $keyboard.enableContext('main'); } + + nextTick(() => { + if (isActionsMenuVisible.value) { + document.getElementById('actions-menu')?.focus(); + } else { + focusSearchInput(); + } + }); }; const closeActionsMenu = () => { @@ -545,34 +553,18 @@ const setupEventListeners = async (): Promise => { } focusSearchInput(); - $keyboard.clearAll(); - $keyboard.setupAppShortcuts({ - onNavigateDown: selectNext, - onNavigateUp: selectPrevious, - onSelect: pasteSelectedItem, - onEscape: () => { - if (isActionsMenuVisible.value) { - closeActionsMenu(); - } else { - hideApp(); - } - }, - onToggleActions: toggleActionsMenu, - contextName: 'main', - priority: $keyboard.PRIORITY.HIGH - }); - + $keyboard.disableContext('actionsMenu'); + $keyboard.disableContext('settings'); + $keyboard.enableContext('main'); if (isActionsMenuVisible.value) { $keyboard.enableContext('actionsMenu'); - } else { - $keyboard.enableContext('main'); } }); await listen("tauri://blur", () => { searchInput.value?.blur(); - $keyboard.clearAll(); $keyboard.disableContext('main'); + $keyboard.disableContext('actionsMenu'); }); $keyboard.setupAppShortcuts({ @@ -588,8 +580,9 @@ const setupEventListeners = async (): Promise => { }, onToggleActions: toggleActionsMenu, contextName: 'main', - priority: $keyboard.PRIORITY.LOW + priority: $keyboard.PRIORITY.HIGH }); + $keyboard.disableContext('settings'); $keyboard.enableContext('main'); }; @@ -625,6 +618,24 @@ onMounted(async () => { ?.viewport?.addEventListener("scroll", handleScroll); await setupEventListeners(); + + $keyboard.setupAppShortcuts({ + onNavigateDown: selectNext, + onNavigateUp: selectPrevious, + onSelect: pasteSelectedItem, + onEscape: () => { + if (isActionsMenuVisible.value) { + closeActionsMenu(); + } else { + hideApp(); + } + }, + onToggleActions: toggleActionsMenu, + contextName: 'main', + priority: $keyboard.PRIORITY.HIGH + }); + $keyboard.disableContext('settings'); + $keyboard.enableContext('main'); } catch (error) { console.error("Error during onMounted:", error); } diff --git a/pages/settings.vue b/pages/settings.vue index d982118..5f0c7c5 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -238,7 +238,6 @@ onMounted(async () => { onUnmounted(() => { $keyboard.disableContext("settings"); - $keyboard.clearAll(); }); diff --git a/plugins/keyboard.ts b/plugins/keyboard.ts index a1f3ab0..1bb6847 100644 --- a/plugins/keyboard.ts +++ b/plugins/keyboard.ts @@ -1,5 +1,4 @@ import { Key, keyboard } from "wrdu-keyboard"; -import { platform } from "@tauri-apps/plugin-os"; type KeyboardHandler = (event: KeyboardEvent) => void; @@ -21,9 +20,6 @@ const PRIORITY = { }; let currentOS = "windows"; -const initOS = async () => { - currentOS = await platform(); -}; const useKeyboard = { PRIORITY, @@ -58,12 +54,29 @@ const useKeyboard = { if (!handlersByContext[contextName]) { useKeyboard.registerContext(contextName); } - handlersByContext[contextName].push({ - keys, - callback, - prevent: options.prevent ?? true, - priority: options.priority ?? PRIORITY.LOW, - }); + + const existingHandlerIndex = handlersByContext[contextName].findIndex( + (handler) => + handler.keys.length === keys.length && + handler.keys.every((key, i) => key === keys[i]) && + handler.callback.toString() === callback.toString() + ); + + if (existingHandlerIndex !== -1) { + handlersByContext[contextName][existingHandlerIndex] = { + keys, + callback, + prevent: options.prevent ?? true, + priority: options.priority ?? PRIORITY.LOW, + }; + } else { + handlersByContext[contextName].push({ + keys, + callback, + prevent: options.prevent ?? true, + priority: options.priority ?? PRIORITY.LOW, + }); + } if (activeContexts.has(contextName)) { initKeyboardHandlers(); @@ -118,8 +131,8 @@ const useKeyboard = { } if (onToggleActions) { - const togglePriority = PRIORITY.HIGH; - + const togglePriority = Math.max(priority, PRIORITY.HIGH); + if (currentOS === "macos") { useKeyboard.on( contextName, @@ -169,52 +182,71 @@ const useKeyboard = { const initKeyboardHandlers = () => { keyboard.clear(); - let allHandlers: Array<{ keys: Key[], callback: KeyboardHandler, prevent: boolean, priority: number, contextName: string }> = []; - + let allHandlers: Array<{ + keys: Key[]; + callback: KeyboardHandler; + prevent: boolean; + priority: number; + contextName: string; + }> = []; + for (const contextName of activeContexts) { const handlers = handlersByContext[contextName] || []; - allHandlers = [...allHandlers, ...handlers.map(handler => ({ - ...handler, - priority: handler.priority ?? PRIORITY.LOW, - contextName - }))]; + allHandlers = [ + ...allHandlers, + ...handlers.map((handler) => ({ + ...handler, + priority: handler.priority ?? PRIORITY.LOW, + contextName, + })), + ]; } - + allHandlers.sort((a, b) => b.priority - a.priority); - - const handlersByKeyCombination: Record> = {}; - - allHandlers.forEach(handler => { - const keyCombo = handler.keys.join('+'); + + const handlersByKeyCombination: Record< + string, + Array<(typeof allHandlers)[0]> + > = {}; + + allHandlers.forEach((handler) => { + const keyCombo = handler.keys.sort().join("+"); if (!handlersByKeyCombination[keyCombo]) { handlersByKeyCombination[keyCombo] = []; } handlersByKeyCombination[keyCombo].push(handler); }); - - Object.values(handlersByKeyCombination).forEach(handlers => { + + Object.entries(handlersByKeyCombination).forEach(([_keyCombo, handlers]) => { + handlers.sort((a, b) => b.priority - a.priority); const handler = handlers[0]; - + const wrappedCallback: KeyboardHandler = (event) => { - const isMetaCombo = handler.keys.length > 1 && - (handler.keys.includes(Key.LeftMeta) || - handler.keys.includes(Key.RightMeta) || - handler.keys.includes(Key.LeftControl) || - handler.keys.includes(Key.RightControl)); - - const isNavigationKey = event.key === 'ArrowUp' || - event.key === 'ArrowDown' || - event.key === 'Enter' || - event.key === 'Escape'; - - const isInInput = event.target instanceof HTMLInputElement || - event.target instanceof HTMLTextAreaElement; - - if (isMetaCombo || isNavigationKey || !isInInput) { + const isMetaCombo = + handler.keys.length > 1 && + (handler.keys.includes(Key.LeftMeta) || + handler.keys.includes(Key.RightMeta) || + handler.keys.includes(Key.LeftControl) || + handler.keys.includes(Key.RightControl)); + + const isNavigationKey = + event.key === "ArrowUp" || + event.key === "ArrowDown" || + event.key === "Enter" || + event.key === "Escape"; + + const isInInput = + event.target instanceof HTMLInputElement || + event.target instanceof HTMLTextAreaElement; + + if ( + (isMetaCombo || isNavigationKey || !isInInput) && + activeContexts.has(handler.contextName) + ) { handler.callback(event); } }; - + if (handler.prevent) { keyboard.prevent.down(handler.keys, wrappedCallback); } else { @@ -223,11 +255,14 @@ const initKeyboardHandlers = () => { }); }; -export default defineNuxtPlugin(async () => { - await initOS(); +export default defineNuxtPlugin(async (nuxtApp) => { initKeyboardHandlers(); + nuxtApp.hook("page:finish", () => { + initKeyboardHandlers(); + }); + return { provide: { keyboard: { From bbd7a549487f40edca2068cd210facecae30a8ec Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:29:38 +0100 Subject: [PATCH 17/19] feat: add platform detection for keyboard functionality to support macOS and Windows --- plugins/keyboard.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/keyboard.ts b/plugins/keyboard.ts index 1bb6847..17e8d63 100644 --- a/plugins/keyboard.ts +++ b/plugins/keyboard.ts @@ -1,4 +1,5 @@ import { Key, keyboard } from "wrdu-keyboard"; +import { platform } from "@tauri-apps/plugin-os"; type KeyboardHandler = (event: KeyboardEvent) => void; @@ -256,6 +257,12 @@ const initKeyboardHandlers = () => { }; export default defineNuxtPlugin(async (nuxtApp) => { + try { + const osName = platform(); + currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows"; + } catch (error) { + console.error("Error detecting platform:", error); + } initKeyboardHandlers(); From 8abf23191269db38453c5f90dfcec4dc4826d45e Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:38:52 +0100 Subject: [PATCH 18/19] feat: enhance keybind input handling with Escape key functionality and improved keyboard context management --- pages/settings.vue | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pages/settings.vue b/pages/settings.vue index 5f0c7c5..4bbf980 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -45,6 +45,7 @@
([]); const keybindInput = ref(null); const lastBlurTime = ref(0); +const blurredByEscape = ref(false); const os = ref(""); const router = useRouter(); const showEmptyKeybindError = ref(false); @@ -127,6 +129,7 @@ const onBlur = () => { const onFocus = () => { isKeybindInputFocused.value = true; + blurredByEscape.value = false; activeModifiers.clear(); keybind.value = []; showEmptyKeybindError.value = false; @@ -137,7 +140,10 @@ const onKeyDown = (event: KeyboardEvent) => { if (key === KeyValues.Escape) { if (keybindInput.value) { + blurredByEscape.value = true; keybindInput.value.blur(); + event.preventDefault(); + event.stopPropagation(); } return; } @@ -201,36 +207,30 @@ onMounted(async () => { if (os.value === "macos") { $keyboard.on("settings", [$keyboard.Key.LeftMeta, $keyboard.Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }, { priority: $keyboard.PRIORITY.MEDIUM }); + saveKeybind(); + }, { priority: $keyboard.PRIORITY.HIGH }); $keyboard.on("settings", [$keyboard.Key.RightMeta, $keyboard.Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }, { priority: $keyboard.PRIORITY.MEDIUM }); + saveKeybind(); + }, { priority: $keyboard.PRIORITY.HIGH }); } else { $keyboard.on("settings", [$keyboard.Key.LeftControl, $keyboard.Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }, { priority: $keyboard.PRIORITY.MEDIUM }); + saveKeybind(); + }, { priority: $keyboard.PRIORITY.HIGH }); $keyboard.on("settings", [$keyboard.Key.RightControl, $keyboard.Key.Enter], () => { - if (!isKeybindInputFocused.value) { - saveKeybind(); - } - }, { priority: $keyboard.PRIORITY.MEDIUM }); + saveKeybind(); + }, { priority: $keyboard.PRIORITY.HIGH }); } $keyboard.on("settings", [$keyboard.Key.Escape], () => { - if (!isKeybindInputFocused.value) { + if (!isKeybindInputFocused.value && !blurredByEscape.value) { router.push("/"); } - }, { priority: $keyboard.PRIORITY.MEDIUM }); + blurredByEscape.value = false; + }, { priority: $keyboard.PRIORITY.HIGH }); + $keyboard.disableContext("main"); $keyboard.enableContext("settings"); autostart.value = (await $settings.getSetting("autostart")) === "true"; From ae5103e8007e628b0dbc6858e5307c369ead53fc Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Sun, 16 Mar 2025 23:38:57 +0100 Subject: [PATCH 19/19] feat: update BottomBar component to include platform-specific key modifiers and improve action button layout --- components/BottomBar.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/components/BottomBar.vue b/components/BottomBar.vue index c978c99..4bb59b9 100644 --- a/components/BottomBar.vue +++ b/components/BottomBar.vue @@ -7,12 +7,16 @@

{{ primaryAction.text }}

- +
+ + + +

{{ secondaryAction.text }}

-
+
@@ -96,7 +100,7 @@ onMounted(async () => { color: var(--text); } - .actions div { + .keys { display: flex; align-items: center; gap: 2px;