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>
173 lines
5.3 KiB
JavaScript
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;
|