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
+30 -1
View File
@@ -1134,9 +1134,17 @@
});
await setStorageData('ss_mappings', currentMappings);
// Update local mappings
// Update local mappings so the fetch interceptor uses them immediately
mappings = currentMappings;
// Replace the PPI value in the current input right now
if (inputEl) {
replaceInInput(inputEl, real, fake);
// Re-scan — will dismiss warning if no more PPI remains
if (inputScanTimer) clearTimeout(inputScanTimer);
inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150);
}
// Visual feedback
btn.textContent = '\u2714';
btn.style.color = '#4ade80';
@@ -1145,6 +1153,27 @@
});
}
// Replace all occurrences of `real` with `fake` in an input or contenteditable element
function replaceInInput(el, real, fake) {
if (!el) return;
const re = new RegExp(real.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
if (el.value !== undefined) {
// <textarea> or <input>
el.value = el.value.replace(re, fake);
} else if (el.isContentEditable) {
// contenteditable div — walk text nodes to avoid breaking inner HTML
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
re.lastIndex = 0;
if (re.test(node.textContent)) {
re.lastIndex = 0;
node.textContent = node.textContent.replace(re, fake);
}
}
}
}
// Storage helpers for page world (uses postMessage to injector)
function getStorageData(key) {
return new Promise(resolve => {