mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 13:14:04 +02:00
Compare commits
21 commits
b3b0dd5d35
...
2016e614ba
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2016e614ba | ||
![]() |
8adbeb1c6e | ||
![]() |
ae5103e800 | ||
![]() |
8abf231912 | ||
![]() |
bbd7a54948 | ||
![]() |
3a5e2cba7e | ||
![]() |
b8238d01ca | ||
![]() |
554943d349 | ||
![]() |
a79268d0f7 | ||
![]() |
be1718d9a5 | ||
![]() |
ddd92a70e2 | ||
![]() |
b828daff08 | ||
![]() |
409e10a8fd | ||
![]() |
843a1ea8b7 | ||
![]() |
fce7eec1fc | ||
![]() |
2865f8749e | ||
![]() |
7ba418f4cc | ||
![]() |
b946b6455f | ||
![]() |
af64b77b74 | ||
![]() |
5e669749d7 | ||
![]() |
dc638cb3ce |
34 changed files with 1749 additions and 274 deletions
|
@ -123,7 +123,7 @@ bun build
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
Qopy is licensed under GPL-3. See the [LICENSE file](./LICENCE) for more information.
|
Qopy is licensed under AGPL-3. See the [LICENSE file](./LICENCE) for more information.
|
||||||
|
|
||||||
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
|
[codespaces-link]: https://codespaces.new/0pandadev/Qopy
|
||||||
[codespaces-shield]: https://github.com/codespaces/badge.svg
|
[codespaces-shield]: https://github.com/codespaces/badge.svg
|
||||||
|
|
2
app.vue
2
app.vue
|
@ -64,6 +64,8 @@ onMounted(async () => {
|
||||||
--accent: #feb453;
|
--accent: #feb453;
|
||||||
--border: #ffffff0d;
|
--border: #ffffff0d;
|
||||||
|
|
||||||
|
--red: #F84E4E;
|
||||||
|
|
||||||
--text: #e5dfd5;
|
--text: #e5dfd5;
|
||||||
--text-secondary: #ada9a1;
|
--text-secondary: #ada9a1;
|
||||||
--text-muted: #78756f;
|
--text-muted: #78756f;
|
||||||
|
|
798
components/ActionsMenu.vue
Normal file
798
components/ActionsMenu.vue
Normal file
|
@ -0,0 +1,798 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="actions-menu"
|
||||||
|
class="actions-menu-overlay"
|
||||||
|
:class="{ visible: isVisible }"
|
||||||
|
@click="$emit('close')"
|
||||||
|
tabindex="-1">
|
||||||
|
<div class="actions-menu" @click.stop>
|
||||||
|
<div v-if="isVisible" class="actions" ref="menuRef">
|
||||||
|
<OverlayScrollbarsComponent ref="scrollbarsRef" class="actions-scrollable"
|
||||||
|
:options="{ scrollbars: { autoHide: 'scroll' } }">
|
||||||
|
<template v-if="searchQuery">
|
||||||
|
<div class="action-group">
|
||||||
|
<div v-if="allFilteredActions.length === 0" class="action no-results">
|
||||||
|
<div class="content">
|
||||||
|
<div class="title">No Results</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else v-for="(action, index) in allFilteredActions" :key="action.action" class="action"
|
||||||
|
@click="executeAction(action)" :class="{ selected: isSelected && currentIndex === index }" :ref="(el) => {
|
||||||
|
if (currentIndex === index) setSelectedElement(el);
|
||||||
|
}
|
||||||
|
" :style="action.color ? { color: action.color } : {}">
|
||||||
|
<div class="content">
|
||||||
|
<component v-if="action.icon" :is="action.icon" class="icon" />
|
||||||
|
<div class="title">{{ action.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="action.shortcut" class="shortcut">
|
||||||
|
<template v-for="(key, keyIndex) in parseShortcut(action.shortcut)" :key="keyIndex">
|
||||||
|
<component :is="key.component" v-if="key.component" :input="key.value" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="action-group">
|
||||||
|
<div v-for="(action, index) in topActions" :key="action.action" class="action" @click="executeAction(action)"
|
||||||
|
:class="{
|
||||||
|
selected:
|
||||||
|
isSelected && currentIndex === getActionIndex(index, 'top'),
|
||||||
|
}" :ref="(el) => {
|
||||||
|
if (currentIndex === getActionIndex(index, 'top'))
|
||||||
|
setSelectedElement(el);
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<div class="content">
|
||||||
|
<component v-if="action.icon" :is="action.icon" class="icon" />
|
||||||
|
<div class="title">{{ action.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="action.shortcut" class="shortcut">
|
||||||
|
<template v-for="(key, index) in parseShortcut(action.shortcut)" :key="index">
|
||||||
|
<component :is="key.component" v-if="key.component" :input="key.value" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider" v-if="
|
||||||
|
topActions.length > 0 && typeSpecificActions.length > 0
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="typeSpecificActions.length > 0" class="action-group">
|
||||||
|
<div v-for="(action, index) in typeSpecificActions" :key="action.action" class="action"
|
||||||
|
@click="executeAction(action)" :class="{
|
||||||
|
selected:
|
||||||
|
isSelected &&
|
||||||
|
currentIndex === getActionIndex(index, 'specific'),
|
||||||
|
}" :ref="(el) => {
|
||||||
|
if (currentIndex === getActionIndex(index, 'specific'))
|
||||||
|
setSelectedElement(el);
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<div class="content">
|
||||||
|
<component v-if="action.icon" :is="action.icon" class="icon" />
|
||||||
|
<div class="title">{{ action.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="action.shortcut" class="shortcut">
|
||||||
|
<template v-for="(key, index) in parseShortcut(action.shortcut)" :key="index">
|
||||||
|
<component :is="key.component" v-if="key.component" :input="key.value" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider" v-if="
|
||||||
|
typeSpecificActions.length > 0 && bottomActions.length > 0
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-group">
|
||||||
|
<div v-for="(action, index) in bottomActions" :key="action.action" class="action"
|
||||||
|
@click="executeAction(action)" :class="{
|
||||||
|
selected:
|
||||||
|
isSelected && currentIndex === getActionIndex(index, 'bottom'),
|
||||||
|
}" :ref="(el) => {
|
||||||
|
if (currentIndex === getActionIndex(index, 'bottom'))
|
||||||
|
setSelectedElement(el);
|
||||||
|
}
|
||||||
|
" :style="action.color ? { color: action.color } : {}">
|
||||||
|
<div class="content">
|
||||||
|
<component v-if="action.icon" :is="action.icon" class="icon" />
|
||||||
|
<div class="title">{{ action.title }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="action.shortcut" class="shortcut">
|
||||||
|
<template v-for="(key, index) in parseShortcut(action.shortcut)" :key="index">
|
||||||
|
<component :is="key.component" v-if="key.component" :input="key.value" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
|
||||||
|
<input type="text" v-model="searchQuery" class="search-input" placeholder="Search..." @keydown="handleSearchKeydown"
|
||||||
|
ref="searchInput" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick, h } from "vue";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
|
import "overlayscrollbars/overlayscrollbars.css";
|
||||||
|
import Enter from "./Keys/Enter.vue";
|
||||||
|
import Cmd from "./Keys/Cmd.vue";
|
||||||
|
import Key from "./Keys/Key.vue";
|
||||||
|
import { ContentType, HistoryItem } from "../types/types";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
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 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 {
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAppInfo = ref<AppInfo>({ name: "Current App" });
|
||||||
|
const isSelected = ref(true);
|
||||||
|
const currentIndex = ref(0);
|
||||||
|
const selectedElement = ref<HTMLElement | null>(null);
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const searchInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const { $keyboard } = useNuxtApp();
|
||||||
|
const menuRef = ref<HTMLElement | null>(null);
|
||||||
|
const scrollbarsRef = ref<InstanceType<
|
||||||
|
typeof OverlayScrollbarsComponent
|
||||||
|
> | null>(null);
|
||||||
|
const { handleAction } = useActions();
|
||||||
|
|
||||||
|
const SCROLL_PADDING = 8;
|
||||||
|
|
||||||
|
const setSelectedElement = (el: any) => {
|
||||||
|
if (el && el instanceof HTMLElement) {
|
||||||
|
selectedElement.value = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAppInfo = async () => {
|
||||||
|
try {
|
||||||
|
const appInfo = await invoke("get_app_info");
|
||||||
|
if (appInfo && typeof appInfo === "object" && "name" in appInfo) {
|
||||||
|
currentAppInfo.value = appInfo as AppInfo;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get app info:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isVisible: boolean;
|
||||||
|
selectedItem?: HistoryItem;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
(e: "toggle"): void;
|
||||||
|
(e: "action", action: string, item?: HistoryItem): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
interface ActionItem {
|
||||||
|
title: string;
|
||||||
|
action: string;
|
||||||
|
shortcut?: string;
|
||||||
|
icon?: any;
|
||||||
|
group: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topActions = computed((): ActionItem[] => [
|
||||||
|
{
|
||||||
|
title: `Paste to ${currentAppInfo.value.name || "Current App"}`,
|
||||||
|
shortcut: "Enter",
|
||||||
|
action: "paste-to-app",
|
||||||
|
group: "top",
|
||||||
|
icon: currentAppInfo.value.icon ? {
|
||||||
|
render() {
|
||||||
|
return h('img', {
|
||||||
|
src: currentAppInfo.value.icon,
|
||||||
|
style: {
|
||||||
|
width: '14px',
|
||||||
|
height: '14px',
|
||||||
|
objectFit: 'contain'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Copy to Clipboard",
|
||||||
|
shortcut: "Ctrl+C",
|
||||||
|
action: "copy",
|
||||||
|
group: "top",
|
||||||
|
icon: Board,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const bottomActions = computed((): ActionItem[] => [
|
||||||
|
{
|
||||||
|
title: "Delete Entry",
|
||||||
|
shortcut: "Alt+X",
|
||||||
|
action: "delete",
|
||||||
|
group: "bottom",
|
||||||
|
icon: Bin,
|
||||||
|
color: "var(--red)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Delete All Entries",
|
||||||
|
shortcut: "Alt+Shift+X",
|
||||||
|
action: "delete-all",
|
||||||
|
group: "bottom",
|
||||||
|
icon: Bin,
|
||||||
|
color: "var(--red)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Settings",
|
||||||
|
shortcut: "Ctrl+S",
|
||||||
|
action: "settings",
|
||||||
|
group: "bottom",
|
||||||
|
icon: Gear,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const textActions = computed((): ActionItem[] => [
|
||||||
|
{
|
||||||
|
title: "Paste as plain text",
|
||||||
|
action: "paste-plain",
|
||||||
|
shortcut: "Ctrl+Shift+V",
|
||||||
|
group: "text",
|
||||||
|
icon: T,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Edit text",
|
||||||
|
action: "edit-text",
|
||||||
|
shortcut: "Ctrl+E",
|
||||||
|
group: "text",
|
||||||
|
icon: Pen,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const imageActions = computed((): ActionItem[] => [
|
||||||
|
{
|
||||||
|
title: "Rotate",
|
||||||
|
action: "rotate-image",
|
||||||
|
shortcut: "Alt+R",
|
||||||
|
group: "image",
|
||||||
|
icon: Rotate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Resize",
|
||||||
|
action: "resize-image",
|
||||||
|
shortcut: "Alt+S",
|
||||||
|
group: "image",
|
||||||
|
icon: Expand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Compress",
|
||||||
|
action: "compress-image",
|
||||||
|
shortcut: "Alt+C",
|
||||||
|
group: "image",
|
||||||
|
icon: Zip,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const fileActions = computed((): ActionItem[] => [
|
||||||
|
{
|
||||||
|
title: "Open",
|
||||||
|
action: "open-file",
|
||||||
|
shortcut: "Ctrl+O",
|
||||||
|
group: "file",
|
||||||
|
icon: Open,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Compress to zip",
|
||||||
|
action: "compress-file",
|
||||||
|
shortcut: "Alt+C",
|
||||||
|
group: "file",
|
||||||
|
icon: Zip,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const linkActions = computed((): ActionItem[] => [
|
||||||
|
{
|
||||||
|
title: "Open in Browser",
|
||||||
|
action: "open-link",
|
||||||
|
shortcut: "Ctrl+O",
|
||||||
|
group: "link",
|
||||||
|
icon: Globe,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const colorActions = computed((): ActionItem[] => [
|
||||||
|
{
|
||||||
|
title: "Copy as HEX",
|
||||||
|
action: "copy-hex",
|
||||||
|
shortcut: "Alt+H",
|
||||||
|
group: "color",
|
||||||
|
icon: Brush,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Copy as RGB(a)",
|
||||||
|
action: "copy-rgba",
|
||||||
|
shortcut: "Alt+R",
|
||||||
|
group: "color",
|
||||||
|
icon: Brush,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Copy as HSL(a)",
|
||||||
|
action: "copy-hsla",
|
||||||
|
shortcut: "Alt+S",
|
||||||
|
group: "color",
|
||||||
|
icon: Brush,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const typeSpecificActions = computed(() => {
|
||||||
|
if (!props.selectedItem) return [];
|
||||||
|
|
||||||
|
switch (props.selectedItem.content_type) {
|
||||||
|
case ContentType.Text:
|
||||||
|
return textActions.value;
|
||||||
|
case ContentType.Image:
|
||||||
|
return imageActions.value;
|
||||||
|
case ContentType.File:
|
||||||
|
return fileActions.value;
|
||||||
|
case ContentType.Link:
|
||||||
|
return linkActions.value;
|
||||||
|
case ContentType.Color:
|
||||||
|
return colorActions.value;
|
||||||
|
case ContentType.Code:
|
||||||
|
return textActions.value;
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const allActions = computed(() => {
|
||||||
|
return [
|
||||||
|
...topActions.value,
|
||||||
|
...typeSpecificActions.value,
|
||||||
|
...bottomActions.value,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const allFilteredActions = computed(() => {
|
||||||
|
if (!searchQuery.value) return allActions.value;
|
||||||
|
|
||||||
|
return allActions.value.filter((action) =>
|
||||||
|
action.title.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getActionIndex = (
|
||||||
|
index: number,
|
||||||
|
group: "top" | "specific" | "bottom"
|
||||||
|
): number => {
|
||||||
|
if (group === "top") {
|
||||||
|
return index;
|
||||||
|
} else if (group === "specific") {
|
||||||
|
return topActions.value.length + index;
|
||||||
|
} else {
|
||||||
|
return topActions.value.length + typeSpecificActions.value.length + index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface KeyPart {
|
||||||
|
type: "modifier" | "key" | "separator";
|
||||||
|
value: string;
|
||||||
|
component?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseShortcut = (shortcut: string): KeyPart[] => {
|
||||||
|
const parts = shortcut.split("+");
|
||||||
|
const result: KeyPart[] = [];
|
||||||
|
|
||||||
|
parts.forEach((part, index) => {
|
||||||
|
const trimmedPart = part.trim();
|
||||||
|
let keyPart: KeyPart;
|
||||||
|
|
||||||
|
if (trimmedPart.toLowerCase() === "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") {
|
||||||
|
keyPart = { type: "key", value: trimmedPart, component: Enter };
|
||||||
|
} else {
|
||||||
|
keyPart = { type: "key", value: trimmedPart, component: Key };
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(keyPart);
|
||||||
|
|
||||||
|
if (index < parts.length - 1) {
|
||||||
|
result.push({ type: "separator", value: "+" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeAction = (action: ActionItem) => {
|
||||||
|
emit("close");
|
||||||
|
handleAction(action.action, props.selectedItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (menuRef.value && !menuRef.value.contains(event.target as Node)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWindowBlur = () => {
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupKeyboardHandlers = () => {
|
||||||
|
$keyboard.on(
|
||||||
|
"actionsMenu",
|
||||||
|
[$keyboard.Key.ArrowDown],
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
selectNext();
|
||||||
|
},
|
||||||
|
{ priority: $keyboard.PRIORITY.HIGH }
|
||||||
|
);
|
||||||
|
|
||||||
|
$keyboard.on(
|
||||||
|
"actionsMenu",
|
||||||
|
[$keyboard.Key.ArrowUp],
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
selectPrevious();
|
||||||
|
},
|
||||||
|
{ priority: $keyboard.PRIORITY.HIGH }
|
||||||
|
);
|
||||||
|
|
||||||
|
$keyboard.on(
|
||||||
|
"actionsMenu",
|
||||||
|
[$keyboard.Key.Enter],
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const action = allFilteredActions.value[currentIndex.value];
|
||||||
|
if (action) executeAction(action);
|
||||||
|
} else {
|
||||||
|
let action;
|
||||||
|
if (currentIndex.value < topActions.value.length) {
|
||||||
|
action = topActions.value[currentIndex.value];
|
||||||
|
} else if (
|
||||||
|
currentIndex.value <
|
||||||
|
topActions.value.length + typeSpecificActions.value.length
|
||||||
|
) {
|
||||||
|
action =
|
||||||
|
typeSpecificActions.value[
|
||||||
|
currentIndex.value - topActions.value.length
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
action =
|
||||||
|
bottomActions.value[
|
||||||
|
currentIndex.value -
|
||||||
|
topActions.value.length -
|
||||||
|
typeSpecificActions.value.length
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (action) executeAction(action);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ priority: $keyboard.PRIORITY.HIGH }
|
||||||
|
);
|
||||||
|
|
||||||
|
$keyboard.on(
|
||||||
|
"actionsMenu",
|
||||||
|
[$keyboard.Key.Escape],
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
{ 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 = () => {
|
||||||
|
if (searchQuery.value) {
|
||||||
|
if (allFilteredActions.value.length === 0) return;
|
||||||
|
currentIndex.value =
|
||||||
|
(currentIndex.value + 1) % allFilteredActions.value.length;
|
||||||
|
} else {
|
||||||
|
const totalActions = allActions.value.length;
|
||||||
|
if (totalActions === 0) return;
|
||||||
|
currentIndex.value = (currentIndex.value + 1) % totalActions;
|
||||||
|
}
|
||||||
|
scrollToSelected();
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPrevious = () => {
|
||||||
|
if (searchQuery.value) {
|
||||||
|
if (allFilteredActions.value.length === 0) return;
|
||||||
|
currentIndex.value =
|
||||||
|
(currentIndex.value - 1 + allFilteredActions.value.length) %
|
||||||
|
allFilteredActions.value.length;
|
||||||
|
} else {
|
||||||
|
const totalActions = allActions.value.length;
|
||||||
|
if (totalActions === 0) return;
|
||||||
|
currentIndex.value = (currentIndex.value - 1 + totalActions) % totalActions;
|
||||||
|
}
|
||||||
|
scrollToSelected();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToSelected = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (!selectedElement.value) return;
|
||||||
|
if (!scrollbarsRef.value) return;
|
||||||
|
|
||||||
|
const viewport = scrollbarsRef.value.osInstance()?.elements().viewport;
|
||||||
|
if (!viewport) {
|
||||||
|
selectedElement.value.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!selectedElement.value) return;
|
||||||
|
|
||||||
|
const viewportRect = viewport.getBoundingClientRect();
|
||||||
|
const elementRect = selectedElement.value.getBoundingClientRect();
|
||||||
|
|
||||||
|
const isAbove = elementRect.top < viewportRect.top + SCROLL_PADDING;
|
||||||
|
const isBelow = elementRect.bottom > viewportRect.bottom - SCROLL_PADDING;
|
||||||
|
|
||||||
|
if (isAbove) {
|
||||||
|
const scrollAmount =
|
||||||
|
viewport.scrollTop +
|
||||||
|
(elementRect.top - viewportRect.top) -
|
||||||
|
SCROLL_PADDING;
|
||||||
|
viewport.scrollTo({
|
||||||
|
top: scrollAmount,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
} else if (isBelow) {
|
||||||
|
const scrollAmount =
|
||||||
|
viewport.scrollTop +
|
||||||
|
(elementRect.bottom - viewportRect.bottom) +
|
||||||
|
SCROLL_PADDING;
|
||||||
|
viewport.scrollTo({
|
||||||
|
top: scrollAmount,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
event.key === "ArrowDown" ||
|
||||||
|
event.key === "ArrowUp" ||
|
||||||
|
event.key === "Enter" ||
|
||||||
|
event.key === "Escape"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.toLowerCase() === "k" && (event.ctrlKey || event.metaKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
emit("toggle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isVisible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
currentIndex.value = 0;
|
||||||
|
searchQuery.value = "";
|
||||||
|
setupKeyboardHandlers();
|
||||||
|
$keyboard.enableContext("actionsMenu");
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (searchInput.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
searchInput.value?.focus();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$keyboard.disableContext("actionsMenu");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(searchQuery, (query) => {
|
||||||
|
currentIndex.value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[allFilteredActions, topActions, typeSpecificActions, bottomActions],
|
||||||
|
() => {
|
||||||
|
if (searchQuery.value) {
|
||||||
|
if (
|
||||||
|
currentIndex.value >= allFilteredActions.value.length &&
|
||||||
|
allFilteredActions.value.length > 0
|
||||||
|
) {
|
||||||
|
currentIndex.value = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const totalActions = allActions.value.length;
|
||||||
|
if (currentIndex.value >= totalActions && totalActions > 0) {
|
||||||
|
currentIndex.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
window.addEventListener("blur", handleWindowBlur);
|
||||||
|
getAppInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
window.removeEventListener("blur", handleWindowBlur);
|
||||||
|
$keyboard.disableContext("actionsMenu");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.actions {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background-color: var(--background);
|
||||||
|
position: fixed;
|
||||||
|
bottom: 48px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 100;
|
||||||
|
width: 350px;
|
||||||
|
max-height: 250px;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-scrollable {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-inline: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border);
|
||||||
|
margin: 8px -8px;
|
||||||
|
width: calc(100% + 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action.no-results {
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-inline: 8px;
|
||||||
|
height: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom-left-radius: 7px;
|
||||||
|
border-bottom-right-radius: 7px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,17 +5,21 @@
|
||||||
<p class="name">Qopy</p>
|
<p class="name">Qopy</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div v-if="primaryAction" class="paste" @click="primaryAction.onClick">
|
<div v-if="primaryAction" class="paste" @click="handlePrimaryClick">
|
||||||
<p class="text">{{ primaryAction.text }}</p>
|
<p class="text">{{ primaryAction.text }}</p>
|
||||||
<component :is="primaryAction.icon" />
|
<div class="keys">
|
||||||
|
<Key v-if="(os === 'windows' || os === 'linux') && primaryAction.showModifier" :input="'Ctrl'" />
|
||||||
|
<IconsCmd v-if="os === 'macos' && primaryAction.showModifier" />
|
||||||
|
<component :is="primaryAction.icon" :input="primaryAction.input" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="secondaryAction" class="divider"></div>
|
<div v-if="secondaryAction" class="divider"></div>
|
||||||
<div v-if="secondaryAction" class="actions" @click="secondaryAction.onClick">
|
<div v-if="secondaryAction" class="actions" @click="handleSecondaryClick">
|
||||||
<p class="text">{{ secondaryAction.text }}</p>
|
<p class="text">{{ secondaryAction.text }}</p>
|
||||||
<div>
|
<div class="keys">
|
||||||
<IconsCtrl v-if="(os === 'windows' || os === 'linux') && secondaryAction.showModifier" />
|
<Key v-if="(os === 'windows' || os === 'linux') && secondaryAction.showModifier" :input="'Ctrl'" />
|
||||||
<IconsCmd v-if="os === 'macos' && secondaryAction.showModifier" />
|
<IconsCmd v-if="os === 'macos' && secondaryAction.showModifier" />
|
||||||
<component :is="secondaryAction.icon" />
|
<component :is="secondaryAction.icon" :input="secondaryAction.input" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,13 +27,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
import IconsCmd from './Keys/Cmd.vue';
|
||||||
|
import Key from './Keys/Key.vue';
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
text: string;
|
text: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
showModifier?: boolean;
|
showModifier?: boolean;
|
||||||
|
input?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -39,8 +47,22 @@ const props = defineProps<{
|
||||||
|
|
||||||
const os = ref<string>("");
|
const os = ref<string>("");
|
||||||
|
|
||||||
onMounted(() => {
|
const handlePrimaryClick = (event: MouseEvent) => {
|
||||||
os.value = platform();
|
event.stopPropagation();
|
||||||
|
if (props.primaryAction?.onClick) {
|
||||||
|
props.primaryAction.onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecondaryClick = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (props.secondaryAction?.onClick) {
|
||||||
|
props.secondaryAction.onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
os.value = await platform();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -78,7 +100,7 @@ onMounted(() => {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions div {
|
.keys {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
@ -111,6 +133,12 @@ onMounted(() => {
|
||||||
background-color: var(--border);
|
background-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.paste:active,
|
||||||
|
.actions:active {
|
||||||
|
background-color: var(--border-active, #444);
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover .paste:hover ~ .divider,
|
&:hover .paste:hover ~ .divider,
|
||||||
&:hover .divider:has(+ .actions:hover) {
|
&:hover .divider:has(+ .actions:hover) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
7
components/Icons/Bin.vue
Normal file
7
components/Icons/Bin.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#F84E4E" fill-rule="evenodd"
|
||||||
|
d="M9 2H7a.5.5 0 0 0-.5.5V3h3v-.5A.5.5 0 0 0 9 2m2 1v-.5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2V3H2.251a.75.75 0 0 0 0 1.5h.312l.317 7.625A3 3 0 0 0 5.878 15h4.245a3 3 0 0 0 2.997-2.875l.318-7.625h.312a.75.75 0 0 0 0-1.5zm.936 1.5H4.064l.315 7.562A1.5 1.5 0 0 0 5.878 13.5h4.245a1.5 1.5 0 0 0 1.498-1.438zm-6.186 2v5a.75.75 0 0 0 1.5 0v-5a.75.75 0 0 0-1.5 0m3.75-.75a.75.75 0 0 1 .75.75v5a.75.75 0 0 1-1.5 0v-5a.75.75 0 0 1 .75-.75"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
14
components/Icons/Board.vue
Normal file
14
components/Icons/Board.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M0 1.38272C0 0.619063 0.596954 0 1.33333 0C1.33333 0 2.66667 0 2.66667 0C3.40305 0 4 0.619063 4 1.38272C4 1.38272 4 1.38272 4 1.38272C4 2.14637 3.40305 2.76543 2.66667 2.76543C2.66667 2.76543 1.33333 2.76543 1.33333 2.76543C0.596954 2.76543 0 2.14637 0 1.38272"
|
||||||
|
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
transform="translate(5 0.778)" />
|
||||||
|
<path
|
||||||
|
d="M2.66667 0C2.66667 0 1.33333 0 1.33333 0C0.596954 0 0 0.619063 0 1.38272C0 1.38272 0 9.67901 0 9.67901C0 10.4427 0.596954 11.0617 1.33333 11.0617C1.33333 11.0617 8 11.0617 8 11.0617C8.73638 11.0617 9.33333 10.4427 9.33333 9.67901C9.33333 9.67901 9.33333 1.38272 9.33333 1.38272C9.33333 0.619063 8.73638 0 8 0C8 0 6.66667 0 6.66667 0"
|
||||||
|
fill="none" stroke-width="1.5" stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
transform="translate(2.333 2.161)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
components/Icons/Brush.vue
Normal file
7
components/Icons/Brush.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M8.922 9.842q.077.425.078.896c0 1.907-1.387 3.66-3.79 3.894a4.78 4.78 0 0 1-4.208-1.774a2 2 0 0 1-.21-.333c-.231-.461-.292-1-.292-1.528c.312.047.599.045.852 0c.635-.112 1.061-.487 1.148-1C2.73 8.637 3.572 7 5.76 7q.224 0 .435.028l3.417-4.784a2.971 2.971 0 1 1 4.145 4.145zm-.56-1.444l2.819-2.013A2.7 2.7 0 0 0 9.615 4.82L7.626 7.605q.43.324.737.793m4.066-2.904l.457-.326a1.471 1.471 0 1 0-2.052-2.052l-.326.457a4.2 4.2 0 0 1 1.921 1.921M3.98 10.247c.086-.507.272-.962.54-1.264c.225-.254.572-.483 1.242-.483c.517 0 .913.197 1.198.523c.297.34.541.906.541 1.715c0 1.121-.786 2.24-2.435 2.4a3.3 3.3 0 0 1-2.63-.922c.76-.337 1.374-.965 1.544-1.969"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
|
@ -1,32 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg
|
|
||||||
width="24px"
|
|
||||||
height="20px"
|
|
||||||
viewBox="0 0 24 20"
|
|
||||||
version="1.1"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="Ctrl" fill-opacity="1">
|
|
||||||
<path
|
|
||||||
d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z"
|
|
||||||
id="Ctrl"
|
|
||||||
fill="none"
|
|
||||||
stroke="none" />
|
|
||||||
<path
|
|
||||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
|
||||||
id="Rectangle"
|
|
||||||
fill="#FFFFFF"
|
|
||||||
fill-opacity="0.050980393"
|
|
||||||
stroke="none" />
|
|
||||||
<path
|
|
||||||
d="M7.97095 9.00977L12 5L16 9.00977"
|
|
||||||
id="Vector"
|
|
||||||
fill="none"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
stroke="#E5E0D5"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
10
components/Icons/Expand.vue
Normal file
10
components/Icons/Expand.vue
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<g fill="none">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M7.47 1.22a.75.75 0 0 1 1.06 0l1.75 1.75a.75.75 0 1 1-1.06 1.06l-.47-.47v8.88l.47-.47a.75.75 0 1 1 1.06 1.06l-1.75 1.75a.75.75 0 0 1-1.06 0l-1.75-1.75a.75.75 0 1 1 1.06-1.06l.47.47V3.56l-.47.47a.75.75 0 0 1-1.06-1.06zM1.22 7.47a.75.75 0 0 0 0 1.06l1.75 1.75a.75.75 0 1 0 1.06-1.06L2.81 8l1.22-1.22a.75.75 0 0 0-1.06-1.06zm13.56 1.06l-1.75 1.75a.75.75 0 1 1-1.06-1.06L13.19 8l-1.22-1.22a.75.75 0 0 1 1.06-1.06l1.75 1.75a.75.75 0 0 1 0 1.06"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
<path stroke="#E5DFD5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 8h11" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
components/Icons/Gear.vue
Normal file
7
components/Icons/Gear.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M7.199 2H8.8a.2.2 0 0 1 .2.2c0 1.808 1.958 2.939 3.524 2.034a.2.2 0 0 1 .271.073l.802 1.388a.2.2 0 0 1-.073.272c-1.566.904-1.566 3.164 0 4.069a.2.2 0 0 1 .073.271l-.802 1.388a.2.2 0 0 1-.271.073C10.958 10.863 9 11.993 9 13.8a.2.2 0 0 1-.199.2H7.2a.2.2 0 0 1-.2-.2c0-1.808-1.958-2.938-3.524-2.034a.2.2 0 0 1-.272-.073l-.8-1.388a.2.2 0 0 1 .072-.271c1.566-.905 1.566-3.165 0-4.07a.2.2 0 0 1-.073-.27l.801-1.389a.2.2 0 0 1 .272-.072C5.042 5.138 7 4.007 7 2.199c0-.11.089-.199.199-.199M5.5 2.2c0-.94.76-1.7 1.699-1.7H8.8c.94 0 1.7.76 1.7 1.7a.85.85 0 0 0 1.274.735a1.7 1.7 0 0 1 2.32.622l.802 1.388c.469.813.19 1.851-.622 2.32a.85.85 0 0 0 0 1.472a1.7 1.7 0 0 1 .622 2.32l-.802 1.388a1.7 1.7 0 0 1-2.32.622a.85.85 0 0 0-1.274.735c0 .939-.76 1.7-1.699 1.7H7.2a1.7 1.7 0 0 1-1.699-1.7a.85.85 0 0 0-1.274-.735a1.7 1.7 0 0 1-2.32-.622l-.802-1.388a1.7 1.7 0 0 1 .622-2.32a.85.85 0 0 0 0-1.471a1.7 1.7 0 0 1-.622-2.32l.801-1.389a1.7 1.7 0 0 1 2.32-.622A.85.85 0 0 0 5.5 2.2m4 5.8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M11 8a3 3 0 1 1-6 0a3 3 0 0 1 6 0"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
components/Icons/Globe.vue
Normal file
7
components/Icons/Globe.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M9.208 12.346c-.485 1-.953 1.154-1.208 1.154s-.723-.154-1.208-1.154c-.372-.768-.647-1.858-.749-3.187a21 21 0 0 0 3.914 0c-.102 1.329-.377 2.419-.75 3.187m.788-4.699C9.358 7.714 8.69 7.75 8 7.75s-1.358-.036-1.996-.103c.037-1.696.343-3.075.788-3.993C7.277 2.654 7.745 2.5 8 2.5s.723.154 1.208 1.154c.445.918.75 2.297.788 3.993m1.478 1.306c-.085 1.516-.375 2.848-.836 3.874a5.5 5.5 0 0 0 2.843-4.364c-.621.199-1.295.364-2.007.49m1.918-2.043c-.572.204-1.21.379-1.901.514c-.056-1.671-.354-3.14-.853-4.251a5.5 5.5 0 0 1 2.754 3.737m-8.883.514c.056-1.671.354-3.14.853-4.251A5.5 5.5 0 0 0 2.608 6.91c.572.204 1.21.379 1.901.514M2.52 8.463a5.5 5.5 0 0 0 2.843 4.364c-.46-1.026-.75-2.358-.836-3.874a15.5 15.5 0 0 1-2.007-.49M15 8A7 7 0 1 0 1 8a7 7 0 0 0 14 0"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg
|
|
||||||
width="24px"
|
|
||||||
height="20px"
|
|
||||||
viewBox="0 0 24 20"
|
|
||||||
version="1.1"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="K" fill-opacity="1">
|
|
||||||
<path
|
|
||||||
d="M-751 -2016L-751 -2016L-751 -1996L-775 -1996L-775 -2016L-751 -2016Z"
|
|
||||||
id="K"
|
|
||||||
fill="none"
|
|
||||||
stroke="none" />
|
|
||||||
<path
|
|
||||||
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
|
||||||
id="Rectangle"
|
|
||||||
fill="#FFFFFF"
|
|
||||||
fill-opacity="0.050980393"
|
|
||||||
stroke="none" />
|
|
||||||
<g id="K" transform="translate(8 0)">
|
|
||||||
<g transform="translate(0, 2.8945312)" id="K" fill="#E5E0D5">
|
|
||||||
<path
|
|
||||||
d="M1.5 11.5254C1.97461 11.5254 2.25586 11.2383 2.25586 10.7402L2.25586 8.70703L3.13477 7.77539L5.87695 11.127C6.11133 11.4199 6.31641 11.5313 6.61523 11.5313C7.02539 11.5313 7.3418 11.2148 7.3418 10.8047C7.3418 10.6055 7.25391 10.3945 7.04883 10.1426L4.25391 6.76172L6.79688 4.10742C6.97266 3.91992 7.04297 3.76172 7.04297 3.55664C7.04297 3.16992 6.74414 2.86523 6.33984 2.86523C6.09375 2.86523 5.91211 2.95898 5.70703 3.18164L2.30859 6.84961L2.25586 6.84961L2.25586 3.65625C2.25586 3.1582 1.97461 2.87109 1.5 2.87109C1.03125 2.87109 0.744141 3.1582 0.744141 3.65625L0.744141 10.7402C0.744141 11.2383 1.03125 11.5254 1.5 11.5254Z" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
7
components/Icons/Open.vue
Normal file
7
components/Icons/Open.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M10 1.5A.75.75 0 0 0 10 3h1.94L6.97 7.97a.75.75 0 0 0 1.06 1.06L13 4.06V6a.75.75 0 0 0 1.5 0V2.25a.75.75 0 0 0-.75-.75zM7.5 3.25a.75.75 0 0 0-.75-.75H4.5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V9.25a.75.75 0 0 0-1.5 0v2.25a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 11.5v-6A1.5 1.5 0 0 1 4.5 4h2.25a.75.75 0 0 0 .75-.75"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
components/Icons/Pen.vue
Normal file
7
components/Icons/Pen.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M11.423 1A3.577 3.577 0 0 1 15 4.577c0 .27-.108.53-.3.722l-.528.529l-1.971 1.971l-5.059 5.059a3 3 0 0 1-1.533.82l-2.638.528a1 1 0 0 1-1.177-1.177l.528-2.638a3 3 0 0 1 .82-1.533l5.059-5.059l2.5-2.5c.191-.191.451-.299.722-.299m-2.31 4.009l-4.91 4.91a1.5 1.5 0 0 0-.41.766l-.38 1.903l1.902-.38a1.5 1.5 0 0 0 .767-.41l4.91-4.91a2.08 2.08 0 0 0-1.88-1.88m3.098.658a3.6 3.6 0 0 0-1.878-1.879l1.28-1.28c.995.09 1.788.884 1.878 1.88z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
components/Icons/Rotate.vue
Normal file
7
components/Icons/Rotate.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M8 1.5a6.5 6.5 0 1 0 6.445 7.348a.75.75 0 1 0-1.487-.194A5.001 5.001 0 1 1 11.57 4.5h-1.32a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 .75-.75v-3a.75.75 0 0 0-1.5 0v1.06A6.48 6.48 0 0 0 8 1.5"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
components/Icons/T.vue
Normal file
7
components/Icons/T.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M3.279 2.544A.75.75 0 0 1 4 2h8a.75.75 0 0 1 .721.544l.5 1.75a.75.75 0 1 1-1.442.412L11.434 3.5H8.75l-.004 9H9.5a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.746l.004-9H4.566L4.22 4.706a.75.75 0 1 1-1.442-.412z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
components/Icons/Zip.vue
Normal file
7
components/Icons/Zip.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" viewBox="0 0 16 16">
|
||||||
|
<path fill="#E5DFD5" fill-rule="evenodd"
|
||||||
|
d="M11 13.5H5A1.5 1.5 0 0 1 3.5 12V4A1.5 1.5 0 0 1 5 2.5h.5v.75c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v2h-.75a.75.75 0 0 0-.75.75v.5c0 .414.336.75.75.75H7v-2h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 8H7V6h.75a.75.75 0 0 0 .75-.75v-.5A.75.75 0 0 0 7.75 4H7V2.5h.757a1.5 1.5 0 0 1 1.061.44l3.243 3.242a1.5 1.5 0 0 1 .439 1.06V12a1.5 1.5 0 0 1-1.5 1.5m2.121-8.379A3 3 0 0 1 14 7.243V12a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3h2.757a3 3 0 0 1 2.122.879z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
30
components/Keys/Key.vue
Normal file
30
components/Keys/Key.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="key-container"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: 'var(--border)',
|
||||||
|
padding: '0 7px',
|
||||||
|
height: '20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: '4px',
|
||||||
|
minWidth: '22px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:style="{
|
||||||
|
color: '#E5E0D5',
|
||||||
|
fontSize: '12px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ input }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
input: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
12
components/Keys/Shift.vue
Normal file
12
components/Keys/Shift.vue
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 0L17.6 0C19.8402 0 20.9603 0 21.816 0.435974Q22.3804 0.723593 22.8284 1.17157Q23.2764 1.61955 23.564 2.18404C24 3.03969 24 4.15979 24 6.4L24 13.6C24 15.8402 24 16.9603 23.564 17.816Q23.2764 18.3804 22.8284 18.8284Q22.3804 19.2764 21.816 19.564C20.9603 20 19.8402 20 17.6 20L6.4 20C4.15979 20 3.03969 20 2.18404 19.564Q1.61955 19.2764 1.17157 18.8284Q0.723594 18.3804 0.435974 17.816C0 16.9603 0 15.8402 0 13.6L0 6.4C0 4.15979 0 3.03969 0.435974 2.18404Q0.723594 1.61955 1.17157 1.17157Q1.61955 0.723594 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0L12 0Z"
|
||||||
|
fill="#FFFFFF" fill-opacity="0.051" />
|
||||||
|
<path
|
||||||
|
d="M4.9427 0.0799475L0.154716 5.27144Q0.144837 5.28216 0.138427 5.29524Q0.132016 5.30833 0.129608 5.3227Q0.127199 5.33707 0.128993 5.35153Q0.130787 5.36599 0.136635 5.37934Q0.142482 5.39269 0.151896 5.40381Q0.16131 5.41493 0.173507 5.42291Q0.185705 5.43088 0.19967 5.43504Q0.213636 5.4392 0.228208 5.4392L3.06459 5.4392Q3.08448 5.4392 3.10285 5.44681Q3.12123 5.45442 3.13529 5.46848Q3.14935 5.48254 3.15696 5.50092Q3.16457 5.51929 3.16457 5.53917L3.16457 9.90003Q3.16457 9.91991 3.17218 9.93828Q3.17979 9.95666 3.19385 9.97072Q3.20791 9.98478 3.22629 9.99239Q3.24466 10 3.26455 10L6.74521 10Q6.7651 10 6.78347 9.99239Q6.80184 9.98478 6.8159 9.97072Q6.82997 9.95666 6.83758 9.93828Q6.84519 9.91991 6.84519 9.90003L6.84519 5.53917Q6.84519 5.51929 6.8528 5.50091Q6.86041 5.48254 6.87447 5.46848Q6.88853 5.45442 6.9069 5.44681Q6.92527 5.4392 6.94516 5.4392L9.77281 5.4392Q9.78736 5.4392 9.8013 5.43505Q9.81525 5.4309 9.82744 5.42295Q9.83962 5.415 9.84904 5.4039Q9.85845 5.39281 9.86431 5.37949Q9.87017 5.36617 9.87199 5.35173Q9.87382 5.3373 9.87145 5.32294Q9.86908 5.30858 9.86271 5.2955Q9.85635 5.28241 9.84652 5.27168L5.0899 0.0801888Q5.07573 0.064719 5.05653 0.0562526Q5.03734 0.0477862 5.01635 0.0477518Q4.99537 0.0477174 4.97615 0.0561208Q4.95692 0.0645242 4.9427 0.0799475Z"
|
||||||
|
fill="none" stroke-width="1.3" stroke="#E5DFD5" transform="translate(7 5)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
222
composables/useActions.ts
Normal file
222
composables/useActions.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
12
composables/useAppControl.ts
Normal file
12
composables/useAppControl.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { app, window } from "@tauri-apps/api";
|
||||||
|
|
||||||
|
export function useAppControl() {
|
||||||
|
const hideApp = async (): Promise<void> => {
|
||||||
|
await app.hide();
|
||||||
|
await window.getCurrentWindow().hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
hideApp
|
||||||
|
};
|
||||||
|
}
|
|
@ -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<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
export const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
|
|
||||||
const selectedItem = computed<HistoryItem | null>(() => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ export default defineNuxtConfig({
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
compatibilityDate: "2024-07-04",
|
compatibilityDate: "2024-07-04",
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
charset: "utf-8",
|
charset: "utf-8",
|
||||||
|
@ -10,6 +11,7 @@ export default defineNuxtConfig({
|
||||||
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0",
|
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
|
|
166
pages/index.vue
166
pages/index.vue
|
@ -10,7 +10,7 @@
|
||||||
<Result v-for="(item, index) in group.items" :key="item.id" :item="item"
|
<Result v-for="(item, index) in group.items" :key="item.id" :item="item"
|
||||||
:selected="isSelected(groupIndex, index)" :image-url="imageUrls[item.id]"
|
:selected="isSelected(groupIndex, index)" :image-url="imageUrls[item.id]"
|
||||||
:dimensions="imageDimensions[item.id]" @select="selectItem(groupIndex, index)" @image-error="onImageError"
|
:dimensions="imageDimensions[item.id]" @select="selectItem(groupIndex, index)" @image-error="onImageError"
|
||||||
@setRef="(el) => (selectedElement = el)" />
|
@setRef="(el: HTMLElement | null) => (selectedElement = el)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
|
@ -49,9 +49,12 @@
|
||||||
onClick: pasteSelectedItem,
|
onClick: pasteSelectedItem,
|
||||||
}" :secondary-action="{
|
}" :secondary-action="{
|
||||||
text: 'Actions',
|
text: 'Actions',
|
||||||
icon: IconsK,
|
icon: IconsKey,
|
||||||
|
input: 'K',
|
||||||
showModifier: true,
|
showModifier: true,
|
||||||
|
onClick: toggleActionsMenu,
|
||||||
}" />
|
}" />
|
||||||
|
<ActionsMenu :selected-item="selectedItem" :is-visible="isActionsMenuVisible" @close="closeActionsMenu" @toggle="toggleActionsMenu" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -59,7 +62,6 @@
|
||||||
import { ref, computed, onMounted, watch, nextTick, shallowRef } from "vue";
|
import { ref, computed, onMounted, watch, nextTick, shallowRef } from "vue";
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
import "overlayscrollbars/overlayscrollbars.css";
|
import "overlayscrollbars/overlayscrollbars.css";
|
||||||
import { app, window } from "@tauri-apps/api";
|
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useNuxtApp } from "#app";
|
import { useNuxtApp } from "#app";
|
||||||
|
@ -73,22 +75,18 @@ import type {
|
||||||
InfoColor,
|
InfoColor,
|
||||||
InfoCode,
|
InfoCode,
|
||||||
} from "~/types/types";
|
} from "~/types/types";
|
||||||
import { Key, keyboard } from "wrdu-keyboard";
|
import IconsEnter from "~/components/Keys/Enter.vue";
|
||||||
import {
|
import IconsKey from "~/components/Keys/Key.vue";
|
||||||
selectedGroupIndex,
|
import ActionsMenu from "~/components/ActionsMenu.vue";
|
||||||
selectedItemIndex,
|
import { useAppControl } from "~/composables/useAppControl";
|
||||||
selectedElement,
|
|
||||||
useSelectedResult,
|
|
||||||
} from "~/lib/selectedResult";
|
|
||||||
import IconsEnter from "~/components/Icons/Enter.vue";
|
|
||||||
import IconsK from "~/components/Icons/K.vue";
|
|
||||||
|
|
||||||
interface GroupedHistory {
|
interface GroupedHistory {
|
||||||
label: string;
|
label: string;
|
||||||
items: HistoryItem[];
|
items: HistoryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { $history } = useNuxtApp();
|
const { $history, $keyboard, $selectedResult } = useNuxtApp();
|
||||||
|
const { selectedGroupIndex, selectedItemIndex, selectedElement, useSelectedResult } = $selectedResult;
|
||||||
|
|
||||||
const CHUNK_SIZE = 50;
|
const CHUNK_SIZE = 50;
|
||||||
const SCROLL_THRESHOLD = 100;
|
const SCROLL_THRESHOLD = 100;
|
||||||
|
@ -113,9 +111,36 @@ const imageLoadError = ref<boolean>(false);
|
||||||
const imageLoading = ref<boolean>(false);
|
const imageLoading = ref<boolean>(false);
|
||||||
const pageTitle = ref<string>("");
|
const pageTitle = ref<string>("");
|
||||||
const pageOgImage = ref<string>("");
|
const pageOgImage = ref<string>("");
|
||||||
|
const isActionsMenuVisible = ref<boolean>(false);
|
||||||
|
|
||||||
const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null);
|
const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null);
|
||||||
|
|
||||||
|
const toggleActionsMenu = () => {
|
||||||
|
isActionsMenuVisible.value = !isActionsMenuVisible.value;
|
||||||
|
|
||||||
|
if (isActionsMenuVisible.value) {
|
||||||
|
$keyboard.disableContext('main');
|
||||||
|
$keyboard.enableContext('actionsMenu');
|
||||||
|
} else {
|
||||||
|
$keyboard.disableContext('actionsMenu');
|
||||||
|
$keyboard.enableContext('main');
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (isActionsMenuVisible.value) {
|
||||||
|
document.getElementById('actions-menu')?.focus();
|
||||||
|
} else {
|
||||||
|
focusSearchInput();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeActionsMenu = () => {
|
||||||
|
isActionsMenuVisible.value = false;
|
||||||
|
$keyboard.disableContext('actionsMenu');
|
||||||
|
$keyboard.enableContext('main');
|
||||||
|
};
|
||||||
|
|
||||||
const isSameDay = (date1: Date, date2: Date): boolean => {
|
const isSameDay = (date1: Date, date2: Date): boolean => {
|
||||||
return (
|
return (
|
||||||
date1.getFullYear() === date2.getFullYear() &&
|
date1.getFullYear() === date2.getFullYear() &&
|
||||||
|
@ -417,13 +442,13 @@ const getYoutubeThumbnail = (url: string): string => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
||||||
const results = await $history.loadHistoryChunk(0, CHUNK_SIZE);
|
offset = 0;
|
||||||
if (results.length > 0) {
|
history.value = [];
|
||||||
const existingIds = new Set(history.value.map((item) => item.id));
|
|
||||||
const uniqueNewItems = results.filter((item) => !existingIds.has(item.id));
|
|
||||||
|
|
||||||
const processedNewItems = await Promise.all(
|
const results = await $history.loadHistoryChunk(offset, CHUNK_SIZE);
|
||||||
uniqueNewItems.map(async (item) => {
|
if (results.length > 0) {
|
||||||
|
const processedItems = await Promise.all(
|
||||||
|
results.map(async (item) => {
|
||||||
const historyItem = new HistoryItem(
|
const historyItem = new HistoryItem(
|
||||||
item.source,
|
item.source,
|
||||||
item.content_type,
|
item.content_type,
|
||||||
|
@ -471,7 +496,8 @@ const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
history.value = [...processedNewItems, ...history.value];
|
history.value = processedItems;
|
||||||
|
offset = results.length;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
resetScroll &&
|
resetScroll &&
|
||||||
|
@ -527,76 +553,40 @@ const setupEventListeners = async (): Promise<void> => {
|
||||||
}
|
}
|
||||||
focusSearchInput();
|
focusSearchInput();
|
||||||
|
|
||||||
keyboard.clear();
|
$keyboard.disableContext('actionsMenu');
|
||||||
keyboard.prevent.down([Key.DownArrow], () => {
|
$keyboard.disableContext('settings');
|
||||||
selectNext();
|
$keyboard.enableContext('main');
|
||||||
});
|
if (isActionsMenuVisible.value) {
|
||||||
|
$keyboard.enableContext('actionsMenu');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await listen("tauri://blur", () => {
|
await listen("tauri://blur", () => {
|
||||||
searchInput.value?.blur();
|
searchInput.value?.blur();
|
||||||
keyboard.clear();
|
$keyboard.disableContext('main');
|
||||||
|
$keyboard.disableContext('actionsMenu');
|
||||||
});
|
});
|
||||||
|
|
||||||
keyboard.prevent.down([Key.DownArrow], () => {
|
$keyboard.setupAppShortcuts({
|
||||||
selectNext();
|
onNavigateDown: selectNext,
|
||||||
});
|
onNavigateUp: selectPrevious,
|
||||||
|
onSelect: pasteSelectedItem,
|
||||||
keyboard.prevent.down([Key.UpArrow], () => {
|
onEscape: () => {
|
||||||
selectPrevious();
|
if (isActionsMenuVisible.value) {
|
||||||
});
|
closeActionsMenu();
|
||||||
|
} else {
|
||||||
keyboard.prevent.down([Key.Enter], () => {
|
|
||||||
pasteSelectedItem();
|
|
||||||
});
|
|
||||||
|
|
||||||
keyboard.prevent.down([Key.Escape], () => {
|
|
||||||
hideApp();
|
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;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onToggleActions: toggleActionsMenu,
|
||||||
|
contextName: 'main',
|
||||||
|
priority: $keyboard.PRIORITY.HIGH
|
||||||
|
});
|
||||||
|
$keyboard.disableContext('settings');
|
||||||
|
$keyboard.enableContext('main');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideApp = async (): Promise<void> => {
|
const { hideApp } = useAppControl();
|
||||||
await app.hide();
|
|
||||||
await window.getCurrentWindow().hide();
|
|
||||||
};
|
|
||||||
|
|
||||||
const focusSearchInput = (): void => {
|
const focusSearchInput = (): void => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
@ -628,6 +618,24 @@ onMounted(async () => {
|
||||||
?.viewport?.addEventListener("scroll", handleScroll);
|
?.viewport?.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
await setupEventListeners();
|
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) {
|
} catch (error) {
|
||||||
console.error("Error during onMounted:", error);
|
console.error("Error during onMounted:", error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
<div
|
<div
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
|
@keydown="onKeyDown"
|
||||||
class="keybind-input"
|
class="keybind-input"
|
||||||
ref="keybindInput"
|
ref="keybindInput"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@ -79,20 +80,21 @@ import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { KeyValues, KeyLabels } from "../types/keys";
|
import { KeyValues, KeyLabels } from "../types/keys";
|
||||||
import { disable, enable } from "@tauri-apps/plugin-autostart";
|
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 BottomBar from "../components/BottomBar.vue";
|
||||||
import IconsEnter from "~/components/Icons/Enter.vue";
|
import IconsEnter from "~/components/Keys/Enter.vue";
|
||||||
|
|
||||||
const activeModifiers = reactive<Set<KeyValues>>(new Set());
|
const activeModifiers = reactive<Set<KeyValues>>(new Set());
|
||||||
const isKeybindInputFocused = ref(false);
|
const isKeybindInputFocused = ref(false);
|
||||||
const keybind = ref<KeyValues[]>([]);
|
const keybind = ref<KeyValues[]>([]);
|
||||||
const keybindInput = ref<HTMLElement | null>(null);
|
const keybindInput = ref<HTMLElement | null>(null);
|
||||||
const lastBlurTime = ref(0);
|
const lastBlurTime = ref(0);
|
||||||
|
const blurredByEscape = ref(false);
|
||||||
const os = ref("");
|
const os = ref("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const showEmptyKeybindError = ref(false);
|
const showEmptyKeybindError = ref(false);
|
||||||
const autostart = ref(false);
|
const autostart = ref(false);
|
||||||
const { $settings } = useNuxtApp();
|
const { $settings, $keyboard } = useNuxtApp();
|
||||||
|
|
||||||
const modifierKeySet = new Set([
|
const modifierKeySet = new Set([
|
||||||
KeyValues.AltLeft,
|
KeyValues.AltLeft,
|
||||||
|
@ -127,6 +129,7 @@ const onBlur = () => {
|
||||||
|
|
||||||
const onFocus = () => {
|
const onFocus = () => {
|
||||||
isKeybindInputFocused.value = true;
|
isKeybindInputFocused.value = true;
|
||||||
|
blurredByEscape.value = false;
|
||||||
activeModifiers.clear();
|
activeModifiers.clear();
|
||||||
keybind.value = [];
|
keybind.value = [];
|
||||||
showEmptyKeybindError.value = false;
|
showEmptyKeybindError.value = false;
|
||||||
|
@ -137,7 +140,10 @@ const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|
||||||
if (key === KeyValues.Escape) {
|
if (key === KeyValues.Escape) {
|
||||||
if (keybindInput.value) {
|
if (keybindInput.value) {
|
||||||
|
blurredByEscape.value = true;
|
||||||
keybindInput.value.blur();
|
keybindInput.value.blur();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -174,56 +180,64 @@ const toggleAutostart = async () => {
|
||||||
os.value = platform();
|
os.value = platform();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
keyboard.prevent.down([Key.All], (event: KeyboardEvent) => {
|
$keyboard.setupKeybindCapture({
|
||||||
|
onCapture: (key: string) => {
|
||||||
if (isKeybindInputFocused.value) {
|
if (isKeybindInputFocused.value) {
|
||||||
onKeyDown(event);
|
const keyValue = key as KeyValues;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
keyboard.prevent.down([Key.Escape], () => {
|
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) {
|
if (isKeybindInputFocused.value) {
|
||||||
keybindInput.value?.blur();
|
keybindInput.value?.blur();
|
||||||
} else {
|
} else {
|
||||||
router.push("/");
|
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 (os.value === "macos") {
|
||||||
if (!isKeybindInputFocused.value) {
|
$keyboard.on("settings", [$keyboard.Key.LeftMeta, $keyboard.Key.Enter], () => {
|
||||||
saveKeybind();
|
saveKeybind();
|
||||||
}
|
}, { priority: $keyboard.PRIORITY.HIGH });
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "linux":
|
$keyboard.on("settings", [$keyboard.Key.RightMeta, $keyboard.Key.Enter], () => {
|
||||||
case "windows":
|
|
||||||
keyboard.prevent.down([Key.LeftControl, Key.Enter], () => {
|
|
||||||
if (!isKeybindInputFocused.value) {
|
|
||||||
saveKeybind();
|
saveKeybind();
|
||||||
}
|
}, { priority: $keyboard.PRIORITY.HIGH });
|
||||||
});
|
} else {
|
||||||
|
$keyboard.on("settings", [$keyboard.Key.LeftControl, $keyboard.Key.Enter], () => {
|
||||||
|
saveKeybind();
|
||||||
|
}, { priority: $keyboard.PRIORITY.HIGH });
|
||||||
|
|
||||||
keyboard.prevent.down([Key.RightControl, Key.Enter], () => {
|
$keyboard.on("settings", [$keyboard.Key.RightControl, $keyboard.Key.Enter], () => {
|
||||||
if (!isKeybindInputFocused.value) {
|
|
||||||
saveKeybind();
|
saveKeybind();
|
||||||
|
}, { priority: $keyboard.PRIORITY.HIGH });
|
||||||
}
|
}
|
||||||
});
|
|
||||||
break;
|
$keyboard.on("settings", [$keyboard.Key.Escape], () => {
|
||||||
|
if (!isKeybindInputFocused.value && !blurredByEscape.value) {
|
||||||
|
router.push("/");
|
||||||
}
|
}
|
||||||
|
blurredByEscape.value = false;
|
||||||
|
}, { priority: $keyboard.PRIORITY.HIGH });
|
||||||
|
|
||||||
|
$keyboard.disableContext("main");
|
||||||
|
$keyboard.enableContext("settings");
|
||||||
|
|
||||||
autostart.value = (await $settings.getSetting("autostart")) === "true";
|
autostart.value = (await $settings.getSetting("autostart")) === "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
keyboard.clear();
|
$keyboard.disableContext("settings");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
281
plugins/keyboard.ts
Normal file
281
plugins/keyboard.ts
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
import { Key, keyboard } from "wrdu-keyboard";
|
||||||
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
|
||||||
|
type KeyboardHandler = (event: KeyboardEvent) => void;
|
||||||
|
|
||||||
|
const activeContexts = new Set<string>();
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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 = Math.max(priority, 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<
|
||||||
|
string,
|
||||||
|
Array<(typeof allHandlers)[0]>
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
allHandlers.forEach((handler) => {
|
||||||
|
const keyCombo = handler.keys.sort().join("+");
|
||||||
|
if (!handlersByKeyCombination[keyCombo]) {
|
||||||
|
handlersByKeyCombination[keyCombo] = [];
|
||||||
|
}
|
||||||
|
handlersByKeyCombination[keyCombo].push(handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
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) &&
|
||||||
|
activeContexts.has(handler.contextName)
|
||||||
|
) {
|
||||||
|
handler.callback(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (handler.prevent) {
|
||||||
|
keyboard.prevent.down(handler.keys, wrappedCallback);
|
||||||
|
} else {
|
||||||
|
keyboard.down(handler.keys, wrappedCallback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
nuxtApp.hook("page:finish", () => {
|
||||||
|
initKeyboardHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
keyboard: {
|
||||||
|
...useKeyboard,
|
||||||
|
Key,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
68
plugins/selectedResult.ts
Normal file
68
plugins/selectedResult.ts
Normal file
|
@ -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<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const useSelectedResult = (groupedHistory: Ref<GroupedHistory[]>) => {
|
||||||
|
const selectedItem = computed<HistoryItem | undefined>(() => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -230,7 +230,6 @@ pub fn setup(app: &AppHandle) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = app_handle.emit("clipboard-content-updated", ());
|
|
||||||
let _ = app_handle.track_event(
|
let _ = app_handle.track_event(
|
||||||
"clipboard_copied",
|
"clipboard_copied",
|
||||||
Some(
|
Some(
|
||||||
|
|
|
@ -5,6 +5,7 @@ use rand::distr::Alphanumeric;
|
||||||
use sqlx::{ Row, SqlitePool };
|
use sqlx::{ Row, SqlitePool };
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tauri_plugin_aptabase::EventTracker;
|
use tauri_plugin_aptabase::EventTracker;
|
||||||
|
use tauri::Emitter;
|
||||||
|
|
||||||
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let id: String = rng()
|
let id: String = rng()
|
||||||
|
@ -71,8 +72,12 @@ pub async fn add_history_item(
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
sqlx
|
sqlx
|
||||||
::query(
|
::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)
|
||||||
.bind(&content_type)
|
.bind(&content_type)
|
||||||
.execute(&*pool).await
|
.execute(&*pool).await
|
||||||
|
@ -103,6 +108,8 @@ pub async fn add_history_item(
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,6 +198,7 @@ pub async fn delete_history_item(
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let _ = app_handle.track_event("history_item_deleted", None);
|
let _ = app_handle.track_event("history_item_deleted", None);
|
||||||
|
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -206,6 +214,7 @@ pub async fn clear_history(
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let _ = app_handle.track_event("history_cleared", None);
|
let _ = app_handle.track_event("history_cleared", None);
|
||||||
|
let _ = app_handle.emit("clipboard-content-updated", ());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,8 @@ fn main() {
|
||||||
db::history::read_image,
|
db::history::read_image,
|
||||||
db::settings::get_setting,
|
db::settings::get_setting,
|
||||||
db::settings::save_setting,
|
db::settings::save_setting,
|
||||||
utils::commands::fetch_page_meta
|
utils::commands::fetch_page_meta,
|
||||||
|
utils::commands::get_app_info
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|
|
@ -36,12 +36,20 @@ pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
pub fn get_app_info() -> (String, Option<String>) {
|
pub fn get_app_info() -> (String, Option<String>) {
|
||||||
println!("Getting app info");
|
println!("Getting app info");
|
||||||
let mut ctx = AppInfoContext::new(vec![]);
|
let mut ctx = AppInfoContext::new(vec![]);
|
||||||
println!("Created AppInfoContext");
|
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");
|
println!("Refreshed apps");
|
||||||
|
|
||||||
|
let result = std::panic::catch_unwind(|| {
|
||||||
match ctx.get_frontmost_application() {
|
match ctx.get_frontmost_application() {
|
||||||
Ok(window) => {
|
Ok(window) => {
|
||||||
println!("Found frontmost application: {}", window.name);
|
println!("Found frontmost application: {}", window.name);
|
||||||
|
@ -49,12 +57,13 @@ pub fn get_app_info() -> (String, Option<String>) {
|
||||||
let icon = window
|
let icon = window
|
||||||
.load_icon()
|
.load_icon()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|i| {
|
.and_then(|i| {
|
||||||
println!("Loading icon for {}", name);
|
println!("Loading icon for {}", name);
|
||||||
let png = i.to_png().unwrap();
|
i.to_png().ok().map(|png| {
|
||||||
let encoded = STANDARD.encode(png.get_bytes());
|
let encoded = STANDARD.encode(png.get_bytes());
|
||||||
println!("Icon encoded successfully");
|
println!("Icon encoded successfully");
|
||||||
encoded
|
encoded
|
||||||
|
})
|
||||||
});
|
});
|
||||||
println!("Returning app info: {} with icon: {}", name, icon.is_some());
|
println!("Returning app info: {} with icon: {}", name, icon.is_some());
|
||||||
(name, icon)
|
(name, icon)
|
||||||
|
@ -64,6 +73,15 @@ pub fn get_app_info() -> (String, Option<String>) {
|
||||||
("System".to_string(), None)
|
("System".to_string(), None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(_) => {
|
||||||
|
println!("Panic occurred while getting app info");
|
||||||
|
("System".to_string(), None)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
|
|
@ -138,7 +138,6 @@ main {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid $divider;
|
border-bottom: 1px solid $divider;
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
@ -146,7 +145,7 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-top: 22px;
|
padding-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue