diff --git a/assets/css/index.scss b/assets/css/index.scss index 003253d..db473f6 100644 --- a/assets/css/index.scss +++ b/assets/css/index.scss @@ -89,7 +89,7 @@ $mutedtext: #78756f; } .icon { - width: 20px; + width: 18px; height: 18px; } } @@ -98,32 +98,32 @@ $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(.image)) { + padding: 8px; + } span { font-family: CommitMono Nerd Font !important; } - .full-image { - width: 100%; - aspect-ratio: 16 / 9; - object-fit: cover; - 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 +212,9 @@ $mutedtext: #78756f; .information { position: absolute; + display: flex; + flex-direction: column; + gap: 14px; bottom: 40px; left: 284px; height: 160px; @@ -225,6 +228,47 @@ $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; + line-height: 1; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + &:first-child { + padding-top: 22px; + } + + p { + font-family: SFRoundedMedium; + color: $text2; + font-weight: 500; + flex-shrink: 0; + } + + span { + font-family: CommitMono; + color: $text; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + margin-left: 32px; + } + } + } } .clothoid-corner { diff --git a/pages/index.vue b/pages/index.vue index 22a727d..c902e9b 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -63,11 +63,13 @@ @error="onImageError" /> - Favicon + +
+ + + + + + + +
-
- Image + +
+ Image +
+
+ YouTube Thumbnail +
+
+ Image
- YouTube Thumbnail - {{ selectedItem?.content || "" }} + {{ selectedItem?.content || "" }} -
+ +
Information
-
+
+
+

{{ row.label }}

