'use strict';

let currentDW = 240;
let api = null;
let sortables = [];
let groupSortable = null;
let groupNavSortable = null;
let lastSuggested = null;
let uidCounter = 0;
let groupUidCounter = 0;
let currentProjectList = [];
let lang = 'zh';
let authUser = null;

const selection = new Set();   // 选中的 uid
const projectSelection = new Set(); // 首页选中的项目 id
let currentProjectId = null;
let activeGroupId = null;
let anchorUid = null;
let justDragged = false;
let draggingMulti = false;
let clipboard = [];            // 复制的 clip 数据
let promptClipboard = "";       // 单条镜头提示词剪贴板
let previewEl = null;
let previewPromptOrig = '';     // 进入预览时的提示词原值，取消时回退
let previewFs = false;          // 预览是否全屏
let groupNavClickTimer = null;
let libName = '已分组';          // 当前项目整理结果顶层文件夹名
let promptPanelEl = null;
let pickedFolder = null;
let saveTimer = null;
let unlocked = false;          // 已入库内容是否解锁可编辑
let fullDirty = false;         // 是否改动了已入库内容（需全量重整）
let collapsedAll = false;      // 是否全部折叠（只看组标题行）
let projectMarquee = { active: false, x0: 0, y0: 0, additive: false, moved: false };
let suppressProjectBlankClick = false;

const $ = (id) => document.getElementById(id);
const SVG = {
  lock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
  unlock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>',
  trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M6 6l1 14a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-14"/></svg>',
  edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>',
  maximize: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M16 3h3a2 2 0 0 1 2 2v3"/><path d="M21 16v3a2 2 0 0 1-2 2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/></svg>',
  folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/></svg>',
  layers: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3 9 5-9 5-9-5Z"/><path d="m3 12 9 5 9-5"/><path d="m3 16 9 5 9-5"/></svg>',
};
const I18N = {
  zh: {
    settings: '设置', settingsTitle: '设置', close: '关闭', language: '语言',
    languageHint: '切换界面主要文字。素材名和项目名不会被翻译。',
    tutorialTitle: '使用教程', lightMode: '浅色模式', darkMode: '深色模式',
    locked: '编辑已锁定', unlocked: '编辑已解锁', myProjects: '我的项目',
    projects: '项目', groups: '分组', allShots: '全部镜头', materialDesk: '素材整理台',
    newProject: '＋ 新建项目', deleteSelected: '删除选中', noPreview: '无预览',
    noProjects: '还没有项目。点击右上角「新建项目」，选一个装短视频的文件夹开始。',
    analyze: '开始分析', scan: '扫描新增', similarity: '相似度', auto: '自动建议',
    selectedNone: '未选中', collapseAll: '折叠全部组', expandAll: '展开全部组',
    thumbSize: '画面大小', copy: '复制', move: '移动', openResult: '打开结果',
    apply: '确认并执行', rebuildApply: '确认并执行（重建整理）',
    allHint: '全部镜头页可以调相似度、拖动分组、确认整理。单击选择，双击预览，右键可加提示词、移动到其他组、设为封面。',
    groupHint: '单组页只处理这一组素材：选择、预览、标注、提示词、移动素材。要调整相似度或执行整理，请回到「全部镜头」。',
    tutorialHtml: '<ol><li>新建项目：点「新建项目」，选择装短视频的文件夹。</li><li>开始分析：波谱会按镜头相似度自动分组。</li><li>调整分组：在「全部镜头」里调相似度、拖动素材或分组；不同前缀的组不会互相混放。</li><li>单组处理：点左侧单组，只处理该组素材，可预览、标注过/保/废、添加提示词。</li><li>锁定保护：整理过的内容默认锁定；需要改已整理内容时先点「编辑已锁定」。</li><li>确认执行：默认复制；选择移动会有确认。删除原文件会进废纸篓。</li></ol>',
  },
  en: {
    settings: 'Settings', settingsTitle: 'Settings', close: 'Close', language: 'Language',
    languageHint: 'Switch main interface text. File names and project names are not translated.',
    tutorialTitle: 'Tutorial', lightMode: 'Light', darkMode: 'Dark',
    locked: 'Editing locked', unlocked: 'Editing unlocked', myProjects: 'My Projects',
    projects: 'Projects', groups: 'Groups', allShots: 'All Clips', materialDesk: 'Asset Desk',
    newProject: '+ New Project', deleteSelected: 'Delete selected', noPreview: 'No preview',
    noProjects: 'No projects yet. Click “New Project” and choose a folder of short videos.',
    analyze: 'Analyze', scan: 'Scan New', similarity: 'Similarity', auto: 'Auto',
    selectedNone: 'None selected', collapseAll: 'Collapse groups', expandAll: 'Expand groups',
    thumbSize: 'Thumbnail size', copy: 'Copy', move: 'Move', openResult: 'Open Result',
    apply: 'Confirm & Run', rebuildApply: 'Confirm & Run (rebuild)',
    allHint: 'All Clips lets you tune similarity, reorder groups, and run organization. Click to select, double-click to preview, right-click for prompts, moving, and cover frames.',
    groupHint: 'Single-group view is for working on this group only: select, preview, tag, prompt, and move clips. Return to All Clips to tune similarity or run organization.',
    tutorialHtml: '<ol><li>Create a project: click “New Project” and choose your video folder.</li><li>Analyze: Bopu groups clips by visual similarity.</li><li>Refine groups: in All Clips, tune similarity and drag clips or groups. Groups with different prefixes are kept separate.</li><li>Work in one group: use the left sidebar to focus a group, preview, tag Pass/Keep/Drop, and add prompts.</li><li>Lock protection: organized clips are locked by default. Unlock before changing organized content.</li><li>Run organization: Copy is the default. Move asks for confirmation. Deleting original files sends them to Trash.</li></ol>',
  },
};
function tr(key) { return (I18N[lang] && I18N[lang][key]) || I18N.zh[key] || key; }
function iconLabel(icon, label) { return `${icon}<span>${label}</span>`; }
function setIconButton(btn, icon, label) {
  if (!btn) return;
  btn.innerHTML = iconLabel(icon, label);
  btn.title = label;
}
function initTheme() {
  const theme = localStorage.getItem('bopu-theme') || 'dark';
  document.body.classList.toggle('light-mode', theme === 'light');
  updateThemeButton();
}
function toggleTheme() {
  const light = !document.body.classList.contains('light-mode');
  document.body.classList.toggle('light-mode', light);
  localStorage.setItem('bopu-theme', light ? 'light' : 'dark');
  updateThemeButton();
}
function updateThemeButton() {
  const btn = $('btn-theme');
  if (btn) btn.textContent = document.body.classList.contains('light-mode') ? tr('darkMode') : tr('lightMode');
}
function initLanguage() {
  lang = localStorage.getItem('bopu-lang') || 'zh';
  applyLanguage();
}
function setLanguage(next) {
  lang = next === 'en' ? 'en' : 'zh';
  localStorage.setItem('bopu-lang', lang);
  applyLanguage();
}
function applyLanguage() {
  document.documentElement.lang = lang === 'en' ? 'en' : 'zh';
  document.querySelectorAll('[data-i18n]').forEach((el) => { el.textContent = tr(el.dataset.i18n); });
  const tutorial = $('tutorial-content');
  if (tutorial) tutorial.innerHTML = tr('tutorialHtml');
  document.querySelectorAll('.seg-btn[data-lang]').forEach((btn) => btn.classList.toggle('active', btn.dataset.lang === lang));
  const brandSmall = document.querySelector('.side-brand small'); if (brandSmall) brandSmall.textContent = tr('materialDesk');
  const settingsBtn = $('btn-settings'); if (settingsBtn) settingsBtn.textContent = tr('settings');
  const homeTitle = document.querySelector('.home-head h1'); if (homeTitle) homeTitle.innerHTML = `<span class="brand">波谱</span> · ${tr('myProjects')}`;
  const backBtn = $('btn-home'); if (backBtn) backBtn.textContent = `← ${tr('myProjects')}`;
  const sideTitles = document.querySelectorAll('.side-title');
  if (sideTitles[0]) sideTitles[0].textContent = tr('projects');
  if (sideTitles[1]) sideTitles[1].textContent = tr('groups');
  $('btn-new-project').textContent = tr('newProject');
  $('btn-delete-projects').textContent = projectSelection.size ? `${tr('deleteSelected')}（${projectSelection.size}）` : tr('deleteSelected');
  $('home-empty').textContent = tr('noProjects');
  $('btn-analyze').textContent = tr('analyze');
  $('btn-scan').textContent = tr('scan');
  document.querySelector('.threshold label').textContent = tr('similarity');
  $('btn-auto').textContent = tr('auto');
  document.querySelector('.zoom label').textContent = tr('thumbSize');
  document.querySelector('label.radio input[value="copy"]').parentElement.lastChild.textContent = ` ${tr('copy')}`;
  document.querySelector('label.radio input[value="move"]').parentElement.lastChild.textContent = ` ${tr('move')}`;
  $('btn-open').textContent = tr('openResult');
  document.querySelector('.hint-all').textContent = tr('allHint');
  document.querySelector('.hint-group').textContent = tr('groupHint');
  updateThemeButton(); updateLockUI(); updateApplyUI(); updateCollapseButton(); updateSelUI();
  if (groupElsAll().length) updateSummary();
  refreshGroupNav();
}
function openSettings() { $('settings-modal').hidden = false; applyLanguage(); }
function closeSettings() { $('settings-modal').hidden = true; }
function openLegal(kind) {
  const isTerms = kind === 'terms';
  $('legal-title').textContent = isTerms ? '用户协议' : '隐私政策';
  $('legal-privacy').hidden = isTerms;
  $('legal-terms').hidden = !isTerms;
  $('legal-modal').hidden = false;
}
function closeLegal() { $('legal-modal').hidden = true; }
function ensureAuthConsent() {
  if ($('auth-consent').checked) return true;
  setAuthMessage('请先阅读并勾选同意隐私政策和用户协议。', 'error');
  return false;
}
function projectGridVisible() { return !$('home').hidden; }
function workspaceVisible() { return !$('workspace').hidden; }
function ready(fn) {
  if (window.pywebview && window.pywebview.api) fn();
  else window.addEventListener('pywebviewready', fn);
}

