<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
import { invoke } from "@tauri-apps/api/core";
import { openPath, openUrl } from "@tauri-apps/plugin-opener";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { getVersion } from "@tauri-apps/api/app";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { getCurrentWebview } from "@tauri-apps/api/webview";

type NavKey = "files" | "transfers" | "shares" | "sync" | "settings";

type FileItem = {
  name: string;
  displayName?: string;
  path?: string;
  type: "file" | "directory";
  size?: number;
  sizeFormatted?: string;
  modifiedAt?: string;
  isDirectory?: boolean;
};

type ShareItem = {
  id: number;
  share_code: string;
  share_url: string;
  share_path: string;
  share_type: "file" | "directory";
  has_password?: boolean;
  view_count?: number;
  download_count?: number;
  created_at?: string;
  expires_at?: string | null;
  storage_type?: string;
};

type OnlineDeviceItem = {
  session_id: string;
  client_type?: string;
  device_name?: string;
  platform?: string;
  ip_address?: string;
  last_active_at?: string;
  created_at?: string;
  expires_at?: string;
  is_current?: boolean;
  is_local?: boolean;
};

type BridgeResponse = {
  ok: boolean;
  status: number;
  data: Record<string, any>;
};

type NativeDownloadProgressEvent = {
  taskId?: string;
  downloadedBytes?: number;
  totalBytes?: number | null;
  progress?: number | null;
  resumedBytes?: number;
  done?: boolean;
};

type NativeUploadProgressEvent = {
  taskId?: string;
  uploadedBytes?: number;
  totalBytes?: number;
  progress?: number;
  done?: boolean;
};

type LocalSyncFileItem = {
  path: string;
  relativePath: string;
  size: number;
  modifiedMs: number;
};

type TransferTaskKind = "upload" | "download";
type TransferTaskStatus = "queued" | "uploading" | "downloading" | "done" | "failed";
type TransferTask = {
  id: string;
  kind: TransferTaskKind;
  name: string;
  speed: string;
  progress: number;
  status: TransferTaskStatus;
  note?: string;
  filePath?: string;
  targetPath?: string;
  downloadUrl?: string;
  fileName?: string;
};

const nav = ref<NavKey>("files");
const authenticated = ref(false);
const user = ref<Record<string, any> | null>(null);

const appConfig = reactive({
  baseUrl: "https://cs.workyai.cn",
});

const loginForm = reactive({
  username: "",
  password: "",
  captcha: "",
});

const loginState = reactive({
  loading: false,
  error: "",
  needCaptcha: false,
});

const pathState = reactive({
  currentPath: "/",
  loading: false,
  error: "",
  mode: "directory" as "directory" | "search",
});

const files = ref<FileItem[]>([]);
const selectedFileName = ref("");
const searchKeyword = ref("");
const shares = ref<ShareItem[]>([]);
const batchMode = ref(false);
const batchSelectedNames = ref<string[]>([]);

const transferTasks = ref<TransferTask[]>([]);
const sharesLoading = ref(false);
const transferQueue = reactive({
  paused: false,
});
const fileViewState = reactive({
  filter: "all",
  sortBy: "modifiedAt",
  sortOrder: "desc" as "asc" | "desc",
});
const syncState = reactive({
  localDir: "",
  remoteBasePath: "/",
  autoEnabled: false,
  intervalMinutes: 15,
  syncing: false,
  scanning: false,
  pendingCount: 0,
  uploadedCount: 0,
  failedCount: 0,
  lastRunAt: "",
  lastSummary: "",
  nextRunAt: "",
});
const updateState = reactive({
  currentVersion: "0.1.24",
  latestVersion: "",
  available: false,
  mandatory: false,
  checking: false,
  downloadUrl: "",
  releaseNotes: "",
  lastCheckedAt: "",
  message: "",
});
const onlineDevices = reactive({
  loading: false,
  kickingSessionId: "",
  items: [] as OnlineDeviceItem[],
  message: "",
  lastLoadedAt: "",
});
const updateRuntime = reactive({
  downloading: false,
  installing: false,
  taskId: "",
  downloadedBytes: 0,
  totalBytes: 0,
  progress: 0,
  speed: "-",
  lastMeasureAt: 0,
  lastMeasureBytes: 0,
  installerPath: "",
});
const updatePrompt = reactive({
  visible: false,
  loading: false,
});
const contextMenu = reactive({
  visible: false,
  x: 0,
  y: 0,
  item: null as FileItem | null,
});
const shareDeleteDialog = reactive({
  visible: false,
  loading: false,
  share: null as ShareItem | null,
});
const fileDeleteDialog = reactive({
  visible: false,
  loading: false,
  file: null as FileItem | null,
});
const operationConfirmDialog = reactive({
  visible: false,
  loading: false,
  mode: "" as "" | "kick-device" | "batch-delete",
  title: "",
  message: "",
  confirmText: "确定",
  sessionId: "",
  batchItems: [] as FileItem[],
});
const inlineRename = reactive({
  active: false,
  originalName: "",
  value: "",
  saving: false,
});
const dropState = reactive({
  active: false,
  uploading: false,
  total: 0,
  done: 0,
  failed: 0,
});
const uploadRuntime = reactive({
  active: false,
  taskId: "",
  fileName: "",
  uploadedBytes: 0,
  totalBytes: 0,
  progress: 0,
  speed: "-",
  lastMeasureAt: 0,
  lastMeasureBytes: 0,
});
let unlistenDragDrop: UnlistenFn | null = null;
let unlistenNativeDownloadProgress: UnlistenFn | null = null;
let unlistenNativeUploadProgress: UnlistenFn | null = null;
let syncTimer: ReturnType<typeof setInterval> | null = null;
let hasCheckedUpdateAfterAuth = false;
let authRefreshPromise: Promise<boolean> | null = null;

const toast = reactive({
  visible: false,
  type: "info",
  message: "",
});
let toastTimer: ReturnType<typeof setTimeout> | null = null;

const navItems = computed(() => [
  { key: "files" as const, label: "全部文件", hint: `${files.value.length} 项` },
  { key: "transfers" as const, label: "传输列表", hint: `${transferTasks.value.length} 个任务` },
  { key: "shares" as const, label: "我的分享", hint: `${shares.value.length} 条` },
  { key: "sync" as const, label: "同步盘", hint: syncState.localDir ? "已配置" : "未配置" },
  { key: "settings" as const, label: "设置", hint: updateState.available ? "发现新版本" : "系统与更新" },
]);

const sortedShares = computed(() => {
  return [...shares.value].sort((a, b) => {
    const ta = new Date(a.created_at || 0).getTime();
    const tb = new Date(b.created_at || 0).getTime();
    return tb - ta;
  });
});

const fileTypeFilterOptions = [
  { value: "all", label: "全部类型" },
  { value: "directory", label: "仅文件夹" },
  { value: "file", label: "仅文件" },
  { value: "image", label: "图片" },
  { value: "video", label: "视频" },
  { value: "document", label: "文档" },
  { value: "archive", label: "压缩包" },
];

const fileSortOptions = [
  { value: "modifiedAt", label: "按时间" },
  { value: "name", label: "按名称" },
  { value: "size", label: "按大小" },
  { value: "type", label: "按类型" },
];

const filteredFiles = computed(() => {
  const key = searchKeyword.value.trim().toLowerCase();
  const filtered = files.value.filter((item) => {
    const name = String(item.displayName || item.name || "").toLowerCase();
    if (key && !name.includes(key)) return false;

    const typeFilter = fileViewState.filter;
    if (typeFilter === "all") return true;
    if (typeFilter === "directory") return Boolean(item.isDirectory || item.type === "directory");
    if (typeFilter === "file") return !item.isDirectory && item.type !== "directory";
    return matchFileTypeFilter(item, typeFilter);
  });

  const orderFactor = fileViewState.sortOrder === "asc" ? 1 : -1;
  const sorted = [...filtered].sort((a, b) => {
    const sortBy = fileViewState.sortBy;
    if (sortBy === "name") {
      const av = String(a.displayName || a.name || "");
      const bv = String(b.displayName || b.name || "");
      return av.localeCompare(bv, "zh-CN", { sensitivity: "base" }) * orderFactor;
    }
    if (sortBy === "size") {
      const av = Number(a.size || 0);
      const bv = Number(b.size || 0);
      return (av - bv) * orderFactor;
    }
    if (sortBy === "type") {
      const av = a.isDirectory || a.type === "directory" ? "directory" : "file";
      const bv = b.isDirectory || b.type === "directory" ? "directory" : "file";
      if (av === bv) {
        return String(a.displayName || a.name || "").localeCompare(String(b.displayName || b.name || ""), "zh-CN", { sensitivity: "base" }) * orderFactor;
      }
      return av.localeCompare(bv, "zh-CN", { sensitivity: "base" }) * orderFactor;
    }
    const av = a.modifiedAt ? new Date(a.modifiedAt).getTime() : 0;
    const bv = b.modifiedAt ? new Date(b.modifiedAt).getTime() : 0;
    return (av - bv) * orderFactor;
  });
  return sorted;
});

const breadcrumbs = computed(() => {
  const normalized = normalizePath(pathState.currentPath);
  if (normalized === "/") {
    return [{ label: "全部文件", path: "/" }];
  }
  const segments = normalized.split("/").filter(Boolean);
  const nodes = [{ label: "全部文件", path: "/" }];
  let cursor = "";
  for (const seg of segments) {
    cursor += `/${seg}`;
    nodes.push({ label: seg, path: cursor });
  }
  return nodes;
});

const toolbarCrumbs = computed(() => {
  if (nav.value === "files") return breadcrumbs.value;
  const map: Record<NavKey, string> = {
    files: "全部文件",
    transfers: "传输列表",
    shares: "我的分享",
    sync: "同步盘",
    settings: "设置",
  };
  return [{ label: "工作台", path: "/" }, { label: map[nav.value], path: "" }];
});

const selectedFile = computed(() => {
  if (!selectedFileName.value) return null;
  return files.value.find((item) => item.name === selectedFileName.value) || null;
});

const batchSelectedItems = computed(() => {
  const selectedSet = new Set(batchSelectedNames.value);
  return files.value.filter((item) => selectedSet.has(item.name));
});

const fileStats = computed(() => {
  const folders = files.value.filter((item) => item.isDirectory || item.type === "directory").length;
  const docs = files.value.length - folders;
  const totalBytes = files.value.reduce((sum, item) => sum + Number(item.size || 0), 0);
  return {
    folders,
    docs,
    total: files.value.length,
    totalBytes,
  };
});

function mapApiItem(raw: Record<string, any>): FileItem {
  const isDirectory = Boolean(raw?.isDirectory || raw?.is_directory || raw?.type === "directory" || raw?.type === "d");
  const fallbackName = String(raw?.name || raw?.displayName || raw?.file_name || "").trim();
  return {
    name: fallbackName,
    displayName: String(raw?.displayName || fallbackName),
    path: typeof raw?.path === "string" ? raw.path : (typeof raw?.file_path === "string" ? raw.file_path : undefined),
    type: isDirectory ? "directory" : "file",
    size: Number(raw?.size || 0),
    sizeFormatted: raw?.sizeFormatted || raw?.size_formatted || undefined,
    modifiedAt: raw?.modifiedAt || raw?.modified_at || raw?.modifyTime || raw?.modifiedTime || raw?.updatedAt || undefined,
    isDirectory,
  };
}

function normalizePath(rawPath: string) {
  const source = String(rawPath || "/").replace(/\\/g, "/");
  const normalized = source.replace(/\/+/g, "/");
  if (!normalized || normalized === ".") return "/";
  return normalized.startsWith("/") ? normalized : `/${normalized}`;
}

function formatDate(value: string | undefined) {
  if (!value) return "-";
  const raw = String(value).trim();
  if (!raw) return "-";
  const localMatch = raw.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
  let date: Date | null = null;
  if (localMatch) {
    date = new Date(
      Number(localMatch[1]),
      Number(localMatch[2]) - 1,
      Number(localMatch[3]),
      Number(localMatch[4] || 0),
      Number(localMatch[5] || 0),
      Number(localMatch[6] || 0),
    );
  } else {
    const fallback = new Date(raw);
    if (!Number.isNaN(fallback.getTime())) {
      date = fallback;
    }
  }
  if (!date) return raw;
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
}

function formatBytes(value: number | undefined) {
  const bytes = Number(value || 0);
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
  const units = ["B", "KB", "MB", "GB", "TB"];
  let num = bytes;
  let unit = 0;
  while (num >= 1024 && unit < units.length - 1) {
    num /= 1024;
    unit += 1;
  }
  const fixed = num >= 10 || unit === 0 ? 1 : 2;
  return `${num.toFixed(fixed)} ${units[unit]}`;
}

function fileTypeLabel(item: FileItem) {
  if (item.isDirectory || item.type === "directory") return "文件夹";
  return "文件";
}

function formatSpeed(bytesPerSecond: number) {
  if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "-";
  return `${formatBytes(bytesPerSecond)}/s`;
}

function measureRate(
  currentBytes: number,
  state: { lastMeasureAt: number; lastMeasureBytes: number },
) {
  const now = Date.now();
  if (!state.lastMeasureAt) {
    state.lastMeasureAt = now;
    state.lastMeasureBytes = currentBytes;
    return "-";
  }
  const elapsedMs = now - state.lastMeasureAt;
  if (elapsedMs < 180) {
    return "-";
  }
  const deltaBytes = Math.max(0, currentBytes - state.lastMeasureBytes);
  state.lastMeasureAt = now;
  state.lastMeasureBytes = currentBytes;
  if (deltaBytes <= 0) return "0 B/s";
  const bytesPerSecond = (deltaBytes * 1000) / elapsedMs;
  return formatSpeed(bytesPerSecond);
}

function resetUpdateRuntime() {
  updateRuntime.downloading = false;
  updateRuntime.installing = false;
  updateRuntime.taskId = "";
  updateRuntime.downloadedBytes = 0;
  updateRuntime.totalBytes = 0;
  updateRuntime.progress = 0;
  updateRuntime.speed = "-";
  updateRuntime.lastMeasureAt = 0;
  updateRuntime.lastMeasureBytes = 0;
  updateRuntime.installerPath = "";
}

function applyNativeDownloadProgress(payload: NativeDownloadProgressEvent) {
  const taskId = String(payload?.taskId || "").trim();
  if (!taskId) return;

  const downloadedBytes = Number(payload?.downloadedBytes || 0);
  const totalBytesRaw = payload?.totalBytes;
  const totalBytes = totalBytesRaw === null || totalBytesRaw === undefined ? NaN : Number(totalBytesRaw);
  const eventProgress = Number(payload?.progress);
  const calculatedProgress = Number.isFinite(eventProgress)
    ? eventProgress
    : (Number.isFinite(totalBytes) && totalBytes > 0 ? (downloadedBytes / totalBytes) * 100 : NaN);
  const boundedProgress = Number.isFinite(calculatedProgress)
    ? Math.max(1, Math.min(payload?.done ? 100 : 99.8, calculatedProgress))
    : NaN;

  if (taskId === updateRuntime.taskId) {
    updateRuntime.downloadedBytes = downloadedBytes;
    updateRuntime.totalBytes = Number.isFinite(totalBytes) ? Math.max(0, totalBytes) : 0;
    updateRuntime.progress = Number.isFinite(boundedProgress)
      ? Number(boundedProgress.toFixed(1))
      : updateRuntime.progress;
    const speedText = measureRate(downloadedBytes, updateRuntime);
    if (speedText !== "-") {
      updateRuntime.speed = speedText;
    }
    if (payload?.done) {
      updateRuntime.progress = 100;
      updateRuntime.speed = "-";
    }
    return;
  }

  const patch: Partial<TransferTask> = {
    speed: "下载中",
    status: "downloading",
    note: Number.isFinite(totalBytes) && totalBytes > 0
      ? `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}`
      : `已下载 ${formatBytes(downloadedBytes)}`,
  };
  if (Number.isFinite(boundedProgress)) {
    patch.progress = Number(boundedProgress.toFixed(1));
  }

  updateTransferTask(taskId, patch);
}

