<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cipher — 私人网盘</title>
  <meta name="description" content="Cipher — 安全、私密的个人文件空间">
  <meta name="theme-color" content="#121212">
  <meta property="og:type" content="website">
  <meta property="og:site_name" content="Cipher">
  <meta property="og:title" content="Cipher — 私人网盘">
  <meta property="og:description" content="安全、私密的个人文件空间">
  <meta property="og:url" content="https://edge.oooops.site/">
  <meta property="og:image" content="https://edge.oooops.site/og-card.svg">
  <meta property="og:image:type" content="image/svg+xml">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:title" content="Cipher — 私人网盘">
  <meta name="twitter:description" content="安全、私密的个人文件空间">
  <meta name="twitter:image" content="https://edge.oooops.site/og-card.svg">
  <link rel="canonical" href="https://edge.oooops.site/">
  <link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTQiIGZpbGw9IiMxZTFlMWUiLz48dGV4dCB4PSIzMiIgeT0iNDMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM5MGNhZjkiIGZvbnQtZmFtaWx5PSJzZXJpZiIgZm9udC1zaXplPSIzOCIgZm9udC13ZWlnaHQ9IjcwMCI+JiN4MjVDOGI7PC90ZXh0Pjwvc3ZnPg==">
  <style>
:root {
  --bg: #121212;
  --panel: #1e1e1e;
  --surface: rgba(255,255,255,.04);
  --line: rgba(255,255,255,.08);
  --text: #f0f0f0;
  --muted: #808080;
  --accent: #90caf9;
  --danger: #ef5350;
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--text);
  background: var(--bg);
  min-height: 100vh;
}

.login-view {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.login-split {
  display: flex;
  width: 680px;
  max-width: 95vw;
  min-height: 380px;
  background: var(--panel);
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--line);
}

.login-brand {
  width: 45%;
  background: linear-gradient(135deg, #1a1a2e, #16213e);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 40px 24px;
  text-align: center;
}

.login-logo { font-size: 48px; color: var(--accent); }
.login-brand h1 { margin: 0; font-size: 22px; font-family: system-ui, -apple-system, sans-serif; letter-spacing: .08em; }
.login-brand p { font-size: 12px; color: var(--muted); line-height: 1.6; margin: 0; }

.login-form-side {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 40px 32px;
}

.login-form-side h2 { margin: 0; font-size: 18px; }

#login-form {
  display: flex;
  gap: 8px;
  width: 100%;
  max-width: 260px;
}

#login-form input {
  flex: 1;
  background: var(--bg);
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 8px;
  padding: 10px 14px;
  color: var(--text);
  font-size: 14px;
  outline: none;
  font-family: var(--font-mono);
}

#login-form input:focus { border-color: var(--accent); }

#login-form button {
  background: var(--accent);
  color: var(--bg);
  border: none;
  border-radius: 8px;
  padding: 10px 18px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

#login-form button:hover { opacity: .85; }

.login-error {
  color: var(--danger);
  font-size: 13px;
  margin: 0;
  min-height: 20px;
}

.topbar {
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  padding: 0 24px;
  height: 56px;
  display: flex;
  align-items: center;
  gap: 16px;
}

.topbar-left {
  display: flex;
  align-items: center;
  gap: 16px;
  flex: 1;
}

.brand {
  font-family: var(--font-mono);
  font-size: 14px;
  letter-spacing: .08em;
  white-space: nowrap;
}

.breadcrumb { font-size: 13px; color: var(--muted); }
.breadcrumb a { color: var(--accent); text-decoration: none; cursor: pointer; }
.breadcrumb a:hover { text-decoration: underline; }

.topbar-right {
  display: flex;
  align-items: center;
  gap: 8px;
}

.usage-text {
  font-size: 11px;
  color: var(--muted);
  font-family: var(--font-mono);
  margin-right: 8px;
}

.btn-action {
  background: var(--surface);
  color: var(--text);
  border: 1px solid rgba(255,255,255,.1);
  border-radius: 8px;
  padding: 7px 14px;
  font-size: 13px;
  cursor: pointer;
  font-family: var(--font-sans);
  white-space: nowrap;
}

.btn-action:hover { background: rgba(144,202,249,.12); border-color: rgba(144,202,249,.24); }
.btn-logout { color: var(--muted); }