ready(() => {
  api = window.pywebview.api;
  setIconButton($('btn-lock'), SVG.lock, '解锁编辑');
  setIconButton($('preview-fullscreen'), SVG.maximize, '全屏');
  initTheme();
  initLanguage();
  $('btn-analyze').addEventListener('click', onAnalyze);
  $('btn-scan').addEventListener('click', onScan);
  $('btn-home').addEventListener('click', showHome);
  $('btn-new-project').addEventListener('click', openNewProject);
  $('btn-delete-projects').addEventListener('click', deleteSelectedProjects);
  $('np-pick').addEventListener('click', onPickFolder);
  $('np-create').addEventListener('click', onCreateProject);
  $('np-cancel').addEventListener('click', () => { $('newproj-modal').hidden = true; });
  $('newproj-modal').querySelector('.modal-backdrop').addEventListener('click', () => { $('newproj-modal').hidden = true; });
  $('np-name').addEventListener('input', updateCreateBtn);
  $('slider').addEventListener('input', onSliderInput);
  $('slider').addEventListener('change', onSliderCommit);
  $('thr-minus').addEventListener('click', () => stepThreshold(-0.1));
  $('thr-plus').addEventListener('click', () => stepThreshold(0.1));
  $('btn-auto').addEventListener('click', onAuto);
  $('btn-collapse').addEventListener('click', toggleCollapseAll);
  $('btn-apply').addEventListener('click', onApply);
  $('btn-open').addEventListener('click', () => api.open_output());
  $('btn-lock').addEventListener('click', toggleLock);
  $('btn-theme').addEventListener('click', toggleTheme);
  $('btn-settings').addEventListener('click', openSettings);
  $('btn-auth-logout').addEventListener('click', onAuthLogout);
  $('btn-side-logout').addEventListener('click', onAuthLogout);
  $('auth-send').addEventListener('click', onAuthSendCode);
  $('auth-login').addEventListener('click', onAuthLogin);
  $('auth-code').addEventListener('keydown', (e) => { if (e.key === 'Enter') onAuthLogin(); });
  $('auth-consent').addEventListener('change', () => {
    localStorage.setItem('bopu-legal-consent', $('auth-consent').checked ? '1' : '0');
    if ($('auth-consent').checked && $('auth-message').classList.contains('error')) setAuthMessage('');
  });
  $('auth-consent').checked = localStorage.getItem('bopu-legal-consent') === '1';
  $('open-privacy').addEventListener('click', () => openLegal('privacy'));
  $('open-terms').addEventListener('click', () => openLegal('terms'));
  $('legal-close').addEventListener('click', closeLegal);
  $('legal-modal').querySelector('.modal-backdrop').addEventListener('click', closeLegal);
  $('settings-close').addEventListener('click', closeSettings);
  $('settings-modal').querySelector('.modal-backdrop').addEventListener('click', closeSettings);
  $('lang-zh').addEventListener('click', () => setLanguage('zh'));
  $('lang-en').addEventListener('click', () => setLanguage('en'));
  $('zoom').addEventListener('input', onZoom);

  $('groups').addEventListener('click', (e) => { if (e.target.id === 'groups') clearSelection(); });
  $('groups').addEventListener('mousedown', onMarqueeStart);
  $('project-grid').addEventListener('mousedown', onProjectMarqueeStart);
  $('project-grid').addEventListener('contextmenu', onProjectGridContextMenu);
  $('home').addEventListener('click', onHomeBlankClick);
  document.addEventListener('click', () => hideCtxMenu());
  document.addEventListener('keydown', onKeydown);
  const onFsChange = () => {
    const inFs = !!(document.fullscreenElement || document.webkitFullscreenElement);
    if (!inFs && previewFs) {
      previewFs = false;
      $('preview-modal').classList.remove('fs');
      setIconButton($('preview-fullscreen'), SVG.maximize, '全屏');
    }
  };
  document.addEventListener('fullscreenchange', onFsChange);
  document.addEventListener('webkitfullscreenchange', onFsChange);

  $('preview-close').addEventListener('click', closePreview);
  $('preview-fullscreen').addEventListener('click', previewFullscreen);
  $('preview-prompt-ok').addEventListener('click', confirmPreviewPrompt);
  $('preview-prompt-cancel').addEventListener('click', cancelPreviewPrompt);
  $('preview-prompt').addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); confirmPreviewPrompt(); }
    else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); cancelPreviewPrompt(); }
  });
  $('preview-modal').querySelector('.modal-backdrop').addEventListener('click', closePreview);
  $('preview-modal').querySelectorAll('.modal-tags .tag-btn').forEach((b) => {
    b.addEventListener('click', () => { if (previewEl) { toggleTagEls([previewEl], b.dataset.tag); updatePreviewTags(); } });
  });
  $('info-close').addEventListener('click', () => { $('info-modal').hidden = true; });
  $('info-modal').querySelector('.modal-backdrop').addEventListener('click', () => { $('info-modal').hidden = true; });

  initAuthFlow();
});

function onKeydown(e) {
  const editing = e.target && e.target.matches && e.target.matches('input, textarea');
  if (!$('preview-modal').hidden && !editing) {
    if (e.key === 'Escape' && previewFs) { e.preventDefault(); setPreviewFs(false); return; }
    if (e.key === 'ArrowLeft') { e.preventDefault(); stepPreview(-1); return; }
    if (e.key === 'ArrowRight') { e.preventDefault(); stepPreview(1); return; }
    if (e.key === ' ') { e.preventDefault(); togglePreviewPlay(); return; }
  }
  if (editing) { if (e.key === 'Escape') e.target.blur(); return; }
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'a' && projectGridVisible()) {
    e.preventDefault();
    currentProjectList.forEach((p) => projectSelection.add(p.id));
    updateProjectSelectionUI();
    renderProjects(currentProjectList);
    return;
  }
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'a') {
    const clips = clipEls();
    if (clips.length) { e.preventDefault(); selection.clear(); clips.forEach((c) => selection.add(c.dataset.uid)); updateSelUI(); }
    return;
  }
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'c') { doCopy(); return; }
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'v') { doPaste(currentGroupForPaste()); return; }
  if (e.key === 'Escape') { hideCtxMenu(); $('info-modal').hidden = true; closePreview(); clearSelection(); return; }
  if ((e.key === 'Delete' || e.key === 'Backspace') && selection.size) { e.preventDefault(); doDelete(); return; }
  const map = { '1': '过', '2': '保', '3': '废', '0': '' };
  if (e.key in map && selection.size) { toggleTagEls(selEls(), map[e.key]); }
}

window.onProgress = (phase, i, n, name) => {
  $('progress').hidden = false;
  $('progress-fill').style.width = (n ? (i / n * 100) : 0) + '%';
  $('progress-text').textContent = `${phase} ${i}/${n}：${name}`;
};

// ---------- 登录 ----------
function setAuthMessage(text, type = '') {
  const el = $('auth-message');
  el.textContent = text || '';
  el.classList.toggle('error', type === 'error');
  el.classList.toggle('ok', type === 'ok');
}
function showAuthScreen(message = '') {
  $('workspace').hidden = true;
  $('home').hidden = true;
  $('auth-screen').hidden = false;
  setAuthMessage(message, message ? 'error' : '');
}
function showAuthedApp(user, notice = '') {
  authUser = user || {};
  $('auth-screen').hidden = true;
  const label = authUser.phone ? `账号 ${authUser.phone}` : '';
  $('auth-user').hidden = !label;
  $('auth-user').textContent = label;
  $('btn-auth-logout').hidden = false;
  $('btn-side-logout').hidden = false;
  if (notice) setTimeout(() => alert(notice), 80);
  showHome();
}
async function initAuthFlow() {
  $('auth-screen').hidden = true;
  $('workspace').hidden = true;
  $('home').hidden = true;
  const status = await api.auth_status();
  if (!status.required || status.logged_in) {
    showAuthedApp(status.user, status.message || '');
    return;
  }
  showAuthScreen(status.error || '请先登录后使用波谱。');
}
async function onAuthSendCode() {
  const phone = $('auth-phone').value.trim();
  if (!ensureAuthConsent()) return;
  if (!phone) { setAuthMessage('请输入手机号。', 'error'); return; }
  $('auth-send').disabled = true;
  setAuthMessage('正在发送验证码…');
  const res = await api.auth_send_code(phone);
  $('auth-send').disabled = false;
  if (res.error) { setAuthMessage(res.error, 'error'); return; }
  if (res.dev_code) {
    $('auth-code').value = res.dev_code;
    setAuthMessage(`开发模式验证码：${res.dev_code}。正式上线后这里会改成真实短信。`, 'ok');
  } else {
    setAuthMessage('验证码已发送，请查看短信。', 'ok');
  }
  $('auth-code').focus();
}
async function onAuthLogin() {
  const phone = $('auth-phone').value.trim();
  const code = $('auth-code').value.trim();
  if (!ensureAuthConsent()) return;
  if (!phone || !code) { setAuthMessage('请输入手机号和验证码。', 'error'); return; }
  $('auth-login').disabled = true;
  setAuthMessage('正在登录…');
  const res = await api.auth_login(phone, code);
  $('auth-login').disabled = false;
  if (res.error) { setAuthMessage(res.error, 'error'); return; }
  showAuthedApp(res.user);
}
async function onAuthLogout() {
  if (!confirm('退出当前账号？')) return;
  await api.auth_logout();
  authUser = null;
  showAuthScreen('已退出登录。');
}