function applyNativeUploadProgress(payload: NativeUploadProgressEvent) {
  const taskId = String(payload?.taskId || "").trim();
  if (!taskId) return;

  const uploadedBytes = Number(payload?.uploadedBytes || 0);
  const totalBytes = Math.max(1, Number(payload?.totalBytes || 0));
  const eventProgress = Number(payload?.progress);
  const progressValue = Number.isFinite(eventProgress)
    ? eventProgress
    : (uploadedBytes / totalBytes) * 100;
  const boundedProgress = Math.max(1, Math.min(payload?.done ? 100 : 99.8, progressValue));
  let transferSpeed = "上传中";
  if (taskId === uploadRuntime.taskId) {
    const sampledSpeed = measureRate(uploadedBytes, uploadRuntime);
    if (sampledSpeed !== "-") {
      transferSpeed = sampledSpeed;
    }
  }
  updateTransferTask(taskId, {
    status: "uploading",
    speed: transferSpeed,
    progress: Number(boundedProgress.toFixed(1)),
    note: `${formatBytes(uploadedBytes)} / ${formatBytes(totalBytes)}`,
  });

  if (taskId === uploadRuntime.taskId) {
    uploadRuntime.uploadedBytes = uploadedBytes;
    uploadRuntime.totalBytes = totalBytes;
    uploadRuntime.progress = Number(boundedProgress.toFixed(1));
    if (transferSpeed !== "上传中") {
      uploadRuntime.speed = transferSpeed;
    }
    if (payload?.done) {
      uploadRuntime.progress = 100;
      uploadRuntime.speed = "-";
    }
  }
}

function getFileExt(item: FileItem) {
  if (item.isDirectory || item.type === "directory") return "";
  const text = String(item.displayName || item.name || "").trim();
  const idx = text.lastIndexOf(".");
  if (idx <= 0 || idx >= text.length - 1) return "";
  return text.slice(idx + 1).toLowerCase();
}

function fileVisualKind(item: FileItem) {
  if (item.isDirectory || item.type === "directory") return "folder";
  const ext = getFileExt(item);
  if (!ext) return "file";
  if (["jpg", "jpeg", "png", "webp", "gif", "bmp", "svg", "heic"].includes(ext)) return "image";
  if (["mp4", "mkv", "mov", "avi", "webm", "flv"].includes(ext)) return "video";
  if (["mp3", "wav", "flac", "aac", "ogg", "m4a"].includes(ext)) return "audio";
  if (["zip", "rar", "7z", "tar", "gz", "bz2", "xz"].includes(ext)) return "archive";
  if (["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "md", "csv"].includes(ext)) return "document";
  if (["apk", "exe", "msi", "dmg", "deb", "rpm"].includes(ext)) return "app";
  return "file";
}

function fileExtLabel(item: FileItem) {
  const ext = getFileExt(item).toUpperCase();
  if (!ext) return "FILE";
  return ext.slice(0, 4);
}

function matchFileTypeFilter(item: FileItem, type: string) {
  if (item.isDirectory || item.type === "directory") return false;
  const name = String(item.name || "").toLowerCase();
  if (type === "image") return /\.(jpg|jpeg|png|webp|gif|bmp|svg)$/.test(name);
  if (type === "video") return /\.(mp4|mkv|mov|avi|webm)$/.test(name);
  if (type === "document") return /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|md|csv)$/.test(name);
  if (type === "archive") return /\.(zip|rar|7z|tar|gz|bz2|xz)$/.test(name);
  return true;
}

function normalizeSharePath(rawPath: string | undefined) {
  const normalized = normalizePath(rawPath || "/");
  return normalized;
}

function getShareDisplayName(share: ShareItem) {
  const normalized = normalizeSharePath(share.share_path);
  if (normalized === "/") return "全部文件";
  const segments = normalized.split("/").filter(Boolean);
  return segments[segments.length - 1] || normalized;
}

function getShareExpireLabel(value: string | null | undefined) {
  if (!value) return "永久有效";
  const text = formatDate(value);
  const expired = new Date(value).getTime() <= Date.now();
  return expired ? `已过期 · ${text}` : text;
}

async function copyText(text: string, successMessage: string) {
  try {
    if (navigator.clipboard?.writeText) {
      await navigator.clipboard.writeText(text);
      showToast(successMessage, "success");
      return;
    }
  } catch {
    // fall through to legacy copy path
  }

  const textarea = document.createElement("textarea");
  textarea.value = text;
  textarea.style.position = "fixed";
  textarea.style.opacity = "0";
  document.body.appendChild(textarea);
  textarea.focus();
  textarea.select();
  const ok = document.execCommand("copy");
  document.body.removeChild(textarea);
  showToast(ok ? successMessage : "复制失败，请手动复制", ok ? "success" : "error");
}

function showToast(message: string, type = "info") {
  toast.message = message;
  toast.type = type;
  toast.visible = true;
  if (toastTimer) clearTimeout(toastTimer);
  toastTimer = setTimeout(() => {
    toast.visible = false;
  }, 2500);
}

function prependTransferTask(task: TransferTask) {
  transferTasks.value = [task, ...transferTasks.value.slice(0, 119)];
}

function updateTransferTask(
  id: string,
  patch: Partial<TransferTask>,
) {
  transferTasks.value = transferTasks.value.map((task) => (task.id === id ? { ...task, ...patch } : task));
}

function getTaskStatusLabel(status: string) {
  if (status === "queued") return "排队中";
  if (status === "uploading") return "上传中";
  if (status === "downloading") return "下载中";
  if (status === "done") return "已完成";
  if (status === "failed") return "失败";
  return status;
}

function isTaskRunning(status: string) {
  return status === "uploading" || status === "downloading";
}

function removeTransferTask(taskId: string) {
  transferTasks.value = transferTasks.value.filter((task) => task.id !== taskId);
}

function clearCompletedTransferTasks() {
  transferTasks.value = transferTasks.value.filter((task) => task.status !== "done" && task.status !== "failed");
}

function toggleTransferQueuePause() {
  transferQueue.paused = !transferQueue.paused;
  showToast(transferQueue.paused ? "传输队列已暂停（进行中的任务会跑完）" : "传输队列已恢复", "info");
}

async function waitForTransferQueue() {
  while (transferQueue.paused) {
    await new Promise((resolve) => setTimeout(resolve, 240));
  }
}

function isBatchSelected(name: string) {
  return batchSelectedNames.value.includes(name);
}

function toggleBatchMode() {
  batchMode.value = !batchMode.value;
  batchSelectedNames.value = [];
}

function clearBatchSelection() {
  batchSelectedNames.value = [];
}

function toggleBatchSelection(item: FileItem) {
  const name = item.name;
  if (!name) return;
  if (isBatchSelected(name)) {
    batchSelectedNames.value = batchSelectedNames.value.filter((v) => v !== name);
    return;
  }
  batchSelectedNames.value = [...batchSelectedNames.value, name];
}

function normalizeRelativePath(rawPath: string) {
  return String(rawPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
}

function getSyncConfigStorageKey() {
  const userId = String(user.value?.id || "guest");
  return `wanwan_desktop_sync_config_v2_${userId}`;
}

function getSyncSnapshotStorageKey(localDir: string) {
  const userId = String(user.value?.id || "guest");
  return `wanwan_desktop_sync_snapshot_v2_${userId}_${encodeURIComponent(localDir || "")}`;
}

function safeParseObject(raw: string | null) {
  if (!raw) return {} as Record<string, string>;
  try {
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === "object" ? parsed as Record<string, string> : {};
  } catch {
    return {};
  }
}

function loadSyncConfig() {
  const key = getSyncConfigStorageKey();
  const raw = localStorage.getItem(key);
  if (!raw) return;
  try {
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== "object") return;
    syncState.localDir = typeof parsed.localDir === "string" ? parsed.localDir : "";
    syncState.remoteBasePath = normalizePath(typeof parsed.remoteBasePath === "string" ? parsed.remoteBasePath : "/");
    syncState.autoEnabled = Boolean(parsed.autoEnabled);
    const interval = Number(parsed.intervalMinutes || 15);
    syncState.intervalMinutes = Number.isFinite(interval) && interval >= 5 ? Math.floor(interval) : 15;
  } catch {
    // ignore invalid cache
  }
}

function saveSyncConfig() {
  const key = getSyncConfigStorageKey();
  const payload = {
    localDir: syncState.localDir,
    remoteBasePath: normalizePath(syncState.remoteBasePath || "/"),
    autoEnabled: syncState.autoEnabled,
    intervalMinutes: syncState.intervalMinutes,
  };
  localStorage.setItem(key, JSON.stringify(payload));
}

function loadSyncSnapshot(localDir: string) {
  if (!localDir) return {} as Record<string, string>;
  const key = getSyncSnapshotStorageKey(localDir);
  return safeParseObject(localStorage.getItem(key));
}

function saveSyncSnapshot(localDir: string, snapshot: Record<string, string>) {
  if (!localDir) return;
  const key = getSyncSnapshotStorageKey(localDir);
  localStorage.setItem(key, JSON.stringify(snapshot));
}

function clearSyncScheduler() {
  if (syncTimer) {
    clearInterval(syncTimer);
    syncTimer = null;
  }
  syncState.nextRunAt = "";
}

function syncFingerprint(item: LocalSyncFileItem) {
  return `${Number(item.size || 0)}:${Number(item.modifiedMs || 0)}`;
}

function getRelativeParentPath(relativePath: string) {
  const normalized = normalizeRelativePath(relativePath);
  if (!normalized) return "";
  const segments = normalized.split("/").filter(Boolean);
  if (segments.length <= 1) return "";
  return segments.slice(0, -1).join("/");
}

function toggleFileSortOrder() {
  fileViewState.sortOrder = fileViewState.sortOrder === "asc" ? "desc" : "asc";
}

function isAuthBridgeCommand(command: string) {
  return command === "api_login" || command === "api_refresh_token";
}

async function refreshAccessToken() {
  if (authRefreshPromise) {
    return authRefreshPromise;
  }
  authRefreshPromise = (async () => {
    try {
      const response = await invoke<BridgeResponse>("api_refresh_token", {
        baseUrl: appConfig.baseUrl,
      });
      return Boolean(response.ok && response.data?.success);
    } catch {
      return false;
    } finally {
      authRefreshPromise = null;
    }
  })();
  return authRefreshPromise;
}

async function invokeBridge(
  command: string,
  payload: Record<string, any>,
  allowAuthRetry = true,
) {
  let response: BridgeResponse;
  try {
    response = await invoke<BridgeResponse>(command, payload);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    response = {
      ok: false,
      status: 0,
      data: {
        success: false,
        message,
      },
    } satisfies BridgeResponse;
  }

  if (
    response.status === 401
    && allowAuthRetry
    && !isAuthBridgeCommand(command)
    && authenticated.value
  ) {
    const refreshed = await refreshAccessToken();
    if (refreshed) {
      return invokeBridge(command, payload, false);
    }
  }

  return response;
}

async function initClientVersion() {
  try {
    const current = await getVersion();
    if (current) {
      updateState.currentVersion = current;
    }
  } catch {
    // keep fallback version
  }
}

function normalizeReleaseNotesText(raw: string | undefined) {
  return String(raw || "")
    .replace(/\\r\\n/g, "\n")
    .replace(/\\n/g, "\n")
    .replace(/\\r/g, "\n")
    .trim();
}

async function checkClientUpdate(showResultToast = true): Promise<boolean> {
  if (updateState.checking) {
    return false;
  }

  updateState.checking = true;
  updateState.message = "";
  const response = await invokeBridge("api_check_client_update", {
    baseUrl: appConfig.baseUrl,
    currentVersion: updateState.currentVersion,
    platform: "windows-x64",
    channel: "stable",
  });
  updateState.checking = false;
  updateState.lastCheckedAt = new Date().toISOString();

  if (response.ok && response.data?.success) {
    updateState.latestVersion = String(response.data.latestVersion || updateState.currentVersion);
    updateState.available = Boolean(response.data.updateAvailable);
    updateState.downloadUrl = String(response.data.downloadUrl || "");
    updateState.releaseNotes = normalizeReleaseNotesText(String(response.data.releaseNotes || ""));
    updateState.mandatory = Boolean(response.data.mandatory);
    updateState.message = String(response.data.message || "");
    if (showResultToast) {
      if (updateState.available) {
        showToast(`发现新版本 ${updateState.latestVersion}`, "success");
      } else {
        showToast("当前已是最新版本", "info");
      }
    }
    return true;
  }

  updateState.available = false;
  updateState.downloadUrl = "";
  updateState.message = String(response.data?.message || "检查更新失败");
  if (showResultToast) {
    showToast(updateState.message, "error");
  }
  return false;
}

function getUpdateSkipStorageKey() {
  return `wanwan_desktop_skip_update_${String(user.value?.id || "guest")}`;
}

function shouldSkipCurrentUpdatePrompt() {
  const latest = String(updateState.latestVersion || "").trim();
  if (!latest) return false;
  return localStorage.getItem(getUpdateSkipStorageKey()) === latest;
}

function skipCurrentUpdatePrompt() {
  const latest = String(updateState.latestVersion || "").trim();
  if (!latest) return;
  localStorage.setItem(getUpdateSkipStorageKey(), latest);
}

async function checkUpdateAfterLogin() {
  if (!authenticated.value || hasCheckedUpdateAfterAuth) return;
  hasCheckedUpdateAfterAuth = true;
  const checked = await checkClientUpdate(false);
  if (!checked || !updateState.available || !updateState.downloadUrl) return;
  if (shouldSkipCurrentUpdatePrompt()) return;
  updatePrompt.visible = true;
}

function dismissUpdatePrompt(ignoreThisVersion = false) {
  if (ignoreThisVersion) {
    skipCurrentUpdatePrompt();
  }
  updatePrompt.visible = false;
}

async function confirmUpdateFromPrompt() {
  if (updatePrompt.loading) return;
  updatePrompt.loading = true;
  updatePrompt.visible = false;
  try {
    nav.value = "settings";
    await installLatestUpdate();
  } finally {
    updatePrompt.loading = false;
  }
}

async function installLatestUpdate(): Promise<boolean> {
  if (updateRuntime.downloading || updateRuntime.installing) {
    showToast("更新包正在下载，请稍候", "info");
    return false;
  }

  if (!updateState.downloadUrl) {
    showToast("当前没有可用的更新下载地址", "info");
    return false;
  }

  resetUpdateRuntime();
  updateRuntime.downloading = true;
  const taskId = `UPD-${Date.now()}`;
  const installerName = `玩玩云_v${updateState.latestVersion || updateState.currentVersion}.exe`;
  updateRuntime.taskId = taskId;
  updateRuntime.progress = 1;
  updateRuntime.speed = "准备下载";

  const response = await invokeBridge("api_native_download", {
    url: updateState.downloadUrl,
    fileName: installerName,
    taskId,
  });

  try {
    if (response.ok && response.data?.success) {
      updateRuntime.downloading = false;
      updateRuntime.installing = true;
      updateRuntime.progress = 100;
      updateRuntime.speed = "-";
      const savePath = String(response.data?.savePath || "").trim();
      updateRuntime.installerPath = savePath;
      if (savePath) {
        const launchResponse = await invokeBridge("api_silent_install_and_restart", {
          installerPath: savePath,
        });
        if (launchResponse.ok && launchResponse.data?.success) {
          showToast("静默安装已启动，完成后会自动重启客户端", "success");
          setTimeout(() => {
            const win = getCurrentWindow();
            void (async () => {
              try {
                await win.close();
              } catch {
                try {
                  await win.destroy();
                } catch {
                  // ignore
                }
              }
            })();
          }, 400);
          return true;
        }

        try {
          await openPath(savePath);
        } catch (error) {
          console.error("open installer fallback failed", error);
        }
      }
      showToast("更新包已下载，请手动运行安装程序", "info");
      setTimeout(() => {
        resetUpdateRuntime();
      }, 1200);
      return true;
    }

    const message = String(response.data?.message || "下载更新包失败");
    resetUpdateRuntime();
    showToast(message, "error");
    return false;
  } finally {
    if (!updateRuntime.installing) {
      updateRuntime.downloading = false;
    }
  }
}

function formatOnlineDeviceType(value: string | undefined) {
  const kind = String(value || "").trim().toLowerCase();
  if (kind === "desktop") return "桌面端";
  if (kind === "mobile") return "移动端";
  if (kind === "api") return "API";
  return "网页端";
}

function openOperationConfirmDialog(options: {
  mode: "" | "kick-device" | "batch-delete";
  title: string;
  message: string;
  confirmText?: string;
  sessionId?: string;
  batchItems?: FileItem[];
}) {
  if (operationConfirmDialog.loading) return;
  operationConfirmDialog.mode = options.mode;
  operationConfirmDialog.title = options.title;
  operationConfirmDialog.message = options.message;
  operationConfirmDialog.confirmText = String(options.confirmText || "确定");
  operationConfirmDialog.sessionId = String(options.sessionId || "").trim();
  operationConfirmDialog.batchItems = Array.isArray(options.batchItems) ? [...options.batchItems] : [];
  operationConfirmDialog.visible = true;
}

function closeOperationConfirmDialog(force = false) {
  if (operationConfirmDialog.loading && !force) return;
  operationConfirmDialog.visible = false;
  operationConfirmDialog.loading = false;
  operationConfirmDialog.mode = "";
  operationConfirmDialog.title = "";
  operationConfirmDialog.message = "";
  operationConfirmDialog.confirmText = "确定";
  operationConfirmDialog.sessionId = "";
  operationConfirmDialog.batchItems = [];
}

async function loadOnlineDevices(silent = false) {
  if (!silent) {
    onlineDevices.loading = true;
  }
  onlineDevices.message = "";
  const response = await invokeBridge("api_list_online_devices", {
    baseUrl: appConfig.baseUrl,
  });
  if (response.ok && response.data?.success) {
    onlineDevices.items = Array.isArray(response.data?.devices) ? response.data.devices : [];
    onlineDevices.lastLoadedAt = new Date().toISOString();
  } else {
    onlineDevices.message = String(response.data?.message || "加载在线设备失败");
    if (!silent) {
      showToast(onlineDevices.message, "error");
    }
  }
  onlineDevices.loading = false;
}

function requestKickOnlineDevice(item: OnlineDeviceItem) {
  const sessionId = String(item?.session_id || "").trim();
  if (!sessionId || onlineDevices.kickingSessionId) return;
  const isCurrent = Boolean(item?.is_current || item?.is_local);
  openOperationConfirmDialog({
    mode: "kick-device",
    title: isCurrent ? "确认下线本机" : "确认踢下线设备",
    message: isCurrent
      ? "确定要下线当前设备吗？下线后需要重新登录。"
      : "确定要强制该设备下线吗？",
    confirmText: isCurrent ? "确认下线" : "确认踢下线",
    sessionId,
  });
}

async function kickOnlineDeviceBySessionId(sessionId: string) {
  onlineDevices.kickingSessionId = sessionId;
  const response = await invokeBridge("api_kick_online_device", {
    baseUrl: appConfig.baseUrl,
    sessionId,
  });
  onlineDevices.kickingSessionId = "";

  if (response.ok && response.data?.success) {
    showToast(String(response.data?.message || "设备已下线"), "success");
    if (response.data?.kicked_current) {
      await handleLogout();
      return;
    }
    await loadOnlineDevices(true);
    return;
  }

  showToast(String(response.data?.message || "踢下线失败"), "error");
}

async function chooseSyncDirectory() {
  try {
    const result = await openDialog({
      directory: true,
      multiple: false,
      title: "选择本地同步文件夹",
    });
    if (typeof result === "string" && result.trim()) {
      syncState.localDir = result.trim();
      showToast("同步目录已更新", "success");
    }
  } catch {
    showToast("选择目录失败", "error");
  }
}

function isRetryableUploadResponse(response: BridgeResponse) {
  const status = Number(response.status || 0);
  if ([0, 408, 425, 429, 500, 502, 503, 504].includes(status)) {
    return true;
  }
  const message = String(response.data?.message || "").toLowerCase();
  return (
    message.includes("timeout")
    || message.includes("timed out")
    || message.includes("network")
    || message.includes("connection")
    || message.includes("超时")
    || message.includes("稍后重试")
  );
}

async function waitMs(ms: number) {
  await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
}

async function uploadFileWithResume(filePath: string, targetPath: string, taskId?: string) {
  const maxAttempts = 3;
  let lastResponse: BridgeResponse | null = null;

  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const resumableResponse = await invokeBridge("api_upload_file_resumable", {
      baseUrl: appConfig.baseUrl,
      filePath,
      targetPath,
      chunkSize: 4 * 1024 * 1024,
      taskId: taskId || null,
    });

    if (resumableResponse.ok && resumableResponse.data?.success) {
      return resumableResponse;
    }

    const message = String(resumableResponse.data?.message || "");
    if (
      message.includes("当前存储模式不支持分片上传")
      || message.includes("分片上传会话")
      || message.includes("上传会话")
    ) {
      const fallbackResponse = await invokeBridge("api_upload_file", {
        baseUrl: appConfig.baseUrl,
        filePath,
        targetPath,
        taskId: taskId || null,
      });
      if (fallbackResponse.ok && fallbackResponse.data?.success) {
        return fallbackResponse;
      }
      lastResponse = fallbackResponse;
    } else {
      lastResponse = resumableResponse;
    }

    if (attempt < maxAttempts - 1 && lastResponse && isRetryableUploadResponse(lastResponse)) {
      await waitMs(600 * (attempt + 1));
      continue;
    }
    break;
  }

  return lastResponse || {
    ok: false,
    status: 0,
    data: {
      success: false,
      message: "上传失败",
    },
  };
}