.error-banner {
  background: rgba(239,83,80,.12);
  color: var(--danger);
  padding: 10px 24px;
  font-size: 13px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid rgba(239,83,80,.2);
}

.error-banner button {
  background: none;
  border: none;
  color: var(--danger);
  cursor: pointer;
  font-size: 16px;
}

.progress-bar {
  height: 3px;
  background: var(--surface);
}

.progress-fill {
  height: 100%;
  background: var(--accent);
  transition: width .2s;
}

.file-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.file-table th {
  text-align: left;
  padding: 10px 24px;
  color: var(--accent);
  font-weight: 500;
  font-size: 12px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.file-table th.sortable {
  cursor: pointer;
  user-select: none;
}

.file-table th.sortable:hover { color: var(--text); }

.sort-arrow { font-size: 10px; }

.file-table td {
  padding: 10px 24px;
  border-bottom: 1px solid rgba(255,255,255,.04);
  color: var(--muted);
}

.file-table tbody tr:hover { background: rgba(255,255,255,.02); }

.file-table .file-name {
  color: var(--text);
  cursor: pointer;
}

.file-table .file-name:hover { color: var(--accent); }

.file-size {
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: 12px;
}

.file-type {
  font-family: var(--font-mono);
  font-size: 11px;
}

.row-action {
  background: none;
  border: 1px solid rgba(255,255,255,.08);
  border-radius: 6px;
  color: var(--muted);
  cursor: pointer;
  padding: 4px 10px;
  font-size: 12px;
  font-family: var(--font-sans);
}

.row-action:hover { background: var(--surface); color: var(--text); }
.row-action.danger:hover { color: var(--danger); border-color: rgba(239,83,80,.3); }

.row-action + .row-action { margin-left: 4px; }

.empty-state, .loading {
  text-align: center;
  padding: 80px 24px;
  color: var(--muted);
  font-size: 14px;
}

.dialog-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.dialog-box {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 24px;
  width: 340px;
  max-width: 90vw;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.dialog-box h3 { margin: 0; font-size: 16px; }

.dialog-box input {
  background: var(--bg);
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 8px;
  padding: 10px 14px;
  color: var(--text);
  font-size: 14px;
  outline: none;
}

.dialog-box input:focus { border-color: var(--accent); }

.dialog-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

.dialog-actions button {
  padding: 8px 18px;
  border-radius: 8px;
  font-size: 13px;
  cursor: pointer;
  border: none;
  font-family: var(--font-sans);
}

.dialog-actions .btn-cancel {
  background: var(--surface);
  color: var(--text);
  border: 1px solid rgba(255,255,255,.1);
}

.dialog-actions .btn-confirm {
  background: var(--accent);
  color: var(--bg);
  font-weight: 600;
}

.dialog-box .danger-text { color: var(--danger); font-size: 13px; }

.btn-confirm-danger {
  background: var(--danger) !important;
  color: #fff !important;
  font-weight: 600;
}

/* preview */
.preview-overlay { z-index: 200; }

.preview-box {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  max-width: 95vw;
  max-height: 95vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.preview-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 20px;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}

.preview-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 22px;
  cursor: pointer;
  line-height: 1;
}

.preview-close:hover { color: var(--text); }

.preview-body {
  flex: 1;
  overflow: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 200px;
}

.preview-body img { border-radius: 4px; }

.preview-text {
  width: 100%;
  max-width: 900px;
  min-height: 300px;
  max-height: 70vh;
  overflow: auto;
  padding: 20px;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-all;
  border-radius: 8px;
}

.preview-loading {
  color: var(--muted);
  padding: 40px;
  font-size: 14px;
}

.preview-media-wrap {
  text-align: center;
  padding: 20px;
}

.preview-media-wrap h3 {
  margin: 0 0 16px;
  font-size: 14px;
  color: var(--muted);
}

.preview-media-wrap audio,
.preview-media-wrap video {
  border-radius: 8px;
  outline: none;
}

.preview-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  color: var(--muted);
}