// ---------- 项目首页 ----------
async function showHome() {
  currentProjectId = null;
  $('workspace').hidden = true;
  $('home').hidden = false;
  renderProjects(await api.list_projects());
}
function renderProjects(list) {
  currentProjectList = list || [];
  const grid = $('project-grid');
  grid.innerHTML = '';
  $('home-empty').hidden = list.length > 0;
  const ids = new Set(list.map((p) => p.id));
  [...projectSelection].forEach((id) => { if (!ids.has(id)) projectSelection.delete(id); });
  list.forEach((p) => {
    const card = document.createElement('div');
    card.dataset.pid = p.id;
    card.className = 'project-card' + (p.exists ? '' : ' missing') + (projectSelection.has(p.id) ? ' selected' : '');
    const thumb = p.thumb
      ? `<div class="project-thumb" style="background-image:url('${p.thumb}?t=${Date.now()}')"></div>`
      : `<div class="project-thumb">${tr('noPreview')}</div>`;
    card.innerHTML = thumb
      + `<div class="project-actions"><button class="pa-btn" data-act="rename" title="重命名">${SVG.edit}</button>`
      + `<button class="pa-btn" data-act="delete" title="删除项目">${SVG.trash}</button></div>`
      + `<div class="project-meta"><div class="pname">${p.name}</div>`
      + `<div class="psub">${p.exists ? (p.has_state ? '已保存 · ' : '') + '上次 ' + p.last_opened : '文件夹已丢失'}</div></div>`;
    card.addEventListener('click', (e) => {
      const actionBtn = e.target.closest('.pa-btn[data-act]');
      const act = actionBtn ? actionBtn.dataset.act : '';
      if (act === 'delete') { e.stopPropagation(); onDeleteProject(p); return; }
      if (act === 'rename') { e.stopPropagation(); onRenameProject(p); return; }
      if (e.metaKey || e.ctrlKey) {
        e.preventDefault();
        projectSelection.has(p.id) ? projectSelection.delete(p.id) : projectSelection.add(p.id);
        updateProjectSelectionUI();
        renderProjects(list);
        return;
      }
      openProject(p.id);
    });
    card.addEventListener('contextmenu', (e) => onProjectContextMenu(e, p, list));
    grid.appendChild(card);
  });
  updateProjectSelectionUI();
}
function onProjectContextMenu(e, p, list) {
  e.preventDefault();
  if (!projectSelection.has(p.id)) {
    projectSelection.clear();
    projectSelection.add(p.id);
    updateProjectSelectionUI();
    renderProjects(list);
  }
  showProjectCtxMenu(e.clientX, e.clientY, p);
}
function onProjectGridContextMenu(e) {
  if (e.target.closest('.project-card')) return;
  e.preventDefault();
  showProjectCtxMenu(e.clientX, e.clientY, null);
}
function onHomeBlankClick(e) {
  if (!projectGridVisible()) return;
  if (suppressProjectBlankClick) return;
  if (e.target.closest('.project-card') || e.target.closest('button') || e.target.closest('.ctx-menu')) return;
  if (!projectSelection.size) return;
  projectSelection.clear();
  updateProjectSelectionUI();
  renderProjects(currentProjectList);
}
function showProjectCtxMenu(x, y, p) {
  const menu = $('ctx-menu');
  menu.innerHTML = '';
  const ids = [...projectSelection];
  const n = ids.length;
  const add = (label, fn, opts = {}) => {
    const it = document.createElement('div');
    it.className = 'ctx-item' + (opts.disabled ? ' disabled' : '') + (opts.danger ? ' danger' : '');
    it.textContent = label;
    if (!opts.disabled) it.addEventListener('click', (ev) => { ev.stopPropagation(); hideCtxMenu(); fn(); });
    menu.appendChild(it);
  };
  const sep = () => { const s = document.createElement('div'); s.className = 'ctx-sep'; menu.appendChild(s); };
  if (!p && n === 0) {
    add(tr('newProject').replace('＋ ', '').replace('+ ', ''), openNewProject);
  } else if (n <= 1) {
    if (!p) p = currentProjectList.find((item) => item.id === ids[0]);
    if (!p) return;
    add('打开', () => openProject(p.id), { disabled: !p.exists });
    add('重命名', () => onRenameProject(p));
    sep();
    add('复制副本', () => duplicateProjects([p.id]));
    add('删除', () => onDeleteProject(p), { danger: true });
    sep();
    add('在访达中显示', () => api.reveal_folder(p.id), { disabled: !p.exists });
  } else {
    add(`复制副本（${n}）`, () => duplicateProjects(ids));
    add(`删除（${n}）`, () => deleteSelectedProjects(), { danger: true });
  }
  menu.hidden = false;
  positionMenu(menu, x, y);
}
function onProjectMarqueeStart(e) {
  if (e.button !== 0 || !projectGridVisible()) return;
  if (e.target.closest('.project-card') || e.target.matches('button,input,textarea')) return;
  projectMarquee.active = true;
  projectMarquee.additive = e.metaKey || e.ctrlKey;
  projectMarquee.moved = false;
  projectMarquee.x0 = e.clientX; projectMarquee.y0 = e.clientY;
  if (!projectMarquee.additive) projectSelection.clear();
  document.body.style.userSelect = 'none';
  document.addEventListener('mousemove', onProjectMarqueeMove);
  document.addEventListener('mouseup', onProjectMarqueeEnd);
}
function onProjectMarqueeMove(e) {
  if (!projectMarquee.active) return;
  const x = Math.min(e.clientX, projectMarquee.x0), y = Math.min(e.clientY, projectMarquee.y0);
  const w = Math.abs(e.clientX - projectMarquee.x0), h = Math.abs(e.clientY - projectMarquee.y0);
  if (w > 3 || h > 3) projectMarquee.moved = true;
  const box = $('marquee'); box.hidden = false;
  box.style.left = x + 'px'; box.style.top = y + 'px'; box.style.width = w + 'px'; box.style.height = h + 'px';
  const rect = { left: x, top: y, right: x + w, bottom: y + h };
  document.querySelectorAll('.project-card').forEach((card) => {
    const r = card.getBoundingClientRect();
    const hit = !(r.right < rect.left || r.left > rect.right || r.bottom < rect.top || r.top > rect.bottom);
    if (hit) projectSelection.add(card.dataset.pid);
    else if (!projectMarquee.additive) projectSelection.delete(card.dataset.pid);
    card.classList.toggle('selected', projectSelection.has(card.dataset.pid));
  });
  updateProjectSelectionUI();
}
function onProjectMarqueeEnd() {
  if (projectMarquee.moved) {
    suppressProjectBlankClick = true;
    setTimeout(() => { suppressProjectBlankClick = false; }, 120);
  }
  projectMarquee.active = false;
  $('marquee').hidden = true;
  document.body.style.userSelect = '';
  document.removeEventListener('mousemove', onProjectMarqueeMove);
  document.removeEventListener('mouseup', onProjectMarqueeEnd);
}
async function duplicateProjects(ids) {
  for (const id of ids) await api.duplicate_project(id);
  projectSelection.clear();
  renderProjects(await api.list_projects());
}
function updateProjectSelectionUI() {
  const btn = $('btn-delete-projects');
  btn.hidden = projectSelection.size === 0;
  btn.textContent = projectSelection.size ? `${tr('deleteSelected')}（${projectSelection.size}）` : tr('deleteSelected');
}
async function deleteSelectedProjects() {
  const ids = [...projectSelection];
  if (!ids.length) return;
  if (!confirm(`删除选中的 ${ids.length} 个项目？
（只删项目记录，不会删你的视频文件和整理好的文件）`)) return;
  for (const id of ids) await api.delete_project(id);
  projectSelection.clear();
  renderProjects(await api.list_projects());
}
function openNewProject() {
  pickedFolder = null;
  $('np-name').value = '';
  $('np-folder').textContent = '未选择';
  updateCreateBtn();
  $('newproj-modal').hidden = false;
}
async function onPickFolder() {
  const res = await api.pick_folder();
  if (!res) return;
  pickedFolder = res;
  $('np-folder').textContent = `${res.path}（${res.count} 个视频）`;
  if (!$('np-name').value.trim()) $('np-name').value = res.name;
  updateCreateBtn();
}
function updateCreateBtn() { $('np-create').disabled = !(pickedFolder && $('np-name').value.trim()); }
async function onCreateProject() {
  if (!pickedFolder) return;
  const rec = await api.create_project(pickedFolder.path, $('np-name').value.trim());
  if (rec.error) { alert(rec.error); return; }
  $('newproj-modal').hidden = true;
  openProject(rec.id);
}
async function onDeleteProject(p) {
  if (!confirm(`删除项目「${p.name}」？\n（只删项目记录，不会删你的视频文件和整理好的文件）`)) return;
  await api.delete_project(p.id);
  renderProjects(await api.list_projects());
}
async function onRenameProject(p) {
  const name = prompt('项目新名称', p.name);
  if (name == null) return;
  await api.rename_project(p.id, name);
  renderProjects(await api.list_projects());
}
async function openProject(pid) {
  projectSelection.clear(); updateProjectSelectionUI();
  unlocked = false; fullDirty = false;
  currentProjectId = pid;
  $('home').hidden = true;
  $('workspace').hidden = false;
  $('progress').hidden = false;
  const res = await api.open_project(pid);
  $('progress').hidden = true;
  if (res.error) { alert(res.error); showHome(); return; }
  refreshSideProjects(pid);
  libName = res.lib || '已分组';
  $('project-name').textContent = res.name;
  $('folder-label').textContent = `${res.path}（${res.count} 个视频）`;
  if (res.last_scan) { $('scan-time').hidden = false; $('scan-time').textContent = `最近扫描：${res.last_scan}`; }
  else $('scan-time').hidden = true;
  $('btn-open').hidden = true;
  if (res.payload) {
    // 已有保存的工作状态：秒还原
    $('btn-analyze').hidden = true;
    $('btn-scan').hidden = false;
    $('controls').hidden = false;
    renderGroups(res.payload.groups);
  } else {
    // 空项目：等待分析
    $('btn-analyze').hidden = false;
    $('btn-analyze').disabled = res.count === 0;
    $('btn-scan').hidden = true;
    $('controls').hidden = true;
    $('groups').innerHTML = '';
    activeGroupId = null;
    refreshGroupNav();
    $('placeholder').hidden = false;
  }
}
async function refreshSideProjects(activeId) {
  const list = await api.list_projects();
  renderSideProjects(list, activeId);
}
function renderSideProjects(list, activeId) {
  const wrap = $('side-projects');
  if (!wrap) return;
  wrap.innerHTML = '';
  list.forEach((p) => {
    const btn = document.createElement('button');
    btn.className = 'side-item project-side-item' + (p.id === activeId ? ' active' : '') + (p.exists ? '' : ' missing');
    btn.innerHTML = `${SVG.folder}<span>${p.name}</span>`;
    btn.title = p.exists ? p.name : `${p.name}（文件夹已丢失）`;
    btn.disabled = !p.exists;
    btn.addEventListener('click', () => { if (p.id !== currentProjectId) openProject(p.id); });
    wrap.appendChild(btn);
  });
}

// ---------- 分析 / 扫描 ----------
async function onAnalyze() {
  $('placeholder').hidden = true;
  $('progress').hidden = false;
  $('btn-analyze').disabled = true; $('btn-home').disabled = true;
  const payload = await api.analyze();
  $('progress').hidden = true;
  $('btn-analyze').disabled = false; $('btn-home').disabled = false;
  if (payload.error) { alert(payload.error); return; }
  lastSuggested = payload.suggested;
  $('btn-analyze').hidden = true;
  $('btn-scan').hidden = false;
  $('controls').hidden = false;
  setSlider(payload.threshold);
  renderGroups(payload.groups);
}
async function onScan() {
  $('progress').hidden = false;
  $('btn-scan').disabled = true;
  const payload = await api.scan(parseFloat($('slider').value));
  $('progress').hidden = true;
  $('btn-scan').disabled = false;
  if (payload.error) { alert(payload.error); return; }
  if (payload.last_scan) { $('scan-time').hidden = false; $('scan-time').textContent = `最近扫描：${payload.last_scan}`; }
  if (!payload.new_count) { alert('没有发现新增的镜头。'); return; }
  renderGroups(payload.groups);
  alert(`发现 ${payload.new_count} 个新镜头，已归入最相近的组，在组尾叠放显示（点箭头展开）。`);
}

let sliderTimer = null;
function onSliderInput() { $('threshold-value').textContent = parseFloat($('slider').value).toFixed(2); }
function onSliderCommit() { clearTimeout(sliderTimer); sliderTimer = setTimeout(() => commitThreshold(parseFloat($('slider').value)), 60); }
async function commitThreshold(t) { renderGroups((await api.recluster(t)).groups); }
function setSlider(t) { $('slider').value = t; $('threshold-value').textContent = parseFloat(t).toFixed(2); }
function stepThreshold(d) {
  let t = Math.round((parseFloat($('slider').value) + d) * 10) / 10;
  t = Math.max(-1, Math.min(1, t));
  setSlider(t); commitThreshold(t);
}
async function onAuto() { if (lastSuggested == null) return; setSlider(lastSuggested); commitThreshold(lastSuggested); }

function toggleCollapseAll() {
  collapsedAll = !collapsedAll;
  document.querySelectorAll('.group:not(.new-zone)').forEach((g) => g.classList.toggle('collapsed', collapsedAll));
  updateCollapseButton();
}
function updateCollapseButton() {
  const groups = [...document.querySelectorAll('.group:not(.new-zone)')];
  collapsedAll = groups.length > 0 && groups.every((g) => g.classList.contains('collapsed'));
  $('btn-collapse').textContent = collapsedAll ? tr('expandAll') : tr('collapseAll');
}
function onZoom() { currentDW = parseInt($('zoom').value, 10); document.querySelectorAll('.clip').forEach(sizeClip); }
function sizeClip(el) {
  const cols = +el.dataset.cols, aspect = +el.dataset.aspect;
  const h = Math.min(340, Math.round(currentDW * aspect));
  el.style.width = currentDW + 'px';
  el.style.height = h + 'px';
  el.style.backgroundSize = `${cols * currentDW}px ${h}px`;
  restThumb(el);
}
function restThumb(el) {
  const frame = Math.min(+el.dataset.cols - 1, Math.max(0, +(el.dataset.frame || 0)));
  el.style.backgroundPositionX = `-${frame * currentDW}px`;
}

// ---------- 自动保存 ----------
function scheduleSave() {
  clearTimeout(saveTimer);
  saveTimer = setTimeout(() => { api.save_state(readGroups()); }, 700);
}
function readGroups() {
  const groups = [];
  document.querySelectorAll('.group:not(.new-zone)').forEach((g) => {
    const clips = [...g.querySelectorAll('.clip')].map((c) => ({ id: c.dataset.sid, tag: c.dataset.tag || '', frame: +(c.dataset.frame || 0), prompt: c.dataset.prompt || '' }));
    if (!clips.length) return;
    groups.push({ name: g.dataset.name || g.querySelector('.group-title').textContent.trim(),
                  note: g.dataset.note || '', prompt: g.dataset.prompt || '', clips });
  });
  return groups;
}

// ---------- 渲染 ----------
function renderGroups(groups) {
  clearSelection();
  groupUidCounter = 0;
  const container = $('groups');
  container.innerHTML = '';
  $('placeholder').hidden = true;
  groups.forEach((g) => container.appendChild(makeGroupEl(g)));
  container.appendChild(makeNewZone());
  activeGroupId = groupElsAll().length ? '__all__' : null;
  rebuildSortables();
  updateSummary();
  updateLockUI();
  updateApplyUI();
  updateCollapseButton();
  ensureActiveGroup(true);
  refreshGroupNav();
}
function groupElsAll() { return Array.from(document.querySelectorAll('.group:not(.new-zone)')); }
function activeGroupEl() {
  return activeGroupId ? document.querySelector(`.group[data-gid="${activeGroupId}"]`) : null;
}
function ensureActiveGroup(preferFirst) {
  const groups = groupElsAll();
  if (!groups.length) { activeGroupId = null; return; }
  if (activeGroupId === '__all__') return;
  if (!activeGroupEl()) activeGroupId = preferFirst ? groups[0].dataset.gid : '__all__';
}
function setActiveGroup(id) {
  activeGroupId = id;
  clearSelection();
  applyActiveGroupView();
  refreshGroupNav();
}
function isAllView() { return activeGroupId === '__all__'; }
function applyActiveGroupView() {
  ensureActiveGroup(false);
  const showAll = activeGroupId === '__all__';
  const active = activeGroupEl();
  document.querySelectorAll('.group').forEach((g) => {
    const isNew = g.classList.contains('new-zone');
    const visible = showAll || (!isNew && active && g.dataset.gid === active.dataset.gid);
    g.classList.toggle('is-filtered', !visible);
  });
  const title = $('view-title');
  if (title) title.textContent = showAll ? tr('allShots') : (active ? active.querySelector('.group-title').textContent : '素材库');
  updateViewControls();
}
function updateViewControls() {
  const showAll = isAllView();
  const hasGroups = groupElsAll().length > 0;
  document.body.classList.toggle('all-shots-view', showAll && hasGroups);
  document.body.classList.toggle('single-group-view', !showAll && hasGroups);
  document.querySelectorAll('.all-only').forEach((el) => { el.hidden = !showAll; });
  const organize = $('organize-actions');
  if (organize) organize.hidden = !(showAll && hasGroups);
  const scan = $('btn-scan');
  if (scan && hasGroups) scan.hidden = !showAll;
  const allHint = document.querySelector('.hint-all');
  const groupHint = document.querySelector('.hint-group');
  if (allHint) allHint.hidden = !showAll;
  if (groupHint) groupHint.hidden = showAll;
  if (groupSortable) groupSortable.option('disabled', !showAll || !canEditGroupStructure());
  if (groupNavSortable) groupNavSortable.option('disabled', !canEditGroupStructure());
  const nav = $('group-nav');
  if (nav) nav.classList.toggle('sortable-disabled', !canEditGroupStructure());
}
function refreshGroupNav() {
  const nav = $('group-nav');
  if (!nav) return;
  ensureActiveGroup(false);
  applyActiveGroupView();
  const groups = groupElsAll();
  const total = groups.reduce((n, g) => n + g.querySelectorAll('.clip').length, 0);
  nav.innerHTML = '';
  const all = document.createElement('button');
  all.className = 'side-item group-side-item group-side-all' + (activeGroupId === '__all__' ? ' active' : '');
  all.innerHTML = `${SVG.layers}<span>${tr('allShots')}</span><em>${total}</em>`;
  all.addEventListener('click', () => setActiveGroup('__all__'));
  nav.appendChild(all);
  const sep = document.createElement('div');
  sep.className = 'group-nav-sep';
  nav.appendChild(sep);
  groups.forEach((g) => {
    const id = g.dataset.gid;
    const btn = document.createElement('button');
    btn.className = 'side-item group-side-item' + (activeGroupId === id ? ' active' : '');
    btn.dataset.gid = id;
    btn.innerHTML = `<span class="side-drag">⠿</span>${SVG.layers}<span class="side-label" title="双击改名">${g.querySelector('.group-title').textContent}</span><em>${g.querySelectorAll('.clip').length}</em><span class="side-actions"><span data-act="delete" title="删除">${SVG.trash}</span></span>`;
    btn.addEventListener('click', (e) => {
      const act = e.target.closest('[data-act]');
      if (act) {
        e.preventDefault();
        e.stopPropagation();
        if (act.dataset.act === 'delete') deleteGroup(g);
        return;
      }
      if (e.target.closest('.side-label')) {
        clearTimeout(groupNavClickTimer);
        groupNavClickTimer = setTimeout(() => setActiveGroup(id), 220);
        return;
      }
      setActiveGroup(id);
    });
    btn.addEventListener('dblclick', (e) => {
      const label = e.target.closest('.side-label');
      if (!label) return;
      e.preventDefault(); e.stopPropagation();
      clearTimeout(groupNavClickTimer);
      startSidebarRename(g, label);
    });
    nav.appendChild(btn);
  });
  rebuildGroupNavSortable();
}
function hasCommitted() { return document.querySelector('.clip[data-committed="1"]') != null; }
function updateLockUI() {
  const show = hasCommitted();
  const btn = $('btn-lock');
  btn.hidden = !show;
  setIconButton(btn, unlocked ? SVG.unlock : SVG.lock, unlocked ? tr('unlocked') : tr('locked'));
  btn.classList.toggle('lock-red', !unlocked);
  btn.classList.toggle('lock-green', unlocked);
}
function toggleLock() {
  unlocked = !unlocked;
  // 重新渲染当前结构以应用锁定态（committed 列表可拖动 / 锁定 class）
  document.querySelectorAll('.clip[data-committed="1"]').forEach((el) => el.classList.toggle('locked', !unlocked));
  rebuildSortables();
  rebuildGroupNavSortable();
  updateLockUI();
  if (unlocked) alert('已解锁。改动已入库的内容或组名后，点「确认并执行」会删除旧的整理文件夹、按当前结果重新整理一遍。');
}
function commitGroupPrefix(group, prefix) {
  const t = prefix.trim().replace(/_\d+$/, '').replace(/_+$/, '') || '组';
  const all = groupElsAll();
  const idx = all.indexOf(group);
  for (let i = idx; i < all.length; i++) all[i].dataset.prefix = t;
  if (group.querySelector('.clip[data-committed="1"]')) markFullDirty();
  renumber(); scheduleSave(); refreshGroupNav();
}
function startSidebarRename(group, label) {
  if (groupLocked(group)) { alert('这一组已入库并锁定。请先点右上角「解锁编辑」。'); return; }
  const cur = group.dataset.prefix || (group.dataset.name || '').replace(/_\d+$/, '') || '组';
  if (!label) { commitGroupPrefix(group, cur); return; }
  const input = document.createElement('input');
  input.className = 'side-rename-input';
  input.value = cur;
  input.addEventListener('click', (e) => e.stopPropagation());
  input.addEventListener('dblclick', (e) => e.stopPropagation());
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') input.blur();
    if (e.key === 'Escape') { input.dataset.cancel = '1'; input.blur(); }
  });
  input.addEventListener('blur', () => {
    if (input.dataset.cancel !== '1') commitGroupPrefix(group, input.value);
    else refreshGroupNav();
  });
  label.replaceWith(input);
  input.focus();
  input.select();
}
function deleteGroup(group) {
  if (!group || group.classList.contains('new-zone')) return;
  if (groupLocked(group)) { alert('这一组已入库并锁定。请先点右上角「解锁编辑」。'); return; }
  const title = group.querySelector('.group-title').textContent.trim();
  const count = group.querySelectorAll('.clip').length;
  if (!confirm(`删除分组「${title}」？\n这只会从当前整理结果里移除 ${count} 个素材，不会删除原视频文件。`)) return;
  if (group.querySelector('.clip[data-committed="1"]')) markFullDirty();
  if (activeGroupId === group.dataset.gid) activeGroupId = '__all__';
  group.remove();
  onDragChange();
}