function rebuildSyncScheduler() {
  clearSyncScheduler();
  if (!authenticated.value || !syncState.autoEnabled || !syncState.localDir.trim()) {
    return;
  }
  const intervalMinutes = Math.max(5, Math.floor(Number(syncState.intervalMinutes) || 15));
  syncState.intervalMinutes = intervalMinutes;
  syncState.nextRunAt = new Date(Date.now() + intervalMinutes * 60 * 1000).toISOString();
  syncTimer = setInterval(() => {
    void runSyncOnce("auto");
  }, intervalMinutes * 60 * 1000);
}

async function clearSyncSnapshot() {
  if (!syncState.localDir.trim()) {
    showToast("请先配置本地同步目录", "info");
    return;
  }
  localStorage.removeItem(getSyncSnapshotStorageKey(syncState.localDir.trim()));
  syncState.lastSummary = "本地同步索引已重置";
  showToast("同步索引已清理，下次会全量扫描", "success");
}

async function runSyncOnce(trigger: "manual" | "auto" = "manual") {
  if (!authenticated.value) return;
  const localDir = syncState.localDir.trim();
  if (!localDir) {
    if (trigger === "manual") {
      showToast("请先配置本地同步目录", "info");
    }
    return;
  }
  if (syncState.syncing || syncState.scanning) {
    if (trigger === "manual") {
      showToast("同步任务正在执行，请稍后", "info");
    }
    return;
  }

  syncState.scanning = true;
  const listResp = await invokeBridge("api_list_local_files", {
    dirPath: localDir,
  });
  syncState.scanning = false;
  if (!(listResp.ok && listResp.data?.success)) {
    const message = String(listResp.data?.message || "扫描本地目录失败");
    syncState.lastSummary = message;
    if (trigger === "manual") showToast(message, "error");
    return;
  }

  const items = Array.isArray(listResp.data?.items) ? listResp.data.items as LocalSyncFileItem[] : [];
  const previousSnapshot = loadSyncSnapshot(localDir);
  const changedItems = items.filter((item) => {
    const rel = normalizeRelativePath(item.relativePath);
    return rel && previousSnapshot[rel] !== syncFingerprint(item);
  });

  syncState.pendingCount = changedItems.length;
  syncState.uploadedCount = 0;
  syncState.failedCount = 0;
  syncState.syncing = true;

  if (changedItems.length === 0) {
    syncState.syncing = false;
    syncState.lastRunAt = new Date().toISOString();
    syncState.lastSummary = "没有检测到变更文件";
    if (trigger === "manual") {
      showToast("没有检测到需要上传的变更", "info");
    }
    if (syncState.autoEnabled) {
      syncState.nextRunAt = new Date(Date.now() + syncState.intervalMinutes * 60 * 1000).toISOString();
    }
    return;
  }

  const successPaths = new Set<string>();
  for (let index = 0; index < changedItems.length; index += 1) {
    await waitForTransferQueue();
    const item = changedItems[index];
    const relPath = normalizeRelativePath(item.relativePath);
    const fileName = extractFileNameFromPath(relPath) || `同步文件${index + 1}`;
    const parent = getRelativeParentPath(relPath);
    const remoteBase = normalizePath(syncState.remoteBasePath || "/");
    const targetPath = parent ? normalizePath(`${remoteBase}/${parent}`) : remoteBase;
    const taskId = `S-${Date.now()}-${index}`;

    prependTransferTask({
      id: taskId,
      kind: "upload",
      name: fileName,
      speed: trigger === "auto" ? "自动同步" : "手动同步",
      progress: 2,
      status: "queued",
      note: targetPath,
      filePath: item.path,
      targetPath,
      fileName,
    });
    updateTransferTask(taskId, { status: "uploading", speed: "上传中", progress: 10 });

    const resp = await uploadFileWithResume(item.path, targetPath, taskId);

    if (resp.ok && resp.data?.success) {
      syncState.uploadedCount += 1;
      successPaths.add(relPath);
      updateTransferTask(taskId, { speed: "-", progress: 100, status: "done", note: "同步完成" });
    } else {
      syncState.failedCount += 1;
      updateTransferTask(taskId, {
        speed: "-",
        progress: 0,
        status: "failed",
        note: String(resp.data?.message || "同步失败"),
      });
    }
  }

  const nextSnapshot: Record<string, string> = {};
  for (const item of items) {
    const rel = normalizeRelativePath(item.relativePath);
    if (!rel) continue;
    const fingerprint = syncFingerprint(item);
    if (previousSnapshot[rel] === fingerprint || successPaths.has(rel)) {
      nextSnapshot[rel] = fingerprint;
    }
  }
  saveSyncSnapshot(localDir, nextSnapshot);

  syncState.syncing = false;
  syncState.lastRunAt = new Date().toISOString();
  syncState.lastSummary = `变更 ${changedItems.length} 个，成功 ${syncState.uploadedCount} 个，失败 ${syncState.failedCount} 个`;
  if (syncState.autoEnabled) {
    syncState.nextRunAt = new Date(Date.now() + syncState.intervalMinutes * 60 * 1000).toISOString();
  }
  if (trigger === "manual") {
    const toastType = syncState.failedCount > 0 ? "info" : "success";
    showToast(syncState.lastSummary, toastType);
  }
  if (syncState.uploadedCount > 0 && nav.value === "files") {
    await loadFiles(pathState.currentPath);
  }
}

async function loadProfile() {
  const response = await invokeBridge("api_get_profile", { baseUrl: appConfig.baseUrl });
  if (response.ok && response.data?.success && response.data?.user) {
    user.value = response.data.user;
    return true;
  }
  return false;
}

async function loadFiles(targetPath = pathState.currentPath) {
  cancelInlineRename(true);
  pathState.loading = true;
  pathState.error = "";
  const normalizedPath = normalizePath(targetPath);
  const response = await invokeBridge("api_list_files", {
    baseUrl: appConfig.baseUrl,
    path: normalizedPath,
  });
  if (response.ok && response.data?.success) {
    files.value = Array.isArray(response.data.items) ? response.data.items.map((item: Record<string, any>) => mapApiItem(item)) : [];
    pathState.currentPath = normalizePath(response.data.path || normalizedPath);
    pathState.mode = "directory";
    selectedFileName.value = files.value[0]?.name || "";
    batchSelectedNames.value = [];
  } else {
    pathState.error = response.data?.message || "读取文件列表失败";
    showToast(pathState.error, "error");
  }
  pathState.loading = false;
}

async function loadShares(silent = false) {
  if (!silent) sharesLoading.value = true;
  const response = await invokeBridge("api_get_my_shares", {
    baseUrl: appConfig.baseUrl,
  });
  if (response.ok && response.data?.success) {
    shares.value = Array.isArray(response.data.shares) ? response.data.shares : [];
  } else if (!silent) {
    showToast(response.data?.message || "获取分享列表失败", "error");
  }
  if (!silent) sharesLoading.value = false;
}

async function getSignedUrlForItem(item: FileItem, mode: "download" | "preview") {
  const targetPath = buildItemPath(item);
  const response = await invokeBridge("api_get_download_url", {
    baseUrl: appConfig.baseUrl,
    path: targetPath,
    mode,
  });
  if (response.ok && response.data?.success && response.data?.downloadUrl) {
    return String(response.data.downloadUrl);
  }
  showToast(response.data?.message || "获取链接失败", "error");
  return "";
}

async function createShareForItem(current: FileItem, silent = false) {
  const response = await invokeBridge("api_create_share", {
    baseUrl: appConfig.baseUrl,
    shareType: current.isDirectory || current.type === "directory" ? "directory" : "file",
    filePath: buildItemPath(current),
    fileName: current.displayName || current.name,
    password: null,
    expiryDays: null,
  });

  if (response.ok && response.data?.success) {
    if (!silent) {
      showToast("分享创建成功", "success");
    }
    await loadShares(true);
    const shareUrl = String(response.data.share_url || "");
    if (!silent && shareUrl) {
      await copyText(shareUrl, "分享链接已复制");
    }
    return;
  }

  if (!silent) {
    showToast(response.data?.message || "创建分享失败", "error");
  }
  throw new Error(String(response.data?.message || "创建分享失败"));
}

async function createDirectLinkForItem(current: FileItem, silent = false) {
  if (current.isDirectory || current.type === "directory") {
    if (!silent) {
      showToast("文件夹不支持生成直链", "info");
    }
    throw new Error("文件夹不支持生成直链");
  }

  const response = await invokeBridge("api_create_direct_link", {
    baseUrl: appConfig.baseUrl,
    filePath: buildItemPath(current),
    fileName: current.displayName || current.name,
    expiryDays: null,
  });

  if (response.ok && response.data?.success) {
    if (!silent) {
      showToast("直链创建成功", "success");
    }
    const directUrl = String(response.data.direct_url || "");
    if (!silent && directUrl) {
      await copyText(directUrl, "直链已复制");
    }
    return;
  }

  if (!silent) {
    showToast(response.data?.message || "创建直链失败", "error");
  }
  throw new Error(String(response.data?.message || "创建直链失败"));
}

