Merge pull request #20 from 0PandaDEV/issue/metadata

Metadata/Information for Items
This commit is contained in:
PandaDEV 2024-12-20 23:29:18 +10:00 committed by GitHub
commit 56fab79713
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1140 additions and 371 deletions

View file

@ -89,7 +89,7 @@ $mutedtext: #78756f;
} }
.icon { .icon {
width: 20px; width: 18px;
height: 18px; height: 18px;
} }
} }
@ -98,32 +98,32 @@ $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(.image)) {
padding: 8px;
}
span { span {
font-family: CommitMono Nerd Font !important; font-family: CommitMono Nerd Font !important;
} }
.full-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
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 +212,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 +228,47 @@ $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;
line-height: 1;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
&:first-child {
padding-top: 22px;
}
p {
font-family: SFRoundedMedium;
color: $text2;
font-weight: 500;
flex-shrink: 0;
}
span {
font-family: CommitMono;
color: $text;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-left: 32px;
}
}
}
} }
.clothoid-corner { .clothoid-corner {

View file

@ -63,11 +63,13 @@
@error="onImageError" /> @error="onImageError" />
<img v-else src="../public/icons/Image.svg" class="icon" /> <img v-else src="../public/icons/Image.svg" class="icon" />
</template> </template>
<img <template v-else-if="hasFavicon(item.favicon ?? '')">
v-else-if="hasFavicon(item.favicon ?? '')" <img
:src="getFaviconFromDb(item.favicon ?? '')" :src="item.favicon ? getFaviconFromDb(item.favicon) : '../public/icons/Link.svg'"
alt="Favicon" alt="Favicon"
class="favicon" /> class="favicon"
@error="($event.target as HTMLImageElement).src = '../public/icons/Link.svg'" />
</template>
<img <img
src="../public/icons/File.svg" src="../public/icons/File.svg"
class="icon" class="icon"
@ -76,6 +78,24 @@
src="../public/icons/Text.svg" src="../public/icons/Text.svg"
class="icon" class="icon"
v-else-if="item.content_type === ContentType.Text" /> v-else-if="item.content_type === ContentType.Text" />
<div v-else-if="item.content_type === ContentType.Color">
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="18" height="18" />
<path
d="M9 18C12.2154 18 15.1865 16.2846 16.7942 13.5C18.4019 10.7154 18.4019 7.28461 16.7942 4.5C15.1865 1.71539 12.2154 -1.22615e-06 9 0C5.78461 0 2.81347 1.71539 1.20577 4.5C-0.401925 7.28461 -0.401923 10.7154 1.20577 13.5C2.81347 16.2846 5.78461 18 9 18Z"
fill="#E5DFD5" />
<path
d="M9 16C7.14348 16 5.36301 15.2625 4.05025 13.9497C2.7375 12.637 2 10.8565 2 9C2 7.14348 2.7375 5.36301 4.05025 4.05025C5.36301 2.7375 7.14348 2 9 2C10.8565 2 12.637 2.7375 13.9497 4.05025C15.2625 5.36301 16 7.14348 16 9C16 10.8565 15.2625 12.637 13.9497 13.9497C12.637 15.2625 10.8565 16 9 16Z"
:fill="item.content" />
</g>
</svg>
</div>
<img <img
src="../public/icons/Code.svg" src="../public/icons/Code.svg"
class="icon" class="icon"
@ -87,20 +107,42 @@
</div> </div>
</template> </template>
</OverlayScrollbarsComponent> </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>
<div
v-else-if="selectedItem && isYoutubeWatchUrl(selectedItem.content)"
class="content">
<img
class="image"
:src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail" />
</div>
<div class="content" v-else-if="selectedItem?.content_type === ContentType.Link && pageOgImage">
<img :src="pageOgImage" alt="Image" class="image">
</div> </div>
<OverlayScrollbarsComponent v-else class="content"> <OverlayScrollbarsComponent v-else class="content">
<img <span>{{ selectedItem?.content || "" }}</span>
v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)"
:src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail"
class="full-image" />
<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> <div class="info-content" v-if="selectedItem && getInfo">
<div class="info-row" v-for="(row, index) in infoRows" :key="index">
<p class="label">{{ row.label }}</p>
<span
:class="{ 'url-truncate': row.isUrl }"
:data-text="row.value">
{{ row.value }}
</span>
</div>
</div>
</OverlayScrollbarsComponent>
<Noise /> <Noise />
</div> </div>
</template> </template>
@ -114,17 +156,18 @@ import { platform } from "@tauri-apps/plugin-os";
import { enable, isEnabled } from "@tauri-apps/plugin-autostart"; import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { useNuxtApp } from "#app"; import { useNuxtApp } from "#app";
import { invoke } from "@tauri-apps/api/core";
import { HistoryItem, ContentType } from "~/types/types"; import { HistoryItem, ContentType } from "~/types/types";
import type { InfoText, InfoImage, InfoFile, InfoLink, InfoColor, InfoCode } from "~/types/types";
interface GroupedHistory { interface GroupedHistory {
label: string; label: string;
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,9 +184,12 @@ 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);
const pageTitle = ref<string>('');
const pageOgImage = ref<string>('');
const keyboard = useKeyboard(); const keyboard = useKeyboard();
@ -230,7 +276,9 @@ const loadHistoryChunk = async (): Promise<void> => {
item.source, item.source,
item.content_type, item.content_type,
item.content, item.content,
item.favicon item.favicon,
item.source_icon,
item.language
); );
Object.assign(historyItem, { Object.assign(historyItem, {
id: item.id, id: item.id,
@ -238,10 +286,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 +353,9 @@ 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 +421,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;
@ -386,73 +460,78 @@ const getYoutubeThumbnail = (url: string): string => {
videoId = url.match(/[?&]v=([^&]+)/)?.[1]; videoId = url.match(/[?&]v=([^&]+)/)?.[1];
} }
return videoId return videoId
? `https://img.youtube.com/vi/${videoId}/0.jpg` ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
: "https://via.placeholder.com/150"; : "https://via.placeholder.com/1280x720";
}; };
const getFaviconFromDb = (favicon: string): string => { const getFaviconFromDb = (favicon: string): string => {
return `data:image/png;base64,${favicon}`; 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<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 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<void> => { const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
history.value = []; const results = await $history.loadHistoryChunk(0, CHUNK_SIZE);
offset = 0; if (results.length > 0) {
await loadHistoryChunk(); const existingIds = new Set(history.value.map((item) => item.id));
const uniqueNewItems = results.filter((item) => !existingIds.has(item.id));
if ( const processedNewItems = await Promise.all(
resetScroll && uniqueNewItems.map(async (item) => {
resultsContainer.value?.osInstance()?.elements().viewport const historyItem = new HistoryItem(
) { item.source,
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({ item.content_type,
top: 0, item.content,
behavior: "smooth", item.favicon
}); );
Object.assign(historyItem, {
id: item.id,
timestamp: new Date(item.timestamp),
});
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<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;
})
);
history.value = [...processedNewItems, ...history.value];
if (
resetScroll &&
resultsContainer.value?.osInstance()?.elements().viewport
) {
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
top: 0,
behavior: "smooth",
});
}
} }
}; };
@ -466,29 +545,13 @@ 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[0]?.items.length > 0) {
handleSelection(0, 0, false);
}
}); });
await listen("tauri://focus", async () => { await listen("tauri://focus", async () => {
@ -506,17 +569,12 @@ const setupEventListeners = async (): Promise<void> => {
lastUpdateTime.value = currentTime; lastUpdateTime.value = currentTime;
handleSelection(previousState.groupIndex, previousState.itemIndex, false); handleSelection(previousState.groupIndex, previousState.itemIndex, false);
nextTick(() => { if (resultsContainer.value?.osInstance()?.elements().viewport?.scrollTo) {
const viewport = resultsContainer.value resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
?.osInstance() top: previousState.scroll,
?.elements().viewport; behavior: "instant",
if (viewport) { });
viewport.scrollTo({ }
top: previousState.scroll,
behavior: "instant",
});
}
});
} }
focusSearchInput(); focusSearchInput();
}); });
@ -558,7 +616,6 @@ const setupEventListeners = async (): Promise<void> => {
if (isMacActionCombo || isOtherOsActionCombo) { if (isMacActionCombo || isOtherOsActionCombo) {
event.preventDefault(); event.preventDefault();
console.log("Actions shortcut triggered");
} }
}); });
}; };
@ -611,40 +668,179 @@ watch([selectedGroupIndex, selectedItemIndex], () =>
scrollToSelectedItem(false) scrollToSelectedItem(false)
); );
const getItemDimensions = async (item: HistoryItem) => { const getFormattedDate = computed(() => {
if (!imageDimensions.value[item.id]) { if (!selectedItem.value?.timestamp) return "";
try { return new Intl.DateTimeFormat("en-US", {
const base64 = await $history.readImage({ filename: item.content }); dateStyle: "medium",
const img = new Image(); timeStyle: "medium",
img.src = `data:image/png;base64,${base64}`; }).format(selectedItem.value.timestamp);
await new Promise<void>((resolve, reject) => { });
img.onload = () => resolve();
img.onerror = () => reject(); const formatFileSize = (bytes: number): string => {
}); if (bytes === 0) return "0 Bytes";
imageDimensions.value[item.id] = `${img.width}x${img.height}`; const k = 1024;
} catch (error) { const sizes = ["Bytes", "KB", "MB", "GB"];
console.error("Error loading image dimensions:", error); const i = Math.floor(Math.log(bytes) / Math.log(k));
imageDimensions.value[item.id] = "Error"; return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}
}
return imageDimensions.value[item.id] || "Loading...";
}; };
const loadImageUrl = async (item: HistoryItem) => { const fetchPageMeta = async (url: string) => {
if (!imageUrls.value[item.id]) { try {
try { const [title, ogImage] = await invoke('fetch_page_meta', { url }) as [string, string | null];
const base64 = await $history.readImage({ filename: item.content }); pageTitle.value = title;
imageUrls.value[item.id] = `data:image/png;base64,${base64}`; if (ogImage) {
} catch (error) { pageOgImage.value = ogImage;
console.error("Error loading image:", error);
} }
} catch (error) {
console.error('Error fetching page meta:', error);
pageTitle.value = 'Error loading title';
} }
}; };
const getComputedImageUrl = (item: HistoryItem | null): string => { watch(() => selectedItem.value, (newItem) => {
if (!item) return ""; if (newItem?.content_type === ContentType.Link) {
return imageUrls.value[item.id] || ""; pageTitle.value = 'Loading...';
}; pageOgImage.value = '';
fetchPageMeta(newItem.content);
} else {
pageTitle.value = '';
pageOgImage.value = '';
}
});
const getInfo = computed(() => {
if (!selectedItem.value) return null;
const baseInfo = {
source: selectedItem.value.source,
copied: selectedItem.value.timestamp,
};
const infoMap: Record<ContentType, () => InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode> = {
[ContentType.Text]: () => ({
...baseInfo,
content_type: ContentType.Text,
characters: selectedItem.value!.content.length,
words: selectedItem.value!.content.trim().split(/\s+/).length,
}),
[ContentType.Image]: () => ({
...baseInfo,
content_type: ContentType.Image,
dimensions: imageDimensions.value[selectedItem.value!.id] || "Loading...",
size: parseInt(imageSizes.value[selectedItem.value!.id] || "0"),
}),
[ContentType.File]: () => ({
...baseInfo,
content_type: ContentType.File,
path: selectedItem.value!.content,
filesize: 0,
}),
[ContentType.Link]: () => ({
...baseInfo,
content_type: ContentType.Link,
title: pageTitle.value,
url: selectedItem.value!.content,
characters: selectedItem.value!.content.length,
}),
[ContentType.Color]: () => {
const hex = selectedItem.value!.content;
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const rNorm = r / 255;
const gNorm = g / 255;
const bNorm = b / 255;
const max = Math.max(rNorm, gNorm, bNorm);
const min = Math.min(rNorm, gNorm, bNorm);
let h = 0, s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case rNorm:
h = (gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0);
break;
case gNorm:
h = (bNorm - rNorm) / d + 2;
break;
case bNorm:
h = (rNorm - gNorm) / d + 4;
break;
}
h /= 6;
}
return {
...baseInfo,
content_type: ContentType.Color,
hex: hex,
rgb: `rgb(${r}, ${g}, ${b})`,
hsl: `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`,
};
},
[ContentType.Code]: () => ({
...baseInfo,
content_type: ContentType.Code,
language: selectedItem.value!.language ?? "Unknown",
lines: selectedItem.value!.content.split('\n').length,
}),
};
return infoMap[selectedItem.value.content_type]();
});
const infoRows = computed(() => {
if (!getInfo.value) return [];
const commonRows = [
{ label: "Source", value: getInfo.value.source, isUrl: false },
{ label: "Content Type", value: getInfo.value.content_type.charAt(0).toUpperCase() + getInfo.value.content_type.slice(1), isUrl: false },
];
const typeSpecificRows: Record<ContentType, Array<{ label: string; value: string | number; isUrl?: boolean }>> = {
[ContentType.Text]: [
{ label: "Characters", value: (getInfo.value as InfoText).characters },
{ label: "Words", value: (getInfo.value as InfoText).words },
],
[ContentType.Image]: [
{ label: "Dimensions", value: (getInfo.value as InfoImage).dimensions },
{ label: "Image size", value: formatFileSize((getInfo.value as InfoImage).size) },
],
[ContentType.File]: [
{ label: "Path", value: (getInfo.value as InfoFile).path },
],
[ContentType.Link]: [
...((getInfo.value as InfoLink).title && (getInfo.value as InfoLink).title !== 'Loading...'
? [{ label: "Title", value: (getInfo.value as InfoLink).title || '' }]
: []),
{ label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true },
{ label: "Characters", value: (getInfo.value as InfoLink).characters },
],
[ContentType.Color]: [
{ label: "Hex", value: (getInfo.value as InfoColor).hex },
{ label: "RGB", value: (getInfo.value as InfoColor).rgb },
{ label: "HSL", value: (getInfo.value as InfoColor).hsl },
],
[ContentType.Code]: [
{ label: "Language", value: (getInfo.value as InfoCode).language },
{ label: "Lines", value: (getInfo.value as InfoCode).lines },
],
};
const specificRows = typeSpecificRows[getInfo.value.content_type]
.filter(row => row.value !== "");
return [
...commonRows,
...specificRows,
{ label: "Copied", value: getFormattedDate.value },
];
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View file

@ -14,7 +14,10 @@
<p>Save</p> <p>Save</p>
<div> <div>
<img alt="" src="../public/cmd.svg" v-if="os === 'macos'" /> <img alt="" src="../public/cmd.svg" v-if="os === 'macos'" />
<img alt="" src="../public/ctrl.svg" v-if="os === 'linux' || os === 'windows'" /> <img
alt=""
src="../public/ctrl.svg"
v-if="os === 'linux' || os === 'windows'" />
<img alt="" src="../public/enter.svg" /> <img alt="" src="../public/enter.svg" />
</div> </div>
</div> </div>
@ -28,16 +31,14 @@
@keydown="onKeyDown" @keydown="onKeyDown"
class="keybind-input" class="keybind-input"
ref="keybindInput" ref="keybindInput"
tabindex="0" tabindex="0">
>
<span class="key" v-if="keybind.length === 0">Click here</span> <span class="key" v-if="keybind.length === 0">Click here</span>
<template v-else> <template v-else>
<span <span
:key="index" :key="index"
class="key" class="key"
:class="{ modifier: isModifier(key) }" :class="{ modifier: isModifier(key) }"
v-for="(key, index) in keybind" v-for="(key, index) in keybind">
>
{{ keyToDisplay(key) }} {{ keyToDisplay(key) }}
</span> </span>
</template> </template>
@ -47,46 +48,54 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { invoke } from '@tauri-apps/api/core'; import { invoke } from "@tauri-apps/api/core";
import { onMounted, onUnmounted, reactive, ref } from 'vue'; import { onMounted, onUnmounted, reactive, ref } from "vue";
import { platform } from '@tauri-apps/plugin-os'; import { platform } from "@tauri-apps/plugin-os";
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
const activeModifiers = reactive<Set<string>>(new Set()); const activeModifiers = reactive<Set<string>>(new Set());
const isKeybindInputFocused = ref(false); const isKeybindInputFocused = ref(false);
const keybind = ref<string[]>([]); const keybind = ref<string[]>([]);
const keybindInput = ref<HTMLElement | null>(null); const keybindInput = ref<HTMLElement | null>(null);
const lastBlurTime = ref(0); const lastBlurTime = ref(0);
const os = ref(''); const os = ref("");
const router = useRouter(); const router = useRouter();
const keyboard = useKeyboard(); const keyboard = useKeyboard();
const keyToDisplayMap: Record<string, string> = { const keyToDisplayMap: Record<string, string> = {
' ': 'Space', " ": "Space",
Alt: 'Alt', Alt: "Alt",
AltLeft: 'Alt L', AltLeft: "Alt L",
AltRight: 'Alt R', AltRight: "Alt R",
ArrowDown: '↓', ArrowDown: "↓",
ArrowLeft: '←', ArrowLeft: "←",
ArrowRight: '→', ArrowRight: "→",
ArrowUp: '↑', ArrowUp: "↑",
Control: 'Ctrl', Control: "Ctrl",
ControlLeft: 'Ctrl L', ControlLeft: "Ctrl L",
ControlRight: 'Ctrl R', ControlRight: "Ctrl R",
Enter: '↵', Enter: "↵",
Meta: 'Meta', Meta: "Meta",
MetaLeft: 'Meta L', MetaLeft: "Meta L",
MetaRight: 'Meta R', MetaRight: "Meta R",
Shift: '⇧', Shift: "⇧",
ShiftLeft: '⇧ L', ShiftLeft: "⇧ L",
ShiftRight: '⇧ R', ShiftRight: "⇧ R",
}; };
const modifierKeySet = new Set([ const modifierKeySet = new Set([
'Alt', 'AltLeft', 'AltRight', "Alt",
'Control', 'ControlLeft', 'ControlRight', "AltLeft",
'Meta', 'MetaLeft', 'MetaRight', "AltRight",
'Shift', 'ShiftLeft', 'ShiftRight' "Control",
"ControlLeft",
"ControlRight",
"Meta",
"MetaLeft",
"MetaRight",
"Shift",
"ShiftLeft",
"ShiftRight",
]); ]);
const isModifier = (key: string): boolean => { const isModifier = (key: string): boolean => {
@ -99,7 +108,7 @@ const keyToDisplay = (key: string): string => {
const updateKeybind = () => { const updateKeybind = () => {
const modifiers = Array.from(activeModifiers).sort(); const modifiers = Array.from(activeModifiers).sort();
const nonModifiers = keybind.value.filter(key => !isModifier(key)); const nonModifiers = keybind.value.filter((key) => !isModifier(key));
keybind.value = [...modifiers, ...nonModifiers]; keybind.value = [...modifiers, ...nonModifiers];
}; };
@ -118,7 +127,7 @@ const onKeyDown = (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
const key = event.code; const key = event.code;
if (key === 'Escape') { if (key === "Escape") {
if (keybindInput.value) { if (keybindInput.value) {
keybindInput.value.blur(); keybindInput.value.blur();
} }
@ -128,7 +137,7 @@ const onKeyDown = (event: KeyboardEvent) => {
if (isModifier(key)) { if (isModifier(key)) {
activeModifiers.add(key); activeModifiers.add(key);
} else if (!keybind.value.includes(key)) { } else if (!keybind.value.includes(key)) {
keybind.value = keybind.value.filter(k => isModifier(k)); keybind.value = keybind.value.filter((k) => isModifier(k));
keybind.value.push(key); keybind.value.push(key);
} }
@ -136,40 +145,45 @@ const onKeyDown = (event: KeyboardEvent) => {
}; };
const saveKeybind = async () => { const saveKeybind = async () => {
console.log('New:', keybind.value); console.log("New:", keybind.value);
const oldKeybind = await invoke<string[]>('get_keybind'); const oldKeybind = await invoke<string[]>("get_keybind");
console.log('Old:', oldKeybind); console.log("Old:", oldKeybind);
await invoke('save_keybind', { keybind: keybind.value }); await invoke("save_keybind", { keybind: keybind.value });
}; };
onMounted(() => { onMounted(() => {
os.value = platform(); os.value = platform();
keyboard.down('all', (event) => { keyboard.down("all", (event) => {
const isMacSaveCombo = os.value === 'macos' && const isMacSaveCombo =
(event.code === 'MetaLeft' || event.code === 'MetaRight') && os.value === "macos" &&
event.key === 'Enter'; (event.code === "MetaLeft" || event.code === "MetaRight") &&
event.key === "Enter";
const isOtherOsSaveCombo = os.value !== 'macos' && const isOtherOsSaveCombo =
(event.code === 'ControlLeft' || event.code === 'ControlRight') && os.value !== "macos" &&
event.key === 'Enter'; (event.code === "ControlLeft" || event.code === "ControlRight") &&
event.key === "Enter";
if ((isMacSaveCombo || isOtherOsSaveCombo) && !isKeybindInputFocused.value) { if (
(isMacSaveCombo || isOtherOsSaveCombo) &&
!isKeybindInputFocused.value
) {
event.preventDefault(); event.preventDefault();
saveKeybind(); saveKeybind();
} }
}); });
keyboard.down('Escape', (event) => { keyboard.down("Escape", (event) => {
const now = Date.now(); const now = Date.now();
if (!isKeybindInputFocused.value && now - lastBlurTime.value > 100) { if (!isKeybindInputFocused.value && now - lastBlurTime.value > 100) {
event.preventDefault(); event.preventDefault();
router.push('/'); router.push("/");
} }
}); });
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@use '~/assets/css/settings.scss'; @use "~/assets/css/settings.scss";
</style> </style>

View file

@ -27,8 +27,12 @@ export default defineNuxtPlugin(() => {
}); });
}, },
async getImagePath(path: string): Promise<string> { async deleteHistoryItem(id: string): Promise<void> {
return await invoke<string>("get_image_path", { path }); await invoke<void>("delete_history_item", { id });
},
async clearHistory(): Promise<void> {
await invoke<void>("clear_history");
}, },
async writeAndPaste(data: { async writeAndPaste(data: {

View file

@ -1,10 +1,7 @@
<svg width="15px" height="18px" viewBox="0 0 15 18" version="1.1" <?xml version="1.0" encoding="utf-8"?>
xmlns:xlink="http://www.w3.org/1999/xlink" <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg"> <g>
<g id="Code" fill-opacity="1"> <rect width="18" height="18" />
<path d="M15 0L15 0L15 18L0 18L0 0L15 0Z" id="Code" fill="none" stroke="none" /> <path id="Shape" d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406922 8.19178 -0.000123172 7.19625 5.59301e-08L3.75 5.59301e-08C1.67893 5.59301e-08 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM8.4 12.2529C8.03443 11.8764 8.03443 11.2665 8.4 10.89L9.6125 9.64286L8.4 8.39571C8.0558 8.01577 8.06595 7.4237 8.42297 7.05648C8.77999 6.68927 9.35561 6.67882 9.725 7.03286L11.6 8.96143C11.9656 9.33791 11.9656 9.94781 11.6 10.3243L9.725 12.2529C9.35898 12.6289 8.76602 12.6289 8.4 12.2529M6.6 8.39571C6.9442 8.01577 6.93404 7.4237 6.57703 7.05649C6.22001 6.68927 5.64439 6.67882 5.275 7.03286L3.4 8.96143C3.03443 9.33791 3.03443 9.94781 3.4 10.3243L5.275 12.2529C5.50871 12.5108 5.86069 12.617 6.19286 12.5298C6.52502 12.4425 6.7844 12.1757 6.86923 11.8341C6.95406 11.4924 6.85081 11.1304 6.6 10.89L5.3875 9.64286L6.6 8.39571Z" fill="#E5DFD5" fill-rule="evenodd" transform="translate(1.5 -0)" />
<path
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.98023e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM8.4 12.2529C8.03443 11.8764 8.03443 11.2665 8.4 10.89L9.6125 9.64286L8.4 8.39571C8.0558 8.01577 8.06595 7.4237 8.42297 7.05648C8.77999 6.68927 9.35561 6.67882 9.725 7.03286L11.6 8.96143C11.9656 9.33791 11.9656 9.94781 11.6 10.3243L9.725 12.2529C9.35898 12.6289 8.76602 12.6289 8.4 12.2529M6.6 8.39571C6.9442 8.01577 6.93404 7.4237 6.57703 7.05649C6.22001 6.68927 5.64439 6.67882 5.275 7.03286L3.4 8.96143C3.03443 9.33791 3.03443 9.94781 3.4 10.3243L5.275 12.2529C5.50871 12.5108 5.86069 12.617 6.19286 12.5298C6.52502 12.4425 6.7844 12.1757 6.86923 11.8341C6.95406 11.4924 6.85081 11.1304 6.6 10.89L5.3875 9.64286L6.6 8.39571Z"
id="Shape" fill="#E5DFD5" fill-rule="evenodd" stroke="none" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

@ -1,10 +1,7 @@
<svg width="15px" height="18px" viewBox="0 0 15 18" version="1.1" <?xml version="1.0" encoding="utf-8"?>
xmlns:xlink="http://www.w3.org/1999/xlink" <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg"> <g>
<g id="File" fill-opacity="1"> <rect width="18" height="18" />
<path d="M15 0L15 0L15 18L0 18L0 0L15 0Z" id="File" fill="none" stroke="none" /> <path id="Shape" d="M11.25 16.0714L3.75 16.0714C2.71447 16.0714 1.875 15.208 1.875 14.1429L1.875 3.85714C1.875 2.79202 2.71447 1.92857 3.75 1.92857L6.25 1.92857L6.25 5.14286C6.25 7.2731 7.92893 9 10 9L13.125 9L13.125 14.1429C13.125 15.208 12.2855 16.0714 11.25 16.0714M12.8788 7.07143C12.7961 6.92188 12.6944 6.78437 12.5763 6.66257L8.5225 2.493C8.40408 2.37151 8.2704 2.26687 8.125 2.18186L8.125 5.14286C8.125 6.20798 8.96447 7.07143 10 7.07143L12.8788 7.07143ZM13.9013 5.29843C14.6049 6.02193 15.0001 7.00338 15 8.02672L15 14.1429C15 16.2731 13.3211 18 11.25 18L3.75 18C1.67893 18 0 16.2731 0 14.1429L0 3.85714C-5.96046e-07 1.7269 1.67893 2.75893e-08 3.75 2.75893e-08L7.19625 2.75893e-08C8.19116 -0.000122281 9.14535 0.406423 9.84875 1.13014L13.9013 5.29843Z" fill="#E5DFD5" fill-rule="evenodd" transform="translate(1.5 -0)" />
<path
d="M11.25 16.0714L3.75 16.0714C2.71447 16.0714 1.875 15.208 1.875 14.1429L1.875 3.85714C1.875 2.79202 2.71447 1.92857 3.75 1.92857L6.25 1.92857L6.25 5.14286C6.25 7.2731 7.92893 9 10 9L13.125 9L13.125 14.1429C13.125 15.208 12.2855 16.0714 11.25 16.0714M12.8788 7.07143C12.7961 6.92188 12.6944 6.78437 12.5763 6.66257L8.5225 2.493C8.40408 2.37151 8.2704 2.26687 8.125 2.18186L8.125 5.14286C8.125 6.20798 8.96447 7.07143 10 7.07143L12.8788 7.07143ZM13.9013 5.29843C14.6049 6.02193 15.0001 7.00338 15 8.02672L15 14.1429C15 16.2731 13.3211 18 11.25 18L3.75 18C1.67893 18 0 16.2731 0 14.1429L0 3.85714C-5.96046e-07 1.7269 1.67893 0 3.75 0L7.19625 0C8.19116 -0.000122309 9.14535 0.406423 9.84875 1.13014L13.9013 5.29843Z"
id="Shape" fill="#E5DFD5" fill-rule="evenodd" stroke="none" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

7
public/icons/Link.svg Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="18" height="18" />
<path id="Shape" d="M2.68978 6.95235C3.0999 6.55662 3.75151 6.56259 4.1543 6.96577C4.5571 7.36895 4.56246 8.02056 4.16634 8.43031C4.16634 8.43031 3.15364 9.443 3.15364 9.443C1.68651 10.9393 1.69832 13.3381 3.18012 14.8199C4.66192 16.3017 7.06072 16.3135 8.55703 14.8464C8.55703 14.8464 9.56973 13.8337 9.56973 13.8337C9.98137 13.4501 10.6228 13.4614 11.0207 13.8593C11.4185 14.2571 11.4299 14.8986 11.0463 15.3102C11.0463 15.3102 10.035 16.3229 10.035 16.3229C7.71847 18.5799 4.01808 18.5559 1.73113 16.2689C-0.555826 13.982 -0.579909 10.2816 1.67708 7.96504C1.67708 7.96504 2.68978 6.95235 2.68978 6.95235ZM13.8337 9.56973C13.4501 9.98138 13.4614 10.6228 13.8593 11.0207C14.2571 11.4185 14.8986 11.4299 15.3103 11.0463C15.3103 11.0463 16.323 10.035 16.323 10.035C18.58 7.71847 18.5559 4.01808 16.2689 1.73113C13.982 -0.555826 10.2816 -0.579908 7.96505 1.67708C7.96505 1.67708 6.95235 2.68978 6.95235 2.68978C6.55662 3.0999 6.56259 3.75151 6.96577 4.15431C7.36895 4.55711 8.02056 4.56247 8.43031 4.16635C8.43031 4.16635 9.44301 3.15365 9.44301 3.15365C10.9393 1.68652 13.3381 1.69833 14.8199 3.18013C16.3017 4.66192 16.3135 7.06073 14.8464 8.55704C14.8464 8.55704 13.8337 9.56973 13.8337 9.56973ZM12.5242 6.9523C12.8038 6.69186 12.9188 6.29961 12.8243 5.92945C12.7297 5.55928 12.4407 5.27024 12.0705 5.1757C11.7004 5.08117 11.3081 5.19623 11.0477 5.47574C11.0477 5.47574 5.47574 11.0477 5.47574 11.0477C5.19623 11.3081 5.08117 11.7004 5.1757 12.0705C5.27024 12.4407 5.55928 12.7297 5.92945 12.8243C6.29961 12.9188 6.69186 12.8037 6.9523 12.5242C6.9523 12.5242 12.5242 6.9523 12.5242 6.9523Z" fill="#E5DFD5" fill-rule="evenodd" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,10 +1,7 @@
<svg width="15px" height="18px" viewBox="0 0 15 18" version="1.1" <?xml version="1.0" encoding="utf-8"?>
xmlns:xlink="http://www.w3.org/1999/xlink" <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg"> <g transform="translate(0 0)">
<g id="Text" fill-opacity="1"> <rect width="18" height="18" />
<path d="M15 0L15 0L15 18L0 18L0 0L15 0Z" id="Text" fill="none" stroke="none" /> <path id="Shape" d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406922 8.19178 -0.0001232 7.19625 2.79714e-08L3.75 2.79714e-08C1.67893 2.79714e-08 0 1.7269 0 3.85714L0 14.1429C2.38419e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM3.75 9.32143C3.75 8.78887 4.16973 8.35714 4.6875 8.35714L10.3125 8.35714C10.8303 8.35714 11.25 8.78887 11.25 9.32143C11.25 9.85399 10.8303 10.2857 10.3125 10.2857L4.6875 10.2857C4.16973 10.2857 3.75 9.85399 3.75 9.32143M4.6875 12.2143C4.35256 12.2143 4.04307 12.3981 3.8756 12.6964C3.70813 12.9948 3.70813 13.3624 3.8756 13.6607C4.04307 13.9591 4.35256 14.1429 4.6875 14.1429L7.8125 14.1429C8.14744 14.1429 8.45693 13.9591 8.6244 13.6607C8.79187 13.3624 8.79187 12.9948 8.6244 12.6964C8.45693 12.3981 8.14744 12.2143 7.8125 12.2143L4.6875 12.2143Z" fill="#E5DFD5" fill-rule="evenodd" transform="translate(1.5 -0)" />
<path
d="M3.75 16.0714L11.25 16.0714C12.2855 16.0714 13.125 15.208 13.125 14.1429L13.125 8.02672C13.1248 7.5147 12.9269 7.02399 12.5748 6.66239L8.52375 2.493C8.17225 2.13169 7.69568 1.92868 7.19875 1.92857L3.75 1.92857C2.71447 1.92857 1.875 2.79202 1.875 3.85714L1.875 14.1429C1.875 15.208 2.71447 16.0714 3.75 16.0714M15 8.02672C15.0003 7.00424 14.6053 6.02271 13.9018 5.29904L9.85 1.13143C9.1465 0.406921 8.19178 -0.000123228 7.19625 0L3.75 0C1.67893 0 0 1.7269 0 3.85714L0 14.1429C2.98023e-07 16.2731 1.67893 18 3.75 18L11.25 18C13.3211 18 15 16.2731 15 14.1429L15 8.02672ZM3.75 9.32143C3.75 8.78887 4.16973 8.35714 4.6875 8.35714L10.3125 8.35714C10.8303 8.35714 11.25 8.78887 11.25 9.32143C11.25 9.85399 10.8303 10.2857 10.3125 10.2857L4.6875 10.2857C4.16973 10.2857 3.75 9.85399 3.75 9.32143M4.6875 12.2143C4.35256 12.2143 4.04307 12.3981 3.8756 12.6964C3.70813 12.9948 3.70813 13.3624 3.8756 13.6607C4.04307 13.9591 4.35256 14.1429 4.6875 14.1429L7.8125 14.1429C8.14744 14.1429 8.45693 13.9591 8.6244 13.6607C8.79187 13.3624 8.79187 12.9948 8.6244 12.6964C8.45693 12.3981 8.14744 12.2143 7.8125 12.2143L4.6875 12.2143Z"
id="Shape" fill="#E5DFD5" fill-rule="evenodd" stroke="none" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

252
src-tauri/Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 3
[[package]] [[package]]
name = "active-win-pos-rs" name = "active-win-pos-rs"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9c1d770875c536934a8e7150061b0dbddb919298f0ff762b0f8fc12c8928877" checksum = "e227f8493de9f5e493f8e762ac7516d2ae42464df2e8122fcafd604f0b16c634"
dependencies = [ dependencies = [
"appkit-nsworkspace-bindings", "appkit-nsworkspace-bindings",
"core-foundation 0.9.4", "core-foundation 0.9.4",
@ -110,6 +110,31 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "applications"
version = "0.2.3"
source = "git+https://github.com/HuakunShen/applications-rs?branch=dev#ac41b051f0ebeac96213c6c32621b098634219ac"
dependencies = [
"anyhow",
"cocoa 0.25.0",
"core-foundation 0.9.4",
"glob",
"image",
"ini",
"lnk",
"objc",
"parselnk",
"plist",
"regex",
"serde",
"serde_derive",
"serde_json",
"tauri-icns",
"thiserror 1.0.63",
"walkdir",
"winreg 0.52.0",
]
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
version = "1.3.2" version = "1.3.2"
@ -411,6 +436,21 @@ dependencies = [
"which", "which",
] ]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.2"
@ -496,6 +536,17 @@ dependencies = [
"alloc-stdlib", "alloc-stdlib",
] ]
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata 0.1.10",
]
[[package]] [[package]]
name = "built" name = "built"
version = "0.7.4" version = "0.7.4"
@ -725,6 +776,22 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "cocoa"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [
"bitflags 1.3.2",
"block",
"cocoa-foundation 0.1.2",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
"foreign-types 0.5.0",
"libc",
"objc",
]
[[package]] [[package]]
name = "cocoa" name = "cocoa"
version = "0.26.0" version = "0.26.0"
@ -733,7 +800,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"block", "block",
"cocoa-foundation", "cocoa-foundation 0.2.0",
"core-foundation 0.10.0", "core-foundation 0.10.0",
"core-graphics 0.24.0", "core-graphics 0.24.0",
"foreign-types 0.5.0", "foreign-types 0.5.0",
@ -741,6 +808,20 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "cocoa-foundation"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [
"bitflags 1.3.2",
"block",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"libc",
"objc",
]
[[package]] [[package]]
name = "cocoa-foundation" name = "cocoa-foundation"
version = "0.2.0" version = "0.2.0"
@ -780,6 +861,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "configparser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7"
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@ -2500,6 +2587,15 @@ dependencies = [
"cfb", "cfb",
] ]
[[package]]
name = "ini"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a9271a5dfd4228fa56a78d7508a35c321639cc71f783bb7a5723552add87bce"
dependencies = [
"configparser",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.13" version = "0.1.13"
@ -2793,6 +2889,20 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "lnk"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e066ce29d4da51727b57c404c1270e3fa2a5ded0db1a4cb67c61f7a132421b2c"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"chrono",
"log",
"num-derive 0.3.3",
"num-traits",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@ -2847,6 +2957,18 @@ dependencies = [
"tendril", "tendril",
] ]
[[package]]
name = "markup5ever_rcdom"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2"
dependencies = [
"html5ever",
"markup5ever",
"tendril",
"xml5ever",
]
[[package]] [[package]]
name = "matches" name = "matches"
version = "0.1.10" version = "0.1.10"
@ -2888,6 +3010,18 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "meta_fetcher"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9da2f9745ec127e7852cb4ee6d4ab6c7125e029b7a69db6010b1fe538b77cb"
dependencies = [
"anyhow",
"select",
"texting_robots",
"ureq",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -3068,6 +3202,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-derive"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "num-derive" name = "num-derive"
version = "0.4.2" version = "0.4.2"
@ -3509,6 +3654,19 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "parselnk"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0088616e6efe53ab79907b9313f4743eb3f8a16eb1e0014af810164808906dc3"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"chrono",
"thiserror 1.0.63",
"widestring",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
@ -3904,12 +4062,15 @@ name = "qopy"
version = "0.2.1" version = "0.2.1"
dependencies = [ dependencies = [
"active-win-pos-rs", "active-win-pos-rs",
"applications",
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"global-hotkey", "global-hotkey",
"image", "image",
"include_dir", "include_dir",
"lazy_static", "lazy_static",
"log",
"meta_fetcher",
"rand 0.8.5", "rand 0.8.5",
"rdev", "rdev",
"regex", "regex",
@ -4127,7 +4288,7 @@ dependencies = [
"maybe-rayon", "maybe-rayon",
"new_debug_unreachable", "new_debug_unreachable",
"noop_proc_macro", "noop_proc_macro",
"num-derive", "num-derive 0.4.2",
"num-traits", "num-traits",
"once_cell", "once_cell",
"paste", "paste",
@ -4235,10 +4396,16 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata", "regex-automata 0.4.8",
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.8" version = "0.4.8"
@ -4417,6 +4584,7 @@ version = "0.23.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
dependencies = [ dependencies = [
"log",
"once_cell", "once_cell",
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -4544,6 +4712,17 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "select"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f9da09dc3f4dfdb6374cbffff7a2cffcec316874d4429899eefdc97b3b94dcd"
dependencies = [
"bit-set",
"html5ever",
"markup5ever_rcdom",
]
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.22.0" version = "0.22.0"
@ -5444,6 +5623,16 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-icns"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b7eb4d0d43724ba9ba6a6717420ee68aee377816a3edbb45db8c18862b1431"
dependencies = [
"byteorder",
"png",
]
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.0.3" version = "2.0.3"
@ -5580,9 +5769,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-prevent-default" name = "tauri-plugin-prevent-default"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d16274f883d2810fa8357124361656074599f5f9b52c8dff381ad82491b8a43" checksum = "ce34a821424cdb5c74b390ddc8f08774d836030c07ab8dd35bd180690ef1331e"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"itertools 0.13.0", "itertools 0.13.0",
@ -5758,6 +5947,22 @@ dependencies = [
"utf-8", "utf-8",
] ]
[[package]]
name = "texting_robots"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82a718a28dda2e67ad6e0464597b58eae39e2e4d0451e03d1028d71e81bb4a"
dependencies = [
"anyhow",
"bstr",
"lazy_static",
"nom",
"percent-encoding",
"regex",
"thiserror 1.0.63",
"url",
]
[[package]] [[package]]
name = "thin-slice" name = "thin-slice"
version = "0.1.1" version = "0.1.1"
@ -6200,6 +6405,22 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a"
dependencies = [
"base64 0.22.1",
"flate2",
"log",
"once_cell",
"rustls",
"rustls-pki-types",
"url",
"webpki-roots",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@ -6604,6 +6825,12 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "widestring"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -7127,6 +7354,17 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "xml5ever"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650"
dependencies = [
"log",
"mac",
"markup5ever",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.4" version = "0.7.4"

View file

@ -22,7 +22,7 @@ tauri-plugin-updater = "2.3.0"
tauri-plugin-dialog = "2.2.0" tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0" tauri-plugin-fs = "2.2.0"
tauri-plugin-clipboard = "2.1.11" tauri-plugin-clipboard = "2.1.11"
tauri-plugin-prevent-default = "1.0.0" tauri-plugin-prevent-default = "1.0.1"
tauri-plugin-global-shortcut = "2.2.0" tauri-plugin-global-shortcut = "2.2.0"
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite", "chrono"] } sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite", "chrono"] }
serde = { version = "1.0.216", features = ["derive"] } serde = { version = "1.0.216", features = ["derive"] }
@ -32,7 +32,7 @@ rdev = "0.5.3"
rand = "0.8.5" rand = "0.8.5"
base64 = "0.22.1" base64 = "0.22.1"
image = "0.25.5" image = "0.25.5"
reqwest = { version = "0.12.9", features = ["blocking"] } reqwest = { version = "0.12.9", features = ["json", "blocking"] }
url = "2.5.4" url = "2.5.4"
regex = "1.11.1" regex = "1.11.1"
sha2 = "0.10.8" sha2 = "0.10.8"
@ -40,9 +40,13 @@ lazy_static = "1.5.0"
time = "0.3.37" time = "0.3.37"
global-hotkey = "0.6.3" global-hotkey = "0.6.3"
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
log = { version = "0.4.22", features = ["std"] }
uuid = "1.11.0" uuid = "1.11.0"
active-win-pos-rs = "0.8.3" active-win-pos-rs = "0.8.4"
include_dir = "0.7.4" include_dir = "0.7.4"
# hyperpolyglot = { git = "https://github.com/0pandadev/hyperpolyglot" }
applications = { git = "https://github.com/HuakunShen/applications-rs", branch = "dev" }
meta_fetcher = "0.1.1"
[features] [features]
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]

View file

@ -1,20 +1,21 @@
use base64::{engine::general_purpose::STANDARD, Engine};
// use hyperpolyglot;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rdev::{simulate, EventType, Key}; use rdev::{simulate, EventType, Key};
use regex::Regex;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use uuid::Uuid;
use std::fs; use std::fs;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::{thread, time::Duration}; use std::{thread, time::Duration};
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime}; use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
use tauri_plugin_clipboard::Clipboard; use tauri_plugin_clipboard::Clipboard;
use tokio::runtime::Runtime as TokioRuntime; use tokio::runtime::Runtime as TokioRuntime;
use regex::Regex;
use url::Url; use url::Url;
use base64::{Engine, engine::general_purpose::STANDARD}; use uuid::Uuid;
use crate::db;
use crate::utils::commands::get_app_info; use crate::utils::commands::get_app_info;
use crate::utils::favicon::fetch_favicon_as_base64; use crate::utils::favicon::fetch_favicon_as_base64;
use crate::db;
use crate::utils::types::{ContentType, HistoryItem}; use crate::utils::types::{ContentType, HistoryItem};
lazy_static! { lazy_static! {
@ -111,17 +112,25 @@ pub fn setup<R: Runtime>(app: &AppHandle<R>) {
.unwrap_or_else(|e| e); .unwrap_or_else(|e| e);
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
pool, pool,
HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon), HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon, None),
).await; ).await;
} }
} else if available_types.files { } else if available_types.files {
println!("Handling files change"); println!("Handling files change");
if let Ok(files) = clipboard.read_files() { if let Ok(files) = clipboard.read_files() {
let files_str = files.join(", "); for file in files {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
pool, pool.clone(),
HistoryItem::new(app_name, ContentType::File, files_str, None, app_icon), HistoryItem::new(
).await; app_name.clone(),
ContentType::File,
file,
None,
app_icon.clone(),
None
),
).await;
}
} }
} else if available_types.text { } else if available_types.text {
println!("Handling text change"); println!("Handling text change");
@ -137,17 +146,36 @@ pub fn setup<R: Runtime>(app: &AppHandle<R>) {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
pool, pool,
HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon) HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon, None)
).await; ).await;
} }
} else { } else {
if text.is_empty() { if text.is_empty() {
return; return;
} }
let _ = db::history::add_history_item(
pool, // Temporarily disabled code detection
HistoryItem::new(app_name, ContentType::Text, text, None, app_icon) /*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
).await; let language = match detection {
hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(),
_ => detection.language().to_string(),
};
let _ = db::history::add_history_item(
pool,
HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language))
).await;
} else*/ if crate::utils::commands::detect_color(&text) {
let _ = db::history::add_history_item(
pool,
HistoryItem::new(app_name, ContentType::Color, text, None, app_icon, None)
).await;
} else {
let _ = db::history::add_history_item(
pool,
HistoryItem::new(app_name, ContentType::Text, text, None, app_icon, None)
).await;
}
} }
} }
} else { } else {
@ -183,7 +211,10 @@ pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
Ok(()) Ok(())
} }
async fn save_image_to_file<R: Runtime>(app_handle: &AppHandle<R>, base64_data: &str) -> Result<String, Box<dyn std::error::Error>> { async fn save_image_to_file<R: Runtime>(
app_handle: &AppHandle<R>,
base64_data: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let app_data_dir = app_handle.path().app_data_dir().unwrap(); let app_data_dir = app_handle.path().app_data_dir().unwrap();
let images_dir = app_data_dir.join("images"); let images_dir = app_data_dir.join("images");
fs::create_dir_all(&images_dir)?; fs::create_dir_all(&images_dir)?;

View file

@ -3,9 +3,9 @@ use global_hotkey::{
hotkey::{Code, HotKey, Modifiers}, hotkey::{Code, HotKey, Modifiers},
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
}; };
use std::str::FromStr;
use std::cell::RefCell; use std::cell::RefCell;
use tauri::{AppHandle, Manager, Listener}; use std::str::FromStr;
use tauri::{AppHandle, Listener, Manager};
thread_local! { thread_local! {
static HOTKEY_MANAGER: RefCell<Option<GlobalHotKeyManager>> = RefCell::new(None); static HOTKEY_MANAGER: RefCell<Option<GlobalHotKeyManager>> = RefCell::new(None);
@ -18,7 +18,8 @@ pub fn setup(app_handle: tauri::AppHandle) {
HOTKEY_MANAGER.with(|m| *m.borrow_mut() = Some(manager)); HOTKEY_MANAGER.with(|m| *m.borrow_mut() = Some(manager));
let rt = app_handle.state::<tokio::runtime::Runtime>(); let rt = app_handle.state::<tokio::runtime::Runtime>();
let initial_keybind = rt.block_on(crate::db::settings::get_keybind(app_handle_clone.clone())) let initial_keybind = rt
.block_on(crate::db::settings::get_keybind(app_handle_clone.clone()))
.expect("Failed to get initial keybind"); .expect("Failed to get initial keybind");
let initial_shortcut = initial_keybind.join("+"); let initial_shortcut = initial_keybind.join("+");
@ -111,13 +112,16 @@ fn parse_hotkey(shortcut: &str) -> Result<HotKey, Box<dyn std::error::Error>> {
key.to_string() key.to_string()
}; };
code = Some(Code::from_str(&key_code) code = Some(
.map_err(|_| format!("Invalid key code: {}", key_code))?); Code::from_str(&key_code)
.map_err(|_| format!("Invalid key code: {}", key_code))?,
);
} }
} }
} }
let key_code = code.ok_or_else(|| format!("No valid key code found in shortcut: {}", shortcut))?; let key_code =
code.ok_or_else(|| format!("No valid key code found in shortcut: {}", shortcut))?;
Ok(HotKey::new(Some(modifiers), key_code)) Ok(HotKey::new(Some(modifiers), key_code))
} }

View file

@ -1,4 +1,4 @@
pub mod updater;
pub mod clipboard; pub mod clipboard;
pub mod tray;
pub mod hotkeys; pub mod hotkeys;
pub mod tray;
pub mod updater;

View file

@ -1,5 +1,7 @@
use tauri::{ use tauri::{
menu::{MenuBuilder, MenuItemBuilder}, tray::TrayIconBuilder, Emitter, Manager menu::{MenuBuilder, MenuItemBuilder},
tray::TrayIconBuilder,
Emitter, Manager,
}; };
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {

View file

@ -1,5 +1,5 @@
use tauri::{AppHandle, async_runtime}; use tauri::{async_runtime, AppHandle};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind, MessageDialogButtons}; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
use tauri_plugin_updater::UpdaterExt; use tauri_plugin_updater::UpdaterExt;
pub async fn check_for_updates(app: AppHandle) { pub async fn check_for_updates(app: AppHandle) {

View file

@ -1,8 +1,8 @@
use include_dir::{include_dir, Dir};
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use std::fs; use std::fs;
use tauri::Manager; use tauri::Manager;
use tokio::runtime::Runtime as TokioRuntime; use tokio::runtime::Runtime as TokioRuntime;
use include_dir::{include_dir, Dir};
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations"); static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
@ -49,39 +49,32 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
} }
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> { async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
println!("Starting migration process");
// Create schema_version table
sqlx::query( sqlx::query(
"CREATE TABLE IF NOT EXISTS schema_version ( "CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY, version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);" );",
) )
.execute(pool) .execute(pool)
.await?; .await?;
let current_version: Option<i64> = sqlx::query_scalar( let current_version: Option<i64> =
"SELECT MAX(version) FROM schema_version" sqlx::query_scalar("SELECT MAX(version) FROM schema_version")
) .fetch_one(pool)
.fetch_one(pool) .await?;
.await?;
let current_version = current_version.unwrap_or(0); let current_version = current_version.unwrap_or(0);
println!("Current database version: {}", current_version);
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR
.files() .files()
.filter_map(|file| { .filter_map(|file| {
let file_name = file.path().file_name()?.to_str()?; let file_name = file.path().file_name()?.to_str()?;
println!("Processing file: {}", file_name);
if file_name.ends_with(".sql") && file_name.starts_with("migration") { if file_name.ends_with(".sql") && file_name.starts_with("migration") {
let version: i64 = file_name let version: i64 = file_name
.trim_start_matches("migration") .trim_start_matches("migration")
.trim_end_matches(".sql") .trim_end_matches(".sql")
.parse() .parse()
.ok()?; .ok()?;
println!("Found migration version: {}", version);
Some((version, file.contents_utf8()?)) Some((version, file.contents_utf8()?))
} else { } else {
None None
@ -93,8 +86,6 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::E
for (version, content) in migration_files { for (version, content) in migration_files {
if version > current_version { if version > current_version {
println!("Applying migration {}", version);
let statements: Vec<&str> = content let statements: Vec<&str> = content
.split(';') .split(';')
.map(|s| s.trim()) .map(|s| s.trim())
@ -102,7 +93,6 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::E
.collect(); .collect();
for statement in statements { for statement in statements {
println!("Executing statement: {}", statement);
sqlx::query(statement) sqlx::query(statement)
.execute(pool) .execute(pool)
.await .await

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,34 +56,45 @@ 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 existing = sqlx::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1", .bind(&content)
) .bind(&content_type)
.bind(content_type.clone()) .fetch_optional(&*pool)
.fetch_one(&*pool) .await
.await .map_err(|e| e.to_string())?;
.unwrap_or(None);
if last_content.as_deref() == Some(&content) { match existing {
return Ok(()); Some(_) => {
sqlx::query(
"UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?"
)
.bind(&content)
.bind(&content_type)
.execute(&*pool)
.await
.map_err(|e| e.to_string())?;
}
None => {
sqlx::query(
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
)
.bind(id)
.bind(source)
.bind(source_icon)
.bind(content_type)
.bind(content)
.bind(favicon)
.bind(timestamp)
.bind(language)
.execute(&*pool)
.await
.map_err(|e| e.to_string())?;
}
} }
sqlx::query(
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)"
)
.bind(id)
.bind(source)
.bind(source_icon)
.bind(content_type)
.bind(content)
.bind(favicon)
.bind(timestamp)
.execute(&*pool)
.await
.map_err(|e| e.to_string())?;
Ok(()) Ok(())
} }
@ -93,7 +105,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 +122,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 +136,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 +154,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,8 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use serde_json; use serde_json;
use tauri::{Emitter, Manager};
use sqlx::Row; use sqlx::Row;
use sqlx::SqlitePool;
use tauri::{Emitter, Manager};
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct KeybindSetting { struct KeybindSetting {
@ -15,12 +15,10 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
}; };
let json = serde_json::to_string(&default_keybind)?; let json = serde_json::to_string(&default_keybind)?;
sqlx::query( sqlx::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
"INSERT INTO settings (key, value) VALUES ('keybind', ?)" .bind(json)
) .execute(pool)
.bind(json) .await?;
.execute(pool)
.await?;
Ok(()) Ok(())
} }
@ -50,7 +48,7 @@ pub async fn save_keybind(
#[tauri::command] #[tauri::command]
pub async fn get_setting( pub async fn get_setting(
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
key: String key: String,
) -> Result<String, String> { ) -> Result<String, String> {
let row = sqlx::query("SELECT value FROM settings WHERE key = ?") let row = sqlx::query("SELECT value FROM settings WHERE key = ?")
.bind(key) .bind(key)
@ -65,7 +63,7 @@ pub async fn get_setting(
pub async fn save_setting( pub async fn save_setting(
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
key: String, key: String,
value: String value: String,
) -> Result<(), String> { ) -> Result<(), String> {
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
.bind(key) .bind(key)
@ -78,9 +76,7 @@ pub async fn save_setting(
} }
#[tauri::command] #[tauri::command]
pub async fn get_keybind( pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
app_handle: tauri::AppHandle,
) -> Result<Vec<String>, String> {
let pool = app_handle.state::<SqlitePool>(); let pool = app_handle.state::<SqlitePool>();
let row = sqlx::query("SELECT value FROM settings WHERE key = 'keybind'") let row = sqlx::query("SELECT value FROM settings WHERE key = 'keybind'")
@ -88,13 +84,10 @@ pub async fn get_keybind(
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let json = row let json = row.map(|r| r.get::<String, _>("value")).unwrap_or_else(|| {
.map(|r| r.get::<String, _>("value")) serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()])
.unwrap_or_else(|| { .expect("Failed to serialize default keybind")
serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()]) });
.expect("Failed to serialize default keybind")
});
serde_json::from_str::<Vec<String>>(&json) serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
.map_err(|e| e.to_string())
} }

View file

@ -34,6 +34,8 @@ fn main() {
) )
.setup(|app| { .setup(|app| {
let app_data_dir = app.path().app_data_dir().unwrap(); let app_data_dir = app.path().app_data_dir().unwrap();
utils::logger::init_logger(&app_data_dir).expect("Failed to initialize logger");
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory"); fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
let db_path = app_data_dir.join("data.db"); let db_path = app_data_dir.join("data.db");
@ -110,6 +112,7 @@ fn main() {
db::settings::save_setting, db::settings::save_setting,
db::settings::save_keybind, db::settings::save_keybind,
db::settings::get_keybind, db::settings::get_keybind,
utils::commands::fetch_page_meta,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View file

@ -1,4 +1,8 @@
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;
use meta_fetcher;
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) { pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
if let Some(monitor) = window.available_monitors().unwrap().iter().find(|m| { if let Some(monitor) = window.available_monitors().unwrap().iter().find(|m| {
@ -28,3 +32,78 @@ 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))
}
pub fn detect_color(color: &str) -> bool {
let color = color.trim().to_lowercase();
// hex
if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() {
let hex = &color[1..];
return match hex.len() {
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
_ => false
};
}
// rgb/rgba
if (color.starts_with("rgb(") || color.starts_with("rgba(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") {
let values = color
.trim_start_matches("rgba(")
.trim_start_matches("rgb(")
.trim_end_matches(')')
.split(',')
.collect::<Vec<&str>>();
return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false
};
}
// hsl/hsla
if (color.starts_with("hsl(") || color.starts_with("hsla(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") {
let values = color
.trim_start_matches("hsla(")
.trim_start_matches("hsl(")
.trim_end_matches(')')
.split(',')
.collect::<Vec<&str>>();
return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false
};
}
false
}
#[tauri::command]
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
let metadata = meta_fetcher::fetch_metadata(&url)
.map_err(|e| format!("Failed to fetch metadata: {}", e))?;
Ok((
metadata.title.unwrap_or_else(|| "No title found".to_string()),
metadata.image
))
}

View file

@ -4,7 +4,9 @@ use image::ImageFormat;
use reqwest; use reqwest;
use url::Url; use url::Url;
pub async fn fetch_favicon_as_base64(url: Url) -> Result<Option<String>, Box<dyn std::error::Error>> { pub async fn fetch_favicon_as_base64(
url: Url,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap()); let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
let response = client.get(&favicon_url).send().await?; let response = client.get(&favicon_url).send().await?;

View file

@ -0,0 +1,49 @@
use chrono;
use log::{LevelFilter, SetLoggerError};
use std::fs::{File, OpenOptions};
use std::io::Write;
pub struct FileLogger {
file: File,
}
impl log::Log for FileLogger {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let mut file = self.file.try_clone().expect("Failed to clone file handle");
writeln!(
file,
"{} - {}: {}",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.args()
)
.expect("Failed to write to log file");
}
}
fn flush(&self) {
self.file.sync_all().expect("Failed to flush log file");
}
}
pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> {
let logs_dir = app_data_dir.join("logs");
std::fs::create_dir_all(&logs_dir).expect("Failed to create logs directory");
let log_path = logs_dir.join("app.log");
let file = OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
.expect("Failed to open log file");
let logger = Box::new(FileLogger { file });
unsafe { log::set_logger_racy(Box::leak(logger))? };
log::set_max_level(LevelFilter::Debug);
Ok(())
}

View file

@ -1,3 +1,4 @@
pub mod types;
pub mod commands; pub mod commands;
pub mod favicon; pub mod favicon;
pub mod types;
pub mod logger;

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)]
@ -26,7 +27,7 @@ pub enum ContentType {
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Text { pub struct InfoText {
pub source: String, pub source: String,
pub content_type: ContentType, pub content_type: ContentType,
pub characters: i32, pub characters: i32,
@ -35,7 +36,7 @@ pub struct Text {
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Image { pub struct InfoImage {
pub source: String, pub source: String,
pub content_type: ContentType, pub content_type: ContentType,
pub dimensions: String, pub dimensions: String,
@ -44,7 +45,7 @@ pub struct Image {
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct File { pub struct InfoFile {
pub source: String, pub source: String,
pub content_type: ContentType, pub content_type: ContentType,
pub path: String, pub path: String,
@ -53,26 +54,26 @@ pub struct File {
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Link { pub struct InfoLink {
pub source: String, pub source: String,
pub content_type: ContentType, pub content_type: ContentType,
pub title: String, pub title: Option<String>,
pub link: String, pub url: String,
pub characters: i32, pub characters: i32,
pub copied: DateTime<Utc>, pub copied: DateTime<Utc>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Color { pub struct InfoColor {
pub source: String, pub source: String,
pub content_type: ContentType, pub content_type: ContentType,
pub hexcode: String, pub hex: String,
pub rgba: String, pub rgb: String,
pub copied: DateTime<Utc>, pub copied: DateTime<Utc>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Code { pub struct InfoCode {
pub source: String, pub source: String,
pub content_type: ContentType, pub content_type: ContentType,
pub language: String, pub language: String,
@ -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,53 @@ export interface Settings {
key: string; key: string;
value: string; value: string;
} }
export interface InfoText {
source: string;
content_type: ContentType.Text;
characters: number;
words: number;
copied: Date;
}
export interface InfoImage {
source: string;
content_type: ContentType.Image;
dimensions: string;
size: number;
copied: Date;
}
export interface InfoFile {
source: string;
content_type: ContentType.File;
path: string;
filesize: number;
copied: Date;
}
export interface InfoLink {
source: string;
content_type: ContentType.Link;
title?: string;
url: string;
characters: number;
copied: Date;
}
export interface InfoColor {
source: string;
content_type: ContentType.Color;
hex: string;
rgb: string;
hsl: string;
copied: Date;
}
export interface InfoCode {
source: string;
content_type: ContentType.Code;
language: string;
lines: number;
copied: Date;
}