{% extends "base.html" %}

{% block extra_head %}
<style>
  body {
    padding-bottom: calc(96px + env(safe-area-inset-bottom)) !important;
  }
  .mode-tabs {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  .mode-tabs::-webkit-scrollbar {
    display: none;
  }
  .mode-tab {
    border: 1px solid rgba(148, 163, 184, 0.25);
    background: rgba(255, 255, 255, 0.86);
    color: #334155;
    border-radius: 999px;
    padding: 7px 12px;
    font-size: 12px;
    white-space: nowrap;
  }
  [data-theme="dark"] .mode-tab {
    background: rgba(30, 41, 59, 0.8);
    color: #cbd5e1;
    border-color: rgba(148, 163, 184, 0.2);
  }
  .mode-tab.is-active {
    background: rgba(15, 23, 42, 0.92);
    color: #fff;
    border-color: rgba(15, 23, 42, 0.92);
  }
  [data-theme="dark"] .mode-tab.is-active {
    background: rgba(248, 250, 252, 0.95);
    color: #0f172a;
    border-color: rgba(248, 250, 252, 0.95);
  }
  .collection-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 2px 2px 4px;
  }
  .collection-nav-back {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: #475569;
    text-decoration: none;
    padding: 6px 8px;
    border-radius: 999px;
    border: 1px solid rgba(148, 163, 184, 0.25);
    background: rgba(255, 255, 255, 0.82);
  }
  .collection-nav-back svg {
    width: 14px;
    height: 14px;
  }
  .collection-nav-title {
    flex: 1;
    min-width: 0;
    text-align: right;
    font-size: 14px;
    font-weight: 700;
    color: #0f172a;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  [data-theme="dark"] .collection-nav-back {
    color: #cbd5e1;
    border-color: rgba(148, 163, 184, 0.2);
    background: rgba(30, 41, 59, 0.8);
  }
  [data-theme="dark"] .collection-nav-title {
    color: #f8fafc;
  }

  .masonry-grid {
    --wf-gap: 8px;
    display: flex;
    align-items: flex-start;
    gap: var(--wf-gap);
  }
  @media (min-width: 768px) {
    .masonry-grid { --wf-gap: 10px; }
  }
  .waterfall-col {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--wf-gap);
  }
  .media-tile {
    border-radius: 12px;
    overflow: hidden;
    position: relative;
    background: #0b0f15;
    cursor: pointer;
  }
  .media-tile img {
    width: 100%;
    height: auto;
    display: block;
  }
  .media-tile video {
    width: 100%;
    height: auto;
    display: block;
    object-fit: cover;
    background: #000;
  }
  .tile-video-badge {
    position: absolute;
    right: 8px;
    bottom: 8px;
    width: 24px;
    height: 24px;
    border-radius: 999px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.52);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.28);
    pointer-events: none;
  }
  .library-loading {
    text-align: center;
    color: #64748b;
    font-size: 12px;
    padding: 16px 0 8px;
  }

  /* Quick Flow Viewer */
  #quick-view {
    position: fixed;
    inset: 0;
    z-index: 260;
    background: #000;
    display: none;
    overflow: hidden;
    touch-action: none;
  }
  #quick-view.active {
    display: block;
  }
  #quick-feed-layer {
    position: absolute;
    inset: 0;
  }
  .quick-media {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: none;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #000;
    z-index: 1;
    opacity: 0;
    transition: opacity 0.18s ease;
  }
  .quick-media.active {
    display: block;
    opacity: 1;
  }
  #quick-overlay {
    position: absolute;
    inset: 0;
    z-index: 10;
  }
  #quick-close-top {
    position: absolute;
    top: calc(12px + env(safe-area-inset-top));
    right: 12px;
    width: 42px;
    height: 42px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.28);
    background: rgba(0, 0, 0, 0.5);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 40;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    transition: transform 0.15s ease, background 0.15s ease;
  }
  #quick-close-top:active {
    transform: scale(0.94);
    background: rgba(0, 0, 0, 0.66);
  }
  #quick-close-top svg {
    width: 18px;
    height: 18px;
  }
  .quick-counter {
    position: absolute;
    top: calc(14px + env(safe-area-inset-top));
    left: 12px;
    font-size: 12px;
    color: rgba(255, 255, 255, 0.94);
    background: rgba(0, 0, 0, 0.42);
    border: 1px solid rgba(255, 255, 255, 0.25);
    border-radius: 999px;
    padding: 4px 10px;
    z-index: 30;
  }
  #quick-controls {
    position: absolute;
    left: 12px;
    right: 12px;
    bottom: calc(92px + env(safe-area-inset-bottom));
    height: 20px;
    display: flex;
    align-items: center;
    gap: 8px;
    z-index: 30;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }
  #quick-controls.is-hidden {
    display: none;
  }
  .quick-time {
    color: rgba(255, 255, 255, 0.9);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    width: 38px;
    text-align: center;
  }
  .quick-progress-wrap {
    position: relative;
    flex: 1;
    height: 100%;
    display: flex;
    align-items: center;
  }
  #quick-progress {
    -webkit-appearance: none;
    position: absolute;
    width: 100%;
    height: 100%;
    opacity: 0;
    z-index: 2;
    cursor: pointer;
    margin: 0;
  }
  #quick-progress:disabled {
    cursor: default;
  }
  .quick-progress-track {
    position: absolute;
    left: 0;
    right: 0;
    height: 4px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.2);
  }
  .quick-progress-fill {
    position: absolute;
    left: 0;
    height: 4px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.95);
    width: 0%;
  }
  .quick-actions {
    position: absolute;
    right: 14px;
    bottom: calc(130px + env(safe-area-inset-bottom));
    display: flex;
    flex-direction: column;
    gap: 12px;
    z-index: 35;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }
  .quick-btn {
    width: 46px;
    height: 46px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.24);
    background: rgba(0, 0, 0, 0.5);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease;
    position: relative;
  }
  .quick-btn:active {
    transform: scale(0.92);
  }
  .quick-btn svg {
    width: 20px;
    height: 20px;
  }
  .quick-btn.is-favorited {
    background: rgba(239, 68, 68, 0.92);
    border-color: rgba(239, 68, 68, 0.96);
  }
  .quick-btn.is-active {
    background: rgba(255, 255, 255, 0.24);
    border-color: rgba(255, 255, 255, 0.78);
  }
  .quick-btn.is-hidden {
    display: none;
  }
  .quick-btn .collection-count {
    position: absolute;
    top: -6px;
    right: -6px;
    min-width: 18px;
    height: 18px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.35);
    background: rgba(15, 23, 42, 0.9);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 0 5px;
  }
  .quick-btn.has-collection {
    background: rgba(255, 255, 255, 0.24);
    border-color: rgba(255, 255, 255, 0.78);
  }
  .quick-btn.has-collection .collection-count {
    display: inline-flex;
  }
  #quick-speed {
    font-size: 14px;
    font-weight: 700;
    font-family: system-ui, -apple-system, sans-serif;
  }
  .quick-image-tool-wrap {
    position: relative;
  }
  .quick-zoom-options {
    position: absolute;
    right: 58px;
    bottom: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
    opacity: 0;
    pointer-events: none;
    transform: translateX(8px);
    transition: all 0.2s ease;
  }
  .quick-zoom-options.show {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(0);
  }
  .quick-zoom-btn {
    width: 44px;
    height: 44px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.16);
    background: rgba(0, 0, 0, 0.62);
    color: rgba(255, 255, 255, 0.75);
    font-size: 11px;
    font-weight: 700;
  }
  .quick-zoom-btn.active {
    background: rgba(255, 255, 255, 0.22);
    color: #fff;
    border-color: rgba(255, 255, 255, 0.78);
  }
  .quick-caption-panel {
    position: absolute;
    left: 16px;
    bottom: calc(114px + env(safe-area-inset-bottom));
    max-width: min(72vw, 420px);
    padding: 14px 16px;
    border-radius: 14px;
    background: rgba(0, 0, 0, 0.55);
    border: 1px solid rgba(255, 255, 255, 0.12);
    color: #fff;
    z-index: 32;
    display: flex;
    flex-direction: column;
    gap: 8px;
    backdrop-filter: blur(10px);
  }
  .quick-caption-panel.is-hidden {
    display: none;
  }
  .quick-caption-title {
    font-size: 14px;
    line-height: 1.4;
    font-weight: 600;
  }
  .quick-caption-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }
  .quick-caption-tag {
    display: inline-flex;
    align-items: center;
    padding: 4px 8px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.12);
    border: 1px solid rgba(255, 255, 255, 0.15);
    font-size: 11px;
    color: rgba(255, 255, 255, 0.86);
  }
  #quick-caption.is-loading::after {
    content: '';
    position: absolute;
    inset: 7px;
    border-radius: 999px;
    border: 1.5px solid rgba(255, 255, 255, 0.7);
    border-top-color: transparent;
    animation: quick-caption-spin 0.9s linear infinite;
  }
  #quick-caption {
    position: relative;
  }
  #quick-caption.is-loading i {
    opacity: 0.35;
  }
  #quick-caption.is-error::after {
    content: '';
    position: absolute;
    inset: 7px;
    border-radius: 999px;
    border: 1.5px solid rgba(248, 113, 113, 0.9);
  }
  @keyframes quick-caption-spin {
    to { transform: rotate(360deg); }
  }
  .quick-magnifier {
    position: absolute;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    border: 3px solid rgba(255, 255, 255, 0.9);
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
    background-repeat: no-repeat;
    background-color: #000;
    cursor: grab;
    z-index: 45;
    display: none;
    transform: translate(-50%, -50%) scale(0);
    transition: transform 0.2s ease, width 0.2s ease, height 0.2s ease;
  }
  .quick-magnifier.quick-magnifier-large {
    width: 300px;
    height: 300px;
  }
  .quick-magnifier.active {
    display: block;
    transform: translate(-50%, -50%) scale(1);
  }
  #quick-play-status {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(1);
    width: 78px;
    height: 78px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.46);
    border: 1px solid rgba(255, 255, 255, 0.2);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    z-index: 28;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.16s ease, transform 0.16s ease;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
  }
  #quick-play-status.visible {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.08);
  }
  #quick-play-status svg {
    width: 38px;
    height: 38px;
    fill: currentColor;
    stroke: none;
  }
  .quick-mag-badge {
    position: absolute;
    left: 50%;
    bottom: 10px;
    transform: translateX(-50%);
    border-radius: 10px;
    padding: 2px 6px;
    font-size: 10px;
    color: #fff;
    background: rgba(0, 0, 0, 0.55);
  }
  #quick-view.immersive .quick-actions,
  #quick-view.immersive #quick-controls,
  #quick-view.immersive .quick-counter,
  #quick-view.immersive .quick-caption-panel {
    opacity: 0;
    pointer-events: none;
    transform: translateY(10px);
  }

  .quick-collection-modal {
    position: fixed;
    inset: 0;
    z-index: 280;
    background: rgba(15, 23, 42, 0.56);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s;
  }
  .quick-collection-modal.active {
    opacity: 1;
    pointer-events: auto;
  }
  .quick-collection-card {
    width: min(440px, 100%);
    border-radius: 14px;
    border: 1px solid rgba(148, 163, 184, 0.28);
    background: rgba(255, 255, 255, 0.98);
    color: #0f172a;
    box-shadow: 0 24px 80px rgba(15, 23, 42, 0.3);
    padding: 12px;
    transform: translateY(20px);
    transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
  }
  .quick-collection-modal.active .quick-collection-card {
    transform: translateY(0);
  }
  [data-theme="dark"] .quick-collection-card {
    background: rgba(15, 23, 42, 0.98);
    border-color: rgba(148, 163, 184, 0.2);
    color: #f8fafc;
  }
  .quick-collection-header {
    margin-bottom: 10px;
  }
  .quick-collection-title-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .quick-collection-header h3 {
    font-size: 14px;
    font-weight: 700;
  }
  .quick-collection-meta {
    margin-top: 4px;
    font-size: 11px;
    color: #64748b;
  }
  [data-theme="dark"] .quick-collection-meta {
    color: #94a3b8;
  }
  .quick-collection-close {
    width: 30px;
    height: 30px;
    border-radius: 999px;
    border: 1px solid rgba(148, 163, 184, 0.28);
    background: transparent;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .quick-collection-close svg {
    width: 15px;
    height: 15px;
  }
  .quick-collection-create {
    display: flex;
    gap: 8px;
    margin-top: 10px;
  }
  .quick-collection-create input {
    flex: 1;
    min-width: 0;
    border-radius: 10px;
    border: 1px solid rgba(148, 163, 184, 0.32);
    background: rgba(248, 250, 252, 0.86);
    color: #0f172a;
    font-size: 13px;
    padding: 8px 10px;
  }
  [data-theme="dark"] .quick-collection-create input {
    background: rgba(15, 23, 42, 0.82);
    color: #f1f5f9;
    border-color: rgba(148, 163, 184, 0.2);
  }
  .quick-collection-create button {
    border-radius: 10px;
    border: 1px solid rgba(148, 163, 184, 0.3);
    background: rgba(15, 23, 42, 0.9);
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    padding: 8px 12px;
  }
  [data-theme="dark"] .quick-collection-create button {
    background: rgba(226, 232, 240, 0.94);
    color: #0f172a;
  }
  .quick-collection-list {
    max-height: min(44vh, 320px);
    overflow: auto;
    border: 1px solid rgba(148, 163, 184, 0.24);
    border-radius: 10px;
    padding: 4px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    background: rgba(248, 250, 252, 0.78);
  }
  [data-theme="dark"] .quick-collection-list {
    background: rgba(15, 23, 42, 0.76);
    border-color: rgba(148, 163, 184, 0.2);
  }
  .quick-collection-item {
    border-radius: 8px;
    padding: 8px 10px;
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    border: 1px solid transparent;
  }
  .quick-collection-item:hover {
    background: rgba(148, 163, 184, 0.12);
  }
  .quick-collection-item.is-pending {
    opacity: 0.6;
    pointer-events: none;
  }
  .quick-collection-item.is-selected {
    background: rgba(99, 102, 241, 0.07);
  }
  .quick-collection-item input[type="checkbox"] {
    position: absolute; opacity: 0; width: 0; height: 0;
  }
  .quick-collection-item span.coll-name {
    flex: 1;
    min-width: 0;
    font-size: 13px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .quick-collection-item .coll-icon {
    width: 28px; height: 28px;
    border-radius: 6px;
    background: rgba(148, 163, 184, 0.12);
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0; color: #64748b;
  }
  .quick-collection-item .coll-check {
    width: 20px; height: 20px;
    border-radius: 50%;
    border: 1.5px solid rgba(148, 163, 184, 0.45);
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0;
    transition: background 0.15s, border-color 0.15s;
  }
  .quick-collection-item .coll-check svg { display: none; color: white; }
  .quick-collection-item.is-selected .coll-check {
    background: #6366f1;
    border-color: #6366f1;
  }
  .quick-collection-item.is-selected .coll-check svg { display: block; }
  .quick-collection-empty {
    text-align: center;
    font-size: 12px;
    color: #64748b;
    padding: 14px 8px;
  }
  [data-theme="dark"] .quick-collection-empty {
    color: #94a3b8;
  }
  .coll-drag-handle {
    width: 36px; height: 4px;
    border-radius: 2px;
    background: rgba(148, 163, 184, 0.35);
    margin: 12px auto 16px;
    display: none;
  }
  @media (max-width: 540px) {
    .quick-collection-modal {
      align-items: flex-end;
      padding: 0;
    }
    .quick-collection-card {
      width: 100%;
      border-radius: 20px 20px 0 0;
      transform: translateY(100%);
      padding: 0 12px 24px;
    }
    .quick-collection-modal.active .quick-collection-card {
      transform: translateY(0);
    }
    .coll-drag-handle { display: block; }
  }
</style>
{% endblock %}

{% block content %}
<main class="container mx-auto px-2 pt-2 pb-1 md:px-3">
  {% if scope == 'all' %}
  <section class="mb-2">
    <div class="mode-tabs" id="mode-tabs">
      <button type="button" class="mode-tab" data-mode="all">全部</button>
      <button type="button" class="mode-tab" data-mode="image_random">图片随机</button>
      <button type="button" class="mode-tab" data-mode="video_latest">视频最新</button>
      <button type="button" class="mode-tab" data-mode="big_files">大文件</button>
    </div>
  </section>
  {% elif scope == 'favorite' %}
  <section class="mb-2">
    <div class="mode-tabs">
      <a class="mode-tab is-active" href="/favorite">收藏媒体</a>
      <a class="mode-tab" href="/collections">我的集合</a>
    </div>
  </section>
  {% elif scope == 'collection' %}
  <section class="mb-2">
    <div class="collection-nav">
      <a class="collection-nav-back" href="/collections">
        <i data-feather="arrow-left"></i>
        <span>我的集合</span>
      </a>
      <div class="collection-nav-title">{{ collection_name }}</div>
    </div>
  </section>
  {% endif %}

  <section id="library-grid" class="masonry-grid"></section>
  <div id="library-loading" class="library-loading">正在加载...</div>
  <div id="library-sentinel" style="height: 1px;"></div>
</main>

<div id="quick-view" aria-hidden="true">
  <div id="quick-feed-layer">
    <video id="quick-video" class="quick-media" playsinline preload="metadata"></video>
    <img id="quick-image" class="quick-media" alt="" />
    <div id="quick-overlay"></div>
    <div id="quick-play-status" aria-hidden="true">
      <svg id="quick-play-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
    </div>
  </div>

  <button id="quick-close-top" type="button" title="关闭">
    <i data-feather="x"></i>
  </button>
  <div class="quick-counter" id="quick-counter">1 / 1</div>

  <div id="quick-controls" class="is-hidden">
    <span class="quick-time" id="quick-time-current">00:00</span>
    <div class="quick-progress-wrap">
      <input type="range" id="quick-progress" min="0" max="100" step="0.1" value="0" />
      <div class="quick-progress-track"></div>
      <div class="quick-progress-fill" id="quick-progress-fill"></div>
    </div>
    <span class="quick-time" id="quick-time-total">00:00</span>
  </div>

  <div class="quick-actions">
    <button id="quick-speed" class="quick-btn" type="button" title="倍速">1x</button>
    <button id="quick-caption" class="quick-btn" type="button" title="生成标题" hidden>
      <i data-feather="zap"></i>
    </button>

    <div class="quick-image-tool-wrap" id="quick-magnifier-tool-wrap" hidden>
      <div class="quick-zoom-options" id="quick-zoom-options">
        <button type="button" class="quick-zoom-btn active" data-level="2.5">2.5x</button>
        <button type="button" class="quick-zoom-btn" data-level="5">5x</button>
      </div>
      <button id="quick-magnifier-toggle" class="quick-btn" type="button" title="放大镜">
        <i data-feather="search"></i>
      </button>
    </div>

    <button id="quick-favorite" class="quick-btn" type="button" title="收藏">
      <i data-feather="heart"></i>
    </button>
    <button id="quick-collection" class="quick-btn" type="button" title="加入集合">
      <i data-feather="bookmark"></i>
      <span class="collection-count" id="quick-collection-count">0</span>
    </button>
    <button id="quick-set-cover" class="quick-btn is-hidden" type="button" title="设为集合封面">
      <i data-feather="image"></i>
    </button>
    <a id="quick-source" class="quick-btn is-hidden" href="#" target="_blank" rel="noopener noreferrer" title="查看来源">
      <i data-feather="external-link"></i>
    </a>
    <a id="quick-detail" class="quick-btn" href="#" title="查看详情">
      <i data-feather="info"></i>
    </a>
  </div>

  <div class="quick-caption-panel is-hidden" id="quick-caption-panel">
    <div class="quick-caption-title" id="quick-caption-title"></div>
    <div class="quick-caption-tags" id="quick-caption-tags"></div>
  </div>

  <div class="quick-magnifier" id="quick-magnifier">
    <div class="quick-mag-badge" id="quick-mag-badge">2.5x</div>
  </div>
</div>

<div id="quick-collection-modal" class="quick-collection-modal" aria-hidden="true">
  <div class="quick-collection-card">
    <div class="coll-drag-handle"></div>
    <div class="quick-collection-header">
      <div class="quick-collection-title-row">
        <h3>加入集合</h3>
        <button id="quick-collection-close" type="button" class="quick-collection-close">
          <i data-feather="x"></i>
        </button>
      </div>
      <div id="quick-collection-meta" class="quick-collection-meta">当前未加入任何集合</div>
    </div>
    <div id="quick-collection-list" class="quick-collection-list"></div>
    <div class="quick-collection-create">
      <input id="quick-collection-name" type="text" maxlength="80" placeholder="新建集合名称" />
      <button id="quick-collection-create-btn" type="button">新建</button>
    </div>
  </div>
</div>
{% endblock %}

{% block extra_body %}
<script src="{{ url_for('static', filename='flow_ui_shared.js') }}"></script>
<script src="{{ url_for('static', filename='flow_state_controller.js') }}"></script>
<script src="{{ url_for('static', filename='flow_session.js') }}"></script>
<script src="{{ url_for('static', filename='flow_actions_shared.js') }}"></script>
<script src="{{ url_for('static', filename='flow_media_actions_controller.js') }}"></script>
<script src="{{ url_for('static', filename='hammer.min.js') }}"></script>
<script>
  (() => {
    const scope = {{ scope | tojson }};
    const collectionId = {{ collection_id | tojson }};
    const pageSize = {{ page_size }};
    const initialItems = {{ initial_items | tojson }};
    const initialHasMore = {{ 'true' if initial_has_more else 'false' }};
    const initialNextOffset = {{ initial_next_offset }};
    const initialMode = {{ active_mode | tojson }};
    const initialSeed = {{ mode_seed | tojson }};
    const minMb = {{ min_mb }};
    const emptyMessage = {{ empty_message | tojson }};
    const focusName = new URLSearchParams(window.location.search).get('focus') || '';

    const grid = document.getElementById('library-grid');
    const loadingEl = document.getElementById('library-loading');
    const sentinel = document.getElementById('library-sentinel');

    const quickView = document.getElementById('quick-view');
    const quickOverlay = document.getElementById('quick-overlay');
    const quickVideo = document.getElementById('quick-video');
    const quickImage = document.getElementById('quick-image');
    const quickPlayStatus = document.getElementById('quick-play-status');
    const quickPlayIcon = document.getElementById('quick-play-icon');
    const quickCloseTop = document.getElementById('quick-close-top');
    const quickCounter = document.getElementById('quick-counter');
    const quickControls = document.getElementById('quick-controls');
    const quickProgress = document.getElementById('quick-progress');
    const quickProgressFill = document.getElementById('quick-progress-fill');
    const quickTimeCurrent = document.getElementById('quick-time-current');
    const quickTimeTotal = document.getElementById('quick-time-total');

    const quickSpeed = document.getElementById('quick-speed');
    const quickCaption = document.getElementById('quick-caption');
    const quickMagnifierToolWrap = document.getElementById('quick-magnifier-tool-wrap');
    const quickMagnifierToggle = document.getElementById('quick-magnifier-toggle');
    const quickZoomOptions = document.getElementById('quick-zoom-options');
    const quickMagBadge = document.getElementById('quick-mag-badge');
    const quickMagnifier = document.getElementById('quick-magnifier');
    const quickCaptionPanel = document.getElementById('quick-caption-panel');
    const quickCaptionTitle = document.getElementById('quick-caption-title');
    const quickCaptionTags = document.getElementById('quick-caption-tags');
    const uiShared = window.FlowUIShared || {};
    const actionsShared = window.FlowActionsShared || {};

    const quickFavorite = document.getElementById('quick-favorite');
    const quickCollection = document.getElementById('quick-collection');
    const quickCollectionCount = document.getElementById('quick-collection-count');
    const quickSetCover = document.getElementById('quick-set-cover');
    const quickSource = document.getElementById('quick-source');
    const quickDetail = document.getElementById('quick-detail');
    const quickCollectionModal = document.getElementById('quick-collection-modal');
    const quickCollectionClose = document.getElementById('quick-collection-close');
    const quickCollectionMeta = document.getElementById('quick-collection-meta');

    const quickCollectionCreateBtn = document.getElementById('quick-collection-create-btn');
    const quickCollectionNameInput = document.getElementById('quick-collection-name');
    const quickCollectionList = document.getElementById('quick-collection-list');

    let mode = scope === 'all' ? initialMode : 'all';
    let seed = initialSeed || '';
    const collectionStateCache = new Map();
    let collectionCatalog = [];
    let collectionSelectedIds = new Set();
    let collectionSelectedNames = [];
    let collectionModalOpenedAt = 0;
    const collectionModalGuardMs = 520;
    const flowSession = window.createFlowSession({
      initialItems,
      initialHasMore: !!initialHasMore,
      initialCursor: { offset: Number(initialNextOffset || 0) },
      keyOf: (item) => String(item?.name || ''),
    });
    const items = flowSession.items;
    let currentSpeedIndex = 1;
    let clickTimer = null;
    let lastClickTime = 0;
    let wheelLocked = false;
    let isDragging = false;
    let wasPlayingBeforeDrag = false;
    let bodyOverflowBackup = '';
    let bodyScrollLocked = false;

    let isMagnifying = false;
    let magX = 0;
    let magY = 0;
    let zoomLevel = 2.5;
    let magnifierFrameRequest = null;
    let focusHandled = false;
    let focusLoading = false;
    const speedOptions = [0.75, 1, 1.25, 1.5, 2];

    function formatTime(seconds) {
      return uiShared.formatTime(seconds);
    }

    function makeRandomSeed() {
      return `${Date.now()}-${Math.floor(Math.random() * 100000)}`;
    }

    function currentItem() {
      return flowSession.currentItem();
    }

    function getCurrentIndex() {
      return flowSession.getIndex();
    }

    function setCurrentIndex(next) {
      return flowSession.setIndex(next);
    }

    const flowState = window.createFlowStateController({
      getMediaType: () => currentItem()?.type || '',
      canMagnifyMedia: (mediaType) => mediaType === 'image' || mediaType === 'video',
      onImmersiveChange: (enabled) => {
        quickView.classList.toggle('immersive', !!enabled);
      },
      onMagnifyingChange: (enabled) => {
        isMagnifying = !!enabled;
        if (!isMagnifying) {
          cancelMagnifierFrameRequest();
          quickMagnifierToggle.classList.remove('is-active');
          quickZoomOptions.classList.remove('show');
          quickMagnifier.classList.remove('active');
        }
      },
    });

    function showPlayStatus(paused) {
      const isPaused = !!paused;
      quickPlayIcon.innerHTML = isPaused
        ? '<rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect>'
        : '<polygon points="5 3 19 12 5 21 5 3"></polygon>';
      quickPlayStatus.classList.add('visible');
      setTimeout(() => quickPlayStatus.classList.remove('visible'), 220);
    }

    function setImmersive(enabled) {
      flowState.setImmersive(enabled);
    }

    function toggleUI() {
      flowState.toggleImmersive();
    }

    function activeTab() {
      document.querySelectorAll('.mode-tab').forEach((tab) => {
        tab.classList.toggle('is-active', tab.dataset.mode === mode);
      });
    }

    function getTileAspectRatio(item) {
      const width = Number(item?.width || 0);
      const height = Number(item?.height || 0);
      if (width > 0 && height > 0) {
        return `${width} / ${height}`;
      }
      return item?.type === 'video' ? '16 / 9' : '4 / 5';
    }

    function getTileHeightRatio(item) {
      const width = Number(item?.width || 0);
      const height = Number(item?.height || 0);
      if (width > 0 && height > 0) {
        return height / width;
      }
      return item?.type === 'video' ? (9 / 16) : (5 / 4);
    }

    const waterfall = {
      count: 0,
      gap: 8,
      columnWidth: 0,
      columns: [],
      heights: [],
    };
    let relayoutTimer = null;

    function getWaterfallColumnCount() {
      const width = window.innerWidth;
      if (width >= 1536) return 5;
      if (width >= 1100) return 4;
      if (width >= 768) return 3;
      return 2;
    }

    function getWaterfallGap() {
      return window.innerWidth >= 768 ? 10 : 8;
    }

    function updateWaterfallMetrics() {
      const count = Math.max(1, waterfall.count || getWaterfallColumnCount());
      const gap = getWaterfallGap();
      const gridWidth = Math.max(1, grid.clientWidth || window.innerWidth);
      waterfall.gap = gap;
      waterfall.columnWidth = Math.max(1, (gridWidth - gap * (count - 1)) / count);
      grid.style.setProperty('--wf-gap', `${gap}px`);
    }

    function resetWaterfallLayout() {
      waterfall.count = 0;
      waterfall.columnWidth = 0;
      waterfall.columns = [];
      waterfall.heights = [];
      grid.innerHTML = '';
    }

    function ensureWaterfallLayout(force = false) {
      const nextCount = getWaterfallColumnCount();
      if (!force && waterfall.count === nextCount && waterfall.columns.length) {
        updateWaterfallMetrics();
        return;
      }

      waterfall.count = nextCount;
      grid.innerHTML = '';
      waterfall.columns = [];
      waterfall.heights = new Array(nextCount).fill(0);
      updateWaterfallMetrics();

      for (let i = 0; i < nextCount; i += 1) {
        const col = document.createElement('div');
        col.className = 'waterfall-col';
        grid.appendChild(col);
        waterfall.columns.push(col);
      }
    }

    function pickShortestColumnIndex() {
      if (!waterfall.heights.length) return 0;
      let target = 0;
      let minHeight = waterfall.heights[0];
      for (let i = 1; i < waterfall.heights.length; i += 1) {
        if (waterfall.heights[i] < minHeight) {
          minHeight = waterfall.heights[i];
          target = i;
        }
      }
      return target;
    }

    function estimateTileHeight(item) {
      return waterfall.columnWidth * getTileHeightRatio(item);
    }

    function createTile(item, index) {
      const tile = document.createElement('article');
      tile.className = 'media-tile';
      tile.dataset.index = String(index);
      const ratio = getTileAspectRatio(item);

      if (item.type === 'video') {
        const img = document.createElement('img');
        img.src = item.thumb_url;
        img.alt = '';
        img.loading = 'lazy';
        img.style.aspectRatio = ratio;
        tile.appendChild(img);

        const badge = document.createElement('span');
        badge.className = 'tile-video-badge';
        badge.innerHTML = feather.icons.play.toSvg({ width: 12, height: 12 });
        tile.appendChild(badge);
      } else {
        const img = document.createElement('img');
        img.src = item.media_url;
        img.alt = '';
        img.loading = 'lazy';
        img.style.aspectRatio = ratio;
        tile.appendChild(img);
      }

      tile.addEventListener('click', () => openViewer(index));
      return tile;
    }

    function appendIndexToWaterfall(index) {
      const item = items[index];
      if (!item) return;
      const colIndex = pickShortestColumnIndex();
      const col = waterfall.columns[colIndex];
      if (!col) return;
      col.appendChild(createTile(item, index));
      waterfall.heights[colIndex] += estimateTileHeight(item) + waterfall.gap;
    }

    function relayoutWaterfall() {
      if (!items.length) {
        resetWaterfallLayout();
        ensureWaterfallLayout(true);
        return;
      }
      ensureWaterfallLayout(true);
      updateWaterfallMetrics();
      for (let i = 0; i < items.length; i += 1) {
        appendIndexToWaterfall(i);
      }
      feather.replace();
    }

    function scheduleWaterfallRelayout() {
      if (relayoutTimer) clearTimeout(relayoutTimer);
      relayoutTimer = setTimeout(() => {
        relayoutTimer = null;
        relayoutWaterfall();
      }, 180);
    }

    function setLoadingText(text) {
      loadingEl.textContent = text;
    }

    function findItemIndexByName(name) {
      const expected = String(name || '').trim();
      if (!expected) return -1;
      for (let i = 0; i < items.length; i += 1) {
        if (String(items[i]?.name || '') === expected) return i;
      }
      return -1;
    }

    function apiParams(offset) {
      const params = new URLSearchParams({
        scope,
        mode,
        offset: String(offset),
        limit: String(pageSize),
      });
      if (scope === 'collection' && collectionId) {
        params.set('collection_id', String(collectionId));
      }
      if (mode === 'big_files') {
        params.set('min_mb', String(minMb));
      }
      if (mode === 'image_random') {
        if (!seed) seed = makeRandomSeed();
        params.set('seed', seed);
      }
      return params;
    }

    async function loadNextPage() {
      if (flowSession.isLoading() || !flowSession.hasMore()) return;
      setLoadingText('正在加载...');
      try {
        await flowSession.loadMore(async (cursor) => {
          const offset = Number(cursor?.offset || 0);
          const res = await fetch(`/api/library/items?${apiParams(offset).toString()}`);
          const data = await res.json();
          if (!data?.success) throw new Error('request failed');

          const payload = data.data || {};
          if (payload.seed && mode === 'image_random') seed = String(payload.seed);
          return {
            items: Array.isArray(payload.items) ? payload.items : [],
            hasMore: payload.has_more === true,
            cursor: { offset: Number(payload.next_offset || (offset + pageSize)) },
          };
        }).then((result) => {
          const appendedIndexes = result.appendedIndices || [];
          if (appendedIndexes.length) {
            ensureWaterfallLayout(!waterfall.columns.length);
            updateWaterfallMetrics();
            appendedIndexes.forEach((idx) => appendIndexToWaterfall(idx));
            feather.replace();
            if (quickView.classList.contains('active') && getCurrentIndex() >= 0) {
              quickCounter.textContent = `${getCurrentIndex() + 1} / ${items.length}`;
            }
          }
        });

        if (!items.length) {
          setLoadingText(emptyMessage);
        } else if (!flowSession.hasMore()) {
          setLoadingText('到底了');
        } else {
          setLoadingText('继续下滑加载更多');
        }
      } catch (error) {
        setLoadingText('加载失败，下滑重试');
      }
    }

    async function maybeOpenFocusedItem() {
      if (!focusName || focusHandled || focusLoading) return;
      const directIndex = findItemIndexByName(focusName);
      if (directIndex >= 0) {
        focusHandled = true;
        openViewer(directIndex);
        return;
      }
      if (!flowSession.hasMore()) {
        focusHandled = true;
        return;
      }

      focusLoading = true;
      try {
        while (!focusHandled && flowSession.hasMore()) {
          await loadNextPage();
          const index = findItemIndexByName(focusName);
          if (index >= 0) {
            focusHandled = true;
            openViewer(index);
            return;
          }
        }
        if (!flowSession.hasMore()) {
          focusHandled = true;
        }
      } finally {
        focusLoading = false;
      }
    }

    async function reloadMode(nextMode) {
      closeViewer();
      mode = nextMode;
      if (mode === 'image_random') seed = makeRandomSeed();
      else seed = '';

      flowSession.reset({
        hasMore: true,
        cursor: { offset: 0 },
      });
      resetWaterfallLayout();
      activeTab();
      setLoadingText('正在加载...');
      await loadNextPage();
    }

    async function collectionsRequest(url, options = {}) {
      const response = await fetch(url, options);
      let payload = null;
      try {
        payload = await response.json();
      } catch (error) {
        payload = null;
      }
      if (!response.ok || !(payload && payload.success)) {
        throw new Error((payload && payload.error) || '请求失败');
      }
      return payload.data || {};
    }

    function flashActionButton(buttonEl) {
      if (!buttonEl) return;
      buttonEl.classList.add('is-active');
      setTimeout(() => buttonEl.classList.remove('is-active'), 220);
    }

    function escapeHtml(value) {
      return String(value || '')
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
    }

    function setCollectionMeta(names) {
      if (!quickCollectionMeta) return;
      const items = Array.isArray(names) ? names.filter(Boolean) : [];
      if (!items.length) {
        quickCollectionMeta.textContent = '当前未加入任何集合';
        return;
      }
      const preview = items.slice(0, 2).join('、');
      const rest = items.length - 2;
      quickCollectionMeta.textContent = rest > 0 ? `已加入：${preview} +${rest}` : `已加入：${preview}`;
    }

    function setCollectionButtonState(names) {
      const items = Array.isArray(names) ? names.filter(Boolean) : [];
      const count = items.length;
      quickCollection.classList.toggle('has-collection', count > 0);
      quickCollectionCount.textContent = count > 99 ? '99+' : String(count);
      const preview = items.slice(0, 2).join('、');
      const suffix = items.length > 2 ? ` +${items.length - 2}` : '';
      quickCollection.title = count > 0 ? `已加入：${preview}${suffix}` : '加入集合';
    }

    async function fetchCollectionMembership(uri, force = false) {
      const key = String(uri || '').trim();
      if (!key) return { ids: new Set(), names: [] };
      if (!force && collectionStateCache.has(key)) {
        const cached = collectionStateCache.get(key) || {};
        return {
          ids: new Set(Array.isArray(cached.ids) ? cached.ids : []),
          names: Array.isArray(cached.names) ? cached.names.slice() : [],
        };
      }
      const encoded = encodeURIComponent(key);
      const selectedData = await collectionsRequest(`/api/collections/by-media?uri=${encoded}`);
      const selectedItems = Array.isArray(selectedData.items) ? selectedData.items : [];
      const ids = [];
      const names = [];
      selectedItems.forEach((entry) => {
        const id = String(entry?.id || '').trim();
        const name = String(entry?.name || '').trim();
        if (id) ids.push(id);
        if (name) names.push(name);
      });
      collectionStateCache.set(key, { ids, names });
      return { ids: new Set(ids), names };
    }

    async function syncCollectionState(item, force = false) {
      const expectedName = String(item?.name || '');
      if (!expectedName) {
        collectionSelectedIds = new Set();
        collectionSelectedNames = [];
        setCollectionButtonState([]);
        setCollectionMeta([]);
        return;
      }
      try {
        const state = await fetchCollectionMembership(expectedName, force);
        if (String(currentItem()?.name || '') !== expectedName) return;
        collectionSelectedIds = new Set(state.ids);
        collectionSelectedNames = state.names.slice();
        setCollectionButtonState(collectionSelectedNames);
        setCollectionMeta(collectionSelectedNames);
      } catch (error) {
        if (String(currentItem()?.name || '') !== expectedName) return;
        collectionSelectedIds = new Set();
        collectionSelectedNames = [];
        setCollectionButtonState([]);
        setCollectionMeta([]);
      }
    }

    function updateCollectionCoverButton(item) {
      const shouldShow = scope === 'collection' && !!collectionId && !!item?.name;
      quickSetCover.classList.toggle('is-hidden', !shouldShow);
    }

    function closeCollectionModal() {
      if (!quickCollectionModal) return;
      quickCollectionModal.classList.remove('active');
      quickCollectionModal.setAttribute('aria-hidden', 'true');
      quickCollectionCreateBtn.disabled = false;
      collectionModalOpenedAt = 0;
    }

    function renderCollectionList(items, selectedIds) {
      if (!quickCollectionList) return;
      if (!Array.isArray(items) || !items.length) {
        quickCollectionList.innerHTML = '<div class="quick-collection-empty">暂无集合，先新建一个。</div>';
        return;
      }
      quickCollectionList.innerHTML = items.map((item) => {
        const id = String(item.id || '');
        const safeName = escapeHtml(String(item.name || '未命名集合'));
        const count = Number(item.item_count || 0);
        const checked = selectedIds.has(id) ? 'checked' : '';
        const isSelected = selectedIds.has(id) ? 'is-selected' : '';
        return `
          <label class="quick-collection-item ${isSelected}" data-id="${id}">
            <span class="coll-icon">
              <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
            </span>
            <span class="coll-name">${safeName}</span>
            <em class="coll-count">${count > 0 ? count : ''}</em>
            <span class="coll-check">
              <svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
            </span>
            <input type="checkbox" ${checked} />
          </label>
        `;
      }).join('');
    }

    async function openCollectionModal() {
      const item = currentItem();
      if (!item || !item.name || !quickCollectionModal) return;
      collectionModalOpenedAt = Date.now();
      quickCollectionModal.classList.add('active');
      quickCollectionModal.setAttribute('aria-hidden', 'false');
      quickCollectionList.innerHTML = '<div class="quick-collection-empty">加载中...</div>';
      try {
        const [allData, selectedState] = await Promise.all([
          collectionsRequest('/api/collections'),
          fetchCollectionMembership(item.name, true),
        ]);
        const allItems = Array.isArray(allData.items) ? allData.items : [];
        collectionCatalog = allItems;
        collectionSelectedIds = new Set(selectedState.ids);
        collectionSelectedNames = selectedState.names.slice();
        setCollectionButtonState(collectionSelectedNames);
        setCollectionMeta(collectionSelectedNames);
        renderCollectionList(allItems, collectionSelectedIds);
      } catch (error) {
        quickCollectionList.innerHTML = '<div class="quick-collection-empty">加载失败，请重试。</div>';
      }
      feather.replace();
    }

    async function createCollectionInModal() {
      const name = String(quickCollectionNameInput?.value || '').trim();
      if (!name) return;
      quickCollectionCreateBtn.disabled = true;
      try {
        const createdData = await collectionsRequest('/api/collections', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name }),
        });
        const createdId = String(createdData?.item?.id || '').trim();
        const item = currentItem();
        if (createdId && item?.name) {
          await collectionsRequest(`/api/collections/${encodeURIComponent(createdId)}/items`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ uris: [item.name] }),
          });
          collectionStateCache.delete(item.name);
        }
        if (quickCollectionNameInput) quickCollectionNameInput.value = '';
        await openCollectionModal();
      } catch (error) {
        quickCollectionCreateBtn.disabled = false;
        return;
      }
      quickCollectionCreateBtn.disabled = false;
    }

    async function toggleCollectionMembership(collectionId, checked, rowEl, inputEl) {
      const item = currentItem();
      if (!item || !item.name) return;
      if (!collectionId) return;
      rowEl?.classList.add('is-pending');
      if (inputEl) inputEl.disabled = true;
      try {
        await collectionsRequest(`/api/collections/${encodeURIComponent(collectionId)}/items`, {
          method: checked ? 'POST' : 'DELETE',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ uris: [item.name] }),
        });
        await syncCollectionState(item, true);
        collectionCatalog = collectionCatalog.map((entry) => {
          if (String(entry?.id || '') !== collectionId) return entry;
          const baseCount = Number(entry?.item_count || 0);
          const nextCount = checked ? (baseCount + 1) : Math.max(0, baseCount - 1);
          return { ...entry, item_count: nextCount };
        });
        renderCollectionList(collectionCatalog, collectionSelectedIds);
      } catch (error) {
        if (inputEl) inputEl.checked = !checked;
        return;
      } finally {
        rowEl?.classList.remove('is-pending');
        if (inputEl) inputEl.disabled = false;
      }
    }

    async function syncFavoriteState(item) {
      if (!item?.name) return;
      try {
        await mediaActions.syncFavorite(item.name);
      } catch (error) {
        updateFavoriteButton(false);
      }
    }

    function updateFavoriteButton(favorited) {
      quickFavorite.classList.toggle('is-favorited', !!favorited);
    }

    function updateSourceButton(sourceMeta) {
      const url = String(sourceMeta?.url || '').trim();
      if (!url) {
        quickSource.classList.add('is-hidden');
        quickSource.removeAttribute('href');
        quickSource.title = '未记录来源';
        return;
      }
      const domain = String(sourceMeta?.source_domain || '').trim();
      quickSource.href = url;
      quickSource.title = domain ? `查看来源（${domain}）` : '查看来源';
        quickSource.classList.remove('is-hidden');
    }

    function clearCaption() {
      quickCaptionTitle.textContent = '';
      quickCaptionTags.innerHTML = '';
      quickCaptionPanel.classList.add('is-hidden');
    }

    function renderCaption(data) {
      const title = String(data?.title || '').trim();
      const tags = Array.isArray(data?.tags) ? data.tags : [];
      if (!title && tags.length === 0) {
        clearCaption(false);
        return;
      }
      quickCaptionPanel.classList.remove('is-hidden');
      quickCaptionTitle.textContent = title;
      quickCaptionTags.innerHTML = '';
      tags.forEach((tag) => {
        const chip = document.createElement('span');
        chip.className = 'quick-caption-tag';
        chip.textContent = String(tag || '');
        quickCaptionTags.appendChild(chip);
      });
    }

    function setCaptionLoading(loadingState) {
      quickCaption.disabled = !!loadingState;
      quickCaption.classList.toggle('is-loading', !!loadingState);
    }

    function flashCaptionError() {
      quickCaption.classList.add('is-error');
      setTimeout(() => quickCaption.classList.remove('is-error'), 1400);
    }

    const mediaActions = window.createFlowMediaActionsController({
      actions: actionsShared,
      onFavoriteChange: (value) => {
        updateFavoriteButton(value);
      },
      onSourceChange: (sourceMeta) => {
        updateSourceButton(sourceMeta);
      },
      onCaptionClear: () => {
        clearCaption();
      },
      onCaptionRender: (payload) => {
        renderCaption(payload);
      },
      onCaptionLoading: (loading) => {
        setCaptionLoading(loading);
      },
      onError: (kind) => {
        if (kind === 'caption_load' || kind === 'caption_generate') {
          flashCaptionError();
        }
      },
    });

    async function loadCaption(uri) {
      await mediaActions.loadCaption(uri);
    }

    async function generateCaption(uri) {
      await mediaActions.generateCaption(uri);
    }

    function currentImageEl() {
      const item = currentItem();
      if (!item || item.type !== 'image') return null;
      return quickImage;
    }

    function currentVideoEl() {
      const item = currentItem();
      if (!item || item.type !== 'video') return null;
      return quickVideo;
    }

    function getImageContainRect(imgEl) {
      return uiShared.getImageContainRect(imgEl);
    }

    function getVideoContainRect(videoEl) {
      return uiShared.getVideoContainRect(videoEl);
    }

    function updateMagnifierPosition() {
      uiShared.setMagnifierPosition(quickMagnifier, magX, magY);
    }

    function cancelMagnifierFrameRequest() {
      if (!magnifierFrameRequest) return;
      if (magnifierFrameRequest.kind === 'rvfc') {
        const videoEl = magnifierFrameRequest.videoEl;
        if (videoEl && typeof videoEl.cancelVideoFrameCallback === 'function') {
          try {
            videoEl.cancelVideoFrameCallback(magnifierFrameRequest.id);
          } catch (error) {
            // Ignore cancellation failures when media element lifecycle changes.
          }
        }
      } else if (magnifierFrameRequest.kind === 'raf') {
        cancelAnimationFrame(magnifierFrameRequest.id);
      } else if (magnifierFrameRequest.kind === 'timeout') {
        clearTimeout(magnifierFrameRequest.id);
      }
      magnifierFrameRequest = null;
    }

    function scheduleMagnifierFrameLoop(videoEl) {
      cancelMagnifierFrameRequest();
      if (!videoEl) return;

      const tick = () => {
        magnifierFrameRequest = null;
        if (!isMagnifying) return;
        const activeVideo = currentVideoEl();
        if (!activeVideo || activeVideo !== videoEl) return;
        updateMagnifierContent();

        if (videoEl.paused || videoEl.ended) {
          const timeoutId = setTimeout(() => {
            scheduleMagnifierFrameLoop(videoEl);
          }, 120);
          magnifierFrameRequest = { kind: 'timeout', id: timeoutId, videoEl };
          return;
        }
        scheduleMagnifierFrameLoop(videoEl);
      };

      if (typeof videoEl.requestVideoFrameCallback === 'function') {
        const id = videoEl.requestVideoFrameCallback(() => tick());
        magnifierFrameRequest = { kind: 'rvfc', id, videoEl };
      } else {
        const id = requestAnimationFrame(tick);
        magnifierFrameRequest = { kind: 'raf', id, videoEl };
      }
    }

    function updateMagnifierContent() {
      const item = currentItem();
      if (!item) return;

      if (item.type === 'video') {
        const videoEl = currentVideoEl();
        if (!videoEl) return;
        uiShared.updateVideoMagnifierContent({
          videoEl,
          lensEl: quickMagnifier,
          centerX: magX,
          centerY: magY,
          zoomLevel,
          maxPixelRatio: 2,
        });
        return;
      }

      const imgEl = currentImageEl();
      if (!imgEl) return;
      uiShared.updateMagnifierContent({
        imageEl: imgEl,
        lensEl: quickMagnifier,
        centerX: magX,
        centerY: magY,
        zoomLevel,
      });
    }

    function applyZoom(level) {
      zoomLevel = level;
      quickMagBadge.textContent = `${level}x`;
      document.querySelectorAll('.quick-zoom-btn').forEach((btn) => {
        const btnLevel = Number.parseFloat(btn.dataset.level || '2.5');
        btn.classList.toggle('active', btnLevel === level);
      });
      if (level >= 5) quickMagnifier.classList.add('quick-magnifier-large');
      else quickMagnifier.classList.remove('quick-magnifier-large');
      if (isMagnifying) {
        setTimeout(updateMagnifierContent, 30);
      }
    }

    function toggleMagnifier(active) {
      isMagnifying = flowState.setMagnifying(active);
      if (isMagnifying) {
        quickMagnifierToggle.classList.add('is-active');
        quickZoomOptions.classList.add('show');
        quickMagnifier.classList.add('active');
        const item = currentItem();
        const rect = item?.type === 'video'
          ? getVideoContainRect(currentVideoEl())
          : getImageContainRect(currentImageEl());
        if (rect) {
          magX = rect.left + rect.width / 2;
          magY = rect.top + rect.height / 2;
        } else {
          magX = window.innerWidth / 2;
          magY = window.innerHeight / 2;
        }
        updateMagnifierPosition();
        updateMagnifierContent();
        if (item?.type === 'video') {
          scheduleMagnifierFrameLoop(currentVideoEl());
        } else {
          cancelMagnifierFrameRequest();
        }
      } else {
        cancelMagnifierFrameRequest();
        quickMagnifierToggle.classList.remove('is-active');
        quickZoomOptions.classList.remove('show');
        quickMagnifier.classList.remove('active');
      }
    }

    function updateControls(item) {
      const isVideo = item?.type === 'video';
      quickControls.classList.toggle('is-hidden', !isVideo);
      quickSpeed.hidden = !isVideo;
      quickCaption.hidden = isVideo;
      quickMagnifierToolWrap.hidden = false;

      if (!isVideo) {
        quickProgress.disabled = true;
        quickProgress.value = 0;
        quickProgressFill.style.width = '0%';
        quickTimeCurrent.textContent = '';
        quickTimeTotal.textContent = '';
        quickPlayStatus.classList.remove('visible');
      } else {
        quickProgress.disabled = false;
      }
    }

    function updateVideoProgress() {
      const item = currentItem();
      if (!item || item.type !== 'video' || isDragging) return;
      const duration = quickVideo.duration;
      if (duration && !Number.isNaN(duration) && duration !== Infinity) {
        quickTimeTotal.textContent = formatTime(duration);
      }
      const pct = duration ? (quickVideo.currentTime / duration) * 100 : 0;
      const safePct = Number.isNaN(pct) ? 0 : pct;
      quickProgress.value = safePct;
      quickProgressFill.style.width = `${safePct}%`;
      quickTimeCurrent.textContent = formatTime(quickVideo.currentTime);
    }

    function togglePlay() {
      const item = currentItem();
      if (!item || item.type !== 'video') return;
      if (quickVideo.paused) {
        quickVideo.play().catch(() => {});
        showPlayStatus(false);
      } else {
        quickVideo.pause();
        showPlayStatus(true);
      }
    }

    function closeViewer() {
      flowState.reset();
      closeCollectionModal();
      quickView.classList.remove('active');
      quickView.setAttribute('aria-hidden', 'true');
      quickVideo.pause();
      quickVideo.removeAttribute('src');
      quickVideo.removeAttribute('poster');
      quickVideo.load();
      quickVideo.classList.remove('active');
      quickImage.classList.remove('active');
      quickImage.src = '';
      quickPlayStatus.classList.remove('visible');
      quickProgress.value = 0;
      quickProgressFill.style.width = '0%';
      quickTimeCurrent.textContent = '00:00';
      quickTimeTotal.textContent = '00:00';
      updateSourceButton(null);
      clearCaption();
      toggleMagnifier(false);
      updateCollectionCoverButton(null);
      setCollectionButtonState([]);
      setCollectionMeta([]);
      setCurrentIndex(-1);
      if (bodyScrollLocked) {
        document.body.style.overflow = bodyOverflowBackup;
        bodyOverflowBackup = '';
        bodyScrollLocked = false;
      }
    }

    async function showItem(index) {
      if (index < 0 || index >= items.length) return;
      setCurrentIndex(index);
      const item = items[index];
      quickCounter.textContent = `${index + 1} / ${items.length}`;
      quickDetail.href = item.detail_url || '#';
      toggleMagnifier(false);
      flowState.onMediaChanged();
      updateControls(item);
      updateCollectionCoverButton(item);

      if (item.type === 'video') {
        clearCaption();
        quickImage.classList.remove('active');
        quickImage.src = '';
        const nextSrc = String(item.media_url || '');
        if (quickVideo.getAttribute('src') !== nextSrc) {
          quickVideo.src = nextSrc;
          quickVideo.load();
        }
        quickVideo.poster = item.thumb_url || '';
        quickVideo.classList.add('active');
        quickVideo.playbackRate = speedOptions[currentSpeedIndex];
        quickProgress.value = 0;
        quickProgressFill.style.width = '0%';
        quickTimeCurrent.textContent = '00:00';
        quickTimeTotal.textContent = formatTime(quickVideo.duration);
        quickVideo.play()
          .then(() => quickPlayStatus.classList.remove('visible'))
          .catch(() => showPlayStatus(true));
      } else {
        quickVideo.pause();
        quickVideo.classList.remove('active');
        quickVideo.removeAttribute('src');
        quickVideo.removeAttribute('poster');
        quickVideo.load();
        quickImage.src = item.media_url;
        quickImage.classList.add('active');
        await loadCaption(item.name);
      }

      syncFavoriteState(item);
      syncCollectionState(item);
      syncSourceState(item);
      if (index >= items.length - 8 && flowSession.hasMore()) loadNextPage();
    }

    function openViewer(index) {
      if (index < 0 || index >= items.length) return;
      if (!quickView.classList.contains('active')) {
        bodyOverflowBackup = document.body.style.overflow || '';
        document.body.style.overflow = 'hidden';
        bodyScrollLocked = true;
      }
      quickView.classList.add('active');
      quickView.setAttribute('aria-hidden', 'false');
      showItem(index);
    }

    async function nextItem() {
      if (getCurrentIndex() >= items.length - 1 && flowSession.hasMore()) await loadNextPage();
      if (getCurrentIndex() < items.length - 1) await showItem(getCurrentIndex() + 1);
    }

    async function prevItem() {
      if (getCurrentIndex() > 0) await showItem(getCurrentIndex() - 1);
    }

    async function syncSourceState(item) {
      if (!item?.name) {
        updateSourceButton(null);
        return;
      }
      try {
        await mediaActions.syncSource(item.name);
      } catch (error) {
        updateSourceButton(null);
      }
    }

    quickFavorite.addEventListener('click', async (event) => {
      event.preventDefault();
      event.stopPropagation();
      const idx = getCurrentIndex();
      if (idx < 0 || idx >= items.length) return;
      const item = items[idx];
      if (!item?.name) return;
      try {
        await mediaActions.toggleFavorite(item.name);
      } catch (error) {
        await syncFavoriteState(item);
      }
    });

    async function setCurrentAsCollectionCover() {
      const item = currentItem();
      if (!item || !item.name || scope !== 'collection' || !collectionId) return;
      quickSetCover.disabled = true;
      try {
        await collectionsRequest(`/api/collections/${encodeURIComponent(collectionId)}`, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ cover_uri: item.name }),
        });
        flashActionButton(quickSetCover);
      } catch (error) {
        quickSetCover.disabled = false;
        return;
      }
      quickSetCover.disabled = false;
    }

    quickCollection.addEventListener('click', async (event) => {
      event.preventDefault();
      event.stopPropagation();
      await openCollectionModal();
    });
    quickCollection.addEventListener('keydown', async (event) => {
      if (event.key !== 'Enter' && event.key !== ' ') return;
      event.preventDefault();
      await openCollectionModal();
    });

    quickSetCover.addEventListener('click', async (event) => {
      event.preventDefault();
      event.stopPropagation();
      await setCurrentAsCollectionCover();
    });

    quickCollectionClose.addEventListener('click', (event) => {
      event.preventDefault();
      closeCollectionModal();
    });

    quickCollectionModal.addEventListener('click', (event) => {
      if (collectionModalOpenedAt && (Date.now() - collectionModalOpenedAt) < collectionModalGuardMs) {
        return;
      }
      if (event.target === quickCollectionModal) closeCollectionModal();
    });
    quickCollectionCreateBtn.addEventListener('click', async (event) => {
      event.preventDefault();
      event.stopPropagation();
      await createCollectionInModal();
    });
    quickCollectionNameInput.addEventListener('keydown', async (event) => {
      if (event.key !== 'Enter') return;
      event.preventDefault();
      await createCollectionInModal();
    });
    quickCollectionList.addEventListener('change', async (event) => {
      const inputEl = event.target.closest('input[type="checkbox"]');
      if (!inputEl) return;
      const rowEl = inputEl.closest('[data-id]');
      const collectionId = String(rowEl?.getAttribute('data-id') || '').trim();
      if (!collectionId) return;
      await toggleCollectionMembership(collectionId, !!inputEl.checked, rowEl, inputEl);
    });

    quickCloseTop.addEventListener('click', (event) => {
      event.preventDefault();
      event.stopPropagation();
      closeViewer();
    });

    document.addEventListener('keydown', (event) => {
      if (event.key === 'Escape' && quickCollectionModal.classList.contains('active')) {
        closeCollectionModal();
        return;
      }
      if (!quickView.classList.contains('active')) return;
      if (event.key === 'Escape') closeViewer();
      if (event.key === 'ArrowDown' || event.key === 'ArrowRight') nextItem();
      if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') prevItem();
      if (event.key === ' ') {
        event.preventDefault();
        togglePlay();
      }
    });

    quickOverlay.addEventListener('click', () => {
      const item = currentItem();
      if (!item) return;
      const now = Date.now();
      const delta = now - lastClickTime;
      if (delta > 0 && delta < 250) {
        if (clickTimer) clearTimeout(clickTimer);
        clickTimer = null;
        if (item.type === 'video') {
          togglePlay();
        }
      } else {
        if (clickTimer) clearTimeout(clickTimer);
        clickTimer = setTimeout(() => {
          toggleUI();
          clickTimer = null;
        }, 250);
      }
      lastClickTime = now;
    });

    quickSpeed.addEventListener('click', (event) => {
      event.preventDefault();
      event.stopPropagation();
      currentSpeedIndex = (currentSpeedIndex + 1) % speedOptions.length;
      const rate = speedOptions[currentSpeedIndex];
      quickSpeed.textContent = `${rate}x`;
      const item = currentItem();
      if (item?.type === 'video') {
        quickVideo.playbackRate = rate;
      }
    });

    quickCaption.addEventListener('click', (event) => {
      event.preventDefault();
      event.stopPropagation();
      const item = currentItem();
      if (!item || item.type !== 'image') return;
      generateCaption(item.name);
    });

    quickMagnifierToggle.addEventListener('click', (event) => {
      event.preventDefault();
      event.stopPropagation();
      toggleMagnifier(!isMagnifying);
    });

    document.querySelectorAll('.quick-zoom-btn').forEach((btn) => {
      btn.addEventListener('click', (event) => {
        event.preventDefault();
        event.stopPropagation();
        const level = Number.parseFloat(btn.dataset.level || '2.5');
        applyZoom(level);
      });
    });

    quickProgress.addEventListener('input', () => {
      const item = currentItem();
      if (!item || item.type !== 'video') return;

      if (!isDragging) {
        isDragging = true;
        wasPlayingBeforeDrag = !quickVideo.paused;
        quickVideo.pause();
      }
      const duration = quickVideo.duration || 0;
      const seekTime = (quickProgress.value / 100) * duration;
      quickTimeCurrent.textContent = formatTime(seekTime);
      quickProgressFill.style.width = `${quickProgress.value}%`;
    });

    quickProgress.addEventListener('change', () => {
      const item = currentItem();
      if (!item || item.type !== 'video') return;
      const duration = quickVideo.duration || 0;
      quickVideo.currentTime = (quickProgress.value / 100) * duration;
      if (wasPlayingBeforeDrag) {
        quickVideo.play().catch(() => {});
      }
      isDragging = false;
    });

    quickView.addEventListener('wheel', (event) => {
      if (!quickView.classList.contains('active')) return;
      const target = event.target;
      if (target && typeof target.closest === 'function') {
        if (target.closest('.quick-actions, #quick-controls, #quick-close-top, #quick-caption-panel, .quick-magnifier')) {
          return;
        }
      }
      if (isMagnifying) return;
      const deltaY = event.deltaY || 0;
      if (Math.abs(deltaY) < 24) return;
      event.preventDefault();
      if (wheelLocked) return;
      wheelLocked = true;
      if (deltaY > 0) nextItem();
      else prevItem();
      setTimeout(() => {
        wheelLocked = false;
      }, 260);
    }, { passive: false });

    const swipe = new Hammer(quickOverlay);
    swipe.get('swipe').set({ direction: Hammer.DIRECTION_VERTICAL });
    swipe.on('swipeup', () => {
      if (isMagnifying) return;
      nextItem();
    });
    swipe.on('swipedown', () => {
      if (isMagnifying) return;
      prevItem();
    });

    const magHammer = new Hammer(quickMagnifier);
    magHammer.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 0 });
    let startMagX = 0;
    let startMagY = 0;
    magHammer.on('panstart', () => {
      startMagX = magX;
      startMagY = magY;
      quickMagnifier.style.transition = 'none';
    });
    magHammer.on('panmove', (event) => {
      magX = startMagX + event.deltaX;
      magY = startMagY + event.deltaY;
      updateMagnifierPosition();
      updateMagnifierContent();
    });
    magHammer.on('panend', () => {
      quickMagnifier.style.transition = 'transform 0.2s ease, width 0.2s ease, height 0.2s ease';
    });

    window.addEventListener('resize', () => {
      if (isMagnifying) updateMagnifierContent();
      scheduleWaterfallRelayout();
    });

    quickVideo.addEventListener('timeupdate', updateVideoProgress);
    quickVideo.addEventListener('loadedmetadata', updateVideoProgress);
    quickVideo.addEventListener('durationchange', updateVideoProgress);
    quickVideo.addEventListener('play', () => {
      quickPlayStatus.classList.remove('visible');
      if (isMagnifying && currentVideoEl() === quickVideo) {
        scheduleMagnifierFrameLoop(quickVideo);
      }
    });
    quickVideo.addEventListener('seeked', () => {
      if (isMagnifying && currentVideoEl() === quickVideo) {
        updateMagnifierContent();
      }
    });
    quickVideo.addEventListener('pause', () => {
      const item = currentItem();
      if (item?.type === 'video') showPlayStatus(true);
    });

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) loadNextPage();
      });
    }, { rootMargin: '1000px 0px' });
    observer.observe(sentinel);

    if (scope === 'all') {
      document.querySelectorAll('.mode-tab').forEach((tab) => {
        tab.addEventListener('click', () => {
          const targetMode = tab.dataset.mode || 'all';
          if (targetMode === mode) return;
          reloadMode(targetMode);
        });
      });
      activeTab();
    }

    quickSpeed.textContent = `${speedOptions[currentSpeedIndex]}x`;
    applyZoom(2.5);
    relayoutWaterfall();
    if (!items.length && !flowSession.hasMore()) {
      setLoadingText(emptyMessage);
    } else if (!flowSession.hasMore()) {
      setLoadingText('到底了');
    } else {
      setLoadingText('继续下滑加载更多');
    }
    feather.replace();
    maybeOpenFocusedItem();
  })();
</script>
{% endblock %}
