diff --git a/src-tauri/src/engine.rs b/src-tauri/src/engine.rs index 610c489..7d865a5 100644 --- a/src-tauri/src/engine.rs +++ b/src-tauri/src/engine.rs @@ -53,33 +53,60 @@ pub fn get_image_base64(path: String) -> Result { #[tauri::command] pub fn get_timeline(date: String, base_dir: String) -> Vec { let mut results = Vec::new(); - let dir_path = PathBuf::from(base_dir).join(date); + let base_path = PathBuf::from(base_dir); - if !dir_path.exists() || !dir_path.is_dir() { - return results; - } + // Parse current date + let current_date = match chrono::NaiveDate::parse_from_str(&date, "%Y-%m-%d") { + Ok(d) => d, + Err(_) => return results, + }; + let next_date = current_date + chrono::Duration::days(1); + let next_date_str = next_date.format("%Y-%m-%d").to_string(); - if let Ok(entries) = fs::read_dir(dir_path) { - let mut paths: Vec<_> = entries - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| p.is_file() && p.extension().and_then(|s| s.to_str()) == Some("jpg")) - .collect(); - - paths.sort(); - - for path in paths { - if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) { - // file_name format: "14-30-00" or "14-30-00_0" - let time_str = file_name.replace("-", ":").replace("_", " "); - results.push(serde_json::json!({ - "time": time_str, - "path": path.to_string_lossy().to_string() - })); + // Helper to process a directory with a time filter + let mut process_dir = |dir_date: &str, is_next_day: bool| { + let dir_path = base_path.join(dir_date); + if let Ok(entries) = fs::read_dir(dir_path) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jpg") { + if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) { + // file_name format: "14-30-00" + let parts: Vec<&str> = file_name.split('-').collect(); + if parts.len() >= 1 { + if let Ok(hour) = parts[0].parse::() { + // Logic: If current day, must be >= 3. If next day, must be < 3. + let keep = if !is_next_day { hour >= 3 } else { hour < 3 }; + if keep { + let time_str = file_name.replace("-", ":").replace("_", " "); + results.push(serde_json::json!({ + "time": time_str, + "path": path.to_string_lossy().to_string(), + "isNextDay": is_next_day + })); + } + } + } + } + } } } - } + }; + + process_dir(&date, false); + process_dir(&next_date_str, true); + // Sort results by isNextDay (false first) then by time + results.sort_by(|a, b| { + let a_next = a["isNextDay"].as_bool().unwrap_or(false); + let b_next = b["isNextDay"].as_bool().unwrap_or(false); + if a_next != b_next { + a_next.cmp(&b_next) + } else { + a["time"].as_str().unwrap_or("").cmp(b["time"].as_str().unwrap_or("")) + } + }); + results } diff --git a/src/App.vue b/src/App.vue index be994a1..f16a66e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,11 +10,13 @@ import { FolderOpen, Settings, Play, Pause, Maximize2, X, RefreshCw } from "luci interface TimelineItem { time: string; // "HH:mm:ss" path: string; + isNextDay?: boolean; } const PIXELS_PER_MINUTE = 1.5; // Controls the vertical stretch of the timeline const TOTAL_MINUTES = 24 * 60; const RULER_HEIGHT = TOTAL_MINUTES * PIXELS_PER_MINUTE; +const TIME_OFFSET_MINUTES = 3 * 60; // 03:00 logical day start // --- State --- const isSetupComplete = ref(false); @@ -23,7 +25,7 @@ const isPaused = ref(false); const currentDate = ref(new Date().toISOString().split("T")[0]); const timelineImages = ref([]); const selectedImage = ref(null); -const lockedImage = ref(null); // State for the clicked/locked point +const lockedImage = ref(null); const previewSrc = ref(""); const isFullscreen = ref(false); const isSettingsOpen = ref(false); @@ -47,10 +49,7 @@ onMounted(async () => { mergeScreens.value = (await store.get("mergeScreens")) as boolean || false; retainDays.value = (await store.get("retainDays")) as number || 30; captureInterval.value = (await store.get("captureInterval")) as number || 30; - - // Sync initial interval to Rust await invoke("update_interval", { seconds: captureInterval.value }); - isPaused.value = await invoke("get_pause_state"); await loadTimeline(); } @@ -59,7 +58,6 @@ onMounted(async () => { isPaused.value = event.payload; }); - // Listen for refresh triggers if any captureUnlisten = await listen("refresh-timeline", () => { loadTimeline(); }); @@ -85,8 +83,6 @@ const updateSettings = async () => { await store.set("retainDays", retainDays.value); await store.set("captureInterval", captureInterval.value); await store.save(); - - // Sync interval to Rust await invoke("update_interval", { seconds: captureInterval.value }); }; @@ -102,16 +98,13 @@ const loadTimeline = async () => { }); }; -// Internal function to update preview with caching/optimization const updatePreview = async (img: TimelineItem | null) => { if (!img) { selectedImage.value = null; previewSrc.value = ""; return; } - if (selectedImage.value?.path === img.path && previewSrc.value) return; - selectedImage.value = img; try { const base64 = await invoke("get_image_base64", { path: img.path }); @@ -121,23 +114,30 @@ const updatePreview = async (img: TimelineItem | null) => { } }; -// --- Timeline Helper Functions --- -const timeToMinutes = (timeStr: string) => { +// --- Helper Functions for Logical Day --- + +// Converts absolute HH:mm to minutes relative to 03:00 start +const timeToLogicalMinutes = (timeStr: string, isNextDay = false) => { const [h, m] = timeStr.split(":").map(Number); - return h * 60 + m; + let total = h * 60 + m; + if (isNextDay) total += 24 * 60; + return total - TIME_OFFSET_MINUTES; }; -const minutesToTime = (totalMinutes: number) => { - const h = Math.floor(totalMinutes / 60); - const m = Math.floor(totalMinutes % 60); +// Converts logical minutes (0 to 1439) back to HH:mm +const logicalMinutesToTime = (logicalMinutes: number) => { + let total = logicalMinutes + TIME_OFFSET_MINUTES; + total = total % (24 * 60); + const h = Math.floor(total / 60); + const m = Math.floor(total % 60); return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; }; -const findClosestImage = (minutes: number) => { +const findClosestImage = (logicalMin: number) => { if (timelineImages.value.length === 0) return null; return timelineImages.value.reduce((prev, curr) => { - const prevDiff = Math.abs(timeToMinutes(prev.time) - minutes); - const currDiff = Math.abs(timeToMinutes(curr.time) - minutes); + const prevDiff = Math.abs(timeToLogicalMinutes(prev.time, prev.isNextDay) - logicalMin); + const currDiff = Math.abs(timeToLogicalMinutes(curr.time, curr.isNextDay) - logicalMin); return currDiff < prevDiff ? curr : prev; }, timelineImages.value[0]); }; @@ -146,32 +146,27 @@ const handleTimelineMouseMove = (e: MouseEvent) => { if (!timelineRef.value) return; const rect = timelineRef.value.getBoundingClientRect(); const y = e.clientY - rect.top + timelineRef.value.scrollTop; - const minutes = Math.floor(y / PIXELS_PER_MINUTE); + const logicalMin = Math.floor(y / PIXELS_PER_MINUTE); - if (minutes >= 0 && minutes < TOTAL_MINUTES) { - hoveredTime.value = minutesToTime(minutes); - const closest = findClosestImage(minutes); - if (closest) { - updatePreview(closest); - } + if (logicalMin >= 0 && logicalMin < TOTAL_MINUTES) { + hoveredTime.value = logicalMinutesToTime(logicalMin); + const closest = findClosestImage(logicalMin); + if (closest) updatePreview(closest); } }; const handleTimelineMouseLeave = () => { hoveredTime.value = null; - // Revert to locked image if exists, otherwise keep current - if (lockedImage.value) { - updatePreview(lockedImage.value); - } + if (lockedImage.value) updatePreview(lockedImage.value); }; const handleTimelineClick = (e: MouseEvent) => { if (!timelineRef.value) return; const rect = timelineRef.value.getBoundingClientRect(); const y = e.clientY - rect.top + timelineRef.value.scrollTop; - const minutes = Math.floor(y / PIXELS_PER_MINUTE); + const logicalMin = Math.floor(y / PIXELS_PER_MINUTE); - const closest = findClosestImage(minutes); + const closest = findClosestImage(logicalMin); if (closest) { lockedImage.value = closest; updatePreview(closest); @@ -229,13 +224,13 @@ const handleTimelineClick = (e: MouseEvent) => { >
- +
- {{ String(h-1).padStart(2, '0') }}:00 + {{ String((h - 1 + 3) % 24).padStart(2, '0') }}:00
@@ -248,14 +243,14 @@ const handleTimelineClick = (e: MouseEvent) => { selectedImage?.path === img.path ? 'bg-[#007AFF]/60 h-2 shadow-sm z-10' : '', lockedImage?.path === img.path ? 'bg-[#007AFF] h-2.5 ring-2 ring-[#007AFF]/20 z-20' : '' ]" - :style="{ top: timeToMinutes(img.time) * PIXELS_PER_MINUTE + 'px' }" + :style="{ top: timeToLogicalMinutes(img.time, img.isNextDay) * PIXELS_PER_MINUTE + 'px' }" >
{{ hoveredTime }}