function requestDeleteShare(share: ShareItem) {
  if (shareDeleteDialog.loading) return;
  shareDeleteDialog.share = share;
  shareDeleteDialog.visible = true;
}

function closeDeleteShareDialog(force = false) {
  if (shareDeleteDialog.loading && !force) return;
  shareDeleteDialog.visible = false;
  shareDeleteDialog.loading = false;
  shareDeleteDialog.share = null;
}

function requestDeleteFile(target?: FileItem | null) {
  if (fileDeleteDialog.loading) return;
  const current = target || selectedFile.value;
  if (!current) {
    showToast("请先选中文件或文件夹", "info");
    return;
  }
  fileDeleteDialog.file = current;
  fileDeleteDialog.visible = true;
}

function closeDeleteFileDialog(force = false) {
  if (fileDeleteDialog.loading && !force) return;
  fileDeleteDialog.visible = false;
  fileDeleteDialog.loading = false;
  fileDeleteDialog.file = null;
}

async function deleteShare(share: ShareItem) {
  const response = await invokeBridge("api_delete_share", {
    baseUrl: appConfig.baseUrl,
    shareId: share.id,
  });
  if (response.ok && response.data?.success) {
    showToast("分享已删除", "success");
    await loadShares(true);
    return true;
  }
  showToast(response.data?.message || "删除分享失败", "error");
  return false;
}

async function confirmDeleteShare() {
  const share = shareDeleteDialog.share;
  if (!share || shareDeleteDialog.loading) return;

  shareDeleteDialog.loading = true;
  try {
    const ok = await deleteShare(share);
    if (ok) {
      closeDeleteShareDialog(true);
      return;
    }
    shareDeleteDialog.loading = false;
  } catch {
    shareDeleteDialog.loading = false;
  }
}

async function confirmDeleteFile() {
  const file = fileDeleteDialog.file;
  if (!file || fileDeleteDialog.loading) return;

  fileDeleteDialog.loading = true;
  try {
    const ok = await deleteSelected(file, true);
    if (ok) {
      closeDeleteFileDialog(true);
      showToast("删除成功", "success");
      await loadFiles(pathState.currentPath);
      return;
    }
    fileDeleteDialog.loading = false;
  } catch {
    fileDeleteDialog.loading = false;
  }
}

async function copyShareLink(share: ShareItem) {
  const url = String(share.share_url || "").trim();
  if (!url) {
    showToast("分享链接为空", "error");
    return;
  }
  await copyText(url, "链接已复制");
}

async function openShareLink(share: ShareItem) {
  const url = String(share.share_url || "").trim();
  if (!url) {
    showToast("分享链接为空", "error");
    return;
  }
  await openUrl(url);
}

async function restoreSession() {
  const ok = await loadProfile();
  if (!ok) return false;
  authenticated.value = true;
  loadSyncConfig();
  rebuildSyncScheduler();
  await loadFiles("/");
  await loadShares(true);
  void loadOnlineDevices(true);
  await checkUpdateAfterLogin();
  return true;
}

async function tryAutoLoginFromSavedState() {
  if (authenticated.value) return true;
  const stateResponse = await invokeBridge("api_load_login_state", {});
  if (!(stateResponse.ok && stateResponse.data?.success && stateResponse.data?.hasState)) {
    return false;
  }

  const savedBase = String(stateResponse.data?.baseUrl || "").trim();
  const savedUsername = String(stateResponse.data?.username || "").trim();
  const savedPassword = String(stateResponse.data?.password || "");
  if (!savedBase || !savedUsername || !savedPassword) {
    return false;
  }

  appConfig.baseUrl = savedBase.replace(/\/+$/, "");
  loginForm.username = savedUsername;
  loginState.loading = true;
  const loginResponse = await invokeBridge("api_login", {
    baseUrl: appConfig.baseUrl,
    username: savedUsername,
    password: savedPassword,
    captcha: null,
  });
  loginState.loading = false;
  if (!(loginResponse.ok && loginResponse.data?.success)) {
    await invokeBridge("api_clear_login_state", {});
    return false;
  }

  authenticated.value = true;
  user.value = loginResponse.data.user || null;
  nav.value = "files";
  loginForm.password = savedPassword;
  loadSyncConfig();
  rebuildSyncScheduler();
  await loadFiles("/");
  if (!user.value) {
    await loadProfile();
  }
  void loadOnlineDevices(true);
  await checkUpdateAfterLogin();
  showToast("已恢复登录状态", "success");
  return true;
}

function buildItemPath(item: FileItem) {
  if (item.path && item.path.trim()) {
    return normalizePath(item.path);
  }
  return normalizePath(`${pathState.currentPath}/${item.name}`);
}

function getItemParentPath(item: FileItem) {
  const fullPath = buildItemPath(item);
  const segments = fullPath.split("/").filter(Boolean);
  if (segments.length <= 1) return "/";
  return `/${segments.slice(0, -1).join("/")}`;
}

async function runGlobalSearch() {
  const keyword = searchKeyword.value.trim();
  if (!keyword) {
    await loadFiles(pathState.currentPath);
    return;
  }
  pathState.loading = true;
  pathState.error = "";
  const response = await invokeBridge("api_search_files", {
    baseUrl: appConfig.baseUrl,
    path: pathState.currentPath,
    keyword,
    searchType: "all",
    limit: 200,
  });

  if (response.ok && response.data?.success) {
    files.value = Array.isArray(response.data.items) ? response.data.items.map((item: Record<string, any>) => mapApiItem(item)) : [];
    pathState.mode = "search";
    selectedFileName.value = files.value[0]?.name || "";
    showToast(`搜索完成，共 ${files.value.length} 条结果`, "success");
  } else {
    pathState.error = response.data?.message || "搜索失败";
    showToast(pathState.error, "error");
  }
  pathState.loading = false;
}

async function handleLogin() {
  if (loginState.loading) return;
  loginState.loading = true;
  loginState.error = "";

  const response = await invokeBridge("api_login", {
    baseUrl: appConfig.baseUrl,
    username: loginForm.username.trim(),
    password: loginForm.password,
    captcha: loginForm.captcha.trim() || null,
  });

  loginState.loading = false;
  if (response.ok && response.data?.success) {
    authenticated.value = true;
    user.value = response.data.user || null;
    nav.value = "files";
    showToast("登录成功，正在同步文件目录", "success");
    hasCheckedUpdateAfterAuth = false;
    loadSyncConfig();
    rebuildSyncScheduler();
    await loadFiles("/");
    if (!user.value) {
      await loadProfile();
    }
    await invokeBridge("api_save_login_state", {
      baseUrl: appConfig.baseUrl,
      username: loginForm.username.trim(),
      password: loginForm.password,
    });
    void loadOnlineDevices(true);
    await checkUpdateAfterLogin();
    return;
  }

  loginState.needCaptcha = !!response.data?.needCaptcha;
  loginState.error = response.data?.message || "登录失败";
  showToast(loginState.error, "error");
}

async function handleLogout() {
  await invokeBridge("api_logout", { baseUrl: appConfig.baseUrl });
  await invokeBridge("api_clear_login_state", {});
  clearSyncScheduler();
  authenticated.value = false;
  user.value = null;
  files.value = [];
  selectedFileName.value = "";
  batchMode.value = false;
  batchSelectedNames.value = [];
  loginForm.password = "";
  nav.value = "files";
  syncState.localDir = "";
  syncState.remoteBasePath = "/";
  syncState.autoEnabled = false;
  syncState.intervalMinutes = 15;
  syncState.syncing = false;
  syncState.scanning = false;
  syncState.pendingCount = 0;
  syncState.uploadedCount = 0;
  syncState.failedCount = 0;
  syncState.lastRunAt = "";
  syncState.lastSummary = "";
  syncState.nextRunAt = "";
  updateRuntime.downloading = false;
  updateRuntime.installing = false;
  updatePrompt.visible = false;
  fileDeleteDialog.visible = false;
  fileDeleteDialog.loading = false;
  fileDeleteDialog.file = null;
  closeOperationConfirmDialog(true);
  cancelInlineRename(true);
  onlineDevices.loading = false;
  onlineDevices.kickingSessionId = "";
  onlineDevices.items = [];
  onlineDevices.message = "";
  onlineDevices.lastLoadedAt = "";
  hasCheckedUpdateAfterAuth = false;
  showToast("已退出客户端", "info");
}

async function createFolder() {
  if (pathState.loading) return;
  const folderName = window.prompt("请输入新文件夹名称");
  if (!folderName || !folderName.trim()) return;
  const response = await invokeBridge("api_mkdir", {
    baseUrl: appConfig.baseUrl,
    path: pathState.currentPath,
    folderName: folderName.trim(),
  });
  if (response.ok && response.data?.success) {
    showToast("文件夹创建成功", "success");
    await loadFiles(pathState.currentPath);
    return;
  }
  showToast(response.data?.message || "创建文件夹失败", "error");
}

function isInlineRenaming(item: FileItem | null | undefined) {
  if (!item) return false;
  return inlineRename.active && inlineRename.originalName === item.name;
}

function focusInlineRenameInput() {
  nextTick(() => {
    const input = document.querySelector<HTMLInputElement>(".inline-rename-input[data-renaming='1']");
    if (!input) return;
    input.focus();
    input.select();
  });
}

function cancelInlineRename(force = false) {
  if (inlineRename.saving && !force) return;
  inlineRename.active = false;
  inlineRename.originalName = "";
  inlineRename.value = "";
  inlineRename.saving = false;
}

function startInlineRename(target?: FileItem | null) {
  const current = target || selectedFile.value;
  if (!current) {
    showToast("请先选中文件或文件夹", "info");
    return;
  }
  selectedFileName.value = current.name;
  inlineRename.active = true;
  inlineRename.originalName = current.name;
  inlineRename.value = current.displayName || current.name;
  inlineRename.saving = false;
  focusInlineRenameInput();
}

async function submitInlineRename(target?: FileItem | null) {
  const current = target || files.value.find((item) => item.name === inlineRename.originalName) || null;
  if (!current || !inlineRename.active || inlineRename.saving) return;

  const nextName = String(inlineRename.value || "").trim();
  if (!nextName) {
    cancelInlineRename(true);
    return;
  }
  if (nextName === current.name) {
    cancelInlineRename(true);
    return;
  }

  inlineRename.saving = true;
  const response = await invokeBridge("api_rename_file", {
    baseUrl: appConfig.baseUrl,
    path: getItemParentPath(current),
    oldName: current.name,
    newName: nextName,
  });
  inlineRename.saving = false;

  if (response.ok && response.data?.success) {
    showToast("重命名成功", "success");
    cancelInlineRename(true);
    await loadFiles(pathState.currentPath);
    selectedFileName.value = nextName;
    return;
  }

  showToast(response.data?.message || "重命名失败", "error");
}

async function renameSelected(target?: FileItem | null) {
  startInlineRename(target);
}

async function deleteSelected(target?: FileItem | null, silent = false) {
  const current = target || selectedFile.value;
  if (!current) {
    if (!silent) showToast("请先选中文件或文件夹", "info");
    return false;
  }
  if (!silent) {
    requestDeleteFile(current);
    return false;
  }

  const response = await invokeBridge("api_delete_file", {
    baseUrl: appConfig.baseUrl,
    path: getItemParentPath(current),
    fileName: current.name,
  });
  if (response.ok && response.data?.success) {
    return true;
  }
  if (!silent) {
    showToast(response.data?.message || "删除失败", "error");
  }
  throw new Error(String(response.data?.message || "删除失败"));
}

async function downloadSelected(target?: FileItem | null) {
  const current = target || selectedFile.value;
  if (!current) {
    showToast("请先选中文件", "info");
    return;
  }
  if (current.isDirectory || current.type === "directory") {
    showToast("当前仅支持下载文件", "info");
    return;
  }
  const signedUrl = await getSignedUrlForItem(current, "download");
  if (!signedUrl) return;

  const taskId = `D-${Date.now()}`;
  prependTransferTask({
    id: taskId,
    kind: "download",
    name: current.displayName || current.name,
    speed: "等待下载",
    progress: 1,
    status: "queued",
    downloadUrl: signedUrl,
    fileName: current.displayName || current.name,
    note: "支持断点续传",
  });
  await waitForTransferQueue();
  updateTransferTask(taskId, { speed: "原生下载", status: "downloading", progress: 10, note: "下载中" });

  const nativeResponse = await invokeBridge("api_native_download", {
    url: signedUrl,
    fileName: current.displayName || current.name,
    taskId,
  });

  if (nativeResponse.ok && nativeResponse.data?.success) {
    const resumedBytes = Number(nativeResponse.data?.resumedBytes || 0);
    const resumeText = resumedBytes > 0 ? `，已续传 ${formatBytes(resumedBytes)}` : "";
    updateTransferTask(taskId, { speed: "-", progress: 100, status: "done", note: `下载成功${resumeText}` });
    const savedPath = nativeResponse.data?.savePath ? `\n${nativeResponse.data.savePath}` : "";
    showToast(`下载完成${savedPath}`, "success");
    return;
  }

  const message = String(nativeResponse.data?.message || "原生下载失败");
  updateTransferTask(taskId, { speed: "-", progress: 0, status: "failed", note: message });
  showToast(message, "error");
}

function selectFile(item: FileItem) {
  if (batchMode.value) {
    toggleBatchSelection(item);
    return;
  }
  selectedFileName.value = item.name;
}

function handleFileCardClick(item: FileItem) {
  if (isInlineRenaming(item)) return;
  selectFile(item);
}

async function handleFileCardDoubleClick(item: FileItem) {
  if (isInlineRenaming(item)) return;
  await openItem(item);
}

async function openItem(item: FileItem) {
  if (batchMode.value) return;
  if (isInlineRenaming(item)) return;
  selectFile(item);
  if (item.isDirectory || item.type === "directory") {
    const nextPath = buildItemPath(item);
    await loadFiles(nextPath);
    return;
  }
  const previewUrl = await getSignedUrlForItem(item, "preview");
  if (previewUrl) {
    await openUrl(previewUrl);
  }
}

async function jumpToPath(nextPath: string) {
  await loadFiles(nextPath);
}

function closeContextMenu() {
  contextMenu.visible = false;
  contextMenu.item = null;
}

function openContextMenu(event: MouseEvent, item: FileItem) {
  if (batchMode.value) return;
  if (inlineRename.active && !isInlineRenaming(item)) {
    cancelInlineRename(true);
  }
  selectFile(item);
  contextMenu.item = item;
  const maxX = window.innerWidth - 220;
  const maxY = window.innerHeight - 260;
  contextMenu.x = Math.max(8, Math.min(event.clientX, maxX));
  contextMenu.y = Math.max(8, Math.min(event.clientY, maxY));
  contextMenu.visible = true;
}

async function executeContextAction(action: "open" | "download" | "rename" | "delete" | "share" | "direct") {
  const item = contextMenu.item;
  closeContextMenu();
  if (!item) return;

  try {
    if (action === "open") {
      await openItem(item);
      return;
    }
    if (action === "download") {
      await downloadSelected(item);
      return;
    }
    if (action === "rename") {
      await renameSelected(item);
      return;
    }
    if (action === "delete") {
      await deleteSelected(item);
      return;
    }
    if (action === "share") {
      await createShareForItem(item);
      return;
    }
    await createDirectLinkForItem(item);
  } catch {
    // errors already handled by action method
  }
}