function makeGroupEl(groupData) {
  const items = Array.isArray(groupData) ? groupData : groupData.items;
  const group = document.createElement('section');
  group.className = 'group';
  group.dataset.gid = 'g' + (++groupUidCounter);
  if (!Array.isArray(groupData)) {
    if (groupData.name) {
      group.dataset.name = groupData.name;
      const mm = String(groupData.name).match(/^(.+)_\d+$/);   // 解析出"文字"前缀
      group.dataset.prefix = mm ? mm[1] : groupData.name;
    }
    if (groupData.note) group.dataset.note = groupData.note;
    if (groupData.prompt) group.dataset.prompt = groupData.prompt;
  }
  if (collapsedAll) group.classList.add('collapsed');
  const head = document.createElement('div');
  head.className = 'group-head';
  head.innerHTML = '<button class="collapse-btn" title="折叠/展开本组">▾</button>'
    + '<span class="drag-dots" title="拖动给组排序">⠿</span>'
    + '<span class="group-title" title="双击重命名"></span><span class="group-count"></span>'
    + '<button class="prompt-btn" title="提示词">提示词 ▾</button>';
  head.addEventListener('click', (e) => onGroupHeadClick(e, group));
  head.querySelector('.collapse-btn').addEventListener('click', (e) => { e.stopPropagation(); group.classList.toggle('collapsed'); updateCollapseButton(); });
  head.querySelector('.group-title').addEventListener('dblclick', (e) => { e.stopPropagation(); startRename(group); });
  head.querySelector('.prompt-btn').addEventListener('click', (e) => { e.stopPropagation(); togglePrompt(group); });
  group.appendChild(head);

  const note = document.createElement('input');
  note.className = 'group-note';
  note.type = 'text';
  note.placeholder = '备注（可选，导出时存为 备注.txt）';
  note.value = group.dataset.note || '';
  note.addEventListener('input', () => { group.dataset.note = note.value; scheduleSave(); });
  note.addEventListener('mousedown', (e) => e.stopPropagation());
  group.appendChild(note);

  const promptBox = document.createElement('textarea');
  promptBox.className = 'group-prompt';
  promptBox.placeholder = '这一组的提示词（导出时存为 提示词.txt）';
  promptBox.value = group.dataset.prompt || '';
  promptBox.hidden = !group.dataset.prompt;
  promptBox.addEventListener('input', () => { group.dataset.prompt = promptBox.value; updatePromptBtn(group); scheduleSave(); });
  promptBox.addEventListener('mousedown', (e) => e.stopPropagation());
  group.appendChild(promptBox);
  updatePromptBtn(group);

  const committed = items.filter((it) => it.committed);
  const normal = items.filter((it) => !it.committed && !it.isNew);
  const fresh = items.filter((it) => !it.committed && it.isNew);

  const plist = document.createElement('div');
  plist.className = 'clip-list pending main-clips';
  committed.forEach((it) => plist.appendChild(makeClipEl(it)));
  normal.forEach((it) => plist.appendChild(makeClipEl(it)));
  group.appendChild(plist);

  if (fresh.length) {
    const div = document.createElement('div');
    div.className = 'new-divider';
    group.appendChild(div);
    const toggle = document.createElement('div');
    toggle.className = 'stack-toggle';
    toggle.innerHTML = `<span class="arrow">▸</span><span class="stack-txt">${fresh.length} 个新增镜头（点击展开）</span>`
      + '<button class="merge-btn">并入</button>';
    const flist = document.createElement('div');
    flist.className = 'clip-list pending newclips stacked';
    fresh.forEach((it) => flist.appendChild(makeClipEl(it)));
    toggle.addEventListener('click', (e) => {
      if (e.target.closest('.merge-btn')) return;
      const open = flist.classList.toggle('stacked');
      toggle.classList.toggle('open', !open);
    });
    toggle.querySelector('.merge-btn').addEventListener('click', (e) => { e.stopPropagation(); mergeNew(group); });
    group.appendChild(toggle);
    group.appendChild(flist);
  }
  return group;
}

function makeNewZone() {
  const g = makeGroupEl([]);
  g.classList.add('new-zone');
  const t = g.querySelector('.group-title');
  t.textContent = '拖到这里新建一组';
  t.style.color = 'var(--text-dim)';
  return g;
}

function groupLocked(group) { return !unlocked && group.querySelector('.clip[data-committed="1"]') != null; }
function canEditGroupStructure() { return unlocked || !hasCommitted(); }
function sameGroupPrefix(a, b) {
  if (!a || !b || a.classList.contains('new-zone') || b.classList.contains('new-zone')) return true;
  return (a.dataset.prefix || '') === (b.dataset.prefix || '');
}
function groupByGid(gid) { return gid ? document.querySelector(`.group[data-gid="${gid}"]`) : null; }
function onGroupSortMove(evt) {
  const dragged = evt.dragged && evt.dragged.classList.contains('group') ? evt.dragged : groupByGid(evt.dragged && evt.dragged.dataset.gid);
  const related = evt.related && evt.related.classList.contains('group') ? evt.related : groupByGid(evt.related && evt.related.dataset.gid);
  if (!sameGroupPrefix(dragged, related)) return false;
  return true;
}
// 改名只改"文字前缀"，编号由系统按前缀自动连续分配（不可手改、不重复、不中断）
function startRename(group) {
  if (groupLocked(group)) { alert('这一组已入库并锁定。请先点右上角「解锁编辑」。'); return; }
  const title = group.querySelector('.group-title');
  const cur = group.dataset.prefix || (group.dataset.name || '').replace(/_\d+$/, '') || '组';
  const input = document.createElement('input');
  input.className = 'rename-input';
  input.value = cur;
  input.placeholder = '输入前缀文字（编号自动连续，如 组 / 镜头）';
  input.addEventListener('mousedown', (e) => e.stopPropagation());
  input.addEventListener('keydown', (e) => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { input.value = cur; input.blur(); } });
  input.addEventListener('blur', () => {
    const t = input.value.trim().replace(/_\d+$/, '').replace(/_+$/, '') || '组';
    // 改这一组的前缀，其后所有组也延续这个前缀（之前的组不受影响）；编号由 renumber 连续分配
    const all = [...document.querySelectorAll('.group:not(.new-zone)')];
    const idx = all.indexOf(group);
    for (let i = idx; i < all.length; i++) all[i].dataset.prefix = t;
    input.replaceWith(title);
    if (group.querySelector('.clip[data-committed="1"]')) markFullDirty();
    renumber(); scheduleSave(); refreshGroupNav();
  });
  title.replaceWith(input); input.focus(); input.select();
}
function togglePrompt(group) {
  const box = group.querySelector('.group-prompt');
  box.hidden = !box.hidden;
  if (!box.hidden) box.focus();
}
function updatePromptBtn(group) {
  const btn = group.querySelector('.prompt-btn');
  if (btn) btn.classList.toggle('has', !!(group.dataset.prompt && group.dataset.prompt.trim()));
}

