From d83d787b1554819beb817c4e9b90de00e77da3a2 Mon Sep 17 00:00:00 2001 From: Louis Beaumont Date: Wed, 6 Nov 2024 13:14:13 -0800 Subject: [PATCH] feat: add app icons on timeline (macos only) --- screenpipe-app-tauri/app/timeline/page.tsx | 22 +++- .../components/timeline/timeline-block.tsx | 38 +++--- .../components/timeline/timeline-dock.tsx | 123 +++++++++++++----- screenpipe-app-tauri/src-tauri/Cargo.lock | 2 +- 4 files changed, 132 insertions(+), 53 deletions(-) diff --git a/screenpipe-app-tauri/app/timeline/page.tsx b/screenpipe-app-tauri/app/timeline/page.tsx index a90cea49f..f669d25b5 100644 --- a/screenpipe-app-tauri/app/timeline/page.tsx +++ b/screenpipe-app-tauri/app/timeline/page.tsx @@ -777,10 +777,20 @@ export default function Timeline() {
+ style={{ left: `${getCurrentTimePercentage()}%` }} + > +
+ {frames[currentIndex] && + new Date(frames[currentIndex].timestamp).toLocaleTimeString( + [], + { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + } + )} +
+
{selectionRange && (
)} - {/* {loadedTimeRange && frames.length > 0 && ( + {loadedTimeRange && frames.length > 0 && ( - )} */} + )}
{Array(7) diff --git a/screenpipe-app-tauri/components/timeline/timeline-block.tsx b/screenpipe-app-tauri/components/timeline/timeline-block.tsx index 073aad509..5110b6220 100644 --- a/screenpipe-app-tauri/components/timeline/timeline-block.tsx +++ b/screenpipe-app-tauri/components/timeline/timeline-block.tsx @@ -23,22 +23,30 @@ export function TimelineBlocks({ frames, timeRange }: TimelineBlocksProps) { // Cache colors to avoid recalculating const colorCache = useMemo(() => new Map(), []); - const getAppColor = (appName: string): string => { - const cached = colorCache.get(appName); - if (cached) return cached; - - let hash = 0; - for (let i = 0; i < appName.length; i++) { - hash = appName.charCodeAt(i) + ((hash << 5) - hash); - } - const hue = Math.abs(hash) % 360; - const color = `hsl(${hue}, 70%, 50%)`; - colorCache.set(appName, color); - return color; - }; - // Calculate blocks without sampling const blocks = useMemo(() => { + const getAppColor = (appName: string): string => { + const cached = colorCache.get(appName); + if (cached) return cached; + + // Use a better hash distribution + const hash = Array.from(appName).reduce( + (h, c) => (Math.imul(31, h) + c.charCodeAt(0)) | 0, + 0 + ); + + // Use golden ratio for better color distribution + const golden_ratio = 0.618033988749895; + const hue = ((hash * golden_ratio) % 1) * 360; + + // Vary saturation and lightness slightly based on hash + const sat = 65 + (hash % 20); // 65-85% + const light = 45 + (hash % 15); // 45-60% + + const color = `hsl(${hue}, ${sat}%, ${light}%)`; + colorCache.set(appName, color); + return color; + }; if (frames.length === 0) return []; const blocks: TimeBlock[] = []; @@ -83,7 +91,7 @@ export function TimelineBlocks({ frames, timeRange }: TimelineBlocksProps) { if (currentBlock) blocks.push(currentBlock); return blocks; - }, [frames, timeRange, getAppColor]); + }, [frames, timeRange]); // Debug output console.log("timeline blocks:", blocks); diff --git a/screenpipe-app-tauri/components/timeline/timeline-dock.tsx b/screenpipe-app-tauri/components/timeline/timeline-dock.tsx index 9bcb6f051..8e598a0c8 100644 --- a/screenpipe-app-tauri/components/timeline/timeline-dock.tsx +++ b/screenpipe-app-tauri/components/timeline/timeline-dock.tsx @@ -4,6 +4,7 @@ import { cn } from "@/lib/utils"; import { platform } from "@tauri-apps/plugin-os"; import { invoke } from "@tauri-apps/api/core"; import { debounce } from "lodash"; +import { StreamTimeSeriesResponse } from "@/app/timeline/page"; interface TimelineDockProps { children: React.ReactNode; @@ -36,6 +37,7 @@ export function TimelineDock({ const handleMouseMove = useMemo( () => debounce((e: React.MouseEvent) => { + if (!e.currentTarget) return; const bounds = e.currentTarget.getBoundingClientRect(); setMouseX(e.clientX - bounds.left); }, 16), // ~60fps @@ -156,41 +158,100 @@ export const TimelineDockIcon = React.memo(function TimelineDockIcon({ ); }); -// Optimize TimelineIconsSection -export function TimelineIconsSection({ blocks }: { blocks: any[] }) { +interface ProcessedBlock { + appName: string; + percentThroughDay: number; + timestamp: Date; + iconSrc?: string; +} + +export function TimelineIconsSection({ + blocks, +}: { + blocks: StreamTimeSeriesResponse[]; +}) { const [iconCache, setIconCache] = useState<{ [key: string]: string }>({}); // Memoize significant blocks calculation const significantBlocks = useMemo( () => blocks.filter((block) => { - const minutesInDay = 24 * 60; - const blockMinutes = (block.width / 100) * minutesInDay; - return blockMinutes > 10; + return block.devices.every((device) => device.metadata.app_name); }), [blocks] ); - // Memoize unique apps calculation - const uniqueApps = useMemo( - () => [...new Set(significantBlocks.map((block) => block.appName))], - [significantBlocks] - ); + // Get the visible time range + const timeRange = useMemo(() => { + if (blocks.length === 0) return null; + const startTime = new Date(blocks[blocks.length - 1].timestamp); + const endTime = new Date(blocks[0].timestamp); + return { start: startTime, end: endTime }; + }, [blocks]); + + // Memoize processed blocks with position calculations + const processedBlocks = useMemo(() => { + if (!timeRange) return []; + + const totalRange = timeRange.end.getTime() - timeRange.start.getTime(); + + return ( + significantBlocks + .map((block) => { + const appName = block.devices.find( + (device) => device.metadata.app_name + )?.metadata.app_name!; + const blockTime = new Date(block.timestamp); + + // Calculate percentage based on visible time range + const percentPosition = + ((blockTime.getTime() - timeRange.start.getTime()) / totalRange) * + 100; + + return { + appName, + percentThroughDay: percentPosition, + timestamp: blockTime, + iconSrc: iconCache[appName], + }; + }) + // Filter out blocks that are too close to each other (less than 1% apart) + .filter((block, index, array) => { + if (index === 0) return true; + const prevBlock = array[index - 1]; + const minDuration = 0.4; // 1% minimum gap between icons + return ( + Math.abs(block.percentThroughDay - prevBlock.percentThroughDay) > + minDuration + ); + }) + ); + }, [significantBlocks, iconCache, timeRange]); const loadAppIcon = useCallback( async (appName: string, appPath?: string) => { - if (iconCache[appName]) return; + try { + // Check platform first to avoid unnecessary invokes + const p = platform(); + if (p !== "macos") return; // Early return for unsupported platforms - const icon = await invoke<{ base64: string; path: string } | null>( - "get_app_icon", - { appName, appPath } - ); + if (iconCache[appName]) return; + + const icon = await invoke<{ base64: string; path: string } | null>( + "get_app_icon", + { appName, appPath } + ); - if (icon) { - setIconCache((prev) => ({ - ...prev, - [appName]: icon.base64, - })); + if (icon?.base64) { + // Add null check for base64 + setIconCache((prev) => ({ + ...prev, + [appName]: icon.base64, + })); + } + } catch (error) { + console.error(`failed to load icon for ${appName}:`, error); + // Fail silently - the UI will just not show an icon } }, [iconCache] @@ -202,29 +263,29 @@ export function TimelineIconsSection({ blocks }: { blocks: any[] }) { if (p !== "macos") return; // Load icons for unique app names only - uniqueApps.forEach((appName) => { - loadAppIcon(appName); + processedBlocks.forEach((block) => { + loadAppIcon(block.appName); }); }; loadIcons(); - }, [uniqueApps, loadAppIcon]); + }, [processedBlocks, loadAppIcon]); return ( -
- {significantBlocks.map((block, i) => ( +
+ {processedBlocks.map((block, i) => (
- {iconCache[block.appName] && ( + {block.iconSrc && (