chore(deps): update keyboard dependency and refactor keyboard handling

This commit is contained in:
0PandaDEV 2025-05-27 14:44:14 +02:00
commit aa928f7094
No known key found for this signature in database
GPG key ID: F4C8370B55232C8E
5 changed files with 331 additions and 501 deletions

View file

@ -1,41 +1,68 @@
<template>
<main>
<TopBar ref="topBar" @search="searchHistory" @searchStarted="searchStarted" />
<TopBar
ref="topBar"
@search="searchHistory"
@searchStarted="searchStarted" />
<div class="container">
<OverlayScrollbarsComponent class="results" ref="resultsContainer"
<OverlayScrollbarsComponent
class="results"
ref="resultsContainer"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<div v-for="(group, groupIndex) in groupedHistory" :key="groupIndex" class="group">
<div
v-for="(group, groupIndex) in groupedHistory"
:key="groupIndex"
class="group">
<div class="time-separator">{{ group.label }}</div>
<div class="results-group">
<Result v-for="(item, index) in group.items" :key="item.id" :item="item"
:selected="isSelected(groupIndex, index)" :image-url="imageUrls[item.id]"
:dimensions="imageDimensions[item.id]" @select="selectItem(groupIndex, index)" @image-error="onImageError"
<Result
v-for="(item, index) in group.items"
:key="item.id"
:item="item"
:selected="isSelected(groupIndex, index)"
:image-url="imageUrls[item.id]"
:dimensions="imageDimensions[item.id]"
@select="selectItem(groupIndex, index)"
@image-error="onImageError"
@setRef="(el: HTMLElement | null) => (selectedElement = el)" />
</div>
</div>
</OverlayScrollbarsComponent>
<div class="right">
<div class="content" v-if="selectedItem?.content_type === ContentType.Image">
<div
class="content"
v-if="selectedItem?.content_type === ContentType.Image">
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
</div>
<div v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)" class="content">
<img class="image" :src="getYoutubeThumbnail(selectedItem.content)" alt="YouTube Thumbnail" />
<div
v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)"
class="content">
<img
class="image"
:src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail" />
</div>
<div class="content" v-else-if="
selectedItem?.content_type === ContentType.Link && pageOgImage
">
<div
class="content"
v-else-if="
selectedItem?.content_type === ContentType.Link && pageOgImage
">
<img :src="pageOgImage" alt="Image" class="image" />
</div>
<OverlayScrollbarsComponent v-else class="content">
<span class="content-text">{{ selectedItem?.content || "" }}</span>
</OverlayScrollbarsComponent>
<OverlayScrollbarsComponent class="information" :options="{ scrollbars: { autoHide: 'scroll' } }">
<OverlayScrollbarsComponent
class="information"
:options="{ scrollbars: { autoHide: 'scroll' } }">
<div class="title">Information</div>
<div class="info-content" v-if="selectedItem && getInfo">
<div class="info-row" v-for="(row, index) in infoRows" :key="index">
<p class="label">{{ row.label }}</p>
<span :class="{ 'url-truncate': row.isUrl }" :data-text="row.value">
<img v-if="row.icon" :src="row.icon" :alt="String(row.value)">
<span
:class="{ 'url-truncate': row.isUrl }"
:data-text="row.value">
<img v-if="row.icon" :src="row.icon" :alt="String(row.value)" />
{{ row.value }}
</span>
</div>
@ -43,26 +70,39 @@
</OverlayScrollbarsComponent>
</div>
</div>
<BottomBar :primary-action="{
text: 'Paste',
icon: IconsEnter,
onClick: pasteSelectedItem,
}" :secondary-action="{
text: 'Actions',
icon: IconsKey,
input: 'K',
showModifier: true,
onClick: toggleActionsMenu,
}" />
<ActionsMenu :selected-item="selectedItem" :is-visible="isActionsMenuVisible" @close="closeActionsMenu" @toggle="toggleActionsMenu" />
<BottomBar
:primary-action="{
text: 'Paste',
icon: IconsEnter,
onClick: pasteSelectedItem,
}"
:secondary-action="{
text: 'Actions',
icon: IconsKey,
input: 'K',
showModifier: true,
onClick: toggleActionsMenu,
}" />
<ActionsMenu
:selected-item="selectedItem"
:is-visible="isActionsMenuVisible"
@close="closeActionsMenu"
@toggle="toggleActionsMenu" />
</main>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch, nextTick, shallowRef } from "vue";
import {
ref,
computed,
onMounted,
onUnmounted,
watch,
nextTick,
shallowRef,
} from "vue";
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import "overlayscrollbars/overlayscrollbars.css";
import { platform } from "@tauri-apps/plugin-os";
import { listen } from "@tauri-apps/api/event";
import { useNuxtApp } from "#app";
import { invoke } from "@tauri-apps/api/core";
@ -86,7 +126,14 @@ interface GroupedHistory {
}
const { $history, $keyboard, $selectedResult } = useNuxtApp();
const { selectedGroupIndex, selectedItemIndex, selectedElement, useSelectedResult } = $selectedResult;
const {
selectedGroupIndex,
selectedItemIndex,
selectedElement,
useSelectedResult,
} = $selectedResult;
const listeners: Array<() => void> = [];
const CHUNK_SIZE = 50;
const SCROLL_THRESHOLD = 100;
@ -102,7 +149,6 @@ const resultsContainer = shallowRef<InstanceType<
> | null>(null);
const searchQuery = ref("");
const searchInput = ref<HTMLInputElement | null>(null);
const os = ref<string>("");
const imageUrls = shallowRef<Record<string, string>>({});
const imageDimensions = shallowRef<Record<string, string>>({});
const imageSizes = shallowRef<Record<string, string>>({});
@ -117,18 +163,9 @@ 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();
document.getElementById("actions-menu")?.focus();
} else {
focusSearchInput();
}
@ -137,8 +174,7 @@ const toggleActionsMenu = () => {
const closeActionsMenu = () => {
isActionsMenuVisible.value = false;
$keyboard.disableContext('actionsMenu');
$keyboard.enableContext('main');
focusSearchInput();
};
const isSameDay = (date1: Date, date2: Date): boolean => {
@ -155,7 +191,7 @@ const getWeekNumber = (date: Date): number => {
((date.getTime() - firstDayOfYear.getTime()) / 86400000 +
firstDayOfYear.getDay() +
1) /
7
7
);
};
@ -176,8 +212,8 @@ const groupedHistory = computed<GroupedHistory[]>(() => {
const filteredItems = searchQuery.value
? history.value.filter((item) =>
item.content.toLowerCase().includes(searchQuery.value.toLowerCase())
)
item.content.toLowerCase().includes(searchQuery.value.toLowerCase())
)
: history.value;
const yesterday = new Date(today.getTime() - 86400000);
@ -243,16 +279,16 @@ const loadHistoryChunk = async (): Promise<void> => {
img.src = `data:image/png;base64,${base64}`;
imageUrls.value[historyItem.id] = img.src;
await new Promise<void>((resolve) => {
await new Promise<void>((resolveProm) => {
img.onload = () => {
imageDimensions.value[
historyItem.id
] = `${img.width}x${img.height}`;
resolve();
resolveProm();
};
img.onerror = () => {
imageDimensions.value[historyItem.id] = "Error";
resolve();
resolveProm();
};
});
} catch (error) {
@ -366,7 +402,11 @@ const processSearchQueue = async () => {
{ id: item.id, timestamp: new Date(item.timestamp) }
)
);
if (groupedHistory.value.length > 0) {
handleSelection(0, 0, true);
} else {
selectItem(-1, -1);
}
} catch (error) {
console.error("Search error:", error);
} finally {
@ -379,13 +419,13 @@ const processSearchQueue = async () => {
const searchHistory = async (query: string): Promise<void> => {
searchQuery.value = query;
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
searchQueue.push(query);
if (!isProcessingSearch) {
processSearchQueue();
@ -394,10 +434,14 @@ const searchHistory = async (query: string): Promise<void> => {
watch(
() => groupedHistory.value,
(newGroupedHistory) => {
if (newGroupedHistory.length > 0) {
(newGroupedHistory, oldGroupedHistory) => {
if (
newGroupedHistory.length > 0 &&
oldGroupedHistory &&
oldGroupedHistory.length === 0
) {
handleSelection(0, 0, true);
} else {
} else if (newGroupedHistory.length === 0) {
selectItem(-1, -1);
}
},
@ -444,7 +488,7 @@ const getYoutubeThumbnail = (url: string): string => {
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
offset = 0;
history.value = [];
const results = await $history.loadHistoryChunk(offset, CHUNK_SIZE);
if (results.length > 0) {
const processedItems = await Promise.all(
@ -474,16 +518,16 @@ const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
img.src = `data:image/png;base64,${base64}`;
imageUrls.value[historyItem.id] = img.src;
await new Promise<void>((resolve) => {
await new Promise<void>((resolveProm) => {
img.onload = () => {
imageDimensions.value[
historyItem.id
] = `${img.width}x${img.height}`;
resolve();
resolveProm();
};
img.onerror = () => {
imageDimensions.value[historyItem.id] = "Error";
resolve();
resolveProm();
};
});
} catch (error) {
@ -520,72 +564,6 @@ const handleSelection = (
if (shouldScroll) scrollToSelectedItem();
};
const setupEventListeners = async (): Promise<void> => {
await listen("clipboard-content-updated", async () => {
lastUpdateTime.value = Date.now();
await updateHistory(true);
if (groupedHistory.value[0]?.items.length > 0) {
handleSelection(0, 0, false);
}
});
await listen("tauri://focus", async () => {
const currentTime = Date.now();
if (currentTime - lastUpdateTime.value > 0) {
const previousState = {
groupIndex: selectedGroupIndex.value,
itemIndex: selectedItemIndex.value,
scroll:
resultsContainer.value?.osInstance()?.elements().viewport
?.scrollTop || 0,
};
await updateHistory();
lastUpdateTime.value = currentTime;
handleSelection(previousState.groupIndex, previousState.itemIndex, false);
if (resultsContainer.value?.osInstance()?.elements().viewport?.scrollTo) {
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
top: previousState.scroll,
behavior: "instant",
});
}
}
focusSearchInput();
$keyboard.disableContext('actionsMenu');
$keyboard.disableContext('settings');
$keyboard.enableContext('main');
if (isActionsMenuVisible.value) {
$keyboard.enableContext('actionsMenu');
}
});
await listen("tauri://blur", () => {
searchInput.value?.blur();
$keyboard.disableContext('main');
$keyboard.disableContext('actionsMenu');
});
$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');
};
const { hideApp } = useAppControl();
const focusSearchInput = (): void => {
@ -609,38 +587,147 @@ watch(
onMounted(async () => {
try {
os.value = platform();
await loadHistoryChunk();
if (groupedHistory.value.length > 0 && !selectedItem.value) {
handleSelection(0, 0, true);
}
resultsContainer.value
?.osInstance()
?.elements()
?.viewport?.addEventListener("scroll", handleScroll);
await setupEventListeners();
$keyboard.setupAppShortcuts({
onNavigateDown: selectNext,
onNavigateUp: selectPrevious,
onSelect: pasteSelectedItem,
onEscape: () => {
if (isActionsMenuVisible.value) {
closeActionsMenu();
} else {
hideApp();
listeners.push(
await listen("clipboard-content-updated", async () => {
lastUpdateTime.value = Date.now();
await updateHistory(true);
if (groupedHistory.value[0]?.items.length > 0) {
handleSelection(0, 0, false);
}
},
onToggleActions: toggleActionsMenu,
contextName: 'main',
priority: $keyboard.PRIORITY.HIGH
});
$keyboard.disableContext('settings');
$keyboard.enableContext('main');
})
);
listeners.push(
await listen("tauri://focus", async () => {
console.log("Tauri window focused");
// Attempt to re-initialize keyboard listeners
if ($keyboard && typeof $keyboard.init === "function") {
console.log("Re-initializing keyboard via $keyboard.init()");
$keyboard.init();
} else {
console.warn("$keyboard.init is not available");
}
const currentTime = Date.now();
if (currentTime - lastUpdateTime.value > 0) {
const previousState = {
groupIndex: selectedGroupIndex.value,
itemIndex: selectedItemIndex.value,
scroll:
resultsContainer.value?.osInstance()?.elements().viewport
?.scrollTop || 0,
};
await updateHistory();
lastUpdateTime.value = currentTime;
handleSelection(
previousState.groupIndex,
previousState.itemIndex,
false
);
if (
resultsContainer.value?.osInstance()?.elements().viewport?.scrollTo
) {
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
top: previousState.scroll,
behavior: "instant",
});
}
}
focusSearchInput();
})
);
listeners.push(
await listen("tauri://blur", () => {
searchInput.value?.blur();
})
);
listeners.push(
$keyboard.listen(
[$keyboard.Key.DownArrow],
() => {
console.log(
"Down Arrow pressed. Active element:",
document.activeElement
);
selectNext();
},
{ prevent: true }
)
);
listeners.push(
$keyboard.listen(
[$keyboard.Key.UpArrow],
() => {
console.log(
"Up Arrow pressed. Active element:",
document.activeElement
);
selectPrevious();
},
{ prevent: true }
)
);
listeners.push(
$keyboard.listen([$keyboard.Key.Enter], pasteSelectedItem, {
prevent: true,
})
);
listeners.push(
$keyboard.listen(
[$keyboard.Key.Escape],
() => {
if (isActionsMenuVisible.value) {
closeActionsMenu();
} else {
hideApp();
}
},
{ prevent: true }
)
);
const metaOrCtrlKey =
$keyboard.currentOS === "macos"
? $keyboard.Key.Meta
: $keyboard.Key.Control;
listeners.push(
$keyboard.listen([metaOrCtrlKey, $keyboard.Key.K], toggleActionsMenu, {
prevent: true,
ignoreIfEditable: true,
})
);
} catch (error) {
console.error("Error during onMounted:", error);
}
});
onUnmounted(() => {
listeners.forEach((unlisten) => {
if (typeof unlisten === "function") {
unlisten();
}
});
listeners.length = 0;
const viewport = resultsContainer.value?.osInstance()?.elements()?.viewport;
if (viewport) {
viewport.removeEventListener("scroll", handleScroll);
}
});
const getFormattedDate = computed(() => {
if (!selectedItem.value?.timestamp) return "";
return new Intl.DateTimeFormat("en-US", {
@ -787,7 +874,9 @@ const infoRows = computed(() => {
label: "Source",
value: getInfo.value.source,
isUrl: false,
icon: selectedItem.value?.source_icon ? `data:image/png;base64,${selectedItem.value.source_icon}` : undefined
icon: selectedItem.value?.source_icon
? `data:image/png;base64,${selectedItem.value.source_icon}`
: undefined,
},
{
label: "Content Type",
@ -800,7 +889,12 @@ const infoRows = computed(() => {
const typeSpecificRows: Record<
ContentType,
Array<{ label: string; value: string | number; isUrl?: boolean; icon?: string }>
Array<{
label: string;
value: string | number;
isUrl?: boolean;
icon?: string;
}>
> = {
[ContentType.Text]: [
{ label: "Characters", value: (getInfo.value as InfoText).characters },
@ -818,7 +912,7 @@ const infoRows = computed(() => {
],
[ContentType.Link]: [
...((getInfo.value as InfoLink).title &&
(getInfo.value as InfoLink).title !== "Loading..."
(getInfo.value as InfoLink).title !== "Loading..."
? [{ label: "Title", value: (getInfo.value as InfoLink).title || "" }]
: []),
{ label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true },