mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-06-16 20:07:33 +02:00
chore(deps): update keyboard dependency and refactor keyboard handling
This commit is contained in:
commit
aa928f7094
5 changed files with 331 additions and 501 deletions
|
@ -20,7 +20,7 @@
|
||||||
"sass-embedded": "1.85.1",
|
"sass-embedded": "1.85.1",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
"wrdu-keyboard": "github:0PandaDEV/keyboard"
|
"@waradu/keyboard": "4.2.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"chokidar": "^3.6.0"
|
"chokidar": "^3.6.0"
|
||||||
|
|
390
pages/index.vue
390
pages/index.vue
|
@ -1,41 +1,68 @@
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<TopBar ref="topBar" @search="searchHistory" @searchStarted="searchStarted" />
|
<TopBar
|
||||||
|
ref="topBar"
|
||||||
|
@search="searchHistory"
|
||||||
|
@searchStarted="searchStarted" />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<OverlayScrollbarsComponent class="results" ref="resultsContainer"
|
<OverlayScrollbarsComponent
|
||||||
|
class="results"
|
||||||
|
ref="resultsContainer"
|
||||||
:options="{ scrollbars: { autoHide: 'scroll' } }">
|
: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="time-separator">{{ group.label }}</div>
|
||||||
<div class="results-group">
|
<div class="results-group">
|
||||||
<Result v-for="(item, index) in group.items" :key="item.id" :item="item"
|
<Result
|
||||||
:selected="isSelected(groupIndex, index)" :image-url="imageUrls[item.id]"
|
v-for="(item, index) in group.items"
|
||||||
:dimensions="imageDimensions[item.id]" @select="selectItem(groupIndex, index)" @image-error="onImageError"
|
: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)" />
|
@setRef="(el: HTMLElement | null) => (selectedElement = el)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
<div class="right">
|
<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" />
|
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)" class="content">
|
<div
|
||||||
<img class="image" :src="getYoutubeThumbnail(selectedItem.content)" alt="YouTube Thumbnail" />
|
v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)"
|
||||||
|
class="content">
|
||||||
|
<img
|
||||||
|
class="image"
|
||||||
|
:src="getYoutubeThumbnail(selectedItem.content)"
|
||||||
|
alt="YouTube Thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<div class="content" v-else-if="
|
<div
|
||||||
selectedItem?.content_type === ContentType.Link && pageOgImage
|
class="content"
|
||||||
">
|
v-else-if="
|
||||||
|
selectedItem?.content_type === ContentType.Link && pageOgImage
|
||||||
|
">
|
||||||
<img :src="pageOgImage" alt="Image" class="image" />
|
<img :src="pageOgImage" alt="Image" class="image" />
|
||||||
</div>
|
</div>
|
||||||
<OverlayScrollbarsComponent v-else class="content">
|
<OverlayScrollbarsComponent v-else class="content">
|
||||||
<span class="content-text">{{ selectedItem?.content || "" }}</span>
|
<span class="content-text">{{ selectedItem?.content || "" }}</span>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
<OverlayScrollbarsComponent class="information" :options="{ scrollbars: { autoHide: 'scroll' } }">
|
<OverlayScrollbarsComponent
|
||||||
|
class="information"
|
||||||
|
:options="{ scrollbars: { autoHide: 'scroll' } }">
|
||||||
<div class="title">Information</div>
|
<div class="title">Information</div>
|
||||||
<div class="info-content" v-if="selectedItem && getInfo">
|
<div class="info-content" v-if="selectedItem && getInfo">
|
||||||
<div class="info-row" v-for="(row, index) in infoRows" :key="index">
|
<div class="info-row" v-for="(row, index) in infoRows" :key="index">
|
||||||
<p class="label">{{ row.label }}</p>
|
<p class="label">{{ row.label }}</p>
|
||||||
<span :class="{ 'url-truncate': row.isUrl }" :data-text="row.value">
|
<span
|
||||||
<img v-if="row.icon" :src="row.icon" :alt="String(row.value)">
|
:class="{ 'url-truncate': row.isUrl }"
|
||||||
|
:data-text="row.value">
|
||||||
|
<img v-if="row.icon" :src="row.icon" :alt="String(row.value)" />
|
||||||
{{ row.value }}
|
{{ row.value }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,26 +70,39 @@
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BottomBar :primary-action="{
|
<BottomBar
|
||||||
text: 'Paste',
|
:primary-action="{
|
||||||
icon: IconsEnter,
|
text: 'Paste',
|
||||||
onClick: pasteSelectedItem,
|
icon: IconsEnter,
|
||||||
}" :secondary-action="{
|
onClick: pasteSelectedItem,
|
||||||
text: 'Actions',
|
}"
|
||||||
icon: IconsKey,
|
:secondary-action="{
|
||||||
input: 'K',
|
text: 'Actions',
|
||||||
showModifier: true,
|
icon: IconsKey,
|
||||||
onClick: toggleActionsMenu,
|
input: 'K',
|
||||||
}" />
|
showModifier: true,
|
||||||
<ActionsMenu :selected-item="selectedItem" :is-visible="isActionsMenuVisible" @close="closeActionsMenu" @toggle="toggleActionsMenu" />
|
onClick: toggleActionsMenu,
|
||||||
|
}" />
|
||||||
|
<ActionsMenu
|
||||||
|
:selected-item="selectedItem"
|
||||||
|
:is-visible="isActionsMenuVisible"
|
||||||
|
@close="closeActionsMenu"
|
||||||
|
@toggle="toggleActionsMenu" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
import "overlayscrollbars/overlayscrollbars.css";
|
import "overlayscrollbars/overlayscrollbars.css";
|
||||||
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";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
@ -86,7 +126,14 @@ interface GroupedHistory {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { $history, $keyboard, $selectedResult } = useNuxtApp();
|
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 CHUNK_SIZE = 50;
|
||||||
const SCROLL_THRESHOLD = 100;
|
const SCROLL_THRESHOLD = 100;
|
||||||
|
@ -102,7 +149,6 @@ const resultsContainer = shallowRef<InstanceType<
|
||||||
> | null>(null);
|
> | null>(null);
|
||||||
const searchQuery = ref("");
|
const searchQuery = ref("");
|
||||||
const searchInput = ref<HTMLInputElement | null>(null);
|
const searchInput = ref<HTMLInputElement | null>(null);
|
||||||
const os = ref<string>("");
|
|
||||||
const imageUrls = shallowRef<Record<string, string>>({});
|
const imageUrls = shallowRef<Record<string, string>>({});
|
||||||
const imageDimensions = shallowRef<Record<string, string>>({});
|
const imageDimensions = shallowRef<Record<string, string>>({});
|
||||||
const imageSizes = shallowRef<Record<string, string>>({});
|
const imageSizes = shallowRef<Record<string, string>>({});
|
||||||
|
@ -117,18 +163,9 @@ const topBar = ref<{ searchInput: HTMLInputElement | null } | null>(null);
|
||||||
|
|
||||||
const toggleActionsMenu = () => {
|
const toggleActionsMenu = () => {
|
||||||
isActionsMenuVisible.value = !isActionsMenuVisible.value;
|
isActionsMenuVisible.value = !isActionsMenuVisible.value;
|
||||||
|
|
||||||
if (isActionsMenuVisible.value) {
|
|
||||||
$keyboard.disableContext('main');
|
|
||||||
$keyboard.enableContext('actionsMenu');
|
|
||||||
} else {
|
|
||||||
$keyboard.disableContext('actionsMenu');
|
|
||||||
$keyboard.enableContext('main');
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (isActionsMenuVisible.value) {
|
if (isActionsMenuVisible.value) {
|
||||||
document.getElementById('actions-menu')?.focus();
|
document.getElementById("actions-menu")?.focus();
|
||||||
} else {
|
} else {
|
||||||
focusSearchInput();
|
focusSearchInput();
|
||||||
}
|
}
|
||||||
|
@ -137,8 +174,7 @@ const toggleActionsMenu = () => {
|
||||||
|
|
||||||
const closeActionsMenu = () => {
|
const closeActionsMenu = () => {
|
||||||
isActionsMenuVisible.value = false;
|
isActionsMenuVisible.value = false;
|
||||||
$keyboard.disableContext('actionsMenu');
|
focusSearchInput();
|
||||||
$keyboard.enableContext('main');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSameDay = (date1: Date, date2: Date): boolean => {
|
const isSameDay = (date1: Date, date2: Date): boolean => {
|
||||||
|
@ -155,7 +191,7 @@ const getWeekNumber = (date: Date): number => {
|
||||||
((date.getTime() - firstDayOfYear.getTime()) / 86400000 +
|
((date.getTime() - firstDayOfYear.getTime()) / 86400000 +
|
||||||
firstDayOfYear.getDay() +
|
firstDayOfYear.getDay() +
|
||||||
1) /
|
1) /
|
||||||
7
|
7
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -176,8 +212,8 @@ const groupedHistory = computed<GroupedHistory[]>(() => {
|
||||||
|
|
||||||
const filteredItems = searchQuery.value
|
const filteredItems = searchQuery.value
|
||||||
? history.value.filter((item) =>
|
? history.value.filter((item) =>
|
||||||
item.content.toLowerCase().includes(searchQuery.value.toLowerCase())
|
item.content.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||||
)
|
)
|
||||||
: history.value;
|
: history.value;
|
||||||
|
|
||||||
const yesterday = new Date(today.getTime() - 86400000);
|
const yesterday = new Date(today.getTime() - 86400000);
|
||||||
|
@ -243,16 +279,16 @@ const loadHistoryChunk = async (): Promise<void> => {
|
||||||
img.src = `data:image/png;base64,${base64}`;
|
img.src = `data:image/png;base64,${base64}`;
|
||||||
imageUrls.value[historyItem.id] = img.src;
|
imageUrls.value[historyItem.id] = img.src;
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolveProm) => {
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
imageDimensions.value[
|
imageDimensions.value[
|
||||||
historyItem.id
|
historyItem.id
|
||||||
] = `${img.width}x${img.height}`;
|
] = `${img.width}x${img.height}`;
|
||||||
resolve();
|
resolveProm();
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
imageDimensions.value[historyItem.id] = "Error";
|
imageDimensions.value[historyItem.id] = "Error";
|
||||||
resolve();
|
resolveProm();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -366,7 +402,11 @@ const processSearchQueue = async () => {
|
||||||
{ id: item.id, timestamp: new Date(item.timestamp) }
|
{ id: item.id, timestamp: new Date(item.timestamp) }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
if (groupedHistory.value.length > 0) {
|
||||||
|
handleSelection(0, 0, true);
|
||||||
|
} else {
|
||||||
|
selectItem(-1, -1);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Search error:", error);
|
console.error("Search error:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -379,13 +419,13 @@ const processSearchQueue = async () => {
|
||||||
|
|
||||||
const searchHistory = async (query: string): Promise<void> => {
|
const searchHistory = async (query: string): Promise<void> => {
|
||||||
searchQuery.value = query;
|
searchQuery.value = query;
|
||||||
|
|
||||||
if (searchController) {
|
if (searchController) {
|
||||||
searchController.abort();
|
searchController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
searchController = new AbortController();
|
searchController = new AbortController();
|
||||||
|
|
||||||
searchQueue.push(query);
|
searchQueue.push(query);
|
||||||
if (!isProcessingSearch) {
|
if (!isProcessingSearch) {
|
||||||
processSearchQueue();
|
processSearchQueue();
|
||||||
|
@ -394,10 +434,14 @@ const searchHistory = async (query: string): Promise<void> => {
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => groupedHistory.value,
|
() => groupedHistory.value,
|
||||||
(newGroupedHistory) => {
|
(newGroupedHistory, oldGroupedHistory) => {
|
||||||
if (newGroupedHistory.length > 0) {
|
if (
|
||||||
|
newGroupedHistory.length > 0 &&
|
||||||
|
oldGroupedHistory &&
|
||||||
|
oldGroupedHistory.length === 0
|
||||||
|
) {
|
||||||
handleSelection(0, 0, true);
|
handleSelection(0, 0, true);
|
||||||
} else {
|
} else if (newGroupedHistory.length === 0) {
|
||||||
selectItem(-1, -1);
|
selectItem(-1, -1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -444,7 +488,7 @@ const getYoutubeThumbnail = (url: string): string => {
|
||||||
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
||||||
offset = 0;
|
offset = 0;
|
||||||
history.value = [];
|
history.value = [];
|
||||||
|
|
||||||
const results = await $history.loadHistoryChunk(offset, CHUNK_SIZE);
|
const results = await $history.loadHistoryChunk(offset, CHUNK_SIZE);
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
const processedItems = await Promise.all(
|
const processedItems = await Promise.all(
|
||||||
|
@ -474,16 +518,16 @@ const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
||||||
img.src = `data:image/png;base64,${base64}`;
|
img.src = `data:image/png;base64,${base64}`;
|
||||||
imageUrls.value[historyItem.id] = img.src;
|
imageUrls.value[historyItem.id] = img.src;
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolveProm) => {
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
imageDimensions.value[
|
imageDimensions.value[
|
||||||
historyItem.id
|
historyItem.id
|
||||||
] = `${img.width}x${img.height}`;
|
] = `${img.width}x${img.height}`;
|
||||||
resolve();
|
resolveProm();
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
imageDimensions.value[historyItem.id] = "Error";
|
imageDimensions.value[historyItem.id] = "Error";
|
||||||
resolve();
|
resolveProm();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -520,72 +564,6 @@ const handleSelection = (
|
||||||
if (shouldScroll) scrollToSelectedItem();
|
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 { hideApp } = useAppControl();
|
||||||
|
|
||||||
const focusSearchInput = (): void => {
|
const focusSearchInput = (): void => {
|
||||||
|
@ -609,38 +587,147 @@ watch(
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
os.value = platform();
|
|
||||||
await loadHistoryChunk();
|
await loadHistoryChunk();
|
||||||
|
if (groupedHistory.value.length > 0 && !selectedItem.value) {
|
||||||
|
handleSelection(0, 0, true);
|
||||||
|
}
|
||||||
|
|
||||||
resultsContainer.value
|
resultsContainer.value
|
||||||
?.osInstance()
|
?.osInstance()
|
||||||
?.elements()
|
?.elements()
|
||||||
?.viewport?.addEventListener("scroll", handleScroll);
|
?.viewport?.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
await setupEventListeners();
|
listeners.push(
|
||||||
|
await listen("clipboard-content-updated", async () => {
|
||||||
$keyboard.setupAppShortcuts({
|
lastUpdateTime.value = Date.now();
|
||||||
onNavigateDown: selectNext,
|
await updateHistory(true);
|
||||||
onNavigateUp: selectPrevious,
|
if (groupedHistory.value[0]?.items.length > 0) {
|
||||||
onSelect: pasteSelectedItem,
|
handleSelection(0, 0, false);
|
||||||
onEscape: () => {
|
|
||||||
if (isActionsMenuVisible.value) {
|
|
||||||
closeActionsMenu();
|
|
||||||
} else {
|
|
||||||
hideApp();
|
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
onToggleActions: toggleActionsMenu,
|
);
|
||||||
contextName: 'main',
|
|
||||||
priority: $keyboard.PRIORITY.HIGH
|
listeners.push(
|
||||||
});
|
await listen("tauri://focus", async () => {
|
||||||
$keyboard.disableContext('settings');
|
console.log("Tauri window focused");
|
||||||
$keyboard.enableContext('main');
|
// 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) {
|
} catch (error) {
|
||||||
console.error("Error during onMounted:", 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(() => {
|
const getFormattedDate = computed(() => {
|
||||||
if (!selectedItem.value?.timestamp) return "";
|
if (!selectedItem.value?.timestamp) return "";
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
|
@ -787,7 +874,9 @@ const infoRows = computed(() => {
|
||||||
label: "Source",
|
label: "Source",
|
||||||
value: getInfo.value.source,
|
value: getInfo.value.source,
|
||||||
isUrl: false,
|
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",
|
label: "Content Type",
|
||||||
|
@ -800,7 +889,12 @@ const infoRows = computed(() => {
|
||||||
|
|
||||||
const typeSpecificRows: Record<
|
const typeSpecificRows: Record<
|
||||||
ContentType,
|
ContentType,
|
||||||
Array<{ label: string; value: string | number; isUrl?: boolean; icon?: string }>
|
Array<{
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
isUrl?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
}>
|
||||||
> = {
|
> = {
|
||||||
[ContentType.Text]: [
|
[ContentType.Text]: [
|
||||||
{ label: "Characters", value: (getInfo.value as InfoText).characters },
|
{ label: "Characters", value: (getInfo.value as InfoText).characters },
|
||||||
|
@ -818,7 +912,7 @@ const infoRows = computed(() => {
|
||||||
],
|
],
|
||||||
[ContentType.Link]: [
|
[ContentType.Link]: [
|
||||||
...((getInfo.value as InfoLink).title &&
|
...((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: "Title", value: (getInfo.value as InfoLink).title || "" }]
|
||||||
: []),
|
: []),
|
||||||
{ label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true },
|
{ label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true },
|
||||||
|
|
|
@ -45,7 +45,6 @@
|
||||||
<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"
|
||||||
|
@ -75,7 +74,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
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";
|
||||||
|
@ -88,14 +87,14 @@ 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 blurredByEscape = ref(false);
|
const blurredByEscape = ref(false);
|
||||||
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, $keyboard } = useNuxtApp();
|
const { $settings, $keyboard } = useNuxtApp();
|
||||||
|
|
||||||
|
const listeners: Array<() => void> = [];
|
||||||
|
|
||||||
const modifierKeySet = new Set([
|
const modifierKeySet = new Set([
|
||||||
KeyValues.AltLeft,
|
KeyValues.AltLeft,
|
||||||
KeyValues.AltRight,
|
KeyValues.AltRight,
|
||||||
|
@ -115,15 +114,15 @@ const keyToLabel = (key: KeyValues): string => {
|
||||||
return KeyLabels[key] || key;
|
return KeyLabels[key] || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateKeybind = () => {
|
const updateKeybindDisplay = () => {
|
||||||
const modifiers = Array.from(activeModifiers);
|
const modifiers = Array.from(activeModifiers);
|
||||||
const nonModifiers = keybind.value.filter((key) => !isModifier(key));
|
const nonModifiers = keybind.value.filter((key) => !isModifier(key));
|
||||||
keybind.value = [...modifiers, ...nonModifiers];
|
const sortedModifiers = modifiers.sort();
|
||||||
|
keybind.value = [...sortedModifiers, ...nonModifiers];
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBlur = () => {
|
const onBlur = () => {
|
||||||
isKeybindInputFocused.value = false;
|
isKeybindInputFocused.value = false;
|
||||||
lastBlurTime.value = Date.now();
|
|
||||||
showEmptyKeybindError.value = false;
|
showEmptyKeybindError.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -133,35 +132,39 @@ const onFocus = () => {
|
||||||
activeModifiers.clear();
|
activeModifiers.clear();
|
||||||
keybind.value = [];
|
keybind.value = [];
|
||||||
showEmptyKeybindError.value = false;
|
showEmptyKeybindError.value = false;
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const unlistenAll = $keyboard.listen([$keyboard.Key.All], (event: KeyboardEvent) => {
|
||||||
const key = event.code as KeyValues;
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const key = event.code as KeyValues;
|
||||||
|
|
||||||
if (key === KeyValues.Escape) {
|
if (key === KeyValues.Escape) {
|
||||||
if (keybindInput.value) {
|
|
||||||
blurredByEscape.value = true;
|
blurredByEscape.value = true;
|
||||||
keybindInput.value.blur();
|
keybindInput.value?.blur();
|
||||||
event.preventDefault();
|
return;
|
||||||
event.stopPropagation();
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isModifier(key)) {
|
if (isModifier(key)) {
|
||||||
activeModifiers.add(key);
|
activeModifiers.add(key);
|
||||||
} else if (!keybind.value.includes(key)) {
|
} else {
|
||||||
keybind.value = keybind.value.filter((k) => isModifier(k));
|
const nonModifierKey = keybind.value.find(k => !isModifier(k));
|
||||||
keybind.value.push(key);
|
if (!nonModifierKey || nonModifierKey === key) {
|
||||||
}
|
keybind.value = Array.from(activeModifiers);
|
||||||
|
if (nonModifierKey !== key) keybind.value.push(key);
|
||||||
updateKeybind();
|
} else {
|
||||||
showEmptyKeybindError.value = false;
|
keybind.value = [ ...Array.from(activeModifiers), key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateKeybindDisplay();
|
||||||
|
showEmptyKeybindError.value = false;
|
||||||
|
}, { prevent: true });
|
||||||
|
listeners.push(unlistenAll);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveKeybind = async () => {
|
const saveKeybind = async () => {
|
||||||
if (keybind.value.length > 0) {
|
const finalKeybind = keybind.value.filter(k => k);
|
||||||
await $settings.saveSetting("keybind", JSON.stringify(keybind.value));
|
if (finalKeybind.length > 0) {
|
||||||
|
await $settings.saveSetting("keybind", JSON.stringify(finalKeybind));
|
||||||
router.push("/");
|
router.push("/");
|
||||||
} else {
|
} else {
|
||||||
showEmptyKeybindError.value = true;
|
showEmptyKeybindError.value = true;
|
||||||
|
@ -177,67 +180,27 @@ const toggleAutostart = async () => {
|
||||||
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
|
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
|
||||||
};
|
};
|
||||||
|
|
||||||
os.value = platform();
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
$keyboard.setupKeybindCapture({
|
autostart.value = (await $settings.getSetting("autostart")) === "true";
|
||||||
onCapture: (key: string) => {
|
|
||||||
if (isKeybindInputFocused.value) {
|
const metaOrCtrlKey = $keyboard.currentOS === "macos" ? $keyboard.Key.Meta : $keyboard.Key.Control;
|
||||||
const keyValue = key as KeyValues;
|
listeners.push(
|
||||||
|
$keyboard.listen([metaOrCtrlKey, $keyboard.Key.Enter], saveKeybind, { prevent: true, ignoreIfEditable: true })
|
||||||
if (isModifier(keyValue)) {
|
);
|
||||||
activeModifiers.add(keyValue);
|
|
||||||
} else if (!keybind.value.includes(keyValue)) {
|
listeners.push(
|
||||||
keybind.value = keybind.value.filter((k) => isModifier(k));
|
$keyboard.listen([$keyboard.Key.Escape], () => {
|
||||||
keybind.value.push(keyValue);
|
if (!isKeybindInputFocused.value && !blurredByEscape.value) {
|
||||||
}
|
|
||||||
|
|
||||||
updateKeybind();
|
|
||||||
showEmptyKeybindError.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
if (isKeybindInputFocused.value) {
|
|
||||||
keybindInput.value?.blur();
|
|
||||||
} else {
|
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
}
|
if(blurredByEscape.value) blurredByEscape.value = false;
|
||||||
});
|
}, { prevent: true })
|
||||||
|
);
|
||||||
if (os.value === "macos") {
|
|
||||||
$keyboard.on("settings", [$keyboard.Key.LeftMeta, $keyboard.Key.Enter], () => {
|
|
||||||
saveKeybind();
|
|
||||||
}, { priority: $keyboard.PRIORITY.HIGH });
|
|
||||||
|
|
||||||
$keyboard.on("settings", [$keyboard.Key.RightMeta, $keyboard.Key.Enter], () => {
|
|
||||||
saveKeybind();
|
|
||||||
}, { priority: $keyboard.PRIORITY.HIGH });
|
|
||||||
} else {
|
|
||||||
$keyboard.on("settings", [$keyboard.Key.LeftControl, $keyboard.Key.Enter], () => {
|
|
||||||
saveKeybind();
|
|
||||||
}, { priority: $keyboard.PRIORITY.HIGH });
|
|
||||||
|
|
||||||
$keyboard.on("settings", [$keyboard.Key.RightControl, $keyboard.Key.Enter], () => {
|
|
||||||
saveKeybind();
|
|
||||||
}, { priority: $keyboard.PRIORITY.HIGH });
|
|
||||||
}
|
|
||||||
|
|
||||||
$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";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
$keyboard.disableContext("settings");
|
listeners.forEach(unlisten => unlisten());
|
||||||
|
listeners.length = 0;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,281 +1,27 @@
|
||||||
import { Key, keyboard } from "wrdu-keyboard";
|
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
|
import { useKeyboard, Key } from "@waradu/keyboard";
|
||||||
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) => {
|
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||||
|
const keyboardInstance = useKeyboard();
|
||||||
|
let currentOS = "windows";
|
||||||
try {
|
try {
|
||||||
const osName = platform();
|
const osName = await Promise.resolve(platform());
|
||||||
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
|
currentOS = osName.toLowerCase().includes("mac") ? "macos" : "windows";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error detecting platform:", error);
|
console.error("Error detecting platform:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
initKeyboardHandlers();
|
// Defer initialization until the app is mounted
|
||||||
|
nuxtApp.hook('app:mounted', () => {
|
||||||
nuxtApp.hook("page:finish", () => {
|
keyboardInstance.init();
|
||||||
initKeyboardHandlers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
nuxtApp.provide('keyboard', {
|
||||||
provide: {
|
listen: keyboardInstance.listen.bind(keyboardInstance),
|
||||||
keyboard: {
|
init: keyboardInstance.init.bind(keyboardInstance),
|
||||||
...useKeyboard,
|
Key,
|
||||||
Key,
|
currentOS,
|
||||||
},
|
// Provide a clear method if users need to manually clear all listeners from the instance
|
||||||
},
|
clearAll: keyboardInstance.clear ? keyboardInstance.clear.bind(keyboardInstance) : () => { console.warn('@waradu/keyboard instance does not have a clear method'); }
|
||||||
};
|
});
|
||||||
});
|
});
|
||||||
|
|
27
types/keyboard.d.ts
vendored
Normal file
27
types/keyboard.d.ts
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Key as WaraduKey, useKeyboard } from '@waradu/keyboard';
|
||||||
|
|
||||||
|
declare module '#app' {
|
||||||
|
interface NuxtApp {
|
||||||
|
$keyboard: {
|
||||||
|
listen: ReturnType<typeof useKeyboard>['listen'];
|
||||||
|
init: ReturnType<typeof useKeyboard>['init'];
|
||||||
|
Key: typeof WaraduKey;
|
||||||
|
currentOS: string;
|
||||||
|
clearAll: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
$keyboard: {
|
||||||
|
listen: ReturnType<typeof useKeyboard>['listen'];
|
||||||
|
init: ReturnType<typeof useKeyboard>['init'];
|
||||||
|
Key: typeof WaraduKey;
|
||||||
|
currentOS: string;
|
||||||
|
clearAll: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
Loading…
Add table
Add a link
Reference in a new issue