From 149e72802c1f9e5e7f18f198fd2beb99c4157be1 Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:38:35 +1000 Subject: [PATCH 01/16] feat: add information and update chunk loading --- assets/css/index.scss | 73 ++++- pages/index.vue | 359 +++++++++++++-------- src-tauri/src/db/history.rs | 15 +- src-tauri/src/db/migrations/migration2.sql | 1 + src-tauri/src/utils/commands.rs | 23 +- src-tauri/src/utils/types.rs | 27 +- types/types.ts | 88 ++++- 7 files changed, 435 insertions(+), 151 deletions(-) diff --git a/assets/css/index.scss b/assets/css/index.scss index 003253d..33d4af9 100644 --- a/assets/css/index.scss +++ b/assets/css/index.scss @@ -98,15 +98,22 @@ $mutedtext: #78756f; position: absolute; top: 53px; left: 284px; - padding: 8px; - height: calc(100vh - 256px); + height: calc(100vh - 254px); font-family: CommitMono Nerd Font !important; - font-size: 14px; + font-size: 12px; letter-spacing: 1; border-radius: 10px; width: calc(100vw - 286px); white-space: pre-wrap; word-wrap: break-word; + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; + + &:not(:has(.full-image, .image)) { + padding: 8px; + } span { font-family: CommitMono Nerd Font !important; @@ -114,16 +121,26 @@ $mutedtext: #78756f; .full-image { width: 100%; - aspect-ratio: 16 / 9; - object-fit: cover; - object-position: center; + height: 100%; + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + } } .image { - max-width: 100%; - max-height: 100%; + width: 100%; + height: 100%; object-fit: contain; - object-position: top left; + object-position: center; } } @@ -212,6 +229,9 @@ $mutedtext: #78756f; .information { position: absolute; + display: flex; + flex-direction: column; + gap: 14px; bottom: 40px; left: 284px; height: 160px; @@ -225,6 +245,41 @@ $mutedtext: #78756f; font-size: 12px; letter-spacing: 0.6px; } + + .info-content { + display: flex; + gap: 0; + flex-direction: column; + } + + .info-row { + display: flex; + width: 100%; + font-size: 12px; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid $divider; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + &:first-child { + padding-top: 22px; + } + + p { + font-family: SFRoundedMedium; + color: $text2; + font-weight: 500; + } + + span { + font-family: CommitMono; + color: $text; + } + } } .clothoid-corner { diff --git a/pages/index.vue b/pages/index.vue index 22a727d..c218720 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -87,20 +87,114 @@ -
- Image +
+ Image
- YouTube Thumbnail +
+ YouTube Thumbnail +
{{ selectedItem?.content || "" }}
-
+
Information
-
+
+ +
+

Source

+ {{ selectedItem.source }} +
+
+

Content Type

+ {{ + selectedItem.content_type.charAt(0).toUpperCase() + + selectedItem.content_type.slice(1) + }} +
+ + + + + + + + + + + + + + + + + + + + +
+

Copied

