<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Telegram 下载器</title>
  <style>
    *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

    :root {
      --bg:       #0d1117;
      --surface:  #161b22;
      --surface2: #1c2128;
      --border:   #30363d;
      --accent:   #58a6ff;
      --green:    #3fb950;
      --orange:   #d29922;
      --red:      #f85149;
      --text:     #e6edf3;
      --muted:    #8b949e;
      --hdr-h:    52px;
      --bar-h:    68px;
    }

    html, body { height: 100%; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: var(--bg); color: var(--text);
      font-size: 14px; line-height: 1.5;
    }

    /* ════ HEADER ════ */
    .hdr {
      position: fixed; top: 0; left: 0; right: 0;
      height: var(--hdr-h);
      background: var(--surface);
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center;
      padding: 0 16px; gap: 12px; z-index: 200;
    }
    .hdr-brand {
      font-size: 15px; font-weight: 700; color: var(--accent);
      white-space: nowrap; letter-spacing: -.3px;
    }
    .hdr-brand small { font-weight: 400; font-size: 11px; color: var(--muted); margin-left: 4px; }
    .hdr-divider { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
    .hdr-stats { display: flex; gap: 20px; flex: 1; justify-content: center; }
    .hstat { text-align: center; }
    .hstat-v { font-size: 14px; font-weight: 700; color: var(--green); line-height: 1.1; }
    .hstat-v.s { color: var(--accent); }
    .hstat-v.o { color: var(--orange); }
    .hstat-l { font-size: 10px; color: var(--muted); margin-top: 1px; }
    .hdr-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
    .state-pill {
      display: flex; align-items: center; gap: 5px;
      padding: 3px 8px; border-radius: 20px;
      border: 1px solid var(--border); font-size: 12px; color: var(--muted);
    }
    .state-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); }
    .state-dot.off { background: var(--orange); }
    .btn-pause {
      padding: 5px 14px; border-radius: 6px; font-size: 12px; font-weight: 600;
      border: 1px solid var(--border); background: transparent;
      color: var(--text); cursor: pointer; transition: all .15s;
    }
    .btn-pause:hover { border-color: var(--accent); color: var(--accent); }
    .btn-pause.resuming { border-color: var(--green); color: var(--green); }
    .btn-pause.resuming:hover { background: rgba(63,185,80,.08); }

    /* ════ FORM BAR ════ */
    .formbar {
      position: fixed;
      top: var(--hdr-h); left: 0; right: 0;
      height: var(--bar-h);
      background: var(--surface2);
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center;
      padding: 0 16px; gap: 0;
      z-index: 150;
      overflow: visible;
    }
    .fb-field {
      position: relative;
      display: flex; flex-direction: column; justify-content: center;
      padding: 0 12px;
      height: 100%;
    }
    .fb-field-channel { flex: 2; min-width: 200px; border-right: 1px solid var(--border); }
    .fb-field-date    { width: 148px; border-right: 1px solid var(--border); }
    .fb-field-path    { flex: 1.5; min-width: 180px; border-right: 1px solid var(--border); }
    .fb-field-submit  { flex-shrink: 0; padding: 0 14px; }

    .fb-label {
      font-size: 10px; font-weight: 600;
      color: var(--muted); text-transform: uppercase;
      letter-spacing: .5px; margin-bottom: 5px;
      white-space: nowrap;
      display: flex; align-items: center; gap: 6px;
    }
    .fb-input-row { display: flex; gap: 6px; }
    .fb-input-row .field-input { flex: 1; min-width: 0; }

    .field-input {
      width: 100%; padding: 6px 10px;
      background: var(--bg); border: 1px solid var(--border);
      border-radius: 6px; color: var(--text); font-size: 13px;
      transition: border-color .15s;
    }
    .field-input:focus { outline: none; border-color: var(--accent); }
    .field-input::placeholder { color: #555; }

    /* 验证结果下拉 */
    .val-drop {
      display: none;
      position: absolute; top: calc(100% + 2px); left: 12px;
      width: 280px; z-index: 400;
      background: var(--surface2); border: 1px solid var(--border);
      border-radius: 8px; padding: 10px 12px;
      box-shadow: 0 8px 24px rgba(0,0,0,.45);
      gap: 10px; align-items: center;
    }
    .val-drop.ok  { display: flex; border-color: rgba(63,185,80,.3); }
    .val-drop.err { display: flex; border-color: rgba(248,81,73,.3); }
    .val-icon { font-size: 18px; flex-shrink: 0; }
    .val-info b { display: block; font-size: 13px; }
    .val-drop.ok  .val-info b { color: var(--green); }
    .val-drop.err .val-info b { color: var(--red); }
    .val-info span { font-size: 11px; color: var(--muted); }

    /* 保存路径区 */
    .path-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
    .path-display {
      flex: 1; font-size: 12px; color: var(--green);
      font-family: monospace; overflow: hidden;
      text-overflow: ellipsis; white-space: nowrap; min-width: 0;
    }
    .path-edit-drop {
      display: none;
      position: absolute; top: calc(100% + 2px); right: 0; left: 0;
      z-index: 400;
      background: var(--surface2); border: 1px solid var(--border);
      border-radius: 8px; padding: 12px;
      box-shadow: 0 8px 24px rgba(0,0,0,.45);
    }
    .path-edit-drop.on { display: block; }
    .path-edit-drop .field-input { margin-bottom: 8px; }
    .path-edit-btns { display: flex; gap: 6px; }

    /* 按钮 */
    .btn {
      padding: 6px 13px; border-radius: 6px; font-size: 13px;
      font-weight: 500; cursor: pointer; border: none;
      display: inline-flex; align-items: center; gap: 5px;
      transition: opacity .15s; white-space: nowrap;
    }
    .btn:disabled { opacity: .4; cursor: not-allowed; }
    .btn-primary  { background: var(--accent); color: #0d1117; }
    .btn-primary:hover:not(:disabled) { opacity: .85; }
    .btn-outline  { background: transparent; border: 1px solid var(--border); color: var(--muted); }
    .btn-outline:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
    .btn-sm { padding: 4px 10px; font-size: 12px; }
    .btn-submit {
      padding: 9px 20px; border-radius: 7px;
      background: var(--accent); color: #0d1117;
      font-size: 13px; font-weight: 700;
      border: none; cursor: pointer;
      display: inline-flex; align-items: center; gap: 6px;
      transition: opacity .15s; white-space: nowrap;
    }
    .btn-submit:hover:not(:disabled) { opacity: .88; }
    .btn-submit:disabled { opacity: .4; cursor: not-allowed; }

    /* ════ MAIN SCROLL ════ */
    .main-scroll {
      position: fixed;
      top: calc(var(--hdr-h) + var(--bar-h));
      left: 0; right: 0; bottom: 0;
      overflow-y: auto; overflow-x: hidden;
      padding: 16px;
      background: var(--bg);
      display: flex; flex-direction: column; gap: 14px;
    }

    /* 卡片 */
    .card {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 8px;
    }
    .card-hd {
      display: flex; align-items: center; justify-content: space-between;
      padding: 10px 14px; border-bottom: 1px solid var(--border);
      font-size: 13px; font-weight: 600;
    }
    .card-hd-badge { font-size: 11px; font-weight: 400; color: var(--muted); }
    .card-bd { padding: 12px 14px; }
    /* 可折叠的 card-hd */
    .card-hd.collapsible { cursor: pointer; user-select: none; }
    .card-hd.collapsed { border-bottom: none; }
    .card-hd-left { display: flex; align-items: center; gap: 6px; }

    /* 任务队列总览 */
    .task-banner {
      display: none; padding: 10px 14px;
      border-radius: 8px;
      background: var(--surface); border: 1px solid var(--border);
    }
    .task-banner.show { display: block; }
    .qbanner-head {
      display: flex; align-items: center; justify-content: space-between;
      font-size: 12px; color: var(--muted); margin-bottom: 8px;
    }
    .qbanner-title { font-weight: 600; color: var(--text); font-size: 13px; }
    .qbanner-summary { font-size: 11px; }

    /* 队列卡片（已完成 / 当前 / 排队中） */
    #qbanner-list { display: flex; flex-wrap: wrap; gap: 8px; }
    .qcard {
      display: flex; align-items: flex-start; gap: 10px;
      padding: 8px 10px; border-radius: 6px;
      border: 1px solid transparent;
      flex: 1 1 220px; min-width: 200px; max-width: 380px;
    }
    .qcard-icon { font-size: 14px; line-height: 1.5; flex-shrink: 0; }
    .qcard-main { flex: 1; min-width: 0; }
    .qcard-name { font-weight: 600; font-size: 13px; color: var(--text);
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .qcard-state { font-size: 11px; color: var(--muted); margin-top: 2px; }
    .qcard-prog {
      height: 4px; background: rgba(255,255,255,.12);
      border-radius: 2px; overflow: hidden; margin-top: 6px;
    }
    .qcard-prog-fill { height: 100%; width: 0%; background: #4caf50; transition: width .3s; }
    .qcard-active { background: rgba(74,144,226,.08); border-color: rgba(74,144,226,.25); }
    .qcard-active .qcard-name { color: var(--accent); }
    .qcard-done { background: rgba(76,175,80,.07); opacity: .85; }
    .qcard-pending { background: rgba(128,128,128,.06); opacity: .6; }

    /* 历史频道卡片 */
    .hist-head {
      display: flex; align-items: center; justify-content: space-between;
      padding: 10px 14px; cursor: pointer; user-select: none;
    }
    .hist-head-left { display: flex; align-items: center; gap: 6px;
      font-size: 13px; font-weight: 600; color: var(--text); }
    .hist-arrow { font-size: 10px; color: var(--muted); transition: transform .2s; }
    .hist-arrow.closed { transform: rotate(-90deg); }
    .btn-clear { background: none; border: none; cursor: pointer;
      font-size: 11px; color: var(--muted); padding: 2px 6px;
      border-radius: 4px; }
    .btn-clear:hover { color: var(--red); background: rgba(248,81,73,.1); }

    .hist-body { padding: 0 8px 10px; }
    .hist-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
      align-content: start;
      align-items: start;
      gap: 8px;
      height: 46px;
      min-height: 46px;
      overflow-y: auto;
      resize: vertical;
      box-sizing: border-box;
    }
    .hist-item {
      display: flex; align-items: center; gap: 8px;
      padding: 5px 10px; border-radius: 7px;
      cursor: pointer; border: 1px solid transparent;
      transition: all .12s; background: var(--bg);
    }
    .hist-item:hover { background: rgba(88,166,255,.07); border-color: rgba(88,166,255,.2); }
    .hist-item.sel { background: rgba(88,166,255,.12); border-color: var(--accent); }
    .hist-emoji { font-size: 16px; flex-shrink: 0; }
    .hist-info { flex: 1; min-width: 0; }
    .hist-name { font-size: 12px; font-weight: 500;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .hist-meta { font-size: 10px; color: var(--muted); margin-top: 1px; }
    .hist-del {
      width: 20px; height: 20px; border: none; background: none;
      color: var(--muted); cursor: pointer; border-radius: 4px;
      display: flex; align-items: center; justify-content: center;
      opacity: 0; transition: all .12s; font-size: 11px; flex-shrink: 0;
    }
    .hist-item:hover .hist-del { opacity: 1; }
    .hist-del:hover { background: rgba(248,81,73,.15); color: var(--red); }
    .hist-empty { text-align: center; padding: 14px 8px; color: var(--muted); font-size: 12px; }

    /* 下载项 */
    .dl-item {
      padding: 9px 0;
      border-bottom: 1px solid var(--border);
    }
    .dl-item:last-child { border-bottom: none; }
    .dl-top { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
    .dl-name {
      flex: 1; min-width: 0; font-size: 12px;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .dl-pct { font-size: 12px; font-weight: 700; color: var(--accent); flex-shrink: 0; }
    .dl-spd { font-size: 11px; color: var(--muted); flex-shrink: 0; }
    .prog { height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
    .prog-fill {
      height: 100%; border-radius: 2px;
      background: linear-gradient(90deg, var(--accent), var(--green));
      transition: width .4s ease;
    }
    .prog-fill.done { background: var(--green); }

    /* 下载项控制按钮 */
    .dl-ctrl { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
    .dl-btn {
      width: 24px; height: 24px; border: none; background: none; cursor: pointer;
      color: var(--muted); border-radius: 4px; display: flex; align-items: center; justify-content: center;
      font-size: 12px; flex-shrink: 0; transition: all .12s;
    }
    .dl-btn:hover { background: rgba(88,166,255,.12); color: var(--accent); }
    .dl-btn.dl-skip:hover { background: rgba(248,81,73,.12); color: var(--red); }
    .dl-btn.dl-paused { color: var(--green); }

    /* 已完成项 */
    .done-item {
      display: flex; align-items: center; gap: 10px;
      padding: 7px 0; border-bottom: 1px solid var(--border);
      font-size: 12px;
    }
    .done-item:last-child { border-bottom: none; }
    .done-tick { color: var(--green); font-size: 11px; flex-shrink: 0; }
    .done-tick.skipped { color: var(--orange); }
    .done-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .done-size { color: var(--muted); flex-shrink: 0; font-size: 11px; }
    .done-skip-tag { font-size: 10px; color: var(--orange); flex-shrink: 0; padding: 1px 5px; border: 1px solid rgba(210,153,34,.4); border-radius: 3px; }
    .done-undo {
      font-size: 11px; color: var(--muted); cursor: pointer; padding: 2px 7px;
      border: 1px solid var(--border); border-radius: 4px; background: none;
      transition: all .12s; flex-shrink: 0; white-space: nowrap;
    }
    .done-undo:hover { color: var(--accent); border-color: var(--accent); }

    /* 分页控件 */
    .done-pager {
      display: flex; align-items: center; justify-content: space-between;
      padding: 6px 0 2px; font-size: 12px; color: var(--muted);
    }
    .done-pager-info { flex-shrink: 0; }
    .done-pager-btns { display: flex; gap: 4px; }
    .pg-btn {
      padding: 2px 10px; border-radius: 5px; border: 1px solid var(--border);
      background: none; color: var(--muted); font-size: 12px; cursor: pointer;
      transition: all .12s;
    }
    .pg-btn:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
    .pg-btn:disabled { opacity: .35; cursor: default; }

    /* 列表卡片内容区可滚动 */
    #dl-list, #done-list {
      height: 260px;
      min-height: 60px;
      overflow-y: auto;
      resize: vertical;
      box-sizing: border-box;
    }

    /* ════ 过滤器按钮 ════ */
    .fb-field-filter { flex-shrink: 0; border-right: 1px solid var(--border); }
    .btn-filter {
      display: inline-flex; align-items: center; gap: 5px;
      padding: 5px 11px; border-radius: 6px; font-size: 12px; font-weight: 500;
      background: transparent; border: 1px solid var(--border);
      color: var(--muted); cursor: pointer; transition: all .15s; white-space: nowrap;
    }
    .btn-filter:hover { border-color: var(--accent); color: var(--accent); }
    .btn-filter.active { border-color: var(--orange); color: var(--orange); background: rgba(210,153,34,.08); }
    .filter-badge {
      display: inline-flex; align-items: center; justify-content: center;
      width: 16px; height: 16px; border-radius: 50%;
      background: var(--orange); color: #0d1117; font-size: 10px; font-weight: 700;
    }

    /* ════ 文件类型多选下拉 ════ */
    .fb-field-ext { flex-shrink: 0; border-right: 1px solid var(--border); padding: 0 12px; }
    .ext-pop {
      display: none;
      position: absolute; top: calc(100% - 4px); left: 8px; z-index: 320;
      width: 320px; max-height: 360px; overflow: auto;
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 8px; box-shadow: 0 6px 18px rgba(0,0,0,.45);
      padding: 10px 12px;
    }
    .ext-pop.on { display: block; }
    .ext-grp-title {
      font-size: 11px; color: var(--muted); font-weight: 600;
      margin: 8px 0 5px; letter-spacing: .5px;
    }
    .ext-grp-title:first-child { margin-top: 0; }
    .ext-grp { display: flex; flex-wrap: wrap; gap: 5px; }
    .ext-chip {
      display: inline-flex; align-items: center;
      padding: 3px 9px; border-radius: 12px;
      border: 1px solid var(--border); background: transparent;
      color: var(--muted); font-size: 11px; cursor: pointer;
      transition: all .12s; user-select: none;
    }
    .ext-chip:hover { border-color: var(--accent); color: var(--accent); }
    .ext-chip.on { border-color: var(--orange); color: var(--orange); background: rgba(210,153,34,.12); }
    .ext-pop-foot {
      display: flex; justify-content: space-between; align-items: center;
      margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border);
      font-size: 11px;
    }
    .ext-pop-foot a {
      color: var(--muted); cursor: pointer; text-decoration: none;
    }
    .ext-pop-foot a:hover { color: var(--accent); }
    .ext-pop-foot a.danger:hover { color: var(--red); }

    /* ════ 过滤器弹窗 ════ */
    .filter-mask {
      display: none; position: fixed; inset: 0;
      background: rgba(0,0,0,.7); z-index: 450;
      align-items: center; justify-content: center; padding: 16px;
    }
    .filter-mask.on { display: flex; }
    .filter-box {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 12px; width: 100%; max-width: 680px;
      max-height: 88vh; display: flex; flex-direction: column;
    }
    .filter-head {
      display: flex; align-items: center; justify-content: space-between;
      padding: 16px 20px; border-bottom: 1px solid var(--border);
    }
    .filter-head-title { font-size: 15px; font-weight: 700; }
    .filter-mode-tabs { display: flex; gap: 2px; background: var(--bg); border-radius: 7px; padding: 2px; }
    .filter-tab {
      padding: 4px 12px; border-radius: 5px; font-size: 12px;
      cursor: pointer; border: none; background: transparent; color: var(--muted);
      transition: all .15s;
    }
    .filter-tab.on { background: var(--surface2); color: var(--text); }
    .filter-body { padding: 16px 20px; overflow-y: auto; flex: 1; }

    /* 规则行 */
    .filter-rules { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
    .filter-rule {
      display: flex; align-items: center; gap: 7px;
      background: var(--bg); border: 1px solid var(--border);
      border-radius: 8px; padding: 8px 10px;
    }
    .filter-rule select, .filter-rule input {
      background: var(--surface2); border: 1px solid var(--border);
      border-radius: 5px; color: var(--text); font-size: 12px;
      padding: 5px 8px; outline: none; transition: border-color .15s;
    }
    .filter-rule select:focus, .filter-rule input:focus { border-color: var(--accent); }
    .rule-field { flex: 1.4; min-width: 0; }
    .rule-op    { flex: 1; min-width: 0; }
    .rule-val   { flex: 1.6; min-width: 0; }
    .rule-unit  { width: 68px; flex-shrink: 0; }
    .rule-negate {
      display: flex; align-items: center; gap: 4px;
      font-size: 11px; color: var(--muted); flex-shrink: 0; white-space: nowrap; cursor: pointer;
    }
    .rule-negate input { width: 13px; height: 13px; cursor: pointer; accent-color: var(--orange); }
    .rule-del {
      width: 24px; height: 24px; border: none; background: none; cursor: pointer;
      color: var(--muted); border-radius: 4px; display: flex; align-items: center; justify-content: center;
      font-size: 13px; flex-shrink: 0; transition: all .12s;
    }
    .rule-del:hover { background: rgba(248,81,73,.15); color: var(--red); }
    .btn-add-rule {
      display: inline-flex; align-items: center; gap: 5px;
      padding: 6px 14px; border-radius: 6px; font-size: 12px;
      background: transparent; border: 1px dashed var(--border);
      color: var(--muted); cursor: pointer; transition: all .15s;
    }
    .btn-add-rule:hover { border-color: var(--accent); color: var(--accent); }

    /* 预览区 */
    .filter-preview {
      margin-top: 12px; padding: 10px 12px; border-radius: 7px;
      background: var(--bg); border: 1px solid var(--border);
    }
    .filter-preview-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 5px; }
    .filter-preview-expr {
      font-family: monospace; font-size: 12px; color: var(--green);
      word-break: break-all; min-height: 18px;
    }
    .filter-preview-expr.empty { color: var(--muted); font-style: italic; }
    .filter-val-tip {
      display: flex; align-items: center; gap: 6px; margin-top: 6px;
      font-size: 11px; padding: 5px 8px; border-radius: 5px;
    }
    .filter-val-tip.ok  { background: rgba(63,185,80,.07); color: var(--green); }
    .filter-val-tip.err { background: rgba(248,81,73,.07); color: var(--red); }

    /* 手动模式 */
    .filter-manual { display: none; }
    .filter-manual.on { display: block; }
    .filter-visual { display: block; }
    .filter-visual.hide { display: none; }
    .filter-manual textarea {
      width: 100%; height: 80px; resize: vertical;
      background: var(--bg); border: 1px solid var(--border);
      border-radius: 6px; color: var(--text); font-family: monospace; font-size: 13px;
      padding: 8px 10px; outline: none; transition: border-color .15s;
    }
    .filter-manual textarea:focus { border-color: var(--accent); }
    .filter-manual .hint { font-size: 11px; color: var(--muted); margin-top: 5px; }

    .filter-foot {
      display: flex; align-items: center; justify-content: space-between;
      padding: 12px 20px; border-top: 1px solid var(--border); gap: 8px;
    }

    /* 空状态 */
    .empty { text-align: center; padding: 28px 16px; color: var(--muted); font-size: 13px; }
    .empty svg { display: block; margin: 0 auto 10px; opacity: .25; }

    /* Toast */
    .toasts { position: fixed; top: 60px; right: 14px; display: flex; flex-direction: column; gap: 7px; z-index: 999; pointer-events: none; }
    .toast { padding: 9px 16px; border-radius: 7px; font-size: 13px; animation: tin .2s ease; }
    .toast-ok  { background: rgba(63,185,80,.92); color: #0d1117; }
    .toast-err { background: rgba(248,81,73,.92); color: #fff; }
    @keyframes tin { from { opacity:0; transform:translateX(16px); } to { opacity:1; transform:none; } }

    /* Spinner */
    .spin {
      display: inline-block; width: 11px; height: 11px;
      border: 2px solid rgba(255,255,255,.3); border-radius: 50%;
      border-top-color: currentColor; animation: sp 1s linear infinite;
    }
    @keyframes sp { to { transform: rotate(360deg); } }

    /* ════ 设置向导 ════ */
    .wizard-mask {
      display: none; position: fixed; inset: 0;
      background: rgba(0,0,0,.82); z-index: 500;
      align-items: center; justify-content: center; padding: 16px;
    }
    .wizard-mask.on { display: flex; }
    .wizard-box {
      background: var(--surface); border: 1px solid var(--border);
      border-radius: 12px; width: 100%; max-width: 580px;
      max-height: 90vh; overflow-y: auto;
      display: flex; flex-direction: column;
    }
    .wiz-steps {
      display: flex; align-items: center;
      padding: 20px 24px 0; gap: 0;
    }
    .ws {
      display: flex; align-items: center; gap: 7px;
      flex: 1; position: relative;
    }
    .ws:not(:last-child)::after {
      content: ''; position: absolute;
      left: 26px; right: 0; top: 13px;
      height: 1px; background: var(--border); z-index: 0;
    }
    .ws.done::after, .ws.active::after { background: var(--accent); }
    .ws-num {
      width: 26px; height: 26px; border-radius: 50%;
      border: 2px solid var(--border);
      display: flex; align-items: center; justify-content: center;
      font-size: 11px; font-weight: 700; color: var(--muted);
      background: var(--surface); flex-shrink: 0; position: relative; z-index: 1;
      transition: all .2s;
    }
    .ws.active .ws-num { border-color: var(--accent); color: var(--accent); }
    .ws.done .ws-num { border-color: var(--green); background: var(--green); color: #0d1117; }
    .ws-lbl { font-size: 11px; color: var(--muted); white-space: nowrap; }
    .ws.active .ws-lbl { color: var(--accent); }
    .ws.done .ws-lbl { color: var(--green); }
    @media (max-width: 440px) { .ws-lbl { display: none; } }

    .wiz-body { padding: 22px 24px; flex: 1; }
    .wiz-title { font-size: 17px; font-weight: 700; margin-bottom: 4px; }
    .wiz-sub { font-size: 12px; color: var(--muted); margin-bottom: 18px; }

    .guide-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 18px; }
    .guide-item { display: flex; gap: 12px; align-items: flex-start; }
    .guide-n {
      width: 24px; height: 24px; flex-shrink: 0; border-radius: 50%;
      background: rgba(88,166,255,.12); border: 1px solid rgba(88,166,255,.25);
      display: flex; align-items: center; justify-content: center;
      font-size: 11px; font-weight: 700; color: var(--accent);
    }
    .guide-c b { font-size: 13px; display: block; margin-bottom: 2px; }
    .guide-c p { font-size: 12px; color: var(--muted); line-height: 1.6; }
    .guide-c a { color: var(--accent); }
    .guide-c code {
      background: rgba(88,166,255,.1); border: 1px solid rgba(88,166,255,.2);
      border-radius: 3px; padding: 0 5px; font-size: 11px; color: var(--accent);
    }

    .info-tip {
      padding: 10px 12px; border-radius: 7px;
      font-size: 12px; line-height: 1.6; margin-bottom: 14px;
      background: rgba(88,166,255,.07); border: 1px solid rgba(88,166,255,.18); color: var(--muted);
    }
    .info-warn {
      padding: 10px 12px; border-radius: 7px; font-size: 12px; line-height: 1.6; margin-bottom: 14px;
      background: rgba(210,153,34,.08); border: 1px solid rgba(210,153,34,.25); color: #d29922;
    }
    .info-ok {
      padding: 10px 12px; border-radius: 7px; font-size: 12px; line-height: 1.6; margin-bottom: 14px;
      background: rgba(63,185,80,.08); border: 1px solid rgba(63,185,80,.25); color: var(--green);
    }

    .toggle-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
    .tog { position: relative; width: 38px; height: 20px; }
    .tog input { opacity: 0; width: 0; height: 0; }
    .tog-track {
      position: absolute; inset: 0; background: var(--border);
      border-radius: 10px; cursor: pointer; transition: background .2s;
    }
    .tog-track::after {
      content: ''; position: absolute; width: 14px; height: 14px;
      border-radius: 50%; background: #fff; top: 3px; left: 3px; transition: transform .2s;
    }
    .tog input:checked + .tog-track { background: var(--accent); }
    .tog input:checked + .tog-track::after { transform: translateX(18px); }
    .proxy-fields { display: none; margin-bottom: 6px; }
    .proxy-fields.on { display: block; }
    .proxy-2col { display: grid; grid-template-columns: 1fr 90px; gap: 8px; }
    .wiz-field { margin-bottom: 10px; }
    .wiz-field-label { display: block; font-size: 11px; color: var(--muted); margin-bottom: 4px; }
    .proxy-tbl { font-size: 11px; color: var(--muted); border-collapse: collapse; width: 100%; margin-top: 8px; }
    .proxy-tbl th { text-align: left; padding: 2px 6px; color: var(--text); font-weight: 500; }
    .proxy-tbl td { padding: 2px 6px; text-align: center; }

    .summary-box { background: var(--bg); border: 1px solid var(--border); border-radius: 7px; overflow: hidden; margin-bottom: 16px; }
    .sum-row { display: flex; justify-content: space-between; padding: 9px 12px; font-size: 12px; border-bottom: 1px solid var(--border); }
    .sum-row:last-child { border-bottom: none; }
    .sum-k { color: var(--muted); }
    .sum-v { color: var(--green); font-family: monospace; }

    .login-box { background: rgba(210,153,34,.07); border: 1px solid rgba(210,153,34,.25); border-radius: 7px; padding: 12px 14px; margin-bottom: 14px; font-size: 12px; }
    .login-box-title { color: #d29922; font-weight: 600; margin-bottom: 6px; }
    .login-box ol { color: var(--muted); padding-left: 16px; line-height: 1.9; }
    .login-box code { color: var(--accent); }

    .wiz-foot {
      display: flex; align-items: center; justify-content: space-between;
      padding: 14px 24px; border-top: 1px solid var(--border); gap: 10px;
    }

    /* ════ 下载队列 ════ */
    .submit-row { display: flex; gap: 6px; align-items: stretch; }
    .btn-add-queue {
      padding: 9px 14px; border-radius: 7px;
      background: transparent; border: 1px solid var(--border);
      color: var(--muted); font-size: 13px; font-weight: 600;
      cursor: pointer; transition: all .15s; white-space: nowrap;
    }
    .btn-add-queue:hover { border-color: var(--accent); color: var(--accent); }
    .queue-item {
      display: flex; align-items: center; gap: 10px;
      padding: 9px 2px; border-bottom: 1px solid var(--border);
    }
    .queue-item:last-child { border-bottom: none; }
    .queue-num {
      width: 22px; height: 22px; border-radius: 50%;
      background: rgba(88,166,255,.12); border: 1px solid rgba(88,166,255,.25);
      display: flex; align-items: center; justify-content: center;
      font-size: 11px; font-weight: 700; color: var(--accent); flex-shrink: 0;
    }
    .queue-info { flex: 1; min-width: 0; }
    .queue-name { font-size: 13px; font-weight: 500;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .queue-filter { font-size: 11px; color: var(--muted); margin-top: 2px;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .queue-del {
      width: 26px; height: 26px; border: none; background: none;
      color: var(--muted); cursor: pointer; border-radius: 4px;
      display: flex; align-items: center; justify-content: center;
      font-size: 13px; flex-shrink: 0; transition: all .12s;
    }
    .queue-del:hover { background: rgba(248,81,73,.15); color: var(--red); }

    /* 响应式：手机适配（≤ 768px） */
    @media (max-width: 768px) {
      /* 顶部导航变两行，把 --hdr-h 调大供 formbar margin 跟随 */
      :root { --hdr-h: 88px; }

      /* 页面滚动交给整页而非固定容器，避免 fixed 高度硬编码 bug */
      html, body { height: auto; }

      /* 顶部导航：允许换行，分两行显示，防止文字被挤压竖排 */
      .hdr {
        height: auto;
        min-height: var(--hdr-h);
        flex-wrap: wrap;
        padding: 6px 10px;
        gap: 8px;
        align-items: center;
      }
      .hdr-brand { font-size: 13px; flex-shrink: 0; }
      .hdr-brand small { display: none; }
      .hdr-divider { display: none; }
      .hdr-right { margin-left: auto; gap: 6px; flex-shrink: 0; }
      .state-pill { padding: 3px 8px; font-size: 11px; white-space: nowrap; flex-shrink: 0; }
      .btn-pause { padding: 5px 12px; font-size: 11px; white-space: nowrap; flex-shrink: 0; }

      /* 统计数字独占第二行（order: 5 让它换到 brand/right 之后） */
      .hdr-stats {
        order: 5;
        flex-basis: 100%;
        width: 100%;
        gap: 0;
        padding-top: 6px;
        border-top: 1px solid var(--border);
        justify-content: space-around;
      }
      .hstat { text-align: center; white-space: nowrap; }
      .hstat-v { font-size: 13px; white-space: nowrap; }
      .hstat-l { font-size: 10px; white-space: nowrap; margin-top: 1px; }

      /* 表单栏：取消固定定位，改为跟内容一起滚；字段全占满宽度 */
      .formbar {
        position: static;
        height: auto;
        flex-wrap: wrap;
        padding: 10px 12px;
        gap: 10px;
        margin-top: var(--hdr-h);
      }
      .fb-field { padding: 0; width: 100%; border-right: none !important; min-width: 0; }
      .fb-field-submit { padding: 0; }
      .submit-row { flex-direction: column; gap: 8px; }
      .btn-add-queue { width: 100%; padding: 11px; font-size: 13px; }
      .btn-submit { width: 100%; justify-content: center; padding: 12px; font-size: 14px; }
      .queue-filter { font-size: 10px; }

      /* 验证下拉宽度自适应，避免溢出 */
      .val-drop { width: auto; left: 12px; right: 12px; }

      /* 主内容区：取消固定，走正常流 */
      .main-scroll {
        position: static;
        padding: 12px;
        gap: 10px;
      }

      /* 卡片内边距缩小 */
      .card-hd { padding: 8px 12px; }
      .card-bd { padding: 10px 12px; }

      /* 历史频道网格一行两列，更紧凑 */
      .hist-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }

      /* 下载项操作按钮加大便于点按 */
      .dl-btn { width: 28px; height: 28px; font-size: 13px; }

      /* 列表高度减小 */
      #dl-list, #done-list { height: 220px; }

      /* 过滤弹窗：占满屏幕、规则行竖排堆叠 */
      .filter-mask { padding: 8px; align-items: flex-start; padding-top: 8vh; }
      .filter-box { max-width: 100%; max-height: 85vh; }
      .filter-head { padding: 12px 14px; }
      .filter-head-title { font-size: 14px; }
      .filter-body { padding: 12px 14px; }
      .filter-foot { padding: 10px 14px; flex-wrap: wrap; }
      .filter-rule { flex-wrap: wrap; gap: 6px; padding: 8px; }
      .rule-field, .rule-op, .rule-val, .rule-unit { flex: 1 1 calc(50% - 3px); min-width: 0; }
      .rule-negate { flex: 1 1 auto; }
      .rule-del { margin-left: auto; }

      /* 向导弹窗：占满屏幕、步骤圆点紧凑 */
      .wizard-mask { padding: 8px; align-items: flex-start; padding-top: 5vh; }
      .wizard-box { max-width: 100%; max-height: 90vh; }
      .wiz-steps { padding: 14px 14px 0; }
      .wiz-body { padding: 14px; }
      .wiz-foot { padding: 12px 14px; gap: 8px; }
      .wiz-title { font-size: 15px; }
      .ws-num { width: 22px; height: 22px; font-size: 10px; }

      /* Toast 通知全宽居中（避开变高的 hdr） */
      .toasts { top: calc(var(--hdr-h) + 6px); right: 8px; left: 8px; }
      .toast { font-size: 12px; }
    }
  </style>
</head>
<body>

<!-- ════ 过滤器弹窗 ════ -->
<div class="filter-mask" id="filter-mask">
  <div class="filter-box">
    <div class="filter-head">
      <div class="filter-head-title">⚙ 过滤条件配置</div>
      <div class="filter-mode-tabs">
        <button class="filter-tab on" id="ftab-visual" onclick="switchFilterTab('visual')">可视化</button>
        <button class="filter-tab"    id="ftab-manual" onclick="switchFilterTab('manual')">手动输入</button>
      </div>
    </div>

    <div class="filter-body">
      <!-- 可视化模式 -->
      <div class="filter-visual" id="fv-visual">
        <div class="filter-rules" id="filter-rules"></div>
        <button class="btn-add-rule" onclick="addFilterRule()">＋ 添加条件</button>
        <div style="margin-top:10px;font-size:11px;color:var(--muted);">
          多个条件之间为 <b style="color:var(--text)">AND（且）</b> 关系；勾选「排除」则对该条件取反（NOT）
        </div>
      </div>

      <!-- 手动输入模式 -->
      <div class="filter-manual" id="fv-manual">
        <textarea id="filter-manual-input" placeholder="如：message_date >= 2024-01-01 00:00:00 and media_file_size >= 10485760" oninput="onManualFilterInput()"></textarea>
        <div class="hint">
          可用字段：message_date、media_file_size、media_width、media_height、media_duration、media_file_name、message_caption<br>
          操作符：>=、<=、>、<、==、!=、matches（正则）、contains（包含）、not（取反）<br>
          示例：<code style="color:var(--accent)">not media_file_name matches '.*\.txt$'</code>
        </div>
      </div>

      <!-- 预览 -->
      <div class="filter-preview">
        <div class="filter-preview-label">生成的过滤表达式</div>
        <div class="filter-preview-expr empty" id="filter-expr-preview">（无过滤条件）</div>
        <div id="filter-val-tip" style="display:none" class="filter-val-tip"></div>
      </div>
    </div>

    <div class="filter-foot">
      <button class="btn btn-outline btn-sm" onclick="closeFilterModal()">取消</button>
      <button class="btn btn-outline btn-sm" id="filter-validate-btn" onclick="validateFilterExpr()"><span id="fvbtn-txt">校验语法</span></button>
      <div style="flex:1"></div>
      <button class="btn btn-outline btn-sm" onclick="clearFilter()" style="color:var(--red)">清除过滤</button>
      <button class="btn btn-primary btn-sm" onclick="applyFilter()">应用</button>
    </div>
  </div>
</div>

<!-- ════ 设置向导 ════ -->
<div class="wizard-mask" id="wiz-mask">
  <div class="wizard-box">
    <div class="wiz-steps" id="wiz-steps">
      <div class="ws active" id="ws-0"><div class="ws-num">1</div><span class="ws-lbl">API 凭证</span></div>
      <div class="ws"        id="ws-1"><div class="ws-num">2</div><span class="ws-lbl">网络 & 路径</span></div>
      <div class="ws"        id="ws-2"><div class="ws-num">3</div><span class="ws-lbl">确认</span></div>
    </div>

    <!-- Step 0 -->
    <div class="wiz-body" id="wp-0">
      <div class="wiz-title">获取 Telegram API 凭证</div>
      <div class="wiz-sub">连接 Telegram 需要你自己的 API 密钥，申请免费，约 1 分钟</div>
      <div class="guide-list">
        <div class="guide-item">
          <div class="guide-n">1</div>
          <div class="guide-c">
            <b>打开官方开发者平台</b>
            <p>访问 <a href="https://my.telegram.org/apps" target="_blank">my.telegram.org/apps</a>，用 Telegram 账号登录。<br>⚠️ 验证码发到 <b>Telegram App 内收件箱</b>，不是短信</p>
          </div>
        </div>
        <div class="guide-item">
          <div class="guide-n">2</div>
          <div class="guide-c">
            <b>创建应用</b>
            <p>填写任意名称，Platform 选 <code>Desktop</code>，点击「Create application」</p>
          </div>
        </div>
        <div class="guide-item">
          <div class="guide-n">3</div>
          <div class="guide-c">
            <b>复制密钥到下方</b>
            <p>页面会显示 <code>App api_id</code> 和 <code>App api_hash</code></p>
          </div>
        </div>
      </div>
      <div class="wiz-field">
        <label class="wiz-field-label">api_id（纯数字）</label>
        <input class="field-input" type="number" id="w-api-id" placeholder="如：12345678">
      </div>
      <div class="wiz-field">
        <label class="wiz-field-label">api_hash（32位字符串）</label>
        <input class="field-input" type="text" id="w-api-hash" placeholder="从 my.telegram.org 复制">
      </div>
      <div class="info-tip">💡 遇到「too many tries」提示，等待 1～2 小时后重试</div>
    </div>

    <!-- Step 1 -->
    <div class="wiz-body" id="wp-1" style="display:none">
      <div class="wiz-title">网络代理 & 保存路径</div>
      <div class="wiz-sub">国内需要代理才能连接 Telegram</div>
      <div class="toggle-row">
        <span style="font-size:13px;">启用代理（国内用户必选）</span>
        <label class="tog"><input type="checkbox" id="w-proxy-on" onchange="toggleProxyFields()"><div class="tog-track"></div></label>
      </div>
      <div class="proxy-fields" id="w-proxy-fields">
        <div class="wiz-field">
          <label class="wiz-field-label">代理类型</label>
          <select class="field-input" id="w-proxy-scheme">
            <option value="socks5">SOCKS5（Clash 默认 7891）</option>
            <option value="http">HTTP（Clash 默认 7890）</option>
          </select>
        </div>
        <div class="proxy-2col">
          <div class="wiz-field" style="margin-bottom:0">
            <label class="wiz-field-label">地址</label>
            <input class="field-input" type="text" id="w-proxy-host" value="127.0.0.1">
          </div>
          <div class="wiz-field" style="margin-bottom:0">
            <label class="wiz-field-label">端口</label>
            <input class="field-input" type="number" id="w-proxy-port" value="7891">
          </div>
        </div>
        <table class="proxy-tbl">
          <tr><th>代理软件</th><th>SOCKS5</th><th>HTTP</th></tr>
          <tr><td>Clash</td><td>7891</td><td>7890</td></tr>
          <tr><td>V2Ray</td><td>10808</td><td>10809</td></tr>
          <tr><td>Shadowsocks</td><td>1080</td><td>—</td></tr>
        </table>
        <div style="margin-top:10px;">
          <button class="btn btn-outline btn-sm" onclick="testWizProxy()" id="w-proxy-test-btn">🔌 测试代理</button>
          <span id="w-proxy-test-result" style="font-size:11px;margin-left:8px;"></span>
        </div>
      </div>
      <div class="wiz-field" style="margin-top:12px">
        <label class="wiz-field-label">媒体文件保存路径</label>
        <input class="field-input" type="text" id="w-save-path" placeholder="/Users/yourname/Downloads/tg">
        <div style="font-size:11px;color:var(--muted);margin-top:4px;">留空使用当前配置，建议填写绝对路径</div>
      </div>
    </div>

    <!-- Step 2 -->
    <div class="wiz-body" id="wp-2" style="display:none">
      <div style="text-align:center;font-size:48px;margin-bottom:12px;">🎉</div>
      <div class="wiz-title" style="text-align:center;">配置确认</div>
      <div class="wiz-sub" style="text-align:center;margin-bottom:16px;">点击「保存并启动」完成设置</div>
      <div class="summary-box" id="w-summary"></div>
      <div class="login-box" id="w-login-notice" style="display:none">
        <div class="login-box-title">⚠️ 首次运行需在终端完成账号验证</div>
        <ol>
          <li>保存后程序自动重启</li>
          <li>切换到<b>终端窗口</b></li>
          <li>输入手机号（如 <code>+8613800138000</code>）</li>
          <li>输入 Telegram App 内收到的验证码</li>
          <li>验证成功后刷新此页面</li>
        </ol>
      </div>
      <div class="info-ok" id="w-session-ok" style="display:none">✅ 已有登录记录，保存后直接可用</div>
    </div>

    <div class="wiz-foot">
      <button class="btn btn-outline btn-sm" id="w-back" onclick="wizBack()" style="display:none">← 上一步</button>
      <div style="flex:1"></div>
      <button class="btn btn-primary" id="w-next" onclick="wizNext()">下一步 →</button>
    </div>
  </div>
</div>

<!-- ════ HEADER ════ -->
<header class="hdr">
  <div class="hdr-brand">TG 下载器 <small>v<span id="app-ver">—</span></small></div>
  <div class="hdr-divider"></div>
  <div class="hdr-stats">
    <div class="hstat"><div class="hstat-v s" id="h-speed">0 B/s</div><div class="hstat-l">速度</div></div>
    <div class="hstat"><div class="hstat-v"   id="h-dl">0</div><div class="hstat-l">下载中</div></div>
    <div class="hstat"><div class="hstat-v"   id="h-done">0</div><div class="hstat-l">已完成</div></div>
    <div class="hstat"><div class="hstat-v o" id="h-skip">0</div><div class="hstat-l">已跳过</div></div>
  </div>
  <div class="hdr-right">
    <div class="state-pill"><div class="state-dot" id="state-dot"></div><span id="state-txt">连接中</span></div>
    <a href="/settings" class="btn-pause" style="text-decoration:none;" title="设置">⚙</a>
    <button class="btn-pause" id="btn-pause" onclick="toggleDownload()">⏸ 暂停</button>
  </div>
</header>

<!-- ════ FORM BAR ════ -->
<div class="formbar">

  <!-- 频道 -->
  <div class="fb-field fb-field-channel">
    <div class="fb-label">频道</div>
    <div class="fb-input-row">
      <input class="field-input" id="channel-input" placeholder="t.me/xxx 或 @username">
      <button class="btn btn-outline btn-sm" id="validate-btn" onclick="validateChannel()">验证</button>
    </div>
    <div id="val-result" class="val-drop">
      <div class="val-icon" id="val-icon"></div>
      <div class="val-info"><b id="val-name"></b><span id="val-type"></span></div>
    </div>
  </div>

  <!-- 开始日期 -->
  <div class="fb-field fb-field-date">
    <div class="fb-label">开始日期</div>
    <input class="field-input" type="date" id="start-date">
  </div>

  <!-- 结束日期 -->
  <div class="fb-field fb-field-date">
    <div class="fb-label">结束日期（可选）</div>
    <input class="field-input" type="date" id="end-date">
  </div>

  <!-- 过滤器 -->
  <div class="fb-field fb-field-filter" style="padding:0 12px;">
    <div class="fb-label">过滤条件</div>
    <button class="btn-filter" id="filter-open-btn" onclick="openFilterModal()">
      ⚙ 过滤器 <span class="filter-badge" id="filter-badge" style="display:none">0</span>
    </button>
  </div>

  <!-- 文件类型 -->
  <div class="fb-field fb-field-ext">
    <div class="fb-label">文件类型</div>
    <button class="btn-filter" id="ext-open-btn" onclick="toggleExtPop(event)">
      🗂 全部 <span class="filter-badge" id="ext-badge" style="display:none">0</span>
    </button>
    <div class="ext-pop" id="ext-pop" onclick="event.stopPropagation()"></div>
  </div>

  <!-- 保存路径 -->
  <div class="fb-field fb-field-path">
    <div class="fb-label">
      保存路径
      <button class="btn btn-outline btn-sm" id="edit-path-btn" onclick="togglePathEdit()"
        style="padding:1px 7px;font-size:10px;margin-left:2px;">编辑</button>
    </div>
    <div class="path-row">
      <span class="path-display" id="cfg-path" title="">加载中…</span>
    </div>
    <div class="path-edit-drop" id="path-edit-area">
      <input class="field-input" id="path-input" type="text" placeholder="输入绝对路径">
      <div class="path-edit-btns">
        <button class="btn btn-primary btn-sm" style="flex:1" onclick="savePath()">保存</button>
        <button class="btn btn-outline btn-sm" style="flex:1" onclick="togglePathEdit()">取消</button>
      </div>
    </div>
  </div>

  <!-- 提交 -->
  <div class="fb-field fb-field-submit">
    <div class="submit-row">
      <button class="btn-add-queue" id="add-queue-btn" onclick="addToQueue()">
        ➕ 加入队列
      </button>
      <button class="btn-submit" id="save-btn" onclick="saveAndRestart()" style="flex:1">
        🔄 保存并重启下载
      </button>
    </div>
  </div>

</div>

<div class="toasts" id="toasts"></div>

<!-- ════ MAIN CONTENT ════ -->
<div class="main-scroll">

  <!-- 任务队列总览 -->
  <div class="task-banner" id="task-banner">
    <div class="qbanner-head">
      <span class="qbanner-title">任务队列</span>
      <span class="qbanner-summary" id="qbanner-summary"></span>
    </div>
    <div id="qbanner-list"></div>
  </div>

  <!-- 下载队列（待处理） -->
  <div class="card" id="queue-card" style="display:none">
    <div class="card-hd">
      <span>
        待处理队列
        <span class="card-hd-badge" id="queue-count" style="margin-left:6px">0 个</span>
      </span>
      <button class="btn-clear" onclick="clearQueue()">清空</button>
    </div>
    <div class="card-bd" id="queue-list"></div>
  </div>

  <!-- 历史频道 -->
  <div class="card">
    <div class="hist-head" onclick="toggleHist()">
      <div class="hist-head-left">
        <span class="hist-arrow" id="hist-arrow">▾</span>
        历史频道
      </div>
      <button class="btn-clear" id="hist-clear-btn" style="display:none"
        onclick="event.stopPropagation();clearHistory()">清空</button>
    </div>
    <div id="hist-body" class="hist-body">
      <div class="hist-grid" id="hist-list">
        <div class="hist-empty">暂无历史记录</div>
      </div>
    </div>
  </div>

  <!-- 正在下载 -->
  <div class="card">
    <div class="card-hd collapsible" id="dl-head" onclick="toggleDl()">
      <div class="card-hd-left">
        <span class="hist-arrow" id="dl-arrow">▾</span>
        <span>正在下载</span>
      </div>
      <span class="card-hd-badge" id="dl-badge">0 个文件</span>
    </div>
    <div class="card-bd" id="dl-list">
      <div class="empty">
        <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
          <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
          <polyline points="7 10 12 15 17 10"/>
          <line x1="12" y1="15" x2="12" y2="3"/>
        </svg>
        暂无下载任务
      </div>
    </div>
  </div>

  <!-- 已完成 -->
  <div class="card">
    <div class="card-hd collapsible" id="done-head" onclick="toggleDone()">
      <div class="card-hd-left">
        <span class="hist-arrow" id="done-arrow">▾</span>
        <span>已完成</span>
      </div>
      <span class="card-hd-badge" id="done-badge">0 个文件</span>
    </div>
    <div class="card-bd" id="done-list">
      <div class="empty">暂无已完成文件</div>
    </div>
    <div class="done-pager" id="done-pager" style="display:none">
      <span class="done-pager-info" id="done-pager-info"></span>
      <div class="done-pager-btns">
        <button class="pg-btn" id="pg-prev" onclick="donePageGo(-1)">‹ 上一页</button>
        <button class="pg-btn" id="pg-next" onclick="donePageGo(1)">下一页 ›</button>
      </div>
    </div>
  </div>

</div>

<script>
  let dlState = '{{ download_state }}';
  let validatedChannel = null;
  let histOpen = true;
  let dlOpen = true;
  let doneOpen = true;
  let setupStatus = {};
  let wizStep = 0;
  let donePage = 1;
  const donePageSize = 50;

  // ── Toast ──
  function toast(msg, type = 'ok') {
    const el = document.createElement('div');
    el.className = 'toast toast-' + type;
    el.textContent = msg;
    document.getElementById('toasts').appendChild(el);
    setTimeout(() => el.remove(), 3200);
  }

  // ── 工具 ──
  function parseChannelId(s) {
    s = (s || '').trim();
    const m = s.match(/t\.me\/([a-zA-Z0-9_]+)/);
    if (m) return m[1];
    return s.startsWith('@') ? s.slice(1) : s;
  }
  const TICON = { CHANNEL:'📺', SUPERGROUP:'👥', GROUP:'👥', PRIVATE:'👤', BOT:'🤖' };
  const TNAME = { CHANNEL:'频道', SUPERGROUP:'超级群组', GROUP:'群组', PRIVATE:'私聊', BOT:'机器人' };
  function fmtTime(iso) {
    if (!iso) return '';
    const d = new Date(iso), diff = Date.now() - d;
    if (diff < 60000) return '刚刚';
    if (diff < 3600000) return Math.floor(diff/60000) + '分钟前';
    if (diff < 86400000) return Math.floor(diff/3600000) + '小时前';
    return d.toLocaleDateString('zh-CN');
  }

  // ── 文件类型多选 ──
  // 分组定义；扩展名要全小写（底层 file_extension 字段保证小写）
  const EXT_GROUPS = [
    { name: '🎬 视频', items: ['mp4','mov','mkv','avi','webm','flv','wmv'] },
    { name: '🎵 音频', items: ['mp3','flac','wav','m4a','aac','ogg','opus'] },
    { name: '🖼 图片', items: ['jpg','png','gif','webp','heic','bmp'] },
    { name: '📄 文档', items: ['pdf','zip','rar','7z','txt','doc','docx','xlsx','pptx','epub'] },
  ];
  let selectedExtensions = [];   // 当前主表单选中的扩展名列表

  function renderExtPop() {
    const pop = document.getElementById('ext-pop');
    if (!pop) return;
    const groups = EXT_GROUPS.map(g => {
      const chips = g.items.map(ext => {
        const on = selectedExtensions.includes(ext) ? ' on' : '';
        return `<span class="ext-chip${on}" onclick="toggleExt('${ext}')">${ext}</span>`;
      }).join('');
      return `<div class="ext-grp-title">${g.name}</div><div class="ext-grp">${chips}</div>`;
    }).join('');
    pop.innerHTML = groups + `
      <div class="ext-pop-foot">
        <a onclick="clearExtensions()" class="danger">清空</a>
        <a onclick="closeExtPop()">完成</a>
      </div>`;
  }

  function toggleExtPop(ev) {
    if (ev) ev.stopPropagation();
    const pop = document.getElementById('ext-pop');
    const open = !pop.classList.contains('on');
    if (open) {
      renderExtPop();
      pop.classList.add('on');
      // 点击外部关闭：注册一次性 listener
      setTimeout(() => document.addEventListener('click', closeExtPopOnOutside), 0);
    } else {
      closeExtPop();
    }
  }
  function closeExtPop() {
    document.getElementById('ext-pop').classList.remove('on');
    document.removeEventListener('click', closeExtPopOnOutside);
  }
  function closeExtPopOnOutside() { closeExtPop(); }

  function toggleExt(ext) {
    const i = selectedExtensions.indexOf(ext);
    if (i >= 0) selectedExtensions.splice(i, 1);
    else selectedExtensions.push(ext);
    renderExtPop();
    updateExtBtn();
  }
  function clearExtensions() {
    selectedExtensions = [];
    renderExtPop();
    updateExtBtn();
  }
  function updateExtBtn() {
    const btn = document.getElementById('ext-open-btn');
    const badge = document.getElementById('ext-badge');
    if (!btn) return;
    const n = selectedExtensions.length;
    if (n > 0) {
      btn.className = 'btn-filter active';
      btn.firstChild.textContent = '🗂 已选 ';
      badge.style.display = '';
      badge.textContent = n;
    } else {
      btn.className = 'btn-filter';
      btn.firstChild.textContent = '🗂 全部 ';
      badge.style.display = 'none';
    }
  }
  function buildExtensionFilter(exts) {
    if (!exts || !exts.length) return '';
    if (exts.length === 1) return `file_extension == '${exts[0]}'`;
    return `file_extension == r'(${exts.join('|')})'`;
  }
  // 从表达式里提取已经写在 file_extension == r'(...)' / 'xxx' 里的扩展名列表
  function parseExtensionsFromFilter(filter) {
    if (!filter) return [];
    const reg = filter.match(/file_extension\s*==\s*r'\(([^)]+)\)'/i);
    if (reg) return reg[1].split('|').map(s => s.trim().toLowerCase()).filter(Boolean);
    const single = filter.match(/file_extension\s*==\s*'([^']+)'/i);
    if (single) return [single[1].trim().toLowerCase()];
    return [];
  }

  // ── 加载配置 ──
  function parseFilterDisplay(filter) {
    if (!filter) return '';
    const startM = filter.match(/message_date\s*>=\s*(\d{4}-\d{2}-\d{2})/);
    const endM   = filter.match(/message_date\s*<=\s*(\d{4}-\d{2}-\d{2})/);
    const exts   = parseExtensionsFromFilter(filter);
    const parts = [];
    if (startM || endM) {
      if (startM && endM) parts.push('📅 ' + startM[1] + ' 至 ' + endM[1]);
      else if (startM)   parts.push('📅 ' + startM[1] + ' 起');
      else               parts.push('📅 至 ' + endM[1]);
    }
    if (exts.length) parts.push('🗂 ' + exts.join(', '));
    // 检查是否还有日期/扩展名之外的自定义条件
    const rest = filter
      .replace(/message_date\s*>=\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/gi, '')
      .replace(/message_date\s*<=\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/gi, '')
      .replace(/file_extension\s*==\s*r'\([^)]+\)'/gi, '')
      .replace(/file_extension\s*==\s*'[^']+'/gi, '')
      .replace(/\band\b/gi, '').trim();
    if (rest) parts.push('🔍 ' + rest);
    if (!parts.length) parts.push('🔍 ' + filter);
    return parts.join(' · ');
  }

  function loadConfig() {
    fetch('/api/get_config').then(r => r.json()).then(d => {
      const p = d.save_path || '默认路径';
      const el = document.getElementById('cfg-path');
      el.textContent = p; el.title = p;
      document.getElementById('path-input').value = d.save_path || '';
    }).catch(() => {});
  }

  function togglePathEdit() {
    const area = document.getElementById('path-edit-area');
    const btn  = document.getElementById('edit-path-btn');
    const open = !area.classList.contains('on');
    area.classList.toggle('on', open);
    btn.textContent = open ? '收起' : '编辑';
    if (open) document.getElementById('path-input').focus();
  }

  function savePath() {
    const val = document.getElementById('path-input').value.trim();
    if (!val) { toast('路径不能为空', 'err'); return; }
    fetch('/api/save_path', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ save_path: val }) })
      .then(r => r.json()).then(d => {
        if (d.success) {
          const el = document.getElementById('cfg-path');
          el.textContent = val; el.title = val;
          togglePathEdit(); toast('保存路径已更新');
        } else toast('保存失败：' + (d.error||''), 'err');
      }).catch(e => toast('保存失败：' + e.message, 'err'));
  }

  // ── 历史频道 ──
  function toggleHist() {
    histOpen = !histOpen;
    document.getElementById('hist-body').style.display = histOpen ? '' : 'none';
    document.getElementById('hist-arrow').className = 'hist-arrow' + (histOpen ? '' : ' closed');
  }

  // ── 正在下载 / 已完成 折叠 ──
  function toggleDl() {
    dlOpen = !dlOpen;
    document.getElementById('dl-list').style.display = dlOpen ? '' : 'none';
    document.getElementById('dl-arrow').className = 'hist-arrow' + (dlOpen ? '' : ' closed');
    document.getElementById('dl-head').classList.toggle('collapsed', !dlOpen);
  }
  function toggleDone() {
    doneOpen = !doneOpen;
    document.getElementById('done-list').style.display = doneOpen ? '' : 'none';
    document.getElementById('done-arrow').className = 'hist-arrow' + (doneOpen ? '' : ' closed');
    document.getElementById('done-head').classList.toggle('collapsed', !doneOpen);
    // 分页器跟 done-list 联动，仅在展开且本来就显示时才恢复
    const pager = document.getElementById('done-pager');
    if (!doneOpen) {
      pager.dataset.hiddenByToggle = pager.style.display !== 'none' ? '1' : '';
      pager.style.display = 'none';
    } else if (pager.dataset.hiddenByToggle === '1') {
      pager.style.display = '';
      pager.dataset.hiddenByToggle = '';
    }
  }

  function loadHistory() {
    fetch('/api/channel_history').then(r => r.json()).then(d => {
      const list = document.getElementById('hist-list');
      const clrBtn = document.getElementById('hist-clear-btn');
      if (!d.success || !d.history?.length) {
        list.innerHTML = '<div class="hist-empty">暂无历史记录</div>';
        clrBtn.style.display = 'none'; return;
      }
      clrBtn.style.display = '';
      list.innerHTML = d.history.map(item => `
        <div class="hist-item" id="hi-${item.chat_id}"
          onclick="selHistory('${item.chat_id}','${(item.chat_title||'').replace(/'/g,"\\'")}','${item.chat_type||''}')">
          <div class="hist-emoji">${TICON[item.chat_type]||'📁'}</div>
          <div class="hist-info">
            <div class="hist-name">${item.chat_title||item.chat_id}</div>
            <div class="hist-meta">@${item.chat_id} · ${fmtTime(item.last_used)}</div>
          </div>
          <button class="hist-del" onclick="event.stopPropagation();delHistory('${item.chat_id}')">✕</button>
        </div>`).join('');
    }).catch(() => {});
  }

  function selHistory(chatId, chatTitle, chatType) {
    document.getElementById('channel-input').value = chatId;
    validatedChannel = { valid:true, chat_id:chatId, chat_title:chatTitle||chatId, chat_type:chatType };
    showVal(validatedChannel);
    document.querySelectorAll('.hist-item').forEach(e => e.classList.remove('sel'));
    const el = document.getElementById('hi-' + chatId);
    if (el) el.classList.add('sel');
    toast('已选择: ' + (chatTitle||chatId));
  }

  function delHistory(chatId) {
    fetch('/api/channel_history/' + encodeURIComponent(chatId), { method:'DELETE' })
      .then(r => r.json()).then(d => {
        if (d.success) { loadHistory(); toast('已删除'); }
        else toast('删除失败', 'err');
      });
  }

  function clearHistory() {
    if (!confirm('确定清空所有历史记录？')) return;
    fetch('/api/channel_history/clear', { method:'POST' })
      .then(r => r.json()).then(d => {
        if (d.success) { loadHistory(); toast('已清空'); }
      });
  }

  // ── 频道验证 ──
  function showVal(result) {
    const box = document.getElementById('val-result');
    const icon = document.getElementById('val-icon');
    const name = document.getElementById('val-name');
    const type = document.getElementById('val-type');
    if (result.valid) {
      box.className = 'val-drop ok';
      icon.textContent = TICON[result.chat_type] || '✅';
      name.textContent = result.chat_title;
      type.textContent = TNAME[result.chat_type] || result.chat_type || '';
    } else {
      box.className = 'val-drop err';
      icon.textContent = '❌';
      name.textContent = '验证失败';
      type.textContent = result.error || '无法访问该频道';
    }
  }

  function validateChannel() {
    const channelId = parseChannelId(document.getElementById('channel-input').value);
    if (!channelId) { toast('请输入频道', 'err'); return; }
    const btn = document.getElementById('validate-btn');
    btn.disabled = true; btn.innerHTML = '<span class="spin"></span>';
    document.getElementById('val-result').className = 'val-drop';
    validatedChannel = null;
    fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
      .then(r => r.json()).then(d => {
        showVal(d);
        if (d.valid) { validatedChannel = d; toast('验证成功: ' + d.chat_title); }
        else toast('验证失败: ' + (d.error||''), 'err');
      }).catch(e => { showVal({ valid:false, error: e.message }); toast('验证失败', 'err'); })
      .finally(() => { btn.disabled = false; btn.textContent = '验证'; });
  }

  document.addEventListener('DOMContentLoaded', () => {
    document.getElementById('channel-input').addEventListener('input', () => {
      validatedChannel = null;
      document.getElementById('val-result').className = 'val-drop';
      document.querySelectorAll('.hist-item').forEach(e => e.classList.remove('sel'));
    });
  });

  // ── 下载队列 ──
  let downloadQueue = [];

  function escapeHtml(s) {
    return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
      '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
    })[c]);
  }

  function buildFilterFromForm(startDate, endDate) {
    // 高级过滤器表达式存在时优先用它，文件类型选择被忽略（与日期同样的策略）
    const custom = filterExpression || '';
    if (custom) return custom;
    const segs = [];
    if (startDate) {
      segs.push('message_date >= ' + startDate + ' 00:00:00');
      if (endDate) segs.push('message_date <= ' + endDate + ' 23:59:59');
    }
    const extSeg = buildExtensionFilter(selectedExtensions);
    if (extSeg) segs.push(extSeg);
    return segs.join(' and ');
  }

  // 把队列里的起止日期/扩展名格式化展示；优先用解析 filter（能覆盖日期+文件类型）
  function formatQueueDateRange(startDate, endDate, fallbackFilter) {
    const parsed = parseFilterDisplay(fallbackFilter || '');
    if (parsed) return parsed;
    if (startDate) return '📅 ' + startDate + ' 至 ' + (endDate || startDate);
    return '无过滤条件';
  }

  function renderQueue() {
    const card = document.getElementById('queue-card');
    const list = document.getElementById('queue-list');
    const count = document.getElementById('queue-count');
    if (!downloadQueue.length) { card.style.display = 'none'; return; }
    card.style.display = '';
    count.textContent = downloadQueue.length + ' 个';
    list.innerHTML = downloadQueue.map((q, i) => {
      const dateStr = formatQueueDateRange(q.start_date, q.end_date, q.download_filter);
      return `
      <div class="queue-item">
        <div class="queue-num">${i + 1}</div>
        <div class="queue-info">
          <div class="queue-name">${escapeHtml(q.chat_title || q.chat_id)}</div>
          <div class="queue-filter" title="${escapeHtml(q.download_filter || '')}">${escapeHtml(dateStr)}</div>
        </div>
        <button class="queue-del" onclick="removeFromQueue(${i})" title="移除">✕</button>
      </div>
    `;
    }).join('');
  }

  function addToQueue() {
    const channelId = parseChannelId(document.getElementById('channel-input').value);
    const startDate = document.getElementById('start-date').value;
    const endDate   = document.getElementById('end-date').value;
    if (!channelId) { toast('请输入有效的频道', 'err'); return; }
    if (!startDate) { toast('请选择开始日期', 'err'); return; }
    if (downloadQueue.some(q => q.chat_id === channelId)) {
      toast('该频道已在队列中', 'err'); return;
    }

    const doAdd = (chatTitle, chatType) => {
      downloadQueue.push({
        chat_id: channelId,
        chat_title: chatTitle || channelId,
        chat_type: chatType || '',
        download_filter: buildFilterFromForm(startDate, endDate),
        start_date: startDate,
        end_date: endDate,
      });
      renderQueue();
      // 清空表单以便继续添加下一个频道
      document.getElementById('channel-input').value = '';
      validatedChannel = null;
      const valBox = document.getElementById('val-result');
      if (valBox) valBox.className = 'val-drop';
      toast('已加入队列: ' + (chatTitle || channelId));
    };

    const btn = document.getElementById('add-queue-btn');
    if (validatedChannel && validatedChannel.chat_id === channelId) {
      doAdd(validatedChannel.chat_title, validatedChannel.chat_type);
    } else {
      btn.disabled = true;
      fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
        .then(r => r.json()).then(d => {
          btn.disabled = false;
          if (d.valid) { showVal(d); validatedChannel = d; doAdd(d.chat_title, d.chat_type); }
          else { showVal(d); toast('频道验证失败', 'err'); }
        }).catch(e => { btn.disabled = false; toast('验证失败: ' + e.message, 'err'); });
    }
  }

  function removeFromQueue(idx) {
    downloadQueue.splice(idx, 1);
    renderQueue();
  }

  function clearQueue() {
    if (!downloadQueue.length) return;
    if (!confirm('确认清空队列里的 ' + downloadQueue.length + ' 个频道？')) return;
    downloadQueue = [];
    renderQueue();
  }

  // ── 保存并重启（队列 + 表单一起提交） ──
  function saveAndRestart() {
    const channelId = parseChannelId(document.getElementById('channel-input').value);
    const startDate = document.getElementById('start-date').value;
    const endDate   = document.getElementById('end-date').value;

    const hasForm  = !!channelId;
    const hasQueue = downloadQueue.length > 0;
    if (!hasForm && !hasQueue) { toast('请输入频道或先加入队列', 'err'); return; }
    if (hasForm && !startDate) { toast('请选择开始日期', 'err'); return; }

    const btn = document.getElementById('save-btn');
    btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 重启中…';

    const submitAll = (formItem) => {
      const items = downloadQueue.slice();
      if (formItem && !items.some(q => q.chat_id === formItem.chat_id)) items.push(formItem);
      fetch('/api/save_and_restart_multi', {
        method:'POST', headers:{'Content-Type':'application/json'},
        body: JSON.stringify({ items })
      }).then(r => r.json()).then(d => {
        if (d.success) {
          toast('✅ ' + d.message + '，5秒后刷新…');
          setTimeout(() => location.reload(), 5000);
        } else {
          toast('失败: ' + (d.error||''), 'err');
          btn.disabled = false; btn.textContent = '🔄 保存并重启下载';
        }
      }).catch(e => {
        toast('操作失败: ' + e.message, 'err');
        btn.disabled = false; btn.textContent = '🔄 保存并重启下载';
      });
    };

    if (!hasForm) { submitAll(null); return; }

    const buildFormItem = (chatTitle, chatType) => ({
      chat_id: channelId,
      chat_title: chatTitle || channelId,
      chat_type: chatType || '',
      download_filter: buildFilterFromForm(startDate, endDate),
      start_date: startDate,
      end_date: endDate,
    });

    if (validatedChannel && validatedChannel.chat_id === channelId) {
      submitAll(buildFormItem(validatedChannel.chat_title, validatedChannel.chat_type));
    } else {
      fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
        .then(r => r.json()).then(d => {
          if (d.valid) { showVal(d); validatedChannel = d; submitAll(buildFormItem(d.chat_title, d.chat_type)); }
          else { showVal(d); toast('频道验证失败', 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; }
        }).catch(e => { toast('验证失败: ' + e.message, 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; });
    }
  }

  // ── 暂停/继续 ──
  function applyState(state) {
    dlState = state;
    const dot = document.getElementById('state-dot');
    const txt = document.getElementById('state-txt');
    const btn = document.getElementById('btn-pause');
    if (state === 'pause') {
      dot.className = 'state-dot'; txt.textContent = '下载中';
      btn.className = 'btn-pause'; btn.textContent = '⏸ 暂停';
    } else {
      dot.className = 'state-dot off'; txt.textContent = '已暂停';
      btn.className = 'btn-pause resuming'; btn.textContent = '▶ 继续';
    }
  }

  function toggleDownload() {
    const btn = document.getElementById('btn-pause');
    btn.disabled = true;
    fetch('/set_download_state?state=' + dlState, { method:'POST', credentials:'include' })
      .then(r => { if (r.redirected || r.status === 401) { location.href='/login'; throw 0; } return r.text(); })
      .then(resp => {
        applyState(resp);
        if (resp === 'pause') toast('已恢复下载');
        else { toast('已暂停下载'); document.getElementById('h-speed').textContent = '0 B/s'; }
      }).catch(() => {}).finally(() => { btn.disabled = false; });
  }

  // ── 任务队列总览渲染 ──
  function renderTaskQueue(p) {
    const banner = document.getElementById('task-banner');
    const list = document.getElementById('qbanner-list');
    const summary = document.getElementById('qbanner-summary');

    const queue = Array.isArray(p.task_queue) ? p.task_queue : [];
    const completed = Array.isArray(p.completed_chats) ? p.completed_chats : [];
    const currentChat = p.current_chat || '';

    // 若没有队列也没有当前任务，隐藏整个 banner
    if (!queue.length && !currentChat) {
      banner.classList.remove('show');
      list.innerHTML = '';
      summary.textContent = '';
      return;
    }
    banner.classList.add('show');

    // completed 按 chat_id 建索引
    const completedMap = {};
    completed.forEach(c => { completedMap[c.chat_id] = c; });

    // 兜底：队列为空但有 current（多频道队列元信息还没写入时），用 current 顶一下
    const effectiveQueue = queue.length
      ? queue
      : [{ chat_id: currentChat, chat_title: p.current_chat_title || currentChat }];

    const paused = dlState === 'continue';
    let doneCount = 0, runningCount = 0;

    list.innerHTML = effectiveQueue.map(q => {
      if (completedMap[q.chat_id]) {
        doneCount++;
        return renderDoneCard(q, completedMap[q.chat_id]);
      } else if (q.chat_id === currentChat) {
        runningCount++;
        return renderActiveCard(q, p, paused);
      } else {
        return renderPendingCard(q);
      }
    }).join('');

    summary.textContent = `${doneCount}/${effectiveQueue.length} 完成`;
  }

  function renderDoneCard(q, c) {
    const title = escapeHtml(q.chat_title || q.chat_id);
    const skipStr = (c.existing_skip && c.existing_skip > 0)
      ? `跳过${c.skip||0}，其中本次跳过${c.existing_skip}`
      : `跳过${c.skip||0}`;
    const totalStr = (c.total && c.total > 0)
      ? `${c.done||0} / ${c.total} 完成`
      : `${c.done||0} 个完成`;
    const filterLine = renderFilterLine(q.download_filter);
    return `
      <div class="qcard qcard-done">
        <div class="qcard-icon">✅</div>
        <div class="qcard-main">
          <div class="qcard-name">${title}</div>
          ${filterLine}
          <div class="qcard-state">${totalStr}，${skipStr}</div>
        </div>
      </div>`;
  }

  function renderFilterLine(filter) {
    const txt = parseFilterDisplay(filter || '');
    if (!txt) return '';
    return `<div class="qcard-state" style="color:var(--muted);">${escapeHtml(txt)}</div>`;
  }

  function renderActiveCard(q, p, paused) {
    const title = escapeHtml(q.chat_title || p.current_chat_title || q.chat_id);
    const dl = p.downloading_files||0, done = p.completed_files||0, skip = p.skipped_files||0;
    const qual = p.qualified_files||0, est = p.estimated_total||0;
    const existingSkip = p.existing_skipped || 0;
    const rawTotal = est || qual;
    const realTotal = Math.max(0, rawTotal - existingSkip);
    const doneStr = rawTotal ? `${done} / ${realTotal}` : `${done}`;
    const skipStr = existingSkip > 0 ? `跳过${skip}，其中本次跳过${existingSkip}` : `跳过${skip}`;

    let st = '';
    if (p.is_checking && paused)    st = `⏸ 已暂停 · 扫描中… (${skipStr})`;
    else if (p.is_checking && dl>0) st = `🔍 扫描+下载中 (${dl}个，${doneStr}，${skipStr})`;
    else if (p.is_checking)         st = `🔍 扫描中… (${doneStr}，${skipStr})`;
    else if (paused)                st = `⏸ 已暂停 (${doneStr}，${skipStr})`;
    else if (dl>0)                  st = `🚀 下载中 (${dl}个，${doneStr}，${skipStr})`;
    else if (done>0||skip>0)        st = `✅ 完成 (${doneStr}，${skipStr})`;
    else                            st = '⏳ 等待开始';

    let progBar = '';
    if (realTotal > 0) {
      const pct = Math.min(100, Math.round(done * 100 / realTotal));
      const suffix = (!est && p.is_checking) ? '（扫描中…）' : '';
      progBar = `
        <div class="qcard-prog"><div class="qcard-prog-fill" style="width:${pct}%"></div></div>
        <div class="qcard-state" style="margin-top:3px;">${done} / ${realTotal}${suffix}</div>`;
    }

    const filterLine = renderFilterLine(q.download_filter);
    return `
      <div class="qcard qcard-active">
        <div class="qcard-icon">🚀</div>
        <div class="qcard-main">
          <div class="qcard-name">${title}</div>
          ${filterLine}
          <div class="qcard-state">${st}</div>
          ${progBar}
        </div>
      </div>`;
  }

  function renderPendingCard(q) {
    const title = escapeHtml(q.chat_title || q.chat_id);
    const filterLine = renderFilterLine(q.download_filter);
    return `
      <div class="qcard qcard-pending">
        <div class="qcard-icon">⏳</div>
        <div class="qcard-main">
          <div class="qcard-name">${title}</div>
          ${filterLine}
          <div class="qcard-state">排队中…</div>
        </div>
      </div>`;
  }

  // ── 轮询 ──
  function poll() {
    fetch('/get_download_status').then(r => r.json()).then(d => {
      document.getElementById('h-speed').textContent = d.download_speed || '0 B/s';
      applyState(d.state);
    }).catch(() => {});

    fetch('/api/task_progress').then(r => r.json()).then(p => {
      // h-skip 要跨频道累加：本次运行里已完成频道的 skip 汇总 + 当前频道实时的 skipped_files
      // 否则切频道时 reset_task_progress 会把计数清零，视觉上会「跳过数突然归零」
      const completed = Array.isArray(p.completed_chats) ? p.completed_chats : [];
      const completedSkip = completed.reduce((s, c) => s + (c.skip || 0), 0);
      document.getElementById('h-skip').textContent = completedSkip + (p.skipped_files || 0);
      renderTaskQueue(p);
    }).catch(() => {});

    fetch('/get_download_list?already_down=false').then(r => r.json()).then(data => {
      const active = data.filter(d => d.download_progress < 100);
      document.getElementById('h-dl').textContent = active.length;
      document.getElementById('h-done').textContent = data.filter(d => d.download_progress >= 100).length;
      document.getElementById('dl-badge').textContent = active.length + ' 个文件';
      const list = document.getElementById('dl-list');
      if (!active.length) {
        list.innerHTML = '<div class="empty"><svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>暂无下载任务</div>';
        return;
      }
      list.innerHTML = active.map(item => `
        <div class="dl-item">
          <div class="dl-top">
            <div class="dl-name" title="${item.filename}">${item.filename}</div>
            <span class="dl-pct">${item.download_progress}%</span>
            <span class="dl-spd">${item.download_speed||''}</span>
            <div class="dl-ctrl">
              <button class="dl-btn ${item.paused?'dl-paused':''}"
                onclick="pauseItem('${item.chat}','${item.id}',this)"
                title="${item.paused?'继续下载':'暂停下载'}">${item.paused?'▶':'⏸'}</button>
              <button class="dl-btn dl-skip"
                onclick="skipItem('${item.chat}','${item.id}','${(item.filename||'').replace(/'/g,'')}',this)"
                title="跳过此文件">✕</button>
            </div>
          </div>
          <div class="prog"><div class="prog-fill ${item.download_progress>=100?'done':''}" style="width:${item.download_progress}%"></div></div>
        </div>`).join('');
    }).catch(() => {});

    loadDonePage();
  }

  function loadDonePage() {
    fetch(`/get_download_list?already_down=true&page=${donePage}&page_size=${donePageSize}`)
      .then(r => r.json()).then(resp => {
        const { items, total, page, total_pages } = resp;
        document.getElementById('done-badge').textContent = total + ' 个文件';
        const list = document.getElementById('done-list');
        if (!items.length) {
          list.innerHTML = '<div class="empty">暂无已完成文件</div>';
          document.getElementById('done-pager').style.display = 'none';
          return;
        }
        list.innerHTML = items.map(item => {
          const isSkip = item.status === 'skip';
          return `<div class="done-item">
            <span class="done-tick ${isSkip?'skipped':''}">${isSkip?'⊘':'✓'}</span>
            <span class="done-name" title="${item.filename||''}">${item.filename||'(未知文件)'}</span>
            ${isSkip?'<span class="done-skip-tag">跳过</span>':''}
            <span class="done-size">${isSkip?'':item.total_size||''}</span>
            ${isSkip?`<button class="done-undo" onclick="undoSkip('${item.chat_id||item.chat}','${item.id}',this)">撤销</button>`:''}
          </div>`;
        }).join('');
        const pager = document.getElementById('done-pager');
        if (total_pages <= 1) {
          pager.style.display = 'none';
        } else {
          // 卡片被折叠时不显示分页器，但记下"原本该显示"的状态，展开后由 toggleDone 恢复
          if (doneOpen) {
            pager.style.display = 'flex';
          } else {
            pager.style.display = 'none';
            pager.dataset.hiddenByToggle = '1';
          }
          document.getElementById('done-pager-info').textContent =
            `第 ${page} / ${total_pages} 页，共 ${total} 条`;
          document.getElementById('pg-prev').disabled = page <= 1;
          document.getElementById('pg-next').disabled = page >= total_pages;
        }
      }).catch(() => {});
  }

  function donePageGo(delta) {
    donePage = Math.max(1, donePage + delta);
    loadDonePage();
  }

  // ── 初始化 ──
  applyState(dlState);
  document.getElementById('start-date').value = '2024-01-01';
  fetch('/get_app_version').then(r => r.text()).then(v => document.getElementById('app-ver').textContent = v).catch(() => {});
  loadConfig();
  loadHistory();
  poll();
  setInterval(poll, 2000);
  checkSetup();

  // ════════════════════════════
  //  设置向导
  // ════════════════════════════
  function checkSetup() {
    fetch('/api/setup_status').then(r => r.json()).then(s => {
      setupStatus = s;
      if (!s.has_api_credentials) {
        if (s.proxy?.hostname) {
          document.getElementById('w-proxy-on').checked = true;
          document.getElementById('w-proxy-host').value = s.proxy.hostname || '127.0.0.1';
          document.getElementById('w-proxy-port').value = s.proxy.port || 7891;
          if (s.proxy.scheme === 'http') document.getElementById('w-proxy-scheme').value = 'http';
          toggleProxyFields();
        }
        if (s.save_path) document.getElementById('w-save-path').value = s.save_path;
        document.getElementById('wiz-mask').classList.add('on');
        wizGoto(0);
      }
    }).catch(() => {});
  }

  function wizGoto(step) {
    wizStep = step;
    for (let i = 0; i < 3; i++) {
      document.getElementById('wp-' + i).style.display = i === step ? '' : 'none';
      const ws = document.getElementById('ws-' + i);
      ws.className = 'ws' + (i < step ? ' done' : i === step ? ' active' : '');
      ws.querySelector('.ws-num').textContent = i < step ? '✓' : (i + 1);
    }
    document.getElementById('w-back').style.display = step === 0 ? 'none' : '';
    document.getElementById('w-next').textContent = step === 2 ? '💾 保存并启动' : '下一步 →';
    if (step === 2) buildWizSummary();
  }

  function wizBack() { if (wizStep > 0) wizGoto(wizStep - 1); }

  function wizNext() {
    if (wizStep === 0) {
      const id = document.getElementById('w-api-id').value.trim();
      const hash = document.getElementById('w-api-hash').value.trim();
      if (!id || isNaN(+id) || +id === 0) { toast('请输入有效的 api_id', 'err'); return; }
      if (!hash || hash.length < 8) { toast('请输入有效的 api_hash', 'err'); return; }
      wizGoto(1);
    } else if (wizStep === 1) {
      const on = document.getElementById('w-proxy-on').checked;
      if (on && !document.getElementById('w-proxy-host').value.trim()) { toast('请填写代理地址', 'err'); return; }
      wizGoto(2);
    } else {
      doWizSave();
    }
  }

  function toggleProxyFields() {
    const on = document.getElementById('w-proxy-on').checked;
    document.getElementById('w-proxy-fields').className = 'proxy-fields' + (on ? ' on' : '');
  }

  function buildWizSummary() {
    const id = document.getElementById('w-api-id').value.trim();
    const hash = document.getElementById('w-api-hash').value.trim();
    const on = document.getElementById('w-proxy-on').checked;
    const host = document.getElementById('w-proxy-host').value.trim();
    const port = document.getElementById('w-proxy-port').value.trim();
    const scheme = document.getElementById('w-proxy-scheme').value;
    const path = document.getElementById('w-save-path').value.trim();
    const masked = hash.length > 8 ? hash.slice(0,4) + '****' + hash.slice(-4) : '****';
    document.getElementById('w-summary').innerHTML = `
      <div class="sum-row"><span class="sum-k">api_id</span><span class="sum-v">${id}</span></div>
      <div class="sum-row"><span class="sum-k">api_hash</span><span class="sum-v">${masked}</span></div>
      <div class="sum-row"><span class="sum-k">代理</span><span class="sum-v">${on ? scheme+'://'+host+':'+port : '未启用'}</span></div>
      <div class="sum-row"><span class="sum-k">保存路径</span><span class="sum-v">${path||'使用当前配置'}</span></div>`;
    if (setupStatus.has_session) {
      document.getElementById('w-session-ok').style.display = '';
      document.getElementById('w-login-notice').style.display = 'none';
    } else {
      document.getElementById('w-session-ok').style.display = 'none';
      document.getElementById('w-login-notice').style.display = '';
    }
  }

  // ════════════════════════════
  //  代理测试（向导内）
  // ════════════════════════════
  function testWizProxy() {
    const btn = document.getElementById('w-proxy-test-btn');
    const result = document.getElementById('w-proxy-test-result');
    btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 测试中…';
    result.textContent = '';

    const scheme = document.getElementById('w-proxy-scheme').value;
    const host   = document.getElementById('w-proxy-host').value.trim();
    const port   = +document.getElementById('w-proxy-port').value || 0;

    fetch('/api/test_proxy', {
      method: 'POST', headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({ scheme, hostname: host, port })
    }).then(r => r.json()).then(d => {
      if (d.success) {
        result.style.color = 'var(--green)';
        result.textContent = '✓ ' + d.message;
      } else {
        result.style.color = 'var(--red)';
        result.textContent = '✗ ' + d.error;
      }
    }).catch(e => {
      result.style.color = 'var(--red)';
      result.textContent = '✗ 请求失败：' + e.message;
    }).finally(() => {
      btn.disabled = false; btn.textContent = '🔌 测试代理';
    });
  }

  // ════════════════════════════
  //  过滤器弹窗
  // ════════════════════════════
  let filterRules = [];         // [{field, op, value, unit, negate}]
  let filterExpression = '';    // 最终生成/手动输入的表达式
  let filterMode = 'visual';    // 'visual' | 'manual'

  const F_FIELDS = [
    { value: 'message_date',   label: '消息日期',  type: 'date' },
    { value: 'media_file_size',label: '文件大小',  type: 'size' },
    { value: 'media_width',    label: '视频宽度',  type: 'number', unit: 'px' },
    { value: 'media_height',   label: '视频高度',  type: 'number', unit: 'px' },
    { value: 'media_duration', label: '视频时长',  type: 'duration' },
    { value: 'media_file_name',label: '文件名',    type: 'string' },
    { value: 'message_caption',label: '消息说明',  type: 'string' },
  ];

  const F_OPS = {
    date:     [['>=','在此之后'],['<=','在此之前'],['>','晚于'],['<','早于']],
    size:     [['>=','大于等于'],['<=','小于等于'],['>','大于'],['<','小于']],
    number:   [['>=','≥'],['<=','≤'],['>','>'],['<','<'],['==','等于']],
    duration: [['>=','大于等于'],['<=','小于等于'],['>','大于'],['<','小于']],
    string:   [['contains','包含'],['matches','正则匹配'],['==','等于'],['!=','不等于']],
  };

  function getFieldType(fieldVal) {
    return (F_FIELDS.find(f => f.value === fieldVal) || {}).type || 'string';
  }

  function ruleToExpr(r) {
    if (!r.field || !r.op || r.value === '') return null;
    const ftype = getFieldType(r.field);
    let val = r.value;
    let expr = '';
    if (ftype === 'date') {
      expr = `${r.field} ${r.op} ${val} 00:00:00`;
    } else if (ftype === 'size') {
      const bytes = sizeToBytes(val, r.unit || 'MB');
      expr = `${r.field} ${r.op} ${bytes}`;
    } else if (ftype === 'duration') {
      const secs = durationToSeconds(val, r.unit || 'sec');
      expr = `${r.field} ${r.op} ${secs}`;
    } else if (ftype === 'string') {
      if (r.op === 'contains' || r.op === 'matches') {
        expr = `${r.field} ${r.op} '${val}'`;
      } else {
        expr = `${r.field} ${r.op} '${val}'`;
      }
    } else {
      expr = `${r.field} ${r.op} ${val}`;
    }
    return r.negate ? `not (${expr})` : expr;
  }

  function sizeToBytes(val, unit) {
    const n = parseFloat(val) || 0;
    const m = { 'B':1, 'KB':1024, 'MB':1024**2, 'GB':1024**3 };
    return Math.round(n * (m[unit] || 1024**2));
  }

  function durationToSeconds(val, unit) {
    const n = parseFloat(val) || 0;
    return unit === 'min' ? Math.round(n * 60) : Math.round(n);
  }

  function buildExprFromRules() {
    return filterRules.map(ruleToExpr).filter(Boolean).join(' and ');
  }

  function renderRules() {
    const container = document.getElementById('filter-rules');
    if (!filterRules.length) {
      container.innerHTML = '<div style="text-align:center;padding:14px 0;color:var(--muted);font-size:12px;">暂无条件，点击下方「添加条件」</div>';
      updateFilterPreview('');
      return;
    }
    container.innerHTML = filterRules.map((r, i) => {
      const ftype = getFieldType(r.field);
      const ops = F_OPS[ftype] || F_OPS.string;
      const fieldOpts = F_FIELDS.map(f => `<option value="${f.value}"${f.value===r.field?' selected':''}>${f.label}</option>`).join('');
      const opOpts = ops.map(([v,l]) => `<option value="${v}"${v===r.op?' selected':''}>${l}</option>`).join('');

      let valInput = '';
      if (ftype === 'date') {
        valInput = `<input class="rule-val" type="date" value="${r.value||''}" oninput="updateRule(${i},'value',this.value)">`;
      } else if (ftype === 'size') {
        const unitOpts = ['B','KB','MB','GB'].map(u=>`<option${u===(r.unit||'MB')?' selected':''}>${u}</option>`).join('');
        valInput = `<input class="rule-val" type="number" min="0" placeholder="数值" value="${r.value||''}" oninput="updateRule(${i},'value',this.value)" style="flex:1.2">
          <select class="rule-unit" onchange="updateRule(${i},'unit',this.value)">${unitOpts}</select>`;
      } else if (ftype === 'duration') {
        const unitOpts = ['秒','分钟'].map((u,ui)=>`<option value="${ui?'min':'sec'}"${(r.unit||'sec')===(ui?'min':'sec')?' selected':''}>${u}</option>`).join('');
        valInput = `<input class="rule-val" type="number" min="0" placeholder="数值" value="${r.value||''}" oninput="updateRule(${i},'value',this.value)" style="flex:1.2">
          <select class="rule-unit" onchange="updateRule(${i},'unit',this.value)">${unitOpts}</select>`;
      } else {
        valInput = `<input class="rule-val" type="text" placeholder="${ftype==='string'?'输入文本或正则':'数值'}" value="${r.value||''}" oninput="updateRule(${i},'value',this.value)">`;
      }

      return `<div class="filter-rule">
        <select class="rule-field" onchange="updateRule(${i},'field',this.value)">${fieldOpts}</select>
        <select class="rule-op" onchange="updateRule(${i},'op',this.value)">${opOpts}</select>
        ${valInput}
        <label class="rule-negate"><input type="checkbox" ${r.negate?'checked':''} onchange="updateRule(${i},'negate',this.checked)">排除</label>
        <button class="rule-del" onclick="deleteRule(${i})">✕</button>
      </div>`;
    }).join('');

    updateFilterPreview(buildExprFromRules());
  }

  function updateRule(idx, key, val) {
    if (key === 'field') {
      // 切换字段时重置op和value
      const ftype = getFieldType(val);
      filterRules[idx] = { ...filterRules[idx], field: val, op: (F_OPS[ftype]||F_OPS.string)[0][0], value: '', unit: '' };
    } else {
      filterRules[idx][key] = val;
    }
    renderRules();
  }

  function addFilterRule() {
    filterRules.push({ field: 'message_date', op: '>=', value: '', unit: '', negate: false });
    renderRules();
  }

  function deleteRule(idx) {
    filterRules.splice(idx, 1);
    renderRules();
  }

  function updateFilterPreview(expr) {
    const el = document.getElementById('filter-expr-preview');
    const tip = document.getElementById('filter-val-tip');
    if (!expr) {
      el.textContent = '（无过滤条件）';
      el.className = 'filter-preview-expr empty';
      tip.style.display = 'none';
    } else {
      el.textContent = expr;
      el.className = 'filter-preview-expr';
      tip.style.display = 'none';
    }
  }

  function switchFilterTab(mode) {
    filterMode = mode;
    document.getElementById('ftab-visual').className = 'filter-tab' + (mode==='visual'?' on':'');
    document.getElementById('ftab-manual').className = 'filter-tab' + (mode==='manual'?' on':'');
    document.getElementById('fv-visual').className = 'filter-visual' + (mode==='visual'?'':' hide');
    document.getElementById('fv-manual').className = 'filter-manual' + (mode==='manual'?' on':'');
    if (mode === 'manual') {
      const expr = buildExprFromRules() || filterExpression;
      document.getElementById('filter-manual-input').value = expr;
      updateFilterPreview(expr);
    }
  }

  function onManualFilterInput() {
    const expr = document.getElementById('filter-manual-input').value.trim();
    updateFilterPreview(expr);
  }

  function validateFilterExpr() {
    const expr = filterMode === 'manual'
      ? document.getElementById('filter-manual-input').value.trim()
      : buildExprFromRules();
    if (!expr) { toast('没有要校验的表达式', 'err'); return; }

    const btn = document.getElementById('filter-validate-btn');
    const txt = document.getElementById('fvbtn-txt');
    btn.disabled = true; txt.innerHTML = '<span class="spin"></span>';

    fetch('/api/validate_filter', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({ filter: expr })
    }).then(r => r.json()).then(d => {
      const tip = document.getElementById('filter-val-tip');
      tip.style.display = 'flex';
      if (d.valid) {
        tip.className = 'filter-val-tip ok';
        tip.textContent = '✓ 语法正确';
      } else {
        tip.className = 'filter-val-tip err';
        tip.textContent = '✗ ' + (d.error || '语法错误');
      }
    }).catch(e => toast('校验失败：' + e.message, 'err'))
      .finally(() => { btn.disabled = false; txt.textContent = '校验语法'; });
  }

  function openFilterModal() {
    renderRules();
    if (filterExpression && filterMode === 'manual') {
      document.getElementById('filter-manual-input').value = filterExpression;
    }
    document.getElementById('filter-mask').classList.add('on');
  }

  function closeFilterModal() {
    document.getElementById('filter-mask').classList.remove('on');
    document.getElementById('filter-val-tip').style.display = 'none';
  }

  function clearFilter() {
    filterRules = [];
    filterExpression = '';
    document.getElementById('filter-manual-input').value = '';
    renderRules();
    updateFilterBtn(0);
  }

  function applyFilter() {
    let expr = '';
    if (filterMode === 'manual') {
      expr = document.getElementById('filter-manual-input').value.trim();
    } else {
      expr = buildExprFromRules();
    }
    filterExpression = expr;
    updateFilterBtn(filterMode === 'manual' ? (expr ? 1 : 0) : filterRules.length);
    closeFilterModal();
    toast(expr ? `已应用 ${filterMode==='manual'?'自定义':filterRules.length+' 条'}过滤条件` : '已清除过滤条件');
  }

  function updateFilterBtn(count) {
    const btn = document.getElementById('filter-open-btn');
    const badge = document.getElementById('filter-badge');
    if (count > 0) {
      btn.className = 'btn-filter active';
      badge.style.display = '';
      badge.textContent = count;
    } else {
      btn.className = 'btn-filter';
      badge.style.display = 'none';
    }
  }

  // ── 单条下载控制 ──
  function pauseItem(chat, id, btn) {
    const isPaused = btn.classList.contains('dl-paused');
    const action = isPaused ? 'resume' : 'pause';
    fetch('/api/message_control', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({chat_id: chat, message_id: +id, action})
    }).then(r => r.json()).then(d => {
      if (d.success) {
        btn.classList.toggle('dl-paused', !isPaused);
        btn.textContent = isPaused ? '⏸' : '▶';
        btn.title = isPaused ? '暂停下载' : '继续下载';
      } else toast(d.error || '操作失败', 'err');
    }).catch(() => toast('请求失败', 'err'));
  }

  function skipItem(chat, id, fileName, btn) {
    btn.disabled = true;
    fetch('/api/message_control', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({chat_id: chat, message_id: +id, action: 'skip', file_name: fileName})
    }).then(r => r.json()).then(d => {
      if (!d.success) { toast(d.error || '跳过失败', 'err'); btn.disabled = false; }
    }).catch(() => { toast('请求失败', 'err'); btn.disabled = false; });
  }

  function undoSkip(chatId, id, btn) {
    btn.disabled = true;
    fetch('/api/undo_skip', {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({chat_id: chatId, message_id: +id})
    }).then(r => r.json()).then(d => {
      if (d.success) toast('已撤销跳过，下次运行将重新下载');
      else { toast(d.error || '撤销失败', 'err'); btn.disabled = false; }
    }).catch(() => { toast('请求失败', 'err'); btn.disabled = false; });
  }

  function doWizSave() {
    const btn = document.getElementById('w-next');
    btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 保存中…';
    fetch('/api/save_initial_config', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({
        api_id:         +document.getElementById('w-api-id').value.trim(),
        api_hash:        document.getElementById('w-api-hash').value.trim(),
        proxy_enabled:   document.getElementById('w-proxy-on').checked,
        proxy_scheme:    document.getElementById('w-proxy-scheme').value,
        proxy_hostname:  document.getElementById('w-proxy-host').value.trim(),
        proxy_port:     +document.getElementById('w-proxy-port').value.trim() || 7891,
        save_path:       document.getElementById('w-save-path').value.trim(),
      })
    }).then(r => r.json()).then(d => {
      if (d.success) {
        document.getElementById('wiz-mask').classList.remove('on');
        toast('✅ 配置已保存，重启中…');
        setTimeout(() => location.reload(), 6000);
      } else { toast('保存失败：' + (d.error||''), 'err'); btn.disabled = false; btn.textContent = '💾 保存并启动'; }
    }).catch(e => { toast('保存失败：' + e.message, 'err'); btn.disabled = false; btn.textContent = '💾 保存并启动'; });
  }
</script>
</body>
</html>
