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]()
+
+
-
+
+
![YouTube Thumbnail]()
+
{{ selectedItem?.content || "" }}
-
+
+
+
+
Source
+
{{ selectedItem.source }}
+
+
+
Content Type
+
{{
+ selectedItem.content_type.charAt(0).toUpperCase() +
+ selectedItem.content_type.slice(1)
+ }}
+
+
+
+
+
+
Characters
+
{{ getCharacterCount }}
+
+
+
Words
+
{{ getWordCount }}
+
+
+
+
+
+
+
Dimensions
+
{{ imageDimensions[selectedItem.id] || "Loading..." }}
+
+
+
Image size
+
{{ imageSizes[selectedItem.id] || "Loading..." }}
+
+
+
+
+
+
+
Path
+
{{ selectedItem.content }}
+
+
+
+
+
+
+
URL
+
{{ selectedItem.content }}
+
+
+
Characters
+
{{ getCharacterCount }}
+
+
+
+
+
+
+
+
Value
+
{{ selectedItem.content }}
+
+
+
+
+
+
+
Language
+
{{ selectedItem.language }}
+
+
+
Lines
+
{{ getLineCount }}
+
+
+
+
+
+
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]}`;
+};