function makeClipEl(it) {
  const el = document.createElement('div');
  el.className = 'clip' + (it.committed && !unlocked ? ' locked' : '');
  if (it.committed) el.dataset.committed = '1';
  el.dataset.uid = String(++uidCounter);
  el.dataset.sid = it.id;            // orig 文件名（稳定标识）
  el.dataset.cols = it.cols;
  el.dataset.aspect = it.aspect;
  el.dataset.name = it.name;
  el.dataset.frame = it.frame != null ? it.frame : Math.floor(it.cols / 2);
  if (it.video) el.dataset.video = it.video;
  if (it.prompt) el.dataset.prompt = it.prompt;
  if (it.tag) el.dataset.tag = it.tag;
  el.style.backgroundImage = `url("${it.sprite}")`;
  el.innerHTML = `<span class="dur">${it.seconds}s</span>`
    + '<span class="scrub-tick"></span>'
    + '<div class="tag-bar"><button class="tag-chip" data-tag="过">过</button>'
    + '<button class="tag-chip" data-tag="保">保</button>'
    + '<button class="tag-chip" data-tag="废">废</button></div>'
    + '<span class="tag-flag"></span>'
    + '<span class="prompt-flag">提</span>'
    + `<span class="cap">${it.name}</span>`;
  el.addEventListener('mousemove', onScrub);
  el.addEventListener('mouseleave', onScrubLeave);
  el.addEventListener('click', onClipClick);
  el.addEventListener('dblclick', (e) => { e.preventDefault(); openPreview(el); });
  el.addEventListener('contextmenu', (e) => onClipContextMenu(e, el));
  el.querySelectorAll('.tag-chip').forEach((chip) => {
    chip.addEventListener('click', (e) => {
      e.stopPropagation();
      const els = (selection.has(el.dataset.uid) && selection.size > 1) ? selEls() : [el];
      toggleTagEls(els, chip.dataset.tag);
    });
  });
  refreshTagFlag(el);
  refreshPromptFlag(el);
  sizeClip(el);
  return el;
}
function refreshTagFlag(el) { const f = el.querySelector('.tag-flag'); if (f) f.textContent = el.dataset.tag || ''; }
function refreshPromptFlag(el) {
  el.classList.toggle('has-prompt', !!(el.dataset.prompt && el.dataset.prompt.trim()));
}

// ---------- 滑动预览（步进用 currentDW，避免错位）----------
function onScrub(e) {
  const el = e.currentTarget;
  const cols = +el.dataset.cols;
  const frac = Math.max(0, Math.min(0.999, e.offsetX / el.clientWidth));
  const idx = Math.min(cols - 1, Math.floor(frac * cols));
  el.dataset.curidx = idx;
  el.style.backgroundPositionX = `-${idx * currentDW}px`;
  const tick = el.querySelector('.scrub-tick'); if (tick) tick.style.left = e.offsetX + 'px';
}
function onScrubLeave(e) { restThumb(e.currentTarget); }

// ---------- 多选 ----------
function clipEls() { return Array.from(document.querySelectorAll('.group:not(.is-filtered) .clip')); }
function selEls() { return clipEls().filter((el) => selection.has(el.dataset.uid)); }
function movableSelEls() { return selEls().filter((el) => !el.classList.contains('locked')); }
function clearSelection() { selection.clear(); anchorUid = null; updateSelUI(); }
function updateSelUI() {
  clipEls().forEach((el) => el.classList.toggle('selected', selection.has(el.dataset.uid)));
  $('sel-info').textContent = selection.size ? (lang === 'en' ? `${selection.size} selected` : `已选中 ${selection.size} 个`) : tr('selectedNone');
}
function onClipClick(e) {
  if (justDragged) return;
  const uid = e.currentTarget.dataset.uid;
  if (e.shiftKey && anchorUid != null) {
    const uids = clipEls().map((x) => x.dataset.uid);
    let a = uids.indexOf(anchorUid), b = uids.indexOf(uid);
    if (a > b) [a, b] = [b, a];
    if (!(e.metaKey || e.ctrlKey)) selection.clear();
    for (let i = a; i <= b; i++) selection.add(uids[i]);
  } else if (e.metaKey || e.ctrlKey) {
    selection.has(uid) ? selection.delete(uid) : selection.add(uid);
    anchorUid = uid;
  } else { selection.clear(); selection.add(uid); anchorUid = uid; }
  updateSelUI();
  e.stopPropagation();
}
function onGroupHeadClick(e, group) {
  if (e.target.closest('button') || e.target.closest('input') || e.target.closest('.group-title')) return;
  const uids = [...group.querySelectorAll('.clip')].map((c) => c.dataset.uid);
  if (!(e.metaKey || e.ctrlKey)) selection.clear();
  uids.forEach((u) => selection.add(u));
  if (uids.length) anchorUid = uids[0];
  updateSelUI();
}
function mergeNew(group) {
  const fl = group.querySelector('.clip-list.newclips');
  const pl = group.querySelector('.clip-list.pending:not(.newclips)');
  if (!fl || !pl) return;
  [...fl.children].forEach((c) => pl.appendChild(c));
  onDragChange();
}

// ---------- 标注 ----------
function toggleTagEls(els, tag) {
  els = els.filter(Boolean);
  if (!els.length) return;
  const touchesCommitted = els.some((el) => el.dataset.committed === '1');
  if (touchesCommitted && !unlocked) {
    alert('已整理的镜头已锁定。请先点右上角「解锁编辑」，修改过/保/废后需要重新确认并执行。');
    return;
  }
  const allSame = els.length && els.every((el) => (el.dataset.tag || '') === tag);
  els.forEach((el) => {
    if (allSame || !tag) delete el.dataset.tag; else el.dataset.tag = tag;
    refreshTagFlag(el);
  });
  if (touchesCommitted) markFullDirty();
  scheduleSave();
}