async function retryTransferTask(taskId: string) {
  const task = transferTasks.value.find((item) => item.id === taskId);
  if (!task) return;
  if (isTaskRunning(task.status)) return;
  if (task.status === "done") return;

  if (task.kind === "upload") {
    if (!task.filePath || !task.targetPath) {
      updateTransferTask(taskId, { status: "failed", note: "缺少上传任务参数" });
      return;
    }
    await waitForTransferQueue();
    updateTransferTask(taskId, { status: "uploading", speed: "重试上传", progress: 10, note: "正在重试" });
    const response = await uploadFileWithResume(task.filePath, task.targetPath, taskId);
    if (response.ok && response.data?.success) {
      updateTransferTask(taskId, { status: "done", speed: "-", progress: 100, note: "重试成功" });
      if (nav.value === "files") {
        await loadFiles(pathState.currentPath);
      }
      return;
    }
    updateTransferTask(taskId, {
      status: "failed",
      speed: "-",
      progress: 0,
      note: String(response.data?.message || "重试上传失败"),
    });
    return;
  }

  if (!task.downloadUrl) {
    updateTransferTask(taskId, { status: "failed", note: "缺少下载地址" });
    return;
  }
  await waitForTransferQueue();
  updateTransferTask(taskId, { status: "downloading", speed: "重试下载", progress: 10, note: "正在重试" });
  const response = await invokeBridge("api_native_download", {
    url: task.downloadUrl,
    fileName: task.fileName || task.name,
    taskId,
  });
  if (response.ok && response.data?.success) {
    const resumedBytes = Number(response.data?.resumedBytes || 0);
    const resumeText = resumedBytes > 0 ? `，已续传 ${formatBytes(resumedBytes)}` : "";
    updateTransferTask(taskId, { status: "done", speed: "-", progress: 100, note: `下载成功${resumeText}` });
    return;
  }
  updateTransferTask(taskId, {
    status: "failed",
    speed: "-",
    progress: 0,
    note: String(response.data?.message || "重试下载失败"),
  });
}

async function batchDeleteSelected() {
  if (!batchMode.value || batchSelectedItems.value.length === 0) {
    showToast("请先勾选批量文件", "info");
    return;
  }
  openOperationConfirmDialog({
    mode: "batch-delete",
    title: "确认批量删除",
    message: `确认删除已勾选的 ${batchSelectedItems.value.length} 个项目吗？删除后将无法恢复。`,
    confirmText: "确定删除",
    batchItems: [...batchSelectedItems.value],
  });
}

async function runBatchDelete(items: FileItem[]) {
  let success = 0;
  let failed = 0;
  for (const item of items) {
    try {
      await deleteSelected(item, true);
      success += 1;
    } catch {
      failed += 1;
    }
  }

  await loadFiles(pathState.currentPath);
  clearBatchSelection();
  const failedText = failed > 0 ? `，失败 ${failed} 个` : "";
  showToast(`批量删除完成：成功 ${success} 个${failedText}`, failed > 0 ? "info" : "success");
}

async function confirmOperationDialog() {
  if (operationConfirmDialog.loading) return;
  operationConfirmDialog.loading = true;
  try {
    if (operationConfirmDialog.mode === "kick-device") {
      const sessionId = operationConfirmDialog.sessionId;
      if (sessionId) {
        await kickOnlineDeviceBySessionId(sessionId);
      }
      closeOperationConfirmDialog(true);
      return;
    }

    if (operationConfirmDialog.mode === "batch-delete") {
      await runBatchDelete([...operationConfirmDialog.batchItems]);
      closeOperationConfirmDialog(true);
      return;
    }

    closeOperationConfirmDialog(true);
  } catch {
    operationConfirmDialog.loading = false;
  }
}

async function batchShareSelected() {
  if (!batchMode.value || batchSelectedItems.value.length === 0) {
    showToast("请先勾选批量文件", "info");
    return;
  }

  let success = 0;
  let failed = 0;
  const items = [...batchSelectedItems.value];
  for (const item of items) {
    try {
      await createShareForItem(item, true);
      success += 1;
    } catch {
      failed += 1;
    }
  }
  await loadShares(true);
  const failedText = failed > 0 ? `，失败 ${failed} 个` : "";
  showToast(`批量分享完成：成功 ${success} 个${failedText}`, failed > 0 ? "info" : "success");
}

async function batchDirectLinkSelected() {
  if (!batchMode.value || batchSelectedItems.value.length === 0) {
    showToast("请先勾选批量文件", "info");
    return;
  }

  let success = 0;
  let failed = 0;
  const items = [...batchSelectedItems.value];
  for (const item of items) {
    try {
      await createDirectLinkForItem(item, true);
      success += 1;
    } catch {
      failed += 1;
    }
  }
  await loadShares(true);
  const failedText = failed > 0 ? `，失败 ${failed} 个` : "";
  showToast(`批量直链完成：成功 ${success} 个${failedText}`, failed > 0 ? "info" : "success");
}

function handleGlobalClick() {
  if (contextMenu.visible) closeContextMenu();
}

function handleGlobalKey(event: KeyboardEvent) {
  if (event.key === "Escape") {
    if (operationConfirmDialog.visible) {
      closeOperationConfirmDialog();
      return;
    }
    if (inlineRename.active) {
      cancelInlineRename();
      return;
    }
    if (contextMenu.visible) {
      closeContextMenu();
    }
    return;
  }

  if (
    event.key === "F2"
    && authenticated.value
    && nav.value === "files"
    && !batchMode.value
    && !inlineRename.active
  ) {
    event.preventDefault();
    startInlineRename();
  }
}

function handleGlobalContextMenu(event: MouseEvent) {
  const target = event.target as HTMLElement | null;
  if (!target) return;
  if (target.closest(".context-menu")) return;
  if (target.closest("input, textarea, [contenteditable=\"true\"]")) return;
  event.preventDefault();
}

function extractFileNameFromPath(filePath: string) {
  const trimmed = String(filePath || "").trim();
  if (!trimmed) return "";
  const normalized = trimmed.replace(/\\/g, "/");
  const segments = normalized.split("/").filter(Boolean);
  return segments[segments.length - 1] || normalized;
}

function canUseDragUpload() {
  return authenticated.value && nav.value === "files";
}

async function uploadDroppedFiles(paths: string[]) {
  const uniquePaths = [...new Set((paths || []).map((item) => String(item || "").trim()).filter(Boolean))];
  if (uniquePaths.length === 0) {
    showToast("未识别到可上传文件", "info");
    return;
  }

  if (dropState.uploading) {
    showToast("已有上传任务进行中，请稍后再试", "info");
    return;
  }

  dropState.uploading = true;
  dropState.total = uniquePaths.length;
  dropState.done = 0;
  dropState.failed = 0;

  let successCount = 0;
  for (let index = 0; index < uniquePaths.length; index += 1) {
    await waitForTransferQueue();
    const filePath = uniquePaths[index];
    const displayName = extractFileNameFromPath(filePath) || `文件${index + 1}`;
    const taskId = `U-${Date.now()}-${index}`;

    prependTransferTask({
      id: taskId,
      kind: "upload",
      name: displayName,
      speed: "等待上传",
      progress: 2,
      status: "queued",
      filePath,
      targetPath: pathState.currentPath,
      fileName: displayName,
    });
    updateTransferTask(taskId, { status: "uploading", speed: "上传中", progress: 10 });

    uploadRuntime.active = true;
    uploadRuntime.taskId = taskId;
    uploadRuntime.fileName = displayName;
    uploadRuntime.uploadedBytes = 0;
    uploadRuntime.totalBytes = 0;
    uploadRuntime.progress = 1;
    uploadRuntime.speed = "准备上传";
    uploadRuntime.lastMeasureAt = 0;
    uploadRuntime.lastMeasureBytes = 0;

    const response = await uploadFileWithResume(filePath, pathState.currentPath, taskId);

    if (response.ok && response.data?.success) {
      successCount += 1;
      dropState.done += 1;
      uploadRuntime.progress = 100;
      uploadRuntime.speed = "-";
      uploadRuntime.uploadedBytes = Math.max(uploadRuntime.uploadedBytes, uploadRuntime.totalBytes);
      updateTransferTask(taskId, {
        speed: "-",
        progress: 100,
        status: "done",
        note: "上传成功",
      });
    } else {
      dropState.failed += 1;
      const message = String(response.data?.message || "上传失败");
      uploadRuntime.speed = "-";
      updateTransferTask(taskId, {
        speed: "-",
        progress: 0,
        status: "failed",
        note: message,
      });
    }
  }

  dropState.uploading = false;
  setTimeout(() => {
    uploadRuntime.active = false;
    uploadRuntime.taskId = "";
  }, 1600);
  const failedMessage = dropState.failed > 0 ? `，失败 ${dropState.failed} 个` : "";
  showToast(`上传完成：成功 ${dropState.done} 个${failedMessage}`, dropState.failed > 0 ? "info" : "success");
  if (successCount > 0) {
    await loadFiles(pathState.currentPath);
  }
}

async function registerDragDropListener() {
  try {
    const currentWebview = getCurrentWebview();
    unlistenDragDrop = await currentWebview.onDragDropEvent((event) => {
      const payload = event.payload;
      if (payload.type === "enter" || payload.type === "over") {
        if (canUseDragUpload()) {
          dropState.active = true;
        }
        return;
      }

      if (payload.type === "leave") {
        dropState.active = false;
        return;
      }

      if (payload.type === "drop") {
        dropState.active = false;
        if (!canUseDragUpload()) {
          if (authenticated.value) {
            showToast("请先切换到“全部文件”页面再上传", "info");
          }
          return;
        }
        void uploadDroppedFiles(payload.paths || []);
      }
    });
  } catch (error) {
    console.error("register drag drop listener failed", error);
  }
}

async function registerNativeDownloadProgressListener() {
  try {
    unlistenNativeDownloadProgress = await listen<NativeDownloadProgressEvent>("native-download-progress", (event) => {
      applyNativeDownloadProgress(event.payload || {});
    });
  } catch (error) {
    console.error("register native download progress listener failed", error);
  }
}

async function registerNativeUploadProgressListener() {
  try {
    unlistenNativeUploadProgress = await listen<NativeUploadProgressEvent>("native-upload-progress", (event) => {
      applyNativeUploadProgress(event.payload || {});
    });
  } catch (error) {
    console.error("register native upload progress listener failed", error);
  }
}

watch(nav, async (next) => {
  if (next !== "files" && inlineRename.active) {
    cancelInlineRename(true);
  }
  if (next === "shares" && authenticated.value) {
    await loadShares();
    return;
  }
  if (next === "settings" && authenticated.value) {
    await checkClientUpdate(false);
    await loadOnlineDevices(true);
  }
});

watch(
  () => [syncState.localDir, syncState.remoteBasePath, syncState.autoEnabled, syncState.intervalMinutes, authenticated.value],
  () => {
    if (!authenticated.value) return;
    syncState.remoteBasePath = normalizePath(syncState.remoteBasePath || "/");
    if (syncState.intervalMinutes < 5) {
      syncState.intervalMinutes = 5;
    }
    saveSyncConfig();
    rebuildSyncScheduler();
  },
);

onMounted(async () => {
  window.addEventListener("click", handleGlobalClick);
  window.addEventListener("keydown", handleGlobalKey);
  window.addEventListener("contextmenu", handleGlobalContextMenu);
  await registerDragDropListener();
  await registerNativeDownloadProgressListener();
  await registerNativeUploadProgressListener();
  await initClientVersion();
  const restored = await restoreSession();
  if (!restored) {
    await tryAutoLoginFromSavedState();
  }
});

onBeforeUnmount(() => {
  window.removeEventListener("click", handleGlobalClick);
  window.removeEventListener("keydown", handleGlobalKey);
  window.removeEventListener("contextmenu", handleGlobalContextMenu);
  clearSyncScheduler();
  if (unlistenDragDrop) {
    unlistenDragDrop();
    unlistenDragDrop = null;
  }
  if (unlistenNativeDownloadProgress) {
    unlistenNativeDownloadProgress();
    unlistenNativeDownloadProgress = null;
  }
  if (unlistenNativeUploadProgress) {
    unlistenNativeUploadProgress();
    unlistenNativeUploadProgress = null;
  }
});
</script>

<template>
  <div class="desktop-root">
    <div v-if="!authenticated" class="login-shell">
      <section class="login-brand">
        <div class="brand-chip">玩玩云 Desktop</div>
        <h1>企业网盘桌面客户端</h1>
        <p>更快的目录操作、更清晰的传输队列、更像桌面软件的使用体验。</p>
        <div class="brand-grid">
          <div class="brand-card">
            <strong>多任务传输</strong>
            <span>上传下载独立队列管理</span>
          </div>
          <div class="brand-card">
            <strong>目录式检索</strong>
            <span>路径导航 + 快速筛选</span>
          </div>
          <div class="brand-card">
            <strong>独立工作台</strong>
            <span>文件、分享、传输一体化</span>
          </div>
          <div class="brand-card">
            <strong>可接现有后端</strong>
            <span>直接复用当前接口体系</span>
          </div>
        </div>
      </section>

      <section class="login-panel">
        <h2>登录云盘</h2>
        <label>
          用户名
          <input v-model="loginForm.username" type="text" placeholder="请输入账号" />
        </label>
        <label>
          密码
          <input v-model="loginForm.password" type="password" placeholder="请输入密码" @keyup.enter="handleLogin" />
        </label>
        <label v-if="loginState.needCaptcha">
          验证码
          <input v-model="loginForm.captcha" type="text" placeholder="当前服务要求验证码" />
        </label>

        <button class="primary-btn" :disabled="loginState.loading" @click="handleLogin">
          {{ loginState.loading ? "登录中..." : "进入客户端" }}
        </button>
        <p v-if="loginState.error" class="error-text">{{ loginState.error }}</p>
      </section>
    </div>

    <div v-else class="work-shell">
      <aside class="left-nav">
        <div class="left-header">
          <div class="app-mark">W</div>
          <div>
            <strong>玩玩云</strong>
            <span>Desktop</span>
          </div>
        </div>

        <button
          v-for="item in navItems"
          :key="item.key"
          class="nav-btn"
          :class="{ active: nav === item.key }"
          @click="nav = item.key"
        >
          <span>{{ item.label }}</span>
          <small>{{ item.hint }}</small>
        </button>

        <div class="user-card">
          <div class="avatar">{{ String(user?.username || "U").slice(0, 1).toUpperCase() }}</div>
          <div class="meta">
            <strong>{{ user?.username || "-" }}</strong>
            <span>{{ user?.current_storage_type === "local" ? "本地存储" : "OSS 存储" }}</span>
          </div>
          <button class="ghost-btn" @click="handleLogout">退出</button>
        </div>
      </aside>

      <section class="work-area">
        <header class="top-tools">
          <div class="crumbs">
            <button
              v-for="(crumb, idx) in toolbarCrumbs"
              :key="crumb.path"
              class="crumb-btn"
              @click="nav === 'files' ? jumpToPath(crumb.path) : null"
            >
              {{ crumb.label }}<span v-if="idx < toolbarCrumbs.length - 1"> / </span>
            </button>
          </div>
          <div class="tool-right" :class="{ 'tool-right-files': nav === 'files' }">
            <template v-if="nav === 'files'">
              <div class="file-search-row">
                <input
                  v-model="searchKeyword"
                  type="text"
                  class="search-input"
                  placeholder="输入关键词回车全局搜索..."
                  @keyup.enter="runGlobalSearch"
                />
                <button class="action-btn" @click="runGlobalSearch">搜索</button>
              </div>
              <div class="file-actions-row">
                <select v-model="fileViewState.filter" class="compact-select">
                  <option v-for="opt in fileTypeFilterOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
                </select>
                <select v-model="fileViewState.sortBy" class="compact-select">
                  <option v-for="opt in fileSortOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
                </select>
                <button class="action-btn" @click="toggleFileSortOrder">{{ fileViewState.sortOrder === "asc" ? "升序" : "降序" }}</button>
                <button class="action-btn" :class="{ active: batchMode }" @click="toggleBatchMode">{{ batchMode ? "退出批量" : "批量选择" }}</button>
                <button v-if="batchMode" class="action-btn" @click="clearBatchSelection">清空勾选</button>
                <button v-if="batchMode" class="action-btn danger" :disabled="batchSelectedItems.length === 0" @click="batchDeleteSelected">批量删除</button>
                <button v-if="batchMode" class="action-btn" :disabled="batchSelectedItems.length === 0" @click="batchShareSelected">批量分享</button>
                <button v-if="batchMode" class="action-btn" :disabled="batchSelectedItems.length === 0" @click="batchDirectLinkSelected">批量直链</button>
                <button class="action-btn" @click="createFolder">新建文件夹</button>
                <button class="action-btn" @click="loadFiles(pathState.currentPath)">刷新</button>
              </div>
            </template>
            <template v-else-if="nav === 'transfers'">
              <button class="action-btn" @click="toggleTransferQueuePause">{{ transferQueue.paused ? "继续队列" : "暂停队列" }}</button>
              <button class="action-btn" @click="clearCompletedTransferTasks">清理已结束</button>
            </template>
            <template v-else-if="nav === 'shares'">
              <button class="action-btn" @click="loadShares()">刷新分享</button>
            </template>
            <template v-else-if="nav === 'sync'">
              <button class="action-btn" :disabled="syncState.syncing || syncState.scanning" @click="runSyncOnce('manual')">
                {{ syncState.syncing ? "同步中..." : "立即同步" }}
              </button>
              <button class="action-btn" @click="clearSyncSnapshot">重建索引</button>
            </template>
            <template v-else-if="nav === 'settings'">
              <button class="action-btn" :disabled="updateState.checking || updateRuntime.downloading" @click="checkClientUpdate()">
                {{ updateState.checking ? "检查中..." : "检查更新" }}
              </button>
              <button class="action-btn" :disabled="updateRuntime.downloading || !updateState.available || !updateState.downloadUrl" @click="installLatestUpdate()">
                {{ updateRuntime.downloading ? "下载中..." : "立即更新" }}
              </button>
              <button class="action-btn" :disabled="onlineDevices.loading || !!onlineDevices.kickingSessionId" @click="loadOnlineDevices()">
                {{ onlineDevices.loading ? "刷新中..." : "刷新设备" }}
              </button>
            </template>
          </div>
        </header>

        <main class="main-grid" :class="{ 'focus-content': nav === 'files' }">
          <section class="panel content-panel">
            <template v-if="nav === 'files'">
              <div class="panel-head">
                <h3>文件视图</h3>
                <span>{{ pathState.mode === "search" ? "搜索结果" : "当前目录" }} {{ pathState.currentPath }}</span>
              </div>

              <div class="files-summary-bar">
                <span class="files-summary-main">
                  {{ pathState.mode === "search" ? `搜索完成，共 ${filteredFiles.length} 个结果` : `已加载完成，共 ${filteredFiles.length} 个` }}
                </span>
                <span class="files-summary-sub">双击打开，右键查看更多操作</span>
              </div>

              <div class="file-drop-surface" :class="{ active: dropState.active }">
                <div v-if="pathState.loading" class="empty-tip">正在加载目录...</div>
                <div v-else-if="pathState.error" class="empty-tip error">{{ pathState.error }}</div>
                <div v-else-if="filteredFiles.length === 0" class="empty-tip">当前目录暂无文件</div>
                <div v-else class="icon-grid">
                  <div
                    v-for="item in filteredFiles"
                    :key="item.name"
                    class="file-card"
                    :class="{ selected: selectedFileName === item.name, batchSelected: isBatchSelected(item.name), renaming: isInlineRenaming(item) }"
                    role="button"
                    tabindex="0"
                    @click="handleFileCardClick(item)"
                    @dblclick="handleFileCardDoubleClick(item)"
                    @keydown.enter.prevent="handleFileCardDoubleClick(item)"
                    @keydown.space.prevent="handleFileCardClick(item)"
                    @contextmenu.prevent="openContextMenu($event, item)"
                  >
                    <div v-if="batchMode" class="batch-check" :class="{ active: isBatchSelected(item.name) }">
                      {{ isBatchSelected(item.name) ? "✓" : "" }}
                    </div>
                    <div class="file-icon-shell" :class="`kind-${fileVisualKind(item)}`">
                      <template v-if="item.isDirectory || item.type === 'directory'">
                        <span class="folder-tab" />
                      </template>
                      <template v-else>
                        <span class="file-corner" />
                        <span class="file-ext">{{ fileExtLabel(item) }}</span>
                      </template>
                    </div>
                    <div class="file-name" :title="item.displayName || item.name">
                      <template v-if="isInlineRenaming(item)">
                        <input
                          v-model="inlineRename.value"
                          class="inline-rename-input"
                          data-renaming="1"
                          :disabled="inlineRename.saving"
                          @click.stop
                          @dblclick.stop
                          @keydown.enter.prevent="submitInlineRename(item)"
                          @keydown.esc.prevent="cancelInlineRename()"
                          @blur="cancelInlineRename()"
                        />
                      </template>
                      <template v-else>
                        {{ item.displayName || item.name }}
                      </template>
                    </div>
                  </div>
                </div>
                <div v-if="dropState.active" class="drop-overlay">
                  <div class="drop-overlay-card">
                    <strong>拖拽到此处上传到当前目录</strong>
                    <span>仅支持文件，文件夹会自动跳过</span>
                  </div>
                </div>
              </div>
            </template>

            <template v-else-if="nav === 'transfers'">
              <div class="panel-head">
                <h3>传输任务</h3>
                <span>{{ transferQueue.paused ? "队列已暂停（进行中任务会继续）" : "上传/下载队列" }}</span>
              </div>
              <div v-if="transferTasks.length === 0" class="empty-tip">暂无传输任务</div>
              <div v-else class="task-list">
                <div v-for="task in transferTasks" :key="task.id" class="task-row">
                  <div>
                    <strong>{{ task.name }}</strong>
                    <small>{{ task.id }} · {{ getTaskStatusLabel(task.status) }}</small>
                    <small v-if="task.note" class="task-note" :class="{ error: task.status === 'failed' }">{{ task.note }}</small>
                  </div>
                  <div class="task-right">
                    <span>{{ task.speed }}</span>
                    <div class="progress">
                      <div class="bar" :style="{ width: `${task.progress}%` }" />
                    </div>
                    <small class="task-percent">{{ Math.max(0, Math.min(100, Math.round(task.progress))) }}%</small>
                    <div class="task-actions">
                      <button v-if="task.status === 'failed'" class="mini-btn" @click="retryTransferTask(task.id)">重试</button>
                      <button v-if="task.status === 'done' || task.status === 'failed'" class="mini-btn ghost" @click="removeTransferTask(task.id)">移除</button>
                    </div>
                  </div>
                </div>
              </div>
            </template>

            <template v-else-if="nav === 'shares'">
              <div class="panel-head">
                <h3>我的分享</h3>
                <span>仅展示已分享文件及操作</span>
              </div>

              <div v-if="sharesLoading" class="empty-tip">正在加载分享列表...</div>
              <div v-else-if="sortedShares.length === 0" class="empty-tip">暂无分享记录</div>
              <div v-else class="share-list">
                <div v-for="share in sortedShares" :key="share.id" class="share-item">
                  <div class="share-main">
                    <div class="share-title">
                      <strong :title="share.share_path">{{ getShareDisplayName(share) }}</strong>
                      <span class="share-badge">{{ share.share_type === "directory" ? "文件夹" : "文件" }}</span>
                      <span class="share-badge">{{ share.has_password ? "密码保护" : "公开" }}</span>
                    </div>
                    <div class="share-link" :title="share.share_url">{{ share.share_url }}</div>
                    <div class="share-meta">
                      <span>分享码 {{ share.share_code }}</span>
                      <span>访问 {{ share.view_count || 0 }}</span>
                      <span>下载 {{ share.download_count || 0 }}</span>
                      <span>到期 {{ getShareExpireLabel(share.expires_at) }}</span>
                    </div>
                  </div>
                  <div class="share-actions">
                    <button class="action-btn" @click="openShareLink(share)">打开</button>
                    <button class="action-btn" @click="copyShareLink(share)">复制</button>
                    <button class="action-btn danger" @click="requestDeleteShare(share)">删除</button>
                  </div>
                </div>
              </div>
            </template>

            <template v-else-if="nav === 'sync'">
              <div class="panel-head">
                <h3>同步盘</h3>
                <span>本地目录增量上传到云端目录（仅上传变更文件）</span>
              </div>

              <div class="sync-layout">
                <label>
                  本地同步目录
                  <div class="sync-path-row">
                    <input :value="syncState.localDir || ''" type="text" readonly placeholder="请选择本地目录" />
                    <button class="action-btn" @click="chooseSyncDirectory">选择目录</button>
                  </div>
                </label>

                <label>
                  云端目标目录
                  <input v-model="syncState.remoteBasePath" type="text" placeholder="/项目同步目录" />
                </label>

                <div class="sync-option-row">
                  <label class="check-line">
                    <input v-model="syncState.autoEnabled" type="checkbox" />
                    <span>开启自动同步</span>
                  </label>
                  <div class="sync-interval">
                    <span>间隔</span>
                    <select v-model.number="syncState.intervalMinutes" :disabled="!syncState.autoEnabled" class="compact-select">
                      <option :value="5">5 分钟</option>
                      <option :value="15">15 分钟</option>
                      <option :value="30">30 分钟</option>
                      <option :value="60">60 分钟</option>
                    </select>
                  </div>
                </div>

                <div class="sync-summary-grid">
                  <div>
                    <strong>{{ syncState.pendingCount }}</strong>
                    <span>待同步</span>
                  </div>
                  <div>
                    <strong>{{ syncState.uploadedCount }}</strong>
                    <span>已成功</span>
                  </div>
                  <div>
                    <strong>{{ syncState.failedCount }}</strong>
                    <span>失败数</span>
                  </div>
                  <div>
                    <strong>{{ syncState.autoEnabled ? "开启" : "关闭" }}</strong>
                    <span>自动同步</span>
                  </div>
                </div>

                <div class="sync-meta">
                  <p>上次执行：{{ syncState.lastRunAt ? formatDate(syncState.lastRunAt) : "-" }}</p>
                  <p>下次执行：{{ syncState.nextRunAt ? formatDate(syncState.nextRunAt) : "-" }}</p>
                  <p>结果：{{ syncState.lastSummary || "等待执行同步任务" }}</p>
                </div>
              </div>
            </template>

            <template v-else-if="nav === 'settings'">
              <div class="panel-head">
                <h3>设置</h3>
                <span>版本更新与在线设备管理</span>
              </div>

              <div class="update-layout">
                <div class="update-main">
                  <div class="update-version-row">
                    <div>
                      <strong>当前版本</strong>
                      <span>v{{ updateState.currentVersion }}</span>
                    </div>
                    <div>
                      <strong>最新版本</strong>
                      <span>v{{ updateState.latestVersion || updateState.currentVersion }}</span>
                    </div>
                    <div>
                      <strong>更新状态</strong>
                      <span :class="{ 'update-available': updateState.available }">
                        {{ updateState.available ? "发现新版本" : "已是最新版" }}
                      </span>
                    </div>
                  </div>

                  <div class="update-notes">
                    <h4>更新说明</h4>
                    <p>{{ updateState.releaseNotes || "暂无发布说明" }}</p>
                    <p v-if="updateState.mandatory" class="update-mandatory">该版本为强制更新版本</p>
                  </div>

                  <div class="update-meta">
                    <span>上次检查：{{ updateState.lastCheckedAt ? formatDate(updateState.lastCheckedAt) : "-" }}</span>
                    <span>提示：{{ updateState.message || "可手动点击“检查更新”获取最新信息" }}</span>
                  </div>
                </div>

                <div class="settings-device-card">
                  <div class="settings-device-head">
                    <strong>在线设备</strong>
                    <span>{{ onlineDevices.items.length }} 台</span>
                  </div>
                  <p class="settings-device-tip">可强制下线异常设备，标记“本机”的为当前客户端。</p>
                  <p v-if="onlineDevices.message" class="settings-device-error">{{ onlineDevices.message }}</p>
                  <div v-if="onlineDevices.loading && onlineDevices.items.length === 0" class="empty-tip">正在加载在线设备...</div>
                  <div v-else-if="onlineDevices.items.length === 0" class="empty-tip">暂无在线设备</div>
                  <div v-else class="settings-device-list">
                    <div v-for="item in onlineDevices.items" :key="item.session_id" class="settings-device-item">
                      <div class="settings-device-main">
                        <div class="settings-device-name-row">
                          <strong>{{ item.device_name || "未知设备" }}</strong>
                          <span class="share-badge">{{ formatOnlineDeviceType(item.client_type) }}</span>
                          <span v-if="item.is_current || item.is_local" class="share-badge local">本机</span>
                        </div>
                        <div class="settings-device-meta">
                          <span>平台 {{ item.platform || "-" }}</span>
                          <span>IP {{ item.ip_address || "-" }}</span>
                          <span>活跃 {{ formatDate(item.last_active_at) }}</span>
                          <span>登录 {{ formatDate(item.created_at) }}</span>
                        </div>
                      </div>
                      <button
                        class="action-btn danger"
                        :disabled="onlineDevices.kickingSessionId === item.session_id || operationConfirmDialog.loading"
                        @click="requestKickOnlineDevice(item)"
                      >
                        {{ onlineDevices.kickingSessionId === item.session_id ? "处理中..." : (item.is_current || item.is_local ? "下线本机" : "踢下线") }}
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            </template>
          </section>

          <aside class="panel detail-panel">
            <h3>详情面板</h3>
            <template v-if="nav === 'files'">
              <div class="stat-grid">
                <div>
                  <strong>{{ fileStats.total }}</strong>
                  <span>当前项目数</span>
                </div>
                <div>
                  <strong>{{ fileStats.folders }}</strong>
                  <span>文件夹</span>
                </div>
                <div>
                  <strong>{{ fileStats.docs }}</strong>
                  <span>文件数</span>
                </div>
                <div>
                  <strong>{{ formatBytes(fileStats.totalBytes) }}</strong>
                  <span>目录容量</span>
                </div>
              </div>

              <div v-if="batchMode" class="selected-info">
                <h4>批量操作</h4>
                <p>已勾选：{{ batchSelectedItems.length }} 项</p>
                <p>可执行：批量删除、批量分享、批量直链</p>
              </div>
              <div v-if="selectedFile" class="selected-info">
                <h4>{{ selectedFile.displayName || selectedFile.name }}</h4>
                <p>类型：{{ fileTypeLabel(selectedFile) }}</p>
                <p>路径：{{ buildItemPath(selectedFile) }}</p>
                <p>大小：{{ selectedFile.isDirectory ? "-" : (selectedFile.sizeFormatted || formatBytes(selectedFile.size)) }}</p>
                <p>时间：{{ formatDate(selectedFile.modifiedAt) }}</p>
              </div>
              <div v-else class="empty-tip">选中一个文件可查看详细信息</div>
            </template>

            <template v-else-if="nav === 'sync'">
              <div class="stat-grid">
                <div>
                  <strong>{{ syncState.autoEnabled ? "已开启" : "已关闭" }}</strong>
                  <span>自动同步</span>
                </div>
                <div>
                  <strong>{{ syncState.intervalMinutes }} 分钟</strong>
                  <span>同步周期</span>
                </div>
                <div>
                  <strong>{{ syncState.localDir ? "已设置" : "未设置" }}</strong>
                  <span>本地目录</span>
                </div>
                <div>
                  <strong>{{ syncState.remoteBasePath }}</strong>
                  <span>云端目录</span>
                </div>
              </div>
              <div class="selected-info">
                <h4>同步说明</h4>
                <p>仅上传本地变更文件，不会自动删除云端历史文件。</p>
                <p>网络中断或失败项会在下次同步自动重试。</p>
                <p>目录切换到“全部文件”后可立即看到最新结果。</p>
              </div>
            </template>

            <template v-else-if="nav === 'settings'">
              <div class="stat-grid">
                <div>
                  <strong>v{{ updateState.currentVersion }}</strong>
                  <span>当前版本</span>
                </div>
                <div>
                  <strong>v{{ updateState.latestVersion || updateState.currentVersion }}</strong>
                  <span>最新版本</span>
                </div>
                <div>
                  <strong>{{ updateState.available ? "可升级" : "最新" }}</strong>
                  <span>状态</span>
                </div>
                <div>
                  <strong>{{ updateState.downloadUrl ? "已提供" : "未配置" }}</strong>
                  <span>下载地址</span>
                </div>
              </div>
              <div class="selected-info">
                <h4>设备与升级</h4>
                <p>当前在线设备：{{ onlineDevices.items.length }} 台。</p>
                <p>更新下载和静默安装状态会显示在右下角状态卡。</p>
              </div>
            </template>

            <template v-else>
              <div class="stat-grid">
                <div>
                  <strong>{{ transferTasks.length }}</strong>
                  <span>传输任务</span>
                </div>
                <div>
                  <strong>{{ shares.length }}</strong>
                  <span>分享数量</span>
                </div>
                <div>
                  <strong>{{ files.length }}</strong>
                  <span>目录项目</span>
                </div>
                <div>
                  <strong>{{ formatBytes(fileStats.totalBytes) }}</strong>
                  <span>目录容量</span>
                </div>
              </div>
            </template>
          </aside>
        </main>
      </section>
    </div>

    <div v-if="contextMenu.visible" class="context-mask" @click="closeContextMenu"></div>
    <div
      v-if="contextMenu.visible && contextMenu.item"
      class="context-menu"
      :style="{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }"
      @click.stop
    >
      <button class="context-item" @click="executeContextAction('open')">
        {{ contextMenu.item.isDirectory ? "打开文件夹" : "打开预览" }}
      </button>
      <button v-if="!contextMenu.item.isDirectory" class="context-item" @click="executeContextAction('download')">下载文件</button>
      <button class="context-item" @click="executeContextAction('rename')">重命名</button>
      <button class="context-item danger" @click="executeContextAction('delete')">删除</button>
      <div class="context-divider"></div>
      <button class="context-item" @click="executeContextAction('share')">生成分享链接</button>
      <button v-if="!contextMenu.item.isDirectory" class="context-item" @click="executeContextAction('direct')">生成直链</button>
    </div>

    <div v-if="shareDeleteDialog.visible" class="confirm-mask" @click="closeDeleteShareDialog()">
      <div class="confirm-card" @click.stop>
        <h4>确认删除分享</h4>
        <p>
          确认删除分享码 <strong>{{ shareDeleteDialog.share?.share_code || "-" }}</strong> 吗？
          删除后外链将立即失效。
        </p>
        <div class="confirm-actions">
          <button class="action-btn danger" :disabled="shareDeleteDialog.loading" @click="confirmDeleteShare()">
            {{ shareDeleteDialog.loading ? "删除中..." : "确定删除" }}
          </button>
          <button class="action-btn" :disabled="shareDeleteDialog.loading" @click="closeDeleteShareDialog()">取消</button>
        </div>
      </div>
    </div>

    <div v-if="fileDeleteDialog.visible" class="confirm-mask" @click="closeDeleteFileDialog()">
      <div class="confirm-card" @click.stop>
        <h4>确认删除文件</h4>
        <p>
          确认删除 <strong>{{ fileDeleteDialog.file?.displayName || fileDeleteDialog.file?.name || "-" }}</strong> 吗？
          删除后将无法恢复。
        </p>
        <div class="confirm-actions">
          <button class="action-btn danger" :disabled="fileDeleteDialog.loading" @click="confirmDeleteFile()">
            {{ fileDeleteDialog.loading ? "删除中..." : "确定删除" }}
          </button>
          <button class="action-btn" :disabled="fileDeleteDialog.loading" @click="closeDeleteFileDialog()">取消</button>
        </div>
      </div>
    </div>

    <div v-if="operationConfirmDialog.visible" class="confirm-mask" @click="closeOperationConfirmDialog()">
      <div class="confirm-card" @click.stop>
        <h4>{{ operationConfirmDialog.title || "确认操作" }}</h4>
        <p>{{ operationConfirmDialog.message || "确认继续执行该操作吗？" }}</p>
        <div class="confirm-actions">
          <button class="action-btn danger" :disabled="operationConfirmDialog.loading" @click="confirmOperationDialog()">
            {{ operationConfirmDialog.loading ? "处理中..." : (operationConfirmDialog.confirmText || "确定") }}
          </button>
          <button class="action-btn" :disabled="operationConfirmDialog.loading" @click="closeOperationConfirmDialog()">取消</button>
        </div>
      </div>
    </div>

    <div v-if="updatePrompt.visible" class="confirm-mask" @click="dismissUpdatePrompt()">
      <div class="confirm-card" @click.stop>
        <h4>发现新版本 v{{ updateState.latestVersion || "-" }}</h4>
        <p>
          已在登录后检测到可用更新，是否现在进行静默升级？
          升级完成后将自动重启客户端。
        </p>
        <div class="confirm-actions">
          <button class="action-btn danger" :disabled="updatePrompt.loading" @click="confirmUpdateFromPrompt()">
            {{ updatePrompt.loading ? "处理中..." : "立即更新" }}
          </button>
          <button class="action-btn" :disabled="updatePrompt.loading" @click="dismissUpdatePrompt(true)">稍后提醒</button>
        </div>
      </div>
    </div>

    <div v-if="uploadRuntime.active || updateRuntime.downloading || updateRuntime.installing" class="status-stack">
      <div v-if="uploadRuntime.active" class="status-card">
        <div class="status-head">
          <strong>上传中</strong>
          <span>{{ Math.max(0, Math.min(100, Math.round(uploadRuntime.progress))) }}%</span>
        </div>
        <div class="status-name" :title="uploadRuntime.fileName">{{ uploadRuntime.fileName || "正在上传文件" }}</div>
        <div class="progress compact">
          <div class="bar" :style="{ width: `${uploadRuntime.progress}%` }" />
        </div>
        <small>{{ formatBytes(uploadRuntime.uploadedBytes) }} / {{ formatBytes(uploadRuntime.totalBytes) }} · {{ uploadRuntime.speed }}</small>
      </div>

      <div v-if="updateRuntime.downloading || updateRuntime.installing" class="status-card">
        <div class="status-head">
          <strong>{{ updateRuntime.installing ? "安装更新" : "下载更新" }}</strong>
          <span>{{ Math.max(0, Math.min(100, Math.round(updateRuntime.progress))) }}%</span>
        </div>
        <div class="status-name">
          {{ updateRuntime.installing ? "静默安装中，完成后自动重启" : `v${updateState.latestVersion || "-"}` }}
        </div>
        <div class="progress compact">
          <div class="bar" :style="{ width: `${updateRuntime.progress}%` }" />
        </div>
        <small v-if="updateRuntime.downloading">{{ formatBytes(updateRuntime.downloadedBytes) }} / {{ formatBytes(updateRuntime.totalBytes) }} · {{ updateRuntime.speed }}</small>
        <small v-else>请稍候，应用将自动退出并重启</small>
      </div>
    </div>

    <div v-if="toast.visible" class="toast" :class="toast.type">{{ toast.message }}</div>
  </div>