@media (max-width: 640px) {
  .login-split { flex-direction: column; min-height: auto; }
  .login-brand { width: 100%; padding: 32px 24px; gap: 8px; }
  .login-form-side { padding: 24px 20px; }

  .topbar {
    flex-direction: column;
    height: auto;
    padding: 12px 16px;
    gap: 8px;
    align-items: stretch;
  }

  .topbar-left { flex-direction: column; gap: 4px; }
  .topbar-right { flex-wrap: wrap; }

  .file-table th:nth-child(3),
  .file-table td:nth-child(3),
  .file-table th:nth-child(4),
  .file-table td:nth-child(4) { display: none; }

  .file-table th, .file-table td { padding: 8px 12px; }
}
  </style>
</head>
<body>

<main id="view-login" class="login-view">
  <div class="login-split">
    <div class="login-brand">
      <div class="login-logo">&#x25C8;</div>
      <h1>Cipher</h1>
      <p>安全、私密<br>您的个人文件空间</p>
    </div>
    <div class="login-form-side">
      <h2>登录</h2>
      <form id="login-form">
        <input id="login-password" type="password" placeholder="输入密码" required autofocus>
        <button type="submit">登录</button>
      </form>
      <p id="login-error" class="login-error"></p>
    </div>
  </div>
</main>

<main id="view-files" class="files-view" style="display:none">
  <header class="topbar">
    <div class="topbar-left">
      <span class="brand">&#x25C8; Cipher</span>
      <nav id="breadcrumb" class="breadcrumb"></nav>
    </div>
    <div class="topbar-right">
      <span id="usage-display" class="usage-text"></span>
      <button id="btn-upload" class="btn-action">&#x2B06; 上传</button>
      <button id="btn-mkdir" class="btn-action">&#x1F4C1; 新建</button>
      <button id="btn-logout" class="btn-action btn-logout">退出</button>
    </div>
    <input type="file" id="upload-input" style="display:none">
  </header>
  <div id="error-banner" class="error-banner" style="display:none"></div>
  <div id="progress-bar" class="progress-bar" style="display:none">
    <div class="progress-fill"></div>
  </div>
  <div id="content-area">
    <div id="loading-indicator" class="loading">加载中...</div>
    <table id="file-table" class="file-table" style="display:none">
      <thead>
        <tr>
          <th data-sort="name" class="sortable">名称 <span class="sort-arrow">&#x25B2;</span></th>
          <th data-sort="size" class="sortable">大小</th>
          <th data-sort="mod_time" class="sortable">修改时间</th>
          <th data-sort="type">类型</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody id="file-tbody"></tbody>
    </table>
    <div id="empty-state" class="empty-state" style="display:none">
      &#x1F4C2; 此文件夹为空，上传您的第一个文件
    </div>
  </div>
</main>

