init scale timeline
This commit is contained in:
61
src/App.vue
61
src/App.vue
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onUnmounted, computed } from "vue";
|
||||||
import { load } from "@tauri-apps/plugin-store";
|
import { load } from "@tauri-apps/plugin-store";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
@@ -13,9 +13,7 @@ interface TimelineItem {
|
|||||||
isNextDay?: boolean;
|
isNextDay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PIXELS_PER_MINUTE = 1.5; // Controls the vertical stretch of the timeline
|
|
||||||
const TOTAL_MINUTES = 24 * 60;
|
const TOTAL_MINUTES = 24 * 60;
|
||||||
const RULER_HEIGHT = TOTAL_MINUTES * PIXELS_PER_MINUTE;
|
|
||||||
const TIME_OFFSET_MINUTES = 3 * 60; // 03:00 logical day start
|
const TIME_OFFSET_MINUTES = 3 * 60; // 03:00 logical day start
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
@@ -32,10 +30,14 @@ const isSettingsOpen = ref(false);
|
|||||||
const mergeScreens = ref(false);
|
const mergeScreens = ref(false);
|
||||||
const retainDays = ref(30);
|
const retainDays = ref(30);
|
||||||
const captureInterval = ref(30);
|
const captureInterval = ref(30);
|
||||||
|
const timelineZoom = ref(1.5); // pixels per minute
|
||||||
|
|
||||||
const hoveredTime = ref<string | null>(null);
|
const hoveredTime = ref<string | null>(null);
|
||||||
const timelineRef = ref<HTMLElement | null>(null);
|
const timelineRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// Computed ruler height based on zoom
|
||||||
|
const rulerHeight = computed(() => TOTAL_MINUTES * timelineZoom.value);
|
||||||
|
|
||||||
let store: any = null;
|
let store: any = null;
|
||||||
let captureUnlisten: any = null;
|
let captureUnlisten: any = null;
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@ onMounted(async () => {
|
|||||||
mergeScreens.value = (await store.get("mergeScreens")) as boolean || false;
|
mergeScreens.value = (await store.get("mergeScreens")) as boolean || false;
|
||||||
retainDays.value = (await store.get("retainDays")) as number || 30;
|
retainDays.value = (await store.get("retainDays")) as number || 30;
|
||||||
captureInterval.value = (await store.get("captureInterval")) as number || 30;
|
captureInterval.value = (await store.get("captureInterval")) as number || 30;
|
||||||
|
timelineZoom.value = (await store.get("timelineZoom")) as number || 1.5;
|
||||||
|
|
||||||
await invoke("update_interval", { seconds: captureInterval.value });
|
await invoke("update_interval", { seconds: captureInterval.value });
|
||||||
isPaused.value = await invoke("get_pause_state");
|
isPaused.value = await invoke("get_pause_state");
|
||||||
await loadTimeline();
|
await loadTimeline();
|
||||||
@@ -82,6 +86,7 @@ const updateSettings = async () => {
|
|||||||
await store.set("mergeScreens", mergeScreens.value);
|
await store.set("mergeScreens", mergeScreens.value);
|
||||||
await store.set("retainDays", retainDays.value);
|
await store.set("retainDays", retainDays.value);
|
||||||
await store.set("captureInterval", captureInterval.value);
|
await store.set("captureInterval", captureInterval.value);
|
||||||
|
await store.set("timelineZoom", timelineZoom.value);
|
||||||
await store.save();
|
await store.save();
|
||||||
await invoke("update_interval", { seconds: captureInterval.value });
|
await invoke("update_interval", { seconds: captureInterval.value });
|
||||||
};
|
};
|
||||||
@@ -146,7 +151,7 @@ const handleTimelineMouseMove = (e: MouseEvent) => {
|
|||||||
if (!timelineRef.value) return;
|
if (!timelineRef.value) return;
|
||||||
const rect = timelineRef.value.getBoundingClientRect();
|
const rect = timelineRef.value.getBoundingClientRect();
|
||||||
const y = e.clientY - rect.top + timelineRef.value.scrollTop;
|
const y = e.clientY - rect.top + timelineRef.value.scrollTop;
|
||||||
const logicalMin = Math.floor(y / PIXELS_PER_MINUTE);
|
const logicalMin = Math.floor(y / timelineZoom.value);
|
||||||
|
|
||||||
if (logicalMin >= 0 && logicalMin < TOTAL_MINUTES) {
|
if (logicalMin >= 0 && logicalMin < TOTAL_MINUTES) {
|
||||||
hoveredTime.value = logicalMinutesToTime(logicalMin);
|
hoveredTime.value = logicalMinutesToTime(logicalMin);
|
||||||
@@ -164,7 +169,7 @@ const handleTimelineClick = (e: MouseEvent) => {
|
|||||||
if (!timelineRef.value) return;
|
if (!timelineRef.value) return;
|
||||||
const rect = timelineRef.value.getBoundingClientRect();
|
const rect = timelineRef.value.getBoundingClientRect();
|
||||||
const y = e.clientY - rect.top + timelineRef.value.scrollTop;
|
const y = e.clientY - rect.top + timelineRef.value.scrollTop;
|
||||||
const logicalMin = Math.floor(y / PIXELS_PER_MINUTE);
|
const logicalMin = Math.floor(y / timelineZoom.value);
|
||||||
|
|
||||||
const closest = findClosestImage(logicalMin);
|
const closest = findClosestImage(logicalMin);
|
||||||
if (closest) {
|
if (closest) {
|
||||||
@@ -173,6 +178,15 @@ const handleTimelineClick = (e: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTimelineWheel = (e: WheelEvent) => {
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = e.deltaY > 0 ? -0.2 : 0.2;
|
||||||
|
timelineZoom.value = Math.min(Math.max(0.5, timelineZoom.value + delta), 15);
|
||||||
|
updateSettings();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -199,19 +213,33 @@ const handleTimelineClick = (e: MouseEvent) => {
|
|||||||
<!-- Vertical Ruler Sidebar -->
|
<!-- Vertical Ruler Sidebar -->
|
||||||
<div class="w-72 bg-[#F8FAFD] border-r border-[#E5E5E7] flex flex-col select-none relative">
|
<div class="w-72 bg-[#F8FAFD] border-r border-[#E5E5E7] flex flex-col select-none relative">
|
||||||
<!-- Date Picker Header -->
|
<!-- Date Picker Header -->
|
||||||
<div class="p-6 bg-[#F8FAFD]/80 backdrop-blur-md sticky top-0 z-20 border-b border-[#E5E5E7]/50">
|
<div class="p-6 bg-[#F8FAFD]/80 backdrop-blur-md sticky top-0 z-20 border-b border-[#E5E5E7]/50">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-lg font-bold tracking-tight">历史活动</h2>
|
<h2 class="text-lg font-bold tracking-tight">历史活动</h2>
|
||||||
<button @click="loadTimeline" class="p-2 hover:bg-white rounded-xl transition-colors text-[#86868B] hover:text-[#007AFF]">
|
<button @click="loadTimeline" class="p-2 hover:bg-white rounded-xl transition-colors text-[#86868B] hover:text-[#007AFF]">
|
||||||
<RefreshCw :size="18" />
|
<RefreshCw :size="18" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<div class="space-y-4">
|
||||||
type="date"
|
<input
|
||||||
v-model="currentDate"
|
type="date"
|
||||||
@change="loadTimeline"
|
v-model="currentDate"
|
||||||
class="w-full bg-white border border-[#E5E5E7] rounded-xl px-4 py-2.5 text-sm font-medium focus:ring-2 focus:ring-[#007AFF]/20 focus:border-[#007AFF] outline-none transition-all"
|
@change="loadTimeline"
|
||||||
/>
|
class="w-full bg-white border border-[#E5E5E7] rounded-xl px-4 py-2 text-sm font-medium focus:ring-2 focus:ring-[#007AFF]/20 focus:border-[#007AFF] outline-none transition-all"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center gap-3 px-1">
|
||||||
|
<span class="text-[10px] font-bold text-[#86868B]">缩放</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model.number="timelineZoom"
|
||||||
|
min="0.5"
|
||||||
|
max="10"
|
||||||
|
step="0.1"
|
||||||
|
@change="updateSettings"
|
||||||
|
class="flex-1 h-1 bg-[#E5E5E7] rounded-full appearance-none cursor-pointer accent-[#007AFF]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scrollable Ruler -->
|
<!-- Scrollable Ruler -->
|
||||||
@@ -221,13 +249,14 @@ const handleTimelineClick = (e: MouseEvent) => {
|
|||||||
@mousemove="handleTimelineMouseMove"
|
@mousemove="handleTimelineMouseMove"
|
||||||
@mouseleave="handleTimelineMouseLeave"
|
@mouseleave="handleTimelineMouseLeave"
|
||||||
@click="handleTimelineClick"
|
@click="handleTimelineClick"
|
||||||
|
@wheel="handleTimelineWheel"
|
||||||
>
|
>
|
||||||
<!-- Ruler Canvas -->
|
<!-- Ruler Canvas -->
|
||||||
<div :style="{ height: RULER_HEIGHT + 'px' }" class="relative ml-16">
|
<div :style="{ height: rulerHeight + 'px' }" class="relative ml-16">
|
||||||
<!-- Hour Markers (Starting from 03:00) -->
|
<!-- Hour Markers (Starting from 03:00) -->
|
||||||
<div v-for="h in 24" :key="h"
|
<div v-for="h in 24" :key="h"
|
||||||
class="absolute left-0 w-full border-t border-[#E5E5E7]/60 flex items-start"
|
class="absolute left-0 w-full border-t border-[#E5E5E7]/60 flex items-start"
|
||||||
:style="{ top: (h-1) * 60 * PIXELS_PER_MINUTE + 'px' }"
|
:style="{ top: (h-1) * 60 * timelineZoom + 'px' }"
|
||||||
>
|
>
|
||||||
<span class="absolute -left-12 -top-2.5 text-[10px] font-bold text-[#86868B] tracking-tighter">
|
<span class="absolute -left-12 -top-2.5 text-[10px] font-bold text-[#86868B] tracking-tighter">
|
||||||
{{ String((h - 1 + 3) % 24).padStart(2, '0') }}:00
|
{{ String((h - 1 + 3) % 24).padStart(2, '0') }}:00
|
||||||
@@ -243,14 +272,14 @@ const handleTimelineClick = (e: MouseEvent) => {
|
|||||||
selectedImage?.path === img.path ? 'bg-[#007AFF]/60 h-2 shadow-sm z-10' : '',
|
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' : ''
|
lockedImage?.path === img.path ? 'bg-[#007AFF] h-2.5 ring-2 ring-[#007AFF]/20 z-20' : ''
|
||||||
]"
|
]"
|
||||||
:style="{ top: timeToLogicalMinutes(img.time, img.isNextDay) * PIXELS_PER_MINUTE + 'px' }"
|
:style="{ top: timeToLogicalMinutes(img.time, img.isNextDay) * timelineZoom + 'px' }"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Hover Cursor Line -->
|
<!-- Hover Cursor Line -->
|
||||||
<div
|
<div
|
||||||
v-if="hoveredTime"
|
v-if="hoveredTime"
|
||||||
class="absolute left-0 right-0 border-t-2 border-[#007AFF] z-30 pointer-events-none"
|
class="absolute left-0 right-0 border-t-2 border-[#007AFF] z-30 pointer-events-none"
|
||||||
:style="{ top: timeToLogicalMinutes(hoveredTime, hoveredTime < '03:00' && hoveredTime >= '00:00' && hoveredTime !== '03:00') * PIXELS_PER_MINUTE + 'px' }"
|
:style="{ top: timeToLogicalMinutes(hoveredTime, hoveredTime < '03:00' && hoveredTime >= '00:00' && hoveredTime !== '03:00') * timelineZoom + 'px' }"
|
||||||
>
|
>
|
||||||
<div class="absolute -left-14 -top-3 bg-[#007AFF] text-white text-[10px] px-1.5 py-0.5 rounded font-bold shadow-sm">
|
<div class="absolute -left-14 -top-3 bg-[#007AFF] text-white text-[10px] px-1.5 py-0.5 rounded font-bold shadow-sm">
|
||||||
{{ hoveredTime }}
|
{{ hoveredTime }}
|
||||||
|
|||||||
Reference in New Issue
Block a user