fix: PPI + button replaces text immediately; add auto-sync folder

PPI + button fix:
- After adding a mapping, the real PPI value is now immediately replaced
  with the fake value in the input/contenteditable element via the new
  replaceInInput() helper (handles both <textarea>/<input> and
  contenteditable divs by walking text nodes).
- Triggers a re-scan 150ms later so the pre-send warning updates or
  dismisses itself if no more PPI remains.

Auto-sync folder (File System Access API):
- User picks a folder once per browser via "Choose Sync Folder".
  Any settings/mappings/identity change writes silent-send-sync.json
  to that folder automatically (via api.storage.onChanged listener).
- On options page open and every time the page regains focus, the file
  is read back; if its lastModified is newer than local data, settings
  are imported immediately and the UI refreshes.
- File handle is stored in IndexedDB (ss_sync_handles) so it persists
  across browser sessions without repeated permission prompts.
- Pick the SAME folder in each browser (or a synced cloud folder for
  cross-computer sync) — fully automatic after that, no copy-paste.
- sync.js gains saveSyncDirHandle / loadSyncDirHandle / clearSyncDirHandle
  helpers backed by IndexedDB.

https://claude.ai/code/session_01TKpSR9M8JgHLXCp5CeDsQP
This commit is contained in:
Claude
2026-03-26 14:32:53 +00:00
parent 84f6e675db
commit f05375c2ec
4 changed files with 238 additions and 1 deletions
+55
View File
@@ -171,6 +171,61 @@ const SilentSendSync = {
return [];
}
},
// ----------------------------------------------------------------
// File System Access API helpers — folder-based sync
// The directory handle is stored in IndexedDB so the user only
// needs to grant access once per browser session.
// ----------------------------------------------------------------
_dbPromise: null,
_openDB() {
if (this._dbPromise) return this._dbPromise;
this._dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open('ss_sync_handles', 1);
req.onupgradeneeded = () => req.result.createObjectStore('handles');
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
return this._dbPromise;
},
async saveSyncDirHandle(handle) {
const db = await this._openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction('handles', 'readwrite');
tx.objectStore('handles').put(handle, 'syncDir');
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
},
async loadSyncDirHandle() {
try {
const db = await this._openDB();
return new Promise((resolve) => {
const tx = db.transaction('handles', 'readonly');
const req = tx.objectStore('handles').get('syncDir');
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => resolve(null);
});
} catch {
return null;
}
},
async clearSyncDirHandle() {
try {
const db = await this._openDB();
return new Promise((resolve) => {
const tx = db.transaction('handles', 'readwrite');
tx.objectStore('handles').delete('syncDir');
tx.oncomplete = resolve;
tx.onerror = resolve;
});
} catch { /* ignore */ }
},
};
export default SilentSendSync;