|
|
@@ -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 & 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");
|