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
+136
View File
@@ -95,6 +95,29 @@ document.addEventListener('DOMContentLoaded', async () => {
setSyncStatus('', 'neutral');
});
// --- File-based auto-sync ---
await initFileSync();
$('#btnPickSyncFolder').addEventListener('click', pickSyncFolder);
$('#btnClearSyncFolder').addEventListener('click', async () => {
await SilentSendSync.clearSyncDirHandle();
syncDirHandle = null;
updateFileSyncUI();
setFileSyncStatus('Sync folder cleared.', 'neutral');
});
// Auto-write when local storage changes (catches popup edits, page-world adds, etc.)
api.storage.onChanged.addListener(async (changes, area) => {
if (area === 'local' && (changes.ss_settings || changes.ss_mappings || changes.ss_identity)) {
await writeToSyncFile();
}
});
// Re-check sync file whenever the options page regains focus
window.addEventListener('focus', async () => {
await checkFileSyncUpdate();
});
// Transfer data
$('#btnExportAll').addEventListener('click', exportAllPlain);
$('#btnExportEncrypted').addEventListener('click', exportAllEncrypted);
@@ -465,6 +488,119 @@ async function importAll(e) {
e.target.value = '';
}
// ----------------------------------------------------------------
// File-based auto-sync (File System Access API)
// ----------------------------------------------------------------
let syncDirHandle = null;
const SYNC_FILE_NAME = 'silent-send-sync.json';
async function initFileSync() {
syncDirHandle = await SilentSendSync.loadSyncDirHandle();
updateFileSyncUI();
if (syncDirHandle) {
await checkFileSyncUpdate();
}
}
async function pickSyncFolder() {
if (!window.showDirectoryPicker) {
setFileSyncStatus('Your browser does not support the File System Access API.', 'error');
return;
}
try {
const handle = await window.showDirectoryPicker({ mode: 'readwrite', id: 'ss-sync' });
syncDirHandle = handle;
await SilentSendSync.saveSyncDirHandle(handle);
updateFileSyncUI();
// Write current settings immediately so the file exists for the other browser
await writeToSyncFile();
setFileSyncStatus('Sync folder set. Settings will sync automatically.', 'ok');
} catch (e) {
if (e.name !== 'AbortError') {
setFileSyncStatus('Could not set sync folder: ' + e.message, 'error');
}
}
}
async function writeToSyncFile() {
if (!syncDirHandle) return;
try {
// Re-verify permission is still granted (required after browser restart)
const perm = await syncDirHandle.requestPermission({ mode: 'readwrite' });
if (perm !== 'granted') return;
const data = await SilentSendSync._getAllData();
const fileHandle = await syncDirHandle.getFileHandle(SYNC_FILE_NAME, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
} catch (e) {
// Permission denied or folder removed — don't spam errors
console.warn('[Silent Send] writeToSyncFile failed:', e.message);
}
}
async function checkFileSyncUpdate() {
if (!syncDirHandle) return;
try {
const perm = await syncDirHandle.queryPermission({ mode: 'readwrite' });
if (perm === 'prompt') {
// Need a user gesture to re-request — skip silently
return;
}
if (perm !== 'granted') return;
const fileHandle = await syncDirHandle.getFileHandle(SYNC_FILE_NAME);
const file = await fileHandle.getFile();
const data = JSON.parse(await file.text());
if (!data.version || !data.lastModified) return;
const local = await SilentSendSync._getAllData();
if (data.lastModified > (local.lastModified || 0)) {
await SilentSendSync._applyData(data);
mappings = await Storage.getMappings();
settings = await Storage.getSettings();
$('#browserSync').checked = settings.browserSync === true;
renderMappings();
renderDomains();
renderLog();
setFileSyncStatus(
'Auto-synced from folder (' + new Date(data.lastModified).toLocaleString() + ').',
'ok'
);
}
} catch (e) {
if (e.name !== 'NotFoundError') {
console.warn('[Silent Send] checkFileSyncUpdate failed:', e.message);
}
}
}
function updateFileSyncUI() {
const nameEl = $('#syncFolderName');
const clearBtn = $('#btnClearSyncFolder');
const pickBtn = $('#btnPickSyncFolder');
if (!nameEl) return;
if (syncDirHandle) {
nameEl.textContent = syncDirHandle.name + '/';
clearBtn.style.display = '';
pickBtn.textContent = 'Change Folder';
} else {
nameEl.textContent = '';
clearBtn.style.display = 'none';
pickBtn.textContent = 'Choose Sync Folder';
}
}
function setFileSyncStatus(msg, type) {
const el = $('#fileSyncStatus');
if (!el) return;
el.textContent = msg;
el.style.color = type === 'ok' ? '#10b981' : type === 'warn' ? '#f59e0b' : type === 'error' ? '#dc2626' : '#6b7280';
}
function setSyncStatus(msg, type) {
const el = $('#syncStatus');
if (!el) return;