Files
nextsnap/app/static/js/storage.js
kamaji 3f0b0ea2e2 Live-update queue list when upload status changes
Storage.updatePhoto() now fires a 'photo-updated' CustomEvent so the
queue page refreshes immediately (300ms debounce) when the sync engine
changes a photo's status, instead of waiting for the 5s poll.

Also reduces background poll to 30s (just a fallback now), and revokes
stale ObjectURLs on each rebuild to prevent memory leaks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:07:53 -06:00

173 lines
5.3 KiB
JavaScript

// NextSnap - IndexedDB Storage using Dexie.js
'use strict';
const Storage = {
db: null,
init() {
// Initialize Dexie database
this.db = new Dexie('nextsnap');
// Define schema with compound indexes
this.db.version(1).stores({
photos: '++id, username, timestamp, filename, targetPath, status, [username+status], [username+timestamp]',
settings: '++id, username, [username+key]'
});
// Request persistent storage so the browser won't evict our data
this.requestPersistence();
return this.db.open();
},
async requestPersistence() {
if (navigator.storage && navigator.storage.persist) {
const granted = await navigator.storage.persist();
console.log('[STORAGE] Persistence:', granted ? 'granted' : 'denied');
}
},
async savePhoto(photoData) {
// Convert Blob to ArrayBuffer for storage resilience.
// iOS Safari can evict Blob file-backed data from IndexedDB,
// but ArrayBuffer is stored inline and won't be evicted.
let imageData = photoData.blob;
if (imageData instanceof Blob) {
imageData = await imageData.arrayBuffer();
}
const id = await this.db.photos.add({
username: photoData.username,
timestamp: photoData.timestamp,
filename: photoData.filename,
targetPath: photoData.targetPath,
blob: imageData,
status: photoData.status || 'pending',
retryCount: photoData.retryCount || 0,
lastError: photoData.lastError || null
});
return id;
},
// Convert stored photo data back to a Blob for upload or display.
// Handles both legacy Blob storage and new ArrayBuffer storage.
getBlob(photo) {
if (!photo || !photo.blob) return null;
if (photo.blob instanceof Blob) return photo.blob;
return new Blob([photo.blob], { type: 'image/jpeg' });
},
async getPhoto(id) {
return await this.db.photos.get(id);
},
async getAllPhotos(username = null) {
if (username) {
return await this.db.photos
.where('username').equals(username)
.reverse()
.sortBy('timestamp');
}
return await this.db.photos.reverse().sortBy('timestamp');
},
async getPendingPhotos(username = null) {
if (username) {
return await this.db.photos
.where('[username+status]')
.equals([username, 'pending'])
.sortBy('timestamp');
}
return await this.db.photos
.where('status').equals('pending')
.sortBy('timestamp');
},
async getRecentPhotos(username, limit = 5) {
return await this.db.photos
.where('username').equals(username)
.reverse()
.limit(limit)
.sortBy('timestamp');
},
async updatePhoto(id, updates) {
const result = await this.db.photos.update(id, updates);
// Notify any listening UI (e.g. queue page) of the change
try {
window.dispatchEvent(new CustomEvent('photo-updated', {
detail: { id: id, updates: updates }
}));
} catch (e) { /* ignore if no window context */ }
return result;
},
async deletePhoto(id) {
return await this.db.photos.delete(id);
},
async getPhotoCount(username = null, status = null) {
let collection = this.db.photos;
if (username && status) {
return await collection
.where('[username+status]')
.equals([username, status])
.count();
} else if (username) {
return await collection
.where('username').equals(username)
.count();
} else if (status) {
return await collection
.where('status').equals(status)
.count();
}
return await collection.count();
},
async saveSetting(username, key, value) {
// Check if setting exists
const existing = await this.db.settings
.where('[username+key]')
.equals([username, key])
.first();
if (existing) {
await this.db.settings.update(existing.id, { value: value });
} else {
await this.db.settings.add({
username: username,
key: key,
value: value
});
}
},
async getSetting(username, key, defaultValue = null) {
const setting = await this.db.settings
.where('[username+key]')
.equals([username, key])
.first();
return setting ? setting.value : defaultValue;
},
async clearVerifiedPhotos(username = null) {
if (username) {
return await this.db.photos
.where('[username+status]')
.equals([username, 'verified'])
.delete();
}
return await this.db.photos
.where('status').equals('verified')
.delete();
}
};
// Make Storage globally available
window.Storage = Storage;