import { createApp, ref, reactive, computed, onMounted, onUnmounted } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js' const App = { setup() { const view = ref('settings') const config = reactive({ sources: '', date_start: '', date_end: '', destination: '', }) const configError = ref('') const downloadStatus = reactive({ status: 'downloading', error_message: null, progress: { copied: 0, total: 0, current_folder: '' }, }) let pollInterval = null const photo = reactive({ filename: '', folder_name: '', folder_index: 0, folder_count: 1, photo_index: 0, photo_total: 0, already_accepted: false, session_done: false, loading: false, imageUrl: '', }) const upcomingUrls = ref([]) onMounted(async () => { document.addEventListener('keydown', handleKey) const res = await fetch('/api/config') const data = await res.json() if (data.configured) { config.sources = data.sources.join('\n') config.date_start = data.date_start config.date_end = data.date_end config.destination = data.destination } const statusRes = await fetch('/api/session/status') if (statusRes.ok) { const s = await statusRes.json() if (s.status === 'ready') { view.value = 'viewer' await loadCurrentPhoto() } else if (s.status === 'downloading') { Object.assign(downloadStatus, s) view.value = 'downloading' startPolling() } } }) onUnmounted(() => { document.removeEventListener('keydown', handleKey) stopPolling() }) function handleKey(e) { if (view.value !== 'viewer' || photo.loading) return if (e.key === 'ArrowRight') sendAction('accept') else if (e.key === 'ArrowLeft') sendAction('ignore') else if (e.key === 'ArrowUp') sendAction('remind') } async function startSession() { configError.value = '' const sources = config.sources.split('\n').map(s => s.trim()).filter(Boolean) if (!sources.length) { configError.value = 'Enter at least one source path'; return } if (!config.date_start || !config.date_end) { configError.value = 'Select a date range'; return } if (!config.destination.trim()) { configError.value = 'Enter a destination folder'; return } const cfgRes = await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sources, date_start: config.date_start, date_end: config.date_end, destination: config.destination.trim(), }), }) if (!cfgRes.ok) { const err = await cfgRes.json() configError.value = err.detail || 'Failed to save config' return } const startRes = await fetch('/api/session/start', { method: 'POST' }) if (!startRes.ok) { const err = await startRes.json() configError.value = err.detail || 'Failed to start session' return } downloadStatus.status = 'downloading' downloadStatus.error_message = null downloadStatus.progress = { copied: 0, total: 0, current_folder: sources[0] } view.value = 'downloading' startPolling() } function startPolling() { pollInterval = setInterval(async () => { const res = await fetch('/api/session/status') if (!res.ok) { stopPolling(); return } const data = await res.json() Object.assign(downloadStatus, data) if (data.status === 'ready') { stopPolling() view.value = 'viewer' await loadCurrentPhoto() } else if (data.status === 'error' || data.status === 'empty') { stopPolling() } }, 1000) } function stopPolling() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null } } function refreshUpcoming() { const t = Date.now() upcomingUrls.value = [1, 2, 3].map(offset => `/api/photo/image?offset=${offset}&t=${t}`) } async function loadCurrentPhoto() { photo.loading = true const res = await fetch('/api/photo/current') if (!res.ok) { photo.loading = false; return } const data = await res.json() if (data.session_done) { view.value = 'done'; photo.loading = false; return } if (data.loading) { await new Promise(r => setTimeout(r, 2000)) return loadCurrentPhoto() } Object.assign(photo, data) photo.imageUrl = `/api/photo/image?t=${Date.now()}` photo.loading = false refreshUpcoming() } async function sendAction(action) { if (photo.loading) return photo.loading = true const res = await fetch('/api/photo/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }), }) if (!res.ok) { photo.loading = false; return } const data = await res.json() if (data.next.session_done) { view.value = 'done'; photo.loading = false; return } if (data.next.loading) { await new Promise(r => setTimeout(r, 2000)) return loadCurrentPhoto() } Object.assign(photo, data.next) photo.imageUrl = `/api/photo/image?t=${Date.now()}` photo.loading = false refreshUpcoming() } async function resetSession() { await fetch('/api/session', { method: 'DELETE' }) view.value = 'settings' } const progressPct = computed(() => { const { copied, total } = downloadStatus.progress if (!total) return 0 return Math.min(100, Math.round((copied / total) * 100)) }) return { view, config, configError, downloadStatus, progressPct, photo, upcomingUrls, startSession, sendAction, resetSession, } }, template: `
{{ configError }}
{{ downloadStatus.progress.current_folder }}
Reading file list from cloud
{{ downloadStatus.progress.current_folder }}
{{ downloadStatus.progress.copied }} / {{ downloadStatus.progress.total }} files
{{ downloadStatus.error_message }}
No photos matched the selected date range.
Accepted photos are in:
{{ config.destination }}