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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user