Skip to content

Commit 0b80bb3

Browse files
committed
Persist local data and add clear confirmation
1 parent 2505d6a commit 0b80bb3

5 files changed

Lines changed: 135 additions & 9 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ If you do not change a record, the file is written back byte-for-byte.
2222
- **Fast and lightweight** React + Vite stack
2323

2424
## Screenshots
25-
Add screenshots or GIFs here.
25+
![PLU key layout](docs/screenshots/scaleconfig-dashboard.png)
26+
![PLU editor](docs/screenshots/scaleconfig-plu-edit.png)
2627

2728
## Quick Start
2829
### Prerequisites
409 KB
Loading
390 KB
Loading

frontend/src/components/layout/AppShell.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function AppShell() {
1212
const isDirty = useStore(s => s.isDirty);
1313
const loadFile = useStore(s => s.loadFile);
1414
const exportFile = useStore(s => s.exportFile);
15+
const clearLocalData = useStore(s => s.clearLocalData);
1516

1617
function handleImport() {
1718
const input = document.createElement('input');
@@ -39,6 +40,14 @@ export function AppShell() {
3940
URL.revokeObjectURL(url);
4041
}
4142

43+
function handleClearLocalData() {
44+
const ok = window.confirm(
45+
'This will clear locally saved data and reset the app. You will need to re-import your .TMS file. Continue?'
46+
);
47+
if (!ok) return;
48+
clearLocalData();
49+
}
50+
4251
const dirty = isDirty();
4352

4453
return (
@@ -89,7 +98,20 @@ export function AppShell() {
8998
{currentScreen === 'keyboard' && <KeyboardScreen />}
9099
{currentScreen === 'plu' && <PluScreen />}
91100
{currentScreen === 'settings' && (
92-
<div className="p-6 text-warm-500 text-sm">Settings (coming soon)</div>
101+
<div className="p-6">
102+
<h2 className="text-lg font-semibold text-warm-900 mb-4">Settings</h2>
103+
<div className="bg-white border border-warm-200 rounded-lg p-4 max-w-lg">
104+
<div className="text-sm text-warm-700 mb-3">
105+
Clear locally saved data to reset the app and remove the last loaded file.
106+
</div>
107+
<button
108+
onClick={handleClearLocalData}
109+
className="px-4 py-2 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
110+
>
111+
Clear Local Data
112+
</button>
113+
</div>
114+
</div>
93115
)}
94116
</main>
95117
</div>

frontend/src/store/useStore.ts

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,61 @@ import { writeFile, updateRowField, createPluRow } from '../parser/writer';
55
import type { NewPluParams } from '../parser/writer';
66
import { PLU_FIELDS, SCP_FIELDS } from '../parser/fieldMap';
77

8+
const STORAGE_KEY = 'scaleconfig:last-session';
9+
10+
function canUseStorage(): boolean {
11+
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
12+
}
13+
14+
function bytesToBase64(bytes: Uint8Array): string {
15+
let binary = '';
16+
const chunkSize = 0x8000;
17+
for (let i = 0; i < bytes.length; i += chunkSize) {
18+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
19+
}
20+
return btoa(binary);
21+
}
22+
23+
function base64ToBytes(base64: string): Uint8Array {
24+
const binary = atob(base64);
25+
const bytes = new Uint8Array(binary.length);
26+
for (let i = 0; i < binary.length; i++) {
27+
bytes[i] = binary.charCodeAt(i);
28+
}
29+
return bytes;
30+
}
31+
32+
function loadPersistedFile(): { fileName: string; bytes: Uint8Array } | null {
33+
if (!canUseStorage()) return null;
34+
try {
35+
const raw = window.localStorage.getItem(STORAGE_KEY);
36+
if (!raw) return null;
37+
const parsed = JSON.parse(raw) as { fileName: string; data: string };
38+
if (!parsed?.data) return null;
39+
const bytes = base64ToBytes(parsed.data);
40+
return { fileName: parsed.fileName || 'restored.TMS', bytes };
41+
} catch {
42+
window.localStorage.removeItem(STORAGE_KEY);
43+
return null;
44+
}
45+
}
46+
47+
function persistFile(fileName: string, bytes: Uint8Array): void {
48+
if (!canUseStorage()) return;
49+
try {
50+
const data = bytesToBase64(bytes);
51+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ fileName, data }));
52+
} catch {
53+
// Ignore storage errors (quota, private mode, etc.)
54+
}
55+
}
56+
57+
function persistDoc(fileDoc: FileDoc | null, fileName: string): void {
58+
if (!fileDoc) return;
59+
const bytes = writeFile(fileDoc);
60+
persistFile(fileName || 'output.TMS', bytes);
61+
}
62+
863
interface AppState {
964
// File state
1065
fileDoc: FileDoc | null;
@@ -26,6 +81,7 @@ interface AppState {
2681
// Actions - File
2782
loadFile: (buffer: ArrayBuffer, fileName: string) => void;
2883
exportFile: () => Uint8Array | null;
84+
clearLocalData: () => void;
2985

3086
// Actions - PLU editing
3187
addPlu: (params: NewPluParams) => { success: boolean; error?: string };
@@ -49,17 +105,40 @@ interface AppState {
49105
isDirty: () => boolean;
50106
}
51107

108+
const persisted = loadPersistedFile();
109+
const persistedState = (() => {
110+
if (!persisted) return null;
111+
try {
112+
const buffer = persisted.bytes.buffer.slice(
113+
persisted.bytes.byteOffset,
114+
persisted.bytes.byteOffset + persisted.bytes.byteLength,
115+
);
116+
const doc = parseFile(buffer);
117+
return {
118+
fileDoc: doc,
119+
fileName: persisted.fileName,
120+
originalBytes: new Uint8Array(buffer),
121+
pluRecords: extractPluRecords(doc),
122+
scpEntries: extractScpEntries(doc),
123+
dptRecords: extractDptRecords(doc),
124+
currentScreen: 'keyboard' as ScreenId,
125+
};
126+
} catch {
127+
return null;
128+
}
129+
})();
130+
52131
export const useStore = create<AppState>((set, get) => ({
53132
// Initial state
54-
fileDoc: null,
55-
fileName: '',
56-
originalBytes: null,
57-
pluRecords: [],
58-
scpEntries: [],
59-
dptRecords: [],
133+
fileDoc: persistedState?.fileDoc ?? null,
134+
fileName: persistedState?.fileName ?? '',
135+
originalBytes: persistedState?.originalBytes ?? null,
136+
pluRecords: persistedState?.pluRecords ?? [],
137+
scpEntries: persistedState?.scpEntries ?? [],
138+
dptRecords: persistedState?.dptRecords ?? [],
60139
activeLayer: 0,
61140
selectedKeyIndex: null,
62-
currentScreen: 'dashboard',
141+
currentScreen: persistedState?.currentScreen ?? 'dashboard',
63142
searchQuery: '',
64143
pluSearchQuery: '',
65144

@@ -82,6 +161,7 @@ export const useStore = create<AppState>((set, get) => ({
82161
currentScreen: 'keyboard',
83162
selectedKeyIndex: null,
84163
});
164+
persistFile(fileName, bytes);
85165
},
86166

87167
exportFile: () => {
@@ -90,6 +170,25 @@ export const useStore = create<AppState>((set, get) => ({
90170
return writeFile(fileDoc);
91171
},
92172

173+
clearLocalData: () => {
174+
if (canUseStorage()) {
175+
window.localStorage.removeItem(STORAGE_KEY);
176+
}
177+
set({
178+
fileDoc: null,
179+
fileName: '',
180+
originalBytes: null,
181+
pluRecords: [],
182+
scpEntries: [],
183+
dptRecords: [],
184+
activeLayer: 0,
185+
selectedKeyIndex: null,
186+
currentScreen: 'dashboard',
187+
searchQuery: '',
188+
pluSearchQuery: '',
189+
});
190+
},
191+
93192
// ─── PLU editing ──────────────────────────────────
94193

95194
addPlu: (params) => {
@@ -111,6 +210,7 @@ export const useStore = create<AppState>((set, get) => ({
111210
// Refresh parsed records
112211
const updatedRecords = extractPluRecords(fileDoc);
113212
set({ pluRecords: updatedRecords });
213+
persistDoc(fileDoc, get().fileName);
114214

115215
return { success: true };
116216
},
@@ -129,6 +229,7 @@ export const useStore = create<AppState>((set, get) => ({
129229

130230
const pluRecords = extractPluRecords(fileDoc);
131231
set({ pluRecords });
232+
persistDoc(fileDoc, get().fileName);
132233
},
133234

134235
updatePluField: (pluId, field, value) => {
@@ -156,6 +257,7 @@ export const useStore = create<AppState>((set, get) => ({
156257
// Refresh parsed records
157258
const pluRecords = extractPluRecords(fileDoc);
158259
set({ pluRecords });
260+
persistDoc(fileDoc, get().fileName);
159261
},
160262

161263
// ─── Keyboard mapping ────────────────────────────
@@ -184,6 +286,7 @@ export const useStore = create<AppState>((set, get) => ({
184286
// Refresh SCP entries
185287
const scpEntries = extractScpEntries(fileDoc);
186288
set({ scpEntries });
289+
persistDoc(fileDoc, get().fileName);
187290
},
188291

189292
clearKey: (layer, keyIndex) => {

0 commit comments

Comments
 (0)