// ---------- 右键菜单 ----------
function onClipContextMenu(e, el) {
  e.preventDefault();
  if (!selection.has(el.dataset.uid)) { selection.clear(); selection.add(el.dataset.uid); anchorUid = el.dataset.uid; updateSelUI(); }
  showCtxMenu(e.clientX, e.clientY, el);
}
function hideCtxMenu() { $('ctx-menu').hidden = true; $('ctx-menu').innerHTML = ''; }
function showCtxMenu(x, y, el) {
  const menu = $('ctx-menu');
  menu.innerHTML = '';
  const n = selection.size;
  const add = (label, fn, opts = {}) => {
    const it = document.createElement('div');
    it.className = 'ctx-item' + (opts.disabled ? ' disabled' : '') + (opts.danger ? ' danger' : '');
    it.textContent = label;
    if (!opts.disabled) it.addEventListener('click', (ev) => { ev.stopPropagation(); hideCtxMenu(); fn(); });
    menu.appendChild(it);
  };
  const sep = () => { const s = document.createElement('div'); s.className = 'ctx-sep'; menu.appendChild(s); };
  const locked = el.classList.contains('locked');
  add('放大预览', () => openPreview(el));
  add('设为封面（用当前帧）', () => setThumbFromScrub(el));
  sep();
  add('添加提示词', () => showPromptPanel(el));
  add('复制提示词', () => { promptClipboard = el.dataset.prompt || ''; });
  add('粘贴提示词到选中', () => pastePromptToSelection(el), { disabled: promptClipboard === '' });
  add('删除提示词', () => clearPromptFromSelection(el), { disabled: !promptTargetEls(el).some((node) => node.dataset.prompt) });
  if (!locked || unlocked) {
    sep();
    add(`复制（${n}）`, doCopy);
    add(`粘贴（${clipboard.length}）`, () => doPaste(el.closest('.group')), { disabled: clipboard.length === 0 });
    add(`删除（${n}）`, doDelete);
    add('删除原文件', deleteOriginalFiles, { danger: true });
    add('移动到其他组…', () => showMoveSubmenu(x, y));
    add(`用素材新建组（${n}）`, doNewGroupFromSelection);
  }
  sep();
  add('信息', () => showInfo(el.dataset.sid));
  add('在访达中显示', () => api.reveal(el.dataset.sid));
  if (locked) { sep(); add('（已入库的镜头已锁定）', () => {}, { disabled: true }); }
  menu.hidden = false;
  positionMenu(menu, x, y);
}
function positionMenu(menu, x, y) {
  menu.style.left = '0px'; menu.style.top = '0px';
  const r = menu.getBoundingClientRect();
  menu.style.left = Math.min(x, window.innerWidth - r.width - 8) + 'px';
  menu.style.top = Math.min(y, window.innerHeight - r.height - 8) + 'px';
}
function showMoveSubmenu(x, y) {
  const menu = $('ctx-menu');
  menu.innerHTML = '';
  const wrap = document.createElement('div'); wrap.className = 'ctx-submenu';
  document.querySelectorAll('.group:not(.new-zone)').forEach((g) => {
    const it = document.createElement('div'); it.className = 'ctx-item';
    it.textContent = g.querySelector('.group-title').textContent + `（${g.querySelectorAll('.clip').length}）`;
    it.addEventListener('click', (ev) => { ev.stopPropagation(); hideCtxMenu(); moveSelectionTo(g); });
    wrap.appendChild(it);
  });
  const it = document.createElement('div'); it.className = 'ctx-item'; it.textContent = '＋ 新建一组';
  it.addEventListener('click', (ev) => { ev.stopPropagation(); hideCtxMenu(); doNewGroupFromSelection(); });
  wrap.appendChild(it);
  menu.appendChild(wrap); menu.hidden = false; positionMenu(menu, x, y);
}
function setThumbFromScrub(el) {
  const idx = +(el.dataset.curidx != null ? el.dataset.curidx : el.dataset.frame || 0);
  el.dataset.frame = idx;
  restThumb(el);
  api.set_thumb_frame(el.dataset.sid, idx);
  scheduleSave();
}
function doCopy() { clipboard = movableSelEls().map((el) => clipFromEl(el)); updateSelUI(); }
function clipFromEl(el) {
  return { id: el.dataset.sid, name: el.dataset.name, cols: +el.dataset.cols, aspect: +el.dataset.aspect,
           sprite: el.style.backgroundImage.slice(5, -2), seconds: +el.querySelector('.dur').textContent.replace('s', ''),
           tag: el.dataset.tag || '', frame: +(el.dataset.frame || 0), prompt: el.dataset.prompt || '', video: el.dataset.video, committed: false, isNew: false };
}
function currentGroupForPaste() {
  const a = anchorUid && document.querySelector(`.clip[data-uid="${anchorUid}"]`);
  return a ? a.closest('.group') : document.querySelector('.group:not(.new-zone):not(.is-filtered)');
}
function doPaste(group) {
  if (!clipboard.length || !group) return;
  const list = group.querySelector('.clip-list.pending');
  if (!list) return;
  clipboard.forEach((it) => list.appendChild(makeClipEl(it)));
  onDragChange();
}
function doDelete() {
  const els = movableSelEls(); if (!els.length) return;
  if (els.some((el) => el.dataset.committed === '1')) markFullDirty();
  els.forEach((el) => el.remove()); clearSelection(); onDragChange();
}
function promptTargetEls(el) {
  return (selection.has(el.dataset.uid) && selection.size > 1) ? selEls() : [el];
}
function setPromptEls(els, value) {
  els.forEach((node) => { node.dataset.prompt = value; refreshPromptFlag(node); });
  if (previewEl && els.includes(previewEl)) $('preview-prompt').value = value;
  scheduleSave();
}
function ensurePromptPanel(el) {
  if (promptPanelEl && promptPanelEl.parentElement !== el) promptPanelEl.remove();
  if (!promptPanelEl || promptPanelEl.parentElement !== el) {
    promptPanelEl = document.createElement('div');
    promptPanelEl.className = 'clip-prompt-panel';
    promptPanelEl.innerHTML = '<textarea class="clip-prompt-input" placeholder="输入这条镜头的提示词"></textarea>'
      + '<div class="clip-prompt-actions"><span class="clip-prompt-scope"></span>'
      + '<span class="clip-prompt-btns">'
      + '<button class="btn btn-ghost btn-sm clip-prompt-cancel">取消</button>'
      + '<button class="btn btn-primary btn-sm clip-prompt-ok">确认</button></span></div>';
    promptPanelEl.addEventListener('mousedown', (e) => e.stopPropagation());
    promptPanelEl.addEventListener('click', (e) => e.stopPropagation());
    const input = promptPanelEl.querySelector('.clip-prompt-input');
    input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); confirmPromptPanel(el); }
      if (e.key === 'Escape') { e.preventDefault(); hidePromptPanel(); }
    });
    promptPanelEl.querySelector('.clip-prompt-ok').addEventListener('click', () => confirmPromptPanel(el));
    promptPanelEl.querySelector('.clip-prompt-cancel').addEventListener('click', () => hidePromptPanel());
    el.appendChild(promptPanelEl);
  }
  return promptPanelEl;
}
function showPromptPanel(el) {
  const panel = ensurePromptPanel(el);
  const els = promptTargetEls(el);
  const input = panel.querySelector('.clip-prompt-input');
  const scope = panel.querySelector('.clip-prompt-scope');
  input.value = els.length === 1 ? (els[0].dataset.prompt || '') : '';
  input.placeholder = els.length > 1 ? `输入后会应用到 ${els.length} 条选中素材` : '输入这条镜头的提示词';
  scope.textContent = els.length > 1 ? `应用到 ${els.length} 条` : 'Enter 确认';
  el.classList.add('prompt-open');
  requestAnimationFrame(() => panel.classList.add('open'));
  setTimeout(() => input.focus(), 80);
}
function hidePromptPanel() {
  if (!promptPanelEl) return;
  const owner = promptPanelEl.parentElement;
  promptPanelEl.classList.remove('open');
  if (owner) owner.classList.remove('prompt-open');
}
function confirmPromptPanel(el) {
  if (!promptPanelEl) return;
  const value = promptPanelEl.querySelector('.clip-prompt-input').value;
  setPromptEls(promptTargetEls(el), value);
  hidePromptPanel();
}
function pastePromptToSelection(el) {
  setPromptEls(promptTargetEls(el), promptClipboard);
}
function clearPromptFromSelection(el) {
  setPromptEls(promptTargetEls(el), '');
}
async function deleteOriginalFiles() {
  const els = selEls();
  if (!els.length) return;
  const touchesCommitted = els.some((el) => el.dataset.committed === '1');
  if (touchesCommitted && !unlocked) { alert('已整理的镜头已锁定。请先解锁，再删除原文件。'); return; }
  if (!confirm(`确定把 ${els.length} 个原文件移到「废纸篓」吗？\n它们会从素材文件夹移走，但仍可在废纸篓里找回。`)) return;
  const ids = els.map((el) => el.dataset.sid);
  const res = await api.delete_files(ids);
  if (res.error) { alert(res.error); return; }
  if (touchesCommitted) markFullDirty();
  els.forEach((el) => el.remove());
  clearSelection(); onDragChange();
}
function moveSelectionTo(group) {
  const list = group.querySelector('.clip-list.pending'); if (!list) return;
  const els = movableSelEls();
  const fromGroups = new Set(els.map((el) => el.closest('.group')).filter(Boolean));
  if ([...fromGroups].some((g) => !sameGroupPrefix(g, group))) {
    alert('不同前缀的分组不能互相混放。请先把分组改成同一前缀，或新建同前缀分组。');
    return;
  }
  if (els.some((el) => el.dataset.committed === '1')) markFullDirty();
  els.forEach((el) => list.appendChild(el)); onDragChange();
}
function doNewGroupFromSelection() {
  const els = movableSelEls(); if (!els.length) return;
  if (els.some((el) => el.dataset.committed === '1')) markFullDirty();
  const g = makeGroupEl([]);
  $('groups').insertBefore(g, document.querySelector('.group.new-zone'));
  const list = g.querySelector('.clip-list.pending');
  els.forEach((el) => list.appendChild(el));
  onDragChange();
}
async function showInfo(orig) {
  const info = await api.media_info(orig);
  const tbl = $('info-table'); tbl.innerHTML = '';
  if (info.error) tbl.innerHTML = `<tr><td>错误</td><td>${info.error}</td></tr>`;
  else Object.entries(info).forEach(([k, v]) => { const tr = document.createElement('tr'); tr.innerHTML = `<td>${k}</td><td>${v}</td>`; tbl.appendChild(tr); });
  $('info-modal').hidden = false;
}

// ---------- 放大预览 ----------
function openPreview(el) {
  previewEl = el;
  const v = $('preview-video');
  v.src = el.dataset.video || ('videos/' + encodeURIComponent(el.dataset.name));
  v.controls = true;
  v.loop = true;
  $('preview-name').textContent = el.dataset.name;
  $('preview-prompt').value = el.dataset.prompt || '';
  previewPromptOrig = el.dataset.prompt || '';
  updatePreviewTags();
  $('preview-modal').hidden = false;
  setTimeout(() => {
    const p = v.play();
    if (p && p.catch) p.catch(() => {});
  }, 50);
}
function updatePreviewTags() {
  const tag = previewEl ? (previewEl.dataset.tag || '') : '';
  $('preview-modal').querySelectorAll('.tag-btn').forEach((b) => b.classList.toggle('active', b.dataset.tag === tag && tag !== ''));
}
function closePreview() {
  if ($('preview-modal').hidden) return;
  if (previewFs) setPreviewFs(false);
  $('preview-modal').hidden = true;
  const v = $('preview-video'); v.pause(); v.removeAttribute('src'); v.load();
  previewEl = null;
}
function stepPreview(delta) {
  if (!previewEl) return;
  const list = clipEls();
  const i = list.indexOf(previewEl);
  if (i < 0 || !list.length) return;
  const next = list[(i + delta + list.length) % list.length];
  openPreview(next);
}
function togglePreviewPlay() {
  const v = $('preview-video');
  if (!v.src) return;
  if (v.paused) {
    const p = v.play();
    if (p && p.catch) p.catch(() => {});
  } else v.pause();
}
function previewFullscreen() { setPreviewFs(!previewFs); }
function setPreviewFs(on) {
  previewFs = on;
  const modal = $('preview-modal');
  modal.classList.toggle('fs', on);
  setIconButton($('preview-fullscreen'), SVG.maximize, on ? '退出全屏' : '全屏');
  // 用浏览器原生全屏：视频真正占满整块屏幕，且不依赖/不改变软件窗口的全屏状态，
  // 从而不会把软件本身的全屏给退掉。若 WebKit 不支持，.fs 样式也会让视频铺满窗口兜底。
  try {
    if (on) {
      const req = modal.requestFullscreen || modal.webkitRequestFullscreen;
      if (req) { const p = req.call(modal); if (p && p.catch) p.catch(() => {}); }
    } else {
      const el = document.fullscreenElement || document.webkitFullscreenElement;
      if (el) {
        const exit = document.exitFullscreen || document.webkitExitFullscreen;
        if (exit) exit.call(document);
      }
    }
  } catch (e) { /* 忽略：兜底靠 .fs 样式 */ }
}
function confirmPreviewPrompt() {
  if (!previewEl) return;
  const value = $('preview-prompt').value;
  previewEl.dataset.prompt = value;
  previewPromptOrig = value;
  refreshPromptFlag(previewEl);
  scheduleSave();
  $('preview-prompt').blur();
}
function cancelPreviewPrompt() {
  $('preview-prompt').value = previewPromptOrig;
  $('preview-prompt').blur();
}

