mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 13:14:04 +02:00
refactor: serach function now async which is way faster
This commit is contained in:
parent
ae878b7203
commit
f435a7b20a
14 changed files with 203 additions and 106 deletions
9
app.vue
9
app.vue
|
@ -38,25 +38,25 @@ onMounted(async () => {
|
|||
@font-face {
|
||||
font-family: SFRoundedRegular;
|
||||
font-display: swap;
|
||||
src: url("~/assets/fonts/SFRoundedRegular.otf") format("opentype");
|
||||
src: url("/fonts/SFRoundedRegular.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SFRoundedMedium;
|
||||
font-display: swap;
|
||||
src: url("~/assets/fonts/SFRoundedMedium.otf") format("opentype");
|
||||
src: url("/fonts/SFRoundedMedium.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: SFRoundedSemiBold;
|
||||
font-display: swap;
|
||||
src: url("~/assets/fonts/SFRoundedSemiBold.otf") format("opentype");
|
||||
src: url("/fonts/SFRoundedSemiBold.otf") format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: CommitMono;
|
||||
font-display: swap;
|
||||
src: url("~/assets/fonts/CommitMono.woff2") format("woff2");
|
||||
src: url("/fonts/CommitMono.woff2") format("woff2");
|
||||
}
|
||||
|
||||
:root {
|
||||
|
@ -85,6 +85,7 @@ onMounted(async () => {
|
|||
font-family: SFRoundedRegular;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: thin;
|
||||
user-select: none;
|
||||
|
||||
--os-handle-bg: #ada9a1;
|
||||
--os-handle-bg-hover: #78756f;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchQuery"
|
||||
@input="onSearch"
|
||||
@input="onInputChange"
|
||||
class="search"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
|
@ -21,10 +21,12 @@ const searchInput = ref<HTMLInputElement | null>(null);
|
|||
|
||||
const emit = defineEmits<{
|
||||
(e: "search", query: string): void;
|
||||
(e: "searchStarted"): void;
|
||||
(e: "focus"): void;
|
||||
}>();
|
||||
|
||||
const onSearch = () => {
|
||||
const onInputChange = () => {
|
||||
emit("searchStarted");
|
||||
emit("search", searchQuery.value);
|
||||
};
|
||||
|
||||
|
|
192
pages/index.vue
192
pages/index.vue
|
@ -1,64 +1,40 @@
|
|||
<template>
|
||||
<main>
|
||||
<TopBar ref="topBar" @search="searchHistory" />
|
||||
<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) => (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">
|
||||
<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>
|
||||
|
@ -67,17 +43,15 @@
|
|||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar
|
||||
:primary-action="{
|
||||
text: 'Paste',
|
||||
icon: IconsEnter,
|
||||
onClick: pasteSelectedItem,
|
||||
}"
|
||||
:secondary-action="{
|
||||
text: 'Actions',
|
||||
icon: IconsK,
|
||||
showModifier: true,
|
||||
}" />
|
||||
<BottomBar :primary-action="{
|
||||
text: 'Paste',
|
||||
icon: IconsEnter,
|
||||
onClick: pasteSelectedItem,
|
||||
}" :secondary-action="{
|
||||
text: 'Actions',
|
||||
icon: IconsK,
|
||||
showModifier: true,
|
||||
}" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
@ -156,7 +130,7 @@ const getWeekNumber = (date: Date): number => {
|
|||
((date.getTime() - firstDayOfYear.getTime()) / 86400000 +
|
||||
firstDayOfYear.getDay() +
|
||||
1) /
|
||||
7
|
||||
7
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -177,8 +151,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);
|
||||
|
@ -321,32 +295,78 @@ const scrollToSelectedItem = (): void => {
|
|||
});
|
||||
};
|
||||
|
||||
let searchController: AbortController | null = null;
|
||||
let searchQueue: Array<string> = [];
|
||||
let isProcessingSearch = false;
|
||||
|
||||
const searchStarted = () => {
|
||||
if (searchController) {
|
||||
searchController.abort();
|
||||
}
|
||||
};
|
||||
|
||||
const processSearchQueue = async () => {
|
||||
if (isProcessingSearch || searchQueue.length === 0) return;
|
||||
|
||||
isProcessingSearch = true;
|
||||
const query = searchQueue.pop();
|
||||
searchQueue = [];
|
||||
|
||||
try {
|
||||
if (!query || !query.trim()) {
|
||||
history.value = [];
|
||||
offset = 0;
|
||||
await loadHistoryChunk();
|
||||
isProcessingSearch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await $history.searchHistory(query);
|
||||
|
||||
if (searchController?.signal.aborted) {
|
||||
isProcessingSearch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
history.value = results.map((item) =>
|
||||
Object.assign(
|
||||
new HistoryItem(
|
||||
item.source,
|
||||
item.content_type,
|
||||
item.content,
|
||||
item.favicon,
|
||||
item.source_icon,
|
||||
item.language
|
||||
),
|
||||
{ id: item.id, timestamp: new Date(item.timestamp) }
|
||||
)
|
||||
);
|
||||
|
||||
if (groupedHistory.value.length > 0) {
|
||||
handleSelection(0, 0, false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Search error:", error);
|
||||
} finally {
|
||||
isProcessingSearch = false;
|
||||
if (searchQueue.length > 0) {
|
||||
requestAnimationFrame(() => processSearchQueue());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const searchHistory = async (query: string): Promise<void> => {
|
||||
searchQuery.value = query;
|
||||
if (!query.trim()) {
|
||||
history.value = [];
|
||||
offset = 0;
|
||||
await loadHistoryChunk();
|
||||
return;
|
||||
|
||||
if (searchController) {
|
||||
searchController.abort();
|
||||
}
|
||||
|
||||
const results = await $history.searchHistory(query);
|
||||
history.value = results.map((item) =>
|
||||
Object.assign(
|
||||
new HistoryItem(
|
||||
item.source,
|
||||
item.content_type,
|
||||
item.content,
|
||||
item.favicon,
|
||||
item.source_icon,
|
||||
item.language
|
||||
),
|
||||
{ id: item.id, timestamp: new Date(item.timestamp) }
|
||||
)
|
||||
);
|
||||
searchController = new AbortController();
|
||||
|
||||
if (groupedHistory.value.length > 0) {
|
||||
handleSelection(0, 0, false);
|
||||
searchQueue.push(query);
|
||||
if (!isProcessingSearch) {
|
||||
processSearchQueue();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -517,14 +537,14 @@ const setupEventListeners = async (): Promise<void> => {
|
|||
|
||||
switch (os.value) {
|
||||
case "macos":
|
||||
keyboard.prevent.down([Key.LeftMeta, Key.K], () => {});
|
||||
keyboard.prevent.down([Key.RightMeta, Key.K], () => {});
|
||||
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], () => {});
|
||||
keyboard.prevent.down([Key.LeftControl, Key.K], () => { });
|
||||
keyboard.prevent.down([Key.RightControl, Key.K], () => { });
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -552,14 +572,14 @@ const setupEventListeners = async (): Promise<void> => {
|
|||
|
||||
switch (os.value) {
|
||||
case "macos":
|
||||
keyboard.prevent.down([Key.LeftMeta, Key.K], () => {});
|
||||
keyboard.prevent.down([Key.RightMeta, Key.K], () => {});
|
||||
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], () => {});
|
||||
keyboard.prevent.down([Key.LeftControl, Key.K], () => { });
|
||||
keyboard.prevent.down([Key.RightControl, Key.K], () => { });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -750,9 +770,9 @@ const infoRows = computed(() => {
|
|||
if (!getInfo.value) return [];
|
||||
|
||||
const commonRows = [
|
||||
{
|
||||
label: "Source",
|
||||
value: getInfo.value.source,
|
||||
{
|
||||
label: "Source",
|
||||
value: getInfo.value.source,
|
||||
isUrl: false,
|
||||
icon: selectedItem.value?.source_icon ? `data:image/png;base64,${selectedItem.value.source_icon}` : undefined
|
||||
},
|
||||
|
@ -785,7 +805,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 },
|
||||
|
@ -815,5 +835,5 @@ const infoRows = computed(() => {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "~/assets/css/index.scss";
|
||||
@use "/styles/index.scss";
|
||||
</style>
|
||||
|
|
|
@ -228,5 +228,5 @@ onUnmounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "~/assets/css/settings.scss";
|
||||
@use "/styles/settings.scss";
|
||||
</style>
|
||||
|
|
|
@ -14,17 +14,27 @@ export default defineNuxtPlugin(() => {
|
|||
},
|
||||
|
||||
async searchHistory(query: string): Promise<HistoryItem[]> {
|
||||
return await invoke<HistoryItem[]>("search_history", { query });
|
||||
try {
|
||||
return await invoke<HistoryItem[]>("search_history", { query });
|
||||
} catch (error) {
|
||||
console.error("Error searching history:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadHistoryChunk(
|
||||
offset: number,
|
||||
limit: number
|
||||
): Promise<HistoryItem[]> {
|
||||
return await invoke<HistoryItem[]>("load_history_chunk", {
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
try {
|
||||
return await invoke<HistoryItem[]>("load_history_chunk", {
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading history chunk:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async deleteHistoryItem(id: string): Promise<void> {
|
||||
|
|
49
src-tauri/Cargo.lock
generated
49
src-tauri/Cargo.lock
generated
|
@ -77,15 +77,17 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
|||
[[package]]
|
||||
name = "applications"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/HuakunShen/applications-rs?branch=load_icon#bcfbebb93a57918aca4ba51257b2788be90892da"
|
||||
source = "git+https://github.com/HuakunShen/applications-rs?branch=fix/win-app-detection#ddcec7cdc3cc4f6678f1b1b525b3e509987b21ae"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cocoa 0.25.0",
|
||||
"core-foundation 0.9.4",
|
||||
"env_logger",
|
||||
"glob",
|
||||
"image",
|
||||
"ini",
|
||||
"lnk",
|
||||
"log",
|
||||
"objc",
|
||||
"parselnk",
|
||||
"plist",
|
||||
|
@ -1359,6 +1361,19 @@ dependencies = [
|
|||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
|
@ -2121,6 +2136,12 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
|
@ -2208,6 +2229,12 @@ version = "1.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.4.1"
|
||||
|
@ -2584,6 +2611,17 @@ version = "2.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.0",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
|
@ -5957,6 +5995,15 @@ dependencies = [
|
|||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "texting_robots"
|
||||
version = "0.2.2"
|
||||
|
|
|
@ -49,7 +49,7 @@ log = { version = "0.4.26", features = ["std"] }
|
|||
uuid = "1.16.0"
|
||||
include_dir = "0.7.4"
|
||||
# hyperpolyglot = { git = "https://github.com/0pandadev/hyperpolyglot" }
|
||||
applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "load_icon" }
|
||||
applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "fix/win-app-detection" }
|
||||
glob = "0.3.2"
|
||||
meta_fetcher = "0.1.1"
|
||||
parking_lot = "0.12.3"
|
||||
|
|
|
@ -111,18 +111,27 @@ pub async fn search_history(
|
|||
pool: tauri::State<'_, SqlitePool>,
|
||||
query: String
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
if query.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let query = format!("%{}%", query);
|
||||
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history WHERE content LIKE ? ORDER BY timestamp DESC"
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language
|
||||
FROM history
|
||||
WHERE content LIKE ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100"
|
||||
)
|
||||
.bind(query)
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(|row| HistoryItem {
|
||||
let mut items = Vec::with_capacity(rows.len());
|
||||
for row in rows.iter() {
|
||||
items.push(HistoryItem {
|
||||
id: row.get("id"),
|
||||
source: row.get("source"),
|
||||
source_icon: row.get("source_icon"),
|
||||
|
@ -131,8 +140,8 @@ pub async fn search_history(
|
|||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
|
|
@ -176,3 +176,11 @@ main {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue