ui: simplify scanner workflow and host status
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
parent
41bc1a0ea7
commit
4a768555dc
@ -34,9 +34,10 @@ export function HostStatus() {
|
||||
const pending = useLiveQuery(() => database.outbox.filter((event: OutboxRecord) => !event.syncedAt).count(), []) ?? 0
|
||||
const errors = useLiveQuery(() => database.outbox.filter((event: OutboxRecord) => !event.syncedAt && Boolean(event.lastError)).count(), []) ?? 0
|
||||
const [authenticated, setAuthenticated] = useState(Boolean(keycloak?.authenticated))
|
||||
const [station, setStation] = useState('Local / unassigned')
|
||||
const [station, setStation] = useState('This device')
|
||||
const [access, setAccess] = useState<AccessState | null>(null)
|
||||
const [requests, setRequests] = useState<AccessRequest[]>([])
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [showRequests, setShowRequests] = useState(false)
|
||||
|
||||
async function refreshAccess() {
|
||||
@ -85,34 +86,62 @@ export function HostStatus() {
|
||||
if (next) await refreshRequests()
|
||||
}
|
||||
|
||||
const summary = !authenticated
|
||||
? 'Sign in to sync'
|
||||
: !access
|
||||
? 'Checking access'
|
||||
: access.status !== 'approved'
|
||||
? access.status === 'pending'
|
||||
? 'Access pending'
|
||||
: access.status === 'rejected'
|
||||
? 'Access rejected'
|
||||
: 'Access required'
|
||||
: errors
|
||||
? `${errors} retrying`
|
||||
: pending
|
||||
? `${pending} waiting to sync`
|
||||
: 'Synced'
|
||||
|
||||
const tone = !authenticated || access?.status !== 'approved'
|
||||
? 'muted'
|
||||
: errors
|
||||
? 'error'
|
||||
: pending
|
||||
? 'pending'
|
||||
: 'synced'
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="host-status" aria-label="Arrowstack synchronization status">
|
||||
<span>
|
||||
<strong>{station}</strong>
|
||||
<small>
|
||||
{authenticated && access?.status !== 'approved'
|
||||
? access?.status === 'pending' ? 'Access pending approval' : access?.status === 'rejected' ? 'Access request rejected' : 'No FieldLens access'
|
||||
: `${pending} pending${errors ? ` · ${errors} retrying` : ''}`}
|
||||
</small>
|
||||
</span>
|
||||
{!authenticated ? (
|
||||
<>
|
||||
<button type="button" onClick={() => void login()}>Sign in</button>
|
||||
<button type="button" className="secondary" onClick={() => void register()}>Create account</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{(access?.status === 'none' || access?.status === 'rejected') && (
|
||||
<button type="button" onClick={() => void submitRequest()}>Request access</button>
|
||||
<aside className={`host-status ${expanded ? 'expanded' : ''}`} aria-label="Arrowstack synchronization status">
|
||||
<button className="host-status-trigger" type="button" aria-expanded={expanded} onClick={() => setExpanded((value) => !value)}>
|
||||
<i className={`sync-dot ${tone}`} aria-hidden="true" />
|
||||
<span className="host-status-copy">
|
||||
<strong>{station}</strong>
|
||||
<small>{summary}</small>
|
||||
</span>
|
||||
<span className="host-status-more" aria-hidden="true">{expanded ? '×' : '•••'}</span>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="host-status-menu">
|
||||
{authenticated && access?.username && <p className="host-user">{access.username}{access.email ? <small>{access.email}</small> : null}</p>}
|
||||
{!authenticated ? (
|
||||
<div className="host-actions">
|
||||
<button type="button" onClick={() => void login()}>Sign in</button>
|
||||
<button type="button" className="secondary" onClick={() => void register()}>Create account</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="host-actions">
|
||||
{(access?.status === 'none' || access?.status === 'rejected') && (
|
||||
<button type="button" onClick={() => void submitRequest()}>Request access</button>
|
||||
)}
|
||||
{access?.canApprove && (
|
||||
<button type="button" className="secondary" onClick={() => void toggleRequests()}>Requests</button>
|
||||
)}
|
||||
<button type="button" className="secondary" onClick={() => void logout()}>Sign out</button>
|
||||
</div>
|
||||
)}
|
||||
{access?.canApprove && (
|
||||
<button type="button" className="secondary" onClick={() => void toggleRequests()}>
|
||||
Requests
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="secondary" onClick={() => void logout()}>Sign out</button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
|
||||
@ -1 +1,112 @@
|
||||
.host-status{position:fixed;z-index:80;top:max(10px,env(safe-area-inset-top));right:max(10px,env(safe-area-inset-right));display:flex;align-items:center;gap:8px;padding:7px 8px 7px 11px;border:1px solid #314039;border-radius:10px;background:#17211fe8;color:#fff;box-shadow:0 5px 20px #0003;backdrop-filter:blur(10px);font:11px/1.2 system-ui,sans-serif}.host-status span{display:grid;gap:2px;min-width:0}.host-status strong{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.host-status small{color:#aebbb6}.host-status button,.access-panel button{border:0;border-radius:7px;padding:7px 9px;background:#f4d35e;color:#17211f;font-weight:800}.host-status button.secondary,.access-panel button.secondary{background:#314039;color:#fff}.access-panel{position:fixed;z-index:81;top:58px;right:max(10px,env(safe-area-inset-right));width:min(430px,calc(100vw - 20px));max-height:70vh;overflow:auto;padding:12px;border:1px solid #314039;border-radius:12px;background:#17211ff7;color:#fff;box-shadow:0 12px 36px #0006;font:12px/1.35 system-ui,sans-serif}.access-panel header,.access-panel article{display:flex;align-items:center;justify-content:space-between;gap:12px}.access-panel header{padding-bottom:10px;border-bottom:1px solid #314039}.access-panel header button{font-size:18px;line-height:1;padding:4px 8px}.access-panel article{padding:11px 0;border-bottom:1px solid #26332e}.access-panel article:last-child{border-bottom:0}.access-panel article span{display:grid;gap:3px;min-width:0}.access-panel article small{color:#aebbb6;overflow-wrap:anywhere}.access-panel article div{display:flex;gap:6px;flex-shrink:0}.access-panel p{color:#aebbb6}@media(max-width:760px){.host-status{top:max(7px,env(safe-area-inset-top));right:max(7px,env(safe-area-inset-right));left:max(7px,env(safe-area-inset-left));max-width:none;flex-wrap:wrap}.host-status span{margin-right:auto}.host-status strong{max-width:120px}.host-status button{padding:6px 8px;font-size:9px}.access-panel{top:94px;right:7px;width:calc(100vw - 14px)}}
|
||||
.host-status {
|
||||
position: fixed;
|
||||
z-index: 80;
|
||||
top: calc(78px + env(safe-area-inset-top));
|
||||
right: max(18px, env(safe-area-inset-right));
|
||||
width: min(238px, calc(100vw - 36px));
|
||||
font: 11px/1.25 system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.host-status-trigger {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 9px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 8px 9px 8px 11px;
|
||||
border: 1px solid rgba(255,255,255,.16);
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
background: rgba(23,33,31,.88);
|
||||
box-shadow: 0 5px 18px rgba(0,0,0,.18);
|
||||
backdrop-filter: blur(12px);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sync-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #87938e;
|
||||
box-shadow: 0 0 0 3px rgba(135,147,142,.12);
|
||||
}
|
||||
.sync-dot.synced { background: #66cf9d; box-shadow: 0 0 0 3px rgba(102,207,157,.14); }
|
||||
.sync-dot.pending { background: #f4d35e; box-shadow: 0 0 0 3px rgba(244,211,94,.14); }
|
||||
.sync-dot.error { background: #ed736a; box-shadow: 0 0 0 3px rgba(237,115,106,.14); }
|
||||
|
||||
.host-status-copy { min-width: 0; display: grid; gap: 1px; }
|
||||
.host-status-copy strong,
|
||||
.host-status-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.host-status-copy strong { font-size: 11px; }
|
||||
.host-status-copy small { color: #aebbb6; font-size: 9px; }
|
||||
.host-status-more { color: #aebbb6; font-size: 12px; letter-spacing: 1px; }
|
||||
|
||||
.host-status-menu {
|
||||
margin-top: 6px;
|
||||
padding: 9px;
|
||||
border: 1px solid #314039;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
background: rgba(23,33,31,.97);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,.28);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.host-user { margin: 0 0 8px; padding: 0 2px 8px; border-bottom: 1px solid #314039; font-weight: 800; }
|
||||
.host-user small { display: block; margin-top: 2px; color: #aebbb6; font-size: 9px; font-weight: 500; overflow-wrap: anywhere; }
|
||||
.host-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.host-actions button,
|
||||
.access-panel button {
|
||||
min-height: 31px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 0 9px;
|
||||
background: #f4d35e;
|
||||
color: #17211f;
|
||||
font-size: 9px;
|
||||
font-weight: 900;
|
||||
}
|
||||
.host-actions button.secondary,
|
||||
.access-panel button.secondary { background: #314039; color: white; }
|
||||
|
||||
.access-panel {
|
||||
position: fixed;
|
||||
z-index: 81;
|
||||
top: calc(170px + env(safe-area-inset-top));
|
||||
right: max(18px, env(safe-area-inset-right));
|
||||
width: min(430px, calc(100vw - 36px));
|
||||
max-height: 65vh;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border: 1px solid #314039;
|
||||
border-radius: 10px;
|
||||
background: rgba(23,33,31,.98);
|
||||
color: white;
|
||||
box-shadow: 0 12px 36px rgba(0,0,0,.38);
|
||||
font: 12px/1.35 system-ui, sans-serif;
|
||||
}
|
||||
.access-panel header,
|
||||
.access-panel article { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.access-panel header { padding-bottom: 10px; border-bottom: 1px solid #314039; }
|
||||
.access-panel header button { min-width: 30px; font-size: 18px; line-height: 1; }
|
||||
.access-panel article { padding: 11px 0; border-bottom: 1px solid #26332e; }
|
||||
.access-panel article:last-child { border-bottom: 0; }
|
||||
.access-panel article span { display: grid; gap: 3px; min-width: 0; }
|
||||
.access-panel article small { color: #aebbb6; overflow-wrap: anywhere; }
|
||||
.access-panel article div { display: flex; gap: 6px; flex-shrink: 0; }
|
||||
.access-panel p { color: #aebbb6; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.host-status {
|
||||
top: calc(86px + env(safe-area-inset-top));
|
||||
right: max(12px, env(safe-area-inset-right));
|
||||
width: min(218px, calc(100vw - 24px));
|
||||
}
|
||||
.host-status-trigger { padding: 7px 8px 7px 10px; }
|
||||
.host-status-menu { padding: 8px; }
|
||||
.access-panel {
|
||||
top: calc(184px + env(safe-area-inset-top));
|
||||
right: max(12px, env(safe-area-inset-right));
|
||||
width: calc(100vw - 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ main { min-width: 0; min-height: 100vh; min-height: 100dvh; background: var(--pa
|
||||
.scanner-detecting .local-badge span { animation: status-pulse 1.4s ease-out infinite; }
|
||||
.scanner-duplicate .local-badge span, .scanner-permission-denied .local-badge span, .scanner-camera-error .local-badge span { background: #ed736a; box-shadow: 0 0 0 3px rgba(237,115,106,.16); }
|
||||
.install-button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 31px; padding: 0 10px; border: 1px solid var(--yellow); border-radius: 4px; color: var(--ink); background: var(--yellow); font-size: 10px; font-weight: 900; }
|
||||
.camera-tools { position: absolute; z-index: 3; top: calc(112px + env(safe-area-inset-top)); right: max(40px, env(safe-area-inset-right)); display: flex; align-items: center; gap: 7px; }
|
||||
.camera-tools { position: absolute; z-index: 3; top: calc(142px + env(safe-area-inset-top)); right: max(40px, env(safe-area-inset-right)); display: flex; align-items: center; gap: 7px; }
|
||||
.camera-select { display: flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 8px; border: 1px solid rgba(255,255,255,.24); border-radius: 4px; color: white; background: rgba(23,33,31,.72); }
|
||||
.camera-select select { width: min(26vw, 180px); border: 0; outline: 0; color: white; background: transparent; font-size: 10px; font-weight: 800; }
|
||||
.camera-select option { color: var(--ink); background: white; }
|
||||
@ -180,7 +180,7 @@ button:focus-visible, select:focus-visible { outline: 3px solid white; outline-o
|
||||
@media (max-width: 760px) {
|
||||
.app-shell { display: block; padding-bottom: calc(70px + env(safe-area-inset-bottom)); }
|
||||
.sidebar { display: none; }
|
||||
.mobile-nav { position: fixed; z-index: 20; left: 0; right: 0; bottom: 0; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); height: calc(70px + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom); border-top: 1px solid #34413d; background: var(--ink); }
|
||||
.mobile-nav { position: fixed; z-index: 20; left: 0; right: 0; bottom: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); height: calc(70px + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom); border-top: 1px solid #34413d; background: var(--ink); }
|
||||
.mobile-nav button { display: grid; min-width: 0; place-items: center; align-content: center; gap: 4px; padding: 0 2px; border: 0; color: #8f9a96; background: transparent; font-size: 8px; font-weight: 800; }
|
||||
.mobile-nav button span { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mobile-nav button.active { color: var(--yellow); }
|
||||
@ -189,7 +189,7 @@ button:focus-visible, select:focus-visible { outline: 3px solid white; outline-o
|
||||
.camera-heading { padding: calc(24px + env(safe-area-inset-top)) max(20px, env(safe-area-inset-right)) 24px max(20px, env(safe-area-inset-left)); }
|
||||
.camera-heading h1 { font-size: 29px; }
|
||||
.camera-actions { flex-direction: column; align-items: flex-end; }
|
||||
.camera-tools { top: calc(126px + env(safe-area-inset-top)); left: max(20px, env(safe-area-inset-left)); right: max(20px, env(safe-area-inset-right)); flex-wrap: wrap; justify-content: flex-end; }
|
||||
.camera-tools { top: calc(152px + env(safe-area-inset-top)); left: max(20px, env(safe-area-inset-left)); right: max(20px, env(safe-area-inset-right)); flex-wrap: wrap; justify-content: flex-end; }
|
||||
.quality-panel { left: max(20px, env(safe-area-inset-left)); right: max(20px, env(safe-area-inset-right)); bottom: calc(86px + env(safe-area-inset-bottom)); width: auto; }
|
||||
.camera-select select { width: min(34vw, 140px); }
|
||||
.scan-frame { width: 88%; }
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { ClipboardCheck, CloudDownload, FlaskConical, History, LoaderCircle, ScanLine, Settings, Share, Smartphone, SquarePlus, Wifi, WifiOff, X } from 'lucide-react'
|
||||
import { CloudDownload, FlaskConical, History, LoaderCircle, ScanLine, Settings, Share, Smartphone, SquarePlus, Wifi, WifiOff, X } from 'lucide-react'
|
||||
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
|
||||
@ -7,13 +7,11 @@ import './App.css'
|
||||
import { database } from './lib/database'
|
||||
|
||||
const ScannerView = lazy(() => import('./features/scanner/ScannerView').then((module) => ({ default: module.ScannerView })))
|
||||
const EanPalletView = lazy(() => import('./features/ean-pallet/EanPalletView').then((module) => ({ default: module.EanPalletView })))
|
||||
const LabelCheckView = lazy(() => import('./features/label-check/LabelCheckView').then((module) => ({ default: module.LabelCheckView })))
|
||||
const HistoryView = lazy(() => import('./features/history/HistoryView').then((module) => ({ default: module.HistoryView })))
|
||||
const CodeLab = lazy(() => import('./features/codes/CodeLab').then((module) => ({ default: module.CodeLab })))
|
||||
const SettingsView = lazy(() => import('./features/settings/SettingsView').then((module) => ({ default: module.SettingsView })))
|
||||
|
||||
type View = 'scanner' | 'ean-pallet' | 'label-check' | 'history' | 'lab' | 'settings'
|
||||
type View = 'scanner' | 'history' | 'lab' | 'settings'
|
||||
type NavigationItem = { id: View; label: string; mobileLabel?: string; icon: typeof ScanLine }
|
||||
|
||||
const UPDATE_CHECK_INTERVAL = 60 * 60 * 1000
|
||||
@ -25,8 +23,6 @@ interface BeforeInstallPromptEvent extends Event {
|
||||
|
||||
const navigation: NavigationItem[] = [
|
||||
{ id: 'scanner', label: 'Scanner', icon: ScanLine },
|
||||
{ id: 'ean-pallet', label: 'EAN pallet', mobileLabel: 'EAN', icon: ScanLine },
|
||||
{ id: 'label-check', label: 'Carton labels', mobileLabel: 'OCR', icon: ClipboardCheck },
|
||||
{ id: 'history', label: 'History', icon: History },
|
||||
{ id: 'lab', label: 'Code lab', mobileLabel: 'Lab', icon: FlaskConical },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
@ -190,8 +186,6 @@ function App() {
|
||||
<main>
|
||||
<Suspense fallback={<div className="loading-view">Loading workspace</div>}>
|
||||
{view === 'scanner' && <ScannerView installAction={installAction} />}
|
||||
{view === 'ean-pallet' && <EanPalletView />}
|
||||
{view === 'label-check' && <LabelCheckView onOpenSettings={() => setView('settings')} />}
|
||||
{view === 'history' && <HistoryView />}
|
||||
{view === 'lab' && <CodeLab />}
|
||||
{view === 'settings' && (
|
||||
|
||||
@ -283,7 +283,7 @@ export function ScannerView({ installAction }: { installAction?: ReactNode }) {
|
||||
|
||||
const statusLabel = {
|
||||
starting: 'STARTING CAMERA',
|
||||
detecting: 'AUTO DETECTING',
|
||||
detecting: 'READY TO SCAN',
|
||||
captured: 'CAPTURED',
|
||||
duplicate: 'DUPLICATE',
|
||||
'permission-denied': 'CAMERA BLOCKED',
|
||||
@ -344,7 +344,7 @@ export function ScannerView({ installAction }: { installAction?: ReactNode }) {
|
||||
<div className={`scan-frame ${latest && /QR|Data Matrix|Aztec|MaxiCode/i.test(latest.format) ? 'matrix' : ''}`} aria-hidden="true"><span /><span /><span /><span /></div>
|
||||
{detectionPoints.map((point, index) => <i className="detection-point" style={{ left: `${point.left}%`, top: `${point.top}%` }} key={index} />)}
|
||||
<p className="camera-instruction" aria-live="polite">
|
||||
{cameraError || (showQualitySettings ? 'Scanning paused while settings are open' : qualityMessage || (status === 'starting' ? 'Preparing the rear camera' : armed ? `Barcode type is detected automatically${confirmationProgress ? ` · ${confirmationProgress}/${confirmationFrames}` : ''}` : autoAdvance ? 'Captured · scanner will rearm automatically' : 'Capture held for review'))}
|
||||
{cameraError || (showQualitySettings ? 'Scanning paused while settings are open' : qualityMessage || (status === 'starting' ? 'Preparing the rear camera' : armed ? `Point the camera at a barcode${confirmationProgress ? ` · confirming ${confirmationProgress}/${confirmationFrames}` : ''}` : autoAdvance ? 'Captured · scanner will rearm automatically' : 'Captured · press Scan next when ready'))}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -357,7 +357,7 @@ export function ScannerView({ installAction }: { installAction?: ReactNode }) {
|
||||
{status === 'storage-error' ? 'CAPTURE NOT SAVED' : cameraError ? 'CAMERA UNAVAILABLE' : latest?.isDuplicate ? 'DUPLICATE DETECTED' : latest ? 'CAPTURE SAVED' : 'SCANNER READY'}
|
||||
</span>
|
||||
<strong>{status === 'storage-error' ? pendingCapture?.rawValue : cameraError ? 'Camera access required' : latest?.rawValue ?? 'Waiting for a code'}</strong>
|
||||
<small>{status === 'storage-error' ? 'Local storage failed. Retry saving or discard this capture.' : cameraError ? (status === 'permission-denied' ? 'Allow camera access in browser settings, then try again.' : 'Check that a camera is available, then try again.') : latest ? `${latest.format} · stored on this device` : 'Nothing leaves the device until sync is configured.'}</small>
|
||||
<small>{status === 'storage-error' ? 'Local storage failed. Retry saving or discard this capture.' : cameraError ? (status === 'permission-denied' ? 'Allow camera access in browser settings, then try again.' : 'Check that a camera is available, then try again.') : latest ? `${latest.format} · saved locally first` : 'Captures are saved locally first and sync when signed in.'}</small>
|
||||
</div>
|
||||
{status === 'storage-error' && pendingCapture ? (
|
||||
<div className="result-actions">
|
||||
|
||||
@ -25,6 +25,14 @@ describe('scanner preferences', () => {
|
||||
expect(loadScannerPreferences()).toEqual(preferences)
|
||||
})
|
||||
|
||||
it('migrates existing installs to one-at-a-time scanning by default', () => {
|
||||
localStorage.setItem('fieldlens-scanner-preferences', JSON.stringify({
|
||||
...defaultScannerPreferences,
|
||||
autoAdvance: true,
|
||||
}))
|
||||
expect(loadScannerPreferences().autoAdvance).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to defaults for invalid stored data', () => {
|
||||
localStorage.setItem('fieldlens-scanner-preferences', '{invalid')
|
||||
expect(loadScannerPreferences()).toEqual(defaultScannerPreferences)
|
||||
|
||||
@ -10,9 +10,12 @@ export interface ScannerPreferences {
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'fieldlens-scanner-preferences'
|
||||
const PREFERENCE_VERSION = 2
|
||||
|
||||
type StoredScannerPreferences = Partial<ScannerPreferences> & { preferenceVersion?: number }
|
||||
|
||||
export const defaultScannerPreferences: ScannerPreferences = {
|
||||
autoAdvance: true,
|
||||
autoAdvance: false,
|
||||
soundEnabled: false,
|
||||
formatMode: 'standard',
|
||||
confirmationFrames: 2,
|
||||
@ -22,9 +25,9 @@ export const defaultScannerPreferences: ScannerPreferences = {
|
||||
|
||||
export function loadScannerPreferences(): ScannerPreferences {
|
||||
try {
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Partial<ScannerPreferences>
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as StoredScannerPreferences
|
||||
return {
|
||||
autoAdvance: typeof stored.autoAdvance === 'boolean' ? stored.autoAdvance : defaultScannerPreferences.autoAdvance,
|
||||
autoAdvance: stored.preferenceVersion === PREFERENCE_VERSION && typeof stored.autoAdvance === 'boolean' ? stored.autoAdvance : defaultScannerPreferences.autoAdvance,
|
||||
soundEnabled: typeof stored.soundEnabled === 'boolean' ? stored.soundEnabled : defaultScannerPreferences.soundEnabled,
|
||||
formatMode: stored.formatMode === 'standard' || stored.formatMode === 'products' || stored.formatMode === 'matrix' || stored.formatMode === 'all' ? stored.formatMode : defaultScannerPreferences.formatMode,
|
||||
confirmationFrames: stored.confirmationFrames === 3 ? 3 : 2,
|
||||
@ -41,7 +44,7 @@ export function loadScannerPreferences(): ScannerPreferences {
|
||||
}
|
||||
|
||||
export function saveScannerPreferences(preferences: ScannerPreferences) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences))
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...preferences, preferenceVersion: PREFERENCE_VERSION }))
|
||||
}
|
||||
|
||||
export function resetScannerPreferences() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user