diff --git a/README.md b/README.md index b552a7c..b3234e0 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ PORT=8090 ./build/immich-sync Open `http://localhost:8090`. +After both share URLs are valid, starting a job updates the browser fragment in the form +`#left=&right=`. Reloading or bookmarking that URL restores both fields and +automatically starts a new inspection. Passwords are intentionally excluded from the fragment and must be entered +again for protected shares. + ## WebSocket protocol All messages are JSON. Each WebSocket accepts exactly one complete job request and closes after its terminal response. A client that does not send a request within 10 seconds is disconnected. Passwords are accepted only in client messages and are never placed in URLs or echoed in status responses. diff --git a/web/app.js b/web/app.js index a606e52..49f38ca 100644 --- a/web/app.js +++ b/web/app.js @@ -21,6 +21,43 @@ let jobCompleted = false; let abortRequested = false; let syncRunning = false; +function normalizedShareUrl(value) { + try { + const url = new URL(value.trim()); + if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password) return undefined; + const marker = url.pathname.lastIndexOf('/share/'); + if (marker < 0) return undefined; + const credential = url.pathname.slice(marker + 7).replace(/\/+$/, ''); + if (!credential || credential.includes('/')) return undefined; + url.search = ''; + url.hash = ''; + return url.toString(); + } catch { + return undefined; + } +} + +function updateShareFragment() { + const left = normalizedShareUrl($('#leftUrl').value); + const right = normalizedShareUrl($('#rightUrl').value); + if (!left || !right) { + if (location.hash) history.replaceState(null, '', `${location.pathname}${location.search}`); + return; + } + const fragment = new URLSearchParams({ left, right }); + history.replaceState(null, '', `${location.pathname}${location.search}#${fragment}`); +} + +function restoreShareFragment() { + const fragment = new URLSearchParams(location.hash.slice(1)); + const left = normalizedShareUrl(fragment.get('left') || ''); + const right = normalizedShareUrl(fragment.get('right') || ''); + if (!left || !right) return false; + $('#leftUrl').value = left; + $('#rightUrl').value = right; + return true; +} + function openJob(request) { if (socket && socket.readyState !== WebSocket.CLOSED) { ui.message.textContent = 'Please wait for the current job to finish.'; @@ -34,6 +71,7 @@ function openJob(request) { jobCompleted = false; abortRequested = false; ui.inspect.disabled = true; + updateShareFragment(); setConnection('connecting', 'Connecting'); jobSocket.addEventListener('open', () => { @@ -242,6 +280,7 @@ function handle(message) { } ui.inspect.addEventListener('click', inspect); +for (const input of [$('#leftUrl'), $('#rightUrl')]) input.addEventListener('input', updateShareFragment); for (const input of $$('input')) input.addEventListener('keydown', (event) => { if (event.key === 'Enter') inspect(); }); @@ -254,3 +293,4 @@ ui.abort.addEventListener('click', () => { }); setConnection('idle', 'Idle'); +if (restoreShareFragment()) inspect();