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]}`; +};