// ---------- 圈选 ----------
let marquee = { active: false, x0: 0, y0: 0, additive: false };
function onMarqueeStart(e) {
  if (e.button !== 0) return;
  if (e.target.closest('.clip') || e.target.closest('.group-head') || e.target.matches('input,button,textarea')) return;
  marquee.active = true; marquee.additive = e.metaKey || e.ctrlKey;
  marquee.x0 = e.clientX; marquee.y0 = e.clientY;
  if (!marquee.additive) clearSelection();
  document.body.style.userSelect = 'none';
  document.addEventListener('mousemove', onMarqueeMove);
  document.addEventListener('mouseup', onMarqueeEnd);
}
function onMarqueeMove(e) {
  if (!marquee.active) return;
  const x = Math.min(e.clientX, marquee.x0), y = Math.min(e.clientY, marquee.y0);
  const w = Math.abs(e.clientX - marquee.x0), h = Math.abs(e.clientY - marquee.y0);
  const box = $('marquee'); box.hidden = false;
  box.style.left = x + 'px'; box.style.top = y + 'px'; box.style.width = w + 'px'; box.style.height = h + 'px';
  const rect = { left: x, top: y, right: x + w, bottom: y + h };
  clipEls().forEach((el) => {
    const r = el.getBoundingClientRect();
    const hit = !(r.right < rect.left || r.left > rect.right || r.bottom < rect.top || r.top > rect.bottom);
    if (hit) selection.add(el.dataset.uid); else if (!marquee.additive) selection.delete(el.dataset.uid);
  });
  updateSelUI();
}
function onMarqueeEnd() {
  marquee.active = false; $('marquee').hidden = true; document.body.style.userSelect = '';
  document.removeEventListener('mousemove', onMarqueeMove);
  document.removeEventListener('mouseup', onMarqueeEnd);
}

// ---------- 拖拽 ----------
function rebuildSortables() {
  sortables.forEach((s) => s.destroy()); sortables = [];
  // 关键：拖动中不要重建 Sortable（否则会销毁正在拖动的实例，导致批量搬运失效），
  // 所有结构变更都放到 onEnd 之后处理。
  const opt = {
    group: 'shots', animation: 140, ghostClass: 'sortable-ghost', dragClass: 'sortable-drag',
    filter: '.locked', preventOnFilter: false, onStart: onDragStart, onMove: onClipDragMove, onEnd: onDragEnd,
  };
  document.querySelectorAll('.clip-list.pending').forEach((list) => sortables.push(new Sortable(list, opt)));
  // 解锁后，已入库的 committed 列表也可拖动
  if (unlocked) document.querySelectorAll('.clip-list.committed').forEach((list) => sortables.push(new Sortable(list, opt)));
  if (groupSortable) groupSortable.destroy();
  groupSortable = new Sortable($('groups'), {
    handle: '.group-head', draggable: '.group', filter: '.new-zone', animation: 160,
    onMove: onGroupSortMove,
    onEnd: () => { const z = document.querySelector('.group.new-zone'); if (z) $('groups').appendChild(z); renumber(); updateSummary(); scheduleSave(); },
  });
  groupSortable.option('disabled', !isAllView() || !canEditGroupStructure());
}
function onClipDragMove(evt) {
  const fromGroup = evt.from && evt.from.closest('.group');
  const toGroup = evt.to && evt.to.closest('.group');
  if (!sameGroupPrefix(fromGroup, toGroup)) return false;
  return true;
}
function rebuildGroupNavSortable() {
  const nav = $('group-nav');
  if (!nav || typeof Sortable === 'undefined') return;
  if (groupNavSortable) groupNavSortable.destroy();
  groupNavSortable = new Sortable(nav, {
    handle: '.side-drag',
    draggable: '.group-side-item[data-gid]',
    filter: '.group-side-all,.group-nav-sep',
    animation: 140,
    onMove: onGroupSortMove,
    onEnd: () => {
      const order = [...nav.querySelectorAll('.group-side-item[data-gid]')].map((el) => el.dataset.gid);
      const map = new Map(groupElsAll().map((g) => [g.dataset.gid, g]));
      const root = $('groups');
      order.forEach((id) => { const g = map.get(id); if (g) root.appendChild(g); });
      const z = document.querySelector('.group.new-zone');
      if (z) root.appendChild(z);
      renumber(); updateSummary(); scheduleSave(); refreshGroupNav();
    },
  });
  groupNavSortable.option('disabled', !canEditGroupStructure());
  nav.classList.toggle('sortable-disabled', !canEditGroupStructure());
}
function onDragStart(evt) {
  const uid = evt.item.dataset.uid;
  if (!selection.has(uid)) { selection.clear(); selection.add(uid); anchorUid = uid; updateSelUI(); }
  draggingMulti = selection.size > 1;
}
function onDragEnd(evt) {
  justDragged = true; setTimeout(() => { justDragged = false; }, 0);
  const moved = [evt.item];
  if (draggingMulti) {
    const others = selEls().filter((el) => el !== evt.item);
    let ref = evt.item; others.forEach((node) => { ref.after(node); ref = node; moved.push(node); });
  }
  draggingMulti = false;
  if (moved.some((el) => el.dataset.committed === '1')) markFullDirty();
  onDragChange();
}
function markFullDirty() { fullDirty = true; updateApplyUI(); }
function updateApplyUI() {
  const btn = $('btn-apply');
  if (fullDirty) { btn.textContent = tr('rebuildApply'); btn.classList.add('warn'); }
  else { btn.textContent = tr('apply'); btn.classList.remove('warn'); }
}
function onDragChange() {
  document.querySelectorAll('.group.new-zone').forEach((z) => {
    if (z.querySelector('.clip-list.pending').children.length > 0) {
      z.classList.remove('new-zone');
      const t = z.querySelector('.group-title'); if (t) t.style.color = '';
    }
  });
  if (!document.querySelector('.group.new-zone')) { $('groups').appendChild(makeNewZone()); }
  removeEmptyGroups();
  rebuildSortables();
  renumber(); ensureActiveGroup(false); updateSummary(); updateSelUI(); refreshGroupNav(); scheduleSave();
}
function removeEmptyGroups() {
  document.querySelectorAll('.group:not(.new-zone)').forEach((g) => { if (g.querySelectorAll('.clip').length === 0) g.remove(); });
}
// 同一文字前缀下，编号按"出现顺序"从1连续递增（不重复、不中断）；
// 没有前缀的新组继承它上面一组的前缀。增删/拖动换位后实时重排。
function renumber() {
  let prev = '组';
  const counters = {};
  document.querySelectorAll('.group:not(.new-zone)').forEach((g) => {
    let prefix = g.dataset.prefix;
    if (!prefix) { prefix = prev; g.dataset.prefix = prefix; }   // 新组跟随上一栏前缀
    prev = prefix;
    counters[prefix] = (counters[prefix] || 0) + 1;
    const name = `${prefix}_${counters[prefix]}`;
    g.dataset.name = name;
    const t = g.querySelector('.group-title');
    if (t) t.textContent = name;
    g.querySelector('.group-count').textContent = `（${g.querySelectorAll('.clip').length} 个）`;
  });
  updateStackToggles();
  applyActiveGroupView();
}
// #2 新增镜头数量实时更新：拖走一个减1，全部拖走后提示去确认并执行
function updateStackToggles() {
  document.querySelectorAll('.group:not(.new-zone)').forEach((g) => {
    const toggle = g.querySelector('.stack-toggle');
    if (!toggle) return;
    const flist = g.querySelector('.clip-list.newclips');
    const count = flist ? flist.querySelectorAll('.clip').length : 0;
    const txt = toggle.querySelector('.stack-txt');
    if (count > 0) {
      txt.textContent = `${count} 个新增镜头（点击展开）`;
      toggle.classList.remove('done');
    } else {
      txt.textContent = '新增镜头已就位，待「确认并执行」';
      toggle.classList.add('done');
    }
  });
}
function updateSummary() {
  renumber();
  const groups = readGroups();
  const clips = groups.reduce((a, g) => a + g.clips.length, 0);
  $('summary').textContent = lang === 'en' ? `${clips} clips in ${groups.length} groups` : `共 ${clips} 个素材，分成 ${groups.length} 组`;
  refreshGroupNav();
}

async function onApply() {
  const groups = readGroups();
  if (!groups.length) return;
  const modeEl = document.querySelector('input[name="mode"]:checked');
  const mode = modeEl ? modeEl.value : 'copy';
  const verb = mode === 'move' ? '移动' : '复制';
  let res;
  $('btn-apply').disabled = true;
  if (fullDirty) {
    if (!confirm(`你改动了已入库的内容/组名/标记。\n确认并执行将【删除旧的「${libName}」文件夹，按当前结果整体重新${verb}整理一遍】。继续吗？`)) { $('btn-apply').disabled = false; return; }
    res = await api.apply_full(groups, mode);
  } else {
    if (!confirm(`确定把新增镜头${verb}到「${libName}」吗？\n（已入库的老镜头不会重复处理）`)) { $('btn-apply').disabled = false; return; }
    res = await api.apply(groups, mode);
  }
  $('btn-apply').disabled = false;
  if (res.error) { alert(res.error); return; }
  $('summary').textContent = `完成！本次${verb} ${res.count} 个文件到「${libName}」`;
  $('btn-open').hidden = false;
  fullDirty = false;
  unlocked = false;   // 执行后重新锁定
  renderGroups((await api.current_payload()).groups);
}
