mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 13:14:04 +02:00
feat: add information and update chunk loading
This commit is contained in:
parent
c48a0d8239
commit
149e72802c
7 changed files with 435 additions and 151 deletions
|
@ -98,15 +98,22 @@ $mutedtext: #78756f;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 53px;
|
top: 53px;
|
||||||
left: 284px;
|
left: 284px;
|
||||||
padding: 8px;
|
height: calc(100vh - 254px);
|
||||||
height: calc(100vh - 256px);
|
|
||||||
font-family: CommitMono Nerd Font !important;
|
font-family: CommitMono Nerd Font !important;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
letter-spacing: 1;
|
letter-spacing: 1;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width: calc(100vw - 286px);
|
width: calc(100vw - 286px);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:not(:has(.full-image, .image)) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-family: CommitMono Nerd Font !important;
|
font-family: CommitMono Nerd Font !important;
|
||||||
|
@ -114,16 +121,26 @@ $mutedtext: #78756f;
|
||||||
|
|
||||||
.full-image {
|
.full-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 16 / 9;
|
height: 100%;
|
||||||
object-fit: cover;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
max-height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
object-position: top left;
|
object-position: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +229,9 @@ $mutedtext: #78756f;
|
||||||
|
|
||||||
.information {
|
.information {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
left: 284px;
|
left: 284px;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
|
@ -225,6 +245,41 @@ $mutedtext: #78756f;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 0.6px;
|
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 {
|
.clothoid-corner {
|
||||||
|
|
319
pages/index.vue
319
pages/index.vue
|
@ -87,20 +87,114 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
<div class="content" v-if="selectedItem?.content_type === 'image'">
|
<div
|
||||||
<img :src="getComputedImageUrl(selectedItem)" alt="Image" class="image" />
|
class="content"
|
||||||
|
v-if="selectedItem?.content_type === ContentType.Image">
|
||||||
|
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
|
||||||
</div>
|
</div>
|
||||||
<OverlayScrollbarsComponent v-else class="content">
|
<OverlayScrollbarsComponent v-else class="content">
|
||||||
|
<div v-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)" class="full-image">
|
||||||
<img
|
<img
|
||||||
v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)"
|
|
||||||
:src="getYoutubeThumbnail(selectedItem.content)"
|
:src="getYoutubeThumbnail(selectedItem.content)"
|
||||||
alt="YouTube Thumbnail"
|
alt="YouTube Thumbnail" />
|
||||||
class="full-image" />
|
</div>
|
||||||
<span v-else>{{ selectedItem?.content || "" }}</span>
|
<span v-else>{{ selectedItem?.content || "" }}</span>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
<div class="information">
|
<OverlayScrollbarsComponent
|
||||||
|
class="information"
|
||||||
|
:options="{ scrollbars: { autoHide: 'scroll' } }">
|
||||||
<div class="title">Information</div>
|
<div class="title">Information</div>
|
||||||
|
<div class="info-content" v-if="selectedItem">
|
||||||
|
<!-- Common Information -->
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Source</p>
|
||||||
|
<span>{{ selectedItem.source }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Content Type</p>
|
||||||
|
<span>{{
|
||||||
|
selectedItem.content_type.charAt(0).toUpperCase() +
|
||||||
|
selectedItem.content_type.slice(1)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Information -->
|
||||||
|
<template v-if="selectedItem.content_type === ContentType.Text">
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Characters</p>
|
||||||
|
<span>{{ getCharacterCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Words</p>
|
||||||
|
<span>{{ getWordCount }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Image Information -->
|
||||||
|
<template v-if="selectedItem.content_type === ContentType.Image">
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Dimensions</p>
|
||||||
|
<span>{{ imageDimensions[selectedItem.id] || "Loading..." }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Image size</p>
|
||||||
|
<span>{{ imageSizes[selectedItem.id] || "Loading..." }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- File Information -->
|
||||||
|
<template v-if="selectedItem.content_type === ContentType.File">
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Path</p>
|
||||||
|
<span>{{ selectedItem.content }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Link Information -->
|
||||||
|
<template v-if="selectedItem.content_type === ContentType.Link">
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">URL</p>
|
||||||
|
<span>{{ selectedItem.content }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Characters</p>
|
||||||
|
<span>{{ getCharacterCount }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Color Information -->
|
||||||
|
<template v-if="selectedItem.content_type === ContentType.Color">
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Color</p>
|
||||||
|
<div
|
||||||
|
class="color-preview"
|
||||||
|
:style="{ backgroundColor: selectedItem.content }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Value</p>
|
||||||
|
<span>{{ selectedItem.content }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Code Information -->
|
||||||
|
<template v-if="selectedItem.content_type === ContentType.Code">
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Language</p>
|
||||||
|
<span>{{ selectedItem.language }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Lines</p>
|
||||||
|
<span>{{ getLineCount }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Common Information -->
|
||||||
|
<div class="info-row">
|
||||||
|
<p class="label">Copied</p>
|
||||||
|
<span>{{ getFormattedDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
<Noise />
|
<Noise />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -121,10 +215,9 @@ interface GroupedHistory {
|
||||||
items: HistoryItem[];
|
items: HistoryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { $history, $settings } = useNuxtApp();
|
const { $history } = useNuxtApp();
|
||||||
const CHUNK_SIZE = 50;
|
const CHUNK_SIZE = 50;
|
||||||
const SCROLL_THRESHOLD = 100;
|
const SCROLL_THRESHOLD = 100;
|
||||||
const IMAGE_LOAD_DEBOUNCE = 300;
|
|
||||||
|
|
||||||
const history = shallowRef<HistoryItem[]>([]);
|
const history = shallowRef<HistoryItem[]>([]);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
@ -141,6 +234,7 @@ const searchInput = ref<HTMLInputElement | null>(null);
|
||||||
const os = ref<string>("");
|
const os = ref<string>("");
|
||||||
const imageUrls = shallowRef<Record<string, string>>({});
|
const imageUrls = shallowRef<Record<string, string>>({});
|
||||||
const imageDimensions = shallowRef<Record<string, string>>({});
|
const imageDimensions = shallowRef<Record<string, string>>({});
|
||||||
|
const imageSizes = shallowRef<Record<string, string>>({});
|
||||||
const lastUpdateTime = ref<number>(Date.now());
|
const lastUpdateTime = ref<number>(Date.now());
|
||||||
const imageLoadError = ref<boolean>(false);
|
const imageLoadError = ref<boolean>(false);
|
||||||
const imageLoading = ref<boolean>(false);
|
const imageLoading = ref<boolean>(false);
|
||||||
|
@ -238,10 +332,34 @@ const loadHistoryChunk = async (): Promise<void> => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (historyItem.content_type === ContentType.Image) {
|
if (historyItem.content_type === ContentType.Image) {
|
||||||
await Promise.all([
|
try {
|
||||||
getItemDimensions(historyItem),
|
const base64 = await $history.readImage({
|
||||||
loadImageUrl(historyItem),
|
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<void>((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;
|
return historyItem;
|
||||||
})
|
})
|
||||||
|
@ -281,7 +399,7 @@ const scrollToSelectedItem = (forceScrollTop: boolean = false): void => {
|
||||||
|
|
||||||
if (isAbove || isBelow) {
|
if (isAbove || isBelow) {
|
||||||
const scrollOffset = isAbove
|
const scrollOffset = isAbove
|
||||||
? elementRect.top - viewportRect.top - 8
|
? elementRect.top - viewportRect.top - (selectedItemIndex.value === 0 ? 36 : 8)
|
||||||
: elementRect.bottom - viewportRect.bottom + 9;
|
: elementRect.bottom - viewportRect.bottom + 9;
|
||||||
|
|
||||||
viewport.scrollBy({ top: scrollOffset, behavior: "smooth" });
|
viewport.scrollBy({ top: scrollOffset, behavior: "smooth" });
|
||||||
|
@ -347,7 +465,7 @@ const pasteSelectedItem = async (): Promise<void> => {
|
||||||
let contentType: string = selectedItem.value.content_type;
|
let contentType: string = selectedItem.value.content_type;
|
||||||
if (contentType === "image") {
|
if (contentType === "image") {
|
||||||
try {
|
try {
|
||||||
content = await $history.getImagePath(content);
|
content = await $history.readImage({ filename: content });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading image file:", error);
|
console.error("Error reading image file:", error);
|
||||||
return;
|
return;
|
||||||
|
@ -394,66 +512,68 @@ const getFaviconFromDb = (favicon: string): string => {
|
||||||
return `data:image/png;base64,${favicon}`;
|
return `data:image/png;base64,${favicon}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImageData = async (
|
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
||||||
item: HistoryItem
|
const results = await $history.loadHistoryChunk(0, CHUNK_SIZE);
|
||||||
): Promise<{ url: string; dimensions: string }> => {
|
if (results.length > 0) {
|
||||||
try {
|
const existingIds = new Set(history.value.map(item => item.id));
|
||||||
const base64 = await $history.readImage({ filename: item.content });
|
const uniqueNewItems = results.filter(item => !existingIds.has(item.id));
|
||||||
const dataUrl = `data:image/png;base64,${base64}`;
|
|
||||||
const img = new Image();
|
|
||||||
img.src = dataUrl;
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
const processedNewItems = await Promise.all(
|
||||||
img.onload = () => resolve();
|
uniqueNewItems.map(async (item) => {
|
||||||
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<HistoryItem> => {
|
|
||||||
const historyItem = new HistoryItem(
|
const historyItem = new HistoryItem(
|
||||||
item.content_type as string,
|
item.source,
|
||||||
ContentType[item.content_type as keyof typeof ContentType],
|
item.content_type,
|
||||||
item.content,
|
item.content,
|
||||||
item.favicon
|
item.favicon
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(historyItem, {
|
Object.assign(historyItem, {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
timestamp: new Date(item.timestamp),
|
timestamp: new Date(item.timestamp),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (historyItem.content_type === ContentType.Image) {
|
if (historyItem.content_type === ContentType.Image) {
|
||||||
const { url, dimensions } = await getImageData(historyItem);
|
try {
|
||||||
imageUrls.value[historyItem.id] = url;
|
const base64 = await $history.readImage({
|
||||||
imageDimensions.value[historyItem.id] = dimensions;
|
filename: historyItem.content,
|
||||||
}
|
});
|
||||||
|
const size = Math.ceil((base64.length * 3) / 4);
|
||||||
|
imageSizes.value[historyItem.id] = formatFileSize(size);
|
||||||
|
|
||||||
return historyItem;
|
const img = new Image();
|
||||||
|
img.src = `data:image/png;base64,${base64}`;
|
||||||
|
imageUrls.value[historyItem.id] = img.src;
|
||||||
|
|
||||||
|
await new Promise<void>((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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
history.value = [...processedNewItems, ...history.value];
|
||||||
history.value = [];
|
|
||||||
offset = 0;
|
|
||||||
await loadHistoryChunk();
|
|
||||||
|
|
||||||
if (
|
if (resetScroll && resultsContainer.value?.osInstance()?.elements().viewport) {
|
||||||
resetScroll &&
|
|
||||||
resultsContainer.value?.osInstance()?.elements().viewport
|
|
||||||
) {
|
|
||||||
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
|
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelection = (
|
const handleSelection = (
|
||||||
|
@ -466,29 +586,16 @@ const handleSelection = (
|
||||||
if (shouldScroll) scrollToSelectedItem();
|
if (shouldScroll) scrollToSelectedItem();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaContent = async (
|
|
||||||
content: string,
|
|
||||||
type: string
|
|
||||||
): Promise<string> => {
|
|
||||||
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<void> => {
|
const setupEventListeners = async (): Promise<void> => {
|
||||||
await listen("clipboard-content-updated", async () => {
|
await listen("clipboard-content-updated", async () => {
|
||||||
lastUpdateTime.value = Date.now();
|
lastUpdateTime.value = Date.now();
|
||||||
handleSelection(0, 0, false);
|
|
||||||
await updateHistory(true);
|
await updateHistory(true);
|
||||||
|
if (
|
||||||
|
groupedHistory.value.length > 0 &&
|
||||||
|
groupedHistory.value[0].items.length > 0
|
||||||
|
) {
|
||||||
|
handleSelection(0, 0, false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await listen("tauri://focus", async () => {
|
await listen("tauri://focus", async () => {
|
||||||
|
@ -497,9 +604,7 @@ const setupEventListeners = async (): Promise<void> => {
|
||||||
const previousState = {
|
const previousState = {
|
||||||
groupIndex: selectedGroupIndex.value,
|
groupIndex: selectedGroupIndex.value,
|
||||||
itemIndex: selectedItemIndex.value,
|
itemIndex: selectedItemIndex.value,
|
||||||
scroll:
|
scroll: resultsContainer.value?.osInstance()?.elements().viewport?.scrollTop || 0,
|
||||||
resultsContainer.value?.osInstance()?.elements().viewport
|
|
||||||
?.scrollTop || 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await updateHistory();
|
await updateHistory();
|
||||||
|
@ -507,9 +612,7 @@ const setupEventListeners = async (): Promise<void> => {
|
||||||
handleSelection(previousState.groupIndex, previousState.itemIndex, false);
|
handleSelection(previousState.groupIndex, previousState.itemIndex, false);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const viewport = resultsContainer.value
|
const viewport = resultsContainer.value?.osInstance()?.elements().viewport;
|
||||||
?.osInstance()
|
|
||||||
?.elements().viewport;
|
|
||||||
if (viewport) {
|
if (viewport) {
|
||||||
viewport.scrollTo({
|
viewport.scrollTo({
|
||||||
top: previousState.scroll,
|
top: previousState.scroll,
|
||||||
|
@ -611,40 +714,38 @@ watch([selectedGroupIndex, selectedItemIndex], () =>
|
||||||
scrollToSelectedItem(false)
|
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<void>((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 => {
|
const getComputedImageUrl = (item: HistoryItem | null): string => {
|
||||||
if (!item) return "";
|
if (!item) return "";
|
||||||
return imageUrls.value[item.id] || "";
|
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]}`;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
|
@ -28,7 +28,7 @@ pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::er
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
||||||
let rows = sqlx::query(
|
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)
|
.fetch_all(&*pool)
|
||||||
.await
|
.await
|
||||||
|
@ -44,6 +44,7 @@ pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<Histo
|
||||||
content: row.get("content"),
|
content: row.get("content"),
|
||||||
favicon: row.get("favicon"),
|
favicon: row.get("favicon"),
|
||||||
timestamp: row.get("timestamp"),
|
timestamp: row.get("timestamp"),
|
||||||
|
language: row.get("language"),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -55,7 +56,8 @@ pub async fn add_history_item(
|
||||||
pool: tauri::State<'_, SqlitePool>,
|
pool: tauri::State<'_, SqlitePool>,
|
||||||
item: HistoryItem,
|
item: HistoryItem,
|
||||||
) -> Result<(), String> {
|
) -> 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<String> = sqlx::query_scalar(
|
let last_content: Option<String> = sqlx::query_scalar(
|
||||||
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",
|
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",
|
||||||
|
@ -70,7 +72,7 @@ pub async fn add_history_item(
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.bind(source)
|
.bind(source)
|
||||||
|
@ -79,6 +81,7 @@ pub async fn add_history_item(
|
||||||
.bind(content)
|
.bind(content)
|
||||||
.bind(favicon)
|
.bind(favicon)
|
||||||
.bind(timestamp)
|
.bind(timestamp)
|
||||||
|
.bind(language)
|
||||||
.execute(&*pool)
|
.execute(&*pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
@ -93,7 +96,7 @@ pub async fn search_history(
|
||||||
) -> Result<Vec<HistoryItem>, String> {
|
) -> Result<Vec<HistoryItem>, String> {
|
||||||
let query = format!("%{}%", query);
|
let query = format!("%{}%", query);
|
||||||
let rows = sqlx::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)
|
.bind(query)
|
||||||
.fetch_all(&*pool)
|
.fetch_all(&*pool)
|
||||||
|
@ -110,6 +113,7 @@ pub async fn search_history(
|
||||||
content: row.get("content"),
|
content: row.get("content"),
|
||||||
favicon: row.get("favicon"),
|
favicon: row.get("favicon"),
|
||||||
timestamp: row.get("timestamp"),
|
timestamp: row.get("timestamp"),
|
||||||
|
language: row.get("language"),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -123,7 +127,7 @@ pub async fn load_history_chunk(
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<HistoryItem>, String> {
|
) -> Result<Vec<HistoryItem>, String> {
|
||||||
let rows = sqlx::query(
|
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(limit)
|
||||||
.bind(offset)
|
.bind(offset)
|
||||||
|
@ -141,6 +145,7 @@ pub async fn load_history_chunk(
|
||||||
content: row.get("content"),
|
content: row.get("content"),
|
||||||
favicon: row.get("favicon"),
|
favicon: row.get("favicon"),
|
||||||
timestamp: row.get("timestamp"),
|
timestamp: row.get("timestamp"),
|
||||||
|
language: row.get("language"),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
|
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
|
||||||
ALTER TABLE history ADD COLUMN source_icon TEXT;
|
ALTER TABLE history ADD COLUMN source_icon TEXT;
|
||||||
|
ALTER TABLE history ADD COLUMN language TEXT;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
use active_win_pos_rs::get_active_window;
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
|
use image::codecs::png::PngEncoder;
|
||||||
use tauri::PhysicalPosition;
|
use tauri::PhysicalPosition;
|
||||||
|
|
||||||
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||||
|
@ -28,3 +31,21 @@ pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_app_info() -> (String, Option<String>) {
|
||||||
|
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<String, Box<dyn std::error::Error>> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ pub struct HistoryItem {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub favicon: Option<String>,
|
pub favicon: Option<String>,
|
||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||||
|
@ -108,7 +109,14 @@ impl From<String> for ContentType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryItem {
|
impl HistoryItem {
|
||||||
pub fn new(source: String, content_type: ContentType, content: String, favicon: Option<String>, source_icon: Option<String>) -> Self {
|
pub fn new(
|
||||||
|
source: String,
|
||||||
|
content_type: ContentType,
|
||||||
|
content: String,
|
||||||
|
favicon: Option<String>,
|
||||||
|
source_icon: Option<String>,
|
||||||
|
language: Option<String>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
source,
|
source,
|
||||||
|
@ -117,10 +125,22 @@ impl HistoryItem {
|
||||||
content,
|
content,
|
||||||
favicon,
|
favicon,
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
|
language,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_row(&self) -> (String, String, Option<String>, String, String, Option<String>, DateTime<Utc>) {
|
pub fn to_row(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
DateTime<Utc>,
|
||||||
|
Option<String>,
|
||||||
|
) {
|
||||||
(
|
(
|
||||||
self.id.clone(),
|
self.id.clone(),
|
||||||
self.source.clone(),
|
self.source.clone(),
|
||||||
|
@ -129,6 +149,7 @@ impl HistoryItem {
|
||||||
self.content.clone(),
|
self.content.clone(),
|
||||||
self.favicon.clone(),
|
self.favicon.clone(),
|
||||||
self.timestamp,
|
self.timestamp,
|
||||||
|
self.language.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
export enum ContentType {
|
export enum ContentType {
|
||||||
Text = "text",
|
Text = "text",
|
||||||
|
@ -11,21 +11,52 @@ export enum ContentType {
|
||||||
|
|
||||||
export class HistoryItem {
|
export class HistoryItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
source: string;
|
||||||
|
source_icon?: string;
|
||||||
content_type: ContentType;
|
content_type: ContentType;
|
||||||
content: string;
|
content: string;
|
||||||
favicon?: string;
|
favicon?: string;
|
||||||
timestamp: Date;
|
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.id = uuidv4();
|
||||||
|
this.source = source;
|
||||||
|
this.source_icon = source_icon;
|
||||||
this.content_type = content_type;
|
this.content_type = content_type;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.favicon = favicon;
|
this.favicon = favicon;
|
||||||
this.timestamp = new Date();
|
this.timestamp = new Date();
|
||||||
|
this.language = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
toRow(): [string, string, string, string | undefined, Date] {
|
toRow(): [
|
||||||
return [this.id, this.content_type, this.content, this.favicon, this.timestamp];
|
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,52 @@ export interface Settings {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Text {
|
||||||
|
source: string;
|
||||||
|
content_type: ContentType.Text;
|
||||||
|
characters: number;
|
||||||
|
words: number;
|
||||||
|
copied: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
source: string;
|
||||||
|
content_type: ContentType.Image;
|
||||||
|
dimensions: string;
|
||||||
|
size: number;
|
||||||
|
copied: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface File {
|
||||||
|
source: string;
|
||||||
|
content_type: ContentType.File;
|
||||||
|
path: string;
|
||||||
|
filesize: number;
|
||||||
|
copied: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Link {
|
||||||
|
source: string;
|
||||||
|
content_type: ContentType.Link;
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
characters: number;
|
||||||
|
copied: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Color {
|
||||||
|
source: string;
|
||||||
|
content_type: ContentType.Color;
|
||||||
|
hexcode: string;
|
||||||
|
rgba: string;
|
||||||
|
copied: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Code {
|
||||||
|
source: string;
|
||||||
|
content_type: ContentType.Code;
|
||||||
|
language: string;
|
||||||
|
lines: number;
|
||||||
|
copied: Date;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue