Merge pull request #20 from 0PandaDEV/issue/metadata
Metadata/Information for Items
|
@ -89,7 +89,7 @@ $mutedtext: #78756f;
|
|||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
@ -98,32 +98,32 @@ $mutedtext: #78756f;
|
|||
position: absolute;
|
||||
top: 53px;
|
||||
left: 284px;
|
||||
padding: 8px;
|
||||
height: calc(100vh - 256px);
|
||||
height: calc(100vh - 254px);
|
||||
font-family: CommitMono Nerd Font !important;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1;
|
||||
border-radius: 10px;
|
||||
width: calc(100vw - 286px);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
&:not(:has(.image)) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: CommitMono Nerd Font !important;
|
||||
}
|
||||
|
||||
.full-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: top left;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,6 +212,9 @@ $mutedtext: #78756f;
|
|||
|
||||
.information {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
bottom: 40px;
|
||||
left: 284px;
|
||||
height: 160px;
|
||||
|
@ -225,6 +228,47 @@ $mutedtext: #78756f;
|
|||
font-size: 12px;
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-direction: column;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid $divider;
|
||||
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 {
|
||||
|
|
418
pages/index.vue
|
@ -63,11 +63,13 @@
|
|||
@error="onImageError" />
|
||||
<img v-else src="../public/icons/Image.svg" class="icon" />
|
||||
</template>
|
||||
<template v-else-if="hasFavicon(item.favicon ?? '')">
|
||||
<img
|
||||
v-else-if="hasFavicon(item.favicon ?? '')"
|
||||
:src="getFaviconFromDb(item.favicon ?? '')"
|
||||
:src="item.favicon ? getFaviconFromDb(item.favicon) : '../public/icons/Link.svg'"
|
||||
alt="Favicon"
|
||||
class="favicon" />
|
||||
class="favicon"
|
||||
@error="($event.target as HTMLImageElement).src = '../public/icons/Link.svg'" />
|
||||
</template>
|
||||
<img
|
||||
src="../public/icons/File.svg"
|
||||
class="icon"
|
||||
|
@ -76,6 +78,24 @@
|
|||
src="../public/icons/Text.svg"
|
||||
class="icon"
|
||||
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
|
||||
src="../public/icons/Code.svg"
|
||||
class="icon"
|
||||
|
@ -87,20 +107,42 @@
|
|||
</div>
|
||||
</template>
|
||||
</OverlayScrollbarsComponent>
|
||||
<div class="content" v-if="selectedItem?.content_type === 'image'">
|
||||
<img :src="getComputedImageUrl(selectedItem)" alt="Image" class="image" />
|
||||
|
||||
<div
|
||||
class="content"
|
||||
v-if="selectedItem?.content_type === ContentType.Image">
|
||||
<img :src="imageUrls[selectedItem.id]" alt="Image" class="image" />
|
||||
</div>
|
||||
<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>
|
||||
<OverlayScrollbarsComponent v-else class="content">
|
||||
<img
|
||||
v-if="selectedItem?.content && isYoutubeWatchUrl(selectedItem.content)"
|
||||
:src="getYoutubeThumbnail(selectedItem.content)"
|
||||
alt="YouTube Thumbnail"
|
||||
class="full-image" />
|
||||
<span v-else>{{ selectedItem?.content || "" }}</span>
|
||||
<span>{{ selectedItem?.content || "" }}</span>
|
||||
</OverlayScrollbarsComponent>
|
||||
<div class="information">
|
||||
|
||||
<OverlayScrollbarsComponent
|
||||
class="information"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }">
|
||||
<div class="title">Information</div>
|
||||
<div class="info-content" v-if="selectedItem && 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 />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -114,17 +156,18 @@ import { platform } from "@tauri-apps/plugin-os";
|
|||
import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useNuxtApp } from "#app";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { HistoryItem, ContentType } from "~/types/types";
|
||||
import type { InfoText, InfoImage, InfoFile, InfoLink, InfoColor, InfoCode } from "~/types/types";
|
||||
|
||||
interface GroupedHistory {
|
||||
label: string;
|
||||
items: HistoryItem[];
|
||||
}
|
||||
|
||||
const { $history, $settings } = useNuxtApp();
|
||||
const { $history } = useNuxtApp();
|
||||
const CHUNK_SIZE = 50;
|
||||
const SCROLL_THRESHOLD = 100;
|
||||
const IMAGE_LOAD_DEBOUNCE = 300;
|
||||
|
||||
const history = shallowRef<HistoryItem[]>([]);
|
||||
let offset = 0;
|
||||
|
@ -141,9 +184,12 @@ const searchInput = ref<HTMLInputElement | null>(null);
|
|||
const os = ref<string>("");
|
||||
const imageUrls = shallowRef<Record<string, string>>({});
|
||||
const imageDimensions = shallowRef<Record<string, string>>({});
|
||||
const imageSizes = shallowRef<Record<string, string>>({});
|
||||
const lastUpdateTime = ref<number>(Date.now());
|
||||
const imageLoadError = ref<boolean>(false);
|
||||
const imageLoading = ref<boolean>(false);
|
||||
const pageTitle = ref<string>('');
|
||||
const pageOgImage = ref<string>('');
|
||||
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
|
@ -230,7 +276,9 @@ const loadHistoryChunk = async (): Promise<void> => {
|
|||
item.source,
|
||||
item.content_type,
|
||||
item.content,
|
||||
item.favicon
|
||||
item.favicon,
|
||||
item.source_icon,
|
||||
item.language
|
||||
);
|
||||
Object.assign(historyItem, {
|
||||
id: item.id,
|
||||
|
@ -238,10 +286,34 @@ const loadHistoryChunk = async (): Promise<void> => {
|
|||
});
|
||||
|
||||
if (historyItem.content_type === ContentType.Image) {
|
||||
await Promise.all([
|
||||
getItemDimensions(historyItem),
|
||||
loadImageUrl(historyItem),
|
||||
]);
|
||||
try {
|
||||
const base64 = await $history.readImage({
|
||||
filename: historyItem.content,
|
||||
});
|
||||
const size = Math.ceil((base64.length * 3) / 4);
|
||||
imageSizes.value[historyItem.id] = formatFileSize(size);
|
||||
|
||||
const img = new Image();
|
||||
img.src = `data:image/png;base64,${base64}`;
|
||||
imageUrls.value[historyItem.id] = img.src;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
img.onload = () => {
|
||||
imageDimensions.value[
|
||||
historyItem.id
|
||||
] = `${img.width}x${img.height}`;
|
||||
resolve();
|
||||
};
|
||||
img.onerror = () => {
|
||||
imageDimensions.value[historyItem.id] = "Error";
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing image:", error);
|
||||
imageDimensions.value[historyItem.id] = "Error";
|
||||
imageSizes.value[historyItem.id] = "Error";
|
||||
}
|
||||
}
|
||||
return historyItem;
|
||||
})
|
||||
|
@ -281,7 +353,9 @@ const scrollToSelectedItem = (forceScrollTop: boolean = false): void => {
|
|||
|
||||
if (isAbove || isBelow) {
|
||||
const scrollOffset = isAbove
|
||||
? elementRect.top - viewportRect.top - 8
|
||||
? elementRect.top -
|
||||
viewportRect.top -
|
||||
(selectedItemIndex.value === 0 ? 36 : 8)
|
||||
: elementRect.bottom - viewportRect.bottom + 9;
|
||||
|
||||
viewport.scrollBy({ top: scrollOffset, behavior: "smooth" });
|
||||
|
@ -347,7 +421,7 @@ const pasteSelectedItem = async (): Promise<void> => {
|
|||
let contentType: string = selectedItem.value.content_type;
|
||||
if (contentType === "image") {
|
||||
try {
|
||||
content = await $history.getImagePath(content);
|
||||
content = await $history.readImage({ filename: content });
|
||||
} catch (error) {
|
||||
console.error("Error reading image file:", error);
|
||||
return;
|
||||
|
@ -386,64 +460,68 @@ const getYoutubeThumbnail = (url: string): string => {
|
|||
videoId = url.match(/[?&]v=([^&]+)/)?.[1];
|
||||
}
|
||||
return videoId
|
||||
? `https://img.youtube.com/vi/${videoId}/0.jpg`
|
||||
: "https://via.placeholder.com/150";
|
||||
? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
|
||||
: "https://via.placeholder.com/1280x720";
|
||||
};
|
||||
|
||||
const getFaviconFromDb = (favicon: string): string => {
|
||||
return `data:image/png;base64,${favicon}`;
|
||||
};
|
||||
|
||||
const getImageData = async (
|
||||
item: HistoryItem
|
||||
): Promise<{ url: string; dimensions: string }> => {
|
||||
try {
|
||||
const base64 = await $history.readImage({ filename: item.content });
|
||||
const dataUrl = `data:image/png;base64,${base64}`;
|
||||
const img = new Image();
|
||||
img.src = dataUrl;
|
||||
const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
||||
const results = await $history.loadHistoryChunk(0, CHUNK_SIZE);
|
||||
if (results.length > 0) {
|
||||
const existingIds = new Set(history.value.map((item) => item.id));
|
||||
const uniqueNewItems = results.filter((item) => !existingIds.has(item.id));
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = reject;
|
||||
});
|
||||
|
||||
return {
|
||||
url: dataUrl,
|
||||
dimensions: `${img.width}x${img.height}`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing image:", error);
|
||||
return { url: "", dimensions: "Error" };
|
||||
}
|
||||
};
|
||||
|
||||
const processHistoryItem = async (item: any): Promise<HistoryItem> => {
|
||||
const processedNewItems = await Promise.all(
|
||||
uniqueNewItems.map(async (item) => {
|
||||
const historyItem = new HistoryItem(
|
||||
item.content_type as string,
|
||||
ContentType[item.content_type as keyof typeof ContentType],
|
||||
item.source,
|
||||
item.content_type,
|
||||
item.content,
|
||||
item.favicon
|
||||
);
|
||||
|
||||
Object.assign(historyItem, {
|
||||
id: item.id,
|
||||
timestamp: new Date(item.timestamp),
|
||||
});
|
||||
|
||||
if (historyItem.content_type === ContentType.Image) {
|
||||
const { url, dimensions } = await getImageData(historyItem);
|
||||
imageUrls.value[historyItem.id] = url;
|
||||
imageDimensions.value[historyItem.id] = dimensions;
|
||||
}
|
||||
try {
|
||||
const base64 = await $history.readImage({
|
||||
filename: historyItem.content,
|
||||
});
|
||||
const size = Math.ceil((base64.length * 3) / 4);
|
||||
imageSizes.value[historyItem.id] = formatFileSize(size);
|
||||
|
||||
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 = [];
|
||||
offset = 0;
|
||||
await loadHistoryChunk();
|
||||
history.value = [...processedNewItems, ...history.value];
|
||||
|
||||
if (
|
||||
resetScroll &&
|
||||
|
@ -454,6 +532,7 @@ const updateHistory = async (resetScroll: boolean = false): Promise<void> => {
|
|||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelection = (
|
||||
|
@ -466,29 +545,13 @@ const handleSelection = (
|
|||
if (shouldScroll) scrollToSelectedItem();
|
||||
};
|
||||
|
||||
const handleMediaContent = async (
|
||||
content: string,
|
||||
type: string
|
||||
): Promise<string> => {
|
||||
if (type === "image") {
|
||||
return await $history.getImagePath(content);
|
||||
}
|
||||
|
||||
if (isYoutubeWatchUrl(content)) {
|
||||
const videoId = content.includes("youtu.be")
|
||||
? content.split("youtu.be/")[1]
|
||||
: content.match(/[?&]v=([^&]+)/)?.[1];
|
||||
return videoId ? `https://img.youtube.com/vi/${videoId}/0.jpg` : "";
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const setupEventListeners = async (): Promise<void> => {
|
||||
await listen("clipboard-content-updated", async () => {
|
||||
lastUpdateTime.value = Date.now();
|
||||
handleSelection(0, 0, false);
|
||||
await updateHistory(true);
|
||||
if (groupedHistory.value[0]?.items.length > 0) {
|
||||
handleSelection(0, 0, false);
|
||||
}
|
||||
});
|
||||
|
||||
await listen("tauri://focus", async () => {
|
||||
|
@ -506,17 +569,12 @@ const setupEventListeners = async (): Promise<void> => {
|
|||
lastUpdateTime.value = currentTime;
|
||||
handleSelection(previousState.groupIndex, previousState.itemIndex, false);
|
||||
|
||||
nextTick(() => {
|
||||
const viewport = resultsContainer.value
|
||||
?.osInstance()
|
||||
?.elements().viewport;
|
||||
if (viewport) {
|
||||
viewport.scrollTo({
|
||||
if (resultsContainer.value?.osInstance()?.elements().viewport?.scrollTo) {
|
||||
resultsContainer.value.osInstance()?.elements().viewport?.scrollTo({
|
||||
top: previousState.scroll,
|
||||
behavior: "instant",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
focusSearchInput();
|
||||
});
|
||||
|
@ -558,7 +616,6 @@ const setupEventListeners = async (): Promise<void> => {
|
|||
|
||||
if (isMacActionCombo || isOtherOsActionCombo) {
|
||||
event.preventDefault();
|
||||
console.log("Actions shortcut triggered");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -611,40 +668,179 @@ watch([selectedGroupIndex, selectedItemIndex], () =>
|
|||
scrollToSelectedItem(false)
|
||||
);
|
||||
|
||||
const getItemDimensions = async (item: HistoryItem) => {
|
||||
if (!imageDimensions.value[item.id]) {
|
||||
try {
|
||||
const base64 = await $history.readImage({ filename: item.content });
|
||||
const img = new Image();
|
||||
img.src = `data:image/png;base64,${base64}`;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject();
|
||||
const getFormattedDate = computed(() => {
|
||||
if (!selectedItem.value?.timestamp) return "";
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium",
|
||||
}).format(selectedItem.value.timestamp);
|
||||
});
|
||||
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 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]}`;
|
||||
};
|
||||
|
||||
const loadImageUrl = async (item: HistoryItem) => {
|
||||
if (!imageUrls.value[item.id]) {
|
||||
const fetchPageMeta = async (url: string) => {
|
||||
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 [title, ogImage] = await invoke('fetch_page_meta', { url }) as [string, string | null];
|
||||
pageTitle.value = title;
|
||||
if (ogImage) {
|
||||
pageOgImage.value = ogImage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching page meta:', error);
|
||||
pageTitle.value = 'Error loading title';
|
||||
}
|
||||
};
|
||||
|
||||
const getComputedImageUrl = (item: HistoryItem | null): string => {
|
||||
if (!item) return "";
|
||||
return imageUrls.value[item.id] || "";
|
||||
watch(() => selectedItem.value, (newItem) => {
|
||||
if (newItem?.content_type === ContentType.Link) {
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -14,7 +14,10 @@
|
|||
<p>Save</p>
|
||||
<div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,16 +31,14 @@
|
|||
@keydown="onKeyDown"
|
||||
class="keybind-input"
|
||||
ref="keybindInput"
|
||||
tabindex="0"
|
||||
>
|
||||
tabindex="0">
|
||||
<span class="key" v-if="keybind.length === 0">Click here</span>
|
||||
<template v-else>
|
||||
<span
|
||||
:key="index"
|
||||
class="key"
|
||||
:class="{ modifier: isModifier(key) }"
|
||||
v-for="(key, index) in keybind"
|
||||
>
|
||||
v-for="(key, index) in keybind">
|
||||
{{ keyToDisplay(key) }}
|
||||
</span>
|
||||
</template>
|
||||
|
@ -47,46 +48,54 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { platform } from '@tauri-apps/plugin-os';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const activeModifiers = reactive<Set<string>>(new Set());
|
||||
const isKeybindInputFocused = ref(false);
|
||||
const keybind = ref<string[]>([]);
|
||||
const keybindInput = ref<HTMLElement | null>(null);
|
||||
const lastBlurTime = ref(0);
|
||||
const os = ref('');
|
||||
const os = ref("");
|
||||
const router = useRouter();
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const keyToDisplayMap: Record<string, string> = {
|
||||
' ': 'Space',
|
||||
Alt: 'Alt',
|
||||
AltLeft: 'Alt L',
|
||||
AltRight: 'Alt R',
|
||||
ArrowDown: '↓',
|
||||
ArrowLeft: '←',
|
||||
ArrowRight: '→',
|
||||
ArrowUp: '↑',
|
||||
Control: 'Ctrl',
|
||||
ControlLeft: 'Ctrl L',
|
||||
ControlRight: 'Ctrl R',
|
||||
Enter: '↵',
|
||||
Meta: 'Meta',
|
||||
MetaLeft: 'Meta L',
|
||||
MetaRight: 'Meta R',
|
||||
Shift: '⇧',
|
||||
ShiftLeft: '⇧ L',
|
||||
ShiftRight: '⇧ R',
|
||||
" ": "Space",
|
||||
Alt: "Alt",
|
||||
AltLeft: "Alt L",
|
||||
AltRight: "Alt R",
|
||||
ArrowDown: "↓",
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
ArrowUp: "↑",
|
||||
Control: "Ctrl",
|
||||
ControlLeft: "Ctrl L",
|
||||
ControlRight: "Ctrl R",
|
||||
Enter: "↵",
|
||||
Meta: "Meta",
|
||||
MetaLeft: "Meta L",
|
||||
MetaRight: "Meta R",
|
||||
Shift: "⇧",
|
||||
ShiftLeft: "⇧ L",
|
||||
ShiftRight: "⇧ R",
|
||||
};
|
||||
|
||||
const modifierKeySet = new Set([
|
||||
'Alt', 'AltLeft', 'AltRight',
|
||||
'Control', 'ControlLeft', 'ControlRight',
|
||||
'Meta', 'MetaLeft', 'MetaRight',
|
||||
'Shift', 'ShiftLeft', 'ShiftRight'
|
||||
"Alt",
|
||||
"AltLeft",
|
||||
"AltRight",
|
||||
"Control",
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
"Meta",
|
||||
"MetaLeft",
|
||||
"MetaRight",
|
||||
"Shift",
|
||||
"ShiftLeft",
|
||||
"ShiftRight",
|
||||
]);
|
||||
|
||||
const isModifier = (key: string): boolean => {
|
||||
|
@ -99,7 +108,7 @@ const keyToDisplay = (key: string): string => {
|
|||
|
||||
const updateKeybind = () => {
|
||||
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];
|
||||
};
|
||||
|
||||
|
@ -118,7 +127,7 @@ const onKeyDown = (event: KeyboardEvent) => {
|
|||
event.preventDefault();
|
||||
const key = event.code;
|
||||
|
||||
if (key === 'Escape') {
|
||||
if (key === "Escape") {
|
||||
if (keybindInput.value) {
|
||||
keybindInput.value.blur();
|
||||
}
|
||||
|
@ -128,7 +137,7 @@ const onKeyDown = (event: KeyboardEvent) => {
|
|||
if (isModifier(key)) {
|
||||
activeModifiers.add(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);
|
||||
}
|
||||
|
||||
|
@ -136,40 +145,45 @@ const onKeyDown = (event: KeyboardEvent) => {
|
|||
};
|
||||
|
||||
const saveKeybind = async () => {
|
||||
console.log('New:', keybind.value);
|
||||
const oldKeybind = await invoke<string[]>('get_keybind');
|
||||
console.log('Old:', oldKeybind);
|
||||
await invoke('save_keybind', { keybind: keybind.value });
|
||||
console.log("New:", keybind.value);
|
||||
const oldKeybind = await invoke<string[]>("get_keybind");
|
||||
console.log("Old:", oldKeybind);
|
||||
await invoke("save_keybind", { keybind: keybind.value });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
os.value = platform();
|
||||
|
||||
keyboard.down('all', (event) => {
|
||||
const isMacSaveCombo = os.value === 'macos' &&
|
||||
(event.code === 'MetaLeft' || event.code === 'MetaRight') &&
|
||||
event.key === 'Enter';
|
||||
keyboard.down("all", (event) => {
|
||||
const isMacSaveCombo =
|
||||
os.value === "macos" &&
|
||||
(event.code === "MetaLeft" || event.code === "MetaRight") &&
|
||||
event.key === "Enter";
|
||||
|
||||
const isOtherOsSaveCombo = os.value !== 'macos' &&
|
||||
(event.code === 'ControlLeft' || event.code === 'ControlRight') &&
|
||||
event.key === 'Enter';
|
||||
const isOtherOsSaveCombo =
|
||||
os.value !== "macos" &&
|
||||
(event.code === "ControlLeft" || event.code === "ControlRight") &&
|
||||
event.key === "Enter";
|
||||
|
||||
if ((isMacSaveCombo || isOtherOsSaveCombo) && !isKeybindInputFocused.value) {
|
||||
if (
|
||||
(isMacSaveCombo || isOtherOsSaveCombo) &&
|
||||
!isKeybindInputFocused.value
|
||||
) {
|
||||
event.preventDefault();
|
||||
saveKeybind();
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.down('Escape', (event) => {
|
||||
keyboard.down("Escape", (event) => {
|
||||
const now = Date.now();
|
||||
if (!isKeybindInputFocused.value && now - lastBlurTime.value > 100) {
|
||||
event.preventDefault();
|
||||
router.push('/');
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use '~/assets/css/settings.scss';
|
||||
@use "~/assets/css/settings.scss";
|
||||
</style>
|
|
@ -27,8 +27,12 @@ export default defineNuxtPlugin(() => {
|
|||
});
|
||||
},
|
||||
|
||||
async getImagePath(path: string): Promise<string> {
|
||||
return await invoke<string>("get_image_path", { path });
|
||||
async deleteHistoryItem(id: string): Promise<void> {
|
||||
await invoke<void>("delete_history_item", { id });
|
||||
},
|
||||
|
||||
async clearHistory(): Promise<void> {
|
||||
await invoke<void>("clear_history");
|
||||
},
|
||||
|
||||
async writeAndPaste(data: {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
<svg width="15px" height="18px" viewBox="0 0 15 18" version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Code" fill-opacity="1">
|
||||
<path d="M15 0L15 0L15 18L0 18L0 0L15 0Z" id="Code" fill="none" stroke="none" />
|
||||
<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" />
|
||||
<?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="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)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -1,10 +1,7 @@
|
|||
<svg width="15px" height="18px" viewBox="0 0 15 18" version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="File" fill-opacity="1">
|
||||
<path d="M15 0L15 0L15 18L0 18L0 0L15 0Z" id="File" fill="none" stroke="none" />
|
||||
<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" />
|
||||
<?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="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)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1 KiB |
7
public/icons/Link.svg
Normal 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 |
|
@ -1,10 +1,7 @@
|
|||
<svg width="15px" height="18px" viewBox="0 0 15 18" version="1.1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Text" fill-opacity="1">
|
||||
<path d="M15 0L15 0L15 18L0 18L0 0L15 0Z" id="Text" fill="none" stroke="none" />
|
||||
<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" />
|
||||
<?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 transform="translate(0 0)">
|
||||
<rect width="18" height="18" />
|
||||
<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)" />
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
252
src-tauri/Cargo.lock
generated
|
@ -4,9 +4,9 @@ version = 3
|
|||
|
||||
[[package]]
|
||||
name = "active-win-pos-rs"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c1d770875c536934a8e7150061b0dbddb919298f0ff762b0f8fc12c8928877"
|
||||
checksum = "e227f8493de9f5e493f8e762ac7516d2ae42464df2e8122fcafd604f0b16c634"
|
||||
dependencies = [
|
||||
"appkit-nsworkspace-bindings",
|
||||
"core-foundation 0.9.4",
|
||||
|
@ -110,6 +110,31 @@ dependencies = [
|
|||
"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]]
|
||||
name = "arbitrary"
|
||||
version = "1.3.2"
|
||||
|
@ -411,6 +436,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
|
@ -496,6 +536,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "built"
|
||||
version = "0.7.4"
|
||||
|
@ -725,6 +776,22 @@ dependencies = [
|
|||
"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]]
|
||||
name = "cocoa"
|
||||
version = "0.26.0"
|
||||
|
@ -733,7 +800,7 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
|
|||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"cocoa-foundation 0.2.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types 0.5.0",
|
||||
|
@ -741,6 +808,20 @@ dependencies = [
|
|||
"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]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.2.0"
|
||||
|
@ -780,6 +861,12 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "configparser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
|
@ -2500,6 +2587,15 @@ dependencies = [
|
|||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ini"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a9271a5dfd4228fa56a78d7508a35c321639cc71f783bb7a5723552add87bce"
|
||||
dependencies = [
|
||||
"configparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
|
@ -2793,6 +2889,20 @@ version = "0.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
|
@ -2847,6 +2957,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
|
@ -2888,6 +3010,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
@ -3068,6 +3202,17 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
|
@ -3509,6 +3654,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
|
@ -3904,12 +4062,15 @@ name = "qopy"
|
|||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"active-win-pos-rs",
|
||||
"applications",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"global-hotkey",
|
||||
"image",
|
||||
"include_dir",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"meta_fetcher",
|
||||
"rand 0.8.5",
|
||||
"rdev",
|
||||
"regex",
|
||||
|
@ -4127,7 +4288,7 @@ dependencies = [
|
|||
"maybe-rayon",
|
||||
"new_debug_unreachable",
|
||||
"noop_proc_macro",
|
||||
"num-derive",
|
||||
"num-derive 0.4.2",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
|
@ -4235,10 +4396,16 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.8"
|
||||
|
@ -4417,6 +4584,7 @@ version = "0.23.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
@ -4544,6 +4712,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "selectors"
|
||||
version = "0.22.0"
|
||||
|
@ -5444,6 +5623,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.3"
|
||||
|
@ -5580,9 +5769,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tauri-plugin-prevent-default"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d16274f883d2810fa8357124361656074599f5f9b52c8dff381ad82491b8a43"
|
||||
checksum = "ce34a821424cdb5c74b390ddc8f08774d836030c07ab8dd35bd180690ef1331e"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"itertools 0.13.0",
|
||||
|
@ -5758,6 +5947,22 @@ dependencies = [
|
|||
"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]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
|
@ -6200,6 +6405,22 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
|
@ -6604,6 +6825,12 @@ dependencies = [
|
|||
"wasite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
@ -7127,6 +7354,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "yoke"
|
||||
version = "0.7.4"
|
||||
|
|
|
@ -22,7 +22,7 @@ tauri-plugin-updater = "2.3.0"
|
|||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-fs = "2.2.0"
|
||||
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"
|
||||
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite", "chrono"] }
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
|
@ -32,7 +32,7 @@ rdev = "0.5.3"
|
|||
rand = "0.8.5"
|
||||
base64 = "0.22.1"
|
||||
image = "0.25.5"
|
||||
reqwest = { version = "0.12.9", features = ["blocking"] }
|
||||
reqwest = { version = "0.12.9", features = ["json", "blocking"] }
|
||||
url = "2.5.4"
|
||||
regex = "1.11.1"
|
||||
sha2 = "0.10.8"
|
||||
|
@ -40,9 +40,13 @@ lazy_static = "1.5.0"
|
|||
time = "0.3.37"
|
||||
global-hotkey = "0.6.3"
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
log = { version = "0.4.22", features = ["std"] }
|
||||
uuid = "1.11.0"
|
||||
active-win-pos-rs = "0.8.3"
|
||||
active-win-pos-rs = "0.8.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]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
// use hyperpolyglot;
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::{simulate, EventType, Key};
|
||||
use regex::Regex;
|
||||
use sqlx::SqlitePool;
|
||||
use uuid::Uuid;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{thread, time::Duration};
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime};
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use regex::Regex;
|
||||
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::favicon::fetch_favicon_as_base64;
|
||||
use crate::db;
|
||||
use crate::utils::types::{ContentType, HistoryItem};
|
||||
|
||||
lazy_static! {
|
||||
|
@ -111,18 +112,26 @@ pub fn setup<R: Runtime>(app: &AppHandle<R>) {
|
|||
.unwrap_or_else(|e| e);
|
||||
let _ = db::history::add_history_item(
|
||||
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;
|
||||
}
|
||||
} else if available_types.files {
|
||||
println!("Handling files change");
|
||||
if let Ok(files) = clipboard.read_files() {
|
||||
let files_str = files.join(", ");
|
||||
for file in files {
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::File, files_str, None, app_icon),
|
||||
pool.clone(),
|
||||
HistoryItem::new(
|
||||
app_name.clone(),
|
||||
ContentType::File,
|
||||
file,
|
||||
None,
|
||||
app_icon.clone(),
|
||||
None
|
||||
),
|
||||
).await;
|
||||
}
|
||||
}
|
||||
} else if available_types.text {
|
||||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
|
@ -137,17 +146,36 @@ pub fn setup<R: Runtime>(app: &AppHandle<R>) {
|
|||
|
||||
let _ = db::history::add_history_item(
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon)
|
||||
HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon, None)
|
||||
).await;
|
||||
}
|
||||
} else {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily disabled code detection
|
||||
/*if let Some(detection) = hyperpolyglot::detect_from_text(&text) {
|
||||
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::Text, text, None, app_icon)
|
||||
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 {
|
||||
|
@ -183,7 +211,10 @@ pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
|
|||
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 images_dir = app_data_dir.join("images");
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
|
|
|
@ -3,9 +3,9 @@ use global_hotkey::{
|
|||
hotkey::{Code, HotKey, Modifiers},
|
||||
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::cell::RefCell;
|
||||
use tauri::{AppHandle, Manager, Listener};
|
||||
use std::str::FromStr;
|
||||
use tauri::{AppHandle, Listener, Manager};
|
||||
|
||||
thread_local! {
|
||||
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));
|
||||
|
||||
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");
|
||||
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()
|
||||
};
|
||||
|
||||
code = Some(Code::from_str(&key_code)
|
||||
.map_err(|_| format!("Invalid key code: {}", key_code))?);
|
||||
code = Some(
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub mod updater;
|
||||
pub mod clipboard;
|
||||
pub mod tray;
|
||||
pub mod hotkeys;
|
||||
pub mod tray;
|
||||
pub mod updater;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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>> {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use tauri::{AppHandle, async_runtime};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind, MessageDialogButtons};
|
||||
use tauri::{async_runtime, AppHandle};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
pub async fn check_for_updates(app: AppHandle) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use include_dir::{include_dir, Dir};
|
||||
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use include_dir::{include_dir, Dir};
|
||||
|
||||
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>> {
|
||||
println!("Starting migration process");
|
||||
|
||||
// Create schema_version table
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);"
|
||||
);",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
let current_version: Option<i64> = sqlx::query_scalar(
|
||||
"SELECT MAX(version) FROM schema_version"
|
||||
)
|
||||
let current_version: Option<i64> =
|
||||
sqlx::query_scalar("SELECT MAX(version) FROM schema_version")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
let current_version = current_version.unwrap_or(0);
|
||||
println!("Current database version: {}", current_version);
|
||||
|
||||
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR
|
||||
.files()
|
||||
.filter_map(|file| {
|
||||
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") {
|
||||
let version: i64 = file_name
|
||||
.trim_start_matches("migration")
|
||||
.trim_end_matches(".sql")
|
||||
.parse()
|
||||
.ok()?;
|
||||
println!("Found migration version: {}", version);
|
||||
Some((version, file.contents_utf8()?))
|
||||
} else {
|
||||
None
|
||||
|
@ -93,8 +86,6 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::E
|
|||
|
||||
for (version, content) in migration_files {
|
||||
if version > current_version {
|
||||
println!("Applying migration {}", version);
|
||||
|
||||
let statements: Vec<&str> = content
|
||||
.split(';')
|
||||
.map(|s| s.trim())
|
||||
|
@ -102,7 +93,6 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::E
|
|||
.collect();
|
||||
|
||||
for statement in statements {
|
||||
println!("Executing statement: {}", statement);
|
||||
sqlx::query(statement)
|
||||
.execute(pool)
|
||||
.await
|
||||
|
|
|
@ -28,7 +28,7 @@ pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::er
|
|||
#[tauri::command]
|
||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp FROM history ORDER BY timestamp DESC",
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC",
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
|
@ -44,6 +44,7 @@ pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<Histo
|
|||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -55,22 +56,30 @@ pub async fn add_history_item(
|
|||
pool: tauri::State<'_, SqlitePool>,
|
||||
item: HistoryItem,
|
||||
) -> Result<(), String> {
|
||||
let (id, source, source_icon, content_type, content, favicon, timestamp) = item.to_row();
|
||||
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
|
||||
item.to_row();
|
||||
|
||||
let last_content: Option<String> = sqlx::query_scalar(
|
||||
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",
|
||||
)
|
||||
.bind(content_type.clone())
|
||||
.fetch_one(&*pool)
|
||||
let existing = sqlx::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if last_content.as_deref() == Some(&content) {
|
||||
return Ok(());
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match existing {
|
||||
Some(_) => {
|
||||
sqlx::query(
|
||||
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
"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)
|
||||
|
@ -79,9 +88,12 @@ pub async fn add_history_item(
|
|||
.bind(content)
|
||||
.bind(favicon)
|
||||
.bind(timestamp)
|
||||
.bind(language)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -93,7 +105,7 @@ pub async fn search_history(
|
|||
) -> Result<Vec<HistoryItem>, String> {
|
||||
let query = format!("%{}%", query);
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp FROM history WHERE content LIKE ? ORDER BY timestamp DESC"
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history WHERE content LIKE ? ORDER BY timestamp DESC"
|
||||
)
|
||||
.bind(query)
|
||||
.fetch_all(&*pool)
|
||||
|
@ -110,6 +122,7 @@ pub async fn search_history(
|
|||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -123,7 +136,7 @@ pub async fn load_history_chunk(
|
|||
limit: i64,
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
|
@ -141,6 +154,7 @@ pub async fn load_history_chunk(
|
|||
content: row.get("content"),
|
||||
favicon: row.get("favicon"),
|
||||
timestamp: row.get("timestamp"),
|
||||
language: row.get("language"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
|
||||
ALTER TABLE history ADD COLUMN source_icon TEXT;
|
||||
ALTER TABLE history ADD COLUMN language TEXT;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use serde_json;
|
||||
use tauri::{Emitter, Manager};
|
||||
use sqlx::Row;
|
||||
use sqlx::SqlitePool;
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct KeybindSetting {
|
||||
|
@ -15,9 +15,7 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
|
|||
};
|
||||
let json = serde_json::to_string(&default_keybind)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO settings (key, value) VALUES ('keybind', ?)"
|
||||
)
|
||||
sqlx::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
@ -50,7 +48,7 @@ pub async fn save_keybind(
|
|||
#[tauri::command]
|
||||
pub async fn get_setting(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String
|
||||
key: String,
|
||||
) -> Result<String, String> {
|
||||
let row = sqlx::query("SELECT value FROM settings WHERE key = ?")
|
||||
.bind(key)
|
||||
|
@ -65,7 +63,7 @@ pub async fn get_setting(
|
|||
pub async fn save_setting(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String,
|
||||
value: String
|
||||
value: String,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.bind(key)
|
||||
|
@ -78,9 +76,7 @@ pub async fn save_setting(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_keybind(
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<Vec<String>, String> {
|
||||
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let pool = app_handle.state::<SqlitePool>();
|
||||
|
||||
let row = sqlx::query("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
|
@ -88,13 +84,10 @@ pub async fn get_keybind(
|
|||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let json = row
|
||||
.map(|r| r.get::<String, _>("value"))
|
||||
.unwrap_or_else(|| {
|
||||
let json = row.map(|r| r.get::<String, _>("value")).unwrap_or_else(|| {
|
||||
serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()])
|
||||
.expect("Failed to serialize default keybind")
|
||||
});
|
||||
|
||||
serde_json::from_str::<Vec<String>>(&json)
|
||||
.map_err(|e| e.to_string())
|
||||
serde_json::from_str::<Vec<String>>(&json).map_err(|e| e.to_string())
|
||||
}
|
|
@ -34,6 +34,8 @@ fn main() {
|
|||
)
|
||||
.setup(|app| {
|
||||
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");
|
||||
|
||||
let db_path = app_data_dir.join("data.db");
|
||||
|
@ -110,6 +112,7 @@ fn main() {
|
|||
db::settings::save_setting,
|
||||
db::settings::save_keybind,
|
||||
db::settings::get_keybind,
|
||||
utils::commands::fetch_page_meta,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
|
|
@ -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 meta_fetcher;
|
||||
|
||||
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
))
|
||||
}
|
|
@ -4,7 +4,9 @@ use image::ImageFormat;
|
|||
use reqwest;
|
||||
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 favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
|
||||
let response = client.get(&favicon_url).send().await?;
|
||||
|
|
49
src-tauri/src/utils/logger.rs
Normal 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(())
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod types;
|
||||
pub mod commands;
|
||||
pub mod favicon;
|
||||
pub mod types;
|
||||
pub mod logger;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -12,6 +12,7 @@ pub struct HistoryItem {
|
|||
pub content: String,
|
||||
pub favicon: Option<String>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
|
@ -26,7 +27,7 @@ pub enum ContentType {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Text {
|
||||
pub struct InfoText {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub characters: i32,
|
||||
|
@ -35,7 +36,7 @@ pub struct Text {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Image {
|
||||
pub struct InfoImage {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub dimensions: String,
|
||||
|
@ -44,7 +45,7 @@ pub struct Image {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct File {
|
||||
pub struct InfoFile {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub path: String,
|
||||
|
@ -53,26 +54,26 @@ pub struct File {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Link {
|
||||
pub struct InfoLink {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub title: Option<String>,
|
||||
pub url: String,
|
||||
pub characters: i32,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Color {
|
||||
pub struct InfoColor {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub hexcode: String,
|
||||
pub rgba: String,
|
||||
pub hex: String,
|
||||
pub rgb: String,
|
||||
pub copied: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Code {
|
||||
pub struct InfoCode {
|
||||
pub source: String,
|
||||
pub content_type: ContentType,
|
||||
pub language: String,
|
||||
|
@ -108,7 +109,14 @@ impl From<String> for ContentType {
|
|||
}
|
||||
|
||||
impl HistoryItem {
|
||||
pub fn new(source: String, content_type: ContentType, content: String, favicon: Option<String>, source_icon: Option<String>) -> Self {
|
||||
pub fn new(
|
||||
source: String,
|
||||
content_type: ContentType,
|
||||
content: String,
|
||||
favicon: Option<String>,
|
||||
source_icon: Option<String>,
|
||||
language: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
source,
|
||||
|
@ -117,10 +125,22 @@ impl HistoryItem {
|
|||
content,
|
||||
favicon,
|
||||
timestamp: Utc::now(),
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_row(&self) -> (String, String, Option<String>, String, String, Option<String>, DateTime<Utc>) {
|
||||
pub fn to_row(
|
||||
&self,
|
||||
) -> (
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
String,
|
||||
Option<String>,
|
||||
DateTime<Utc>,
|
||||
Option<String>,
|
||||
) {
|
||||
(
|
||||
self.id.clone(),
|
||||
self.source.clone(),
|
||||
|
@ -129,6 +149,7 @@ impl HistoryItem {
|
|||
self.content.clone(),
|
||||
self.favicon.clone(),
|
||||
self.timestamp,
|
||||
self.language.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export enum ContentType {
|
||||
Text = "text",
|
||||
|
@ -11,21 +11,52 @@ export enum ContentType {
|
|||
|
||||
export class HistoryItem {
|
||||
id: string;
|
||||
source: string;
|
||||
source_icon?: string;
|
||||
content_type: ContentType;
|
||||
content: string;
|
||||
favicon?: string;
|
||||
timestamp: Date;
|
||||
language?: string;
|
||||
|
||||
constructor(content_type: ContentType, content: string, favicon?: string) {
|
||||
constructor(
|
||||
source: string,
|
||||
content_type: ContentType,
|
||||
content: string,
|
||||
favicon?: string,
|
||||
source_icon?: string,
|
||||
language?: string
|
||||
) {
|
||||
this.id = uuidv4();
|
||||
this.source = source;
|
||||
this.source_icon = source_icon;
|
||||
this.content_type = content_type;
|
||||
this.content = content;
|
||||
this.favicon = favicon;
|
||||
this.timestamp = new Date();
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
toRow(): [string, string, string, string | undefined, Date] {
|
||||
return [this.id, this.content_type, this.content, this.favicon, this.timestamp];
|
||||
toRow(): [
|
||||
string,
|
||||
string,
|
||||
string | undefined,
|
||||
string,
|
||||
string,
|
||||
string | undefined,
|
||||
Date,
|
||||
string | undefined
|
||||
] {
|
||||
return [
|
||||
this.id,
|
||||
this.source,
|
||||
this.source_icon,
|
||||
this.content_type,
|
||||
this.content,
|
||||
this.favicon,
|
||||
this.timestamp,
|
||||
this.language,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,3 +64,53 @@ export interface Settings {
|
|||
key: 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;
|
||||
}
|
||||
|
|