@rohal12/spindle 0.31.2 → 0.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/components/macros/MenubarButtons.tsx +1 -1
- package/src/saves/save-manager.ts +57 -7
- package/src/store.ts +24 -13
package/package.json
CHANGED
|
@@ -119,7 +119,7 @@ defineMenubarAction({
|
|
|
119
119
|
setup: () => {
|
|
120
120
|
const load = useStoryStore((s) => s.load);
|
|
121
121
|
const hasSave = useStoryStore((s) => s.hasSave);
|
|
122
|
-
useStoryStore((s) => s.
|
|
122
|
+
useStoryStore((s) => s.knownSaves);
|
|
123
123
|
return { perform: () => load(), disabled: !hasSave() };
|
|
124
124
|
},
|
|
125
125
|
});
|
|
@@ -258,16 +258,25 @@ export async function getSavesGrouped(
|
|
|
258
258
|
return result;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
// --- Quick Save ---
|
|
261
|
+
// --- Quick Save / Slot Save ---
|
|
262
262
|
|
|
263
263
|
const AUTOSAVE_KEY_PREFIX = 'autosave.';
|
|
264
|
+
const SLOT_KEY_PREFIX = 'slot.';
|
|
265
|
+
const SLOT_INDEX_KEY_PREFIX = 'slotIndex.';
|
|
266
|
+
|
|
267
|
+
function slotMetaKey(ifid: string, slot?: string): string {
|
|
268
|
+
return slot != null
|
|
269
|
+
? `${SLOT_KEY_PREFIX}${slot}.${ifid}`
|
|
270
|
+
: `${AUTOSAVE_KEY_PREFIX}${ifid}`;
|
|
271
|
+
}
|
|
264
272
|
|
|
265
273
|
export async function quickSave(
|
|
266
274
|
ifid: string,
|
|
267
275
|
playthroughId: string,
|
|
268
276
|
payload: SavePayload,
|
|
277
|
+
slot?: string,
|
|
269
278
|
): Promise<SaveRecord> {
|
|
270
|
-
const metaKey =
|
|
279
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
271
280
|
const existingId = await getMeta<string>(metaKey);
|
|
272
281
|
|
|
273
282
|
if (existingId) {
|
|
@@ -275,16 +284,30 @@ export async function quickSave(
|
|
|
275
284
|
if (updated) return updated;
|
|
276
285
|
}
|
|
277
286
|
|
|
278
|
-
// Create new
|
|
287
|
+
// Create new save
|
|
279
288
|
const record = await createSave(ifid, playthroughId, payload, {
|
|
280
|
-
isAutosave:
|
|
289
|
+
isAutosave: !slot,
|
|
290
|
+
...(slot != null ? { slot } : {}),
|
|
281
291
|
});
|
|
282
292
|
await setMeta(metaKey, record.meta.id);
|
|
293
|
+
|
|
294
|
+
// Track named slots in the index
|
|
295
|
+
if (slot != null) {
|
|
296
|
+
const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
297
|
+
const existing = (await getMeta<string[]>(indexKey)) ?? [];
|
|
298
|
+
if (!existing.includes(slot)) {
|
|
299
|
+
await setMeta(indexKey, [...existing, slot]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
283
303
|
return record;
|
|
284
304
|
}
|
|
285
305
|
|
|
286
|
-
export async function hasQuickSave(
|
|
287
|
-
|
|
306
|
+
export async function hasQuickSave(
|
|
307
|
+
ifid: string,
|
|
308
|
+
slot?: string,
|
|
309
|
+
): Promise<boolean> {
|
|
310
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
288
311
|
const existingId = await getMeta<string>(metaKey);
|
|
289
312
|
if (!existingId) return false;
|
|
290
313
|
const record = await getSave(existingId);
|
|
@@ -293,13 +316,40 @@ export async function hasQuickSave(ifid: string): Promise<boolean> {
|
|
|
293
316
|
|
|
294
317
|
export async function loadQuickSave(
|
|
295
318
|
ifid: string,
|
|
319
|
+
slot?: string,
|
|
296
320
|
): Promise<SavePayload | undefined> {
|
|
297
|
-
const metaKey =
|
|
321
|
+
const metaKey = slotMetaKey(ifid, slot);
|
|
298
322
|
const existingId = await getMeta<string>(metaKey);
|
|
299
323
|
if (!existingId) return undefined;
|
|
300
324
|
return loadSave(existingId);
|
|
301
325
|
}
|
|
302
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Check IDB for all known saves and return a map of slot keys to true.
|
|
329
|
+
* The default (autosave) slot uses empty string as key.
|
|
330
|
+
*/
|
|
331
|
+
export async function populateKnownSaves(
|
|
332
|
+
ifid: string,
|
|
333
|
+
): Promise<Record<string, true>> {
|
|
334
|
+
const result: Record<string, true> = {};
|
|
335
|
+
|
|
336
|
+
// Check default autosave
|
|
337
|
+
if (await hasQuickSave(ifid)) {
|
|
338
|
+
result[''] = true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check named slots from the index
|
|
342
|
+
const indexKey = `${SLOT_INDEX_KEY_PREFIX}${ifid}`;
|
|
343
|
+
const slots = (await getMeta<string[]>(indexKey)) ?? [];
|
|
344
|
+
for (const slot of slots) {
|
|
345
|
+
if (await hasQuickSave(ifid, slot)) {
|
|
346
|
+
result[slot] = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
303
353
|
// --- Session Persistence (survives F5, cleared on tab close) ---
|
|
304
354
|
|
|
305
355
|
const SESSION_KEY_PREFIX = 'spindle.session.';
|
package/src/store.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getCurrentPlaythroughId,
|
|
18
18
|
quickSave,
|
|
19
19
|
loadQuickSave,
|
|
20
|
+
populateKnownSaves,
|
|
20
21
|
saveSession,
|
|
21
22
|
clearSession,
|
|
22
23
|
} from './saves/save-manager';
|
|
@@ -198,7 +199,7 @@ export interface StoryState {
|
|
|
198
199
|
historyIndex: number;
|
|
199
200
|
visitCounts: Record<string, number>;
|
|
200
201
|
renderCounts: Record<string, number>;
|
|
201
|
-
|
|
202
|
+
knownSaves: Record<string, true>;
|
|
202
203
|
playthroughId: string;
|
|
203
204
|
maxHistory: number;
|
|
204
205
|
saveError: string | null;
|
|
@@ -243,7 +244,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
243
244
|
historyIndex: -1,
|
|
244
245
|
visitCounts: {},
|
|
245
246
|
renderCounts: {},
|
|
246
|
-
|
|
247
|
+
knownSaves: {},
|
|
247
248
|
playthroughId: '',
|
|
248
249
|
maxHistory: 40,
|
|
249
250
|
saveError: null,
|
|
@@ -307,6 +308,14 @@ export const useStoryStore = create<StoryState>()(
|
|
|
307
308
|
state.playthroughId = newId;
|
|
308
309
|
});
|
|
309
310
|
}
|
|
311
|
+
|
|
312
|
+
// Populate knownSaves from IDB so hasSave() works after reload
|
|
313
|
+
const saves = await populateKnownSaves(ifid);
|
|
314
|
+
if (Object.keys(saves).length > 0) {
|
|
315
|
+
set((state) => {
|
|
316
|
+
state.knownSaves = saves;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
310
319
|
})
|
|
311
320
|
.catch((err) =>
|
|
312
321
|
console.error('spindle: failed to init save system', err),
|
|
@@ -487,7 +496,7 @@ export const useStoryStore = create<StoryState>()(
|
|
|
487
496
|
);
|
|
488
497
|
},
|
|
489
498
|
|
|
490
|
-
save: () => {
|
|
499
|
+
save: (slot?: string) => {
|
|
491
500
|
const { storyData, playthroughId } = get();
|
|
492
501
|
if (!storyData) return;
|
|
493
502
|
|
|
@@ -496,14 +505,17 @@ export const useStoryStore = create<StoryState>()(
|
|
|
496
505
|
set((state) => {
|
|
497
506
|
state.saveError = null;
|
|
498
507
|
});
|
|
499
|
-
quickSave(storyData.ifid, playthroughId, payload)
|
|
508
|
+
quickSave(storyData.ifid, playthroughId, payload, slot)
|
|
500
509
|
.then(() => {
|
|
501
510
|
set((state) => {
|
|
502
|
-
state.
|
|
511
|
+
state.knownSaves = {
|
|
512
|
+
...state.knownSaves,
|
|
513
|
+
[slot ?? '']: true,
|
|
514
|
+
};
|
|
503
515
|
});
|
|
504
516
|
})
|
|
505
517
|
.catch((err) => {
|
|
506
|
-
console.error('spindle: failed to
|
|
518
|
+
console.error('spindle: failed to save', err);
|
|
507
519
|
set((state) => {
|
|
508
520
|
state.saveError =
|
|
509
521
|
err instanceof Error ? err.message : 'Failed to save';
|
|
@@ -511,20 +523,20 @@ export const useStoryStore = create<StoryState>()(
|
|
|
511
523
|
});
|
|
512
524
|
},
|
|
513
525
|
|
|
514
|
-
load: () => {
|
|
526
|
+
load: (slot?: string) => {
|
|
515
527
|
const { storyData } = get();
|
|
516
528
|
if (!storyData) return;
|
|
517
529
|
|
|
518
530
|
set((state) => {
|
|
519
531
|
state.loadError = null;
|
|
520
532
|
});
|
|
521
|
-
loadQuickSave(storyData.ifid)
|
|
533
|
+
loadQuickSave(storyData.ifid, slot)
|
|
522
534
|
.then((payload) => {
|
|
523
535
|
if (!payload) return;
|
|
524
536
|
get().loadFromPayload(payload);
|
|
525
537
|
})
|
|
526
538
|
.catch((err) => {
|
|
527
|
-
console.error('spindle: failed to load
|
|
539
|
+
console.error('spindle: failed to load save', err);
|
|
528
540
|
set((state) => {
|
|
529
541
|
state.loadError =
|
|
530
542
|
err instanceof Error ? err.message : 'Failed to load';
|
|
@@ -532,11 +544,10 @@ export const useStoryStore = create<StoryState>()(
|
|
|
532
544
|
});
|
|
533
545
|
},
|
|
534
546
|
|
|
535
|
-
hasSave: () => {
|
|
536
|
-
|
|
537
|
-
const { storyData, saveVersion } = get();
|
|
547
|
+
hasSave: (slot?: string) => {
|
|
548
|
+
const { storyData, knownSaves } = get();
|
|
538
549
|
if (!storyData) return false;
|
|
539
|
-
return
|
|
550
|
+
return (slot ?? '') in knownSaves;
|
|
540
551
|
},
|
|
541
552
|
|
|
542
553
|
getSavePayload: (): SavePayload => {
|