OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli commits / 88bc235

web-ui: local folder picker so folder-sharing works from the browser
Daemon adds GET /fs/list?path= (lists the daemon's own fs — LOCAL-only, so your directories). bridge-shim.js browseShareFolder/browseDownloadFolder now open a folder-picker modal that navigates via /fs/list and resolves the chosen absolute path (matching browseShareFolder(): Promise<string|null>), replacing Electron's native dialog. File drag-drop (upload endpoint) still deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88bc2358c645a6d0b4d0a273cf11c3288edd9b59
by plankatron <fleeky@gmail.com> · 2026-07-22T00:40:41.000Z

Changed files (2) +60 -2

bin/mirall.js +17 -0
……@@ -177,6 +177,23 @@ async function runDaemon () {
177177 req.on('end', () => { try { host.sc.write(b4a.concat(parts)) } catch {} res.writeHead(204); res.end() })
178178 return
179179 }
180 if (url === '/fs/list') { // local folder-picker (LOCAL-ONLY = your own fs)
181 const qs = (req.url || '').split('?')[1] || ''
182 const m = qs.match(/(?:^|&)path=([^&]*)/)
183 let dir
184 try { dir = path.resolve(m ? decodeURIComponent(m[1]) : os.homedir()) } catch { dir = os.homedir() }
185 let entries = []
186 try {
187 entries = fs.readdirSync(dir)
188 .filter((n) => !n.startsWith('.'))
189 .map((n) => { let d = false; try { d = fs.statSync(path.join(dir, n)).isDirectory() } catch {} return { name: n, dir: d } })
190 .sort((a, b) => (Number(b.dir) - Number(a.dir)) || a.name.localeCompare(b.name))
191 } catch {}
192 const parent = path.dirname(dir)
193 res.writeHead(200, { 'content-type': 'application/json' })
194 res.end(JSON.stringify({ path: dir, parent: parent === dir ? null : parent, entries }))
195 return
196 }
180197 const file = path.join(WEB, url === '/' ? 'index.html' : url)
181198 if (!file.startsWith(WEB)) { res.writeHead(403); res.end(); return }
182199 try {
web/bridge-shim.js +43 -2
……@@ -20,6 +20,47 @@
2020 var cfg = function () { try { return JSON.parse(localStorage.getItem('mirall:config') || '{}') } catch (e) { return {} } }
2121 var setCfg = function (patch) { var c = Object.assign(cfg(), patch); localStorage.setItem('mirall:config', JSON.stringify(c)); return c }
2222
23 // Folder picker: replaces Electron's native dialog. Browses the DAEMON's filesystem via
24 // /fs/list — and since the daemon is LOCAL, that's your own machine. Resolves the chosen
25 // absolute path (string), or null on cancel. Matches browseShareFolder(): Promise<string|null>.
26 function pickFolder () {
27 return new Promise(function (resolve) {
28 var cur = null
29 var ov = document.createElement('div')
30 ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2147483647;display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif'
31 var box = document.createElement('div')
32 box.style.cssText = 'background:#fff;color:#111;width:min(560px,92vw);max-height:78vh;border-radius:12px;box-shadow:0 12px 48px rgba(0,0,0,.4);display:flex;flex-direction:column;overflow:hidden'
33 if (document.documentElement.classList.contains('dark')) { box.style.background = '#1b1b1f'; box.style.color = '#eee' }
34 var title = document.createElement('div'); title.textContent = 'Choose a folder'; title.style.cssText = 'padding:14px 16px;font-weight:600;border-bottom:1px solid rgba(128,128,128,.25)'
35 var pathEl = document.createElement('div'); pathEl.style.cssText = 'padding:8px 16px;font-size:13px;opacity:.75;word-break:break-all'
36 var list = document.createElement('div'); list.style.cssText = 'overflow:auto;flex:1;min-height:140px;border-top:1px solid rgba(128,128,128,.15)'
37 var footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;padding:12px 16px;border-top:1px solid rgba(128,128,128,.25)'
38 var cancel = document.createElement('button'); cancel.textContent = 'Cancel'; cancel.style.cssText = 'padding:8px 14px;border-radius:8px;border:1px solid rgba(128,128,128,.4);background:transparent;color:inherit;cursor:pointer'
39 var sel = document.createElement('button'); sel.textContent = 'Select this folder'; sel.style.cssText = 'padding:8px 14px;border-radius:8px;border:0;background:#2563eb;color:#fff;cursor:pointer'
40 footer.appendChild(cancel); footer.appendChild(sel)
41 box.appendChild(title); box.appendChild(pathEl); box.appendChild(list); box.appendChild(footer); ov.appendChild(box); document.body.appendChild(ov)
42 function done (v) { try { document.body.removeChild(ov) } catch (e) {} resolve(v) }
43 cancel.onclick = function () { done(null) }
44 sel.onclick = function () { done(cur) }
45 ov.onclick = function (e) { if (e.target === ov) done(null) }
46 function row (label, isDir, onClick) {
47 var r = document.createElement('div')
48 r.textContent = (isDir ? '📁 ' : '📄 ') + label
49 r.style.cssText = 'padding:9px 16px;font-size:14px;' + (isDir ? 'cursor:pointer;' : 'opacity:.4;')
50 if (onClick) { r.onmouseenter = function () { r.style.background = 'rgba(128,128,128,.15)' }; r.onmouseleave = function () { r.style.background = '' }; r.onclick = onClick }
51 list.appendChild(r)
52 }
53 function load (p) {
54 fetch('/fs/list' + (p ? '?path=' + encodeURIComponent(p) : '')).then(function (r) { return r.json() }).then(function (d) {
55 cur = d.path; pathEl.textContent = d.path; list.innerHTML = ''
56 if (d.parent) row('.. (up)', true, function () { load(d.parent) })
57 d.entries.forEach(function (e) { row(e.name, e.dir, e.dir ? function () { load(d.path + '/' + e.name) } : null) })
58 }).catch(function () { pathEl.textContent = '(cannot read this folder)' })
59 }
60 load(null)
61 })
62 }
63
2364 window.bridge = {
2465 // ── LOAD-BEARING: the raw NDJSON worker pipe ──
2566 startWorker: function () { return Promise.resolve() }, // daemon already runs the worker
……@@ -57,8 +98,8 @@
5798 // ── downloads / folders (LOCAL machine — fs-browse picker replaces these, see notes) ──
5899 getDownloadFolder: function () { return Promise.resolve(cfg().downloadFolder || '') },
59100 setDownloadFolder: function (f) { return Promise.resolve(setCfg({ downloadFolder: f })) },
60 browseDownloadFolder: function () { return Promise.resolve(null) }, // TODO fs:list-dir picker
61 browseShareFolder: function () { return Promise.resolve(null) }, // TODO fs:list-dir picker
101 browseDownloadFolder: function () { return pickFolder() },
102 browseShareFolder: function () { return pickFolder() },
62103 startOwnedFolderWatcher: function () { return Promise.resolve() }, stopOwnedFolderWatcher: function () { return Promise.resolve() },
63104
64105 // ── notifications → browser Notification API ──