Skwik/src/stores/app.ts
Samuel Prevost 11e8013b6a feat(cache): persist datums per file hash and user settings
- Add file-hash.ts: SHA-256 hash of uploaded files via Web Crypto API
- Add datum-cache.ts: localStorage save/load/clear for datums by hash
- Add settings-cache.ts: persist scalePxPerMm and includeScaleBar
- Restore datums from cache on re-upload of same file
- Discreet "Clear cache" button on upload page
- Store fileHash and cacheRestoreMessage in Pinia store
- Auto-save datums on every change via deep watcher
- Track maxStepReached for clickable step navigation

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-14 23:19:34 +02:00

127 lines
3.6 KiB
TypeScript

import { defineStore } from "pinia"
import { ref, computed } from "vue"
import type { AppStep, Datum, DeskewResult, ExifData } from "@/types"
import { DEFAULT_SCALE_PX_PER_MM } from "@/types"
import { loadSettings } from "@/lib/settings-cache"
export const useAppStore = defineStore("app", () => {
const cached = loadSettings()
const currentStep = ref<AppStep>(1)
const maxStepReached = ref<AppStep>(1)
const originalFile = ref<File | null>(null)
const loadedImage = ref<HTMLImageElement | null>(null)
const exifData = ref<ExifData>({})
const datums = ref<Datum[]>([])
const deskewResult = ref<DeskewResult | null>(null)
const isProcessing = ref(false)
const processingStatus = ref("")
const selectedDatumId = ref<string | null>(null)
const scalePxPerMm = ref(
cached?.scalePxPerMm ?? DEFAULT_SCALE_PX_PER_MM,
)
const fileHash = ref<string | null>(null)
const cacheRestoreMessage = ref("")
const canProceedToStep2 = computed(() => loadedImage.value !== null)
const canProceedToStep3 = computed(() => canProceedToStep2.value)
const canProceedToStep4 = computed(() => {
if (!canProceedToStep3.value || datums.value.length === 0) return false
return datums.value.every((d) => {
if (d.type === "rectangle") return d.widthMm > 0 && d.heightMm > 0
return d.lengthMm > 0
})
})
function setImage(file: File, image: HTMLImageElement) {
originalFile.value = file
loadedImage.value = image
}
function setExif(data: ExifData) {
exifData.value = data
}
function goToStep(step: AppStep) {
currentStep.value = step
if (step > maxStepReached.value) {
maxStepReached.value = step
}
}
function addDatum(datum: Datum) {
datums.value.push(datum)
selectedDatumId.value = datum.id
}
function updateDatum(id: string, updates: Partial<Datum>) {
const index = datums.value.findIndex((d) => d.id === id)
const existing = datums.value[index]
if (index !== -1 && existing) {
datums.value[index] = {
...existing,
...updates,
} as Datum
}
}
function removeDatum(id: string) {
datums.value = datums.value.filter((d) => d.id !== id)
if (selectedDatumId.value === id) {
selectedDatumId.value = datums.value[0]?.id ?? null
}
}
function setResult(result: DeskewResult) {
deskewResult.value = result
}
function setFileHash(hash: string) {
fileHash.value = hash
}
function reset() {
currentStep.value = 1
maxStepReached.value = 1
originalFile.value = null
loadedImage.value = null
exifData.value = {}
datums.value = []
deskewResult.value = null
isProcessing.value = false
processingStatus.value = ""
selectedDatumId.value = null
scalePxPerMm.value = DEFAULT_SCALE_PX_PER_MM
fileHash.value = null
cacheRestoreMessage.value = ""
}
return {
currentStep,
maxStepReached,
originalFile,
loadedImage,
exifData,
datums,
deskewResult,
isProcessing,
processingStatus,
selectedDatumId,
scalePxPerMm,
fileHash,
cacheRestoreMessage,
canProceedToStep2,
canProceedToStep3,
canProceedToStep4,
setImage,
setExif,
goToStep,
addDatum,
updateDatum,
removeDatum,
setResult,
setFileHash,
reset,
}
})