mirror of
https://github.com/tcsenpai/pensieve.git
synced 2025-06-06 19:25:24 +00:00
feat(web): ui update
This commit is contained in:
parent
955ec76638
commit
42e8421ab4
@ -4,7 +4,9 @@
|
|||||||
import CopyToClipboard from "$lib/components/CopyToClipboard.svelte"
|
import CopyToClipboard from "$lib/components/CopyToClipboard.svelte"
|
||||||
import OCRTable from './OCRTable.svelte';
|
import OCRTable from './OCRTable.svelte';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { ChevronLeft, ChevronRight, X } from 'lucide-svelte';
|
import { ChevronLeft, ChevronRight, X, Hash, Library, Folder, FileClock } from 'lucide-svelte';
|
||||||
|
import { appIconMap } from '$lib/utils';
|
||||||
|
import LucideIcon from '$lib/components/LucideIcon.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -38,6 +40,7 @@
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
export let title;
|
export let title;
|
||||||
|
export let app_name;
|
||||||
/**
|
/**
|
||||||
* @type {Array<string>}
|
* @type {Array<string>}
|
||||||
*/
|
*/
|
||||||
@ -47,6 +50,16 @@
|
|||||||
* @type {Array<{key: string, source: string, value: any}>}
|
* @type {Array<{key: string, source: string, value: any}>}
|
||||||
*/
|
*/
|
||||||
export let metadata_entries = [];
|
export let metadata_entries = [];
|
||||||
|
|
||||||
|
// Remove items with key "timestamp" or "sequence" and sort metadata_entries, placing "ocr_result" at the end
|
||||||
|
$: sortedMetadataEntries = [...metadata_entries]
|
||||||
|
.filter(entry => entry.key !== "timestamp" && entry.key !== "sequence" && entry.key !== "active_app" && entry.key !== "active_window")
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.key === "ocr_result") return 1;
|
||||||
|
if (b.key === "ocr_result") return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
@ -111,55 +124,68 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row h-full">
|
<div class="flex flex-col md:flex-row h-full">
|
||||||
<!-- Image container -->
|
<!-- Image container -->
|
||||||
<div class="flex-none w-full md:w-1/2 h-full">
|
<div class="flex-none w-full md:w-1/2 flex flex-col">
|
||||||
<a href={video} target="_blank" rel="noopener noreferrer">
|
<div class="mb-4">
|
||||||
<img class="w-full h-full object-contain" src={image} alt={title} />
|
<div class="flex items-center space-x-2 text-lg leading-tight font-medium text-black hover:underline">
|
||||||
</a>
|
<LucideIcon name={appIconMap[app_name] || 'Image'} size={24} />
|
||||||
</div>
|
<p>{title}</p>
|
||||||
<!-- Description container -->
|
</div>
|
||||||
<ScrollArea class="mt-4 md:mt-0 md:ml-6 overflow-y-auto max-h-full">
|
</div>
|
||||||
<div class="mb-2 mr-2 pb-2 border-b border-gray-300">
|
<div class="mb-2 mr-2 pb-2 border-b border-gray-300">
|
||||||
<span class="uppercase tracking-wide text-sm text-indigo-600 font-bold">ID</span>
|
|
||||||
<span class="mt-1 text-sm leading-tight font-medium text-gray-500 font-mono">
|
<span class="mt-1 text-sm leading-tight font-medium text-gray-500 font-mono">
|
||||||
{id}
|
<span class="inline-flex mr-4">
|
||||||
</span>
|
<Library size={16} class="uppercase tracking-wide text-sm text-indigo-600 font-bold mr-1"/>
|
||||||
<span class="uppercase tracking-wide text-sm text-indigo-600 font-bold ml-4"
|
{library_id}
|
||||||
>Library ID</span
|
</span>
|
||||||
>
|
|
||||||
<span class="mt-1 text-sm leading-tight font-medium text-gray-500 font-mono">
|
<span class="inline-flex mr-4">
|
||||||
{library_id}
|
<Folder size={16} class="uppercase tracking-wide text-sm text-indigo-600 font-bold mr-1"/>
|
||||||
</span>
|
{folder_id}
|
||||||
<span class="uppercase tracking-wide text-sm text-indigo-600 font-bold ml-4"
|
</span>
|
||||||
>Folder ID</span
|
|
||||||
>
|
<span class="inline-flex mr-4">
|
||||||
<span class="mt-1 text-sm leading-tight font-medium text-gray-500 font-mono">
|
<Hash size={16} class="uppercase tracking-wide text-sm text-indigo-600 font-bold mr-1"/>
|
||||||
{folder_id}
|
{id}
|
||||||
</span>
|
</span>
|
||||||
<span class="uppercase tracking-wide text-sm text-indigo-600 font-bold ml-4"
|
|
||||||
>DATETIME</span
|
<span class="inline-flex mr-4">
|
||||||
>
|
<FileClock size={16} class="uppercase tracking-wide text-sm text-indigo-600 font-bold mr-1"/>
|
||||||
<span class="mt-1 text-xs leading-tight font-xs text-gray-500 font-mono">
|
{new Date(created_at).toLocaleString()}
|
||||||
{new Date(created_at).toLocaleString()}
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span class="mt-1 text-xs leading-tight font-xs text-gray-500 font-mono">
|
<span class="mt-1 text-xs leading-tight font-xs text-gray-500 font-mono">
|
||||||
{filepath}
|
{filepath}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">Image Title</div>
|
<div class="flex-grow overflow-hidden">
|
||||||
<p class="block mt-1 text-lg leading-tight font-medium text-black hover:underline">
|
<a href={video} target="_blank" rel="noopener noreferrer" class="block h-full">
|
||||||
{title}
|
<img
|
||||||
</p>
|
class="w-full h-full object-contain rounded-lg drop-shadow-md"
|
||||||
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">TAGS</div>
|
src={image}
|
||||||
<div class="mt-2 text-gray-600">
|
alt={title}
|
||||||
{#each tags as tag}
|
/>
|
||||||
<span class="text-base text-gray-500 inline-block">{tag}</span>
|
</a>
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Description container -->
|
||||||
|
<ScrollArea class="mt-4 md:mt-0 md:ml-6 overflow-y-auto max-h-full">
|
||||||
|
|
||||||
|
{#if tags.length > 0}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">TAGS</div>
|
||||||
|
<div class=" text-gray-600">
|
||||||
|
{#each tags as tag}
|
||||||
|
<span class="text-base text-gray-500 inline-block">{tag}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">METADATA</div>
|
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">METADATA</div>
|
||||||
<div class="mt-2 text-gray-600">
|
<div class="mt-2 text-gray-600">
|
||||||
{#each metadata_entries as entry}
|
{#each sortedMetadataEntries as entry}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<span class="font-bold flex items-center">
|
<span class="font-bold flex items-center">
|
||||||
{entry.key}
|
{entry.key}
|
||||||
@ -195,4 +221,4 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,61 @@
|
|||||||
export let class_ = '';
|
export let class_ = '';
|
||||||
export let withBorder = true;
|
export let withBorder = true;
|
||||||
|
|
||||||
function prepareMatrixFromRandomColors(withBorder: boolean): string[][] {
|
function prepareMatrixFromFixedIndexAndLittleRadom(withBorder: boolean): string[][] {
|
||||||
|
const bgColors = ['#f2f2f2', '#e9e9e9', '#d8d8d8']
|
||||||
|
// const colors = ['#d0e8ff', '#F2295F', '#E0A0F2', '#F2B705'];
|
||||||
|
const colors = ['#d0e8ff', '#BF244E', '#8C2685', '#21A650'];
|
||||||
|
const mShape = withBorder
|
||||||
|
? [
|
||||||
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 2, 0, 0, 0, 2, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 2, 2, 0, 2, 2, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 0, 2, 2, 2, 0, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3, 0],
|
||||||
|
[0, 1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3, 0],
|
||||||
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
[1, 1, 0, 0, 0, 0, 0, 0, 0, 3, 3],
|
||||||
|
[1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3],
|
||||||
|
[1, 1, 1, 2, 0, 0, 0, 2, 3, 3, 3],
|
||||||
|
[1, 1, 1, 2, 2, 0, 2, 2, 3, 3, 3],
|
||||||
|
[1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3],
|
||||||
|
[1, 1, 1, 0, 2, 2, 2, 0, 3, 3, 3],
|
||||||
|
[1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3],
|
||||||
|
[1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3],
|
||||||
|
[1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3],
|
||||||
|
[1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3],
|
||||||
|
[1, 1, 1, 0, 0, 0, 0, 0, 3, 3, 3]
|
||||||
|
];
|
||||||
|
|
||||||
|
const gridSize = withBorder ? 13 : 11;
|
||||||
|
let seed = 42;
|
||||||
|
const matrix: string[][] = [];
|
||||||
|
|
||||||
|
for (let row = 0; row < gridSize; row++) {
|
||||||
|
const rowColors: string[] = [];
|
||||||
|
const bgSize = bgColors.length;
|
||||||
|
for (let col = 0; col < gridSize; col++) {
|
||||||
|
if (mShape[row][col] === 0) {
|
||||||
|
rowColors.push(bgColors[Math.floor(seededRandom(seed++) * bgSize)]);
|
||||||
|
} else {
|
||||||
|
rowColors.push(colors[mShape[row][col]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matrix.push(rowColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareMatrixFromRandomColors(withBorder: boolean): string[][] {
|
||||||
const colors = ['#d0e8ff', '#a1d2ff', '#64b5f6', '#1565c0', '#0d47a1'];
|
const colors = ['#d0e8ff', '#a1d2ff', '#64b5f6', '#1565c0', '#0d47a1'];
|
||||||
const mShape = withBorder
|
const mShape = withBorder
|
||||||
? [
|
? [
|
||||||
@ -83,7 +137,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateMemosLogo(size: number, withBorder: boolean): string {
|
function generateMemosLogo(size: number, withBorder: boolean): string {
|
||||||
const matrix = prepareMatrixFromRandomColors(withBorder);
|
const matrix = prepareMatrixFromFixedIndexAndLittleRadom(withBorder);
|
||||||
return generateSvg(matrix, size);
|
return generateSvg(matrix, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
web/src/lib/components/LucideIcon.svelte
Normal file
6
web/src/lib/components/LucideIcon.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
import * as icons from 'lucide-svelte';
|
||||||
|
export let name;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component this="{icons[name]}" {...$$props} />
|
@ -59,4 +59,32 @@ export const flyAndScale = (
|
|||||||
},
|
},
|
||||||
easing: cubicOut
|
easing: cubicOut
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const appIconMap: Record<string, string> = {
|
||||||
|
"Cursor": "Code",
|
||||||
|
"Google Chrome": "Chrome",
|
||||||
|
"IINA": "Youtube",
|
||||||
|
"微信": "MessageSquareCode",
|
||||||
|
"预览": "Eye",
|
||||||
|
"iTerm2": "SquareTerminal",
|
||||||
|
"企业微信": "MessageSquareCode",
|
||||||
|
"IntelliJ IDEA": "Code",
|
||||||
|
"Microsoft Edge": "Globe",
|
||||||
|
"腾讯会议": "MessagesSquare",
|
||||||
|
"访达": "Folder",
|
||||||
|
"邮件": "Mail",
|
||||||
|
"备忘录": "NotebookTabs",
|
||||||
|
"日历": "CalendarFold",
|
||||||
|
"UserNotificationCenter": "Bell",
|
||||||
|
"Electron": "Atom",
|
||||||
|
"Firefox": "Globe",
|
||||||
|
"Safari浏览器": "Compass",
|
||||||
|
"熊掌记": "NotebookTabs",
|
||||||
|
"Alacritty": "SquareTerminal",
|
||||||
|
"系统设置": "Settings",
|
||||||
|
"股市": "CircleDollarSign",
|
||||||
|
"活动监视器": "Activity",
|
||||||
|
"Brave Browser": "Globe",
|
||||||
|
"Code": "Code",
|
||||||
|
};
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { appIconMap } from '$lib/utils';
|
||||||
|
import LucideIcon from '$lib/components/LucideIcon.svelte';
|
||||||
|
|
||||||
let searchString = '';
|
let searchString = '';
|
||||||
/**
|
/**
|
||||||
@ -255,6 +257,24 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add this function near the top of the <script> section
|
||||||
|
function getEntityTitle(document: any): string {
|
||||||
|
if (document.metadata_entries &&
|
||||||
|
document.metadata_entries.some((entry: any) => entry.key === 'active_window')) {
|
||||||
|
return document.metadata_entries.find((entry: any) => entry.key === 'active_window').value;
|
||||||
|
}
|
||||||
|
return filename(document.filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppName(document: any): string | null {
|
||||||
|
if (document.metadata_entries && document.metadata_entries.some((entry) => entry.key === 'active_app')) {
|
||||||
|
return document.metadata_entries.find((entry) => entry.key === 'active_app').value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -330,11 +350,7 @@
|
|||||||
>
|
>
|
||||||
<div class="px-4 pt-4">
|
<div class="px-4 pt-4">
|
||||||
<h2 class="line-clamp-2 h-12">
|
<h2 class="line-clamp-2 h-12">
|
||||||
{hit.document.metadata_entries &&
|
{getEntityTitle(hit.document)}
|
||||||
hit.document.metadata_entries.some((entry) => entry.key === 'active_window')
|
|
||||||
? hit.document.metadata_entries.find((entry) => entry.key === 'active_window')
|
|
||||||
.value
|
|
||||||
: filename(hit.document.filepath)}
|
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-gray-700 text-xs">
|
<p class="text-gray-700 text-xs">
|
||||||
{formatDistanceToNow(new Date(hit.document.file_created_at * 1000), {
|
{formatDistanceToNow(new Date(hit.document.file_created_at * 1000), {
|
||||||
@ -348,11 +364,12 @@
|
|||||||
src={`${apiEndpoint}/files/${hit.document.filepath}`}
|
src={`${apiEndpoint}/files/${hit.document.filepath}`}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
{#if hit.document.metadata_entries && hit.document.metadata_entries.some((entry) => entry.key === 'active_app')}
|
{#if getAppName(hit.document)}
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-2 left-6 bg-white bg-opacity-75 px-2 py-1 rounded-full text-xs font-semibold border border-gray-200"
|
class="absolute bottom-2 left-6 bg-white bg-opacity-75 px-2 py-1 rounded-full text-xs font-semibold border border-gray-200 flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
{hit.document.metadata_entries.find((entry) => entry.key === 'active_app').value}
|
<LucideIcon name={appIconMap[getAppName(hit.document)] || 'Hexagon'} size={16} />
|
||||||
|
<span>{getAppName(hit.document)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</figure>
|
</figure>
|
||||||
@ -376,7 +393,8 @@
|
|||||||
video={`${apiEndpoint}/files/video/${searchResult.hits[selectedImage].document.filepath}`}
|
video={`${apiEndpoint}/files/video/${searchResult.hits[selectedImage].document.filepath}`}
|
||||||
created_at={searchResult.hits[selectedImage].document.file_created_at * 1000}
|
created_at={searchResult.hits[selectedImage].document.file_created_at * 1000}
|
||||||
filepath={searchResult.hits[selectedImage].document.filepath}
|
filepath={searchResult.hits[selectedImage].document.filepath}
|
||||||
title={filename(searchResult.hits[selectedImage].document.filepath)}
|
title={getEntityTitle(searchResult.hits[selectedImage].document)}
|
||||||
|
app_name={getAppName(searchResult.hits[selectedImage].document)}
|
||||||
tags={searchResult.hits[selectedImage].document.tags}
|
tags={searchResult.hits[selectedImage].document.tags}
|
||||||
metadata_entries={searchResult.hits[selectedImage].document.metadata_entries}
|
metadata_entries={searchResult.hits[selectedImage].document.metadata_entries}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
@ -396,4 +414,4 @@
|
|||||||
<a href="/changelog">Changelog</a>
|
<a href="/changelog">Changelog</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 18 KiB |
Loading…
x
Reference in New Issue
Block a user