+ {{ getFormattedDate }} +
+
+
@@ -121,10 +215,9 @@ interface GroupedHistory { items: HistoryItem[]; } -const { $history, $settings } = useNuxtApp(); +const { $history } = useNuxtApp(); const CHUNK_SIZE = 50; const SCROLL_THRESHOLD = 100; -const IMAGE_LOAD_DEBOUNCE = 300; const history = shallowRef([]); let offset = 0; @@ -141,6 +234,7 @@ const searchInput = ref(null); const os = ref(""); const imageUrls = shallowRef>({}); const imageDimensions = shallowRef>({}); +const imageSizes = shallowRef>({}); const lastUpdateTime = ref(Date.now()); const imageLoadError = ref(false); const imageLoading = ref(false); @@ -238,10 +332,34 @@ const loadHistoryChunk = async (): Promise => { }); if (historyItem.content_type === ContentType.Image) { - await Promise.all([ - getItemDimensions(historyItem), - loadImageUrl(historyItem), - ]); + try { + const base64 = await $history.readImage({ + filename: historyItem.content, + }); + const size = Math.ceil((base64.length * 3) / 4); + imageSizes.value[historyItem.id] = formatFileSize(size); + + const img = new Image(); + img.src = `data:image/png;base64,${base64}`; + imageUrls.value[historyItem.id] = img.src; + + await new Promise((resolve) => { + img.onload = () => { + imageDimensions.value[ + historyItem.id + ] = `${img.width}x${img.height}`; + resolve(); + }; + img.onerror = () => { + imageDimensions.value[historyItem.id] = "Error"; + resolve(); + }; + }); + } catch (error) { + console.error("Error processing image:", error); + imageDimensions.value[historyItem.id] = "Error"; + imageSizes.value[historyItem.id] = "Error"; + } } return historyItem; }) @@ -281,7 +399,7 @@ const scrollToSelectedItem = (forceScrollTop: boolean = false): void => { if (isAbove || isBelow) { const scrollOffset = isAbove - ? elementRect.top - viewportRect.top - 8 + ? elementRect.top - viewportRect.top - (selectedItemIndex.value === 0 ? 36 : 8) : elementRect.bottom - viewportRect.bottom + 9; viewport.scrollBy({ top: scrollOffset, behavior: "smooth" }); @@ -347,7 +465,7 @@ const pasteSelectedItem = async (): Promise => { let contentType: string = selectedItem.value.content_type; if (contentType === "image") { try { - content = await $history.getImagePath(content); + content = await $history.readImage({ filename: content }); } catch (error) { console.error("Error reading image file:", error); return; @@ -394,65 +512,67 @@ const getFaviconFromDb = (favicon: string): string => { return `data:image/png;base64,${favicon}`; }; -const getImageData = async ( - item: HistoryItem -): Promise<{ url: string; dimensions: string }> => { - try { - const base64 = await $history.readImage({ filename: item.content }); - const dataUrl = `data:image/png;base64,${base64}`; - const img = new Image(); - img.src = dataUrl; - - await new Promise((resolve, reject) => { - img.onload = () => resolve(); - img.onerror = reject; - }); - - return { - url: dataUrl, - dimensions: `${img.width}x${img.height}`, - }; - } catch (error) { - console.error("Error processing image:", error); - return { url: "", dimensions: "Error" }; - } -}; - -const processHistoryItem = async (item: any): Promise => { - const historyItem = new HistoryItem( - item.content_type as string, - ContentType[item.content_type as keyof typeof ContentType], - item.content, - item.favicon - ); - - Object.assign(historyItem, { - id: item.id, - timestamp: new Date(item.timestamp), - }); - - if (historyItem.content_type === ContentType.Image) { - const { url, dimensions } = await getImageData(historyItem); - imageUrls.value[historyItem.id] = url; - imageDimensions.value[historyItem.id] = dimensions; - } - - return historyItem; -}; - const updateHistory = async (resetScroll: boolean = false): Promise => { - history.value = []; - offset = 0; - await loadHistoryChunk(); + const results = await $history.loadHistoryChunk(0, CHUNK_SIZE); + if (results.length > 0) { + const existingIds = new Set(history.value.map(item => item.id)); + const uniqueNewItems = results.filter(item => !existingIds.has(item.id)); + + const processedNewItems = await Promise.all( + uniqueNewItems.map(async (item) => { + const historyItem = new HistoryItem( + item.source, + item.content_type, + item.content, + item.favicon + ); + Object.assign(historyItem, { + id: item.id, + timestamp: new Date(item.timestamp), + }); - if ( - resetScroll && - resultsContainer.value?.osInstance()?.elements().viewport - ) { - resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({ - top: 0, - behavior: "smooth", - }); + if (historyItem.content_type === ContentType.Image) { + try { + const base64 = await $history.readImage({ + filename: historyItem.content, + }); + const size = Math.ceil((base64.length * 3) / 4); + imageSizes.value[historyItem.id] = formatFileSize(size); + + const img = new Image(); + img.src = `data:image/png;base64,${base64}`; + imageUrls.value[historyItem.id] = img.src; + + await new Promise((resolve) => { + img.onload = () => { + imageDimensions.value[ + historyItem.id + ] = `${img.width}x${img.height}`; + resolve(); + }; + img.onerror = () => { + imageDimensions.value[historyItem.id] = "Error"; + resolve(); + }; + }); + } catch (error) { + console.error("Error processing image:", error); + imageDimensions.value[historyItem.id] = "Error"; + imageSizes.value[historyItem.id] = "Error"; + } + } + return historyItem; + }) + ); + + history.value = [...processedNewItems, ...history.value]; + + if (resetScroll && resultsContainer.value?.osInstance()?.elements().viewport) { + resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({ + top: 0, + behavior: "smooth", + }); + } } }; @@ -466,29 +586,16 @@ const handleSelection = ( if (shouldScroll) scrollToSelectedItem(); }; -const handleMediaContent = async ( - content: string, - type: string -): Promise => { - if (type === "image") { - return await $history.getImagePath(content); - } - - if (isYoutubeWatchUrl(content)) { - const videoId = content.includes("youtu.be") - ? content.split("youtu.be/")[1] - : content.match(/[?&]v=([^&]+)/)?.[1]; - return videoId ? `https://img.youtube.com/vi/${videoId}/0.jpg` : ""; - } - - return content; -}; - const setupEventListeners = async (): Promise => { await listen("clipboard-content-updated", async () => { lastUpdateTime.value = Date.now(); - handleSelection(0, 0, false); await updateHistory(true); + if ( + groupedHistory.value.length > 0 && + groupedHistory.value[0].items.length > 0 + ) { + handleSelection(0, 0, false); + } }); await listen("tauri://focus", async () => { @@ -497,9 +604,7 @@ const setupEventListeners = async (): Promise => { const previousState = { groupIndex: selectedGroupIndex.value, itemIndex: selectedItemIndex.value, - scroll: - resultsContainer.value?.osInstance()?.elements().viewport - ?.scrollTop || 0, + scroll: resultsContainer.value?.osInstance()?.elements().viewport?.scrollTop || 0, }; await updateHistory(); @@ -507,9 +612,7 @@ const setupEventListeners = async (): Promise => { handleSelection(previousState.groupIndex, previousState.itemIndex, false); nextTick(() => { - const viewport = resultsContainer.value - ?.osInstance() - ?.elements().viewport; + const viewport = resultsContainer.value?.osInstance()?.elements().viewport; if (viewport) { viewport.scrollTo({ top: previousState.scroll, @@ -611,40 +714,38 @@ watch([selectedGroupIndex, selectedItemIndex], () => scrollToSelectedItem(false) ); -const getItemDimensions = async (item: HistoryItem) => { - if (!imageDimensions.value[item.id]) { - try { - const base64 = await $history.readImage({ filename: item.content }); - const img = new Image(); - img.src = `data:image/png;base64,${base64}`; - await new Promise((resolve, reject) => { - img.onload = () => resolve(); - img.onerror = () => reject(); - }); - imageDimensions.value[item.id] = `${img.width}x${img.height}`; - } catch (error) { - console.error("Error loading image dimensions:", error); - imageDimensions.value[item.id] = "Error"; - } - } - return imageDimensions.value[item.id] || "Loading..."; -}; - -const loadImageUrl = async (item: HistoryItem) => { - if (!imageUrls.value[item.id]) { - try { - const base64 = await $history.readImage({ filename: item.content }); - imageUrls.value[item.id] = `data:image/png;base64,${base64}`; - } catch (error) { - console.error("Error loading image:", error); - } - } -}; - const getComputedImageUrl = (item: HistoryItem | null): string => { if (!item) return ""; return imageUrls.value[item.id] || ""; }; + +const getCharacterCount = computed(() => { + return selectedItem.value?.content.length ?? 0; +}); + +const getWordCount = computed(() => { + return selectedItem.value?.content.trim().split(/\s+/).length ?? 0; +}); + +const getLineCount = computed(() => { + return selectedItem.value?.content.split("\n").length ?? 0; +}); + +const getFormattedDate = computed(() => { + if (!selectedItem.value?.timestamp) return ""; + return new Intl.DateTimeFormat("en-US", { + dateStyle: "medium", + timeStyle: "short", + }).format(selectedItem.value.timestamp); +}); + +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +}; \ No newline at end of file +@use "~/assets/css/settings.scss"; + diff --git a/plugins/history.ts b/plugins/history.ts index dd880b0..934ded5 100644 --- a/plugins/history.ts +++ b/plugins/history.ts @@ -27,8 +27,12 @@ export default defineNuxtPlugin(() => { }); }, - async getImagePath(path: string): Promise { - return await invoke("get_image_path", { path }); + async deleteHistoryItem(id: string): Promise { + await invoke("delete_history_item", { id }); + }, + + async clearHistory(): Promise { + await invoke("clear_history"); }, async writeAndPaste(data: { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a17efef..7293135 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -94,6 +94,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -110,6 +119,31 @@ dependencies = [ "objc", ] +[[package]] +name = "applications" +version = "0.2.3" +source = "git+https://github.com/HuakunShen/applications-rs?branch=dev#ac41b051f0ebeac96213c6c32621b098634219ac" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-foundation 0.9.4", + "glob", + "image", + "ini", + "lnk", + "objc", + "parselnk", + "plist", + "regex", + "serde", + "serde_derive", + "serde_json", + "tauri-icns", + "thiserror 1.0.63", + "walkdir", + "winreg 0.52.0", +] + [[package]] name = "arbitrary" version = "1.3.2" @@ -315,6 +349,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "auto-launch" version = "0.5.0" @@ -496,6 +541,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "built" version = "0.7.4" @@ -675,6 +730,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "circular-queue" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34327ead1c743a10db339de35fb58957564b99d248a67985c55638b22c59b5" +dependencies = [ + "version_check", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -686,6 +750,21 @@ dependencies = [ "libloading 0.8.6", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "clipboard-rs" version = "0.2.1" @@ -725,6 +804,22 @@ dependencies = [ "objc", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.26.0" @@ -733,7 +828,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ "bitflags 2.6.0", "block", - "cocoa-foundation", + "cocoa-foundation 0.2.0", "core-foundation 0.10.0", "core-graphics 0.24.0", "foreign-types 0.5.0", @@ -741,6 +836,20 @@ dependencies = [ "objc", ] +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "cocoa-foundation" version = "0.2.0" @@ -780,6 +889,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "configparser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7" + [[package]] name = "const-oid" version = "0.9.6" @@ -1065,7 +1180,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.87", ] @@ -1931,6 +2046,19 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -2060,6 +2188,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2233,6 +2370,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperpolyglot" +version = "0.1.7" +source = "git+https://github.com/0pandadev/hyperpolyglot#f4f463d7430d870568584ffd55c901f4576a6bae" +dependencies = [ + "clap", + "ignore", + "lazy_static", + "num_cpus", + "pcre2", + "phf 0.11.2", + "phf_codegen 0.11.2", + "polyglot_tokenizer", + "regex", + "serde", + "serde_yaml", + "termcolor", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -2411,6 +2567,22 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "image" version = "0.25.5" @@ -2500,6 +2672,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "ini" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9271a5dfd4228fa56a78d7508a35c321639cc71f783bb7a5723552add87bce" +dependencies = [ + "configparser", +] + [[package]] name = "instant" version = "0.1.13" @@ -2781,6 +2962,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2793,6 +2980,20 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +[[package]] +name = "lnk" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e066ce29d4da51727b57c404c1270e3fa2a5ded0db1a4cb67c61f7a132421b2c" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "chrono", + "log", + "num-derive 0.3.3", + "num-traits", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -3068,6 +3269,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -3120,6 +3332,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "num_enum" version = "0.7.3" @@ -3509,12 +3731,47 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parselnk" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0088616e6efe53ab79907b9313f4743eb3f8a16eb1e0014af810164808906dc3" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "chrono", + "thiserror 1.0.63", + "widestring", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pcre2" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be55c43ac18044541d58d897e8f4c55157218428953ebd39d86df3ba0286b2b" +dependencies = [ + "libc", + "log", + "pcre2-sys", +] + +[[package]] +name = "pcre2-sys" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550f5d18fb1b90c20b87e161852c10cde77858c3900c5059b5ad2a1449f11d8a" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -3586,6 +3843,16 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -3781,6 +4048,14 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "polyglot_tokenizer" +version = "0.2.1" +source = "git+https://github.com/0pandadev/hyperpolyglot#f4f463d7430d870568584ffd55c901f4576a6bae" +dependencies = [ + "circular-queue", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3904,9 +4179,11 @@ name = "qopy" version = "0.2.1" dependencies = [ "active-win-pos-rs", + "applications", "base64 0.22.1", "chrono", "global-hotkey", + "hyperpolyglot", "image", "include_dir", "lazy_static", @@ -4127,7 +4404,7 @@ dependencies = [ "maybe-rayon", "new_debug_unreachable", "noop_proc_macro", - "num-derive", + "num-derive 0.4.2", "num-traits", "once_cell", "paste", @@ -4689,6 +4966,18 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap 1.9.3", + "ryu", + "serde", + "yaml-rust", +] + [[package]] name = "serialize-to-javascript" version = "0.1.1" @@ -5147,6 +5436,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.11.1" @@ -5444,6 +5739,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-icns" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b7eb4d0d43724ba9ba6a6717420ee68aee377816a3edbb45db8c18862b1431" +dependencies = [ + "byteorder", + "png", +] + [[package]] name = "tauri-macros" version = "2.0.3" @@ -5580,9 +5885,9 @@ dependencies = [ [[package]] name = "tauri-plugin-prevent-default" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d16274f883d2810fa8357124361656074599f5f9b52c8dff381ad82491b8a43" +checksum = "ce34a821424cdb5c74b390ddc8f08774d836030c07ab8dd35bd180690ef1331e" dependencies = [ "bitflags 2.6.0", "itertools 0.13.0", @@ -5758,6 +6063,24 @@ 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 = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thin-slice" version = "0.1.1" @@ -6188,6 +6511,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -6269,6 +6598,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.2.0" @@ -6604,6 +6939,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + [[package]] name = "winapi" version = "0.3.9" @@ -7127,6 +7468,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yoke" version = "0.7.4" diff --git a/src-tauri/src/api/hotkeys.rs b/src-tauri/src/api/hotkeys.rs index f35d9c8..7a0b015 100644 --- a/src-tauri/src/api/hotkeys.rs +++ b/src-tauri/src/api/hotkeys.rs @@ -3,9 +3,9 @@ use global_hotkey::{ hotkey::{Code, HotKey, Modifiers}, GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, }; -use std::str::FromStr; use std::cell::RefCell; -use tauri::{AppHandle, Manager, Listener}; +use std::str::FromStr; +use tauri::{AppHandle, Listener, Manager}; thread_local! { static HOTKEY_MANAGER: RefCell> = RefCell::new(None); @@ -18,20 +18,21 @@ pub fn setup(app_handle: tauri::AppHandle) { HOTKEY_MANAGER.with(|m| *m.borrow_mut() = Some(manager)); let rt = app_handle.state::(); - let initial_keybind = rt.block_on(crate::db::settings::get_keybind(app_handle_clone.clone())) + let initial_keybind = rt + .block_on(crate::db::settings::get_keybind(app_handle_clone.clone())) .expect("Failed to get initial keybind"); let initial_shortcut = initial_keybind.join("+"); - + let initial_shortcut_for_update = initial_shortcut.clone(); let initial_shortcut_for_save = initial_shortcut.clone(); - + if let Err(e) = register_shortcut(&initial_shortcut) { eprintln!("Error registering initial shortcut: {:?}", e); } app_handle.listen("update-shortcut", move |event| { let payload_str = event.payload().to_string(); - + if let Ok(old_hotkey) = parse_hotkey(&initial_shortcut_for_update) { HOTKEY_MANAGER.with(|manager| { if let Some(manager) = manager.borrow().as_ref() { @@ -47,7 +48,7 @@ pub fn setup(app_handle: tauri::AppHandle) { app_handle.listen("save_keybind", move |event| { let payload_str = event.payload().to_string(); - + if let Ok(old_hotkey) = parse_hotkey(&initial_shortcut_for_save) { HOTKEY_MANAGER.with(|manager| { if let Some(manager) = manager.borrow().as_ref() { @@ -110,14 +111,17 @@ fn parse_hotkey(shortcut: &str) -> Result> { } else { key.to_string() }; - - code = Some(Code::from_str(&key_code) - .map_err(|_| format!("Invalid key code: {}", key_code))?); + + code = Some( + Code::from_str(&key_code) + .map_err(|_| format!("Invalid key code: {}", key_code))?, + ); } } } - let key_code = code.ok_or_else(|| format!("No valid key code found in shortcut: {}", shortcut))?; + let key_code = + code.ok_or_else(|| format!("No valid key code found in shortcut: {}", shortcut))?; Ok(HotKey::new(Some(modifiers), key_code)) } diff --git a/src-tauri/src/api/mod.rs b/src-tauri/src/api/mod.rs index e6643f9..e3f7afa 100644 --- a/src-tauri/src/api/mod.rs +++ b/src-tauri/src/api/mod.rs @@ -1,4 +1,4 @@ -pub mod updater; pub mod clipboard; -pub mod tray; pub mod hotkeys; +pub mod tray; +pub mod updater; diff --git a/src-tauri/src/api/tray.rs b/src-tauri/src/api/tray.rs index 6b78b8c..82dd843 100644 --- a/src-tauri/src/api/tray.rs +++ b/src-tauri/src/api/tray.rs @@ -1,5 +1,7 @@ use tauri::{ - menu::{MenuBuilder, MenuItemBuilder}, tray::TrayIconBuilder, Emitter, Manager + menu::{MenuBuilder, MenuItemBuilder}, + tray::TrayIconBuilder, + Emitter, Manager, }; pub fn setup(app: &mut tauri::App) -> Result<(), Box> { diff --git a/src-tauri/src/api/updater.rs b/src-tauri/src/api/updater.rs index b5d1b3c..a5e16f9 100644 --- a/src-tauri/src/api/updater.rs +++ b/src-tauri/src/api/updater.rs @@ -1,5 +1,5 @@ -use tauri::{AppHandle, async_runtime}; -use tauri_plugin_dialog::{DialogExt, MessageDialogKind, MessageDialogButtons}; +use tauri::{async_runtime, AppHandle}; +use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; use tauri_plugin_updater::UpdaterExt; pub async fn check_for_updates(app: AppHandle) { diff --git a/src-tauri/src/db/database.rs b/src-tauri/src/db/database.rs index 30867c5..4a3468d 100644 --- a/src-tauri/src/db/database.rs +++ b/src-tauri/src/db/database.rs @@ -1,8 +1,8 @@ +use include_dir::{include_dir, Dir}; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; use std::fs; use tauri::Manager; use tokio::runtime::Runtime as TokioRuntime; -use include_dir::{include_dir, Dir}; static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations"); @@ -49,39 +49,32 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box> { } async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box> { - println!("Starting migration process"); - - // Create schema_version table sqlx::query( "CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP - );" + );", ) .execute(pool) .await?; - let current_version: Option = sqlx::query_scalar( - "SELECT MAX(version) FROM schema_version" - ) - .fetch_one(pool) - .await?; + let current_version: Option = + sqlx::query_scalar("SELECT MAX(version) FROM schema_version") + .fetch_one(pool) + .await?; let current_version = current_version.unwrap_or(0); - println!("Current database version: {}", current_version); let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR .files() .filter_map(|file| { let file_name = file.path().file_name()?.to_str()?; - println!("Processing file: {}", file_name); if file_name.ends_with(".sql") && file_name.starts_with("migration") { let version: i64 = file_name .trim_start_matches("migration") .trim_end_matches(".sql") .parse() .ok()?; - println!("Found migration version: {}", version); Some((version, file.contents_utf8()?)) } else { None @@ -93,8 +86,6 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box current_version { - println!("Applying migration {}", version); - let statements: Vec<&str> = content .split(';') .map(|s| s.trim()) @@ -102,7 +93,6 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box Result<(), Box, - key: String + key: String, ) -> Result { let row = sqlx::query("SELECT value FROM settings WHERE key = ?") .bind(key) @@ -65,7 +63,7 @@ pub async fn get_setting( pub async fn save_setting( pool: tauri::State<'_, SqlitePool>, key: String, - value: String + value: String, ) -> Result<(), String> { sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") .bind(key) @@ -78,23 +76,18 @@ pub async fn save_setting( } #[tauri::command] -pub async fn get_keybind( - app_handle: tauri::AppHandle, -) -> Result, String> { +pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result, String> { let pool = app_handle.state::(); - + let row = sqlx::query("SELECT value FROM settings WHERE key = 'keybind'") .fetch_optional(&*pool) .await .map_err(|e| e.to_string())?; - let json = row - .map(|r| r.get::("value")) - .unwrap_or_else(|| { - serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()]) - .expect("Failed to serialize default keybind") - }); + let json = row.map(|r| r.get::("value")).unwrap_or_else(|| { + serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()]) + .expect("Failed to serialize default keybind") + }); - serde_json::from_str::>(&json) - .map_err(|e| e.to_string()) -} \ No newline at end of file + serde_json::from_str::>(&json).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/utils/favicon.rs b/src-tauri/src/utils/favicon.rs index 0091313..38321f3 100644 --- a/src-tauri/src/utils/favicon.rs +++ b/src-tauri/src/utils/favicon.rs @@ -4,7 +4,9 @@ use image::ImageFormat; use reqwest; use url::Url; -pub async fn fetch_favicon_as_base64(url: Url) -> Result, Box> { +pub async fn fetch_favicon_as_base64( + url: Url, +) -> Result, Box> { let client = reqwest::Client::new(); let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap()); let response = client.get(&favicon_url).send().await?; @@ -18,4 +20,4 @@ pub async fn fetch_favicon_as_base64(url: Url) -> Result, Box Date: Mon, 16 Dec 2024 23:53:29 +1000 Subject: [PATCH 04/16] feat: add logging for debugging --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 2 ++ src-tauri/src/utils/logger.rs | 49 +++++++++++++++++++++++++++++++++++ src-tauri/src/utils/mod.rs | 1 + 5 files changed, 54 insertions(+) create mode 100644 src-tauri/src/utils/logger.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7293135..a05f2b9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4187,6 +4187,7 @@ dependencies = [ "image", "include_dir", "lazy_static", + "log", "rand 0.8.5", "rdev", "regex", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ce4353d..fc33f12 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -40,6 +40,7 @@ lazy_static = "1.5.0" time = "0.3.37" global-hotkey = "0.6.3" chrono = { version = "0.4.39", features = ["serde"] } +log = { version = "0.4.22", features = ["std"] } uuid = "1.11.0" active-win-pos-rs = "0.8.3" include_dir = "0.7.4" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 36a3072..26d1048 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -34,6 +34,8 @@ fn main() { ) .setup(|app| { let app_data_dir = app.path().app_data_dir().unwrap(); + utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger"); + fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory"); let db_path = app_data_dir.join("data.db"); diff --git a/src-tauri/src/utils/logger.rs b/src-tauri/src/utils/logger.rs new file mode 100644 index 0000000..d2f1442 --- /dev/null +++ b/src-tauri/src/utils/logger.rs @@ -0,0 +1,49 @@ +use chrono; +use log::{LevelFilter, SetLoggerError}; +use std::fs::{File, OpenOptions}; +use std::io::Write; + +pub struct FileLogger { + file: File, +} + +impl log::Log for FileLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + let mut file = self.file.try_clone().expect("Failed to clone file handle"); + writeln!( + file, + "{} - {}: {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + record.level(), + record.args() + ) + .expect("Failed to write to log file"); + } + } + + fn flush(&self) { + self.file.sync_all().expect("Failed to flush log file"); + } +} + +pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> { + let logs_dir = app_data_dir.join("logs"); + std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory"); + + let log_path = logs_dir.join("app.log"); + let file = OpenOptions::new() + .create(true) + .append(true) + .open(log_path) + .expect("Failed to open log file"); + + let logger = Box::new(FileLogger { file }); + unsafe { log::set_logger_racy(Box::leak(logger))? }; + log::set_max_level(LevelFilter::Debug); + Ok(()) +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 5ff2fca..b888b1f 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod commands; pub mod favicon; pub mod types; +pub mod logger; From 345f7e3f09bc12f30072852406a6a71b6f8c86d2 Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Tue, 17 Dec 2024 00:00:21 +1000 Subject: [PATCH 05/16] fix: youtube thumbnail having strange formats --- assets/css/index.scss | 19 +------------------ pages/index.vue | 44 ++++++++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/assets/css/index.scss b/assets/css/index.scss index 33d4af9..43fe481 100644 --- a/assets/css/index.scss +++ b/assets/css/index.scss @@ -111,7 +111,7 @@ $mutedtext: #78756f; align-items: center; overflow: hidden; - &:not(:has(.full-image, .image)) { + &:not(:has(.image)) { padding: 8px; } @@ -119,23 +119,6 @@ $mutedtext: #78756f; font-family: CommitMono Nerd Font !important; } - .full-image { - width: 100%; - height: 100%; - position: relative; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - - img { - width: 100%; - height: 100%; - object-fit: contain; - object-position: center; - } - } - .image { width: 100%; height: 100%; diff --git a/pages/index.vue b/pages/index.vue index c218720..4d7be6a 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -87,19 +87,24 @@ +
Image
+
+ YouTube Thumbnail +
-
- YouTube Thumbnail -
- {{ selectedItem?.content || "" }} + {{ selectedItem?.content || "" }}
+ @@ -399,7 +404,9 @@ const scrollToSelectedItem = (forceScrollTop: boolean = false): void => { if (isAbove || isBelow) { const scrollOffset = isAbove - ? elementRect.top - viewportRect.top - (selectedItemIndex.value === 0 ? 36 : 8) + ? elementRect.top - + viewportRect.top - + (selectedItemIndex.value === 0 ? 36 : 8) : elementRect.bottom - viewportRect.bottom + 9; viewport.scrollBy({ top: scrollOffset, behavior: "smooth" }); @@ -504,8 +511,8 @@ const getYoutubeThumbnail = (url: string): string => { videoId = url.match(/[?&]v=([^&]+)/)?.[1]; } return videoId - ? `https://img.youtube.com/vi/${videoId}/0.jpg` - : "https://via.placeholder.com/150"; + ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg` + : "https://via.placeholder.com/1280x720"; }; const getFaviconFromDb = (favicon: string): string => { @@ -515,9 +522,9 @@ const getFaviconFromDb = (favicon: string): string => { const updateHistory = async (resetScroll: boolean = false): Promise => { const results = await $history.loadHistoryChunk(0, CHUNK_SIZE); if (results.length > 0) { - const existingIds = new Set(history.value.map(item => item.id)); - const uniqueNewItems = results.filter(item => !existingIds.has(item.id)); - + const existingIds = new Set(history.value.map((item) => item.id)); + const uniqueNewItems = results.filter((item) => !existingIds.has(item.id)); + const processedNewItems = await Promise.all( uniqueNewItems.map(async (item) => { const historyItem = new HistoryItem( @@ -567,7 +574,10 @@ const updateHistory = async (resetScroll: boolean = false): Promise => { history.value = [...processedNewItems, ...history.value]; - if (resetScroll && resultsContainer.value?.osInstance()?.elements().viewport) { + if ( + resetScroll && + resultsContainer.value?.osInstance()?.elements().viewport + ) { resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({ top: 0, behavior: "smooth", @@ -604,7 +614,9 @@ const setupEventListeners = async (): Promise => { const previousState = { groupIndex: selectedGroupIndex.value, itemIndex: selectedItemIndex.value, - scroll: resultsContainer.value?.osInstance()?.elements().viewport?.scrollTop || 0, + scroll: + resultsContainer.value?.osInstance()?.elements().viewport + ?.scrollTop || 0, }; await updateHistory(); @@ -612,7 +624,9 @@ const setupEventListeners = async (): Promise => { handleSelection(previousState.groupIndex, previousState.itemIndex, false); nextTick(() => { - const viewport = resultsContainer.value?.osInstance()?.elements().viewport; + const viewport = resultsContainer.value + ?.osInstance() + ?.elements().viewport; if (viewport) { viewport.scrollTo({ top: previousState.scroll, From 4ab938a3de060e4aa0a29cb2fd082d2f639af70c Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:15:20 +1000 Subject: [PATCH 06/16] feat: detect color --- pages/index.vue | 102 +++++++++++++++++++++++--------- src-tauri/src/api/clipboard.rs | 5 ++ src-tauri/src/utils/commands.rs | 46 ++++++++++++++ 3 files changed, 126 insertions(+), 27 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index 4d7be6a..0426d08 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -63,11 +63,13 @@ @error="onImageError" /> - Favicon + +
+ + + + + + + +
@@ -600,10 +656,7 @@ const setupEventListeners = async (): Promise => { await listen("clipboard-content-updated", async () => { lastUpdateTime.value = Date.now(); await updateHistory(true); - if ( - groupedHistory.value.length > 0 && - groupedHistory.value[0].items.length > 0 - ) { + if (groupedHistory.value[0]?.items.length > 0) { handleSelection(0, 0, false); } }); @@ -623,17 +676,12 @@ const setupEventListeners = async (): Promise => { lastUpdateTime.value = currentTime; handleSelection(previousState.groupIndex, previousState.itemIndex, false); - nextTick(() => { - const viewport = resultsContainer.value - ?.osInstance() - ?.elements().viewport; - if (viewport) { - viewport.scrollTo({ - top: previousState.scroll, - behavior: "instant", - }); - } - }); + if (resultsContainer.value?.osInstance()?.elements().viewport?.scrollTo) { + resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({ + top: previousState.scroll, + behavior: "instant", + }); + } } focusSearchInput(); }); @@ -749,7 +797,7 @@ const getFormattedDate = computed(() => { if (!selectedItem.value?.timestamp) return ""; return new Intl.DateTimeFormat("en-US", { dateStyle: "medium", - timeStyle: "short", + timeStyle: "medium", }).format(selectedItem.value.timestamp); }); diff --git a/src-tauri/src/api/clipboard.rs b/src-tauri/src/api/clipboard.rs index 927b70e..405ee12 100644 --- a/src-tauri/src/api/clipboard.rs +++ b/src-tauri/src/api/clipboard.rs @@ -164,6 +164,11 @@ pub fn setup(app: &AppHandle) { pool, HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language)) ).await; + } else if crate::utils::commands::detect_color(&text) { + let _ = db::history::add_history_item( + pool, + HistoryItem::new(app_name, ContentType::Color, text, None, app_icon, None) + ).await; } else { let _ = db::history::add_history_item( pool, diff --git a/src-tauri/src/utils/commands.rs b/src-tauri/src/utils/commands.rs index 13a5d04..6ffa6c9 100644 --- a/src-tauri/src/utils/commands.rs +++ b/src-tauri/src/utils/commands.rs @@ -49,3 +49,49 @@ fn _process_icon_to_base64(path: &str) -> Result bool { + let color = color.trim().to_lowercase(); + + // hex + if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() { + let hex = &color[1..]; + return match hex.len() { + 3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()), + _ => false + }; + } + + // rgb/rgba + if (color.starts_with("rgb(") || color.starts_with("rgba(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") { + let values = color + .trim_start_matches("rgba(") + .trim_start_matches("rgb(") + .trim_end_matches(')') + .split(',') + .collect::>(); + + return match values.len() { + 3 | 4 => values.iter().all(|v| v.trim().parse::().is_ok()), + _ => false + }; + } + + // hsl/hsla + if (color.starts_with("hsl(") || color.starts_with("hsla(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") { + let values = color + .trim_start_matches("hsla(") + .trim_start_matches("hsl(") + .trim_end_matches(')') + .split(',') + .collect::>(); + + return match values.len() { + 3 | 4 => values.iter().all(|v| v.trim().parse::().is_ok()), + _ => false + }; + } + + false +} \ No newline at end of file From 460888a1e72cd8373905e8e3ce98bd87e1b37147 Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:15:34 +1000 Subject: [PATCH 07/16] feat: move item to the top if copied again --- src-tauri/src/db/history.rs | 57 +++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/db/history.rs b/src-tauri/src/db/history.rs index 67895e5..fc24bcb 100644 --- a/src-tauri/src/db/history.rs +++ b/src-tauri/src/db/history.rs @@ -59,33 +59,42 @@ pub async fn add_history_item( let (id, source, source_icon, content_type, content, favicon, timestamp, language) = item.to_row(); - let last_content: Option = sqlx::query_scalar( - "SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1", - ) - .bind(content_type.clone()) - .fetch_one(&*pool) - .await - .unwrap_or(None); + let existing = sqlx::query("SELECT id FROM history WHERE content = ? AND content_type = ?") + .bind(&content) + .bind(&content_type) + .fetch_optional(&*pool) + .await + .map_err(|e| e.to_string())?; - if last_content.as_deref() == Some(&content) { - return Ok(()); + match existing { + Some(_) => { + sqlx::query( + "UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?" + ) + .bind(&content) + .bind(&content_type) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; + } + None => { + sqlx::query( + "INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + ) + .bind(id) + .bind(source) + .bind(source_icon) + .bind(content_type) + .bind(content) + .bind(favicon) + .bind(timestamp) + .bind(language) + .execute(&*pool) + .await + .map_err(|e| e.to_string())?; + } } - sqlx::query( - "INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" - ) - .bind(id) - .bind(source) - .bind(source_icon) - .bind(content_type) - .bind(content) - .bind(favicon) - .bind(timestamp) - .bind(language) - .execute(&*pool) - .await - .map_err(|e| e.to_string())?; - Ok(()) } From ab57f1fa72131c3557a1b61bda85197ce1f08ca2 Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:15:44 +1000 Subject: [PATCH 08/16] fix: icon inconsitencies --- assets/css/index.scss | 2 +- public/icons/Code.svg | 13 +++++-------- public/icons/File.svg | 13 +++++-------- public/icons/Link.svg | 7 +++++++ public/icons/Text.svg | 13 +++++-------- 5 files changed, 23 insertions(+), 25 deletions(-) create mode 100644 public/icons/Link.svg diff --git a/assets/css/index.scss b/assets/css/index.scss index 43fe481..da464c5 100644 --- a/assets/css/index.scss +++ b/assets/css/index.scss @@ -89,7 +89,7 @@ $mutedtext: #78756f; } .icon { - width: 20px; + width: 18px; height: 18px; } } diff --git a/public/icons/Code.svg b/public/icons/Code.svg index b5c1a42..5c34abf 100644 --- a/public/icons/Code.svg +++ b/public/icons/Code.svg @@ -1,10 +1,7 @@ - - - - + + + + + \ No newline at end of file diff --git a/public/icons/File.svg b/public/icons/File.svg index a0b4fdd..8341a3c 100644 --- a/public/icons/File.svg +++ b/public/icons/File.svg @@ -1,10 +1,7 @@ - - - - + + + + + \ No newline at end of file diff --git a/public/icons/Link.svg b/public/icons/Link.svg new file mode 100644 index 0000000..64cc120 --- /dev/null +++ b/public/icons/Link.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/icons/Text.svg b/public/icons/Text.svg index 83f8532..0ca21e8 100644 --- a/public/icons/Text.svg +++ b/public/icons/Text.svg @@ -1,10 +1,7 @@ - - - - + + + + + \ No newline at end of file From c42141f7c72112129b4078406cbd234addac947b Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:56:12 +1000 Subject: [PATCH 09/16] fix: overflow for urls --- assets/css/index.scss | 51 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/assets/css/index.scss b/assets/css/index.scss index da464c5..a013480 100644 --- a/assets/css/index.scss +++ b/assets/css/index.scss @@ -233,34 +233,39 @@ $mutedtext: #78756f; display: flex; gap: 0; flex-direction: column; - } - .info-row { - display: flex; - width: 100%; - font-size: 12px; - justify-content: space-between; - padding: 8px 0; - border-bottom: 1px solid $divider; + .info-row { + display: flex; + width: 100%; + font-size: 12px; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid $divider; - &:last-child { - border-bottom: none; - padding-bottom: 0; - } + &:last-child { + border-bottom: none; + padding-bottom: 0; + } - &:first-child { - padding-top: 22px; - } + &:first-child { + padding-top: 22px; + } - p { - font-family: SFRoundedMedium; - color: $text2; - font-weight: 500; - } + p { + font-family: SFRoundedMedium; + color: $text2; + font-weight: 500; + flex-shrink: 0; + } - span { - font-family: CommitMono; - color: $text; + span { + font-family: CommitMono; + color: $text; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + margin-left: 16px; + } } } } From 5943fc86fb8310ac4041d93432280a46f38c14d4 Mon Sep 17 00:00:00 2001 From: PandaDEV <70103896+0PandaDEV@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:57:10 +1000 Subject: [PATCH 10/16] feat: better implementation of info rows --- pages/index.vue | 299 ++++++++++++++++++++--------------- src-tauri/src/utils/types.rs | 20 +-- types/types.ts | 21 +-- 3 files changed, 190 insertions(+), 150 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index 0426d08..9c5b52e 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -129,130 +129,10 @@ class="information" :options="{ scrollbars: { autoHide: 'scroll' } }">
Information
-
- -
-

Source

- {{ selectedItem.source }} -
-
-

Content Type

- {{ - selectedItem.content_type.charAt(0).toUpperCase() + - selectedItem.content_type.slice(1) - }} -
- - - - - - - - - - - - - - - - - - - - -
-

Copied

- {{ getFormattedDate }} +
+
+

{{ row.label }}

+ {{ row.value }}
@@ -269,7 +149,9 @@ import { platform } from "@tauri-apps/plugin-os"; import { enable, isEnabled } from "@tauri-apps/plugin-autostart"; import { listen } from "@tauri-apps/api/event"; import { useNuxtApp } from "#app"; +import { invoke } from "@tauri-apps/api/core"; import { HistoryItem, ContentType } from "~/types/types"; +import type { InfoText, InfoImage, InfoFile, InfoLink, InfoColor, InfoCode } from "~/types/types"; interface GroupedHistory { label: string; @@ -299,6 +181,8 @@ const imageSizes = shallowRef>({}); const lastUpdateTime = ref(Date.now()); const imageLoadError = ref(false); const imageLoading = ref(false); +const pageTitle = ref(''); +const pageOgImage = ref(''); const keyboard = useKeyboard(); @@ -385,7 +269,9 @@ const loadHistoryChunk = async (): Promise => { item.source, item.content_type, item.content, - item.favicon + item.favicon, + item.source_icon, + item.language ); Object.assign(historyItem, { id: item.id, @@ -776,11 +662,6 @@ watch([selectedGroupIndex, selectedItemIndex], () => scrollToSelectedItem(false) ); -const getComputedImageUrl = (item: HistoryItem | null): string => { - if (!item) return ""; - return imageUrls.value[item.id] || ""; -}; - const getCharacterCount = computed(() => { return selectedItem.value?.content.length ?? 0; }); @@ -808,6 +689,164 @@ const formatFileSize = (bytes: number): string => { const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; }; + +const fetchPageMeta = async (url: string) => { + try { + console.log('Fetching metadata for:', url); + const [title, ogImage] = await invoke('fetch_page_meta', { url }) as [string, string | null]; + console.log('Received title:', title); + pageTitle.value = title; + if (ogImage) { + pageOgImage.value = ogImage; + } + } catch (error) { + console.error('Error fetching page meta:', error); + pageTitle.value = 'Error loading title'; + } +}; + +watch(() => selectedItem.value, (newItem) => { + if (newItem?.content_type === ContentType.Link) { + pageTitle.value = 'Loading...'; + pageOgImage.value = ''; + fetchPageMeta(newItem.content); + } else { + pageTitle.value = ''; + pageOgImage.value = ''; + } +}); + +const getInfo = computed(() => { + if (!selectedItem.value) return null; + + const baseInfo = { + source: selectedItem.value.source, + copied: selectedItem.value.timestamp, + }; + + const infoMap: Record InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode> = { + [ContentType.Text]: () => ({ + ...baseInfo, + content_type: ContentType.Text, + characters: selectedItem.value!.content.length, + words: selectedItem.value!.content.trim().split(/\s+/).length, + }), + [ContentType.Image]: () => ({ + ...baseInfo, + content_type: ContentType.Image, + dimensions: imageDimensions.value[selectedItem.value!.id] || "Loading...", + size: parseInt(imageSizes.value[selectedItem.value!.id] || "0"), + }), + [ContentType.File]: () => ({ + ...baseInfo, + content_type: ContentType.File, + path: selectedItem.value!.content, + filesize: 0, + }), + [ContentType.Link]: () => ({ + ...baseInfo, + content_type: ContentType.Link, + title: pageTitle.value, + url: selectedItem.value!.content, + characters: selectedItem.value!.content.length, + }), + [ContentType.Color]: () => { + const hex = selectedItem.value!.content; + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + + const rNorm = r / 255; + const gNorm = g / 255; + const bNorm = b / 255; + + const max = Math.max(rNorm, gNorm, bNorm); + const min = Math.min(rNorm, gNorm, bNorm); + let h = 0, s = 0; + const l = (max + min) / 2; + + if (max !== min) { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case rNorm: + h = (gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0); + break; + case gNorm: + h = (bNorm - rNorm) / d + 2; + break; + case bNorm: + h = (rNorm - gNorm) / d + 4; + break; + } + h /= 6; + } + + return { + ...baseInfo, + content_type: ContentType.Color, + hex: hex, + rgb: `rgb(${r}, ${g}, ${b})`, + hsl: `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`, + }; + }, + [ContentType.Code]: () => ({ + ...baseInfo, + content_type: ContentType.Code, + language: selectedItem.value!.language ?? "Unknown", + lines: selectedItem.value!.content.split('\n').length, + }), + }; + + return infoMap[selectedItem.value.content_type](); +}); + +const infoRows = computed(() => { + if (!getInfo.value) return []; + + const commonRows = [ + { label: "Source", value: getInfo.value.source }, + { label: "Content Type", value: getInfo.value.content_type.charAt(0).toUpperCase() + getInfo.value.content_type.slice(1) }, + ]; + + const typeSpecificRows: Record> = { + [ContentType.Text]: [ + { label: "Characters", value: (getInfo.value as InfoText).characters }, + { label: "Words", value: (getInfo.value as InfoText).words }, + ], + [ContentType.Image]: [ + { label: "Dimensions", value: (getInfo.value as InfoImage).dimensions }, + { label: "Image size", value: formatFileSize((getInfo.value as InfoImage).size) }, + ], + [ContentType.File]: [ + { label: "Path", value: (getInfo.value as InfoFile).path }, + ], + [ContentType.Link]: [ + { label: "Title", value: (getInfo.value as InfoLink).title || "No Title Found" }, + { label: "URL", value: (getInfo.value as InfoLink).url }, + { label: "Characters", value: (getInfo.value as InfoLink).characters }, + ], + [ContentType.Color]: [ + { label: "Hex Code", value: (getInfo.value as InfoColor).hex }, + { label: "RGB", value: (getInfo.value as InfoColor).rgb }, + { label: "HSL", value: (getInfo.value as InfoColor).hsl }, + ], + [ContentType.Code]: [ + { label: "Language", value: (getInfo.value as InfoCode).language }, + { label: "Lines", value: (getInfo.value as InfoCode).lines }, + ], + }; + + const specificRows = typeSpecificRows[getInfo.value.content_type] + .filter(row => row.value !== ""); + + return [ + ...commonRows, + ...specificRows, + { label: "Copied", value: getFormattedDate.value }, + ]; +});