<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";
import {
  PhArrowClockwise,
  PhArrowsClockwise,
  PhArrowsDownUp,
  PhCaretDown,
  PhCaretRight,
  PhCheck,
  PhCloudArrowUp,
  PhDownloadSimple,
  PhDotsThreeVertical,
  PhFile,
  PhFileArchive,
  PhFileAudio,
  PhFileDoc,
  PhFileImage,
  PhFilePdf,
  PhFilePpt,
  PhFileText,
  PhFileVideo,
  PhFileXls,
  PhFileZip,
  PhFolderSimple,
  PhFolderSimplePlus,
  PhGearSix,
  PhInfo,
  PhMagnifyingGlass,
  PhMinus,
  PhPencilSimple,
  PhPlus,
  PhSelectionAll,
  PhShareNetwork,
  PhSignOut,
  PhSortAscending,
  PhSortDescending,
  PhTrash,
  PhUploadSimple,
  PhX,
} from "@phosphor-icons/vue";
import dogLogo from "../src-tauri/icons/wanwan-dog-source.png";

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

type FileItem = {
  name: string;
  displayName?: string;
  path?: string;
  type: "file" | "directory";
  size?: number;
  sizeFormatted?: string;
  modifiedAt?: string;
  createdAt?: string;
  owner?: string;
  updatedBy?: string;
  tags?: string[];
  description?: 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 ShareExpiryType = "never" | "7" | "30" | "custom";
type ShareDeviceLimit = "all" | "mobile" | "desktop";

type ShareCreateOptions = {
  password: string | null;
  expiryDays: number | null;
  maxDownloads: number | null;
  ipWhitelist: string | null;
  deviceLimit: ShareDeviceLimit;
  accessTimeStart: string | null;
  accessTimeEnd: string | null;
};

type ShareCreateResult = {
  itemName: string;
  shareUrl: string;
  shareCode: string;
  expiresAt: string | null;
  hasPassword: boolean;
  password: string;
  reused: boolean;
  securityPolicy: Record<string, any> | null;
};

type ShareCreateFailure = {
  itemName: string;
  message: string;
};

type DirectLinkItem = {
  id: number;
  link_code: string;
  direct_url: string;
  file_path: string;
  file_name?: string;
  storage_type?: string;
  created_at?: string;
  expires_at?: string | null;
};

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" | "processing" | "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;
};

type RateMeasureState = {
  lastMeasureAt: number;
  lastMeasureBytes: number;
};

const nav = ref<NavKey>("files");
const authenticated = ref(false);
const user = ref<Record<string, any> | null>(null);
const uiPreviewMode = (import.meta.env.DEV || import.meta.env.VITE_UI_REVIEW === "true")
  && new URLSearchParams(window.location.search).get("ui-preview") === "files";

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 selectedFileKey = ref("");
const searchKeyword = ref("");
const shares = ref<ShareItem[]>([]);
const directLinks = ref<DirectLinkItem[]>([]);
const directLinksLoading = ref(false);
const batchSelectedKeys = ref<string[]>([]);
const batchMode = computed(() => batchSelectedKeys.value.length > 0);

const transferTasks = ref<TransferTask[]>([]);
const unreadTransferTaskIds = ref<string[]>([]);
const unreadTransferCount = computed(() => unreadTransferTaskIds.value.length);
const activeTransferTaskIds = computed(() => transferTasks.value
  .filter((task) => task.status === "queued" || isTaskRunning(task.status))
  .map((task) => task.id));
const transferBadgeCount = computed(() => new Set([
  ...activeTransferTaskIds.value,
  ...unreadTransferTaskIds.value,
]).size);
const downloadRateSamples = new Map<string, RateMeasureState>();
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.38",
  latestVersion: "",
  available: false,
  mandatory: false,
  checking: false,
  downloadUrl: "",
  packageSha256: "",
  packageSize: 0,
  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 shareCreateDialog = reactive({
  visible: false,
  loading: false,
  submitted: false,
  processedCount: 0,
  items: [] as FileItem[],
  enablePassword: false,
  password: "",
  expiryType: "never" as ShareExpiryType,
  customDays: 7,
  enableAdvancedSecurity: false,
  maxDownloadsEnabled: false,
  maxDownloads: 10,
  ipWhitelist: "",
  deviceLimit: "all" as ShareDeviceLimit,
  accessTimeEnabled: false,
  accessTimeStart: "09:00",
  accessTimeEnd: "23:00",
  error: "",
  results: [] as ShareCreateResult[],
  failures: [] as ShareCreateFailure[],
});
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,
  itemKey: "",
  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;
let fileViewRequestId = 0;

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

const currentPageTitle = computed(() => {
  const labels: Record<NavKey, string> = {
    files: "全部文件",
    transfers: "传输列表",
    shares: "我的分享",
    sync: "同步盘",
    settings: "设置",
  };
  return labels[nav.value];
});

const currentStorage = computed(() => {
  const isLocal = user.value?.current_storage_type === "local";
  const used = Number(isLocal ? user.value?.local_storage_used : user.value?.storage_used) || 0;
  const quota = Number(isLocal ? user.value?.local_storage_quota : user.value?.oss_storage_quota) || 0;
  return { used: Math.max(0, used), quota: Math.max(0, quota) };
});

const storageUsagePercent = computed(() => {
  if (currentStorage.value.quota <= 0) return 0;
  return Math.min(100, Math.max(0, (currentStorage.value.used / currentStorage.value.quota) * 100));
});

const storageUsageLabel = computed(() => {
  const { used, quota } = currentStorage.value;
  return quota > 0 ? `已用 ${formatBytes(used)} / ${formatBytes(quota)}` : `已用 ${formatBytes(used)}`;
});

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 aIsDirectory = Boolean(a.isDirectory || a.type === "directory");
    const bIsDirectory = Boolean(b.isDirectory || b.type === "directory");
    if (aIsDirectory !== bIsDirectory) return aIsDirectory ? -1 : 1;

    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 (!selectedFileKey.value) return null;
  return files.value.find((item) => fileSelectionKey(item) === selectedFileKey.value) || null;
});

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

const shareDialogIsBatch = computed(() => shareCreateDialog.items.length > 1);
const shareDialogReusedCount = computed(() => shareCreateDialog.results.filter((item) => item.reused).length);
const shareDialogTargetName = computed(() => {
  if (shareDialogIsBatch.value) return `已选择 ${shareCreateDialog.items.length} 项`;
  const item = shareCreateDialog.items[0];
  return item?.displayName || item?.name || "未选择项目";
});
const shareDialogTargetPath = computed(() => {
  const item = shareCreateDialog.items[0];
  return item ? buildItemPath(item) : "";
});

const areAllVisibleFilesSelected = computed(() => {
  return filteredFiles.value.length > 0
    && filteredFiles.value.every((item) => batchSelectedKeys.value.includes(fileSelectionKey(item)));
});

const areSomeVisibleFilesSelected = computed(() => {
  if (filteredFiles.value.length === 0 || areAllVisibleFilesSelected.value) return false;
  return filteredFiles.value.some((item) => batchSelectedKeys.value.includes(fileSelectionKey(item)));
});

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,
    createdAt: raw?.createdAt || raw?.created_at || undefined,
    owner: raw?.owner || raw?.creator_name || raw?.created_by || undefined,
    updatedBy: raw?.updatedBy || raw?.updated_by || raw?.modifier_name || undefined,
    tags: Array.isArray(raw?.tags) ? raw.tags.map(String) : undefined,
    description: typeof raw?.description === "string" ? raw.description : 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 "文件夹";
  const ext = getFileExt(item);
  return ext ? `${ext.toUpperCase()} 文件` : "文件";
}

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

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

function measureRate(
  currentBytes: number,
  state: RateMeasureState,
) {
  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;
  // Progress events can repeat the same byte offset. Keep the last valid rate
  // instead of briefly flashing a misleading 0 B/s value.
  if (deltaBytes <= 0) return "-";
  const bytesPerSecond = (deltaBytes * 1000) / elapsedMs;
  return formatSpeed(bytesPerSecond);
}

