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>
This commit is contained in:
parent
0cb9009eaa
commit
11e8013b6a
@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue"
|
import { ref, onMounted } from "vue"
|
||||||
import { useAppStore } from "@/stores/app"
|
import { useAppStore } from "@/stores/app"
|
||||||
import { loadImage } from "@/lib/image-loader"
|
import { loadImage } from "@/lib/image-loader"
|
||||||
import { extractExif } from "@/lib/exif"
|
import { extractExif } from "@/lib/exif"
|
||||||
|
import { hashFile } from "@/lib/file-hash"
|
||||||
|
import { loadDatums, clearCache, getCacheSize } from "@/lib/datum-cache"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -10,33 +12,62 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const error = ref("")
|
const error = ref("")
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const cacheCount = ref(0)
|
||||||
|
|
||||||
const ACCEPTED = ".jpg,.jpeg,.heic,.heif"
|
const ACCEPTED = ".jpg,.jpeg,.heic,.heif"
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
cacheCount.value = getCacheSize()
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleClearCache() {
|
||||||
|
clearCache()
|
||||||
|
cacheCount.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFile(file: File) {
|
async function handleFile(file: File) {
|
||||||
error.value = ""
|
error.value = ""
|
||||||
store.isProcessing = true
|
store.isProcessing = true
|
||||||
store.processingStatus = "Reading file..."
|
store.processingStatus = "Reading file..."
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { image, convertedFile } = await loadImage(file, (status) => {
|
const { image, convertedFile } = await loadImage(
|
||||||
store.processingStatus = status
|
file,
|
||||||
})
|
(status) => {
|
||||||
|
store.processingStatus = status
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
store.processingStatus = "Extracting EXIF data..."
|
store.processingStatus = "Extracting EXIF data..."
|
||||||
const exif = await extractExif(file)
|
const exif = await extractExif(file)
|
||||||
|
|
||||||
|
store.processingStatus = "Computing file hash..."
|
||||||
|
const hash = await hashFile(file)
|
||||||
|
store.setFileHash(hash)
|
||||||
|
|
||||||
|
const cached = loadDatums(hash)
|
||||||
|
if (cached && cached.length > 0) {
|
||||||
|
store.datums = cached
|
||||||
|
store.cacheRestoreMessage =
|
||||||
|
`Restored ${String(cached.length)} datum${cached.length === 1 ? "" : "s"} from cache`
|
||||||
|
setTimeout(() => {
|
||||||
|
store.cacheRestoreMessage = ""
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
|
||||||
store.setImage(convertedFile, image)
|
store.setImage(convertedFile, image)
|
||||||
store.setExif(exif)
|
store.setExif(exif)
|
||||||
store.goToStep(2)
|
store.goToStep(2)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : "Failed to load image"
|
error.value =
|
||||||
|
e instanceof Error ? e.message : "Failed to load image"
|
||||||
} finally {
|
} finally {
|
||||||
store.isProcessing = false
|
store.isProcessing = false
|
||||||
store.processingStatus = ""
|
store.processingStatus = ""
|
||||||
@ -57,16 +88,17 @@ function onFileSelect(e: Event) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-[60vh] items-center justify-center">
|
<div class="flex min-h-[60vh] items-start justify-center pt-12">
|
||||||
<Card class="w-full max-w-lg">
|
<div class="w-full max-w-2xl space-y-6">
|
||||||
<CardHeader class="text-center">
|
<Card>
|
||||||
<CardTitle class="text-2xl">Upload an Image</CardTitle>
|
<CardHeader class="text-center">
|
||||||
<CardDescription>
|
<CardTitle class="text-lg">Load Source Image</CardTitle>
|
||||||
Drop a JPG or HEIC image, or click to browse. HEIC files
|
<CardDescription>
|
||||||
will be converted automatically.
|
Drop a JPG or HEIC file, or click to browse. HEIC
|
||||||
</CardDescription>
|
is converted automatically.
|
||||||
</CardHeader>
|
</CardDescription>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
<div
|
<div
|
||||||
class="relative flex min-h-[200px] cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors"
|
class="relative flex min-h-[200px] cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors"
|
||||||
:class="
|
:class="
|
||||||
@ -112,8 +144,10 @@ function onFileSelect(e: Event) {
|
|||||||
>browse</span
|
>browse</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 text-xs text-muted-foreground/70">
|
<p
|
||||||
JPG, JPEG, HEIC, HEIF
|
class="mt-1 font-mono text-xs text-muted-foreground/60"
|
||||||
|
>
|
||||||
|
.jpg .jpeg .heic .heif
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -132,7 +166,193 @@ function onFileSelect(e: Event) {
|
|||||||
>
|
>
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<div
|
||||||
|
v-if="cacheCount > 0"
|
||||||
|
class="flex justify-end"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 gap-1.5 text-xs text-muted-foreground/60 hover:text-destructive"
|
||||||
|
@click="handleClearCache"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 6h18" />
|
||||||
|
<path
|
||||||
|
d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"
|
||||||
|
/>
|
||||||
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
||||||
|
</svg>
|
||||||
|
Clear cache ({{ cacheCount }})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 text-left">
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground/70">Example</p>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<img
|
||||||
|
src="/example-before.jpg"
|
||||||
|
alt="Before: angled photograph of a Pioneer CDJ-1000MK3 top case"
|
||||||
|
class="w-full rounded-md border border-border object-cover"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">Before — angled shot</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<img
|
||||||
|
src="/example-after.jpg"
|
||||||
|
alt="After: perspective-corrected front-facing view"
|
||||||
|
class="w-full rounded-md border border-border object-cover"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">After — corrected perspective</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 text-left">
|
||||||
|
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Correct perspective distortion in photographs using
|
||||||
|
known reference dimensions. Useful when you need to
|
||||||
|
use a photo as a scale reference for design work,
|
||||||
|
measure objects from photographs, or recover accurate
|
||||||
|
geometry from angled shots.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 text-left">
|
||||||
|
<h3 class="text-sm font-medium text-foreground">How it works</h3>
|
||||||
|
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
Place an object with known dimensions (a ruler, credit card, or A4 sheet) next to the subject
|
||||||
|
you want to photograph. Take the picture from any angle. Skwik uses the reference object to
|
||||||
|
compute a perspective transform and produce a corrected, front-facing image with accurate
|
||||||
|
proportions.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||||
|
This is especially handy for reverse-engineering enclosure cutouts, measuring parts you
|
||||||
|
can't easily reach, or getting a dimensionally accurate top-down view without a tripod.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="mt-8 text-sm font-medium text-foreground">Tips for best results</h3>
|
||||||
|
<ul class="list-disc space-y-2 pl-5 text-sm leading-relaxed text-muted-foreground">
|
||||||
|
<li>
|
||||||
|
<strong class="text-foreground/80">Use a large, rigid rectangle with precise dimensions.</strong>
|
||||||
|
An A4 magazine cover works well. Plain paper is acceptable but can bend or curl,
|
||||||
|
which degrades accuracy.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="text-foreground/80">Lay everything on a flat surface.</strong>
|
||||||
|
Both the subject and the reference object must sit on the same plane.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="text-foreground/80">Shoot from as high up as possible and zoom in.</strong>
|
||||||
|
Stand on a chair or stool and hold your phone at arm's length above the scene.
|
||||||
|
Use optical zoom (2× or more) to narrow the field of view — this compresses
|
||||||
|
perspective closer to an orthogonal projection and makes the correction more accurate.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Side-view illustration: person on chair photographing downward -->
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 360 260"
|
||||||
|
class="w-full max-w-xl"
|
||||||
|
role="img"
|
||||||
|
aria-label="Side-view illustration: a person standing on a chair holds a phone high, zooming in on an object on the ground below"
|
||||||
|
>
|
||||||
|
<!-- Ground line -->
|
||||||
|
<line x1="0" y1="240" x2="360" y2="240" stroke="currentColor" stroke-width="1.5" class="text-border" />
|
||||||
|
|
||||||
|
<!-- Object on the ground directly below phone (centered at x=180) -->
|
||||||
|
<rect x="145" y="230" width="70" height="10" rx="2"
|
||||||
|
class="text-primary" fill="currentColor" opacity="0.18"
|
||||||
|
stroke="currentColor" stroke-width="1.2" />
|
||||||
|
<rect x="165" y="224" width="30" height="16" rx="1"
|
||||||
|
fill="none" stroke="currentColor" stroke-width="0.8"
|
||||||
|
stroke-dasharray="3 2" class="text-muted-foreground" />
|
||||||
|
<text x="170" y="245" font-size="7" class="text-muted-foreground" fill="currentColor">ref</text>
|
||||||
|
|
||||||
|
<!-- Chair (simple side-view) -->
|
||||||
|
<g class="text-muted-foreground" stroke="currentColor" stroke-width="1.5" fill="none">
|
||||||
|
<!-- seat -->
|
||||||
|
<line x1="150" y1="170" x2="190" y2="170" />
|
||||||
|
<!-- legs -->
|
||||||
|
<line x1="153" y1="170" x2="150" y2="240" />
|
||||||
|
<line x1="187" y1="170" x2="190" y2="240" />
|
||||||
|
<!-- back rest -->
|
||||||
|
<line x1="150" y1="170" x2="147" y2="125" />
|
||||||
|
<line x1="147" y1="125" x2="157" y2="125" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Person (stick figure standing on the chair) -->
|
||||||
|
<g class="text-foreground" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- head -->
|
||||||
|
<circle cx="170" cy="102" r="9" />
|
||||||
|
<!-- body -->
|
||||||
|
<line x1="170" y1="111" x2="170" y2="150" />
|
||||||
|
<!-- legs on chair seat -->
|
||||||
|
<line x1="170" y1="150" x2="160" y2="170" />
|
||||||
|
<line x1="170" y1="150" x2="180" y2="170" />
|
||||||
|
<!-- arms reaching forward/down holding phone -->
|
||||||
|
<polyline points="170,125 183,115 180,108" />
|
||||||
|
<!-- other arm at side for balance -->
|
||||||
|
<line x1="170" y1="125" x2="152" y2="140" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Phone in hand (held out, pointing straight down) -->
|
||||||
|
<rect x="175" y="100" width="10" height="16" rx="2"
|
||||||
|
class="text-foreground" fill="currentColor" opacity="0.85" />
|
||||||
|
<!-- small lens dot -->
|
||||||
|
<circle cx="180" cy="116" r="1.5" class="text-background" fill="currentColor" />
|
||||||
|
|
||||||
|
<!-- Camera FOV cone — wide angle (faded, dashed) pointing straight down -->
|
||||||
|
<polygon points="180,116 124,238 236,238"
|
||||||
|
fill="currentColor" class="text-muted-foreground" opacity="0.06" />
|
||||||
|
<line x1="180" y1="116" x2="124" y2="238"
|
||||||
|
stroke="currentColor" stroke-width="0.8" stroke-dasharray="4 3"
|
||||||
|
class="text-muted-foreground" opacity="0.3" />
|
||||||
|
<line x1="180" y1="116" x2="236" y2="238"
|
||||||
|
stroke="currentColor" stroke-width="0.8" stroke-dasharray="4 3"
|
||||||
|
class="text-muted-foreground" opacity="0.3" />
|
||||||
|
|
||||||
|
<!-- Camera FOV cone — zoomed in (narrow, stronger) pointing straight down -->
|
||||||
|
<polygon points="180,116 155,230 205,230"
|
||||||
|
fill="currentColor" class="text-primary" opacity="0.10" />
|
||||||
|
<line x1="180" y1="116" x2="155" y2="230"
|
||||||
|
stroke="currentColor" stroke-width="1.2"
|
||||||
|
class="text-primary" opacity="0.6" />
|
||||||
|
<line x1="180" y1="116" x2="205" y2="230"
|
||||||
|
stroke="currentColor" stroke-width="1.2"
|
||||||
|
class="text-primary" opacity="0.6" />
|
||||||
|
|
||||||
|
<!-- Labels -->
|
||||||
|
<text x="238" y="230" font-size="8" class="text-muted-foreground" fill="currentColor">wide angle</text>
|
||||||
|
<text x="238" y="240" font-size="7" class="text-muted-foreground" fill="currentColor">(more distortion)</text>
|
||||||
|
<text x="207" y="215" font-size="8" class="text-primary" fill="currentColor" font-weight="600">zoomed in</text>
|
||||||
|
|
||||||
|
<!-- Small arrow showing "higher = better" -->
|
||||||
|
<g class="text-muted-foreground" stroke="currentColor" stroke-width="1" opacity="0.5">
|
||||||
|
<line x1="50" y1="230" x2="50" y2="105" />
|
||||||
|
<polyline points="45,112 50,105 55,112" fill="none" />
|
||||||
|
</g>
|
||||||
|
<text x="28" y="168" font-size="7" class="text-muted-foreground" fill="currentColor"
|
||||||
|
transform="rotate(-90 40 168)">higher is better</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
48
src/lib/datum-cache.ts
Normal file
48
src/lib/datum-cache.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { Datum } from "@/types"
|
||||||
|
|
||||||
|
const KEY_PREFIX = "skwik-datums-"
|
||||||
|
|
||||||
|
export function saveDatums(hash: string, datums: Datum[]): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
KEY_PREFIX + hash,
|
||||||
|
JSON.stringify(datums),
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// localStorage full or unavailable — silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadDatums(hash: string): Datum[] | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(KEY_PREFIX + hash)
|
||||||
|
if (!raw) return null
|
||||||
|
return JSON.parse(raw) as Datum[]
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearCache(): void {
|
||||||
|
const toRemove: string[] = []
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i)
|
||||||
|
if (key?.startsWith(KEY_PREFIX)) {
|
||||||
|
toRemove.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of toRemove) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCacheSize(): number {
|
||||||
|
let count = 0
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i)
|
||||||
|
if (key?.startsWith(KEY_PREFIX)) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
6
src/lib/file-hash.ts
Normal file
6
src/lib/file-hash.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export async function hashFile(file: File): Promise<string> {
|
||||||
|
const buffer = await file.arrayBuffer()
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||||
|
}
|
||||||
24
src/lib/settings-cache.ts
Normal file
24
src/lib/settings-cache.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export interface SkwikSettings {
|
||||||
|
scalePxPerMm: number
|
||||||
|
includeScaleBar: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEY = "skwik-settings"
|
||||||
|
|
||||||
|
export function saveSettings(settings: SkwikSettings): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(KEY, JSON.stringify(settings))
|
||||||
|
} catch {
|
||||||
|
// localStorage full or unavailable — silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadSettings(): SkwikSettings | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(KEY)
|
||||||
|
if (!raw) return null
|
||||||
|
return JSON.parse(raw) as SkwikSettings
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,9 +2,13 @@ import { defineStore } from "pinia"
|
|||||||
import { ref, computed } from "vue"
|
import { ref, computed } from "vue"
|
||||||
import type { AppStep, Datum, DeskewResult, ExifData } from "@/types"
|
import type { AppStep, Datum, DeskewResult, ExifData } from "@/types"
|
||||||
import { DEFAULT_SCALE_PX_PER_MM } from "@/types"
|
import { DEFAULT_SCALE_PX_PER_MM } from "@/types"
|
||||||
|
import { loadSettings } from "@/lib/settings-cache"
|
||||||
|
|
||||||
export const useAppStore = defineStore("app", () => {
|
export const useAppStore = defineStore("app", () => {
|
||||||
|
const cached = loadSettings()
|
||||||
|
|
||||||
const currentStep = ref<AppStep>(1)
|
const currentStep = ref<AppStep>(1)
|
||||||
|
const maxStepReached = ref<AppStep>(1)
|
||||||
const originalFile = ref<File | null>(null)
|
const originalFile = ref<File | null>(null)
|
||||||
const loadedImage = ref<HTMLImageElement | null>(null)
|
const loadedImage = ref<HTMLImageElement | null>(null)
|
||||||
const exifData = ref<ExifData>({})
|
const exifData = ref<ExifData>({})
|
||||||
@ -13,7 +17,11 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
const isProcessing = ref(false)
|
const isProcessing = ref(false)
|
||||||
const processingStatus = ref("")
|
const processingStatus = ref("")
|
||||||
const selectedDatumId = ref<string | null>(null)
|
const selectedDatumId = ref<string | null>(null)
|
||||||
const scalePxPerMm = ref(DEFAULT_SCALE_PX_PER_MM)
|
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 canProceedToStep2 = computed(() => loadedImage.value !== null)
|
||||||
const canProceedToStep3 = computed(() => canProceedToStep2.value)
|
const canProceedToStep3 = computed(() => canProceedToStep2.value)
|
||||||
@ -36,6 +44,9 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
|
|
||||||
function goToStep(step: AppStep) {
|
function goToStep(step: AppStep) {
|
||||||
currentStep.value = step
|
currentStep.value = step
|
||||||
|
if (step > maxStepReached.value) {
|
||||||
|
maxStepReached.value = step
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDatum(datum: Datum) {
|
function addDatum(datum: Datum) {
|
||||||
@ -65,8 +76,13 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
deskewResult.value = result
|
deskewResult.value = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFileHash(hash: string) {
|
||||||
|
fileHash.value = hash
|
||||||
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
currentStep.value = 1
|
currentStep.value = 1
|
||||||
|
maxStepReached.value = 1
|
||||||
originalFile.value = null
|
originalFile.value = null
|
||||||
loadedImage.value = null
|
loadedImage.value = null
|
||||||
exifData.value = {}
|
exifData.value = {}
|
||||||
@ -76,10 +92,13 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
processingStatus.value = ""
|
processingStatus.value = ""
|
||||||
selectedDatumId.value = null
|
selectedDatumId.value = null
|
||||||
scalePxPerMm.value = DEFAULT_SCALE_PX_PER_MM
|
scalePxPerMm.value = DEFAULT_SCALE_PX_PER_MM
|
||||||
|
fileHash.value = null
|
||||||
|
cacheRestoreMessage.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentStep,
|
currentStep,
|
||||||
|
maxStepReached,
|
||||||
originalFile,
|
originalFile,
|
||||||
loadedImage,
|
loadedImage,
|
||||||
exifData,
|
exifData,
|
||||||
@ -89,6 +108,8 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
processingStatus,
|
processingStatus,
|
||||||
selectedDatumId,
|
selectedDatumId,
|
||||||
scalePxPerMm,
|
scalePxPerMm,
|
||||||
|
fileHash,
|
||||||
|
cacheRestoreMessage,
|
||||||
canProceedToStep2,
|
canProceedToStep2,
|
||||||
canProceedToStep3,
|
canProceedToStep3,
|
||||||
canProceedToStep4,
|
canProceedToStep4,
|
||||||
@ -99,6 +120,7 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
updateDatum,
|
updateDatum,
|
||||||
removeDatum,
|
removeDatum,
|
||||||
setResult,
|
setResult,
|
||||||
|
setFileHash,
|
||||||
reset,
|
reset,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user