feat: add information and update chunk loading

This commit is contained in:
PandaDEV 2024-12-16 23:38:35 +10:00
parent c48a0d8239
commit 149e72802c
No known key found for this signature in database
GPG key ID: 13EFF9BAF70EE75C
7 changed files with 435 additions and 151 deletions

View file

@ -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 {

View file

@ -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">

View file

@ -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();

View file

@ -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;

View file

@ -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))
}

View file

@ -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(),
) )
} }
} }

View file

@ -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;
}