Browse Source

change file prompt to browser page based

xdw 2 months ago
parent
commit
debe5a54a3
1 changed files with 164 additions and 23 deletions
  1. 164 23
      server/index.ts

+ 164 - 23
server/index.ts

@@ -250,28 +250,166 @@ except Exception as e:
 };
 
 // ─── Linux: LibreOffice process manager ──────────────────────────────────────
-// Prompts the user to pick a presentation file via zenity (GTK file dialog),
-// launches LibreOffice with the UNO socket listener, and re-prompts when
-// the process exits.
+// Serves a web file picker at http://<host>:PORT/picker.
+// When a file is selected it POSTs to /picker/open, which launches LibreOffice.
+// Re-prompts (via broadcast) when LibreOffice exits.
 
 const UNO_PORT = 2002;
 let libreofficeProc: ReturnType<typeof Bun.spawn> | null = null;
 
-async function pickFile(): Promise<string | null> {
-  try {
-    const result = await run("zenity", [
-      "--file-selection",
-      "--title=Open Presentation",
-    ]);
-    return result || null;
-  } catch {
-    // zenity not available — fall back to terminal prompt
-    process.stdout.write("Enter path to presentation file: ");
-    for await (const line of console) {
-      return line.trim() || null;
+// Resolves when the user picks a file via the web UI
+let pickerResolve: ((path: string) => void) | null = null;
+
+function waitForFilePick(): Promise<string> {
+  return new Promise((resolve) => {
+    pickerResolve = resolve;
+  });
+}
+
+const PICKER_HTML = `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Open Presentation — PPT Remote</title>
+<style>
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
+  header { background: #16213e; padding: 16px 24px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid #0f3460; }
+  header h1 { font-size: 1.1rem; font-weight: 600; }
+  #breadcrumb { padding: 10px 24px; background: #16213e; font-size: 0.85rem; color: #888; border-bottom: 1px solid #0f3460; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
+  #breadcrumb span { cursor: pointer; color: #e94560; } #breadcrumb span:hover { text-decoration: underline; }
+  #breadcrumb .sep { color: #555; }
+  #list { padding: 12px 16px; }
+  .entry { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: background 0.15s; user-select: none; }
+  .entry:hover { background: #0f3460; }
+  .entry .icon { font-size: 1.3rem; width: 28px; text-align: center; flex-shrink: 0; }
+  .entry .name { flex: 1; font-size: 0.95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+  .entry .name.file { color: #a8d8ea; }
+  #status { position: fixed; bottom: 0; left: 0; right: 0; background: #0f3460; padding: 12px 24px; font-size: 0.9rem; display: none; }
+  #status.show { display: block; }
+  .loading { text-align: center; padding: 40px; color: #555; }
+</style>
+</head>
+<body>
+<header>
+  <span style="font-size:1.5rem">📂</span>
+  <h1>Select a Presentation — PPT Remote</h1>
+</header>
+<div id="breadcrumb"></div>
+<div id="list"><div class="loading">Loading…</div></div>
+<div id="status"></div>
+<script>
+  let cwd = '/';
+
+  async function browse(path) {
+    cwd = path;
+    document.getElementById('list').innerHTML = '<div class="loading">Loading…</div>';
+    const res = await fetch('/picker/ls?path=' + encodeURIComponent(path));
+    const { entries, error } = await res.json();
+    renderBreadcrumb(path);
+    if (error) { document.getElementById('list').innerHTML = '<div class="loading">' + error + '</div>'; return; }
+    renderList(entries);
+  }
+
+  function renderBreadcrumb(path) {
+    const parts = path.split('/').filter(Boolean);
+    let html = '<span onclick="browse(\\'/\\')">/ root</span>';
+    let acc = '';
+    for (const p of parts) {
+      acc += '/' + p;
+      const cur = acc;
+      html += '<span class="sep">›</span><span onclick="browse(\\''+cur+'\\')">'+p+'</span>';
+    }
+    document.getElementById('breadcrumb').innerHTML = html;
+  }
+
+  function renderList(entries) {
+    if (!entries.length) { document.getElementById('list').innerHTML = '<div class="loading">Empty folder</div>'; return; }
+    const el = document.getElementById('list');
+    el.innerHTML = '';
+    // parent dir
+    if (cwd !== '/') {
+      const up = document.createElement('div');
+      up.className = 'entry';
+      up.innerHTML = '<span class="icon">⬆️</span><span class="name">..</span>';
+      up.onclick = () => browse(cwd.split('/').slice(0,-1).join('/') || '/');
+      el.appendChild(up);
+    }
+    for (const e of entries) {
+      const div = document.createElement('div');
+      div.className = 'entry';
+      div.innerHTML = \`<span class="icon">\${e.dir ? '📁' : '📄'}</span><span class="name \${e.dir ? '' : 'file'}">\${e.name}</span>\`;
+      if (e.dir) div.onclick = () => browse(e.path);
+      else div.onclick = () => openFile(e.path);
+      el.appendChild(div);
+    }
+  }
+
+  async function openFile(path) {
+    const st = document.getElementById('status');
+    st.textContent = 'Opening: ' + path + ' …';
+    st.className = 'show';
+    const res = await fetch('/picker/open', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ path }) });
+    const { ok, error } = await res.json();
+    if (ok) {
+      st.textContent = '✅ Opened! You can close this tab.';
+    } else {
+      st.textContent = '❌ Error: ' + error;
     }
-    return null;
   }
+
+  browse(cwd);
+</script>
+</body>
+</html>`;
+
+async function servePickerRequest(req: Request): Promise<Response | null> {
+  const url = new URL(req.url);
+
+  if (url.pathname === "/picker") {
+    return new Response(PICKER_HTML, { headers: { "Content-Type": "text/html; charset=utf-8" } });
+  }
+
+  if (url.pathname === "/picker/ls") {
+    const dirPath = url.searchParams.get("path") || "/";
+    try {
+      const entries: { name: string; path: string; dir: boolean }[] = [];
+      const dir = await import("node:fs/promises");
+      const items = await dir.readdir(dirPath, { withFileTypes: true });
+      for (const entry of items) {
+        if (entry.name.startsWith(".")) continue;
+        entries.push({
+          name: entry.name,
+          path: `${dirPath.replace(/\/$/, "")}/${entry.name}`,
+          dir: entry.isDirectory(),
+        });
+      }
+      // dirs first, then files, both alphabetical
+      entries.sort((a, b) => {
+        if (a.dir !== b.dir) return a.dir ? -1 : 1;
+        return a.name.localeCompare(b.name);
+      });
+      return Response.json({ entries });
+    } catch (e) {
+      return Response.json({ entries: [], error: String(e) });
+    }
+  }
+
+  if (url.pathname === "/picker/open" && req.method === "POST") {
+    const { path } = await req.json() as { path: string };
+    if (!path) return Response.json({ ok: false, error: "No path provided" });
+    if (pickerResolve) {
+      pickerResolve(path);
+      pickerResolve = null;
+      return Response.json({ ok: true });
+    }
+    // No active picker waiting — launch directly (e.g. re-open after close)
+    launchLibreOffice(path).catch(console.error);
+    return Response.json({ ok: true });
+  }
+
+  return null; // not a picker route
 }
 
 async function launchLibreOffice(filePath: string): Promise<void> {
@@ -304,12 +442,9 @@ async function launchLibreOffice(filePath: string): Promise<void> {
 }
 
 async function promptAndLaunch(): Promise<void> {
-  const file = await pickFile();
-  if (!file) {
-    console.log("No file selected. Waiting... (clients will be notified when a file is opened)");
-    broadcast({ event: "presentation", status: "none" });
-    return;
-  }
+  console.log(`Open a presentation at http://localhost:${PORT}/picker`);
+  broadcast({ event: "presentation", status: "none", pickerUrl: `/picker` });
+  const file = await waitForFilePick();
   await launchLibreOffice(file);
 }
 
@@ -349,10 +484,16 @@ const server = Bun.serve({
   port: PORT,
 
   fetch(req, server) {
+    // Linux file picker routes
+    if (IS_LINUX && req.url.includes("/picker")) {
+      return servePickerRequest(req).then(r => r ?? new Response("Not found", { status: 404 }));
+    }
+
     if (!req.headers.get("upgrade")) {
+      const pickerNote = IS_LINUX ? `\nFile picker: http://<host>:${PORT}/picker` : "";
       return new Response(
         `PPT Remote Server — ${IS_LINUX ? "LibreOffice" : "PowerPoint"} mode\n` +
-        `WebSocket: ws://<host>:${PORT}`,
+        `WebSocket: ws://<host>:${PORT}` + pickerNote,
         { headers: { "Content-Type": "text/plain" } }
       );
     }