function measureDownloadTaskRate(taskId: string, currentBytes: number) {
  let state = downloadRateSamples.get(taskId);
  if (!state) {
    state = { lastMeasureAt: 0, lastMeasureBytes: 0 };
    downloadRateSamples.set(taskId, state);
  }
  return measureRate(currentBytes, state);
}

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 progressRaw = payload?.progress;
  const eventProgress = progressRaw === null || progressRaw === undefined
    ? NaN
    : Number(progressRaw);
  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 currentTask = transferTasks.value.find((task) => task.id === taskId);
  const sampledSpeed = measureDownloadTaskRate(taskId, downloadedBytes);
  const previousSpeed = currentTask?.speed?.endsWith("/s") ? currentTask.speed : "下载中";
  const patch: Partial<TransferTask> = {
    speed: sampledSpeed === "-" ? previousSpeed : sampledSpeed,
    status: "downloading",
    note: Number.isFinite(totalBytes) && totalBytes > 0
      ? `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}`
      : `已下载 ${formatBytes(downloadedBytes)}`,
  };
  if (Number.isFinite(boundedProgress)) {
    patch.progress = Number(boundedProgress.toFixed(1));
  }
  if (payload?.done) {
    patch.speed = "-";
    downloadRateSamples.delete(taskId);
  }

  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: boundedProgress >= 99.5 && !payload?.done ? "processing" : "uploading",
    speed: boundedProgress >= 99.5 && !payload?.done ? "服务器处理中" : transferSpeed,
    progress: Number(boundedProgress.toFixed(1)),
    note: boundedProgress >= 99.5 && !payload?.done ? "分片已上传完成，等待服务器处理..." : `${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 fileIconComponent(item: FileItem) {
  if (item.isDirectory || item.type === "directory") return PhFolderSimple;
  const ext = getFileExt(item);
  if (ext === "pdf") return PhFilePdf;
  if (["doc", "docx"].includes(ext)) return PhFileDoc;
  if (["xls", "xlsx", "csv"].includes(ext)) return PhFileXls;
  if (["ppt", "pptx"].includes(ext)) return PhFilePpt;
  if (["zip", "rar", "7z"].includes(ext)) return PhFileZip;
  if (["tar", "gz", "bz2", "xz"].includes(ext)) return PhFileArchive;
  if (["jpg", "jpeg", "png", "webp", "gif", "bmp", "svg", "heic"].includes(ext)) return PhFileImage;
  if (["mp4", "mkv", "mov", "avi", "webm", "flv"].includes(ext)) return PhFileVideo;
  if (["mp3", "wav", "flac", "aac", "ogg", "m4a"].includes(ext)) return PhFileAudio;
  if (["txt", "md"].includes(ext)) return PhFileText;
  return PhFile;
}

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 isTerminalTransferStatus(status: TransferTaskStatus | undefined) {
  return status === "done" || status === "failed";
}

function clearUnreadTransferTask(taskId: string) {
  unreadTransferTaskIds.value = unreadTransferTaskIds.value.filter((id) => id !== taskId);
}

function markTransferTaskUnread(taskId: string) {
  if (nav.value === "transfers" || unreadTransferTaskIds.value.includes(taskId)) return;
  unreadTransferTaskIds.value = [...unreadTransferTaskIds.value, taskId];
}

function acknowledgeTransferResults() {
  unreadTransferTaskIds.value = [];
}

function pruneUnreadTransferTasks() {
  const retainedIds = new Set(transferTasks.value.map((task) => task.id));
  unreadTransferTaskIds.value = unreadTransferTaskIds.value.filter((id) => retainedIds.has(id));
}

function prependTransferTask(task: TransferTask) {
  transferTasks.value = [task, ...transferTasks.value.slice(0, 119)];
  pruneUnreadTransferTasks();
  if (isTerminalTransferStatus(task.status)) {
    markTransferTaskUnread(task.id);
  }
}

function updateTransferTask(
  id: string,
  patch: Partial<TransferTask>,
) {
  let previousStatus: TransferTaskStatus | undefined;
  let nextStatus: TransferTaskStatus | undefined;
  transferTasks.value = transferTasks.value.map((task) => {
    if (task.id !== id) return task;
    previousStatus = task.status;
    const updated = { ...task, ...patch };
    nextStatus = updated.status;
    return updated;
  });

  if (!isTerminalTransferStatus(previousStatus) && isTerminalTransferStatus(nextStatus)) {
    markTransferTaskUnread(id);
    return;
  }
  if (isTerminalTransferStatus(previousStatus) && !isTerminalTransferStatus(nextStatus)) {
    clearUnreadTransferTask(id);
  }
}

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

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

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

function clearCompletedTransferTasks() {
  transferTasks.value
    .filter((task) => task.status === "done" || task.status === "failed")
    .forEach((task) => downloadRateSamples.delete(task.id));
  transferTasks.value = transferTasks.value.filter((task) => task.status !== "done" && task.status !== "failed");
  pruneUnreadTransferTasks();
}

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(item: FileItem) {
  return batchSelectedKeys.value.includes(fileSelectionKey(item));
}

function isFocusedFile(item: FileItem) {
  return selectedFileKey.value === fileSelectionKey(item);
}

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

function clearFocusedFile() {
  selectedFileKey.value = "";
}

function clearFileInteractionState() {
  clearFocusedFile();
  clearBatchSelection();
}

function toggleBatchSelection(item: FileItem) {
  const key = fileSelectionKey(item);
  if (!key) return;
  if (isBatchSelected(item)) {
    batchSelectedKeys.value = batchSelectedKeys.value.filter((value) => value !== key);
    return;
  }
  clearFocusedFile();
  batchSelectedKeys.value = [...batchSelectedKeys.value, key];
}

function toggleRowSelection(item: FileItem) {
  toggleBatchSelection(item);
}

function toggleSelectAllVisible() {
  const visibleKeys = filteredFiles.value.map(fileSelectionKey).filter(Boolean);
  if (areAllVisibleFilesSelected.value) {
    const visibleSet = new Set(visibleKeys);
    batchSelectedKeys.value = batchSelectedKeys.value.filter((key) => !visibleSet.has(key));
    return;
  }
  clearFocusedFile();
  batchSelectedKeys.value = [...new Set([...batchSelectedKeys.value, ...visibleKeys])];
}

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

function sanitizeSyncFolderSegment(raw: string, fallback = "默认同步盘") {
  const normalized = String(raw || "")
    .trim()
    .replace(/[\/\\:*?"<>|]/g, "_")
    .replace(/\.\./g, "_")
    .replace(/\s+/g, " ");
  const compact = normalized.replace(/^_+|_+$/g, "").trim();
  if (!compact) return fallback;
  return compact.slice(0, 60);
}

function deriveDefaultSyncRemoteBasePath() {
  const localDirName = sanitizeSyncFolderSegment(extractFileNameFromPath(syncState.localDir || ""), "");
  const owner = sanitizeSyncFolderSegment(String(user.value?.username || user.value?.id || "desktop"), "desktop");
  const folderName = localDirName || `同步盘_${owner}`;
  return normalizePath(`/同步盘/${folderName}`);
}

function applyDefaultSyncRemoteBasePath(force = false) {
  const normalizedCurrent = normalizePath(syncState.remoteBasePath || "/");
  if (!force && normalizedCurrent !== "/") return;
  syncState.remoteBasePath = deriveDefaultSyncRemoteBasePath();
}

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) {
    applyDefaultSyncRemoteBasePath(true);
    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
  }
  applyDefaultSyncRemoteBasePath(false);
}

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();
}

function normalizeSha256(raw: string | undefined) {
  const digest = String(raw || "").trim().toLowerCase();
  return /^[a-f0-9]{64}$/.test(digest) ? digest : "";
}

function normalizePackageSize(raw: unknown) {
  const value = Number(raw);
  if (!Number.isFinite(value) || value <= 0) return 0;
  return Math.floor(value);
}

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.packageSha256 = normalizeSha256(String(response.data.sha256 || ""));
    updateState.packageSize = normalizePackageSize(response.data.packageSize);
    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.packageSha256 = "";
  updateState.packageSize = 0;
  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 = `wanwan-cloud-desktop_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.progress = 100;
      updateRuntime.speed = "校验中";
      const savePath = String(response.data?.savePath || "").trim();
      updateRuntime.installerPath = savePath;
      if (savePath) {
        const expectedSha = normalizeSha256(updateState.packageSha256);
        const expectedSize = normalizePackageSize(updateState.packageSize);
        if (expectedSha || expectedSize > 0) {
          const verifyResponse = await invokeBridge("api_compute_file_sha256", {
            filePath: savePath,
          });
          if (!(verifyResponse.ok && verifyResponse.data?.success)) {
            const message = String(verifyResponse.data?.message || "校验更新包失败");
            resetUpdateRuntime();
            showToast(message, "error");
            return false;
          }
          const actualSha = normalizeSha256(String(verifyResponse.data?.sha256 || ""));
          const actualSize = normalizePackageSize(verifyResponse.data?.fileSize);
          if (expectedSize > 0 && actualSize > 0 && expectedSize !== actualSize) {
            resetUpdateRuntime();
            showToast(`更新包大小校验失败（期望 ${formatBytes(expectedSize)}，实际 ${formatBytes(actualSize)}）`, "error");
            return false;
          }
          if (expectedSha && actualSha !== expectedSha) {
            resetUpdateRuntime();
            showToast("更新包完整性校验失败，请重试下载", "error");
            return false;
          }
        }

        updateRuntime.installing = true;
        updateRuntime.progress = 100;
        updateRuntime.speed = "-";
        const launchResponse = await invokeBridge("api_silent_install_and_restart", {
          installerPath: savePath,
        });
        const logFilePath = String(launchResponse.data?.logFilePath || "").trim();
        if (launchResponse.ok && launchResponse.data?.success) {
          const launchTip = logFilePath
            ? `静默安装已启动（日志：${logFilePath}）`
            : "静默安装已启动，完成后会自动重启客户端";
          showToast(launchTip, "success");
          setTimeout(() => {
            const win = getCurrentWindow();
            void (async () => {
              try {
                await win.destroy();
              } catch {
                try {
                  await win.close();
                } catch {
                  // ignore
                }
              }
            })();
          }, 400);
          setTimeout(() => {
            if (!updateRuntime.installing) return;
            resetUpdateRuntime();
            showToast("更新程序未能接管，请重新更新或手动运行安装包", "error");
          }, 8000);
          return true;
        }
        if (logFilePath) {
          console.warn("silent updater log path:", logFilePath);
        }

        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()) {
      const hadCustomRemoteBase = normalizePath(syncState.remoteBasePath || "/") !== "/";
      syncState.localDir = result.trim();
      if (!hadCustomRemoteBase) {
        applyDefaultSyncRemoteBasePath(true);
      }
      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 ensureRemoteFolderPath(targetPath: string) {
  const normalized = normalizePath(targetPath || "/");
  if (normalized === "/") {
    return { ok: true, path: "/" };
  }

  const segments = normalized.split("/").filter(Boolean);
  let current = "/";
  for (const segment of segments) {
    const response = await invokeBridge("api_mkdir", {
      baseUrl: appConfig.baseUrl,
      path: current,
      folderName: segment,
    });
    if (!(response.ok && response.data?.success)) {
      const message = String(response.data?.message || "");
      if (!message.includes("已存在")) {
        return {
          ok: false,
          path: normalizePath(`${current}/${segment}`),
          message: message || "创建远程同步目录失败",
        };
      }
    }
    current = normalizePath(`${current}/${segment}`);
  }

  return { ok: true, path: normalized };
}

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;
  }

  applyDefaultSyncRemoteBasePath(false);
  const remoteBase = normalizePath(syncState.remoteBasePath || "/");
  const ensuredRemotePaths = new Set<string>();
  const baseEnsureResult = await ensureRemoteFolderPath(remoteBase);
  if (!baseEnsureResult.ok) {
    syncState.syncing = false;
    syncState.lastRunAt = new Date().toISOString();
    syncState.lastSummary = String(baseEnsureResult.message || "创建远程同步目录失败");
    if (trigger === "manual") {
      showToast(syncState.lastSummary, "error");
    }
    return;
  }
  ensuredRemotePaths.add(remoteBase);

  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 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 });

    if (!ensuredRemotePaths.has(targetPath)) {
      const ensureResult = await ensureRemoteFolderPath(targetPath);
      if (!ensureResult.ok) {
        syncState.failedCount += 1;
        updateTransferTask(taskId, {
          speed: "-",
          progress: 0,
          status: "failed",
          note: String(ensureResult.message || "创建远程同步目录失败"),
        });
        continue;
      }
      ensuredRemotePaths.add(targetPath);
    }

    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) {
  const requestId = ++fileViewRequestId;
  cancelInlineRename(true);
  clearFileInteractionState();
  pathState.loading = true;
  pathState.error = "";
  const normalizedPath = normalizePath(targetPath);
  const response = await invokeBridge("api_list_files", {
    baseUrl: appConfig.baseUrl,
    path: normalizedPath,
  });
  if (requestId !== fileViewRequestId) return;
  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";
  } 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 loadDirectLinks(silent = false) {
  if (!silent) directLinksLoading.value = true;
  const response = await invokeBridge("api_get_my_direct_links", {
    baseUrl: appConfig.baseUrl,
  });
  if (response.ok && response.data?.success) {
    directLinks.value = Array.isArray(response.data.links) ? response.data.links : [];
  } else if (!silent) {
    showToast(response.data?.message || "获取直链列表失败", "error");
  }
  if (!silent) directLinksLoading.value = false;
}

async function copyDirectLink(link: DirectLinkItem) {
  const url = link.direct_url || "";
  if (url) {
    await copyText(url, "直链已复制到剪贴板");
  }
}

async function deleteDirectLink(link: DirectLinkItem) {
  const response = await invokeBridge("api_delete_direct_link", {
    baseUrl: appConfig.baseUrl,
    linkId: link.id,
  });
  if (response.ok && response.data?.success) {
    showToast("直链已删除", "success");
    await loadDirectLinks(true);
  } else {
    showToast(response.data?.message || "删除直链失败", "error");
  }
}

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 "";
}

function openShareCreateDialog(items: FileItem[]) {
  const targets = items.filter(Boolean);
  if (targets.length === 0) {
    showToast("请先选择要分享的文件或文件夹", "info");
    return;
  }

  Object.assign(shareCreateDialog, {
    visible: true,
    loading: false,
    submitted: false,
    processedCount: 0,
    items: [...targets],
    enablePassword: false,
    password: "",
    expiryType: "never" as ShareExpiryType,
    customDays: 7,
    enableAdvancedSecurity: false,
    maxDownloadsEnabled: false,
    maxDownloads: 10,
    ipWhitelist: "",
    deviceLimit: "all" as ShareDeviceLimit,
    accessTimeEnabled: false,
    accessTimeStart: "09:00",
    accessTimeEnd: "23:00",
    error: "",
    results: [],
    failures: [],
  });
}

function closeShareCreateDialog(force = false) {
  if (shareCreateDialog.loading && !force) return;
  shareCreateDialog.visible = false;
  shareCreateDialog.loading = false;
  shareCreateDialog.submitted = false;
  shareCreateDialog.processedCount = 0;
  shareCreateDialog.items = [];
  shareCreateDialog.error = "";
  shareCreateDialog.results = [];
  shareCreateDialog.failures = [];
}

function createShareForItem(current: FileItem) {
  openShareCreateDialog([current]);
}

