Browse Source

File picker improvements on linux

xdw 2 months ago
parent
commit
a4c499c228
1 changed files with 66 additions and 22 deletions
  1. 66 22
      server/index.ts

+ 66 - 22
server/index.ts

@@ -274,34 +274,71 @@ const PICKER_HTML = `<!DOCTYPE html>
 <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; }
+  body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; display: flex; flex-direction: column; }
+  header { background: #16213e; padding: 12px 16px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #0f3460; flex-shrink: 0; }
+  header h1 { font-size: 1rem; font-weight: 600; flex: 1; }
+  header button { background: #0f3460; border: none; color: #e0e0e0; border-radius: 6px; padding: 6px 12px; cursor: pointer; font-size: 0.85rem; display: flex; align-items: center; gap: 5px; }
+  header button:hover { background: #e94560; }
+  #breadcrumb { padding: 8px 16px; background: #16213e; font-size: 0.82rem; color: #888; border-bottom: 1px solid #0f3460; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; flex-shrink: 0; }
+  #breadcrumb .crumb { cursor: pointer; color: #e94560; } #breadcrumb .crumb:hover { text-decoration: underline; }
+  #breadcrumb .sep { color: #444; }
+
+  /* Drop zone */
+  #dropzone { margin: 12px 16px; border: 2px dashed #0f3460; border-radius: 12px; padding: 20px; text-align: center; color: #555; font-size: 0.9rem; transition: border-color 0.2s, background 0.2s; flex-shrink: 0; }
+  #dropzone.drag-over { border-color: #e94560; background: rgba(233,69,96,0.08); color: #e0e0e0; }
+  #dropzone span { font-size: 1.6rem; display: block; margin-bottom: 6px; }
+
+  #list { padding: 8px 16px 80px; flex: 1; overflow-y: auto; }
+  .entry { display: flex; align-items: center; gap: 10px; padding: 9px 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 .icon { font-size: 1.2rem; width: 26px; text-align: center; flex-shrink: 0; }
+  .entry .name { flex: 1; font-size: 0.92rem; 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 { position: fixed; bottom: 0; left: 0; right: 0; background: #0f3460; padding: 12px 20px; font-size: 0.9rem; display: none; border-top: 1px solid #e94560; }
   #status.show { display: block; }
   .loading { text-align: center; padding: 40px; color: #555; }
 </style>
 </head>
 <body>
 <header>
-  <span style="font-size:1.5rem">๐Ÿ“‚</span>
+  <span style="font-size:1.4rem">๐Ÿ“‚</span>
   <h1>Select a Presentation โ€” PPT Remote</h1>
+  <button onclick="browse(cwd)" title="Reload current folder">๐Ÿ”„ Reload</button>
+  <button onclick="browse(HOME)" title="Go to home directory">๐Ÿ  Home</button>
 </header>
 <div id="breadcrumb"></div>
+
+<div id="dropzone" id="dropzone">
+  <span>โฌ†๏ธ</span>
+  Drag &amp; drop a presentation file here to open it
+</div>
+
 <div id="list"><div class="loading">Loadingโ€ฆ</div></div>
 <div id="status"></div>
+
 <script>
-  let cwd = '/';
+  const HOME = '__HOME__';
+  let cwd = HOME;
+
+  // โ”€โ”€ Drag & drop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
+  const dz = document.getElementById('dropzone');
+
+  document.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag-over'); });
+  document.addEventListener('dragleave', e => { if (e.relatedTarget === null) dz.classList.remove('drag-over'); });
+  document.addEventListener('drop', async e => {
+    e.preventDefault();
+    dz.classList.remove('drag-over');
+    const files = e.dataTransfer.files;
+    if (!files.length) return;
+    // DataTransfer gives us File objects but not full paths in browsers.
+    // We use the webkitRelativePath or name and resolve against cwd.
+    const file = files[0];
+    // Try to get the full path via the non-standard .path property (Electron/some browsers)
+    const fullPath = file.path || (cwd.replace(/\\/$/, '') + '/' + file.name);
+    await openFile(fullPath);
+  });
 
+  // โ”€โ”€ Browsing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
   async function browse(path) {
     cwd = path;
     document.getElementById('list').innerHTML = '<div class="loading">Loadingโ€ฆ</div>';
@@ -314,12 +351,12 @@ const PICKER_HTML = `<!DOCTYPE html>
 
   function renderBreadcrumb(path) {
     const parts = path.split('/').filter(Boolean);
-    let html = '<span onclick="browse(\\'/\\')">/ root</span>';
+    let html = '<span class="crumb" 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>';
+      html += '<span class="sep">โ€บ</span><span class="crumb" onclick="browse(\\''+cur+'\\')">'+p+'</span>';
     }
     document.getElementById('breadcrumb').innerHTML = html;
   }
@@ -328,7 +365,6 @@ const PICKER_HTML = `<!DOCTYPE html>
     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';
@@ -346,16 +382,21 @@ const PICKER_HTML = `<!DOCTYPE html>
     }
   }
 
+  // โ”€โ”€ Open file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
   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) {
+    const res = await fetch('/picker/open', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ path })
+    });
+    const data = await res.json();
+    if (data.ok) {
       st.textContent = 'โœ… Opened! You can close this tab.';
     } else {
-      st.textContent = 'โŒ Error: ' + error;
+      st.textContent = 'โŒ Error: ' + data.error;
     }
   }
 
@@ -368,11 +409,14 @@ 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" } });
+    const home = process.env.HOME || "/root";
+    const html = PICKER_HTML.replace("'__HOME__'", JSON.stringify(home));
+    return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
   }
 
   if (url.pathname === "/picker/ls") {
-    const dirPath = url.searchParams.get("path") || "/";
+    const home = process.env.HOME || "/root";
+    const dirPath = url.searchParams.get("path") || home;
     try {
       const entries: { name: string; path: string; dir: boolean }[] = [];
       const dir = await import("node:fs/promises");