initial commit

This commit is contained in:
2026-06-08 22:49:50 -07:00
commit f6a6681e16
40 changed files with 3026 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
import type { Marker, SwitchType } from './types';
import { ZOOM_DEFAULT } from './types';
export const app = $state({
markers: [] as Marker[],
selectedIds: new Set<string>(),
isPlaying: false,
currentTime: 0,
duration: 0,
fps: 30,
pixelsPerSecond: ZOOM_DEFAULT,
scrollLeft: 0,
activeSwitch: 'cherry-mx-blue' as SwitchType,
videoUrl: null as string | null,
videoName: null as string | null,
timelineWidth: 800,
});
let markerCounter = 0;
export function createMarkerId(): string {
markerCounter += 1;
return `m${markerCounter}`;
}
export function setMarkers(value: Marker[]) {
app.markers = value;
const maxId = value.reduce((max, m) => {
const num = parseInt(m.id.replace(/\D/g, ''), 10);
return Number.isFinite(num) ? Math.max(max, num) : max;
}, 0);
markerCounter = maxId;
}
export function setSelectedIds(ids: Set<string>) {
app.selectedIds = ids;
}
export function setCurrentTime(value: number) {
app.currentTime = value;
}
export function setDuration(value: number) {
app.duration = value;
}
export function setIsPlaying(value: boolean) {
app.isPlaying = value;
}
export function setFps(value: number) {
app.fps = value;
}
export function setPixelsPerSecond(value: number) {
app.pixelsPerSecond = value;
}
export function setScrollLeft(value: number) {
app.scrollLeft = value;
}
export function setActiveSwitch(value: SwitchType) {
app.activeSwitch = value;
}
export function setTimelineWidth(value: number) {
app.timelineWidth = value;
}
export function addMarker(time: number, key: string): Marker {
const marker: Marker = { id: createMarkerId(), time, key };
app.markers = [...app.markers, marker].sort((a, b) => a.time - b.time);
return marker;
}
export function updateMarker(id: string, patch: Partial<Pick<Marker, 'time' | 'key'>>) {
app.markers = app.markers
.map((m) => (m.id === id ? { ...m, ...patch } : m))
.sort((a, b) => a.time - b.time);
}
export function setMarkerTimes(updates: Map<string, number>) {
app.markers = app.markers
.map((m) => {
const time = updates.get(m.id);
return time === undefined ? m : { ...m, time };
})
.sort((a, b) => a.time - b.time);
}
export function deleteMarkers(ids: Set<string>) {
app.markers = app.markers.filter((m) => !ids.has(m.id));
const next = new Set(app.selectedIds);
for (const id of ids) next.delete(id);
app.selectedIds = next;
}
export function selectAll() {
app.selectedIds = new Set(app.markers.map((m) => m.id));
}
export function toggleSelection(id: string, additive: boolean) {
const next = additive ? new Set(app.selectedIds) : new Set<string>();
if (next.has(id)) next.delete(id);
else next.add(id);
app.selectedIds = next;
}
export function setVideoUrl(url: string | null, name: string | null = null) {
if (app.videoUrl) URL.revokeObjectURL(app.videoUrl);
app.videoUrl = url;
app.videoName = name;
}
export function snapToFrame(time: number): number {
const frameDuration = 1 / app.fps;
return Math.round(time / frameDuration) * frameDuration;
}
export function nudgeSelected(deltaFrames: number) {
if (app.selectedIds.size === 0) return;
const frameDuration = 1 / app.fps;
const delta = deltaFrames * frameDuration;
app.markers = app.markers
.map((m) => {
if (!app.selectedIds.has(m.id)) return m;
const next = Math.max(0, Math.min(app.duration || Infinity, m.time + delta));
return { ...m, time: snapToFrame(next) };
})
.sort((a, b) => a.time - b.time);
}