function validateShareCreateOptions(): ShareCreateOptions | null {
  shareCreateDialog.error = "";

  const password = shareCreateDialog.enablePassword ? shareCreateDialog.password.trim() : null;
  if (shareCreateDialog.enablePassword && !password) {
    shareCreateDialog.error = "已启用密码保护，请输入访问密码";
    return null;
  }
  if (password && password.length > 32) {
    shareCreateDialog.error = "访问密码不能超过 32 个字符";
    return null;
  }

  let expiryDays: number | null = null;
  if (shareCreateDialog.expiryType !== "never") {
    const rawDays = shareCreateDialog.expiryType === "custom"
      ? Number(shareCreateDialog.customDays)
      : Number(shareCreateDialog.expiryType);
    if (!Number.isInteger(rawDays) || rawDays < 1 || rawDays > 365) {
      shareCreateDialog.error = "有效期必须是 1 到 365 天的整数";
      return null;
    }
    expiryDays = rawDays;
  }

  let maxDownloads: number | null = null;
  let ipWhitelist: string | null = null;
  let deviceLimit: ShareDeviceLimit = "all";
  let accessTimeStart: string | null = null;
  let accessTimeEnd: string | null = null;

  if (shareCreateDialog.enableAdvancedSecurity) {
    if (shareCreateDialog.maxDownloadsEnabled) {
      const value = Number(shareCreateDialog.maxDownloads);
      if (!Number.isInteger(value) || value < 1 || value > 1_000_000) {
        shareCreateDialog.error = "下载次数上限需为 1 到 1000000 的整数";
        return null;
      }
      maxDownloads = value;
    }

    ipWhitelist = shareCreateDialog.ipWhitelist.trim() || null;
    if (!["all", "mobile", "desktop"].includes(shareCreateDialog.deviceLimit)) {
      shareCreateDialog.error = "设备限制参数无效";
      return null;
    }
    deviceLimit = shareCreateDialog.deviceLimit;

    if (shareCreateDialog.accessTimeEnabled) {
      const start = shareCreateDialog.accessTimeStart.trim();
      const end = shareCreateDialog.accessTimeEnd.trim();
      const timePattern = /^([01]\d|2[0-3]):([0-5]\d)$/;
      if (!timePattern.test(start) || !timePattern.test(end)) {
        shareCreateDialog.error = "访问时段格式必须为 HH:mm";
        return null;
      }
      accessTimeStart = start;
      accessTimeEnd = end;
    }
  }

  return {
    password,
    expiryDays,
    maxDownloads,
    ipWhitelist,
    deviceLimit,
    accessTimeStart,
    accessTimeEnd,
  };
}

async function requestShareCreation(current: FileItem, options: ShareCreateOptions): Promise<BridgeResponse> {
  if (uiPreviewMode) {
    await new Promise((resolve) => setTimeout(resolve, 180));
    const shareCode = Math.random().toString(36).slice(2, 8).toUpperCase();
    return {
      ok: true,
      status: 200,
      data: {
        success: true,
        reused: false,
        share_code: shareCode,
        share_url: `${appConfig.baseUrl}/s/${shareCode}`,
        expires_at: options.expiryDays
          ? new Date(Date.now() + options.expiryDays * 86_400_000).toISOString()
          : null,
        has_password: Boolean(options.password),
        security_policy: {
          max_downloads: options.maxDownloads,
          ip_whitelist_count: options.ipWhitelist ? options.ipWhitelist.split(/[\s,]+/).filter(Boolean).length : 0,
          device_limit: options.deviceLimit,
          access_time_start: options.accessTimeStart,
          access_time_end: options.accessTimeEnd,
        },
      },
    };
  }

  return invokeBridge("api_create_share", {
    baseUrl: appConfig.baseUrl,
    shareType: current.isDirectory || current.type === "directory" ? "directory" : "file",
    filePath: buildItemPath(current),
    fileName: current.displayName || current.name,
    password: options.password,
    expiryDays: options.expiryDays,
    maxDownloads: options.maxDownloads,
    ipWhitelist: options.ipWhitelist,
    deviceLimit: options.deviceLimit,
    accessTimeStart: options.accessTimeStart,
    accessTimeEnd: options.accessTimeEnd,
  });
}

async function submitShareCreateDialog() {
  if (shareCreateDialog.loading) return;
  const options = validateShareCreateOptions();
  if (!options) return;

  const targets = [...shareCreateDialog.items];
  if (targets.length === 0) {
    shareCreateDialog.error = "没有可分享的项目";
    return;
  }

  shareCreateDialog.loading = true;
  shareCreateDialog.submitted = false;
  shareCreateDialog.processedCount = 0;
  shareCreateDialog.results = [];
  shareCreateDialog.failures = [];

  const results: ShareCreateResult[] = [];
  const failures: ShareCreateFailure[] = [];
  for (const current of targets) {
    const itemName = current.displayName || current.name || "未命名项目";
    try {
      const response = await requestShareCreation(current, options);
      if (!response.ok || !response.data?.success) {
        throw new Error(String(response.data?.message || "创建分享失败"));
      }

      results.push({
        itemName,
        shareUrl: String(response.data.share_url || ""),
        shareCode: String(response.data.share_code || ""),
        expiresAt: response.data.expires_at ? String(response.data.expires_at) : null,
        hasPassword: Boolean(response.data.has_password),
        password: response.data.reused ? "" : (options.password || ""),
        reused: Boolean(response.data.reused),
        securityPolicy: response.data.security_policy && typeof response.data.security_policy === "object"
          ? response.data.security_policy
          : null,
      });
    } catch (error) {
      failures.push({
        itemName,
        message: error instanceof Error ? error.message : String(error),
      });
    } finally {
      shareCreateDialog.processedCount += 1;
    }
  }

  shareCreateDialog.results = results;
  shareCreateDialog.failures = failures;
  shareCreateDialog.submitted = true;
  shareCreateDialog.loading = false;

  if (results.length > 0) {
    if (!uiPreviewMode) await loadShares(true);
    if (targets.length > 1) clearBatchSelection();
    if (targets.length === 1 && results[0].reused) {
      showToast("已复用现有分享，原安全设置保持不变", "info");
    } else {
      const failedText = failures.length > 0 ? `，失败 ${failures.length} 项` : "";
      showToast(`分享创建完成：成功 ${results.length} 项${failedText}`, failures.length > 0 ? "info" : "success");
    }
    return;
  }

  showToast(failures[0]?.message || "创建分享失败", "error");
}

function returnToShareSettings() {
  if (shareCreateDialog.loading) return;
  shareCreateDialog.submitted = false;
  shareCreateDialog.processedCount = 0;
  shareCreateDialog.error = "";
  shareCreateDialog.results = [];
  shareCreateDialog.failures = [];
}

function getShareDeviceLabel(value: unknown) {
  if (value === "mobile") return "仅移动端";
  if (value === "desktop") return "仅桌面端";
  return "全部设备";
}

async function copyCreatedShareLink(result: ShareCreateResult) {
  if (!result.shareUrl) {
    showToast("分享链接为空", "error");
    return;
  }
  await copyText(result.shareUrl, "分享链接已复制");
}

async function openCreatedShareLink(result: ShareCreateResult) {
  if (!result.shareUrl) {
    showToast("分享链接为空", "error");
    return;
  }
  if (uiPreviewMode) {
    window.open(result.shareUrl, "_blank", "noopener,noreferrer");
    return;
  }
  try {
    await openUrl(result.shareUrl);
  } catch {
    showToast("打开分享链接失败", "error");
  }
}

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 (uiPreviewMode) {
    pathState.mode = keyword ? "search" : "directory";
    clearFileInteractionState();
    if (keyword) showToast(`已筛选与“${keyword}”相关的文件`, "success");
    return;
  }
  if (!keyword) {
    await loadFiles(pathState.currentPath);
    return;
  }
  const requestId = ++fileViewRequestId;
  clearFileInteractionState();
  pathState.loading = true;
  pathState.error = "";
  const response = await invokeBridge("api_search_files", {
    baseUrl: appConfig.baseUrl,
    path: pathState.currentPath,
    keyword,
    searchType: "all",
    limit: 200,
  });
  if (requestId !== fileViewRequestId) return;

  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";
    showToast(`搜索完成，共 ${files.value.length} 条结果`, "success");
  } else {
    pathState.error = response.data?.message || "搜索失败";
    showToast(pathState.error, "error");
  }
  pathState.loading = false;
}

async function resetFileSearch() {
  searchKeyword.value = "";
  pathState.mode = "directory";
  if (uiPreviewMode) {
    clearFileInteractionState();
    return;
  }
  await loadFiles(pathState.currentPath);
}

async function refreshCurrentFiles() {
  if (uiPreviewMode) {
    showToast("文件列表已刷新", "success");
    return;
  }
  await loadFiles(pathState.currentPath);
}

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 = [];
  clearFileInteractionState();
  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.itemKey === fileSelectionKey(item);
}

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.itemKey = "";
  inlineRename.originalName = "";
  inlineRename.value = "";
  inlineRename.saving = false;
}