<script>
(() => {
  const $ = (s) => document.querySelector(s);
  const $$ = (s) => document.querySelectorAll(s);

  let currentPath = '/';
  let currentSort = 'name';
  let currentSortDir = 'asc';
  let entriesCache = [];

  function route() {
    const hash = location.hash || '#/login';
    if (hash === '#/login') {
      showLogin();
    } else {
      if (hash.startsWith('#/files')) {
        const p = hash.slice('#/files'.length) || '/';
        currentPath = decodeURIComponent(p);
        showFiles();
      } else {
        location.hash = '#/files/';
      }
    }
  }

  window.addEventListener('hashchange', route);

  function showLogin() {
    $('#view-login').style.display = 'flex';
    $('#view-files').style.display = 'none';
    $('#login-error').textContent = '';
  }

  $('#login-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const pw = $('#login-password').value;
    const res = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({ password: pw })
    });
    if (res.ok) {
      location.hash = '#/files/';
    } else {
      const data = await res.json();
      $('#login-error').textContent = data.error || '登录失败';
    }
  });

  async function showFiles() {
    $('#view-login').style.display = 'none';
    $('#view-files').style.display = 'block';
    showLoading(true);
    hideError();
    try {
      const [filesRes, usageRes] = await Promise.all([
        fetch(`/api/files?path=${encodeURIComponent(currentPath)}`),
        fetch('/api/usage')
      ]);
      if (filesRes.status === 401) { location.hash = '#/login'; return; }
      if (!filesRes.ok) throw new Error('加载失败');
      const data = await filesRes.json();
      entriesCache = data.entries || [];
      sortEntries();
      renderTable();
      if (usageRes.ok) {
        const u = await usageRes.json();
        $('#usage-display').textContent = `\u{1F4BE} ${formatSize(u.used)} / ${formatSize(u.total)}`;
      }
    } catch (err) {
      showError(err.message);
    } finally {
      showLoading(false);
    }
    renderBreadcrumb();
  }

  function showLoading(on) {
    $('#loading-indicator').style.display = on ? 'block' : 'none';
    if (on) {
      $('#file-table').style.display = 'none';
      $('#empty-state').style.display = 'none';
    }
  }

  function showError(msg) {
    const b = $('#error-banner');
    b.innerHTML = `<span>${msg}</span><button onclick="this.parentElement.style.display='none'">×</button>`;
    b.style.display = 'flex';
  }

  function hideError() { $('#error-banner').style.display = 'none'; }

  function renderBreadcrumb() {
    const parts = currentPath.split('/').filter(Boolean);
    let acc = '';
    let html = '<a data-nav="/">\u{1F3E0} 全部文件</a>';
    for (const p of parts) {
      acc += '/' + p;
      html += ` / <a data-nav="${acc}">${p}</a>`;
    }
    $('#breadcrumb').innerHTML = html;
    $$('#breadcrumb a').forEach(a => {
      a.addEventListener('click', () => navigateTo(a.dataset.nav));
    });
  }

  function navigateTo(p) {
    location.hash = '#/files' + encodeURIComponent(p);
  }

  function sortEntries() {
    const dir = currentSortDir === 'asc' ? 1 : -1;
    entriesCache.sort((a, b) => {
      if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
      if (currentSort === 'size') return (a.size - b.size) * dir;
      if (currentSort === 'mod_time') return a.modified.localeCompare(b.modified) * dir;
      return a.name.localeCompare(b.name) * dir;
    });
    updateSortArrows();
  }

  function updateSortArrows() {
    $$('.sort-arrow').forEach(s => s.textContent = '');
    const th = $(`.sortable[data-sort="${currentSort}"]`);
    if (th) {
      th.querySelector('.sort-arrow').textContent = currentSortDir === 'asc' ? '▲' : '▼';
    }
  }

  function renderTable() {
    if (entriesCache.length === 0) {
      $('#file-table').style.display = 'none';
      $('#empty-state').style.display = 'block';
      return;
    }
    $('#file-table').style.display = '';
    $('#empty-state').style.display = 'none';
    const tbody = $('#file-tbody');
    tbody.innerHTML = '';
    for (const e of entriesCache) {
      const tr = document.createElement('tr');
      const icon = e.type === 'dir' ? '\u{1F4C1}' : fileIcon(e.name);
      const nameCell = `<td><span class="file-name" data-nav="${h(e.name)}" data-type="${e.type}">${icon} ${h(e.name)}</span></td>`;
      const sizeCell = `<td class="file-size">${e.type === 'dir' ? '—' : formatSize(e.size)}</td>`;
      const timeCell = `<td>${formatTime(e.modified)}</td>`;
      const typeCell = `<td class="file-type">${e.type === 'dir' ? '文件夹' : ext(e.name)}</td>`;
      const actionCell = `<td>
        ${e.type === 'file' ? `<button class="row-action" data-dl="${h(e.name)}">下载</button>` : ''}
        <button class="row-action danger" data-rm="${h(e.name)}">删除</button>
      </td>`;
      tr.innerHTML = nameCell + sizeCell + timeCell + typeCell + actionCell;
      tbody.appendChild(tr);
    }

    tbody.querySelectorAll('.file-name').forEach(el => {
      el.addEventListener('click', () => {
        if (el.dataset.type === 'dir') {
          navigateTo(currentPath === '/' ? '/' + el.dataset.nav : currentPath + '/' + el.dataset.nav);
        } else {
          previewFile(el.dataset.nav);
        }
      });
    });

    tbody.querySelectorAll('[data-dl]').forEach(btn => {
      btn.addEventListener('click', (e) => {
        e.stopPropagation();
        downloadFile(btn.dataset.dl);
      });
    });

    tbody.querySelectorAll('[data-rm]').forEach(btn => {
      btn.addEventListener('click', (e) => {
        e.stopPropagation();
        confirmDelete(btn.dataset.rm, entriesCache.find(en => en.name === btn.dataset.rm)?.type);
      });
    });
  }

  function downloadFile(name) {
    const p = currentPath === '/' ? '/' + name : currentPath + '/' + name;
    window.open(`/api/download?path=${encodeURIComponent(p)}`, '_blank');
  }

  function filePath(name) {
    return currentPath === '/' ? '/' + name : currentPath + '/' + name;
  }

  const PREVIEW = {
    image: ['png','jpg','jpeg','gif','webp','svg','ico','bmp'],
    text:  ['txt','md','log','conf','cfg','ini','yaml','yml','json','xml','html','htm','css','js','ts','py','go','rs','sh','bash','zsh','toml'],
    pdf:   ['pdf'],
    audio: ['mp3','wav','flac','aac','ogg','wma'],
    video: ['mp4','webm','mov','mkv','avi']
  };

  function previewType(name) {
    const e = ext(name).toLowerCase();
    for (const [t, exts] of Object.entries(PREVIEW)) {
      if (exts.includes(e)) return t;
    }
    return null;
  }

  function previewFile(name) {
    const p = filePath(name);
    const url = `/api/preview?path=${encodeURIComponent(p)}`;
    const type = previewType(name);
    if (!type) { downloadFile(name); return; }

    const overlay = document.createElement('div');
    overlay.className = 'dialog-overlay preview-overlay';
    overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });

    let body = '';
    if (type === 'image') {
      body = `<img src="${url}" alt="${h(name)}" style="max-width:90vw;max-height:85vh;object-fit:contain">`;
    } else if (type === 'text') {
      body = `<div class="preview-loading">加载中...</div>`;
    } else if (type === 'pdf') {
      body = `<iframe src="${url}" style="width:90vw;height:85vh;border:none;border-radius:8px"></iframe>`;
    } else if (type === 'audio') {
      body = `<div class="preview-media-wrap"><h3>${h(name)}</h3><audio controls autoplay style="width:100%;max-width:500px"><source src="${url}"></audio></div>`;
    } else if (type === 'video') {
      body = `<div class="preview-media-wrap"><h3>${h(name)}</h3><video controls autoplay style="max-width:90vw;max-height:80vh"><source src="${url}"></video></div>`;
    }

    overlay.innerHTML = `<div class="preview-box">
      <div class="preview-header"><span>${h(name)}</span><button class="preview-close">×</button></div>
      <div class="preview-body">${body}</div>
      <div class="preview-footer">
        <span>${type === 'text' ? '' : formatSize(entriesCache.find(e => e.name === name)?.size || 0)}</span>
        <button class="btn-action preview-dl">下载</button>
      </div>
    </div>`;
    document.body.appendChild(overlay);

    overlay.querySelector('.preview-close').addEventListener('click', () => overlay.remove());
    overlay.querySelector('.preview-dl').addEventListener('click', () => downloadFile(name));

    if (type === 'text') {
      fetch(url).then(r => r.text()).then(txt => {
        const body = overlay.querySelector('.preview-body');
        body.innerHTML = `<pre class="preview-text">${h(txt)}</pre>`;
      }).catch(() => {
        overlay.querySelector('.preview-body').innerHTML = '<p class="preview-loading">加载失败</p>';
      });
    }
  }

  function confirmDelete(name, type) {
    const overlay = document.createElement('div');
    overlay.className = 'dialog-overlay';
    overlay.innerHTML = `<div class="dialog-box">
      <h3>确认删除</h3>
      <p class="danger-text">${type === 'dir' ? '此文件夹及其所有内容' : '此文件'}将被永久删除，不可恢复。</p>
      <p style="color:var(--text);font-size:13px;margin:0">${h(name)}</p>
      <div class="dialog-actions">
        <button class="btn-cancel">取消</button>
        <button class="btn-confirm btn-confirm-danger">删除</button>
      </div>
    </div>`;
    document.body.appendChild(overlay);
    overlay.querySelector('.btn-cancel').addEventListener('click', () => overlay.remove());
    overlay.querySelector('.btn-confirm').addEventListener('click', async () => {
      overlay.remove();
      const p = currentPath === '/' ? '/' + name : currentPath + '/' + name;
      const res = await fetch(`/api/files?path=${encodeURIComponent(p)}`, { method: 'DELETE' });
      if (res.ok) {
        showFiles();
      } else {
        const data = await res.json();
        showError(data.error || '删除失败');
      }
    });
  }

  $$('.sortable').forEach(th => {
    th.addEventListener('click', () => {
      const col = th.dataset.sort;
      if (currentSort === col) {
        currentSortDir = currentSortDir === 'asc' ? 'desc' : 'asc';
      } else {
        currentSort = col;
        currentSortDir = 'asc';
      }
      sortEntries();
      renderTable();
    });
  });

  $('#btn-upload').addEventListener('click', () => $('#upload-input').click());

  $('#upload-input').addEventListener('change', async () => {
    const file = $('#upload-input').files[0];
    if (!file) return;
    const formData = new FormData();
    formData.append('file', file);
    const progBar = $('#progress-bar');
    const progFill = progBar.querySelector('.progress-fill');
    progBar.style.display = 'block';
    progFill.style.width = '0%';

    const xhr = new XMLHttpRequest();
    xhr.open('POST', `/api/upload?path=${encodeURIComponent(currentPath)}`);

    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        progFill.style.width = Math.round(e.loaded / e.total * 100) + '%';
      }
    });

    xhr.addEventListener('load', () => {
      progBar.style.display = 'none';
      $('#upload-input').value = '';
      if (xhr.status === 200) {
        showFiles();
      } else {
        try {
          const data = JSON.parse(xhr.responseText);
          showError(data.error || '上传失败');
        } catch (_) {
          showError('上传失败');
        }
      }
    });

    xhr.addEventListener('error', () => {
      progBar.style.display = 'none';
      showError('上传失败: 网络错误');
    });

    xhr.send(formData);
  });

  $('#btn-mkdir').addEventListener('click', () => {
    const overlay = document.createElement('div');
    overlay.className = 'dialog-overlay';
    overlay.innerHTML = `<div class="dialog-box">
      <h3>新建文件夹</h3>
      <input type="text" placeholder="文件夹名称" id="mkdir-name" autofocus>
      <div class="dialog-actions">
        <button class="btn-cancel">取消</button>
        <button class="btn-confirm">创建</button>
      </div>
    </div>`;
    document.body.appendChild(overlay);

    const input = overlay.querySelector('#mkdir-name');
    overlay.querySelector('.btn-cancel').addEventListener('click', () => overlay.remove());
    overlay.querySelector('.btn-confirm').addEventListener('click', async () => {
      const name = input.value.trim();
      if (!name) return;
      overlay.remove();
      const res = await fetch(`/api/mkdir?path=${encodeURIComponent(currentPath)}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name })
      });
      if (res.ok) {
        showFiles();
      } else {
        const data = await res.json();
        showError(data.error || '创建失败');
      }
    });
  });

  $('#btn-logout').addEventListener('click', async () => {
    await fetch('/api/logout', { method: 'POST' });
    location.hash = '#/login';
  });

  function h(s) {
    const d = document.createElement('div');
    d.textContent = s;
    return d.innerHTML;
  }

  function formatSize(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
    if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
    return (bytes / 1073741824).toFixed(2) + ' GB';
  }

  function formatTime(s) {
    if (!s) return '—';
    return s.slice(0, 10);
  }

  function ext(name) {
    const m = name.match(/\.([a-zA-Z0-9]+)$/);
    return m ? m[1].toUpperCase() : '—';
  }

  function fileIcon(name) {
    const e = ext(name).toLowerCase();
    const map = { jpg: '\u{1F5BC}', jpeg: '\u{1F5BC}', png: '\u{1F5BC}', gif: '\u{1F5BC}', svg: '\u{1F5BC}', webp: '\u{1F5BC}',
      mp4: '\u{1F3AC}', mov: '\u{1F3AC}', avi: '\u{1F3AC}', mkv: '\u{1F3AC}',
      mp3: '\u{1F3B5}', wav: '\u{1F3B5}', flac: '\u{1F3B5}', aac: '\u{1F3B5}',
      pdf: '\u{1F4D5}', doc: '\u{1F4C4}', docx: '\u{1F4C4}', md: '\u{1F4DD}', txt: '\u{1F4DD}',
      zip: '\u{1F4E6}', gz: '\u{1F4E6}', tar: '\u{1F4E6}', rar: '\u{1F4E6}' };
    return map[e] || '\u{1F4C4}';
  }

  route();
})();
</script>
</body>
</html>