+ + {{ row.value }} + +
+
+
@@ -114,17 +156,18 @@ 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; 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,9 +184,12 @@ 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); +const pageTitle = ref(''); +const pageOgImage = ref(''); const keyboard = useKeyboard(); @@ -230,7 +276,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, @@ -238,10 +286,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 +353,9 @@ 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 +421,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; @@ -386,73 +460,78 @@ 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 => { 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)); - if ( - resetScroll && - resultsContainer.value?.osInstance()?.elements().viewport - ) { - resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({ - top: 0, - behavior: "smooth", - }); + 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 (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 +545,13 @@ 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[0]?.items.length > 0) { + handleSelection(0, 0, false); + } }); await listen("tauri://focus", async () => { @@ -506,17 +569,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(); }); @@ -558,7 +616,6 @@ const setupEventListeners = async (): Promise => { if (isMacActionCombo || isOtherOsActionCombo) { event.preventDefault(); - console.log("Actions shortcut triggered"); } }); }; @@ -611,40 +668,179 @@ 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 getFormattedDate = computed(() => { + if (!selectedItem.value?.timestamp) return ""; + return new Intl.DateTimeFormat("en-US", { + dateStyle: "medium", + timeStyle: "medium", + }).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]}`; }; -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 fetchPageMeta = async (url: string) => { + try { + const [title, ogImage] = await invoke('fetch_page_meta', { url }) as [string, string | null]; + pageTitle.value = title; + if (ogImage) { + pageOgImage.value = ogImage; } + } catch (error) { + console.error('Error fetching page meta:', error); + pageTitle.value = 'Error loading title'; } }; -const getComputedImageUrl = (item: HistoryItem | null): string => { - if (!item) return ""; - return imageUrls.value[item.id] || ""; -}; +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, isUrl: false }, + { label: "Content Type", value: getInfo.value.content_type.charAt(0).toUpperCase() + getInfo.value.content_type.slice(1), isUrl: false }, + ]; + + 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]: [ + ...((getInfo.value as InfoLink).title && (getInfo.value as InfoLink).title !== 'Loading...' + ? [{ label: "Title", value: (getInfo.value as InfoLink).title || '' }] + : []), + { label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true }, + { label: "Characters", value: (getInfo.value as InfoLink).characters }, + ], + [ContentType.Color]: [ + { label: "Hex", 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 }, + ]; +}); \ 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/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 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a17efef..06b6c81 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "active-win-pos-rs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9c1d770875c536934a8e7150061b0dbddb919298f0ff762b0f8fc12c8928877" +checksum = "e227f8493de9f5e493f8e762ac7516d2ae42464df2e8122fcafd604f0b16c634" dependencies = [ "appkit-nsworkspace-bindings", "core-foundation 0.9.4", @@ -110,6 +110,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" @@ -411,6 +436,21 @@ dependencies = [ "which", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit_field" version = "0.10.2" @@ -496,6 +536,17 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata 0.1.10", +] + [[package]] name = "built" version = "0.7.4" @@ -725,6 +776,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 +800,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 +808,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 +861,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" @@ -2500,6 +2587,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" @@ -2793,6 +2889,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" @@ -2847,6 +2957,18 @@ dependencies = [ "tendril", ] +[[package]] +name = "markup5ever_rcdom" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "matches" version = "0.1.10" @@ -2888,6 +3010,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "meta_fetcher" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9da2f9745ec127e7852cb4ee6d4ab6c7125e029b7a69db6010b1fe538b77cb" +dependencies = [ + "anyhow", + "select", + "texting_robots", + "ureq", +] + [[package]] name = "mime" version = "0.3.17" @@ -3068,6 +3202,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" @@ -3509,6 +3654,19 @@ 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" @@ -3904,12 +4062,15 @@ name = "qopy" version = "0.2.1" dependencies = [ "active-win-pos-rs", + "applications", "base64 0.22.1", "chrono", "global-hotkey", "image", "include_dir", "lazy_static", + "log", + "meta_fetcher", "rand 0.8.5", "rdev", "regex", @@ -4127,7 +4288,7 @@ dependencies = [ "maybe-rayon", "new_debug_unreachable", "noop_proc_macro", - "num-derive", + "num-derive 0.4.2", "num-traits", "once_cell", "paste", @@ -4235,10 +4396,16 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.8", "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-automata" version = "0.4.8" @@ -4417,6 +4584,7 @@ version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -4544,6 +4712,17 @@ dependencies = [ "libc", ] +[[package]] +name = "select" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9da09dc3f4dfdb6374cbffff7a2cffcec316874d4429899eefdc97b3b94dcd" +dependencies = [ + "bit-set", + "html5ever", + "markup5ever_rcdom", +] + [[package]] name = "selectors" version = "0.22.0" @@ -5444,6 +5623,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 +5769,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 +5947,22 @@ dependencies = [ "utf-8", ] +[[package]] +name = "texting_robots" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82a718a28dda2e67ad6e0464597b58eae39e2e4d0451e03d1028d71e81bb4a" +dependencies = [ + "anyhow", + "bstr", + "lazy_static", + "nom", + "percent-encoding", + "regex", + "thiserror 1.0.63", + "url", +] + [[package]] name = "thin-slice" version = "0.1.1" @@ -6200,6 +6405,22 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots", +] + [[package]] name = "url" version = "2.5.4" @@ -6604,6 +6825,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 +7354,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "xml5ever" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "yoke" version = "0.7.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index be1fba0..36b5af1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,7 +22,7 @@ tauri-plugin-updater = "2.3.0" tauri-plugin-dialog = "2.2.0" tauri-plugin-fs = "2.2.0" tauri-plugin-clipboard = "2.1.11" -tauri-plugin-prevent-default = "1.0.0" +tauri-plugin-prevent-default = "1.0.1" tauri-plugin-global-shortcut = "2.2.0" sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite", "chrono"] } serde = { version = "1.0.216", features = ["derive"] } @@ -32,7 +32,7 @@ rdev = "0.5.3" rand = "0.8.5" base64 = "0.22.1" image = "0.25.5" -reqwest = { version = "0.12.9", features = ["blocking"] } +reqwest = { version = "0.12.9", features = ["json", "blocking"] } url = "2.5.4" regex = "1.11.1" sha2 = "0.10.8" @@ -40,9 +40,13 @@ 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" +active-win-pos-rs = "0.8.4" include_dir = "0.7.4" +# hyperpolyglot = { git = "https://github.com/0pandadev/hyperpolyglot" } +applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "dev" } +meta_fetcher = "0.1.1" [features] custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/src/api/clipboard.rs b/src-tauri/src/api/clipboard.rs index 28debc3..76f245f 100644 --- a/src-tauri/src/api/clipboard.rs +++ b/src-tauri/src/api/clipboard.rs @@ -1,20 +1,21 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +// use hyperpolyglot; use lazy_static::lazy_static; use rdev::{simulate, EventType, Key}; +use regex::Regex; use sqlx::SqlitePool; -use uuid::Uuid; use std::fs; use std::sync::atomic::{AtomicBool, Ordering}; use std::{thread, time::Duration}; use tauri::{AppHandle, Emitter, Listener, Manager, Runtime}; use tauri_plugin_clipboard::Clipboard; use tokio::runtime::Runtime as TokioRuntime; -use regex::Regex; use url::Url; -use base64::{Engine, engine::general_purpose::STANDARD}; +use uuid::Uuid; +use crate::db; use crate::utils::commands::get_app_info; use crate::utils::favicon::fetch_favicon_as_base64; -use crate::db; use crate::utils::types::{ContentType, HistoryItem}; lazy_static! { @@ -111,43 +112,70 @@ pub fn setup(app: &AppHandle) { .unwrap_or_else(|e| e); let _ = db::history::add_history_item( pool, - HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon), + HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon, None), ).await; } } else if available_types.files { println!("Handling files change"); if let Ok(files) = clipboard.read_files() { - let files_str = files.join(", "); - let _ = db::history::add_history_item( - pool, - HistoryItem::new(app_name, ContentType::File, files_str, None, app_icon), - ).await; + for file in files { + let _ = db::history::add_history_item( + pool.clone(), + HistoryItem::new( + app_name.clone(), + ContentType::File, + file, + None, + app_icon.clone(), + None + ), + ).await; + } } } else if available_types.text { println!("Handling text change"); if let Ok(text) = clipboard.read_text() { let url_regex = Regex::new(r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$").unwrap(); - + if url_regex.is_match(&text) { if let Ok(url) = Url::parse(&text) { let favicon = match fetch_favicon_as_base64(url).await { Ok(Some(f)) => Some(f), _ => None, }; - + let _ = db::history::add_history_item( pool, - HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon) + HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon, None) ).await; } } else { if text.is_empty() { return; } - let _ = db::history::add_history_item( - pool, - HistoryItem::new(app_name, ContentType::Text, text, None, app_icon) - ).await; + + // Temporarily disabled code detection + /*if let Some(detection) = hyperpolyglot::detect_from_text(&text) { + let language = match detection { + hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(), + _ => detection.language().to_string(), + }; + + let _ = db::history::add_history_item( + 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, + HistoryItem::new(app_name, ContentType::Text, text, None, app_icon, None) + ).await; + } } } } else { @@ -183,16 +211,19 @@ pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> { Ok(()) } -async fn save_image_to_file(app_handle: &AppHandle, base64_data: &str) -> Result> { +async fn save_image_to_file( + app_handle: &AppHandle, + base64_data: &str, +) -> Result> { let app_data_dir = app_handle.path().app_data_dir().unwrap(); let images_dir = app_data_dir.join("images"); fs::create_dir_all(&images_dir)?; - + let file_name = format!("{}.png", Uuid::new_v4()); let file_path = images_dir.join(&file_name); - + let bytes = STANDARD.decode(base64_data)?; fs::write(&file_path, bytes)?; - + Ok(file_path.to_string_lossy().into_owned()) -} \ No newline at end of file +} 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) -> Result, String> { let rows = sqlx::query( - "SELECT id, source, source_icon, content_type, content, favicon, timestamp FROM history ORDER BY timestamp DESC", + "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC", ) .fetch_all(&*pool) .await @@ -44,6 +44,7 @@ pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result, item: HistoryItem, ) -> Result<(), String> { - let (id, source, source_icon, content_type, content, favicon, timestamp) = item.to_row(); + 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) VALUES (?, ?, ?, ?, ?, ?, ?)" - ) - .bind(id) - .bind(source) - .bind(source_icon) - .bind(content_type) - .bind(content) - .bind(favicon) - .bind(timestamp) - .execute(&*pool) - .await - .map_err(|e| e.to_string())?; - Ok(()) } @@ -93,7 +105,7 @@ pub async fn search_history( ) -> Result, String> { let query = format!("%{}%", query); let rows = sqlx::query( - "SELECT id, source, source_icon, content_type, content, favicon, timestamp 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" ) .bind(query) .fetch_all(&*pool) @@ -110,6 +122,7 @@ pub async fn search_history( content: row.get("content"), favicon: row.get("favicon"), timestamp: row.get("timestamp"), + language: row.get("language"), }) .collect(); @@ -123,7 +136,7 @@ pub async fn load_history_chunk( limit: i64, ) -> Result, String> { let rows = sqlx::query( - "SELECT id, source, source_icon, content_type, content, favicon, timestamp FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?" + "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?" ) .bind(limit) .bind(offset) @@ -141,6 +154,7 @@ pub async fn load_history_chunk( content: row.get("content"), favicon: row.get("favicon"), timestamp: row.get("timestamp"), + language: row.get("language"), }) .collect(); diff --git a/src-tauri/src/db/migrations/migration2.sql b/src-tauri/src/db/migrations/migration2.sql index 648cff0..312c5ef 100644 --- a/src-tauri/src/db/migrations/migration2.sql +++ b/src-tauri/src/db/migrations/migration2.sql @@ -1,2 +1,3 @@ ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL; ALTER TABLE history ADD COLUMN source_icon TEXT; +ALTER TABLE history ADD COLUMN language TEXT; diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index ce02d97..8c147c6 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -1,3 +1,3 @@ pub mod database; pub mod history; -pub mod settings; \ No newline at end of file +pub mod settings; diff --git a/src-tauri/src/db/settings.rs b/src-tauri/src/db/settings.rs index 280d701..406bee0 100644 --- a/src-tauri/src/db/settings.rs +++ b/src-tauri/src/db/settings.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; -use sqlx::SqlitePool; use serde_json; -use tauri::{Emitter, Manager}; use sqlx::Row; +use sqlx::SqlitePool; +use tauri::{Emitter, Manager}; #[derive(Deserialize, Serialize)] struct KeybindSetting { @@ -15,12 +15,10 @@ pub async fn initialize_settings(pool: &SqlitePool) -> 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/main.rs b/src-tauri/src/main.rs index 36a3072..0621936 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"); @@ -110,6 +112,7 @@ fn main() { db::settings::save_setting, db::settings::save_keybind, db::settings::get_keybind, + utils::commands::fetch_page_meta, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/utils/commands.rs b/src-tauri/src/utils/commands.rs index 5b1a736..f87a866 100644 --- a/src-tauri/src/utils/commands.rs +++ b/src-tauri/src/utils/commands.rs @@ -1,4 +1,8 @@ +use active_win_pos_rs::get_active_window; +use base64::{engine::general_purpose::STANDARD, Engine}; +use image::codecs::png::PngEncoder; use tauri::PhysicalPosition; +use meta_fetcher; pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) { if let Some(monitor) = window.available_monitors().unwrap().iter().find(|m| { @@ -27,4 +31,79 @@ pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) { )) .unwrap(); } +} + +pub fn get_app_info() -> (String, Option) { + match get_active_window() { + Ok(window) => { + let app_name = window.app_name; + (app_name, None) + } + Err(_) => ("System".to_string(), None), + } +} + +fn _process_icon_to_base64(path: &str) -> Result> { + let img = image::open(path)?; + let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3); + let mut png_buffer = Vec::new(); + resized.write_with_encoder(PngEncoder::new(&mut png_buffer))?; + Ok(STANDARD.encode(png_buffer)) +} + + +pub fn detect_color(color: &str) -> 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 +} + +#[tauri::command] +pub async fn fetch_page_meta(url: String) -> Result<(String, Option), String> { + let metadata = meta_fetcher::fetch_metadata(&url) + .map_err(|e| format!("Failed to fetch metadata: {}", e))?; + + Ok(( + metadata.title.unwrap_or_else(|| "No title found".to_string()), + metadata.image + )) } \ No newline at end of file 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 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 35b6d67..b888b1f 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,3 +1,4 @@ -pub mod types; pub mod commands; -pub mod favicon; \ No newline at end of file +pub mod favicon; +pub mod types; +pub mod logger; diff --git a/src-tauri/src/utils/types.rs b/src-tauri/src/utils/types.rs index 94f9bab..76c846e 100644 --- a/src-tauri/src/utils/types.rs +++ b/src-tauri/src/utils/types.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use std::fmt; use uuid::Uuid; @@ -12,6 +12,7 @@ pub struct HistoryItem { pub content: String, pub favicon: Option, pub timestamp: DateTime, + pub language: Option, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] @@ -26,7 +27,7 @@ pub enum ContentType { } #[derive(Debug, Deserialize, Serialize)] -pub struct Text { +pub struct InfoText { pub source: String, pub content_type: ContentType, pub characters: i32, @@ -35,7 +36,7 @@ pub struct Text { } #[derive(Debug, Deserialize, Serialize)] -pub struct Image { +pub struct InfoImage { pub source: String, pub content_type: ContentType, pub dimensions: String, @@ -44,7 +45,7 @@ pub struct Image { } #[derive(Debug, Deserialize, Serialize)] -pub struct File { +pub struct InfoFile { pub source: String, pub content_type: ContentType, pub path: String, @@ -53,26 +54,26 @@ pub struct File { } #[derive(Debug, Deserialize, Serialize)] -pub struct Link { +pub struct InfoLink { pub source: String, pub content_type: ContentType, - pub title: String, - pub link: String, + pub title: Option, + pub url: String, pub characters: i32, pub copied: DateTime, } #[derive(Debug, Deserialize, Serialize)] -pub struct Color { +pub struct InfoColor { pub source: String, pub content_type: ContentType, - pub hexcode: String, - pub rgba: String, + pub hex: String, + pub rgb: String, pub copied: DateTime, } #[derive(Debug, Deserialize, Serialize)] -pub struct Code { +pub struct InfoCode { pub source: String, pub content_type: ContentType, pub language: String, @@ -108,7 +109,14 @@ impl From for ContentType { } impl HistoryItem { - pub fn new(source: String, content_type: ContentType, content: String, favicon: Option, source_icon: Option) -> Self { + pub fn new( + source: String, + content_type: ContentType, + content: String, + favicon: Option, + source_icon: Option, + language: Option, + ) -> Self { Self { id: Uuid::new_v4().to_string(), source, @@ -117,10 +125,22 @@ impl HistoryItem { content, favicon, timestamp: Utc::now(), + language, } } - pub fn to_row(&self) -> (String, String, Option, String, String, Option, DateTime) { + pub fn to_row( + &self, + ) -> ( + String, + String, + Option, + String, + String, + Option, + DateTime, + Option, + ) { ( self.id.clone(), self.source.clone(), @@ -129,6 +149,7 @@ impl HistoryItem { self.content.clone(), self.favicon.clone(), self.timestamp, + self.language.clone(), ) } } diff --git a/types/types.ts b/types/types.ts index 99b8cd4..43cdb16 100644 --- a/types/types.ts +++ b/types/types.ts @@ -1,4 +1,4 @@ -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; export enum ContentType { Text = "text", @@ -11,21 +11,52 @@ export enum ContentType { export class HistoryItem { id: string; + source: string; + source_icon?: string; content_type: ContentType; content: string; favicon?: string; timestamp: Date; + language?: string; - constructor(content_type: ContentType, content: string, favicon?: string) { + constructor( + source: string, + content_type: ContentType, + content: string, + favicon?: string, + source_icon?: string, + language?: string + ) { this.id = uuidv4(); + this.source = source; + this.source_icon = source_icon; this.content_type = content_type; this.content = content; this.favicon = favicon; this.timestamp = new Date(); + this.language = language; } - toRow(): [string, string, string, string | undefined, Date] { - return [this.id, this.content_type, this.content, this.favicon, this.timestamp]; + toRow(): [ + string, + string, + string | undefined, + string, + string, + string | undefined, + Date, + string | undefined + ] { + return [ + this.id, + this.source, + this.source_icon, + this.content_type, + this.content, + this.favicon, + this.timestamp, + this.language, + ]; } } @@ -33,3 +64,53 @@ export interface Settings { key: string; value: string; } + +export interface InfoText { + source: string; + content_type: ContentType.Text; + characters: number; + words: number; + copied: Date; +} + +export interface InfoImage { + source: string; + content_type: ContentType.Image; + dimensions: string; + size: number; + copied: Date; +} + +export interface InfoFile { + source: string; + content_type: ContentType.File; + path: string; + filesize: number; + copied: Date; +} + +export interface InfoLink { + source: string; + content_type: ContentType.Link; + title?: string; + url: string; + characters: number; + copied: Date; +} + +export interface InfoColor { + source: string; + content_type: ContentType.Color; + hex: string; + rgb: string; + hsl: string; + copied: Date; +} + +export interface InfoCode { + source: string; + content_type: ContentType.Code; + language: string; + lines: number; + copied: Date; +}