</template>

<style scoped>
:global(html, body, #app) {
  width: 100%;
  height: 100%;
  margin: 0;
  font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  background: linear-gradient(155deg, #f4f7fb 0%, #eef3ff 40%, #e8eef8 100%);
  color: #1f2b3a;
}

:global(*) {
  box-sizing: border-box;
}

.desktop-root {
  width: 100%;
  height: 100%;
  padding: 8px;
}

.login-shell {
  height: 100%;
  display: grid;
  grid-template-columns: 1.2fr 0.9fr;
  gap: 16px;
}

.login-brand,
.login-panel,
.panel {
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(141, 164, 196, 0.25);
  border-radius: 20px;
  box-shadow: 0 14px 28px rgba(28, 59, 102, 0.08);
}

.login-brand {
  padding: 32px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.brand-chip {
  width: fit-content;
  background: #1d6fff;
  color: #fff;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  letter-spacing: 0.6px;
}

.login-brand h1 {
  margin: 0;
  font-size: 34px;
  line-height: 1.2;
}

.login-brand p {
  margin: 0;
  color: #51667f;
  font-size: 15px;
}

.brand-grid {
  margin-top: 10px;
  display: grid;
  gap: 10px;
  grid-template-columns: 1fr 1fr;
}

.brand-card {
  padding: 16px;
  border-radius: 14px;
  background: #f2f7ff;
  border: 1px solid #d7e5ff;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.brand-card strong {
  font-size: 14px;
}

.brand-card span {
  font-size: 12px;
  color: #5c7189;
}

.login-panel {
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.login-panel h2 {
  margin: 0 0 8px;
  font-size: 23px;
}

label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 13px;
  color: #435872;
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="email"] {
  width: 100%;
  height: 42px;
  border: 1px solid #d6dfe9;
  border-radius: 12px;
  padding: 0 12px;
  font-size: 14px;
  color: #1f2b3a;
  background: #fff;
}

select {
  width: 100%;
  height: 42px;
  border: 1px solid #d6dfe9;
  border-radius: 12px;
  padding: 0 12px;
  font-size: 14px;
  color: #1f2b3a;
  background: #fff;
}

input[type="text"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
select:focus {
  border-color: #1d6fff;
  outline: none;
  box-shadow: 0 0 0 3px rgba(29, 111, 255, 0.12);
}

.primary-btn,
.solid-btn,
.ghost-btn {
  height: 40px;
  border-radius: 12px;
  border: 0;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
}

.primary-btn,
.solid-btn {
  background: linear-gradient(90deg, #1d6fff 0%, #3f8cff 100%);
  color: #fff;
}

.primary-btn:disabled {
  opacity: 0.65;
  cursor: not-allowed;
}

.error-text {
  margin: 0;
  color: #d04848;
  font-size: 13px;
}

.work-shell {
  height: 100%;
  display: grid;
  grid-template-columns: 248px 1fr;
  gap: 14px;
}

.left-nav {
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid rgba(141, 164, 196, 0.3);
  border-radius: 20px;
  padding: 16px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.left-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px;
  margin-bottom: 4px;
}

.app-mark {
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: linear-gradient(140deg, #1d6fff, #57a2ff);
  color: #fff;
  display: grid;
  place-items: center;
  font-weight: 800;
}

.left-header strong {
  display: block;
  font-size: 16px;
}

.left-header span {
  font-size: 12px;
  color: #6b8097;
}

.nav-btn {
  text-align: left;
  padding: 10px 12px;
  border-radius: 12px;
  border: 0;
  background: transparent;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-btn span {
  font-size: 14px;
  color: #223244;
}

.nav-btn small {
  color: #6b8097;
  font-size: 11px;
}

.nav-btn:hover {
  background: #eef4ff;
}

.nav-btn.active {
  background: #1d6fff;
}

.nav-btn.active span,
.nav-btn.active small {
  color: #fff;
}

.user-card {
  margin-top: auto;
  background: #eff4fb;
  border-radius: 14px;
  padding: 10px;
  display: grid;
  grid-template-columns: 38px 1fr auto;
  gap: 10px;
  align-items: center;
}

.avatar {
  width: 38px;
  height: 38px;
  border-radius: 12px;
  background: #d9e8ff;
  display: grid;
  place-items: center;
  font-weight: 700;
  color: #2456a8;
}

.user-card .meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.user-card .meta strong {
  font-size: 13px;
}

.user-card .meta span {
  font-size: 11px;
  color: #60768f;
}

.ghost-btn {
  width: 56px;
  height: 28px;
  background: #fff;
  border: 1px solid #d8e2ee;
  font-size: 12px;
}

.work-area {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 12px;
}

.top-tools {
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid rgba(141, 164, 196, 0.3);
  border-radius: 16px;
  padding: 8px 12px;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
}

.crumbs {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  overflow-x: auto;
  padding-top: 3px;
}

.crumb-btn {
  height: 30px;
  display: inline-flex;
  align-items: center;
  border: 0;
  background: transparent;
  color: #3f5876;
  cursor: pointer;
  font-size: 13px;
  white-space: nowrap;
}

.crumb-btn:hover {
  color: #1d6fff;
}

.tool-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
  min-width: 0;
}

.tool-right-files {
  flex: 1 1 700px;
  min-width: 460px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}

.file-search-row {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

.file-actions-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-start;
}

.search-input {
  width: 280px;
  height: 36px;
  border-radius: 10px;
  font-size: 13px;
}

.tool-right-files .search-input {
  width: auto;
  flex: 1 1 420px;
  min-width: 220px;
}

.compact-select {
  width: 120px;
  height: 36px;
  border-radius: 10px;
  font-size: 12px;
  padding: 0 8px;
}

.solid-btn {
  height: 36px;
  min-width: 78px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

.action-btn {
  height: 36px;
  min-width: 74px;
  border: 1px solid #cad8ea;
  border-radius: 10px;
  background: #fff;
  color: #284666;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  font-size: 13px;
}

.action-btn:hover {
  background: #f2f7ff;
}

.action-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.action-btn.active {
  border-color: #6a99de;
  background: #eaf3ff;
  color: #1d4f93;
}

.action-btn.danger {
  border-color: #efc3c3;
  color: #b53f3f;
  background: #fff8f8;
}

.main-grid {
  display: grid;
  grid-template-columns: 1fr 270px;
  gap: 14px;
  min-height: 0;
}

.main-grid.focus-content {
  grid-template-columns: 1fr;
}

.main-grid.focus-content .detail-panel {
  display: none;
}

.content-panel,
.detail-panel {
  padding: 16px;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.panel-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

.panel-head h3 {
  margin: 0;
  font-size: 16px;
}

.panel-head span {
  color: #60768f;
  font-size: 12px;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.files-summary-bar {
  height: 34px;
  border: 1px solid #e8eef7;
  border-radius: 10px;
  background: #f8fbff;
  padding: 0 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}

.files-summary-main {
  font-size: 12px;
  color: #385373;
  font-weight: 600;
}

.files-summary-sub {
  font-size: 12px;
  color: #6b8097;
}

.icon-grid {
  display: grid;
  gap: 8px;
  grid-template-columns: repeat(auto-fill, 108px);
  justify-content: flex-start;
  align-content: start;
  min-height: 0;
  overflow: auto;
  padding: 4px;
}

.file-drop-surface {
  position: relative;
  min-height: 0;
  flex: 1;
  border: 1px solid #e8eef7;
  border-radius: 12px;
  background: #fff;
  padding: 12px 10px 10px;
}

.file-drop-surface.active .icon-grid {
  filter: saturate(1.04) blur(0.2px);
}

.file-card {
  border: 1px solid transparent;
  border-radius: 9px;
  background: transparent;
  text-align: left;
  padding: 7px 6px 6px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 4px;
  width: 108px;
  min-height: 124px;
  overflow: hidden;
  position: relative;
}

.file-card:focus-visible {
  outline: 2px solid #79a8f0;
  outline-offset: 1px;
}

.file-card:hover {
  border-color: #d7e6fb;
  background: #f3f8ff;
}

.file-card.selected {
  border-color: #a5c7f8;
  background: #e8f2ff;
  box-shadow: 0 0 0 1px rgba(73, 128, 216, 0.14) inset;
}

.file-card.batchSelected {
  border-color: #82b0f3;
  background: #eaf3ff;
  box-shadow: 0 0 0 1px rgba(63, 132, 236, 0.2) inset;
}

.file-card.renaming {
  border-color: #7baaf8;
  background: #eff6ff;
}

.batch-check {
  position: absolute;
  right: 6px;
  top: 6px;
  width: 16px;
  height: 16px;
  border-radius: 999px;
  border: 1px solid #bad0eb;
  background: #fff;
  color: #1f67c9;
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 700;
}

.batch-check.active {
  border-color: #2e7be8;
  background: #2e7be8;
  color: #fff;
}

.file-icon-shell {
  width: 60px;
  height: 50px;
  position: relative;
  margin-top: 1px;
  border-radius: 10px;
  border: 1px solid #cfdced;
  background: linear-gradient(180deg, #7ab5ff 0%, #4689dd 100%);
  box-shadow: 0 2px 4px rgba(32, 77, 131, 0.14);
}

.file-icon-shell.kind-folder {
  height: 44px;
  margin-top: 8px;
  border-radius: 8px;
  border-color: #d8b860;
  background: linear-gradient(180deg, #f2c85e 0%, #e3b145 100%);
  box-shadow: 0 2px 4px rgba(152, 106, 14, 0.14);
}

.folder-tab {
  position: absolute;
  left: 9px;
  top: -7px;
  width: 24px;
  height: 10px;
  border-radius: 6px 6px 0 0;
  background: linear-gradient(180deg, #f8d379 0%, #e8bc53 100%);
  border: 1px solid #d8b860;
  border-bottom: 0;
}

.file-corner {
  position: absolute;
  right: 0;
  top: 0;
  width: 16px;
  height: 16px;
  background: rgba(255, 255, 255, 0.9);
  clip-path: polygon(100% 0, 0 0, 100% 100%);
  border-top-right-radius: 8px;
}

.file-ext {
  position: absolute;
  left: 50%;
  bottom: 7px;
  transform: translateX(-50%);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.4px;
  line-height: 1;
}

.file-icon-shell.kind-document {
  background: linear-gradient(180deg, #79b6ff 0%, #4c8ee2 100%);
}

.file-icon-shell.kind-image {
  background: linear-gradient(180deg, #6fc7a1 0%, #3fa67b 100%);
}

.file-icon-shell.kind-video {
  background: linear-gradient(180deg, #88b4ff 0%, #5b7fd9 100%);
}

.file-icon-shell.kind-audio {
  background: linear-gradient(180deg, #ff9f7b 0%, #eb7059 100%);
}

.file-icon-shell.kind-archive {
  background: linear-gradient(180deg, #f6bc74 0%, #e58e3a 100%);
}

.file-icon-shell.kind-app {
  background: linear-gradient(180deg, #9cc36f 0%, #6ea649 100%);
}

.file-icon-shell.kind-file {
  background: linear-gradient(180deg, #95b0d3 0%, #6684ad 100%);
}

.file-name {
  font-size: 12px;
  font-weight: 500;
  color: #1f2b3a;
  line-height: 1.4;
  width: 100%;
  text-align: center;
  overflow: hidden;
  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  min-height: 32px;
  max-height: 32px;
}

.inline-rename-input {
  width: 100%;
  height: 30px;
  border: 1px solid #7baaf8;
  border-radius: 8px;
  background: #fff;
  padding: 0 8px;
  font-size: 13px;
  font-weight: 600;
  color: #1f2b3a;
}

.inline-rename-input:focus {
  outline: 0;
  border-color: #3f84ec;
  box-shadow: 0 0 0 2px rgba(63, 132, 236, 0.16);
}

.task-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.task-row {
  padding: 10px;
  border: 1px solid #d8e1ee;
  border-radius: 12px;
  display: grid;
  grid-template-columns: 1fr 190px;
  gap: 10px;
  align-items: center;
}

.task-row strong {
  display: block;
  font-size: 13px;
}

.task-row small {
  display: block;
  color: #647b94;
}

.task-note {
  margin-top: 2px;
  color: #5f7895;
}

.task-note.error {
  color: #c24747;
}

.task-right {
  display: grid;
  gap: 6px;
}

.task-right span {
  font-size: 12px;
  color: #4a6381;
}

.task-actions {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
}

.mini-btn {
  height: 26px;
  padding: 0 10px;
  border-radius: 8px;
  border: 1px solid #c5d4e8;
  background: #fff;
  color: #2f4f74;
  cursor: pointer;
  font-size: 12px;
}

.mini-btn:hover {
  background: #f3f8ff;
}

.mini-btn.ghost {
  color: #6882a0;
}

.progress {
  height: 10px;
  background: #dbe7f8;
  border: 1px solid #cadef6;
  border-radius: 999px;
  overflow: hidden;
}

.bar {
  height: 100%;
  background: linear-gradient(90deg, #1d6fff, #57a2ff);
  transition: width 0.14s linear;
}

.progress.compact {
  height: 8px;
}

.task-percent {
  justify-self: end;
  color: #3f5f86;
  font-size: 11px;
  font-weight: 600;
}

.share-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: auto;
  min-height: 0;
}

.share-item {
  border: 1px solid #d8e1ee;
  border-radius: 12px;
  background: #fff;
  padding: 12px;
  display: grid;
  gap: 12px;
  grid-template-columns: 1fr auto;
  align-items: center;
}

.share-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.share-title {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.share-title strong {
  font-size: 14px;
  color: #203043;
}

.share-badge {
  display: inline-flex;
  align-items: center;
  height: 22px;
  border-radius: 999px;
  padding: 0 8px;
  background: #eef5ff;
  color: #3d5f8a;
  font-size: 11px;
}

.share-link,
.share-meta {
  font-size: 12px;
  color: #5a718c;
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.share-meta {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  row-gap: 4px;
}

.share-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.share-badge.local {
  background: #e8f8ee;
  color: #1f8f4f;
}

.settings-device-card {
  border: 1px solid #d8e1ee;
  border-radius: 12px;
  background: #fff;
  padding: 12px;
  display: grid;
  gap: 10px;
}

.settings-device-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}

.settings-device-head strong {
  font-size: 14px;
  color: #203754;
}

.settings-device-head span {
  font-size: 12px;
  color: #5d7898;
}

.settings-device-tip {
  margin: 0;
  color: #5a718c;
  font-size: 12px;
}

.settings-device-error {
  margin: 0;
  color: #c24747;
  font-size: 12px;
}

.settings-device-list {
  display: grid;
  gap: 8px;
  max-height: 310px;
  overflow: auto;
}

.settings-device-item {
  border: 1px solid #d8e1ee;
  border-radius: 10px;
  background: #f8fbff;
  padding: 10px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 10px;
  align-items: center;
}

.settings-device-main {
  min-width: 0;
  display: grid;
  gap: 6px;
}

.settings-device-name-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.settings-device-name-row strong {
  font-size: 13px;
  color: #203043;
}

.settings-device-meta {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  row-gap: 4px;
}

.settings-device-meta span {
  font-size: 11px;
  color: #60768f;
}

.sync-layout,
.update-layout {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 0;
}

.sync-path-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 8px;
}

.sync-option-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.check-line {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: #3b5574;
}

.check-line input[type="checkbox"] {
  width: 16px;
  height: 16px;
}

.sync-interval {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: #5c738f;
}

.sync-summary-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}

.sync-summary-grid > div {
  border: 1px solid #d8e1ee;
  border-radius: 10px;
  background: #f7faff;
  padding: 10px 12px;
}

.sync-summary-grid strong {
  display: block;
  font-size: 15px;
  color: #1f3653;
}

.sync-summary-grid span {
  font-size: 11px;
  color: #60768f;
}

.sync-meta {
  border: 1px dashed #cfdced;
  border-radius: 10px;
  background: #f9fbff;
  padding: 10px 12px;
}

.sync-meta p {
  margin: 4px 0;
  font-size: 12px;
  color: #48627f;
}

.update-main {
  border: 1px solid #d8e1ee;
  border-radius: 12px;
  background: #fff;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.update-version-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

.update-version-row > div {
  border: 1px solid #d8e1ee;
  border-radius: 10px;
  background: #f7faff;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.update-version-row strong {
  font-size: 12px;
  color: #60748e;
}

.update-version-row span {
  font-size: 14px;
  color: #1e3857;
}

.update-available {
  color: #1f9a63 !important;
  font-weight: 700;
}

.update-notes {
  border: 1px dashed #cedbeb;
  border-radius: 10px;
  padding: 10px 12px;
  background: #f9fbff;
}

.update-notes h4 {
  margin: 0 0 8px;
  font-size: 13px;
}

.update-notes p {
  margin: 0 0 6px;
  font-size: 12px;
  color: #4f6984;
  line-height: 1.5;
  white-space: pre-wrap;
}

.update-mandatory {
  color: #c24747 !important;
  font-weight: 700;
}

.update-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.update-meta span {
  font-size: 12px;
  color: #4f6984;
}

.context-mask {
  position: fixed;
  inset: 0;
  z-index: 80;
}

.context-menu {
  position: fixed;
  z-index: 90;
  width: 224px;
  border: 1px solid #d1dced;
  border-radius: 14px;
  background: #ffffff;
  box-shadow: 0 12px 24px rgba(25, 52, 86, 0.2);
  padding: 8px;
}

.context-item {
  width: 100%;
  height: 36px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  text-align: left;
  padding: 0 12px;
  display: flex;
  align-items: center;
  font-size: 13px;
  color: #2a4664;
  cursor: pointer;
}

.context-item:hover {
  background: #eef4ff;
}

.context-item.danger {
  color: #b53f3f;
}

.context-divider {
  height: 1px;
  background: #e5edf7;
  margin: 6px 4px;
}

.confirm-mask {
  position: fixed;
  inset: 0;
  z-index: 1200;
  background: rgba(20, 33, 54, 0.42);
  display: grid;
  place-items: center;
  padding: 20px;
}

.confirm-card {
  width: min(460px, 100%);
  border-radius: 14px;
  background: #fff;
  border: 1px solid #d7e2f0;
  box-shadow: 0 16px 36px rgba(30, 52, 88, 0.2);
  padding: 18px;
  display: grid;
  gap: 10px;
}

.confirm-card h4 {
  margin: 0;
  font-size: 16px;
  color: #203043;
}

.confirm-card p {
  margin: 0;
  color: #4f6784;
  line-height: 1.6;
  font-size: 13px;
}

.confirm-actions {
  display: flex;
  justify-content: flex-start;
  gap: 8px;
}

.status-stack {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 1250;
  width: min(360px, calc(100vw - 24px));
  display: grid;
  gap: 8px;
}

.status-card {
  border-radius: 12px;
  border: 1px solid #d3dfef;
  background: rgba(255, 255, 255, 0.96);
  box-shadow: 0 10px 20px rgba(31, 56, 92, 0.16);
  padding: 10px 12px;
  display: grid;
  gap: 6px;
}

.status-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}

.status-head strong {
  font-size: 13px;
  color: #203754;
}

.status-head span {
  font-size: 12px;
  color: #456892;
}

.status-name {
  font-size: 12px;
  color: #4f6784;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.status-card small {
  color: #5d7898;
  font-size: 11px;
}

.detail-panel h3 {
  margin: 0;
  font-size: 16px;
}

.stat-grid {
  margin-top: 12px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.stat-grid > div {
  border-radius: 10px;
  border: 1px solid #d8e1ee;
  background: #f7faff;
  padding: 12px;
  min-height: 68px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 4px;
}

.stat-grid strong {
  display: block;
  font-size: 15px;
}

.stat-grid span {
  font-size: 11px;
  color: #60768f;
}

.selected-info {
  margin-top: 14px;
  border-top: 1px solid #dbe6f2;
  padding-top: 14px;
}

.selected-info h4 {
  margin: 0 0 8px;
  font-size: 14px;
}

.selected-info p {
  margin: 0 0 7px;
  font-size: 12px;
  line-height: 1.45;
  color: #4f6984;
  word-break: break-all;
}

.empty-tip {
  margin: auto;
  padding: 24px 12px;
  text-align: center;
  line-height: 1.5;
  color: #5a7089;
  font-size: 13px;
}

.empty-tip.error {
  color: #cc4242;
}

.drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 12;
  border-radius: 12px;
  background: rgba(29, 111, 255, 0.08);
  border: 1px dashed rgba(29, 111, 255, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  pointer-events: none;
}

.drop-overlay-card {
  min-width: 260px;
  max-width: 380px;
  border-radius: 12px;
  padding: 14px 16px;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(141, 164, 196, 0.4);
  box-shadow: 0 10px 20px rgba(22, 44, 73, 0.12);
  text-align: center;
}

.drop-overlay-card strong {
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  color: #1d3f73;
}

.drop-overlay-card span {
  display: block;
  font-size: 12px;
  color: #5f7896;
}

.toast {
  position: fixed;
  right: 24px;
  bottom: 24px;
  padding: 10px 14px;
  border-radius: 10px;
  color: #fff;
  font-size: 13px;
  box-shadow: 0 10px 18px rgba(22, 44, 73, 0.18);
}

.toast.info {
  background: #4f6e8f;
}

.toast.success {
  background: #1f9a63;
}

.toast.error {
  background: #d04848;
}

@media (max-width: 1260px) {
  .top-tools {
    flex-direction: column;
    align-items: stretch;
  }

  .crumbs {
    padding-top: 0;
  }

  .tool-right {
    justify-content: flex-start;
  }

  .tool-right-files {
    min-width: 0;
  }

  .file-search-row,
  .file-actions-row {
    justify-content: flex-start;
  }

  .tool-right-files .search-input {
    min-width: 0;
  }

  .main-grid {
    grid-template-columns: 1fr;
  }

  .share-item {
    grid-template-columns: 1fr;
  }

  .settings-device-item {
    grid-template-columns: 1fr;
  }

  .share-actions {
    justify-content: flex-start;
  }

  .sync-summary-grid,
  .update-version-row {
    grid-template-columns: 1fr 1fr;
  }

  .compact-select {
    width: 108px;
  }
}
</style>
