feat: enhance ActionsMenu component with dynamic action icons, improved keyboard shortcuts, and integrated action handling logic

This commit is contained in:
pandadev 2025-03-16 21:32:55 +01:00
parent b828daff08
commit ddd92a70e2
No known key found for this signature in database
GPG key ID: C39629DACB8E762F
2 changed files with 310 additions and 48 deletions

View file

@ -13,7 +13,7 @@
@click="executeAction(action)" :class="{ selected: isSelected && currentIndex === index }" :ref="(el) => { @click="executeAction(action)" :class="{ selected: isSelected && currentIndex === index }" :ref="(el) => {
if (currentIndex === index) setSelectedElement(el); if (currentIndex === index) setSelectedElement(el);
} }
"> " :style="action.color ? { color: action.color } : {}">
<div class="content"> <div class="content">
<component v-if="action.icon" :is="action.icon" class="icon" /> <component v-if="action.icon" :is="action.icon" class="icon" />
<div class="title">{{ action.title }}</div> <div class="title">{{ action.title }}</div>
@ -64,8 +64,8 @@
setSelectedElement(el); setSelectedElement(el);
} }
"> ">
<component v-if="action.icon" :is="action.icon" class="icon" />
<div class="content"> <div class="content">
<component v-if="action.icon" :is="action.icon" class="icon" />
<div class="title">{{ action.title }}</div> <div class="title">{{ action.title }}</div>
</div> </div>
<div v-if="action.shortcut" class="shortcut"> <div v-if="action.shortcut" class="shortcut">
@ -88,7 +88,7 @@
if (currentIndex === getActionIndex(index, 'bottom')) if (currentIndex === getActionIndex(index, 'bottom'))
setSelectedElement(el); setSelectedElement(el);
} }
"> " :style="action.color ? { color: action.color } : {}">
<div class="content"> <div class="content">
<component v-if="action.icon" :is="action.icon" class="icon" /> <component v-if="action.icon" :is="action.icon" class="icon" />
<div class="title">{{ action.title }}</div> <div class="title">{{ action.title }}</div>
@ -109,16 +109,29 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from "vue"; import { ref, onMounted, onUnmounted, computed, watch, nextTick, h } from "vue";
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import "overlayscrollbars/overlayscrollbars.css"; import "overlayscrollbars/overlayscrollbars.css";
import Enter from "./Icons/Enter.vue"; import Enter from "./Keys/Enter.vue";
import Ctrl from "./Icons/Ctrl.vue"; import Cmd from "./Keys/Cmd.vue";
import Cmd from "./Icons/Cmd.vue"; import Key from "./Keys/Key.vue";
import Key from "./Icons/Key.vue";
import { ContentType, HistoryItem } from "../types/types"; import { ContentType, HistoryItem } from "../types/types";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { useNuxtApp } from "#app"; import { useNuxtApp } from "#app";
import Shift from "./Keys/Shift.vue";
import Gear from "./Icons/Gear.vue";
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";
import Brush from "./Icons/Brush.vue";
import Rotate from "./Icons/Rotate.vue";
import Expand from "./Icons/Expand.vue";
import { useActions } from "../composables/useActions";
interface AppInfo { interface AppInfo {
name: string; name: string;
@ -136,6 +149,7 @@ const menuRef = ref<HTMLElement | null>(null);
const scrollbarsRef = ref<InstanceType< const scrollbarsRef = ref<InstanceType<
typeof OverlayScrollbarsComponent typeof OverlayScrollbarsComponent
> | null>(null); > | null>(null);
const { handleAction, isProcessing } = useActions();
const SCROLL_PADDING = 8; const SCROLL_PADDING = 8;
@ -172,136 +186,152 @@ interface ActionItem {
shortcut?: string; shortcut?: string;
icon?: any; icon?: any;
group: string; group: string;
color?: string;
} }
const topActions = computed(() => [ const topActions = computed((): ActionItem[] => [
{ {
title: `Paste to ${currentAppInfo.value.name || "Current App"}`, title: `Paste to ${currentAppInfo.value.name || "Current App"}`,
shortcut: "Enter", shortcut: "Enter",
action: "paste-to-app", action: "paste-to-app",
group: "top", group: "top",
icon: undefined, icon: currentAppInfo.value.icon ? {
render() {
return h('img', {
src: currentAppInfo.value.icon,
style: {
width: '14px',
height: '14px',
objectFit: 'contain'
}
});
}
} : Cube,
}, },
{ {
title: "Copy to Clipboard", title: "Copy to Clipboard",
shortcut: "Ctrl+C", shortcut: "Ctrl+C",
action: "copy", action: "copy",
group: "top", group: "top",
icon: undefined, icon: Board,
}, },
]); ]);
const bottomActions = computed(() => [ const bottomActions = computed((): ActionItem[] => [
{ {
title: "Delete Entry", title: "Delete Entry",
shortcut: "Del", shortcut: "Alt+X",
action: "delete", action: "delete",
group: "bottom", group: "bottom",
icon: undefined, icon: Bin,
color: "var(--red)"
}, },
{ {
title: "Delete All Entries", title: "Delete All Entries",
shortcut: "Alt+Shift+X",
action: "delete-all", action: "delete-all",
group: "bottom", group: "bottom",
icon: undefined, icon: Bin,
color: "var(--red)"
}, },
{ {
title: "Settings", title: "Settings",
shortcut: "Ctrl+S",
action: "settings", action: "settings",
group: "bottom", group: "bottom",
icon: undefined, icon: Gear,
}, },
]); ]);
const textActions = computed(() => [ const textActions = computed((): ActionItem[] => [
{ {
title: "Paste as plain text", title: "Paste as plain text",
action: "paste-plain", action: "paste-plain",
shortcut: "", shortcut: "Ctrl+Shift+V",
group: "text", group: "text",
icon: undefined, icon: T,
}, },
{ {
title: "Edit text", title: "Edit text",
action: "edit-text", action: "edit-text",
shortcut: "", shortcut: "Ctrl+E",
group: "text", group: "text",
icon: undefined, icon: Pen,
}, },
]); ]);
const imageActions = computed(() => [ const imageActions = computed((): ActionItem[] => [
{ {
title: "Rotate", title: "Rotate",
action: "rotate-image", action: "rotate-image",
shortcut: "", shortcut: "Alt+R",
group: "image", group: "image",
icon: undefined, icon: Rotate,
}, },
{ {
title: "Resize", title: "Resize",
action: "resize-image", action: "resize-image",
shortcut: "", shortcut: "Alt+S",
group: "image", group: "image",
icon: undefined, icon: Expand,
}, },
{ {
title: "Compress", title: "Compress",
action: "compress-image", action: "compress-image",
shortcut: "", shortcut: "Alt+C",
group: "image", group: "image",
icon: undefined, icon: Zip,
}, },
]); ]);
const fileActions = computed(() => [ const fileActions = computed((): ActionItem[] => [
{ {
title: "Open", title: "Open",
action: "open-file", action: "open-file",
shortcut: "", shortcut: "Ctrl+O",
group: "file", group: "file",
icon: undefined, icon: Open,
}, },
{ {
title: "Compress to zip/7z", title: "Compress to zip",
action: "compress-file", action: "compress-file",
shortcut: "", shortcut: "Alt+C",
group: "file", group: "file",
icon: undefined, icon: Zip,
}, },
]); ]);
const linkActions = computed(() => [ const linkActions = computed((): ActionItem[] => [
{ {
title: "Open in Browser", title: "Open in Browser",
action: "open-link", action: "open-link",
shortcut: "", shortcut: "Ctrl+O",
group: "link", group: "link",
icon: undefined, icon: Globe,
}, },
]); ]);
const colorActions = computed(() => [ const colorActions = computed((): ActionItem[] => [
{ {
title: "Copy as HEX", title: "Copy as HEX",
action: "copy-hex", action: "copy-hex",
shortcut: "", shortcut: "Alt+H",
group: "color", group: "color",
icon: undefined, icon: Brush,
}, },
{ {
title: "Copy as RGB(a)", title: "Copy as RGB(a)",
action: "copy-rgba", action: "copy-rgba",
shortcut: "", shortcut: "Alt+R",
group: "color", group: "color",
icon: undefined, icon: Brush,
}, },
{ {
title: "Copy as HSL(a)", title: "Copy as HSL(a)",
action: "copy-hsla", action: "copy-hsla",
shortcut: "", shortcut: "Alt+S",
group: "color", group: "color",
icon: undefined, icon: Brush,
}, },
]); ]);
@ -369,10 +399,10 @@ const parseShortcut = (shortcut: string): KeyPart[] => {
const trimmedPart = part.trim(); const trimmedPart = part.trim();
let keyPart: KeyPart; let keyPart: KeyPart;
if (trimmedPart.toLowerCase() === "ctrl") { if (trimmedPart.toLowerCase() === "cmd") {
keyPart = { type: "modifier", value: trimmedPart, component: Ctrl };
} else if (trimmedPart.toLowerCase() === "cmd") {
keyPart = { type: "modifier", value: trimmedPart, component: Cmd }; keyPart = { type: "modifier", value: trimmedPart, component: Cmd };
} else if (trimmedPart.toLowerCase() === "shift") {
keyPart = { type: "modifier", value: trimmedPart, component: Shift };
} else if (trimmedPart.toLowerCase() === "enter") { } else if (trimmedPart.toLowerCase() === "enter") {
keyPart = { type: "key", value: trimmedPart, component: Enter }; keyPart = { type: "key", value: trimmedPart, component: Enter };
} else { } else {
@ -391,7 +421,7 @@ const parseShortcut = (shortcut: string): KeyPart[] => {
const executeAction = (action: ActionItem) => { const executeAction = (action: ActionItem) => {
emit("close"); emit("close");
emit("action", action.action, props.selectedItem); handleAction(action.action, props.selectedItem);
}; };
const close = () => { const close = () => {
@ -404,6 +434,10 @@ const handleClickOutside = (event: MouseEvent) => {
} }
}; };
const handleWindowBlur = () => {
close();
};
const setupKeyboardHandlers = () => { const setupKeyboardHandlers = () => {
$keyboard.on( $keyboard.on(
"actionsMenu", "actionsMenu",
@ -603,11 +637,13 @@ watch(
onMounted(() => { onMounted(() => {
document.addEventListener("click", handleClickOutside); document.addEventListener("click", handleClickOutside);
window.addEventListener("blur", handleWindowBlur);
getAppInfo(); getAppInfo();
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener("click", handleClickOutside); document.removeEventListener("click", handleClickOutside);
window.removeEventListener("blur", handleWindowBlur);
$keyboard.disableContext("actionsMenu"); $keyboard.disableContext("actionsMenu");
}); });
</script> </script>
@ -683,6 +719,10 @@ onUnmounted(() => {
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
.title {
color: inherit;
}
} }
} }

222
composables/useActions.ts Normal file
View file

@ -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,
};
}