function startInlineRename(target?: FileItem | null) {
  const current = target || selectedFile.value;
  if (!current) {
    showToast("请先选中文件或文件夹", "info");
    return;
  }
  selectedFileKey.value = fileSelectionKey(current);
  inlineRename.active = true;
  inlineRename.itemKey = fileSelectionKey(current);
  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) => fileSelectionKey(item) === inlineRename.itemKey) || 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) {
    const renamedPath = normalizePath(`${getItemParentPath(current)}/${nextName}`);
    showToast("重命名成功", "success");
    cancelInlineRename(true);
    await loadFiles(pathState.currentPath);
    selectedFileKey.value = renamedPath;
    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()}`;
  downloadRateSamples.delete(taskId);
  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) {
    downloadRateSamples.delete(taskId);
    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 || "原生下载失败");
  downloadRateSamples.delete(taskId);
  updateTransferTask(taskId, { speed: "-", progress: 0, status: "failed", note: message });
  showToast(message, "error");
}

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

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();
  downloadRateSamples.delete(taskId);
  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) {
    downloadRateSamples.delete(taskId);
    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 || "重试下载失败"),
  });
  downloadRateSamples.delete(taskId);
}

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;
  }
}

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

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);
  clearBatchSelection();
  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 (shareCreateDialog.visible) {
      closeShareCreateDialog();
      return;
    }
    if (operationConfirmDialog.visible) {
      closeOperationConfirmDialog();
      return;
    }
    if (inlineRename.active) {
      cancelInlineRename();
      return;
    }
    if (contextMenu.visible) {
      closeContextMenu();
      return;
    }
    if (batchMode.value) {
      clearBatchSelection();
      return;
    }
    if (selectedFile.value) clearFocusedFile();
    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 chooseUploadFiles() {
  if (uiPreviewMode) {
    showToast("桌面客户端中可选择并上传多个文件", "info");
    return;
  }
  try {
    const result = await openDialog({
      directory: false,
      multiple: true,
      title: "选择要上传的文件",
    });
    const paths = Array.isArray(result) ? result : (typeof result === "string" ? [result] : []);
    await uploadDroppedFiles(paths);
  } catch {
    showToast("选择文件失败", "error");
  }
}

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 === "transfers") {
    acknowledgeTransferResults();
  }
  if (next !== "files" && inlineRename.active) {
    cancelInlineRename(true);
  }
  if (next !== "files") {
    fileViewRequestId += 1;
    pathState.loading = false;
    clearFileInteractionState();
  }
  if (uiPreviewMode) return;
  if (next === "shares" && authenticated.value) {
    await loadShares();
    await loadDirectLinks();
    return;
  }
  if (next === "settings" && authenticated.value) {
    await checkClientUpdate(false);
    await loadOnlineDevices(true);
  }
});

watch(
  () => [fileViewState.filter, searchKeyword.value],
  () => clearFileInteractionState(),
);

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();
  },
);

function applyUiPreviewData() {
  authenticated.value = true;
  nav.value = "files";
  user.value = {
    id: 1,
    username: "张小明",
    current_storage_type: "local",
    local_storage_used: 42.36 * 1024 * 1024 * 1024,
    local_storage_quota: 200 * 1024 * 1024 * 1024,
  };
  pathState.currentPath = "/项目资料";
  pathState.mode = "directory";
  files.value = [
    { name: "产品资料", displayName: "产品资料", type: "directory", isDirectory: true, modifiedAt: "2026-07-20 18:32" },
    { name: "项目归档", displayName: "项目归档", type: "directory", isDirectory: true, modifiedAt: "2026-07-15 10:21" },
    { name: "设计素材", displayName: "设计素材", type: "directory", isDirectory: true, modifiedAt: "2026-07-10 09:44" },
    {
      name: "需求评审.pdf",
      displayName: "需求评审.pdf",
      type: "file",
      size: 2.45 * 1024 * 1024,
      modifiedAt: "2026-07-21 14:28",
      createdAt: "2026-07-21 14:28",
      owner: "张小明",
      updatedBy: "张小明",
      tags: ["项目评审"],
    },
    { name: "功能清单.docx", displayName: "功能清单.docx", type: "file", size: 1.28 * 1024 * 1024, modifiedAt: "2026-07-19 16:03" },
    { name: "项目排期表.xlsx", displayName: "项目排期表.xlsx", type: "file", size: 98.64 * 1024, modifiedAt: "2026-07-18 11:22" },
    { name: "客户交付.zip", displayName: "客户交付.zip", type: "file", size: 156.35 * 1024 * 1024, modifiedAt: "2026-07-16 17:45" },
    { name: "使用说明.txt", displayName: "使用说明.txt", type: "file", size: 3.21 * 1024, modifiedAt: "2026-07-12 08:53" },
  ];
  const previewTransferId = "D-PREVIEW-001";
  transferTasks.value = [{
    id: previewTransferId,
    kind: "download",
    name: "需求评审.pdf",
    speed: "-",
    progress: 100,
    status: "done",
    note: "下载成功",
  }];
  unreadTransferTaskIds.value = [previewTransferId];
  clearFileInteractionState();
}

onMounted(async () => {
  window.addEventListener("click", handleGlobalClick);
  window.addEventListener("keydown", handleGlobalKey);
  window.addEventListener("contextmenu", handleGlobalContextMenu);
  if (uiPreviewMode) {
    applyUiPreviewData();
    return;
  }
  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="login-brand-mark">
          <img :src="dogLogo" alt="玩玩云" />
          <span>玩玩云 Desktop</span>
        </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">
          <img class="app-mark" :src="dogLogo" alt="玩玩云" />
          <div>
            <strong>玩玩云</strong>
            <span>Desktop</span>
          </div>
        </div>

        <nav class="nav-list" aria-label="主导航">
          <button
            v-for="item in navItems"
            :key="item.key"
            class="nav-btn"
            :class="{ active: nav === item.key }"
            :title="item.hint"
            @click="nav = item.key"
          >
            <component :is="item.icon" class="nav-icon" :size="21" weight="regular" />
            <span>{{ item.label }}</span>
            <span
              v-if="item.key === 'transfers' && transferBadgeCount"
              class="nav-count"
              :aria-label="`${transferBadgeCount} 个待关注传输任务`"
            >{{ transferBadgeCount }}</span>
            <span v-else-if="item.key === 'settings' && updateState.available" class="nav-dot" aria-label="发现新版本"></span>
          </button>
        </nav>

        <div class="user-card">
          <div class="user-card-main">
            <img class="avatar" :src="dogLogo" alt="用户头像" />
            <div class="meta">
              <strong>{{ user?.username || "-" }}</strong>
              <span>{{ user?.current_storage_type === "local" ? "本地存储" : "OSS 存储" }}</span>
            </div>
            <button class="logout-btn" title="退出登录" aria-label="退出登录" @click="handleLogout">
              <PhSignOut :size="18" />
            </button>
          </div>
          <div class="storage-meta">
            <span>{{ storageUsageLabel }}</span>
            <span>{{ Math.round(storageUsagePercent) }}%</span>
          </div>
          <div class="storage-track" aria-label="存储空间使用率">
            <span :style="{ width: `${storageUsagePercent}%` }"></span>
          </div>
          <div class="storage-footer">
            <span>{{ user?.current_storage_type === "local" ? "本地空间" : "云存储" }}</span>
            <button @click="nav = 'settings'">管理空间</button>
          </div>
        </div>
      </aside>

      <section class="work-area">
        <header class="top-tools">
          <div class="topbar-main">
            <div class="page-title-block">
              <h1>{{ currentPageTitle }}</h1>
              <p>{{ nav === "files" ? `${filteredFiles.length} 个项目` : toolbarCrumbs[toolbarCrumbs.length - 1]?.label }}</p>
            </div>
            <div v-if="nav === 'files'" class="search-box">
              <PhMagnifyingGlass :size="19" />
              <input
                v-model="searchKeyword"
                type="text"
                placeholder="搜索文件或文件夹"
                aria-label="搜索文件或文件夹"
                @keyup.enter="runGlobalSearch"
              />
              <button v-if="searchKeyword" title="清空搜索" aria-label="清空搜索" @click="resetFileSearch">
                <PhX :size="16" />
              </button>
              <span v-else>Enter</span>
            </div>
          </div>

          <div class="crumbs">
            <template v-for="(crumb, idx) in toolbarCrumbs" :key="`${crumb.path}-${idx}`">
              <button
                class="crumb-btn"
                :class="{ current: idx === toolbarCrumbs.length - 1 }"
                @click="nav === 'files' ? jumpToPath(crumb.path) : null"
              >
                {{ crumb.label }}
              </button>
              <PhCaretRight v-if="idx < toolbarCrumbs.length - 1" class="crumb-separator" :size="13" />
            </template>
          </div>

          <div class="tool-right" :class="{ 'tool-right-files': nav === 'files' }">
            <template v-if="nav === 'files'">
              <div class="file-actions-row">
                <template v-if="batchMode">
                  <span class="batch-selection-summary">
                    <PhSelectionAll :size="18" />
                    已选 <strong>{{ batchSelectedItems.length }}</strong> 项
                  </span>
                  <button class="action-btn danger" @click="batchDeleteSelected">批量删除</button>
                  <button class="action-btn" @click="batchShareSelected">批量分享</button>
                  <button class="action-btn" @click="batchDirectLinkSelected">批量直链</button>
                  <button class="action-btn" @click="clearBatchSelection">取消选择</button>
                </template>
                <template v-else>
                  <button class="solid-btn" @click="chooseUploadFiles">
                    <PhCloudArrowUp :size="19" />
                    上传
                  </button>
                  <button class="action-btn" @click="createFolder">
                    <PhFolderSimplePlus :size="18" />
                    新建文件夹
                  </button>
                  <button class="action-btn" aria-label="下载当前查看文件" :disabled="!selectedFile || selectedFile.isDirectory" @click="downloadSelected()">
                    <PhDownloadSimple :size="18" />
                    下载
                  </button>
                  <button class="action-btn" aria-label="分享当前查看项目" :disabled="!selectedFile" @click="selectedFile && createShareForItem(selectedFile)">
                    <PhShareNetwork :size="18" />
                    分享
                  </button>
                  <button class="action-btn danger subtle" aria-label="删除当前查看项目" :disabled="!selectedFile" @click="deleteSelected()">
                    <PhTrash :size="18" />
                    删除
                  </button>
                </template>
                <span class="toolbar-spacer"></span>
                <select v-model="fileViewState.filter" class="compact-select" title="文件类型筛选" aria-label="文件类型筛选">
                  <option v-for="opt in fileTypeFilterOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
                </select>
                <select v-model="fileViewState.sortBy" class="compact-select" title="排序字段" aria-label="排序字段">
                  <option v-for="opt in fileSortOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
                </select>
                <button class="icon-btn" :title="fileViewState.sortOrder === 'asc' ? '当前升序' : '当前降序'" @click="toggleFileSortOrder">
                  <component :is="fileViewState.sortOrder === 'asc' ? PhSortAscending : PhSortDescending" :size="18" />
                </button>
                <button class="icon-btn" title="刷新" aria-label="刷新" @click="refreshCurrentFiles">
                  <PhArrowClockwise :size="18" />
                </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(); loadDirectLinks()">刷新分享</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">
          <section class="panel content-panel">
            <template v-if="nav === 'files'">
              <div class="file-drop-surface" :class="{ active: dropState.active }">
                <div class="file-table-head" role="row">
                  <div class="selection-cell">
                    <button
                      class="table-checkbox"
                      :class="{ active: areAllVisibleFilesSelected || areSomeVisibleFilesSelected, mixed: areSomeVisibleFilesSelected }"
                      role="checkbox"
                      :aria-checked="areAllVisibleFilesSelected ? 'true' : (areSomeVisibleFilesSelected ? 'mixed' : 'false')"
                      :title="areAllVisibleFilesSelected ? '取消全选当前列表' : '全选当前列表'"
                      :aria-label="areAllVisibleFilesSelected ? '取消全选当前列表' : '全选当前列表'"
                      @click="toggleSelectAllVisible"
                    >
                      <PhCheck v-if="areAllVisibleFilesSelected" :size="12" weight="bold" />
                      <PhMinus v-else-if="areSomeVisibleFilesSelected" :size="12" weight="bold" />
                    </button>
                  </div>
                  <button class="column-sort name-column" @click="fileViewState.sortBy = 'name'; toggleFileSortOrder()">
                    文件名
                    <PhCaretDown :size="13" />
                  </button>
                  <button class="column-sort size-column" @click="fileViewState.sortBy = 'size'; toggleFileSortOrder()">大小</button>
                  <button class="column-sort time-column" @click="fileViewState.sortBy = 'modifiedAt'; toggleFileSortOrder()">
                    修改时间
                    <PhCaretDown :size="13" />
                  </button>
                  <div class="row-menu-cell"><PhInfo :size="17" /></div>
                </div>

                <div v-if="pathState.loading" class="file-state">
                  <span class="state-icon loading"><PhArrowsClockwise :size="28" /></span>
                  <strong>正在加载目录</strong>
                  <p>文件列表马上就好</p>
                </div>
                <div v-else-if="pathState.error" class="file-state error">
                  <span class="state-icon"><PhX :size="28" /></span>
                  <strong>目录加载失败</strong>
                  <p>{{ pathState.error }}</p>
                  <button class="action-btn" @click="loadFiles(pathState.currentPath)">重新加载</button>
                </div>
                <div v-else-if="filteredFiles.length === 0" class="file-state">
                  <span class="state-icon"><PhFolderSimple :size="30" /></span>
                  <strong>当前目录暂无文件</strong>
                  <p>上传文件或新建文件夹开始整理</p>
                  <button class="solid-btn" @click="chooseUploadFiles"><PhUploadSimple :size="18" />上传文件</button>
                </div>
                <div v-else class="file-table-body" role="rowgroup" @click.self="clearFocusedFile">
                  <div
                    v-for="item in filteredFiles"
                    :key="fileSelectionKey(item)"
                    class="file-row"
                    :class="{ selected: isFocusedFile(item), batchSelected: isBatchSelected(item), renaming: isInlineRenaming(item) }"
                    role="row"
                    :aria-selected="isBatchSelected(item)"
                    :aria-current="isFocusedFile(item) ? 'true' : undefined"
                    tabindex="0"
                    @click="handleFileCardClick(item)"
                    @dblclick="handleFileCardDoubleClick(item)"
                    @keydown.enter.prevent="handleFileCardDoubleClick(item)"
                    @keydown.space.prevent="toggleRowSelection(item)"
                    @contextmenu.prevent="openContextMenu($event, item)"
                  >
                    <div class="selection-cell">
                      <button
                        class="table-checkbox"
                        :class="{ active: isBatchSelected(item) }"
                        role="checkbox"
                        :aria-checked="isBatchSelected(item) ? 'true' : 'false'"
                        :aria-label="`${isBatchSelected(item) ? '取消选择' : '选择'} ${item.displayName || item.name}`"
                        @click.stop="toggleRowSelection(item)"
                      >
                        <PhCheck v-if="isBatchSelected(item)" :size="12" weight="bold" />
                      </button>
                    </div>
                    <div class="file-primary" role="cell">
                      <span class="file-icon" :class="[`kind-${fileVisualKind(item)}`, `ext-${getFileExt(item) || 'file'}`]">
                        <component :is="fileIconComponent(item)" :size="24" :weight="item.isDirectory ? 'fill' : 'duotone'" />
                      </span>
                      <div class="file-name-wrap">
                        <input
                          v-if="isInlineRenaming(item)"
                          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="submitInlineRename(item)"
                        />
                        <template v-else>
                          <strong :title="item.displayName || item.name">{{ item.displayName || item.name }}</strong>
                        </template>
                      </div>
                    </div>
                    <span class="file-size" role="cell">{{ item.isDirectory || item.type === "directory" ? "—" : (item.sizeFormatted || formatBytes(item.size)) }}</span>
                    <span class="file-time" role="cell">{{ formatDate(item.modifiedAt) }}</span>
                    <button class="row-more" title="更多操作" :aria-label="`更多操作 ${item.displayName || item.name}`" @click.stop="openContextMenu($event, item)">
                      <PhDotsThreeVertical :size="20" weight="bold" />
                    </button>
                  </div>
                </div>
                <div v-if="!pathState.loading && !pathState.error && filteredFiles.length" class="file-table-footer">
                  <span>{{ pathState.mode === "search" ? `搜索到 ${filteredFiles.length} 个结果` : `共 ${filteredFiles.length} 项` }}</span>
                  <span>双击打开 · 右键查看更多操作</span>
                </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 class="shares-section">
                <div class="section-label">分享链接</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>
              </div>

              <div class="shares-section" style="margin-top: 20px;">
                <div class="section-label">直链列表</div>
                <div v-if="directLinksLoading" class="empty-tip">正在加载直链列表...</div>
                <div v-else-if="directLinks.length === 0" class="empty-tip">暂无直链记录</div>
                <div v-else class="share-list">
                  <div v-for="link in directLinks" :key="link.id" class="share-item direct-link-item">
                    <div class="share-main">
                      <div class="share-title">
                        <strong :title="link.file_path">{{ link.file_name || link.file_path.split('/').pop() || '未命名' }}</strong>
                        <span class="share-badge direct-badge">直链</span>
                      </div>
                      <div class="share-link direct-link-url" :title="link.direct_url">{{ link.direct_url }}</div>
                      <div class="share-meta">
                        <span>到期 {{ getShareExpireLabel(link.expires_at) }}</span>
                      </div>
                    </div>
                    <div class="share-actions">
                      <button class="action-btn" @click="copyDirectLink(link)">复制直链</button>
                      <button class="action-btn danger" @click="deleteDirectLink(link)">删除</button>
                    </div>
                  </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 v-if="nav !== 'files'">详情面板</h3>
            <template v-if="nav === 'files'">
              <div class="detail-head">
                <h3>详情信息</h3>
                <button v-if="selectedFile || batchMode" title="关闭详情或选择" aria-label="关闭详情或选择" @click="clearFileInteractionState">
                  <PhX :size="18" />
                </button>
              </div>

              <div v-if="batchSelectedItems.length" class="batch-detail">
                <span class="detail-file-icon batch"><PhSelectionAll :size="34" /></span>
                <strong>已选择 {{ batchSelectedItems.length }} 项</strong>
                <p>可以在顶部工具栏执行批量删除、分享或生成直链。</p>
              </div>

              <div v-else-if="selectedFile" class="file-detail-content">
                <span class="detail-file-icon" :class="[`kind-${fileVisualKind(selectedFile)}`, `ext-${getFileExt(selectedFile) || 'file'}`]">
                  <component :is="fileIconComponent(selectedFile)" :size="52" :weight="selectedFile.isDirectory ? 'fill' : 'duotone'" />
                </span>
                <h4 :title="selectedFile.displayName || selectedFile.name">{{ selectedFile.displayName || selectedFile.name }}</h4>
                <p class="detail-file-size">{{ selectedFile.isDirectory ? "文件夹" : (selectedFile.sizeFormatted || formatBytes(selectedFile.size)) }}</p>

                <dl class="detail-list">
                  <div>
                    <dt>类型</dt>
                    <dd>{{ fileTypeLabel(selectedFile) }}</dd>
                  </div>
                  <div>
                    <dt>位置</dt>
                    <dd :title="buildItemPath(selectedFile)">{{ buildItemPath(selectedFile) }}</dd>
                  </div>
                  <div>
                    <dt>修改时间</dt>
                    <dd>{{ formatDate(selectedFile.modifiedAt) }}</dd>
                  </div>
                  <div>
                    <dt>文件格式</dt>
                    <dd>{{ selectedFile.isDirectory ? "目录" : fileExtLabel(selectedFile) }}</dd>
                  </div>
                  <div>
                    <dt>创建时间</dt>
                    <dd>{{ formatDate(selectedFile.createdAt || selectedFile.modifiedAt) }}</dd>
                  </div>
                  <div>
                    <dt>创建者</dt>
                    <dd>{{ selectedFile.owner || user?.display_name || user?.username || "当前用户" }}</dd>
                  </div>
                  <div>
                    <dt>最近更新者</dt>
                    <dd>{{ selectedFile.updatedBy || selectedFile.owner || user?.display_name || user?.username || "当前用户" }}</dd>
                  </div>
                </dl>

                <div class="detail-extra">
                  <div class="detail-extra-title"><span>标签</span></div>
                  <div class="detail-tags">
                    <span v-for="tag in selectedFile.tags || []" :key="tag">{{ tag }}</span>
                    <button @click="showToast('标签编辑将在后续版本开放', 'info')"><PhPlus :size="14" />添加标签</button>
                  </div>
                  <div class="detail-description">
                    <span>描述</span>
                    <button @click="showToast('描述编辑将在后续版本开放', 'info')"><PhPencilSimple :size="14" />{{ selectedFile.description || "添加描述..." }}</button>
                  </div>
                </div>

                <div class="detail-actions">
                  <button class="action-btn" aria-label="下载详情中的文件" :disabled="selectedFile.isDirectory" @click="downloadSelected(selectedFile)">
                    <PhDownloadSimple :size="17" />下载
                  </button>
                  <button class="action-btn" aria-label="分享详情中的项目" @click="createShareForItem(selectedFile)">
                    <PhShareNetwork :size="17" />分享
                  </button>
                </div>
              </div>

              <div v-else class="detail-empty">
                <span><PhInfo :size="26" /></span>
                <strong>选择一个项目</strong>
                <p>查看文件类型、位置、大小和修改时间</p>
              </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="shareCreateDialog.visible" class="share-dialog-mask" @click.self="closeShareCreateDialog()">
      <section class="share-create-card" role="dialog" aria-modal="true" aria-labelledby="share-create-title" @click.stop>
        <header class="share-dialog-header">
          <div>
            <span class="share-dialog-eyebrow">安全分享</span>
            <h3 id="share-create-title">{{ shareDialogIsBatch ? "批量创建分享" : "创建分享链接" }}</h3>
          </div>
          <button class="share-dialog-close" :disabled="shareCreateDialog.loading" aria-label="关闭分享设置" @click="closeShareCreateDialog()">
            <PhX :size="18" />
          </button>
        </header>

        <template v-if="!shareCreateDialog.submitted">
          <div class="share-target-card">
            <span class="share-target-icon">
              <PhSelectionAll v-if="shareDialogIsBatch" :size="22" />
              <PhFolderSimple v-else-if="shareCreateDialog.items[0]?.isDirectory" :size="22" weight="duotone" />
              <PhFile v-else :size="22" weight="duotone" />
            </span>
            <div>
              <strong :title="shareDialogTargetName">{{ shareDialogTargetName }}</strong>
              <span>{{ shareDialogIsBatch ? "以下设置将应用到全部选中项" : shareDialogTargetPath }}</span>
            </div>
          </div>

          <div class="share-settings-scroll">
            <section class="share-setting-section">
              <div class="share-section-heading">
                <div>
                  <strong>访问设置</strong>
                  <span>设置链接有效期和访问密码</span>
                </div>
              </div>

              <div class="share-field-grid">
                <label class="share-field">
                  <span>有效期</span>
                  <select v-model="shareCreateDialog.expiryType">
                    <option value="never">永久有效</option>
                    <option value="7">7 天</option>
                    <option value="30">30 天</option>
                    <option value="custom">自定义</option>
                  </select>
                </label>
                <label v-if="shareCreateDialog.expiryType === 'custom'" class="share-field">
                  <span>自定义天数</span>
                  <input v-model.number="shareCreateDialog.customDays" type="number" min="1" max="365" inputmode="numeric" />
                </label>
              </div>

              <label class="share-switch-row">
                <span>
                  <strong>密码保护</strong>
                  <small>访问链接时必须输入密码</small>
                </span>
                <input v-model="shareCreateDialog.enablePassword" type="checkbox" />
              </label>
              <label v-if="shareCreateDialog.enablePassword" class="share-field share-password-field">
                <span>访问密码</span>
                <input
                  v-model="shareCreateDialog.password"
                  type="password"
                  maxlength="32"
                  autocomplete="new-password"
                  placeholder="请输入访问密码（最多 32 个字符）"
                />
                <small>建议通过其他安全渠道单独发送密码。</small>
              </label>
            </section>

            <section class="share-setting-section advanced">
              <label class="share-switch-row advanced-toggle">
                <span>
                  <strong>高级安全策略</strong>
                  <small>限制下载次数、IP、设备和访问时段</small>
                </span>
                <input v-model="shareCreateDialog.enableAdvancedSecurity" type="checkbox" />
              </label>

              <div v-if="shareCreateDialog.enableAdvancedSecurity" class="share-advanced-fields">
                <label class="share-switch-row compact">
                  <span>
                    <strong>限制下载次数</strong>
                    <small>达到上限后停止下载</small>
                  </span>
                  <input v-model="shareCreateDialog.maxDownloadsEnabled" type="checkbox" />
                </label>
                <label v-if="shareCreateDialog.maxDownloadsEnabled" class="share-field">
                  <span>下载次数上限</span>
                  <input v-model.number="shareCreateDialog.maxDownloads" type="number" min="1" max="1000000" inputmode="numeric" />
                </label>

                <label class="share-field">
                  <span>IP 白名单 <small>可选</small></span>
                  <textarea
                    v-model="shareCreateDialog.ipWhitelist"
                    rows="2"
                    spellcheck="false"
                    placeholder="支持逗号或空格分隔，例如：1.2.3.4, 5.6.7.*"
                  ></textarea>
                </label>

                <label class="share-field">
                  <span>设备限制</span>
                  <select v-model="shareCreateDialog.deviceLimit">
                    <option value="all">全部设备</option>
                    <option value="mobile">仅移动端</option>
                    <option value="desktop">仅桌面端</option>
                  </select>
                </label>

                <label class="share-switch-row compact">
                  <span>
                    <strong>限制访问时段</strong>
                    <small>支持跨天时段，例如 22:00 到 06:00</small>
                  </span>
                  <input v-model="shareCreateDialog.accessTimeEnabled" type="checkbox" />
                </label>
                <div v-if="shareCreateDialog.accessTimeEnabled" class="share-time-grid">
                  <label class="share-field">
                    <span>开始时间</span>
                    <input v-model="shareCreateDialog.accessTimeStart" type="time" />
                  </label>
                  <label class="share-field">
                    <span>结束时间</span>
                    <input v-model="shareCreateDialog.accessTimeEnd" type="time" />
                  </label>
                </div>
              </div>
            </section>
          </div>

          <div v-if="shareCreateDialog.error" class="share-dialog-error" role="alert">{{ shareCreateDialog.error }}</div>

          <footer class="share-dialog-actions">
            <button class="action-btn" :disabled="shareCreateDialog.loading" @click="closeShareCreateDialog()">取消</button>
            <button class="solid-btn" :disabled="shareCreateDialog.loading" @click="submitShareCreateDialog()">
              <template v-if="shareCreateDialog.loading">
                正在创建 {{ shareCreateDialog.processedCount }}/{{ shareCreateDialog.items.length }}
              </template>
              <template v-else>{{ shareDialogIsBatch ? `创建 ${shareCreateDialog.items.length} 个分享` : "创建分享" }}</template>
            </button>
          </footer>
        </template>

        <template v-else>
          <div class="share-result-summary" :class="{ partial: shareCreateDialog.failures.length > 0 }">
            <span><PhCheck :size="24" weight="bold" /></span>
            <div>
              <strong>{{ shareCreateDialog.results.length > 0 ? "分享创建完成" : "分享创建失败" }}</strong>
              <small>
                成功 {{ shareCreateDialog.results.length }} 项
                <template v-if="shareDialogReusedCount">，复用已有 {{ shareDialogReusedCount }} 项</template>
                <template v-if="shareCreateDialog.failures.length">，失败 {{ shareCreateDialog.failures.length }} 项</template>
              </small>
            </div>
          </div>

          <div v-if="shareCreateDialog.failures.length" class="share-failure-list" role="alert">
            <div v-for="failure in shareCreateDialog.failures" :key="`${failure.itemName}-${failure.message}`">
              <strong>{{ failure.itemName }}</strong>
              <span>{{ failure.message }}</span>
            </div>
          </div>

          <div v-if="shareCreateDialog.results.length" class="share-created-list">
            <article v-for="result in shareCreateDialog.results" :key="`${result.itemName}-${result.shareCode}`" class="share-created-item">
              <div class="share-created-head">
                <strong :title="result.itemName">{{ result.itemName }}</strong>
                <span v-if="result.reused" class="share-result-badge reused">已复用</span>
                <span v-else class="share-result-badge">新建</span>
              </div>
              <div class="share-created-url" :title="result.shareUrl">{{ result.shareUrl || "未返回分享链接" }}</div>
              <div class="share-created-meta">
                <span>{{ result.hasPassword ? "密码保护" : "公开访问" }}</span>
                <span v-if="result.password">密码 {{ result.password }}</span>
                <span>{{ getShareExpireLabel(result.expiresAt) }}</span>
                <span v-if="result.shareCode">分享码 {{ result.shareCode }}</span>
                <span v-if="result.securityPolicy?.max_downloads">最多下载 {{ result.securityPolicy.max_downloads }} 次</span>
                <span v-if="result.securityPolicy?.ip_whitelist_count">IP 白名单 {{ result.securityPolicy.ip_whitelist_count }} 条</span>
                <span v-if="result.securityPolicy?.device_limit && result.securityPolicy.device_limit !== 'all'">
                  {{ getShareDeviceLabel(result.securityPolicy.device_limit) }}
                </span>
                <span v-if="result.securityPolicy?.access_time_start && result.securityPolicy?.access_time_end">
                  {{ result.securityPolicy.access_time_start }} - {{ result.securityPolicy.access_time_end }}
                </span>
              </div>
              <div v-if="result.reused" class="share-reused-note">已存在相同目标的分享，本次设置未覆盖原分享的安全策略。</div>
              <div class="share-created-actions">
                <button class="action-btn" @click="copyCreatedShareLink(result)">复制链接</button>
                <button class="action-btn" @click="openCreatedShareLink(result)">打开链接</button>
                <button v-if="result.password" class="action-btn" @click="copyText(result.password, '访问密码已复制')">复制密码</button>
              </div>
            </article>
          </div>

          <footer class="share-dialog-actions result-actions">
            <button v-if="shareCreateDialog.results.length === 0" class="action-btn" @click="returnToShareSettings">返回设置</button>
            <button class="solid-btn" @click="closeShareCreateDialog(true)">完成</button>
          </footer>
        </template>
      </section>
    </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-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.nav-icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  color: #4a6381;
}

.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,
.nav-btn.active .nav-icon {
  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, 120px);
  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: 10px;
  background: transparent;
  text-align: left;
  padding: 8px 6px 6px;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  width: 120px;
  min-height: 130px;
  overflow: hidden;
  position: relative;
  transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.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: 64px;
  height: 56px;
  position: relative;
  margin-top: 1px;
  border-radius: 12px;
  border: 1px solid rgba(0, 0, 0, 0.08);
  background: linear-gradient(180deg, #7ab5ff 0%, #4689dd 100%);
  box-shadow: 0 3px 8px rgba(32, 77, 131, 0.15);
  display: flex;
  align-items: center;
  justify-content: center;
}

.file-type-svg {
  width: 28px;
  height: 28px;
  margin-top: -4px;
}

.file-icon-shell.kind-folder {
  height: 52px;
  margin-top: 4px;
  border-radius: 10px;
  border-color: rgba(210, 170, 50, 0.3);
  background: linear-gradient(180deg, #f2c85e 0%, #e3b145 100%);
  box-shadow: 0 3px 8px rgba(152, 106, 14, 0.15);
}

.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: 4px;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.95);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.5px;
  line-height: 1;
  text-transform: uppercase;
}

.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;
  }
}

.section-label {
  font-size: 13px;
  font-weight: 600;
  color: #3a5274;
  margin-bottom: 10px;
  padding-left: 2px;
}

.shares-section {
  display: flex;
  flex-direction: column;
}

.direct-link-item {
  border-left: 3px solid #06b6d4;
}

.direct-badge {
  background: #06b6d4 !important;
  color: #fff !important;
}

.direct-link-url {
  color: #0891b2 !important;
}

/* 2026 desktop redesign: large-company cloud-drive workspace */
:global(html),
:global(body),
:global(#app) {
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
  font-family: "Noto Sans SC Variable", "Noto Sans SC", "Microsoft YaHei", sans-serif;
  background: #f7f9fc;
  color: #172033;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

:global(button),
:global(input),
:global(select),
:global(textarea) {
  font: inherit;
}

:global(button:focus-visible),
:global(input:focus-visible),
:global(select:focus-visible) {
  outline: 2px solid rgba(31, 111, 235, 0.55);
  outline-offset: 2px;
}

.desktop-root {
  width: 100%;
  height: 100dvh;
  min-height: 100dvh;
  padding: 0;
  background: #f7f9fc;
}

.work-shell {
  height: 100dvh;
  min-width: 0;
  display: grid;
  grid-template-columns: 224px minmax(0, 1fr);
  gap: 0;
  background: #fff;
}

.left-nav {
  min-width: 0;
  padding: 22px 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 0;
  overflow: hidden;
  background: #f7f9fc;
  border: 0;
  border-right: 1px solid #e6ebf2;
  border-radius: 0;
}

.left-header {
  min-height: 46px;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 0 10px;
  margin: 0 0 24px;
}

.app-mark {
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  display: block;
  object-fit: cover;
  border-radius: 50%;
  background: #ffe1a6;
  box-shadow: 0 0 0 1px rgba(158, 107, 40, 0.12);
}

.left-header strong {
  display: block;
  font-size: 17px;
  line-height: 1.25;
  letter-spacing: -0.02em;
  color: #162238;
}

.left-header span {
  display: block;
  margin-top: 2px;
  color: #1f6feb;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.nav-list {
  display: grid;
  gap: 7px;
}

.nav-btn {
  width: 100%;
  min-height: 48px;
  padding: 0 14px;
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: #344054;
  text-align: left;
  cursor: pointer;
  transition: color 150ms ease, background-color 150ms ease, transform 150ms ease;
}

.nav-btn:hover {
  background: #eef3fa;
  color: #18243a;
}

.nav-btn:active {
  transform: translateY(1px);
}

.nav-btn.active {
  color: #1f6feb;
  background: #eaf2ff;
}

.nav-btn > span:not(.nav-count):not(.nav-dot) {
  overflow: hidden;
  color: inherit;
  font-size: 14px;
  font-weight: 520;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.nav-btn.active > span:not(.nav-count):not(.nav-dot) {
  color: inherit;
  font-weight: 650;
}

.nav-icon {
  width: 21px;
  height: 21px;
  color: currentColor;
}

.nav-btn small,
.nav-btn-row {
  display: none;
}

.nav-count {
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: #e6ebf2;
  color: #536175;
  font-size: 11px;
  font-weight: 650;
}

.nav-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #1f6feb;
}

.user-card {
  margin-top: auto;
  padding: 16px 10px 0;
  display: block;
  border-top: 1px solid #e3e8f0;
  border-radius: 0;
  background: transparent;
}

.user-card-main {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr) 30px;
  align-items: center;
  gap: 10px;
}

.avatar {
  width: 38px;
  height: 38px;
  display: block;
  object-fit: cover;
  border-radius: 50%;
  background: #ffe1a6;
  box-shadow: 0 0 0 1px rgba(158, 107, 40, 0.12);
}

.user-card .meta {
  min-width: 0;
  display: grid;
  gap: 2px;
}

.user-card .meta strong,
.user-card .meta span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.user-card .meta strong {
  color: #1d2939;
  font-size: 13px;
  font-weight: 650;
}

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

.logout-btn,
.top-icon-btn,
.icon-btn,
.row-more,
.detail-head button,
.search-box button {
  border: 0;
  background: transparent;
  color: #667085;
  cursor: pointer;
}

.logout-btn {
  width: 30px;
  height: 30px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 7px;
}

.logout-btn:hover {
  color: #c53d3d;
  background: #fff0f0;
}

.storage-meta,
.storage-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  color: #7a8799;
  font-size: 10px;
}

.storage-meta {
  margin-top: 16px;
}

.storage-track {
  height: 5px;
  margin-top: 8px;
  overflow: hidden;
  border-radius: 999px;
  background: #e3e8ef;
}

.storage-track span {
  height: 100%;
  display: block;
  border-radius: inherit;
  background: #1f6feb;
  transition: width 220ms ease;
}

.storage-footer {
  margin-top: 10px;
}

.storage-footer button {
  padding: 0;
  border: 0;
  background: transparent;
  color: #1f6feb;
  font-size: 10px;
  cursor: pointer;
}

.work-area {
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  gap: 0;
  overflow: hidden;
  background: #fff;
}

.top-tools {
  min-width: 0;
  padding: 20px 28px 13px;
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  border: 0;
  border-bottom: 1px solid #e8edf3;
  border-radius: 0;
  background: #fff;
}

.topbar-main {
  min-width: 0;
  min-height: 46px;
  display: grid;
  grid-template-columns: minmax(180px, 1fr) minmax(280px, 420px) minmax(180px, 1fr);
  align-items: center;
  gap: 20px;
}

.page-title-block {
  min-width: 0;
}

.page-title-block h1 {
  margin: 0;
  color: #172033;
  font-size: 22px;
  font-weight: 720;
  letter-spacing: -0.025em;
  line-height: 1.25;
}

.page-title-block p {
  margin: 3px 0 0;
  color: #8792a3;
  font-size: 11px;
}

.search-box {
  height: 42px;
  min-width: 0;
  padding: 0 12px;
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  border: 1px solid #dce3ec;
  border-radius: 9px;
  color: #7c889a;
  background: #fbfcfe;
  transition: border-color 150ms ease, box-shadow 150ms ease, background-color 150ms ease;
}

.search-box:focus-within {
  border-color: #8eb9f7;
  background: #fff;
  box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.09);
}

.search-box input {
  min-width: 0;
  height: 38px;
  padding: 0;
  border: 0;
  outline: 0;
  color: #26364d;
  background: transparent;
  font-size: 13px;
}

.search-box input::placeholder {
  color: #a0a9b8;
}

.search-box span {
  padding: 3px 6px;
  border-radius: 5px;
  background: #eef1f5;
  color: #98a2b3;
  font-size: 9px;
}

.search-box button {
  width: 26px;
  height: 26px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 6px;
}

.search-box button:hover {
  color: #1f6feb;
  background: #edf4ff;
}

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

.crumb-btn {
  height: 24px;
  padding: 0 2px;
  border: 0;
  background: transparent;
  color: #7a8799;
  font-size: 12px;
  cursor: pointer;
  white-space: nowrap;
}

.crumb-btn:hover,
.crumb-btn.current {
  color: #344054;
}

.crumb-btn.current {
  font-weight: 600;
}

.crumb-separator {
  flex: 0 0 auto;
  color: #b1bac7;
}

.tool-right,
.tool-right-files {
  min-width: 0;
  width: 100%;
  display: block;
}

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

.batch-selection-summary {
  min-width: 108px;
  height: 38px;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid #cfe0fa;
  border-radius: 8px;
  background: #edf5ff;
  color: #315f9f;
  font-size: 12px;
  white-space: nowrap;
}

.batch-selection-summary strong {
  color: #1f6feb;
  font-size: 13px;
}

.toolbar-spacer {
  min-width: 12px;
  flex: 1 1 24px;
}

.primary-btn,
.solid-btn,
.ghost-btn,
.action-btn,
.icon-btn,
.mini-btn {
  transition: color 140ms ease, border-color 140ms ease, background-color 140ms ease, transform 140ms ease;
}

.primary-btn:active,
.solid-btn:active,
.action-btn:active,
.icon-btn:active,
.mini-btn:active {
  transform: translateY(1px);
}

.primary-btn,
.solid-btn {
  height: 38px;
  min-width: 78px;
  padding: 0 15px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  border: 1px solid #1f6feb;
  border-radius: 8px;
  background: #1f6feb;
  color: #fff;
  font-size: 13px;
  font-weight: 620;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 7px rgba(31, 111, 235, 0.16);
}

.primary-btn:hover,
.solid-btn:hover {
  border-color: #185fc8;
  background: #185fc8;
}

.action-btn {
  height: 38px;
  min-width: 0;
  padding: 0 13px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  border: 1px solid #d7dee8;
  border-radius: 8px;
  background: #fff;
  color: #344054;
  font-size: 12px;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}

.action-btn:hover {
  border-color: #b9c7da;
  background: #f7f9fc;
}

.action-btn.active {
  border-color: #a6c6f5;
  background: #edf4ff;
  color: #1f6feb;
}

.action-btn.danger,
.action-btn.danger.subtle {
  border-color: #ead6d6;
  background: #fff;
  color: #b54747;
}

.action-btn.danger:hover {
  border-color: #e4b8b8;
  background: #fff4f4;
}

.action-btn:disabled,
.solid-btn:disabled,
.icon-btn:disabled {
  opacity: 0.42;
  cursor: not-allowed;
  transform: none;
}

.icon-btn {
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid #d7dee8;
  border-radius: 8px;
  background: #fff;
  color: #667085;
  cursor: pointer;
}

.icon-btn:hover,
.icon-btn.active,
.icon-btn.active-view {
  border-color: #b4cef3;
  color: #1f6feb;
  background: #edf4ff;
}

.compact-select {
  width: 96px;
  height: 38px;
  padding: 0 26px 0 9px;
  border: 1px solid #d7dee8;
  border-radius: 8px;
  background-color: #fff;
  color: #536175;
  font-size: 11px;
}

.main-grid {
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 288px;
  gap: 0;
  overflow: hidden;
  background: #fff;
}

.main-grid.focus-content {
  grid-template-columns: minmax(0, 1fr) 288px;
}

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

.panel,
.content-panel,
.detail-panel {
  min-width: 0;
  min-height: 0;
  border: 0;
  border-radius: 0;
  background: #fff;
  box-shadow: none;
}

.content-panel {
  padding: 0 28px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.detail-panel {
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  overflow: auto;
  border-left: 1px solid #e8edf3;
}

.file-drop-surface {
  position: relative;
  min-height: 0;
  flex: 1;
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 0;
  border-radius: 0;
  background: #fff;
}

.file-table-head,
.file-row {
  min-width: 680px;
  display: grid;
  grid-template-columns: 36px minmax(250px, 1fr) 118px 170px 36px;
  align-items: center;
  column-gap: 12px;
}

.file-table-head {
  min-height: 44px;
  flex: 0 0 44px;
  border-bottom: 1px solid #e8edf3;
  color: #7a8799;
}

.column-sort {
  min-width: 0;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 4px;
  border: 0;
  background: transparent;
  color: #7a8799;
  font-size: 11px;
  cursor: pointer;
}

.column-sort:hover {
  color: #344054;
}

.row-menu-cell {
  display: grid;
  place-items: center;
  color: #a0a9b8;
}

.file-table-body {
  min-width: 0;
  min-height: 0;
  flex: 1;
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: #d3dbe6 transparent;
}

.file-row {
  min-height: 66px;
  padding: 5px 0;
  border-bottom: 1px solid #edf1f5;
  color: #344054;
  cursor: default;
  transition: background-color 130ms ease, box-shadow 130ms ease;
}

.file-row:hover {
  background: #f8faff;
}

.file-row.selected,
.file-row.batchSelected {
  background: #eef5ff;
  box-shadow: inset 2px 0 0 #1f6feb;
}

.file-row:focus-visible {
  outline: 2px solid rgba(31, 111, 235, 0.5);
  outline-offset: -2px;
}

.selection-cell {
  display: grid;
  place-items: center;
}

.table-checkbox {
  width: 17px;
  height: 17px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid #c9d2df;
  border-radius: 4px;
  background: #fff;
  color: #fff;
  cursor: pointer;
}

.table-checkbox:hover {
  border-color: #7ba9ea;
}

.table-checkbox.active {
  border-color: #1f6feb;
  background: #1f6feb;
}

.table-checkbox:focus-visible {
  outline: 2px solid rgba(31, 111, 235, 0.38);
  outline-offset: 2px;
}

.file-primary {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 12px;
}

.file-icon {
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  color: #4e8fe8;
  background: #eef5ff;
}

.file-icon.kind-folder,
.detail-file-icon.kind-folder {
  color: #efa91c;
  background: #fff7df;
}

.file-icon.ext-pdf,
.detail-file-icon.ext-pdf {
  color: #e65353;
  background: #fff0f0;
}

.file-icon.ext-doc,
.file-icon.ext-docx,
.detail-file-icon.ext-doc,
.detail-file-icon.ext-docx {
  color: #3478db;
  background: #edf4ff;
}

.file-icon.ext-xls,
.file-icon.ext-xlsx,
.file-icon.ext-csv,
.detail-file-icon.ext-xls,
.detail-file-icon.ext-xlsx,
.detail-file-icon.ext-csv {
  color: #2f9d66;
  background: #ecf8f1;
}

.file-icon.ext-zip,
.file-icon.ext-rar,
.file-icon.ext-7z,
.detail-file-icon.ext-zip,
.detail-file-icon.ext-rar,
.detail-file-icon.ext-7z {
  color: #6d79d8;
  background: #f0f1ff;
}

.file-name-wrap {
  min-width: 0;
  display: grid;
  gap: 3px;
}

.file-name-wrap strong {
  overflow: hidden;
  color: #253247;
  font-size: 13px;
  font-weight: 560;
  line-height: 1.35;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-name-wrap span {
  color: #98a2b3;
  font-size: 10px;
}

.file-size,
.file-time {
  overflow: hidden;
  color: #667085;
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-more {
  width: 30px;
  height: 30px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 7px;
  opacity: 0.25;
}

.file-row:hover .row-more,
.file-row.selected .row-more,
.row-more:focus-visible {
  opacity: 1;
}

.row-more:hover {
  color: #1f6feb;
  background: #e6f0ff;
}

.inline-rename-input {
  width: min(340px, 100%);
  height: 32px;
  padding: 0 9px;
  border: 1px solid #78aaf0;
  border-radius: 6px;
  background: #fff;
  color: #253247;
  font-size: 12px;
  font-weight: 560;
}

.file-table-footer {
  min-height: 42px;
  flex: 0 0 42px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  color: #98a2b3;
  font-size: 10px;
}

.file-state {
  min-height: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: #667085;
  text-align: center;
}

.state-icon,
.detail-empty > span {
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 16px;
  background: #eef4fc;
  color: #7193c4;
}

.state-icon.loading svg {
  animation: cloud-spin 900ms linear infinite;
}

@keyframes cloud-spin {
  to { transform: rotate(360deg); }
}

.file-state strong {
  color: #344054;
  font-size: 14px;
}

.file-state p {
  margin: 0 0 4px;
  color: #98a2b3;
  font-size: 11px;
}

.file-state.error .state-icon {
  color: #c84a4a;
  background: #fff0f0;
}

.detail-head {
  min-height: 34px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.detail-head h3,
.detail-panel > h3 {
  margin: 0;
  color: #27364c;
  font-size: 14px;
  font-weight: 680;
}

.detail-head button {
  width: 30px;
  height: 30px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 7px;
}

.detail-head button:hover {
  color: #344054;
  background: #f0f3f7;
}

.file-detail-content {
  width: 100%;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 0;
  padding-top: 34px;
  overflow: auto;
  scrollbar-width: thin;
}

.detail-file-icon {
  flex: 0 0 auto;
  width: 88px;
  height: 88px;
  display: grid;
  place-items: center;
  border-radius: 18px;
  color: #4e8fe8;
  background: #eef5ff;
}

.file-detail-content h4 {
  flex: 0 0 auto;
  width: 100%;
  margin: 20px 0 0;
  overflow: hidden;
  color: #253247;
  font-size: 14px;
  font-weight: 650;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.detail-file-size {
  flex: 0 0 auto;
  margin: 6px 0 0;
  color: #8a96a8;
  font-size: 11px;
}

.detail-list {
  flex: 0 0 auto;
  width: 100%;
  margin: 28px 0 0;
  display: grid;
  gap: 0;
}

.detail-list > div {
  min-width: 0;
  padding: 12px 0;
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: 8px;
  border-bottom: 1px solid #edf1f5;
}

.detail-list dt,
.detail-list dd {
  margin: 0;
  font-size: 11px;
}

.detail-list dt {
  color: #8a96a8;
}

.detail-list dd {
  overflow: hidden;
  color: #536175;
  text-align: right;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.detail-extra {
  flex: 0 0 auto;
  width: 100%;
}

.detail-extra-title,
.detail-description {
  padding: 12px 0;
  color: #8a96a8;
  font-size: 11px;
}

.detail-tags {
  min-height: 36px;
  padding: 0 0 12px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  border-bottom: 1px solid #edf1f5;
}

.detail-tags span,
.detail-tags button {
  min-height: 26px;
  padding: 0 9px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1px solid #cfe0fb;
  border-radius: 6px;
  color: #2871d6;
  background: #f6f9ff;
  font-size: 10px;
}

.detail-description {
  display: grid;
  gap: 8px;
  border-bottom: 1px solid #edf1f5;
}

.detail-description button {
  width: 100%;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border: 0;
  color: #9aa5b5;
  background: transparent;
  font-size: 10px;
  text-align: left;
}

.detail-actions {
  flex: 0 0 auto;
  width: 100%;
  margin-top: auto;
  padding-top: 18px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.detail-empty,
.batch-detail {
  min-height: 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: #667085;
  text-align: center;
}

.detail-empty strong,
.batch-detail strong {
  color: #344054;
  font-size: 13px;
}

.detail-empty p,
.batch-detail p {
  max-width: 210px;
  margin: 0;
  color: #98a2b3;
  font-size: 11px;
  line-height: 1.55;
}

.detail-file-icon.batch {
  color: #1f6feb;
  background: #edf4ff;
}

.share-dialog-mask {
  position: fixed;
  inset: 0;
  z-index: 1400;
  padding: 24px;
  display: grid;
  place-items: center;
  background: rgba(24, 34, 51, 0.5);
  backdrop-filter: blur(3px);
}

.share-create-card {
  width: min(680px, 100%);
  max-height: calc(100dvh - 48px);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  border: 1px solid #dbe4f0;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 28px 70px rgba(24, 45, 76, 0.24);
}

.share-dialog-header {
  flex: 0 0 auto;
  min-height: 76px;
  padding: 18px 22px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  border-bottom: 1px solid #e8edf4;
}

.share-dialog-eyebrow {
  display: block;
  margin-bottom: 3px;
  color: #1f6feb;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
}

.share-dialog-header h3 {
  margin: 0;
  color: #172033;
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.share-dialog-close {
  width: 34px;
  height: 34px;
  flex: 0 0 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 9px;
  color: #667085;
  background: transparent;
  cursor: pointer;
}

.share-dialog-close:hover {
  color: #344054;
  background: #f2f5f9;
}

.share-dialog-close:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.share-target-card {
  flex: 0 0 auto;
  margin: 18px 22px 0;
  padding: 12px 14px;
  display: flex;
  align-items: center;
  gap: 12px;
  border: 1px solid #d8e6fa;
  border-radius: 12px;
  background: #f5f9ff;
}

.share-target-icon {
  width: 38px;
  height: 38px;
  flex: 0 0 38px;
  display: grid;
  place-items: center;
  border-radius: 10px;
  color: #1f6feb;
  background: #e8f1ff;
}

.share-target-card > div {
  min-width: 0;
}

.share-target-card strong,
.share-target-card span {
  display: block;
}

.share-target-card strong {
  overflow: hidden;
  color: #253247;
  font-size: 13px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.share-target-card > div > span {
  margin-top: 3px;
  overflow: hidden;
  color: #718096;
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.share-settings-scroll {
  min-height: 0;
  padding: 16px 22px 4px;
  display: grid;
  gap: 12px;
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: #c9d5e5 transparent;
}

.share-setting-section {
  padding: 16px;
  display: grid;
  gap: 14px;
  border: 1px solid #e3e9f1;
  border-radius: 13px;
  background: #fff;
}

.share-setting-section.advanced {
  background: #fbfcfe;
}

.share-section-heading strong,
.share-section-heading span {
  display: block;
}

.share-section-heading strong {
  color: #253247;
  font-size: 13px;
}

.share-section-heading span {
  margin-top: 3px;
  color: #8a96a8;
  font-size: 11px;
}

.share-field-grid,
.share-time-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.share-field {
  min-width: 0;
  display: grid;
  gap: 7px;
  color: #4b5b70;
  font-size: 11px;
  font-weight: 600;
}

.share-field > span small {
  color: #98a2b3;
  font-size: 10px;
  font-weight: 500;
}

.share-field > small {
  color: #8a96a8;
  font-size: 10px;
  font-weight: 500;
  line-height: 1.5;
}

.share-field input,
.share-field select,
.share-field textarea {
  width: 100%;
  min-width: 0;
  border: 1px solid #d5deea;
  border-radius: 9px;
  color: #253247;
  background: #fff;
  font-size: 12px;
  font-weight: 500;
  transition: border-color 140ms ease, box-shadow 140ms ease;
}

.share-field input,
.share-field select {
  height: 38px;
  padding: 0 11px;
}

.share-field textarea {
  min-height: 64px;
  padding: 9px 11px;
  resize: vertical;
  line-height: 1.5;
}

.share-field input:focus,
.share-field select:focus,
.share-field textarea:focus {
  outline: 0;
  border-color: #77a7ee;
  box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.1);
}

.share-password-field {
  padding: 12px;
  border-radius: 10px;
  background: #f7f9fc;
}

.share-switch-row {
  min-height: 52px;
  padding: 0 2px;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  cursor: pointer;
}

.share-switch-row > span {
  min-width: 0;
}

.share-switch-row strong,
.share-switch-row small {
  display: block;
}

.share-switch-row strong {
  color: #344054;
  font-size: 12px;
  font-weight: 650;
}

.share-switch-row small {
  margin-top: 3px;
  color: #8a96a8;
  font-size: 10px;
  line-height: 1.45;
}

.share-switch-row input[type="checkbox"] {
  width: 34px;
  height: 20px;
  flex: 0 0 34px;
  position: relative;
  margin: 0;
  padding: 0;
  appearance: none;
  border: 1px solid #c7d1df;
  border-radius: 999px;
  background: #dfe5ed;
  cursor: pointer;
  transition: border-color 140ms ease, background-color 140ms ease;
}

.share-switch-row input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(35, 52, 76, 0.28);
  transition: transform 140ms ease;
}

.share-switch-row input[type="checkbox"]:checked {
  border-color: #1f6feb;
  background: #1f6feb;
}

.share-switch-row input[type="checkbox"]:checked::after {
  transform: translateX(14px);
}

.share-switch-row input[type="checkbox"]:focus-visible {
  outline: 2px solid rgba(31, 111, 235, 0.42);
  outline-offset: 2px;
}

.share-switch-row.advanced-toggle {
  min-height: 44px;
}

.share-switch-row.compact {
  min-height: 44px;
  padding: 0;
}

.share-advanced-fields {
  display: grid;
  gap: 13px;
  padding-top: 13px;
  border-top: 1px solid #e6ebf2;
}

.share-dialog-error {
  flex: 0 0 auto;
  margin: 10px 22px 0;
  padding: 9px 11px;
  border: 1px solid #f0caca;
  border-radius: 9px;
  color: #b54747;
  background: #fff7f7;
  font-size: 11px;
  line-height: 1.5;
}

.share-dialog-actions {
  flex: 0 0 auto;
  min-height: 70px;
  padding: 15px 22px 17px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 9px;
  border-top: 1px solid #e8edf4;
  background: #fff;
}

.share-dialog-actions .solid-btn,
.share-dialog-actions .action-btn {
  min-width: 92px;
}

.share-result-summary {
  flex: 0 0 auto;
  margin: 20px 22px 0;
  padding: 15px;
  display: flex;
  align-items: center;
  gap: 12px;
  border: 1px solid #bfe5d2;
  border-radius: 12px;
  background: #f1fbf6;
}

.share-result-summary.partial {
  border-color: #ead8b8;
  background: #fffaf0;
}

.share-result-summary > span {
  width: 40px;
  height: 40px;
  flex: 0 0 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: #fff;
  background: #27a66b;
}

.share-result-summary.partial > span {
  background: #d5902e;
}

.share-result-summary strong,
.share-result-summary small {
  display: block;
}

.share-result-summary strong {
  color: #253247;
  font-size: 14px;
}

.share-result-summary small {
  margin-top: 4px;
  color: #667085;
  font-size: 11px;
}

.share-failure-list {
  flex: 0 0 auto;
  margin: 12px 22px 0;
  max-height: 112px;
  padding: 8px 11px;
  overflow: auto;
  display: grid;
  gap: 7px;
  border: 1px solid #f0caca;
  border-radius: 10px;
  color: #8f3e3e;
  background: #fff8f8;
  font-size: 11px;
}

.share-failure-list > div {
  display: grid;
  grid-template-columns: minmax(100px, 0.45fr) minmax(0, 1fr);
  gap: 10px;
}

.share-failure-list span {
  overflow-wrap: anywhere;
}

.share-created-list {
  min-height: 0;
  margin: 12px 22px 0;
  padding-right: 3px;
  overflow: auto;
  display: grid;
  gap: 10px;
  scrollbar-width: thin;
}

.share-created-item {
  padding: 14px;
  display: grid;
  gap: 9px;
  border: 1px solid #e0e7f0;
  border-radius: 12px;
  background: #fbfcfe;
}

.share-created-head {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

.share-created-head strong {
  min-width: 0;
  overflow: hidden;
  color: #253247;
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.share-result-badge {
  flex: 0 0 auto;
  padding: 3px 7px;
  border-radius: 999px;
  color: #2568c6;
  background: #eaf2ff;
  font-size: 9px;
  font-weight: 700;
}

.share-result-badge.reused {
  color: #9a6617;
  background: #fff1d6;
}

.share-created-url {
  overflow: hidden;
  color: #1f6feb;
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.share-created-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.share-created-meta span {
  padding: 3px 7px;
  border: 1px solid #dde5ef;
  border-radius: 999px;
  color: #667085;
  background: #fff;
  font-size: 9px;
}

.share-reused-note {
  padding: 8px 9px;
  border-radius: 8px;
  color: #946319;
  background: #fff7e6;
  font-size: 10px;
  line-height: 1.5;
}

.share-created-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}

.share-created-actions .action-btn {
  height: 32px;
  padding-inline: 10px;
  font-size: 10px;
}

.share-dialog-actions.result-actions {
  margin-top: 16px;
}

.drop-overlay {
  border: 2px dashed rgba(31, 111, 235, 0.62);
  border-radius: 10px;
  background: rgba(235, 244, 255, 0.9);
}

.drop-overlay-card {
  border: 1px solid #c6daf8;
  border-radius: 10px;
  background: #fff;
  box-shadow: 0 10px 24px rgba(55, 96, 153, 0.12);
}

.progress {
  height: 7px;
  border: 0;
  background: #e8edf4;
}

.bar {
  background: #1f6feb;
}

.login-shell {
  height: 100%;
  padding: clamp(24px, 5vw, 72px);
  display: grid;
  grid-template-columns: minmax(0, 1.12fr) minmax(340px, 0.72fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: center;
  background: #f4f7fc;
}

.login-brand,
.login-panel {
  border: 1px solid #e0e6ef;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 18px 45px rgba(48, 77, 119, 0.08);
}

.login-brand {
  min-height: 470px;
  padding: clamp(32px, 5vw, 64px);
  justify-content: center;
}

.login-brand-mark {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #1f6feb;
  font-size: 13px;
  font-weight: 650;
}

.login-brand-mark img {
  width: 42px;
  height: 42px;
  object-fit: cover;
  border-radius: 50%;
}

.brand-chip {
  display: none;
}

.login-brand h1 {
  max-width: 560px;
  margin: 14px 0 0;
  color: #172033;
  font-size: clamp(30px, 4vw, 48px);
  font-weight: 720;
  letter-spacing: -0.04em;
  line-height: 1.16;
}

.login-brand p {
  max-width: 540px;
  color: #68778d;
  font-size: 14px;
  line-height: 1.7;
}

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

.brand-card {
  min-height: 82px;
  padding: 16px;
  border: 1px solid #e5eaf1;
  border-radius: 10px;
  background: #f8fafc;
}

.login-panel {
  padding: 34px;
}

.login-panel h2 {
  color: #172033;
  font-size: 22px;
  font-weight: 680;
}

.login-panel input[type="text"],
.login-panel input[type="password"] {
  height: 44px;
  border-radius: 8px;
}

.login-panel .primary-btn {
  width: 100%;
  height: 44px;
  margin-top: 4px;
}

@media (max-width: 1260px) {
  .work-shell {
    grid-template-columns: 204px minmax(0, 1fr);
  }

  .top-tools {
    padding-inline: 22px;
    display: grid;
  }

  .topbar-main {
    grid-template-columns: minmax(160px, 1fr) minmax(260px, 380px) minmax(40px, 0.45fr);
  }

  .main-grid {
    grid-template-columns: minmax(0, 1fr) 260px;
  }

  .content-panel {
    padding-inline: 22px;
  }

  .file-table-head,
  .file-row {
    grid-template-columns: 34px minmax(220px, 1fr) 100px 148px 34px;
    column-gap: 10px;
  }
}

@media (max-width: 1160px) {
  .main-grid,
  .main-grid.focus-content {
    grid-template-columns: minmax(0, 1fr);
  }

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

  .compact-select {
    display: none;
  }
}

@media (max-height: 760px) {
  .top-tools {
    padding-top: 13px;
    padding-bottom: 10px;
    gap: 8px;
  }

  .topbar-main {
    min-height: 40px;
  }

  .file-row {
    min-height: 58px;
  }

  .file-table-footer {
    min-height: 34px;
    flex-basis: 34px;
  }

  .share-dialog-mask {
    padding: 12px;
  }

  .share-create-card {
    max-height: calc(100dvh - 24px);
  }

  .share-dialog-header {
    min-height: 64px;
    padding-block: 13px;
  }

  .share-target-card {
    margin-top: 12px;
  }

  .share-settings-scroll {
    padding-top: 12px;
  }
}

@media (max-height: 900px) {
  .file-detail-content {
    padding-top: 18px;
  }

  .detail-file-icon {
    width: 72px;
    height: 72px;
    border-radius: 14px;
  }

  .file-detail-content h4 {
    margin-top: 12px;
  }

  .detail-list {
    margin-top: 18px;
  }

  .detail-list > div {
    padding: 8px 0;
  }

  .detail-extra-title,
  .detail-description {
    padding: 9px 0;
  }

  .detail-tags {
    padding-bottom: 9px;
  }

  .detail-actions {
    padding-top: 10px;
  }
}

@media (max-width: 720px) {
  .share-dialog-mask {
    padding: 10px;
  }

  .share-create-card {
    max-height: calc(100dvh - 20px);
    border-radius: 14px;
  }

  .share-dialog-header,
  .share-dialog-actions {
    padding-inline: 16px;
  }

  .share-target-card,
  .share-result-summary,
  .share-failure-list,
  .share-created-list {
    margin-inline: 16px;
  }

  .share-settings-scroll {
    padding-inline: 16px;
  }

  .share-field-grid,
  .share-time-grid {
    grid-template-columns: 1fr;
  }

  .share-dialog-actions .solid-btn,
  .share-dialog-actions .action-btn {
    flex: 1;
  }

  .share-failure-list > div {
    grid-template-columns: 1fr;
    gap: 2px;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}
</style>
