mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 21:24:05 +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;
|
||||
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;
|
||||
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 {
|
||||
|
|
319
pages/index.vue
319
pages/index.vue
|
@ -87,20 +87,114 @@
|
|||
</div>
|
||||
</template>
|
||||
</OverlayScrollbarsComponent>
|
||||
<div class="content" v-if="selectedItem?.content_type === 'image'">
|
||||
<img :src="getComputedImageUrl(selectedItem)" alt="Image" class="image" />
|
||||
<div
|
||||
class="content"
|
||||
v-if="selectedItem?.content_type === ContentType.Image">
|
||||
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
|
||||
</div>
|
||||
<OverlayScrollbarsComponent v-else class="content">
|
||||
<div v-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)" class="full-image">
|
||||
<img
|
||||
v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)"
|
||||
:src="getYoutubeThumbnail(selectedItem.content)"
|
||||
alt="YouTube Thumbnail"
|
||||
class="full-image" />
|
||||
alt="YouTube Thumbnail" />
|
||||
</div>
|
||||
<span v-else>{{ selectedItem?.content || "" }}</span>
|
||||
</OverlayScrollbarsComponent>
|
||||
<div class="information">
|
||||
<OverlayScrollbarsComponent
|
||||
class="information"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }">
|
||||
<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 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 />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -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<HistoryItem[]>([]);
|
||||
let offset = 0;
|
||||
|
@ -141,6 +234,7 @@ const searchInput = ref<HTMLInputElement | null>(null);
|
|||
const os = ref<string>("");
|
||||
const imageUrls = shallowRef<Record<string, string>>({});
|
||||
const imageDimensions = shallowRef<Record<string, string>>({});
|
||||
const imageSizes = shallowRef<Record<string, string>>({});
|
||||
const lastUpdateTime = ref<number>(Date.now());
|
||||
const imageLoadError = ref<boolean>(false);
|
||||
const imageLoading = ref<boolean>(false);
|
||||
|
@ -238,10 +332,34 @@ const loadHistoryChunk = async (): Promise<void> => {
|
|||
});
|
||||
|
||||
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<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;
|
||||
})
|
||||
|
@ -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<void> => {
|
|||
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,66 +512,68 @@ 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;
|
||||
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
||||
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));
|
||||
|
||||
await new Promise<void>((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<HistoryItem> => {
|
||||
const processedNewItems = await Promise.all(
|
||||
uniqueNewItems.map(async (item) => {
|
||||
const historyItem = new HistoryItem(
|
||||
item.content_type as string,
|
||||
ContentType[item.content_type as keyof typeof ContentType],
|
||||
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) {
|
||||
const { url, dimensions } = await getImageData(historyItem);
|
||||
imageUrls.value[historyItem.id] = url;
|
||||
imageDimensions.value[historyItem.id] = dimensions;
|
||||
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<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 = [];
|
||||
offset = 0;
|
||||
await loadHistoryChunk();
|
||||
history.value = [...processedNewItems, ...history.value];
|
||||
|
||||
if (
|
||||
resetScroll &&
|
||||
resultsContainer.value?.osInstance()?.elements().viewport
|
||||
) {
|
||||
if (resetScroll && resultsContainer.value?.osInstance()?.elements().viewport) {
|
||||
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelection = (
|
||||
|
@ -466,29 +586,16 @@ const handleSelection = (
|
|||
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> => {
|
||||
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<void> => {
|
|||
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<void> => {
|
|||
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<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 => {
|
||||
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]}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -28,7 +28,7 @@ pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::er
|
|||
#[tauri::command]
|
||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, 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<Vec<Histo
|
|||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -55,7 +56,8 @@ pub async fn add_history_item(
|
|||
pool: tauri::State<'_, SqlitePool>,
|
||||
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<String> = sqlx::query_scalar(
|
||||
"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(
|
||||
"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(source)
|
||||
|
@ -79,6 +81,7 @@ pub async fn add_history_item(
|
|||
.bind(content)
|
||||
.bind(favicon)
|
||||
.bind(timestamp)
|
||||
.bind(language)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
@ -93,7 +96,7 @@ pub async fn search_history(
|
|||
) -> Result<Vec<HistoryItem>, 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 +113,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 +127,7 @@ pub async fn load_history_chunk(
|
|||
limit: i64,
|
||||
) -> Result<Vec<HistoryItem>, 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 +145,7 @@ pub async fn load_history_chunk(
|
|||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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 serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -12,6 +12,7 @@ pub struct HistoryItem {
|
|||
pub content: String,
|
||||
pub favicon: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
|
@ -108,7 +109,14 @@ impl From<String> for ContentType {
|
|||
}
|
||||
|
||||
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 {
|
||||
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, 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.source.clone(),
|
||||
|
@ -129,6 +149,7 @@ impl HistoryItem {
|
|||
self.content.clone(),
|
||||
self.favicon.clone(),
|
||||
self.timestamp,
|
||||
self.language.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,52 @@ export interface Settings {
|
|||
key: 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