// ==UserScript== // @name PikPak 增强大师 // @name:zh-CN PikPak 增强大师 // @name:zh-TW PikPak 增強大師 // @name:en PikPak Enhancement Master // @name:ko PikPak 인핸서 마스터 // @name:ja PikPak 拡張マスター // @namespace https://github.com/digbug82/ // @version 1.2.0 // @author digbug82 // @license CC-BY-NC-SA-4.0 // @description 桌面级PikPak网盘管家!包含多模态文件查重(哈希/时长/名称)、多模态文件夹查重(名称/相似度/包含率)、多模态批量重命名(正则替换/剧集流水号生成/文本格式化/FC2名称清洗/前缀去广告/后缀智能修复)、清理空文件夹、内置解压密码库的批量解压、Aria2/Motrix带目录结构推送、夹杂无关文字或“去头”的污染磁链智能识别、自定义资源黑白名单:清理垃圾文件/文件夹、分享提取次数限制、导出目录树等。沉浸式媒体播放引擎:以图搜图、高级字幕加载、跳过片头尾及进度条缩略图预览。叫“增强大师”是有原因的,何不进来看看? // @description:zh-CN 桌面级PikPak网盘管家!包含多模态文件查重(哈希/时长/名称)、多模态文件夹查重(名称/相似度/包含率)、多模态批量重命名(正则替换/剧集流水号生成/文本格式化/FC2名称清洗/前缀去广告/后缀智能修复)、清理空文件夹、内置解压密码库的批量解压、Aria2/Motrix带目录结构推送、夹杂无关文字或“去头”的污染磁链智能识别、自定义资源黑白名单:清理垃圾文件/文件夹、分享提取次数限制、导出目录树等。沉浸式媒体播放引擎:以图搜图、高级字幕加载、跳过片头尾及进度条缩略图预览。叫“增强大师”是有原因的,何不进来看看? // @description:zh-TW 桌面級PikPak網盤管家!包含多模態檔案重複檢查(雜湊/時長/名稱)、多模態資料夾重複檢查(名稱/相似度/包含率)、多模態批次重新命名(正規替換/劇集流水號產生/文字格式化/FC2名稱清洗/前綴去廣告/副檔名智慧修復)、清理空資料夾、內建解壓縮密碼庫的批次解壓縮、Aria2/Motrix帶目錄結構推送、夾雜無關文字或「去頭」的污染磁鏈智慧識別、自訂資源黑白名單:清理垃圾檔案/資料夾、分享提取次數限制、匯出目錄樹等。沉浸式媒體播放引擎:以圖搜圖、進階字幕載入、跳過片頭尾及進度列縮圖預覽。叫「增強大師」是有原因的,何不進來看看? // @description:en Desktop-grade PikPak file manager! Features multi-modal file deduplication (hash/duration/name), folder deduplication (name/similarity/containment), bulk renaming (regex/serialization/formatting/FC2 cleaning/ad-removal/MIME-fix), empty folder pruning, batch extraction with password vault, Aria2/Motrix push with directory structure, smart corrupted magnet link recognition, custom resource black/whitelist, share limits, directory tree export, etc. Immersive media player: reverse image search, advanced subtitles, intro/outro skipping, and thumbnail previews. There's a reason it's called the "Enhancement Master", why not take a look? // @description:ko 데스크톱 수준의 PikPak 클라우드 관리자! 다중 모드 파일 중복 체크(해시/시간/이름), 폴더 중복 체크(이름/유사도/포함율), 다중 모드 일괄 이름 변경(정규식/에피소드 번호/포맷팅/FC2 정리/광고 제거/확장자 복구), 빈 폴더 정리, 비밀번호 금고 기반 일괄 압축 해제, 디렉토리 구조 유지 Aria2/Motrix 푸시, 손상된 마그넷 링크 스마트 인식, 리소스 블랙/화이트리스트, 공유 횟수 제한, 디렉토리 트리 내보내기 등을 제공합니다. 몰입형 미디어 플레이어: 이미지 검색, 고급 자막, 오프닝/엔딩 건너뛰기, 진행률 썸네일 미리보기. "인핸서 마스터"라고 불리는 데에는 이유가 있습니다. 한번 확인해 보세요! // @description:ja デスクトップクラスのPikPakマネージャー!マルチモーダルなファイル重複チェック(ハッシュ/時間/名前)、フォルダ重複チェック(名前/類似度/包含率)、一括リネーム(正規表現/連番/フォーマット化/FC2クリーンアップ/広告削除/拡張子修復)、空フォルダのクリーンアップ、パスワード庫連携の自動一括解凍、ディレクトリ構造を保持したAria2/Motrixプッシュ、破損マグネットリンクのスマート認識、リソースのブラック/ホワイトリスト、共有回数制限、ディレクトリツリーのエクスポートなどを備えています。没入型メディアプレーヤー:画像検索、高度な字幕、OP/EDスキップ、プログレスバーのサムネイル。なぜ「拡張マスター」と呼ばれるのか、ぜひお試しください! // @match https://mypikpak.com/drive/* // @match https://app.mypikpak.com/* // @match https://drive.mypikpak.com/* // @icon https://raw.githubusercontent.com/digbug82/PikPak_Enhancement_Master/main/img/logo.svg // @homepage https://github.com/digbug82/PikPak_Enhancement_Master // @supportURL https://github.com/digbug82/PikPak_Enhancement_Master/issues // @compatible chrome // @compatible edge // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @connect catbox.moe // @connect litterbox.catbox.moe // @connect uguu.se // @connect mypikpak.com // @connect localhost // @run-at document-start // @require https://cdn.jsdelivr.net/npm/hls.js@1.5.8/dist/hls.min.js // @require https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js // @downloadURL none // ==/UserScript== /* * ============================================================================ * COPYRIGHT & LICENSE NOTICE * ============================================================================ * This project (PikPak Enhancement Master) is a derivative work created by digbug82. * * [1] NEW CONTRIBUTIONS & ENHANCEMENTS: * All new features, extensive refactoring, UI overhaul, and advanced management * suites (e.g., Image Search, Blacklist, Smart Deduplication, Aria2 integration) * are licensed under the Creative Commons Attribution-NonCommercial-ShareAlike * 4.0 International License (CC-BY-NC-SA-4.0). * Copyright (c) 2025-2026 digbug82. * You may NOT use this material for commercial purposes. * * ---------------------------------------------------------------------------- * [2] ORIGINAL PROJECT ACKNOWLEDGEMENT (MIT License): * The base framework and original API logics are derived from * "PikPak File Manager v1.2.0" (Original Repository: https://github.com/poihoii/PikPak_FileManager). * * MIT License * Copyright (c) 2025 브랜뉴 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * ============================================================================ */ (() => { "use strict"; const NativeTokenSniffer = { init: () => { const isTurbo = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; if (!isTurbo) { console.log('🚀 [PikPak Master] Turbo Mode OFF. Native Hijacking Disabled.'); return; } if (location.href.includes('/login') || location.pathname.includes('login')) { console.log('🚀 [PikPak Master] Login page detected. OOM-Guard suspended.'); return; } const inject = () => { const s = document.createElement('script'); s.textContent = `(function(){ const _W = window.Worker; window.Worker = function(url, opts) { if (location.href.includes('/login') || location.pathname.includes('login')) { return new _W(url, opts); } const u = url ? url.toString() : ''; const blockList = ['query_db', 'sync', 'database', 'index', 'calc_sha1', 'query_docs']; if (blockList.some(k => u.includes(k))) { console.log('🚫 [PikPak Master] OOM-Guard blocked worker:', u); return new _W('data:application/javascript,self.onmessage=()=>{}'); } return new _W(url, opts); }; const _f = window.fetch; window.fetch = async function(...args) { if (location.href.includes('/login') || location.pathname.includes('login')) { return _f.apply(this, args); } const url = args[0] ? args[0].toString() : ''; if (url.includes(':incremental_sync') || url.includes(':sync')) { return new Response(JSON.stringify({ error_code: 0, data: [], files: [], tasks: [], next_page_token: '' }), {status: 200, headers: {'Content-Type': 'application/json'}}); } try { const opts = args[1] || {}; let cap = null; if (opts.headers) { if (opts.headers instanceof Headers) cap = opts.headers.get('x-captcha-token') || opts.headers.get('X-Captcha-Token'); else if (typeof opts.headers === 'object') { const key = Object.keys(opts.headers).find(k => k.toLowerCase() === 'x-captcha-token'); if (key) cap = opts.headers[key]; } } if (cap && cap.length > 20) localStorage.setItem('pk_captured_captcha', cap); } catch (e) {} return _f.apply(this, args); }; })()`; (document.head || document.documentElement).appendChild(s).remove(); }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', inject) : inject(); } }; NativeTokenSniffer.init(); window.addEventListener('beforeunload', (e) => { const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; const tasks = pkState?.uploadTasks || []; if (tasks.some(t => activeStatus.includes(t.status))) { e.preventDefault(); return e.returnValue; } }); ; const CONF = { rowHeight: 40, buffer: 20, SYSTEM_FOLDER_NAME: 'My Pack', logoSVG: ``, emptySVG: ``, dupHashSVG: ``, dupSimSVG: ``, dupNameSVG: ``, dupContainSVG: ``, crumbIcons: { right: ``, down: ``, sortAZ: ``, sortZA: ``, sortNew: ``, sortOld: `` }, icons: { offline: ``, navShare: ``, unshare: ``, refresh: ``, retry: ``, settings: ``, home: ``, recent: ``, history: ``, trash: ``, emptyTrash: ``, restore: ``, delForever: ``, newfolder: ``, del: ``, deselect: ``, copy: ``, cut: ``, paste: ``, rename: ``, bulkrename: ``, unzip: ``, prune: ``, blacklist: ``, invert: ``, folderFirst: ``, analyze: ``, scanDup: ``, export: ``, stop: ``, ext: ``, download: ``, aria2:``, info: ``, moon: ``, sun: ``, cloudDownload: ``, share: ``, maximize: ``, minimize: ``, close: ``, help: ``, warning: ``, lock: ``, uploadBtn: ``, upFile: ``, upFolder: ``, navUpload: ``, taskStart: ``, taskPause: ``, cleanAll: ``, logout: ``, blMarker: ``, vault: `` }, typeIcons: { folder: ``, systemFolder: ``, video: ``, image: ``, audio: ``, archive: ``, archiveUnzipped: ``, text: ``, pdf: ``, subtitle: ``, executable: ``, web: ``, apk: ``, file: `` } }; ; const CSS = ` :root { --pk-zoom: 1; --pk-bg: #ffffff; --pk-bg-rgb: 255, 255, 255; --pk-fg: #1a1a1a; --pk-bd: #e5e5e5; --pk-hl: #f0f0f0; --pk-sel-bg: #e6f3ff; --pk-sel-bd: #cce8ff; --pk-pri: #0067c0; --pk-btn-hov: #e0e0e0; --pk-gh: #f5f5f5; --pk-gh-fg: #333; --pk-sb-bg: transparent; --pk-sb-th: #ccc; --pk-sb-hov: #aaa; --pk-icon-c: #888; --pk-tip-bg: rgba(255, 255, 255, 0.95); --pk-tip-fg: #1a1a1a; --pk-tip-bd: rgba(0, 0, 0, 0.06); --pk-tip-sd: rgba(0, 0, 0, 0.12); --pk-toast-bg: rgba(255, 255, 255, 0.95); --pk-toast-fg: #1a1a1a; --pk-toast-bd: rgba(0, 0, 0, 0.08); --pk-match-bg: #fff2cc; --pk-match-fg: #d93025; --pk-v-line: #d1d1d1; } .pk-no-transition, .pk-no-transition * { transition: none !important; } .pk-dark { --pk-bg: #202020; --pk-bg-rgb: 32, 32, 32; --pk-fg: #f5f5f5; --pk-bd: #333333; --pk-hl: #2d2d2d; --pk-sel-bg: #2b3a4a; --pk-sel-bd: #0067c0; --pk-pri: #4cc2ff; --pk-btn-hov: #3a3a3a; --pk-gh: #2a2a2a; --pk-gh-fg: #eee; --pk-sb-th: #555; --pk-sb-hov: #777; --pk-icon-c: #aaa; --pk-tip-bg: rgba(20, 20, 20, 0.95); --pk-tip-fg: #ffffff; --pk-tip-bd: rgba(255, 255, 255, 0.1); --pk-tip-sd: rgba(0, 0, 0, 0.4); --pk-toast-bg: rgba(45, 45, 45, 0.95); --pk-toast-fg: #ffffff; --pk-toast-bd: rgba(255, 255, 255, 0.15); --pk-v-line: rgba(255, 255, 255, 0.25); } .pk-dark .pk-loading-ov { background: rgba(0,0,0,0.8); } .pk-ov { position: fixed; top: 0; left: 0; width: calc(100vw / var(--pk-zoom, 1)); height: calc(100vh / var(--pk-zoom, 1)); zoom: var(--pk-zoom, 1); transform-origin: top left; z-index: 10000; background: rgba(0,0,0,0.4); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; font-family: inherit; outline: none; overscroll-behavior: none; -webkit-user-select: none; user-select: none; } .pk-ov input, .pk-ov textarea { -webkit-user-select: text !important; user-select: text !important; cursor: text; } .pk-win { width: 90%; max-width: calc(1600px / var(--pk-zoom, 1)); min-width: 720px; min-height: 340px; height: 80%; background: var(--pk-bg); color: var(--pk-fg); border-radius: 8px; box-shadow: 0 25px 50px rgba(0,0,0,0.25); display: flex; flex-direction: row; overflow: hidden; border: 1px solid var(--pk-bd); position: relative; } .pk-sidebar { width: 68px; background: var(--pk-bg); border-right: 1px solid var(--pk-bd); display: flex; flex-direction: column; align-items: center; padding: 16px 0; flex-shrink: 0; z-index: 10; gap: 0; } .pk-nav-btn { width: 44px !important; height: 44px !important; border-radius: 10px; color: var(--pk-icon-c); padding: 0 !important; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; margin-bottom: 0; transition: background-color 0.1s ease, color 0.1s ease; position: relative !important; } .pk-nav-btn:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-nav-btn.act { background: var(--pk-sel-bg); color: var(--pk-pri); } #pk-btn-cloud { background: var(--pk-pri) !important; color: #fff !important; border-radius: 50% !important; margin-bottom: 12px !important; padding: 0 !important; overflow: visible !important; display: flex !important; align-items: center !important; justify-content: center !important; } #pk-btn-cloud:hover { filter: brightness(1.1); } #pk-btn-cloud svg { width: 24px !important; height: 24px !important; transform: scale(1.3); transform-origin: center center; margin: 0 !important; transition: transform 0.2s; } .pk-nav-btn svg { width: 24px !important; height: 24px !important; } .pk-maximized #pk-btn-cloud { background: var(--pk-pri) !important; border-radius: 8px !important; width: calc(100% - 20px) !important; height: 48px !important; padding: 0 15px !important; margin-bottom: 20px !important; } .pk-maximized #pk-btn-cloud svg { width: 24px !important; height: 24px !important; transform: scale(1.4); margin-right: 6px; } .pk-maximized #pk-btn-cloud:hover { filter: brightness(1.1); } .pk-sidebar #pk-settings { margin-top: auto; } .pk-btn-danger { background: #d93025 !important; color: #fff !important; border: none !important; } .pk-btn-danger:hover, #pk-empty-trash:hover { background: #d93025 !important; color: #fff !important; filter: brightness(1.15); opacity: 1 !important; } .pk-sidebar #pk-settings:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-sidebar #pk-settings svg { width: 26px !important; height: 26px !important; } .pk-main-col { flex: 1; display: flex; flex-direction: column; overflow: hidden; position: relative; min-width: 0; } .pk-hd { height: 48px; border-bottom: 1px solid var(--pk-bd); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; background: var(--pk-bg); } .pk-tt { font-weight: 700; font-size: 20px; display: flex; align-items: center; gap: 10px; } .pk-tt svg { color: #333; margin-right: 10px; transition: color 0.2s ease; } .pk-ov.pk-dark .pk-tt svg { color: #fff; } .pk-tb { height: 50px !important; padding: 0 8px !important; border-bottom: 1px solid var(--pk-bd); display: flex; gap: clamp(4px, 0.6vw, 8px) !important; align-items: center !important; background: var(--pk-bg); overflow: visible !important; flex-wrap: nowrap !important; position: relative; } #pk-top-bar { z-index: 30; } #pk-actionbar, #pk-trash-bar { z-index: 20; } .pk-btn { height: 32px; padding: 0 12px; border-radius: 4px; border: 1px solid transparent; background: transparent; color: var(--pk-fg); cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px; transition: background-color 0.1s; position: relative; font-weight: 500; white-space: nowrap; flex-shrink: 0; backface-visibility: hidden; } #pk-theme svg { transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } #pk-theme:active svg { transform: rotate(90deg) scale(0.8); } .pk-btn:hover:not(:disabled) { background: var(--pk-btn-hov) !important; } .pk-btn.pri:hover:not(:disabled) { background: var(--pk-pri) !important; filter: brightness(1.15); color: #fff !important; } .pk-btn:disabled { opacity: 0.4; cursor: not-allowed; } .pk-btn.pri { color: var(--pk-pri); font-weight: 600; } .pk-btn svg { width: 18px; height: 18px; flex-shrink: 0; display: inline-block; vertical-align: -4px; } .pk-btn span { white-space: nowrap; pointer-events: none; } @media (max-width: 1550px) { .pk-maximized .pk-btn:not(#pk-btn-folder-first):not(#pk-btn-invert):not(#pk-filter-btn):not(#pk-btn-exit):not(#pk-scan-dup):not(#pk-analyze):not(#pk-export):not(#pk-ext):not(#pk-aria2):not(#pk-down) span { display: none !important; } .pk-maximized .pk-btn { padding: 0 8px !important; } } @media (max-width: 1360px) { .pk-btn:not(#pk-btn-folder-first):not(#pk-btn-invert):not(#pk-filter-btn):not(#pk-btn-exit):not(#pk-scan-dup):not(#pk-analyze):not(#pk-export):not(#pk-ext):not(#pk-aria2):not(#pk-down):not(#btn_cfg_clean):not(#btn_cfg_export):not(#btn_cfg_import) span { display: none !important; } .pk-btn { padding: 0 8px !important; } } @media (max-width: 1150px) { #pk-btn-folder-first span, #pk-btn-invert span, #pk-filter-btn span, #pk-btn-exit span, #pk-scan-dup span, #pk-analyze span, #pk-export span { display: none !important; } } @media (max-width: 1000px) { #pk-ext span, #pk-aria2 span, #pk-down span { display: none !important; } } .pk-blacklist-area { display: flex; align-items: center; gap: 8px; margin-left: auto; max-width: 300px; min-width: 150px; flex-shrink: 1; } .pk-blacklist-area input { height: 32px; padding: 0 8px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); font-size: 13px; width: 100%; transition: border-color 0.2s; } .pk-blacklist-area input:focus { border-color: var(--pk-pri); outline: none; } .pk-global-chk { display: flex; align-items: center; cursor: pointer; margin-right: 8px; font-size: 13px; color: var(--pk-fg); user-select: none; white-space: nowrap; } .pk-global-chk input { margin: 0 4px 0 0; width: 16px; height: 16px; accent-color: var(--pk-pri); } .pk-search { position: relative; display: flex !important; align-items: center !important; margin: 0 !important; flex: 1 1 auto; min-width: 100px; max-width: 300px; transition: all 0.2s; z-index: 100; } .pk-search input { height: 32px; padding: 0 56px 0 10px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); font-size: 13px; width: 100% !important; margin: 0 !important; transition: border-color 0.2s; box-sizing: border-box; } .pk-search input:focus { border-color: var(--pk-pri); outline: none; } #pk-search-btn { position: absolute; right: 10px; left: auto; width: 14px; height: 14px; color: #888; cursor: pointer; transition: color 0.2s; } .pk-search input:focus + svg { color: var(--pk-pri); } .pk-f-ext { cursor: pointer; color: var(--pk-fg); padding: 4px 8px; border-radius: 4px; transition: background 0.2s, color 0.2s; font-weight: 500; font-size: 13px; } .pk-f-ext:hover { background: var(--pk-hl); } .pk-f-ext.act { color: var(--pk-pri); font-weight: bold; } .pk-fc-btn { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; background: var(--pk-hl); color: var(--pk-fg); transition: all 0.2s; border: 1px solid transparent; white-space: nowrap; } .pk-fc-btn:hover { background: rgba(0, 103, 192, 0.05); color: var(--pk-pri); border-color: var(--pk-pri); } .pk-fc-btn.act { background: var(--pk-pri); color: #fff; border: none; font-weight: bold; } .pk-fc-btn.act:hover { background: var(--pk-pri); color: #fff; } .pk-search-clear { position: absolute; right: 32px; top: 50%; transform: translateY(-50%); width: 20px; height: 20px; display: none; align-items: center; justify-content: center; cursor: pointer; color: #999; border-radius: 50%; transition: background 0.2s; } .pk-search-clear:hover { background: var(--pk-hl); color: #666; } .pk-search-clear svg { position: static !important; width: 14px !important; height: 14px !important; color: inherit !important; } .pk-hist-pop { position: absolute; top: 100%; left: 0; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 10010 !important; display: none; flex-direction: column; overflow: hidden; padding: 4px; margin-top: 4px; } .pk-hist-hd { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; font-size: 11px; color: var(--pk-pri); font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--pk-bd); margin-bottom: 4px; } .pk-hist-clear-btn { cursor: pointer; } .pk-hist-clear-btn:hover { color: var(--pk-pri); } .pk-hist-item { padding: 8px 12px; font-size: 13px; cursor: pointer; color: var(--pk-fg); display: flex; align-items: center; gap: 8px; border-bottom: 1px solid transparent; } .pk-hist-item:hover { background: var(--pk-sel-bg); } .pk-hist-item svg { position: static !important; width: 14px !important; height: 14px !important; color: #999; margin: 0 !important; } .pk-dup-toolbar { display: none; align-items: center; gap: 4px; padding: 0 8px; height: 100%; margin-left: 8px; background: transparent; border: none; flex-shrink: 0; } .pk-dup-lbl, .pk-dup-chk { white-space: nowrap !important; font-weight: 500; color: var(--pk-fg); font-size: 13px; margin-right: 6px; opacity: 0.8; flex-shrink: 0; cursor: pointer; display: flex; align-items: center; } .pk-dup-chk input { margin: 0 4px 0 0; vertical-align: middle; accent-color: var(--pk-pri); width: 16px; height: 16px; } .pk-txt-short { display: none; } .pk-txt-long { display: inline; } #pk-search-path-con .pk-txt-short { font-weight: 500; color: var(--pk-fg); } #pk-dup-folder-sel { max-width: 300px !important; min-width: auto; height: 30px !important; transition: max-width 0.2s; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-align: left; padding-right: 20px; } @media (max-width: 1150px) { .pk-txt-long { display: none !important; } .pk-txt-short { display: inline !important; } #pk-btn-exit, #pk-scan-dup { padding: 0 8px !important; } #pk-dup-folder-sel { max-width: 100px !important; } } .pk-btn-toggle { border: 1px solid var(--pk-bd); background: var(--pk-bg); color: var(--pk-fg); height: 30px; border-radius: 4px; padding: 0 10px; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; flex-shrink: 0; } .pk-btn-toggle:hover { background: var(--pk-btn-hov); border-color: var(--pk-pri); } .pk-btn-toggle span { font-weight: 700; color: var(--pk-pri); } #pk-dup-exit { flex-shrink: 0; } .pk-nav { display: flex !important; align-items: center !important; gap: 4px; overflow: hidden; white-space: nowrap; font-size: 13px; color: #666; margin: 0 8px; height: 100%; max-width: 60%; } .pk-nav span { cursor: pointer; padding: 2px 6px; border-radius: 4px; } .pk-nav span:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-nav span.act { font-weight: 600; color: var(--pk-fg); cursor: default; } .pk-grid-hd, .pk-row { display: grid; column-gap: 10px; align-items: center; font-size: 14px; color: var(--pk-fg); box-sizing: border-box; width: 100%; } .pk-grid-hd > div, .pk-row > div { display: flex; align-items: center; justify-content: flex-start !important; overflow: hidden; white-space: nowrap; text-align: left; } .pk-grid-hd > div:first-child, .pk-row > div:first-child { justify-content: center !important; overflow: visible !important; } .pk-grid-hd { height: 36px; border-bottom: 1px solid var(--pk-bd); font-size: 13px; color: #666; user-select: none; padding: 0 22px 0 16px; } .pk-row { padding: 0 16px; } .pk-col { cursor: pointer; font-weight: 600; display: flex; align-items: center; justify-content: flex-start; } .pk-col:hover { color: var(--pk-fg); } .pk-modal::-webkit-scrollbar, .pk-vp::-webkit-scrollbar, .pk-prev-list::-webkit-scrollbar, .pk-scroll::-webkit-scrollbar, #pk-rn-vp::-webkit-scrollbar, .pk-bl-area::-webkit-scrollbar, textarea::-webkit-scrollbar, .pk-sub-pane::-webkit-scrollbar, #pk_sub_search_list::-webkit-scrollbar, .pk-p-pop::-webkit-scrollbar, .pk-share-modal::-webkit-scrollbar { width: 6px; height: 6px; } .pk-no-scrollbar::-webkit-scrollbar { display: none !important; } .pk-no-scrollbar { -ms-overflow-style: none !important; scrollbar-width: none !important; } .pk-vp::-webkit-scrollbar-track, .pk-modal::-webkit-scrollbar-track, .pk-prev-list::-webkit-scrollbar-track, .pk-scroll::-webkit-scrollbar-track, #pk-rn-vp::-webkit-scrollbar-track, .pk-bl-area::-webkit-scrollbar-track, textarea::-webkit-scrollbar-track, .pk-sub-pane::-webkit-scrollbar-track, #pk_sub_search_list::-webkit-scrollbar-track, .pk-p-pop::-webkit-scrollbar-track { background: var(--pk-sb-bg); } .pk-vp::-webkit-scrollbar-thumb, .pk-modal::-webkit-scrollbar-thumb, .pk-prev-list::-webkit-scrollbar-thumb, .pk-scroll::-webkit-scrollbar-thumb, #pk-rn-vp::-webkit-scrollbar-thumb, .pk-bl-area::-webkit-scrollbar-thumb, textarea::-webkit-scrollbar-thumb, .pk-sub-pane::-webkit-scrollbar-thumb, #pk_sub_search_list::-webkit-scrollbar-thumb, .pk-p-pop::-webkit-scrollbar-thumb { background: var(--pk-sb-th); border-radius: 3px; } .pk-vp::-webkit-scrollbar-thumb:hover, .pk-modal::-webkit-scrollbar-thumb:hover, .pk-prev-list::-webkit-scrollbar-thumb:hover, .pk-scroll::-webkit-scrollbar-thumb:hover { background: var(--pk-sb-hov); } ::-webkit-scrollbar { cursor: default; } .pk-vp { flex: 1; overflow-y: auto; position: relative; background: var(--pk-bg); scrollbar-gutter: stable; } .pk-in { position: absolute; width: 100%; top: 0; } .pk-row { height: 40px; border: 1px solid transparent; cursor: default; padding: 0 16px; border-radius: 4px; } .pk-row:hover { background: var(--pk-hl); } .pk-row.sel { background: var(--pk-sel-bg); border: 1px solid transparent; } .pk-row.sel.pk-focused { border: 1px solid var(--pk-pri); border-radius: 4px; } .pk-name { display: flex; align-items: center; overflow: visible; min-width: 0; cursor: default; } .pk-name .pk-name-txt { transition: color 0.1s; border-bottom: 1px solid transparent; } .pk-name svg { flex-shrink: 0; margin-right: 8px; cursor: default; } .pk-win:not(.pk-mode-trash) .pk-name .pk-name-txt { cursor: pointer; } .pk-win:not(.pk-mode-trash) .pk-name .pk-name-txt:hover { color: var(--pk-pri); } .pk-tag-default { cursor: default !important; color: #999 !important; border: 1px solid #ccc !important; font-weight: normal !important; } .pk-tag-default:hover { color: #999 !important; } .pk-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; line-height: 1.5; padding: 2px 0; margin-top: -2px; } .pk-group-hd { display: flex; background: var(--pk-gh); color: var(--pk-gh-fg); font-weight: bold; align-items: center; padding: 0 16px; border-top: 4px solid var(--pk-bg) !important; border-bottom: 4px solid var(--pk-bg) !important; background-clip: padding-box; height: 40px !important; box-sizing: border-box; margin-top: 0 !important; } .pk-group-hd .pk-tag { margin-left: auto; background: #666; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; border: 1px solid #555; } .pk-group-hd .pk-cnt { margin-left: 10px; color: var(--pk-fg); font-size: 12px; opacity: 0.9; } .pk-loading-ov { position: absolute; inset: 0; background: rgba(var(--pk-bg-rgb), 0.75); z-index: 999; display: none; flex-direction: column; align-items: center; justify-content: center; color: var(--pk-fg); gap: 28px; backdrop-filter: blur(10px) saturate(180%); -webkit-backdrop-filter: blur(10px) saturate(180%); transition: all 0.3s ease; } .pk-spin-lg { width: 56px; height: 56px; border: 4px solid rgba(136, 136, 136, 0.25); border-top-color: var(--pk-pri); border-radius: 50%; position: relative; animation: pk-ultra-spin 1s linear infinite; transform: translateZ(0); } .pk-loading-txt { font-size: 15px; font-weight: 600; text-align: center; white-space: pre-line; line-height: 1.6; letter-spacing: 1px; } @keyframes pk-ultra-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes pk-text-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(0.98); } } .pk-stop-btn { display: flex !important; align-items: center !important; justify-content: center !important; gap: 6px; padding: 8px 24px; background: #d93025; color: white; border: none; border-radius: 20px; font-size: 14px; cursor: pointer; font-weight: bold; box-shadow: 0 4px 10px rgba(217, 48, 37, 0.3); transition: transform 0.1s; } .pk-stop-btn svg { display: block; } .pk-stop-btn:hover { background: #b02a20; transform: scale(1.05); } .pk-stop-btn:active { transform: scale(0.95); } .pk-ft { height: 48px; border-top: 1px solid var(--pk-bd); background: var(--pk-bg); display: flex; align-items: center; padding: 0 16px; justify-content: space-between; font-size: 13px; } .pk-stat { color: var(--pk-fg); font-size: 13px; } .pk-grp { display: flex; gap: 8px; } .pk-pop { position: fixed; pointer-events: none; z-index: 2147483647 !important; background: #000; border: 1px solid #333; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); border-radius: 6px; display: none; overflow: hidden; } .pk-pop img { display: block; max-width: 320px; max-height: 240px; object-fit: contain; } .pk-ctx { position: fixed; z-index: 2147483647 !important; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); min-width: 150px; padding: 4px 0; display: none; } .pk-ctx-item { padding: 8px 16px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px; color: var(--pk-fg); } .pk-ctx-item:hover { background: var(--pk-hl); } .pk-ctx-sep { height: 1px; background: var(--pk-bd); margin: 4px 0; } .pk-modal-ov { position: fixed; top: 0; left: 0; width: calc(100vw / var(--pk-zoom, 1)); height: calc(100vh / var(--pk-zoom, 1)); zoom: var(--pk-zoom, 1); transform-origin: top left; background: rgba(0, 0, 0, 0.5); z-index: 10001; display: flex; align-items: center; justify-content: center; } .pk-modal { position: relative; background: var(--pk-bg); padding: 25px; border-radius: 12px; width: 500px; max-height: 85vh; overflow: hidden !important; display: flex; flex-direction: column; gap: 15px; border: 1px solid var(--pk-bd); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); overscroll-behavior: none; } .pk-modal-ov { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10001; display: flex; align-items: center; justify-content: center; overscroll-behavior: none; overflow: hidden; } .pk-modal h3 { margin: 0 0 5px 0; font-size: 16px; border-bottom: 1px solid var(--pk-bd); padding-bottom: 10px; padding-right: 40px; color: var(--pk-fg); } .pk-modal-close { position: absolute; top: 15px; right: 15px; cursor: pointer; color: var(--pk-icon-c); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background 0.1s, color: 0.1s; } .pk-modal-close:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-field { display: flex; flex-direction: column; gap: 5px; font-size: 13px; } .pk-field input, .pk-field select { padding: 6px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); } .pk-field select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; background-size: 16px; padding-right: 32px !important; cursor: pointer; } .pk-modal-act { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } .pk-credit { font-size: 11px; color: #888; text-align: center; margin-top: 20px; border-top: 1px solid var(--pk-bd); padding-top: 10px; } .pk-credit a { color: #888; text-decoration: none; } .pk-credit a:hover { text-decoration: underline; } .pk-prev-list { flex: 1; overflow-y: auto; border: 1px solid var(--pk-bd); max-height: 300px; } .pk-prev-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 5px 10px; border-bottom: 1px solid var(--pk-bd); font-size: 12px; } .pk-prev-row:nth-child(odd) { background: var(--pk-hl); } .pk-sep { width: 1px; height: 16px; background: var(--pk-bd); margin: 0 4px; display: none; flex-shrink: 0; } .pk-sep-sm { width: 1px; height: 16px; background: var(--pk-bd); margin: 0 8px; flex-shrink: 0; } #pk-refresh, #pk-trash-refresh { width: auto !important; justify-content: center !important; flex-shrink: 0; } #pk-refresh svg, #pk-trash-refresh svg { width: 18px !important; height: 18px !important; transform: none !important; stroke-width: 2 !important; } .pk-grid-hd input[type="checkbox"], .pk-row input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; margin: 0 auto !important; flex-shrink: 0; accent-color: var(--pk-pri); box-sizing: content-box; transform: translateZ(0); display: block; position: relative; } .pk-player-box { position: relative; width: 100%; height: 100%; background: #000; display: flex; flex-direction: column; user-select: none; overflow: hidden; } .pk-player-video { width: 100%; height: 100%; object-fit: contain; outline: none; transform: translateZ(0); backface-visibility: hidden; image-rendering: -webkit-optimize-contrast; -webkit-font-smoothing: antialiased; } .pk-player-top { position: absolute; top: 0; left: 0; right: 0; height: 64px; background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.3) 60%, transparent 100%); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; z-index: 100 !important; opacity: 1; transition: opacity 0.3s; pointer-events: auto; } .pk-player-box.ui-hidden .pk-player-top { opacity: 0 !important; pointer-events: none !important; } .pk-player-title { color: #fff; font-size: 16px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); letter-spacing: 0.5px; } .pk-player-controls { position: absolute; bottom: 0; left: 0; right: 0; height: 64px; background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.35) 60%, transparent 100%); display: flex; align-items: center; padding: 0 16px; z-index: 80 !important; gap: 4px; opacity: 0; transition: opacity 0.3s; } .pk-player-box:hover .pk-player-controls, .pk-player-box.paused .pk-player-controls { opacity: 1; } .pk-player-progress-container { position: absolute; bottom: 64px; left: 12px; right: 12px; height: 14px; cursor: pointer; z-index: 11; display: flex; align-items: center; } .pk-player-progress-bg { width: 100%; height: 4px; background: rgba(255, 255, 255, 0.25); position: relative; border-radius: 2px; transition: height 0.1s, transform 0.1s; backdrop-filter: blur(2px); } .pk-player-progress-container:hover .pk-player-progress-bg { height: 6px; transform: scaleY(1.1); } .pk-player-progress-filled { height: 100%; background: var(--pk-pri); width: 0; position: relative; border-radius: 2px; transition: none !important; will-change: width; } .pk-player-progress-thumb { position: absolute; right: -7px; top: 50%; transform: translateY(-50%) scale(0); width: 14px; height: 14px; border-radius: 50%; background: #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .pk-player-progress-container:hover .pk-player-progress-thumb { transform: translateY(-50%) scale(1); } .pk-p-btn { color: #eee; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 50px; height: 40px; border-radius: 4px; transition: all 0.2s ease; position: relative; flex-shrink: 0; } .pk-p-btn:hover { color: var(--pk-pri); background: transparent; transform: scale(1.05); } .pk-p-btn:active { color: var(--pk-pri); background: transparent; transform: scale(0.95); } #pk_p_play, #pk_p_vol, #pk_p_close { display: inline-flex !important; width: 40px !important; height: 40px !important; border-radius: 50% !important; margin: 0 4px !important; padding: 0 !important; box-sizing: border-box !important; justify-content: center !important; align-items: center !important; } #pk_p_play svg, #pk_p_vol svg, #pk_p_close svg { margin: 0 !important; padding: 0 !important; } #pk_p_play:hover, #pk_p_vol:hover, #pk_p_close:hover { color: #fff !important; filter: brightness(1.5); background: rgba(255, 255, 255, 0.15) !important; transform: none !important; } .pk-p-btn svg { width: 24px; height: 24px; fill: currentColor; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); } #pk_sub_trigger .pk-p-btn svg { width: 21px; height: 21px; } #pk_p_full svg { width: 28px; height: 28px; } .pk-p-time { color: #ddd; font-size: 13px; font-family: "Segoe UI", Roboto, monospace; min-width: 90px; text-align: center; font-variant-numeric: tabular-nums; margin: 0 5px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); } .pk-p-menu-con { position: relative; display: flex; align-items: center; justify-content: center; height: 40px; cursor: pointer; font-size: 15px; color: #ddd; font-weight: 600; padding: 0; min-width: 50px; transition: color 0.2s; border-radius: 4px; } #pk_sub_trigger .pk-p-btn { width: 50px; height: 40px; } .pk-p-menu-con:hover { color: var(--pk-pri); background: transparent; } .pk-p-pop { position: absolute; bottom: 45px; left: 50%; transform: translateX(-50%); background: rgba(20, 20, 20, 0.9); border-radius: 8px; padding: 6px 0; display: none; flex-direction: column-reverse; min-width: 100px; text-align: center; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); z-index: 20; backdrop-filter: blur(10px); } .pk-p-pop::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 45px; background: transparent; } .pk-p-menu-con:hover .pk-p-pop { display: flex; animation: pkFadeIn 0.2s ease; } @keyframes pkFadeIn { from { opacity: 0; transform: translate(-50%, 10px); } to { opacity: 1; transform: translate(-50%, 0); } } .pk-p-item { padding: 8px 16px; color: #ccc; cursor: pointer; font-size: 13px; position: relative; z-index: 2; transition: background 0.1s; text-align: center; display: flex; align-items: center; justify-content: center; } .pk-p-item:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } .pk-p-item.active { color: var(--pk-pri); font-weight: bold; } .pk-p-vol-wrap { display: flex; align-items: center; height: 100%; gap: 0px; margin-right: 5px; } .pk-p-vol-slider { -webkit-appearance: none; width: 70px; height: 4px; background: rgba(255, 255, 255, 0.3); border-radius: 2px; outline: none; cursor: pointer; transition: height 0.1s; } .pk-p-vol-slider:hover { height: 6px; } .pk-p-vol-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.1s; } .pk-p-vol-slider::-webkit-slider-thumb:hover { transform: scale(1.2); } .pk-p-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; display: none; } .pk-player-box.buffering:not(.pk-is-seeking) .pk-p-loading { display: block; } .pk-player-box.pk-is-seeking .pk-p-loading { display: none !important; } .pk-p-center-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) translateZ(0); width: 72px; height: 72px; background: rgba(0, 0, 0, 0.35); border-radius: 50%; display: none; align-items: center; justify-content: center; z-index: 36; pointer-events: none; border: 1px solid rgba(255, 255, 255, 0.25); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); backface-visibility: hidden; will-change: transform, opacity; } .pk-p-center-play svg { width: 36px; height: 36px; fill: #fff; margin-left: 4px; filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.3)); } .pk-player-box.paused.pk-v-started:not(.buffering):not(.pk-is-seeking) .pk-p-center-play { display: flex !important; animation: pkPlayPop 0.35s cubic-bezier(0.2, 0, 0.2, 1) forwards; } .pk-player-box.buffering .pk-p-center-play { display: none !important; opacity: 0 !important; } .pk-player-box.pk-is-seeking .pk-p-center-play { display: none !important; opacity: 0 !important; } @keyframes pkPlayPop { from { opacity: 0; transform: translate(-50%, -50%) scale(0.8) translateZ(0); } to { opacity: 1; transform: translate(-50%, -50%) scale(1) translateZ(0); } } .pk-p-seek-indicator { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) translateZ(0); height: 54px; min-width: 190px; background: rgba(0, 0, 0, 0.65); color: #fff; padding: 0 22px; border-radius: 12px; font-size: 22px; font-weight: 300; font-family: "Inter", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", sans-serif; z-index: 50; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); font-variant-numeric: tabular-nums; pointer-events: none; white-space: nowrap; letter-spacing: 0px; } body.pk-dragging { cursor: pointer !important; user-select: none !important; -webkit-user-select: none !important; } .pk-player-box.pk-is-seeking .pk-player-progress-thumb { transform: translateY(-50%) scale(1) !important; opacity: 1 !important; transition: none !important; } .pk-player-box.pk-is-seeking .pk-player-progress-bg { height: 6px !important; transform: scaleY(1.1) !important; transition: none !important; } .pk-player-box.pk-is-seeking .pk-player-progress-filled { transition: none !important; will-change: width; } .pk-p-resume-toast { position: absolute; bottom: 85px; left: 24px; background: rgba(28, 28, 28, 0.95); color: #fff; padding: 8px 16px; border-radius: 99px; font-size: 13px; display: flex; align-items: center; gap: 8px; z-index: 100; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); animation: pkFadeInUp 0.2s ease; } .pk-p-plist-ov { position: absolute; top: 100%; left: 0; right: 0; z-index: 25; display: flex; flex-direction: column; pointer-events: none; } .pk-p-plist-strip { height: 84px; background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); display: flex; align-items: center; position: relative; border-top: none; pointer-events: auto; } .pk-p-plist-tab { position: absolute !important; bottom: 100%; left: 50%; transform: translateX(-50%); height: 25px; pointer-events: auto !important; z-index: 70; margin-bottom: -1px; } .pk-p-plist-tab { align-self: center; position: relative; z-index: 26; color: rgba(255, 255, 255, 0.8); padding: 0 20px; height: 30px; font-size: 12px; font-weight: 600; cursor: pointer; border: none; display: flex; align-items: center; justify-content: center; gap: 4px; background: transparent; margin-bottom: -1px; letter-spacing: 0.5px; transition: opacity 0.3s ease; opacity: 0; } .pk-p-plist-tab:hover, .pk-player-box.plist-active .pk-p-plist-tab { opacity: 1; } .pk-p-plist-tab:hover, .pk-player-box.plist-active .pk-p-plist-tab, .pk-img-box.plist-active .pk-p-plist-tab { opacity: 1; } .pk-p-plist-tab::before { content: ''; position: absolute; inset: 0; z-index: -1; left: -50px; right: -50px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='20' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20 Z' fill='rgba(20, 20, 20, 0.9)'/%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20' fill='none' stroke='rgba(255,255,255,0.1)' stroke-width='1' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E"); background-size: 100% 100%; background-repeat: no-repeat; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); transform: translateZ(0); backface-visibility: hidden; -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='20' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20 Z' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='20' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20 Z' fill='black'/%3E%3C/svg%3E"); -webkit-mask-size: 100% 100%; mask-size: 100% 100%; } #pk_p_box:fullscreen, #pk_p_box:-webkit-full-screen, #pk_p_box:-moz-full-screen { width: 100vw !important; height: 100vh !important; top: 0 !important; left: 0 !important; transform: none !important; margin: 0 !important; border-radius: 0 !important; overflow: hidden !important; } #pk_p_box:fullscreen #pk_video, #pk_p_box:-webkit-full-screen #pk_video, #pk_p_box.full #pk_video, #pk_p_box:fullscreen #pk_p_poster, #pk_p_box:-webkit-full-screen #pk_p_poster, #pk_p_box.full #pk_p_poster { height: 100% !important; bottom: auto !important; top: 0 !important; transform: translateZ(0); } #pk_p_box:fullscreen.plist-active #pk_video, #pk_p_box:-webkit-full-screen.plist-active #pk_video, #pk_p_box.full.plist-active #pk_video, #pk_p_box:fullscreen.plist-active #pk_p_poster, #pk_p_box:-webkit-full-screen.plist-active #pk_p_poster, #pk_p_box.full.plist-active #pk_p_poster { height: calc(100% - 84px) !important; } #pk_p_box:fullscreen.plist-active .pk-p-side-nav, #pk_p_box:fullscreen.plist-active .pk-p-center-play, #pk_p_box:fullscreen.plist-active .pk-p-seek-indicator, #pk_p_box:-webkit-full-screen.plist-active .pk-p-side-nav, #pk_p_box:-webkit-full-screen.plist-active .pk-p-center-play, #pk_p_box:-webkit-full-screen.plist-active .pk-p-seek-indicator { top: calc(50% - 42px) !important; } #pk_p_box:fullscreen #pk_p_plist, #pk_p_box:-webkit-full-screen #pk_p_plist { top: auto !important; bottom: 0 !important; transform: translateY(100%) translateZ(0); backface-visibility: hidden; perspective: 1000px; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); } #pk_p_box:fullscreen .pk-p-plist-tab, #pk_p_box:-webkit-full-screen .pk-p-plist-tab { bottom: auto !important; top: 0 !important; transform: translateY(-100%) translateZ(0) !important; margin-top: 1px !important; margin-bottom: 0 !important; backface-visibility: hidden; z-index: 100 !important; } #pk_p_box:fullscreen.plist-active #pk_p_plist, #pk_p_box:-webkit-full-screen.plist-active #pk_p_plist { transform: translateY(0); } #pk_p_box:fullscreen.plist-active .pk-player-controls, #pk_p_box:-webkit-full-screen.plist-active .pk-player-controls { bottom: 84px !important; } #pk_p_box:fullscreen.plist-active .pk-p-prog-wrap, #pk_p_box:-webkit-full-screen.plist-active .pk-p-prog-wrap { bottom: 148px !important; } .pk-p-plist-tab:hover { color: rgba(255, 255, 255, 0.8); } .pk-p-plist-tab:hover::before { opacity: 1; filter: none; } .pk-p-plist-tab svg { transition: transform 0.3s; } .pk-p-plist-ov.open .pk-p-plist-tab svg { transform: rotate(180deg); } .pk-p-plist-strip { height: 110px; background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); display: flex; align-items: center; position: relative; border-top: 1px solid rgba(255, 255, 255, 0.1); } .pk-p-plist-scroll { flex: 1; display: flex; overflow-x: auto; gap: 2px; padding: 0 50px; height: 100%; align-items: center; } .pk-p-plist-scroll::-webkit-scrollbar { display: none; } .pk-p-plist-item { flex-shrink: 0; width: 140px; height: 80px; background: #333; cursor: pointer; position: relative; overflow: hidden; border: 2px solid transparent; border-radius: 4px; will-change: opacity, transform; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .pk-p-plist-item.active { border-color: var(--pk-pri); box-shadow: 0 0 10px var(--pk-pri); } .pk-p-plist-item img { width: 100%; height: 100%; object-fit: cover; opacity: 1; transition: opacity 0.3s ease-in-out; } .pk-p-plist-item img:error { opacity: 0 !important; } .pk-p-plist-nav { position: absolute; top: 0; bottom: 0; width: 50px; background: transparent !important; color: rgba(255, 255, 255, 0.7); cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 35; transition: all 0.2s; border: none; } .pk-p-plist-nav:hover { background: rgba(255, 255, 255, 0.15) !important; color: #fff; } .pk-p-plist-nav.L { left: 0; } .pk-p-plist-nav.R { right: 0; } .pk-p-plist-nav svg { width: 28px; height: 28px; stroke-width: 3; } .pk-p-plist-tip { position: fixed; background: rgba(0, 0, 0, 0.9); color: #fff; padding: 8px 12px; border-radius: 6px; font-size: 12px; pointer-events: none; z-index: 2147483647; max-width: 340px; line-height: 1.4; display: none; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-p-resume-btn { color: #4aa1ff; cursor: pointer; font-weight: bold; text-decoration: none; } .pk-p-resume-btn:hover { text-decoration: underline; } .pk-p-resume-close { cursor: pointer; color: #888; margin-left: 4px; display: flex; align-items: center; } @keyframes pkFadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .pk-img-ov { position: fixed; top: 0; left: 0; width: calc(100vw / var(--pk-zoom, 1)); height: calc(100vh / var(--pk-zoom, 1)); zoom: var(--pk-zoom, 1); transform-origin: top left; z-index: 2147483640; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; outline: none; user-select: none; } .pk-img-box { position: absolute !important; top: 10%; left: 50%; transform: translateX(-50%); background: #000; border-radius: 8px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); overflow: visible !important; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 90%; max-width: calc(1600px / var(--pk-zoom, 1)); min-width: 480px; height: 80%; transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1), height 0.2s cubic-bezier(0.4, 0, 0.2, 1), border-radius 0.2s, top 0.2s, left 0.2s, transform 0.2s; z-index: 10; box-sizing: border-box; border: none; } .pk-img-box.plist-active { height: calc(80% - 84px); border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .pk-img-box.full { width: 100%; max-width: none; height: 100%; border-radius: 0; top: 0 !important; left: 0 !important; transform: none !important; } .pk-img-box.full.plist-active { height: calc(100vh - 84px); } .pk-img-obj { flex: 1; width: 100%; height: 100%; min-height: 0; object-fit: contain; cursor: grab; transition: transform 0.1s linear; transform-origin: center center; } #pk_img_plist { position: absolute; top: 100%; left: 0; right: 0; z-index: 25; height: 84px; display: flex; flex-direction: column; pointer-events: none; } #pk_img_plist .pk-p-plist-strip { pointer-events: none; opacity: 0; transition: opacity 0.2s; } .pk-img-box.plist-active #pk_img_plist .pk-p-plist-strip { display: flex !important; opacity: 1 !important; pointer-events: auto !important; animation: pkFadeInOnly 0.2s ease; } #pk_img_plist_tab { pointer-events: auto !important; z-index: 30; cursor: pointer; } #pk_img_plist_scroll { pointer-events: auto !important; } .pk-img-obj:active { cursor: grabbing; } .pk-img-bar { position: absolute; top: 0; left: 0; right: 0; height: 50px; background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; z-index: 20; opacity: 0; transition: opacity 0.3s; pointer-events: none; } .pk-img-bar > * { pointer-events: auto; } .pk-img-box:hover .pk-img-bar { opacity: 1; } .pk-img-title { color: #fff; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: bold; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); } .pk-img-actions { display: flex; gap: 10px; } .pk-img-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 50px; height: 100px; display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.5); cursor: pointer; z-index: 5; transition: background 0.2s, color 0.2s; border-radius: 4px; opacity: 0; } .pk-img-box:hover .pk-img-nav { opacity: 1; } .pk-img-nav:hover { background: rgba(0, 0, 0, 0.3); color: #fff; } .pk-img-prev { left: 10px; } .pk-img-next { right: 10px; } .pk-img-nav svg { width: 36px; height: 36px; stroke-width: 3; } .pk-img-btn { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; color: #eee; cursor: pointer; border-radius: 50%; background: transparent; transition: all 0.2s ease; flex-shrink: 0; } .pk-img-btn:not(#pk_img_close):hover { color: var(--pk-pri) !important; background: transparent !important; } #pk_img_close:hover { background: rgba(255, 255, 255, 0.15) !important; color: #fff !important; } .pk-img-btn svg { width: 20px; height: 20px; } .pk-tag-default { margin-top: -1px; margin-left: 10px; flex-shrink: 0; min-width: 32px; box-sizing: border-box; font-size: 10px; height: 18px; padding: 1px 6px 0 6px !important; border-radius: 20px; font-weight: normal; white-space: nowrap; cursor: default; display: inline-flex; align-items: center; justify-content: center; user-select: none; background-color: transparent; color: #999; border: 1px solid #ccc; } .pk-ov.pk-dark .pk-tag-default { background-color: transparent; color: #888; border-color: #555; text-shadow: none; } #pk-scan-dup, #pk-btn-exit { align-items: center !important; margin: 0 !important; flex-shrink: 0 !important; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .pk-status-dot::after { content: ''; position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; background: #1a5eff; border-radius: 50%; border: 2px solid var(--pk-bg); box-shadow: 0 0 5px rgba(26, 94, 255, 0.5); animation: pk-pulse 2s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; z-index: 11; pointer-events: none; } @keyframes pk-pulse { 0% { transform: scale(0.9); opacity: 0.6; box-shadow: 0 0 0 0 rgba(26, 94, 255, 0.7); } 50% { transform: scale(1.1); opacity: 1; box-shadow: 0 0 0 4px rgba(26, 94, 255, 0); } 100% { transform: scale(0.9); opacity: 0.6; box-shadow: 0 0 0 0 rgba(26, 94, 255, 0); } } .pk-tooltip { position: fixed; z-index: 2147483647 !important; background: var(--pk-tip-bg); color: var(--pk-tip-fg); border: 1px solid var(--pk-tip-bd); padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; line-height: 1.4; pointer-events: none; opacity: 0; transform: translateY(5px) scale(0.95); transition: opacity 0.15s ease, transform 0.15s ease; box-shadow: 0 4px 16px var(--pk-tip-sd); backdrop-filter: blur(4px); max-width: 300px; white-space: pre-wrap; } .pk-tooltip.show { opacity: 1; transform: translateY(0) scale(1); } .pk-drag-ghost { position: fixed; z-index: 2147483647; background: var(--pk-bg); border: 1px solid var(--pk-pri); color: var(--pk-fg); padding: 8px 12px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); pointer-events: none; font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 8px; opacity: 0.9; transform: translate(15px, 15px); } .pk-row.pk-drop-target { background: var(--pk-sel-bg) !important; outline: 2px dashed var(--pk-pri); outline-offset: -2px; z-index: 10; } #pk-crumb span.pk-drop-target { background: var(--pk-sel-bg) !important; color: var(--pk-pri) !important; outline: 2px dashed var(--pk-pri); outline-offset: -2px; border-radius: 4px; z-index: 10; opacity: 1 !important; } .pk-crumb-item.pk-drop-target { background: var(--pk-sel-bg) !important; color: var(--pk-pri) !important; outline: 2px dashed var(--pk-pri); outline-offset: -2px; } .pk-empty { position: absolute; inset: 0; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; padding-bottom: 10vh; z-index: 1; opacity: 0; animation: pkFadeInOnly 0.4s ease forwards; } .pk-empty svg { width: 35vmin; max-width: 220px; height: auto; margin-bottom: 3vmin; filter: drop-shadow(0 15px 25px rgba(0, 0, 0, 0.05)); } .pk-empty-txt { font-size: clamp(13px, 3vmin, 16px); color: #94a3b8; font-weight: 500; letter-spacing: 1px; } .pk-ov.pk-dark .pk-empty-txt { color: #666e75; } .pk-selection-box { position: fixed; inset: 0 auto auto 0; background: rgba(0, 103, 192, 0.1); border: 1px solid rgba(0, 103, 192, 0.4); z-index: 2147483647 !important; pointer-events: none; display: none; will-change: transform; box-sizing: border-box; } .pk-p-vol-indicator { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.75); color: #fff; padding: 12px 24px; border-radius: 12px; font-size: 20px; font-weight: bold; z-index: 120; display: none; align-items: center; gap: 10px; backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.15); pointer-events: none; font-family: "Inter", sans-serif; } .pk-p-vol-indicator svg { fill: #fff !important; width: 100%; height: 100%; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); } .pk-is-vol-active .pk-p-center-play, .pk-is-vol-active .pk-p-loading { display: none !important; opacity: 0 !important; } .pk-input-err-msg { color: #ff4d4f; font-size: 12px; margin-top: 8px; min-height: 18px; visibility: hidden; transition: opacity 0.2s; } .pk-ana-select { position: relative; width: 85px; flex-shrink: 0; } #an_val_min::-webkit-outer-spin-button, #an_val_min::-webkit-inner-spin-button, #an_val_max::-webkit-outer-spin-button, #an_val_max::-webkit-inner-spin-button, #sc_val_min::-webkit-outer-spin-button, #sc_val_min::-webkit-inner-spin-button, #sc_val_max::-webkit-outer-spin-button, #sc_val_max::-webkit-inner-spin-button, #sh_mod_cnt_val::-webkit-outer-spin-button, #sh_mod_cnt_val::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } #an_val_min, #an_val_max, #sc_val_min, #sc_val_max, #sh_mod_cnt_val { -moz-appearance: textfield; } #an_val_min, #an_val_max { padding-right:32px !important; } .pk-num-ctrl { position: absolute; right: 8px; top: 4px; bottom: 4px; display: flex; flex-direction: column; width: 24px; gap: 1px; z-index: 5; } .pk-num-btn { flex: 1; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-icon-c); border-radius: 3px; transition: all 0.1s; } .pk-num-btn:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-num-btn:active { transform: scale(0.9); } .pk-num-btn svg { width: 14px; height: 14px; stroke-width: 3; } .pk-ana-trigger { width: 100%; height: 42px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; border: 2px solid var(--pk-bd); border-radius: 8px; background: var(--pk-bg); color: var(--pk-fg); cursor: pointer; font-weight: 700; font-size: 14px; transition: border-color 0.2s; box-sizing: border-box; } .pk-ana-trigger:hover { border-color: var(--pk-pri); } .pk-ana-menu { position: absolute; top: 100%; left: 0; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; margin-top: 6px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); z-index: 10010; display: none; overflow: hidden; } .pk-ana-item { padding: 10px 12px; cursor: pointer; font-size: 13px; color: var(--pk-fg); transition: background 0.1s; } .pk-ana-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-ana-item.act { color: var(--pk-pri); font-weight: 700; background: rgba(0, 103, 192, 0.05); } .pk-msg-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--pk-toast-bg); color: var(--pk-toast-fg); padding: 12px 28px; border-radius: 12px; z-index: 20000; font-size: 14px; font-weight: 600; pointer-events: none; opacity: 0; transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); text-align: left; display: flex; align-items: center; justify-content: center; gap: 12px; backdrop-filter: blur(8px); border: 1px solid var(--pk-toast-bd); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .pk-msg-toast.show { opacity: 1; transform: translate(-50%, -60%); } .pk-crumb-sep { width: 22px; height: 22px; margin: 0 2px; cursor: pointer; border-radius: 4px; color: #aaa; transition: all 0.2s; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } .pk-crumb-sep:hover, .pk-crumb-sep.pk-active { background: var(--pk-hl); color: currentColor; } .pk-crumb-sep svg { width: 14px; height: 14px; stroke-width: 3.5; transition: transform 0.2s ease; } .pk-crumb-pop { position: fixed; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); z-index: 2147483647 !important; min-width: 180px; max-width: 320px; max-height: 50vh; overflow-y: auto; padding: 6px 0; opacity: 0; pointer-events: none; transition: opacity 0.1s ease; border-top: 1px solid rgba(255, 255, 255, 0.05); } .pk-crumb-pop.pk-show { opacity: 1; pointer-events: auto; } .pk-crumb-item { padding: 8px 12px; font-size: 13px; line-height: 1.5; cursor: pointer; display: flex; align-items: center; gap: 8px; color: var(--pk-fg); } .pk-crumb-item:hover { background: var(--pk-hl); } .pk-crumb-item svg { flex-shrink: 0; } .pk-crumb-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; padding-bottom: 2px; margin-bottom: -2px; } @keyframes pkFadeInOnly { from { opacity: 0; } to { opacity: 1; } } .pk-share-modal { width: 540px !important; box-sizing: border-box; display: flex; flex-direction: column; gap: 20px; padding: 0 !important; overflow: visible !important; } .pk-s-sec { display: flex; flex-direction: column; gap: 12px; padding: 0 28px; box-sizing: border-box; } .pk-detail-cancel-btn { cursor: pointer; color: var(--pk-pri); font-size: 15px; padding: 8px 12px; border-radius: 6px; transition: background 0.2s; margin-left: -12px; } .pk-detail-cancel-btn:hover { background: var(--pk-hl); } .pk-share-stat-box { display: flex; background: var(--pk-hl); border-radius: 10px; padding: 10px 0; margin-bottom: 18px; border: 1px solid var(--pk-bd); } .pk-share-stat-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; } .pk-share-stat-item:first-child::after { content: ''; position: absolute; right: 0; top: 25%; bottom: 25%; width: 1px; background: var(--pk-bd); } .pk-share-stat-val { font-size: 20px; font-weight: 700; color: var(--pk-fg); line-height: 1.1; font-family: "Inter", system-ui, sans-serif; } .pk-share-stat-lbl { font-size: 11px; color: #888; margin-top: 2px; font-weight: 500; } .pk-s-lbl { font-size: 13px; font-weight: 700; color: var(--pk-fg); opacity: 0.6; text-transform: uppercase; letter-spacing: 0.5px; } .pk-s-tabs { display: flex; background: var(--pk-hl); border-radius: 8px; padding: 4px; gap: 4px; border: 1px solid var(--pk-bd); } .pk-s-tab { flex: 1; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-fg); font-size: 14px; border-radius: 6px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0.7; } .pk-s-tab.act { background: var(--pk-bg); color: var(--pk-pri); opacity: 1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); font-weight: 700; } .pk-s-opts { display: flex; flex-flow: row nowrap; gap: 20px; align-items: center; justify-content: flex-start; } .pk-s-opt { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; color: var(--pk-fg); white-space: nowrap; flex-shrink: 0; } .pk-s-lbl { font-size: 13px; font-weight: 600; color: var(--pk-fg); opacity: 0.8; } .pk-s-tabs { display: flex; background: var(--pk-hl); border-radius: 6px; padding: 3px; gap: 2px; border: 1px solid var(--pk-bd); } .pk-s-tab { flex: 1; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-fg); font-size: 13px; border-radius: 4px; transition: all 0.2s; opacity: 0.7; } .pk-s-tab.act { background: var(--pk-bg); color: var(--pk-pri); opacity: 1; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); font-weight: bold; } .pk-s-opts { display: flex; flex-wrap: nowrap; gap: 12px; align-items: center; justify-content: space-between; } .pk-s-opt { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; color: var(--pk-fg); white-space: nowrap; flex-shrink: 0; } .pk-s-opt input[type="radio"] { appearance: none; width: 16px; height: 16px; border: 1px solid var(--pk-icon-c); border-radius: 50%; position: relative; cursor: pointer; margin: 0; background: var(--pk-bg); transition: all 0.2s; } .pk-s-opt input:checked { border-color: var(--pk-pri); border-width: 5px; } .pk-s-input { flex: 1; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 4px; color: var(--pk-fg); padding: 5px 10px; font-size: 13px; outline: none; transition: border-color 0.2s; min-width: 0; } .pk-s-input:focus { border-color: var(--pk-pri); } .pk-s-input:disabled { opacity: 0.3; cursor: not-allowed; background: var(--pk-hl); } .pk-share-res-val { font-family: "SF Mono", "Consolas", monospace; font-weight: 600; color: var(--pk-pri) !important; } .pk-cal-pop { position: absolute; background: var(--pk-bg); color: var(--pk-fg); border: 1px solid var(--pk-bd); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); border-radius: 8px; z-index: 10005; display: flex; font-size: 13px; overflow: hidden; animation: pkFadeIn 0.2s; user-select: none; } .pk-cal-side { width: 110px; background: var(--pk-hl); border-right: 1px solid var(--pk-bd); display: flex; flex-direction: column; padding: 8px 0; } .pk-cal-side-item { padding: 8px 16px; cursor: pointer; color: var(--pk-fg); transition: background 0.2s; } .pk-cal-side-item:hover { background: rgba(0, 0, 0, 0.05); } .pk-cal-side-item.active { color: var(--pk-pri); font-weight: bold; background: var(--pk-bg); } .pk-cal-main { width: 280px; padding: 10px; display: flex; flex-direction: column; } .pk-cal-hd { display: flex; justify-content: space-between; align-items: center; padding: 0 8px 10px 8px; border-bottom: 1px solid var(--pk-bd); } .pk-cal-nav-btn { cursor: pointer; padding: 4px; border-radius: 4px; color: #888; } .pk-cal-nav-btn:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-top: 10px; } .pk-cal-th { text-align: center; color: #888; font-size: 12px; padding-bottom: 6px; } .pk-cal-td { height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; transition: all 0.2s; position: relative; color: var(--pk-fg); } .pk-cal-td:hover:not(.disabled):not(.selected) { background: var(--pk-hl); color: var(--pk-pri); } .pk-cal-td.selected { background: var(--pk-pri); color: #fff; font-weight: bold; } .pk-cal-td.disabled { color: #888; cursor: not-allowed; opacity: 0.7; } .pk-share-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; padding: 16px 24px 24px 24px; border-top: 1px solid var(--pk-bd); background: transparent; } .pk-btn-quiet-red { color: #d93025; font-size: 14px; font-weight: 500; cursor: pointer; padding: 8px 12px; border-radius: 6px; transition: background 0.2s; margin-left: -8px; } .pk-btn-quiet-red:hover { background: rgba(217, 48, 37, 0.08); } .pk-btn-primary-action { background: var(--pk-pri); color: #fff; border: none; height: 38px; padding: 0 20px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .pk-btn-primary-action:hover { filter: brightness(1.08); box-shadow: 0 2px 8px rgba(0, 103, 192, 0.25); } .pk-cal-td.today::after { content: ''; position: absolute; bottom: 4px; width: 4px; height: 4px; background: currentColor; border-radius: 50%; opacity: 0.5; } .pk-share-icon-wrap { position: relative; width: 30px; height: 30px; margin-right: 12px; flex-shrink: 0; transition: transform 0.2s ease; transform-origin: center center; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; } .pk-share-lock { position: absolute; bottom: -2px; right: -4px; width: 14px; height: 14px; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 10; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); border: none; box-shadow: none; pointer-events: none; } .pk-ov.pk-dark .pk-share-lock { filter: drop-shadow(0 0 2px rgba(0, 0, 0, 1)) drop-shadow(0 2px 5px rgba(0, 0, 0, 1)); } .pk-name { display: flex !important; align-items: center; min-width: 0; width: 100%; overflow: hidden; } .pk-name-txt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .pk-select-item { padding: 10px 12px; border-radius: 5px; cursor: pointer; color: var(--pk-fg); font-size: 14px; transition: background 0.1s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-select-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-select-item.act { background: rgba(0, 103, 192, 0.1); color: var(--pk-pri); font-weight: 700; } .pk-select-label { position: absolute; top: 0; transform: translate3d(0, -50%, 0); -webkit-transform: translate3d(0, -50%, 0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; left: 10px; background: var(--pk-bg); padding: 0 5px; font-size: 11px; color: var(--pk-pri); font-weight: bold; pointer-events: none; z-index: 10; line-height: 1; pointer-events: none; } .pk-field input, .pk-field select, .pk-share-modal input, .pk-ov input { transform: translateZ(0); will-change: transform; } .pk-maximized { position: fixed !important; width: 100% !important; height: 100% !important; max-width: none !important; top: 0 !important; left: 0 !important; border-radius: 0 !important; border: none !important; z-index: 2147483647 !important; } .pk-maximized .pk-sidebar { width: 190px !important; align-items: flex-start !important; padding: 20px 10px !important; } .pk-maximized .pk-nav-btn { width: 100% !important; justify-content: flex-start !important; padding: 0 15px !important; gap: 10px; height: 54px !important; border-radius: 8px !important; } .pk-nav-btn span { display: none; font-size: 14px; font-weight: 600; white-space: nowrap; } .pk-maximized .pk-nav-btn span { display: inline-block; } .pk-maximized #pk-quota-panel { align-items: flex-start !important; padding: 0 10px !important; margin-bottom: 6px !important; opacity: 0.9 !important; gap: 4px !important; transition: none !important; } .pk-cloud-area { width: 100%; height: 180px; background: #f1f3f5; border: none; border-radius: 8px; padding: 15px; font-size: 14px; line-height: 1.6; color: #1a1a1a; resize: none; outline: none; font-family: inherit; cursor: auto; } .pk-dark .pk-cloud-area { background: #2d2d2d !important; color: #f5f5f5 !important; caret-color: #fff !important; } .pk-dark .pk-cloud-area::placeholder { color: #666 !important; } .pk-cloud-area::placeholder { color: #adb5bd; } #pk_cloud_torrent_trigger:hover, #pk_cloud_change_dir:hover { text-decoration: underline; } .pk-maximized #pk-quota-bar-box { width: 100% !important; height: 6px !important; transition: none !important; border-radius: 3px !important; } .pk-maximized #pk-quota-txt { font-size: 12px !important; transform: none !important; opacity: 1; font-weight: normal !important; white-space: nowrap; } .pk-maximized .pk-hd { height: 60px !important; padding: 0 20px !important; } .pk-maximized .pk-tt { font-size: 24px !important; } .pk-maximized .pk-tt svg { width: 32px !important; height: 32px !important; } .pk-maximized .pk-tb { height: 60px !important; padding: 0 16px !important; gap: 10px !important; } .pk-maximized .pk-btn { height: 40px !important; font-size: 15px !important; padding: 0 16px !important; } .pk-btn svg { width: auto; height: auto; } #pk-theme svg, #pk-maximize svg { width: 16px !important; height: 16px !important; } #pk-help svg { width: 19px !important; height: 19px !important; } #pk-close svg { width: 19px !important; height: 19px !important; } .pk-maximized #pk-theme svg, .pk-maximized #pk-maximize svg { width: 24px !important; height: 24px !important; } .pk-maximized #pk-help svg { width: 26px !important; height: 29px !important; } .pk-maximized #pk-close svg { width: 28px !important; height: 28px !important; } .pk-maximized .pk-grid-hd { height: 50px !important; font-size: 15px !important; padding: 0 26px 0 20px !important; } .pk-maximized .pk-row, .pk-maximized .pk-group-hd { height: 60px !important; font-size: 16px !important; padding: 0 20px !important; } .pk-maximized .pk-group-hd { border-top-width: 10px !important; border-bottom-width: 10px !important; } .pk-maximized .pk-name svg { width: 60px !important; height: 60px !important; margin-right: 20px !important; } .pk-maximized .pk-row .pk-name .pk-name-txt { white-space: normal !important; display: -webkit-box !important; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden !important; text-overflow: ellipsis !important; line-height: 1.35 !important; word-break: break-all !important; margin-top: 0 !important; } .pk-max-icon-box { position: relative !important; width: 50px !important; height: 50px !important; margin-right: 20px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; flex-shrink: 0 !important; vertical-align: middle !important; overflow: visible !important; } .pk-placeholder-icon { position: absolute !important; inset: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; z-index: 1 !important; transition: opacity 0.2s, visibility 0.2s; } .pk-placeholder-icon svg { width: 44px !important; height: 44px !important; object-fit: contain !important; } .pk-max-icon-box .pk-max-thumb { position: absolute !important; top: 0 !important; left: 0 !important; width: 50px !important; height: 50px !important; object-fit: cover !important; border-radius: 4px !important; opacity: 0; z-index: 2 !important; transition: opacity 0.25s ease-in-out; background: transparent; } .pk-maximized .pk-share-icon-wrap { width: 50px !important; height: 50px !important; margin-right: 20px !important; background: transparent !important; overflow: visible !important; } .pk-maximized .pk-row:has(.pk-max-thumb) .pk-name-txt { padding-left: 0 !important; } .pk-maximized .pk-nav-btn svg { width: 28px !important; height: 28px !important; } body:not(.pk-body-max):has(.pk-modal-ov) #pk-launch { filter: grayscale(1) brightness(0.6) !important; opacity: 0.5 !important; pointer-events: none !important; cursor: not-allowed !important; transition: filter 0.3s, opacity 0.3s; } body:has(.pk-ov:not([style*="display: none"])), body:has(.pk-img-ov), body:has(#pk-player-ov) { #pk-launch { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } } .pk-maximized .pk-search input { height: 40px !important; font-size: 15px !important; padding: 0 70px 0 15px !important; } .pk-maximized #pk-search-btn { width: 18px !important; height: 18px !important; right: 14px !important; } .pk-maximized .pk-search-clear { width: 24px !important; height: 24px !important; right: 40px !important; } .pk-maximized input[type="checkbox"] { width: 18px !important; height: 18px !important; transform: none !important; margin: 0 8px 0 0 !important; } .pk-maximized .pk-star-icon, .pk-maximized .pk-star-toggle, .pk-maximized .pk-col[data-k="starred"] svg { width: 16px !important; height: 16px !important; } .pk-maximized .pk-name { overflow: visible !important; } .pk-maximized .pk-share-icon-wrap { width: 40px !important; height: 40px !important; margin-right: 16px !important; position: relative !important; display: block !important; overflow: visible !important; } .pk-maximized .pk-share-icon-wrap { width: 50px !important; height: 50px !important; margin-right: 20px !important; display: flex !important; align-items: center !important; justify-content: center !important; transform: none !important; overflow: visible !important; } .pk-maximized .pk-share-icon-wrap > svg { width: 100% !important; height: 100% !important; min-width: 100% !important; min-height: 100% !important; transform: scale(1.25) !important; transform-origin: center center !important; margin: 0 !important; } .pk-maximized .pk-share-icon-wrap > div:not(.pk-share-lock) { width: 100% !important; height: 100% !important; display: flex !important; align-items: center !important; justify-content: center !important; transform: none !important; margin: 0 !important; } .pk-maximized .pk-share-icon-wrap > div:not(.pk-share-lock) > svg { width: 100% !important; height: 100% !important; min-width: 100% !important; min-height: 100% !important; transform: scale(1.25) !important; transform-origin: center center !important; margin: 0 !important; } .pk-maximized .pk-share-icon-wrap img { width: 100% !important; height: 100% !important; min-width: 100% !important; min-height: 100% !important; object-fit: contain !important; transform: none !important; } .pk-maximized .pk-share-lock { width: 18px !important; height: 18px !important; bottom: -2px !important; right: -12px !important; z-index: 15 !important; } .pk-maximized .pk-row > div { font-size: 16px !important; font-weight: normal !important; } .pk-maximized .pk-row div[style*="font-size:12px"], .pk-maximized .pk-row div[style*="font-size: 12px"] { font-size: 15px !important; font-weight: 400 !important; opacity: 0.9; } .pk-maximized .pk-row div[style*="tabular-nums"] { font-weight: 400 !important; } .pk-maximized .pk-share-lock svg { width: 100% !important; height: 100% !important; color: #fff !important; } .pk-maximized .pk-ft { height: 50px !important; font-size: 15px !important; padding: 0 20px !important; } .pk-maximized .pk-stat { font-size: 15px !important; } .pk-maximized .pk-ft .pk-btn { height: 36px !important; font-size: 15px !important; } .pk-maximized #pk-filter-cat-label { height: 40px !important; font-size: 15px !important; padding: 0 16px !important; } .pk-maximized #pk-filter-exts-wrap { height: 40px !important; } .pk-maximized .pk-f-ext { font-size: 15px !important; padding: 6px 12px !important; } .pk-maximized #pk-filter-exit-btn { height: 40px !important; font-size: 15px !important; padding: 0 20px !important; } .pk-maximized .pk-bl-area { font-size: 15px !important; line-height: 1.6 !important; } .pk-maximized #pk-crumb span { font-size: 16px !important; display: inline-flex !important; align-items: center !important; height: 32px !important; padding: 0 8px !important; border-radius: 4px !important; margin: auto 2px !important; } .pk-maximized #pk-crumb div span { font-size: 18px !important; } .pk-maximized #pk-crumb div span[style*="font-size:11px"], .pk-maximized #pk-crumb div span[style*="font-size: 11px"] { font-size: 14px !important; margin-left: 12px !important; } .pk-maximized #pk-crumb svg { width: 18px !important; height: 18px !important; vertical-align: middle !important; margin-top: -1px !important; } .pk-maximized .pk-crumb-sep { width: 26px !important; height: 26px !important; margin: auto 4px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; } .pk-maximized .pk-crumb-sep svg { width: 20px !important; height: 20px !important; } .pk-custom-select { position: relative; width: 100%; } .pk-select-trigger { display: flex; align-items: center; justify-content: space-between; height: 44px; padding: 0 15px; border: 2px solid var(--pk-bd); border-radius: 8px; background: var(--pk-bg); color: var(--pk-fg); font-size: 14px; font-weight: 600; cursor: pointer; box-sizing: border-box; transition: all 0.2s; } .pk-select-trigger:hover { border-color: var(--pk-pri); } .pk-select-trigger svg { width: 16px !important; height: 16px !important; min-width: 16px; min-height: 16px; color: #999; flex-shrink: 0; } .pk-dropdown-wrap { position: relative; display: inline-flex; height: 32px; align-items: center; } .pk-dropdown-menu { position: absolute; top: 100%; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); display: none; z-index: 10010; min-width: 140px; padding: 4px 0; margin-top: 6px; flex-direction: column; overflow: hidden; } .pk-dropdown-item { padding: 10px 16px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--pk-fg); font-size: 13px; transition: background 0.1s; white-space: nowrap; } .pk-dropdown-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-pop-max { width: 170px !important; min-width: 170px !important; padding: 6px 0 !important; border-radius: 10px !important; box-shadow: 0 8px 30px rgba(0,0,0,0.3) !important; } .pk-pop-max .pk-dropdown-item { padding: 12px 15px !important; font-size: 15px !important; gap: 10px !important; font-weight: 600 !important; } .pk-pop-max .pk-dropdown-item svg { width: 22px !important; height: 22px !important; } .pk-btn-arrow { margin-left: 2px; opacity: 0.6; transition: transform 0.2s; } .pk-aria-status-box { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: bold; margin-top: 6px; transform: translateZ(0); -webkit-transform: translateZ(0); backface-visibility: hidden; will-change: transform; cursor: default; } .pk-aria-dot { width: 8px; height: 8px; border-radius: 50%; background: #ccc; transform: translateZ(0); } .pk-aria-dot.ok { background: #52c41a; box-shadow: 0 0 8px rgba(82, 196, 26, 0.5); } .pk-aria-dot.err { background: #ff4d4f; } .pk-aria-dot.wait { background: var(--pk-pri); animation: pk-pulse 1.5s infinite; } .pk-dropdown-wrap.active .pk-btn-arrow { transform: rotate(180deg); } .pk-select-menu { position: absolute; top: 100%; left: 0; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; margin-top: 6px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); z-index: 10010; display: none; overflow: hidden; max-height: 240px; } .pk-share-icon-wrap { position: relative; width: 30px; height: 30px; margin-right: 12px; flex-shrink: 0; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; } .pk-share-lock { position: absolute; bottom: -3px; right: -6px; width: 17px; height: 17px; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 10; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); border: none; box-shadow: none; pointer-events: none; } .pk-ov.pk-dark .pk-share-lock { filter: drop-shadow(0 0 2px rgba(0, 0, 0, 1)) drop-shadow(0 2px 5px rgba(0, 0, 0, 1)); } .pk-bl-marker { position: absolute !important; bottom: 0px !important; left: 4px !important; width: 10px !important; height: 10px !important; display: flex !important; align-items: center; justify-content: center; z-index: 99 !important; pointer-events: none; filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 2px rgba(255,255,255,0.5)); } .pk-maximized .pk-bl-marker { width: 18px !important; height: 18px !important; bottom: -1px !important; left: 9px !important; } .pk-min-icon, .pk-max-icon-box { position: relative !important; overflow: visible !important; display: inline-flex !important; } .pk-active-border { border-color: var(--pk-pri) !important; } #pk_dl_group { transform: translateZ(0); backface-visibility: hidden; will-change: border-color; } #pk_dl_group:hover { border-color: var(--pk-pri) !important; } #pk_dl_group.pk-typing-active:hover { border-color: var(--pk-bd) !important; } .pk-row > div.pk-name, .pk-min-icon, .pk-max-icon-box, .pk-min-media-box { overflow: visible !important; } .pk-maximized .pk-row .pk-name>img[style*="width:24px"] { width: 48px !important; height: 48px !important; margin-right: 20px !important; margin-left: -4px !important; } .pk-maximized .pk-row .pk-name>div[style*="width:24px"] { width: 48px !important; height: 48px !important; margin-right: 20px !important; margin-left: -4px !important; } .pk-maximized .pk-row .pk-name>div[style*="width:24px"] svg { width: 36px !important; height: 36px !important; } .pk-maximized .pk-row div[style*="width:100px"] { width: 200px !important; height: 8px !important; } .pk-min-icon-box { position: relative; width: 24px; height: 24px; margin-right: 12px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; } .pk-min-placeholder { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 1; transition: opacity 0.2s; } .pk-min-thumb { position: absolute; inset: 0; width: 24px; height: 24px; object-fit: cover; border-radius: 4px; z-index: 2; opacity: 0; transition: opacity 0.2s; } .pk-drag-mask { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.92); z-index: 9999; display: none; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; backdrop-filter: blur(4px); } .pk-dark .pk-drag-mask { background: rgba(32, 32, 32, 0.92); } .pk-drag-icon { width: 80px; height: 80px; background: var(--pk-pri); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; margin-bottom: 24px; box-shadow: 0 10px 30px rgba(26, 94, 255, 0.3); } .pk-drag-hint { font-size: 20px; font-weight: 700; color: var(--pk-fg); margin-bottom: 12px; } .pk-drag-path { font-size: 14px; color: #888; display: flex; align-items: center; gap: 6px; } .pk-help-scroll { max-height: 380px; overflow-y: auto; overscroll-behavior: contain; } .pk-body-max .pk-help-scroll { max-height: 75vh; } body.pk-hide-all-ui #pk-launch, body.pk-hide-all-ui .pk-ov, body.pk-hide-all-ui .pk-modal-ov, body.pk-hide-all-ui .pk-img-ov, body.pk-hide-all-ui #pk-player-ov, body.pk-hide-all-ui .pk-cal-pop, body.pk-hide-all-ui .pk-crumb-pop, body.pk-hide-all-ui .pk-hist-pop, body.pk-hide-all-ui .pk-tooltip, body.pk-hide-all-ui .pk-msg-toast, body.pk-hide-all-ui .pk-float-bar-item, body.pk-hide-all-ui .pk-selection-box, body.pk-hide-all-ui .pk-drag-ghost { display: none !important; opacity: 0 !important; pointer-events: none !important; } .pk-ana-select-btn { border: 1px solid var(--pk-bd); background: var(--pk-bg); color: var(--pk-fg); height: 32px; border-radius: 6px; padding: 0 10px; font-size: 13px; cursor: pointer; display: none; align-items: center; gap: 6px; transition: all 0.2s; white-space: nowrap; margin-right: 8px; } .pk-ana-select-btn:hover { border-color: var(--pk-pri); color: var(--pk-pri); background: rgba(var(--pk-bg-rgb), 0.05); } .pk-ana-pop { position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 1000; display: none; flex-direction: column; padding: 8px; width: 340px; gap: 4px; pointer-events: auto; } .pk-ana-pop-row { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } .pk-ana-opt { padding: 8px 4px; border-radius: 4px; cursor: pointer; font-size: 12px; color: var(--pk-fg); transition: background 0.1s; text-align: center; border: 1px solid transparent; background: var(--pk-hl); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-ana-opt:hover { background: var(--pk-sel-bg); color: var(--pk-pri); border-color: var(--pk-sel-bd); } `; ; const sleep = ms => new Promise(r => setTimeout(r, ms)); const esc = s => (s || '').replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); const fmtSize = n => { n = parseInt(n || 0, 10); if (isNaN(n)) return ''; if (n === 0) return '0 KB'; const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return (n < 10 ? n.toFixed(2) : n.toFixed(1)) + ' ' + u[i]; }; const fmtDate = t => { if (!t) return '-'; const d = new Date(new Date(t).getTime() + (8 * 60 * 60 * 1000)); const pad = n => String(n).padStart(2, '0'); return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`; }; const fmtDur = s => { s = Math.max(0, parseInt(s, 10) || 0); if (s <= 0) return "00:00"; const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sc = s % 60; const mmss = String(m).padStart(2, '0') + ':' + String(sc).padStart(2, '0'); return h > 0 ? String(h).padStart(2, '0') + ':' + mmss : mmss; }; function gmGet(key, def) { if (typeof GM_getValue !== 'undefined') { let v = GM_getValue(key, def); return (v === null) ? def : v; } return def; } function gmSet(key, val) { if (typeof GM_setValue !== 'undefined') GM_setValue(key, val); } const calcSha1 = async (file) => { if (!window.crypto || !window.crypto.subtle) return ""; const CHUNK_SIZE = 20 * 1024 * 1024; const chunks = Math.ceil(file.size / CHUNK_SIZE); if (file.size > 100 * 1024 * 1024) { const head = file.slice(0, 1024 * 1024); const mid = file.slice(file.size / 2, file.size / 2 + 1024 * 1024); const tail = file.slice(file.size - 1024 * 1024); const combined = new Blob([head, mid, tail]); const buffer = await combined.arrayBuffer(); const hash = await crypto.subtle.digest('SHA-1', buffer); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); } const buffer = await file.arrayBuffer(); const hash = await crypto.subtle.digest('SHA-1', buffer); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); }; function getLang(){const u=gmGet('pk_lang','');if(u)return u;const n=navigator.language.toLowerCase();return (n==='zh'||n.startsWith('zh-cn')||n.startsWith('zh-sg'))?'zh':(n.startsWith('zh-tw')||n.startsWith('zh-hk')||n.startsWith('zh-mo'))?'tc':n.startsWith('ko')?'ko':n.startsWith('ja')?'ja':'en';} const T = { zh: { /* --- 通用与基础UI --- */ title: "PikPak 增强大师", str_original: "原画", str_original_fast: "原画 (高速)", str_folders: "文件夹", str_files: "文件", unit_folders: "个文件夹", unit_days: "天", unit_month: "月", unit_sec: "秒", str_no_files: "暂无文件", str_items: "项", col_name: "名称", col_size: "大小", col_dur: "类型/时长", col_duration_only: "时长", col_progress: "播放进度", col_play_time: "播放时间", col_date: "修改日期", col_remaining: "剩余时长", col_path: "路径", col_old: "原名称", col_new: "新名称", col_type: "类型", col_path_name: "路径 / 名称", col_action: "操作", lbl_folder_first: "文件夹置顶", tag_default: "默认", current_dir: "当前目录", str_same_folder: "(同文件夹)", lbl_dont_show: "不再提醒", lbl_dont_show_session: "本次查重不再提示", str_empty_filename: "(空文件名)", str_empty_dir: "(空目录)", btn_filter: "筛选", title_file_filter: "文件筛选", cat_all: "全部", cat_video: "视频", cat_audio: "音频", cat_image: "图片", cat_document: "文档", cat_software: "软件", cat_archive: "压缩包", cat_torrent: "BT种子", cat_other: "其他", btn_exit_filter: "退出筛选", /* --- 属性面板 --- */ ctx_property: "属性", title_property: "文件属性", lbl_prop_name: "文件名称", lbl_prop_size: "文件大小", lbl_prop_count: "文件数量", lbl_prop_ctime: "创建时间", lbl_prop_mtime: "修改时间", lbl_prop_source: "添加来源", lbl_prop_link: "资源链接", lbl_prop_path: "文件位置", str_prop_cloud: "云添加", str_prop_share: "来自分享", str_prop_user: "用户上传", str_prop_unknown: "未知来源", fmt_prop_count: "包含 {f} 个文件,{d} 个文件夹", str_prop_offline: "离线任务", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "主页", btn_nav_share: "我的分享", btn_nav_offline: "离线下载", btn_nav_recent: "最近添加", btn_nav_history: "播放历史", btn_nav_starred: "收藏夹", btn_nav_trash: "回收站", btn_nav_upload: "我的上传", title_offline: "我的离线", trash_title: "回收站", trash_notice: "回收站的文件最多保存15天", history_notice: "仅记录在脚本环境内产生的播放进度", ctx_open: "打开", ctx_add_bl: "添加到资源管理器", ctx_remove_bl: "从资源管理器移除", ctx_rename: "重命名", ctx_copy: "复制", ctx_copy_name: "复制文件名", ctx_copy_link: "复制链接", ctx_cut: "移动", ctx_del: "删除", ctx_down: "下载", ctx_star: "添加星标", ctx_unstar: "取消星标", ctx_locate: "在文件夹中查看", ctx_share: "分享", /* --- 通用文件操作按钮 --- */ btn_down: "下载", tip_down: "下载 [Alt] + [D]", btn_aria2: "发送 Aria2", tip_aria2: "发送 Aria2 [Alt] + [A]", btn_refresh_short: "刷新", tip_refresh: "刷新 [F5]", btn_newfolder: "新建文件夹", tip_newfolder: "新建文件夹 [F8]", btn_del: "删除", tip_del: "删除 [Delete]", btn_deselect: "取消选择", tip_deselect: "取消选择 [Esc]", btn_invert: "反选", btn_copy: "复制", tip_copy: "复制 [Ctrl] + [C]", btn_cut: "移动", tip_cut: "移动 [Ctrl] + [X]", btn_paste: "粘贴", tip_paste: "粘贴 [Ctrl] + [V]", btn_clear_history: "删除历史", tip_clear_history: "删除历史 [Delete]", btn_restore: "还原", tip_restore: "还原 [R]", btn_del_forever: "永久删除", tip_del_forever: "永久删除 [Delete]", btn_empty_trash: "清空回收站", tip_empty_trash: "清空回收站 [Shift] + [Delete]", btn_exit: "退出", btn_close: "关闭", tip_close: "关闭 [Esc]", tip_theme: "切换主题 [Alt] + [T]", tip_rotate: "旋转 [R]", tip_mirror: "镜像翻转 [H]", tip_flip_v: "垂直翻转 [V]", tip_maximize: "最大化 [M]", tip_minimize: "最小化 [M]", tip_full_screen: "全屏 [Enter]", btn_help: "帮助", tip_help: "帮助 [Alt] + [H]", btn_view_file: "查看文件", btn_jump: "跳转", btn_copy_text: "复制", btn_stop: "停止", tip_stop: "立即停止当前操作", btn_settings: "设置", btn_logout: "退出 PikPak", msg_logout_confirm: "确定要退出登录吗?", tip_settings: "设置和更多 [Alt] + [S]", lbl_upload_to: "上传文件至: ", msg_move_done: "移动完成。", /* --- 离线、上传与云下载 --- */ btn_upload: "本地上传", btn_up_file: "上传文件", btn_up_folder: "上传文件夹", btn_cloud_download: "云下载", btn_up_pause: "暂停任务", tip_up_pause: "暂停任务 [Alt] + [P]", btn_up_start: "开始任务", tip_up_start: "开始任务 [Alt] + [G]", btn_up_del: "删除任务", tip_up_del: "删除任务 [Delete]", btn_up_clear_all: "清空任务", tip_up_clear_all: "清空任务 [Shift] + [Delete]", btn_retry_task: "重试任务", tip_retry_task: "重试任务 [R]", col_task_status: "任务状态", col_task_progress: "离线进度", col_up_speed: "速度", col_up_status: "状态", lbl_task_run: "进行中", lbl_task_fail: "已失败", lbl_task_ok: "已完成", lbl_up_run: "进行中", lbl_up_pause: "已中断", lbl_up_downloading: "下载中", lbl_up_done: "已完成", tip_up_pause_desc: "包含手动暂停及报错的任务", title_cloud_task: "创建云下载任务", ph_cloud_links: "支持链接格式:\n- 各种下载链接,如magnet。\n- YouTube、X (Twitter)、TikTok、Facebook等分享链接。\n通过换行可一次性添加多条链接。", lbl_save_to: "文件将被保存至:", lbl_default_folder: "默认文件夹", btn_via_torrent: "通过 Torrent 创建", tip_cloud_save_path: "常规云下载文件会保存在 My Pack 目录,来自其他 App 的云下载文件会保存至 My [XYZ] 目录,[XYZ] 为 App 名称。", lbl_smart_fix: "自动修复防屏蔽磁链 (提取特征码/剔除文字干扰)", title_save_method: "保存方式", msg_save_snapshot_desc: "此链接只能被存储为网页快照。", tip_snapshot_details: "PikPak 不能从此链接中直接采集媒体文件,您可以保存网页快照。PikPak 将尽可能保存完整的网页内容到快照文件中。", btn_save_snapshot: "保存快照", btn_create_now: "立即创建", btn_modify: "修改", str_snap_link_count_suffix: " 等 {n} 个链接", /* --- 分享管理 --- */ btn_cancel_share: "取消分享", share_copy_suffix: "复制这段内容后打开 PikPak-App,畅享极速秒播", share_copy_pwd: "密码", title_share_detail: "分享详情", ctx_share_detail: "查看分享详情", ctx_share_copy: "复制链接和密码", col_view: "浏览", col_save: "保存", col_share_time: "分享时间", col_share_status: "分享状态", lbl_limit_reached: "次数已满", lbl_limit_tip: "限制次数", lbl_share_view: "浏览", lbl_share_save: "保存", lbl_share_link_title: "分享链接", lbl_share_pwd_title: "密码", lbl_share_expire_title: "有效期", btn_copy_link_pwd: "复制链接和密码", str_expire_suffix: "天后过期", ph_edit_pwd: "输入分享密码,支持 4-10 位", btn_close_pwd: "关闭密码", str_no_pwd: "无密码", title_edit_share_code: "分享代码修改", ph_edit_share_code: "支持 5-18 位文字、数字、及符号等", btn_add_share_code: "添加分享代码", btn_del_share_code: "删除分享代码", share_title: "分享文件", share_mode: "分享方式", share_public: "公开链接", share_encrypted: "加密链接", share_expiry: "设置有效期", share_pass: "设置提取码", share_count: "设置提取次数", share_count_ed: "提取次数", share_perm: "永久有效", share_unlimit: "不限", share_rand: "系统随机", share_custom: "自定义", share_days: "天", share_times: "次", btn_share_start: "立即分享", cal_custom_title: "自定义有效期", lbl_share_link: "链接", lbl_share_code: "提取码", btn_copy_share: "复制全部", str_share_expired: "已过期", str_share_deleted: "文件已删", title_edit_pwd: "密码修改", lbl_share_code_title: "分享代码", ph_password: "密码", ph_pass_range: "4-10位字符", cal_week_days:["日", "一", "二", "三", "四", "五", "六"], cal_months:["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "文件分析", btn_scan: "文件透视", lbl_scan_selected: "对选中的 {n} 个项目执行文件透视", lbl_keyword_filter: "排除关键词", ph_keyword_filter: "排除包含的关键词,多个用逗号分隔", lbl_scan_current: "对当前路径下所有项目执行文件透视", tip_dup: "文件查重", lbl_dup_selected: "对选中的 {n} 个项目执行文件查重", lbl_dup_current: "对当前路径下所有项目执行文件查重", tip_scan_dup: "筛选或查重文件", lbl_dup_tool: "选择删除对象:", lbl_dup_reset: "↺ 复原 (取消置顶 & 清空选择)", lbl_dup_select_folder: "📂 按文件夹选择", lbl_dup_select_folder_short: "📂 文件夹", lbl_dup_invert: "反选模式", lbl_dup_invert_short: "反选", tip_dup_invert_limit: "仅按文件夹选择可用", fmt_dup_count: "({n}个重复)", btn_start_scan: "开始扫描", tag_hash: "精准匹配", tag_hash_short: "精准", tag_name: "名称相似", tag_name_short: "名称", tag_sim: "时长相似", tag_sim_short: "时长", label_dup_video: "视频文件 (精准匹配 + 时长相似 + 名称相似)", label_dup_image: "图片文件 (精准匹配 + 名称相似)", label_dup_other: "其他文件 (精准匹配 + 名称相似)", btn_analyze: "文件夹分析", tip_analyze: "筛选或查重文件夹", btn_export: "导出目录", tip_export: "生成并下载当前路径的文件树列表", title_export_format: "导出目录样式", lbl_export_current: "对当前路径下所有项目执行目录导出", opt_tree_view: "目录树", opt_list_view: "目录列表", msg_exporting: "正在生成目录树...", str_analyze_results: "匹配结果", lbl_size_threshold: "检测阈值", title_analyze_result: "文件夹分析结果", opt_ana_large: "文件夹透视", lbl_analyze_selected: "对选中的 {n} 个项目执行文件夹透视", lbl_analyze_current: "对当前路径下所有项目执行文件夹透视", opt_ana_sim: "文件夹查重", lbl_ana_sim_selected: "对选中的 {n} 个项目执行文件夹查重", lbl_ana_sim_current: "对当前路径下所有项目执行文件夹查重", title_algo_help: "查重算法说明", algo_help_content: "名称匹配:查找名称和体积相近的文件夹群组。\n相似度匹配:查找内部文件高度重合的文件夹群组。\n包含率匹配:查找小文件夹的内容被大文件夹完全覆盖的子集冗余。\n\n精度:相似度匹配 > 包含率匹配 > 名称匹配\n范围:包含率匹配 > 相似度匹配", lbl_threshold: "阈值", lbl_sim_score: "相似度", lbl_containment: "包含率", lbl_name_match: "名称匹配", lbl_sim_match: "相似度匹配", lbl_contain_match: "包含率匹配", lbl_ana_min: "下限", lbl_ana_max: "上限", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "清理空文件夹", tip_prune: "清理空文件夹 [Ctrl] + [Delete]", btn_rename: "重命名", tip_rename: "重命名 [F2]", btn_bulkrename: "批量重命名", tip_bulkrename: "批量重命名 [F2]", title_blacklist: "资源管理器", btn_blacklist_run: "立即运行清理", btn_clear_list: "清空列表", tip_bl_desc: "下列项目在【删除】时会跳过,仅通过【立即运行清理】查找删除", tip_blacklist_input: "资源管理器 [Alt] +[Delete]", label_bl_folder: "文件夹名单 (精准查找)", label_bl_file: "文件名单 (精准查找)", lbl_type_folder: "文件夹", lbl_type_file: "文件", ph_bl_folder: "请通过“粘贴”或文件右键菜单“添加到资源管理器”导入。", ph_bl_file: "请通过“粘贴”或文件右键菜单“添加到资源管理器”导入。", modal_bl_preview: "检索结果", btn_bl_delete: "删除选中项", modal_rename_title: "重命名", modal_rename_multi_title: "批量重命名", btn_preview: "预览", modal_preview_title: "确认更改", label_pattern: "模式 (例: Video {n})", label_replace: "替换/删除", label_replace_note: "区分大小写", label_include_ext: "包含后缀", label_regex: "正则 (Regex)", placeholder_find: "查找内容", placeholder_replace: "替换为 (留空删除)", label_jav: "FC2 纯净命名", lbl_rn_pattern: "命名模板", lbl_rn_case_convert: "大小写转换", opt_rn_keep_origin: "(保持原样)", opt_rn_lower: "全部小写 (abc)", lbl_rn_mode_series: "剧集模式", lbl_rn_mode_format: "格式化", lbl_rn_mode_ad: "前缀去广告", lbl_rn_mode_ext: "后缀修复", opt_rn_upper: "全部大写 (ABC)", opt_rn_title: "首字母大写 (Abc)", lbl_rn_width_convert: "全半角转换", opt_rn_width_half: "全角转半角 (A->A)", opt_rn_width_full: "半角转全角 (A->A)", lbl_rn_preview_title: "变更预览", tip_jav_mode_desc: "✨ 智能提取FC2并去除无关字符", tip_ad_remove_desc: "🧹 智能滤除头部广告、网址及垃圾符号,自动清洗Emoji并修复括号格式", tip_ext_fix_desc: "🧩 根据文件真实类型 (MIME) 智能修正后缀", label_replace_find: "查找内容", label_replace_to: "替换为", /* --- 解压相关 --- */ btn_unzip: "批量解压", tip_unzip: "批量解压 [Alt] + [U]", btn_unzip_all: "全部解压", btn_understand_unzip: "理解并解压", title_input_pwd: "需要解压密码", lbl_pwd_prompt: "请输入密码:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "外部播放", tip_ext: "使用PotPlayer播放或获取播放链接 [Alt] +[E]", btn_img_search: "以图搜图 [F]", tip_play_search: "以图搜图 [F]", tip_pip: "画中画 [P]", str_no_sub: "无字幕", lbl_sub_sel: "字幕选择", lbl_show_sub: "显示字幕", btn_sub_search: "搜索在线字幕", btn_sub_cloud: "打开云盘字幕", btn_sub_local: "打开本地字幕", lbl_sub_pos: "字幕位置", lbl_sub_bottom: "底部", lbl_sub_top: "顶部", lbl_sub_bg_op: "背景透明", lbl_sub_size: "字幕大小", lbl_sub_offset: "字幕进度", title_sel_sub: "选择字幕", ph_sub_search: "输入关键词,下方链接自动更新...", btn_force_play: "尝试强行播放", str_compat_mode: "兼容模式", lang_code: "zh", btn_go_search: "🔍 去 {n} 手动搜", btn_restart: "从头播放", btn_prev_video: "上一个 [Ctrl + ←]", btn_next_video: "下一个 [Ctrl + →]", tip_plist_open: "展开 [E]", tip_plist_close: "收起 [E]", tab_sub: "字幕", tab_size: "尺寸", tab_more: "更多", lbl_ratio: "比例", lbl_direction: "方向", opt_ratio_def: "默认", btn_rot_l: "向左旋转", btn_rot_r: "向右旋转", btn_flip_h: "水平翻转", btn_flip_v: "垂直翻转", lbl_play_end: "当播放结束", opt_list_loop: "列表循环", opt_single_loop: "单集循环", opt_play_stop: "播完暂停", lbl_skip_op: "跳过片头", lbl_skip_ed: "跳过片尾", export_link_title: "导出视频串流链接", btn_start_play: "开始播放", btn_copy_link: "复制链接", tip_copy_link: "复制链接 [Alt] + [C]", opt_player_other: "其他 (导出链接)", lbl_player: "播放器", btn_mark: "标记", lbl_resolution: "清晰度", str_switch_compat: "当前清晰度不可用,已为您恢复至 {n}", type_img: "图像", type_doc: "文档", type_archive: "压缩文件", type_sub: "字幕文件", type_app: "应用程序", type_suffix: "文件", /* --- 设置与搜索 --- */ label_turbo_mode: "极速模式", desc_turbo_mode: "自动开启并替代网页界面 (推荐)", lbl_aria2_status: "连接状态", ph_aria2_secret: "密钥 (选填)", str_connected: "连接成功", str_conn_fail: "连接失败", str_connecting: "正在测试...", tip_mixed_content: "常用端口参考:\n• 6800 (Aria2 标准版)\n• 16800 (Motrix 默认)\n• 6881 (其他集成版)", picker_title: "选择文件夹", picker_all: "全部文件", picker_new: "新建文件夹", picker_sort_new: "最新", picker_sort_old: "最旧", title_select_file: "选择文件", placeholder_search: "搜索文件...", placeholder_search_short: "搜索", title_search_hist: "搜索历史", btn_clear_hist: "清空", lbl_global_search: "全盘搜索", lbl_search_path: "搜索包含路径", lbl_search_path_short: "路径", str_search_results: "搜索结果", modal_settings_title: "设置", label_lang: "语言 (Language)", label_thumb: "模糊略缩图 (隐私模式)", label_keep_pos: "保持浏览位置 (返回时定位)", label_sort_pref: "排序偏好", opt_sort_indep: "每个文件夹独立", opt_sort_global: "全部相同", desc_sort_indep: "调整排序方式仅针对当前文件夹生效", desc_sort_global: "全部文件夹使用相同的排序 (时间倒序)", label_search_engine: "搜图引擎", opt_engine_google: "Google Lens (综合)", opt_engine_yandex: "Yandex (综合)", opt_engine_saucenao: "SauceNAO (Pixiv/插画)", opt_engine_tracemoe: "trace.moe (动漫截图)", label_dup_strictness: "相似匹配阈值", opt_strict: "严格", opt_loose: "宽松", label_comic_mode: "媒体模式", desc_comic_mode: "纯图片或纯视频文件夹默认 A-Z 排序", label_aria2_url: "Aria2 地址", label_aria2_token: "Aria2 密钥", label_privacy_mode: "隐私模式", label_blur_cover: "模糊封面图", label_dl_filter_ext: "下载后缀过滤 (例: .txt, .jpg)", label_dl_filter_name: "下载名称过滤 (关键词或全名)", lbl_dl_filter: "文件夹下载过滤", desc_dl_filter: "文件夹下载/推送时自动排除匹配的文件", lbl_config_manage: "配置管理", btn_export_data: "导出备份", btn_import_data: "导入备份", btn_clean_data: "清除本地数据", title_clean_data: "选择清理项目", msg_clean_confirm: "确定要彻底删除选中的本地数据吗?此操作无法撤销。", msg_clean_success: "本地数据已清理,页面即将刷新...", opt_cfg_index: "全盘索引 (已同步的目录结构/文件快照)", opt_cfg_pref: "偏好设置 (UI 外观/操作习惯/排序偏好)", opt_cfg_rules: "管理规则 (资源管理器/分享次数限制/搜索记录/下载规则)", opt_cfg_vault: "密码金库 (解压密码记忆)", opt_cfg_history: "视频缓存 (视频播放进度/视频时长缓存)", opt_cfg_cache: "运行缓存 (文件夹修改时间/最后浏览位置/指纹)", msg_import_confirm: "导入的配置将与当前设置合并(合并名单/记录,覆盖冲突的基础设置),是否继续?", msg_import_success: "配置导入成功,页面即将刷新...", err_invalid_config: "无效的配置文件:未检测到指纹标识或格式错误", err_json_format: "文件解析失败:JSON 语法错误或文件已损坏", lbl_storage: "存储空间", lbl_browse_exp: "浏览体验", lbl_skip_bl_on_del: "删除时跳过管理器中记录资源", lbl_pwd_manage: "解压密码管理", title_pwd_vault: "密码金库", lbl_pwd_try_count: "单个压缩包密码匹配上限", tip_pwd_manual: "每行记录一个密码,回车换行", str_root_dir_cn: "根目录", btn_ana_select: "一键勾选", opt_keep_new: "保留最新的", opt_keep_old: "保留最旧的", opt_keep_large: "保留最大的", opt_keep_small: "保留最小的", opt_keep_short: "保留名称最短的", opt_keep_long: "保留名称最长的", /* --- 状态、进度与加载短语 --- */ loading: "加载中...", loading_detail: "正在全速索引目录结构...", loading_fetch: "获取中... ({n})", loading_dup: "分析重复项... ({p}%)", str_loading_placeholder: "加载中...", str_load_failed: " (加载失败)", str_load_failed_simple: "加载失败", str_waiting_token: "正在同步登录状态...", str_speed: "速度", status_scanning_selection: "扫描选中项... {n}", status_ready: "{n} 个项目", sel_count: "已选择 {n} 个项目", str_cached: "已缓存:", str_retries: "重试:", str_failed: "失败:", str_success: "成功:", str_stopping: "正在停止...", str_merging: "合并数据中...", str_rendering: "渲染列表中...", str_scanning: "扫描中...", str_analyzing: "分析中...", str_deleting: "删除中...", str_saving: "保存中...", str_saving_dots: "保存中...", str_checking_bl: "匹配名单记录中...", str_processing: "系统正在全速处理中...", str_cleanup_done: "清理完成。", str_waiting_preload: "等待预加载...", str_copying: "复制到剪贴板...", str_moving: "准备移动...", str_sorting: "正在排序...", str_refreshing: "刷新中...", str_refreshing_cache: "刷新缓存中...", str_syncing_stars: "同步星标状态...", str_updating_view: "更新视图中...", str_generating_view: "生成视图中...", str_group: "组", str_init_rename: "初始化重命名...", str_renaming: "重命名中...", str_calc_changes: "计算变更中...", str_scanning_dir: "扫描目录结构...", str_init_op: "初始化操作...", str_init_scan: "正在初始化全盘扫描...", str_rebuilding: "正在重建索引...", str_upload_1: "正在上传 (节点 1/3)...", str_upload_2: "节点1超时,切换节点 2...", str_upload_3: "节点2超时,尝试最后节点...", str_upload_fail_copy: "上传失败,准备写入剪贴板...", msg_transcoding: "云端转码中...", msg_transcoding_wait: "服务器正在处理此视频,请稍候", str_preparing: "准备解压...", str_unzipping: "正在解压: {n}", str_unzipping_state: "解压中...", str_unzipping_prog_0: "解压中: 0%", str_unzipping_prog_100: "解压中: 100%", str_unzipping_prog_fmt: "解压中: {n}%", msg_task_waiting: "等待中...", msg_task_hashing: "校验文件中...", msg_task_init_upload: "初始化上传...", msg_task_uploading: "正在上传...", msg_task_init_part: "初始化分片...", msg_task_uploading_2: "正在上传...", str_loc_tracing: "正在回溯路径...", str_loc_stale: "缓存已过期,正在同步云端...", str_verifying: "正在验证...", str_server_indexing: "服务器索引中... ({n}/5)", str_creating_task_n: "创建任务 ({n}/{t})...", msg_submit_request: "正在提交请求... {c}/{t}", msg_wait_server: "等待服务器处理... ({c}/{t})", msg_server_processing: "服务器处理中... ({c}/{t})", str_jav_querying: "正在查询...", lbl_done_check: "✔ 完成", msg_limit_updated: "提取次数已更新", /* --- 提示、确认与交互消息 --- */ title_alert: "提示", title_confirm: "确认", title_prompt: "输入", btn_ok: "确定", btn_yes: "是", btn_no: "否", btn_save: "保存配置", btn_cancel: "取消", btn_create: "创建", btn_skip: "跳过", msg_down_success: "已成功调用浏览器下载 {n} 个文件。", msg_batch_txt: "已生成下载列表 (.txt)。", msg_clear_history_done: "已从历史记录中移除", msg_skip_unzipped: "已跳过 {n} 个已解压的项目。", msg_unzip_skip_del_confirm: "检测到 {n} 个已解压的压缩包,是否将其移入回收站?", msg_cancel_share_confirm: "确定要取消选中的 {n} 个分享吗?\n链接将立即失效。", msg_pwd_updating: "正在更新密码...", msg_pwd_updated: "密码已更新", msg_exp_updated: "有效期已更新", msg_cancel_share_done: "已取消 {n} 个分享。", msg_drag_drop_hint: "将文件拖拽到此处并释放", str_drag_files: " 等 {n} 个文件", msg_creating_share: "正在创建分享...", title_share_result: "分享成功", msg_no_files: "没有项目。", msg_no_selection: "请先选择项目。", warn_del: "确定要删除选中的 {n} 项吗?", msg_clear_sel_confirm: "已选中 {n} 个重复文件,确认要取消当前的勾选吗?", str_bl_stat: "匹配: {n} 项 | 已选中: {m} 项", str_hits: "命中", msg_settings_saved: "设置已保存。页面将刷新。", msg_name_exists: "名称已存在: {n}", str_name_conflict: "(可能重名)", msg_newfolder_prompt: "新文件夹名称:", msg_rename_prompt: "输入新名称:", msg_copy_done: "已复制。请选择粘贴位置。", msg_cut_done: "准备移动。请选择粘贴位置。", msg_paste_empty: "没有可粘贴的项目。", msg_copy_empty: "剪贴板为空或无文本数据。", msg_add_success: "已追加 {n} 行数据。", msg_del_done: "已删除选中行。", msg_del_select: "请先点击选中要删除的行!", msg_del_items_done: "已删除 {n} 个项目。", msg_copy_success: "复制成功", str_redirecting: "正在跳转 Google Lens...", msg_manual_paste: "图床上传超时。已复制截图,请在新窗口按 {cmd}", msg_starring: "正在添加星标...", msg_unstarring: "正在取消星标...", msg_star_added: "已添加星标", msg_unstar_done: "已取消星标", msg_empty_trash_confirm: "确定要清空回收站吗?此操作无法撤销!", msg_trash_emptied: "回收站已清空。", msg_del_forever_confirm: "确定要彻底删除这 {n} 个项目吗?此操作无法撤销!", msg_del_forever_done: "已彻底删除 {n} 个项目。", msg_restore_done: "已成功还原 {n} 个项目。", msg_auto_sub_load: "已自动加载字幕:{n}", msg_dl_sub: "正在下载字幕...", msg_transcode_done: "✅ 转码完成,开始播放", msg_fallback_report: "⚠️ 原画不可播放(MPEG4/HEVC),已自动切换至 {n}", tip_manual_sub: "提示:下载 .srt 或 .vtt 后,直接拖入播放器即可加载。", msg_sub_drop_load: "已通过拖拽加载字幕:{n}", msg_resume_hint: "已为您从 {t} 继续播放,点击这里 ", msg_unzip_confirm_n: "确定要在当前目录解压这 {n} 个文件吗?", msg_task_paused: "已暂停", msg_task_added: "已添加 {n} 个上传任务", msg_task_fast_success: "秒传成功", msg_task_upload_done: "上传完成", log_dirty_data: "拦截到脏数据入侵!请求模式与当前视图不符,已物理熔断。", msg_network_unstable: "网络连接波动,正在自动恢复...", msg_skip_locked: "{n} 个被锁定", msg_skip_self: "{n} 个原地移动", msg_skip_conflict: "{n} 个子路径忙碌", msg_skip_invalid: "已自动跳过无效项: ", msg_creating_cloud_task: "正在创建云下载任务...", str_parsing_torrent: "正在解析种子文件...", err_torrent_no_info: "解析失败:未发现有效信息", err_file_read: "文件读取失败", msg_cloud_task_finish: "创建完成:{s} 成功,{f} 失败", msg_cloud_task_success: "🎉 已成功创建 {n} 个任务", msg_prepare_restore: "准备还原...", msg_smart_matching_n: "正在智能匹配密码 ({n}个)...", msg_system_busy_retry: "系统繁忙,等待重试 ({n}/5)...", msg_unzip_running_bg: "[{n}] 已在云端解压中,请稍后刷新查看", msg_share_code_updated: "分享代码已更新", msg_retry_submitted: "已重试提交 {n} 个任务", msg_aria2_batch_fail_log: "\n\n检测到失败项较多,已为您自动导出完整错误清单 (.txt)", str_aria2_fetch_err: "(获取链接失败)", str_aria2_rpc_err: "(投递失败)", str_aria2_aborted: "(已取消)", str_aria2_fail_file_name: "Aria2_失败清单", msg_op_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运,请等待当前任务完成后再操作。", msg_op_blocked_analyzing: "⚠️ 操作拦截\n\n后台正在执行文件搬运。为确保文件夹分析数据准确,请等待当前任务完成后再操作。", msg_op_blocked_exporting: "⚠️ 操作拦截\n\n后台正在执行文件搬运。为确保导出目录文本准确,请等待当前任务完成后再操作。", msg_analyze_only_normal_dir: "请选择文件夹。", msg_analyze_no_large_folders: "没有发现符合阈值范围的文件夹 ({s})", msg_analyze_summary_fmt: "共发现 {n} 个大于 {s}GB 的文件夹 (已按大小降序排列)", msg_prune_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运,暂时无法执行清理。", msg_global_index_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运。全盘索引需要稳定的目录结构,请等待当前任务完成后再开启全盘搜索。", msg_resource_locked_download: "⚠️ 操作拦截\n\n选中项包含正在移动的文件或文件夹。请等待搬运完成后再发起下载。", msg_resource_locked_aria2: "⚠️ 操作拦截\n\n选中项包含正在移动的文件或文件夹。请等待搬运完成后再推送至 Aria2。", msg_flatten_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运。此时执行文件分析会导致文件列表缺失或重复,请稍后再试。", err_task_conflict: "⚠️ 操作拦截\n\n后台正在执行文件搬运。全局清理需要稳定的目录结构,请等待搬运完成后再执行。", title_del_task_confirm_fmt: "确认删除 {n} 条传输任务?", lbl_del_cloud_files_too: "同时删除云盘内的文件", msg_file_del_failed: "文件删除失败: ", msg_task_del_success_fmt: "已删除 {n} 个任务", title_clear_task_confirm: "确认清空所有上传任务?", msg_task_clear_success_fmt: "已清空 {n} 个上传任务", msg_unzip_virtual_view_warn: "您正在虚拟视图中操作。解压后的文件将存放在各压缩包所在的原始文件夹中,而不会直接出现在当前列表中。

是否继续?", msg_smart_matching_file: "智能匹配密码中... ({n})", msg_unzip_batch_submitted: "✅ 已完成 {n} 个解压任务", msg_unzip_batch_skipped: " ({n} 个跳过)", msg_unzip_check_source: "。请前往源目录查看结果。", tip_jump_to_folder: "跳转到此文件夹", msg_task_deleted: "任务已删除", msg_scan_done: "扫描完成!\n共发现 {n} 个文件,遍历了 {f} 个文件夹。", msg_scan_fail: "\n\n❌ 有 {n} 个失败。", msg_scan_fix: "\n\n✅ 自动修复了 {n} 次网络错误。", msg_down_scanning: "正在解析文件夹内容...", msg_down_progress: "正在调用浏览器下载...", msg_down_confirm_total: "✅ 扫描完毕,共找到 {n} 个文件。\n\n⚠️ 警告:浏览器直接下载大量文件极易导致页面卡死或被拦截。\n建议超过 10 个文件使用 Aria2 导出。\n\n是否坚持使用浏览器下载?", msg_aria2_sending_batch: "🚀 正在分批发送任务至 Aria2...", msg_aria2_check_fail: "Aria2 连接失败!\n请检查 URL 和 Token。", msg_aria2_check_ok: "Aria2 连接成功!", msg_aria2_sent: "已将 {n} 个文件发送到 Aria2。", msg_aria2_test_fail: "Aria2 连接失败。\n仍然保存设置吗?", title_aria2_fail: "连接测试失败", msg_batch_scanning: "🚀 正在高速扫描目录结构...", msg_batch_hydrating: "⚡ 正在并行提取下载链路...", msg_batch_no_files: "未发现可下载的文件。", msg_dup_warn: "是否开始搜索重复文件?", msg_dup_result: "发现 {n} 组重复项。", msg_dup_none: "未发现重复文件。", msg_bl_stop: "操作已停止。", msg_bl_add_done: "已将 {n} 个项目添加到记录。", msg_bl_remove_done: "已从记录中移除 {n} 个项目。", msg_bl_empty: "名单列表为空,无法运行。", msg_bl_clear_confirm: "确定要清空所有记录条目吗?此操作不可恢复。", msg_blacklist_run_none: "网盘中未发现符合名单条件的项目。", msg_blacklist_run_confirm: "在网盘中发现了 {n} 个已记录项目。\n\n是否立即移入回收站?", msg_bl_run_limit: "⚠️ 模式限制\n\n清理操作涉及物理文件递归操作。目前处于非标准目录,无法准确定位物理扫描范围。\n\n请返回主页常规文件夹后再执行清理。", msg_del_protected: "已保护 {n} 个已记录文件不被删除。", msg_del_none: "没有可删除的文件。", msg_bl_scanning: "全盘搜索中... \n已扫描目录: {d} | 命中: {f}", rn_tip_wait: "请设置规则", rn_tip_jav: "点击上方按钮开始智能匹配", rn_tip_none: "没有匹配的项目或名称", rn_stat: "匹配: {n} 项 | 有效变更: {m} 项", rn_warn_confirm: "确定要重命名 {n} 个文件吗?", msg_bulkrename_done: "已重命名 {n} 个项目。", msg_rn_all_skipped: "❌ 所有项目均因重名被跳过,未执行任何操作。", msg_rn_fail_count: "跳过已重名 {n} 个项目", msg_prune_confirm: "是否开始搜索当前列表中的空文件夹?", msg_prune_none: "未发现空文件夹。", msg_prune_found: "发现了 {n} 个空文件夹。\n是否立即删除?", msg_deleting_folders: "正在删除 {n} 个文件夹...", msg_global_warn: "即将开始全盘文件同步。\n\n文件同步后缓存到本地内存中,网页刷新前持续存在。\n\n是否继续?", msg_init_scan_sel: "正在初始化选中项扫描...", warn_clear_history: "确定要从历史记录中移除选中的 {n} 项吗?\n(这不会删除您的云端文件)", msg_img_copy_hint: "在新窗口中,请按下 {cmd} 即可搜索。", msg_aria2_not_set: "检测到您尚未配置 Aria2,请填写后继续:", str_jav_no_match: "(未匹配到番号)", msg_unzip_fail: "解压请求失败", msg_jszip_fail: "JSZip 加载失败,请检查网络。", msg_turbo_activated: "极速模式已激活:脚本已深度接管网页逻辑,确保稳定流畅。", msg_console_legal: "严禁商业用途:本项目仅供个人学习与交流使用。", msg_ana_warn: "文件夹查重提示:判定基于算法推测,删除前请务必人工核对,以防误删。", /* --- 错误提示 --- */ err_invalid_links: "请输入正确的链接", err_pwd_format: "密码必须为 4-10 位字母或数字", err_invalid_torrent: "无效的种子文件格式", err_torrent_complex: "解析复杂度过高,可能是非法文件", err_torrent_format: "种子文件结构损坏", err_torrent_len: "字段长度解析异常", err_torrent_char: "解析遇到非法字符", err_share_code_exists: "该分享代码已被占用", err_folder_not_ready: "云端文件夹正在创建中,请稍后再试", err_item_deleted: "该项目不存在", err_network: "网络错误", err_clipboard_denied: "剪贴板访问被拒绝", err_worker: "工作线程错误", err_api: "API 错误", err_capture: "截图失败。", err_captcha_simple: "验证失败。请在网页列表手动收藏一次文件以完成验证。", err_sub_dl_fail: "字幕下载失败: ", err_req_blocked: "网络请求失败 (可能被拦截)", err_req_timeout: "请求超时", err_sub_drop_type: "只解析字幕类型文件", err_codec_t1: "无法播放视频编码 ({c})", err_codec_t2: "您的浏览器不支持该视频格式。
请点击下方按钮调用外部播放器。", err_pwd_simple: "密码错误", err_task_exists: "任务已存在", err_network_break: "图片节点网络断流,请再次点击重试", err_no_failed_task: "未选中失败的任务", err_unknown: "未知错误", err_invalid_regex: "无效的正则表达式", err_parent_not_found: "文件夹不存在", msg_sys_error: "不允许操作系统文件夹", msg_download_fail: "无法获取下载链接。", msg_video_fail: "无法获取视频链接。", err_star_sync_fail: "星标同步失败", err_paste_descendant: "不能移动或复制到当前或当前子目录下", err_quota_exceeded: "存储空间不足", err_name_exists: "文件名称不能重复", err_share_pass: "自定义提取码需为 4-10 位字符", str_error: "错误", str_error_crit: "严重错误", str_error_paste: "粘贴错误", str_action_failed: "操作失败", str_scan_error: "扫描错误", err_limit_too_low: "修改失败:新次数 ({n}) 必须大于当前已保存次数 ({s})", err_vault_max: "密码金库最多仅支持存储 50 个常用密码", err_pwd_len: "单个密码长度不能超过 127 个字符", /* --- 帮助文档 --- */ modal_help_title: "帮助", help_desc: `
✨ 体验与导航引擎
交互重构:在官方功能基础上,界面仿 Windows 文件资源管理器 重构。
极速模式:开启后接管原生逻辑,彻底解决海量文件下的卡顿与崩溃问题。
高级路径栏:支持滚轮滑动、下拉菜单同级切换定位。全盘搜索、分析套件均集成路径栏,支持路径回显与溯源跳转。
体验增强:支持星标等多维度排序,及一键模糊封面与暗黑皮肤切换。后台采用 SWR 策略静默无感刷新视图。
后台索引与保护:主页蓝点闪烁表示正同步目录树。系统自带并发操作物理锁,拦截冲突操作,严防脏数据产生。
* 注:默认文件夹(My Pack)受官方保护,严防误删、复制、移动及重命名。
📂 批量与空间管理
批量重命名:支持正则替换/删除剧集流水号、文本格式化FC2 规范命名前缀去广告及基于 MIME 的后缀修复
分析套件文件分析整合了筛选与查重(哈希/时长/名称三模态);文件夹分析整合了筛选与查重(名称/相似度/包含率三模态);并支持导出当前目录树列表。
智能整理:一键清理空文件夹;批量解压集成密码自动记忆与智能填充,支持跳过并删除已解压项。
资源管理器:自定义文件黑名单一键清理垃圾资源;或作为文件白名单,在批量删除时自动保护。
* 注:为避免数据同步冲突,处理期间请勿在其他客户端修改文件。
🌐 传输与分享中心
分享管理:支持设定提取次数上限,次数达标后链接自动失效取消分享。
极速上传:支持全局将本地文件/文件夹拖拽至网页直传,突破官方限制并大幅降低小文件传输中断率
云下载增强:批量离线链接自动去重。内置磁链智能清洗引擎(自动提取 Base32/Hex 哈希去干扰);支持解析 .torrent 种子文件;针对受限链接提供保存网页快照兜底方案。
* 注:提取次数拦截仅在网页保持开启且电脑未休眠时生效。
🎬 沉浸式媒体增强
播放引擎:支持 0.5x-3.0x 倍速、旋转翻转、强制比例、自动跳过片头片尾及连播/循环模式,进度条支持缩略图预览。内置看门狗,遇黑屏或不支持编码自动回退兼容画质。
字幕系统:支持加载云端同名字幕、本地文件及跨站在线搜索。支持字幕轴毫秒级偏移微调,及本地文本直接拖拽解析挂载。
视觉辅助:内嵌多引擎支持图片或视频当前帧以图搜图;设置中可激活“媒体模式”,使剧集/漫画文件夹自动按名称 A-Z 顺序排列。
* 注:播放历史列表持续记录在脚本环境内产生的播放进度。
⚙️ 配置与数据管理
配置备份:支持将偏好设置、管理规则、密码金库等导出为带数字指纹的 JSON 备份文件,导入时支持智能合并去重
数据清理:支持对全盘索引、偏好设置、管理规则、密码金库与缓存按需清除,释放本地空间并保障隐私。
* 注:全盘索引在网页关闭后清空,而偏好设置、密码金库等则持久化保存。
⚡ 下载与分发
外部直连:支持一键获取视频流直链,或唤起 PotPlayer 播放。支持将文件通过 RPC 协议一键推送到 Aria2 节点。
分发增强:推送文件夹至 Aria2 时自动还原云盘树状目录结构。支持长连接监控,遇错自动导出错误清单。支持设置文件夹下载过滤
本项目严格遵循 CC-BY-NC-SA-4.0 协议,严禁用于任何商业用途
` }, tc: { /* --- 通用与基础UI --- */ title: "PikPak 增強大師", str_original: "原畫", str_original_fast: "原畫 (高速)", str_folders: "資料夾", str_files: "檔案", unit_folders: "個資料夾", unit_days: "天", unit_month: "月", unit_sec: "秒", str_no_files: "暫無檔案", str_items: "項", col_name: "名稱", col_size: "大小", col_dur: "類型/時長", col_duration_only: "時長", col_progress: "播放進度", col_play_time: "播放時間", col_date: "修改日期", col_remaining: "剩餘時長", col_path: "路徑", col_old: "原名稱", col_new: "新名稱", col_type: "類型", col_path_name: "路徑 / 名稱", col_action: "操作", lbl_folder_first: "資料夾置頂", tag_default: "預設", current_dir: "目前目錄", str_same_folder: "(同資料夾)", lbl_dont_show: "不再提醒", lbl_dont_show_session: "本次查重不再提示", str_empty_filename: "(空檔名)", str_empty_dir: "(空目錄)", btn_filter: "篩選", title_file_filter: "檔案篩選", cat_all: "全部", cat_video: "影片", cat_audio: "音訊", cat_image: "圖片", cat_document: "文件", cat_software: "軟體", cat_archive: "壓縮檔", cat_torrent: "BT種子", cat_other: "其他", btn_exit_filter: "退出篩選", /* --- 属性面板 --- */ ctx_property: "屬性", title_property: "檔案屬性", lbl_prop_name: "檔案名稱", lbl_prop_size: "檔案大小", lbl_prop_count: "檔案數量", lbl_prop_ctime: "建立時間", lbl_prop_mtime: "修改時間", lbl_prop_source: "加入來源", lbl_prop_link: "資源連結", lbl_prop_path: "檔案位置", str_prop_cloud: "雲端加入", str_prop_share: "來自分享", str_prop_user: "使用者上傳", str_prop_unknown: "未知來源", fmt_prop_count: "包含 {f} 個檔案,{d} 個資料夾", str_prop_offline: "離線任務", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "首頁", btn_nav_share: "我的分享", btn_nav_offline: "離線下載", btn_nav_recent: "最近加入", btn_nav_history: "播放歷史", btn_nav_starred: "收藏夾", btn_nav_trash: "回收站", btn_nav_upload: "我的上傳", title_offline: "我的離線", trash_title: "回收站", trash_notice: "資源回收筒的檔案最多儲存 15 天", history_notice: "僅記錄在腳本環境內產生的播放進度", ctx_open: "開啟", ctx_add_bl: "添加到資源管理器", ctx_remove_bl: "從資源管理器移除", ctx_rename: "重新命名", ctx_copy: "複製", ctx_copy_name: "複製檔名", ctx_copy_link: "複製連結", ctx_cut: "移動", ctx_del: "刪除", ctx_down: "下載", ctx_star: "加入星號", ctx_unstar: "取消星號", ctx_locate: "在資料夾中檢視", ctx_share: "分享", /* --- 通用文件操作按钮 --- */ btn_down: "下載", tip_down: "下載 [Alt] + [D]", btn_aria2: "傳送至 Aria2", tip_aria2: "傳送至 Aria2 [Alt] + [A]", btn_refresh_short: "刷新", tip_refresh: "刷新 [F5]", btn_newfolder: "新增資料夾", tip_newfolder: "新增資料夾 [F8]", btn_del: "刪除", tip_del: "刪除 [Delete]", btn_deselect: "取消選取", tip_deselect: "取消選取 [Esc]", btn_invert: "反向選取", btn_copy: "複製", tip_copy: "複製 [Ctrl] + [C]", btn_cut: "移動", tip_cut: "移動 [Ctrl] + [X]", btn_paste: "貼上", tip_paste: "貼上 [Ctrl] + [V]", btn_clear_history: "刪除歷史", tip_clear_history: "刪除歷史 [Delete]", btn_restore: "還原", tip_restore: "還原 [R]", btn_del_forever: "永久刪除", tip_del_forever: "永久刪除 [Delete]", btn_empty_trash: "清空資源回收筒", tip_empty_trash: "清空資源回收筒 [Shift] + [Delete]", btn_exit: "退出", btn_close: "關閉", tip_close: "關閉 [Esc]", tip_theme: "切換主題 [Alt] + [T]", tip_rotate: "旋轉 [R]", tip_mirror: "鏡像翻轉 [H]", tip_flip_v: "垂直翻轉 [V]", tip_maximize: "最大化 [M]", tip_minimize: "最小化 [M]", tip_full_screen: "全螢幕 [Enter]", btn_help: "說明", tip_help: "說明 [Alt] + [H]", btn_view_file: "檢視檔案", btn_jump: "跳轉", btn_copy_text: "複製", btn_stop: "停止", tip_stop: "立即停止目前操作", btn_settings: "設定", btn_logout: "登出 PikPak", msg_logout_confirm: "確定要登出嗎?", tip_settings: "設定與更多 [Alt] + [S]", lbl_upload_to: "上傳檔案至: ", msg_move_done: "移動完成。", /* --- 离线、上传与云下载 --- */ btn_upload: "本機上傳", btn_up_file: "上傳檔案", btn_up_folder: "上傳資料夾", btn_cloud_download: "雲下載", btn_up_pause: "暫停任務", tip_up_pause: "暫停任務 [Alt] + [P]", btn_up_start: "開始任務", tip_up_start: "開始任務 [Alt] + [G]", btn_up_del: "刪除任務", tip_up_del: "刪除任務 [Delete]", btn_up_clear_all: "清空任務", tip_up_clear_all: "清空任務 [Shift] + [Delete]", btn_retry_task: "重試任務", tip_retry_task: "重試任務 [R]", col_task_status: "任務狀態", col_task_progress: "離線進度", col_up_speed: "速度", col_up_status: "狀態", lbl_task_run: "進行中", lbl_task_fail: "已失敗", lbl_task_ok: "已完成", lbl_up_run: "進行中", lbl_up_pause: "已中斷", lbl_up_downloading: "下載中", lbl_up_done: "已完成", tip_up_pause_desc: "包含手動暫停及發生錯誤的任務", title_cloud_task: "建立雲端下載任務", ph_cloud_links: "支援連結格式:\n- 各種下載連結,如 magnet。\n- YouTube、X (Twitter)、TikTok、Facebook 等分享連結。\n透過換行可一次加入多條連結。", lbl_save_to: "檔案將被儲存至:", lbl_default_folder: "預設資料夾", btn_via_torrent: "透過 Torrent 建立", tip_cloud_save_path: "正規雲端下載檔案會儲存在 My Pack 目錄,來自其他 App 的雲端下載檔案會儲存至 My [XYZ] 目錄,[XYZ] 為 App 名稱。", lbl_smart_fix: "自動修復防屏蔽磁鏈 (提取特徵碼/剔除文字干擾)", title_save_method: "儲存方式", msg_save_snapshot_desc: "此連結只能被儲存為網頁快照。", tip_snapshot_details: "PikPak 無法從此連結中直接擷取媒體檔案,您可以儲存網頁快照。PikPak 將盡可能儲存完整的網頁內容到快照檔案中。", btn_save_snapshot: "儲存快照", btn_create_now: "立即建立", btn_modify: "修改", str_snap_link_count_suffix: " 等 {n} 個連結", /* --- 分享管理 --- */ btn_cancel_share: "取消分享", share_copy_suffix: "複製這段內容後開啟 PikPak App,暢享極速秒播", share_copy_pwd: "密碼", title_share_detail: "分享詳情", ctx_share_detail: "檢視分享詳情", ctx_share_copy: "複製連結和密碼", col_view: "瀏覽", col_save: "儲存", col_share_time: "分享時間", col_share_status: "分享狀態", lbl_limit_reached: "次數已滿", lbl_limit_tip: "限制次數", lbl_share_view: "瀏覽", lbl_share_save: "儲存", lbl_share_link_title: "分享連結", lbl_share_pwd_title: "密碼", lbl_share_expire_title: "有效期限", btn_copy_link_pwd: "複製連結和密碼", str_expire_suffix: "天後過期", ph_edit_pwd: "輸入分享密碼,支援 4-10 個字元", btn_close_pwd: "關閉密碼", str_no_pwd: "無密碼", title_edit_share_code: "分享代碼修改", ph_edit_share_code: "支援 5-18 位文字、數字及符號等", btn_add_share_code: "加入分享代碼", btn_del_share_code: "刪除分享代碼", share_title: "分享檔案", share_mode: "分享方式", share_public: "公開連結", share_encrypted: "加密連結", share_expiry: "設定有效期限", share_pass: "設定提取碼", share_count: "設定提取次數", share_count_ed: "提取次數", share_perm: "永久有效", share_unlimit: "不限", share_rand: "系統隨機", share_custom: "自訂", share_days: "天", share_times: "次", btn_share_start: "立即分享", cal_custom_title: "自訂有效期限", lbl_share_link: "連結", lbl_share_code: "提取碼", btn_copy_share: "複製全部", str_share_expired: "已過期", str_share_deleted: "檔案已刪", title_edit_pwd: "密碼修改", lbl_share_code_title: "分享代碼", ph_password: "密碼", ph_pass_range: "4-10位字元", cal_week_days:["日", "一", "二", "三", "四", "五", "六"], cal_months:["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "檔案分析", btn_scan: "文件透視", lbl_scan_selected: "對選取的 {n} 個項目執行文件透視", lbl_keyword_filter: "排除關鍵字", ph_keyword_filter: "排除包含的關鍵字,多個用逗號分隔", lbl_scan_current: "對目前路徑下所有項目執行文件透視", tip_dup: "檔案查重", lbl_dup_selected: "對選取的 {n} 個項目執行文件查重", lbl_dup_current: "對目前路徑下所有項目執行文件查重", tip_scan_dup: "篩選或查重檔案", lbl_dup_tool: "選擇刪除對象:", lbl_dup_reset: "↺ 復原 (取消置頂 & 清空選取)", lbl_dup_select_folder: "📂 依資料夾選擇", lbl_dup_select_folder_short: "📂 資料夾", lbl_dup_invert: "反向選取模式", lbl_dup_invert_short: "反向選取", tip_dup_invert_limit: "僅依資料夾選擇時可用", fmt_dup_count: "({n}個重複)", btn_start_scan: "開始掃描", tag_hash: "精準比對", tag_hash_short: "精準", tag_name: "名稱相似", tag_name_short: "名稱", tag_sim: "時長相似", tag_sim_short: "時長", label_dup_video: "影片檔案 (精準比對 + 時長相似 + 名稱相似)", label_dup_image: "圖片檔案 (精準比對 + 名稱相似)", label_dup_other: "其他檔案 (精準比對 + 名稱相似)", btn_analyze: "資料夾分析", tip_analyze: "篩選或查重資料夾", btn_export: "匯出目錄", tip_export: "產生並下載目前路徑的檔案樹狀清單", title_export_format: "匯出目錄樣式", lbl_export_current: "對目前路徑下所有項目執行目錄匯出", opt_tree_view: "目錄樹", opt_list_view: "目錄列表", msg_exporting: "正在產生目錄樹...", str_analyze_results: "比對結果", lbl_size_threshold: "偵測閾值", title_analyze_result: "資料夾分析結果", opt_ana_large: "資料夾透視", lbl_analyze_selected: "對選取的 {n} 個項目執行資料夾透視", lbl_analyze_current: "對目前路徑下所有項目執行資料夾透視", opt_ana_sim: "資料夾查重", lbl_ana_sim_selected: "對選取的 {n} 個項目執行資料夾查重", lbl_ana_sim_current: "對目前路徑下所有項目執行資料夾查重", title_algo_help: "查重演算法說明", algo_help_content: "名稱比對:查找名稱和體積相近的資料夾群組。\n相似度比對:查找內部檔案高度重合的資料夾群組。\n包含率比對:查找小資料夾的內容被大資料夾完全覆蓋的子集冗餘。\n\n精度:相似度比對 > 包含率比對 > 名稱比對\n範圍:包含率比對 > 相似度比對", lbl_threshold: "閾值", lbl_sim_score: "相似度", lbl_containment: "包含率", lbl_name_match: "名稱比對", lbl_sim_match: "相似度比對", lbl_contain_match: "包含率比對", lbl_ana_min: "下限", lbl_ana_max: "上限", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "清理空資料夾", tip_prune: "清理空資料夾 [Ctrl] + [Delete]", btn_rename: "重新命名", tip_rename: "重新命名 [F2]", btn_bulkrename: "批次重新命名", tip_bulkrename: "批次重新命名 [F2]", title_blacklist: "資源管理器", btn_blacklist_run: "立即執行清理", btn_clear_list: "清空清單", tip_bl_desc: "下列項目在【刪除】時會跳過,僅透過【立即執行清理】尋找並刪除", tip_blacklist_input: "資源管理器 [Alt] +[Delete]", label_bl_folder: "資料夾名單 (精準查找)", label_bl_file: "檔案名單 (精準查找)", lbl_type_folder: "資料夾", lbl_type_file: "檔案", ph_bl_folder: "請透過「貼上」或檔案右鍵選單「添加到資源管理器」匯入。", ph_bl_file: "請透過「貼上」或檔案右鍵選單「添加到資源管理器」匯入。", modal_bl_preview: "搜尋結果", btn_bl_delete: "刪除選取項目", modal_rename_title: "重新命名", modal_rename_multi_title: "批次重新命名", btn_preview: "預覽", modal_preview_title: "確認變更", label_pattern: "模式 (例:Video {n})", label_replace: "取代/刪除", label_replace_note: "區分大小寫", label_include_ext: "包含副檔名", label_regex: "正規表示式 (Regex)", placeholder_find: "尋找內容", placeholder_replace: "取代為 (留空則刪除)", label_jav: "FC2 純淨命名", lbl_rn_pattern: "命名範本", lbl_rn_case_convert: "大小寫轉換", opt_rn_keep_origin: "(保持原樣)", opt_rn_lower: "全部小寫 (abc)", lbl_rn_mode_series: "影集模式", lbl_rn_mode_format: "格式化", lbl_rn_mode_ad: "前綴去廣告", lbl_rn_mode_ext: "副檔名修復", opt_rn_upper: "全部大寫 (ABC)", opt_rn_title: "首字母大寫 (Abc)", lbl_rn_width_convert: "全形半形轉換", opt_rn_width_half: "全形轉半形 (A->A)", opt_rn_width_full: "半形轉全形 (A->A)", lbl_rn_preview_title: "變更預覽", tip_jav_mode_desc: "✨ 智慧提取 FC2 並去除無關字元", tip_ad_remove_desc: "🧹 智慧過濾頭部廣告、網址及垃圾符號,自動清除 Emoji 並修復括號格式", tip_ext_fix_desc: "🧩 根據檔案真實類型 (MIME) 智慧修正副檔名", label_replace_find: "尋找內容", label_replace_to: "取代為", /* --- 解压相关 --- */ btn_unzip: "批次解壓縮", tip_unzip: "批次解壓縮 [Alt] + [U]", btn_unzip_all: "全部解壓縮", btn_understand_unzip: "理解並解壓縮", title_input_pwd: "需要解壓縮密碼", lbl_pwd_prompt: "請輸入密碼:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "外部播放", tip_ext: "使用PotPlayer播放或獲取播放連結 [Alt] +[E]", btn_img_search: "以圖搜圖 [F]", tip_play_search: "以圖搜圖 [F]", tip_pip: "子母畫面 [P]", str_no_sub: "無字幕", lbl_sub_sel: "字幕選擇", lbl_show_sub: "顯示字幕", btn_sub_search: "搜尋線上字幕", btn_sub_cloud: "開啟雲端字幕", btn_sub_local: "開啟本機字幕", lbl_sub_pos: "字幕位置", lbl_sub_bottom: "底部", lbl_sub_top: "頂部", lbl_sub_bg_op: "背景透明", lbl_sub_size: "字幕大小", lbl_sub_offset: "字幕進度", title_sel_sub: "選擇字幕", ph_sub_search: "輸入關鍵字,下方連結將自動更新...", btn_force_play: "嘗試強制播放", str_compat_mode: "相容模式", lang_code: "zh-TW", btn_go_search: "🔍 去 {n} 手動搜尋", btn_restart: "從頭播放", btn_prev_video: "上一個 [Ctrl + ←]", btn_next_video: "下一個 [Ctrl + →]", tip_plist_open: "展開 [E]", tip_plist_close: "收合 [E]", tab_sub: "字幕", tab_size: "尺寸", tab_more: "更多", lbl_ratio: "比例", lbl_direction: "方向", opt_ratio_def: "預設", btn_rot_l: "向左旋轉", btn_rot_r: "向右旋轉", btn_flip_h: "水平翻轉", btn_flip_v: "垂直翻轉", lbl_play_end: "當播放結束", opt_list_loop: "清單循環", opt_single_loop: "單集循環", opt_play_stop: "播完暫停", lbl_skip_op: "跳過片頭", lbl_skip_ed: "跳過片尾", export_link_title: "匯出影片串流連結", btn_start_play: "開始播放", btn_copy_link: "複製連結", tip_copy_link: "複製連結 [Alt] + [C]", opt_player_other: "其他 (匯出連結)", lbl_player: "播放器", btn_mark: "標記", lbl_resolution: "畫質", str_switch_compat: "目前畫質無法使用,已為您恢復至 {n}", type_img: "影像", type_doc: "文件", type_archive: "壓縮檔", type_sub: "字幕檔", type_app: "應用程式", type_suffix: "檔案", /* --- 设置与搜索 --- */ label_turbo_mode: "極速模式", desc_turbo_mode: "自動開啟並替代網頁介面 (推薦)", lbl_aria2_status: "連線狀態", ph_aria2_secret: "密鑰 (選填)", str_connected: "連線成功", str_conn_fail: "連線失敗", str_connecting: "正在測試...", tip_mixed_content: "常用連接埠參考:\n• 6800 (Aria2 標準版)\n• 16800 (Motrix 預設)\n• 6881 (其他整合版)", picker_title: "選擇資料夾", picker_all: "全部檔案", picker_new: "新增資料夾", picker_sort_new: "最新", picker_sort_old: "最舊", title_select_file: "選擇檔案", placeholder_search: "搜尋檔案...", placeholder_search_short: "搜尋", title_search_hist: "搜尋歷史", btn_clear_hist: "清空", lbl_global_search: "全盤搜尋", lbl_search_path: "搜尋包含路徑", lbl_search_path_short: "路徑", str_search_results: "搜尋結果", modal_settings_title: "設定", label_lang: "語言 (Language)", label_thumb: "模糊縮圖 (隱私模式)", label_keep_pos: "保持瀏覽位置 (返回時定位)", label_sort_pref: "排序偏好", opt_sort_indep: "每個資料夾獨立", opt_sort_global: "全部相同", desc_sort_indep: "調整排序方式僅對目前資料夾生效", desc_sort_global: "所有資料夾使用相同的排序 (時間倒序)", label_search_engine: "搜圖引擎", opt_engine_google: "Google Lens (綜合)", opt_engine_yandex: "Yandex (綜合)", opt_engine_saucenao: "SauceNAO (Pixiv/插畫)", opt_engine_tracemoe: "trace.moe (動漫截圖)", label_dup_strictness: "相似比對閾值", opt_strict: "嚴格", opt_loose: "寬鬆", label_comic_mode: "媒體模式", desc_comic_mode: "純圖片或純影片資料夾預設依 A-Z 排序", label_aria2_url: "Aria2 位址", label_aria2_token: "Aria2 密鑰", label_privacy_mode: "隱私模式", label_blur_cover: "模糊封面圖", label_dl_filter_ext: "下載字尾過濾 (例: .txt, .jpg)", label_dl_filter_name: "下載名稱過濾 (關鍵字或全名)", lbl_dl_filter: "資料夾下載過濾", desc_dl_filter: "資料夾下載/推送時自動排除匹配的檔案", lbl_config_manage: "配置管理", btn_export_data: "匯出備份", btn_import_data: "匯入備份", btn_clean_data: "清除本機資料", title_clean_data: "選擇清理項目", msg_clean_confirm: "確定要徹底刪除選取的本機資料嗎?此操作無法還原。", msg_clean_success: "本機資料已清理,頁面即將重新整理...", opt_cfg_index: "全盤索引 (已同步的目錄結構/檔案快照)", opt_cfg_pref: "偏好設定 (UI 外觀/操作習慣/排序偏好)", opt_cfg_rules: "管理規則 (資源管理器/分享次數限制/搜尋紀錄/下載規則)", opt_cfg_vault: "密碼金庫 (解壓縮密碼記憶)", opt_cfg_history: "影片快取 (影片播放進度/影片時長快取)", opt_cfg_cache: "運行快取 (資料夾修改時間/最後瀏覽位置/指紋)", msg_import_confirm: "匯入的配置將與目前設定合併(合併名單/紀錄,覆蓋衝突的基礎設定),是否繼續?", msg_import_success: "配置匯入成功,頁面即將重新整理...", err_invalid_config: "無效的設定檔:未偵測到指紋標識或格式錯誤", err_json_format: "檔案解析失敗:JSON 語法錯誤或檔案已損壞", lbl_storage: "儲存空間", lbl_browse_exp: "瀏覽體驗", lbl_skip_bl_on_del: "刪除時跳過管理器中記錄資源", lbl_pwd_manage: "解壓密碼管理", title_pwd_vault: "密碼金庫", lbl_pwd_try_count: "單個壓縮檔密碼匹配上限", tip_pwd_manual: "每行記錄一個密碼,回車換行", str_root_dir_cn: "根目錄", btn_ana_select: "一鍵勾選", opt_keep_new: "保留最新的", opt_keep_old: "保留最舊的", opt_keep_large: "保留最大的", opt_keep_small: "保留最小的", opt_keep_short: "保留名稱最短的", opt_keep_long: "保留名稱最長的", /* --- 状态、进度与加载短语 --- */ loading: "載入中...", loading_detail: "正在全速索引目錄結構...", loading_fetch: "取得中... ({n})", loading_dup: "分析重複項目... ({p}%)", str_loading_placeholder: "載入中...", str_load_failed: " (載入失敗)", str_load_failed_simple: "載入失敗", str_waiting_token: "正在同步登入狀態...", str_speed: "速度", status_scanning_selection: "掃描選取項目... {n}", status_ready: "{n} 個項目", sel_count: "已選取 {n} 個項目", str_cached: "已快取:", str_retries: "重試:", str_failed: "失敗:", str_success: "成功:", str_stopping: "正在停止...", str_merging: "合併資料中...", str_rendering: "渲染清單中...", str_scanning: "掃描中...", str_analyzing: "分析中...", str_deleting: "刪除中...", str_saving: "儲存中...", str_saving_dots: "儲存中...", str_checking_bl: "匹配名單記錄中...", str_processing: "系統正在全速處理中...", str_cleanup_done: "清理完成。", str_waiting_preload: "等待預先載入...", str_copying: "複製到剪貼簿...", str_moving: "準備移動...", str_sorting: "正在排序...", str_refreshing: "重新整理中...", str_refreshing_cache: "重新整理快取中...", str_syncing_stars: "同步星號狀態...", str_updating_view: "更新視圖中...", str_generating_view: "產生視圖中...", str_group: "組", str_init_rename: "初始化重新命名...", str_renaming: "重新命名中...", str_calc_changes: "計算變更中...", str_scanning_dir: "掃描目錄結構...", str_init_op: "初始化操作...", str_init_scan: "正在初始化全盤掃描...", str_rebuilding: "正在重建索引...", str_upload_1: "正在上傳 (節點 1/3)...", str_upload_2: "節點1超時,切換節點 2...", str_upload_3: "節點2超時,嘗試最後節點...", str_upload_fail_copy: "上傳失敗,準備寫入剪貼簿...", msg_transcoding: "雲端轉碼中...", msg_transcoding_wait: "伺服器正在處理此影片,請稍候", str_preparing: "準備解壓縮...", str_unzipping: "正在解壓縮:{n}", str_unzipping_state: "解壓縮中...", str_unzipping_prog_0: "解壓縮中:0%", str_unzipping_prog_100: "解壓縮中:100%", str_unzipping_prog_fmt: "解壓縮中:{n}%", msg_task_waiting: "等待中...", msg_task_hashing: "校驗檔案中...", msg_task_init_upload: "初始化上傳...", msg_task_uploading: "正在上傳...", msg_task_init_part: "初始化分片...", msg_task_uploading_2: "正在上傳...", str_loc_tracing: "正在回溯路徑...", str_loc_stale: "快取已過期,正在同步雲端...", str_verifying: "正在驗證...", str_server_indexing: "伺服器索引中... ({n}/5)", str_creating_task_n: "建立任務 ({n}/{t})...", msg_submit_request: "正在提交請求... {c}/{t}", msg_wait_server: "等待伺服器處理... ({c}/{t})", msg_server_processing: "伺服器處理中... ({c}/{t})", str_jav_querying: "正在查詢...", lbl_done_check: "✔ 完成", msg_limit_updated: "提取次數已更新", /* --- 提示、确认与交互消息 --- */ title_alert: "提示", title_confirm: "確認", title_prompt: "輸入", btn_ok: "確定", btn_yes: "是", btn_no: "否", btn_save: "儲存設定", btn_cancel: "取消", btn_create: "建立", btn_skip: "跳過", msg_down_success: "已成功呼叫瀏覽器下載 {n} 個檔案。", msg_batch_txt: "已產生下載清單 (.txt)。", msg_clear_history_done: "已從歷史紀錄中移除", msg_skip_unzipped: "已跳過 {n} 個已解壓縮的項目。", msg_unzip_skip_del_confirm: "偵測到 {n} 個已解壓縮的壓縮檔,是否將其移入資源回收筒?", msg_cancel_share_confirm: "確定要取消選取的 {n} 個分享嗎?\n連結將立即失效。", msg_pwd_updating: "正在更新密碼...", msg_pwd_updated: "密碼已更新", msg_exp_updated: "有效期限已更新", msg_cancel_share_done: "已取消 {n} 個分享。", msg_drag_drop_hint: "將檔案拖曳到此處並放開", str_drag_files: " 等 {n} 個檔案", msg_creating_share: "正在建立分享...", title_share_result: "分享成功", msg_no_files: "沒有項目。", msg_no_selection: "請先選擇項目。", warn_del: "確定要刪除選取的 {n} 項嗎?", msg_clear_sel_confirm: "已選取 {n} 個重複檔案,確認要取消目前的勾選嗎?", str_bl_stat: "比對:{n} 項 | 已選取:{m} 項", str_hits: "命中", msg_settings_saved: "設定已儲存。頁面將重新整理。", msg_name_exists: "名稱已存在:{n}", str_name_conflict: "(可能同名)", msg_newfolder_prompt: "新資料夾名稱:", msg_rename_prompt: "輸入新名稱:", msg_copy_done: "已複製。請選擇貼上位置。", msg_cut_done: "準備移動。請選擇貼上位置。", msg_paste_empty: "沒有可貼上的項目。", msg_copy_empty: "剪貼簿為空或無文字資料。", msg_add_success: "已附加 {n} 列資料。", msg_del_done: "已刪除選取列。", msg_del_select: "請先點擊選取要刪除的列!", msg_del_items_done: "已刪除 {n} 個項目。", msg_copy_success: "複製成功", str_redirecting: "正在跳轉 Google Lens...", msg_manual_paste: "圖床上傳超時。已複製截圖,請在新視窗按 {cmd}", msg_starring: "正在加入星號...", msg_unstarring: "正在取消星號...", msg_star_added: "已加入星號", msg_unstar_done: "已取消星號", msg_empty_trash_confirm: "確定要清空資源回收筒嗎?此操作無法還原!", msg_trash_emptied: "資源回收筒已清空。", msg_del_forever_confirm: "確定要徹底刪除這 {n} 個項目嗎?此操作無法還原!", msg_del_forever_done: "已徹底刪除 {n} 個項目。", msg_restore_done: "已成功還原 {n} 個項目。", msg_auto_sub_load: "已自動載入字幕:{n}", msg_dl_sub: "正在下載字幕...", msg_transcode_done: "✅ 轉碼完成,開始播放", msg_fallback_report: "⚠️ 原畫無法播放 (MPEG4/HEVC),已自動切換至 {n}", tip_manual_sub: "提示:下載 .srt 或 .vtt 後,直接拖入播放器即可載入。", msg_sub_drop_load: "已透過拖曳載入字幕:{n}", msg_resume_hint: "已為您從 {t} 繼續播放,點擊這裡 ", msg_unzip_confirm_n: "確定要在目前目錄解壓縮這 {n} 個檔案嗎?", msg_task_paused: "已暫停", msg_task_added: "已加入 {n} 個上傳任務", msg_task_fast_success: "秒傳成功", msg_task_upload_done: "上傳完成", log_dirty_data: "攔截到髒資料入侵!請求模式與目前視圖不符,已強制中斷。", msg_network_unstable: "網路連線波動,正在自動恢復...", msg_skip_locked: "{n} 個被鎖定", msg_skip_self: "{n} 個原地移動", msg_skip_conflict: "{n} 個子路徑忙碌", msg_skip_invalid: "已自動跳過無效項目:", msg_creating_cloud_task: "正在建立雲端下載任務...", str_parsing_torrent: "正在解析種子檔案...", err_torrent_no_info: "解析失敗:未發現有效資訊", err_file_read: "檔案讀取失敗", msg_cloud_task_finish: "建立完成:{s} 成功,{f} 失敗", msg_cloud_task_success: "🎉 已成功建立 {n} 個任務", msg_prepare_restore: "準備還原...", msg_smart_matching_n: "正在智慧比對密碼 ({n}個)...", msg_system_busy_retry: "系統忙碌,等待重試 ({n}/5)...", msg_unzip_running_bg: "[{n}] 已在雲端解壓縮中,請稍後重新整理檢視", msg_share_code_updated: "分享代碼已更新", msg_retry_submitted: "已重試提交 {n} 個任務", msg_aria2_batch_fail_log: "\n\n檢測到失敗項較多,已為您自動匯出完整錯誤清單 (.txt)", str_aria2_fetch_err: "(獲取連結失敗)", str_aria2_rpc_err: "(投遞失敗)", str_aria2_aborted: "(已取消)", str_aria2_fail_file_name: "Aria2_失敗清單", msg_op_blocked_moving: "⚠️ 操作攔截\n\n背景正在執行檔案搬移,請等待目前任務完成後再操作。", msg_op_blocked_analyzing: "⚠️ 操作攔截\n\n後台正在執行檔案搬移。為確保資料夾分析數據準確,請等待目前任務完成後再操作。", msg_op_blocked_exporting: "⚠️ 操作攔截\n\n後台正在執行檔案搬移。為確保匯出目錄文本準確,請等待目前任務完成後再操作。", msg_analyze_only_normal_dir: "請選擇資料夾。", msg_analyze_no_large_folders: "沒有發現符合閾值範圍的資料夾 ({s})", msg_analyze_summary_fmt: "共發現 {n} 個大於 {s}GB 的資料夾 (已依大小降序排列)", msg_prune_blocked_moving: "⚠️ 操作攔截\n\n背景正在執行檔案搬移,暫時無法執行清理。", msg_global_index_blocked_moving: "⚠️ 操作攔截\n\n背景正在執行檔案搬移。全盤索引需要穩定的目錄結構,請等待目前任務完成後再開啟全盤搜尋。", msg_resource_locked_download: "⚠️ 操作攔截\n\n選取項目包含正在移動的檔案或資料夾。請等待搬移完成後再發起下載。", msg_resource_locked_aria2: "⚠️ 操作攔截\n\n選取項目包含正在移動的檔案或資料夾。請等待搬移完成後再推送至 Aria2。", msg_flatten_blocked_moving: "⚠️ 操作攔截\n\n後台正在執行檔案搬移。此時執行文件分析會導致檔案列表缺失或重複,請稍後再試。", err_task_conflict: "⚠️ 操作攔截\n\n背景正在執行檔案搬移。全域清理需要穩定的目錄結構,請等待搬移完成後再執行。", title_del_task_confirm_fmt: "確認刪除 {n} 條傳輸任務?", lbl_del_cloud_files_too: "同時刪除雲端硬碟內的檔案", msg_file_del_failed: "檔案刪除失敗:", msg_task_del_success_fmt: "已刪除 {n} 個任務", title_clear_task_confirm: "確認清空所有上傳任務?", msg_task_clear_success_fmt: "已清空 {n} 個上傳任務", msg_unzip_virtual_view_warn: "您正在虛擬視圖中操作。解壓縮後的檔案將存放在各壓縮檔所在的原始資料夾中,而不會直接出現在目前清單中。

是否繼續?", msg_smart_matching_file: "智慧比對密碼中... ({n})", msg_unzip_batch_submitted: "✅ 已完成 {n} 個解壓縮任務", msg_unzip_batch_skipped: " ({n} 個跳過)", msg_unzip_check_source: "。請前往來源目錄檢視結果。", tip_jump_to_folder: "跳轉到此資料夾", msg_task_deleted: "任務已刪除", msg_scan_done: "掃描完成!\n共發現 {n} 個檔案,遍歷了 {f} 個資料夾。", msg_scan_fail: "\n\n❌ 有 {n} 個失敗。", msg_scan_fix: "\n\n✅ 自動修復了 {n} 次網路錯誤。", msg_down_scanning: "正在解析資料夾內容...", msg_down_progress: "正在呼叫瀏覽器下載...", msg_down_confirm_total: "✅ 掃描完畢,共找到 {n} 個檔案。\n\n⚠️ 警告:瀏覽器直接下載大量檔案極易導致頁面卡死或被攔截。\n建議超過 10 個檔案使用 Aria2 匯出。\n\n是否堅持使用瀏覽器下載?", msg_aria2_sending_batch: "🚀 正在分批傳送任務至 Aria2...", msg_aria2_check_fail: "Aria2 連線失敗!\n請檢查 URL 和 Token。", msg_aria2_check_ok: "Aria2 連線成功!", msg_aria2_sent: "已將 {n} 個檔案傳送到 Aria2。", msg_aria2_test_fail: "Aria2 連線失敗。\n仍然要儲存設定嗎?", title_aria2_fail: "連線測試失敗", msg_batch_scanning: "🚀 正在高速掃描目錄結構...", msg_batch_hydrating: "⚡ 正在平行擷取下載連結...", msg_batch_no_files: "未發現可下載的檔案。", msg_dup_warn: "是否開始搜尋重複檔案?", msg_dup_result: "發現 {n} 組重複項目。", msg_dup_none: "未發現重複檔案。", msg_bl_stop: "操作已停止。", msg_bl_add_done: "已將 {n} 個項目添加到記錄。", msg_bl_remove_done: "已從記錄中移除 {n} 個項目。", msg_bl_empty: "名單列表為空,無法運行。", msg_bl_clear_confirm: "確定要清空所有記錄條目嗎?此操作不可恢復。", msg_blacklist_run_none: "網盤中未發現符合名單條件的項目。", msg_blacklist_run_confirm: "在網盤中發現了 {n} 個已記錄項目。\n\n是否立即移入回收站?", msg_bl_run_limit: "⚠️ 模式限制\n\n清理操作涉及物理文件遞歸操作。目前處於非標準目錄,無法準確定位物理掃描範圍。\n\n請返回主頁常規文件夾後再執行清理。", msg_del_protected: "已保護 {n} 個已記錄文件不被刪除。", msg_del_none: "沒有可刪除的檔案。", msg_bl_scanning: "全盤搜尋中...\n已掃描目錄:{d} | 命中:{f}", rn_tip_wait: "請設定規則", rn_tip_jav: "點擊上方按鈕開始智慧匹配", rn_tip_none: "沒有符合的項目或名稱", rn_stat: "比對:{n} 項 | 有效變更:{m} 項", rn_warn_confirm: "確定要重新命名 {n} 個檔案嗎?", msg_bulkrename_done: "已重新命名 {n} 個項目。", msg_rn_all_skipped: "❌ 所有項目均因同名被跳過,未執行任何操作。", msg_rn_fail_count: "跳過已重名 {n} 個項目", msg_prune_confirm: "是否開始搜尋目前清單中的空資料夾?", msg_prune_none: "未發現空資料夾。", msg_prune_found: "發現了 {n} 個空資料夾。\n是否立即刪除?", msg_deleting_folders: "正在刪除 {n} 個資料夾...", msg_global_warn: "即將開始全盤檔案同步。\n\n檔案同步後快取到本機記憶體中,網頁重新整理前持續存在。\n\n是否繼續?", msg_init_scan_sel: "正在初始化選取項目掃描...", warn_clear_history: "確定要從歷史紀錄中移除選取的 {n} 項嗎?\n(這不會刪除您的雲端檔案)", msg_img_copy_hint: "在新視窗中,請按下 {cmd} 即可搜尋。", msg_aria2_not_set: "偵測到您尚未設定 Aria2,請填寫後繼續:", str_jav_no_match: "(未比對到番號)", msg_unzip_fail: "解壓縮請求失敗", msg_jszip_fail: "JSZip 載入失敗,請檢查網路。", msg_turbo_activated: "極速模式已激活:腳本已深度接管網頁邏輯,確保穩定流暢。", msg_console_legal: "嚴禁商業用途:本項目僅供個人學習與交流使用。", msg_ana_warn: "資料夾查重提示:判定基於演算法推測,刪除前請務必人工核對,以防誤刪。", /* --- 错误提示 --- */ err_invalid_links: "請輸入正確的連結", err_pwd_format: "密碼必須為 4-10 位字母或數字", err_invalid_torrent: "無效的種子檔案格式", err_torrent_complex: "解析複雜度過高,可能是非法檔案", err_torrent_format: "種子檔案結構損壞", err_torrent_len: "欄位長度解析異常", err_torrent_char: "解析遇到非法字符", err_share_code_exists: "該分享代碼已被佔用", err_folder_not_ready: "雲端資料夾正在建立中,請稍後再試", err_item_deleted: "該項目不存在", err_network: "網路錯誤", err_clipboard_denied: "剪貼簿存取被拒絕", err_worker: "工作執行緒錯誤", err_api: "API 錯誤", err_capture: "截圖失敗。", err_captcha_simple: "驗證失敗。請在網頁清單手動收藏一次檔案以完成驗證。", err_sub_dl_fail: "字幕下載失敗:", err_req_blocked: "網路請求失敗 (可能被攔截)", err_req_timeout: "請求超時", err_sub_drop_type: "僅解析字幕類型檔案", err_codec_t1: "無法播放影片編碼 ({c})", err_codec_t2: "您的瀏覽器不支援該影片格式。
請點擊下方按鈕呼叫外部播放器。", err_pwd_simple: "密碼錯誤", err_task_exists: "任務已存在", err_network_break: "圖片節點網路斷流,請再次點擊重試", err_no_failed_task: "未選取失敗的任務", err_unknown: "未知錯誤", err_invalid_regex: "無效的正規表示式", err_parent_not_found: "資料夾不存在", msg_sys_error: "不允許作業系統資料夾", msg_download_fail: "無法取得下載連結。", msg_video_fail: "無法取得影片連結。", err_star_sync_fail: "星號同步失敗", err_paste_descendant: "不能移動或複製到目前或目前子目錄下", err_quota_exceeded: "儲存空間不足", err_name_exists: "檔案名稱不能重複", err_share_pass: "自訂提取碼需為 4-10 個字元", str_error: "錯誤", str_error_crit: "嚴重錯誤", str_error_paste: "貼上錯誤", str_action_failed: "操作失敗", str_scan_error: "掃描錯誤", err_limit_too_low: "修改失敗:新次數 ({n}) 必須大於目前已儲存次數 ({s})", err_vault_max: "密碼金庫最多僅支援儲存 50 個常用密碼", err_pwd_len: "單個密碼長度不能超過 127 個字元", /* --- 帮助文档 --- */ modal_help_title: "說明", help_desc: `
✨ 體驗與導覽引擎
互動重構:在官方功能基礎上,介面仿 Windows 檔案總管 重構。
極速模式:開啟後接管原生邏輯,徹底解決海量檔案下的卡頓與崩潰問題。
進階路徑列:支援滾輪滑動、下拉選單同級切換定位。全盤搜尋、分析套件均整合於路徑列,支援路徑回顯與溯源跳轉。
體驗增強:支援星號等多維度排序,及一鍵模糊封面與深色佈景切換。後台採用 SWR 策略靜默無感重新整理視圖。
後台索引與保護:首頁藍點閃爍表示正同步目錄樹。系統自帶併發操作物理鎖,攔截衝突操作,嚴防髒資料產生。
* 註:預設資料夾(My Pack)受官方保護,嚴防誤刪、複製、移動及重新命名。
📂 批次與空間管理
批次重新命名:支援正規替換/刪除劇集流水號、文字格式化FC2 規範命名前綴去廣告及基於 MIME 的副檔名修復
分析套件檔案分析整合了篩選與查重(雜湊/時長/名稱三模態);資料夾分析整合了篩選與查重(名稱/相似度/包含率三模態);並支援匯出目前目錄樹清單。
智慧整理:一鍵清理空資料夾;批次解壓縮整合密碼自動記憶與智慧填入,支援跳過並刪除已解壓縮項目。
資源管理器:自訂檔案黑名單一鍵清理垃圾資源;或作為檔案白名單,在批次刪除時自動保護。
* 註:為避免資料同步衝突,處理期間請勿在其他用戶端修改檔案。
🌐 傳輸與分享中心
分享管理:支援設定提取次數上限,次數達標後連結自動失效取消分享。
極速上傳:支援全域將本機檔案/資料夾拖曳至網頁直傳,突破官方限制並大幅降低小檔案傳輸中斷率
雲下載增強:批次離線連結自動去重。內建磁鏈智慧清洗引擎(自動提取 Base32/Hex 雜湊去干擾);支援解析 .torrent 種子檔案;針對受限連結提供儲存網頁快照兜底方案。
* 註:提取次數攔截僅在網頁保持開啟且電腦未休眠時生效。
🎬 沉浸式媒體增強
播放引擎:支援 0.5x-3.0x 倍速、旋轉翻轉、強制比例、自動跳過片頭片尾及連播/循環模式,進度列支援縮圖預覽。內建看門狗,遇黑螢幕或不支援編碼自動回退相容畫質。
字幕系統:支援載入雲端同名字幕、本機檔案及跨站線上搜尋。支援字幕軸毫秒級偏移微調,及本機文字直接拖曳解析掛載。
視覺輔助:內嵌多引擎支援圖片或影片當前影格以圖搜圖;設定中可啟動「媒體模式」,使劇集/漫畫資料夾自動按名稱 A-Z 順序排列。
* 註:播放歷史列表持續記錄在腳本環境內產生的播放進度。
⚙️ 配置與資料管理
配置備份:支援將偏好設定、管理規則、密碼金庫等匯出為帶數位指紋的 JSON 備份檔案,匯入時支援智慧合併去重
資料清理:支援對全盤索引、偏好設定、管理規則、密碼金庫與快取按需清除,釋放本機空間並保障隱私。
* 註:全盤索引在網頁關閉後清空,而偏好設定、密碼金庫等則持久化保存。
⚡ 下載與分發
外部直連:支援一鍵獲取影片流直鏈,或喚起 PotPlayer 播放。支援將檔案透過 RPC 協定一鍵推播到 Aria2 節點。
分發增強:推播資料夾至 Aria2 時自動還原雲端樹狀目錄結構。支援長連線監控,遇錯自動匯出錯誤清單。支援設定資料夾下載過濾
本項目嚴格遵循 CC-BY-NC-SA-4.0 協議,嚴禁於任何形式的商業用途
` }, en: { /* --- 通用与基础UI --- */ title: "PikPak Master Enhancer", str_original: "Original", str_original_fast: "Original (High Speed)", str_folders: "Folders", str_files: "Files", unit_folders: "folders", unit_days: "days", unit_month: "month", unit_sec: "sec", str_no_files: "No files found", str_items: "items", col_name: "Name", col_size: "Size", col_dur: "Type/Duration", col_duration_only: "Duration", col_progress: "Progress", col_play_time: "Play Time", col_date: "Date Modified", col_remaining: "Remaining", col_path: "Path", col_old: "Original Name", col_new: "New Name", col_type: "Type", col_path_name: "Path / Name", col_action: "Action", lbl_folder_first: "Folders on top", tag_default: "Default", current_dir: "Current Directory", str_same_folder: "(Same Folder)", lbl_dont_show: "Don't show again", lbl_dont_show_session: "Don't remind me this session", str_empty_filename: "(Empty Filename)", str_empty_dir: "(Empty Directory)", btn_filter: "Filter", title_file_filter: "File Filter", cat_all: "All", cat_video: "Videos", cat_audio: "Audio", cat_image: "Images", cat_document: "Documents", cat_software: "Software", cat_archive: "Archives", cat_torrent: "Torrent", cat_other: "Others", btn_exit_filter: "Exit Filter", /* --- 属性面板 --- */ ctx_property: "Properties", title_property: "File Properties", lbl_prop_name: "File Name", lbl_prop_size: "File Size", lbl_prop_count: "File Count", lbl_prop_ctime: "Created", lbl_prop_mtime: "Modified", lbl_prop_source: "Source", lbl_prop_link: "Resource Link", lbl_prop_path: "Location", str_prop_cloud: "Cloud Download", str_prop_share: "From Share", str_prop_user: "User Upload", str_prop_unknown: "Unknown Source", fmt_prop_count: "Contains {f} files, {d} folders", str_prop_offline: "Offline Task", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "Home", btn_nav_share: "My Shares", btn_nav_offline: "Offline Transfers", btn_nav_recent: "Recent Added", btn_nav_history: "Watch History", btn_nav_starred: "Starred", btn_nav_trash: "Trash", btn_nav_upload: "My Uploads", title_offline: "My Transfers", trash_title: "Trash", trash_notice: "Files in Trash will be deleted after 15 days", history_notice: "Only records progress generated within the script environment", ctx_open: "Open", ctx_add_bl: "Add to Resource Manager", ctx_remove_bl: "Remove from Resource Manager", ctx_rename: "Rename", ctx_copy: "Copy", ctx_copy_name: "Copy Name", ctx_copy_link: "Copy Link", ctx_cut: "Move", ctx_del: "Delete", ctx_down: "Download", ctx_star: "Add Star", ctx_unstar: "Remove Star", ctx_locate: "Open file location", ctx_share: "Share", /* --- 通用文件操作按钮 --- */ btn_down: "Download", tip_down: "Download [Alt] + [D]", btn_aria2: "Send to Aria2", tip_aria2: "Send to Aria2 [Alt] + [A]", btn_refresh_short: "Refresh", tip_refresh: "Refresh [F5]", btn_newfolder: "New Folder", tip_newfolder: "New Folder [F8]", btn_del: "Delete", tip_del: "Delete [Delete]", btn_deselect: "Deselect", tip_deselect: "Deselect [Esc]", btn_invert: "Invert Selection", btn_copy: "Copy", tip_copy: "Copy [Ctrl] + [C]", btn_cut: "Move", tip_cut: "Cut [Ctrl] + [X]", btn_paste: "Paste", tip_paste: "Paste [Ctrl] + [V]", btn_clear_history: "Delete History", tip_clear_history: "Delete History [Delete]", btn_restore: "Restore", tip_restore: "Restore [R]", btn_del_forever: "Delete Permanently", tip_del_forever: "Delete Permanently [Delete]", btn_empty_trash: "Empty Trash", tip_empty_trash: "Empty Trash [Shift] + [Delete]", btn_exit: "Exit", btn_close: "Close", tip_close: "Close [Esc]", tip_theme: "Switch Theme [Alt] + [T]", tip_rotate: "Rotate [R]", tip_mirror: "Mirror [H]", tip_flip_v: "Vertical Flip [V]", tip_maximize: "Maximize [M]", tip_minimize: "Minimize [M]", tip_full_screen: "Fullscreen [Enter]", btn_help: "Help", tip_help: "Help [Alt] + [H]", btn_view_file: "View File", btn_jump: "Jump", btn_copy_text: "Copy", btn_stop: "Stop", tip_stop: "Stop current operation", btn_settings: "Settings", btn_logout: "Logout PikPak", msg_logout_confirm: "Are you sure you want to logout?", tip_settings: "Settings and more [Alt] + [S]", lbl_upload_to: "Upload to: ", msg_move_done: "Move completed.", /* --- 离线、上传与云下载 --- */ btn_upload: "Local Upload", btn_up_file: "Upload File", btn_up_folder: "Upload Folder", btn_cloud_download: "Cloud Download", btn_up_pause: "Pause", tip_up_pause: "Pause [Alt] + [P]", btn_up_start: "Start", tip_up_start: "Start [Alt] + [G]", btn_up_del: "Remove", tip_up_del: "Remove [Delete]", btn_up_clear_all: "Clear All", tip_up_clear_all: "Clear All [Shift] + [Delete]", btn_retry_task: "Retry", tip_retry_task: "Retry [R]", col_task_status: "Task Status", col_task_progress: "Cloud Progress", col_up_speed: "Speed", col_up_status: "Status", lbl_task_run: "In Progress", lbl_task_fail: "Failed", lbl_task_ok: "Completed", lbl_up_run: "Uploading", lbl_up_pause: "Paused", lbl_up_downloading: "Downloading", lbl_up_done: "Completed", tip_up_pause_desc: "Includes manually paused and error tasks", title_cloud_task: "Create Cloud Download", ph_cloud_links: "Supported links:\n- Magnet, HTTP, FTP, etc.\n- YouTube, X (Twitter), TikTok, Facebook, etc.\nAdd multiple links by starting a new line.", lbl_save_to: "Save to:", lbl_default_folder: "Default Folder", btn_via_torrent: "Upload Torrent", tip_cloud_save_path: "Standard cloud downloads are saved in the My Pack folder. Downloads from other apps are saved to the My [XYZ] folder, where [XYZ] is the app name.", lbl_smart_fix: "Auto-fix masked magnets (Extract hash / Remove text noise)", title_save_method: "Save Method", msg_save_snapshot_desc: "This link can only be saved as a Web Snapshot.", tip_snapshot_details: "PikPak cannot extract media from this link directly. It will save the complete web content as a snapshot file instead.", btn_save_snapshot: "Save Snapshot", btn_create_now: "Create Now", btn_modify: "Modify", str_snap_link_count_suffix: " and {n} other links", /* --- 分享管理 --- */ btn_cancel_share: "Stop Sharing", share_copy_suffix: "Copy this content and open the PikPak app to watch instantly.", share_copy_pwd: "Password", title_share_detail: "Share Details", ctx_share_detail: "View Share Details", ctx_share_copy: "Copy Link & Password", col_view: "Views", col_save: "Saves", col_share_time: "Shared on", col_share_status: "Status", lbl_limit_reached: "Limit Reached", lbl_limit_tip: "Limit Count", lbl_share_view: "Views", lbl_share_save: "Saves", lbl_share_link_title: "Share Link", lbl_share_pwd_title: "Password", lbl_share_expire_title: "Expires", btn_copy_link_pwd: "Copy Link & Password", str_expire_suffix: " days left", ph_edit_pwd: "Enter password (4-10 characters)", btn_close_pwd: "Disable Password", str_no_pwd: "No Password", title_edit_share_code: "Edit Share Code", ph_edit_share_code: "5-18 characters (letters, numbers, symbols)", btn_add_share_code: "Add Share Code", btn_del_share_code: "Delete Share Code", share_title: "Share Files", share_mode: "Share Method", share_public: "Public Link", share_encrypted: "Encrypted Link", share_expiry: "Expiration", share_pass: "Access Code", share_count: "Access Limit", share_count_ed: "Limit", share_perm: "Permanent", share_unlimit: "Unlimited", share_rand: "Random", share_custom: "Custom", share_days: "days", share_times: "times", btn_share_start: "Share Now", cal_custom_title: "Custom Expiry", lbl_share_link: "Link", lbl_share_code: "Code", btn_copy_share: "Copy All", str_share_expired: "Expired", str_share_deleted: "File Deleted", title_edit_pwd: "Change Password", lbl_share_code_title: "Share Code", ph_password: "Password", ph_pass_range: "4-10 characters", cal_week_days:["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], cal_months:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "File Analysis", btn_scan: "File Perspective", lbl_scan_selected: "Execute File Perspective on {n} selected items", lbl_keyword_filter: "Exclude Keywords", ph_keyword_filter: "Exclude keywords, comma-separated", lbl_scan_current: "Execute File Perspective on all items in the current path", tip_dup: "Deduplicate Files", lbl_dup_selected: "Execute File Deduplication on {n} selected items", lbl_dup_current: "Execute File Deduplication on all items in the current path", tip_scan_dup: "Filter or Deduplicate Files", lbl_dup_tool: "Selection Criteria:", lbl_dup_reset: "↺ Reset (Clear selection)", lbl_dup_select_folder: "📂 Select by Folder", lbl_dup_select_folder_short: "📂 Folder", lbl_dup_invert: "Invert Mode", lbl_dup_invert_short: "Invert", tip_dup_invert_limit: "Only available for Folder selection", fmt_dup_count: "({n} duplicates)", btn_start_scan: "Start Scan", tag_hash: "Exact Match", tag_hash_short: "Exact", tag_name: "Name Sim", tag_name_short: "Name", tag_sim: "Duration Sim", tag_sim_short: "Duration", label_dup_video: "Videos (Exact Match + Duration Sim + Name Sim)", label_dup_image: "Images (Exact Match + Name Sim)", label_dup_other: "Others (Exact Match + Name Sim)", btn_analyze: "Folder Analysis", tip_analyze: "Filter or Deduplicate Folders", btn_export: "Export Directory", tip_export: "Generate and download the file tree of the current path", title_export_format: "Export Format", lbl_export_current: "Export directory structure for all items in the current path", opt_tree_view: "Tree View", opt_list_view: "List View", msg_exporting: "Generating directory tree...", str_analyze_results: "Match Results", lbl_size_threshold: "Size Threshold", title_analyze_result: "Folder Analysis Results", opt_ana_large: "Folder Perspective", lbl_analyze_selected: "Execute Folder Perspective on {n} selected items", lbl_analyze_current: "Execute Folder Perspective on all items in the current path", opt_ana_sim: "Folder Deduplication", lbl_ana_sim_selected: "Execute Folder Deduplication on {n} selected items", lbl_ana_sim_current: "Execute Folder Deduplication on all items in the current path", title_algo_help: "Algorithm Guide", algo_help_content: "Name Match: Finds folder groups with similar names and sizes.\nSimilarity Match: Finds folder groups with highly overlapping internal files.\nContainment Match: Finds subset redundancies where small folder content is fully covered by a large folder.\n\nAccuracy: Similarity > Containment > Name\nScope: Containment > Similarity", lbl_threshold: "Threshold", lbl_sim_score: "Similarity", lbl_containment: "Containment", lbl_name_match: "Name Match", lbl_sim_match: "Similarity Match", lbl_contain_match: "Containment Match", lbl_ana_min: "Min", lbl_ana_max: "Max", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "Prune Empty Folders", tip_prune: "Delete empty folders [Ctrl] + [Delete]", btn_rename: "Rename", tip_rename: "Rename [F2]", btn_bulkrename: "Bulk Rename", tip_bulkrename: "Bulk Rename [F2]", title_blacklist: "Resource Manager", btn_blacklist_run: "Run Cleanup Now", btn_clear_list: "Clear List", tip_bl_desc: "Blacklisted items only removed via 'Run Cleanup Now'.", tip_blacklist_input: "Resource Manager [Alt] + [Delete]", label_bl_folder: "Folder List (Exact)", label_bl_file: "File List (Exact)", lbl_type_folder: "Folder", lbl_type_file: "File", ph_bl_folder: "Paste or use right-click menu to add folders.", ph_bl_file: "Paste or use right-click menu to add files.", modal_bl_preview: "Scan Results", btn_bl_delete: "Delete Selected", modal_rename_title: "Rename", modal_rename_multi_title: "Bulk Rename", btn_preview: "Preview", modal_preview_title: "Confirm Changes", label_pattern: "Pattern (e.g., Video {n})", label_replace: "Replace/Delete", label_replace_note: "Case sensitive", label_include_ext: "Include Extension", label_regex: "Regex", placeholder_find: "Search for...", placeholder_replace: "Replace with... (leave empty to delete)", label_jav: "FC2 Clean Naming", lbl_rn_pattern: "Naming Template", lbl_rn_case_convert: "Case Conversion", opt_rn_keep_origin: "(No change)", opt_rn_lower: "lowercase (abc)", lbl_rn_mode_series: "TV Show Mode", lbl_rn_mode_format: "Formatting", lbl_rn_mode_ad: "Ad-Cleaner", lbl_rn_mode_ext: "Fix Extension", opt_rn_upper: "UPPERCASE (ABC)", opt_rn_title: "Title Case (Abc)", lbl_rn_width_convert: "Width Conversion", opt_rn_width_half: "Full-width to Half (A->A)", opt_rn_width_full: "Half-width to Full (A->A)", lbl_rn_preview_title: "Change Preview", tip_jav_mode_desc: "✨ Smartly extract FC2 and remove irrelevant characters", tip_ad_remove_desc: "🧹 Removes ads, URLs, junk symbols, cleans Emojis, and fixes bracket formats.", tip_ext_fix_desc: "🧩 Corrects extensions based on actual file type (MIME).", label_replace_find: "Find", label_replace_to: "Replace With", /* --- 解压相关 --- */ btn_unzip: "Bulk Extract", tip_unzip: "Bulk Extract [Alt] + [U]", btn_unzip_all: "Extract All", btn_understand_unzip: "I Understand & Extract", title_input_pwd: "Password Required", lbl_pwd_prompt: "Enter password:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "External Play", tip_ext: "Play with PotPlayer or get stream link [Alt] + [E]", btn_img_search: "Image Search [F]", tip_play_search: "Search by Image [F]", tip_pip: "Picture-in-Picture [P]", str_no_sub: "No Subtitles", lbl_sub_sel: "Subtitles", lbl_show_sub: "Show Subtitles", btn_sub_search: "Search Online", btn_sub_cloud: "Cloud", btn_sub_local: "Local File", lbl_sub_pos: "Subtitle Position", lbl_sub_bottom: "Bottom", lbl_sub_top: "Top", lbl_sub_bg_op: "BG Opacity", lbl_sub_size: "Subtitle Size", lbl_sub_offset: "Subtitle Sync", title_sel_sub: "Select Subtitles", ph_sub_search: "Type keywords, links will update below...", btn_force_play: "Force Playback", str_compat_mode: "Compatibility Mode", lang_code: "en", btn_go_search: "🔍 Manual Search on {n}", btn_restart: "Restart", btn_prev_video: "Previous [Ctrl + ←]", btn_next_video: "Next [Ctrl + →]", tip_plist_open: "Expand [E]", tip_plist_close: "Collapse [E]", tab_sub: "Subtitles", tab_size: "Aspect", tab_more: "More", lbl_ratio: "Ratio", lbl_direction: "Rotation", opt_ratio_def: "Default", btn_rot_l: "Rot L", btn_rot_r: "Rot R", btn_flip_h: "Flip H", btn_flip_v: "Flip V", lbl_play_end: "On End", opt_list_loop: "Repeat List", opt_single_loop: "Repeat One", opt_play_stop: "Pause", lbl_skip_op: "Skip Intro", lbl_skip_ed: "Skip Outro", export_link_title: "Export Video Stream Link", btn_start_play: "Start Play", btn_copy_link: "Copy Link", tip_copy_link: "Copy Link [Alt] + [C]", opt_player_other: "Others (Export Link)", lbl_player: "Player", btn_mark: "Mark", lbl_resolution: "Quality", str_switch_compat: "Current quality unavailable, reverted to {n}", type_img: "Image", type_doc: "Document", type_archive: "Archive", type_sub: "Subtitle", type_app: "Application", type_suffix: "File", /* --- 设置与搜索 --- */ label_turbo_mode: "Turbo Mode", desc_turbo_mode: "Auto-replace native UI (Recommended)", lbl_aria2_status: "Connection", ph_aria2_secret: "Secret (Optional)", str_connected: "Connected", str_conn_fail: "Failed", str_connecting: "Testing...", tip_mixed_content: "Common Ports:\n• 6800 (Aria2 Standard)\n• 16800 (Motrix Default)\n• 6881 (Others)", picker_title: "Select Folder", picker_all: "All Files", picker_new: "New Folder", picker_sort_new: "Newer", picker_sort_old: "Older", title_select_file: "Select File", placeholder_search: "Search files...", placeholder_search_short: "Search", title_search_hist: "Search History", btn_clear_hist: "Clear", lbl_global_search: "Global Search", lbl_search_path: "Include path in search", lbl_search_path_short: "Path", str_search_results: "Search Results", modal_settings_title: "Settings", label_lang: "Language", label_thumb: "Privacy Mode (Blur Thumbnails)", label_keep_pos: "Restore scroll position on return", label_sort_pref: "Sorting Preference", opt_sort_indep: "Independent for each folder", opt_sort_global: "Same for all folders", desc_sort_indep: "Sorting changes only apply to the current folder.", desc_sort_global: "All folders use the same sorting (Newest first).", label_search_engine: "Image Search Engine", opt_engine_google: "Google Lens", opt_engine_yandex: "Yandex", opt_engine_saucenao: "SauceNAO", opt_engine_tracemoe: "trace.moe", label_dup_strictness: "Similarity Threshold", opt_strict: "Strict", opt_loose: "Loose", label_comic_mode: "Media Mode", desc_comic_mode: "Default A-Z sorting for Image/Video only folders.", label_aria2_url: "Aria2 RPC URL", label_aria2_token: "Aria2 RPC Token", label_privacy_mode: "Privacy Mode", label_blur_cover: "Blur Cover Images", label_dl_filter_ext: "Download Extension Filter (e.g. .txt, .jpg)", label_dl_filter_name: "Download Name Filter (Keyword or Full Name)", lbl_dl_filter: "Folder Download Filter", desc_dl_filter: "Auto-exclude matching files when downloading/pushing folders", lbl_config_manage: "Config Management", btn_export_data: "Export Backup", btn_import_data: "Import Backup", btn_clean_data: "Clear Local Data", title_clean_data: "Select Items to Clear", msg_clean_confirm: "Are you sure you want to permanently delete the selected local data? This cannot be undone.", msg_clean_success: "Local data cleared. Page will refresh...", opt_cfg_index: "Global Index (Synced Directory Structure/File Snapshots)", opt_cfg_pref: "Preferences (UI Looks/Habits/Sort Order)", opt_cfg_rules: "Rules (Resource Manager/Share Limits/Search/Download Rules)", opt_cfg_vault: "Vault (Extract Password Memory)", opt_cfg_history: "Video Cache (Play Progress / Duration Cache)", opt_cfg_cache: "Cache (Folder Mod-Time/Last Position/Fingerprint)", msg_import_confirm: "The imported config will be merged with current settings (lists/records combined, conflicting basic settings overwritten). Continue?", msg_import_success: "Config imported successfully. Page will refresh...", err_invalid_config: "Invalid config file: Missing fingerprint or format error", err_json_format: "Parse failed: JSON syntax error or file corrupted", lbl_storage: "Storage", lbl_browse_exp: "Browsing Experience", lbl_skip_bl_on_del: "Skip resources recorded in manager on delete", lbl_pwd_manage: "Extraction Password Management", title_pwd_vault: "Password Vault", lbl_pwd_try_count: "Max password retries per archive", tip_pwd_manual: "One password per line, Enter to wrap", str_root_dir_cn: "Root", btn_ana_select: "Smart Select", opt_keep_new: "Keep Newest", opt_keep_old: "Keep Oldest", opt_keep_large: "Keep Largest", opt_keep_small: "Keep Smallest", opt_keep_short: "Keep Shortest Name", opt_keep_long: "Keep Longest Name", /* --- 状态、进度与加载短语 --- */ loading: "Loading...", loading_detail: "Full-speed indexing directory...", loading_fetch: "Fetching... ({n})", loading_dup: "Analyzing duplicates... ({p}%)", str_loading_placeholder: "Loading...", str_load_failed: " (Load Failed)", str_load_failed_simple: "Load Failed", str_waiting_token: "Syncing login status...", str_speed: "Speed", status_scanning_selection: "Scanning selection... {n}", status_ready: "{n} items", sel_count: "{n} items selected", str_cached: "Cached:", str_retries: "Retries:", str_failed: "Failed:", str_success: "Success:", str_stopping: "Stopping...", str_merging: "Merging data...", str_rendering: "Rendering list...", str_scanning: "Scanning...", str_analyzing: "Analyzing...", str_deleting: "Deleting...", str_saving: "Saving...", str_saving_dots: "Saving...", str_checking_bl: "Matching records...", str_processing: "System processing at full speed...", str_cleanup_done: "Cleanup completed.", str_waiting_preload: "Waiting for preload...", str_copying: "Copying to clipboard...", str_moving: "Preparing to move...", str_sorting: "Sorting...", str_refreshing: "Refreshing...", str_refreshing_cache: "Refreshing cache...", str_syncing_stars: "Syncing favorites...", str_updating_view: "Updating view...", str_generating_view: "Generating view...", str_group: "Group", str_init_rename: "Initializing rename...", str_renaming: "Renaming...", str_calc_changes: "Calculating changes...", str_scanning_dir: "Scanning directory...", str_init_op: "Initializing operation...", str_init_scan: "Initializing global scan...", str_rebuilding: "Rebuilding index...", str_upload_1: "Uploading (Node 1/3)...", str_upload_2: "Node 1 timeout, switching to Node 2...", str_upload_3: "Node 2 timeout, trying last node...", str_upload_fail_copy: "Upload failed, copying to clipboard...", msg_transcoding: "Transcoding in cloud...", msg_transcoding_wait: "Server is processing this video, please wait.", str_preparing: "Preparing extraction...", str_unzipping: "Extracting: {n}", str_unzipping_state: "Extracting...", str_unzipping_prog_0: "Extracting: 0%", str_unzipping_prog_100: "Extracting: 100%", str_unzipping_prog_fmt: "Extracting: {n}%", msg_task_waiting: "Waiting...", msg_task_hashing: "Verifying checksum...", msg_task_init_upload: "Initializing upload...", msg_task_uploading: "Uploading...", msg_task_init_part: "Initializing parts...", msg_task_uploading_2: "Uploading...", str_loc_tracing: "Tracing path...", str_loc_stale: "Stale cache, syncing with cloud...", str_verifying: "Verifying...", str_server_indexing: "Server indexing... ({n}/5)", str_creating_task_n: "Creating tasks ({n}/{t})...", msg_submit_request: "Submitting request... {c}/{t}", msg_wait_server: "Waiting for server... ({c}/{t})", msg_server_processing: "Server processing... ({c}/{t})", str_jav_querying: "Querying...", lbl_done_check: "✔ Done", msg_limit_updated: "Access limit updated", /* --- 提示、确认与交互消息 --- */ title_alert: "Notice", title_confirm: "Confirm", title_prompt: "Input", btn_ok: "OK", btn_yes: "Yes", btn_no: "No", btn_save: "Save Config", btn_cancel: "Cancel", btn_create: "Create", btn_skip: "Skip", msg_down_success: "Browser download called for {n} files.", msg_batch_txt: "Download list (.txt) generated.", msg_clear_history_done: "Removed from history.", msg_skip_unzipped: "Skipped {n} already extracted items.", msg_unzip_skip_del_confirm: "Detected {n} already extracted archives. Move them to Trash?", msg_cancel_share_confirm: "Are you sure you want to stop {n} shares?\nLinks will expire immediately.", msg_pwd_updating: "Updating password...", msg_pwd_updated: "Password updated.", msg_exp_updated: "Expiry updated.", msg_cancel_share_done: "Stopped {n} shares.", msg_drag_drop_hint: "Drop files here to upload", str_drag_files: " and {n} other files", msg_creating_share: "Creating share...", title_share_result: "Share Success", msg_no_files: "No items.", msg_no_selection: "Please select items first.", warn_del: "Are you sure you want to delete {n} items?", msg_clear_sel_confirm: "You have {n} duplicates selected. Clear selection?", str_bl_stat: "Matches: {n} | Selected: {m}", str_hits: "Hits", msg_settings_saved: "Settings saved. Page will refresh.", msg_name_exists: "Name already exists: {n}", str_name_conflict: "(Conflict)", msg_newfolder_prompt: "Folder name:", msg_rename_prompt: "Enter new name:", msg_copy_done: "Copied. Navigate to destination and paste.", msg_cut_done: "Ready to move. Navigate to destination and paste.", msg_paste_empty: "Nothing to paste.", msg_copy_empty: "Clipboard is empty.", msg_add_success: "Added {n} rows.", msg_del_done: "Selected rows deleted.", msg_del_select: "Select rows to delete first!", msg_del_items_done: "Deleted {n} items.", msg_copy_success: "Copied to clipboard", str_redirecting: "Redirecting to Google Lens...", msg_manual_paste: "Upload timeout. Screenshot copied to clipboard, press {cmd} in new window.", msg_starring: "Adding to favorites...", msg_unstarring: "Removing from favorites...", msg_star_added: "Added to Favorites", msg_unstar_done: "Removed from Favorites", msg_empty_trash_confirm: "Empty the trash? This cannot be undone!", msg_trash_emptied: "Trash emptied.", msg_del_forever_confirm: "Permanently delete these {n} items? This cannot be undone!", msg_del_forever_done: "{n} items deleted permanently.", msg_restore_done: "Successfully restored {n} items.", msg_auto_sub_load: "Auto-loaded subtitle: {n}", msg_dl_sub: "Downloading subtitles...", msg_transcode_done: "✅ Transcode finished. Starting playback.", msg_fallback_report: "⚠️ Original quality unplayable (MPEG4/HEVC). Switched to {n}.", tip_manual_sub: "Tip: Drag and drop .srt or .vtt files into the player to load.", msg_sub_drop_load: "Subtitle loaded via drag-and-drop: {n}", msg_resume_hint: "Resumed from {t}. Click here to ", msg_unzip_confirm_n: "Extract {n} files to current directory?", msg_task_paused: "Paused", msg_task_added: "Added {n} upload tasks", msg_task_fast_success: "Instant upload successful", msg_task_upload_done: "Upload complete", log_dirty_data: "Dirty data detected! View mismatch intercepted.", msg_network_unstable: "Network unstable, attempting to recover...", msg_skip_locked: "{n} items locked", msg_skip_self: "{n} items skipped (already at destination)", msg_skip_conflict: "{n} paths busy", msg_skip_invalid: "Auto-skipped invalid items: ", msg_creating_cloud_task: "Creating cloud download...", str_parsing_torrent: "Parsing torrent file...", err_torrent_no_info: "Parse failed: No valid info found", err_file_read: "File read error", msg_cloud_task_finish: "Finished: {s} successful, {f} failed", msg_cloud_task_success: "🎉 Successfully created {n} tasks", msg_prepare_restore: "Preparing to restore...", msg_smart_matching_n: "Smart-matching passwords ({n})...", msg_system_busy_retry: "System busy, retrying ({n}/5)...", msg_unzip_running_bg: "[{n}] is being extracted in the cloud. Refresh later.", msg_share_code_updated: "Share code updated.", msg_retry_submitted: "Resubmitted {n} tasks.", msg_aria2_batch_fail_log: "\n\nToo many failures. A complete log (.txt) has been exported.", str_aria2_fetch_err: "(Fetch Error)", str_aria2_rpc_err: "(RPC Error)", str_aria2_aborted: "(Aborted)", str_aria2_fail_file_name: "Aria2_Failed_List", msg_op_blocked_moving: "⚠️ Operation Blocked\n\nFile transfer in progress. Please wait.", msg_op_blocked_analyzing: "⚠️ Operation Blocked\n\nFile transfer in progress. For accurate Folder Analysis data, please wait for the current task to finish.", msg_op_blocked_exporting: "⚠️ Operation Blocked\n\nFile transfer in progress. To ensure accurate export text, please wait for the current task to finish.", msg_analyze_only_normal_dir: "Please select folder(s)", msg_analyze_no_large_folders: "No folders found within the threshold range ({s})", msg_analyze_summary_fmt: "Found {n} folders larger than {s}GB (sorted by size)", msg_prune_blocked_moving: "⚠️ Operation Blocked\n\nCannot prune folders during a transfer.", msg_global_index_blocked_moving: "⚠️ Operation Blocked\n\nGlobal indexing requires a stable structure. Please wait for the transfer.", msg_resource_locked_download: "⚠️ Operation Blocked\n\nSelected items are being moved. Please wait.", msg_resource_locked_aria2: "⚠️ Operation Blocked\n\nCannot send to Aria2 while files are moving.", msg_flatten_blocked_moving: "⚠️ Operation Blocked\n\nFile transfer in progress. Performing File Analysis at this time may result in missing or duplicate file lists, please try again later.", err_task_conflict: "⚠️ Operation Blocked\n\nGlobal cleanup requires a stable structure. Please wait.", title_del_task_confirm_fmt: "Delete {n} transfer tasks?", lbl_del_cloud_files_too: "Also delete files in cloud drive", msg_file_del_failed: "File deletion failed: ", msg_task_del_success_fmt: "Deleted {n} tasks", title_clear_task_confirm: "Clear all upload tasks?", msg_task_clear_success_fmt: "Cleared {n} upload tasks", msg_unzip_virtual_view_warn: "You are in a Virtual View. Extracted files will appear in the original folders of the archives, not in this list.

Continue?", msg_smart_matching_file: "Matching password... ({n})", msg_unzip_batch_submitted: "✅ Completed {n} extraction tasks", msg_unzip_batch_skipped: " ({n} skipped)", msg_unzip_check_source: ". Please check source directory for results.", tip_jump_to_folder: "Go to folder", msg_task_deleted: "Task deleted", msg_scan_done: "Scan complete!\nFound {n} files across {f} folders.", msg_scan_fail: "\n\n❌ {n} failures.", msg_scan_fix: "\n\n✅ Auto-fixed {n} network errors.", msg_down_scanning: "Parsing folder contents...", msg_down_progress: "Calling browser download...", msg_down_confirm_total: "✅ Scan complete. {n} files found.\n\n⚠️ Warning: Downloading many files via browser may freeze the page or get blocked.\nSuggest using Aria2 for >10 files.\n\nProceed anyway?", msg_aria2_sending_batch: "🚀 Sending tasks to Aria2 in batches...", msg_aria2_check_fail: "Aria2 connection failed!\nCheck URL and Token.", msg_aria2_check_ok: "Aria2 connection successful!", msg_aria2_sent: "Sent {n} files to Aria2.", msg_aria2_test_fail: "Aria2 test failed. Save settings anyway?", title_aria2_fail: "Connection Failed", msg_batch_scanning: "🚀 Scanning directory structure...", msg_batch_hydrating: "⚡ Extracting download links...", msg_batch_no_files: "No downloadable files found.", msg_dup_warn: "Search for duplicate files?", msg_dup_result: "Found {n} duplicate groups.", msg_dup_none: "No duplicates found.", msg_bl_stop: "Operation stopped.", msg_bl_add_done: "Added {n} items to records.", msg_bl_remove_done: "Removed {n} items from records.", msg_bl_empty: "Record list is empty, cannot run.", msg_bl_clear_confirm: "Clear all records? This cannot be undone.", msg_blacklist_run_none: "No matching records found in cloud drive.", msg_blacklist_run_confirm: "Found {n} recorded items.\n\nMove to trash now?", msg_bl_run_limit: "⚠️ Mode Restriction\n\nCleanup requires physical recursive scanning. Please run from Home or a standard folder.", msg_del_protected: "Protected {n} recorded files from deletion.", msg_del_none: "No files to delete.", msg_bl_scanning: "Global scanning... \nFolders: {d} | Hits: {f}", rn_tip_wait: "Please set rules", rn_tip_jav: "Click the button above to start matching", rn_tip_none: "No matching items", rn_stat: "Matches: {n} | Valid changes: {m}", rn_warn_confirm: "Rename {n} files?", msg_bulkrename_done: "Renamed {n} items.", msg_rn_all_skipped: "❌ All items skipped due to name conflicts.", msg_rn_fail_count: "Skipped {n} items (name conflict)", msg_prune_confirm: "Search for empty folders in the current list?", msg_prune_none: "No empty folders found.", msg_prune_found: "Found {n} empty folders.\nDelete them now?", msg_deleting_folders: "Deleting {n} folders...", msg_global_warn: "Start full drive sync?\n\nFiles will be cached in memory until page refresh.\n\nContinue?", msg_init_scan_sel: "Initializing scan for selected items...", warn_clear_history: "Remove {n} items from history?\n(This will not delete the cloud files)", msg_img_copy_hint: "Press {cmd} in the new window to search.", msg_aria2_not_set: "Aria2 not configured. Please fill in details:", str_jav_no_match: "(No match found)", msg_unzip_fail: "Extraction request failed.", msg_jszip_fail: "JSZip failed to load. Check your network.", msg_turbo_activated: "Turbo Mode Active: Script has taken over web logic for peak performance.", msg_console_legal: "Strictly Non-Commercial: This project is strictly for personal study and communication only.", msg_ana_warn: "Folder Deduplication Hint: Based on algorithmic inference. Please double-check manually before deleting.", /* --- 错误提示 --- */ err_invalid_links: "Please enter valid links.", err_pwd_format: "Password must be 4-10 alphanumeric characters.", err_invalid_torrent: "Invalid torrent file format", err_torrent_complex: "Parsing complexity too high (possibly invalid file)", err_torrent_format: "Torrent structure corrupted", err_torrent_len: "Field length parsing error", err_torrent_char: "Unexpected character encountered", err_share_code_exists: "Share code already taken.", err_folder_not_ready: "Cloud folder is being created, please try again later.", err_item_deleted: "Item does not exist.", err_network: "Network error.", err_clipboard_denied: "Clipboard access denied.", err_worker: "Worker thread error.", err_api: "API error.", err_capture: "Screenshot failed.", err_captcha_simple: "Verification failed. Manually save a file once to pass verification.", err_sub_dl_fail: "Subtitle download failed: ", err_req_blocked: "Request blocked (Check firewall/adblock).", err_req_timeout: "Request timeout.", err_sub_drop_type: "Only subtitle file types are supported.", err_codec_t1: "Codec not supported ({c}).", err_codec_t2: "Browser cannot play this format.
Please use an external player.", err_pwd_simple: "Incorrect password.", err_task_exists: "Task already exists.", err_network_break: "Image node disconnected, click again to retry.", err_no_failed_task: "No failed tasks selected.", err_unknown: "Unknown error.", err_invalid_regex: "Invalid Regular Expression.", err_parent_not_found: "Folder not found.", msg_sys_error: "System folders cannot be modified.", msg_download_fail: "Could not retrieve download link.", msg_video_fail: "Could not retrieve video link.", err_star_sync_fail: "Favorites sync failed.", err_paste_descendant: "Cannot move/copy into the same or a sub-folder.", err_quota_exceeded: "Storage quota exceeded.", err_name_exists: "File name already exists.", err_share_pass: "Access code must be 4-10 characters.", str_error: "Error", str_error_crit: "Critical Error", str_error_paste: "Paste Error", str_action_failed: "Action Failed", str_scan_error: "Scan Error", err_limit_too_low: "Failed: New limit ({n}) must be higher than current ({s}).", err_vault_max: "Password vault supports storing up to 50 common passwords", err_pwd_len: "A single password length cannot exceed 127 characters", /* --- 帮助文档 --- */ modal_help_title: "Help", help_desc: `
✨ Experience & Navigation Engine
UI Refactoring: Interface redesigned to resemble Windows File Explorer.
Turbo Mode: Takes over native logic completely to resolve lag and crashes with massive files.
Advanced Path Bar: Supports scroll wheel navigation and dropdown peer-level switching. Global search and analysis suites are integrated, supporting path echoing and source backtracking.
Enhanced UX: Supports multi-dimensional sorting, one-click cover blurring, and dark theme switching. Background uses SWR strategy for silent view updates.
Background Indexing & Protection: A blinking blue dot on the home icon indicates directory tree syncing. Features a physical concurrency lock to intercept conflicting operations and prevent dirty data.
* Note: The default folder (My Pack) is officially protected against accidental deletion, copying, moving, and renaming.
📂 Batch & Space Management
Batch Rename: Supports Regex replace/delete, episode serialization, text formatting, AV/FC2 standard naming, ad prefix removal, and MIME-based extension fixing.
Analysis Suite: File Analysis integrates filtering and deduplication (hash/duration/name tri-modal); Folder Analysis integrates filtering and deduplication (name/similarity/containment tri-modal); and supports exporting the directory tree.
Smart Organizing: One-click empty folder cleanup; Batch Unzip integrates automatic password memory and smart auto-filling, supporting auto-skip and deletion of extracted items.
Resource Manager: Can be used as a File Blacklist to clean up junk resources; or as a File Whitelist to auto-skip and protect items during batch deletion.
* Note: To avoid data sync conflicts, please do not modify files via other clients during processing.
🌐 Transfer & Sharing Hub
Share Management: Supports setting extraction limits. Shares are automatically canceled once the limit is reached.
Ultra-fast Upload: Supports global drag-and-drop direct upload, bypassing official limits and significantly reducing the interruption rate of small file transfers.
Cloud Download Enhancements: Auto-deduplicates batch offline links. Built-in magnet smart cleaning engine (extracts Base32/Hex hash to remove noise); supports parsing .torrent files; provides web snapshot saving as a fallback for restricted links.
* Note: Extraction limit interception only works when the webpage is kept open and the computer is awake.
🎬 Immersive Media Enhancements
Playback Engine: Supports 0.5x-3.0x speed, rotation/mirroring, forced aspect ratios, auto-skip intro/outro, and autoplay/loop modes. The progress bar supports thumbnail previews. Built-in Watchdog automatically falls back to compatible quality upon black screens or unsupported codecs.
Subtitle System: Supports loading cloud subtitles, local files, and cross-site online search. Supports millisecond-level subtitle offset tweaking and direct drag-and-drop parsing of local text.
Visual Aids: Built-in multi-engine reverse image search; "Media Mode" can be activated to auto-sort series or manga folders alphabetically (A-Z).
* Note: The playback history list continuously records progress generated within the script environment.
⚙️ Configuration & Data Management
Config Backup: Supports exporting preferences, rules, password vaults as JSON backup files with digital fingerprints. Supports smart merge & deduplication upon importing.
Data Cleanup: Supports on-demand clearing of global index, preferences, rules, password vaults, and caches to free up local storage and protect privacy.
* Note: The global index is cleared when the webpage is closed, while preferences, password vaults, etc., are saved persistently.
⚡ Download & Distribution
External Direct Connection: Supports one-click video stream link extraction, or launching PotPlayer. Supports pushing files to Aria2 nodes via RPC protocol.
Distribution Enhancements: Auto-reconstructs cloud tree directory structures when pushing folders to Aria2. Supports persistent connection monitoring and auto-exports error lists upon failure. Supports setting folder download filters.
This project strictly adheres to the CC-BY-NC-SA-4.0 license and is strictly prohibited for any commercial use.
` }, ko: { /* --- 通用与基础UI --- */ title: "PikPak 인핸서 마스터", str_original: "원본 화질", str_original_fast: "원본 (고속)", str_folders: "폴더", str_files: "파일", unit_folders: "개 폴더", unit_days: "일", unit_month: "개월", unit_sec: "초", str_no_files: "파일이 없습니다", str_items: "개 항목", col_name: "이름", col_size: "크기", col_dur: "유형/길이", col_duration_only: "길이", col_progress: "재생 진행도", col_play_time: "재생 시간", col_date: "수정 날짜", col_remaining: "남은 시간", col_path: "경로", col_old: "기존 이름", col_new: "새 이름", col_type: "유형", col_path_name: "경로 / 이름", col_action: "작업", lbl_folder_first: "폴더 상단 고정", tag_default: "기본", current_dir: "현재 디렉토리", str_same_folder: "(동일 폴더)", lbl_dont_show: "다시 보지 않기", lbl_dont_show_session: "이번 검사에서 다시 표시 안 함", str_empty_filename: "(파일 이름 없음)", str_empty_dir: "(빈 디렉토리)", btn_filter: "필터", title_file_filter: "파일 필터", cat_all: "전체", cat_video: "비디오", cat_audio: "오디오", cat_image: "이미지", cat_document: "문서", cat_software: "앱", cat_archive: "압축 파일", cat_torrent: "BT 토렌트", cat_other: "기타", btn_exit_filter: "필터 종료", /* --- 属性面板 --- */ ctx_property: "속성", title_property: "파일 속성", lbl_prop_name: "파일 이름", lbl_prop_size: "파일 크기", lbl_prop_count: "파일 개수", lbl_prop_ctime: "생성 시간", lbl_prop_mtime: "수정 시간", lbl_prop_source: "추가 출처", lbl_prop_link: "리소스 링크", lbl_prop_path: "파일 위치", str_prop_cloud: "클라우드 추가", str_prop_share: "공유 링크", str_prop_user: "사용자 업로드", str_prop_unknown: "알 수 없는 출처", fmt_prop_count: "파일 {f}개, 폴더 {d}개 포함", str_prop_offline: "오프라인 작업", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "홈", btn_nav_share: "내 공유", btn_nav_offline: "오프라인 전송", btn_nav_recent: "최근 항목", btn_nav_history: "재생 기록", btn_nav_starred: "즐겨찾기", btn_nav_trash: "휴지통", btn_nav_upload: "내 업로드", title_offline: "내 오프라인", trash_title: "휴지통", trash_notice: "휴지통의 파일은 최대 15일 동안 보관됩니다", history_notice: "스크립트 환경 내에서 발생한 재생 진행률만 기록됩니다", ctx_open: "열기", ctx_add_bl: "리소스 관리자에 추가", ctx_remove_bl: "리소스 관리자에서 제거", ctx_rename: "이름 바꾸기", ctx_copy: "복사", ctx_copy_name: "파일명 복사", ctx_copy_link: "링크 복사", ctx_cut: "이동", ctx_del: "삭제", ctx_down: "다운로드", ctx_star: "즐겨찾기 추가", ctx_unstar: "즐겨찾기 취소", ctx_locate: "폴더 위치 열기", ctx_share: "공유", /* --- 通用文件操作按钮 --- */ btn_down: "다운로드", tip_down: "다운로드 [Alt] + [D]", btn_aria2: "Aria2 전송", tip_aria2: "Aria2로 보내기 [Alt] + [A]", btn_refresh_short: "새로고침", tip_refresh: "새로고침 [F5]", btn_newfolder: "새 폴더", tip_newfolder: "새 폴더 만들기 [F8]", btn_del: "삭제", tip_del: "삭제 [Delete]", btn_deselect: "선택 해제", tip_deselect: "선택 해제 [Esc]", btn_invert: "선택 반전", btn_copy: "복사", tip_copy: "복사 [Ctrl] + [C]", btn_cut: "이동", tip_cut: "이동 [Ctrl] + [X]", btn_paste: "붙여넣기", tip_paste: "붙여넣기 [Ctrl] + [V]", btn_clear_history: "히스토리 삭제", tip_clear_history: "히스토리 삭제 [Delete]", btn_restore: "복원", tip_restore: "복원 [R]", btn_del_forever: "영구 삭제", tip_del_forever: "영구 삭제 [Delete]", btn_empty_trash: "휴지통 비우기", tip_empty_trash: "휴지통 비우기 [Shift] + [Delete]", btn_exit: "나가기", btn_close: "닫기", tip_close: "닫기 [Esc]", tip_theme: "테마 전환 [Alt] + [T]", tip_rotate: "회전 [R]", tip_mirror: "좌우 반전 [H]", tip_flip_v: "상하 반전 [V]", tip_maximize: "최대화 [M]", tip_minimize: "최소화 [M]", tip_full_screen: "전체 화면 [Enter]", btn_help: "도움말", tip_help: "도움말 [Alt] + [H]", btn_view_file: "파일 보기", btn_jump: "이동", btn_copy_text: "복사", btn_stop: "중지", tip_stop: "현재 작업 즉시 중지", btn_settings: "설정", btn_logout: "PikPak 로그아웃", msg_logout_confirm: "로그아웃 하시겠습니까?", tip_settings: "설정 및 기타 [Alt] + [S]", lbl_upload_to: "업로드 위치: ", msg_move_done: "이동 완료.", /* --- 离线、上传与云下载 --- */ btn_upload: "로컬 업로드", btn_up_file: "파일 업로드", btn_up_folder: "폴더 업로드", btn_cloud_download: "링크 저장", btn_up_pause: "작업 일시정지", tip_up_pause: "작업 일시정지 [Alt] + [P]", btn_up_start: "작업 시작", tip_up_start: "작업 시작 [Alt] + [G]", btn_up_del: "작업 삭제", tip_up_del: "작업 삭제 [Delete]", btn_up_clear_all: "목록 비우기", tip_up_clear_all: "모든 작업 삭제 [Shift] + [Delete]", btn_retry_task: "작업 재시도", tip_retry_task: "재시도 [R]", col_task_status: "작업 상태", col_task_progress: "오프라인 진행도", col_up_speed: "속도", col_up_status: "상태", lbl_task_run: "진행 중", lbl_task_fail: "실패", lbl_task_ok: "완료", lbl_up_run: "업로드 중", lbl_up_pause: "중단됨", lbl_up_downloading: "다운로드 중", lbl_up_done: "완료됨", tip_up_pause_desc: "수동 일시정지 및 오류가 발생한 작업 포함", title_cloud_task: "클라우드 다운로드 작업 생성", ph_cloud_links: "지원 링크 형식:\n- 마그넷(magnet) 등 각종 다운로드 링크\n- YouTube, X(Twitter), TikTok, Facebook 등 공유 링크\n줄바꿈을 통해 여러 개의 링크를 동시에 추가할 수 있습니다.", lbl_save_to: "파일 저장 위치:", lbl_default_folder: "기본 폴더", btn_via_torrent: "토렌트 파일로 생성", tip_cloud_save_path: "일반 클라우드 다운로드 파일은 My Pack 디렉토리에 저장되며, 다른 앱의 클라우드 다운로드 파일은 My [XYZ] 디렉토리에 저장됩니다([XYZ]는 앱 이름).", lbl_smart_fix: "차단 방지 마그넷 자동 복구 (해시 추출 / 텍스트 노이즈 제거)", title_save_method: "저장 방식", msg_save_snapshot_desc: "이 링크는 웹페이지 스냅샷으로만 저장할 수 있습니다.", tip_snapshot_details: "PikPak이 이 링크에서 미디어 파일을 직접 수집할 수 없습니다. 대신 웹페이지 스냅샷으로 저장하며, 최대한 전체 내용을 보존합니다.", btn_save_snapshot: "스냅샷 저장", btn_create_now: "지금 생성", btn_modify: "수정", str_snap_link_count_suffix: " 외 {n}개 링크", /* --- 分享管理 --- */ btn_cancel_share: "공유 취소", share_copy_suffix: "이 내용을 복사한 후 PikPak 앱을 열면 초고속 재생을 즐길 수 있습니다", share_copy_pwd: "비밀번호", title_share_detail: "공유 상세 정보", ctx_share_detail: "공유 상세 보기", ctx_share_copy: "링크 및 비번 복사", col_view: "조회", col_save: "저장", col_share_time: "공유 시간", col_share_status: "공유 상태", lbl_limit_reached: "횟수 초과", lbl_limit_tip: "제한 횟수", lbl_share_view: "조회", lbl_share_save: "저장", lbl_share_link_title: "공유 링크", lbl_share_pwd_title: "비밀번호", lbl_share_expire_title: "유효 기간", btn_copy_link_pwd: "링크 및 비밀번호 복사", str_expire_suffix: "일 후 만료", ph_edit_pwd: "공유 비밀번호 입력 (4-10자)", btn_close_pwd: "비밀번호 해제", str_no_pwd: "비밀번호 없음", title_edit_share_code: "공유 코드 수정", ph_edit_share_code: "5~18자 문자, 숫자, 기호 등 지원", btn_add_share_code: "공유 코드 추가", btn_del_share_code: "공유 코드 삭제", share_title: "파일 공유", share_mode: "공유 방식", share_public: "공개 링크", share_encrypted: "암호화 링크", share_expiry: "유효 기간 설정", share_pass: "추출 코드 설정", share_count: "추출 횟수 설정", share_count_ed: "추출 횟수", share_perm: "영구적", share_unlimit: "제한 없음", share_rand: "랜덤 생성", share_custom: "사용자 지정", share_days: "일", share_times: "회", btn_share_start: "지금 공유", cal_custom_title: "유효 기간 직접 선택", lbl_share_link: "링크", lbl_share_code: "추출 코드", btn_copy_share: "모두 복사", str_share_expired: "만료됨", str_share_deleted: "파일 삭제됨", title_edit_pwd: "비밀번호 변경", lbl_share_code_title: "공유 코드", ph_password: "비밀번호", ph_pass_range: "4-10자", cal_week_days:["일", "월", "화", "수", "목", "금", "토"], cal_months:["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "파일 분석", btn_scan: "파일 투시", lbl_scan_selected: "선택한 {n}개 항목에 대해 파일 투시를 실행합니다", lbl_keyword_filter: "키워드 제외", ph_keyword_filter: "제외할 키워드, 쉼표로 구분", lbl_scan_current: "현재 경로의 모든 항목에 대해 파일 투시를 실행합니다", tip_dup: "파일 중복 확인", lbl_dup_selected: "선택한 {n}개 항목에 대해 파일 중복 확인을 실행합니다", lbl_dup_current: "현재 경로의 모든 항목에 대해 파일 중복 확인을 실행합니다", tip_scan_dup: "파일 필터링 또는 중복 확인", lbl_dup_tool: "삭제 대상 선택:", lbl_dup_reset: "↺ 초기화 (고정 해제 및 선택 취소)", lbl_dup_select_folder: "📂 폴더별 선택", lbl_dup_select_folder_short: "📂 폴더", lbl_dup_invert: "반전 모드", lbl_dup_invert_short: "반전", tip_dup_invert_limit: "폴더별 선택 시에만 사용 가능", fmt_dup_count: "({n}개 중복)", btn_start_scan: "스캔 시작", tag_hash: "정밀 일치", tag_hash_short: "정밀", tag_name: "이름 유사", tag_name_short: "이름", tag_sim: "재생시간 유사", tag_sim_short: "재생시간", label_dup_video: "비디오 파일 (정밀 일치 + 재생시간 유사 + 이름 유사)", label_dup_image: "이미지 파일 (정밀 일치 + 이름 유사)", label_dup_other: "기타 파일 (정밀 일치 + 이름 유사)", btn_analyze: "폴더 분석", tip_analyze: "폴더 필터링 또는 중복 확인", btn_export: "디렉토리 내보내기", tip_export: "현재 경로의 파일 트리 목록 생성 및 다운로드", title_export_format: "내보내기 스타일", lbl_export_current: "현재 경로의 모든 항목에 대해 디렉토리 내보내기를 실행합니다", opt_tree_view: "트리 뷰", opt_list_view: "리스트 뷰", msg_exporting: "디렉토리 트리 생성 중...", str_analyze_results: "일치 결과", lbl_size_threshold: "감지 임계값", title_analyze_result: "폴더 분석 결과", opt_ana_large: "폴더 투시", lbl_analyze_selected: "선택한 {n}개 항목에 대해 폴더 투시를 실행합니다", lbl_analyze_current: "현재 경로의 모든 항목에 대해 폴더 투시를 실행합니다", opt_ana_sim: "폴더 중복 확인", lbl_ana_sim_selected: "선택한 {n}개 항목에 대해 폴더 중복 확인을 실행합니다", lbl_ana_sim_current: "현재 경로의 모든 항목에 대해 폴더 중복 확인을 실행합니다", title_algo_help: "알고리즘 설명", algo_help_content: "이름 일치: 이름과 크기가 유사한 폴더 그룹을 찾습니다.\n유사도 일치: 내부 파일이 고도로 중복되는 폴더 그룹을 찾습니다.\n포함율 일치: 작은 폴더의 내용이 큰 폴더에 완전히 포함된 부분 집합 중복을 찾습니다.\n\n정확도: 유사도 일치 > 포함율 일치 > 이름 일치\n범위: 포함율 일치 > 유사도 일치", lbl_threshold: "임계값", lbl_sim_score: "유사도", lbl_containment: "포함율", lbl_name_match: "이름 일치", lbl_sim_match: "유사도 일치", lbl_contain_match: "포함율 일치", lbl_ana_min: "최소", lbl_ana_max: "최대", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "빈 폴더 정리", tip_prune: "빈 폴더 정리 [Ctrl] + [Delete]", btn_rename: "이름 바꾸기", tip_rename: "이름 바꾸기 [F2]", btn_bulkrename: "일괄 이름 바꾸기", tip_bulkrename: "일괄 이름 바꾸기 [F2]", title_blacklist: "리소스 관리자", btn_blacklist_run: "정리 즉시 실행", btn_clear_list: "목록 비우기", tip_bl_desc: "아래 항목들은 일반 '삭제' 시 건너뛰며, '정리 즉시 실행'을 통해서만 삭제됩니다.", tip_blacklist_input: "리소스 관리자 [Alt] + [Delete]", label_bl_folder: "폴더 목록 (정밀 검색)", label_bl_file: "파일 목록 (정밀 검색)", lbl_type_folder: "폴더", lbl_type_file: "파일", ph_bl_folder: "'붙여넣기' 또는 우클릭 메뉴의 '리소스 관리자에 추가'를 통해 가져오세요.", ph_bl_file: "'붙여넣기' 또는 우클릭 메뉴의 '리소스 관리자에 추가'를 통해 가져오세요.", modal_bl_preview: "검색 결과", btn_bl_delete: "선택 항목 삭제", modal_rename_title: "이름 바꾸기", modal_rename_multi_title: "일괄 이름 바꾸기", btn_preview: "미리보기", modal_preview_title: "변경 사항 확인", label_pattern: "패턴 (예: 비디오 {n})", label_replace: "바꾸기/삭제", label_replace_note: "대소문자 구분", label_include_ext: "확장자 포함", label_regex: "정규식 (Regex)", placeholder_find: "찾을 내용", placeholder_replace: "바꿀 내용 (비워두면 삭제)", label_jav: "FC2 클린 네이밍", lbl_rn_pattern: "이름 템플릿", lbl_rn_case_convert: "대소문자 변환", opt_rn_keep_origin: "(변경 없음)", opt_rn_lower: "모두 소문자 (abc)", lbl_rn_mode_series: "에피소드 모드", lbl_rn_mode_format: "포맷팅", lbl_rn_mode_ad: "접두사 광고 제거", lbl_rn_mode_ext: "확장자 복구", opt_rn_upper: "모두 대문자 (ABC)", opt_rn_title: "첫 글자만 대문자 (Abc)", lbl_rn_width_convert: "전각/반각 변환", opt_rn_width_half: "전각을 반각으로 (A->A)", opt_rn_width_full: "반각을 전각으로 (A->A)", lbl_rn_preview_title: "변경 미리보기", tip_jav_mode_desc: "✨ FC2 스마트 추출 및 불필요한 문자 제거", tip_ad_remove_desc: "🧹 제목 앞부분의 광고, URL, 불필요한 기호를 제거하고 이모지 정리 및 괄호 형식 수정", tip_ext_fix_desc: "🧩 실제 파일 유형(MIME)에 기반하여 확장자를 지능적으로 수정", label_replace_find: "찾을 내용", label_replace_to: "바꿀 내용", /* --- 解压相关 --- */ btn_unzip: "일괄 압축 해제", tip_unzip: "일괄 압축 해제 [Alt] + [U]", btn_unzip_all: "전체 압축 해제", btn_understand_unzip: "확인 및 해제", title_input_pwd: "압축 해제 비밀번호 필요", lbl_pwd_prompt: "비밀번호를 입력하세요:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "외부 재생", tip_ext: "PotPlayer 재생 / 링크 가져오기 [Alt] + [E]", btn_img_search: "이미지로 검색 [F]", tip_play_search: "이미지로 검색 [F]", tip_pip: "PIP 모드 [P]", str_no_sub: "자막 없음", lbl_sub_sel: "자막 선택", lbl_show_sub: "자막 표시", btn_sub_search: "자막 검색", btn_sub_cloud: "클라우드", btn_sub_local: "로컬 자막", lbl_sub_pos: "자막 위치", lbl_sub_bottom: "하단", lbl_sub_top: "상단", lbl_sub_bg_op: "배경 투명도", lbl_sub_size: "자막 크기", lbl_sub_offset: "자막 싱크", title_sel_sub: "자막 선택", ph_sub_search: "키워드 입력 시 아래 링크가 자동 업데이트됩니다...", btn_force_play: "강제 재생 시도", str_compat_mode: "호환 모드", lang_code: "ko", btn_go_search: "🔍 {n}에서 직접 검색", btn_restart: "처음부터 재생", btn_prev_video: "이전 영상 [Ctrl + ←]", btn_next_video: "다음 영상 [Ctrl + →]", tip_plist_open: "목록 펼치기 [E]", tip_plist_close: "목록 접기 [E]", tab_sub: "자막", tab_size: "크기", tab_more: "기타", lbl_ratio: "비율", lbl_direction: "방향", opt_ratio_def: "기본", btn_rot_l: "좌측회전", btn_rot_r: "우측회전", btn_flip_h: "좌우반전", btn_flip_v: "상하반전", lbl_play_end: "재생 종료 시", opt_list_loop: "목록 반복", opt_single_loop: "한 곡 반복", opt_play_stop: "재생 후 중지", lbl_skip_op: "오프닝 건너뛰기", lbl_skip_ed: "엔딩 건너뛰기", export_link_title: "비디오 스트리밍 링크 내보내기", btn_start_play: "재생 시작", btn_copy_link: "링크 복사", tip_copy_link: "링크 복사 [Alt] + [C]", opt_player_other: "기타 (링크 내보내기)", lbl_player: "플레이어", btn_mark: "마크", lbl_resolution: "해상도", str_switch_compat: "현재 해상도를 사용할 수 없어 {n} 화질로 전환되었습니다", type_img: "이미지", type_doc: "문서", type_archive: "압축", type_sub: "자막", type_app: "앱", type_suffix: "파일", /* --- 设置与搜索 --- */ label_turbo_mode: "초고속 모드", desc_turbo_mode: "순정 UI 자동 대체 (권장)", lbl_aria2_status: "연결 상태", ph_aria2_secret: "보안 비밀 (선택 사항)", str_connected: "연결됨", str_conn_fail: "실패", str_connecting: "테스트 중...", tip_mixed_content: "상용 포트 참고:\n• 6800 (Aria2 기본)\n• 16800 (Motrix 기본)\n• 6881 (기타 통합판)", picker_title: "폴더 선택", picker_all: "모든 파일", picker_new: "새 폴더", picker_sort_new: "최신", picker_sort_old: "오래된", title_select_file: "파일 선택", placeholder_search: "파일 검색...", placeholder_search_short: "검색", title_search_hist: "검색 기록", btn_clear_hist: "비우기", lbl_global_search: "전체 검색", lbl_search_path: "경로 포함 검색", lbl_search_path_short: "경로", str_search_results: "검색 결과", modal_settings_title: "설정", label_lang: "언어 (Language)", label_thumb: "썸네일 흐리게 (프라이버시 모드)", label_keep_pos: "탐색 위치 기억 (뒤로 가기 시 위치 고정)", label_sort_pref: "정렬 방식 설정", opt_sort_indep: "폴더별 개별 설정", opt_sort_global: "전체 동일 적용", desc_sort_indep: "정렬 방식 변경이 현재 폴더에만 적용됩니다", desc_sort_global: "모든 폴더에 동일한 정렬 방식(날짜 역순)을 적용합니다", label_search_engine: "이미지 검색 엔진", opt_engine_google: "Google 렌즈 (종합)", opt_engine_yandex: "Yandex (종합)", opt_engine_saucenao: "SauceNAO (Pixiv/일러스트)", opt_engine_tracemoe: "trace.moe (애니메이션 스크린샷)", label_dup_strictness: "유사 일치 임계값", opt_strict: "엄격", opt_loose: "느슨하게", label_comic_mode: "미디어 모드", desc_comic_mode: "이미지/동영상 전용 폴더는 기본 A-Z 정렬", label_aria2_url: "Aria2 주소", label_aria2_token: "Aria2 RPC Token", label_privacy_mode: "프라이버시 모드", label_blur_cover: "커버 이미지 흐리게", label_dl_filter_ext: "다운로드 확장자 필터 (예: .txt, .jpg)", label_dl_filter_name: "다운로드 이름 필터 (키워드 또는 전체 이름)", lbl_dl_filter: "폴더 다운로드 필터링", desc_dl_filter: "폴더 다운로드/전송 시 매칭되는 파일을 자동으로 제외합니다", lbl_config_manage: "설정 관리", btn_export_data: "백업 내보내기", btn_import_data: "백업 가져오기", btn_clean_data: "로컬 데이터 지우기", title_clean_data: "정리할 항목 선택", msg_clean_confirm: "선택한 로컬 데이터를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", msg_clean_success: "로컬 데이터가 정리되었습니다. 페이지가 새로고침됩니다...", opt_cfg_index: "전체 인덱스 (동기화된 디렉토리 구조/파일 스냅샷)", opt_cfg_pref: "기본 설정 (UI 외형/작업 습관/정렬 방식)", opt_cfg_rules: "관리 규칙 (리소스 관리자/공유 횟수 제한/검색 기록/다운로드 규칙)", opt_cfg_vault: "비밀번호 금고 (압축 해제 비밀번호 기억)", opt_cfg_history: "비디오 캐시 (비디오 재생 진행도/비디오 길이 캐시)", opt_cfg_cache: "실행 캐시 (폴더 수정 시간/최근 탐색 위치/지문)", msg_import_confirm: "가져온 설정이 현재 설정과 병합됩니다(목록/기록 병합, 충돌하는 기본 설정 덮어쓰기). 계속하시겠습니까?", msg_import_success: "설정을 성공적으로 가져왔습니다. 페이지가 새로고침됩니다...", err_invalid_config: "잘못된 설정 파일: 지문 식별이 없거나 형식 오류입니다", err_json_format: "파일 분석 실패: JSON 구문 오류 또는 파일이 손상되었습니다", lbl_storage: "저장 공간", lbl_browse_exp: "탐색 경험", lbl_skip_bl_on_del: "삭제 시 관리 도구 기록 리소스 건너뛰기", lbl_pwd_manage: "압축 해제 암호 관리", title_pwd_vault: "비밀번호 금고", lbl_pwd_try_count: "압축 파일당 암호 매칭 시도 제한", tip_pwd_manual: "한 줄에 암호 하나씩, Enter로 줄바꿈", str_root_dir_cn: "루트 디렉토리", btn_ana_select: "일괄 선택", opt_keep_new: "최신 항목 유지", opt_keep_old: "오래된 항목 유지", opt_keep_large: "가장 큰 항목 유지", opt_keep_small: "가장 작은 항목 유지", opt_keep_short: "이름이 가장 짧은 항목 유지", opt_keep_long: "이름이 가장 긴 항목 유지", /* --- 状态、进度与加载短语 --- */ loading: "로딩 중...", loading_detail: "디렉토리 구조를 최고 속도로 인덱싱 중...", loading_fetch: "가져오는 중... ({n})", loading_dup: "중복 항목 분석 중... ({p}%)", str_loading_placeholder: "로딩 중...", str_load_failed: " (로딩 실패)", str_load_failed_simple: "로딩 실패", str_waiting_token: "로그인 상태 동기화 중...", str_speed: "속도", status_scanning_selection: "선택 항목 스캔 중... {n}", status_ready: "{n}개 항목", sel_count: "{n}개 항목 선택됨", str_cached: "캐시됨:", str_retries: "재시도:", str_failed: "실패:", str_success: "성공:", str_stopping: "중지 중...", str_merging: "데이터 병합 중...", str_rendering: "목록 렌더링 중...", str_scanning: "스캔 중...", str_analyzing: "분석 중...", str_deleting: "삭제 중...", str_saving: "저장 중...", str_saving_dots: "저장 중...", str_checking_bl: "명단 기록 매칭 중...", str_processing: "시스템이 최고 속도로 처리 중입니다...", str_cleanup_done: "정리 완료.", str_waiting_preload: "프리로드 대기 중...", str_copying: "클립보드에 복사 중...", str_moving: "이동 준비 중...", str_sorting: "정렬 중...", str_refreshing: "새로고침 중...", str_refreshing_cache: "캐시 새로고침 중...", str_syncing_stars: "즐겨찾기 상태 동기화 중...", str_updating_view: "뷰 업데이트 중...", str_generating_view: "뷰 생성 중...", str_group: "그룹", str_init_rename: "이름 바꾸기 초기화 중...", str_renaming: "이름 바꾸는 중...", str_calc_changes: "변경 사항 계산 중...", str_scanning_dir: "디렉토리 구조 스캔 중...", str_init_op: "작업 초기화 중...", str_init_scan: "전체 스캔 초기화 중...", str_rebuilding: "인덱스 재구성 중...", str_upload_1: "업로드 중 (노드 1/3)...", str_upload_2: "노드 1 타임아웃, 노드 2로 전환...", str_upload_3: "노드 2 타임아웃, 마지막 노드 시도...", str_upload_fail_copy: "업로드 실패, 클립보드 작성을 준비합니다...", msg_transcoding: "클라우드 인코딩 중...", msg_transcoding_wait: "서버에서 비디오를 처리 중입니다. 잠시만 기다려 주세요.", str_preparing: "압축 해제 준비 중...", str_unzipping: "압축 해제 중: {n}", str_unzipping_state: "압축 해제 중...", str_unzipping_prog_0: "압축 해제 중: 0%", str_unzipping_prog_100: "압축 해제 중: 100%", str_unzipping_prog_fmt: "압축 해제 중: {n}%", msg_task_waiting: "대기 중...", msg_task_hashing: "파일 체크섬 확인 중...", msg_task_init_upload: "업로드 초기화 중...", msg_task_uploading: "업로드 중...", msg_task_init_part: "파티션 초기화 중...", msg_task_uploading_2: "업로드 중...", str_loc_tracing: "경로 추적 중...", str_loc_stale: "캐시 만료됨, 클라우드와 동기화 중...", str_verifying: "확인 중...", str_server_indexing: "서버 인덱싱 중... ({n}/5)", str_creating_task_n: "작업 생성 중 ({n}/{t})...", msg_submit_request: "요청 제출 중... {c}/{t}", msg_wait_server: "서버 응답 대기 중... ({c}/{t})", msg_server_processing: "서버 처리 중... ({c}/{t})", str_jav_querying: "조회 중...", lbl_done_check: "✔ 완료", msg_limit_updated: "추출 횟수가 업데이트되었습니다", /* --- 提示、确认与交互消息 --- */ title_alert: "알림", title_confirm: "확인", title_prompt: "입력", btn_ok: "확인", btn_yes: "예", btn_no: "아니요", btn_save: "설정 저장", btn_cancel: "취소", btn_create: "생성", btn_skip: "건너뛰기", msg_down_success: "브라우저를 통해 {n}개의 파일 다운로드를 시작했습니다.", msg_batch_txt: "다운로드 목록(.txt)이 생성되었습니다.", msg_clear_history_done: "기록에서 제거되었습니다.", msg_skip_unzipped: "이미 압축 해제된 항목 {n}개를 건너뛰었습니다.", msg_unzip_skip_del_confirm: "이미 압축 해제된 {n}개의 압축 파일을 감지했습니다. 휴지통으로 이동하시겠습니까?", msg_cancel_share_confirm: "선택한 {n}개의 공유를 취소하시겠습니까?\n링크가 즉시 무효화됩니다.", msg_pwd_updating: "비밀번호 업데이트 중...", msg_pwd_updated: "비밀번호가 업데이트되었습니다", msg_exp_updated: "유효 기간이 업데이트되었습니다", msg_cancel_share_done: "{n}개의 공유가 취소되었습니다.", msg_drag_drop_hint: "파일을 여기에 끌어다 놓으세요", str_drag_files: " 외 {n}개 파일", msg_creating_share: "공유 링크 생성 중...", title_share_result: "공유 성공", msg_no_files: "항목이 없습니다.", msg_no_selection: "먼저 항목을 선택해 주세요.", warn_del: "선택한 {n}개 항목을 삭제하시겠습니까?", msg_clear_sel_confirm: "{n}개의 중복 파일이 선택되었습니다. 선택을 취소하시겠습니까?", str_bl_stat: "일치: {n}개 | 선택됨: {m}개", str_hits: "적중", msg_settings_saved: "설정이 저장되었습니다. 페이지가 새로고침됩니다.", msg_name_exists: "이미 존재하는 이름입니다: {n}", str_name_conflict: "(이름 중복 가능성)", msg_newfolder_prompt: "새 폴더 이름:", msg_rename_prompt: "새 이름 입력:", msg_copy_done: "복사되었습니다. 붙여넣을 위치를 선택해 주세요.", msg_cut_done: "이동 준비 완료. 붙여넣을 위치를 선택해 주세요.", msg_paste_empty: "붙여넣을 항목이 없습니다.", msg_copy_empty: "클립보드가 비어 있거나 텍스트 데이터가 없습니다.", msg_add_success: "{n}행의 데이터가 추가되었습니다.", msg_del_done: "선택한 행이 삭제되었습니다.", msg_del_select: "삭제할 행을 먼저 클릭하여 선택하세요!", msg_del_items_done: "{n}개 항목이 삭제되었습니다.", msg_copy_success: "복사 성공", str_redirecting: "Google 렌즈로 이동 중...", msg_manual_paste: "이미지 업로드 시간 초과. 스크린샷이 복사되었습니다. 새 창에서 {cmd}를 눌러주세요.", msg_starring: "즐겨찾기 추가 중...", msg_unstarring: "즐겨찾기 취소 중...", msg_star_added: "즐겨찾기에 추가되었습니다", msg_unstar_done: "즐겨찾기가 취소되었습니다", msg_empty_trash_confirm: "휴지통을 비우시겠습니까? 이 작업은 되돌릴 수 없습니다!", msg_trash_emptied: "휴지통을 비웠습니다.", msg_del_forever_confirm: "선택한 {n}개 항목을 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다!", msg_del_forever_done: "{n}개 항목이 영구적으로 삭제되었습니다.", msg_restore_done: "{n}개 항목이 성공적으로 복원되었습니다.", msg_auto_sub_load: "자막이 자동으로 로드되었습니다: {n}", msg_dl_sub: "자막 다운로드 중...", msg_transcode_done: "✅ 인코딩 완료, 재생을 시작합니다", msg_fallback_report: "⚠️ 원본 재생 불가(MPEG4/HEVC), 자동으로 {n} 화질로 전환되었습니다", tip_manual_sub: "팁: .srt 또는 .vtt 파일을 플레이어에 드래그하여 로드할 수 있습니다.", msg_sub_drop_load: "드래그를 통해 자막이 로드되었습니다: {n}", msg_resume_hint: "{t} 지점부터 이어서 재생합니다. 여기를 클릭하세요 ", msg_unzip_confirm_n: "현재 폴더에 {n}개의 파일을 압축 해제하시겠습니까?", msg_task_paused: "일시정지됨", msg_task_added: "{n}개의 업로드 작업이 추가되었습니다", msg_task_fast_success: "순식간에 전송 완료(초고속 업로드)", msg_task_upload_done: "업로드 완료", log_dirty_data: "잘못된 데이터 감지! 요청 모드가 현재 뷰와 일치하지 않아 작업을 중단했습니다.", msg_network_unstable: "네트워크가 불안정합니다. 자동으로 복구를 시도 중...", msg_skip_locked: "{n}개 항목이 잠겨 있음", msg_skip_self: "{n}개 항목이 현재 위치와 동일함", msg_skip_conflict: "{n}개 하위 경로가 사용 중임", msg_skip_invalid: "유효하지 않은 항목을 건너뛰었습니다: ", msg_creating_cloud_task: "클라우드 다운로드 작업 생성 중...", str_parsing_torrent: "토렌트 파일 분석 중...", err_torrent_no_info: "분석 실패: 유효한 정보를 찾을 수 없음", err_file_read: "파일 읽기 오류", msg_cloud_task_finish: "생성 완료: {s} 성공, {f} 실패", msg_cloud_task_success: "🎉 {n}개의 작업이 성공적으로 생성되었습니다", msg_prepare_restore: "복원 준비 중...", msg_smart_matching_n: "비밀번호 스마트 매칭 중 ({n}개)...", msg_system_busy_retry: "시스템이 바쁩니다. 재시도 중 ({n}/5)...", msg_unzip_running_bg: "[{n}] 클라우드에서 압축 해제 중입니다. 잠시 후 새로고침하여 확인하세요.", msg_share_code_updated: "공유 코드가 업데이트되었습니다", msg_retry_submitted: "{n}개의 작업을 다시 제출했습니다", msg_aria2_batch_fail_log: "\n\n실패 항목이 많아 전체 오류 목록(.txt)을 자동으로 내보냈습니다.", str_aria2_fetch_err: "(링크 가져오기 실패)", str_aria2_rpc_err: "(전송 실패)", str_aria2_aborted: "(취소됨)", str_aria2_fail_file_name: "Aria2_실패_목록", msg_op_blocked_moving: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 완료 후 시도해 주세요.", msg_op_blocked_analyzing: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 정확한 폴더 분석 데이터를 위해 현재 작업이 완료된 후 다시 시도해 주세요.", msg_op_blocked_exporting: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 정확한 디렉토리 내보내기를 위해 현재 작업이 완료된 후 다시 시도해 주세요.", msg_analyze_only_normal_dir: "폴더를 선택해 주세요", msg_analyze_no_large_folders: "설정된 임계값 범위 내의 폴더를 찾을 수 없습니다 ({s})", msg_analyze_summary_fmt: "용량이 {n}개인 {s}GB 이상의 폴더를 발견했습니다 (크기순 정렬)", msg_prune_blocked_moving: "⚠️ 작업 차단\n\n파일 이동 중에는 정리를 수행할 수 없습니다.", msg_global_index_blocked_moving: "⚠️ 작업 차단\n\n파일 이동 중에는 전체 검색 인덱스를 생성할 수 없습니다. 완료 후 검색해 주세요.", msg_resource_locked_download: "⚠️ 작업 차단\n\n선택 항목에 이동 중인 파일이 포함되어 있습니다. 이동 완료 후 다운로드해 주세요.", msg_resource_locked_aria2: "⚠️ 작업 차단\n\n선택 항목에 이동 중인 파일이 포함되어 있습니다. 완료 후 Aria2로 전송해 주세요.", msg_flatten_blocked_moving: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 현재 파일 분석을 실행하면 파일 목록이 누락되거나 중복될 수 있으니 잠시 후 다시 시도해 주세요.", err_task_conflict: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 중입니다. 완료 후 정리를 실행해 주세요.", title_del_task_confirm_fmt: "{n}개의 전송 작업을 삭제하시겠습니까?", lbl_del_cloud_files_too: "클라우드 내 실제 파일도 함께 삭제", msg_file_del_failed: "파일 삭제 실패: ", msg_task_del_success_fmt: "{n}개의 작업이 삭제되었습니다", title_clear_task_confirm: "모든 업로드 작업을 삭제하시겠습니까?", msg_task_clear_success_fmt: "{n}개의 업로드 작업이 삭제되었습니다", msg_unzip_virtual_view_warn: "현재 가상 뷰에서 작업 중입니다. 압축 해제된 파일은 각 압축 파일이 위치한 실제 폴더에 저장되며, 현재 목록에 즉시 나타나지 않을 수 있습니다.

계속하시겠습니까?", msg_smart_matching_file: "비밀번호 스마트 매칭 중... ({n})", msg_unzip_batch_submitted: "✅ {n}개의 압축 해제 완료", msg_unzip_batch_skipped: " ({n}개 건너뜀)", msg_unzip_check_source: ". 원본 폴더에서 결과를 확인하세요.", tip_jump_to_folder: "이 폴더로 이동", msg_task_deleted: "작업이 삭제되었습니다", msg_scan_done: "스캔 완료!\n총 {n}개의 파일 발견, {f}개의 폴더 탐색.", msg_scan_fail: "\n\n❌ {n}개 실패.", msg_scan_fix: "\n\n✅ {n}회의 네트워크 오류를 자동 복구했습니다.", msg_down_scanning: "폴더 내용 분석 중...", msg_down_progress: "브라우저 다운로드 호출 중...", msg_down_confirm_total: "✅ 스캔 완료, 총 {n}개의 파일을 찾았습니다.\n\n⚠️ 주의: 다량의 파일을 브라우저로 다운로드하면 페이지가 멈추거나 차단될 수 있습니다.\n10개 이상의 파일은 Aria2 내보내기를 권장합니다.\n\n브라우저 다운로드를 강행하시겠습니까?", msg_aria2_sending_batch: "🚀 Aria2로 작업을 분할 전송 중입니다...", msg_aria2_check_fail: "Aria2 연결 실패!\nURL 및 토큰을 확인해 주세요.", msg_aria2_check_ok: "Aria2 연결 성공!", msg_aria2_sent: "{n}개의 파일을 Aria2로 전송했습니다.", msg_aria2_test_fail: "Aria2 연결에 실패했습니다.\n설정을 그대로 저장하시겠습니까?", title_aria2_fail: "연결 테스트 실패", msg_batch_scanning: "🚀 디렉토리 구조 고속 스캔 중...", msg_batch_hydrating: "⚡ 다운로드 링크 병렬 추출 중...", msg_batch_no_files: "다운로드 가능한 파일을 찾을 수 없습니다.", msg_dup_warn: "중복 파일 검색을 시작하시겠습니까?", msg_dup_result: "{n}그룹의 중복 항목을 발견했습니다.", msg_dup_none: "중복 파일을 찾지 못했습니다.", msg_bl_stop: "작업이 중지되었습니다.", msg_bl_add_done: "{n}개의 항목이 기록에 추가되었습니다.", msg_bl_remove_done: "{n}개의 항목이 기록에서 제거되었습니다.", msg_bl_empty: "명단 목록이 비어 있어 실행할 수 없습니다.", msg_bl_clear_confirm: "모든 기록 항목을 지우시겠습니까? 이 작업은 복구할 수 없습니다.", msg_blacklist_run_none: "클라우드 내에 명단 조건에 맞는 항목이 없습니다.", msg_blacklist_run_confirm: "클라우드 내에서 {n}개의 기록된 항목을 발견했습니다.\n\n지금 휴지통으로 이동하시겠습니까?", msg_bl_run_limit: "⚠️ 모드 제한\n\n정리 작업은 실제 파일 재귀 작업이 필요합니다. 홈 폴더의 일반 폴더로 돌아가서 실행해 주세요.", msg_del_protected: "{n}개의 기록된 파일이 삭제되지 않도록 보호되었습니다.", msg_del_none: "삭제할 파일이 없습니다.", msg_bl_scanning: "전체 검색 중... \n스캔한 폴더: {d} | 일치: {f}", rn_tip_wait: "규칙을 설정해 주세요", rn_tip_jav: "위 버튼을 클릭하여 매칭을 시작하세요", rn_tip_none: "일치하는 항목이나 이름이 없습니다", rn_stat: "일치: {n}개 | 유효 변경: {m}개", rn_warn_confirm: "{n}개 파일의 이름을 바꾸시겠습니까?", msg_bulkrename_done: "{n}개 항목의 이름을 바꿨습니다.", msg_rn_all_skipped: "❌ 이름이 중복되어 모든 작업을 건너뛰었습니다.", msg_rn_fail_count: "이름 중복으로 {n}개 항목 건너뜀", msg_prune_confirm: "현재 목록에서 빈 폴더 검색을 시작하시겠습니까?", msg_prune_none: "빈 폴더가 없습니다.", msg_prune_found: "{n}개의 빈 폴더를 발견했습니다.\n지금 삭제하시겠습니까?", msg_deleting_folders: "{n}개의 폴더 삭제 중...", msg_global_warn: "전체 파일 동기화를 시작합니다.\n\n동기화된 파일은 로컬 메모리에 캐싱되며 페이지 새로고침 전까지 유지됩니다.\n\n계속하시겠습니까?", msg_init_scan_sel: "선택 항목 스캔 초기화 중...", warn_clear_history: "선택한 {n}개 항목을 재생 기록에서 제거하시겠습니까?\n(클라우드 파일은 삭제되지 않습니다)", msg_img_copy_hint: "새 창에서 {cmd}를 누르면 검색이 시작됩니다.", msg_aria2_not_set: "Aria2가 설정되지 않았습니다. 정보를 입력하고 계속하세요:", str_jav_no_match: "(품번 매칭 실패)", msg_unzip_fail: "압축 해제 요청 실패", msg_jszip_fail: "JSZip 로드 실패. 네트워크 상태를 확인하세요.", msg_turbo_activated: "터보 모드 활성화: 스크립트가 웹 로직을 제어하여 최상의 성능을 유지합니다.", msg_console_legal: "상업적 이용 엄격 금지: 이 프로젝트는 개인 학습 및 교류 목적으로만 사용됩니다.", msg_ana_warn: "폴더 중복 확인 팁: 알고리즘 추측을 바탕으로 합니다. 삭제 전 반드시 직접 확인하여 오삭제를 방지하세요。", /* --- 错误提示 --- */ err_invalid_links: "올바른 링크를 입력해 주세요", err_pwd_format: "비밀번호는 4-10자의 영문 또는 숫자여부야 합니다", err_invalid_torrent: "유효하지 않은 토렌트 파일 형식", err_torrent_complex: "구문 분석 복잡도가 너무 높음 (잘못된 파일 가능성)", err_torrent_format: "토렌트 구조가 손상됨", err_torrent_len: "필드 길이 분석 오류", err_torrent_char: "잘못된 문자 발견", err_share_code_exists: "이미 사용 중인 공유 코드입니다", err_folder_not_ready: "클라우드 폴더를 생성 중입니다. 잠시 후 다시 시도해 주세요", err_item_deleted: "항목이 존재하지 않습니다", err_network: "네트워크 오류", err_clipboard_denied: "클립보드 액세스가 거부되었습니다", err_worker: "워커 스레드 오류", err_api: "API 오류", err_capture: "캡처 실패.", err_captcha_simple: "인증 실패. 웹 리스트에서 수동으로 파일을 한 번 저장하여 인증을 완료해 주세요.", err_sub_dl_fail: "자막 다운로드 실패: ", err_req_blocked: "네트워크 요청 실패 (차단되었을 수 있음)", err_req_timeout: "요청 시간 초과", err_sub_drop_type: "자막 형식의 파일만 지원합니다", err_codec_t1: "비디오 코덱을 재생할 수 없습니다 ({c})", err_codec_t2: "브라우저에서 지원하지 않는 비디오 형식입니다.
아래 버튼을 눌러 외부 플레이어를 사용하세요.", err_pwd_simple: "비밀번호 오류", err_task_exists: "이미 존재하는 작업입니다", err_network_break: "이미지 노드 네트워크 단절. 다시 클릭하여 시도하세요.", err_no_failed_task: "실패한 작업이 선택되지 않았습니다", err_unknown: "알 수 없는 오류", err_invalid_regex: "유효하지 않은 정규식입니다", err_parent_not_found: "폴더를 찾을 수 없습니다", msg_sys_error: "시스템 폴더는 조작할 수 없습니다", msg_download_fail: "다운로드 링크를 가져올 수 없습니다.", msg_video_fail: "비디오 링크를 가져올 수 없습니다.", err_star_sync_fail: "즐겨찾기 동기화 실패", err_paste_descendant: "현재 폴더 또는 하위 폴더로 이동/복사할 수 없습니다", err_quota_exceeded: "저장 공간 부족", err_name_exists: "파일 이름이 중복될 수 없습니다", err_share_pass: "추출 코드는 4-10자여야 합니다", str_error: "오류", str_error_crit: "치명적 오류", str_error_paste: "붙여넣기 오류", str_action_failed: "작업 실패", str_scan_error: "스캔 오류", err_limit_too_low: "수정 실패: 새 횟수({n})는 현재 저장된 횟수({s})보다 커야 합니다", err_vault_max: "비밀번호 금고는 최대 50개의 자주 사용하는 비밀번호만 저장할 수 있습니다", err_pwd_len: "단일 비밀번호의 길이는 127자를 초과할 수 없습니다", /* --- 帮助文档 --- */ modal_help_title: "도움말", help_desc: `
✨ 경험 및 탐색 엔진
인터랙션 재구성: 인터페이스를 Windows 파일 탐색기 스타일로 재구성했습니다.
초고속 모드: 활성화 시 기본 웹 로직을 완전히 제어하여 대량 파일 환경에서의 렉과 충돌을 해결합니다.
고급 경로 표시줄: 마우스 휠 스크롤 및 드롭다운 메뉴 동급 전환을 지원합니다. 전체 검색 및 분석 도구가 통합되어 경로 복원 및 상위 이동을 지원합니다.
사용자 경험 향상: 즐겨찾기 등 다차원 정렬, 원클릭 커버 블러 처리 및 다크 테마 전환을 지원합니다. 백그라운드에서는 SWR 전략을 사용하여 무감각하게 뷰를 새로 고칩니다.
백그라운드 인덱싱 및 보호: 홈 아이콘의 파란색 점멸은 디렉토리 트리 동기화를 나타냅니다. 충돌하는 작업을 차단하고 데이터 손상을 방지하는 동시성 물리적 잠금을 제공합니다.
* 참고: 기본 폴더(My Pack)는 시스템 보호를 받아 삭제, 복사, 이동 및 이름 변경이 제한됩니다.
📂 일괄 처리 및 공간 관리
일괄 이름 변경: 정규식 치환/삭제, 에피소드 번호 생성, 텍스트 포맷팅, FC2 표준 네이밍, 광고 제거 및 MIME 기반 확장자 복구를 지원합니다.
분석 도구: 파일 분석(필터링 및 해시/시간/이름 3가지 모드 중복 검사)과 폴더 분석(필터링 및 이름/유사도/포함율 3가지 모드 중복 검사)을 통합 제공하며, 현재 경로의 디렉토리 트리 내보내기를 지원합니다.
스마트 정리: 원클릭 빈 폴더 정리; 일괄 압축 해제 기능은 암호 자동 기억 및 스마트 입력을 지원하며, 해제된 항목 건너뛰기 및 삭제를 지원합니다.
리소스 관리자: 파일 블랙리스트를 설정하여 스팸 리소스를 한 번에 정리하거나, 화이트리스트로 설정하여 일괄 삭제 시 해당 파일을 보호할 수 있습니다.
* 참고: 데이터 동기화 충돌을 방지하기 위해 처리 중에는 다른 클라이언트에서 파일을 수정하지 마십시오.
🌐 전송 및 공유 센터
공유 관리: 추출 횟수 상한 설정을 지원하며, 횟수 도달 시 링크가 자동으로 무효화됩니다.
초고속 업로드: 로컬 대용량 파일 및 폴더를 웹으로 드래그 앤 드롭하여 바로 업로드할 수 있으며, 공식 제한을 돌파하고 소용량 파일 전송 중단율을 대폭 낮췄습니다.
클라우드 다운로드 향상: 일괄 오프라인 링크 자동 중복 제거. 내장형 마그넷 스마트 정리 엔진(Base32/Hex 해시를 자동 추출하여 간섭 제거); .torrent 시드 파일 파싱 지원; 제한된 링크에 대한 웹 스냅샷 저장 기능 제공.
* 참고: 추출 횟수 차단은 웹페이지가 열려 있고 컴퓨터가 절전 모드가 아닐 때만 작동합니다.
🎬 몰입형 미디어 기능 향상
재생 엔진: 0.5x~3.0x 배속, 화면 회전/미러링, 강제 비율 조절, 오프닝/엔딩 건너뛰기 및 연속 재생/루프 모드를 지원하며 진행률 바 썸네일 미리보기를 지원합니다. 내장된 감시견(Watchdog)을 통해 블랙 스크린이나 지원되지 않는 코덱 발생 시 호환 화질로 자동 폴백합니다.
자막 시스템: 클라우드 내 동일 이름 자막, 로컬 파일 및 온라인 자막 검색을 지원합니다. 자막 싱크 미세 조정 및 로컬 텍스트 드래그 앤 드롭 파싱 마운트를 지원합니다.
시각 보조: 다중 엔진 기반 이미지로 검색 기능을 지원합니다. "미디어 모드" 활성화 시 시리즈/만화 폴더가 자동으로 A-Z 순서로 정렬됩니다.
* 참고: 재생 기록 목록은 스크립트 환경 내에서 발생한 재생 진행도만 지속적으로 기록합니다.
⚙️ 설정 및 데이터 관리
설정 백업: 개인 설정, 관리 규칙, 암호 금고 등을 디지털 지문이 포함된 JSON 파일로 내보낼 수 있으며, 가져오기 시 스마트 병합 및 중복 제거를 지원합니다.
데이터 정리: 전체 인덱스, 설정, 규칙, 암호 금고 및 캐시를 필요에 따라 지워 로컬 공간을 확보하고 개인 정보를 보호할 수 있습니다.
* 참고: 전체 인덱스는 웹페이지 종료 시 삭제되지만, 설정 및 비밀번호 금고 데이터는 영구 보관됩니다.
⚡ 다운로드 및 배포
외부 직접 연결: 클릭 한 번으로 비디오 스트리밍 직링크를 얻거나 PotPlayer 재생을 호출합니다. RPC 프로토콜을 통해 파일을 즉시 Aria2 노드로 전송할 수 있습니다.
배포 향상: 폴더를 Aria2로 푸시할 때 클라우드 트리 디렉토리 구조를 자동 복원합니다. 장기 연결 모니터링을 지원하며 오류 발생 시 오류 목록을 자동 내보냅니다. 폴더 다운로드 필터링 설정을 지원합니다.
이 프로젝트는 CC-BY-NC-SA-4.0 라이선스를 엄격히 준수하며, 상업적 이용을 금지합니다.
` }, ja: { /* --- 通用与基础UI --- */ title: "PikPak 拡張マスター", str_original: "原画", str_original_fast: "原画 (高速)", str_folders: "フォルダ", str_files: "ファイル", unit_folders: "個のフォルダ", unit_days: "日", unit_month: "ヶ月", unit_sec: "秒", str_no_files: "ファイルがありません", str_items: "項目", col_name: "名前", col_size: "サイズ", col_dur: "種別/長さ", col_duration_only: "長さ", col_progress: "再生進捗", col_play_time: "再生時間", col_date: "更新日時", col_remaining: "残り時間", col_path: "パス", col_old: "元の名前", col_new: "新しい名前", col_type: "種類", col_path_name: "パス / 名前", col_action: "操作", lbl_folder_first: "フォルダを先頭に表示", tag_default: "既定", current_dir: "現在のディレクトリ", str_same_folder: "(同一フォルダ内)", lbl_dont_show: "次回から表示しない", lbl_dont_show_session: "このセッションで表示しない", str_empty_filename: "(空のファイル名)", str_empty_dir: "(空のディレクトリ)", btn_filter: "フィルタ", title_file_filter: "ファイルフィルタ", cat_all: "すべて", cat_video: "動画", cat_audio: "音声", cat_image: "画像", cat_document: "文書", cat_software: "ソフト", cat_archive: "圧縮画庫", cat_torrent: "BT種子", cat_other: "その他", btn_exit_filter: "フィルタ解除", /* --- 属性面板 --- */ ctx_property: "プロパティ", title_property: "ファイルプロパティ", lbl_prop_name: "ファイル名", lbl_prop_size: "容量", lbl_prop_count: "ファイル数", lbl_prop_ctime: "作成日時", lbl_prop_mtime: "更新日時", lbl_prop_source: "追加ソース", lbl_prop_link: "リソースリンク", lbl_prop_path: "場所", str_prop_cloud: "クラウド追加", str_prop_share: "共有から", str_prop_user: "ユーザーアップロード", str_prop_unknown: "不明なソース", fmt_prop_count: "{f} 個のファイル、{d} 個のフォルダ", str_prop_offline: "オフラインタスク", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "ホーム", btn_nav_share: "マイ共有", btn_nav_offline: "リンク追加", btn_nav_recent: "最近の追加", btn_nav_history: "再生履歴", btn_nav_starred: "スター付き", btn_nav_trash: "ゴミ箱", btn_nav_upload: "ローカル", title_offline: "マイオフライン", trash_title: "ゴミ箱", trash_notice: "ゴミ箱のファイルは最大15日間保存されます", history_notice: "スクリプト環境内で発生した再生進捗のみを記録します", ctx_open: "開く", ctx_add_bl: "リソース管理に追加", ctx_remove_bl: "リソース管理から削除", ctx_rename: "名前の変更", ctx_copy: "コピー", ctx_copy_name: "名前をコピー", ctx_copy_link: "リンクをコピー", ctx_cut: "移動", ctx_del: "削除", ctx_down: "ダウンロード", ctx_star: "スターを追加", ctx_unstar: "スターを外す", ctx_locate: "ファイルの場所を開く", ctx_share: "共有", /* --- 通用文件操作按钮 --- */ btn_down: "ダウンロード", tip_down: "ダウンロード [Alt] + [D]", btn_aria2: "Aria2 へ送信", tip_aria2: "Aria2 へ送信 [Alt] + [A]", btn_refresh_short: "更新", tip_refresh: "更新 [F5]", btn_newfolder: "新規フォルダ", tip_newfolder: "新規フォルダ [F8]", btn_del: "削除", tip_del: "削除 [Delete]", btn_deselect: "選択解除", tip_deselect: "選択解除 [Esc]", btn_invert: "選択反転", btn_copy: "コピー", tip_copy: "コピー [Ctrl] + [C]", btn_cut: "移動", tip_cut: "移動 [Ctrl] + [X]", btn_paste: "貼り付け", tip_paste: "貼り付け [Ctrl] + [V]", btn_clear_history: "履歴を削除", tip_clear_history: "履歴を削除 [Delete]", btn_restore: "元に戻す", tip_restore: "元に戻す [R]", btn_del_forever: "完全に削除", tip_del_forever: "完全に削除 [Delete]", btn_empty_trash: "ゴミ箱を空にする", tip_empty_trash: "ゴミ箱を空にする [Shift] + [Delete]", btn_exit: "終了", btn_close: "閉じる", tip_close: "閉じる [Esc]", tip_theme: "テーマ切替 [Alt] + [T]", tip_rotate: "回転 [R]", tip_mirror: "左右反転 [H]", tip_flip_v: "上下反転 [V]", tip_maximize: "最大化 [M]", tip_minimize: "最小化 [M]", tip_full_screen: "全画面 [Enter]", btn_help: "ヘルプ", tip_help: "ヘルプ [Alt] + [H]", btn_view_file: "ファイルを表示", btn_jump: "ジャンプ", btn_copy_text: "コピー", btn_stop: "停止", tip_stop: "現在の操作を停止", btn_settings: "設定", btn_logout: "サインアウト", msg_logout_confirm: "ログアウトしますか?", tip_settings: "設定とその他 [Alt] + [S]", lbl_upload_to: "アップロード先: ", msg_move_done: "移動が完了しました。", /* --- 离线、上传与云下载 --- */ btn_upload: "アップロード", btn_up_file: "ファイル選択", btn_up_folder: "フォルダ選択", btn_cloud_download: "クラウドDL", btn_up_pause: "一時停止", tip_up_pause: "一時停止 [Alt] + [P]", btn_up_start: "再開", tip_up_start: "再開 [Alt] + [G]", btn_up_del: "タスク削除", tip_up_del: "タスク削除 [Delete]", btn_up_clear_all: "タスクをクリア", tip_up_clear_all: "タスクをクリア [Shift] + [Delete]", btn_retry_task: "リトライ", tip_retry_task: "リトライ [R]", col_task_status: "タスク状態", col_task_progress: "転送進捗", col_up_speed: "速度", col_up_status: "状態", lbl_task_run: "実行中", lbl_task_fail: "失敗", lbl_task_ok: "完了", lbl_up_run: "実行中", lbl_up_pause: "中断", lbl_up_downloading: "ダウンロード中", lbl_up_done: "完了", tip_up_pause_desc: "手動停止およびエラーが発生したタスク", title_cloud_task: "クラウドタスク作成", ph_cloud_links: "対応リンク形式:\n- magnetなどの各種ダウンロードリンク\n- YouTube、X (Twitter)、TikTok、Facebookなどの共有リンク\n改行で複数リンクの一括追加が可能です。", lbl_save_to: "保存先:", lbl_default_folder: "デフォルトフォルダ", btn_via_torrent: "Torrent経由", tip_cloud_save_path: "通常のクラウドダウンロードファイルは My Pack ディレクトリに保存されます。他アプリからのクラウドダウンロードファイルは My [XYZ] ディレクトリ([XYZ] はアプリ名)に保存されます。", lbl_smart_fix: "規制回避磁気リンクの自動修復 (ハッシュ抽出 / 文字ノイズ削除)", title_save_method: "保存方法", msg_save_snapshot_desc: "このリンクはウェブスナップショットとしてのみ保存可能です。", tip_snapshot_details: "PikPakはこのリンクからメディアファイルを直接抽出できませんが、ウェブページの内容をスナップショットとして保存できます。", btn_save_snapshot: "スナップショットを保存", btn_create_now: "作成", btn_modify: "変更", str_snap_link_count_suffix: " 他 {n} 件のリンク", /* --- 分享管理 --- */ btn_cancel_share: "共有を解除", share_copy_suffix: "この内容をコピーして PikPak アプリを開くと、高速再生を楽しめます", share_copy_pwd: "パスワード", title_share_detail: "共有の詳細", ctx_share_detail: "共有の詳細を表示", ctx_share_copy: "リンクとパスワードをコピー", col_view: "閲覧数", col_save: "保存数", col_share_time: "共有日時", col_share_status: "共有状態", lbl_limit_reached: "制限到達", lbl_limit_tip: "回数制限", lbl_share_view: "閲覧", lbl_share_save: "保存", lbl_share_link_title: "共有リンク", lbl_share_pwd_title: "パスワード", lbl_share_expire_title: "有効期限", btn_copy_link_pwd: "リンクとパスワードをコピー", str_expire_suffix: "日で失効", ph_edit_pwd: "パスワードを入力 (4-10桁)", btn_close_pwd: "パスワードを無効化", str_no_pwd: "パスワードなし", title_edit_share_code: "共有コードの変更", ph_edit_share_code: "5-18桁の英数字・記号に対応", btn_add_share_code: "共有コードを追加", btn_del_share_code: "共有コードを削除", share_title: "ファイルを共有", share_mode: "共有方法", share_public: "公開リンク", share_encrypted: "非公開リンク", share_expiry: "有効期限を設定", share_pass: "抽出コードを設定", share_count: "抽出回数を設定", share_count_ed: "抽出回数", share_perm: "無期限", share_unlimit: "無制限", share_rand: "ランダム生成", share_custom: "カスタム", share_days: "日", share_times: "回", btn_share_start: "共有を開始", cal_custom_title: "有効期限の選択", lbl_share_link: "リンク", lbl_share_code: "コード", btn_copy_share: "すべてコピー", str_share_expired: "期限切れ", str_share_deleted: "ファイル削除済み", title_edit_pwd: "パスワード変更", lbl_share_code_title: "共有コード", ph_password: "パスワード", ph_pass_range: "4-10文字", cal_week_days: ["日", "月", "火", "水", "木", "金", "土"], cal_months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "ファイル分析", btn_scan: "ファイル透視", lbl_scan_selected: "選択した {n} 項目にファイル透視を実行します", lbl_keyword_filter: "除外キーワード", ph_keyword_filter: "除外するキーワード、カンマ区切り", lbl_scan_current: "現在のパスのすべての項目にファイル透視を実行します", tip_dup: "重複チェック", lbl_dup_selected: "選択した {n} 項目にファイル重複チェックを実行します", lbl_dup_current: "現在のパスのすべての項目にファイル重複チェックを実行します", tip_scan_dup: "ファイルの絞り込みまたは重複チェック", lbl_dup_tool: "削除対象の選択:", lbl_dup_reset: "↺ リセット (固定解除 & 選択クリア)", lbl_dup_select_folder: "📂 フォルダごとに選択", lbl_dup_select_folder_short: "📂 フォルダ", lbl_dup_invert: "反転モード", lbl_dup_invert_short: "反転", tip_dup_invert_limit: "「フォルダごとに選択」時のみ有効", fmt_dup_count: "({n}個の重複)", btn_start_scan: "スキャン開始", tag_hash: "完全一致", tag_hash_short: "完全", tag_name: "名前類似", tag_name_short: "名前", tag_sim: "長さ類似", tag_sim_short: "長さ", label_dup_video: "動画ファイル (完全一致 + 長さ類似 + 名前類似)", label_dup_image: "画像ファイル (完全一致 + 名前類似)", label_dup_other: "その他のファイル (完全一致 + 名前類似)", btn_analyze: "フォルダ分析", tip_analyze: "フォルダのフィルタまたは重複チェック", btn_export: "ディレクトリ出力", tip_export: "現在のパスのファイルツリーを生成してダウンロード", title_export_format: "エクスポート形式", lbl_export_current: "現在のパスのすべての項目にディレクトリ出力を実行します", opt_tree_view: "ツリービュー", opt_list_view: "リストビュー", msg_exporting: "ディレクトリツリーを作成中...", str_analyze_results: "分析結果", lbl_size_threshold: "検出しきい値", title_analyze_result: "フォルダ分析結果", opt_ana_large: "フォルダ透視", lbl_analyze_selected: "選択した {n} 項目にフォルダ透視を実行します", lbl_analyze_current: "現在のパスのすべての項目にフォルダ透視を実行します", opt_ana_sim: "フォルダ重複チェック", lbl_ana_sim_selected: "選択した {n} 項目にフォルダ重複チェックを実行します", lbl_ana_sim_current: "現在のパスのすべての項目にフォルダ重複チェックを実行します", title_algo_help: "アルゴリズム解説", algo_help_content: "名前一致:名称と容量が近いフォルダ群を検索します。\n類似度一致:内部ファイルが高度に重複するフォルダ群を検索します。\n包含率一致:小フォルダの内容が大フォルダに完全に含まれているサブセット冗余を検索します。\n\n精度:類似度一致 > 包含率一致 > 名前一致\n範囲:包含率一致 > 類似度一致", lbl_threshold: "しきい値", lbl_sim_score: "類似度", lbl_containment: "包含率", lbl_name_match: "名前一致", lbl_sim_match: "類似度一致", lbl_contain_match: "包含率一致", lbl_ana_min: "下限", lbl_ana_max: "上限", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "空フォルダ削除", tip_prune: "空フォルダ削除 [Ctrl] + [Delete]", btn_rename: "名前の変更", tip_rename: "名前の変更 [F2]", btn_bulkrename: "一括リネーム", tip_bulkrename: "一括リネーム [F2]", title_blacklist: "リソース管理", btn_blacklist_run: "今すぐクリーンアップ", btn_clear_list: "リストをクリア", tip_bl_desc: "以下の項目は通常の削除ではスキップされ、「今すぐクリーンアップ」でのみ削除されます", tip_blacklist_input: "リソース管理 [Alt] + [Delete]", label_bl_folder: "フォルダリスト (完全一致)", label_bl_file: "ファイルリスト (完全一致)", lbl_type_folder: "フォルダ", lbl_type_file: "ファイル", ph_bl_folder: "ペーストするか、右クリックで追加", ph_bl_file: "ペーストするか、右クリックで追加", modal_bl_preview: "検索結果", btn_bl_delete: "選択項目を削除", modal_rename_title: "リネーム", modal_rename_multi_title: "一括リネーム", btn_preview: "プレビュー", modal_preview_title: "変更の確認", label_pattern: "パターン (例: Video {n})", label_replace: "置換 / 削除", label_replace_note: "大文字・小文字を区別", label_include_ext: "拡張子を含める", label_regex: "正規表現 (Regex)", placeholder_find: "検索する文字列", placeholder_replace: "置換後の文字列 (空で削除)", label_jav: "FC2 クリーン命名", lbl_rn_pattern: "命名テンプレート", lbl_rn_case_convert: "大文字・小文字変換", opt_rn_keep_origin: "(そのまま)", opt_rn_lower: "すべて小写 (abc)", lbl_rn_mode_series: "シリーズモード", lbl_rn_mode_format: "フォーマット化", lbl_rn_mode_ad: "広告タグ削除", lbl_rn_mode_ext: "拡張子修復", opt_rn_upper: "すべて大写 (ABC)", opt_rn_title: "先頭のみ大写 (Abc)", lbl_rn_width_convert: "全角・半角変換", opt_rn_width_half: "全角から半角 (A->A)", opt_rn_width_full: "半角から全角 (A->A)", lbl_rn_preview_title: "変更プレビュー", tip_jav_mode_desc: "✨ FC2をスマートに抽出し、無関係な文字を削除", tip_ad_remove_desc: "🧹 プレフィックス広告、URL、ゴミ記号を自動除去し、絵文字や括弧の書式を修正", tip_ext_fix_desc: "🧩 ファイルの真のタイプ (MIME) に基づいて拡張子を自動修正", label_replace_find: "検索内容", label_replace_to: "置換先", /* --- 解压相关 --- */ btn_unzip: "一括解凍", tip_unzip: "一括解凍 [Alt] + [U]", btn_unzip_all: "すべて解凍", btn_understand_unzip: "理解して解凍", title_input_pwd: "解凍パスワードが必要", lbl_pwd_prompt: "パスワードを入力してください:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "外部プレーヤーで再生", tip_ext: "PotPlayerで再生 / リンク取得 [Alt] + [E]", btn_img_search: "画像で検索 [F]", tip_play_search: "画像で検索 [F]", tip_pip: "PiP [P]", str_no_sub: "字幕なし", lbl_sub_sel: "字幕選択", lbl_show_sub: "字幕を表示", btn_sub_search: "オンライン", btn_sub_cloud: "クラウド", btn_sub_local: "ローカル", lbl_sub_pos: "字幕の位置", lbl_sub_bottom: "下部", lbl_sub_top: "上部", lbl_sub_bg_op: "背景の透過", lbl_sub_size: "字幕のサイズ", lbl_sub_offset: "字幕のズレ調整", title_sel_sub: "字幕の選択", ph_sub_search: "キーワードを入力すると、下のリンクが自動更新されます...", btn_force_play: "強制再生を試行", str_compat_mode: "互換モード", lang_code: "ja", btn_go_search: "🔍 {n} で手動検索", btn_restart: "最初から再生", btn_prev_video: "前へ [Ctrl + ←]", btn_next_video: "次へ [Ctrl + →]", tip_plist_open: "リスト表示 [E]", tip_plist_close: "リストを閉じる [E]", tab_sub: "字幕", tab_size: "サイズ", tab_more: "詳細", lbl_ratio: "比率", lbl_direction: "方向", opt_ratio_def: "デフォルト", btn_rot_l: "左に回転", btn_rot_r: "右に回転", btn_flip_h: "左右反転", btn_flip_v: "上下反転", lbl_play_end: "再生終了時", opt_list_loop: "リストをループ", opt_single_loop: "1曲ループ", opt_play_stop: "停止", lbl_skip_op: "OPをスキップ", lbl_skip_ed: "EDをスキップ", export_link_title: "ストリーミングリンクをエクスポート", btn_start_play: "再生開始", btn_copy_link: "リンクをコピー", tip_copy_link: "リンクをコピー [Alt] + [C]", opt_player_other: "その他 (リンク出力)", lbl_player: "プレーヤー", btn_mark: "マーク", lbl_resolution: "画質", str_switch_compat: "現在の画質は利用できません。{n} に戻しました", type_img: "画像", type_doc: "文書", type_archive: "圧縮", type_sub: "字幕", type_app: "アプリ", type_suffix: "ファイル", /* --- 设置与搜索 --- */ label_turbo_mode: "極速モード", desc_turbo_mode: "標準UIを自動代替 (推奨)", lbl_aria2_status: "接続状態", ph_aria2_secret: "シークレット (任意)", str_connected: "接続完了", str_conn_fail: "接続失敗", str_connecting: "テスト中...", tip_mixed_content: "一般的なポート:\n• 6800 (Aria2 標準)\n• 16800 (Motrix 既定)\n• 6881 (その他)", picker_title: "フォルダの選択", picker_all: "すべてのファイル", picker_new: "新規フォルダ", picker_sort_new: "新しい", picker_sort_old: "古い", title_select_file: "ファイルの選択", placeholder_search: "ファイルを検索...", placeholder_search_short: "検索", title_search_hist: "検索履歴", btn_clear_hist: "クリア", lbl_global_search: "全ドライブ検索", lbl_search_path: "検索パスを含める", lbl_search_path_short: "パス", str_search_results: "検索結果", modal_settings_title: "設定", label_lang: "言語 (Language)", label_thumb: "サムネイルをぼかす (プライバシーモード)", label_keep_pos: "閲覧位置を記憶 (戻った時に復元)", label_sort_pref: "ソート設定", opt_sort_indep: "フォルダごとに独立", opt_sort_global: "すべて共通", desc_sort_indep: "現在のフォルダのみ適用", desc_sort_global: "全フォルダ共通の並び順 (新しい順)", label_search_engine: "画像検索エンジン", opt_engine_google: "Google レンズ (総合)", opt_engine_yandex: "Yandex (総合)", opt_engine_saucenao: "SauceNAO (イラスト/Pixiv)", opt_engine_tracemoe: "trace.moe (アニメ)", label_dup_strictness: "類似一致のしきい値", opt_strict: "厳密", opt_loose: "緩め", label_comic_mode: "メディアモード", desc_comic_mode: "画像/動画のみのフォルダはデフォルトA-Z順", label_aria2_url: "Aria2 RPC URL", label_aria2_token: "Aria2 RPC Token", label_privacy_mode: "プライバシーモード", label_blur_cover: "カバー画像をぼかす", label_dl_filter_ext: "ダウンロード拡張子フィルタ (例: .txt, .jpg)", label_dl_filter_name: "ダウンロード名前フィルタ (キーワードまたはフルネーム)", lbl_dl_filter: "フォルダのダウンロードフィルタ", desc_dl_filter: "転送時に一致するファイルを自動除外", lbl_config_manage: "設定管理", btn_export_data: "データ出力", btn_import_data: "データ読込", btn_clean_data: "ローカルデータをクリア", title_clean_data: "クリアする項目を選択", msg_clean_confirm: "選択したローカルデータを完全に削除しますか?この操作は元に戻せません。", msg_clean_success: "ローカルデータをクリアしました。ページをリロードします...", opt_cfg_index: "全検索インデックス (同期済みディレクトリ構成/ファイルスナップショット)", opt_cfg_pref: "基本設定 (UI 外観/操作習慣/ソート順)", opt_cfg_rules: "管理ルール (リソース管理/共有回数制限/検索履歴/ダウンロードルール)", opt_cfg_vault: "パスワード庫 (解凍パスワード記憶)", opt_cfg_history: "動画キャッシュ (再生進捗/動画の長さキャッシュ)", opt_cfg_cache: "実行キャッシュ (フォルダ更新時刻/閲覧位置/指紋)", msg_import_confirm: "インポートされた設定は現在の設定とマージされます(リスト/記録は統合され、競合する基本設定は上書きされます)。続行しますか?", msg_import_success: "設定のインポートに成功しました。ページをリロードします...", err_invalid_config: "無効な設定ファイル:指紋識別がないか、フォーマットエラーです", err_json_format: "ファイルの解析に失敗:JSON 構文エラーまたはファイルが破損しています", lbl_storage: "ストレージ", lbl_browse_exp: "閲覧体験", lbl_skip_bl_on_del: "削除時に記録済みリソースをスキップ", lbl_pwd_manage: "解凍パスワード管理", title_pwd_vault: "パスワード庫", lbl_pwd_try_count: "パスワード試行上限", tip_pwd_manual: "1行に1つのパスワード、Enterで改行", str_root_dir_cn: "ルート", btn_ana_select: "一括選択", opt_keep_new: "最新を保持", opt_keep_old: "最古を保持", opt_keep_large: "最大を保持", opt_keep_small: "最小を保持", opt_keep_short: "名前が最短のものを保持", opt_keep_long: "名前が最長のものを保持", /* --- 状态、进度与加载短语 --- */ loading: "読み込み中...", loading_detail: "ディレクトリ構造を全速でインデックス中...", loading_fetch: "取得中... ({n})", loading_dup: "重複を分析中... ({p}%)", str_loading_placeholder: "読み込み中...", str_load_failed: " (読み込み失敗)", str_load_failed_simple: "読み込み失敗", str_waiting_token: "ログイン状態を同期中...", str_speed: "速度", status_scanning_selection: "選択範囲をスキャン中... {n}", status_ready: "{n} 個の項目", sel_count: "{n} 個の項目を選択中", str_cached: "キャッシュ済み:", str_retries: "リトライ:", str_failed: "失敗:", str_success: "成功:", str_stopping: "停止中...", str_merging: "データを統合中...", str_rendering: "リストをレンダリング中...", str_scanning: "スキャン中...", str_analyzing: "分析中...", str_deleting: "削除中...", str_saving: "保存中...", str_saving_dots: "保存中...", str_checking_bl: "リスト照合中...", str_processing: "システムが全速で処理しています...", str_cleanup_done: "クリーンアップ完了。", str_waiting_preload: "プリロード待機中...", str_copying: "クリップボードにコピー中...", str_moving: "移動の準備中...", str_sorting: "ソート中...", str_refreshing: "更新中...", str_refreshing_cache: "キャッシュを更新中...", str_syncing_stars: "スター状態を同期中...", str_updating_view: "表示を更新中...", str_generating_view: "ビューを生成中...", str_group: "グループ", str_init_rename: "リネームを初期化中...", str_renaming: "リネーム中...", str_calc_changes: "変更を計算中...", str_scanning_dir: "ディレクトリ構造をスキャン中...", str_init_op: "操作を初期化中...", str_init_scan: "全ドライブスキャンを初期化中...", str_rebuilding: "インデックスを再構築中...", str_upload_1: "アップロード中 (ノード 1/3)...", str_upload_2: "ノード1タイムアウト、ノード2へ切替...", str_upload_3: "ノード2タイムアウト、最終ノードを試行...", str_upload_fail_copy: "アップロード失敗、クリップボードへ書き込み中...", msg_transcoding: "クラウドでトランスコード中...", msg_transcoding_wait: "サーバーで処理中です。少々お待ちください", str_preparing: "解凍準備中...", str_unzipping: "解凍中: {n}", str_unzipping_state: "解凍中...", str_unzipping_prog_0: "解凍中: 0%", str_unzipping_prog_100: "解凍中: 100%", str_unzipping_prog_fmt: "解凍中: {n}%", msg_task_waiting: "待機中...", msg_task_hashing: "ファイルを検証中...", msg_task_init_upload: "アップロード初期化中...", msg_task_uploading: "アップロード中...", msg_task_init_part: "分割処理の初期化中...", msg_task_uploading_2: "アップロード中...", str_loc_tracing: "パスを追跡中...", str_loc_stale: "キャッシュが古いため、クラウドと同期中...", str_verifying: "検証中...", str_server_indexing: "サーバーでインデックス作成中... ({n}/5)", str_creating_task_n: "タスク作成中 ({n}/{t})...", msg_submit_request: "リクエスト送信中... {c}/{t}", msg_wait_server: "サーバーの応答待機中... ({c}/{t})", msg_server_processing: "サーバー処理中... ({c}/{t})", str_jav_querying: "照会中...", lbl_done_check: "✔ 完了", msg_limit_updated: "抽出回数が更新されました", /* --- 提示、确认与交互消息 --- */ title_alert: "ヒント", title_confirm: "確認", title_prompt: "入力", btn_ok: "OK", btn_yes: "はい", btn_no: "いいえ", btn_save: "設定を保存", btn_cancel: "キャンセル", btn_create: "作成", btn_skip: "スキップ", msg_down_success: "ブラウザで {n} 個のファイルのダウンロードを開始しました。", msg_batch_txt: "ダウンロードリスト (.txt) を作成しました。", msg_clear_history_done: "履歴から削除しました", msg_skip_unzipped: "解凍済みの {n} 個の項目をスキップしました。", msg_unzip_skip_del_confirm: "解凍済みの {n} 個の圧縮ファイルを検出しました。ゴミ箱に移動しますか?", msg_cancel_share_confirm: "選択した {n} 個の共有を解除しますか?\nリンクは即座に無効になります。", msg_pwd_updating: "パスワードを更新中...", msg_pwd_updated: "パスワードを更新しました", msg_exp_updated: "有効期限を更新しました", msg_cancel_share_done: "{n} 個の共有を解除しました。", msg_drag_drop_hint: "ファイルをここにドラッグ&ドロップ", str_drag_files: " 他 {n} 個のファイル", msg_creating_share: "共有を作成中...", title_share_result: "共有成功", msg_no_files: "項目がありません。", msg_no_selection: "項目を選択してください。", warn_del: "選択した {n} 項目を削除しますか?", msg_clear_sel_confirm: "{n} 個の重複ファイルが選択されています。選択を解除しますか?", str_bl_stat: "一致: {n} | 選択中: {m}", str_hits: "ヒット", msg_settings_saved: "設定を保存しました。ページをリロードします。", msg_name_exists: "名前が既に存在します: {n}", str_name_conflict: "(名前衝突の可能性)", msg_newfolder_prompt: "新規フォルダ名:", msg_rename_prompt: "新しい名前を入力してください:", msg_copy_done: "コピーしました。貼り付け先を選択してください。", msg_cut_done: "移動準備完了。貼り付け先を選択してください。", msg_paste_empty: "貼り付ける項目がありません。", msg_copy_empty: "クリップボードが空、またはデータがありません。", msg_add_success: "{n} 行のデータを追加しました。", msg_del_done: "選択した行を削除しました。", msg_del_select: "削除する行を選択してください。", msg_del_items_done: "{n} 個の項目を削除しました。", msg_copy_success: "コピー成功", str_redirecting: "Google レンズへ転送中...", msg_manual_paste: "画像アップロードがタイムアウトしました。コピーした画像を新しいウィンドウで {cmd} を押して貼り付けてください", msg_starring: "スターを追加中...", msg_unstarring: "スターを解除中...", msg_star_added: "スターを追加しました", msg_unstar_done: "スターを解除しました", msg_empty_trash_confirm: "ゴミ箱を空にしますか?この操作は取り消せません!", msg_trash_emptied: "ゴミ箱を空にしました。", msg_del_forever_confirm: "{n} 個の項目を完全に削除しますか?この操作は取り消せません!", msg_del_forever_done: "{n} 個の項目を完全に削除しました。", msg_restore_done: "{n} 個の項目を復元しました。", msg_auto_sub_load: "字幕を自動読み込みしました:{n}", msg_dl_sub: "字幕をダウンロード中...", msg_transcode_done: "✅ トランスコード完了。再生を開始します", msg_fallback_report: "⚠️ 原画が再生不可 (MPEG4/HEVC) なため、{n} に切り替えました", tip_manual_sub: "ヒント: .srt または .vtt ファイルをプレーヤーにドラッグ&ドロップして読み込めます。", msg_sub_drop_load: "ドラッグ&ドロップで字幕を読み込みました:{n}", msg_resume_hint: "{t} から再生を再開します。ここをクリック ", msg_unzip_confirm_n: "現在のディレクトリに {n} 個のファイルを解凍しますか?", msg_task_paused: "停止中", msg_task_added: "{n} 個のアップロードタスクを追加しました", msg_task_fast_success: "高速アップロード成功", msg_task_upload_done: "アップロード完了", log_dirty_data: "異常データを検出しました。リクエストを遮断します。", msg_network_unstable: "ネットワークが不安定です。自動復旧を試みています...", msg_skip_locked: "{n} 個がロックされています", msg_skip_self: "{n} 個を同じ場所に移動しようとしています", msg_skip_conflict: "{n} 個のサブパスが使用中です", msg_skip_invalid: "無効な項目をスキップしました: ", msg_creating_cloud_task: "クラウドタスクを作成中...", str_parsing_torrent: "種子ファイルを解析中...", err_torrent_no_info: "解析失敗:有効な情報が見つかりません", err_file_read: "ファイルの読み取りに失敗しました", msg_cloud_task_finish: "作成完了:成功 {s}、失敗 {f}", msg_cloud_task_success: "🎉 {n} 個のタスクを正常に作成しました", msg_prepare_restore: "復元準備中...", msg_smart_matching_n: "パスワードを自動照合中 ({n}個)...", msg_system_busy_retry: "システム混雑中。リトライしています ({n}/5)...", msg_unzip_running_bg: "[{n}] クラウドで解凍中です。後で更新して確認してください", msg_share_code_updated: "共有コードが更新されました", msg_retry_submitted: "{n} 個のタスクを再送信しました", msg_aria2_batch_fail_log: "\n\n失敗項目が多いため、詳細なエラーリスト(.txt)を自動的に書き出しました。", str_aria2_fetch_err: "(リンク取得失敗)", str_aria2_rpc_err: "(送信失敗)", str_aria2_aborted: "(取り消し済み)", str_aria2_fail_file_name: "Aria2_失敗リスト", msg_op_blocked_moving: "⚠️ 操作ブロック\n\nファイル移動タスクが進行中です。完了までお待ちください。", msg_op_blocked_analyzing: "⚠️ 操作ブロック\n\nバックグラウンドでファイル移動が進行中です。フォルダ分析の精度を保つため、現在のタスクが完了してから操作してください。", msg_op_blocked_exporting: "⚠️ 操作ブロック\n\nバックグラウンドでファイル移動が進行中です。正確なディレクトリ出力を確保するため、現在のタスクが完了してから操作してください。", msg_analyze_only_normal_dir: "フォルダを選択してください", msg_analyze_no_large_folders: "設定されたしきい値の範囲内にフォルダが見つかりませんでした ({s})", msg_analyze_summary_fmt: "{n} 個の {s}GB 以上のフォルダが見つかりました(サイズ順)", msg_prune_blocked_moving: "⚠️ 操作ブロック\n\nファイル移動タスクが進行中です。クリーンアップを実行できません。", msg_global_index_blocked_moving: "⚠️ 操作ブロック\n\nファイル移動中です。構造が安定してから全検索を実行してください。", msg_resource_locked_download: "⚠️ 操作ブロック\n\n移動中のファイルが含まれています。完了までお待ちください。", msg_resource_locked_aria2: "⚠️ 操作ブロック\n\n移動中のファイルが含まれています。完了までお待ちください。", msg_flatten_blocked_moving: "⚠️ 操作ブロック\n\nバックグラウンドでファイル移動が進行中です。このタイミングでファイル分析を実行すると、ファイルリストの欠損や重複が発生する可能性があるため、後ほど再試行してください。", err_task_conflict: "⚠️ 操作ブロック\n\nファイル移動中です。完了までお待ちください。", title_del_task_confirm_fmt: "{n} 件の転送タスクを削除しますか?", lbl_del_cloud_files_too: "クラウド上のファイルも同時に削除する", msg_file_del_failed: "ファイル削除失敗: ", msg_task_del_success_fmt: "{n} 個のタスクを削除しました", title_clear_task_confirm: "すべてのアップロードタスクをクリアしますか?", msg_task_clear_success_fmt: "{n} 個のアップロードタスクをクリアしました", msg_unzip_virtual_view_warn: "仮想ビューで操作しています。解凍されたファイルは各圧縮ファイルの元のフォルダに保存され、現在のリストには表示されません。

続行しますか?", msg_smart_matching_file: "パスワードを照合中... ({n})", msg_unzip_batch_submitted: "✅ {n} 個の解凍が完了しました", msg_unzip_batch_skipped: " ({n} 個スキップ)", msg_unzip_check_source: "。元のディレクトリで結果を確認してください。", tip_jump_to_folder: "このフォルダにジャンプ", msg_task_deleted: "タスクを削除しました", msg_scan_done: "スキャン完了!\n{n} 個のファイル、{f} 個のフォルダを確認しました。", msg_scan_fail: "\n\n❌ {n} 件のエラーが発生しました。", msg_scan_fix: "\n\n✅ {n} 回のネットワークエラーを自動修復しました。", msg_down_scanning: "フォルダの内容を解析中...", msg_down_progress: "ブラウザでダウンロード中...", msg_down_confirm_total: "✅ スキャン完了。{n} 個のファイルが見つかりました。\n\n⚠️ 警告:大量のファイルを一度にダウンロードするとブラウザがフリーズする可能性があります。10個以上の場合は Aria2 を推奨します。\n\nブラウザで続行しますか?", msg_aria2_sending_batch: "🚀 Aria2 へタスクを送信中...", msg_aria2_check_fail: "Aria2 への接続に失敗しました!\nURLとTokenを確認してください。", msg_aria2_check_ok: "Aria2 への接続に成功しました!", msg_aria2_sent: "{n} 個のファイルを Aria2 へ送信しました。", msg_aria2_test_fail: "Aria2 への接続に失敗しました。\n設定を保存しますか?", title_aria2_fail: "接続テスト失敗", msg_batch_scanning: "🚀 ディレクトリ構造を高速スキャン中...", msg_batch_hydrating: "⚡ ダウンロードリンクを並列抽出中...", msg_batch_no_files: "ダウンロード可能なファイルが見つかりませんでした。", msg_dup_warn: "重複ファイルの検索を開始しますか?", msg_dup_result: "{n} 組の重複が見つかりました。", msg_dup_none: "重複ファイルは見つかりませんでした。", msg_bl_stop: "操作を停止しました。", msg_bl_add_done: "{n} 個の項目を記録しました。", msg_bl_remove_done: "{n} 個の項目を記録から削除しました。", msg_bl_empty: "リストが空です。", msg_bl_clear_confirm: "すべての記録を削除しますか?この操作は取り消せません。", msg_blacklist_run_none: "該当する記録は見つかりませんでした。", msg_blacklist_run_confirm: "{n} 個の該当項目が見つかりました。\n\n今すぐゴミ箱へ移動しますか?", msg_bl_run_limit: "⚠️ 制限事項\n\nクリーンアップは物理的な再帰操作を伴います。ホーム画面に戻ってから実行してください。", msg_del_protected: "{n} 個の記録済みファイルを保護しました。", msg_del_none: "削除可能なファイルはありません。", msg_bl_scanning: "全検索中... \nスキャン済み: {d} | ヒット: {f}", rn_tip_wait: "ルールを設定してください", rn_tip_jav: "上のボタンをクリックして照合を開始します", rn_tip_none: "一致する項目はありません", rn_stat: "一致: {n} | 有効な変更: {m}", rn_warn_confirm: "{n} 個のファイルをリネームしますか?", msg_bulkrename_done: "{n} 個の項目をリネームしました。", msg_rn_all_skipped: "❌ すべての名前が重複しているため、変更されませんでした。", msg_rn_fail_count: "同名のため {n} 件スキップしました", msg_prune_confirm: "空フォルダの検索を開始しますか?", msg_prune_none: "空フォルダは見つかりませんでした。", msg_prune_found: "{n} 個の空フォルダが見つかりました。\n今すぐ削除しますか?", msg_deleting_folders: "{n} 個のフォルダを削除中...", msg_global_warn: "全ドライブの同期を開始します。\n同期されたデータはブラウザを更新するまでメモリに保持されます。\n続行しますか?", msg_init_scan_sel: "選択項目のスキャンを初期化中...", warn_clear_history: "選択した {n} 項目を履歴から削除しますか?\n(クラウド上のファイルは削除されません)", msg_img_copy_hint: "新しいウィンドウで {cmd} を押して検索してください。", msg_aria2_not_set: "Aria2 が設定されていません。設定を入力してください:", str_jav_no_match: "(品番が見つかりません)", msg_unzip_fail: "解凍リクエストが失敗しました", msg_jszip_fail: "JSZip の読み込みに失敗しました。ネットワークを確認してください。", msg_turbo_activated: "ターボモード起動:スクリプトがウェブ論理を完全制御し、最高の快適さを提供。", msg_console_legal: "商用利用厳禁:このプロジェクトは個人の学習および交流目的のみに使用されます。", msg_ana_warn: "フォルダ重複チェックのヒント:判定はアルゴリズムに基づいています。誤削除を防ぐため、削除前に必ず目視で確認してください。", /* --- 错误提示 --- */ err_invalid_links: "有効なリンクを入力してください", err_pwd_format: "パスワードは4-10桁の英数字である必要があります", err_invalid_torrent: "無効な種子ファイルの形式", err_torrent_complex: "解析の複雑度が高すぎます(不正なファイルの可能性)", err_torrent_format: "種子ファイルの構造が破損しています", err_torrent_len: "フィールド長の解析エラー", err_torrent_char: "不正な文字を検出しました", err_share_code_exists: "この共有コードは既に使用されています", err_folder_not_ready: "クラウドフォルダを作成中です。後で再試行してください", err_item_deleted: "項目が見つかりません", err_network: "ネットワークエラー", err_clipboard_denied: "クリップボードへのアクセスが拒否されました", err_worker: "ワーカーエラー", err_api: "APIエラー", err_capture: "スクリーンショットに失敗しました。", err_captcha_simple: "検証に失敗しました。Web版で一度ファイルをお気に入りに追加して、認証を完了させてください。", err_sub_dl_fail: "字幕のダウンロードに失敗しました: ", err_req_blocked: "リクエストがブロックされました (ファイアウォールなど)", err_req_timeout: "リクエストがタイムアウトしました", err_sub_drop_type: "字幕形式のファイルのみ対応しています", err_codec_t1: "再生不可能なビデオコーデックです ({c})", err_codec_t2: "ブラウザがこのビデオ形式をサポートしていません。
下のボタンから外部プレーヤーを呼び出してください。", err_pwd_simple: "パスワードが間違っています", err_task_exists: "タスクは既に存在します", err_network_break: "画像ノードの接続が切れました。もう一度クリックしてください", err_no_failed_task: "失敗したタスクは選択されていません", err_unknown: "不明なエラー", err_invalid_regex: "無効な正規表現", err_parent_not_found: "フォルダが見つかりません", msg_sys_error: "システムフォルダの操作は許可されていません", msg_download_fail: "ダウンロードリンクを取得できませんでした。", msg_video_fail: "ビデオリンクを取得できませんでした。", err_star_sync_fail: "スターの同期に失敗しました", err_paste_descendant: "自身または自身の子フォルダにはコピー・移動できません", err_quota_exceeded: "ストレージ容量が不足しています", err_name_exists: "同名のファイルが既に存在します", err_share_pass: "抽出コードは4-10文字である必要があります", str_error: "エラー", str_error_crit: "致命的なエラー", str_error_paste: "貼り付けエラー", str_action_failed: "操作に失敗しました", str_scan_error: "スキャンエラー", err_limit_too_low: "更新失敗:新しい回数 ({n}) は現在の保存数 ({s}) より大きい必要があります", err_vault_max: "パスワード保管庫は最大50個のよく使うパスワードのみ保存できます", err_pwd_len: "単一のパスワードの長さは127文字を超えることはできません", /* --- 帮助文档 --- */ modal_help_title: "ヘルプ", help_desc: `
✨ エクスペリエンス&ナビゲーション
インタラクションの再構築:公式機能をベースに、インターフェースを Windows エクスプローラー 風に刷新しました。
ターボモード:有効にするとウェブ版のロジックを完全に引き継ぎ、大量のファイルによるフリーズやクラッシュを根本から解決します。
高機能パスバー:マウスホイールのスクロールやドロップダウンでの同階層切り替えに対応。全体検索や分析スイートが統合され、パスの履歴表示や遡及ジャンプが可能です。
UXの強化:スター等の多角的なソート、ワンクリックでのサムネイルぼかし、ダークテーマ切り替えをサポート。バックグラウンドでは SWR 戦略 を採用し、シームレスにビューを更新します。
バックグラウンドインデックスと保護:ホームアイコンの青い点滅はディレクトリツリーの同期中を示します。競合する操作をブロックし、データの破損を防ぐ同時実行の物理的ロックを備えています。
* 注:デフォルトフォルダ(My Pack)はシステムにより保護されており、誤削除、コピー、移動、リネームが制限されています。
📂 一括処理&ストレージ管理
一括リネーム正規表現による置換/削除エピソード番号生成、テキスト整形FC2 標準命名広告プレフィックスの削除、MIMEに基づく拡張子の修復に対応しています。
分析スイートファイル分析(フィルタおよびハッシュ/時間/名前による重複チェック)とフォルダ分析(フィルタおよび名前/類似度/包含率による重複チェック)を統合。現在のパスのディレクトリツリー出力もサポートします。
スマート整理:ワンクリックで空フォルダを削除。一括解凍はパスワード自動記憶とスマート入力を統合し、解凍済み項目のスキップや削除も可能です。
リソースマネージャー:カスタムブラックリストとして不要ファイルを一括クリーンアップ、またはホワイトリストとして一括削除時に自動保護します。
* 注:データの同期競合を避けるため、処理中は他のクライアントでファイルを変更しないでください。
🌐 転送&共有センター
共有管理:抽出回数の上限設定に対応。制限に達すると自動的に共有が解除されます。
高速アップロード:ローカルファイルやフォルダのドラッグ&ドロップによる直接アップロードに対応。公式の制限を突破し、小容量ファイルの転送中断率を大幅に低減しました。
クラウドダウンロード強化:一括オフラインリンクの自動重複排除。内蔵のマグネットスマートクリーンエンジン(Base32/Hex ハッシュを自動抽出してノイズを除去)。.torrent シードファイルの解析をサポート。制限付きリンクに対するウェブスナップショット保存のフォールバック機能を提供。
* 注:抽出回数による自動解除は、ページを開いており、コンピュータがスリープ状態でない場合にのみ動作します。
🎬 没入型メディア拡張
再生エンジン:0.5x〜3.0xの倍速再生、画面の回転/反転、アスペクト比の強制調整、OP/EDスキップ、連続再生/ループモードをサポートし、プログレスバーのサムネイルプレビューに対応。内蔵のウォッチドッグにより、ブラックスクリーンや非対応コーデック時に互換画質へ自動フォールバックします。
字幕システム:クラウド内の同名字幕、ローカルファイル、およびオンライン字幕検索の読み込みに対応。字幕のズレをミリ秒単位で微調整でき、ローカルテキストのドラッグ&ドロップ解析も可能です。
ビジュアルアシスト:複数のエンジンによる画像検索(逆引き)を内蔵。「メディアモード」を有効にすると、マンガやアニメのフォルダが自動的に名前順(A-Z)でソートされます。
* 注:再生履歴リストは、スクリプト環境内で発生した再生進捗のみを記録します。
⚙️ 設定&データ管理
設定のバックアップ:個人設定、管理ルール、パスワード庫等をデジタル指紋付きの JSON ファイルとしてエクスポートでき、インポート時のスマート結合と重複排除をサポートします。
データの削除:インデックス、設定、ルール、パスワード、キャッシュデータを必要に応じて個別に削除し、ストレージの解放とプライバシー保護を行えます。
* 注:インデックスはページを閉じると消去されますが、設定やパスワード庫などのデータは永続的に保存されます。
⚡ ダウンロード&配布
外部ダイレクト接続:ワンクリックでビデオストリームの直リンクを取得、または PotPlayer を起動。RPC プロトコルを通じてファイルを Aria2 ノードへ即座に転送できます。
配布の強化:フォルダを Aria2 にプッシュする際、クラウドのツリーディレクトリ構造を自動復元します。持続的な接続監視をサポートし、エラー時にエラーリストを自動出力します。フォルダダウンロードフィルタの設定をサポート。
このプロジェクトは CC-BY-NC-SA-4.0 ライセンスに厳格に従っており、あらゆる形式の商用利用を禁止します。
` }, }; function getStrings() { return T[getLang()] || T.zh; }; let cachedCredKey = null; let cachedCaptchaKey = null; function resetHeaderCache() { cachedCredKey = null; cachedCaptchaKey = null; } function purgeAllCachesOnLogout() { resetHeaderCache(); if (typeof globalCache !== 'undefined') globalCache.clear(); if (typeof globalLineageMap !== 'undefined') globalLineageMap.clear(); if (typeof globalParentIndex !== 'undefined') globalParentIndex.clear(); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.clear(); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.clear(); if (typeof backgroundQueue !== 'undefined') backgroundQueue.length = 0; if (typeof isBackgroundRunning !== 'undefined') isBackgroundRunning = false; if (typeof DurationProber !== 'undefined') DurationProber.reset(); const ui = document.querySelector('.pk-ov'); if (ui) { ui.remove(); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; } const btn = document.getElementById('pk-launch'); if (btn) btn.remove(); if (typeof pkState !== 'undefined' && pkState) pkState = null; if (typeof globalSavedState !== 'undefined') globalSavedState = null; if (typeof globalPreloadPromise !== 'undefined') globalPreloadPromise = null; } (() => { window.addEventListener('storage', (e) => { if (e.key && (e.key.startsWith('credentials') || e.key.startsWith('captcha') || e.key === 'pk_captured_captcha')) { console.log(`[Auth Sync] Token storage change detected (${e.key}), flushing auth cache.`); if (!e.newValue) purgeAllCachesOnLogout(); else resetHeaderCache(); } }); const _origSetItem = Storage.prototype.setItem; Storage.prototype.setItem = function(key, value) { _origSetItem.apply(this, arguments); if (key && (key.startsWith('credentials') || key.startsWith('captcha') || key === 'pk_captured_captcha')) { resetHeaderCache(); } }; const _origRemoveItem = Storage.prototype.removeItem; Storage.prototype.removeItem = function(key) { _origRemoveItem.apply(this, arguments); if (key && (key.startsWith('credentials') || key.startsWith('captcha'))) { console.log(`[Auth Sync] Local credential removed (${key}), triggering deep purge.`); purgeAllCachesOnLogout(); } }; const checkAuthRoute = () => { if (location.href.includes('/login') || location.pathname.includes('login')) { console.log(`[Auth Sync] SPA route changed to /login, triggering deep purge.`); purgeAllCachesOnLogout(); } }; const _origPushState = history.pushState; history.pushState = function() { _origPushState.apply(this, arguments); checkAuthRoute(); }; const _origReplaceState = history.replaceState; history.replaceState = function() { _origReplaceState.apply(this, arguments); checkAuthRoute(); }; window.addEventListener('popstate', checkAuthRoute); })(); function getHeaders() { let token = '', captcha = ''; if (cachedCredKey) { try { const v = JSON.parse(localStorage.getItem(cachedCredKey)); if (v && v.access_token) token = v.token_type + ' ' + v.access_token; } catch {} } if (!token) { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('credentials')) { try { const v = JSON.parse(localStorage.getItem(k)); if (v && v.access_token) { token = v.token_type + ' ' + v.access_token; cachedCredKey = k; break; } } catch { } } } } if (cachedCaptchaKey) { try { const v = JSON.parse(localStorage.getItem(cachedCaptchaKey)); if (v) captcha = v.captcha_token; } catch {} } if (!captcha) { captcha = localStorage.getItem('pk_captured_captcha') || ''; if (!captcha) { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('captcha')) { try { const v = JSON.parse(localStorage.getItem(k)); if(v) { captcha = v.captcha_token; cachedCaptchaKey = k; } } catch { } } } } } return { 'Content-Type': 'application/json', 'Authorization': token, 'x-device-id': localStorage.getItem('deviceid') || '', 'x-captcha-token': captcha }; } async function waitForAuth(timeout = 10000) { const start = Date.now(); while (Date.now() - start < timeout) { const h = getHeaders(); if (h.Authorization && h.Authorization.length > 10) { return true; } await sleep(200); } return false; } async function apiList(parentId, limit = 1000, onProgress, signal, trashed = false, isBackground = false) { let all = [], next = null, safe = 5000; const TIMEOUT_MS = 25000; const filters = trashed ? `&filters=${encodeURIComponent('{"trashed":{"eq":true}}')}&parent_id=*` : `&parent_id=${parentId || ''}`; do { let pageRetries = 0; const MAX_PAGE_RETRIES = 5; let pageSuccess = false; while (pageRetries < MAX_PAGE_RETRIES && !pageSuccess) { if (signal?.aborted) throw new DOMException('Aborted by user', 'AbortError'); const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&with_audit=true&_t=${Date.now()}${next ? `&page_token=${next}` : ''}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true }); try { const res = await fetch(url, { headers: getHeaders(), signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) { if (res.status === 404) { console.warn(`[API] 404 Not Found (Skipped): ${parentId || 'Root'}`); return []; } if (res.status === 401 || res.status === 400) { console.warn(`[API] ${res.status} Error. Flushing auth cache...`); localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); if (res.status === 400) { try { if (typeof showToast !== 'undefined') showToast(getStrings().err_captcha_simple, 'error'); } catch(e){} throw new Error('CAPTCHA_INTERCEPT'); } throw new Error('AUTH_RETRY'); } if (res.status === 429) { await sleep(3000 * (pageRetries + 1)); throw new Error('RATE_LIMIT'); } throw new Error(`HTTP_${res.status}`); } syncTime(res.headers); const data = await res.json(); if (!data.files && data.next_page_token) { throw new Error('PAGINATION_INCOMPLETE'); } if (data.files) { const validFiles = data.files.filter(f => trashed ? f.trashed : !f.trashed).map(f => minifyFile(f, isBackground)); all.push(...validFiles); if (onProgress) onProgress(all.length); if (isBackground && parentId !== undefined && typeof globalCache !== 'undefined') { globalCache.set(parentId, { items: [...all], nextToken: data.next_page_token }); } } next = data.next_page_token; pageSuccess = true; if (next) await sleep(50); } catch (e) { clearTimeout(timeoutId); pageRetries++; safe--; let isTimeout = false; let errMsg = e.message; if (e.name === 'AbortError' && !(signal && signal.aborted)) { isTimeout = true; e = new Error('FETCH_TIMEOUT'); errMsg = 'Local Timeout'; } const isNetworkError = e.name === 'TypeError' || errMsg.includes('fetch') || errMsg.includes('PAGINATION') || isTimeout; if ((isNetworkError || errMsg === 'AUTH_RETRY') && safe > 0) { const backoff = pageRetries === 1 ? 500 : Math.min(pageRetries * 2000, 10000); console.warn(`[API] Retry ${pageRetries}/${MAX_PAGE_RETRIES} for ${parentId || 'Root'} due to ${errMsg}. Wait ${backoff}ms`); await sleep(backoff); continue; } throw e; } } } while (next && safe > 0); return all; } async function apiGet(id) { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${id}?thumbnail_size=SIZE_MEDIUM&_t=${Date.now()}`, { headers: getHeaders() }); if (!res.ok) throw new Error(`API Error ${res.status}`); return res.json(); } async function apiAction(action, data) { const method = action.includes('batch') ? 'POST' : 'PATCH'; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files${action}`, { method: method, headers: getHeaders(), body: JSON.stringify(data) }); syncTime(res.headers); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error_description || `API Error ${res.status}`); } return res.json(); }; async function apiStar(ids, star = true) { const action = star ? 'star' : 'unstar'; const url = `https://api-drive.mypikpak.com/drive/v1/files:${action}`; const res = await fetch(url, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ "ids": ids }) }); syncTime(res.headers); if (!res.ok) throw new Error(`API Error ${res.status}`); return true; } async function apiShareList(limit = 100) { let all =[], next = null, safe = 50; do { const url = `https://api-drive.mypikpak.com/drive/v1/share/list?limit=${limit}&thumbnail_size=SIZE_SMALL&_t=${Date.now()}${next ? `&page_token=${next}` : ''}`; const res = await fetch(url, { headers: getHeaders() }); if (!res.ok) throw new Error(`Share API Error ${res.status}`); const json = await res.json(); const list = json.data || []; const normalized = list.map(item => ({ id: item.share_id, kind: 'pikpak#share', name: item.title, size: item.file_size, modified_time: item.create_time, icon_link: item.icon_link, view_count: item.view_count, save_count: item.restore_count, limit_count: (function(){ const store = JSON.parse(gmGet('pk_share_limits', '{}')); return store[item.share_id] || parseInt(item.limit_count || 0); })(), share_status: item.share_status, share_status_text: item.share_status_text, expiration_days: item.expiration_days, expiration_left: item.expiration_left, expiration_at: item.expiration_at, phrase: item.phrase || "", share_url: item.share_url, pass_code: item.pass_code, parent_id: 'share_root' })); all.push(...normalized); if (!next) { const graveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); const serverIds = new Set(all.map(x => x.id)); const phantoms = graveyard.filter(x => !serverIds.has(x.id)); all.push(...phantoms); } next = json.next_page_token; safe--; } while (next && safe > 0); const graveyard = JSON.parse(gmGet("pk_expired_shares", "[]")); const graveyardIds = new Set(graveyard.map(x => x.id)); const filteredServerList = all.filter(it => !graveyardIds.has(it.id)); return [...filteredServerList, ...graveyard]; } async function apiTaskList(limit = 500, onBatch = null, session = null, signal = null) { let totalFetched = 0; const state = session || { phase: 'active', nextToken: null, completed: false }; const fetchList = async (phases, maxCount, startToken = null) => { let next = startToken; let page = 0; const maxPages = 100; do { if (signal && signal.aborted) break; const filters = encodeURIComponent(JSON.stringify({ "phase": { "in": phases } })); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?type=offline&limit=${limit}&filters=${filters}&thumbnail_size=SIZE_SMALL&with_reference_resource=true&_t=${Date.now()}${next ? `&page_token=${next}` : ''}`; let res = null; for (let i = 0; i < 3; i++) { try { res = await fetch(url, { headers: getHeaders() }); if (res.ok) break; if (res.status === 429) await sleep(2000); } catch (e) { await sleep(1000); } } if (!res || !res.ok) break; const data = await res.json(); const tasks = data.tasks || []; if (tasks.length > 0) { state.nextToken = data.next_page_token; if (onBatch) onBatch(tasks, data.next_page_token, phases.includes('COMPLETE') ? 'done' : 'active'); totalFetched += tasks.length; } next = data.next_page_token; page++; if (totalFetched >= maxCount || page >= maxPages) break; if (next) await sleep(50); } while (next); return next; }; if (state.phase === 'active') { const activePhases = "PHASE_TYPE_UNKNOW,PHASE_TYPE_PENDING,PHASE_TYPE_RUNNING,PHASE_TYPE_PAUSED,PHASE_TYPE_ERROR"; const lastToken = await fetchList(activePhases, 2000, state.nextToken); if (!lastToken) { state.phase = 'done'; state.nextToken = null; } else { return totalFetched; } } if (state.phase === 'done') { const donePhases = "PHASE_TYPE_COMPLETE"; const lastToken = await fetchList(donePhases, 15000, state.nextToken); if (!lastToken) { state.completed = true; state.nextToken = null; } } return totalFetched; } async function apiCancelTask(ids, deleteFiles = false) { let filesToDelete = []; if (deleteFiles && typeof pkState !== 'undefined' && pkState.itemMap) { ids.forEach(taskId => { const task = pkState.itemMap.get(taskId); if (task && task.id === taskId && task.file_id) { filesToDelete.push(task.file_id); } }); } const BATCH_SIZE = 50; for (let i = 0; i < ids.length; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); const idStr = chunk.join(','); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?task_ids=${idStr}&delete_files=${deleteFiles}&_t=${Date.now()}`; try { const res = await fetch(url, { method: 'DELETE', headers: getHeaders() }); if (!res.ok && res.status !== 404) throw new Error(`Task Del Err ${res.status}`); } catch(e) { console.warn("Task delete warning:", e); } } if (filesToDelete.length > 0) { console.log(`[Task] Also deleting ${filesToDelete.length} associated files...`); const trashUrl = `https://api-drive.mypikpak.com/drive/v1/files:batchTrash`; for (let i = 0; i < filesToDelete.length; i += 100) { const fileChunk = filesToDelete.slice(i, i + 100); await fetch(trashUrl, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: fileChunk }) }); } } return true; } async function apiAddOfflineTask(url, parentId = '', extraParams = {}) { const apiUrl = `https://api-drive.mypikpak.com/drive/v1/files`; const payload = { kind: "drive#file", upload_type: "UPLOAD_TYPE_URL", url: { "url": url }, params: { from: 'manual', with_thumbnail: 'true', ...extraParams } }; if (parentId) { payload.parent_id = parentId; } else { payload.folder_type = 'DOWNLOAD'; } const res = await fetch(apiUrl, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 400 && err.error_code === 9) throw new Error(getStrings().err_task_exists); throw new Error(err.error_description || `Add Task Error ${res.status}`); } return await res.json(); } async function apiGetSharePhrases(shareId) { const url = `https://api-drive.mypikpak.com/phrase/v1/content/info/share?id=${shareId}`; const res = await fetch(url, { headers: getHeaders() }); if (!res.ok) return []; const json = await res.json(); return (json.data || []).map(x => x.phrase); } async function apiUpdateSharePhrase(shareId, newPhrase, oldPhrase = "") { const url = `https://api-drive.mypikpak.com/phrase/v1/content/share`; const isDelete = newPhrase === ""; const isCreate = !oldPhrase && !isDelete; const payload = { content: shareId, type: "share" }; if (isCreate) { payload.phrase = newPhrase; } else { payload.old_phrase = oldPhrase; payload.new_phrase = newPhrase; } const res = await fetch(url, { method: isCreate ? 'POST' : 'PATCH', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 400) throw new Error(getStrings().err_share_code_exists); throw new Error(err.error_description || `API Error ${res.status}`); } return res.json(); } async function apiUpdateShare(shareId, data) { const url = `https://api-drive.mypikpak.com/drive/v1/share`; const payload = { share_id: shareId, ...data }; const res = await fetch(url, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify(payload) }); syncTime(res.headers); if (!res.ok) { const err = await res.json().catch(()=>({})); throw new Error(err.error_description || `API Error ${res.status}`); } return await res.json(); } async function apiCancelShare(ids) { const url = `https://api-drive.mypikpak.com/drive/v1/share:batchDelete`; const res = await fetch(url, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ids }) }); if (!res.ok) { console.warn("[Share] Batch delete failed, trying sequential delete..."); for (const id of ids) { await fetch(`https://api-drive.mypikpak.com/drive/v1/share/${id}`, { method: 'DELETE', headers: getHeaders() }); } return true; } return true; } // Original Logic by digbug82. Modification does not grant ownership. async function coreRecursiveEngine(roots, options) { const { signal, onFile, onFolder, onProgress } = options; const L = getStrings(); let queue = [...roots]; let activeTasks = new Set(); let inFlight = new Set(); let pendingRetries = 0; const stats = { folders: 0, files: 0, retries: 0, cacheHits: 0, currentConcurrency: 10 }; const USER_LIMIT = parseInt(localStorage.getItem('pk_user_limit') || "200"); const ABSOLUTE_MAX = 128; const MIN_CONCURRENCY = 5; const processFolder = async (current) => { inFlight.add(current.id); try { let files = []; if (typeof globalCache !== 'undefined' && globalCache.has(current.id)) { const cachedData = globalCache.get(current.id); if (Array.isArray(cachedData)) { files = cachedData; stats.cacheHits++; } } let isFromNetwork = false; let start = 0; if (files.length === 0) { start = performance.now(); files = await apiList(current.id, 1000, null, signal, false, true); isFromNetwork = true; if (typeof globalCache !== 'undefined') globalCache.set(current.id, files); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.add(current.id); } if (signal && signal.aborted) return; const nextSubFolders = []; for (const f of files) { if (f.kind === 'drive#folder') { const myLineage = [...(current.lineage || []), { id: f.id, name: f.name }]; nextSubFolders.push({ ...f, lineage: myLineage, depth: (current.depth || 0) + 1, retryCount: 0 }); } else { stats.files++; if (onFile) onFile(f, current); } } if (onFolder) onFolder(current, files, nextSubFolders); queue.push(...nextSubFolders); stats.folders++; if (isFromNetwork) { const rtt = performance.now() - start; const DYNAMIC_MAX = Math.min(USER_LIMIT, ABSOLUTE_MAX); if (rtt < 800) { if (stats.currentConcurrency < DYNAMIC_MAX) stats.currentConcurrency += 1; } else if (rtt > 3000) { stats.currentConcurrency = Math.max(MIN_CONCURRENCY, Math.floor(stats.currentConcurrency * 0.8)); } } } catch (err) { if (err.name === 'AbortError' || (signal && signal.aborted)) return; stats.currentConcurrency = Math.max(MIN_CONCURRENCY, Math.floor(stats.currentConcurrency * 0.5)); stats.retries++; current.retryCount = (current.retryCount || 0) + 1; pendingRetries++; try { const backoff = current.retryCount === 1 ? 1000 : Math.min(current.retryCount * 5000, 60000); const reason = err.message || "Unknown Error"; console.warn(`[ZeroLoss] Folder: ${current.name} | Reason: ${reason} | Attempt: ${current.retryCount} | Re-queueing...`); stats.isRetrying = true; if (onProgress) onProgress(stats); await sleep(backoff); if (signal && !signal.aborted) { queue.unshift(current); } } finally { stats.isRetrying = false; pendingRetries--; } } finally { inFlight.delete(current.id); if (onProgress) onProgress(stats); } }; while ((queue.length > 0 || inFlight.size > 0 || pendingRetries > 0) && (!signal || !signal.aborted)) { while (queue.length > 0 && activeTasks.size < stats.currentConcurrency && (!signal || !signal.aborted)) { const folder = queue.pop(); if (inFlight.has(folder.id) && folder.retryCount === 0) continue; const p = processFolder(folder); const pWrapper = p.finally(() => activeTasks.delete(pWrapper)); activeTasks.add(pWrapper); } if (activeTasks.size > 0) { await Promise.race(activeTasks).catch(() => {}); } else if (pendingRetries > 0 || inFlight.size > 0) { await sleep(100); } } } const version = (typeof GM_info !== 'undefined' && GM_info.script) ? GM_info.script.version : "1.0.0"; console.log("%c PikPak Enhancement Master %c v" + version + " %c digbug82 %c CC-BY-NC-SA-4.0 ", "color:#fff; background:#1a5eff; padding:3px 0; border-radius:4px 0 0 4px; font-weight:bold;", "color:#fff; background:#333; padding:3px 8px;", "color:#fff; background:#f57c00; padding:3px 8px; font-weight:bold;", "color:#fff; background:#d93025; padding:3px 8px; border-radius:0 4px 4px 0; font-weight:bold;"); console.log("%c" + getStrings().msg_console_legal + "%c", "color:#d93025; font-weight:bold;", ""); function getIcon(item) { const isFolder = item.kind === 'drive#folder' || (item.kind === 'drive#task' && ( (item.mime_type && item.mime_type.includes('folder')) || (item.icon_link && item.icon_link.includes('folder')) )); if (isFolder) { const isRootMyPack = (item.name === CONF.SYSTEM_FOLDER_NAME) && (!item.parent_id || item.parent_id === '' || item.parent_id === 'root' || item._isSystemRoot); if (isRootMyPack) return CONF.typeIcons.systemFolder; return CONF.typeIcons.folder; } const name = (item.name || '').toLowerCase(); const ext = name.split('.').pop(); const mime = (item.mime_type || '').toLowerCase(); const duration = (item.params && item.params.duration) || 0; if (ext === 'apk') return CONF.typeIcons.apk; if (ext === 'txt') return CONF.typeIcons.text; if (ext === 'html' || ext === 'htm') return CONF.typeIcons.web; if (['srt', 'vtt', 'ass', 'ssa'].includes(ext)) return CONF.typeIcons.subtitle; if (['exe', 'msi', 'bat', 'cmd', 'elf', 'dmg', 'pkg', 'app'].includes(ext)) return CONF.typeIcons.executable; const isArchiveExt = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso'].includes(ext); if (mime.startsWith('video/') || duration > 0) return CONF.typeIcons.video; if (mime.startsWith('image/')) return CONF.typeIcons.image; if (mime.startsWith('audio/')) return CONF.typeIcons.audio; if (mime === 'application/pdf') return CONF.typeIcons.pdf; if (mime.startsWith('text/') || mime.includes('word') || mime.includes('excel') || mime.includes('powerpoint') || mime.includes('officedocument') || mime === 'application/rtf') return CONF.typeIcons.text; if (isArchiveExt || mime.includes('zip') || mime.includes('rar') || mime.includes('7z') || mime.includes('tar') || mime.includes('archive') || mime.includes('compressed')) { const isUnzipped = item.params && (item.params.global_file_kind === '1' || item.params.global_file_root); return isUnzipped ? CONF.typeIcons.archiveUnzipped : CONF.typeIcons.archive; } return CONF.typeIcons.file; } async function openManager(initialCache, preloadPromise) { const L = getStrings(); const lang = getLang(); if (document.querySelector('.pk-ov')) return; document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; const S = { path: (globalSavedState && globalSavedState.path) ? [...globalSavedState.path] : [{ id: '', name: L.btn_nav_home }], items: [], itemMap: new Map(), display: [], sel: new Set(), cache: initialCache || new Map(), sort: (globalSavedState && globalSavedState.sort) ? globalSavedState.sort : 'modified_time', dir: (globalSavedState && globalSavedState.dir !== undefined) ? globalSavedState.dir : 1, scanning: false, dupMode: false, dupRunning: false, folderFirst: false, dupReasons: new Map(), dupGroups: new Map(), dupRawGroups: [], offlineFilters: { running: true, failed: true, complete: true }, uploadFilters: { running: true, paused: true, complete: true }, activeId: null, clipItems: [], clipType: '', clipSourceParentId: null, loading: false, lastSelIdx: -1, dupConfig: { video: true, image: true, other: true }, search: '', sortId: 0, isFlattened: false, filterState: { active: false, cat: 'all', ext: 'all' }, suppressClearConfirm: false, preSearchPath: null, lastGlobalResults: [], folderLineageMap: globalLineageMap, trashMode: (globalSavedState && globalSavedState.trashMode) || false, shareMode: (globalSavedState && globalSavedState.shareMode) || false, starredMode: (globalSavedState && globalSavedState.starredMode) || false, recentMode: (globalSavedState && globalSavedState.recentMode) || false, historyMode: (globalSavedState && globalSavedState.historyMode) || false, offlineMode: (globalSavedState && globalSavedState.offlineMode) || false, uploadMode: (globalSavedState && globalSavedState.uploadMode) || false, scanId: 0, preloaded: (globalSavedState || (initialCache && initialCache.has('root'))) ? true : false, preLoadPromise: preloadPromise || null, blSet: new Set(), blFolderSet: new Set(), starredSet: new Set(), pendingMap: new Map(), durationMap: new Map(), loadedThumbs: new Set(), movingIds: new Set(), movingSourceId: null, movingDestId: null, uploadTasks: (globalSavedState && globalSavedState.uploadTasks) ? globalSavedState.uploadTasks : [], broadcast: new BroadcastChannel('pk_act_sync'), clearSelection: () => { S.sel.clear(); S.lastSelIdx = -1; S.activeId = null; if (typeof updateStat === 'function') updateStat(); }, updateBlCache: () => { const parse = (key) => new Set(gmGet(key, '').toLowerCase().split('\n').map(s => s.trim()).filter(s => s)); S.blSet = parse('pk_blacklist'); S.blFolderSet = parse('pk_blacklist_folders'); }, getRealCacheKey: (id) => { if (id === 'offline_root') return 'offline_root'; if (id === 'recent_root') return 'recent_root'; if (id === 'root' || id === '' || !id) { if (S.shareMode) return 'share_root'; if (S.starredMode) return 'starred_root'; if (S.recentMode) return 'recent_root'; if (S.trashMode) return 'root_trashed'; if (S.offlineMode) return 'offline_root'; return 'root'; } return id; } }; pkState = S; if (S.uploadTasks.length > 0) { S.uploadTasks.forEach(task => { if (task.status === 'WAITING') task.message = L.msg_task_waiting; else if (task.status === 'DONE') task.message = L.msg_task_upload_done; else if (task.status === 'PAUSED') task.message = L.msg_task_paused; else if (task.status === 'HASHING') task.message = L.msg_task_hashing; else if (task.status === 'UPLOADING') task.message = L.msg_task_uploading; }); } const FloatBarManager = (() => { const activeBars = []; const BASE_BOTTOM = 30; const GAP = 10; const reposition = () => { let currentBottom = BASE_BOTTOM; activeBars.forEach(item => { item.el.style.bottom = `${currentBottom}px`; currentBottom += (item.el.offsetHeight || 40) + GAP; }); }; const create = (initialText) => { const id = 'pk_fb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const el = document.createElement('div'); el.className = 'pk-float-bar-item'; el.style.cssText = ` position: absolute; left: 50%; transform: translateX(-50%); background: var(--pk-toast-bg); color: var(--pk-toast-fg); padding: 10px 24px; border-radius: 30px; font-size: 13px; font-weight: 600; box-shadow: 0 10px 30px var(--pk-tip-sd); z-index: 20000; border: 1px solid var(--pk-toast-bd); display: flex; align-items: center; gap: 12px; transition: bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; opacity: 0; pointer-events: none; white-space: nowrap; backdrop-filter: blur(8px); `; el.innerHTML = `
${esc(initialText)}`; if (UI.win) UI.win.appendChild(el); else { const container = document.querySelector('.pk-ov') || document.body; container.appendChild(el); } const entry = { id, el }; activeBars.push(entry); requestAnimationFrame(() => { reposition(); el.style.opacity = '1'; }); return { update: (text) => { const span = el.querySelector('.pk-float-txt'); if (span) span.textContent = text; }, destroy: () => { const idx = activeBars.findIndex(x => x.id === id); if (idx !== -1) { activeBars.splice(idx, 1); el.style.opacity = '0'; el.style.transform = 'translateX(-50%) scale(0.9)'; reposition(); setTimeout(() => el.remove(), 300); } } }; }; return { create }; })(); S.broadcast.onmessage = (e) => { const { type, ids, src, dst } = e.data; if (!ids) return; if (type === 'LOCK_ADD') { ids.forEach(id => S.movingIds.add(id)); if (src) S.movingSourceId = src; if (dst) S.movingDestId = dst; } else if (type === 'LOCK_REM') { ids.forEach(id => S.movingIds.delete(id)); } else if (type === 'LOCK_CLR') { S.movingIds.clear(); S.movingSourceId = null; S.movingDestId = null; } if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); if (typeof updateStat === 'function') updateStat(); }; const updateGlobalLockCSS = () => { let style = document.getElementById('pk-lock-css'); if (!style) { style = document.createElement('style'); style.id = 'pk-lock-css'; document.head.appendChild(style); } if (!S.movingIds || S.movingIds.size === 0 || !S.movingSourceId) { style.textContent = ''; return; } const selector = Array.from(S.movingIds) .map(id => `.pk-win .pk-row[data-id="${id}"]`) .join(','); style.textContent = `${selector} { opacity: 0.4 !important; filter: grayscale(100%) !important; pointer-events: none !important; cursor: wait !important; }`; }; const isPathBusy = (targetFolderId) => { if (!S.movingIds || S.movingIds.size === 0) return false; const sourceId = S.movingSourceId; const destId = S.movingDestId; const tid = targetFolderId === 'root' ? '' : (targetFolderId || ''); if (tid === '') return true; if (tid === sourceId || tid === destId) return true; const checkAncestry = (startId, forbiddenId) => { let curr = startId; let safety = 50; while (curr && curr !== 'root' && safety > 0) { if (curr === forbiddenId) return true; const parent = globalParentIndex.get(curr); curr = parent ? parent.id : null; safety--; } return false; }; if (checkAncestry(sourceId, tid) || checkAncestry(destId, tid)) return true; if (checkAncestry(tid, sourceId) || checkAncestry(tid, destId)) return true; return false; }; const resumeBackgroundDiscovery = () => { if (S.scanning || S.loading) { return; } const isVirtual = S.isFlattened || S.dupMode || S.path.some(p => p.id === 'virtual_search_root'); if (isVirtual) { return; } let addedCount = 0; if (typeof globalCache !== 'undefined') { for (const [parentFolderId, files] of globalCache) { if (!files || !Array.isArray(files)) continue; for (let i = 0; i < files.length; i++) { const f = files[i]; if (f.kind === 'drive#folder' && !scannedFolderIds.has(f.id) && !globalCache.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name, retryCount: 0 }); scannedFolderIds.add(f.id); addedCount++; } } if (addedCount > 500) break; } } if (addedCount === 0) { const visibleSubFolders = S.items.filter(f => f.kind === 'drive#folder'); for (let i = visibleSubFolders.length - 1; i >= 0; i--) { const sub = visibleSubFolders[i]; if (!scannedFolderIds.has(sub.id) && !globalCache.has(sub.id)) { backgroundQueue.push({ id: sub.id, name: sub.name, retryCount: 0 }); scannedFolderIds.add(sub.id); addedCount++; } } } if (addedCount > 0 || backgroundQueue.length > 0) { console.log(`♻️ Background crawler resumed: ${addedCount} new tasks found in map.`); runBackgroundCrawler(); } }; S.updateBlCache(); if (S.items && S.items.length > 0) { S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); } const el = document.createElement('div'); el.className = 'pk-ov'; let siteFont = window.getComputedStyle(document.body).fontFamily || ''; siteFont = siteFont.replace(/,?\s*sans-serif\s*$/i, ''); el.style.fontFamily = siteFont ? `${siteFont}, "Noto Sans", sans-serif` : '"Noto Sans", sans-serif'; el.addEventListener('wheel', (e) => { e.stopPropagation(); const scrollTarget = e.target.closest('.pk-vp, .pk-modal, #pk-rn-vp, .pk-prev-list, textarea, .pk-scroll, .pk-help-scroll'); if (!scrollTarget) { e.preventDefault(); return; } const st = scrollTarget.scrollTop; const sh = scrollTarget.scrollHeight; const ch = scrollTarget.clientHeight; const isUp = e.deltaY < 0; const isDown = e.deltaY > 0; const isAtTop = st <= 0.5; const isAtBottom = (st + ch) >= (sh - 1.5); if ((isUp && isAtTop) || (isDown && isAtBottom)) { if (e.cancelable) e.preventDefault(); } }, { passive: false }); const mkLbl = (id, txt, shortTxt, tip) => { return ``; }; const savedTheme = gmGet('pk_theme', 'auto'); const sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; let isDark = savedTheme === 'dark' || (savedTheme === 'auto' && sysDark); if (isDark) el.classList.add('pk-dark'); const themeIcon = isDark ? CONF.icons.sun : CONF.icons.moon; const winClass = 'pk-win pk-lang-' + lang; const ctxIcons = { locate: ``, info: ``, open: ``, star: ``, unstar: ``, download: ``, copy: ``, move: ``, rename: ``, renameBulk: ``, blAdd: ``, blRem: ``, share: CONF.icons.share, copyLink: ``, copyName: ``, trash: ``, restore: ``, delForever: `` }; el.innerHTML = `
${L.loading_detail}
${CONF.icons.cloudDownload}${L.btn_cloud_download}
${CONF.icons.home}${L.btn_nav_home}
${CONF.icons.navUpload}${L.btn_nav_upload}
${CONF.icons.offline}${L.btn_nav_offline}
${CONF.icons.recent}${L.btn_nav_recent}
${CONF.icons.history}${L.btn_nav_history}
${CONF.icons.navShare}${L.btn_nav_share}
${L.btn_nav_starred}
${CONF.icons.trash}${L.btn_nav_trash}
--%
${CONF.icons.settings}${L.btn_settings}
${CONF.logoSVG}
${L.title} by digbug82
${themeIcon}
${CONF.icons.help}
${CONF.icons.maximize}
${CONF.icons.close}
${mkLbl('pk-chk-hash', L.tag_hash, L.tag_hash_short, L.tag_hash)} ${mkLbl('pk-chk-sim', L.tag_sim, L.tag_sim_short, L.tag_sim)} ${mkLbl('pk-chk-name', L.tag_name, L.tag_name_short, L.tag_name)}
${CONF.icons.upFile} ${L.btn_up_file}
${CONF.icons.upFolder} ${L.btn_up_folder}
${L.col_name}
${CONF.icons.folderFirst} ${L.lbl_folder_first}
${L.col_size}
${L.col_dur}
${L.col_date}
${L.status_ready.replace('{n}', 0)}
${CONF.icons.share} ${L.ctx_share}
${ctxIcons.open} ${L.ctx_open}
${ctxIcons.star} ${L.ctx_star}
${ctxIcons.info} ${L.ctx_property}
${ctxIcons.download} ${L.ctx_down}
${ctxIcons.copyName} ${L.ctx_copy_name}
${ctxIcons.move} ${L.btn_cut}
${ctxIcons.copy} ${L.ctx_copy}
${ctxIcons.rename} ${L.ctx_rename}
${CONF.icons.prune} ${L.btn_prune}
${ctxIcons.trash} ${L.ctx_del}
${ctxIcons.blAdd} ${L.ctx_add_bl}
`; document.body.appendChild(el); const destroyTooltip = (() => { const tipEl = document.createElement('div'); tipEl.className = 'pk-tooltip'; const style = document.createElement('style'); style.textContent = ` .pk-tooltip { zoom: var(--pk-zoom, 1); width: max-content; max-width: 280px; min-width: auto; background: var(--pk-tip-bg); color: var(--pk-tip-fg); border: 1px solid var(--pk-tip-bd); padding: 6px; box-sizing: border-box; border-radius: 8px; font-size: 12px; line-height: 1.4; position: fixed; z-index: 2147483647 !important; pointer-events: none; box-shadow: 0 4px 16px var(--pk-tip-sd); backdrop-filter: blur(4px); display: flex; flex-direction: column; gap: 4px; white-space: normal; word-break: break-all; text-align: justify; opacity: 0; transform: translateY(5px) scale(0.95); transition: opacity 0.15s ease, transform 0.15s ease; } .pk-tooltip.pk-dark { --pk-tip-bg: rgba(20, 20, 20, 0.95); --pk-tip-fg: #ffffff; --pk-tip-bd: rgba(255, 255, 255, 0.1); --pk-tip-sd: rgba(0, 0, 0, 0.4); } .pk-tooltip.show { opacity: 1; transform: translateY(0) scale(1); } body.pk-dragging .pk-tooltip { display: none !important; opacity: 0 !important; } .pk-tooltip img { display: none; width: 100%; max-width: 100%; max-height: 320px; height: auto; object-fit: cover; border-radius: 4px; margin-bottom: 0 !important; display: block; } `; document.head.appendChild(style); document.body.appendChild(tipEl); let activeTarget = null; let isMenuOpen = false; let lastMouseX = 0, lastMouseY = 0; const hideTip = () => { tipEl.style.display = 'none'; tipEl.classList.remove('show'); tipEl.style.left = '-9999px'; activeTarget = null; }; const renderTip = (target) => { const isDark = document.querySelector('.pk-ov')?.classList.contains('pk-dark'); tipEl.classList.toggle('pk-dark', !!isDark); const text = target.getAttribute('data-pk-tip'); const thumb = target.getAttribute('data-pk-thumb'); if (!text && !thumb) { hideTip(); return; } activeTarget = target; let html = ''; if (thumb && thumb !== 'undefined' && thumb !== 'null' && (thumb.startsWith('http') || thumb.startsWith('blob:'))) { const isBlur = gmGet('pk_blur_thumb', false); html += ``; } if (text) { html += `
${text}
`; } tipEl.innerHTML = html; tipEl.style.display = 'block'; requestAnimationFrame(() => tipEl.classList.add('show')); }; document.addEventListener('mouseover', (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; if (isMenuOpen) return; if (document.body.classList.contains('pk-dragging')) { hideTip(); return; } if (document.querySelector('#pk-ctx')?.style.display === 'block') return; const target = e.target.closest('[data-pk-tip], [data-pk-thumb]'); if (!target) return; if (target === activeTarget) return; const isName = target.classList.contains('pk-name') || target.closest('.pk-name'); const isPath = target.classList.contains('pk-path') || target.closest('.pk-path'); const isThumb = target.hasAttribute('data-pk-thumb'); const isUI = target.closest('.pk-tb, .pk-sidebar, .pk-hd, .pk-ft, .pk-player-box, .pk-modal, .pk-img-box'); if (!isName && !isPath && !isThumb && !isUI) { if (target.scrollWidth <= target.clientWidth + 1) return; } renderTip(target); updatePos({ clientX: lastMouseX, clientY: lastMouseY }); }); const updatePos = (e) => { if (!tipEl || isMenuOpen || tipEl.style.display === 'none') return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let left = (e.clientX / scale) + 15; let top = (e.clientY / scale) + 15; const rect = tipEl.getBoundingClientRect(); const w = rect.width / scale; const h = rect.height / scale; const winW = window.innerWidth / scale; const winH = window.innerHeight / scale; if (left + w > winW - 10) left = (e.clientX / scale) - w - 15; if (top + h > winH - 10) top = (e.clientY / scale) - h - 15; tipEl.style.left = `${left}px`; tipEl.style.top = `${top}px`; }; const onGlobalMouseMove = (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; if (document.body.classList.contains('pk-dragging')) { hideTip(); return; } updatePos(e); if (activeTarget && tipEl.style.display !== 'none') { const actualTarget = e.target.closest('[data-pk-tip],[data-pk-thumb]'); if (actualTarget !== activeTarget) { hideTip(); } } }; document.addEventListener('mousemove', onGlobalMouseMove); const onGlobalMouseOut = (e) => { const target = e.target.closest('[data-pk-tip],[data-pk-thumb]'); if (target && target === activeTarget) { if (target.contains(e.relatedTarget)) return; hideTip(); } }; document.addEventListener('mouseout', onGlobalMouseOut); const onGlobalCtxMenu = () => { isMenuOpen = true; hideTip(); }; const onGlobalClick = () => { isMenuOpen = false; if(activeTarget) hideTip(); }; const onGlobalWheel = () => { isMenuOpen = false; hideTip(); }; window.addEventListener('contextmenu', onGlobalCtxMenu, true); window.addEventListener('click', onGlobalClick, true); window.addEventListener('wheel', onGlobalWheel, { passive: true, capture: true }); window.pkRefreshTooltip = () => { if (isMenuOpen || document.body.classList.contains('pk-dragging')) { hideTip(); return; } const elUnder = document.elementFromPoint(lastMouseX, lastMouseY); if (!elUnder) { hideTip(); return; } const target = elUnder.closest('[data-pk-tip], [data-pk-thumb]'); if (!target) { hideTip(); return; } const isName = target.classList.contains('pk-name') || target.closest('.pk-name'); const isPath = target.classList.contains('pk-path') || target.closest('.pk-path'); const isThumb = target.hasAttribute('data-pk-thumb'); const isUI = target.closest('.pk-tb, .pk-sidebar, .pk-hd, .pk-ft, .pk-player-box, .pk-modal, .pk-img-box'); if (!isName && !isPath && !isThumb && !isUI) { if (target.scrollWidth <= target.clientWidth + 1) { hideTip(); return; } } renderTip(target); updatePos({ clientX: lastMouseX, clientY: lastMouseY }); }; return () => { document.removeEventListener('mousemove', onGlobalMouseMove); document.removeEventListener('mouseout', onGlobalMouseOut); window.removeEventListener('contextmenu', onGlobalCtxMenu, true); window.removeEventListener('click', onGlobalClick, true); window.removeEventListener('wheel', onGlobalWheel, { passive: true, capture: true }); delete window.pkRefreshTooltip; if (tipEl && tipEl.parentNode) tipEl.remove(); if (style && style.parentNode) style.remove(); }; })(); const UI = { win: el.querySelector('.pk-win'), vp: el.querySelector('#pk-vp'), in: el.querySelector('#pk-in'), loader: el.querySelector('#pk-loader'), loadTxt: el.querySelector('#pk-load-txt'), stopBtn: el.querySelector('#pk-stop-load'), crumb: el.querySelector('#pk-crumb'), stat: el.querySelector('#pk-stat'), chkAll: el.querySelector('#pk-all'), scan: el.querySelector('#pk-scan-dup'), dupTools: el.querySelector('#pk-dup-tools'), dupFilters: el.querySelector('#pk-dup-filters'), chkName: el.querySelector('#pk-chk-name'), chkSim: el.querySelector('#pk-chk-sim'), chkHash: el.querySelector('#pk-chk-hash'), selDupFolder: el.querySelector('#pk-dup-folder-sel'), offTools: el.querySelector('#pk-offline-tools'), chkOffRun: el.querySelector('#pk-off-run'), chkOffFail: el.querySelector('#pk-off-fail'), chkOffOk: el.querySelector('#pk-off-ok'), upTools: el.querySelector('#pk-upload-tools'), chkUpRun: el.querySelector('#pk-chk-up-run'), chkUpPause: el.querySelector('#pk-chk-up-pause'), chkUpDone: el.querySelector('#pk-chk-up-done'), btnAnalyze: el.querySelector('#pk-analyze'), btnExport: el.querySelector('#pk-export'), btnFolderFirst: el.querySelector('#pk-btn-folder-first'), btnNavHome: el.querySelector('#pk-nav-home'), btnNavOffline: el.querySelector('#pk-nav-offline'), btnNavUpload: el.querySelector('#pk-nav-upload'), btnNavRecent: el.querySelector('#pk-nav-recent'), btnNavHistory: el.querySelector('#pk-nav-history'), btnNavShare: el.querySelector('#pk-nav-share'), btnNavStarred: el.querySelector('#pk-nav-starred'), btnNavTrash: el.querySelector('#pk-nav-trash'), trashBar: el.querySelector('#pk-trash-bar'), btnTrashRefresh: el.querySelector('#pk-trash-refresh'), bottomGrp: el.querySelector('.pk-ft .pk-grp'), actionBar: el.querySelector('#pk-actionbar'), btnRestore: el.querySelector('#pk-restore'), btnDelForever: el.querySelector('#pk-del-forever'), btnEmptyTrash: el.querySelector('#pk-empty-trash'), btnTrashBlacklistManager: el.querySelector('#pk-trash-blacklist-manager'), btnDupSmart: el.querySelector('#pk-dup-smart-btn'), btnExit: el.querySelector('#pk-btn-exit'), btnCopy: el.querySelector('#pk-copy'), btnCut: el.querySelector('#pk-cut'), btnDel: el.querySelector('#pk-del'), btnDeselect: el.querySelector('#pk-deselect'), btnRename: el.querySelector('#pk-rename'), btnBulkRename: el.querySelector('#pk-bulkrename'), btnPrune: el.querySelector('#pk-prune'), btnUnzip: el.querySelector('#pk-unzip'), btnBlacklistManager: el.querySelector('#pk-blacklist-manager'), uploadWrap: el.querySelector('#pk-upload-wrap'), btnUpload: el.querySelector('#pk-btn-upload'), actUpFile: el.querySelector('#pk-act-upload-file'), actUpFolder: el.querySelector('#pk-act-upload-folder'), inpFile: el.querySelector('#pk-file-selector'), inpFolder: el.querySelector('#pk-folder-selector'), btnCancelShare: el.querySelector('#pk-cancel-share'), btnRetryTask: el.querySelector('#pk-retry-task'), btnCopyLinkOffline: el.querySelector('#pk-off-copy-link'), btnUpPause: el.querySelector('#pk-up-pause'), btnUpStart: el.querySelector('#pk-up-start'), btnUpDel: el.querySelector('#pk-up-del'), btnUpClearAll: el.querySelector('#pk-up-clear-all'), btnPaste: el.querySelector('#pk-paste'), btnPaste: el.querySelector('#pk-paste'), btnRefresh: el.querySelector('#pk-refresh'), btnNewFolder: el.querySelector('#pk-newfolder'), btnSettings: el.querySelector('#pk-settings'), btnClose: el.querySelector('#pk-close'), btnHelp: el.querySelector('#pk-help'), btnTheme: el.querySelector('#pk-theme'), btnExt: el.querySelector('#pk-ext'), btnAria2: el.querySelector('#pk-aria2'), btnDown: el.querySelector('#pk-down'), pop: el.querySelector('#pk-pop'), ctx: el.querySelector('#pk-ctx'), cols: el.querySelectorAll('.pk-col'), searchInput: el.querySelector('#pk-search-input'), chkGlobal: el.querySelector('#pk-chk-global'), lblGlobal: el.querySelector('#pk-lbl-global'), chkSearchPath: el.querySelector('#pk-chk-search-path'), lblSearchPath: el.querySelector('#pk-search-path-con'), btnAnaSelect: (function() { const b = document.createElement('button'); b.className = 'pk-ana-select-btn'; b.innerHTML = ` ${L.btn_ana_select}`; const parent = el.querySelector('#pk-search-path-con'); if (parent) parent.parentNode.insertBefore(b, parent); return b; })(), topBar: el.querySelector('#pk-top-bar'), searchClear: el.querySelector('#pk-search-clear'), searchBtn: el.querySelector('#pk-search-btn'), searchHist: el.querySelector('#pk-search-hist'), filterBar: el.querySelector('#pk-filter-bar'), filterBtn: el.querySelector('#pk-filter-btn'), filterActiveUI: el.querySelector('#pk-filter-active-ui'), filterCatLabel: el.querySelector('#pk-filter-cat-label'), filterExtsWrap: el.querySelector('#pk-filter-exts-wrap'), filterExtsMain: el.querySelector('#pk-filter-exts-main'), filterExtsMoreBtn: el.querySelector('#pk-filter-exts-more-btn'), filterExitBtn: el.querySelector('#pk-filter-exit-btn') }; const invokeExternalPlayer = async (item) => { const player = gmGet('pk_ext_player', 'system'); const L = getStrings(); let link = item.web_content_link; if (!link) { try { const detail = await apiGet(item.id); link = detail.web_content_link; } catch (e) { showAlert(L.msg_video_fail); return; } } if (!link) { showAlert(L.msg_video_fail); return; } if (player === 'potplayer') { let cleanLink = link.replace('&ext=.m3u8', ''); if (cleanLink.includes('ts_downloader') && cleanLink.includes('url=')) { const urlParam = new URL(cleanLink).searchParams.get('url'); if (urlParam) cleanLink = decodeURIComponent(urlParam); } const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanLink} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; } else { playVideo(item); } }; const isSystemItem = (item) => { if (!item) return false; if (item.kind !== 'drive#folder') return false; if (item._isSystemRoot) return true; const isRootLocation = S.path.length === 1 && S.path[0].id === ''; if (isRootLocation && item.name === CONF.SYSTEM_FOLDER_NAME) return true; if (!item.parent_id && item.name === CONF.SYSTEM_FOLDER_NAME) return true; return false; }; const updateCrawlerUI = () => { if (!UI.btnNavHome) return; if (typeof isBackgroundRunning !== 'undefined' && isBackgroundRunning) { UI.btnNavHome.classList.add('pk-status-dot'); } else { UI.btnNavHome.classList.remove('pk-status-dot'); } }; window.pkUpdateCrawlerUI = updateCrawlerUI; let isForcedHidden = false; const checkGuiResponsiveness = () => { const width = window.innerWidth; const height = window.innerHeight; const screenW = window.screen.width; let z = 1; if (screenW <= 1600 || width <= 1600) z = 0.8; if (screenW <= 1280 || width <= 1280) z = 0.7; document.documentElement.style.setProperty('--pk-zoom', z); const isTurboCurrent = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; const MIN_WIDTH = 940 * z; const MIN_HEIGHT = 450; const isTooSmall = width < MIN_WIDTH || height < MIN_HEIGHT; const isGuiVisible = el.style.display !== 'none'; if (isTooSmall && !isTurboCurrent) { document.body.classList.add('pk-hide-all-ui'); if (isGuiVisible) { el.style.display = 'none'; isForcedHidden = true; } } else { document.body.classList.remove('pk-hide-all-ui'); if (isForcedHidden) { el.style.display = 'flex'; if (el.focus) el.focus(); isForcedHidden = false; } } const isCompact = width <= 1200; if (UI.searchInput) { UI.searchInput.placeholder = isCompact ? L.placeholder_search_short : L.placeholder_search; } const folderSelPlaceholder = document.querySelector('#pk-dup-folder-sel option[value=""]'); if (folderSelPlaceholder) { folderSelPlaceholder.textContent = isCompact ? L.lbl_dup_select_folder_short : L.lbl_dup_select_folder; } }; window.addEventListener('resize', () => { checkGuiResponsiveness(); if (typeof renderVisible === 'function') requestAnimationFrame(renderVisible); }); checkGuiResponsiveness(); if (UI.btnClose) { UI.btnClose.addEventListener('click', () => { isForcedHidden = false; }); } if (UI.btnTheme) { UI.btnTheme.onclick = (e) => { if (e) e.stopPropagation(); el.classList.add('pk-no-transition'); void el.offsetHeight; const wasDark = el.classList.contains('pk-dark'); const newTheme = wasDark ? 'light' : 'dark'; const newIcon = wasDark ? CONF.icons.moon : CONF.icons.sun; UI.btnTheme.innerHTML = newIcon; if (wasDark) { el.classList.remove('pk-dark'); } else { el.classList.add('pk-dark'); } gmSet('pk_theme', newTheme); void el.offsetHeight; requestAnimationFrame(() => { el.classList.remove('pk-no-transition'); }); }; } const btnMax = el.querySelector('#pk-maximize'); const isTurbo = gmGet('pk_turbo_mode', false); let isWinMaximized = (globalSavedState && typeof globalSavedState.isMaximized !== 'undefined') ? globalSavedState.isMaximized : isTurbo; if (isTurbo) { if (btnMax) btnMax.style.display = 'none'; if (UI.btnClose) UI.btnClose.style.display = 'none'; } const win = el.querySelector('.pk-win'); if (isWinMaximized) { if (win) win.classList.add('pk-maximized'); document.body.classList.add('pk-body-max'); CONF.rowHeight = 60; if (btnMax) { btnMax.innerHTML = CONF.icons.minimize; btnMax.setAttribute('data-pk-tip', L.tip_minimize); } } else { if (win) win.classList.remove('pk-maximized'); document.body.classList.remove('pk-body-max'); CONF.rowHeight = 40; if (btnMax) { btnMax.innerHTML = CONF.icons.maximize; btnMax.setAttribute('data-pk-tip', L.tip_maximize); } } if (btnMax) { btnMax.onclick = (e) => { if (e) e.stopPropagation(); el.classList.add('pk-no-transition'); const vp = UI.vp; const oldRowHeight = CONF.rowHeight; const centerIndex = (vp.scrollTop + vp.clientHeight / 2) / oldRowHeight; isWinMaximized = !isWinMaximized; btnMax.innerHTML = isWinMaximized ? CONF.icons.minimize : CONF.icons.maximize; btnMax.setAttribute('data-pk-tip', isWinMaximized ? L.tip_minimize : L.tip_maximize); const win = el.querySelector('.pk-win'); if (isWinMaximized) { win.classList.add('pk-maximized'); document.body.classList.add('pk-body-max'); CONF.rowHeight = 60; } else { win.classList.remove('pk-maximized'); document.body.classList.remove('pk-body-max'); CONF.rowHeight = 40; } if (typeof renderList === 'function') { renderList(); } else if (typeof renderVisible === 'function') { if(UI.in) UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; renderVisible(); } if (typeof refreshQuotaText === 'function') refreshQuotaText(); requestAnimationFrame(() => { void el.offsetHeight; const newRowHeight = CONF.rowHeight; const newVpHeight = vp.clientHeight; const targetScrollTop = (centerIndex * newRowHeight) - (newVpHeight / 2); vp.scrollTop = Math.max(0, targetScrollTop); renderVisible(); el.classList.remove('pk-no-transition'); }); }; } let modalZIndexCounter = 100000; function showModal(html) { let container = document.getElementById('pk-toast-container'); if (container) document.body.appendChild(container); const m = document.createElement('div'); m.className = 'pk-modal-ov'; m.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) { m.classList.add('pk-dark'); } m.innerHTML = `
${CONF.icons.close}
${html}
`; const actBars = m.querySelectorAll('.pk-modal-act'); actBars.forEach(bar => { const btns = Array.from(bar.children).filter(child => child.classList.contains('pk-btn')); if (!bar.querySelector('.pk-bl-btn') && (btns.length === 1 || btns.length === 2)) { bar.style.setProperty('display', 'grid', 'important'); bar.style.setProperty('grid-template-columns', btns.length === 1 ? '1fr' : '1fr 1fr', 'important'); bar.style.setProperty('gap', '15px', 'important'); bar.style.setProperty('width', '100%', 'important'); bar.style.setProperty('margin', '0', 'important'); bar.style.setProperty('margin-top', '20px', 'important'); btns.forEach(btn => { btn.style.setProperty('height', '46px', 'important'); btn.style.setProperty('border-radius', '12px', 'important'); btn.style.setProperty('font-size', '15px', 'important'); btn.style.setProperty('font-weight', '600', 'important'); btn.style.setProperty('justify-content', 'center', 'important'); btn.style.setProperty('padding', '0', 'important'); btn.style.setProperty('margin', '0', 'important'); btn.style.setProperty('min-width', '0', 'important'); if (btn.classList.contains('pri')) { btn.style.setProperty('background', 'var(--pk-pri)', 'important'); btn.style.setProperty('color', '#fff', 'important'); btn.style.setProperty('border', 'none', 'important'); btn.style.setProperty('transition', 'filter 0.2s', 'important'); } else { btn.style.setProperty('background', 'transparent', 'important'); btn.style.setProperty('color', 'var(--pk-fg)', 'important'); btn.style.setProperty('border', '1px solid transparent', 'important'); btn.onmouseover = () => btn.style.setProperty('background', 'var(--pk-hl)', 'important'); btn.onmouseout = () => btn.style.setProperty('background', 'transparent', 'important'); } }); } }); document.body.appendChild(m); m.querySelector('.pk-modal-close').addEventListener('click', () => m.remove()); return m; } function showAlert(msg, title = L.title_alert) { return new Promise((resolve) => { const m = showModal(`

${title}

${msg.replace(/\n/g, '
')}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '420px', padding: '30px', boxSizing: 'border-box' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelector('#alert_ok').onclick = () => { m.remove(); resolve(); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#alert_ok').click(); } }); }); } function showConfirm(msg, title = L.title_confirm) { return new Promise((resolve) => { const m = showModal(`

${title}

${esc(msg).replace(/\n/g, '
')}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '420px', height: 'auto', minHeight: 'auto', padding: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelector('#cfm_no').onclick = () => { m.remove(); resolve(false); }; m.querySelector('#cfm_yes').onclick = () => { m.remove(); resolve(true); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(false); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#cfm_yes').click(); } }); }); } const confirmSelectionClear = async () => { if (S.suppressClearConfirm) return true; return new Promise((resolve) => { const m = showModal(`

${L.title_confirm}

${esc(L.msg_clear_sel_confirm.replace('{n}', S.sel.size)).replace(/\n/g, '
')}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) Object.assign(modalBox.style, { width: '420px', padding: '30px' }); m.querySelector('#cfm_no').onclick = () => { m.remove(); resolve(false); }; m.querySelector('#cfm_yes').onclick = () => { const isChecked = m.querySelector('#pk_session_suppress').checked; if (isChecked) S.suppressClearConfirm = true; m.remove(); resolve(true); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(false); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#cfm_yes').click(); } }); }); }; function showPrompt(msg, val = '', title = L.title_prompt) { return new Promise((resolve) => { const cleanTitle = esc(msg).replace(/[::]$/, ''); const isNewFolder = (title === L.btn_newfolder); const okBtnText = isNewFolder ? L.btn_create : L.btn_ok; const m = showModal(`

${cleanTitle}

${title}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '480px', height: 'auto', minHeight: 'auto', padding: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const inp = m.querySelector('#prm_input'); const err = m.querySelector('#prm_err'); const okBtn = m.querySelector('#prm_ok'); const validate = () => { const v = inp.value.trim(); const isEmpty = v === ''; const isDup = S.items.some(item => item.name === v && item.name !== val); err.style.visibility = isDup ? 'visible' : 'hidden'; if (isDup || isEmpty) { okBtn.disabled = true; okBtn.style.opacity = '0.4'; okBtn.style.cursor = 'not-allowed'; inp.style.borderColor = isDup ? '#ff4d4f' : 'var(--pk-bd)'; } else if (v === val) { okBtn.disabled = false; okBtn.style.opacity = '1'; okBtn.style.cursor = 'pointer'; inp.style.borderColor = 'var(--pk-bd)'; } else { okBtn.disabled = false; okBtn.style.opacity = '1'; okBtn.style.cursor = 'pointer'; inp.style.borderColor = 'var(--pk-pri)'; } }; inp.focus(); if (val && val.includes('.') && val.lastIndexOf('.') > 0) { inp.setSelectionRange(0, val.lastIndexOf('.')); } else { inp.select(); } inp.addEventListener('input', validate); validate(); inp.onkeydown = (e) => { if (e.key === 'Enter' && !okBtn.disabled) okBtn.click(); if (e.key === 'Escape') { e.stopPropagation(); m.remove(); resolve(null); } }; m.querySelector('#prm_cancel').onclick = () => { m.remove(); resolve(null); }; m.querySelector('#prm_ok').onclick = () => { const v = inp.value.trim(); m.remove(); resolve(v); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); }; }); } function showToast(msg, type = 'success', duration = 0) { let container = document.getElementById('pk-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'pk-toast-container'; container.style.cssText = 'position:fixed; top:80px; left:50%; transform:translateX(-50%); display:flex; flex-direction:column; gap:12px; z-index:2147483647; pointer-events:none; align-items:center; zoom:var(--pk-zoom, 1);'; document.body.appendChild(container); } const t = document.createElement('div'); t.className = `pk-msg-toast ${type}`; const isDark = !!document.querySelector('.pk-ov.pk-dark'); if (isDark) t.classList.add('pk-dark'); t.style.cssText = 'position:relative; top:auto; left:auto; transform:translateY(-15px) scale(0.95); opacity:0; transition:all 0.3s cubic-bezier(0.23, 1, 0.32, 1); max-width:80vw;'; if (type === 'error' || type === 'warning') { const icon = CONF.icons.warning.replace('style="', 'style="flex-shrink: 0; '); t.innerHTML = `${icon}${msg}`; t.style.backgroundColor = type === 'warning' ? 'rgba(250, 173, 20, 0.95)' : 'rgba(217, 48, 37, 0.95)'; t.style.color = '#ffffff'; if (type === 'warning') t.style.border = '1px solid rgba(250, 173, 20, 0.2)'; } else { t.textContent = msg; } container.prepend(t); requestAnimationFrame(() => { t.style.transform = 'translateY(0) scale(1)'; t.style.opacity = '1'; }); const displayTime = duration > 0 ? duration : (type === 'success' ? 2200 : 3500); setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateY(-15px) scale(0.95)'; setTimeout(() => { if(t.parentNode) t.remove(); }, 300); }, displayTime); } function showBlacklistModal() { const icons = { paste: ``, delRow: ``, trash: ``, rocket: ``, save: `` }; const toastStyle = ` position: absolute; top: 120px; left: 50%; transform: translateX(-50%); background: var(--pk-toast-bg); backdrop-filter: blur(10px); color: var(--pk-toast-fg); border: 1px solid var(--pk-toast-bd); padding: 8px 24px; border-radius: 99px; font-size: 13px; z-index: 2000; pointer-events: none; opacity: 0; transition: opacity 0.3s, transform 0.3s; box-shadow: 0 8px 20px var(--pk-tip-sd); text-align: center; font-weight: 500; display: flex; align-items: center; gap: 8px; `; const textareaStyle = ` flex: 1; resize: none; border: 2px solid transparent; border-radius: 8px; padding: 15px; background: var(--pk-hl); color: var(--pk-fg); font-size: 13px; font-family: inherit; cursor: auto; outline: none; line-height: 1.6; letter-spacing: 0.3px; transition: background 0.2s, border-color 0.2s, box-shadow 0.2s; width: 100%; box-sizing: border-box; `; const modalInnerStyle = ` `; const m = showModal(` ${modalInnerStyle}

${L.title_blacklist}

${L.tip_bl_desc}
`); const modalEl = m.querySelector('.pk-modal'); Object.assign(modalEl.style, { width: '600px', maxWidth: '90vw', padding: '30px', height: '600px', maxHeight: '85vh' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); const areaFolder = m.querySelector('#bl_folder_input'); const areaFile = m.querySelector('#bl_file_input'); const radios = m.querySelectorAll('input[name="bl_mode"]'); const groupFolder = m.querySelector('#group_bl_folder'); const groupFile = m.querySelector('#group_bl_file'); radios.forEach(r => { r.onchange = () => { const mode = r.value; groupFolder.style.display = mode === 'folder' ? 'flex' : 'none'; groupFile.style.display = mode === 'file' ? 'flex' : 'none'; }; }); const toast = m.querySelector('#pk_bl_toast'); const showToast = (msg) => { toast.textContent = msg; toast.style.opacity = '1'; setTimeout(() => { toast.style.opacity = '0'; }, 2000); }; const loadLargeText = async (el, storageKey, originalPlaceholder) => { el.placeholder = L.str_loading_placeholder; await sleep(350); const data = await new Promise(r => setTimeout(() => r(gmGet(storageKey, '')), 0)); if (data) { const prevDisplay = el.style.display; el.style.display = 'none'; el.value = data; el.style.display = prevDisplay; } el.placeholder = originalPlaceholder; el.setSelectionRange(0, 0); el.blur(); }; loadLargeText(areaFolder, 'pk_blacklist_folders', L.ph_bl_folder); loadLargeText(areaFile, 'pk_blacklist', L.ph_bl_file); const highlightLine = (el) => { if (el.selectionStart !== el.selectionEnd) return; const val = el.value; if (!val) return; const cursor = el.selectionStart; let start = val.lastIndexOf('\n', cursor - 1); start = start === -1 ? 0 : start + 1; let end = val.indexOf('\n', cursor); if (end === -1) end = val.length; el.setSelectionRange(start, end); }; const enableLineSnap = (el) => { el.addEventListener('click', () => highlightLine(el)); el.addEventListener('keyup', (e) => { if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) highlightLine(el); }); }; enableLineSnap(areaFolder); enableLineSnap(areaFile); const setupSafeControls = (el, btnPaste, btnDel) => { btnPaste.onclick = async () => { try { const text = await navigator.clipboard.readText(); if (!text || !text.trim()) return showToast(L.msg_copy_empty); const cleanText = text.split(/\r?\n/).map(line => line.trim()).filter(line => line).join('\n'); const oldVal = el.value; const prefix = (oldVal && !oldVal.endsWith('\n')) ? '\n' : ''; el.value = oldVal + prefix + cleanText; el.scrollTop = el.scrollHeight; const addedCount = cleanText.split('\n').length; showToast(L.msg_add_success.replace('{n}', addedCount)); } catch (err) { showToast(L.err_clipboard_denied); } }; btnDel.onclick = () => { const val = el.value; if (!val) return; const selStart = el.selectionStart; const selEnd = el.selectionEnd; if (selStart === selEnd) { return showToast(L.msg_del_select); } let lineStart = val.lastIndexOf('\n', selStart - 1); lineStart = (lineStart === -1) ? 0 : lineStart + 1; let lineEnd = val.indexOf('\n', selEnd); if (lineEnd === -1) lineEnd = val.length; else lineEnd += 1; const newVal = val.substring(0, lineStart) + val.substring(lineEnd); el.value = newVal; el.setSelectionRange(lineStart, lineStart); highlightLine(el); el.focus(); showToast(L.msg_del_done); }; }; setupSafeControls(areaFolder, m.querySelector('#btn_paste_folder'), m.querySelector('#btn_del_folder')); setupSafeControls(areaFile, m.querySelector('#btn_paste_file'), m.querySelector('#btn_del_file')); m.querySelector('#bl_save').onclick = async () => { const fDir = areaFolder.value.trim(); const fFile = areaFile.value.trim(); gmSet('pk_blacklist_folders', fDir); gmSet('pk_blacklist', fFile); S.updateBlCache(); m.remove(); renderVisible(); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); e.stopPropagation(); m.querySelector('#bl_save').click(); } }); m.querySelector('#bl_clear').onclick = async () => { if (areaFolder.value.trim() === '' && areaFile.value.trim() === '') return; if (await showConfirm(L.msg_bl_clear_confirm, L.title_confirm)) { areaFolder.value = ""; areaFile.value = ""; gmSet('pk_blacklist_folders', ""); gmSet('pk_blacklist', ""); S.updateBlCache(); renderVisible(); showToast(L.str_cleanup_done); } }; m.querySelector('#bl_run').onclick = async () => { if (S.movingIds && S.movingIds.size > 0) { showAlert(L.err_task_conflict); return; } const fDir = areaFolder.value.trim(); const fFile = areaFile.value.trim(); gmSet('pk_blacklist_folders', fDir); gmSet('pk_blacklist', fFile); S.updateBlCache(); const isGlobalSearch = S.path.some(p => p.id === 'virtual_search_root'); if (S.trashMode || S.shareMode || S.offlineMode || S.starredMode || S.recentMode || S.historyMode || S.isFlattened || S.dupMode || S.analyzeMode || isGlobalSearch) { m.remove(); showAlert(L.msg_bl_run_limit); return; } m.remove(); setLoad(true); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; const parseBigTextAsync = async (text, typeLabel) => { const set = new Set(); if (!text) return set; const len = text.length; let start = 0; let end = 0; let count = 0; let lastYieldTime = performance.now(); updateLoadTxt(`${L.str_analyzing}\n${typeLabel}... 0%`); await sleep(16); while (start < len) { if (!S.scanning || signal.aborted || myScanId !== S.scanId) return set; end = text.indexOf('\n', start); if (end === -1) end = len; const line = text.substring(start, end).trim().toLowerCase(); if (line) set.add(line); start = end + 1; count++; if (count % 2000 === 0) { const now = performance.now(); if (now - lastYieldTime > 16) { const progress = Math.min(100, Math.round((start / len) * 100)); updateLoadTxt(`${L.str_analyzing}\n${typeLabel}... ${progress}% (${set.size})`); await sleep(0); lastYieldTime = performance.now(); } } } return set; }; UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); }; try { const folderText = areaFolder.value || ''; const fileText = areaFile.value || ''; if (folderText) S.blFolderSet = await parseBigTextAsync(folderText, L.lbl_type_folder); else S.blFolderSet = new Set(); if (S.scanning && !signal.aborted && myScanId === S.scanId) { if (fileText) S.blSet = await parseBigTextAsync(fileText, L.lbl_type_file); else S.blSet = new Set(); } if (!S.scanning || signal.aborted || myScanId !== S.scanId) { if (myScanId === S.scanId) setLoad(false); return; } if (S.blSet.size === 0 && S.blFolderSet.size === 0) { setLoad(false); showAlert(L.msg_bl_empty); return; } updateLoadTxt(L.str_init_scan); await sleep(50); const foundMatches = []; const rootNodes = [{ id: '', name: 'Root', lineage: [], retryCount: 0 }]; await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { const cleanName = f.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (S.blSet.has(cleanName)) { f._lineage = parent.lineage || []; foundMatches.push({ item: f, type: 'FILE' }); } }, onFolder: (folder, filesInFolder, nextSubFolders) => { const cleanName = folder.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const isSystemRootFolder = (folder.parent_id === '' || folder.parent_id === 'root') && folder.name === CONF.SYSTEM_FOLDER_NAME; if (S.blFolderSet.has(cleanName) && !isSystemRootFolder) { folder._lineage = (folder.lineage || []).slice(0, -1); foundMatches.push({ item: folder, type: 'FOLDER' }); nextSubFolders.length = 0; } }, onProgress: (st) => { const folderText = `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const statusInfo = ` | ${L.str_hits}: ${foundMatches.length} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo); } }); if (!S.scanning || signal.aborted || myScanId !== S.scanId) { if (myScanId === S.scanId) setLoad(false); return; } setLoad(false); if (foundMatches.length === 0) { showAlert(L.msg_blacklist_run_none); return; } showPreviewModal(foundMatches); } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { setLoad(false); showAlert(`${L.str_scan_error}: ${e.message}`); } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } }; } function showPreviewModal(matches) { const modalHtml = `

${L.modal_bl_preview.replace('{n}', matches.length)}

${L.col_type}
${L.col_name}
${L.col_path}
`; const m = showModal(modalHtml); const modalEl = m.querySelector('.pk-modal'); Object.assign(modalEl.style, { width: "800px", maxWidth: "90vw", height: "80vh", padding: "0", display: "flex", flexDirection: "column" }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: "24px", right: "24px" }); const listDiv = m.querySelector('#bl_prev_list'); const listIn = m.querySelector('#bl_prev_in'); const chkAll = m.querySelector('#bl_prev_all'); const btnDel = m.querySelector('#bl_prev_del'); const topStat = m.querySelector('#bl_top_stat'); const ROW_HEIGHT = 44; listIn.style.height = `${matches.length * ROW_HEIGHT}px`; const selectedIds = new Set(matches.map(m => m.item.id)); let lastIdx = -1; const updateCount = () => { const c = selectedIds.size; if (topStat) { topStat.innerHTML = L.str_bl_stat .replace('{n}', matches.length) .replace('{m}', `${c}`); } btnDel.disabled = c === 0; btnDel.style.opacity = c === 0 ? 0.5 : 1; btnDel.style.cursor = c === 0 ? 'not-allowed' : 'pointer'; chkAll.checked = c > 0 && c === matches.length; chkAll.indeterminate = c > 0 && c < matches.length; }; updateCount(); let isRenderScheduled = false; const renderPreviewList = () => { const top = listDiv.scrollTop; const h = listDiv.clientHeight || 500; const buffer = 15; const start = Math.max(0, Math.floor(top / ROW_HEIGHT) - buffer); const end = Math.min(matches.length, Math.ceil((top + h) / ROW_HEIGHT) + buffer); listIn.innerHTML = ''; const fragment = document.createDocumentFragment(); for (let i = start; i < end; i++) { const mMatch = matches[i]; const row = document.createElement('div'); row.dataset.id = mMatch.item.id; row.style.cssText = `position:absolute; top:${i * ROW_HEIGHT}px; width:100%; display:grid; grid-template-columns: 40px 50px 1fr 1fr; padding:0; border-bottom:1px dashed var(--pk-bd); font-size:13px; align-items:center; height:${ROW_HEIGHT}px; transition:background 0.1s; cursor:pointer;`; row.onmouseover = () => row.style.backgroundColor = 'var(--pk-hl)'; row.onmouseout = () => row.style.backgroundColor = 'transparent'; const isFolder = mMatch.type === 'FOLDER'; const it = mMatch.item; const mime = (it.mime_type || '').toLowerCase(); const isMedia = mime.startsWith('video/') || mime.startsWith('image/'); if (isFolder && (!it.thumbnail_link || it.thumbnail_link === it.icon_link) && typeof globalCache !== 'undefined') { const scanDeepCover = (targetId, depth) => { if (depth > 5) return null; const raw = globalCache.get(targetId); if (!raw) return null; const files = (raw && !Array.isArray(raw) && raw.items) ? raw.items : raw; if (!files || files.length === 0) return null; const vid = files.find(f => f.mime_type?.startsWith('video/') && f.thumbnail_link); if (vid) return vid.thumbnail_link; const img = files.find(f => f.mime_type?.startsWith('image/') && f.thumbnail_link); if (img) return img.thumbnail_link; const subFolders = files.filter(f => f.kind === 'drive#folder'); for (const sub of subFolders) { if (globalCache.has(sub.id)) { const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; continue; } if (sub.thumbnail_link && sub.thumbnail_link !== sub.icon_link && !sub._coverResolved) { return sub.thumbnail_link; } const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; } return null; }; const foundThumb = scanDeepCover(it.id, 0); if (foundThumb) { it.thumbnail_link = foundThumb; } } const hasCover = it.thumbnail_link && it.thumbnail_link !== it.icon_link; const fallbackSvg = getIcon(it).replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"'); let iconHtml = ''; if (!isFolder && isMedia && hasCover) { iconHtml = ``; const secondFallback = it.icon_link ? `${fallbackSvg}` : fallbackSvg; iconHtml += `${secondFallback}`; } else { const iconSrc = it.icon_link; iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; } const lineage = mMatch.item._lineage ||[]; const relativePath = lineage.map(x => x.name).join('/'); const homeIcon = ``; row.ondragstart = (e) => { e.preventDefault(); return false; }; let pathHtml = `
${homeIcon}${esc(L.btn_nav_home)}`; if (relativePath) { pathHtml += `/${esc(relativePath)}`; } pathHtml += `
`; const homeGroupTip = `${homeIcon}${esc(L.btn_nav_home)}`; const fullPathTip = `
${homeGroupTip}${relativePath ? '/' + esc(relativePath) : ''}
`; const thumbAttr = (typeof hasCover !== 'undefined' && hasCover) ? `data-pk-thumb="${it.thumbnail_link}"` : ''; const isChecked = selectedIds.has(mMatch.item.id); row.innerHTML = `
${iconHtml}
${esc(mMatch.item.name)}
${pathHtml}
`; const chk = row.querySelector('input'); row.onclick = (e) => { const curIdx = i; const id = mMatch.item.id; if (e.target === chk && !e.shiftKey && !e.ctrlKey && !e.metaKey) return; if (e.shiftKey && lastIdx !== -1) { const s = Math.min(lastIdx, curIdx); const eIdx = Math.max(lastIdx, curIdx); if (!e.ctrlKey && !e.metaKey) selectedIds.clear(); for (let k = s; k <= eIdx; k++) selectedIds.add(matches[k].item.id); } else if (e.ctrlKey || e.metaKey) { if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id); } else { selectedIds.clear(); selectedIds.add(id); } lastIdx = curIdx; updateCount(); renderPreviewList(); }; chk.onchange = (e) => { if(e.target.checked) { selectedIds.add(mMatch.item.id); lastIdx = i; } else selectedIds.delete(mMatch.item.id); updateCount(); renderPreviewList(); }; fragment.appendChild(row); } listIn.appendChild(fragment); }; listDiv.onscroll = () => { if (!isRenderScheduled) { requestAnimationFrame(() => { renderPreviewList(); isRenderScheduled = false; }); isRenderScheduled = true; } }; renderPreviewList(); m.addEventListener('click', (e) => { if (m._blockClick) return; if (e.target.closest('[data-id]') || e.target.closest('button') || e.target.closest('input') || e.target.closest('label')) return; if (selectedIds.size > 0) { selectedIds.clear(); lastIdx = -1; updateCount(); renderPreviewList(); } }); chkAll.onchange = (e) => { const checked = e.target.checked; if(checked) { matches.forEach(m => selectedIds.add(m.item.id)); } else { selectedIds.clear(); } updateCount(); renderPreviewList(); }; const mqBox = document.createElement('div'); mqBox.className = 'pk-bl-mq'; listDiv.appendChild(mqBox); let isDragging = false, isMarquee = false, startX = 0, startY = 0; let blScrollSpeed = 0, blScrollRaf = null, blLastX = 0, blLastY = 0, blLastCtrl = false; listDiv.onmousedown = (e) => { if (e.button !== 0 || e.target.closest('input')) return; const rect = listDiv.getBoundingClientRect(); isDragging = true; isMarquee = false; startX = e.clientX - rect.left; startY = e.clientY - rect.top + listDiv.scrollTop; const rawStartX = e.clientX, rawStartY = e.clientY; const updateMarqueeBox = () => { const curRect = listDiv.getBoundingClientRect(); const curX = blLastX - curRect.left; const maxScrollHeight = listIn.offsetHeight; const curY = Math.max(0, Math.min(maxScrollHeight, blLastY - curRect.top + listDiv.scrollTop)); const top = Math.min(startY, curY); const left = Math.min(startX, curX); const width = Math.abs(curX - startX); const height = Math.abs(curY - startY); mqBox.style.display = 'block'; mqBox.style.top = top + 'px'; mqBox.style.left = left + 'px'; mqBox.style.width = width + 'px'; mqBox.style.height = height + 'px'; const sIdx = Math.floor(top / ROW_HEIGHT); const eIdx = Math.floor((top + height) / ROW_HEIGHT); for (let i = 0; i < matches.length; i++) { const isInside = i >= sIdx && i <= eIdx; if (isInside) { selectedIds.add(matches[i].item.id); } else if (!blLastCtrl) { selectedIds.delete(matches[i].item.id); } } updateCount(); if (!isRenderScheduled) { requestAnimationFrame(() => { renderPreviewList(); isRenderScheduled = false; }); isRenderScheduled = true; } }; const runBlScroll = () => { if (!isMarquee || blScrollSpeed === 0) { blScrollRaf = null; return; } listDiv.scrollTop += blScrollSpeed; updateMarqueeBox(); blScrollRaf = requestAnimationFrame(runBlScroll); }; const onMouseMove = (me) => { if (!isDragging) return; if (!isMarquee) { if (Math.abs(me.clientX - rawStartX) > 5 || Math.abs(me.clientY - rawStartY) > 5) { isMarquee = true; if (!me.ctrlKey && !me.metaKey && !me.shiftKey) { selectedIds.clear(); lastIdx = -1; updateCount(); renderPreviewList(); } } else return; } blLastX = me.clientX; blLastY = me.clientY; blLastCtrl = me.ctrlKey || me.metaKey; const curRect = listDiv.getBoundingClientRect(); if (blLastY > curRect.bottom - 5) blScrollSpeed = Math.min(45, 2 + Math.pow((blLastY - curRect.bottom + 5) / 5, 1.3)); else if (blLastY < curRect.top + 5) blScrollSpeed = -Math.min(45, 2 + Math.pow((curRect.top + 5 - blLastY) / 5, 1.3)); else blScrollSpeed = 0; if (blScrollSpeed !== 0 && !blScrollRaf) blScrollRaf = requestAnimationFrame(runBlScroll); updateMarqueeBox(); }; const onMouseUp = () => { if (isDragging && mqBox.style.display === 'block') { m._blockClick = true; setTimeout(() => m._blockClick = false, 100); } isDragging = false; blScrollSpeed = 0; if (blScrollRaf) { cancelAnimationFrame(blScrollRaf); blScrollRaf = null; } mqBox.style.display = 'none'; window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }; m.querySelector('#bl_prev_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 50); m.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) { e.preventDefault(); e.stopPropagation(); matches.forEach(m => selectedIds.add(m.item.id)); updateCount(); renderPreviewList(); } }, true); btnDel.onclick = async () => { if (selectedIds.size === 0) return; const confirmed = await showConfirm(L.warn_del.replace('{n}', selectedIds.size)); if (!confirmed) return; m.remove(); const allIds = Array.from(selectedIds).filter(id => { const match = matches.find(m => m.item.id === id); if (match && isSystemItem(match.item)) { console.warn(`[Security] Prevented deletion of system folder: ${match.item.name}`); return false; } return true; }); if (allIds.length < selectedIds.size) { console.log("System items were removed from deletion list."); } const itemsToDelete = matches.filter(m => allIds.includes(m.item.id)).map(m => m.item); await executeBatchDelete(allIds, { silent: false, explicitItems: itemsToDelete }); }; } function setLoad(b, isInline = false) { S.loading = b; if (b) { if (isInline) { UI.loader.style.display = 'none'; } else { UI.loader.style.display = 'flex'; if (UI.loadTxt) UI.loadTxt.textContent = L.loading_detail; } } else { UI.loader.style.display = 'none'; } } function updateLoadTxt(txt) { if (UI.loadTxt) UI.loadTxt.innerText = txt; } let activeLoadId = 0; const computeDuplicateGroups = async (candidates, cfg, isRunningFn) => { const groups =[]; const assigned = new Set(); const strictness = gmGet('pk_dup_strictness', 'strict'); const sizeRatioLimit = (strictness === 'loose') ? 0.10 : 0.05; if (isRunningFn()) { updateLoadTxt(L.str_analyzing); const hashMap = new Map(); for (const item of candidates) { const hash = item.gcid || item.md5_checksum || item.hash; const key = hash ? `${hash}|${item.size}` : null; if (key) { if (!hashMap.has(key)) hashMap.set(key,[]); hashMap.get(key).push(item); } } for (const[key, items] of hashMap) { if (items.length > 1) { const ids = items.map(i => i.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_hash); }); groups.push({ ids: ids, type: L.tag_hash }); } } } if (isRunningFn() && cfg.video) { updateLoadTxt(L.str_analyzing); const simCandidates = candidates.filter(i => i.mime_type.startsWith('video') && !assigned.has(i.id)); const validVideos = simCandidates.filter(item => (parseFloat(item.params?.duration || 0) > 0)); validVideos.sort((a, b) => parseFloat(a.params?.duration || 0) - parseFloat(b.params?.duration || 0)); let processedCount = 0; const totalCount = validVideos.length; for (let i = 0; i < totalCount; i++) { processedCount++; if (processedCount % 500 === 0) { if (!isRunningFn()) break; const pct = Math.round(processedCount / totalCount * 100); updateLoadTxt(`${L.str_analyzing} (${pct}%)`); await sleep(0); } if (assigned.has(validVideos[i].id)) continue; const root = validVideos[i]; const rootDur = parseFloat(root.params?.duration || 0); const rootSize = parseInt(root.size || 0); const groupItems = [root]; for (let j = i + 1; j < totalCount; j++) { const target = validVideos[j]; if (assigned.has(target.id)) continue; const durThreshold = (strictness === 'loose') ? 2.0 : 1.0; const targetDur = parseFloat(target.params?.duration || 0); const durDiff = Math.abs(targetDur - rootDur); if (durDiff > durThreshold) break; const targetSize = parseInt(target.size || 0); if (rootSize > 0 && targetSize > 0) { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); const ratio = sizeDiff / maxBase; if (ratio <= sizeRatioLimit) { groupItems.push(target); } } } if (groupItems.length > 1) { const ids = groupItems.map(x => x.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_sim); }); groups.push({ ids: ids, type: L.tag_sim }); } } } if (isRunningFn()) { updateLoadTxt(L.str_analyzing); const cleanNameAd = (oldName) => { let cleanName = oldName; cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, ''); cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, ''); const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])"; const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\\\s)', 'i'); cleanName = cleanName.replace(atRegex, ''); const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i'); cleanName = cleanName.replace(hyphenRegex, ''); cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, ''); cleanName = cleanName.replace(/^[-_. ,,::;;\\p{Extended_Pictographic}]+/u, ''); const pairs = [['【','】'], ['[',']'], ['《','》'],['<','>'], ['(',')'],['(',')'], ['{','}']]; pairs.forEach(([L_char, R_char]) => { const idxR_Fix = cleanName.indexOf(R_char); const idxL_Check = cleanName.indexOf(L_char); if (idxR_Fix > 0 && idxR_Fix <= 10 && (idxL_Check === -1 || idxL_Check > idxR_Fix)) { cleanName = L_char + cleanName; } const chars = cleanName.split(''); const stack =[]; const toRemove = new Set(); for (let i = 0; i < chars.length; i++) { const c = chars[i]; if (c === L_char) { stack.push(i); } else if (c === R_char) { if (stack.length > 0) stack.pop(); else toRemove.add(i); } } stack.forEach(i => toRemove.add(i)); if (toRemove.size > 0) { cleanName = chars.filter((_, i) => !toRemove.has(i)).join(''); } }); const quote2 = (cleanName.match(/'/g) ||[]).length; if (quote2 % 2 !== 0) cleanName = cleanName.replace(/"/, ''); let result = cleanName.trim(); const lastDot = result.lastIndexOf('.'); if (lastDot > 0) result = result.substring(0, lastDot); let finalResult = result ? result.toLowerCase() : oldName.replace(/\.[^/.]+$/, "").toLowerCase().trim(); if (strictness === 'loose') { finalResult = finalResult.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ''); } return finalResult; }; const remainingItems = candidates.filter(i => !assigned.has(i.id)); const getTypeGroup = (mime) => { if (!mime) return 'other'; if (mime.startsWith('video')) return 'video'; if (mime.startsWith('image')) return 'image'; return 'other'; }; const typeNameMap = new Map(); for (const item of remainingItems) { const tGroup = getTypeGroup(item.mime_type); const cleaned = cleanNameAd(item.name); if (!cleaned) continue; const key = tGroup + '|' + cleaned; if (!typeNameMap.has(key)) typeNameMap.set(key,[]); typeNameMap.get(key).push(item); } for (const [key, items] of typeNameMap) { if (items.length > 1) { const sortedForAlgo = [...items].sort((a, b) => parseInt(a.size || 0) - parseInt(b.size || 0)); let currentGroup = [sortedForAlgo[0]]; const tempGroups =[]; for (let i = 1; i < sortedForAlgo.length; i++) { const target = sortedForAlgo[i]; const root = currentGroup[0]; const rootSize = parseInt(root.size || 0); const targetSize = parseInt(target.size || 0); let isMatch = false; if (rootSize === 0 && targetSize === 0) { isMatch = true; } else if (rootSize > 0 && targetSize > 0) { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); if ((sizeDiff / maxBase) <= sizeRatioLimit) { isMatch = true; } } if (isMatch) currentGroup.push(target); else { if (currentGroup.length > 1) tempGroups.push(currentGroup); currentGroup = [target]; } } if (currentGroup.length > 1) tempGroups.push(currentGroup); tempGroups.forEach(grp => { const ids = grp.map(i => i.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_name); }); groups.push({ ids: ids, type: L.tag_name }); }); } } } return groups; }; async function deepPreload(currentItems) { if (S.scanning) return; const folderItems = currentItems.filter(item => item.kind === 'drive#folder'); if (folderItems.length === 0) return; const preloadTasks = folderItems.map(folder => { if (S.cache.has(folder.id)) return Promise.resolve(); if (typeof globalCache !== 'undefined' && globalCache.has(folder.id)) { S.cache.set(folder.id, globalCache.get(folder.id)); return Promise.resolve(); } return apiList(folder.id, 1000) .then(files => { S.cache.set(folder.id, files); if (typeof globalCache !== 'undefined') globalCache.set(folder.id, files); }) .catch(e => { }); }); const MAX_CONCURRENT_PRELOADS = 5; let activePromises = []; for(let i = 0; i < preloadTasks.length; i++) { activePromises.push(preloadTasks[i]); if(activePromises.length >= MAX_CONCURRENT_PRELOADS) { await Promise.race(activePromises.map(p => p.then(() => 0).catch(() => 0))); activePromises = activePromises.filter(p => p.isResolved); } } await Promise.allSettled(activePromises); console.log("Deep preload finished."); } async function load(isHistoryNav = false, forceUpdate = false) { if (S.scanning) return; const snapshot = { trash: S.trashMode, share: S.shareMode, starred: S.starredMode, recent: S.recentMode, pathId: S.path[S.path.length - 1]?.id || '' }; const cur = S.path[S.path.length - 1]; const folderId = cur.id || 'root'; if (S.clipType === 'move' && S.movingIds.size > 0 && folderId === S.movingSourceId) { console.log(`[Cache Guard] Detected active move from this source: ${folderId}. Forcing network sync.`); forceUpdate = true; } let realCacheKey = S.getRealCacheKey(folderId); const isAnalyzeSub = S.path.some(p => p.id === 'analyze_root'); if (isAnalyzeSub) { S.analyzeMode = true; S.sort = 'size'; S.dir = 1; } if (folderId === 'analyze_root') { renderCrumb(); if (UI.scan) UI.scan.style.display = 'none'; S.items = [...(S.analyzeResultItems || [])]; S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); setLoad(false); refresh(); updateStat(); return; } if (globalDirtyFolders.has(folderId) || (folderId === 'root' && globalDirtyFolders.has(''))) { console.log(`[Load] ${folderId} is dirty. Forcing network fetch.`); forceUpdate = true; globalDirtyFolders.delete(folderId); globalDirtyFolders.delete(''); globalDirtyFolders.delete('root'); } S.clearSelection(); if (typeof UI !== 'undefined' && UI.vp && !forceUpdate) { UI.vp.scrollTop = 0; } if (UI.win) UI.win.setAttribute('data-cur-pid', folderId); activeLoadId++; const currentId = activeLoadId; let nextToken = null; const fetchedIds = new Set(); let isResuming = false; if (S.abortController) { S.abortController.abort(); } S.abortController = new AbortController(); const signal = S.abortController.signal; const isInVirtualSearch = S.path.some(p => p.id === 'virtual_search_root'); if (!isInVirtualSearch) { S.search = ''; if (UI.searchInput) { UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; } } if (cur.id === 'virtual_search_root') { renderCrumb(); if (UI.scan) UI.scan.style.display = 'none'; setLoad(false); refresh(); return; } const isInVirtualStack = S.path.some(p => p.id === 'virtual_search_root'); if (isInVirtualStack) { if (UI.chkGlobal) UI.chkGlobal.checked = true; } UI.scan.style.display = (S.trashMode || S.shareMode || S.offlineMode) ? 'none' : 'flex'; UI.btnExit.style.display = 'none'; if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (UI.offTools) UI.offTools.style.display = S.offlineMode ? 'flex' : 'none'; if (UI.upTools) UI.upTools.style.display = S.uploadMode ? 'flex' : 'none'; if (UI.crumb) UI.crumb.style.display = ''; if (UI.searchInput && UI.searchInput.parentNode) { UI.searchInput.parentNode.style.display = S.trashMode ? 'none' : 'flex'; } if (UI.cntFolderFirst) { UI.cntFolderFirst.style.display = S.trashMode ? 'none' : 'flex'; } if (UI.btnAnalyze) { UI.btnAnalyze.style.display = (S.trashMode || S.shareMode || S.offlineMode) ? 'none' : 'flex'; } if (UI.lblGlobal) { UI.lblGlobal.style.display = (S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode || S.offlineMode || S.uploadMode || S.isFlattened || S.dupMode || S.analyzeMode) ? 'none' : 'flex'; if (S.trashMode && UI.chkGlobal) { UI.chkGlobal.checked = false; } if (isInVirtualSearch) { UI.chkGlobal.checked = true; } } S.dupMode = false; S.lastSelIdx = -1; if (!isAnalyzeSub) { if (S.analyzeMode && UI.chkSearchPath) UI.chkSearchPath.checked = false; S.analyzeMode = false; } if (UI.btnNewFolder) { UI.btnNewFolder.style.display = (S.trashMode || S.shareMode) ? 'none' : 'flex'; } S.isFlattened = false; renderCrumb(); if (folderId === 'root' && !S.preloaded) { if (typeof globalCache !== 'undefined' && globalCache.has('root')) { S.cache.set('root', globalCache.get('root')); S.preloaded = true; } else if (S.preLoadPromise) { let preLoadSuccess = false; try { const TIMEOUT_LIMIT = isTurbo ? 1000 : 200; const raceResult = await Promise.race([ S.preLoadPromise, new Promise(r => setTimeout(() => r('TIMEOUT'), TIMEOUT_LIMIT)) ]); if (globalCache.has('root') || raceResult !== 'TIMEOUT') { S.cache.set('root', globalCache.get('root')); S.preloaded = true; preLoadSuccess = true; } } catch(e) {} if (!preLoadSuccess) { console.log("[Load] Preload too slow or failed, forcing direct network fetch."); forceUpdate = true; } } else { forceUpdate = true; } } const syncStars = () => { for (let k = 0; k < S.items.length; k++) { const it = S.items[k]; if (it.starred || (it.tags && it.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(it.id); } } }; if (!forceUpdate) { let cachedData = null; if (S.cache.has(realCacheKey)) { cachedData = S.cache.get(realCacheKey); } else if (typeof globalCache !== 'undefined') { let lookupKey = realCacheKey; if (realCacheKey === 'root' && globalCache.has('')) lookupKey = ''; if (globalCache.has(lookupKey)) { cachedData = globalCache.get(lookupKey); S.cache.set(realCacheKey, cachedData); } } if (cachedData) { if (cachedData && !Array.isArray(cachedData) && cachedData.items) { S.items = [...cachedData.items]; nextToken = cachedData.nextToken; isResuming = !!nextToken; if (isResuming) S.items.forEach(it => fetchedIds.add(it.id)); } else { S.items = Array.isArray(cachedData) ? [...cachedData] : []; nextToken = null; } if (S.analyzeMode && S.analyzeMap) { S.items.forEach(item => { if (item.kind === 'drive#folder' && S.analyzeMap.has(item.id)) { item.size = S.analyzeMap.get(item.id).size.toString(); } }); } S.itemMap.clear(); for (let k = 0; k < S.items.length; k++) { S.itemMap.set(S.items[k].id, S.items[k]); } syncStars(); refresh(); updateStat(); if (S.offlineMode) { const session = globalCache.get('offline_session'); if (session && session.nextToken && !session.completed) { console.log(`[Resuming] Cache snapshot rendered. Continuing pagination from ${S.items.length}...`); isResuming = true; S.items.forEach(it => fetchedIds.add(it.id)); } else { setLoad(false); return; } } else if (!nextToken) { setLoad(false); if (window.pkSmartRefreshTrigger) { console.log(`[SWR] Intent detected: Validating ${realCacheKey} in background...`); window.pkSmartRefreshTrigger(true); } const visibleSubFolders = S.items.filter(f => f.kind === 'drive#folder'); for (let i = visibleSubFolders.length - 1; i >= 0; i--) { const sub = visibleSubFolders[i]; if (!scannedFolderIds.has(sub.id) && !globalCache.has(sub.id)) { backgroundQueue.push({ id: sub.id, name: sub.name, retryCount: 0 }); scannedFolderIds.add(sub.id); } } runBackgroundCrawler(); return; } else { console.log(`[Resuming] Cache snapshot rendered. Continuing pagination from ${S.items.length}...`); } } } if (!nextToken && !isResuming) { S.items = []; S.display = []; S.itemMap.clear(); refresh(); setLoad(true, true); updateLoadTxt(L.loading_detail); } isGUISensitive = false; const currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) { if (!isResuming) { setLoad(true); updateLoadTxt(L.str_waiting_token); } const isAuthReady = await waitForAuth(15000); if (!isAuthReady) console.warn("PikPak Master: Auth token wait timeout."); } if (!isResuming && el) { el.focus(); } UI.stopBtn.onclick = () => { S.abortController.abort(); if (S.loading && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.shareMode && !S.offlineMode && S.path.length > 1) { const canceledNode = S.path.pop(); console.log(`[Navigation] Cancelled entry to: ${canceledNode.name}, rolling back path.`); renderCrumb(); const parentNode = S.path[S.path.length - 1]; const parentKey = S.getRealCacheKey(parentNode.id); const cached = (typeof globalCache !== 'undefined') ? globalCache.get(parentKey) : null; if (cached) { S.items = Array.isArray(cached) ? [...cached] : (cached.items || []); ensureItemMap(); refresh(); } } setLoad(false); isGUISensitive = false; }; try { let pageCount = 0; const limit = 500; let totalItemsLoaded = S.items.length; do { if (currentId !== activeLoadId) return; if (signal.aborted) throw new DOMException('Aborted', 'AbortError'); const isStarredRoot = S.starredMode && S.path.length === 1; let data = {}; if (S.offlineMode) { if (pageCount > 0) break; if (!isResuming) { S.items = []; S.itemMap.clear(); fetchedIds.clear(); totalItemsLoaded = 0; } let session = isResuming ? globalCache.get('offline_session') : { phase: 'active', nextToken: null, completed: false }; if (!isResuming) globalCache.set('offline_session', session); const handleBatch = (batchTasks, nextToken, currentPhase) => { if (signal.aborted || currentId !== activeLoadId) return; const uniqueTasks = batchTasks.filter(t => !fetchedIds.has(t.id) && !S.itemMap.has(t.id)); if (uniqueTasks.length === 0) return; const newItems = uniqueTasks.map(t => { const ref = t.reference_resource || {}; return { id: t.id, kind: 'drive#task', name: ref.name || t.name || t.file_name || 'Untitled Task', size: t.file_size, phase: t.phase, progress: parseInt(t.progress || 0), message: t.message, icon_link: t.icon_link, thumbnail_link: ref.thumbnail_link ? ref.thumbnail_link : t.icon_link, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || '', file_id: t.file_id || '', source_url: (t.params && t.params.url) ? t.params.url : '', params: Object.assign({}, t.params || {}, ref.params || {}), mime_type: ref.mime_type || '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))) }; }); S.items.push(...newItems); newItems.forEach(f => { fetchedIds.add(f.id); S.itemMap.set(f.id, f); }); totalItemsLoaded = S.items.length; session.phase = currentPhase; session.nextToken = nextToken; session.completed = false; globalCache.set('offline_session', session); globalCache.set('offline_root', { items: [...S.items], fetchedIds: fetchedIds }); S.items.sort((a, b) => new Date(b.created_time || 0) - new Date(a.created_time || 0)); if (UI.loader.style.display !== 'none') setLoad(false); refresh(); updateStat(); }; await apiTaskList(500, handleBatch, session, signal); if (signal.aborted || currentId !== activeLoadId) return; const finalSession = { phase: 'done', nextToken: null, completed: true }; globalCache.set('offline_session', finalSession); data = { files: [], next_page_token: null }; break; } else if (S.historyMode && S.path.length === 1) { if (pageCount > 0) break; let keys = []; if (typeof GM_listValues !== 'undefined') { keys = GM_listValues(); } else { keys = Object.keys(localStorage); } const historyItems = []; const historyMap = new Map(); keys.forEach(k => { if (k.startsWith('pk_progress_')) { const id = k.replace('pk_progress_', ''); const raw = gmGet(k); let data = { t: 0, d: 0, ts: 0 }; if (typeof raw === 'number') { data.t = raw; data.ts = 0; } else if (typeof raw === 'object' && raw !== null) { data = raw; } if (data.t > 1 || data.ts > 0) { historyItems.push({ id, ...data }); historyMap.set(id, data); } } }); historyItems.sort((a, b) => b.ts - a.ts); const topItems = historyItems.slice(0, 1000); const targetIds = topItems.map(x => x.id); if (targetIds.length > 0) { const fetchDetail = async (id) => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${id}?thumbnail_size=SIZE_MEDIUM`, { headers: getHeaders() }); if (!res.ok) return null; const f = await res.json(); if (f && !f.trashed) return f; return null; } catch (e) { return null; } }; const validFiles = []; const CONCURRENCY = 6; for (let i = 0; i < targetIds.length; i += CONCURRENCY) { if (signal.aborted) break; const chunk = targetIds.slice(i, i + CONCURRENCY); const results = await Promise.all(chunk.map(id => fetchDetail(id))); results.forEach(rawF => { if (rawF) { const f = minifyFile(rawF); const local = historyMap.get(f.id) || {}; f._history_progress = local.t || 0; const cloudDur = f.params?.duration || 0; f._history_duration = cloudDur || local.d || 0; f._history_ts = local.ts || 0; validFiles.push(f); } else { } }); if (validFiles.length % 20 === 0) await sleep(10); } data = { files: validFiles, next_page_token: null }; } else { data = { files: [], next_page_token: null }; } } else if (S.recentMode && S.path.length === 1) { const filters = encodeURIComponent('{"phase":{"in":"PHASE_TYPE_COMPLETE"}}'); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?limit=${limit}&filters=${filters}&thumbnail_size=SIZE_MEDIUM&with_reference_resource=true&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const res = await fetch(url, { headers: getHeaders(), signal: signal }); if (!res.ok) throw new Error("Recent API " + res.status); const json = await res.json(); const validTasks = (json.tasks || []).filter(t => t.phase === 'PHASE_TYPE_COMPLETE' && (t.type === 'offline' || t.type === 'upload') && t.file_id !== "" ); data = { files: validTasks.map(t => { const ref = t.reference_resource || {}; const mime = ref.mime_type || ''; const isFolder = (ref.kind === 'drive#folder') || (mime === 'application/x-directory') || (t.icon_link && t.icon_link.includes('folder')); return { id: t.file_id || t.id, kind: isFolder ? 'drive#folder' : 'drive#file', name: ref.name || t.file_name || t.name, size: t.file_size, thumbnail_link: ref.thumbnail_link || t.icon_link || '', icon_link: t.icon_link || '', web_content_link: t.file_id ? null : null, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || t.created_time, mime_type: mime, parent_id: '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))), trashed: false, params: Object.assign({}, t.params || {}, ref.params || {}), _sourceTaskId: t.id }; }).filter(f => f.id && !fetchedIds.has(f.id)), next_page_token: json.next_page_token }; } else if (S.shareMode) { if (pageCount > 0) break; const shares = await apiShareList(); data = { files: shares, next_page_token: null }; } else if (S.uploadMode) { if (pageCount > 0) break; data = { files: [...S.uploadTasks], next_page_token: null }; } else { const isStarredRoot = S.starredMode && S.path.length === 1; const filterObj = { "trashed": { "eq": S.trashMode } }; if (isStarredRoot) { filterObj.trashed = { "eq": false }; filterObj.system_tag = { "in": "STAR" }; } else if (!S.trashMode) { filterObj.phase = { "eq": "PHASE_TYPE_COMPLETE" }; } const filters = `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`; const targetParentId = (S.trashMode || isStarredRoot) ? '*' : (cur.id || ''); const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&parent_id=${targetParentId}&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: 'high' }); if (currentId !== activeLoadId) return; if (!res.ok) { if (res.status === 429) { await sleep(1000); continue; } throw new Error("API " + res.status); } data = await res.json(); } if (data.files && data.files.length >= 0) { if (currentId !== activeLoadId) return; const currentPathId = S.path[S.path.length - 1]?.id || ''; if (S.trashMode !== snapshot.trash || S.shareMode !== snapshot.share || S.starredMode !== snapshot.starred || S.recentMode !== snapshot.recent || currentPathId !== snapshot.pathId) { console.warn("[Load Guard] Context Drift Detected. Aborting render."); return; } const hasDirtyData = data.files.some(f => f && (S.trashMode ? !f.trashed : f.trashed)); if (!S.shareMode && !S.offlineMode && hasDirtyData) { console.warn(L.log_dirty_data); return; } let validFiles = []; if (S.shareMode || S.offlineMode || S.uploadMode) { validFiles = data.files; } else { validFiles = data.files.map(f => minifyFile(f, false)); } const newUniqueFiles = validFiles.filter(f => !fetchedIds.has(f.id)); if (S.analyzeMode && S.analyzeMap) { newUniqueFiles.forEach(item => { if (item.kind === 'drive#folder' && S.analyzeMap.has(item.id)) { item.size = S.analyzeMap.get(item.id).size.toString(); } }); } if (newUniqueFiles.length > 0 || (pageCount === 0 && !isResuming)) { if (pageCount === 0 && !isResuming) { S.items = newUniqueFiles; S.itemMap.clear(); } else { S.items.push(...newUniqueFiles); } newUniqueFiles.forEach(f => { fetchedIds.add(f.id); S.itemMap.set(f.id, f); }); totalItemsLoaded = S.items.length; if (S.recentMode && S.path.length === 1) { S.recentResultItems = [...S.items]; } if (S.offlineMode) { S.cache.set(realCacheKey, [...S.items]); if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, [...S.items]); } else { const tempCache = { items: [...S.items], nextToken: data.next_page_token }; S.cache.set(realCacheKey, tempCache); if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, tempCache); } if (pageCount === 0 || UI.loader.style.display !== 'none') { refresh(); if (UI.loader.style.display !== 'none') { setLoad(false); } if (S.items.length === 0) { UI.in.innerHTML = ''; renderVisible(); } } else { if (pageCount % 4 === 0) refresh(); } } } nextToken = data.next_page_token; pageCount++; if (currentId === activeLoadId) { UI.stat.textContent = `${L.status_ready.replace('{n}', totalItemsLoaded)} (${L.loading_fetch.replace('{n}', pageCount)})`; } if (nextToken) await sleep(0); } while (nextToken && currentId === activeLoadId); if (currentId === activeLoadId) { if (S.trashMode && S.items.length === 0 && pageCount > 1) { } else { S.cache.set(realCacheKey, [...S.items]); } if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, [...S.items]); if (window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(); } indexParents(folderId, cur.name, S.items); S.itemMap.clear(); for (let k = 0; k < S.items.length; k++) S.itemMap.set(S.items[k].id, S.items[k]); syncStars(); if (folderId === 'root' && !S.preloaded) S.preloaded = true; const visibleSubFolders = S.items.filter(f => f.kind === 'drive#folder'); for (let i = visibleSubFolders.length - 1; i >= 0; i--) { const sub = visibleSubFolders[i]; if (!scannedFolderIds.has(sub.id) && !globalCache.has(sub.id)) { backgroundQueue.push({ id: sub.id, name: sub.name, retryCount: 0 }); scannedFolderIds.add(sub.id); } } isGUISensitive = false; runBackgroundCrawler(); refresh(); updateStat(); setLoad(false); } } catch (e) { isGUISensitive = false; if (currentId === activeLoadId) { if (!S._isRetrying) setLoad(false); if (e.name === 'AbortError') { console.log("Load aborted by user."); } else { console.error("API Error encountered:", e); if (typeof resetHeaderCache === 'function') resetHeaderCache(); const isAuthError = e.message.includes('401') || e.message.includes('403') || e.message.includes('400') || e.message.includes('CAPTCHA'); const isNotFoundError = e.message.includes('404'); const isNetworkError = e.name === 'TypeError' || e.message.includes('Failed to fetch') || e.message.includes('NetworkError'); if (!S._isRetrying || isNetworkError) { S._isRetrying = true; setLoad(true); if (isAuthError) { resetHeaderCache(); updateLoadTxt(L.str_waiting_token); await sleep(2000); try { const h = getHeaders(); if (!h.Authorization || h.Authorization.length < 10) { location.reload(); return; } const testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token still rejected by server, forcing page reload to recover..."); location.reload(); return; } } catch(testErr) {} console.log("[Auth] Auth state recovered, resuming load..."); load(false, true).finally(() => { S._isRetrying = false; }); return; } if (isNotFoundError) { if (S.path.length > 1 || S.path[0].id !== '') { S.path = [{ id: '', name: L.btn_nav_home }]; load(false, true).finally(() => { S._isRetrying = false; }); return; } } if (isNetworkError) { const delay = 1000 + Math.random() * 2000; const retryMsg = L.msg_network_unstable; updateLoadTxt(retryMsg); console.warn(`[Network] Connection drop detected. Retrying in ${Math.round(delay)}ms...`); await sleep(delay); load(false, true).finally(() => { }); return; } S._isRetrying = false; } if (S.items.length === 0) { setLoad(false); showAlert(L.str_failed + " " + e.message); } } } } } async function refresh() { S.sortId++; const currentReqId = S.sortId; if (!S.dupMode) S.dupItemMap = null; const isStrictVirtual = S.isFlattened || S.dupMode || S.analyzeMode; const cur = S.path[S.path.length - 1]; const isInSearchContext = S.path.some(p => p.id === 'virtual_search_root'); const shouldHideHeavyOps = isStrictVirtual || isInSearchContext || S.offlineMode || S.uploadMode; if (UI.btnFolderFirst) { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isAnalyzeSub = S.analyzeMode && cur.id !== 'analyze_root'; const shouldHideFF = S.isFlattened || S.dupMode || isAnalyzeRoot; if (UI.btnFolderFirst) { UI.btnFolderFirst.style.display = shouldHideFF ? 'none' : 'flex'; if (isAnalyzeSub) S.renderFolderFirst(); } } if (UI.btnAnalyze) { UI.btnAnalyze.style.display = (shouldHideHeavyOps || S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode) ? 'none' : 'flex'; } if (UI.btnExport) { UI.btnExport.style.display = (shouldHideHeavyOps || S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode) ? 'none' : 'flex'; } if (UI.scan) { UI.scan.style.display = (shouldHideHeavyOps || S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode) ? 'none' : 'flex'; } if (UI.btnExit) { UI.btnExit.style.display = isStrictVirtual ? 'flex' : 'none'; } if (UI.lblSearchPath) { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; UI.lblSearchPath.style.display = (S.dupMode || S.isFlattened || isAnalyzeRoot) ? 'flex' : 'none'; if (UI.btnAnaSelect) UI.btnAnaSelect.style.display = (isAnalyzeRoot && !S.search && S.analyzeSimGroups) ? 'flex' : 'none'; } if (UI.filterBar) { if (S.isFlattened && !S.dupMode) { UI.filterBar.style.display = 'flex'; if (S.filterState.active) { UI.filterBtn.style.display = 'none'; UI.filterActiveUI.style.display = 'flex'; if (typeof renderActiveFilterUI === 'function') renderActiveFilterUI(); } else { UI.filterBtn.style.display = 'flex'; UI.filterActiveUI.style.display = 'none'; } } else { UI.filterBar.style.display = 'none'; } } if (UI.cntFolderFirst) { } const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; if (UI.btnNewFolder) { const isNewFolderBlocked = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || isStarredRoot || isRecentRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; UI.btnNewFolder.style.display = isNewFolderBlocked ? 'none' : (S.trashMode ? 'none' : 'flex'); } if (UI.btnPaste) { const isPasteBlocked = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || isStarredRoot || isRecentRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; UI.btnPaste.style.display = isPasteBlocked ? 'none' : (S.trashMode ? 'none' : 'inline-flex'); } const isInVirtualSearch = S.path.some(p => p.id === 'virtual_search_root'); const isInsideSearchResult = isInVirtualSearch && cur.id !== 'virtual_search_root'; let listToProcess = []; if (S.search && !S.dupMode && !isInsideSearchResult) { const q = S.search.toLowerCase(); const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; if (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups) { const newDisplay = []; S.analyzeSimGroups.forEach((g, gIdx) => { const groupItems = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); const isGroupHit = g.type.toLowerCase().includes(q) || groupItems.some(it => { const nameMatch = it.name.toLowerCase().includes(q); let pathMatch = false; if (includePath && it._pathStr) { const homeText = L.btn_nav_home; const parentPath = (it._pathStr === homeText || it._pathStr.startsWith(homeText + '/')) ? it._pathStr : (homeText + '/' + it._pathStr); const fullItemPath = parentPath.endsWith('/') ? (parentPath + it.name) : (parentPath + '/' + it.name); pathMatch = fullItemPath.toLowerCase().includes(q); } return nameMatch || pathMatch; }); if (isGroupHit) { newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: g.type, count: g.ids.length, type: g.type || L.str_group }); groupItems.sort((a, b) => parseInt(b.size || 0) - parseInt(a.size || 0)); groupItems.forEach(it => { if (it.name.toLowerCase().includes(q)) { const name = it.name; const idx = name.toLowerCase().indexOf(q); const len = q.length; const start = Math.max(0, idx - 15); const end = Math.min(name.length, idx + len + 30); let pre = start > 0 ? "..." : ""; let suf = end < name.length ? "..." : ""; it._hlNameHTML = `${pre}${esc(name.substring(start, idx))}${esc(name.substring(idx, idx + len))}${esc(name.substring(idx + len, end))}${suf}`; } else { delete it._hlNameHTML; } newDisplay.push(it); }); } }); S.display = newDisplay; } else if (UI.chkGlobal && UI.chkGlobal.checked && cur.id === 'virtual_search_root') { let results = []; const seenIds = new Set(); let realMyPackId = ''; if (typeof globalCache !== 'undefined') { const rootFiles = globalCache.get('') || globalCache.get('root'); if (rootFiles) { const realObj = rootFiles.find(f => f.kind === 'drive#folder' && f.name === CONF.SYSTEM_FOLDER_NAME); if (realObj) realMyPackId = realObj.id; } } if (CONF.SYSTEM_FOLDER_NAME.toLowerCase().includes(q)) { const realObj = (globalCache.get('') || globalCache.get('root'))?.find(f => f.name === CONF.SYSTEM_FOLDER_NAME); const sysRoot = { id: realMyPackId, kind: 'drive#folder', name: CONF.SYSTEM_FOLDER_NAME, parent_id: '', size: 0, modified_time: '', starred: false, tags: [], _lineage: [], _isSystemRoot: true, icon_link: realObj ? realObj.icon_link : '', thumbnail_link: realObj ? realObj.icon_link : '' }; results.push(sysRoot); if (realMyPackId) seenIds.add(realMyPackId); } if (typeof globalCache !== 'undefined') { for (const [key, files] of globalCache) { if (currentReqId !== S.sortId) return; if (!Array.isArray(files)) continue; if (key && (key.endsWith('_root') || key === 'root_trashed')) continue; let parentLineage = (typeof globalLineageMap !== 'undefined') ? globalLineageMap.get(key) : undefined; if (!parentLineage) { if (key === '' || key === 'root') parentLineage = []; else parentLineage = null; } for (let i = 0, len = files.length; i < len; i++) { const f = files[i]; if (seenIds.has(f.id)) continue; if (f && f.name && f.name.toLowerCase().includes(q)) { let itemLineage = parentLineage; const fObj = { ...f, _lineage: itemLineage }; const fName = fObj.name; const fIdx = fName.toLowerCase().indexOf(q); if (fIdx !== -1) { const start = Math.max(0, fIdx - 15); const end = Math.min(fName.length, fIdx + q.length + 30); let pre = start > 0 ? "..." : ""; let suf = end < fName.length ? "..." : ""; const b = fName.substring(start, fIdx); const m = fName.substring(fIdx, fIdx + q.length); const a = fName.substring(fIdx + q.length, end); fObj._hlNameHTML = `${pre}${esc(b)}${esc(m)}${esc(a)}${suf}`; } results.push(fObj); seenIds.add(f.id); } } } } S.display = results; S.items = results; S.lastGlobalResults = [...results]; ensureItemMap(); } else if (isInVirtualSearch && cur.id !== 'virtual_search_root') { let ancestors = (typeof globalLineageMap !== 'undefined' && globalLineageMap.get(cur.id)) || cur._lineage || []; const currentFolderAsAncestor = [...ancestors, { id: cur.id, name: cur.name }]; S.display = S.items.map(item => { const itemLineage = (item.kind === 'drive#folder' && typeof globalLineageMap !== 'undefined' && globalLineageMap.has(item.id)) ? globalLineageMap.get(item.id) : currentFolderAsAncestor; return { ...item, _lineage: itemLineage }; }); } else { const query = S.search.toLowerCase(); const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const isPathMode = S.isFlattened || (S.analyzeMode && cur.id === 'analyze_root'); const rootPathStr = S.path.map(p => p.name).join('/'); let filtered = S.items.filter(i => { if (!i) return false; if (i.name && i.name.toLowerCase().includes(query)) return true; if (includePath && isPathMode) { let pStr = ""; if (S.analyzeMode) { pStr = i._pathStr || L.btn_nav_home; } else if (i._lineage) { let cleanLineage = i._lineage.map(x => x.name).filter(n => n && n !== 'Root' && n !== L.str_root_dir_cn); if (cleanLineage[0] !== L.btn_nav_home) cleanLineage.unshift(L.btn_nav_home); pStr = cleanLineage.join('/'); } const fullItemPath = pStr.endsWith('/') ? (pStr + i.name) : (pStr + '/' + i.name); if (fullItemPath.toLowerCase().includes(query)) return true; } return false; }); if (S.offlineMode) { const cfg = S.offlineFilters; filtered = filtered.filter(i => { const p = i.phase; if (p === 'PHASE_TYPE_COMPLETE') return cfg.complete; if (['PHASE_TYPE_RUNNING', 'PHASE_TYPE_PENDING', 'PHASE_TYPE_PAUSED'].includes(p)) return cfg.running; return cfg.failed; }); } filtered.forEach(item => { const name = item.name; const idx = name.toLowerCase().indexOf(query); if (idx !== -1) { const len = query.length; const start = Math.max(0, idx - 15); const end = Math.min(name.length, idx + len + 30); let prefix = start > 0 ? "..." : ""; let suffix = end < name.length ? "..." : ""; const partBefore = name.substring(start, idx); const partMatch = name.substring(idx, idx + len); const partAfter = name.substring(idx + len, end); item._hlNameHTML = `${prefix}${esc(partBefore)}${esc(partMatch)}${esc(partAfter)}${suffix}`; } }); S.display = filtered; } } else { if (S.path.length > 0 && S.path[0].id === 'starred') { S.display = S.items.filter(i => (i.starred || (i.tags && i.tags.some(t => t.name === 'STAR'))) && !i.trashed); } else if (S.isFlattened && S.filterState && S.filterState.active && S.filterState.cat !== 'all') { const cat = S.filterState.cat; const ext = S.filterState.ext; const fExts = { video: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mpg','mpeg','rm','rmvb','asf','vob','dat','divx','f4v','m2ts','mts','tp','trp','ogv','mpe','m2v','m3u8'], audio: ['mp3','wav','flac','aac','ogg','wma','ape','m4a','amr','opus','m4b','alac','aiff','mid','midi','ra','dts','ac3','dsf','dff'], image: ['jpg','jpeg','png','gif','bmp','webp','svg','tif','tiff','ico','heic','heif','raw','cr2','nef','arw','dng','orf','avif','psd','ai','eps','jfif','jpe'], document: ['txt','html','pdf','pptx','chm','docx','xlsx','htm','doc','dwg','mdb','ppt','xls','rtf','odt','ods','odp','epub','mobi','azw3','djvu','cbz','cbr','md','log','csv','xml','json'], software: ['apk','exe','ipa','dmg','rpm','deb','msi','pkg','xapk','apks','aab','jar','bin','sh','bat','cmd'], archive: ['zip','rar','7z','tar','gz','iso','cab','bz2','xz','tgz','wim','esd','img','zst','lzh'], torrent: ['torrent'] }; let matchExts =[]; if (cat === 'other') { const definedExts = new Set([...fExts.video, ...fExts.audio, ...fExts.image, ...fExts.document, ...fExts.software, ...fExts.archive, ...fExts.torrent]); S.display = S.items.filter(i => { if (i.kind === 'drive#folder') return false; const n = (i.name || '').toLowerCase(); const e = n.split('.').pop(); return !definedExts.has(e); }); } else { if (ext === 'all') { matchExts = fExts[cat] ||[]; } else { matchExts = [ext]; } S.display = S.items.filter(i => { if (i.kind === 'drive#folder') return false; const n = (i.name || '').toLowerCase(); const e = n.split('.').pop(); return matchExts.includes(e); }); } } else if (S.offlineMode) { const cfg = S.offlineFilters; S.display = S.items.filter(i => { const p = i.phase; if (p === 'PHASE_TYPE_COMPLETE') return cfg.complete; if (['PHASE_TYPE_RUNNING', 'PHASE_TYPE_PENDING', 'PHASE_TYPE_PAUSED'].includes(p)) return cfg.running; return cfg.failed; }); } else if (S.uploadMode) { const cfg = S.uploadFilters; S.display = S.items.filter(i => { const s = i.status; if (s === 'DONE') return cfg.complete; if (s === 'PAUSED' || s === 'ERROR') return cfg.paused; return cfg.running; }); } else if (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups) { const newDisplay =[]; S.analyzeSimGroups.forEach((g, gIdx) => { newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: g.type, count: g.ids.length, type: g.type || L.str_group }); const groupItems = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); groupItems.sort((a, b) => { const pA = a._pathStr || ""; const pB = b._pathStr || ""; if (pA.length !== pB.length) return pA.length - pB.length; return pA.localeCompare(pB); }); let lastPathInGroup = null; groupItems.forEach(it => { const currentPath = it._pathStr || ""; if (currentPath === lastPathInGroup && currentPath !== "") { it._isSameFolder = true; } else { it._isSameFolder = false; lastPathInGroup = currentPath; } newDisplay.push(it); }); }); S.display = newDisplay; } else { S.display = [...S.items]; } } if (currentReqId !== S.sortId) return; if (S.sel.size > 0) { const visibleSet = new Set(S.display.map(i => i.id)); Array.from(S.sel).forEach(id => { if (!visibleSet.has(id)) S.sel.delete(id); }); } S.dupReasons.clear(); S.dupGroups.clear(); if (!S.dupMode) S.pinnedDupPath = null; const isStandardView = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!cur.id.startsWith('virtual_') || cur.id === 'virtual_search_root'); const isSortIndep = gmGet('pk_sort_independent', false); if (isStandardView) { const folderId = cur.id || 'root'; if (S._sortAppliedForId !== folderId) { if (isSortIndep) { try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); if (prefStore[folderId]) { S.sort = prefStore[folderId].sort; S.dir = prefStore[folderId].dir; S.folderFirst = prefStore[folderId].folderFirst === true; } else { S.sort = 'modified_time'; S.dir = 1; S.folderFirst = false; } } catch(e) {} } else { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); S.sort = globalPref.sort; S.dir = globalPref.dir; if (globalPref.folderFirst !== undefined) S.folderFirst = globalPref.folderFirst; else S.folderFirst = gmGet('pk_folder_first', false); } S._sortAppliedForId = folderId; S._comicApplied = false; if(S.renderFolderFirst) S.renderFolderFirst(); } if (gmGet('pk_comic_mode', false) && !S._comicApplied) { const hasSubFolders = S.items.some(i => i.kind === 'drive#folder'); if (!hasSubFolders && S.items.length > 0) { const isAllImages = S.items.every(i => i.mime_type && i.mime_type.startsWith('image/')); const isAllVideos = S.items.every(i => i.mime_type && i.mime_type.startsWith('video/')); if (isAllImages || isAllVideos) { S.sort = 'name'; S.dir = 1; } S._comicApplied = true; } } } const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isGlobalSearchRoot = cur.id === 'virtual_search_root' && UI.chkGlobal && UI.chkGlobal.checked; const supportsPathSort = S.dupMode || isAnalyzeRoot || S.isFlattened || isGlobalSearchRoot; if (S.sort === 'path' && !supportsPathSort) { S.sort = 'modified_time'; S.dir = 1; } if (S.dupMode) { setLoad(true); S.dupRunning = true; UI.stopBtn.onclick = () => { S.dupRunning = false; }; updateLoadTxt(L.loading_dup.replace('{p}', 0)); await sleep(20); const cfg = S.dupConfig || { video: true, image: false, other: false }; let candidates = S.display.filter(i => { if (!i.mime_type) return false; const isVideo = i.mime_type.startsWith('video'); const isImage = i.mime_type.startsWith('image'); const isOther = !isVideo && !isImage; if (isVideo && cfg.video) return true; if (isImage && cfg.image) return true; if (isOther && cfg.other) return true; return false; }); const groups = await computeDuplicateGroups(candidates, cfg, () => S.dupRunning); groups.sort((a, b) => { const getPriority = (t) => { if (t === L.tag_hash) return 3; if (t === L.tag_sim) return 2; if (t === L.tag_name) return 1; return 0; }; const pA = getPriority(a.type); const pB = getPriority(b.type); if (pA !== pB) return pB - pA; return b.ids.length - a.ids.length; }); if (groups.length === 0) { S.dupRunning = false; setLoad(false); showToast(L.msg_dup_none); if (UI.btnExit) UI.btnExit.click(); return; } S.dupRawGroups = groups; if (UI.chkName) UI.chkName.checked = true; if (UI.chkSim) UI.chkSim.checked = true; if (UI.chkHash) UI.chkHash.checked = true; const applyDupSelection = () => { const targetPath = UI.selDupFolder.value; const invertChk = document.getElementById('pk-dup-invert'); if (!targetPath || targetPath === "__RESET__") { S.pinnedDupPath = null; S.sel.clear(); if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } renderDupView(); updateStat(); if(targetPath) UI.selDupFolder.value = ""; return; } if (invertChk) { invertChk.disabled = false; invertChk.parentNode.style.opacity = '1'; } const isInvert = invertChk ? invertChk.checked : false; S.pinnedDupPath = targetPath; const itemMap = new Map(); for(const item of S.items) itemMap.set(item.id, item); S.sel.clear(); S.dupRawGroups.forEach(g => { const filesInTarget = []; const filesInOther = []; g.ids.forEach(id => { const item = itemMap.get(id); if (item) { let path = ""; if (item._cachedPath !== undefined) { path = item._cachedPath; } else { if (item._lineage && item._lineage.length > 0) { path = item._lineage.map(p => p.name).join('/'); } else { const curFolder = S.path[S.path.length - 1]; path = curFolder ? curFolder.name : L.current_dir; } item._cachedPath = path; } if (path === targetPath) filesInTarget.push(item); else filesInOther.push(item); } }); if (filesInOther.length > 0) { if (isInvert) { filesInOther.forEach(f => S.sel.add(f.id)); } else { filesInTarget.forEach(f => S.sel.add(f.id)); } } else if (filesInTarget.length > 1) { for (let k = 1; k < filesInTarget.length; k++) S.sel.add(filesInTarget[k].id); } }); renderDupView(); updateStat(); }; UI.selDupFolder.onchange = applyDupSelection; setTimeout(() => { const invertChk = document.getElementById('pk-dup-invert'); if (invertChk) { invertChk.onchange = () => { if (UI.selDupFolder.value && UI.selDupFolder.value !== "__RESET__") { applyDupSelection(); } }; } const smartBtn = document.getElementById('pk-dup-smart-btn'); if (smartBtn) { smartBtn.addEventListener('click', () => { if (S.pinnedDupPath) { S.pinnedDupPath = null; UI.selDupFolder.value = ""; if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } renderDupView(); } }, { capture: true }); } }, 0); S.dupRunning = false; setLoad(false); if (UI.dupTools) UI.dupTools.style.display = 'flex'; if (UI.dupFilters) { UI.dupFilters.style.display = 'flex'; const simChkWrapper = UI.dupFilters.querySelector('#pk-chk-sim')?.closest('.pk-dup-chk'); if (simChkWrapper) { simChkWrapper.style.display = S.dupConfig.video ? 'flex' : 'none'; } } renderDupView(); } else { if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (S.uploadMode || (S.analyzeMode && S.analyzeSimGroups && cur.id === 'analyze_root')) { renderList(); if (gmGet('pk_keep_pos', false) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { UI.vp.scrollTop = Math.max(0, (targetIdx * CONF.rowHeight) - (UI.vp.clientHeight / 2) + (CONF.rowHeight / 2)); S.activeId = S.latestChildId; S.sel.clear(); renderVisible(); } S.latestChildId = null; } updateStat(); return; } if (S.display.length > 5000 && !S.offlineMode) { setLoad(true); updateLoadTxt(L.str_sorting); await sleep(10); } const sortedList = await new Promise(resolve => { const isRoot = S.path.length === 1 && S.path[0].id === ''; const proxyList = new Array(S.display.length); const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; const isStarred = S.starredSet.has(item.id) || !!(item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))); const isMyPack = item._isSystemRoot || (!S.trashMode && item.kind === 'drive#folder' && ( (item.id === '' || item.id === 'root') || (isRoot && item.name === CONF.SYSTEM_FOLDER_NAME) )); const ext = item.name.split('.').pop().toLowerCase(); const isVid = ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'ts', 'm4v', '3gp'].includes(ext); const isImg = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff', 'ico'].includes(ext); const mimeGroup = isVid ? 1 : (isImg ? 2 : 3); let finalDur = S.durationMap.get(item.id) || parseFloat(item.params?.duration || 0); if (finalDur > 0 && item.params) item.params.duration = finalDur; let pathStr = ""; if (S.analyzeMode) pathStr = item._pathStr || ""; else if (item._lineage) pathStr = item._lineage.map(x=>x.name).join('/'); proxyList[i] = { i: i, n: item.name, k: item.kind === 'drive#folder' ? 1 : 0, s: item.size || 0, t: item.modified_time, d: finalDur, st: isStarred ? 1 : 0, mp: isMyPack ? 1 : 0, m: mimeGroup, p: pathStr, ct: item.created_time, pt: item._history_ts || 0, v: parseInt(item.view_count || 0), sc: parseInt(item.save_count || 0), ss: item.share_status || 'OK', ed: parseInt(item.expiration_days || -1), lc: parseInt(item.limit_count || 0) }; } const workerCode = ` self.onmessage = function(e) { const { proxy, sort, dir, reqId, folderFirst, locale } = e.data; const parseSize = n => parseInt(n || 0); const parseTime = t => t ? new Date(t).getTime() : 0; const collator = new Intl.Collator(locale, { numeric: true, sensitivity: 'base', caseFirst: 'lower' }); const getCharWeight = (str) => { if (!str) return 0; const c = str.charAt(0); if (/[0-9]/.test(c)) return 20; if (/[\\u4e00-\\u9fa5]/.test(c)) return 30; if (/[a-zA-Z]/.test(c)) return 40; return 10; }; const compareNames = (nameA, nameB) => { const rankA = getCharWeight(nameA); const rankB = getCharWeight(nameB); if (rankA !== rankB) { return rankA - rankB; } return collator.compare(nameA, nameB); }; proxy.sort((a, b) => { if (a.mp !== b.mp) return b.mp - a.mp; if (folderFirst && a.k !== b.k) { return b.k - a.k; } if (sort === 'view_count' || sort === 'save_count') { const valA = sort === 'view_count' ? (a.v || 0) : (a.sc || 0); const valB = sort === 'view_count' ? (b.v || 0) : (b.sc || 0); if (valA !== valB) return (valB - valA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'share_status') { const getStatusRank = (item) => { if (item.lc > 0 && item.sc >= item.lc) return 5; if (item.ss === 'DELETED') return 4; if (item.ss === 'EXPIRED') return 3; if (item.ed === -1) return 1; return 2; }; const rA = getStatusRank(a), rB = getStatusRank(b); if (rA !== rB) return (rA - rB) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'modified_time') { const tA = parseTime(a.t), tB = parseTime(b.t); if (tA !== tB) return (tB - tA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'created_time') { const tA = parseTime(a.ct), tB = parseTime(b.ct); if (tA !== tB) return (tB - tA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'play_time') { if (a.pt !== b.pt) return (b.pt - a.pt) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'path') { const pathCmp = compareNames(a.p, b.p); if (pathCmp !== 0) return pathCmp * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'size') { const sizeA = parseSize(a.s), sizeB = parseSize(b.s); if (sizeA !== sizeB) return (sizeB - sizeA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'duration') { if (a.k !== b.k) return folderFirst ? (b.k - a.k) : (a.k - b.k); if (a.k) return compareNames(a.n, b.n) * dir; const hasDurA = a.d > 0; const hasDurB = b.d > 0; if (hasDurA !== hasDurB) return hasDurA ? -1 : 1; if (hasDurA) { if (a.d !== b.d) return (b.d - a.d) * dir; return compareNames(a.n, b.n) * dir; } const isVidA = (a.m === 1); const isVidB = (b.m === 1); if (isVidA !== isVidB) return isVidA ? -1 : 1; if (isVidA) { return compareNames(a.n, b.n); } else { const getExt = s => { const i = s.lastIndexOf('.'); return i > 0 ? s.slice(i).toLowerCase() : ''; }; const extA = getExt(a.n); const extB = getExt(b.n); const extCmp = compareNames(extA, extB); if (extCmp !== 0) { return extCmp * dir; } return compareNames(a.n, b.n) * dir; } } if (sort === 'starred') { if (a.st !== b.st) return b.st - a.st; return (parseTime(b.t) - parseTime(a.t)) * dir; } return compareNames(a.n, b.n) * dir; }); const sortedIndices = new Uint32Array(proxy.length); for (let i = 0; i < proxy.length; i++) sortedIndices[i] = proxy[i].i; self.postMessage({ indices: sortedIndices, reqId: reqId }, [sortedIndices.buffer]); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); const sortWorker = new Worker(workerUrl); sortWorker.onmessage = (e) => { const { indices, reqId } = e.data; URL.revokeObjectURL(workerUrl); sortWorker.terminate(); if (reqId === S.sortId) { resolve(Array.from(indices).map(idx => S.display[idx])); } else resolve(null); }; const sortLocale = ({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en'})[getLang()] || 'en'; sortWorker.postMessage({ proxy: proxyList, sort: S.sort, dir: S.dir, reqId: currentReqId, folderFirst: S.folderFirst, locale: sortLocale }); }); if (S.display.length > 5000 && !S.offlineMode) setLoad(false); if (sortedList === null) return; S.display = sortedList; } if (currentReqId !== S.sortId) return; if (S.sel.size > 0) { const currentIds = new Set(S.display.map(i => i.id)); for (let id of S.sel) if (!currentIds.has(id)) S.sel.delete(id); } renderList(); if (gmGet('pk_keep_pos', false) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { const rowTop = targetIdx * CONF.rowHeight; const vpHeight = UI.vp.clientHeight; const centerScroll = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); UI.vp.scrollTop = centerScroll; S.activeId = S.latestChildId; S.sel.clear(); renderVisible(); } S.latestChildId = null; } updateStat(); } const getCommonPathPrefix = (pathStrings) => { if (!pathStrings || pathStrings.length < 2) return ""; const splitPaths = pathStrings.map(p => p.split('/')); const firstPath = splitPaths[0]; let commonLen = 0; for (let i = 0; i < firstPath.length; i++) { const segment = firstPath[i]; const isAllSame = splitPaths.every(p => p[i] === segment); if (isAllSame) { commonLen++; } else { break; } } if (commonLen <= 1) return ""; return firstPath.slice(0, commonLen).join('/'); }; function renderDupView() { if (!S.dupMode) return; const L = getStrings(); const showName = UI.chkName.checked; const showSim = UI.chkSim.checked; const showHash = UI.chkHash.checked; const newDisplay = []; if (!S.dupItemMap || S.dupItemMap.size !== S.items.length) { S.dupItemMap = new Map(); const len = S.items.length; for (let i = 0; i < len; i++) { S.dupItemMap.set(S.items[i].id, S.items[i]); } } const itemMap = S.dupItemMap; const getCachedPath = (item) => { if (item._cachedPath !== undefined) return item._cachedPath; let pStr = ""; if (item._lineage && item._lineage.length > 0) { pStr = item._lineage.map(node => node.name).join('/'); } else { const curFolder = S.path[S.path.length - 1]; pStr = curFolder ? curFolder.name : L.current_dir; } item._cachedPath = pStr; return pStr; }; S.dupGroups.clear(); if (S.dupRawGroups && S.dupRawGroups.length > 0) { const activeRawGroups =[]; for (const g of S.dupRawGroups) { const validIds = g.ids.filter(id => itemMap.has(id)); if (validIds.length > 1) { activeRawGroups.push({ ...g, ids: validIds }); } } S.dupRawGroups = activeRawGroups; } if (S.dupRawGroups.length === 0 && !S.dupRunning) { if (UI.btnExit) { UI.btnExit.click(); } else { S.dupMode = false; S.isFlattened = false; refresh(); } return; } let groupsToRender = S.dupRawGroups; if (S.pinnedDupPath) { const pinnedGroups = []; const normalGroups = []; for (const g of S.dupRawGroups) { let isPinned = false; for (const id of g.ids) { const item = itemMap.get(id); if (item) { const path = getCachedPath(item); if (path === S.pinnedDupPath) { isPinned = true; break; } } } if (isPinned) pinnedGroups.push(g); else normalGroups.push(g); } groupsToRender = [...pinnedGroups, ...normalGroups]; } const dynamicFolderStats = new Map(); const groupsLen = groupsToRender.length; const searchKey = S.search ? S.search.toLowerCase().trim() : null; const includePath = S.search && UI.chkSearchPath ? UI.chkSearchPath.checked : false; for (let gIdx = 0; gIdx < groupsLen; gIdx++) { const g = groupsToRender[gIdx]; if (g.type === L.tag_name && !showName) continue; if (g.type === L.tag_sim && !showSim) continue; if (g.type === L.tag_hash && !showHash) continue; const ids = g.ids; const idsLen = ids.length; for (let k = 0; k < idsLen; k++) { S.dupGroups.set(ids[k], gIdx); } const groupItems = []; const groupPaths = []; for (let k = 0; k < idsLen; k++) { const item = itemMap.get(ids[k]); if (item) { groupItems.push(item); groupPaths.push(getCachedPath(item)); } } if (searchKey) { const isMatch = groupItems.some((item, idx) => { const nameHit = item.name.toLowerCase().includes(searchKey); let pathHit = false; if (includePath) { const rawPath = groupPaths[idx] || ""; const homeText = L.btn_nav_home; const parentPath = (rawPath === homeText || rawPath.startsWith(homeText + '/')) ? rawPath : (homeText + '/' + rawPath); const fullItemPath = parentPath.endsWith('/') ? (parentPath + (item.name || "")) : (parentPath + '/' + (item.name || "")); pathHit = fullItemPath.toLowerCase().includes(searchKey); } return nameHit || pathHit; }); if (!isMatch) continue; } const pathsLen = groupPaths.length; for (let p = 0; p < pathsLen; p++) { const pStr = groupPaths[p]; dynamicFolderStats.set(pStr, (dynamicFolderStats.get(pStr) || 0) + 1); } const firstItem = itemMap.get(ids[0]); let featureStr = ""; if (firstItem) { if (g.type === L.tag_hash) { featureStr = `[ ${fmtSize(firstItem.size)} ] `; } else if (g.type === L.tag_sim) { const dur = firstItem.params?.duration || 0; featureStr = `[ ${dur > 0 ? fmtDur(dur) : '--:--'} ] `; } else if (g.type === L.tag_name) { featureStr = `[ ${L.tag_name_short} ] `; } } const baseName = firstItem ? (g.type === L.tag_name ? firstItem.name.replace(/\.[^/.]+$/, "") : firstItem.name) : `${L.str_group} ${gIdx}`; const countStr = `${idsLen} ${L.str_items}`; const smartTitle = `${countStr} | ${featureStr}${baseName}`; newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: smartTitle, count: idsLen, type: g.type || L.str_group }); const sortedGroupData = groupItems.map((it, idx) => ({ it, path: groupPaths[idx] })).sort((a, b) => { if (a.path.length !== b.path.length) return a.path.length - b.path.length; return a.path.localeCompare(b.path); }); let lastFullPath = null; for (let idx = 0; idx < sortedGroupData.length; idx++) { const { it: item, path: fullPath } = sortedGroupData[idx]; if (idx > 0 && fullPath === lastFullPath) { item._isSameFolder = true; item._dupFullPath = L.str_same_folder; } else { item._isSameFolder = false; item._dupFullPath = fullPath; } lastFullPath = fullPath; newDisplay.push(item); } } UI.selDupFolder.style.width = "220px"; UI.selDupFolder.style.flexShrink = "0"; const currentSelection = UI.selDupFolder.value; const invertChk = document.getElementById('pk-dup-invert'); const invertLabel = invertChk ? invertChk.parentNode : null; if (dynamicFolderStats.size <= 1) { UI.selDupFolder.style.display = 'none'; if (invertLabel) invertLabel.style.display = 'none'; } else { UI.selDupFolder.style.display = 'inline-block'; if (invertLabel) invertLabel.style.display = 'inline-flex'; } let dropdownHtml = ``; if (dynamicFolderStats.size > 0) { dropdownHtml += ``; } const truncateMiddle = (str, len = 30) => { if (!str || str.length <= len * 2 + 3) return str; return str.substring(0, len) + ' ... ' + str.substring(str.length - len); }; const sortLocale = ({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en'})[getLang()] || 'en'; const collator = new Intl.Collator(sortLocale, { numeric: true }); const sortedFolders = Array.from(dynamicFolderStats.entries()) .sort((a, b) => { const arrA = a[0].split('/'); const arrB = b[0].split('/'); const len = Math.min(arrA.length, arrB.length); for (let i = 0; i < len; i++) { const cmp = collator.compare(arrA[i], arrB[i]); if (cmp !== 0) return cmp; } return arrA.length - arrB.length; }); const optionsArr = sortedFolders.map(([path, count]) => { return ``; }); dropdownHtml += optionsArr.join(''); UI.selDupFolder.innerHTML = dropdownHtml; if (currentSelection && dynamicFolderStats.has(currentSelection)) { UI.selDupFolder.value = currentSelection; } else if (currentSelection === "__RESET__") { UI.selDupFolder.value = "__RESET__"; } else { if (S.pinnedDupPath && !dynamicFolderStats.has(S.pinnedDupPath)) { S.pinnedDupPath = null; } } S.display = newDisplay; const visibleIds = new Set(newDisplay.map(d => d.id)); for(let id of S.sel) { if (!visibleIds.has(id)) S.sel.delete(id); } if (UI.chkAll) UI.chkAll.checked = false; renderList(); updateStat(); } function renderList() { UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; let colDef; const cur = S.path[S.path.length - 1]; const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isGlobalSearchRoot = cur.id === 'virtual_search_root' && UI.chkGlobal && UI.chkGlobal.checked; const showPathCol = S.dupMode || isAnalyzeRoot || S.isFlattened || isGlobalSearchRoot; const isMax = UI.win.classList.contains('pk-maximized'); if (S.offlineMode) { colDef = "36px 1fr 120px 240px 180px"; } else if (S.uploadMode) { colDef = "36px 1fr 100px 120px 180px"; } else if (S.shareMode) { colDef = isMax ? "36px 1fr 80px 80px 120px 130px" : "36px 1fr 80px 80px 80px 130px"; } else if (S.historyMode) { colDef = "36px 30px 1fr 80px 80px 120px 160px"; } else if (S.trashMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (S.recentMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (isAnalyzeRoot) { colDef = "36px 30px 2fr 1fr 80px 130px"; } else if (showPathCol) { colDef = "36px 30px 2fr 1fr 80px 105px 130px"; } else { colDef = "36px 30px 1fr 80px 105px 130px"; } const hd = UI.win.querySelector('.pk-grid-hd'); if (hd) { hd.style.gridTemplateColumns = colDef; if (S.offlineMode) { if (!hd.querySelector('[data-k="offline_status"]')) { hd.innerHTML = `
${L.col_name}
${L.col_size}
${L.col_task_status}
${L.col_task_progress}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (S.historyMode) { if (!hd.querySelector('[data-k="play_time"]')) { hd.innerHTML = `
${L.col_name}
${L.col_size}
${L.col_duration_only}
${L.col_progress}
${L.col_play_time}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (S.uploadMode) { if (!hd.querySelector('[data-k="upload_speed"]')) { hd.innerHTML = `
${L.col_name}
${L.col_size}
${L.col_up_speed}
${L.col_up_status}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (S.shareMode) { if (!hd.querySelector('[data-k="share_status"]')) { hd.innerHTML = `
${L.col_name}
${L.col_view}
${L.col_save}
${L.col_share_status}
${L.col_share_time}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; const btnInv = hd.querySelector('#pk-btn-invert'); if(btnInv) { btnInv.onclick = (e) => { e.stopPropagation(); if (S.loading || S.display.length === 0) return; const newSel = new Set(); for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader && !S.sel.has(item.id)) newSel.add(item.id); } S.sel = newSel; S.lastSelIdx = -1; S.activeId = null; renderVisible(); updateStat(); }; btnInv.onmouseenter = () => btnInv.style.color = 'var(--pk-pri)'; btnInv.onmouseleave = () => btnInv.style.color = 'var(--pk-fg)'; } } } else { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const hasDurationCol = !!hd.querySelector('[data-k="duration"]'); const hasHistoryCol = !!hd.querySelector('[data-k="play_time"]'); const hasOfflineCol = !!hd.querySelector('[data-k="offline_status"]'); if (hasDurationCol === isAnalyzeRoot || !hd.querySelector('[data-k="name"]') || hasHistoryCol || hasOfflineCol) { hd.innerHTML = `
${L.col_name}
${!isAnalyzeRoot ? `
${CONF.icons.folderFirst} ${L.lbl_folder_first}
` : ''}
${L.col_size}
${!isAnalyzeRoot ? `
${L.col_dur}
` : ''}
${L.col_date}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; const btnFF = hd.querySelector('#pk-btn-folder-first'); if (btnFF) { UI.btnFolderFirst = btnFF; btnFF.onclick = (e) => { e.stopPropagation(); S.folderFirst = !S.folderFirst; gmSet('pk_folder_first', S.folderFirst); if(S.renderFolderFirst) S.renderFolderFirst(); refresh(); }; if(S.renderFolderFirst) S.renderFolderFirst(); } const btnInv = hd.querySelector('#pk-btn-invert'); if(btnInv) { btnInv.onclick = (e) => { e.stopPropagation(); if (S.loading || S.display.length === 0) return; const newSel = new Set(); for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader && !S.sel.has(item.id)) newSel.add(item.id); } S.sel = newSel; S.lastSelIdx = -1; S.activeId = null; renderVisible(); updateStat(); }; btnInv.onmouseenter = () => btnInv.style.color = 'var(--pk-pri)'; btnInv.onmouseleave = () => btnInv.style.color = 'var(--pk-fg)'; } } const colPaths = hd.querySelector('[data-k="path"]'); const colStar = hd.querySelector('[data-k="starred"]'); const colDur = hd.querySelector('[data-k="duration"]'); const colSize = hd.querySelector('[data-k="size"]'); if (colPaths) colPaths.style.display = showPathCol ? 'flex' : 'none'; if (colStar) { colStar.style.display = 'flex'; colStar.style.opacity = '1'; colStar.style.pointerEvents = 'auto'; } if (colDur) { colDur.style.display = isAnalyzeRoot ? 'none' : 'flex'; colDur.style.opacity = '1'; colDur.style.pointerEvents = 'auto'; } if (colSize) colSize.style.display = 'flex'; const dateHeader = hd.querySelector('[data-k="modified_time"]'); if (dateHeader) { dateHeader.childNodes[0].textContent = S.trashMode ? L.col_remaining : L.col_date; } } } const currentCols = hd.querySelectorAll('.pk-col'); currentCols.forEach(c => { const span = c.querySelector('span'); const isSimFolderView = (isAnalyzeRoot && S.analyzeSimGroups); if (S.dupMode || S.offlineMode || S.uploadMode || isSimFolderView) { c.style.cursor = 'default'; c.style.pointerEvents = 'none'; c.style.color = 'var(--pk-fg)'; if(span) span.textContent = ''; if (c.dataset.k === 'name') { const nameWrap = c.querySelector('#pk-name-text-wrap'); if (nameWrap) nameWrap.style.color = 'var(--pk-fg)'; } } else { c.style.cursor = 'pointer'; c.style.pointerEvents = 'auto'; c.onclick = () => { const k = c.dataset.k; if (S.sort === k) S.dir *= -1; else { S.sort = k; S.dir = 1; } const curNode = S.path[S.path.length - 1]; const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curNode.id.startsWith('virtual_') || curNode.id === 'virtual_search_root'); if (isStandard) { if (gmGet('pk_sort_independent', false)) { const folderId = curNode.id || 'root'; try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); prefStore[folderId] = { sort: S.sort, dir: S.dir, folderFirst: S.folderFirst }; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } catch(e) {} } else { gmSet('pk_global_sort_pref', JSON.stringify({ sort: S.sort, dir: S.dir, folderFirst: S.folderFirst })); } } refresh(); }; if (span) span.textContent = (c.dataset.k === S.sort) ? (S.dir === 1 ? ' ▼' : ' ▲') : ''; if (c.dataset.k === 'name') { c.style.color = ''; const nameWrap = c.querySelector('#pk-name-text-wrap'); if (nameWrap) { nameWrap.style.color = (S.sort === 'name') ? 'var(--pk-pri)' : '#666'; } } else { c.style.color = (c.dataset.k === S.sort) ? 'var(--pk-pri)' : ''; } } }); UI.chkAll = hd.querySelector('#pk-all'); if (UI.chkAll) UI.chkAll.onclick = S.handleSelectAll; requestAnimationFrame(renderVisible); } const getStarIcon = (isStarred) => { const color = isStarred ? '#FFC107' : '#ccc'; const fill = isStarred ? '#FFC107' : 'none'; return ` `; }; function renderVisible() { const L = getStrings(); const winEl = el.querySelector('.pk-win'); const listIn = el.querySelector('#pk-in'); if (winEl && listIn) { const isMax = winEl.classList.contains('pk-maximized'); const cur = S.path[S.path.length - 1]; const isGroupedView = S.dupMode || (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups); const gap = isGroupedView ? (isMax ? 10 : 4) : 0; listIn.style.transform = gap > 0 ? `translateY(-${gap}px)` : 'none'; } if (S.display.length === 0) { if (S.loading) { UI.in.style.height = '100%'; UI.in.innerHTML = `
${L.loading_detail}
`; UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; if (UI.ctx) UI.ctx.style.display = 'none'; return; } if (S.items.length > 0 && !S.search && !S.dupMode && !S.trashMode && !S.shareMode && !S.offlineMode && !S.uploadMode && !S.isFlattened) { return; } if (UI.in.querySelector('.pk-empty')) { if (UI.in.style.height !== '100%') UI.in.style.height = '100%'; return; } UI.in.style.height = '100%'; UI.in.innerHTML = `
${CONF.emptySVG}
${L.str_no_files}
`; UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; if (UI.ctx) UI.ctx.style.display = 'none'; return; } UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; const top = UI.vp.scrollTop; const h = UI.vp.clientHeight; const buffer = CONF.buffer || 20; const start = Math.max(0, Math.floor(top / CONF.rowHeight) - buffer); const end = Math.min(S.display.length, Math.ceil((top + h) / CONF.rowHeight) + buffer); const isBlur = gmGet('pk_blur_thumb', false); const rootPathStr = S.path.map(p => p.name).join('/'); const lang = getLang(); let colDef; const cur = S.path[S.path.length - 1]; const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isGlobalSearchRoot = cur.id === 'virtual_search_root' && UI.chkGlobal && UI.chkGlobal.checked; const showPathCol = S.dupMode || isAnalyzeRoot || S.isFlattened || isGlobalSearchRoot; const isMax = UI.win.classList.contains('pk-maximized'); if (S.offlineMode) { colDef = "36px 1fr 120px 240px 180px"; } else if (S.uploadMode) { colDef = "36px 1fr 100px 120px 180px"; } else if (S.shareMode) { colDef = isMax ? "36px 1fr 80px 80px 120px 130px" : "36px 1fr 80px 80px 80px 130px"; } else if (S.historyMode) { colDef = "36px 30px 1fr 80px 80px 120px 160px"; } else if (S.trashMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (S.recentMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (isAnalyzeRoot) { colDef = "36px 30px 2fr 1fr 80px 130px"; } else if (showPathCol) { colDef = "36px 30px 2fr 1fr 80px 105px 130px"; } else { colDef = "36px 30px 1fr 80px 105px 130px"; } UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; const fragment = document.createDocumentFragment(); let nameColWidth = 400; let charCapacity = 50; const pathIds = S.path.map(p => p.id); const isAtGlobalSearchRoot = pathIds[pathIds.length - 1] === 'virtual_search_root'; const isGlobalSearchHistoryPresent = pathIds.includes('virtual_search_root'); const shouldShowHl = (!!S.search && (!isGlobalSearchHistoryPresent || isAtGlobalSearchRoot)); let pathColWidth = 200, pathCharCapacity = 30; if (UI.win) { const charWidth = isMax ? 9.2 : 8; const nameColEl = UI.win.querySelector('.pk-grid-hd .pk-col[data-k="name"]'); if (nameColEl) { nameColWidth = nameColEl.offsetWidth; charCapacity = Math.floor((nameColWidth - 80) / charWidth); } const pathColEl = UI.win.querySelector('.pk-grid-hd .pk-col[data-k="path"]'); if (pathColEl) { pathColWidth = pathColEl.offsetWidth; pathCharCapacity = Math.floor((pathColWidth - 20) / charWidth); } } const getTooltipHlHTML = (text, query) => { if (!query || !shouldShowHl) return esc(text); const lowText = text.toLowerCase(); const q = query.toLowerCase(); const idx = lowText.indexOf(q); if (idx === -1) return esc(text); return esc(text.substring(0, idx)) + `${esc(text.substring(idx, idx + q.length))}` + getTooltipHlHTML(text.substring(idx + q.length), query); }; const getSearchHlHTML = (name, query, capacity) => { const q = query.toLowerCase(); const idx = name.toLowerCase().indexOf(q); if (idx === -1) return esc(name); let start = 0, end = name.length, prefix = "", suffix = ""; if (name.length > capacity) { const preLimit = Math.floor(capacity * 0.3); start = Math.max(0, idx - preLimit); end = Math.min(name.length, start + capacity); if (start > 0) prefix = "..."; if (end < name.length) suffix = "..."; } const targetSlice = name.substring(start, end); return prefix + getTooltipHlHTML(targetSlice, query) + suffix; }; const _internalGetStarIcon = (isStarred) => { const color = isStarred ? '#FFC107' : '#ccc'; const fill = isStarred ? '#FFC107' : 'none'; return ` `; }; const getRemainingDays = (dateStr) => { if (!dateStr) return "-"; const deleteTime = new Date(dateStr).getTime(); const now = getServerNow(); const diffMs = now - deleteTime; const daysPassed = diffMs / (1000 * 60 * 60 * 24); let left = Math.max(0, Math.min(15, Math.ceil(15 - daysPassed))); return left + " " + L.unit_days; }; for (let i = start; i < end; i++) { const d = S.display[i]; if (!d) continue; const row = document.createElement('div'); row.style.position = 'absolute'; row.style.top = `${i * CONF.rowHeight}px`; row.style.width = '100%'; row.style.gridTemplateColumns = colDef; if (d.isHeader) { row.className = 'pk-group-hd'; if (i === 0) { row.style.setProperty('border-top', 'none', 'important'); const borderW = isMax ? 10 : 4; row.style.setProperty('padding-top', borderW + 'px', 'important'); } else { row.style.removeProperty('border-top'); row.style.removeProperty('padding-top'); } if (S.trashMode) row.style.gridColumn = "1 / 7"; else if (S.dupMode || S.analyzeMode) row.style.gridColumn = "1 / 8"; else row.style.gridColumn = "1 / 7"; const gIdx = parseInt(d.id.replace('grp_', '')); const groupData = S.dupMode ? S.dupRawGroups[gIdx] : (S.analyzeMode ? S.analyzeSimGroups[gIdx] : null); const groupItemIds = groupData ? groupData.ids :[]; let selectedInGroup = 0; groupItemIds.forEach(id => { if (S.sel.has(id)) selectedInGroup++; }); const isAllSelected = groupItemIds.length > 0 && selectedInGroup === groupItemIds.length; const isIndeterminate = selectedInGroup > 0 && selectedInGroup < groupItemIds.length; let groupIcon = CONF.dupHashSVG; const isContain = d.name.includes(L.lbl_containment); const isSimLike = d.type === L.tag_sim || d.name.includes(L.lbl_sim_score); const isNameLike = d.type === L.tag_name || (S.analyzeMode && d.name.includes(' | ') && !isSimLike && !isContain); if (isContain) { groupIcon = CONF.dupContainSVG; } else if (isSimLike) { groupIcon = CONF.dupSimSVG; } else if (isNameLike) { groupIcon = CONF.dupNameSVG; } groupIcon = groupIcon.replace(/width:\s*\d+px;?/, 'width:18px;').replace(/height:\s*\d+px;?/, 'height:18px;').replace(/margin-right:\s*\d+px;?/, 'margin:0;'); let headerName = d.name; const headerTip = getTooltipHlHTML(d.name, S.search).replace(/"/g, '"'); row.style.display = 'flex'; row.innerHTML = `
${groupIcon}
${esc(headerName)}
`; const grpChk = row.querySelector('.pk-grp-chk'); if (grpChk) { grpChk.indeterminate = isIndeterminate; grpChk.onclick = (e) => { e.stopPropagation(); const targetState = grpChk.checked; groupItemIds.forEach(id => { if (targetState) S.sel.add(id); else S.sel.delete(id); }); renderVisible(); updateStat(); }; } } else { const isSel = S.sel.has(d.id); const isFocused = S.activeId === d.id; const isMoving = S.movingIds && S.movingIds.has(d.id); row.className = `pk-row ${isSel ? 'sel' : ''} ${isFocused ? 'pk-focused' : ''} ${isMoving ? 'pk-moving' : ''}`; row.ondragstart = (e) => e.preventDefault(); if (isFocused && !isSel) { row.style.backgroundColor = 'var(--pk-sel-bg)'; row.style.border = '1px solid var(--pk-pri)'; row.style.borderRadius = '4px'; } if (isMoving) { row.style.opacity = '0.4'; row.style.filter = 'grayscale(100%)'; row.style.pointerEvents = 'none'; row.style.cursor = 'wait'; } else { row.style.opacity = ''; row.style.filter = ''; row.style.pointerEvents = ''; row.style.cursor = ''; } row.style.display = 'grid'; row.dataset.id = d.id; const isProtected = !S.trashMode && isSystemItem(d); const isMax = UI.win.classList.contains('pk-maximized'); const nameTip = getTooltipHlHTML(d.name, S.search).replace(/"/g, '"'); const getDynamicIcon = (item) => { let isBlacklisted = false; const cleanName = (item.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (item.kind === 'drive#folder') { isBlacklisted = S.blFolderSet && S.blFolderSet.has(cleanName); } else { isBlacklisted = S.blSet && S.blSet.has(cleanName); } const blHtml = isBlacklisted ? `
${CONF.icons.blMarker}
` : ''; if (S.uploadMode) { if (item.status === 'DONE' && item.file && item.mime_type && item.mime_type.startsWith('image/')) { if (!item._localThumbUrl) { try { item._localThumbUrl = URL.createObjectURL(item.file); } catch(e) {} } if (item._localThumbUrl) item.thumbnail_link = item._localThumbUrl; } const hasReadyThumb = item.status === 'DONE' && item.thumbnail_link && item.thumbnail_link !== item.icon_link; if (!hasReadyThumb) { const scriptIcon = getIcon(item); if (isMax) { const boxStyle = "width:54px; min-width:54px; height:100%; display:flex; align-items:center; justify-content:flex-start !important; margin-right:12px; position:relative;"; return `
${scriptIcon}
${blHtml}
`; } return `
${scriptIcon}
${blHtml}
`; } } if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); const isFolder = item.kind === 'drive#folder'; const isTask = item.kind === 'drive#task'; const isUploadTask = item.kind === 'pk#upload'; const lookupId = (isTask || isUploadTask) ? item.file_id : item.id; let hasValidCover = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); if (!window.pkRecentMetaCache) window.pkRecentMetaCache = new Map(); if (S.recentMode && !isFolder) { if (window.pkRecentMetaCache.has(item.id)) { const meta = window.pkRecentMetaCache.get(item.id); item.thumbnail_link = meta.thumbnail_link || item.thumbnail_link; item.icon_link = meta.icon_link || item.icon_link; item.mime_type = meta.mime_type || item.mime_type; if (meta.medias) item.medias = meta.medias; item.params = Object.assign(item.params || {}, meta.params || {}); hasValidCover = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); } else if (!hasValidCover && !item._metaFetching) { item._metaFetching = true; const ext = (item.name || '').split('.').pop().toLowerCase(); const mime = (item.mime_type || '').toLowerCase(); const isLikelyMedia = mime.startsWith('video/') || mime.startsWith('image/') ||['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','jpg','jpeg','png','gif','bmp','webp','heic','svg','tif','tiff','ico'].includes(ext); if (isLikelyMedia) { apiGet(item.id).then(meta => { if (meta) { window.pkRecentMetaCache.set(item.id, meta); item.thumbnail_link = meta.thumbnail_link || item.thumbnail_link; item.icon_link = meta.icon_link || item.icon_link; item.mime_type = meta.mime_type || item.mime_type; if (meta.medias) item.medias = meta.medias; item.params = Object.assign(item.params || {}, meta.params || {}); requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); } }).catch(()=>{}); } } } let forceDeepScan = false; if (item._coverResolved) { hasValidCover = false; forceDeepScan = true; } else if (isFolder && hasValidCover && typeof globalCache !== 'undefined' && globalCache.has(lookupId)) { forceDeepScan = true; } if ((isFolder || isTask || isUploadTask) && lookupId && (!hasValidCover || forceDeepScan) && typeof globalCache !== 'undefined') { const normalize = (data) => (data && !Array.isArray(data) && data.items) ? data.items : data; const scanDeepCover = (targetId, depth) => { if (depth > 5) return null; const raw = globalCache.get(targetId); if (!raw) return null; const files = normalize(raw); if (!files || files.length === 0) return null; const vid = files.find(f => f.mime_type?.startsWith('video/') && f.thumbnail_link); if (vid) return vid.thumbnail_link; const img = files.find(f => f.mime_type?.startsWith('image/') && f.thumbnail_link); if (img) return img.thumbnail_link; const subFolders = files.filter(f => f.kind === 'drive#folder'); for (const sub of subFolders) { if (globalCache.has(sub.id)) { const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; continue; } if (sub.thumbnail_link && sub.thumbnail_link !== sub.icon_link && !sub._coverResolved) { return sub.thumbnail_link; } const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; } return null; }; const foundThumb = scanDeepCover(lookupId, 0); if (foundThumb) { item.thumbnail_link = foundThumb; item._coverResolved = true; item._isFolderLike = true; hasValidCover = true; } else if (item._coverResolved || (isFolder && globalCache.has(lookupId))) { item.thumbnail_link = null; item._coverResolved = false; item._isFolderLike = false; hasValidCover = false; } else if ((isFolder || isTask || isUploadTask) && typeof scannedFolderIds !== 'undefined' && !scannedFolderIds.has(lookupId)) { if (!S.loading && !S.scanning) { scannedFolderIds.add(lookupId); backgroundQueue.push({ id: lookupId, name: 'DeepCoverProbe', retryCount: 0 }); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } } } const iconHtml = getIcon(item); const boxStyle = "width:54px; min-width:54px; height:100%; display:flex; align-items:center; justify-content:flex-start !important; margin-right:12px; position:relative;"; const placeholderStyle = `position: absolute; left: 0; top: 50%; transform: translateY(-50%); z-index: 1; width: 100%; display: flex; align-items: center; transition: opacity 0.3s; pointer-events: none;`; const imgStyle = "width: 48px; height: 48px; object-fit: cover; border-radius: 4px; margin: 0 !important; background: transparent; position: relative; z-index: 2; transition: opacity 0.3s;"; const isDarkTheme = document.body.classList.contains('dark') || document.documentElement.getAttribute('data-theme') === 'dark'; const badgeBg = isDarkTheme ? '#303134' : '#FFFFFF'; const badgeBorder = isDarkTheme ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.08)'; const badgeShadow = isDarkTheme ? '0 2px 6px rgba(0,0,0,0.4)' : '0 2px 6px rgba(0,0,0,0.15)'; const badgeStyle = `position: absolute; bottom: -5px; right: -5px; z-index: 3; width: 24px; height: 24px; border-radius: 50%; background-color: ${badgeBg}; border: ${badgeBorder}; box-shadow: ${badgeShadow}; box-sizing: border-box; transition: opacity 0.3s; pointer-events: none; display: grid; place-items: center; line-height: 0;`; const isBlur = gmGet('pk_blur_thumb', false); if (isMax && isBlur) { const fallbackSvg = iconHtml.replace(/"/g, """).replace(/\n/g, ""); return `
${blHtml}
`; } if (isMax && item.thumbnail_link && !item.isHeader && item.thumbnail_link !== item.icon_link) { const isCached = window.pkGlobalThumbCache.has(item.id); const phOp = isCached ? '0' : '1'; const imgOp = isCached ? '1' : '0'; const placeholderContent = ``; const isFolderLike = isFolder || item._isFolderLike || (item.icon_link && item.icon_link.includes('folder')); const badgeOp = (isFolderLike && isCached) ? '1' : '0'; let badgeHtml = ''; if (isFolderLike) { const innerContent = ``; badgeHtml = `
${innerContent}
`; } const isVideo = !isFolder && item.mime_type && item.mime_type.startsWith('video/'); let videoOvHtml = ''; if (isVideo) { const vOp = isCached ? '1' : '0'; videoOvHtml = `
`; } return `
${badgeHtml} ${videoOvHtml} ${blHtml}
`; } if (isMax) { if (hasValidCover) { const showBadge = isFolder || item._isFolderLike || (item.icon_link && item.icon_link.includes('folder')); let badgeHtml = ''; if (showBadge) { const innerIcon = ``; badgeHtml = `
${innerIcon}
`; } return `
${badgeHtml} ${blHtml}
`; } const finalIconSrc = item.icon_link || item.thumbnail_link; if (finalIconSrc) { return `
'">${blHtml}
`; } return `
${iconHtml}
${blHtml}
`; } const mime = (item.mime_type || '').toLowerCase(); const isMedia = item.kind !== 'drive#folder' && (mime.startsWith('image/') || mime.startsWith('video/') || (item.params && item.params.duration > 0)); const hasRealThumb = isMedia && item.thumbnail_link && item.thumbnail_link !== item.icon_link; if (hasRealThumb) { if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); const isCached = window.pkGlobalThumbCache.has(item.id); const phOp = isCached ? '0' : '1'; const imgOp = isCached ? '1' : '0'; const placeholder = item.icon_link ? `` : iconHtml; return `
${placeholder}
${blHtml}
`; } if (item.icon_link) { return `
${iconHtml}${blHtml}
`; } return `
${iconHtml}${blHtml}
`; }; const checkboxHtml = ``; let html = `
${checkboxHtml}
`; if (S.historyMode) { const isStarred = S.starredSet.has(d.id) || !!(d.starred || (d.tags && d.tags.some(t => t.name === 'STAR'))); const starColor = isStarred ? '#FFC107' : '#ccc'; const starFill = isStarred ? '#FFC107' : 'none'; html += `
`; let iconImg = getDynamicIcon(d); if (isMax) { if (iconImg.includes('margin-right:12px')) iconImg = iconImg.replace('margin-right:12px', 'margin-right:20px'); else if (!iconImg.includes('margin-right')) iconImg = iconImg.replace(/style=['"]/, '$&margin-right:20px; '); } const nameDisplay = (S.search && shouldShowHl) ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${iconImg} ${nameDisplay}
`; html += `
${fmtSize(d.size)}
`; const displayDur = d._history_duration || d.params?.duration || 0; html += `
${displayDur > 0 ? fmtDur(displayDur) : '-'}
`; let progressHtml = '-'; const curT = d._history_progress || 0; const totalT = displayDur; if (totalT > 0) { const pct = Math.min(100, Math.max(0, (curT / totalT) * 100)).toFixed(1); progressHtml = `
${fmtDur(curT)} ${parseInt(pct)}%
`; } html += `
${progressHtml}
`; let playTimeStr = '-'; if (d._history_ts > 0) { playTimeStr = fmtDate(new Date(d._history_ts).toISOString()); } else { playTimeStr = "-"; } html += `
${playTimeStr}
`; } else if (S.offlineMode) { let iconImg = getDynamicIcon(d); if (isMax) { if (iconImg.includes('margin-right:12px')) { iconImg = iconImg.replace('margin-right:12px', 'margin-right:20px'); } else if (!iconImg.includes('margin-right')) { iconImg = iconImg.replace(/style=['"]/, '$&margin-right:20px; '); } } const isFailed = d.phase === 'PHASE_TYPE_ERROR'; const isNavigable = !!d.file_id && !isFailed; const isTaskDone = d.phase === 'PHASE_TYPE_COMPLETE'; const nameStyle = isNavigable ? 'cursor:pointer; opacity:1;' : 'cursor:default; pointer-events:none; color:inherit; opacity:0.6;'; const isMedia = d.mime_type && (d.mime_type.startsWith('video/') || d.mime_type.startsWith('image/')); const hasResolvedCover = d._coverResolved && d.thumbnail_link; const thumbAttr = (isTaskDone && (isMedia || hasResolvedCover)) ? `data-pk-thumb="${d.thumbnail_link}"` : ''; const nameDisplay = S.search ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${iconImg} ${nameDisplay}
`; html += `
${fmtSize(d.size)}
`; let statusHtml = ''; let statusColor = 'var(--pk-fg)'; let statusText = d.phase; if (d.phase === 'PHASE_TYPE_COMPLETE') { statusColor = '#52c41a'; statusText = L.lbl_up_done; } else if (d.phase === 'PHASE_TYPE_RUNNING') { statusColor = '#1890ff'; statusText = L.lbl_up_downloading; } else if (d.phase === 'PHASE_TYPE_ERROR') { statusColor = '#ff4d4f'; statusText = L.str_failed; } else if (d.phase === 'PHASE_TYPE_PENDING') { statusText = L.msg_task_waiting; } else if (d.phase === 'PHASE_TYPE_PAUSED') { statusText = L.msg_task_paused; } if (d.phase === 'PHASE_TYPE_ERROR' && d.message) { statusText = d.message; } html += `
${statusText}
`; let progressHtml = ''; if (d.phase === 'PHASE_TYPE_COMPLETE') { progressHtml = `
100%
`; } else if (d.phase === 'PHASE_TYPE_ERROR') { progressHtml = `
-
`; } else { const pct = d.progress || 0; progressHtml = `
${pct}%
`; } html += `
${progressHtml}
`; } else if (S.uploadMode) { if (!d.mime_type && d.file) d.mime_type = d.file.type; const nameDisplay = shouldShowHl ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); const isDone = d.status === 'DONE'; const nameStyle = isDone ? '' : 'cursor:default; pointer-events:none; color:inherit;'; if (isDone && d.file && d.mime_type && d.mime_type.startsWith('image/')) { if (!d._localThumbUrl) { try { d._localThumbUrl = URL.createObjectURL(d.file); } catch(e) {} } if (d._localThumbUrl) d.thumbnail_link = d._localThumbUrl; } const hasResolvedCover = isDone && d.thumbnail_link && d.thumbnail_link !== d.icon_link; const thumbAttr = hasResolvedCover ? `data-pk-thumb="${d.thumbnail_link}"` : ''; html += `
${getDynamicIcon(d)} ${nameDisplay}
`; html += `
${fmtSize(d.size)}
`; html += `
${(d.status === 'UPLOADING' && S.upMng) ? S.upMng.fmtSpeed(d.speed) : '-'}
`; let statusColor = '#888'; const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; if (activeStatus.includes(d.status)) statusColor = 'var(--pk-pri)'; else if (d.status === 'DONE') statusColor = '#52c41a'; else if (d.status === 'ERROR') statusColor = '#d93025'; else if (d.status === 'PAUSED') statusColor = '#faad14'; html += `
${d.message} ${Math.floor(d.progress)}%
`; } else if (S.shareMode) { const iconUrl = d.icon_link; let iconImg = ''; const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); const lockHtml = d.pass_code ? `
${CONF.icons.lock}
` : ''; if (isShareDisabled) { iconImg = `
${CONF.typeIcons.file}
`; } else { const currentIcon = getDynamicIcon(d); const isUsingThumb = currentIcon.includes('pk-max-thumb'); if (isUsingThumb) { iconImg = `
${currentIcon}${lockHtml}
`; } else if (iconUrl) { iconImg = `
${lockHtml}
`; } else { const isFolder = d.kind === 'drive#folder'; iconImg = `
${getIcon(d)}
${lockHtml}
`; } } const nameDisplay = shouldShowHl ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${iconImg} ${nameDisplay}
`; html += `
${d.view_count || 0}
`; const saveVal = d.limit_count > 0 ? `${d.save_count || 0}/${d.limit_count}` : (d.save_count || 0); const saveTip = d.limit_count > 0 ? `${L.lbl_limit_tip}: ${d.limit_count}` : ''; html += `
${saveVal}
`; let statusColor = 'inherit'; let statusText = ''; const timeLeft = d.expiration_left || ""; if (d.share_status !== 'OK') { statusColor = '#ff4d4f'; statusText = (d.save_count >= d.limit_count && d.limit_count > 0) ? (L.lbl_limit_reached) : (d.share_status_text || d.share_status); } else if (d.expiration_days === "-1" || timeLeft === "-1") { statusColor = '#52c41a'; statusText = L.share_perm; } else { statusText = timeLeft + (L.str_expire_suffix); const isUrgent = timeLeft.includes('小时') || timeLeft.includes('hour') || timeLeft.includes('시간') || timeLeft.includes('時間'); statusColor = isUrgent ? '#ff4d4f' : 'inherit'; } html += `
${statusText}
`; const shareDate = fmtDate(d.modified_time); html += `
${shareDate}
`; } else { const isRoot = S.path.length === 1 && S.path[0].id === ''; const isStarred = S.starredSet.has(d.id) || !!(d.starred || (d.tags && d.tags.some(t => t.name === 'STAR'))); const starColor = isStarred ? '#FFC107' : '#ccc'; const starFill = isStarred ? '#FFC107' : 'none'; let displayPath = rootPathStr; let shortPath = ""; if (S.trashMode) { if (d._lineage && Array.isArray(d._lineage)) { shortPath = d._lineage.map(p => p.name).join('/'); displayPath = shortPath; } } else if (d._lineage && Array.isArray(d._lineage)) { const relativePath = d._lineage.map(p => p.name).join('/'); shortPath = relativePath; if (isGlobalSearchRoot) { displayPath = L.btn_nav_home + (relativePath ? '/' + relativePath : ''); } else if (S.analyzeMode) { displayPath = relativePath || L.btn_nav_home; } else if (relativePath) { displayPath = rootPathStr + '/' + relativePath; } else { displayPath = rootPathStr; } } else if (isGlobalSearchRoot) { displayPath = L.btn_nav_home; shortPath = ""; } if (S.trashMode) { html += `
`; } else { if (isProtected) html += `
`; else html += `
`; } const isRealThumb = d.thumbnail_link && d.thumbnail_link !== d.icon_link; const thumbAttr = isRealThumb ? `data-pk-thumb="${d.thumbnail_link}"` : ''; const nameDisplay = (S.search && shouldShowHl) ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${getDynamicIcon(d)}${nameDisplay}${isProtected ? `${L.tag_default}` : ''}
`; if (showPathCol) { let pathHtml = ''; const homeIcon = ``; const homeText = L.btn_nav_home; let homeQuery = S.search; let restQuery = S.search; let isSlashMatched = false; if (S.search) { const qLower = S.search.toLowerCase(); const hLower = homeText.toLowerCase(); if (qLower.startsWith(hLower + '/')) { homeQuery = S.search.substring(0, homeText.length); restQuery = S.search.substring(homeText.length + 1); isSlashMatched = true; } } const isHomeMatched = shouldShowHl && homeQuery && homeText.toLowerCase().includes(homeQuery.toLowerCase()); const homeDisplay = isHomeMatched ? getSearchHlHTML(homeText, homeQuery, 20) : esc(homeText); const prefix = homeText + '/'; const rootStyle = isMax? "display:inline-flex;align-items:baseline;flex-shrink:0;margin-right:2px;": "display:inline-flex;align-items:baseline;flex-shrink:0;line-height:1.2;padding-bottom:0;"; const contentStyle = isMax ? '' : 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.5;padding-bottom:2px;'; const containerStyle = isMax? "display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-all;line-height:1.4;white-space:normal;color:var(--pk-fg);": "display:flex;align-items:center;overflow:hidden;white-space:nowrap;color:var(--pk-fg);line-height:1.5;padding-bottom:2px;"; if (isAnalyzeRoot) { const pStr = d._pathStr || d.path || ""; displayPath = pStr; let isSameAsPrev = d._isSameFolder || false; if (!isSameAsPrev && S.sort === 'path' && i > 0) { const prevItem = S.display[i - 1]; const prevPStr = prevItem._pathStr || prevItem.path || ""; if (prevPStr === pStr) isSameAsPrev = true; } if (isSameAsPrev) { pathHtml = `${L.str_same_folder}`; } else { let content = pStr; if (pStr === homeText) content = ""; else if (pStr.startsWith(prefix)) content = pStr.substring(prefix.length); let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && content.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const sDisp = (shouldShowHl && sm) ? `/` : '/'; const sHtml = `${sDisp}`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && content) ? getSearchHlHTML(content, hq, pathCharCapacity) : esc(content); let innerHtml = `${homeIcon}${homeDisplay}`; if (content) { innerHtml += `${sHtml}${pathDisplay}`; } pathHtml = `
${innerHtml}
`; } } else if (S.dupMode) { if (d._isSameFolder) { pathHtml = `${L.str_same_folder}`; } else { const rawPath = d._dupFullPath || ""; let realPath = rawPath; if (rawPath === L.current_dir) { realPath = rootPathStr; } else { if (!rawPath.startsWith(rootPathStr)) { realPath = rootPathStr + '/' + rawPath; } } let contentStr = realPath; if (realPath === homeText) { contentStr = ""; } else if (realPath.startsWith(prefix)) { contentStr = realPath.substring(prefix.length); } let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && contentStr.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const sDisp = (shouldShowHl && sm) ? `/` : '/'; const sHtml = `${sDisp}`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && contentStr) ? getSearchHlHTML(contentStr, hq, pathCharCapacity) : esc(contentStr); let innerHtml = `${homeIcon}${homeDisplay}`; if (contentStr) { innerHtml += `${sHtml}${pathDisplay}`; } pathHtml = `
${innerHtml}
`; } } else { let fullPathStr = ""; if (d._lineage === null && d.parent_id && d.parent_id !== 'root') { pathHtml = `...`; displayPath = null; } else if (isGlobalSearchRoot) { fullPathStr = shortPath; } else { if (displayPath && displayPath.startsWith(prefix)) { fullPathStr = displayPath.substring(prefix.length); } else { fullPathStr = shortPath; } } let isSameAsPrev = false; if (S.sort === 'path' && i > 0) { const prevItem = S.display[i - 1]; let prevPathStr = ""; if (S.analyzeMode) { prevPathStr = prevItem._pathStr || ""; } else { if (prevItem._lineage) prevPathStr = prevItem._lineage.map(x=>x.name).join('/'); } if (prevPathStr === shortPath) isSameAsPrev = true; } if (displayPath !== null) { if (isSameAsPrev) { pathHtml = `${L.str_same_folder}`; } else { let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && fullPathStr.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const sDisp = (shouldShowHl && sm) ? `/` : '/'; const sHtml = `${sDisp}`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && fullPathStr) ? getSearchHlHTML(fullPathStr, hq, pathCharCapacity) : esc(fullPathStr); let innerHtml = `${homeIcon}${homeDisplay}`; if (fullPathStr) { innerHtml += `${sHtml}${pathDisplay}`; } pathHtml = `
${innerHtml}
`; } } } let pathTipHtml = getTooltipHlHTML(displayPath || "", S.search); if (displayPath && displayPath.startsWith(homeText)) { const rest = displayPath.substring(homeText.length); const homeDisplayTip = getTooltipHlHTML(homeText, homeQuery); let restDisplayTip = ''; if (rest.startsWith('/')) { const pureRest = rest.substring(1); let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && pureRest.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const slashTip = (shouldShowHl && sm) ? `/` : '/'; restDisplayTip = slashTip + getTooltipHlHTML(pureRest, hq); } else { restDisplayTip = getTooltipHlHTML(rest, restQuery); } const homeGroup = `${homeIcon}${homeDisplayTip}`; pathTipHtml = `
${homeGroup}${restDisplayTip}
`; } html += `
${pathHtml}
`; } const displaySize = (d.kind === 'drive#folder' && !S.analyzeMode) ? '-' : fmtSize(d.size); html += `
${displaySize}
`; if (!isAnalyzeRoot) { const isFolder = d.kind === 'drive#folder'; const displayDur = S.durationMap.get(d.id) || d.params?.duration || 0; const mime = (d.mime_type || '').toLowerCase(); const ext = (d.name || '').split('.').pop().toLowerCase(); const isVideoFormat =['mp4','mkv','avi','mov','wmv','flv','webm','ts'].includes(ext); const isVideo = !isFolder && (mime.startsWith('video/') || isVideoFormat || displayDur > 0); let durHtml = "-"; if (isFolder) { if (!S.trashMode) { let count = d.file_count; if ((count === undefined || count === null) && typeof globalCache !== 'undefined') { const cachedData = globalCache.get(d.id); if (cachedData) { const list = Array.isArray(cachedData) ? cachedData : (cachedData.items ||[]); count = list.length; } } const hasCount = count !== undefined && count !== null; if (hasCount) { durHtml = `${count} ${L.str_items}`; } } } else if (isVideo && displayDur > 0) { durHtml = fmtDur(displayDur); } else { const rawName = d.name || ""; const lastDot = rawName.lastIndexOf('.'); if (lastDot > 0) { const extUpper = rawName.substring(lastDot + 1).toUpperCase(); const SETS = { vid: new Set(['MP4','MKV','AVI','MOV','WMV','FLV','WEBM','TS','M4V','3GP','MPG','MPEG','RM','RMVB','ASF','VOB','DAT','DIVX','F4V','M2TS','MTS','TP','TRP','OGV','MPE','M2V','M3U8']), aud: new Set(['MP3','WAV','FLAC','AAC','OGG','WMA','APE','M4A','AMR','OPUS','M4B','ALAC','AIFF','MID','MIDI','RA','DTS','AC3','DSF','DFF']), img: new Set(['JPG','JPEG','PNG','GIF','BMP','WEBP','SVG','TIF','TIFF','ICO','HEIC','HEIF','RAW','CR2','NEF','ARW','DNG','ORF','AVIF','PSD','AI','EPS','JFIF','JPE']), doc: new Set(['TXT','HTML','PDF','PPTX','CHM','DOCX','XLSX','HTM','DOC','DWG','MDB','PPT','XLS','RTF','ODT','ODS','ODP','EPUB','MOBI','AZW3','DJVU','CBZ','CBR','MD','LOG','CSV','XML','JSON']), app: new Set(['APK','EXE','IPA','DMG','RPM','DEB','MSI','PKG','XAPK','APKS','AAB','JAR','BIN','SH','BAT','CMD']), arc: new Set(['ZIP','RAR','7Z','TAR','GZ','ISO','CAB','BZ2','XZ','TGZ','WIM','ESD','IMG','ZST','LZH']), sub: new Set(['SRT','ASS','SSA','VTT','SMI','SUB','IDX','SUP','LRC']) }; if (SETS.img.has(extUpper)) durHtml = `${extUpper} ${L.type_img}`; else if (SETS.arc.has(extUpper) || /^(PART\d+|\d{3}|[RZ]\d{2})$/.test(extUpper)) durHtml = `${extUpper} ${L.type_archive}`; else if (SETS.doc.has(extUpper)) durHtml = `${extUpper} ${L.type_doc}`; else if (SETS.sub.has(extUpper)) durHtml = `${extUpper} ${L.type_sub}`; else if (SETS.app.has(extUpper)) durHtml = `${extUpper} ${L.type_app}`; else if (SETS.aud.has(extUpper)) durHtml = `${extUpper} ${L.cat_audio}`; else if (SETS.vid.has(extUpper)) durHtml = `${extUpper} ${L.cat_video}`; else durHtml = `${extUpper} ${L.type_suffix}`; } else { durHtml = L.type_suffix; } } html += `
${durHtml}
`; } const dateTxt = S.trashMode ? getRemainingDays(d.modified_time) : fmtDate(d.modified_time); html += `
${dateTxt}
`; } row.innerHTML = html; const thumbImg = row.querySelector('.pk-max-thumb, .pk-min-thumb'); if (thumbImg) { const toggleThumb = () => { if(window.pkGlobalThumbCache) window.pkGlobalThumbCache.add(d.id); thumbImg.style.opacity = '1'; const placeholder = thumbImg.previousElementSibling; if (placeholder && (placeholder.classList.contains('pk-placeholder-layer') || placeholder.classList.contains('pk-min-ph'))) { placeholder.style.opacity = '0'; } const parent = thumbImg.parentElement; const badge = parent ? parent.querySelector('.pk-folder-badge') : null; if (badge) { badge.style.opacity = '1'; } const vidOv = parent ? parent.querySelector('.pk-video-ov') : null; if (vidOv) { vidOv.style.opacity = '1'; } }; const handleThumbError = () => { thumbImg.style.display = 'none'; if (window.pkGlobalThumbCache) window.pkGlobalThumbCache.delete(d.id); const placeholder = thumbImg.previousElementSibling; if (placeholder && (placeholder.classList.contains('pk-placeholder-layer') || placeholder.classList.contains('pk-min-ph'))) { placeholder.style.opacity = '1'; } const parent = thumbImg.parentElement; const badge = parent ? parent.querySelector('.pk-folder-badge') : null; if (badge) { badge.style.opacity = '0'; } }; if (thumbImg.complete) { if (thumbImg.naturalWidth > 0) toggleThumb(); else handleThumbError(); } else { thumbImg.onload = toggleThumb; thumbImg.onerror = handleThumbError; } } if (!d.isHeader && d.thumbnail_link) { const minIcon = row.querySelector('.pk-min-icon'); if (minIcon && !minIcon.querySelector('.pk-proxy-thumb')) { const proxyImg = document.createElement('img'); proxyImg.className = 'pk-proxy-thumb'; proxyImg.src = d.thumbnail_link; proxyImg.style.cssText = 'position: absolute; top: 0; left: 0; width: 0; height: 0; opacity: 0; pointer-events: none; visibility: hidden;'; minIcon.appendChild(proxyImg); } } const chk = row.querySelector('input'); const triggerOpen = () => { if (S.movingIds.has(d.id)) return; if (S.trashMode) return; const checkType = (it) => { const m = (it.mime_type || '').toLowerCase(); const n = (it.name || '').toLowerCase(); const dur = (it.params && it.params.duration) || 0; const vExts = ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp']; const aExts = ['zip','rar','7z','tar','gz','bz2','xz']; return { isVideo: m.startsWith('video/') || dur > 0 || vExts.some(e => n.endsWith('.' + e)), isImage: m.startsWith('image/'), isTorrent: n.endsWith('.torrent'), isArchive: m.includes('zip') || m.includes('rar') || m.includes('archive') || aExts.some(e => n.endsWith('.' + e)) }; }; if (S.uploadMode) { if (d.status !== 'DONE') return; const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isVideo || t.isImage) { if (t.isVideo) playVideo(d); else showImage(d); } else if (d.file_id) { S.sel.clear(); S.sel.add(d.id); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); } return; } if (S.offlineMode) { if (!d.file_id || d.phase === 'PHASE_TYPE_ERROR') return; const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isVideo || t.isImage) { if (t.isVideo) playVideo(d); else showImage(d); } else { S.sel.clear(); S.sel.add(d.id); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) { locateBtn.click(); } } return; } if (S.shareMode) { const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if (isShareDisabled) return; showShareDetail(d); return; } if (d.kind === 'drive#folder') { if (!S.trashMode) { const isExitMode = S.isFlattened || S.dupMode; if (isExitMode) { S.isFlattened = false; S.dupMode = false; UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if(UI.dupTools) UI.dupTools.style.display = 'none'; if(UI.btnFolderFirst) UI.btnFolderFirst.style.display = 'flex'; if(UI.btnNewFolder) UI.btnNewFolder.style.display = 'flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if (d._lineage && d._lineage.length > 0) { const newPath = [{ id: '', name: L.btn_nav_home }]; d._lineage.forEach(n => { if (n.id && n.id !== 'root') newPath.push({ id: n.id, name: n.name }); }); if (newPath.length === 0 || newPath[newPath.length-1].id !== d.id) newPath.push({ id: d.id, name: d.name }); S.path = newPath; } else { S.path = [{ id: '', name: L.btn_nav_home }, { id: d.id, name: d.name }]; } load(); } else { const lastNode = S.path[S.path.length - 1]; if (lastNode && lastNode.id === d.id) return; S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); S.path.push(d); load(); } } } else { const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isArchive) handleOpenArchive(d); else if (t.isVideo) playVideo(d); else if (t.isImage) showImage(d); } }; row.onclick = async (e) => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } const nameText = e.target.closest('.pk-name .pk-name-txt'); const isAnalyzeRoot = S.analyzeMode && S.path[S.path.length - 1].id === 'analyze_root'; const isProtectedView = S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups); if (!S.trashMode && nameText) { if (isProtectedView && S.sel.size > 5) { if (S.sel.size > 1 || !S.sel.has(d.id)) { if (!await confirmSelectionClear()) return; } } S.sel.clear(); S.sel.add(d.id); S.activeId = d.id; triggerOpen(); renderVisible(); updateStat(); return; } S.activeId = d.id; if (e.target === chk) { if (e.shiftKey && S.lastSelIdx !== -1) { const startIdx = Math.min(S.lastSelIdx, i); const endIdx = Math.max(S.lastSelIdx, i); const targetState = chk.checked; for (let k = startIdx; k <= endIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) { if (targetState) S.sel.add(item.id); else S.sel.delete(item.id); } } } else { if (chk.checked) S.sel.add(d.id); else S.sel.delete(d.id); } } else { if (e.shiftKey && S.lastSelIdx !== -1) { const startIdx = Math.min(S.lastSelIdx, i); const endIdx = Math.max(S.lastSelIdx, i); for (let k = startIdx; k <= endIdx; k++) { const item = S.display[k]; if (item && !item.isHeader) S.sel.add(item.id); } } else if (e.ctrlKey || e.metaKey) { if (S.sel.has(d.id)) S.sel.delete(d.id); else S.sel.add(d.id); } else { if (isProtectedView && S.sel.size > 5) { if (S.sel.size > 1 || !S.sel.has(d.id)) { if (!await confirmSelectionClear()) { renderVisible(); return; } } } S.sel.clear(); S.sel.add(d.id); } } S.lastSelIdx = i; const rows = UI.in.children; for (let k = 0; k < rows.length; k++) { const r = rows[k]; if (r.dataset.id) { const rid = r.dataset.id; const isSelected = S.sel.has(rid); const isActiveRow = (S.activeId === rid); const isFocused = isSelected && isActiveRow; if (!(isActiveRow && !isSelected)) { r.style.backgroundColor = ''; r.style.border = ''; r.style.borderRadius = ''; } if (isActiveRow && !isSelected) { r.style.backgroundColor = 'var(--pk-sel-bg)'; r.style.border = '1px solid var(--pk-pri)'; r.style.borderRadius = '4px'; } let cls = 'pk-row'; if (isSelected) cls += ' sel'; if (isFocused) cls += ' pk-focused'; if (r.classList.contains('pk-moving')) cls += ' pk-moving'; if (r.className !== cls) r.className = cls; const cb = r.querySelector('input[type="checkbox"]'); if (cb && cb.checked !== isSelected) cb.checked = isSelected; } else if (r.classList.contains('pk-group-hd')) { const grpChk = r.querySelector('.pk-grp-chk'); if (grpChk) { const gNode = S.display[start + k]; if (gNode && gNode.isHeader) { const gIdx = parseInt(gNode.id.replace('grp_', '')); const gIds = S.dupMode ? (S.dupRawGroups[gIdx]?.ids || []) : (S.analyzeMode ? (S.analyzeSimGroups[gIdx]?.ids || []) : []); let selCount = 0; gIds.forEach(id => { if(S.sel.has(id)) selCount++; }); const isAll = gIds.length > 0 && selCount === gIds.length; const isInd = selCount > 0 && selCount < gIds.length; if (grpChk.checked !== isAll) grpChk.checked = isAll; if (grpChk.indeterminate !== isInd) grpChk.indeterminate = isInd; } } } } updateStat(); }; row.ondblclick = (e) => { e.preventDefault(); if (S.trashMode) return; if (S.uploadMode) { if (d.status !== 'DONE') return; triggerOpen(); return; } if (S.shareMode) { const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if (!isShareDisabled) { showShareDetail(d); } return; } const isNameTxt = e.target.closest('.pk-name .pk-name-txt'); if (e.target === chk || isNameTxt) return; if (S.offlineMode) { triggerOpen(); return; } if (d.kind === 'drive#folder') { const isExitMode = S.isFlattened || S.dupMode; if (isExitMode) { S.isFlattened = false; S.dupMode = false; if (S.filterState) S.filterState = { active: false, cat: 'all', ext: 'all' }; UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if(UI.dupTools) UI.dupTools.style.display = 'none'; if(UI.dupFilters) UI.dupFilters.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if(UI.btnNewFolder) UI.btnNewFolder.style.display = 'flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if (d._lineage && d._lineage.length > 0) { const newPath = [{ id: '', name: L.btn_nav_home }]; d._lineage.forEach(n => { if (n.id && n.id !== 'root') newPath.push({ id: n.id, name: n.name }); }); if (newPath.length === 0 || newPath[newPath.length-1].id !== d.id) { newPath.push({ id: d.id, name: d.name }); } S.path = newPath; } else { S.path = [{ id: '', name: L.btn_nav_home }, { id: d.id, name: d.name }]; } load(); } else { S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); S.path.push(d); load(); } } else { const mime = (d.mime_type || "").toLowerCase(); const name = (d.name || "").toLowerCase(); if (name.endsWith('.torrent')) { handleTorrentFile(d); } else if (mime.includes('zip') || mime.includes('rar') || mime.includes('7z') || mime.includes('compressed') || mime.includes('archive') || name.endsWith('.zip') || name.endsWith('.rar') || name.endsWith('.7z') || name.endsWith('.tar') || name.endsWith('.gz')) { handleOpenArchive(d); } else if (mime.startsWith('video')) { playVideo(d); } else if (mime.startsWith('image')) { showImage(d); } } }; row.oncontextmenu = (e) => { e.preventDefault(); if (!S.sel.has(d.id)) { S.sel.clear(); S.sel.add(d.id); S.lastSelIdx = i; renderVisible(); updateStat(); } const itms = { share: UI.ctx.querySelector('#ctx-share'), open: UI.ctx.querySelector('#ctx-open'), extPlay: UI.ctx.querySelector('#ctx-ext-play'), star: UI.ctx.querySelector('#ctx-star'), prop: UI.ctx.querySelector('#ctx-property'), locate: UI.ctx.querySelector('#ctx-locate'), down: UI.ctx.querySelector('#ctx-down'), cpName: UI.ctx.querySelector('#ctx-copy-name'), cut: UI.ctx.querySelector('#ctx-cut'), copy: UI.ctx.querySelector('#ctx-copy'), rename: UI.ctx.querySelector('#ctx-rename'), prune: UI.ctx.querySelector('#ctx-prune'), addBl: UI.ctx.querySelector('#ctx-add-bl'), del: UI.ctx.querySelector('#ctx-del'), restore: UI.ctx.querySelector('#ctx-restore'), delF: UI.ctx.querySelector('#ctx-del-forever'), shCancel: UI.ctx.querySelector('#ctx-share-cancel'), shDetail: UI.ctx.querySelector('#ctx-share-detail'), shCopy: UI.ctx.querySelector('#ctx-share-copy'), taskRetry: UI.ctx.querySelector('#ctx-task-retry') }; const seps = Array.from(UI.ctx.querySelectorAll('.pk-ctx-sep')); if (!itms.taskRetry) { const retryDiv = document.createElement('div'); retryDiv.className = 'pk-ctx-item'; retryDiv.id = 'ctx-task-retry'; retryDiv.innerHTML = `${CONF.icons.refresh} ${L.btn_retry_task}`; const ref = document.getElementById('ctx-down'); if (ref) ref.parentNode.insertBefore(retryDiv, ref); itms.taskRetry = retryDiv; } if (!UI.ctx.querySelector('#ctx-up-pause')) { const pBtn = document.createElement('div'); pBtn.className = 'pk-ctx-item'; pBtn.id = 'ctx-up-pause'; pBtn.innerHTML = `${CONF.icons.taskPause} ${L.btn_up_pause}`; const delRef = document.getElementById('ctx-del'); if (delRef) delRef.parentNode.insertBefore(pBtn, delRef); } if (!UI.ctx.querySelector('#ctx-up-start')) { const sBtn = document.createElement('div'); sBtn.className = 'pk-ctx-item'; sBtn.id = 'ctx-up-start'; sBtn.innerHTML = `${CONF.icons.taskStart} ${L.btn_up_start}`; const delRef = document.getElementById('ctx-del'); if (delRef) delRef.parentNode.insertBefore(sBtn, delRef); } const allCtxItems = UI.ctx.querySelectorAll('.pk-ctx-item'); const allCtxSeps = UI.ctx.querySelectorAll('.pk-ctx-sep'); allCtxItems.forEach(el => el.style.display = 'none'); allCtxSeps.forEach(el => el.style.display = 'none'); const sepShareExtra = UI.ctx.querySelector('#sep-share-extra'); if (S.shareMode) { const sh = { name: UI.ctx.querySelector('#ctx-copy-name'), cancel: UI.ctx.querySelector('#ctx-sh-cancel'), bl: UI.ctx.querySelector('#ctx-add-bl'), detail: UI.ctx.querySelector('#ctx-sh-detail'), copy: UI.ctx.querySelector('#ctx-sh-copy') }; const isSingle = (S.sel.size === 1); const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if(seps[0]) seps[0].style.display = 'none'; if(seps[1]) seps[1].style.display = 'none'; if(sh.name) sh.name.style.display = 'flex'; if(seps[2]) seps[2].style.display = 'block'; if(sh.cancel) sh.cancel.style.display = 'flex'; if(sh.bl) { sh.bl.style.display = 'flex'; let isRemoveMode = false; for (const id of S.sel) { const target = S.itemMap.get(id); if (target && S.blSet.has((target.name || target.title || "").toLowerCase().trim())) { isRemoveMode = true; break; } } sh.bl.innerHTML = isRemoveMode ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; sh.bl.setAttribute('data-action', isRemoveMode ? 'remove' : 'add'); } const showDetails = isSingle && !isShareDisabled; if (showDetails) { if(sepShareExtra) sepShareExtra.style.display = 'block'; if(sh.detail) sh.detail.style.display = 'flex'; if(sh.copy) sh.copy.style.display = 'flex'; } if(seps[3]) seps[3].style.display = 'none'; } else if (S.uploadMode) { const upEls = { open: UI.ctx.querySelector('#ctx-open'), ext: UI.ctx.querySelector('#ctx-ext-play'), loc: UI.ctx.querySelector('#ctx-locate'), name: UI.ctx.querySelector('#ctx-copy-name'), del: UI.ctx.querySelector('#ctx-del'), pause: UI.ctx.querySelector('#ctx-up-pause'), start: UI.ctx.querySelector('#ctx-up-start'), sep1: UI.ctx.querySelector('#sep-1'), sep2: UI.ctx.querySelector('#sep-2'), sep3: UI.ctx.querySelector('#sep-3') }; if (S.sel.size > 1) { const items = Array.from(S.sel).map(id => S.itemMap.get(id)).filter(Boolean); let hasActive = false, hasPaused = false, hasDone = false; items.forEach(t => { if (['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(t.status)) hasActive = true; else if (['PAUSED', 'ERROR'].includes(t.status)) hasPaused = true; else if (t.status === 'DONE') hasDone = true; }); upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; if (hasActive && !hasPaused && !hasDone) { if (upEls.sep1) { upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; } if (upEls.pause) { upEls.pause.style.order = '20'; upEls.pause.style.display = 'flex'; upEls.pause.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpPause) UI.btnUpPause.click(); }; } } else if (!hasActive && hasPaused && !hasDone) { if (upEls.sep1) { upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; } if (upEls.start) { upEls.start.style.order = '20'; upEls.start.style.display = 'flex'; upEls.start.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpStart) UI.btnUpStart.click(); }; } } else { } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else { const mime = (d.mime_type || '').toLowerCase(); const isImg = mime.startsWith('image/'); const isVideo = mime.startsWith('video/') || ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp'].some(e => (d.name||'').toLowerCase().endsWith('.'+e)); const isActive = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(d.status); const isDone = d.status === 'DONE'; if (isDone) { upEls.open.style.order = '10'; upEls.open.style.display = (isVideo || isImg) ? 'flex' : 'none'; upEls.ext.style.order = '11'; upEls.ext.style.display = isVideo ? 'flex' : 'none'; upEls.loc.style.order = '12'; upEls.loc.style.display = d.file_id ? 'flex' : 'none'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; upEls.name.style.order = '20'; upEls.name.style.display = 'flex'; upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else if (isActive) { upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; if (upEls.pause) { upEls.pause.style.order = '20'; upEls.pause.style.display = 'flex'; upEls.pause.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpPause) UI.btnUpPause.click(); }; } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else { upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; if (upEls.start) { upEls.start.style.order = '20'; upEls.start.style.display = 'flex'; upEls.start.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpStart) UI.btnUpStart.click(); }; } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } } } else if (S.offlineMode) { const els = { open: UI.ctx.querySelector('#ctx-open'), ext: UI.ctx.querySelector('#ctx-ext-play'), loc: UI.ctx.querySelector('#ctx-locate'), prop: UI.ctx.querySelector('#ctx-property'), name: UI.ctx.querySelector('#ctx-copy-name'), retry: UI.ctx.querySelector('#ctx-task-retry'), link: UI.ctx.querySelector('#ctx-copy-link'), del: UI.ctx.querySelector('#ctx-del'), bl: UI.ctx.querySelector('#ctx-add-bl'), sep1: UI.ctx.querySelector('#sep-1'), sep2: UI.ctx.querySelector('#sep-2'), sep3: UI.ctx.querySelector('#sep-3') }; const isSingle = S.sel.size === 1; const ids = Array.from(S.sel); const firstItem = S.itemMap.get(ids[0]); const hasFailed = ids.some(id => S.itemMap.get(id)?.phase === 'PHASE_TYPE_ERROR'); let isVid = false, isImg = false; if (firstItem && firstItem.file_id) { const m = (firstItem.mime_type || '').toLowerCase(); const n = (firstItem.name || '').toLowerCase(); isVid = m.startsWith('video/') || ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp'].some(e => n.endsWith('.'+e)); isImg = m.startsWith('image/'); } let g1 = 0; if (els.open) { els.open.style.order = '10'; const canOpen = isSingle && firstItem?.file_id && firstItem.phase !== 'PHASE_TYPE_ERROR' && (isVid || isImg); els.open.style.display = canOpen ? 'flex' : 'none'; if(canOpen) g1++; } if (els.ext) { els.ext.style.order = '11'; const canExt = isSingle && firstItem?.file_id && firstItem.phase !== 'PHASE_TYPE_ERROR' && isVid; els.ext.style.display = canExt ? 'flex' : 'none'; if(canExt) g1++; } if (els.prop) { els.prop.style.order = '12'; els.prop.style.display = isSingle ? 'flex' : 'none'; if(isSingle) g1++; } if (els.loc) { els.loc.style.order = '13'; const canLocate = isSingle && firstItem?.file_id && firstItem.phase !== 'PHASE_TYPE_ERROR'; els.loc.style.display = canLocate ? 'flex' : 'none'; if(canLocate) g1++; } let g2 = 0; if (els.name) { els.name.style.order = '20'; els.name.style.display = 'flex'; g2++; } let g3 = 0; if (els.retry) { els.retry.style.order = '30'; els.retry.innerHTML = `${CONF.icons.retry} ${L.btn_retry_task}`; els.retry.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if (UI.btnRetryTask) UI.btnRetryTask.click(); }; els.retry.style.display = hasFailed ? 'flex' : 'none'; if(hasFailed) g3++; } if (els.link) { els.link.style.order = '31'; els.link.style.display = 'flex'; els.link.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; const tasks = ids.map(id => S.itemMap.get(id)).filter(t => t && (t.source_url || t.params?.url)); if (tasks.length) { GM_setClipboard(tasks.map(t => t.source_url || t.params.url).join('\n')); showToast(L.msg_copy_success); } }; g3++; } let g4 = 0; if (els.del) { els.del.style.order = '40'; els.del.style.display = 'flex'; els.del.innerHTML = `${CONF.icons.del} ${L.btn_del}`; els.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; UI.btnDel.click(); }; g4++; } if (els.bl) { els.bl.style.order = '41'; els.bl.style.display = 'flex'; let isRemove = false; for (const id of ids) { const t = S.itemMap.get(id); if (t && S.blSet.has((t.name||'').toLowerCase().trim())) { isRemove = true; break; } } els.bl.innerHTML = isRemove ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; els.bl.setAttribute('data-action', isRemove ? 'remove' : 'add'); g4++; } if (els.sep1) { els.sep1.style.order = '15'; els.sep1.style.display = (g1 > 0 && (g2 > 0 || g3 > 0 || g4 > 0)) ? 'block' : 'none'; } if (els.sep2) { els.sep2.style.order = '25'; els.sep2.style.display = (g2 > 0 && (g3 > 0 || g4 > 0)) ? 'block' : 'none'; } if (els.sep3) { els.sep3.style.order = '35'; els.sep3.style.display = (g3 > 0 && g4 > 0) ? 'block' : 'none'; } } else { if(sepShareExtra) sepShareExtra.style.display = 'none'; if (S.trashMode) { const tr = { cpName: UI.ctx.querySelector('#ctx-copy-name'), restore: UI.ctx.querySelector('#ctx-restore'), delF: UI.ctx.querySelector('#ctx-del-forever'), addBl: UI.ctx.querySelector('#ctx-add-bl'), sep1: UI.ctx.querySelector('#sep-trash-1'), sep2: UI.ctx.querySelector('#sep-trash-2') }; if(tr.cpName) tr.cpName.style.display = 'flex'; if(tr.sep1) tr.sep1.style.display = 'block'; if(tr.restore) tr.restore.style.display = 'flex'; if(tr.sep2) tr.sep2.style.display = 'block'; if(tr.delF) tr.delF.style.display = 'flex'; if(tr.addBl) { tr.addBl.style.display = 'flex'; let isRemoveMode = false; for (const id of S.sel) { const item = S.itemMap.get(id); if (item && (item.kind === 'drive#folder' ? S.blFolderSet.has(item.name.toLowerCase().trim()) : S.blSet.has(item.name.toLowerCase().trim()))) { isRemoveMode = true; break; } } tr.addBl.innerHTML = isRemoveMode ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; tr.addBl.setAttribute('data-action', isRemoveMode ? 'remove' : 'add'); } }else { if(itms.share) itms.share.style.display = 'flex'; if(seps[0]) seps[0].style.display = 'block'; let hasGroup2 = false; if (S.sel.size === 1) { [itms.open, itms.prop].forEach(el => { if(el) el.style.display = 'flex'; }); const isVideo = (d.mime_type || '').startsWith('video/') || (d.params && d.params.duration > 0); if (itms.extPlay) itms.extPlay.style.display = isVideo ? 'flex' : 'none'; hasGroup2 = true; } if (itms.star) { let hasUnstarred = false; let hasValidItem = false; for (const id of S.sel) { const it = S.itemMap.get(id); if (it && !isSystemItem(it)) { hasValidItem = true; if (!(it.starred || (it.tags && it.tags.some(t => t.name === 'STAR')))) { hasUnstarred = true; break; } } } itms.star.style.display = hasValidItem ? 'flex' : 'none'; if (hasValidItem) { hasGroup2 = true; itms.star.innerHTML = hasUnstarred ? `${ctxIcons.star} ${L.ctx_star}` : `${ctxIcons.unstar} ${L.ctx_unstar}`; itms.star.setAttribute('data-action', hasUnstarred ? 'star' : 'unstar'); } } const isSearchRoot = S.path.length > 0 && S.path[S.path.length - 1].id === 'virtual_search_root'; const canLocateUpload = S.uploadMode && d.status === 'DONE' && d.file_id; if((S.starredMode || S.recentMode || S.historyMode || isSearchRoot || S.dupMode || S.isFlattened || S.analyzeMode || canLocateUpload) && S.sel.size === 1) { itms.locate.style.display = 'flex'; hasGroup2 = true; } if(seps[1]) seps[1].style.display = hasGroup2 ? 'block' : 'none'; [itms.down, itms.cpName].forEach(el => { if(el) el.style.display = 'flex'; }); if (S.historyMode) { if(seps[2]) seps[2].style.display = 'none'; [itms.cut, itms.copy, itms.rename].forEach(el => { if(el) el.style.display = 'none'; }); } else { if(seps[2]) seps[2].style.display = 'block'; } if (S.historyMode) { if(seps[3]) seps[3].style.display = 'block'; } else { if (!isProtected) { if(itms.cut) itms.cut.style.display = 'flex'; if(itms.rename) { itms.rename.style.display = 'flex'; itms.rename.innerHTML = (S.sel.size > 1) ? `${ctxIcons.renameBulk} ${L.btn_bulkrename}` : `${ctxIcons.rename} ${L.ctx_rename}`; } } if(itms.copy) itms.copy.style.display = 'flex'; let hasFolder = false; for (const id of S.sel) { if (S.itemMap.get(id)?.kind === 'drive#folder') { hasFolder = true; break; } } if(itms.prune && hasFolder) itms.prune.style.display = 'flex'; if(seps[3]) seps[3].style.display = 'block'; } if (itms.addBl) { itms.addBl.style.display = 'flex'; S.updateBlCache(); let isRemoveMode = false; for (const id of S.sel) { const item = S.itemMap.get(id); if (!item) continue; const cleanName = (item.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (item.kind === 'drive#folder') { if (S.blFolderSet.has(cleanName)) { isRemoveMode = true; break; } } else { if (S.blSet.has(cleanName)) { isRemoveMode = true; break; } } } itms.addBl.innerHTML = isRemoveMode ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; itms.addBl.setAttribute('data-action', isRemoveMode ? 'remove' : 'add'); } if (itms.del) itms.del.style.display = 'flex'; } } const useFlexOrder = S.offlineMode || S.uploadMode; UI.ctx.style.display = useFlexOrder ? 'flex' : 'block'; if (useFlexOrder) UI.ctx.style.flexDirection = 'column'; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let x = e.clientX / scale, y = e.clientY / scale, w = 160, h = UI.ctx.offsetHeight || 280; let winW = window.innerWidth / scale, winH = window.innerHeight / scale; if (x + w > winW) x = winW - w - 10; if (y + h > winH) y = winH - h - 10; UI.ctx.style.left = x + 'px'; UI.ctx.style.top = y + 'px'; }; } fragment.appendChild(row); } UI.in.innerHTML = ''; UI.in.appendChild(fragment); if (window.pkRefreshTooltip) { requestAnimationFrame(() => { window.pkRefreshTooltip(); }); } } let isMarquee = false, mqStartX = 0, mqStartY = 0, startScroll = 0, lastMouseX = 0, lastMouseY = 0, scrollSpeed = 0, scrollRaf = null; let lastRngS = -1, lastRngE = -1, cachedVpRect = null; const mqBox = document.createElement('div'); mqBox.className = 'pk-selection-box'; el.appendChild(mqBox); const updateMarqueeUIAndSelection = (targetX, targetY) => { if (!cachedVpRect) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const cssTargetX = targetX / scale; const cssTargetY = targetY / scale; const cssRectTop = cachedVpRect.top / scale; const cssRectBottom = cachedVpRect.bottom / scale; const cssRectLeft = cachedVpRect.left / scale; const cssMqStartX = mqStartX / scale; const cssMqStartY = mqStartY / scale; const curScroll = UI.vp.scrollTop; const clampedY = Math.max(cssRectTop, Math.min(cssRectBottom, cssTargetY)); const logicA = cssMqStartY - cssRectTop + startScroll; const logicB = clampedY - cssRectTop + curScroll; const logTop = Math.max(0, Math.min(logicA, logicB)); const logBot = Math.max(logicA, logicB); const visTop = logTop - curScroll + cssRectTop; const visBot = logBot - curScroll + cssRectTop; const clipT = Math.max(cssRectTop, visTop), clipB = Math.min(cssRectBottom, visBot); const drawH = Math.max(0, clipB - clipT); const safeRight = cssRectLeft + UI.vp.clientWidth; const clampedX = Math.max(cssRectLeft, Math.min(safeRight, cssTargetX)); const drawL = Math.min(cssMqStartX, clampedX); const drawW = Math.abs(clampedX - cssMqStartX); mqBox.style.borderTopWidth = (visTop < cssRectTop) ? '0' : '1px'; mqBox.style.borderBottomWidth = (visBot > cssRectBottom) ? '0' : '1px'; mqBox.style.borderLeftWidth = (cssTargetX < cssRectLeft) ? '0' : '1px'; mqBox.style.borderRightWidth = (cssTargetX > safeRight) ? '0' : '1px'; Object.assign(mqBox.style, { display: drawH > 0 ? 'block' : 'none', width: drawW + 'px', height: drawH + 'px', transform: `translate3d(${drawL}px, ${clipT}px, 0)` }); const sIdx = Math.floor(logTop / CONF.rowHeight), eIdx = Math.min(S.display.length - 1, Math.floor(logBot / CONF.rowHeight)); if (sIdx !== lastRngS || eIdx !== lastRngE) { lastRngS = sIdx; lastRngE = eIdx; if (!window.event?.ctrlKey && !window.event?.metaKey) S.sel.clear(); for (let k = sIdx; k <= eIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) S.sel.add(item.id); } renderVisible(); updateStat(); } }; const runAutoScroll = () => { if (!isMarquee || scrollSpeed === 0) { scrollRaf = null; return; } UI.vp.scrollTop += scrollSpeed; updateMarqueeUIAndSelection(lastMouseX, lastMouseY); scrollRaf = requestAnimationFrame(runAutoScroll); }; const handleMarqueeMove = (e) => { if (!cachedVpRect) return; if (!isMarquee) { const moveX = Math.abs(e.clientX - mqStartX); const moveY = Math.abs(e.clientY - mqStartY); if (moveX > 5 || moveY > 5) { isMarquee = true; UI.win.classList.add('pk-is-seeking'); } else { return; } } lastMouseX = e.clientX; lastMouseY = e.clientY; if (e.clientY > cachedVpRect.bottom - 5) scrollSpeed = Math.min(45, 2 + Math.pow((e.clientY - cachedVpRect.bottom + 5) / 5, 1.3)); else if (e.clientY < cachedVpRect.top + 5) scrollSpeed = -Math.min(45, 2 + Math.pow((cachedVpRect.top + 5 - e.clientY) / 5, 1.3)); else scrollSpeed = 0; if (scrollSpeed !== 0 && !scrollRaf) scrollRaf = requestAnimationFrame(runAutoScroll); updateMarqueeUIAndSelection(e.clientX, e.clientY); }; const stopMarquee = () => { isMarquee = false; scrollSpeed = 0; if (UI.win) UI.win.classList.remove('pk-is-seeking'); lastRngS = -1; lastRngE = -1; cachedVpRect = null; if (mqBox) { mqBox.style.display = 'none'; mqBox.style.transform = 'none'; mqBox.style.width = '0px'; mqBox.style.height = '0px'; } if (scrollRaf) cancelAnimationFrame(scrollRaf); scrollRaf = null; window.removeEventListener('mousemove', handleMarqueeMove); window.removeEventListener('mouseup', stopMarquee); }; let isFileDragging = false; let fileDragGhost = null; let dropTargetId = null; let dropTargetType = null; let autoOpenTimer = null; let lastHoverSep = null; const handleFileDragMove = (e) => { if (!isFileDragging) { const moveX = Math.abs(e.clientX - mqStartX); const moveY = Math.abs(e.clientY - mqStartY); if (moveX > 5 || moveY > 5) { isFileDragging = true; document.body.classList.add('pk-dragging'); fileDragGhost = document.createElement('div'); fileDragGhost.className = 'pk-drag-ghost'; if (el.classList.contains('pk-dark')) fileDragGhost.classList.add('pk-dark'); const count = S.sel.size; const firstId = Array.from(S.sel)[0]; const firstItem = S.itemMap.get(firstId); let text = firstItem ? firstItem.name : 'Selected Items'; if (count > 1) { text += L.str_drag_files.replace('{n}', count); } let dragIcon = CONF.typeIcons.folder.replace('30','20').replace('30','20'); if (firstItem && firstItem.icon_link) { dragIcon = ``; } fileDragGhost.innerHTML = `${dragIcon}${esc(text)}`; document.body.appendChild(fileDragGhost); } else { return; } } if (fileDragGhost) { const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; fileDragGhost.style.zoom = 'var(--pk-zoom, 1)'; fileDragGhost.style.left = (e.clientX / scale) + 'px'; fileDragGhost.style.top = (e.clientY / scale) + 'px'; } const prevTargets = document.querySelectorAll('.pk-drop-target'); prevTargets.forEach(el => el.classList.remove('pk-drop-target')); dropTargetId = null; dropTargetType = null; if (fileDragGhost) fileDragGhost.style.display = 'none'; const elUnder = document.elementFromPoint(e.clientX, e.clientY); if (fileDragGhost) fileDragGhost.style.display = 'flex'; if (!elUnder) return; const menuItem = elUnder.closest('.pk-crumb-item'); if (menuItem && menuItem.dataset.id) { const tid = menuItem.dataset.id; const currentFolderId = S.path[S.path.length - 1]?.id || ''; if (tid !== currentFolderId && tid !== 'loading' && tid !== 'error') { dropTargetId = tid; dropTargetType = 'menu_item'; menuItem.classList.add('pk-drop-target'); return; } } const sep = elUnder.closest('.pk-crumb-sep'); if (sep) { if (lastHoverSep !== sep) { lastHoverSep = sep; if (autoOpenTimer) clearTimeout(autoOpenTimer); autoOpenTimer = setTimeout(() => { if (isFileDragging) sep.click(); }, 500); } } else { lastHoverSep = null; if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } } const row = elUnder.closest('.pk-row'); if (row && row.dataset.id) { const tid = row.dataset.id; const item = S.itemMap.get(tid); if (item && item.kind === 'drive#folder' && !S.sel.has(tid)) { dropTargetId = tid; dropTargetType = 'row'; row.classList.add('pk-drop-target'); return; } } const crumbItem = elUnder.closest('#pk-crumb span'); if (crumbItem && !crumbItem.classList.contains('pk-crumb-sep') && crumbItem.dataset.id) { const tid = crumbItem.dataset.id === 'root' ? '' : crumbItem.dataset.id; const currentFolderId = S.path[S.path.length - 1]?.id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; if (normalize(tid) !== normalize(currentFolderId)) { dropTargetId = tid; dropTargetType = 'crumb'; crumbItem.classList.add('pk-drop-target'); } } }; const executeFileTransfer = async (items, opType, sourcePid, targetPid, targetNameForLog) => { if (!items || items.length === 0) return; if (sourcePid !== '__VIRTUAL__' && sourcePid === targetPid) { showToast(L.err_paste_descendant, 'error'); return; } const foldersToMoveIds = items.filter(it => it.kind === 'drive#folder').map(it => it.id); if (foldersToMoveIds.length > 0) { const isDescendant = (targetId, ancestorIds) => { let currentId = targetId; let safety = 100; while (currentId && currentId !== 'root' && safety > 0) { if (ancestorIds.includes(currentId)) return true; const parent = globalParentIndex.get(currentId); currentId = parent ? parent.id : null; safety--; } return false; }; if (isDescendant(targetPid, foldersToMoveIds)) { showToast(L.err_paste_descendant, 'error'); return; } } S.movingSourceId = sourcePid || 'root'; S.movingDestId = targetPid || 'root'; const allIds = items.map(it => it.id); allIds.forEach(id => S.movingIds.add(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_ADD', ids: allIds, src: S.movingSourceId, dst: S.movingDestId }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); isGUISensitive = true; const progressTask = FloatBarManager.create(`${opType === 'move' ? L.str_moving : L.str_copying}...`); const updateFloat = progressTask.update; const BATCH_SIZE = 500; let processedCount = 0; const total = items.length; const endpoint = opType === 'move' ? 'files:batchMove' : 'files:batchCopy'; const actionText = opType === 'move' ? L.str_moving : L.str_copying; refresh(); try { for (let i = 0; i < total; i += BATCH_SIZE) { const chunk = items.slice(i, i + BATCH_SIZE); const chunkIds = chunk.map(it => it.id); updateFloat(`${actionText} ${processedCount}/${total} -> ${esc(targetNameForLog || 'Target')}`); const apiTargetPid = (targetPid === 'root') ? '' : targetPid; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/${endpoint}`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunkIds, to: { parent_id: apiTargetPid } }) }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error_description || `API ${res.status}`); if (data.task_id) { await new Promise(resolve => { const checkTask = async () => { try { const tRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${data.task_id}`, { headers: getHeaders() }); const tData = await tRes.json(); if (tData.phase === 'PHASE_TYPE_COMPLETE' || tData.phase === 'PHASE_TYPE_ERROR') resolve(); else setTimeout(checkTask, 800); } catch { resolve(); } }; checkTask(); }); } else if (opType === 'move' && chunkIds.length > 0) { const lastId = chunkIds[chunkIds.length - 1]; let retry = 0; while (retry < 20) { try { const meta = await apiGet(lastId); if (meta.parent_id === targetPid || meta.trashed) break; } catch(e) { break; } await sleep(500); retry++; } } chunkIds.forEach(id => S.movingIds.delete(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: chunkIds }); if (opType === 'move') { const movedSet = new Set(chunkIds); S.items = S.items.filter(it => !movedSet.has(it.id)); if (typeof globalCache !== 'undefined') { const cleanListChunk = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !movedSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !movedSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { globalCache.set(key, cleanListChunk(globalCache.get(key))); } for (const key of S.cache.keys()) { S.cache.set(key, cleanListChunk(S.cache.get(key))); } } if (typeof pkState !== 'undefined' && pkState && pkState.lastGlobalResults) { pkState.lastGlobalResults = pkState.lastGlobalResults.filter(x => !movedSet.has(x.id)); } } else { if (targetPid === (S.path[S.path.length-1].id||'')) { const newItems = (data.files || []).map(f => minifyFile(f, false)); S.items.unshift(...newItems); } } if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); refresh(); processedCount += chunk.length; } if (targetPid) gmSet('pk_fmod_' + targetPid, new Date(getServerNow()).toISOString()); if (S.analyzeMap) { const deltaSize = items.reduce((acc, it) => acc + parseInt(it.size || 0), 0); if (deltaSize > 0) { const updateAnalyzeChain = (startId, isAdd) => { let currId = startId; let safety = 50; while (currId && S.analyzeMap.has(currId) && safety > 0) { const node = S.analyzeMap.get(currId); const oldSize = parseInt(node.size || 0); node.size = isAdd ? (oldSize + deltaSize) : Math.max(0, oldSize - deltaSize); currId = node.parentId; safety--; } }; if (S.analyzeMap.has(targetPid)) updateAnalyzeChain(targetPid, true); if (opType === 'move' && S.analyzeMap.has(sourcePid)) updateAnalyzeChain(sourcePid, false); if (S.analyzeResultItems) { S.analyzeResultItems.forEach(resItem => { if (S.analyzeMap.has(resItem.id)) { resItem.size = S.analyzeMap.get(resItem.id).size.toString(); } }); } } } const keysToClear = [sourcePid || 'root', targetPid || 'root']; keysToClear.forEach(k => { S.cache.delete(k); if (typeof globalCache !== 'undefined') globalCache.delete(k); if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(k); if (k === 'root') globalDirtyFolders.add(''); } }); if (opType === 'move') { const purgeMovedDescendants = (fid) => { if (typeof globalLineageMap !== 'undefined') globalLineageMap.delete(fid); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(fid); const data = (typeof globalCache !== 'undefined' ? globalCache.get(fid) : null) || (S.cache ? S.cache.get(fid) : null); if (data) { const list = Array.isArray(data) ? data : (data.items || []); list.forEach(child => { if (child.kind === 'drive#folder') { purgeMovedDescendants(child.id); } }); if (typeof globalCache !== 'undefined') globalCache.delete(fid); if (S.cache) S.cache.delete(fid); } }; items.forEach(it => { if (it.kind === 'drive#folder') purgeMovedDescendants(it.id); }); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); showToast(opType === 'move' ? L.msg_move_done : L.msg_copy_success); } catch (e) { showToast(`${L.str_error}: ${e.message}`, 'error'); load(false, true); } finally { S.movingIds.clear(); S.movingSourceId = null; S.movingDestId = null; if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_CLR', ids: [] }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); isGUISensitive = false; if (progressTask) progressTask.destroy(); updateStat(); } }; const stopFileDrag = async () => { if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } lastHoverSep = null; window.removeEventListener('mousemove', handleFileDragMove); window.removeEventListener('mouseup', stopFileDrag); document.body.classList.remove('pk-dragging'); if (fileDragGhost) fileDragGhost.remove(); fileDragGhost = null; let targetName = "Folder"; if (dropTargetType === 'menu_item') { const activeItem = document.querySelector('.pk-crumb-item.pk-drop-target span'); if (activeItem) targetName = activeItem.textContent; } else if (dropTargetType === 'row') { targetName = S.itemMap.get(dropTargetId)?.name || "Folder"; } else if (dropTargetType === 'crumb') { const pathNode = S.path.find(p => (p.id || 'root') === (dropTargetId || 'root')); targetName = pathNode ? pathNode.name : "Parent"; } const targets = document.querySelectorAll('.pk-drop-target'); targets.forEach(t => t.classList.remove('pk-drop-target')); document.querySelectorAll('.pk-crumb-pop').forEach(pop => { if (typeof pop._cleanup === 'function') pop._cleanup(); else pop.remove(); }); UI.crumb.querySelectorAll('.pk-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); sep.innerHTML = CONF.crumbIcons.right; }); if (!isFileDragging || dropTargetId === null) { isFileDragging = false; return; } isFileDragging = false; const currentFolderId = S.path[S.path.length - 1]?.id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; if (normalize(dropTargetId) === normalize(currentFolderId)) return; let hasSystem = false; for (const id of S.sel) { const item = S.itemMap.get(id); if (item && isSystemItem(item)) { hasSystem = true; break; } } if (hasSystem) { showToast(`${L.msg_sys_error}`, 'error'); return; } const rawItems = []; const validItems = []; let skipLocked = 0; let skipSelf = 0; let skipConflict = 0; const isDescendantConflict = (candidateId) => { if (!S.movingIds || S.movingIds.size === 0) return false; const hotspots = [S.movingSourceId, S.movingDestId].filter(x => x && x !== 'root'); if (hotspots.length === 0) return false; for (const startPoint of hotspots) { let curr = startPoint; let safety = 50; while (curr && curr !== 'root' && safety > 0) { if (curr === candidateId) return true; const parentInfo = globalParentIndex.get(curr); curr = parentInfo ? parentInfo.id : null; safety--; } } return false; }; S.sel.forEach(id => { const item = S.itemMap.get(id); if (!item) return; rawItems.push(item); if (S.movingIds.has(id)) { skipLocked++; return; } const targetId = normalize(dropTargetId); const selfId = normalize(item.id); const parentId = normalize(item.parent_id); if (targetId === selfId || targetId === parentId) { skipSelf++; return; } if (item.kind === 'drive#folder' && isDescendantConflict(item.id)) { skipConflict++; return; } validItems.push(item); }); if (skipLocked > 0 || skipSelf > 0 || skipConflict > 0) { const reasons = []; if (skipLocked) reasons.push(L.msg_skip_locked.replace('{n}', skipLocked)); if (skipSelf) reasons.push(L.msg_skip_self.replace('{n}', skipSelf)); if (skipConflict) reasons.push(L.msg_skip_conflict.replace('{n}', skipConflict)); showToast(`${L.msg_skip_invalid} ${reasons.join(', ')}`, 'warning'); } S.clearSelection(); refresh(); if (validItems.length > 0) { await executeFileTransfer( validItems, 'move', normalize(currentFolderId), normalize(dropTargetId), targetName ); } }; UI.vp.addEventListener('mousedown', (e) => { if (e.button !== 0 || S.loading || e.detail > 1) return; if (e.target.closest('.pk-btn, .pk-star-icon, input')) return; const row = e.target.closest('.pk-row'); const isClickingSelected = row && row.dataset.id && S.sel.has(row.dataset.id); if (isClickingSelected && !S.trashMode && !S.shareMode && !S.offlineMode && !S.historyMode && !S.starredMode && !S.recentMode && !S.isFlattened && !S.dupMode && !S.analyzeMode) { isFileDragging = false; mqStartX = e.clientX; mqStartY = e.clientY; window.addEventListener('mousemove', handleFileDragMove); window.addEventListener('mouseup', stopFileDrag); return; } cachedVpRect = UI.vp.getBoundingClientRect(); const safeRight = cachedVpRect.left + UI.vp.clientWidth; const safeBottom = cachedVpRect.top + UI.vp.clientHeight; if (e.clientX > safeRight || e.clientY > safeBottom) return; mqStartX = Math.max(cachedVpRect.left, Math.min(safeRight, e.clientX)); mqStartY = e.clientY; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const startOffsetY = (e.clientY - cachedVpRect.top) / scale + UI.vp.scrollTop; const startIdx = Math.floor(startOffsetY / CONF.rowHeight); if (startIdx >= 0 && startIdx < S.display.length) { const startItem = S.display[startIdx]; S.activeId = (startItem && !startItem.isHeader) ? startItem.id : null; } else { S.activeId = null; } isMarquee = false; mqStartX = lastMouseX = e.clientX; mqStartY = lastMouseY = e.clientY; startScroll = UI.vp.scrollTop; window.addEventListener('mousemove', handleMarqueeMove); window.addEventListener('mouseup', stopMarquee); }); let isScrollScheduled = false; UI.vp.onscroll = () => { if (UI.ctx && UI.ctx.style.display !== 'none') UI.ctx.style.display = 'none'; if (!isScrollScheduled) { requestAnimationFrame(() => { renderVisible(); if (typeof isMarquee !== 'undefined' && isMarquee) { updateMarqueeUIAndSelection(lastMouseX, lastMouseY); } isScrollScheduled = false; }); isScrollScheduled = true; } }; UI.vp.addEventListener('wheel', () => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } }, { capture: true, passive: true }); const handleBlankClick = async (e) => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } if (e.target.closest('.pk-nav-btn') || e.target.closest('.pk-btn') || e.target.closest('.pk-btn-toggle') || e.target.closest('input') || e.target.closest('textarea') || e.target.closest('select') || e.target.closest('.pk-nav span') || e.target.closest('.pk-crumb-sep') || e.target.closest('.pk-search') || e.target.closest('label') || e.target.closest('.pk-dropdown-wrap') || e.target.closest('.pk-sort-opt') || e.target.closest('a') ) return; if (S.sel.size > 0 || S.activeId) { const isAnalyzeRoot = S.analyzeMode && S.path[S.path.length - 1].id === 'analyze_root'; if ((S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups)) && S.sel.size > 5) { if (!await confirmSelectionClear()) return; } S.clearSelection(); renderVisible(); requestAnimationFrame(() => { if (UI.chkAll) UI.chkAll.checked = false; }); } }; const sidebarArea = el.querySelector('.pk-sidebar'); if (sidebarArea) sidebarArea.onclick = handleBlankClick; const footerArea = el.querySelector('.pk-ft'); if (footerArea) footerArea.onclick = handleBlankClick; const headerArea = el.querySelector('.pk-hd'); if (headerArea) headerArea.onclick = handleBlankClick; const topBarArea = el.querySelector('#pk-top-bar'); if (topBarArea) topBarArea.onclick = handleBlankClick; const actionArea = el.querySelector('#pk-actionbar'); if (actionArea) actionArea.onclick = handleBlankClick; const trashArea = el.querySelector('#pk-trash-bar'); if (trashArea) trashArea.onclick = handleBlankClick; if (UI.vp) { UI.vp.addEventListener('click', (e) => { if (e.target.closest('.pk-row')) return; const rect = UI.vp.getBoundingClientRect(); if (e.clientX > rect.left + UI.vp.clientWidth || e.clientY > rect.top + UI.vp.clientHeight) return; if (Math.abs(e.clientX - mqStartX) > 5 || Math.abs(e.clientY - mqStartY) > 5) return; handleBlankClick(e); }); } const showCrumbDropdown = async (e, parentId, triggerEl) => { if (triggerEl.classList.contains('pk-active')) { const oldPop = document.getElementById('pk-main-crumb-pop'); if (oldPop) oldPop.remove(); triggerEl.classList.remove('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.right; return; } let targetId = parentId || 'root'; if (targetId === 'root' || targetId === '') { if (S.shareMode) targetId = 'share_root'; else if (S.starredMode) targetId = 'starred_root'; else targetId = 'root'; } let folders = []; if (targetId === 'virtual_search_root' || targetId === 'analyze_root' || targetId === 'recent_root') { let sourceItems = []; if (targetId === 'analyze_root') { sourceItems = (S.analyzeResultItems && S.analyzeResultItems.length > 0) ? S.analyzeResultItems : S.items; } else if (targetId === 'recent_root') { if (S.recentResultItems && S.recentResultItems.length > 0) { sourceItems = S.recentResultItems; } else if (typeof globalCache !== 'undefined' && globalCache.has('recent_root')) { const cached = globalCache.get('recent_root'); sourceItems = Array.isArray(cached) ? cached : (cached.items || []); } else { sourceItems = S.items || []; } } else { sourceItems = (S.lastGlobalResults && S.lastGlobalResults.length > 0) ? S.lastGlobalResults : S.items; } folders = sourceItems.filter(f => f.kind === 'drive#folder'); if (folders.length === 0) { const pop = document.createElement('div'); pop.className = 'pk-crumb-pop'; pop.innerHTML = `
${L.str_no_files}
`; el.appendChild(pop); const rect = triggerEl.getBoundingClientRect(); pop.style.left = (rect.left - 4) + 'px'; pop.style.top = (rect.bottom + 6) + 'px'; pop.classList.add('pk-show'); setTimeout(() => { const closer = (evt) => { pop.remove(); document.removeEventListener('mousedown', closer); }; document.addEventListener('mousedown', closer); }, 10); return; } } else { const rawCached = (typeof globalCache !== 'undefined' && globalCache.has(targetId)) ? globalCache.get(targetId) : S.cache.get(targetId); const cachedItems = (rawCached && !Array.isArray(rawCached) && rawCached.items) ? rawCached.items : (Array.isArray(rawCached) ? rawCached : null); if (cachedItems && cachedItems.length > 0) { folders = cachedItems.filter(f => f.kind === 'drive#folder'); } else { folders = [{ id: 'loading', name: L.loading, kind: 'drive#folder' }]; apiList(targetId).then(res => { if (typeof globalCache !== 'undefined') globalCache.set(targetId, res); const pop = el.querySelector('.pk-crumb-pop'); if (pop && pop.dataset.targetId === targetId) renderMenu(res.filter(f => f.kind === 'drive#folder')); }).catch(() => { renderMenu([{ id: 'error', name: L.str_load_failed, kind: 'drive#folder' }]); }); } } const renderMenu = (list) => { let s = S.sort, d = S.dir; const isNormalFolder = targetId && !targetId.includes('_root'); if (isNormalFolder) { if (gmGet('pk_sort_independent', false)) { const prefs = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); if (prefs[targetId]) { s = prefs[targetId].sort; d = prefs[targetId].dir; } else { s = 'modified_time'; d = 1; } } else { const g = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); s = g.sort; d = g.dir; } } const sortKey = (s === 'modified_time') ? 'modified_time' : 'name'; const collator = new Intl.Collator(({ 'zh': 'zh-CN', 'tc': 'zh-TW', 'ja': 'ja', 'ko': 'ko' })[getLang()] || 'en', { numeric: true, sensitivity: 'base' }); list.sort((a, b) => { const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortKey === 'modified_time') { const tA = a.modified_time ? new Date(a.modified_time).getTime() : 0; const tB = b.modified_time ? new Date(b.modified_time).getTime() : 0; return (tB - tA) * d; } return collator.compare(a.name, b.name) * d; }); let pop = document.getElementById('pk-main-crumb-pop'); if (!pop) { pop = document.createElement('div'); pop.id = 'pk-main-crumb-pop'; pop.className = 'pk-crumb-pop pk-scroll'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.style.top = '-9999px'; document.body.appendChild(pop); } pop.dataset.targetId = targetId; pop.innerHTML = ''; list.forEach(f => { const item = document.createElement('div'); item.className = 'pk-crumb-item'; item.dataset.id = f.id; const iconSrc = f.icon_link || ''; const fallbackSvg = CONF.typeIcons.folder.replace(/width="\d+"/, 'width="18"').replace(/height="\d+"/, 'height="18"'); const iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; item.innerHTML = `${iconHtml}${esc(f.name)}`; if (f.id !== 'loading' && f.id !== 'error') { item.onclick = (ev) => { ev.stopPropagation(); closeMenu(); if (targetId === 'virtual_search_root' || targetId === 'analyze_root' || targetId === 'recent_root') { let rootName = L.str_search_results; if (targetId === 'analyze_root') rootName = L.str_analyze_results; else if (targetId === 'recent_root') rootName = L.btn_nav_recent; const newPath = []; if (targetId === 'virtual_search_root') { newPath.push({ id: '', name: L.btn_nav_home }); } newPath.push({ id: targetId, name: rootName }); newPath.push({ id: f.id, name: f.name }); S.path = newPath; } else { const idx = S.path.findIndex(p => p.id === parentId); if (idx !== -1) { S.path = S.path.slice(0, idx + 1); S.path.push({ id: f.id, name: f.name }); } } load(); }; } pop.appendChild(item); }); requestAnimationFrame(() => { const rect = triggerEl.getBoundingClientRect(); const popRect = pop.getBoundingClientRect(); let left = rect.left - 4; const popW = popRect.width; const winW = window.innerWidth; if (left + popW > winW) left = winW - popW - 15; pop.style.left = Math.max(10, left) + 'px'; pop.style.top = (rect.bottom + 6) + 'px'; pop.classList.add('pk-show'); triggerEl.classList.add('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.down; }); const closeMenu = () => { if (pop.parentNode) pop.remove(); if (triggerEl) { triggerEl.classList.remove('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.right; const svg = triggerEl.querySelector('svg'); if (svg) { svg.style.width = '14px'; svg.style.height = '14px'; svg.style.display = 'block'; svg.style.opacity = '0.6'; } } document.removeEventListener('mousedown', onOutsideClick); window.removeEventListener('resize', closeMenu); UI.vp.removeEventListener('scroll', closeMenu); }; const onOutsideClick = (evt) => { if (!pop.contains(evt.target) && !triggerEl.contains(evt.target)) closeMenu(); }; window.addEventListener('resize', closeMenu); UI.vp.addEventListener('scroll', closeMenu, { passive: true }); setTimeout(() => document.addEventListener('mousedown', onOutsideClick), 10); }; if (globalDirtyFolders.has(targetId === 'root' ? '' : targetId)) { folders = null; globalDirtyFolders.delete(targetId === 'root' ? '' : targetId); } renderMenu(folders || []); }; function renderCrumb() { UI.crumb.innerHTML = ''; if (S.offlineMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.title_offline}
`; return; } if (S.uploadMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.btn_nav_upload}
`; return; } if (S.shareMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.btn_nav_share}
`; return; } if (S.historyMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.btn_nav_history} ${L.history_notice}
`; return; } if (S.trashMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.trash_title} ${L.trash_notice}
`; return; } UI.crumb.style.pointerEvents = 'auto'; const svgStyle = 'width:14px;height:14px;vertical-align:-4px;margin-right:4px;display:inline-block;'; const ICON_HOME = ``; const ICON_SEARCH = ``; const ICON_TRASH = ``; const ICON_STAR = ``; S.path.forEach((p, i) => { const s = document.createElement('span'); const isHome = (p.id === '' || p.id === 'root'); const isSearch = (p.id === 'virtual_search_root'); const isAnalyze = (p.id === 'analyze_root'); const isRecentRoot = (p.id === 'recent_root'); if (isHome) { let icon = ICON_HOME; if (S.trashMode) icon = ICON_TRASH; else if (S.starredMode) icon = ICON_STAR; s.innerHTML = `${icon}${p.name}`; } else if (isSearch || isAnalyze) { s.innerHTML = `${ICON_SEARCH}${p.name}`; } else if (isRecentRoot) { const ICON_RECENT = CONF.icons.recent.replace(' { e.stopPropagation(); if (i !== S.path.length - 1) { if (i < S.path.length - 1) { S.latestChildId = S.path[i + 1].id; } S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); S.path = S.path.slice(0, i + 1); load(); } }; UI.crumb.appendChild(s); if (i < S.path.length - 1) { const sep = document.createElement('span'); sep.className = 'pk-crumb-sep'; sep.innerHTML = CONF.crumbIcons.right; sep.onclick = (e) => { e.stopPropagation(); showCrumbDropdown(e, p.id, sep); }; UI.crumb.appendChild(sep); } }); S._crumbIdx = S.path.length - 1; UI.crumb.onwheel = (e) => { e.preventDefault(); const existingPop = document.getElementById('pk-main-crumb-pop'); if (existingPop) { existingPop.remove(); UI.crumb.querySelectorAll('.pk-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); sep.innerHTML = CONF.crumbIcons.right; }); } const nodes = [...UI.crumb.querySelectorAll('span:not(.pk-crumb-sep)')]; if (!nodes.length) return; const now = Date.now(); if (now - (S._lastCrumbScroll || 0) < 120) return; S._lastCrumbScroll = now; if (e.deltaY < 0) S._crumbIdx = Math.max(0, S._crumbIdx - 1); else S._crumbIdx = Math.min(nodes.length - 1, S._crumbIdx + 1); const targetNode = nodes[S._crumbIdx]; const containerWidth = UI.crumb.offsetWidth; const centerOffset = targetNode.offsetLeft + (targetNode.offsetWidth / 2) - (containerWidth / 2); UI.crumb.scrollTo({ left: centerOffset, behavior: 'smooth' }); }; setTimeout(() => { if (UI.crumb) { UI.crumb.scrollTo({ left: UI.crumb.scrollWidth, behavior: 'smooth' }); } }, 0); } el.tabIndex = 0; el.focus(); const keyHandler = (e) => { const win = document.querySelector('.pk-ov'); if (!win || win.style.display === 'none') return; const tag = e.target.tagName; if (tag === 'TEXTAREA' || (tag === 'INPUT' && e.target.type !== 'checkbox' && e.target.type !== 'button' && e.target.type !== 'submit')) return; if (e.key === 'Escape') { const player = document.getElementById('pk-player-ov'); if (player) { player.remove(); return; } const openModal = document.querySelector('.pk-modal-ov'); if (openModal) { openModal.remove(); return; } if (UI.ctx.style.display === 'block') { UI.ctx.style.display = 'none'; return; } const isAtRoot = S.path.length === 1 || (S.path.length === 2 && S.path[1].id === 'virtual_search_root'); if (S.sel.size > 0) { S.sel.clear(); refresh(); } else if (UI.win.classList.contains('pk-maximized')) { if (!isTurbo && btnMax) btnMax.click(); } else if (isAtRoot) { if (!isTurbo) UI.btnClose.click(); } return; } if (e.key === 'Delete' && e.altKey) { e.preventDefault(); if (S.uploadMode) return; const existingModal = Array.from(document.querySelectorAll('.pk-modal-ov')).find(m => { const title = m.querySelector('h3'); return title && title.textContent.includes(L.title_blacklist); }); if (existingModal) { existingModal.remove(); } else { showBlacklistModal(); } } if (e.altKey && (e.key === 'h' || e.key === 'H')) { e.preventDefault(); const existingHelp = Array.from(document.querySelectorAll('.pk-modal-ov')).find(m => { const title = m.querySelector('h3'); return title && title.textContent === L.modal_help_title; }); if (existingHelp) { existingHelp.remove(); } else if (UI.btnHelp) { UI.btnHelp.click(); } return; } const hasActiveOverlay = document.querySelector('.pk-modal-ov, .pk-img-ov, #pk-player-ov'); if (hasActiveOverlay) return; if (e.key === 'F2') { e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; if (S.sel.size === 1) { if (UI.btnRename.disabled) return; UI.btnRename.click(); } else if (S.sel.size > 1) { if (UI.btnBulkRename.disabled) return; UI.btnBulkRename.click(); } } if (e.key === 'F5') { e.preventDefault(); if (S.uploadMode) load(false, true); else if (S.trashMode) load(false, true); else UI.btnRefresh.click(); } if (e.key === 'F8') { e.preventDefault(); const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isHistoryRoot = S.historyMode && S.path.length === 1; const isBlocked = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || isStarredRoot || isRecentRoot || isHistoryRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; if (isBlocked) return; UI.btnNewFolder.click(); } if (e.key === 'r' || e.key === 'R') { if (S.trashMode && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); if (UI.btnRestore && !UI.btnRestore.disabled) UI.btnRestore.click(); return; } if (S.offlineMode && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); if (UI.btnRetryTask && !UI.btnRetryTask.disabled) UI.btnRetryTask.click(); return; } } if (e.key === 'm' || e.key === 'M') { const imgPlayer = document.querySelector('.pk-img-ov'); const vidPlayer = document.getElementById('pk-player-ov'); if (!imgPlayer && !vidPlayer) { const btnMax = document.querySelector('#pk-maximize'); if (!isTurbo && btnMax) btnMax.click(); } } if (e.key === 'Delete' && e.shiftKey) { if (S.trashMode && UI.btnEmptyTrash) { e.preventDefault(); UI.btnEmptyTrash.click(); return; } if (S.uploadMode && UI.btnUpClearAll) { e.preventDefault(); UI.btnUpClearAll.click(); return; } } if (e.key === 'Delete' && !e.ctrlKey && !e.altKey && !e.shiftKey) { if (S.shareMode) { if (UI.btnCancelShare && !UI.btnCancelShare.disabled) UI.btnCancelShare.click(); return; } if (S.uploadMode) { if (UI.btnUpDel && !UI.btnUpDel.disabled) UI.btnUpDel.click(); return; } if (S.trashMode) { if(UI.btnDelForever && !UI.btnDelForever.disabled) UI.btnDelForever.click(); } else { if (!UI.btnDel.disabled) UI.btnDel.click(); } } if (e.key === 'Delete' && e.ctrlKey) { e.preventDefault(); if (S.trashMode) return; if (S.isFlattened || S.dupMode || S.uploadMode) return; if (!UI.btnPrune.disabled) UI.btnPrune.click(); } if (e.ctrlKey || e.metaKey) { if (e.key === 'a' || e.key === 'A') { e.preventDefault(); if (S.handleSelectAll) S.handleSelectAll(); } if (e.key === 'c' || e.key === 'C') { if (window.getSelection().toString()) return; e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; if (!UI.btnCopy.disabled) UI.btnCopy.click(); } if (e.key === 'x' || e.key === 'X') { e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; if (!UI.btnCut.disabled) UI.btnCut.click(); } if (e.key === 'v' || e.key === 'V') { e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isHistoryRoot = S.historyMode && S.path.length === 1; const isPasteBlocked = S.isFlattened || S.dupMode || S.offlineMode || S.historyMode || isStarredRoot || isRecentRoot || isHistoryRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; if (isPasteBlocked) return; if (!UI.btnPaste.disabled) UI.btnPaste.click(); } } if (e.altKey) { if (S.offlineMode && (e.key === 'c' || e.key === 'C')) { e.preventDefault(); if (UI.btnCopyLinkOffline && !UI.btnCopyLinkOffline.disabled) UI.btnCopyLinkOffline.click(); return; } if (e.key === 's' || e.key === 'S') { e.preventDefault(); openSettingsModal(); } if (e.key === 'g' || e.key === 'G') { e.preventDefault(); if (S.uploadMode) { if (UI.btnUpStart && !UI.btnUpStart.disabled) UI.btnUpStart.click(); } } if (e.key === 'p' || e.key === 'P') { e.preventDefault(); if (S.uploadMode) { if (UI.btnUpPause && !UI.btnUpPause.disabled) UI.btnUpPause.click(); } } if (e.key === 'u' || e.key === 'U') { e.preventDefault(); const cur = S.path[S.path.length - 1]; const isRoot = S.path.length === 1; const isUnzipBlocked = S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || (S.analyzeMode && cur.id === 'analyze_root') || (cur.id === 'virtual_search_root') || (S.recentMode && isRoot) || (S.starredMode && isRoot); if (!isUnzipBlocked && !UI.btnUnzip.disabled) UI.btnUnzip.click(); } if (e.key === 'd' || e.key === 'D') { e.preventDefault(); if (S.shareMode || S.uploadMode || S.trashMode || S.offlineMode) return; if (UI.btnDown && !UI.btnDown.disabled) UI.btnDown.click(); } if (e.key === 'a' || e.key === 'A') { e.preventDefault(); if (S.shareMode || S.uploadMode || S.trashMode || S.offlineMode) return; if (UI.btnAria2 && !UI.btnAria2.disabled) UI.btnAria2.click(); } if (e.key === 'e' || e.key === 'E') { e.preventDefault(); if (S.shareMode || S.trashMode) return; if (UI.btnExt && !UI.btnExt.disabled) UI.btnExt.click(); } if (e.key === 't' || e.key === 'T') { e.preventDefault(); if (UI.btnTheme) UI.btnTheme.click(); } } }; document.addEventListener('keydown', keyHandler); const showProperty = (item) => { const L = getStrings(); let rows = []; rows.push({ k: L.lbl_prop_name, v: item.name }); rows.push({ k: L.lbl_prop_size, v: fmtSize(item.size) }); rows.push({ k: L.lbl_prop_ctime, v: fmtDate(item.created_time) }); if (item.modified_time) rows.push({ k: L.lbl_prop_mtime, v: fmtDate(item.modified_time) }); let source = L.str_prop_unknown; if (item.kind === 'drive#task') source = L.str_prop_offline; else if (item.kind === 'drive#folder') source = L.lbl_type_folder; else if (item.from === 'share') source = L.str_prop_share; else source = L.str_prop_user; rows.push({ k: L.lbl_prop_source, v: source }); let link = item.source_url || (item.params && item.params.url) || item.web_content_link || item.url; if (!link && item.params && typeof item.params === 'object') { for (const key in item.params) { if (key.toLowerCase() === 'url') { link = item.params[key]; break; } } } if (link) { rows.push({ k: L.lbl_prop_link, v: link, isLink: true }); } let pathStr = "Root"; if (S.offlineMode) pathStr = L.title_offline; else if (S.shareMode) pathStr = L.btn_nav_share; else if (item._lineage) pathStr = item._lineage.map(x => x.name).join('/'); rows.push({ k: L.lbl_prop_path, v: pathStr }); let html = `
`; rows.forEach(r => { let valHtml = `
${esc(r.v)}
`; if (r.isLink) { valHtml = `
${esc(r.v)}
`; } html += `
${r.k}:
${valHtml}
`; }); html += `
`; const m = showModal(html); m.querySelector('.pk-modal h3').textContent = L.title_property; m.querySelector('.pk-modal').style.width = "520px"; }; const btnProp = document.getElementById('ctx-property'); if (btnProp) { btnProp.onclick = () => { const item = S.itemMap.get(S.activeId); if (item) showProperty(item); UI.ctx.style.display = 'none'; }; } const mouseHandler = (e) => { if (!document.querySelector('.pk-ov') || !UI || !UI.ctx) return; if (UI.ctx.style.display !== 'none' && !UI.ctx.contains(e.target)) { UI.ctx.style.display = 'none'; UI.ctx.style.flexDirection = ''; const isRoot = S.path.length === 1 && S.path[0].id === ''; if (!S.trashMode && isRoot && S.sel.size === 1) { const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder' && item.name === CONF.SYSTEM_FOLDER_NAME) { S.sel.clear(); renderVisible(); updateStat(); } } } }; document.addEventListener('mouseup', mouseHandler); function updateStat() { let total = 0; const validSelectedIds = new Set(); const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; if (item && !item.isHeader) { total++; if (S.sel.has(item.id)) { validSelectedIds.add(item.id); } } } if (validSelectedIds.size !== S.sel.size) { S.sel = validSelectedIds; } let n = S.sel.size; const btnInvert = document.getElementById('pk-btn-invert'); if (btnInvert) { btnInvert.style.display = (!S.dupMode && n > 0) ? 'flex' : 'none'; btnInvert.style.color = 'var(--pk-fg)'; } let hasProtectedItem = false; let hasSelFolder = false; let selSize = 0; if (n > 0) { for (let id of S.sel) { const item = S.itemMap.get(id); if (!item) continue; const sz = parseInt(item.size || 0); if (item.kind === 'drive#folder') { hasSelFolder = true; if (S.analyzeMode) { selSize += sz; } } else { selSize += sz; } if (!S.trashMode && isSystemItem(item)) hasProtectedItem = true; } } let statText = L.status_ready.replace('{n}', total); if (n > 0) { statText += `\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${L.sel_count.replace('{n}', n)}`; if (!S.shareMode && (S.analyzeMode || !hasSelFolder)) { statText += `\u00A0\u00A0${fmtSize(selSize)}`; } } UI.stat.textContent = statText; const hasSel = n > 0; let isSingleVideo = false; if (n === 1) { const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (item && item.kind !== 'drive#folder') { const mime = (item.mime_type || '').toLowerCase(); const ext = (item.name || '').split('.').pop().toLowerCase(); const isVid = mime.startsWith('video/') ||['mp4','mkv','avi','mov','wmv','flv','webm','ts'].includes(ext); let isTaskReady = true; if (S.offlineMode) isTaskReady = item.phase === 'PHASE_TYPE_COMPLETE'; if (S.uploadMode) isTaskReady = item.status === 'DONE'; if (isVid && isTaskReady) isSingleVideo = true; } } if (UI.btnExt) UI.btnExt.disabled = S.trashMode || !isSingleVideo; if (UI.btnAria2) UI.btnAria2.disabled = S.trashMode || !hasSel; if (UI.btnDown) UI.btnDown.disabled = S.trashMode || !hasSel; if (UI.chkAll) { let isAll = false; let isInd = false; if (n > 0 && total > 0) { isAll = (n === total); isInd = (n < total); } else { isAll = false; isInd = false; } UI.chkAll.checked = isAll; UI.chkAll.indeterminate = isInd; } const isShare = S.shareMode; const isOffline = S.offlineMode; const isHistory = S.historyMode; const isUpload = S.uploadMode; if (isUpload) { if (hasSel) { let hasRunning = false; let hasStopped = false; for (const id of S.sel) { const task = S.itemMap.get(id); if (task) { const s = task.status; if (['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(s)) hasRunning = true; if (['PAUSED', 'ERROR'].includes(s)) hasStopped = true; } if (hasRunning && hasStopped) break; } if (UI.btnUpPause) UI.btnUpPause.disabled = !hasRunning; if (UI.btnUpStart) UI.btnUpStart.disabled = !hasStopped; if (UI.btnUpDel) UI.btnUpDel.disabled = false; } else { if (UI.btnUpPause) UI.btnUpPause.disabled = true; if (UI.btnUpStart) UI.btnUpStart.disabled = true; if (UI.btnUpDel) UI.btnUpDel.disabled = true; } if (UI.btnUpClearAll) UI.btnUpClearAll.disabled = (S.uploadTasks.length === 0); } if (UI.btnRetryTask) { UI.btnRetryTask.style.display = isOffline ? 'inline-flex' : 'none'; const hasFailedTask = Array.from(S.sel).some(id => S.itemMap.get(id)?.phase === 'PHASE_TYPE_ERROR'); UI.btnRetryTask.disabled = !hasFailedTask; } if (UI.btnCopyLinkOffline) { UI.btnCopyLinkOffline.style.display = isOffline ? 'inline-flex' : 'none'; UI.btnCopyLinkOffline.disabled = !hasSel; } UI.btnCopy.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnCut.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnRename.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnBulkRename.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnDel.style.display = (isShare || isUpload) ? 'none' : 'inline-flex'; const delBtnSpan = UI.btnDel.querySelector('span'); if (delBtnSpan) { delBtnSpan.textContent = isHistory ? L.btn_clear_history : L.btn_del; UI.btnDel.setAttribute('data-pk-tip', isHistory ? L.tip_clear_history : L.tip_del); } if (isShare && UI.btnCancelShare) { UI.btnCancelShare.disabled = !hasSel; } const hasClipData = S.clipItems && S.clipItems.length > 0; const isPasteLocked = S.movingIds && S.movingIds.size > 0; if (UI.btnPaste) { UI.btnPaste.disabled = !hasClipData || isPasteLocked; const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isHistoryRoot = S.historyMode && S.path.length === 1; const shouldHidePaste = S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || isStarredRoot || isRecentRoot || isHistoryRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; UI.btnPaste.style.display = shouldHidePaste ? 'none' : 'inline-flex'; UI.btnPaste.style.cursor = isPasteLocked ? 'wait' : (UI.btnPaste.disabled ? 'not-allowed' : 'pointer'); } UI.btnCopy.disabled = !hasSel || isPasteLocked; UI.btnCut.disabled = !hasSel || isPasteLocked || hasProtectedItem; if (UI.btnNewFolder) { UI.btnNewFolder.disabled = isPasteLocked; if (isPasteLocked) UI.btnNewFolder.style.cursor = 'wait'; else UI.btnNewFolder.style.cursor = ''; } UI.btnDel.disabled = !hasSel || (n === 1 && hasProtectedItem); const isStandardRenameView = !(isShare || isOffline || isHistory || isUpload); if (isStandardRenameView) { if (n <= 1) { UI.btnRename.style.display = 'inline-flex'; UI.btnBulkRename.style.display = 'none'; UI.btnRename.disabled = (n === 0) || hasProtectedItem; } else { UI.btnRename.style.display = 'none'; UI.btnBulkRename.style.display = 'inline-flex'; UI.btnBulkRename.disabled = false; } } if (UI.btnRestore) UI.btnRestore.disabled = !hasSel; if (UI.btnDelForever) UI.btnDelForever.disabled = !hasSel; if (UI.btnPrune) { const isHiddenMode = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode; UI.btnPrune.style.display = isHiddenMode ? 'none' : 'inline-flex'; UI.btnPrune.disabled = isHiddenMode || !hasSelFolder; } if (UI.btnUnzip) { const cur = S.path[S.path.length - 1]; const isRoot = S.path.length === 1; const isUnzipHidden = S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || (S.analyzeMode && cur.id === 'analyze_root') || (cur.id === 'virtual_search_root') || (S.recentMode && isRoot) || (S.starredMode && isRoot); UI.btnUnzip.style.display = isUnzipHidden ? 'none' : 'inline-flex'; const isArchive = (it) => { if (!it || it.kind === 'drive#folder') return false; const n = (it.name || '').toLowerCase(); const m = (it.mime_type || '').toLowerCase(); return m.includes('zip') || m.includes('rar') || m.includes('7z') || n.endsWith('.zip') || n.endsWith('.rar') || n.endsWith('.7z'); }; const hasArchive = Array.from(S.sel).some(id => isArchive(S.itemMap.get(id))); UI.btnUnzip.disabled = S.trashMode || !hasArchive; } } async function getLinks() { const res = []; for (const id of S.sel) { let item = S.items.find(x => x.id === id); if (item && !item.web_content_link) { try { item = await apiGet(id); } catch { } } if (item?.web_content_link) res.push(item); } return res; } const generateQualityList = (data) => { const list =[]; const seenNames = new Set(); if (data.web_content_link) { list.push({ name: L.str_original, url: data.web_content_link, isOriginal: true }); seenNames.add(L.str_original); } const transcodeList =[]; if (data.medias && Array.isArray(data.medias)) { data.medias.forEach(m => { if (!m.link || !m.link.url) return; let name = m.resolution_name || m.video_stream_id || 'Unknown'; let streamUrl = m.link.url; if (m.video && m.video.video_type === 'mpegts' && !streamUrl.includes('.m3u8')) { streamUrl += '&ext=.m3u8'; } if (name.toLowerCase() === 'unknown' || !name) { name = L.str_original_fast; } if (name === 'Unknown') { } else if (name.includes('1080') || name === 'FHD') name = '1080P'; else if (name.includes('720') || name === 'HD') name = '720P'; else if (name.includes('480') || name === 'SD') name = '480P'; if (seenNames.has(name)) return; seenNames.add(name); transcodeList.push({ name: name, url: streamUrl, isOriginal: false, rawName: name }); }); } if (transcodeList.length > 0) { const getRank = (s) => { const n = s.toUpperCase(); if (n.includes('4K')) return 100; if (n.includes('2K')) return 90; if (n.includes('1080')) return 80; if (n.includes('720')) return 70; if (n.includes('480')) return 60; return 10; }; transcodeList.sort((a, b) => getRank(b.rawName) - getRank(a.rawName)); const highestStream = transcodeList[0]; if (!highestStream.name.includes('速') && !highestStream.name.includes('Fast') && highestStream.name !== L.str_original_fast) { const speedSuffix = (L.str_speed && L.str_speed.includes('速')) ? "(高速)" : "(Fast)"; highestStream.name = `${highestStream.name} ${speedSuffix}`; } } transcodeList.forEach(t => { list.push({ name: t.name, url: t.url, isOriginal: false }); }); list.sort((a, b) => { if (a.isOriginal) return -1; if (b.isOriginal) return 1; const getRank = (s) => { const n = s.toUpperCase(); if (n.includes('4K')) return 100; if (n.includes('2K')) return 90; if (n.includes('1080')) return 80; if (n.includes('720')) return 70; if (n.includes('480')) return 60; return 0; }; const rA = getRank(a.name); const rB = getRank(b.name); if (rA !== rB) return rB - rA; return b.name.localeCompare(a.name, undefined, { numeric: true }); }); return list; }; const getBestSource = (data) => { const fileName = (data.name || "").toLowerCase(); const mime = (data.mime_type || '').toLowerCase(); const generatedList = generateQualityList(data); const list = generatedList.map(item => ({ name: item.name, link: item.url, active: false, weight: 0, isOriginal: item.isOriginal })); const isLegacyFormat = fileName.endsWith('.avi') || fileName.endsWith('.wmv') || fileName.endsWith('.rmvb') || fileName.endsWith('.divx') || fileName.endsWith('.flv') || fileName.endsWith('.vob') || mime.includes('mpeg4') || mime.includes('avi') || mime.includes('x-ms-wmv'); const isMKV = fileName.endsWith('.mkv'); list.forEach(item => { const n = item.name; if (item.isOriginal) { item.weight = (isLegacyFormat || isMKV) ? -9999 : 50; } else { if (n.includes('高速') || n.includes('Fast')) item.weight = 500; else if (n === 'Unknown') item.weight = 400; else if (n.includes('1080P')) item.weight = 300; else if (n.includes('720P')) item.weight = 200; else if (n.includes('480P')) item.weight = 100; else item.weight = 10; } }); let bestMatch = null; if (list.length > 0) { const sortedByWeight = [...list].sort((a, b) => b.weight - a.weight); bestMatch = sortedByWeight[0]; } else { bestMatch = { name: L.str_original, link: data.web_content_link, active: true }; list.push(bestMatch); } bestMatch.active = true; return { src: bestMatch.link, name: bestMatch.name, list: list }; }; async function playVideo(item, startFullscreen = false) { if (S.trashMode) return; const getPhysicalId = (it) => { if ((S.offlineMode && it.kind === 'drive#task') || (S.uploadMode && it.file_id)) { return it.file_id || it.id; } return it.id; }; let pkHls = null; let isPlayerDestroyed = false; const L = getStrings(); const isVideoItem = (it) => { if (!it || it.isHeader || it.kind === 'drive#folder') return false; if (S.offlineMode && it.phase !== 'PHASE_TYPE_COMPLETE') return false; if (S.uploadMode && it.status !== 'DONE') return false; const m = (it.mime_type || '').toLowerCase(); const n = (it.name || '').toLowerCase(); const dur = (it.params && it.params.duration) || 0; const vExts = ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp']; return m.startsWith('video/') || dur > 0 || vExts.some(e => n.endsWith('.' + e)); }; const videoPlaylist = S.display.filter(i => isVideoItem(i)); let curListIdx = videoPlaylist.findIndex(v => v.id === item.id); const totalInList = videoPlaylist.length; let isSwitching = false; let switchReqId = 0; const showSadBox = (codecName) => { if (box.querySelector('.pk-err-dialog')) return; if (posterEl) { posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; posterEl.style.pointerEvents = 'auto'; } const savedPlayer = gmGet('pk_ext_player', 'potplayer'); const sadBoxSVG = ``; const recommended = qualityList.find(q => !q.isOriginal) || qualityList[0]; const recommendedUrl = recommended.link || recommended.url; const resOptions = qualityList.map(q => { const qUrl = q.link || q.url; const isSelected = qUrl === recommendedUrl; return ``; }).join(''); const lblRes = L.lbl_resolution; const dialog = document.createElement('div'); dialog.className = 'pk-err-dialog'; dialog.style.cssText = "position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(30, 30, 30, 0.85);backdrop-filter:blur(12px);border-radius:12px;padding:40px 50px;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.8);z-index:999;min-width:400px;border:1px solid rgba(255,255,255,0.1);"; dialog.innerHTML = `
${sadBoxSVG}
${L.err_codec_t1.replace('{c}', codecName)}
${L.err_codec_t2}
${lblRes}
${L.lbl_player}
`; box.appendChild(dialog); const showLinkModal = (url) => { const old = d.querySelector('.pk-link-export-ov'); if (old) old.remove(); const ov = document.createElement('div'); ov.className = 'pk-link-export-ov'; ov.style.cssText = "position:absolute; inset:0; background:rgba(0,0,0,0.9); backdrop-filter:blur(5px); z-index:2147483647; display:flex; align-items:center; justify-content:center; border-radius:inherit;"; const m = document.createElement('div'); m.className = 'pk-modal'; m.style.cssText = "position:relative;background:#222;padding:30px;border-radius:12px;width:500px;border:1px solid #444;box-shadow:0 20px 50px rgba(0,0,0,0.8);display:flex;flex-direction:column;gap:20px;text-align:left;"; m.innerHTML = `
${mkSvg(icons.close)}

${L.export_link_title}

`; ov.appendChild(m); d.appendChild(ov); const doClose = () => ov.remove(); m.querySelector('.pk-modal-close').onclick = doClose; m.querySelector('#pk_link_cancel').onclick = doClose; const cpBtn = m.querySelector('#pk_copy_link_btn'); const txt = m.querySelector('textarea'); setTimeout(() => txt.select(), 50); cpBtn.onclick = () => { GM_setClipboard(url); cpBtn.textContent = L.msg_copy_success; setTimeout(() => { cpBtn.textContent = L.btn_copy_link; }, 2000); }; ov.onclick = (evt) => { if(evt.target === ov) doClose(); }; m.onclick = (ev) => ev.stopPropagation(); }; dialog.querySelector('.pk-err-close').onclick = (e) => { e.stopPropagation(); dialog.remove(); }; dialog.onclick = (e) => e.stopPropagation(); const playerSel = dialog.querySelector('#pk_err_player_sel'); const launchBtn = dialog.querySelector('#pk_err_launch_btn'); playerSel.onchange = () => { launchBtn.textContent = (playerSel.value === 'other') ? L.btn_copy_link : L.btn_start_play; }; launchBtn.textContent = L.btn_start_play; launchBtn.onclick = (e) => { e.stopPropagation(); const selPlayer = playerSel.value; const selResLink = dialog.querySelector('#pk_err_res_sel').value; const selResName = dialog.querySelector('#pk_err_res_sel').options[dialog.querySelector('#pk_err_res_sel').selectedIndex].text; gmSet('pk_ext_player', selPlayer); let cleanLink = selResLink.replace('&ext=.m3u8', ''); if (cleanLink.includes('ts_downloader') && cleanLink.includes('url=')) { const urlParam = new URL(cleanLink).searchParams.get('url'); if (urlParam) cleanLink = decodeURIComponent(urlParam); } if (selPlayer === 'other') { GM_setClipboard(cleanLink); launchBtn.textContent = L.msg_copy_success; launchBtn.style.background = "#52c41a"; launchBtn.style.color = "#fff"; setTimeout(() => { if (typeof destroyPlayer === 'function') destroyPlayer(); }, 800); } else if (selPlayer === 'potplayer') { const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanLink} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; destroyPlayer(); } else { const curT = v.currentTime; currentLink = selResLink; currentResName = selResName; box.classList.add('buffering'); if (posterEl) { if (v.readyState >= 2 && v.currentTime > 0) { try { const cvs = document.createElement('canvas'); cvs.width = v.videoWidth; cvs.height = v.videoHeight; cvs.getContext('2d').drawImage(v, 0, 0); posterEl.querySelector('img').src = cvs.toDataURL('image/jpeg', 0.7); } catch (err) {} } posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; } if(resTxt) resTxt.textContent = currentResName; if(resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } dialog.remove(); shutterTargetTime = curT > 0.1 ? curT : 0; loadSource(currentLink, curT); v.play().catch(()=>{}); } }; }; const renderPlaylistItems = () => { const RANGE = 150; const start = Math.max(0, curListIdx - RANGE); const end = Math.min(videoPlaylist.length, curListIdx + RANGE + 1); return videoPlaylist.slice(start, end).map((v, i) => { const absIdx = start + i; const imgSrc = v.thumbnail_link || v.icon_link || ''; return `
`}).join(''); }; const triggerResume = (targetVideo, targetItem) => { if (targetVideo._hasShownResumeToast || hasUserSeeked || targetVideo._isRestarting) return; const savedData = gmGet('pk_progress_' + getPhysicalId(targetItem), 0); let savedT = 0; if (typeof savedData === 'number') { savedT = savedData; } else if (savedData && typeof savedData === 'object') { savedT = parseFloat(savedData.t || 0); } const skipOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; if (savedT > 5 || skipOp > 0) { targetVideo._hasShownResumeToast = true; } else { return; } const showToast = (text) => { const oldToast = d.querySelector('.pk-p-resume-toast'); if (oldToast) oldToast.remove(); const toast = document.createElement('div'); toast.className = 'pk-p-resume-toast'; toast.style.zIndex = "150"; toast.innerHTML = `${text}${L.btn_restart}
${mkSvg(icons.close)}
`; d.querySelector('#pk_p_box').appendChild(toast); toast.querySelector('#pk_re_start').onclick = (e) => { e.stopPropagation(); if (tCur) tCur.textContent = "00:00:00"; if (progFilled) progFilled.style.setProperty('width', '0%', 'important'); shutterTargetTime = 0.1; targetVideo._isRestarting = true; targetVideo.currentTime = 0; toast.remove(); }; toast.querySelector('#pk_re_close').onclick = (e) => { e.stopPropagation(); toast.remove(); }; setTimeout(() => { if(toast.parentNode) toast.remove(); }, 8000); }; if (savedT > 5) { showToast(L.msg_resume_hint.replace('{t}', fmtT(savedT))); } else if (skipOp > 0) { const dp = d.querySelector('.pk-player-box'); if(dp && !dp.querySelector('.pk-skip-toast')) { const tip = document.createElement('div'); tip.className = 'pk-skip-toast'; tip.style.cssText = "position:absolute;top:80px;left:20px;background:rgba(0,0,0,0.6);color:#ddd;padding:4px 8px;border-radius:4px;font-size:12px;pointer-events:none;animation:pkFadeIn 0.5s;"; tip.textContent = `${L.lbl_skip_op} ${skipOp}s`; dp.appendChild(tip); setTimeout(()=>tip.remove(), 3000); } } }; const softSwitch = async (newIdx, scrollMode = 'smooth') => { if (!videoPlaylist[newIdx]) return; switchReqId++; const myReqId = switchReqId; isSwitching = true; const v = d.querySelector('#pk_video'); const loader = d.querySelector('.pk-p-loading'); const poster = d.querySelector('#pk_p_poster'); if (v && v.currentTime > 5 && v.duration > 0) { gmSet('pk_progress_' + getPhysicalId(item), { t: v.currentTime, d: v.duration, ts: Date.now() }); } if (v) { v.pause(); v._isRestarting = false; v._hasShownResumeToast = false; Array.from(v.querySelectorAll('track')).forEach(t => t.remove()); if (subState.blobUrl) { URL.revokeObjectURL(subState.blobUrl); subState.blobUrl = null; } subState.hasSub = false; subState.track = null; const subNameLabel = d.querySelector('#pk_sub_name'); if (subNameLabel) subNameLabel.textContent = L.str_no_sub; v.src = ""; v.load(); } isSwitching = false; if (progFilled) progFilled.style.setProperty('width', '0%', 'important'); const newItem = videoPlaylist[newIdx]; if (tCur) tCur.textContent = '00:00:00'; if (tDur) { const newPId = getPhysicalId(newItem); const knownDur = (newItem.params && newItem.params.duration) || S.durationMap.get(newPId) || gmGet('pk_duration_' + newPId, 0); tDur.textContent = knownDur > 0 ? fmtT(knownDur) : '00:00:00'; } transformState = { rotate: 0, flipH: 1, flipV: 1, ratio: 'default' }; if (typeof applyTransform === 'function') applyTransform(); const ratioBtns = d.querySelectorAll('#pk_ratio_opts .pk-size-btn'); ratioBtns.forEach(btn => { if (btn.dataset.ratio === 'default') btn.classList.add('active'); else btn.classList.remove('active'); }); if (loader) loader.style.display = 'block'; hasCheckedProgress = false; const oldResumeToast = d.querySelector('.pk-p-resume-toast'); if (oldResumeToast) oldResumeToast.remove(); const errDialogs = d.querySelectorAll('.pk-err-dialog'); errDialogs.forEach(el => el.remove()); const linkOvs = d.querySelectorAll('.pk-link-export-ov'); linkOvs.forEach(el => el.remove()); const searchModal = d.querySelector('.pk-sub-search-modal'); if (searchModal) searchModal.remove(); const btnSearchS = d.querySelector('#pk_p_search'); if (btnSearchS) btnSearchS.style.setProperty('display', 'none', 'important'); const btnPipS = d.querySelector('#pk_p_pip'); if (btnPipS) btnPipS.style.setProperty('display', 'none', 'important'); curListIdx = newIdx; item = newItem; if (posterEl) { posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; const img = posterEl.querySelector('img'); if (img && newItem.thumbnail_link) { img.style.display = 'block'; img.src = newItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE'); img.style.opacity = '0'; img.onload = () => { img.style.opacity = '1'; if (btnSearchS) btnSearchS.style.setProperty('display', 'flex', 'important'); }; img.onerror = () => img.style.display = 'none'; } else if (img) { img.style.display = 'none'; } setTimeout(() => { posterEl.style.transition = 'opacity 0.4s ease'; }, 50); } if (loaderEl) loaderEl.style.display = 'block'; const pTitle = d.querySelector('.pk-player-title'); if(pTitle) pTitle.textContent = `[${newIdx + 1}/${totalInList}] ${newItem.name}`; const pTab = d.querySelector('#pk_p_plist_tab'); if(pTab) pTab.innerHTML = `${curListIdx + 1} / ${totalInList} `; if (pScroll) { pScroll.innerHTML = renderPlaylistItems(); pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); if (pTip) pTip.style.display = 'none'; const idx = parseInt(e.currentTarget.dataset.idx); if (idx === curListIdx) return; softSwitch(idx, 'instant'); }; el.onmouseenter = (e) => { if (!plist.classList.contains('open')) return; pTip.innerHTML = `${e.currentTarget.dataset.name}
${e.currentTarget.dataset.size}`; pTip.style.display = 'block'; }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => { if (pTip) pTip.style.display = 'none'; }; }); const activeItem = pScroll.querySelector('.pk-p-plist-item.active'); if (activeItem && box.classList.contains('plist-active')) { if (scrollMode === 'instant') { pScroll.style.scrollBehavior = 'auto'; } else { pScroll.style.scrollBehavior = 'smooth'; } activeItem.scrollIntoView({ behavior: scrollMode === 'instant' ? 'auto' : 'smooth', block: 'nearest', inline: 'center' }); if (scrollMode === 'instant') { setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); } } } const btnL = d.querySelector('#pk_p_side_L'); const btnR = d.querySelector('#pk_p_side_R'); if (btnL) btnL.style.display = newIdx === 0 ? 'none' : 'flex'; if (btnR) btnR.style.display = newIdx === totalInList - 1 ? 'none' : 'flex'; if (typeof updatePlistNav === 'function') updatePlistNav(); hasUserSeeked = false; try { const targetApiId = getPhysicalId(newItem); const newData = await apiGet(targetApiId); if (newData) { if (newData.thumbnail_link) newItem.thumbnail_link = newData.thumbnail_link; if (newData.icon_link) newItem.icon_link = newData.icon_link; } if (myReqId !== switchReqId) return; const freshData = getBestSource(newData); currentResName = freshData.name; currentLink = freshData.src; qualityList = freshData.list; const resTxt = d.querySelector('#pk_p_res_txt'); if (resTxt) resTxt.textContent = currentResName; const resList = d.querySelector('#pk_p_res_list'); if (resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } loadSource(freshData.src, null); if (typeof ThumbnailEngine !== 'undefined') { ThumbnailEngine.resetSource(freshData.src); } if(poster) poster.style.display = 'block'; if (d.querySelector('#pk_sub_name')) d.querySelector('#pk_sub_name').textContent = L.str_no_sub; autoMatchSubtitle(newItem); triggerResume(v, newItem); if (!isPlayerDestroyed) { await v.play().catch(()=>{}); } if (!v.paused && !isPlayerDestroyed) { box.classList.add('pk-v-started'); stopSpinner(); } updateState(); } catch (err) { if (myReqId === switchReqId) console.error("[SoftSwitch] Critical Error:", err); } finally { if (myReqId === switchReqId) { if(loader) loader.style.display = 'none'; } } }; const loadSource = (url, customStartTime = null) => { let startTime = 0; const skipOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; const pId = getPhysicalId(item); if (customStartTime !== null) { startTime = customStartTime; } else { const savedData = gmGet('pk_progress_' + pId, 0); let historyTime = 0; if (typeof savedData === 'number') { historyTime = savedData; } else if (savedData && typeof savedData === 'object') { historyTime = parseFloat(savedData.t || 0); } if (historyTime > 5) startTime = historyTime; else if (skipOp > 0) startTime = skipOp; } shutterTargetTime = startTime > 1 ? startTime : 0; if (tCur) tCur.textContent = fmtT(startTime); const totalDur = (item.params && item.params.duration) || S.durationMap.get(pId) || gmGet('pk_duration_' + pId, 0); if (progFilled && totalDur > 0) { const pct = Math.min(100, (startTime / totalDur) * 100); progFilled.style.setProperty('width', `${pct}%`, 'important'); } if (pkHls) { pkHls.destroy(); pkHls = null; } const isM3u8 = url.includes('.m3u8'); if (isM3u8 && window.Hls && window.Hls.isSupported()) { let finalPlayUrl = url; if (url.includes('&ext=.m3u8')) { const isHevc = (item.params?.video_codec === 'hevc') || (item.video_media_metadata?.video_codec === 'hevc'); if (isHevc) { console.log("[Hls] HEVC stream detected. Bypassing JS check to force hardware decoding."); } const realUrl = url.replace('&ext=.m3u8', ''); const duration = (item.params && item.params.duration) ? parseInt(item.params.duration) : 36000; const syntheticManifest = `#EXTM3U\n` + `#EXT-X-VERSION:3\n` + `#EXT-X-TARGETDURATION:${duration + 10}\n` + `#EXT-X-PLAYLIST-TYPE:VOD\n` + `#EXTINF:${duration},\n` + `${realUrl}\n` + `#EXT-X-ENDLIST`; const blob = new Blob([syntheticManifest], { type: 'application/x-mpegurl' }); finalPlayUrl = URL.createObjectURL(blob); console.log("[Hls] Generated synthetic manifest for TS stream"); } pkHls = new window.Hls({ debug: false, enableWorker: true, lowLatencyMode: true, startPosition: startTime, fragLoadingMaxRetry: 1, manifestLoadingMaxRetry: 1, levelLoadingMaxRetry: 1, fragLoadingTimeOut: 15000, manifestLoadingTimeOut: 10000, maxBufferHole: 0.5, maxFragLookUpTolerance: 0.1, nudgeOffset: 0.1, nudgeMaxRetry: 5, maxBufferLength: 120, maxMaxBufferLength: 600, backBufferLength: 90, maxBufferHole: 0.5, startLevel: -1, capLevelToPlayerSize: false }); pkHls.loadSource(finalPlayUrl); pkHls.attachMedia(v); const healthTimer = setInterval(() => { if (!pkHls || isPlayerDestroyed) { clearInterval(healthTimer); return; } if (box.classList.contains('buffering')) { if (!v._bufferingSince) v._bufferingSince = Date.now(); const isColdStart = v.currentTime < 1.0; const timeoutThreshold = isColdStart ? 60000 : 20000; if (Date.now() - v._bufferingSince > timeoutThreshold) { console.warn(`[Watchdog] Buffering timeout (${isColdStart ? 'Cold Start' : 'Mid-Stream'}). Forcing error...`); clearInterval(healthTimer); handleVideoError({ force: true, target: v }); } } else { v._bufferingSince = null; } if (!v.paused && v.currentTime > 0.5) { if (v.videoWidth === 0 || v.videoHeight === 0) { v._blackScreenCount = (v._blackScreenCount || 0) + 1; } else { const quality = v.getVideoPlaybackQuality ? v.getVideoPlaybackQuality() : null; const decodedFrames = quality ? quality.totalVideoFrames : (v.webkitDecodedFrameCount || 0); if (decodedFrames === 0) { v._blackScreenCount = (v._blackScreenCount || 0) + 1; } else { v._blackScreenCount = 0; } } if (v._blackScreenCount > 3) { console.warn(`[Watchdog] Black screen detected (Time: ${v.currentTime.toFixed(1)}, Frames: 0). Forcing external player.`); clearInterval(healthTimer); handleVideoError({ force: true, target: v }); } } }, 1000); pkHls.on(window.Hls.Events.LEVEL_LOADED, function (event, data) { if (!data.details || !data.details.videoCodec) return; const vCodec = data.details.videoCodec.toLowerCase(); const isBadVideo = vCodec.includes('mp4v') || vCodec.includes('hvc1') || vCodec.includes('hev1'); if (isBadVideo) { const testMime = `video/mp4; codecs="${vCodec}"`; if (window.MediaSource && !window.MediaSource.isTypeSupported(testMime)) { console.warn(`[VideoCheck] Unsupported video codec detected: ${vCodec}`); v.pause(); showSadBox(vCodec); if (pkHls) { pkHls.destroy(); pkHls = null; } } } }); pkHls.on(window.Hls.Events.AUDIO_TRACKS_UPDATED, function (event, data) { const tracks = data.audioTracks || []; let detectedBadCodec = null; for (const track of tracks) { const codec = (track.codec || '').toLowerCase(); const name = (track.name || '').toLowerCase(); const isSuspicious = codec.includes('ac-3') || codec.includes('ec-3') || codec.includes('dts') || name.includes('dts') || name.includes('truehd') || name.includes('atmos'); if (isSuspicious) { const testMime = `audio/mp4; codecs="${track.codec || 'ac-3'}"`; if (window.MediaSource && !window.MediaSource.isTypeSupported(testMime)) { detectedBadCodec = codec || name; break; } } } if (detectedBadCodec) { console.warn(`[AudioCheck] Unsupported codec detected: ${detectedBadCodec}`); v.pause(); showSadBox(detectedBadCodec.toUpperCase()); } }); pkHls.on(window.Hls.Events.ERROR, function (event, data) { if (data.fatal) { switch (data.type) { case window.Hls.ErrorTypes.NETWORK_ERROR: if (data.response && (data.response.code === 403 || data.response.code === 404 || data.response.code === 401)) { console.warn(`[Hls] Fatal Network Error (${data.response.code}). Triggering rollback.`); handleVideoError({ target: v }); } else { console.log("[Hls] Network error, trying to recover..."); pkHls.startLoad(); } break; case window.Hls.ErrorTypes.MEDIA_ERROR: pkHls.recoverMediaError(); setTimeout(() => { if (v.paused && !box.querySelector('.pk-err-dialog')) { handleVideoError({ target: v }); } }, 1500); break; default: handleVideoError({ target: v }); break; } } else if (data.details === 'bufferStalledError' && v.currentTime < 1) { handleVideoError({ target: v }); } }); } else { v.src = url; if (startTime > 0) { v.currentTime = startTime; } } }; let initialData = getBestSource(item); let currentLink = initialData.src; let qualityList = initialData.list; let currentResName = initialData.name; let lastWorkingLink = null; let lastWorkingResName = null; const failedUrls = new Set(); let posterUrl = item.thumbnail_link || ''; if (posterUrl && posterUrl.includes('SIZE_MEDIUM')) { posterUrl = posterUrl.replace('SIZE_MEDIUM', 'SIZE_LARGE'); } try { const linkUrl = new URL(currentLink); const linkDomain = linkUrl.origin; if (!document.head.querySelector(`link[href="${linkDomain}"]`)) { const pc = document.createElement('link'); pc.rel = 'preconnect'; pc.href = linkDomain; pc.crossOrigin = 'anonymous'; document.head.appendChild(pc); } } catch(e) {} const d = document.createElement('div'); d.id = 'pk-player-ov'; d.className = 'pk-ov pk-dark'; d.tabIndex = 0; d.style.cssText = "position:fixed;inset:0;z-index:2147483640;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;outline:none;will-change:transform;"; const icons = { play: '', playCenter: '', pause: '', vol: '', mute: '', full: '', settings: '', exitFull: '', close: '', search: '', pip: '', rotL: '', rotR: '', flipH: '', flipV: '' }; const mkSvg = (path) => `${path}`; const renderQualityMenu = (list, activeName) => list.map(q => { const displayName = q.name.replace(/\s(\(|\uff08)/, '
$1'); return `
${displayName}
`; }).join(''); const styleEl = document.createElement('style'); styleEl.textContent = ` .pk-player-box.ui-hidden .pk-player-top, .pk-player-box.ui-hidden .pk-player-controls, .pk-player-box.ui-hidden .pk-p-prog-wrap, .pk-player-box.ui-hidden .pk-p-side-nav { opacity: 0 !important; pointer-events: none !important; } .pk-player-box.ui-hidden:not(.plist-active) .pk-p-plist-tab { opacity: 0 !important; pointer-events: none !important; } .pk-player-box.ui-hidden .pk-p-center-play { opacity: 1 !important; pointer-events: none; } .pk-player-box.ui-hidden { cursor: none; } .pk-p-pop { flex-direction: column !important; } .pk-p-eye { transition: transform 1.0s cubic-bezier(0.2, 0, 0.2, 1); transform: translate3d(0,0,0); backface-visibility: hidden; image-rendering: -webkit-optimize-contrast; } .pk-look-r .pk-p-eye { transform: translate3d(24px,0,0); } .pk-look-l .pk-p-eye { transform: translate3d(-24px,0,0); } .pk-eye-closed { display: none; } .pk-player-progress-thumb.pk-blink-anim .pk-eye-open { display: none; } .pk-player-progress-thumb.pk-blink-anim .pk-eye-closed { display: block; } .pk-player-progress-thumb { position: absolute; top: 50% !important; transform: perspective(1px) translateY(-50%) scale(0) translateZ(0) !important; opacity: 0 !important; will-change: transform, opacity; transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s !important; image-rendering: -webkit-optimize-contrast; } .pk-player-progress-bg { transition: height 0.2s ease !important; transform: translateZ(0); backface-visibility: hidden; -webkit-perspective: 1000; } #pk_p_prog_area:hover .pk-player-progress-thumb, .pk-player-box.pk-is-seeking .pk-player-progress-thumb, .pk-player-progress-thumb.pk-blink-hold { transform: perspective(1px) translateY(-50%) scale(1) translateZ(0) !important; opacity: 1 !important; } #pk_p_prog_area:hover .pk-player-progress-bg, .pk-player-box.pk-is-seeking .pk-player-progress-bg, .pk-player-progress-bg:has(.pk-blink-hold) { height: 6px !important; } #pk_p_box:not(.pk-is-seeking) #pk_p_plist:hover ~ .pk-player-controls, #pk_p_box:not(.pk-is-seeking) #pk_p_plist:hover ~ .pk-p-prog-wrap { opacity: 0 !important; pointer-events: none !important; } .pk-p-side-nav { position: absolute; top: 50%; transform: translateY(-50%) translateZ(0); width: 60px; height: 60px; background: rgba(0, 0, 0, 0.3); display: flex; align-items: center; justify-content: center; color: #fff; cursor: pointer; z-index: 40; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0; border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.18); pointer-events: auto !important; box-shadow: 0 4px 15px rgba(0,0,0,0.15); will-change: transform, opacity; } .pk-player-box:hover .pk-p-side-nav { opacity: 1; } .pk-p-side-nav:hover { background: rgba(0, 0, 0, 0.45); transform: translateY(-50%) scale(1.08) translateZ(0); border-color: rgba(255, 255, 255, 0.3); } .pk-p-side-nav.L { left: 30px; } .pk-p-side-nav.R { right: 30px; } .pk-p-side-nav svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2.8; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 5px rgba(0,0,0,0.2)); } .pk-p-side-nav.L svg { margin-left: -2px; } .pk-p-side-nav.R svg { margin-left: 2px; } .pk-p-sub-pop { position: absolute; bottom: 48px; right: 0; background: #222; border-radius: 8px; padding: 16px; width: 340px; height: 380px; color: #eee; font-size: 12px; box-shadow: 0 12px 40px rgba(0,0,0,0.5); border: 1px solid #333; display: none; flex-direction: column; cursor: default; z-index: 30; font-family: sans-serif; box-sizing: border-box; } .pk-p-sub-pop::after { content: ""; position: absolute; top: 100%; left: 0; right: 0; height: 12px; background: transparent; } .pk-p-menu-con:hover .pk-p-sub-pop, .pk-p-sub-pop:hover { display: flex; } .pk-sub-tabs { display: flex; border-bottom: 1px solid #333; margin-bottom: 15px; flex-shrink: 0; } .pk-sub-tab { padding: 8px 12px; color: #aaa; cursor: pointer; font-size: 13px; position: relative; } .pk-sub-tab.active { color: #4aa1ff; font-weight: bold; border-bottom: 2px solid #4aa1ff; } .pk-sub-tab.active::after { content: ''; position: absolute; bottom: -1px; left: 0; right: 0; height: 2px; background: #4aa1ff; } .pk-sub-pane { display: none; flex-direction: column; gap: 12px; flex: 1; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #444 transparent; } .pk-sub-pane.active { display: flex; } .pk-sub-radio-grp { display: flex; flex-direction: column; gap: 14px; margin-top: 5px; } .pk-sub-radio-item { display: flex; align-items: center; gap: 10px; cursor: pointer; color: #eee; font-size: 13px; user-select: none; } .pk-sub-radio-item input { width: 16px; height: 16px; accent-color: #4aa1ff; cursor: pointer; margin: 0; } .pk-sub-val-input { width: 50px; background: #333; border: 1px solid #444; color: #fff; border-radius: 4px; padding: 2px 5px; text-align: center; font-size: 12px; outline: none; } .pk-sub-val-input:focus { border-color: #4aa1ff; } .pk-more-sep { height: 1px; background: #333; margin: 15px 0; } .pk-size-row { display: flex; align-items: center; margin-bottom: 15px; } .pk-size-label { min-width: 40px; margin-right: 12px; white-space: nowrap; color: #888; font-size: 12px; flex-shrink: 0; } .pk-size-opts { display: flex; gap: 8px; flex: 1; } .pk-size-btn { flex: 1; background: #333; border: 1px solid #444; color: #ddd; padding: 6px 4px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 0; text-align: center; } .pk-size-btn:hover { background: #444; border-color: #555; color: #fff; } .pk-size-btn.active { color: #4aa1ff; border-color: #4aa1ff; background: rgba(74, 161, 255, 0.1); } .pk-size-btn svg { width: 16px; height: 16px; margin-right: 4px; display: block; flex-shrink: 0; margin-bottom: 1px; } .pk-sub-pane::-webkit-scrollbar { width: 6px; } .pk-sub-pane::-webkit-scrollbar-track { background: transparent; } .pk-sub-pane::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; } .pk-sub-pane::-webkit-scrollbar-thumb:hover { background: #555; } .pk-sub-radio-group { display: flex; flex-direction: column; gap: 12px; margin-top: 8px; } .pk-sub-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; } .pk-sub-label { color: #aaa; white-space: nowrap; flex-shrink: 0; margin-right: 10px; } .pk-sub-btn-group { display: flex; gap: 8px; margin-top: 4px; } .pk-sub-btn { flex: 1; background: #333; border: 1px solid #444; color: #ddd; padding: 6px 4px; border-radius: 4px; cursor: pointer; text-align: center; transition: background 0.2s; font-size: 11.5px; line-height: 1.2; display: flex; align-items: center; justify-content: center; } .pk-sub-btn:hover { background: #444; } .pk-sub-btn:active { background: #555; } .pk-sub-ctrl { display: flex; align-items: center; background: #333; border-radius: 4px; border: 1px solid #444; } .pk-sub-ctrl-btn { width: 28px; height: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #ddd; } .pk-sub-ctrl-btn:hover { background: #444; } .pk-sub-val { width: 60px; text-align: center; border-left: 1px solid #444; border-right: 1px solid #444; height: 24px; line-height: 24px; font-variant-numeric: tabular-nums; } .pk-sub-slider { -webkit-appearance: none; width: 100px; height: 4px; background: #444; border-radius: 2px; outline: none; } .pk-sub-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff; cursor: pointer; } .pk-sub-check { width: 14px; height: 14px; accent-color: #4aa1ff; cursor: pointer; } video::cue { background: rgba(0,0,0,0.5); color: white; font-family: sans-serif; } #pk_p_box { position: absolute !important; top: 10% !important; left: 50% !important; transform: translateX(-50%) !important; width: 90% !important; height: 80% !important; background: #000; border-radius: 0 !important; overflow: visible !important; transition: height 0.2s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 25px 50px rgba(0,0,0,0.5); } #pk_p_box.plist-active { height: calc(80% - 84px) !important; } #pk_p_box:fullscreen, #pk_p_box:-webkit-full-screen, #pk_p_box:-moz-full-screen { width: 100% !important; height: 100% !important; top: 0 !important; left: 0 !important; transform: none !important; margin: 0 !important; border-radius: 0 !important; overflow: hidden !important; } #pk_video { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; z-index: 10; transition: none; } .pk-player-controls { position: absolute; bottom: 0; left: 0; right: 0; z-index: 80 !important; transition: none; } .pk-p-prog-wrap { position: absolute; bottom: 64px; left: 20px; right: 20px; z-index: 60 !important; display: flex; align-items: center; gap: 16px; transition: none; pointer-events: none; } .pk-p-prog-wrap > * { pointer-events: auto; } #pk_p_box.pk-tab-hover:not(.pk-is-seeking) .pk-player-controls, #pk_p_box.pk-tab-hover:not(.pk-is-seeking) .pk-p-prog-wrap { opacity: 0 !important; pointer-events: none !important; transition: opacity 0.2s ease; } .pk-player-progress-container { position: relative !important; bottom: auto !important; left: auto !important; right: auto !important; width: auto !important; flex: 1; z-index: auto !important; } .pk-p-side-nav, .pk-p-center-play, .pk-p-seek-indicator { top: 50%; transition: top 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease, transform 0.2s ease !important; } #pk_p_plist { position: absolute; top: 100%; left: 0; right: 0; z-index: 95 !important; height: 84px; opacity: 1; pointer-events: none; } .pk-p-plist-tab { pointer-events: auto !important; margin-bottom: -1px !important; z-index: 90 !important; } .pk-p-plist-strip { opacity: 0; pointer-events: none !important; transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1); } #pk_p_box.plist-active .pk-p-plist-strip { opacity: 1; pointer-events: auto !important; } .pk-p-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 35; pointer-events: none; } #pk_p_poster { z-index: 25 !important; transition: opacity 0.4s ease; width: 100% !important; height: 100% !important; background: #000; } .pk-player-box.pk-v-started #pk_p_poster { pointer-events: none; } .pk-transcode-mask { position: absolute; inset: 0; z-index: 30; background: #0c0c0c; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #ddd; font-size: 14px; } .pk-tc-icon { width: 50px; height: 50px; margin-bottom: 20px; border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--pk-pri); border-radius: 50%; animation: spin 1s linear infinite; } .pk-tc-btn { margin-top: 20px; padding: 6px 16px; border: 1px solid #444; border-radius: 20px; cursor: pointer; font-size: 12px; transition: all 0.2s; } .pk-tc-btn:hover { background: #333; border-color: #666; color: #fff; } #pk_p_preview { position: absolute; bottom: 85px; left: 0; display: flex; flex-direction: column; align-items: center; z-index: 100; pointer-events: none; opacity: 0; transform: translateX(-50%) scale(0.95); transform-origin: bottom center; transition: opacity 0.15s ease, transform 0.15s ease; will-change: transform, opacity, left; } #pk_p_preview.show { opacity: 1; transform: translateX(-50%) scale(1); } .pk-prev-img-box { position: relative; background: #000; border: 2px solid #fff; border-radius: 4px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); overflow: hidden; display: none; min-width: 120px; min-height: 68px; transition: width 0.1s, height 0.1s; } .pk-prev-img-box img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.1s ease-out; } .pk-prev-img-box img.active { opacity: 1; } .pk-prev-time { margin-top: 6px; background: rgba(0,0,0,0.85); color: #fff; font-size: 12px; font-weight: 600; text-align: center; padding: 4px 8px; border-radius: 4px; font-family: "Segoe UI", Roboto, monospace; box-shadow: 0 2px 8px rgba(0,0,0,0.3); text-shadow: 0 1px 2px rgba(0,0,0,0.5); } `; document.head.appendChild(styleEl); d.innerHTML = `
00:00
${mkSvg(icons.playCenter)}
${mkSvg(icons.vol)}
100%
00:00 / 00:00
${mkSvg('')}
${mkSvg('')}
[${curListIdx + 1}/${totalInList}] ${esc(item.name)}
${mkSvg(icons.pip)}
${mkSvg(icons.close)}
${curListIdx + 1} / ${totalInList}
${mkSvg('')}
${renderPlaylistItems()}
${mkSvg('')}
00:00:00
00:00:00
${mkSvg(icons.pause)}
${mkSvg(icons.vol)}
${currentResName}
${renderQualityMenu(qualityList, currentResName)}
1.0x
3.0x
2.0x
1.5x
1.25x
1.0x
0.75x
0.5x
${mkSvg(icons.settings)}
${L.tab_sub}
${L.tab_size}
${L.tab_more}
${L.lbl_sub_sel}
${L.str_no_sub}
${L.btn_sub_search}
${L.btn_sub_cloud}
${L.btn_sub_local}
${L.lbl_sub_pos}
${L.lbl_sub_bottom} ${L.lbl_sub_top}
${L.lbl_sub_bg_op}
0% 100%
${L.lbl_sub_size}
20
${L.lbl_sub_offset}
0.0 ${L.unit_sec}
${L.lbl_ratio}
${L.opt_ratio_def}
16:9
4:3
${L.lbl_direction}
${mkSvg(icons.rotL)} ${L.btn_rot_l}
${mkSvg(icons.rotR)} ${L.btn_rot_r}
${mkSvg(icons.flipH)} ${L.btn_flip_h}
${mkSvg(icons.flipV)} ${L.btn_flip_v}
${L.lbl_play_end}
${L.lbl_skip_op}
${L.btn_mark}
0 ${L.unit_sec}
${L.lbl_skip_ed}
${L.btn_mark}
0 ${L.unit_sec}
${mkSvg(icons.full)}
`; const v = d.querySelector('#pk_video'); const box = d.querySelector('#pk_p_box'); const btnPlay = d.querySelector('#pk_p_play'); const btnVol = d.querySelector('#pk_p_vol'); const slideVol = d.querySelector('#pk_p_vol_slide'); const btnFull = d.querySelector('#pk_p_full'); const btnClose = d.querySelector('#pk_p_close'); const btnSearch = d.querySelector('#pk_p_search'); const tCur = d.querySelector('#pk_t_cur'); const tDur = d.querySelector('#pk_t_dur'); const progArea = d.querySelector('#pk_p_prog_area'); const progFilled = d.querySelector('#pk_p_filled'); const resList = d.querySelector('#pk_p_res_list'); const resTxt = d.querySelector('#pk_p_res_txt'); const spdList = d.querySelector('#pk_p_spd_list'); const plist = d.querySelector('#pk_p_plist'); const pTab = d.querySelector('#pk_p_plist_tab'); const pScroll = d.querySelector('#pk_p_plist_scroll'); pScroll.onwheel = (e) => { e.preventDefault(); pScroll.scrollBy({ left: e.deltaY > 0 ? 300 : -300, behavior: 'smooth' }); }; let pTip = document.getElementById('pk_p_plist_tip_global'); if (!pTip) { pTip = document.createElement('div'); pTip.id = 'pk_p_plist_tip_global'; pTip.className = 'pk-p-plist-tip'; document.body.appendChild(pTip); } d.querySelector('#pk_p_side_L').onclick = (e) => { e.stopPropagation(); const prevIdx = (curListIdx - 1 + totalInList) % totalInList; softSwitch(prevIdx); }; d.querySelector('#pk_p_side_R').onclick = (e) => { e.stopPropagation(); const nextIdx = (curListIdx + 1) % totalInList; softSwitch(nextIdx); }; pTab.onmouseenter = () => box.classList.add('pk-tab-hover'); pTab.onmouseleave = () => box.classList.remove('pk-tab-hover'); const strip = d.querySelector('.pk-p-plist-strip'); if (strip) { strip.onmouseenter = () => box.classList.add('pk-tab-hover'); strip.onmouseleave = () => box.classList.remove('pk-tab-hover'); } pTab.onclick = (e) => { e.stopPropagation(); plist.classList.toggle('open'); box.classList.toggle('plist-active'); pTab.setAttribute('data-pk-tip', plist.classList.contains('open') ? L.tip_plist_close : L.tip_plist_open); if (plist.classList.contains('open')) { const activeItem = pScroll.querySelector('.active'); if (activeItem) activeItem.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'center' }); } }; pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onmouseenter = (e) => { if (!plist.classList.contains('open')) return; pTip.innerHTML = `${e.currentTarget.dataset.name}
${e.currentTarget.dataset.size}`; pTip.style.display = 'block'; }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => pTip.style.display = 'none'; el.onclick = (e) => { e.stopPropagation(); if (pTip) pTip.style.display = 'none'; const idx = parseInt(e.currentTarget.dataset.idx); if (idx === curListIdx) return; softSwitch(idx); }; }); const updatePlistNav = () => { const sl = pScroll.scrollLeft; const sw = pScroll.scrollWidth; const cw = pScroll.clientWidth; d.querySelector('#pk_p_plist_L').style.display = sl <= 5 ? 'none' : 'flex'; d.querySelector('#pk_p_plist_R').style.display = (sl + cw >= sw - 5) ? 'none' : 'flex'; }; d.querySelector('#pk_p_plist_L').onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: -400, behavior: 'smooth' }); setTimeout(updatePlistNav, 300); }; d.querySelector('#pk_p_plist_R').onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: 400, behavior: 'smooth' }); setTimeout(updatePlistNav, 300); }; pScroll.addEventListener('scroll', updatePlistNav, { passive: true }); setTimeout(updatePlistNav, 50); document.body.appendChild(d); const initL = d.querySelector('#pk_p_side_L'); const initR = d.querySelector('#pk_p_side_R'); if (initL) initL.style.display = curListIdx === 0 ? 'none' : 'flex'; if (initR) initR.style.display = curListIdx === totalInList - 1 ? 'none' : 'flex'; let hideTimer = null; let isMouseOverUI = false; const resetHideTimer = () => { box.classList.remove('ui-hidden'); if (hideTimer) clearTimeout(hideTimer); if (isMouseOverUI) return; hideTimer = setTimeout(() => { if (!v.paused) box.classList.add('ui-hidden'); }, 3000); }; const protectedUIs = [ d.querySelector('.pk-player-top'), d.querySelector('.pk-player-controls'), d.querySelector('.pk-p-prog-wrap') ]; protectedUIs.forEach(ui => { if (!ui) return; ui.addEventListener('mouseenter', () => { isMouseOverUI = true; if (hideTimer) clearTimeout(hideTimer); }); ui.addEventListener('mouseleave', () => { isMouseOverUI = false; resetHideTimer(); }); }); box.addEventListener('mouseleave', () => { if (!isMouseOverUI) box.classList.add('ui-hidden'); }); box.addEventListener('mouseenter', resetHideTimer); box.addEventListener('mousemove', resetHideTimer); box.addEventListener('click', resetHideTimer); box.addEventListener('keydown', resetHideTimer); resetHideTimer(); const handleVideoError = (e) => { if (isSwitching) return; if (!v.getAttribute('src') && !pkHls) return; if (v.networkState === 2 && !v.error && !e.force) return; const errCode = v.error ? v.error.code : (e.force ? 4 : 0); const errMsg = v.error ? v.error.message : ""; console.warn(`[VideoError] Code: ${errCode}, Msg: ${errMsg}, Src: ${v.src || 'HLS'}`); if (currentLink) failedUrls.add(currentLink); if (lastWorkingLink && lastWorkingLink !== currentLink && !failedUrls.has(lastWorkingLink)) { console.log(`[Compatibility] Target stream failed, rolling back to: ${lastWorkingResName}`); if (resTxt) resTxt.textContent = `${L.str_compat_mode} (${lastWorkingResName})`; const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(217, 48, 37, 0.9);color:#fff;padding:6px 12px;border-radius:20px;font-size:12px;z-index:100;animation:pkFadeIn 0.5s;"; toast.textContent = L.str_switch_compat.replace('{n}', lastWorkingResName); box.appendChild(toast); setTimeout(() => toast.remove(), 4000); currentResName = lastWorkingResName; currentLink = lastWorkingLink; const savedTime = v.currentTime; loadSource(currentLink, savedTime); v.play().catch(()=>{}); const resList = d.querySelector('#pk_p_res_list'); if (resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } return; } const nextCandidate = qualityList.find(q => { const url = q.link || q.url; return url !== currentLink && !failedUrls.has(url); }); if (nextCandidate) { console.log(`[Compatibility] Auto-switching to next available source: ${nextCandidate.name}`); if (resTxt) resTxt.textContent = `${L.str_compat_mode} (${nextCandidate.name})`; const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(33, 150, 243, 0.9);color:#fff;padding:6px 12px;border-radius:20px;font-size:12px;z-index:100;animation:pkFadeIn 0.5s;"; toast.textContent = L.msg_fallback_report.replace('{n}', nextCandidate.name); box.appendChild(toast); setTimeout(()=>toast.remove(), 4000); currentResName = nextCandidate.name; currentLink = nextCandidate.link || nextCandidate.url; const savedTime = v.currentTime; loadSource(currentLink, savedTime); v.play().catch(()=>{}); const resList = d.querySelector('#pk_p_res_list'); if (resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } return; } if (errCode !== 4 && !e.force) { console.log("[Video] Recoverable error detected, staying in loader."); return; } const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'none'; showSadBox(currentResName); }; v.addEventListener('error', handleVideoError); v.addEventListener('loadstart', () => { const errOv = box.querySelector('.pk-err-ov'); if (errOv) errOv.remove(); const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'block'; }); (async () => { try { const targetApiId = getPhysicalId(item); const newData = await apiGet(targetApiId); const freshData = getBestSource(newData); qualityList = freshData.list; resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); if (v.error || !currentLink || (currentResName === L.str_original && freshData.name !== L.str_original)) { const savedTime = v.currentTime; currentLink = freshData.src; currentResName = freshData.name; resTxt.textContent = currentResName; loadSource(currentLink); v.currentTime = savedTime; v.play().catch(()=>{}); resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } } catch (e) { } })(); const fmtT = (s) => { s = Math.max(0, s || 0); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sc = Math.floor(s % 60); return String(h).padStart(2, '0') + ":" + String(m).padStart(2, '0') + ":" + String(sc).padStart(2, '0'); }; const fmtFullT = fmtT; const togglePlay = () => { if (v.paused) v.play(); else v.pause(); }; const ThumbnailEngine = (() => { let shadowV = null; let canvas = null; let ctx = null; let isInit = false; let cacheStore = null; let currentReqId = 0; const BASE_HEIGHT = 180; const DISPLAY_HEIGHT = 120; const previewBox = d.querySelector('#pk_p_preview'); const imgBox = d.querySelector('#pk_p_img_box'); const previewTime = previewBox.querySelector('.pk-prev-time'); const init = async () => { if (isInit) return; if (window.localforage) { cacheStore = window.localforage.createInstance({ name: 'pk_thumbs', storeName: 'snapshots' }); } shadowV = document.createElement('video'); shadowV.muted = true; shadowV.crossOrigin = 'anonymous'; shadowV.style.display = 'none'; shadowV.preload = 'auto'; shadowV.src = currentLink; canvas = document.createElement('canvas'); canvas.width = 160; canvas.height = 90; ctx = canvas.getContext('2d'); shadowV.onerror = () => console.warn("[Thumb] Shadow player error"); isInit = true; }; const getCacheKey = (time) => `${getPhysicalId(item)}_${Math.floor(time)}`; const generate = async (time) => { if (!isInit) await init(); if (cacheStore) { const cachedBlob = await cacheStore.getItem(getCacheKey(time)); if (cachedBlob) return URL.createObjectURL(cachedBlob); } return new Promise((resolve, reject) => { const seekHandler = () => { try { const vw = shadowV.videoWidth || 160; const vh = shadowV.videoHeight || 90; const ratio = vw / vh; const renderH = BASE_HEIGHT; const renderW = Math.floor(renderH * ratio); if (canvas.width !== renderW || canvas.height !== renderH) { canvas.width = renderW; canvas.height = renderH; } ctx.drawImage(shadowV, 0, 0, renderW, renderH); canvas.toBlob((blob) => { if (cacheStore) cacheStore.setItem(getCacheKey(time), blob).catch(()=>{}); resolve(URL.createObjectURL(blob)); }, 'image/jpeg', 0.8); } catch (e) { reject(e); } finally { shadowV.removeEventListener('seeked', seekHandler); } }; shadowV.addEventListener('seeked', seekHandler); shadowV.currentTime = time; }); }; const show = async (clientX, rect) => { const boxRect = box.getBoundingClientRect(); const pos = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); const targetTime = pos * v.duration; if (!isFinite(targetTime)) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let left = (clientX - boxRect.left) / scale; const halfWidth = (imgBox.offsetWidth / 2) || 80; const minX = halfWidth + 10; const maxX = (boxRect.width / scale) - halfWidth - 10; left = Math.max(minX, Math.min(maxX, left)); previewBox.style.left = `${left}px`; previewTime.textContent = fmtT(targetTime); previewBox.classList.add('show'); const myId = ++currentReqId; try { await sleep(50); if (myId !== currentReqId) return; const url = await generate(targetTime); if (myId !== currentReqId) return; const img = document.createElement('img'); img.src = url; const onImgReady = () => { if (myId !== currentReqId) return; if (img.naturalWidth && img.naturalHeight) { const ratio = img.naturalWidth / img.naturalHeight; imgBox.style.width = `${DISPLAY_HEIGHT * ratio}px`; imgBox.style.height = `${DISPLAY_HEIGHT}px`; } imgBox.style.display = 'flex'; img.classList.add('active'); imgBox.appendChild(img); const oldImages = imgBox.querySelectorAll('img'); if (oldImages.length > 1) { setTimeout(() => { for (let i = 0; i < oldImages.length - 1; i++) { oldImages[i].remove(); } }, 150); } }; if (img.complete) onImgReady(); else img.onload = onImgReady; } catch (e) { } }; const hide = () => { previewBox.classList.remove('show'); imgBox.style.display = 'none'; currentReqId++; setTimeout(() => { if (!previewBox.classList.contains('show')) { const imgs = imgBox.querySelectorAll('img'); imgs.forEach(i => i.remove()); } }, 500); }; const resetSource = (newUrl) => { if (shadowV) shadowV.src = newUrl; }; return { show, hide, resetSource }; })(); const updateState = () => { if (v.paused) { box.classList.add('paused'); btnPlay.innerHTML = mkSvg(icons.play); box.classList.remove('ui-hidden'); if (hideTimer) clearTimeout(hideTimer); } else { box.classList.remove('paused'); btnPlay.innerHTML = mkSvg(icons.pause); resetHideTimer(); } }; const updateVolUI = () => { slideVol.value = v.muted ? 0 : v.volume; btnVol.innerHTML = mkSvg((v.muted || v.volume === 0) ? icons.mute : icons.vol); }; let transcodeTimer = null; const destroyPlayer = () => { if (isPlayerDestroyed) return; isPlayerDestroyed = true; document.removeEventListener('keydown', playerKeyHandler); if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); } window.removeEventListener('resize', onResizeTransform); if (transcodeTimer) { clearInterval(transcodeTimer); transcodeTimer = null; } if (v.duration > 0 && v.currentTime > 5 && v.duration - v.currentTime > 5) { gmSet('pk_progress_' + getPhysicalId(item), { t: v.currentTime, d: v.duration, ts: Date.now() }); } v.pause(); v.muted = true; if (pkHls) { pkHls.stopLoad(); pkHls.detachMedia(); pkHls.destroy(); pkHls = null; } const targetId = item.id; const targetIdx = S.display.findIndex(x => x.id === targetId); if (targetIdx !== -1) { S.sel.clear(); S.sel.add(targetId); S.activeId = targetId; const rowTop = targetIdx * CONF.rowHeight; const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); } v.src = ""; v.load(); if (styleEl) styleEl.remove(); d.remove(); }; v.addEventListener('play', updateState); v.addEventListener('pause', updateState); const markStarted = () => { if (isPlayerDestroyed) return; if (box) { box.classList.add('pk-v-started'); stopSpinner(); updateState(); lastWorkingLink = currentLink; lastWorkingResName = currentResName; } }; v.addEventListener('playing', markStarted); v.addEventListener('seeked', () => { if(v.currentTime > 0.1) markStarted(); }); v.addEventListener('click', () => { markStarted(); togglePlay(); }); v.addEventListener('dblclick', (e) => { e.stopPropagation(); btnFull.click(); }); const posterEl = d.querySelector('#pk_p_poster'); const loaderEl = d.querySelector('.pk-p-loading'); let shutterTargetTime = 0; let isPiPDesired = false; const stopSpinner = (force = false) => { const isErrorVisible = !!box.querySelector('.pk-err-dialog'); if ((shutterTargetTime > 0 || isErrorVisible) && !force) return; box.classList.remove('buffering'); if (v.paused && v.readyState >= 2) { box.classList.add('pk-v-started'); updateState(); } if (loaderEl) loaderEl.style.display = 'none'; if (posterEl) { posterEl.style.pointerEvents = 'none'; posterEl.style.opacity = '0'; setTimeout(() => { if(posterEl.style.opacity === '0') { posterEl.style.display = 'none'; posterEl.style.pointerEvents = 'auto'; } }, 450); } }; const showSpinner = () => { box.classList.add('buffering'); if (loaderEl) loaderEl.style.display = 'block'; }; v.addEventListener('waiting', showSpinner); v.addEventListener('stalled', showSpinner); v.addEventListener('playing', stopSpinner); v.addEventListener('seeked', stopSpinner); v.addEventListener('canplaythrough', stopSpinner); let lastTextUpdate = 0; const updateTimeUI = () => { requestAnimationFrame(() => { if (isDragSeek) return; if (v.currentTime > 0.1) stopSpinner(); const dur = v.duration; const cur = v.currentTime; const now = performance.now(); if (dur > 0) { const pct = (cur / dur) * 100; progFilled.style.width = `${pct}%`; } if (now - lastTextUpdate > 250) { tCur.textContent = fmtT(cur); if (dur > 0 && isFinite(dur)) tDur.textContent = fmtT(dur); lastTextUpdate = now; } }); }; v.addEventListener('timeupdate', updateTimeUI); v.addEventListener('durationchange', updateTimeUI); v.addEventListener('timeupdate', () => { if (shutterTargetTime > 0) { if (v.currentTime >= shutterTargetTime - 0.5 && v.readyState >= 3) { v._isRestarting = false; console.log(`[Shutter] Target reached: ${v.currentTime}, Unlocking...`); shutterTargetTime = 0; stopSpinner(true); } } }); let hasCheckedProgress = false; let lastSaveTime = 0; const applyProgress = () => { if (hasCheckedProgress || v.duration <= 0) return; hasCheckedProgress = true; triggerResume(v, item); }; v.addEventListener('canplay', applyProgress, { once: true }); v.addEventListener('playing', () => { const now = Date.now(); const pId = getPhysicalId(item); const existing = gmGet('pk_progress_' + pId); let data = { t: v.currentTime, d: v.duration || 0, ts: now }; if (existing && typeof existing === 'object') { data.t = existing.t; if (!data.d) data.d = existing.d; } gmSet('pk_progress_' + pId, data); }); v.addEventListener('timeupdate', () => { const now = Date.now(); if (now - lastSaveTime > 3000) { const curT = v.currentTime; const totalT = v.duration || 0; const progressData = { t: curT, d: totalT, ts: now }; const pId = getPhysicalId(item); if (totalT > 0 && (totalT - curT < 5)) { progressData.t = 0; gmSet('pk_progress_' + pId, progressData); } else if (curT > 1) { gmSet('pk_progress_' + pId, progressData); } lastSaveTime = now; } }); v.addEventListener('loadedmetadata', () => { triggerResume(v, item); updateTimeUI(); const dur = v.duration; if (dur > 0 && isFinite(dur)) { const seconds = Math.round(dur); const pId = getPhysicalId(item); gmSet('pk_duration_' + pId, seconds); S.durationMap.set(pId, seconds); if (item.params) item.params.duration = seconds; if (typeof renderVisible === 'function') renderVisible(); } }); const enableMediaControls = () => { if (v.readyState >= 2) { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); const btnPip = d.querySelector('#pk_p_pip'); if (btnPip && document.pictureInPictureEnabled && !v.disablePictureInPicture) { btnPip.style.setProperty('display', 'flex', 'important'); if (isPiPDesired && !document.pictureInPictureElement) { v.requestPictureInPicture().catch(() => { isPiPDesired = false; }); } } } }; v.addEventListener('loadeddata', enableMediaControls); v.addEventListener('canplay', enableMediaControls); if (v.readyState >= 2) enableMediaControls(); let isDragSeek = false; let hasUserSeeked = false; let initialTimeBeforeDrag = 0; let progRectCache = null; let lastSeekRequestTime = 0; let lastMouseX = 0; const seekIndicator = d.querySelector('#pk_p_seek_indicator'); const updateVisualOnly = (clientX) => { if (!progRectCache || !v.duration) return 0; const pos = Math.max(0, Math.min(1, (clientX - progRectCache.left) / progRectCache.width)); const targetTime = pos * v.duration; progFilled.style.setProperty('width', `${pos * 100}%`, 'important'); seekIndicator.textContent = `${fmtFullT(targetTime)} / ${fmtFullT(v.duration)}`; if (tCur) tCur.textContent = fmtT(targetTime); return targetTime; }; const updateVideoOnDemand = (targetTime) => { const now = performance.now(); if (v.paused && (now - lastSeekRequestTime > 40)) { v.currentTime = targetTime; lastSeekRequestTime = now; } }; const stopDragging = (isCancel = false) => { if (!isDragSeek) return; if (isCancel) { v.currentTime = initialTimeBeforeDrag; const revertPos = (initialTimeBeforeDrag / v.duration) * 100; progFilled.style.setProperty('width', `${revertPos}%`, 'important'); } else { const finalT = updateVisualOnly(lastMouseX); v.currentTime = finalT; if (finalT > 5) { gmSet('pk_progress_' + getPhysicalId(item), { t: finalT, d: v.duration, ts: Date.now() }); } } isDragSeek = false; progRectCache = null; document.body.classList.remove('pk-dragging'); box.classList.remove('pk-is-seeking'); if (typeof ThumbnailEngine !== 'undefined') { if (progArea && !progArea.matches(':hover')) { ThumbnailEngine.hide(); } } const thumb = box.querySelector('.pk-player-progress-thumb'); if (thumb) { thumb.classList.remove('pk-look-r', 'pk-look-l'); thumb.classList.add('pk-blink-anim', 'pk-blink-hold'); setTimeout(() => thumb.classList.remove('pk-blink-anim'), 200); setTimeout(() => thumb.classList.remove('pk-blink-hold'), 300); } seekIndicator.style.display = 'none'; updateState(); }; const onMouseMove = (e) => { if (!isDragSeek) return; const topBar = d.querySelector('.pk-player-top'); if (topBar) { const barRect = topBar.getBoundingClientRect(); if (e.clientY >= barRect.top && e.clientY <= barRect.bottom) { stopDragging(true); return; } } const thumb = box.querySelector('.pk-player-progress-thumb'); if (thumb) { if (e.clientX > lastMouseX + 1) { thumb.classList.add('pk-look-r'); thumb.classList.remove('pk-look-l'); } else if (e.clientX < lastMouseX - 1) { thumb.classList.add('pk-look-l'); thumb.classList.remove('pk-look-r'); } } lastMouseX = e.clientX; const targetT = updateVisualOnly(e.clientX); updateVideoOnDemand(targetT); if (typeof ThumbnailEngine !== 'undefined' && progRectCache) { ThumbnailEngine.show(e.clientX, progRectCache); } }; progArea.addEventListener('mousedown', (e) => { if (!v.duration || isNaN(v.duration)) return; if (typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.hide(); isDragSeek = true; hasUserSeeked = true; lastMouseX = e.clientX; initialTimeBeforeDrag = v.currentTime; progRectCache = progArea.getBoundingClientRect(); document.body.classList.add('pk-dragging'); box.classList.add('pk-is-seeking'); seekIndicator.style.display = 'flex'; const targetT = updateVisualOnly(e.clientX); v.currentTime = targetT; e.preventDefault(); }); progArea.addEventListener('mousemove', (e) => { const rect = progArea.getBoundingClientRect(); ThumbnailEngine.show(e.clientX, rect); }); progArea.addEventListener('mouseleave', () => { ThumbnailEngine.hide(); }); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', () => stopDragging(false)); box.addEventListener('mouseleave', (e) => { if (isDragSeek) { const rect = box.getBoundingClientRect(); if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) { stopDragging(true); } } }); btnPlay.onclick = togglePlay; btnClose.onclick = destroyPlayer; btnFull.onclick = () => { if (!document.fullscreenElement) { box.requestFullscreen(); btnFull.innerHTML = mkSvg(icons.exitFull); } else { document.exitFullscreen(); btnFull.innerHTML = mkSvg(icons.full); } }; const subPanel = d.querySelector('#pk_sub_panel'); if (subPanel) { btnFull.addEventListener('mouseenter', () => subPanel.style.setProperty('display', 'none', 'important')); btnFull.addEventListener('mouseleave', () => subPanel.style.removeProperty('display')); } const btnPip = d.querySelector('#pk_p_pip'); if (btnPip) { if (!document.pictureInPictureEnabled || v.disablePictureInPicture) { btnPip.style.display = 'none'; } else { btnPip.onclick = async (e) => { e.stopPropagation(); try { if (document.pictureInPictureElement) { isPiPDesired = false; await document.exitPictureInPicture(); } else { isPiPDesired = true; await v.requestPictureInPicture(); window.focus(); } } catch (err) { console.error("PiP Error:", err); } }; } } btnSearch.onclick = (e) => { e.stopPropagation(); if (v) { v.pause(); setTimeout(() => { if (v) v.pause(); }, 100); } const posterImg = d.querySelector('#pk_p_poster img'); if (v.readyState >= 2 && v.videoWidth > 0) { startImageSearch(v, item.name, d, null); } else if (posterImg && posterImg.style.display !== 'none' && posterImg.src) { startImageSearch(posterImg, item.name, d, posterImg.src); } else { startImageSearch(v, item.name, d, null); } }; const initPosterImg = d.querySelector('#pk_p_poster img'); if (initPosterImg) { if (initPosterImg.complete && initPosterImg.src && initPosterImg.src.startsWith('http')) { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); } else { initPosterImg.addEventListener('load', () => { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); }); } } const savedMute = gmGet('pk_vol_muted', false); const savedVol = parseFloat(gmGet('pk_vol_level', 1.0)); v.muted = savedMute; v.volume = (Number.isFinite(savedVol) && savedVol >= 0 && savedVol <= 1) ? savedVol : 1.0; if (slideVol) slideVol.value = v.volume; updateVolUI(); btnVol.onclick = () => { v.muted = !v.muted; updateVolUI(); gmSet('pk_vol_muted', v.muted); }; slideVol.oninput = (e) => { v.muted = false; const val = parseFloat(e.target.value); v.volume = val; updateVolUI(); gmSet('pk_vol_muted', false); gmSet('pk_vol_level', val); }; spdList.querySelectorAll('.pk-p-item').forEach(item => { item.onclick = () => { const s = parseFloat(item.dataset.spd); v.playbackRate = s; spdList.querySelector('.active').classList.remove('active'); item.classList.add('active'); d.querySelector('#pk_p_spd_txt').textContent = item.textContent; }; }); function bindResEvents() { resList.querySelectorAll('.pk-p-item').forEach(item => { item.onclick = () => { const link = item.dataset.link; if(link === currentLink) return; const curT = v.currentTime; const isPaused = v.paused; const curRate = v.playbackRate; box.classList.add('buffering'); if (loaderEl) loaderEl.style.display = 'block'; if (posterEl) { if (v.readyState >= 2 && v.currentTime > 0) { try { const canvas = document.createElement('canvas'); canvas.width = v.videoWidth; canvas.height = v.videoHeight; canvas.getContext('2d').drawImage(v, 0, 0); const posterImg = posterEl.querySelector('img'); if (posterImg) { posterImg.src = canvas.toDataURL('image/jpeg', 0.7); posterImg.style.filter = 'brightness(0.8)'; } } catch (e) { console.warn("[ClaritySwitch] Frame capture failed."); } } posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; posterEl.style.pointerEvents = 'auto'; } currentLink = link; currentResName = item.textContent.trim(); shutterTargetTime = curT > 0.1 ? curT : 0; v.pause(); loadSource(link, curT); v.playbackRate = curRate; if(!isPaused) v.play().catch(()=>{}); resList.innerHTML = renderQualityMenu(qualityList, currentResName); if(resTxt) resTxt.textContent = currentResName; bindResEvents(); }; }); } bindResEvents(); const subState = { hasSub: false, size: 24, pos: 10, offset: 0, bgOpacity: 0.6, track: null, blobUrl: null }; const processSubtitleFile = (file) => { if (!file) return; const ext = file.name.split('.').pop().toLowerCase(); if (!['srt', 'vtt', 'ass', 'ssa'].includes(ext)) { showToast(L.err_sub_drop_type); return; } const reader = new FileReader(); reader.onload = (evt) => { const buffer = evt.target.result; const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis']; let text = ""; for (let enc of encodings) { try { const decoder = new TextDecoder(enc, { fatal: true }); text = decoder.decode(buffer); break; } catch (e) { if(enc === 'shift_jis') text = new TextDecoder('utf-8').decode(buffer); } } if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl); let vttText = ""; if (ext === 'ass' || ext === 'ssa') { vttText = convertAssToVtt(text); } else { vttText = convertSrtToVtt(text); } const blob = new Blob([vttText], { type: 'text/vtt' }); subState.blobUrl = URL.createObjectURL(blob); let targetTrack = null; if (v.textTracks) { for (let i = 0; i < v.textTracks.length; i++) { const t = v.textTracks[i]; if (t.label === 'pk-subs') targetTrack = t; t.mode = 'disabled'; } } if (!targetTrack) targetTrack = v.addTextTrack("subtitles", "pk-subs", "en"); targetTrack.oncuechange = () => { const cues = targetTrack.activeCues; const txtEl = d.querySelector('#pk_sub_text'); if (cues && cues.length > 0 && txtEl) { const text = cues[cues.length - 1].text; txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
'); txtEl.style.display = 'block'; } else if (txtEl) { txtEl.style.display = 'none'; } }; const vttLines = vttText.split('\n'); let cueStart = null, cueEnd = null, cueText = []; const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/; while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]); vttLines.forEach(line => { const match = line.match(timeReg); if (match) { if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000; cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000; cueText = []; } else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) { if (cueStart !== null) cueText.push(line.trim()); } }); if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } subState.hasSub = true; subState.track = targetTrack; subState.track.mode = 'hidden'; updateSubStyle(); d.querySelector('#pk_sub_toggle').checked = true; d.querySelector('#pk_sub_name').textContent = file.name; updateSubStyle(); console.log(L.msg_sub_drop_load.replace('{n}', file.name)); }; reader.readAsArrayBuffer(file); }; box.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); box.style.boxShadow = "inset 0 0 50px var(--pk-pri)"; }); box.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); box.style.boxShadow = "0 25px 50px rgba(0,0,0,0.5)"; }); box.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); box.style.boxShadow = "0 25px 50px rgba(0,0,0,0.5)"; const file = e.dataTransfer.files[0]; processSubtitleFile(file); }); const subStyleTag = document.createElement('style'); document.head.appendChild(subStyleTag); const updateSubStyle = () => { const txt = d.querySelector('#pk_sub_text'); const layer = d.querySelector('#pk_sub_render_layer'); const toggle = d.querySelector('#pk_sub_toggle'); if (txt && layer) { txt.style.fontSize = `${subState.size}px`; txt.style.backgroundColor = `rgba(0,0,0,${subState.bgOpacity})`; layer.style.paddingBottom = `${subState.pos / 2}%`; const isShow = toggle ? toggle.checked : true; layer.style.display = isShow ? 'flex' : 'none'; } if (subState.track) { subState.track.mode = 'hidden'; } }; const fileInp = d.querySelector('#pk_sub_file'); d.querySelector('#pk_sub_local_btn').onclick = (e) => { e.stopPropagation(); fileInp.click(); }; fileInp.onchange = (e) => { processSubtitleFile(e.target.files[0]); }; const cleanSubText = (text) => { return text.replace(/\{[^}]*?\}/g, '') .replace(/<\/?i>/g, '') .replace(/\\N/gi, '\n') .replace(/\r\n/g, '\n') .trim(); }; const convertAssToVtt = (assText) => { let vtt = "WEBVTT\n\n"; const lines = assText.split('\n'); let inEvents = false; let count = 1; const fmtTime = (t) => { if (!t) return "00:00:00.000"; const parts = t.trim().split('.'); const hms = parts[0].split(':'); const ms = (parts[1] || '00').padEnd(3, '0'); return `${hms[0].padStart(2,'0')}:${hms[1]}:${hms[2]}.${ms}`; }; for (let line of lines) { line = line.trim(); if (line.startsWith('[Events]')) { inEvents = true; continue; } if (!inEvents || !line.startsWith('Dialogue:')) continue; const parts = line.split(','); if (parts.length < 10) continue; const start = fmtTime(parts[1]); const end = fmtTime(parts[2]); const rawText = parts.slice(9).join(','); const text = cleanSubText(rawText); if (text) { vtt += `${count++}\n${start} --> ${end}\n${text}\n\n`; } } return vtt; }; const convertSrtToVtt = (srtText) => { if (/^WEBVTT/i.test(srtText)) return srtText; let vtt = "WEBVTT\n\n"; const text = srtText.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const regex = /(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})/g; let match; const cues = []; while ((match = regex.exec(text)) !== null) { cues.push({ start: match[1], end: match[2], index: match.index, endOfLine: regex.lastIndex }); } let count = 1; for (let i = 0; i < cues.length; i++) { const cue = cues[i]; const nextCue = cues[i + 1]; const contentStart = cue.endOfLine; const contentEnd = nextCue ? nextCue.index : text.length; let rawContent = text.substring(contentStart, contentEnd); rawContent = rawContent.replace(/\n+\s*\d+\s*$/, ''); const cleanContent = cleanSubText(rawContent); const vttStart = cue.start.replace(/,/g, '.'); const vttEnd = cue.end.replace(/,/g, '.'); if (cleanContent) { vtt += `${count++}\n${vttStart} --> ${vttEnd}\n${cleanContent}\n\n`; } } return vtt; }; const autoMatchSubtitle = async (videoItem) => { const parentId = videoItem.parent_id; if (!parentId) return; const videoNameBase = videoItem.name.substring(0, videoItem.name.lastIndexOf('.')); let files = []; if (typeof globalCache !== 'undefined' && globalCache.has(parentId)) { files = globalCache.get(parentId); } else { try { files = await apiList(parentId, 100); } catch(e) { return; } } if (!files || files.length === 0) return; const subFiles = files.filter(f => !f.trashed && f.kind !== 'drive#folder' && /\.(srt|vtt|ass|ssa)$/i.test(f.name) ); if (subFiles.length === 0) return; let targetSub = subFiles.find(f => { const subBase = f.name.substring(0, f.name.lastIndexOf('.')); return subBase === videoNameBase; }); if (!targetSub) { targetSub = subFiles.find(f => f.name.includes(videoNameBase)); } if (targetSub) { console.log(`[AutoSub] Matched: ${targetSub.name}`); const box = d.querySelector('#pk_p_box'); const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:80px;right:20px;background:rgba(0,0,0,0.6);color:#fff;padding:6px 12px;border-radius:4px;font-size:12px;pointer-events:none;animation:pkFadeIn 0.5s;z-index:90;"; toast.textContent = L.msg_auto_sub_load.replace('{n}', targetSub.name); box.appendChild(toast); setTimeout(()=>toast.remove(), 4000); try { let link = targetSub.web_content_link; if (!link) { const detail = await apiGet(targetSub.id); link = detail.web_content_link; } const res = await fetch(link); const buffer = await res.arrayBuffer(); const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis']; let text = ""; for (let enc of encodings) { try { const decoder = new TextDecoder(enc, { fatal: true }); text = decoder.decode(buffer); break; } catch (e) { if(enc === 'shift_jis') text = new TextDecoder('utf-8').decode(buffer); } } let vttText = ""; const subExt = targetSub.name.split('.').pop().toLowerCase(); if (subExt === 'ass' || subExt === 'ssa') { vttText = convertAssToVtt(text); } else { vttText = convertSrtToVtt(text); } const blob = new Blob([vttText], { type: 'text/vtt' }); if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl); subState.blobUrl = URL.createObjectURL(blob); let targetTrack = null; if (v.textTracks) { for (let i = 0; i < v.textTracks.length; i++) { const t = v.textTracks[i]; if (t.label === 'pk-subs') targetTrack = t; t.mode = 'disabled'; } } if (!targetTrack) targetTrack = v.addTextTrack("subtitles", "pk-subs", "en"); targetTrack.oncuechange = () => { const cues = targetTrack.activeCues; const txtEl = d.querySelector('#pk_sub_text'); if (cues && cues.length > 0 && txtEl) { const text = cues[cues.length - 1].text; txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
'); txtEl.style.display = 'block'; } else if (txtEl) { txtEl.style.display = 'none'; } }; const vttLines = vttText.split('\n'); let cueStart = null, cueEnd = null, cueText = []; const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/; while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]); vttLines.forEach(line => { const match = line.match(timeReg); if (match) { if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000; cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000; cueText = []; } else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) { if (cueStart !== null) cueText.push(line.trim()); } }); if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } subState.hasSub = true; subState.track = targetTrack; subState.track.mode = 'hidden'; updateSubStyle(); const toggle = d.querySelector('#pk_sub_toggle'); if(toggle) toggle.checked = true; const nameLabel = d.querySelector('#pk_sub_name'); if(nameLabel) nameLabel.textContent = targetSub.name; updateSubStyle(); } catch (e) { console.warn("[AutoSub] Load failed", e); } } }; const btnCloudSub = d.querySelector('#pk_sub_cloud_btn'); btnCloudSub.onclick = async (e) => { e.stopPropagation(); const subPanel = d.querySelector('#pk_sub_panel'); const subTrigger = d.querySelector('#pk_sub_trigger'); if (subPanel && subTrigger) { subPanel.style.setProperty('display', 'none', 'important'); const clearDisplay = () => { subPanel.style.removeProperty('display'); subTrigger.removeEventListener('mouseleave', clearDisplay); }; subTrigger.addEventListener('mouseleave', clearDisplay); } if (v && !v.paused) v.pause(); const originalText = btnCloudSub.textContent; btnCloudSub.textContent = L.loading; btnCloudSub.style.opacity = "0.6"; btnCloudSub.style.pointerEvents = "none"; let startPath = [{ id: '', name: L.btn_nav_home }]; let targetFolderId = ''; try { let targetItem = item; if (S.offlineMode || S.uploadMode || S.recentMode || item.kind === 'drive#task') { const realFileId = (item.kind === 'drive#task' || S.offlineMode || S.uploadMode) ? (item.file_id || (item.params && item.params.file_id)) : item.id; if (realFileId) { try { targetItem = await apiGet(realFileId); } catch(err) { console.warn("[CloudSub] Failed to resolve task file:", err); } } } if (targetItem.parent_id && targetItem.parent_id !== 'root') { targetFolderId = targetItem.parent_id; } if (targetItem._lineage && Array.isArray(targetItem._lineage) && targetItem._lineage.length > 0) { startPath = targetItem._lineage.map(x => ({ id: x.id || '', name: x.name })); if (startPath.length > 0 && startPath[0].id !== '' && startPath[0].id !== 'root') { startPath.unshift({ id: '', name: L.btn_nav_home }); } } else if (targetFolderId) { const trace = []; let curr = targetFolderId; let safety = 6; while (curr && curr !== 'root' && safety > 0) { try { const f = await apiGet(curr); trace.unshift({ id: f.id, name: f.name }); curr = f.parent_id; } catch(e) { break; } safety--; } if (trace.length > 0) { startPath = [{ id: '', name: L.btn_nav_home }, ...trace]; } } else if (S.path && S.path.length > 0) { const cleanPath = S.path.filter(p => !p.id.startsWith('virtual_') && !p.id.includes('_root') && p.id !== 'analyze_root'); if (cleanPath.length > 0) { startPath = cleanPath; if (startPath[0].id !== '' && startPath[0].id !== 'root') { startPath.unshift({ id: '', name: L.btn_nav_home }); } } } } catch (error) { console.error("[CloudSub] Path resolve error:", error); targetFolderId = ''; startPath = [{ id: '', name: L.btn_nav_home }]; } finally { btnCloudSub.textContent = originalText; btnCloudSub.style.opacity = "1"; btnCloudSub.style.pointerEvents = "auto"; } showFolderSelector( targetFolderId, async (id, name, subItem) => { const box = d.querySelector('#pk_p_box'); const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:80px;right:20px;background:rgba(0,0,0,0.8);color:#fff;padding:8px 16px;border-radius:4px;font-size:13px;pointer-events:none;z-index:90;display:flex;align-items:center;gap:8px;"; toast.innerHTML = `
${L.msg_dl_sub}`; box.appendChild(toast); try { let link = subItem.web_content_link; if (!link) { const detail = await apiGet(subItem.id); link = detail.web_content_link; } const res = await fetch(link); const buffer = await res.arrayBuffer(); const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis', 'windows-1252']; let text = ""; for (let enc of encodings) { try { const decoder = new TextDecoder(enc, { fatal: true }); text = decoder.decode(buffer); break; } catch (e) { if(enc === 'windows-1252') text = new TextDecoder('utf-8').decode(buffer); } } let vttText = ""; const subExt = subItem.name.split('.').pop().toLowerCase(); if (subExt === 'ass' || subExt === 'ssa') { vttText = convertAssToVtt(text); } else { vttText = convertSrtToVtt(text); } const blob = new Blob([vttText], { type: 'text/vtt' }); if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl); subState.blobUrl = URL.createObjectURL(blob); let targetTrack = null; if (v.textTracks) { for (let i = 0; i < v.textTracks.length; i++) { const t = v.textTracks[i]; if (t.label === 'pk-subs') targetTrack = t; t.mode = 'disabled'; } } if (!targetTrack) targetTrack = v.addTextTrack("subtitles", "pk-subs", "en"); targetTrack.oncuechange = () => { const cues = targetTrack.activeCues; const txtEl = d.querySelector('#pk_sub_text'); if (cues && cues.length > 0 && txtEl) { const text = cues[cues.length - 1].text; txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
'); txtEl.style.display = 'block'; } else if (txtEl) { txtEl.style.display = 'none'; } }; const vttLines = vttText.split('\n'); let cueStart = null, cueEnd = null, cueText = []; const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/; while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]); vttLines.forEach(line => { const match = line.match(timeReg); if (match) { if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000; cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000; cueText = []; } else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) { if (cueStart !== null) cueText.push(line.trim()); } }); if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } subState.hasSub = true; subState.track = targetTrack; subState.track.mode = 'hidden'; updateSubStyle(); const toggle = d.querySelector('#pk_sub_toggle'); if(toggle) toggle.checked = true; const nameLabel = d.querySelector('#pk_sub_name'); if(nameLabel) nameLabel.textContent = subItem.name; updateSubStyle(); toast.innerHTML = `✅ ${L.msg_auto_sub_load.replace('{n}', subItem.name)}`; setTimeout(() => toast.remove(), 3000); } catch (err) { console.error(err); toast.innerHTML = `❌ ${L.err_sub_dl_fail}`; setTimeout(() => toast.remove(), 4000); } }, startPath, (f) => /\.(srt|vtt|ass|ssa)$/i.test(f.name), L.title_sel_sub ); }; const loadJSZip = () => { if (window.JSZip) return Promise.resolve(window.JSZip); return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'; script.onload = () => resolve(window.JSZip); script.onerror = () => reject(new Error(L.msg_jszip_fail)); document.head.appendChild(script); }); }; const cleanFilename = (name) => { let n = name.toLowerCase(); n = n.replace(/\.[^/.]+$/, ""); n = n.replace(/^(\d{2,4}|[a-z]\d{2,4})[\.\s\-\_]+/, ""); const garbage = [ /\b(1080p|720p|2160p|4k|uhd|hd)\b.*/, /\b(bluray|web-dl|webrip|remux|hdtv)\b.*/, /\b(x264|x265|hevc|h264|aac|dts|ac3)\b.*/, /\[.*?\]/g, /\(.*?\)/g, /\{.*?\}/g ]; garbage.forEach(g => n = n.replace(g, '')); n = n.replace(/[\._\+]/g, ' ').trim(); const episodeMatch = n.match(/(.*?)s\d+e\d+/); if (episodeMatch) return episodeMatch[0]; return n; }; const gmxRequest = (url, type='text') => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, responseType: type, anonymous: false, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Cache-Control": "no-cache", "Pragma": "no-cache" }, onload: (res) => { if (res.status === 200) { resolve(res.response); } else if (res.status === 404) { resolve(null); } else { console.warn(`[Subtitle] HTTP ${res.status} from ${url}`); resolve(null); } }, onerror: (e) => reject(new Error(L.err_req_blocked)), ontimeout: () => reject(new Error(L.err_req_timeout)) }); }); }; const btnSearchSub = d.querySelector('#pk_sub_search_btn'); btnSearchSub.onclick = async (e) => { e.stopPropagation(); const subPanel = d.querySelector('#pk_sub_panel'); const subTrigger = d.querySelector('#pk_sub_trigger'); if (subPanel && subTrigger) { subPanel.style.setProperty('display', 'none', 'important'); const clearDisplay = () => { subPanel.style.removeProperty('display'); subTrigger.removeEventListener('mouseleave', clearDisplay); }; subTrigger.addEventListener('mouseleave', clearDisplay); } const searchOv = document.createElement('div'); searchOv.className = 'pk-sub-search-modal'; searchOv.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(20,20,20,0.95); border: 1px solid #444; border-radius: 8px; width: 380px; height: 450px; display: flex; flex-direction: column; box-shadow: 0 10px 40px rgba(0,0,0,0.8); z-index: 60; padding: 15px; backdrop-filter: blur(10px); `; box.appendChild(searchOv); searchOv.onclick = (evt) => evt.stopPropagation(); const keyword = cleanFilename(item.name); searchOv.innerHTML = `
`; const resultList = searchOv.querySelector('#pk_sub_search_list'); const input = searchOv.querySelector('#pk_sub_search_input'); const doSearch = (query) => { const cleanQ = query.trim(); if (!cleanQ) return; const plusQ = encodeURIComponent(cleanQ).replace(/%20/g, '+'); const spaceQ = encodeURIComponent(cleanQ); const curLang = L.lang_code; let engines = []; if (curLang === 'zh') { engines = [ { name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` }, { name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` }, { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/zh/search2/sublanguageid-kor/moviename-${plusQ}` }, ]; } else if (curLang === 'tc') { engines = [ { name: 'R3Sub', url: `https://r3sub.com/search.php?s=${plusQ}` }, { name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` }, { name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` } ]; } else if (curLang === 'ko') { engines = [ { name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` }, { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ko/search2/sublanguageid-kor/moviename-${plusQ}` }, { name: 'SubtitleCat', url: `https://www.subtitlecat.com/index.php?search=${plusQ}` } ]; } else if (curLang === 'ja') { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ja/search2/sublanguageid-jpn/moviename-${plusQ}` }, { name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` }, { name: 'Anime Tosho', url: `https://animetosho.org/search?q=${plusQ}` } ]; } else { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/en/search2/sublanguageid-eng/moviename-${plusQ}` }, { name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` }, { name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` } ]; } let html = `
`; engines.forEach(e => { html += ` ${L.btn_go_search.replace('{n}', e.name)} `; }); html += `
${L.tip_manual_sub}
`; resultList.innerHTML = html; }; try { await loadJSZip(); doSearch(input.value); } catch (err) { resultList.innerHTML = `
${L.msg_jszip_fail}
`; } input.oninput = () => doSearch(input.value); input.onkeydown = (ev) => { ev.stopPropagation(); }; searchOv.querySelector('#pk_sub_search_close').onclick = () => searchOv.remove(); }; d.querySelector('#pk_sub_toggle').onchange = (e) => { updateSubStyle(); if (v.textTracks) { Array.from(v.textTracks).forEach(t => { if (t !== subState.track) t.mode = 'disabled'; }); } }; if (v.textTracks) { v.textTracks.addEventListener('addtrack', (e) => { if (subState.hasSub && e.track !== subState.track) { e.track.mode = 'disabled'; } }); } const sizeVal = d.querySelector('#pk_sub_size_val'); d.querySelector('#pk_sub_size_dec').onclick = (e) => { e.stopPropagation(); subState.size = Math.max(12, subState.size - 2); sizeVal.textContent = subState.size; updateSubStyle(); }; d.querySelector('#pk_sub_size_inc').onclick = (e) => { e.stopPropagation(); subState.size = Math.min(80, subState.size + 2); sizeVal.textContent = subState.size; updateSubStyle(); }; d.querySelector('#pk_sub_pos').oninput = (e) => { e.stopPropagation(); subState.pos = parseInt(e.target.value); updateSubStyle(); }; d.querySelector('#pk_sub_bg_opacity').oninput = (e) => { e.stopPropagation(); subState.bgOpacity = parseInt(e.target.value) / 100; updateSubStyle(); }; const timeVal = d.querySelector('#pk_sub_time_val'); const adjustOffset = (delta) => { subState.offset += delta; timeVal.textContent = subState.offset.toFixed(1) + " " + L.unit_sec; if (subState.track && subState.track.cues) { const cues = Array.from(subState.track.cues); cues.forEach(cue => { cue.startTime += delta; cue.endTime += delta; }); } }; d.querySelector('#pk_sub_time_dec').onclick = (e) => { e.stopPropagation(); adjustOffset(-0.5); }; d.querySelector('#pk_sub_time_inc').onclick = (e) => { e.stopPropagation(); adjustOffset(0.5); }; d.querySelector('#pk_sub_panel').onclick = (e) => e.stopPropagation(); d.querySelectorAll('.pk-sub-tab').forEach(t => { t.onclick = () => { d.querySelectorAll('.pk-sub-tab, .pk-sub-pane').forEach(el => el.classList.remove('active')); t.classList.add('active'); d.querySelector('#' + t.dataset.target).classList.add('active'); }; }); const curPMode = gmGet('pk_play_mode', 'stop'); const pRadios = d.querySelectorAll('input[name="pk_pmode"]'); pRadios.forEach(r => { if (r.value === curPMode) r.checked = true; r.onchange = () => gmSet('pk_play_mode', r.value); }); let transformState = { rotate: 0, flipH: 1, flipV: 1, ratio: 'default' }; const applyTransform = () => { if (document.pictureInPictureElement === v) { v.style.transform = 'none'; return; } if (transformState.ratio === 'default') { v.style.objectFit = 'contain'; v.style.width = '100%'; v.style.height = box.classList.contains('plist-active') ? (document.fullscreenElement ? 'calc(100% - 84px)' : '100%') : '100%'; v.style.aspectRatio = 'auto'; v.style.margin = '0'; v.style.inset = 'auto'; } else { v.style.objectFit = 'fill'; v.style.aspectRatio = transformState.ratio; v.style.width = 'auto'; v.style.height = 'auto'; v.style.maxWidth = '100%'; v.style.maxHeight = '100%'; v.style.margin = 'auto'; v.style.inset = '0'; } let autoScale = 1; if (Math.abs(transformState.rotate) % 180 !== 0) { const boxW = box.clientWidth; const boxH = box.clientHeight; const vW = v.offsetWidth || boxW; const vH = v.offsetHeight || boxH; if (vW > 0 && vH > 0) { const scaleW = boxW / vH; const scaleH = boxH / vW; autoScale = Math.min(scaleW, scaleH); } } v.style.transform = `translateZ(0) rotate(${transformState.rotate}deg) scale(${autoScale}) scale(${transformState.flipH}, ${transformState.flipV})`; }; const onResizeTransform = () => requestAnimationFrame(applyTransform); window.addEventListener('resize', onResizeTransform); d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(btn => { btn.onclick = (e) => { d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); transformState.ratio = btn.dataset.ratio; applyTransform(); }; }); d.querySelector('#pk_btn_rot_l').onclick = () => { transformState.rotate -= 90; applyTransform(); }; d.querySelector('#pk_btn_rot_r').onclick = () => { transformState.rotate += 90; applyTransform(); }; d.querySelector('#pk_btn_flip_h').onclick = () => { transformState.flipH *= -1; applyTransform(); }; d.querySelector('#pk_btn_flip_v').onclick = () => { transformState.flipV *= -1; applyTransform(); }; const opVal = d.querySelector('#pk_op_val'); const edVal = d.querySelector('#pk_ed_val'); let valOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; let valEd = parseInt(gmGet('pk_skip_outro', 0)) || 0; opVal.textContent = valOp + " " + L.unit_sec; edVal.textContent = valEd + " " + L.unit_sec; const updateSkip = (type, delta) => { if (type === 'op') { valOp = Math.max(0, valOp + delta); opVal.textContent = valOp + " " + L.unit_sec; gmSet('pk_skip_intro', valOp); } else { valEd = Math.max(0, valEd + delta); edVal.textContent = valEd + " " + L.unit_sec; gmSet('pk_skip_outro', valEd); } }; d.querySelector('#pk_op_dec').onclick = () => updateSkip('op', -5); d.querySelector('#pk_op_inc').onclick = () => updateSkip('op', 5); d.querySelector('#pk_ed_dec').onclick = () => updateSkip('ed', -5); d.querySelector('#pk_ed_inc').onclick = () => updateSkip('ed', 5); d.querySelector('#pk_op_mark').onclick = (e) => { e.stopPropagation(); const markTime = Math.max(0, Math.floor(v.currentTime)); valOp = markTime; opVal.textContent = valOp + " " + L.unit_sec; gmSet('pk_skip_intro', valOp); }; d.querySelector('#pk_ed_mark').onclick = (e) => { e.stopPropagation(); if (!v.duration) return; const markTime = Math.max(0, Math.floor(v.duration - v.currentTime)); valEd = markTime; edVal.textContent = valEd + " " + L.unit_sec; gmSet('pk_skip_outro', valEd); }; let hasTriggeredEnd = false; v.addEventListener('timeupdate', () => { const skipEd = parseInt(gmGet('pk_skip_outro', 0)) || 0; if (skipEd > 0 && v.duration > 0 && !hasTriggeredEnd) { if (v.duration - v.currentTime <= skipEd) { hasTriggeredEnd = true; console.log(`[AutoSkip] Outro skipped at ${v.currentTime}`); v.onended(); } } }); v.addEventListener('play', () => hasTriggeredEnd = false); v.addEventListener('seeking', () => hasTriggeredEnd = false); v.addEventListener('enterpictureinpicture', applyTransform); v.addEventListener('leavepictureinpicture', () => { if (typeof isSwitching !== 'undefined' && !isSwitching) { isPiPDesired = false; } applyTransform(); }); v.onended = () => { const mode = gmGet('pk_play_mode', 'stop'); if (mode === 'single_loop') { v.currentTime = 0; v.play().catch(()=>{}); } else if (mode === 'list_loop') { const nextIdx = (curListIdx + 1) % totalInList; if (totalInList > 1) softSwitch(nextIdx); else { v.currentTime = 0; v.play().catch(()=>{}); } } }; const makeEditable = (el, type, callback) => { el.ondblclick = (e) => { e.stopPropagation(); const oldText = el.textContent; const oldVal = type === 'offset' ? parseFloat(oldText) : parseInt(oldText); el.innerHTML = ``; const input = el.querySelector('input'); input.focus(); input.select(); const finish = () => { const val = parseFloat(input.value); if (isNaN(val)) { el.textContent = oldText; } else { let finalVal = type === 'offset' ? val : Math.round(val); if (type !== 'offset') finalVal = Math.max(0, finalVal); if (type === 'offset') el.textContent = finalVal.toFixed(1) + " " + L.unit_sec; else el.textContent = finalVal; callback(finalVal); } el.ondblclick = (evt) => { evt.stopPropagation(); makeEditable(el, type, callback).ondblclick(evt); }; }; input.onblur = finish; input.onkeydown = (ev) => { ev.stopPropagation(); if (ev.key === 'Enter') { input.blur(); } }; input.onclick = (ev) => ev.stopPropagation(); }; }; makeEditable(d.querySelector('#pk_sub_size_val'), 'int', (val) => { subState.size = val; updateSubStyle(); }); makeEditable(d.querySelector('#pk_sub_time_val'), 'offset', (val) => { const delta = val - subState.offset; adjustOffset(delta); }); makeEditable(d.querySelector('#pk_op_val'), 'int', (val) => { valOp = val; gmSet('pk_skip_intro', val); d.querySelector('#pk_op_val').textContent = val + " " + L.unit_sec; }); makeEditable(d.querySelector('#pk_ed_val'), 'int', (val) => { valEd = val; gmSet('pk_skip_outro', val); d.querySelector('#pk_ed_val').textContent = val + " " + L.unit_sec; }); updateSubStyle(); const playerKeyHandler = (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (!document.getElementById('pk-player-ov')) return; e.stopPropagation(); e.preventDefault(); resetHideTimer(); switch(e.key) { case ' ': case 'k': togglePlay(); break; case 'ArrowRight': if (e.ctrlKey || e.metaKey) { const nextIdx = (curListIdx + 1) % totalInList; softSwitch(nextIdx); } else { const targetTime = Math.min(v.duration, v.currentTime + 10); v.currentTime = targetTime; if (tCur) tCur.textContent = fmtT(targetTime); if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important'); } break; case 'ArrowLeft': if (e.ctrlKey || e.metaKey) { const prevIdx = (curListIdx - 1 + totalInList) % totalInList; softSwitch(prevIdx); } else { const targetTime = Math.max(0, v.currentTime - 10); v.currentTime = targetTime; if (tCur) tCur.textContent = fmtT(targetTime); if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important'); } break; case 'p': case 'P': const btnPip = d.querySelector('#pk_p_pip'); if (btnPip && btnPip.style.display !== 'none') btnPip.click(); break; case 'e': case 'E': if (pTab) pTab.click(); break; case 'ArrowUp': case 'ArrowDown': v.muted = false; const delta = (e.key === 'ArrowUp' ? 0.05 : -0.05); v.volume = Math.max(0, Math.min(1, v.volume + delta)); updateVolUI(); const volInd = d.querySelector('#pk_p_vol_indicator'); const volVal = d.querySelector('#pk_p_vol_val'); if (volInd && volVal) { volVal.textContent = Math.round(v.volume * 100) + '%'; volInd.style.display = 'flex'; box.classList.add('pk-is-vol-active'); clearTimeout(v._volTimer); v._volTimer = setTimeout(() => { volInd.style.display = 'none'; box.classList.remove('pk-is-vol-active'); }, 1000); } break; case 'f': case 'F': if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click(); break; case 'Enter': btnFull.click(); break; case 'Escape': if (document.fullscreenElement) document.exitFullscreen(); else destroyPlayer(); break; } }; document.addEventListener('keydown', playerKeyHandler); const smartLoad = async () => { if (item.phase === 'PHASE_TYPE_RUNNING' || item.phase === 'PHASE_TYPE_PENDING') { console.log("[Transcode] Video is processing, entering polling mode."); const mask = document.createElement('div'); mask.className = 'pk-transcode-mask'; mask.innerHTML = `
${L.msg_transcoding}
${L.msg_transcoding_wait}
${L.btn_force_play}
`; box.appendChild(mask); mask.querySelector('#pk_tc_force').onclick = (e) => { e.stopPropagation(); if (transcodeTimer) clearInterval(transcodeTimer); mask.remove(); loadSource(currentLink); v.play(); }; transcodeTimer = setInterval(async () => { try { const freshData = await apiGet(item.id); if (freshData.phase === 'PHASE_TYPE_COMPLETE') { clearInterval(transcodeTimer); mask.remove(); item = freshData; const best = getBestSource(freshData); currentLink = best.src; loadSource(currentLink); if (typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.resetSource(currentLink); v.play(); qualityList = best.list; currentResName = best.name; const resTxt = d.querySelector('#pk_p_res_txt'); const resList = d.querySelector('#pk_p_res_list'); if(resTxt) resTxt.textContent = currentResName; if(resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(76, 175, 80, 0.9);color:#fff;padding:8px 16px;border-radius:20px;font-size:13px;z-index:100;animation:pkFadeIn 0.5s;"; toast.textContent = L.msg_transcode_done; box.appendChild(toast); setTimeout(()=>toast.remove(), 3000); } } catch(e) { console.warn("[TranscodePoll] Error:", e); } }, 3000); } else { const initDur = (item.params && item.params.duration) || S.durationMap.get(item.id) || gmGet('pk_duration_' + item.id, 0); if (tDur && initDur > 0) tDur.textContent = fmtT(initDur); loadSource(currentLink, null); setTimeout(() => { if (isPlayerDestroyed) return; const p = v.play(); if (p !== undefined) { p.catch(() => updateState()); } if (startFullscreen) { const box = d.querySelector('#pk_p_box'); const btnFull = d.querySelector('#pk_p_full'); if (box && box.requestFullscreen) { box.requestFullscreen().then(() => { if(btnFull) btnFull.innerHTML = mkSvg(icons.exitFull); }).catch(err => console.warn("Fullscreen auto-resume failed", err)); } } }, 100); } }; smartLoad(); autoMatchSubtitle(item); } let isImageOpening = false; async function showImage(startItem) { if (S.trashMode) return; if (document.querySelector('.pk-img-ov')) return; isImageOpening = true; let lastDirection = 1; const item = startItem; const imgList = S.display.filter(i => { if (i.isHeader) return false; if (S.offlineMode && i.phase !== 'PHASE_TYPE_COMPLETE') return false; if (S.uploadMode && i.status !== 'DONE') return false; return i.mime_type && i.mime_type.startsWith('image'); }); if (imgList.length === 0) return; let curIdx = imgList.findIndex(i => i.id === startItem.id); if (curIdx === -1) curIdx = 0; const d = document.createElement('div'); d.className = 'pk-img-ov'; d.tabIndex = 0; const icons = { close: '', full: '', exitFull: '', prev: '', next: '', rotate: '', flipH: '', flipV: '', searchlens: ``, leftArr: ``, rightArr: ``, upArr: `` }; const renderImgListItems = () => { const RANGE = 150; const start = Math.max(0, curIdx - RANGE); const end = Math.min(imgList.length, curIdx + RANGE + 1); return imgList.slice(start, end).map((v, i) => { const absIdx = start + i; const imgSrc = v.thumbnail_link || v.icon_link || ''; return `
`}).join(''); }; const listFixStyle = ``; d.innerHTML = listFixStyle + `
${icons.flipV}
${icons.flipH}
${icons.rotate}
${icons.full}
${icons.close}
${curIdx + 1} / ${imgList.length}
${icons.leftArr}
${renderImgListItems()}
${icons.rightArr}
`; document.body.appendChild(d); const box = d.querySelector('#pk_img_box'); const img = d.querySelector('#pk_img_el'); const viewport = d.querySelector('#pk_img_viewport'); const title = d.querySelector('#pk_img_title'); const loader = d.querySelector('#pk_img_load'); const btnFull = d.querySelector('#pk_img_full'); const btnRot = d.querySelector('#pk_img_rot'); const btnMirror = d.querySelector('#pk_img_mirror'); const btnFlipV = d.querySelector('#pk_img_flip_v'); const btnSearch = d.querySelector('#pk_img_search'); let scale = 1, transX = 0, transY = 0, rotation = 0, flipH = 1, flipV = 1, isDrag = false, startX, startY; let isLongImageMode = false; const updateTransform = () => { if (isLongImageMode) return; img.style.transform = `translate(${transX}px, ${transY}px) scale(${scale}) rotate(${rotation}deg) scaleX(${flipH}) scaleY(${flipV})`; }; const resetView = (keepOrientation = false) => { scale = 1; transX = 0; transY = 0; if (!keepOrientation) { rotation = 0; flipH = 1; flipV = 1; } isLongImageMode = false; viewport.classList.remove('pk-long-image-mode'); viewport.classList.remove('pk-fit-mode'); if(viewport.scrollTop) viewport.scrollTop = 0; img.style.transition = 'none'; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain'; img.style.maxWidth = 'none'; img.style.cursor = 'grab'; if (btnRot) btnRot.style.display = 'flex'; if (btnMirror) btnMirror.style.display = 'flex'; if (btnFlipV) btnFlipV.style.display = 'flex'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; let imgLoadId = 0; const loadCurrent = async (scrollMode = 'smooth') => { if (typeof updateImgPlistUI === 'function') { updateImgPlistUI(scrollMode); } imgLoadId++; const myId = imgLoadId; resetView(); img.style.opacity = '0'; const currentItem = imgList[curIdx]; title.textContent = `[${curIdx + 1}/${imgList.length}] ${currentItem.name}`; btnSearch.style.display = 'none'; btnSearch.onclick = (e) => { e.stopPropagation(); const thumbUrl = currentItem.thumbnail_link ? currentItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE') : (currentItem.icon_link || ''); const thumbImg = new Image(); thumbImg.crossOrigin = 'anonymous'; thumbImg.src = thumbUrl; startImageSearch(thumbImg, currentItem.name, d, thumbUrl); }; loader.style.display = 'block'; const checkLongImage = () => { if (myId !== imgLoadId) return; btnSearch.style.display = 'flex'; const nw = img.naturalWidth; const nh = img.naturalHeight; if (nw > 0 && nh > 0) { if (nh / nw > 2.5) { isLongImageMode = true; viewport.classList.add('pk-long-image-mode'); img.style.transform = 'none'; if (btnRot) btnRot.style.display = 'none'; if (btnMirror) btnMirror.style.display = 'none'; if (btnFlipV) btnFlipV.style.display = 'none'; } } img.style.opacity = '1'; }; img.removeAttribute('crossorigin'); if (currentItem.thumbnail_link) { img.src = currentItem.thumbnail_link; const handleThumb = () => { if (myId !== imgLoadId) return; checkLongImage(); loader.style.display = 'none'; }; img.onerror = () => { if (myId !== imgLoadId) return; img.style.opacity = '0'; }; if (img.complete) handleThumb(); else img.onload = handleThumb; } else { img.removeAttribute('src'); img.style.opacity = '0'; } let targetUrl = currentItem.web_content_link; if (!targetUrl && !currentItem._resolved) { try { const targetApiId = ((S.offlineMode && currentItem.kind === 'drive#task') || (S.uploadMode && currentItem.file_id)) ? currentItem.file_id : currentItem.id; const fullItem = await apiGet(targetApiId); if (fullItem) { if (fullItem.thumbnail_link) currentItem.thumbnail_link = fullItem.thumbnail_link; if (fullItem.icon_link) currentItem.icon_link = fullItem.icon_link; if (myId === imgLoadId) requestAnimationFrame(() => updateImgPlistUI(false)); } if (myId !== imgLoadId) return; targetUrl = fullItem.web_content_link; currentItem.web_content_link = targetUrl; currentItem._resolved = true; } catch (e) { console.warn("API Error", e); } } if (myId !== imgLoadId) return; const performFinalRender = () => { if (myId !== imgLoadId) return; if (targetUrl && targetUrl !== currentItem.thumbnail_link) { img.crossOrigin = 'anonymous'; img.src = targetUrl; } else { img.crossOrigin = 'anonymous'; } if (img.complete && img.naturalWidth > 0) { checkLongImage(); } else { img.addEventListener('load', checkLongImage, { once: true }); } btnSearch.onclick = (e) => { e.stopPropagation(); const thumbUrl = currentItem.thumbnail_link ? currentItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE') : (currentItem.icon_link || ''); const thumbImg = new Image(); thumbImg.crossOrigin = 'anonymous'; thumbImg.src = thumbUrl; startImageSearch(thumbImg, currentItem.name, d, thumbUrl); }; const mainDir = lastDirection, sideDir = -lastDirection, DEPTH = 5; const quickLoad = (url) => { if(!url) return; const p = new Image(); p.src = url; }; const getIdx = (offset) => (curIdx + offset + imgList.length) % imgList.length; quickLoad(imgList[getIdx(sideDir)].thumbnail_link); for (let i = 1; i <= DEPTH; i++) { if (myId !== imgLoadId) return; const next = imgList[getIdx(i * mainDir)]; quickLoad(next.thumbnail_link); if (next.web_content_link) { const p = new Image(); p.crossOrigin = 'anonymous'; p.src = next.web_content_link; } } }; if (targetUrl && targetUrl !== currentItem.thumbnail_link) { const tempImg = new Image(); tempImg.crossOrigin = 'anonymous'; tempImg.onload = performFinalRender; tempImg.onerror = performFinalRender; tempImg.src = targetUrl; } else { performFinalRender(); } }; d.addEventListener('wheel', (e) => { if (isLongImageMode) return; e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; scale = Math.max(0.1, Math.min(10, scale + delta)); updateTransform(); }); img.onclick = () => { if (isLongImageMode) { viewport.classList.toggle('pk-fit-mode'); } }; img.addEventListener('mousedown', (e) => { if (e.button !== 0 || (isLongImageMode && !viewport.classList.contains('pk-fit-mode'))) return; e.preventDefault(); const z = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; isDrag = true; img.style.cursor = 'grabbing'; startX = e.clientX - transX * scale * z; startY = e.clientY - transY * scale * z; }); document.addEventListener('mousemove', (e) => { if (!isDrag || isLongImageMode) return; const z = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let tx = (e.clientX - startX) / (scale * z); let ty = (e.clientY - startY) / (scale * z); if (img.naturalWidth && viewport) { const vw = viewport.clientWidth; const vh = viewport.clientHeight; const iw = img.naturalWidth; const ih = img.naturalHeight; const baseRatio = Math.min(vw / iw, vh / ih); let curW = iw * baseRatio * scale; let curH = ih * baseRatio * scale; if (Math.abs(rotation % 180) === 90) [curW, curH] = [curH, curW]; const limitX = curW > vw ? (curW - vw) / (2 * scale) : 0; const limitY = curH > vh ? (curH - vh) / (2 * scale) : 0; tx = Math.max(-limitX, Math.min(limitX, tx)); ty = Math.max(-limitY, Math.min(limitY, ty)); } transX = tx; transY = ty; updateTransform(); }); document.addEventListener('mouseup', () => { isDrag = false; if (!isLongImageMode && img) img.style.cursor = 'grab'; }); const resizeHandler = () => { if (isLongImageMode) return; resetView(true); }; window.addEventListener('resize', resizeHandler); d.querySelector('#pk_img_close').onclick = (e) => { e.stopPropagation(); const currentItem = imgList[curIdx]; if (currentItem) { const targetId = currentItem.id; const targetIdx = S.display.findIndex(x => x.id === targetId); if (targetIdx !== -1) { S.sel.clear(); S.sel.add(targetId); S.activeId = targetId; const rowTop = targetIdx * CONF.rowHeight; const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); } } window.removeEventListener('resize', resizeHandler); d.remove(); }; btnFull.onclick = (e) => { e.stopPropagation(); box.classList.toggle('full'); const isNowFull = box.classList.contains('full'); btnFull.innerHTML = isNowFull ? icons.exitFull : icons.full; btnFull.setAttribute('data-pk-tip', isNowFull ? L.tip_minimize : L.tip_maximize); if (isLongImageMode && viewport) { viewport.scrollTop = 0; } else { setTimeout(() => resetView(true), 210); } }; btnRot.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; rotation += 90; updateTransform(); }; if (btnMirror) { btnMirror.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; flipH *= -1; img.style.transition = 'none'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; } if (btnFlipV) { btnFlipV.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; flipV *= -1; img.style.transition = 'none'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; } const plist = d.querySelector('#pk_img_plist'); const pTab = d.querySelector('#pk_img_plist_tab'); const pScroll = d.querySelector('#pk_img_plist_scroll'); let pTip = document.getElementById('pk_p_plist_tip_global'); if (!pTip) { pTip = document.createElement('div'); pTip.id = 'pk_p_plist_tip_global'; pTip.className = 'pk-p-plist-tip'; document.body.appendChild(pTip); } const pTxt = d.querySelector('#pk_img_idx_txt'); const updateImgPlistUI = (scrollType = 'smooth') => { if (pTxt) pTxt.textContent = `${curIdx + 1} / ${imgList.length}`; pScroll.innerHTML = renderImgListItems(); pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const idx = parseInt(e.currentTarget.dataset.idx); if (idx === curIdx) return; curIdx = idx; loadCurrent('instant'); }; el.onmouseenter = (e) => { if (plist.classList.contains('open')) { const name = e.currentTarget.dataset.name; const size = e.currentTarget.dataset.size; pTip.innerHTML = `${name}
${size}`; pTip.style.display = 'block'; } }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => { pTip.style.display = 'none'; }; }); const itemsInDom = pScroll.querySelectorAll('.pk-p-plist-item'); itemsInDom.forEach((el) => { const absIdx = parseInt(el.dataset.idx); const isActive = absIdx === curIdx; el.classList.toggle('active', isActive); if (scrollType !== false && isActive && plist.classList.contains('open')) { if (scrollType === 'instant') { pScroll.style.scrollBehavior = 'auto'; } else { pScroll.style.scrollBehavior = 'smooth'; } el.scrollIntoView({ behavior: scrollType === 'instant' ? 'auto' : 'smooth', block: 'nearest', inline: 'center' }); if (scrollType === 'instant') { setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); } } }); const sl = Math.ceil(pScroll.scrollLeft); const sw = pScroll.scrollWidth; const cw = pScroll.clientWidth; if (sw <= cw) { d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important'); d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important'); } else { if (sl <= 5) d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important'); else d.querySelector('#pk_img_plist_L').style.setProperty('display', 'flex', 'important'); if (sl + cw >= sw - 5) d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important'); else d.querySelector('#pk_img_plist_R').style.setProperty('display', 'flex', 'important'); } const btnPrev = d.querySelector('#pk_img_prev'); const btnNext = d.querySelector('#pk_img_next'); if (btnPrev) btnPrev.style.setProperty('display', curIdx === 0 ? 'none' : 'flex', 'important'); if (btnNext) btnNext.style.setProperty('display', curIdx === imgList.length - 1 ? 'none' : 'flex', 'important'); }; pTab.onclick = (e) => { e.stopPropagation(); const willOpen = !plist.classList.contains('open'); plist.classList.toggle('open'); if (typeof box !== 'undefined') box.classList.toggle('plist-active'); pTab.setAttribute('data-pk-tip', plist.classList.contains('open') ? L.tip_plist_close : L.tip_plist_open); if (!isLongImageMode) resetView(); if (willOpen) { updateImgPlistUI(false); pScroll.style.scrollBehavior = 'auto'; const activeItem = pScroll.querySelector('.active'); if (activeItem) { const targetLeft = activeItem.offsetLeft - (pScroll.clientWidth / 2) + (activeItem.clientWidth / 2); pScroll.scrollLeft = targetLeft; } setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); setTimeout(() => updateImgPlistUI(false), 100); } }; const handleListWheel = (e) => { e.stopPropagation(); e.preventDefault(); pScroll.scrollBy({ left: e.deltaY > 0 ? 300 : -300, behavior: 'smooth' }); }; pScroll.removeEventListener('wheel', handleListWheel); pScroll.addEventListener('wheel', handleListWheel, { passive: false }); pScroll.addEventListener('scroll', () => { requestAnimationFrame(() => updateImgPlistUI(false)); }, { passive: true }); const btnListL = d.querySelector('#pk_img_plist_L'); const btnListR = d.querySelector('#pk_img_plist_R'); if (btnListL) { btnListL.onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: -400, behavior: 'smooth' }); setTimeout(() => updateImgPlistUI(false), 300); }; } if (btnListR) { btnListR.onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: 400, behavior: 'smooth' }); setTimeout(() => updateImgPlistUI(false), 300); }; } setTimeout(() => updateImgPlistUI(false), 100); const goPrev = () => { if (curIdx > 0) { lastDirection = -1; curIdx--; loadCurrent(); } }; const goNext = () => { if (curIdx < imgList.length - 1) { lastDirection = 1; curIdx++; loadCurrent(); } }; d.querySelector('#pk_img_prev').onclick = (e) => { e.stopPropagation(); goPrev(); }; d.querySelector('#pk_img_next').onclick = (e) => { e.stopPropagation(); goNext(); }; d.focus(); d.addEventListener('keydown', (e) => { if (e.key === 'Escape') d.remove(); else if (e.key === 'ArrowLeft') goPrev(); else if (e.key === 'ArrowRight') goNext(); else if (e.key === 'f' || e.key === 'F') { if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click(); } else if (e.key === 'e' || e.key === 'E') { if (pTab) pTab.click(); } else if (e.key === 'm' || e.key === 'M') { e.preventDefault(); if (btnFull) btnFull.click(); } else if (e.key === 'r' || e.key === 'R') { e.preventDefault(); if (!isLongImageMode && btnRot) btnRot.click(); } else if (e.key === 'h' || e.key === 'H') { e.preventDefault(); if (!isLongImageMode && btnMirror) btnMirror.click(); } else if (e.key === 'v' || e.key === 'V') { e.preventDefault(); if (!isLongImageMode && btnFlipV) btnFlipV.click(); } }); try { await loadCurrent(); } catch (e) { console.error(e); } finally { isImageOpening = false; } } const SEARCH_CSS = ` .pk-search-ov { position: fixed; inset: 0; z-index: 2147483647; background: rgba(0,0,0,0.5); cursor: crosshair; } .pk-crop-box { position: absolute; border: 2px dashed #fff; background: rgba(255,255,255,0.1); pointer-events: none; box-shadow: 0 0 0 9999px rgba(0,0,0,0.5); } .pk-search-msg { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: #fff; padding: 10px 20px; border-radius: 5px; font-size: 14px; pointer-events: none; } `; async function startImageSearch(mediaElement, fileName, containerElement, originalLink) { if (document.querySelector('.pk-search-running-mask')) return; try { window.focus(); if (containerElement) containerElement.focus(); } catch (e) {} const L = getStrings(); const isVideo = mediaElement.tagName === 'VIDEO'; const MAX_SIDE = 1000; const BLOB_TYPE = 'image/jpeg'; const BLOB_QUALITY = 0.7; const ov = document.createElement('div'); ov.className = 'pk-search-running-mask'; ov.style.cssText = 'position:absolute; inset:0; z-index:2147483647; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; flex-direction:column; gap:20px; border-radius:inherit;'; ov.innerHTML = `
${L.str_processing}
`; const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (fsEl) { fsEl.appendChild(ov); } else if (containerElement) { containerElement.appendChild(ov); } else { document.body.appendChild(ov); } if (isVideo && !mediaElement.paused) mediaElement.pause(); try { let finalBlob = null; const cvs = document.createElement('canvas'); const ctx = cvs.getContext('2d'); const drawScaled = (source, srcW, srcH) => { let w = srcW, h = srcH; if (w === 0 || h === 0) throw new Error("Media dimensions not ready"); if (w > MAX_SIDE || h > MAX_SIDE) { const ratio = Math.min(MAX_SIDE / w, MAX_SIDE / h); w = Math.floor(w * ratio); h = Math.floor(h * ratio); } cvs.width = w; cvs.height = h; ctx.drawImage(source, 0, 0, w, h); }; let fetchUrl = originalLink; const isLocalData = fetchUrl && (fetchUrl.startsWith('blob:') || fetchUrl.startsWith('data:')); const BLOB_TYPE = 'image/jpeg'; const BLOB_QUALITY = 0.85; if (isVideo) { const sourceW = mediaElement.videoWidth; const sourceH = mediaElement.videoHeight; drawScaled(mediaElement, sourceW, sourceH); try { finalBlob = await new Promise((resolve, reject) => { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Empty")), BLOB_TYPE, BLOB_QUALITY); }); } catch (err) { throw new Error("Tainted: Video CORS Blocked"); } } else { let canvasSuccess = false; try { const sourceW = mediaElement.naturalWidth || mediaElement.width; const sourceH = mediaElement.naturalHeight || mediaElement.height; if (sourceW > 0 && sourceH > 0) { drawScaled(mediaElement, sourceW, sourceH); finalBlob = await new Promise((resolve, reject) => { try { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Tainted")), BLOB_TYPE, BLOB_QUALITY); } catch (e) { reject(e); } }); canvasSuccess = !!finalBlob; } } catch (canvasErr) { console.warn("[ImageSearch] DOM Canvas extraction failed (Tainted/Not Ready), fallback to network."); } if (!canvasSuccess && fetchUrl && !isLocalData) { console.log("[ImageSearch] Fetching image via network as fallback..."); try { const res = await fetch(fetchUrl, { mode: 'cors', credentials: 'omit' }); if (!res.ok && res.status !== 206) throw new Error(`Fetch HTTP ${res.status}`); finalBlob = await res.blob(); } catch (fetchErr) { console.warn(`[ImageSearch] Native fetch failed (${fetchErr.message}), fallback to GM_xhr...`); let fetchRetry = 0; let fetchSuccess = false; while (fetchRetry < 3 && !fetchSuccess) { try { finalBlob = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: fetchUrl, responseType: "blob", timeout: 30000, headers: { "Referer": "https://mypikpak.com/", "User-Agent": navigator.userAgent }, onload: (res) => { if (res.status === 200 || res.status === 206) resolve(res.response); else reject(new Error(`HTTP ${res.status}`)); }, onerror: () => reject(new Error("Network Error")), ontimeout: () => reject(new Error("Timeout")) }); }); fetchSuccess = true; } catch (err) { fetchRetry++; if (fetchRetry < 3) { console.warn(`[ImageSearch] GM_xhr retry ${fetchRetry}/3...`); await new Promise(r => setTimeout(r, 1500)); } } } } } if (!finalBlob) { throw new Error(L.err_network_break); } if (finalBlob.size > 8 * 1024 * 1024) { console.log(`[ImageSearch] Image too large (${(finalBlob.size/1024/1024).toFixed(1)}MB), applying compression...`); try { const bmp = await createImageBitmap(finalBlob); drawScaled(bmp, bmp.width, bmp.height); bmp.close(); finalBlob = await new Promise((resolve, reject) => { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Compression Failed")), BLOB_TYPE, BLOB_QUALITY); }); } catch (e) { console.warn("[ImageSearch] Compression failed, using original blob.", e); } } } if (!finalBlob) throw new Error(L.err_capture || "Capture failed"); const uploadAndGetUrl = async () => { if (typeof GM_xmlhttpRequest === 'undefined') throw new Error("Missing GM_xmlhttpRequest"); const txtDiv = ov.lastElementChild; const FAST_TIMEOUT = 4000; const uploadTask = (url, formData, parseType, stageText) => { return new Promise((resolve, reject) => { if (txtDiv) txtDiv.textContent = stageText; GM_xmlhttpRequest({ method: "POST", url: url, data: formData, timeout: FAST_TIMEOUT, responseType: parseType === 'json' ? 'json' : 'text', onload: (res) => { if (res.status === 200) resolve(res); else reject(new Error(`HTTP ${res.status}`)); }, onerror: () => reject(new Error("Network Error")), ontimeout: () => reject(new Error("Timeout")) }); }); }; const tryNode1 = async () => { const fd = new FormData(); fd.append('files[]', finalBlob, `pk_1.jpg`); const res = await uploadTask( "https://uguu.se/upload.php", fd, 'json', L.str_upload_1 ); if (res.response && res.response.success && res.response.files?.[0]?.url) { return res.response.files[0].url; } throw new Error("Uguu API Error"); }; const tryNode2 = async () => { console.warn("Node 1 failed, switching to Litterbox..."); const fd = new FormData(); fd.append('reqtype', 'fileupload'); fd.append('time', '1h'); fd.append('fileToUpload', finalBlob, `pk_2.jpg`); const res = await uploadTask( "https://litterbox.catbox.moe/resources/internals/api.php", fd, 'text', L.str_upload_2 ); return res.responseText.trim(); }; const tryNode3 = async () => { console.warn("Node 2 failed, switching to Catbox..."); const fd = new FormData(); fd.append('reqtype', 'fileupload'); fd.append('userhash', ''); fd.append('fileToUpload', finalBlob, `pk_3.jpg`); const res = await uploadTask( "https://catbox.moe/user/api.php", fd, 'text', L.str_upload_3 ); return res.responseText.trim(); }; try { return await tryNode1(); } catch (e1) { try { return await tryNode2(); } catch (e2) { try { return await tryNode3(); } catch (e3) { console.error("All upload nodes failed:", e1, e2, e3); throw new Error("All Upload Hosts Failed"); } } } }; try { const imgUrl = await uploadAndGetUrl(); if (!imgUrl || !imgUrl.startsWith('http')) { throw new Error("Invalid URL returned."); } const currentEngine = gmGet('pk_search_engine', 'google'); const encUrl = encodeURIComponent(imgUrl); let jumpUrl = ''; let engineName = ''; switch (currentEngine) { case 'yandex': jumpUrl = `https://yandex.com/images/search?rpt=imageview&url=${encUrl}`; engineName = 'Yandex'; break; case 'saucenao': jumpUrl = `https://saucenao.com/search.php?db=999&url=${encUrl}`; engineName = 'SauceNAO'; break; case 'tracemoe': { const cleanUrl = imgUrl.replace(/^https?:\/\//, ''); const proxyUrl = `https://wsrv.nl/?url=${cleanUrl}&output=jpg`; jumpUrl = `https://trace.moe/?url=${encodeURIComponent(proxyUrl)}`; engineName = 'trace.moe'; break; } case 'google': default: jumpUrl = `https://lens.google.com/uploadbyurl?url=${encUrl}`; engineName = 'Google Lens'; break; } const spinDiv = ov.querySelector('.pk-spin-lg'); const txtDiv = ov.lastElementChild; if (spinDiv) spinDiv.style.borderColor = '#4CAF50'; if (txtDiv) txtDiv.textContent = L.str_redirecting.replace('Google Lens', engineName); await sleep(600); window.open(jumpUrl, '_blank'); } catch (err) { console.warn("Auto upload failed, switching to manual fallback:", err); const txtDiv = ov.lastElementChild; if (txtDiv) txtDiv.textContent = L.str_upload_fail_copy; let fallbackBlob = null; try { const bmp = await createImageBitmap(finalBlob); const tmpCvs = document.createElement('canvas'); tmpCvs.width = bmp.width; tmpCvs.height = bmp.height; tmpCvs.getContext('2d').drawImage(bmp, 0, 0); bmp.close(); fallbackBlob = await new Promise(r => tmpCvs.toBlob(r, 'image/png')); } catch (e) { console.warn("Bitmap conversion failed, falling back to origin canvas:", e); fallbackBlob = await new Promise(r => cvs.toBlob(r, 'image/png')); } const MAX_CLIPBOARD_SIZE = 19.5 * 1024 * 1024; if (fallbackBlob.size > MAX_CLIPBOARD_SIZE) { console.log(`PNG too large, resizing...`); try { const ratio = Math.sqrt(MAX_CLIPBOARD_SIZE / fallbackBlob.size); const bmp = await createImageBitmap(fallbackBlob); const newW = Math.floor(bmp.width * ratio); const newH = Math.floor(bmp.height * ratio); const tmpCvs = document.createElement('canvas'); tmpCvs.width = newW; tmpCvs.height = newH; tmpCvs.getContext('2d').drawImage(bmp, 0, 0, newW, newH); bmp.close(); fallbackBlob = await new Promise(r => tmpCvs.toBlob(r, 'image/png')); } catch (e) { console.warn("Resize failed:", e); } } try { const item = new ClipboardItem({ 'image/png': fallbackBlob }); await navigator.clipboard.write([item]); } catch (clipErr) { console.error("Clipboard write failed:", clipErr); if (txtDiv) txtDiv.textContent = "Clipboard Failed"; await sleep(1000); } const ctrlVPhrase = (navigator.platform.toUpperCase().indexOf('MAC') >= 0) ? 'Cmd+V' : 'Ctrl+V'; const hintText = L.msg_manual_paste.replace('{cmd}', `${ctrlVPhrase}`); ov.innerHTML = `
⚠️
${L.msg_copy_success}
${hintText}
`; await sleep(1500); const currentEngine = gmGet('pk_search_engine', 'google'); let manualUrl = ''; switch (currentEngine) { case 'yandex': manualUrl = 'https://yandex.com/images/'; break; case 'saucenao': manualUrl = 'https://saucenao.com/'; break; case 'tracemoe': manualUrl = 'https://trace.moe/'; break; case 'google': default: manualUrl = 'https://lens.google.com/upload'; break; } window.open(manualUrl, '_blank'); } } catch (e) { console.error("Search Error:", e); const errorMsg = e.message.includes('Tainted') ? "Security Error: CORS Blocked" : e.message; ov.innerHTML = `
❌ ${errorMsg}
`; await sleep(2000); } finally { ov.remove(); } } function dataURLtoBlob(dataurl) { let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--){ u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], {type:mime}); } const FILTER_EXTS = { video: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mpg','mpeg','rm','rmvb','asf','vob','dat','divx','f4v','m2ts','mts','tp','trp','ogv','mpe','m2v','m3u8'], audio: ['mp3','wav','flac','aac','ogg','wma','ape','m4a','amr','opus','m4b','alac','aiff','mid','midi','ra','dts','ac3','dsf','dff'], image: ['jpg','jpeg','png','gif','bmp','webp','svg','tif','tiff','ico','heic','heif','raw','cr2','nef','arw','dng','orf','avif','psd','ai','eps','jfif','jpe'], document: ['txt','html','pdf','pptx','chm','docx','xlsx','htm','doc','dwg','mdb','ppt','xls','rtf','odt','ods','odp','epub','mobi','azw3','djvu','cbz','cbr','md','log','csv','xml','json'], software: ['apk','exe','ipa','dmg','rpm','deb','msi','pkg','xapk','apks','aab','jar','bin','sh','bat','cmd'], archive: ['zip','rar','7z','tar','gz','iso','cab','bz2','xz','tgz','wim','esd','img','zst','lzh'], torrent: ['torrent'] }; const FILTER_NAMES = { video: L.cat_video, audio: L.cat_audio, image: L.cat_image, document: L.cat_document, software: L.cat_software, archive: L.cat_archive, torrent: L.cat_torrent, other: L.cat_other }; const fIcons = { all: ``, video: ``, audio: ``, image: ``, document: ``, software: ``, archive: ``, torrent: ``, other: `` }; const renderActiveFilterUI = () => { if (!UI.filterBar) return; const cat = S.filterState.cat; if (cat === 'all') return; UI.filterCatLabel.textContent = FILTER_NAMES[cat] || (L.cat_other); if (cat === 'other') { UI.filterExtsWrap.style.display = 'none'; } else { UI.filterExtsWrap.style.display = 'flex'; const exts = FILTER_EXTS[cat] ||[]; const mainExts = exts.slice(0, 3); const moreExts = exts.slice(3); let html = `${L.cat_all}`; let displayExts = [...mainExts]; if (S.filterState.ext !== 'all' && moreExts.includes(S.filterState.ext)) { displayExts[2] = S.filterState.ext; } displayExts.forEach(e => { html += `${e}`; }); UI.filterExtsMain.innerHTML = html; if (moreExts.length > 0) { UI.filterExtsMoreBtn.style.display = 'flex'; } else { UI.filterExtsMoreBtn.style.display = 'none'; } UI.filterExtsMain.querySelectorAll('.pk-f-ext').forEach(span => { span.onclick = (e) => { e.stopPropagation(); S.filterState.ext = span.dataset.ext; renderActiveFilterUI(); S.sel.clear(); refresh(); }; }); } }; const showFilterCatPopup = (triggerEl, e) => { e.stopPropagation(); const existing = document.querySelector('#pk-filter-cat-pop'); if (existing) { existing.remove(); return; } const pop = document.createElement('div'); pop.id = 'pk-filter-cat-pop'; pop.style.cssText = ` position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; padding: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); z-index: 2147483647; width: 420px; display: flex; flex-direction: column; zoom: var(--pk-zoom, 1); `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.innerHTML = `
${L.title_file_filter}
${fIcons.all} ${L.cat_all}
${fIcons.video} ${L.cat_video}
${fIcons.audio} ${L.cat_audio}
${fIcons.image} ${L.cat_image}
${fIcons.document} ${L.cat_document}
${fIcons.software} ${L.cat_software}
${fIcons.archive} ${L.cat_archive}
${fIcons.torrent} ${L.cat_torrent}
${fIcons.other} ${L.cat_other}
`; document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = triggerEl.getBoundingClientRect(); let popLeft = rect.left / scale; if (popLeft + 420 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 430; pop.style.top = ((rect.bottom / scale) + 5) + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; pop.querySelectorAll('.pk-fc-btn').forEach(btn => { btn.onclick = (ev) => { ev.stopPropagation(); const cat = btn.dataset.cat; S.filterState.cat = cat; S.filterState.ext = 'all'; S.filterState.active = (cat !== 'all'); cleanup(); if (S.filterState.active) { renderActiveFilterUI(); } S.sel.clear(); refresh(); }; }); const closer = (ev) => { if (!pop.contains(ev.target) && !triggerEl.contains(ev.target)) { cleanup(); } }; setTimeout(() => document.addEventListener('mousedown', closer), 10); }; if (UI.filterBtn) UI.filterBtn.onclick = (e) => showFilterCatPopup(UI.filterBtn, e); if (UI.filterCatLabel) UI.filterCatLabel.onclick = (e) => showFilterCatPopup(UI.filterCatLabel, e); if (UI.filterExtsMoreBtn) { UI.filterExtsMoreBtn.onclick = (e) => { e.stopPropagation(); const existing = document.querySelector('#pk-filter-more-pop'); if (existing) { existing.remove(); return; } const pop = document.createElement('div'); pop.id = 'pk-filter-more-pop'; pop.style.cssText = ` position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; padding: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); z-index: 2147483647; max-width: 340px; display: flex; flex-wrap: wrap; gap: 8px; zoom: var(--pk-zoom, 1); `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); const exts = FILTER_EXTS[S.filterState.cat] ||[]; const mainExts = exts.slice(0, 3); const moreExts = exts.slice(3); let displayExts = [...mainExts]; if (S.filterState.ext !== 'all' && moreExts.includes(S.filterState.ext)) { displayExts[2] = S.filterState.ext; } const dropdownExts = exts.filter(ex => !displayExts.includes(ex)); pop.innerHTML = dropdownExts.map(ex => `${ex}`).join(''); document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = UI.filterExtsWrap.getBoundingClientRect(); let popLeft = rect.left / scale; if (popLeft + 340 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 350; pop.style.top = ((rect.bottom / scale) + 5) + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; pop.querySelectorAll('.pk-f-ext').forEach(span => { span.onclick = (ev) => { ev.stopPropagation(); S.filterState.ext = span.dataset.ext; cleanup(); renderActiveFilterUI(); S.sel.clear(); refresh(); }; }); const closer = (ev) => { if (!pop.contains(ev.target) && !UI.filterExtsMoreBtn.contains(ev.target)) { cleanup(); } }; setTimeout(() => document.addEventListener('mousedown', closer), 10); }; } if (UI.filterExitBtn) { UI.filterExitBtn.onclick = () => { S.filterState = { active: false, cat: 'all', ext: 'all' }; UI.filterBtn.style.display = 'flex'; UI.filterActiveUI.style.display = 'none'; S.sel.clear(); refresh(); }; } const getHistory = () => { try { return JSON.parse(gmGet('pk_search_history', '[]')); } catch { return []; } }; const saveHistory = (txt) => { if (!txt) return; let list = getHistory(); list = list.filter(x => x !== txt); list.unshift(txt); if (list.length > 3) list = list.slice(0, 3); gmSet('pk_search_history', JSON.stringify(list)); }; const renderHistory = () => { const list = getHistory(); if (list.length === 0) { UI.searchHist.style.display = 'none'; return; } let html = `
${L.title_search_hist}${L.btn_clear_hist}
`; list.forEach(txt => { html += `
${esc(txt)}
`; }); UI.searchHist.innerHTML = html; UI.searchHist.style.display = 'flex'; UI.searchHist.querySelector('#pk-hist-del').onclick = (e) => { e.stopPropagation(); gmSet('pk_search_history', '[]'); UI.searchHist.style.display = 'none'; }; UI.searchHist.querySelectorAll('.pk-select-item').forEach(el => { el.onclick = (e) => { const val = el.querySelector('span').textContent; UI.searchInput.value = val; performSearch(val); UI.searchHist.style.display = 'none'; }; }); }; const performSearch = (val) => { const txt = val.trim(); if (!txt) { if (S.search && UI.searchClear) UI.searchClear.click(); return; } const isGlobal = UI.chkGlobal && UI.chkGlobal.checked && !S.uploadMode; if (txt) { saveHistory(txt); if (isGlobal && !S.preSearchPath) { S.preSearchPath = [...S.path]; } if (isGlobal) { S.sort = 'modified_time'; S.dir = 1; S.path = [ { id: '', name: L.btn_nav_home }, { id: 'virtual_search_root', name: L.str_search_results } ]; renderCrumb(); } } S.search = txt; if (S.dupMode) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.sel.clear(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); updateStat(); } else if (isGlobal) { if (globalNeedsSync) { setLoad(true); updateLoadTxt(L.str_analyzing); setTimeout(async () => { if (!S.scanning) { S.scanning = true; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); if (UI.chkGlobal) UI.chkGlobal.checked = false; }; try { await runFlattenScanOperation(true, [], true); globalNeedsSync = false; } catch (e) { console.error("[GlobalSearch] Sync Error:", e); } finally { S.scanning = false; } } load(false, true).finally(() => setLoad(false)); }, 50); } else { load(false, false); } } else { refresh(); } UI.searchClear.style.display = txt ? 'flex' : 'none'; UI.searchHist.style.display = 'none'; UI.searchInput.blur(); }; if (UI.searchInput) { UI.searchInput.oninput = (e) => { const val = e.target.value.trim(); UI.searchClear.style.display = val ? 'flex' : 'none'; if (!val && S.search && UI.searchClear) { UI.searchClear.click(); } }; UI.searchInput.onfocus = () => { renderHistory(); if (getHistory()?.length > 0) UI.searchHist.style.display = 'flex'; }; document.addEventListener('click', (e) => { if (!UI || !UI.searchInput || !UI.searchHist) return; if (!UI.searchInput.contains(e.target) && !UI.searchHist.contains(e.target)) { UI.searchHist.style.display = 'none'; } }); UI.searchInput.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') { performSearch(e.target.value); } }; if (UI.searchBtn) { UI.searchBtn.onclick = () => { performSearch(UI.searchInput.value); }; } if (UI.searchClear) { UI.searchClear.onclick = async () => { const wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; if (!S.search && UI.searchInput.value) { UI.searchInput.value = ''; UI.searchClear.style.display = 'none'; UI.searchInput.focus(); return; } const searchWasActive = !!S.search; const isInVirtualStack = S.path.some(node => node.id === 'virtual_search_root'); let requiresReload = false; UI.searchInput.value = ''; S.search = ''; S.lastGlobalResults = []; try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); if (prefStore['virtual_search_root']) { delete prefStore['virtual_search_root']; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } } catch(e) {} if ((wasGlobalChecked || isInVirtualStack) && searchWasActive) { const currentFolder = S.path[S.path.length - 1]; if (currentFolder.id !== 'virtual_search_root' && currentFolder.id !== '') { const traceStack = []; let ptrId = currentFolder.id; let ptrName = currentFolder.name; let safety = 100; traceStack.unshift({ id: ptrId, name: ptrName }); while (ptrId && ptrId !== 'root' && safety > 0) { if (globalParentIndex.has(ptrId)) { const parent = globalParentIndex.get(ptrId); ptrId = parent.id; ptrName = parent.name; if (ptrId === 'root' || ptrId === '') break; traceStack.unshift({ id: ptrId, name: ptrName }); } else { const node = S.itemMap.get(ptrId); if (node && node._lineage && node._lineage.length > 0) { const ancestors = node._lineage.filter(x => x.id !== '' && x.id !== 'root'); for (let k = ancestors.length - 1; k >= 0; k--) { traceStack.unshift(ancestors[k]); } } break; } safety--; } const realPath = [{ id: '', name: L.btn_nav_home }, ...traceStack]; S.path = realPath; } else if (isInVirtualStack || S.preSearchPath) { S.path = S.preSearchPath ? [...S.preSearchPath] : [{ id: '', name: L.btn_nav_home }]; } requiresReload = true; } S.preSearchPath = null; UI.searchClear.style.display = 'none'; UI.searchHist.style.display = 'none'; if (S.dupMode && !isInVirtualStack) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.sel.clear(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); updateStat(); } else if (S.isFlattened && !isInVirtualStack) { refresh(); updateStat(); } else { if (requiresReload) { const targetNode = S.path[S.path.length - 1]; const targetKey = S.getRealCacheKey(targetNode.id); const cachedData = (typeof globalCache !== 'undefined') ? (globalCache.get(targetKey) || globalCache.get('')) : null; if (cachedData) { if (cachedData.items) S.items = [...cachedData.items]; else if (Array.isArray(cachedData)) S.items = [...cachedData]; S.itemMap.clear(); for (const it of S.items) S.itemMap.set(it.id, it); } refresh(); setLoad(true); const p = load(false, true); if (UI.chkGlobal) UI.chkGlobal.checked = wasGlobalChecked; await p; } else { refresh(); updateStat(); } } }; } } UI.btnHelp.onclick = () => { const m = showModal(`

${L.modal_help_title}

${L.help_desc}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { display: 'flex', flexDirection: 'column', maxHeight: '85vh', overflow: 'hidden', padding: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const scrollEl = m.querySelector('.pk-help-scroll'); const fadeEl = m.querySelector('#pk_help_fade'); if (scrollEl) { scrollEl.style.maxHeight = 'none'; scrollEl.style.flex = '1'; scrollEl.style.minHeight = '0'; } if (scrollEl && fadeEl) { scrollEl.style.paddingBottom = "24px"; const updateFade = () => { if (scrollEl.scrollHeight <= scrollEl.clientHeight + 5 || Math.ceil(scrollEl.scrollTop + scrollEl.clientHeight) >= scrollEl.scrollHeight - 30) { fadeEl.style.opacity = '0'; } else { fadeEl.style.opacity = '1'; } }; scrollEl.addEventListener('scroll', updateFade, { passive: true }); const resizeObserver = new ResizeObserver(() => updateFade()); resizeObserver.observe(scrollEl); const _orgRemove = m.remove.bind(m); m.remove = () => { resizeObserver.disconnect(); _orgRemove(); }; setTimeout(updateFade, 50); } m.querySelector('#help_close').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.remove(); } }); }; UI.chkGlobal.onchange = async (e) => { if (e.target.checked) { if (S.movingIds && S.movingIds.size > 0) { e.target.checked = false; showAlert(L.msg_global_index_blocked_moving); return; } const isSuppressed = gmGet('pk_suppress_global_warn', false); if (!isSuppressed && !hasShownGlobalWarnSession) { const userChoice = await new Promise((resolve) => { const m = showModal(`

${L.title_confirm}

${esc(L.msg_global_warn).replace(/\n/g, '
')}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '420px', padding: '30px', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelector('#cfm_cancel').onclick = () => { m.remove(); resolve({ ok: false }); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve({ ok: false }); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#cfm_ok').click(); } }); m.querySelector('#cfm_ok').onclick = () => { const isChecked = m.querySelector('#pk_warn_ignore').checked; m.remove(); resolve({ ok: true, suppress: isChecked }); }; }); if (!userChoice.ok) { e.target.checked = false; return; } hasShownGlobalWarnSession = true; if (userChoice.suppress) { gmSet('pk_suppress_global_warn', true); } } if (!isGlobalIndexReady || globalNeedsSync) { S.scanning = true; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); UI.chkGlobal.checked = false; }; await runFlattenScanOperation(true); } refresh(); } else { if (S.scanning) { S.scanning = false; updateLoadTxt(L.str_stopping); } refresh(); } }; const runFlattenScanOperation = async (isSyncOnly = false, specificTargets =[], isSilent = false) => { S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; let fileMap = new Map(); let processedFolders = 0; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; setLoad(true); const isPartialScan = specificTargets && specificTargets.length > 0; let rootNodes =[]; if (isPartialScan) { updateLoadTxt(L.msg_init_scan_sel); specificTargets.forEach(item => { if (item.kind === 'drive#folder') { rootNodes.push({ id: item.id, name: item.name, lineage:[{ id: item.id, name: item.name }], retryCount: 0 }); } else if (!isSyncOnly) { item._lineage =[]; fileMap.set(item.id, item); } }); } else { updateLoadTxt(`${L.str_scanning} 0`); const startNode = isSyncOnly ? { id: '', name: 'Root' } : S.path[S.path.length - 1]; rootNodes =[{ id: startNode.id || '', name: startNode.name || 'Root', lineage: [], retryCount: 0 }]; } UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); if (S.isFlattened) { UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; } else { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.chkGlobal) UI.chkGlobal.checked = false; S.isFlattened = false; setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { console.log("♻️ Scan interrupted: Forcing background crawler to resume."); resumeBackgroundDiscovery(); } }, 1000); } }; S.scanning = true; try { await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { if (!isSyncOnly) { f._lineage = parent.lineage ||[]; fileMap.set(f.id, f); } }, onFolder: (folder, filesInFolder) => { processedFolders++; indexParents(folder.id, folder.name, filesInFolder); if (typeof globalLineageMap !== 'undefined') { globalLineageMap.set(folder.id, folder.lineage); } }, onProgress: (st) => { const folderText = isPartialScan ? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders) : `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : ""; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo + retryTag); } }); if (S.scanning && !signal.aborted && myScanId === S.scanId) { globalNeedsSync = false; isGlobalIndexReady = true; if (!isSyncOnly) { updateLoadTxt(L.str_merging); let tempItems = Array.from(fileMap.values()); if (S.scanFilter && !isSyncOnly) { const { minBytes, maxBytes, keyword } = S.scanFilter; const kwList = keyword ? keyword.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : []; tempItems = tempItems.filter(item => { if (kwList.length > 0) { const fullLowerName = (item.name || "").toLowerCase(); const lastDot = fullLowerName.lastIndexOf('.'); const nameWithoutExt = (item.kind !== 'drive#folder' && lastDot > 0) ? fullLowerName.substring(0, lastDot) : fullLowerName; if (kwList.some(k => nameWithoutExt.includes(k))) return false; } const sz = parseInt(item.size || 0); if (sz < minBytes) return false; if (maxBytes > 0 && sz > maxBytes) return false; return true; }); } const total = tempItems.length; S.items = new Array(total); S.itemMap.clear(); let lastYield = performance.now(); for (let i = 0; i < total; i++) { const item = tempItems[i]; S.items[i] = item; S.itemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(item.id); } if (i % 5000 === 0 && performance.now() - lastYield > 16) { updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`); await sleep(0); lastYield = performance.now(); } } S.isFlattened = true; S.sort = 'modified_time'; S.dir = 1; UI.chkAll.checked = false; S.sel.clear(); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important'); updateLoadTxt(L.str_rendering); await refresh(); const msg = L.msg_scan_done.replace('{n}', total).replace('{f}', processedFolders); if (!isSilent) showAlert(msg); } } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error_crit}: ${e.message}`); } if (!isSyncOnly && myScanId === S.scanId) { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; UI.lblGlobal.style.display = 'flex'; } if (myScanId === S.scanId) UI.chkGlobal.checked = false; } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } }; const openScanDupModal = async (initialTab) => { if (S.loading || S.scanning) return; const curFolderId = S.path[S.path.length - 1].id || ''; if (isPathBusy(curFolderId)) { showAlert(L.msg_flatten_blocked_moving); return; } S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; const selectedTargets =[]; if (S.sel.size > 0) { S.sel.forEach(id => { const item = S.itemMap.get(id); if (item) selectedTargets.push(item); }); } const lastMin = gmGet('pk_scan_last_min', 0); const lastMax = gmGet('pk_scan_last_max', ''); const lastUnit = gmGet('pk_scan_last_unit', 'MB'); const lastKeyword = gmGet('pk_scan_last_keyword', ''); let currentStrict = gmGet('pk_dup_strictness', 'strict'); const scanTxt = L.btn_scan; const dupTxt = L.tip_dup; const L_min = L.lbl_ana_min; const L_max = L.lbl_ana_max; let scanTargetDesc = selectedTargets.length > 0 ? L.lbl_scan_selected.replace('{n}', selectedTargets.length) : L.lbl_scan_current; let dupTargetDesc = selectedTargets.length > 0 ? L.lbl_dup_selected.replace('{n}', selectedTargets.length) : L.lbl_dup_current; const m = showModal(`

${L.title_file_analysis}

${scanTxt}
${dupTxt}
${scanTargetDesc}
${L.lbl_keyword_filter}
${L_min}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
-
${L_max}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${lastUnit}
MB
GB
TB
${dupTargetDesc}
${L.label_dup_strictness}
${currentStrict === 'loose' ? L.opt_loose : L.opt_strict}${CONF.crumbIcons.down}
${L.opt_strict}
${L.opt_loose}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: 'auto', padding: '0', overflow: 'visible', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelectorAll('.pk-s-tab').forEach(tab => { tab.onclick = () => { m.querySelectorAll('.pk-s-tab').forEach(t => t.classList.remove('act')); tab.classList.add('act'); const curMode = tab.dataset.val; m.querySelector('#pane_scan').style.display = curMode === 'scan' ? 'block' : 'none'; m.querySelector('#pane_dup').style.display = curMode === 'dup' ? 'flex' : 'none'; }; }); const inpMin = m.querySelector('#sc_val_min'); const inpMax = m.querySelector('#sc_val_max'); const unitBtn = m.querySelector('#sc_unit_btn'); const unitMenu = m.querySelector('#sc_unit_menu'); const unitTxt = m.querySelector('#sc_unit_txt'); let currentUnit = lastUnit; m.querySelector('#sc_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; }; m.querySelector('#sc_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); }; m.querySelector('#sc_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; }; m.querySelector('#sc_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); }; unitBtn.onclick = (e) => { e.stopPropagation(); unitMenu.style.display = unitMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('.pk-ana-item').forEach(item => { item.onclick = () => { m.querySelectorAll('.pk-ana-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentUnit = item.dataset.v; unitTxt.textContent = currentUnit; unitMenu.style.display = 'none'; }; }); const closeMenu = () => { if (unitMenu) unitMenu.style.display = 'none'; }; setTimeout(() => document.addEventListener('click', closeMenu), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeMenu); _orgRemove(); }; if (S.dupConfig) { m.querySelector('#scan_video').checked = S.dupConfig.video; m.querySelector('#scan_image').checked = S.dupConfig.image; m.querySelector('#scan_other').checked = S.dupConfig.other; } const scStrictTrigger = m.querySelector('#cs_sc_strict .pk-select-trigger'); const scStrictMenu = m.querySelector('#cs_sc_strict .pk-select-menu'); const scStrictTxt = m.querySelector('#txt_sc_strict'); scStrictTrigger.onclick = (e) => { e.stopPropagation(); scStrictMenu.style.display = scStrictMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('#cs_sc_strict .pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('#cs_sc_strict .pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentStrict = item.dataset.val; scStrictTxt.textContent = item.textContent; scStrictMenu.style.display = 'none'; }; }); const saveScanInputs = () => { gmSet('pk_scan_last_min', parseInt(inpMin.value) || 0); gmSet('pk_scan_last_max', inpMax.value.trim()); gmSet('pk_scan_last_unit', currentUnit); gmSet('pk_scan_last_keyword', m.querySelector('#sc_keyword').value.trim()); gmSet('pk_dup_strictness', currentStrict); }; m.querySelector('#sc_cancel').onclick = () => { saveScanInputs(); m.remove(); }; m.querySelector('.pk-modal-close').onclick = () => { saveScanInputs(); m.remove(); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#sc_start').click(); } }); m.querySelector('#sc_start').onclick = async () => { const mode = m.querySelector('.pk-s-tab.act').dataset.val; saveScanInputs(); if (mode === 'scan') { const vMin = parseInt(inpMin.value) || 0; const vMax = parseInt(inpMax.value) || 0; const kw = m.querySelector('#sc_keyword').value.trim(); if (vMin < 0 || (vMax > 0 && vMin > vMax)) { inpMin.style.borderColor = '#d93025'; if (vMax > 0 && vMin > vMax) inpMax.style.borderColor = '#d93025'; return; } gmSet('pk_scan_last_min', vMin); gmSet('pk_scan_last_max', vMax > 0 ? vMax : ''); gmSet('pk_scan_last_unit', currentUnit); gmSet('pk_scan_last_keyword', kw); let mult = 1; if (currentUnit === 'MB') mult = 1024 * 1024; else if (currentUnit === 'GB') mult = 1024 * 1024 * 1024; else if (currentUnit === 'TB') mult = 1024 * 1024 * 1024 * 1024; S.scanFilter = { minBytes: Math.floor(vMin * mult), maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0, keyword: kw }; m.remove(); S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; S.scanning = true; UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); if (S.isFlattened) { UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; } else { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal) UI.chkGlobal.checked = false; S.isFlattened = false; setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { resumeBackgroundDiscovery(); } }, 1000); } }; S.lastScanTargets = selectedTargets; await runFlattenScanOperation(false, selectedTargets, false); } else { S.dupConfig = { video: m.querySelector('#scan_video').checked, image: m.querySelector('#scan_image').checked, other: m.querySelector('#scan_other').checked }; if (!S.dupConfig.video && !S.dupConfig.image && !S.dupConfig.other) return; m.remove(); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; let fileMap = new Map(); let processedFolders = 0; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; setLoad(true); const isPartialScan = selectedTargets.length > 0; let rootNodes =[]; if (isPartialScan) { updateLoadTxt(L.msg_init_scan_sel); selectedTargets.forEach(item => { if (item.kind === 'drive#folder') { rootNodes.push({ id: item.id, name: item.name, lineage:[{ id: item.id, name: item.name }], retryCount: 0 }); } else { item._lineage =[]; fileMap.set(item.id, item); } }); } else { updateLoadTxt(`${L.str_scanning} 0`); const startNode = S.path[S.path.length - 1]; rootNodes =[{ id: startNode.id || '', name: startNode.name || 'Root', lineage: [], retryCount: 0 }]; } UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); setLoad(false); }; try { await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { f._lineage = parent.lineage ||[]; fileMap.set(f.id, f); }, onFolder: (folder, filesInFolder) => { processedFolders++; if (typeof globalCache !== 'undefined' && !globalCache.has(folder.id)) { globalCache.set(folder.id, [...filesInFolder]); } indexParents(folder.id, folder.name, filesInFolder); if (typeof globalLineageMap !== 'undefined') { globalLineageMap.set(folder.id, folder.lineage); } }, onProgress: (st) => { const folderText = isPartialScan ? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders) : `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : ""; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo + retryTag); } }); if (S.scanning && !signal.aborted && myScanId === S.scanId) { updateLoadTxt(L.str_merging); const tempItems = Array.from(fileMap.values()); const cfg = S.dupConfig || { video: true, image: false, other: false }; let candidates = tempItems.filter(i => { if (!i.mime_type) return false; const isVideo = i.mime_type.startsWith('video'); const isImage = i.mime_type.startsWith('image'); const isOther = !isVideo && !isImage; if (isVideo && cfg.video) return true; if (isImage && cfg.image) return true; if (isOther && cfg.other) return true; return false; }); const preGroups = await computeDuplicateGroups(candidates, cfg, () => S.scanning && !signal.aborted && myScanId === S.scanId); if (preGroups.length === 0) { setLoad(false); showToast(L.msg_dup_none); S.dupRunning = false; S.scanning = false; return; } const total = tempItems.length; S.items = new Array(total); S.itemMap.clear(); let lastYield = performance.now(); for (let i = 0; i < total; i++) { const item = tempItems[i]; S.items[i] = item; S.itemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(item.id); } if (i % 5000 === 0 && performance.now() - lastYield > 16) { updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`); await sleep(0); lastYield = performance.now(); } } S.dupMode = true; S.isFlattened = false; S.sort = 'modified_time'; S.dir = 1; UI.chkAll.checked = false; S.sel.clear(); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; UI.lblGlobal.style.display = 'none'; UI.chkGlobal.checked = false; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important'); if (UI.lblSearchPath) UI.lblSearchPath.style.display = 'flex'; updateLoadTxt(L.str_rendering); S.display =[...S.items]; await refresh(); } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error_crit}: ${e.message}`); } if (myScanId === S.scanId) { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; UI.lblGlobal.style.display = 'flex'; } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } } }; }; UI.scan.onclick = () => openScanDupModal('scan'); const onOfflineFilterChange = () => { if (S.offlineMode) { S.offlineFilters = { running: UI.chkOffRun.checked, failed: UI.chkOffFail.checked, complete: UI.chkOffOk.checked }; refresh(); updateStat(); } }; if (UI.chkOffRun) UI.chkOffRun.onchange = onOfflineFilterChange; if (UI.chkOffFail) UI.chkOffFail.onchange = onOfflineFilterChange; if (UI.chkOffOk) UI.chkOffOk.onchange = onOfflineFilterChange; const onUploadFilterChange = () => { if (S.uploadMode) { S.uploadFilters = { running: UI.chkUpRun.checked, paused: UI.chkUpPause.checked, complete: UI.chkUpDone.checked }; refresh(); updateStat(); } }; if (UI.chkUpRun) UI.chkUpRun.onchange = onUploadFilterChange; if (UI.chkUpPause) UI.chkUpPause.onchange = onUploadFilterChange; if (UI.chkUpDone) UI.chkUpDone.onchange = onUploadFilterChange; const onDupFilterChange = () => { if(S.dupMode) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.sel.clear(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); } }; UI.chkName.onchange = onDupFilterChange; UI.chkSim.onchange = onDupFilterChange; UI.chkHash.onchange = onDupFilterChange; if (UI.chkSearchPath) { UI.chkSearchPath.onchange = () => { if (S.dupMode && S.search) { renderDupView(); } else if ((S.isFlattened || S.analyzeMode) && S.search) { refresh(); } }; } UI.btnExit.onclick = async () => { S.scanning = false; S.dupMode = false; S.suppressClearConfirm = false; S.isFlattened = false; S.scanFilter = null; if (S.filterState) S.filterState = { active: false, cat: 'all', ext: 'all' }; S._sortAppliedForId = null; S._comicApplied = false; if (S.analyzeMode) { S.analyzeMode = false; S.analyzeResultItems = null; S.analyzeSimGroups = null; S.analyzeMap = null; S.path = [{ id: '', name: L.btn_nav_home }]; } S.dupRawGroups = []; S.pinnedDupPath = null; if (UI.selDupFolder) UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } if (UI.chkSearchPath) UI.chkSearchPath.checked = false; isGUISensitive = false; S.sort = 'modified_time'; S.dir = 1; if (UI.crumb) UI.crumb.style.display = ''; S.items.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1; return a.name.localeCompare(b.name); }); S.clearSelection(); const targetNode = S.path[S.path.length - 1]; const targetKey = S.getRealCacheKey(targetNode.id); const cachedData = (typeof globalCache !== 'undefined') ? (globalCache.get(targetKey) || globalCache.get('')) : null; if (cachedData) { if (cachedData.items) S.items = [...cachedData.items]; else if (Array.isArray(cachedData)) S.items = [...cachedData]; S.itemMap.clear(); for (const it of S.items) S.itemMap.set(it.id, it); } else { S.items = []; S.itemMap.clear(); } setLoad(true, true); refresh(); updateQuotaUI(); await load(false, true); if (typeof S.wasGlobalChecked !== 'undefined' && UI.chkGlobal) { UI.chkGlobal.checked = S.wasGlobalChecked; } setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { console.log("♻️ Sandbox exited: Forcing global discovery to resume."); resumeBackgroundDiscovery(); } }, 1500); }; UI.cols.forEach(c => c.onclick = () => { if (S.dupMode) return; const k = c.dataset.k; if (S.sort === k) { S.dir *= -1; } else { S.sort = k; S.dir = 1; } refresh(); }); if (UI.btnFolderFirst) { S.renderFolderFirst = () => { UI.btnFolderFirst.style.color = S.folderFirst ? 'var(--pk-pri)' : '#666'; }; UI.btnFolderFirst.onmouseenter = () => { if (!S.folderFirst) UI.btnFolderFirst.style.color = 'var(--pk-fg)'; }; UI.btnFolderFirst.onmouseleave = () => { if (!S.folderFirst) UI.btnFolderFirst.style.color = '#666'; }; const nameWrap = el.querySelector('#pk-name-text-wrap'); if (nameWrap) { nameWrap.onmouseenter = () => { if (S.sort !== 'name') nameWrap.style.color = 'var(--pk-fg)'; }; nameWrap.onmouseleave = () => { if (S.sort !== 'name') nameWrap.style.color = '#666'; }; } S.renderFolderFirst(); UI.btnFolderFirst.onclick = (e) => { e.stopPropagation(); S.folderFirst = !S.folderFirst; const curNode = S.path[S.path.length - 1]; const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curNode.id.startsWith('virtual_') || curNode.id === 'virtual_search_root'); if (isStandard) { if (gmGet('pk_sort_independent', false)) { const folderId = curNode.id || 'root'; try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); const currentPref = prefStore[folderId] || { sort: S.sort, dir: S.dir }; currentPref.folderFirst = S.folderFirst; currentPref.sort = S.sort; currentPref.dir = S.dir; prefStore[folderId] = currentPref; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } catch(e) {} } else { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); globalPref.folderFirst = S.folderFirst; globalPref.sort = S.sort; globalPref.dir = S.dir; gmSet('pk_global_sort_pref', JSON.stringify(globalPref)); gmSet('pk_folder_first', S.folderFirst); } } else { gmSet('pk_folder_first', S.folderFirst); } S.renderFolderFirst(); refresh(); }; } const btnInvert = document.getElementById('pk-btn-invert'); if (btnInvert) { btnInvert.onclick = (e) => { e.stopPropagation(); if (S.loading || S.display.length === 0) return; const newSel = new Set(); for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader) { if (!S.sel.has(item.id)) { newSel.add(item.id); } } } S.sel = newSel; S.lastSelIdx = -1; S.activeId = null; renderVisible(); updateStat(); }; btnInvert.onmouseenter = () => btnInvert.style.color = 'var(--pk-pri)'; btnInvert.onmouseleave = () => btnInvert.style.color = 'var(--pk-fg)'; } S.handleSelectAll = (e) => { if (!e || !e.target) { if (UI.chkAll) UI.chkAll.checked = !UI.chkAll.checked; } S.activeId = null; S.lastSelIdx = -1; let totalVisible = 0; const allIds = []; const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; if (item.isHeader) continue; if (S.movingIds.has(item.id)) continue; totalVisible++; allIds.push(item.id); } const isSelectAllAction = (S.sel.size < totalVisible); UI.chkAll.checked = isSelectAllAction; if (isSelectAllAction) { S.sel = new Set(allIds); } else { S.sel.clear(); } requestAnimationFrame(() => { renderVisible(); updateStat(); UI.chkAll.checked = isSelectAllAction; }); }; if (UI.btnAnaSelect || UI.btnDupSmart) { const pop = document.createElement('div'); pop.className = 'pk-ana-pop'; pop.innerHTML = `
${L.opt_keep_new}
${L.opt_keep_old}
${L.opt_keep_large}
${L.opt_keep_small}
${L.opt_keep_short}
${L.opt_keep_long}
`; UI.win.appendChild(pop); let activeTargetBtn = null; const updatePopPos = () => { if (pop.style.display !== 'flex' || !activeTargetBtn) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const winRect = UI.win.getBoundingClientRect(); const btnRect = activeTargetBtn.getBoundingClientRect(); let left = (btnRect.left - winRect.left) / scale; const top = (btnRect.bottom - winRect.top) / scale; const winWidth = winRect.width / scale; const popWidth = 340; if (left + popWidth > winWidth - 10) { left = (btnRect.right - winRect.left) / scale - popWidth; } pop.style.left = Math.max(10, left) + 'px'; pop.style.top = (top + 5) + 'px'; }; const togglePop = (e, btn) => { e.stopPropagation(); const isVisible = (pop.style.display === 'flex' && activeTargetBtn === btn); if (isVisible) { pop.style.display = 'none'; activeTargetBtn = null; } else { if (S.analyzeMode && !S.hasShownAnaWarn) { showToast(L.msg_ana_warn, 'warning', 6000); S.hasShownAnaWarn = true; } pop.style.display = 'flex'; activeTargetBtn = btn; updatePopPos(); } }; if (UI.btnAnaSelect) UI.btnAnaSelect.onclick = (e) => togglePop(e, UI.btnAnaSelect); if (UI.btnDupSmart) UI.btnDupSmart.onclick = (e) => togglePop(e, UI.btnDupSmart); window.addEventListener('resize', updatePopPos); pop.querySelectorAll('.pk-ana-opt').forEach(opt => { opt.onclick = () => { const op = opt.dataset.op; S.sel.clear(); const isBetter = (type, curW, curM) => { if (type === 'new') return new Date(curM.modified_time) > new Date(curW.modified_time); if (type === 'old') return new Date(curM.modified_time) < new Date(curW.modified_time); if (type === 'large') return BigInt(curM.size || 0) > BigInt(curW.size || 0); if (type === 'small') return BigInt(curM.size || 0) < BigInt(curW.size || 0); if (type === 'short') return curM.name.length < curW.name.length; if (type === 'long') return curM.name.length > curW.name.length; return false; }; if (S.analyzeMode && S.analyzeSimGroups) { S.analyzeSimGroups.forEach(g => { const members = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); if (members.length < 2) return; let winner = members[0]; members.forEach(m => { if (isBetter(op, winner, m)) winner = m; }); members.forEach(m => { if (m.id !== winner.id) S.sel.add(m.id); }); }); } else if (S.dupMode && S.dupGroups) { const itemMap = new Map(); S.display.forEach(d => { if (d.isHeader) return; const gIdx = S.dupGroups.get(d.id); if (gIdx !== undefined) { if (!itemMap.has(gIdx)) itemMap.set(gIdx,[]); itemMap.get(gIdx).push(d); } }); itemMap.forEach(members => { if (members.length < 2) return; let winner = members[0]; members.forEach(m => { if (isBetter(op, winner, m)) winner = m; }); members.forEach(m => { if (m.id !== winner.id) S.sel.add(m.id); }); }); } pop.style.display = 'none'; activeTargetBtn = null; renderVisible(); updateStat(); }; }); document.addEventListener('mousedown', (e) => { const isClickInsideBtn = (UI.btnAnaSelect && UI.btnAnaSelect.contains(e.target)) || (UI.btnDupSmart && UI.btnDupSmart.contains(e.target)); if (!pop.contains(e.target) && !isClickInsideBtn) { pop.style.display = 'none'; activeTargetBtn = null; } }); } UI.btnRefresh.onclick = async () => { updateQuotaUI(); if (S.isFlattened) { if (!S.scanning) { S.scanning = true; UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; }; runFlattenScanOperation(false, S.lastScanTargets, true).catch(e => { console.error(e); S.scanning = false; }); } return; } const cur = S.path[S.path.length - 1]; const intent = UI.chkGlobal ? UI.chkGlobal.checked : false; if (cur.id === 'analyze_root') { setLoad(true); updateLoadTxt(L.str_refreshing); await sleep(200); await load(); return; } if (cur.id) S.cache.delete(cur.id); const p = load(false, true); if (UI.chkGlobal) UI.chkGlobal.checked = intent; await p; }; if (UI.btnAnalyze) { UI.btnAnalyze.onclick = async () => { if (S.trashMode) return; const curFolderId = S.path[S.path.length - 1].id || ''; if (isPathBusy(curFolderId)) { showAlert(L.msg_op_blocked_analyzing); return; } S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; const lastMin = gmGet('pk_analyze_last_min', 0); const lastMax = gmGet('pk_analyze_last_max', ''); const lastUnit = gmGet('pk_analyze_last_unit', 'GB'); const lastKeyword = gmGet('pk_analyze_last_keyword', ''); const lastSim = gmGet('pk_analyze_last_sim', 1.0); const lastAlgo = gmGet('pk_analyze_last_algo', 'sim'); const result = await new Promise((resolve) => { const L_min = L.lbl_ana_min; const L_max = L.lbl_ana_max; const analyzeTargetDesc = S.sel.size > 0 ? L.lbl_analyze_selected.replace('{n}', S.sel.size) : L.lbl_analyze_current; const anaSimTargetDesc = S.sel.size > 0 ? L.lbl_ana_sim_selected.replace('{n}', S.sel.size) : L.lbl_ana_sim_current; const m = showModal(`

${L.btn_analyze}

${L.opt_ana_large}
${L.opt_ana_sim}
${analyzeTargetDesc}
${L.lbl_keyword_filter}
${L_min}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
-
${L_max}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${lastUnit}
MB
GB
TB
`); let currentMode = 'large'; let currentSim = lastSim; const updateAlgoLabel = () => { const lblEl = m.querySelector('#cs_ana_sim .pk-select-label'); if (lblEl) lblEl.textContent = L.lbl_threshold; }; m.querySelectorAll('input[name="ana_sim_algo"]').forEach(r => r.addEventListener('change', updateAlgoLabel)); updateAlgoLabel(); m.querySelector('#pk_algo_help').onclick = (e) => { e.stopPropagation(); showAlert(L.algo_help_content, L.title_algo_help); }; m.querySelectorAll('.pk-s-tab').forEach(tab => { tab.onclick = () => { m.querySelectorAll('.pk-s-tab').forEach(t => t.classList.remove('act')); tab.classList.add('act'); currentMode = tab.dataset.val; m.querySelector('#ana_pane_large').style.display = currentMode === 'large' ? 'block' : 'none'; m.querySelector('#ana_pane_similar').style.display = currentMode === 'similar' ? 'block' : 'none'; }; }); const simTrigger = m.querySelector('#cs_ana_sim .pk-select-trigger'); const simMenu = m.querySelector('#cs_ana_sim .pk-select-menu'); const simTxt = m.querySelector('#txt_ana_sim'); simTrigger.onclick = (e) => { e.stopPropagation(); simMenu.style.display = simMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('#cs_ana_sim .pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('#cs_ana_sim .pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentSim = parseFloat(item.dataset.val); gmSet('pk_analyze_last_sim', currentSim); simTxt.textContent = (currentSim <= 0.5) ? L.opt_loose : L.opt_strict; simMenu.style.display = 'none'; }; }); const inpMin = m.querySelector('#an_val_min'); const inpMax = m.querySelector('#an_val_max'); const btn = m.querySelector('#an_unit_btn'); m.querySelector('#an_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#an_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#an_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; inpMax.dispatchEvent(new Event('input')); }; m.querySelector('#an_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); inpMax.dispatchEvent(new Event('input')); }; const menu = m.querySelector('#an_unit_menu'); const txt = m.querySelector('#an_unit_txt'); let currentUnit = lastUnit; btn.onclick = (e) => { e.stopPropagation(); menu.style.display = menu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('.pk-ana-item').forEach(item => { item.onclick = () => { m.querySelectorAll('.pk-ana-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentUnit = item.dataset.v; txt.textContent = currentUnit; menu.style.display = 'none'; }; }); const closeMenu = () => { if(menu) menu.style.display = 'none'; if(simMenu) simMenu.style.display = 'none'; }; setTimeout(() => document.addEventListener('click', closeMenu), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeMenu); _orgRemove(); }; setTimeout(() => { inpMin.focus(); inpMin.select(); }, 50); const kHandler = (e) => { if (e.key === 'Enter') m.querySelector('#an_confirm').click(); if (e.key === 'Escape') m.querySelector('#an_cancel').click(); }; inpMin.onkeydown = kHandler; inpMax.onkeydown = kHandler; const saveAnalyzeInputs = () => { gmSet('pk_analyze_last_min', parseInt(inpMin.value) || 0); gmSet('pk_analyze_last_max', inpMax.value.trim()); gmSet('pk_analyze_last_unit', currentUnit); gmSet('pk_analyze_last_keyword', m.querySelector('#an_keyword').value.trim()); const algo = m.querySelector('input[name="ana_sim_algo"]:checked')?.value; if (algo) gmSet('pk_analyze_last_algo', algo); }; m.querySelector('#an_cancel').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); }; m.querySelector('#an_confirm').onclick = () => { if (currentMode === 'large') { const vMin = parseInt(inpMin.value) || 0; const vMax = parseInt(inpMax.value) || 0; const kw = m.querySelector('#an_keyword').value.trim(); if (vMin < 0 || (vMax > 0 && vMin > vMax)) { inpMin.style.borderColor = '#d93025'; if (vMax > 0 && vMin > vMax) inpMax.style.borderColor = '#d93025'; return; } saveAnalyzeInputs(); let mult = 1; if (currentUnit === 'MB') mult = 1024 * 1024; else if (currentUnit === 'GB') mult = 1024 * 1024 * 1024; else if (currentUnit === 'TB') mult = 1024 * 1024 * 1024 * 1024; m.remove(); resolve({ mode: 'large', minBytes: Math.floor(vMin * mult), maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0, keyword: kw }); } else { const algo = m.querySelector('input[name="ana_sim_algo"]:checked').value; gmSet('pk_analyze_last_algo', algo); m.remove(); resolve({ mode: 'similar', threshold: currentSim, algo: algo }); } }; const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '540px', height: 'auto', minHeight: 'auto', overflow: 'visible', paddingBottom: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' }); } }); if (result === null) return; const isSimMode = result.mode === 'similar'; const minBytes = result.minBytes || 0; const maxBytes = result.maxBytes || 0; const simThreshold = result.threshold || 0.9; const simAlgo = result.algo || 'sim'; setLoad(true); isGUISensitive = true; let nodeMap = new Map(); const largeFolders = []; const startNodes = []; const getRealLineage = (item) => { if (item._lineage && item._lineage.length > 0) { return item._lineage; } const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root'); return [...cleanPath, { id: item.id, name: item.name }]; }; if (S.sel.size > 0) { S.sel.forEach(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') { const fullLineage = getRealLineage(item); startNodes.push({ id: item.id, name: item.name, icon_link: item.icon_link, starred: item.starred, tags: item.tags, lineage: fullLineage, retryCount: 0, _pathStr: fullLineage.map(x => x.name).join('/') }); } }); } else { const subFolders = S.items.filter(it => it.kind === 'drive#folder'); if (subFolders.length > 0) { subFolders.forEach(item => { const fullLineage = getRealLineage(item); startNodes.push({ id: item.id, name: item.name, icon_link: item.icon_link, starred: item.starred, tags: item.tags, lineage: fullLineage, retryCount: 0, _pathStr: fullLineage.map(x => x.name).join('/') }); }); } else { const cur = S.path[S.path.length - 1]; const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root'); if (cleanPath.length === 0) cleanPath.push({ id: '', name: L.btn_nav_home }); const rootName = cur.name || 'Root'; const actualCur = S.itemMap.get(cur.id); startNodes.push({ id: cur.id || '', name: rootName, icon_link: cur.icon_link, starred: actualCur ? actualCur.starred : false, tags: actualCur ? actualCur.tags :[], lineage: cleanPath, retryCount: 0, _pathStr: cleanPath.map(x => x.name).join('/') }); } } if (startNodes.length === 0) { setLoad(false); isGUISensitive = false; showToast(L.msg_analyze_only_normal_dir); return; } startNodes.forEach(n => { nodeMap.set(n.id, { id: n.id, name: n.name, icon_link: n.icon_link, starred: n.starred, tags: n.tags, size: 0, parentId: null, marked: false, _pathStr: n._pathStr, lineage: n.lineage, isRoot: true, files:[] }); }); const cacheKey = '__analyze_nodeMap_' + startNodes.map(n => n.id).join('_'); let useCache = false; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey) && globalDirtyFolders.size === 0) { const cachedArr = globalCache.get(cacheKey); nodeMap = new Map(cachedArr); useCache = true; console.log("[Analyze] Using cached global nodeMap."); } const propagateSize = (parentId, addSize) => { let curId = parentId; while (curId !== null && nodeMap.has(curId)) { const node = nodeMap.get(curId); node.size += addSize; if (!node.isRoot && node.size >= minBytes && !node.marked) { node.marked = true; largeFolders.push(node); } curId = node.parentId; } }; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); }; S.scanning = true; let totalFilesScanned = 0; let totalDirsScanned = 0; try { if (!useCache) { await coreRecursiveEngine(startNodes, { signal: signal, onFolder: (folder, filesInFolder, nextSubFolders) => { totalDirsScanned++; nextSubFolders.forEach(sub => { if (!nodeMap.has(sub.id)) { const fullPathStr = sub.lineage.map(x => x.name).join('/'); nodeMap.set(sub.id, { id: sub.id, name: sub.name, icon_link: sub.icon_link, starred: sub.starred, tags: sub.tags, size: 0, parentId: folder.id, marked: false, _pathStr: fullPathStr, lineage: sub.lineage, isRoot: false, files:[] }); } }); }, onFile: (file, parent) => { totalFilesScanned++; const sz = Number(file.size || 0); if (sz > 0) { propagateSize(parent.id, sz); } if (nodeMap.has(parent.id)) { const fingerprint = file.hash ? `${file.hash}_${sz}` : `${file.name}_${sz}`; let curId = parent.id; while (curId !== null && nodeMap.has(curId)) { nodeMap.get(curId).files.push(fingerprint); curId = nodeMap.get(curId).parentId; } } }, onProgress: (st) => { updateLoadTxt(`${L.str_scanning} ${st.folders} ${L.unit_folders} | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`); } }); if (!isRunning || signal.aborted || myScanId !== S.scanId) { throw new Error('StoppedByUser'); } if (typeof globalCache !== 'undefined') { globalCache.set(cacheKey, Array.from(nodeMap.entries())); } } else { Array.from(nodeMap.values()).forEach(node => { if (!node.isRoot && node.size >= minBytes) { largeFolders.push(node); } }); } } catch (e) { if (isRunning && e.message !== 'StoppedByUser' && e.name !== 'AbortError') { showAlert(`${L.str_error}: ${e.message}`); setLoad(false); } } finally { isGUISensitive = false; S.scanning = false; S.scanAbortController = null; if (!isRunning) { setLoad(false); return; } let viewItems = []; if (isSimMode) { updateLoadTxt(`${L.str_analyzing}...`); Array.from(nodeMap.values()).forEach(node => { node._ancestorSet = new Set(node.lineage ? node.lineage.map(p => p.id) : []); if (node.parentId && nodeMap.has(node.parentId)) { const parent = nodeMap.get(node.parentId); if (parent.files.length === node.files.length) { parent.isShell = true; } } }); const isDescendant = (childId, parentId) => { const childNode = nodeMap.get(childId); return childNode ? childNode._ancestorSet.has(parentId) : false; }; const folderArr = Array.from(nodeMap.values()) .filter(f => !f.isShell && (f.files.length >= 2 || f.size > 1024 * 1024)) .map(f => { const counts = new Map(); f.files.forEach(h => counts.set(h, (counts.get(h) || 0) + 1)); return { ...f, fileCounts: counts, _keys: Array.from(counts.keys()), totalFiles: f.files.length }; }) .sort((a, b) => b.totalFiles - a.totalFiles); const invertedIndex = new Map(); folderArr.forEach((f, i) => { f.fileCounts.forEach((_, hash) => { let arr = invertedIndex.get(hash); if (!arr) { arr = []; invertedIndex.set(hash, arr); } arr.push(i); }); }); const totalDocs = folderArr.length; const weightMap = new Map(); invertedIndex.forEach((arr, hash) => { const df = arr.length; let w = 1.0; if (totalDocs >= 20 && (df / totalDocs) > 0.05) { w = 0.05; } weightMap.set(hash, w); }); folderArr.forEach(f => { let wt = 0; f.fileCounts.forEach((count, hash) => { wt += count * weightMap.get(hash); }); f.weightedTotal = wt; }); let groups =[]; const assigned = new Set(); const total = folderArr.length; const candidateSeen = new Uint32Array(total); let lastYieldTime = performance.now(); updateLoadTxt(`${L.str_analyzing}... 0%`); try { if (simAlgo === 'name') { const nameGroups = new Map(); const cleanFolderName = (oldName) => { let cleanName = oldName.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim(); cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, ''); cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, ''); const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])"; const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\s)', 'i'); cleanName = cleanName.replace(atRegex, ''); const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i'); cleanName = cleanName.replace(hyphenRegex, ''); cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, ''); cleanName = cleanName.replace(/^[-_. ,,::;;\p{Extended_Pictographic}]+/u, ''); const pairs = [['【','】'], ['[',']'], ['《','》'],['<','>'], ['(',')'],['(',')'], ['{','}']]; pairs.forEach(([L_char, R_char]) => { const idxR_Fix = cleanName.indexOf(R_char); const idxL_Check = cleanName.indexOf(L_char); if (idxR_Fix > 0 && idxR_Fix <= 10 && (idxL_Check === -1 || idxL_Check > idxR_Fix)) { cleanName = L_char + cleanName; } const chars = cleanName.split(''); const stack = []; const toRemove = new Set(); for (let i = 0; i < chars.length; i++) { const c = chars[i]; if (c === L_char) stack.push(i); else if (c === R_char) { if (stack.length > 0) stack.pop(); else toRemove.add(i); } } stack.forEach(i => toRemove.add(i)); if (toRemove.size > 0) cleanName = chars.filter((_, i) => !toRemove.has(i)).join(''); }); const quoteCount = (cleanName.match(/'/g) || []).length; if (quoteCount % 2 !== 0) cleanName = cleanName.replace(/'/, ''); let finalResult = cleanName.toLowerCase().trim(); if (simThreshold <= 0.5) { finalResult = finalResult.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ''); } return finalResult || oldName.toLowerCase().trim(); }; folderArr.forEach(f => { const k = cleanFolderName(f.name); if (!nameGroups.has(k)) nameGroups.set(k,[]); nameGroups.get(k).push(f); }); const sizeRatioLimit = simThreshold >= 0.5 ? 0.05 : 0.10; for (const[k, items] of nameGroups) { if (items.length > 1) { const sorted = [...items].sort((a,b) => Number(a.size) - Number(b.size)); let currentGroup = [sorted[0]]; for (let i = 1; i < sorted.length; i++) { const target = sorted[i]; const root = currentGroup[0]; const rootSize = Number(root.size || 0); const targetSize = Number(target.size || 0); let isMatch = false; if (rootSize === 0 && targetSize === 0) isMatch = true; else { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); if (maxBase > 0 && (sizeDiff / maxBase) <= sizeRatioLimit) isMatch = true; } if (isMatch) currentGroup.push(target); else { if (currentGroup.length > 1) { const gNodes = currentGroup; let minS = Number.MAX_SAFE_INTEGER, maxS = 0; gNodes.forEach(n => { const sz = Number(n.size||0); if(szmaxS) maxS=sz; }); if (minS === Number.MAX_SAFE_INTEGER) minS = 0; const range = (minS === maxS) ? fmtSize(minS) : `${fmtSize(minS)} ~ ${fmtSize(maxS)}`; groups.push({ ids: gNodes.map(f => f.id), type: `${gNodes.length} ${L.str_items} | ${range}`, _sim: 1 }); gNodes.forEach(f => assigned.add(f.id)); } currentGroup = [target]; } } if (currentGroup.length > 1) { const gNodes = currentGroup; let minS = Number.MAX_SAFE_INTEGER, maxS = 0; gNodes.forEach(n => { const sz = Number(n.size||0); if(szmaxS) maxS=sz; }); if (minS === Number.MAX_SAFE_INTEGER) minS = 0; const range = (minS === maxS) ? fmtSize(minS) : `${fmtSize(minS)} ~ ${fmtSize(maxS)}`; groups.push({ ids: gNodes.map(f => f.id), type: `${gNodes.length} ${L.str_items} | ${range}`, _sim: 1 }); gNodes.forEach(f => assigned.add(f.id)); } } } } else { for (let i = 0; i < total; i++) { if (i % 50 === 0 || performance.now() - lastYieldTime > 16) { if (!isRunning) break; updateLoadTxt(`${L.str_analyzing}\n${Math.round((i / total) * 100)}%`); await sleep(0); lastYieldTime = performance.now(); } if (assigned.has(folderArr[i].id)) continue; const f1 = folderArr[i]; const group = [f1]; let groupMinSim = 1.0; const candidateIndices = []; const marker = i + 1; f1.fileCounts.forEach((_, hash) => { const foldersWithHash = invertedIndex.get(hash); if (foldersWithHash) { for (let k = 0, len = foldersWithHash.length; k < len; k++) { const idx = foldersWithHash[k]; if (idx > i && candidateSeen[idx] !== marker && !assigned.has(folderArr[idx].id)) { candidateSeen[idx] = marker; candidateIndices.push(idx); } } } }); for (let m = 0, cLen = candidateIndices.length; m < cLen; m++) { const j = candidateIndices[m]; const f2 = folderArr[j]; if (simAlgo === 'sim' && (isDescendant(f2.id, f1.id) || isDescendant(f1.id, f2.id))) continue; let total1 = f1.weightedTotal; let total2 = f2.weightedTotal; let intersect = 0; if (isDescendant(f2.id, f1.id)) { total1 -= total2; if (total1 <= 0 || total2 <= 0) continue; const maxS = total1 > total2 ? total1 : total2; const minS = total1 < total2 ? total1 : total2; if (simAlgo === 'sim' && minS / maxS < simThreshold) continue; for (let k = 0, len = f2._keys.length; k < len; k++) { const hash = f2._keys[k]; const c2 = f2.fileCounts.get(hash); const c1 = f1.fileCounts.get(hash) || 0; const diff = c1 - c2; if (diff > 0) intersect += (diff < c2 ? diff : c2) * weightMap.get(hash); } } else if (isDescendant(f1.id, f2.id)) { total2 -= total1; if (total1 <= 0 || total2 <= 0) continue; const maxS = total1 > total2 ? total1 : total2; const minS = total1 < total2 ? total1 : total2; if (simAlgo === 'sim' && minS / maxS < simThreshold) continue; for (let k = 0, len = f1._keys.length; k < len; k++) { const hash = f1._keys[k]; const c1 = f1.fileCounts.get(hash); const c2 = f2.fileCounts.get(hash) || 0; const diff = c2 - c1; if (diff > 0) intersect += (diff < c1 ? diff : c1) * weightMap.get(hash); } } else { if (total1 <= 0 || total2 <= 0) continue; const maxS = total1 > total2 ? total1 : total2; const minS = total1 < total2 ? total1 : total2; if (simAlgo === 'sim' && minS / maxS < simThreshold) continue; const fSmall = f1._keys.length < f2._keys.length ? f1 : f2; const fLarge = f1._keys.length < f2._keys.length ? f2 : f1; for (let k = 0, len = fSmall._keys.length; k < len; k++) { const hash = fSmall._keys[k]; const cLarge = fLarge.fileCounts.get(hash); if (cLarge !== undefined) { const cSmall = fSmall.fileCounts.get(hash); intersect += (cSmall < cLarge ? cSmall : cLarge) * weightMap.get(hash); } } } const minTotal = total1 < total2 ? total1 : total2; const union = total1 + total2 - intersect; const sim = simAlgo === 'contain' ? (minTotal > 0 ? (intersect / minTotal) : 0) : (union > 0 ? (intersect / union) : 0); if (sim >= simThreshold) { let isGroupQualified = true; let currentMinSim = sim; for (let gIdx = 1; gIdx < group.length; gIdx++) { const gMember = group[gIdx]; if (simAlgo === 'sim' && (isDescendant(f2.id, gMember.id) || isDescendant(gMember.id, f2.id))) { isGroupQualified = false; break; } let tA = gMember.weightedTotal; let tB = f2.weightedTotal; let intS = 0; if (isDescendant(f2.id, gMember.id)) { tA -= tB; } else if (isDescendant(gMember.id, f2.id)) { tB -= tA; } if (tA <= 0 || tB <= 0) { isGroupQualified = false; break; } const maxS = tA > tB ? tA : tB; const minS = tA < tB ? tA : tB; if (simAlgo === 'sim' && minS / maxS < simThreshold) { isGroupQualified = false; break; } if (isDescendant(f2.id, gMember.id)) { for (let k = 0, len = f2._keys.length; k < len; k++) { const h = f2._keys[k]; const cB = f2.fileCounts.get(h); const cA = gMember.fileCounts.get(h) || 0; const diff = cA - cB; if (diff > 0) intS += (diff < cB ? diff : cB) * weightMap.get(h); } } else if (isDescendant(gMember.id, f2.id)) { for (let k = 0, len = gMember._keys.length; k < len; k++) { const h = gMember._keys[k]; const cA = gMember.fileCounts.get(h); const cB = f2.fileCounts.get(h) || 0; const diff = cB - cA; if (diff > 0) intS += (diff < cA ? diff : cA) * weightMap.get(h); } } else { const fSmall = gMember._keys.length < f2._keys.length ? gMember : f2; const fLarge = gMember._keys.length < f2._keys.length ? f2 : gMember; for (let k = 0, len = fSmall._keys.length; k < len; k++) { const h = fSmall._keys[k]; const cLarge = fLarge.fileCounts.get(h); if (cLarge !== undefined) { const cSmall = fSmall.fileCounts.get(h); intS += (cSmall < cLarge ? cSmall : cLarge) * weightMap.get(h); } } } const minT = tA < tB ? tA : tB; const un = tA + tB - intS; const pairwiseSim = simAlgo === 'contain' ? (minT > 0 ? (intS / minT) : 0) : (un > 0 ? (intS / un) : 0); if (pairwiseSim < simThreshold) { isGroupQualified = false; break; } if (pairwiseSim < currentMinSim) { currentMinSim = pairwiseSim; } } if (isGroupQualified) { group.push(f2); if (currentMinSim < groupMinSim) groupMinSim = currentMinSim; } } } if (group.length > 1) { group.forEach(f => assigned.add(f.id)); groups.push({ ids: group.map(f => f.id), type: `${group.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${Math.round(groupMinSim * 100)}%`, _sim: groupMinSim }); } } } if (simAlgo !== 'name') { const folderObjMap = new Map(); folderArr.forEach(f => folderObjMap.set(f.id, f)); const finalGroups =[]; groups.forEach(g => { const nodes = g.ids.map(id => folderObjMap.get(id)).filter(Boolean); const toRemove = new Set(); nodes.forEach(node => { const descendants = nodes.filter(n => n.id !== node.id && isDescendant(n.id, node.id)); if (descendants.length === 0) return; const hasExternal = nodes.some(n => n.id !== node.id && !isDescendant(n.id, node.id) && !isDescendant(node.id, n.id)); if (hasExternal) return; const topDescendants = descendants.filter(d1 => !descendants.some(d2 => d1.id !== d2.id && isDescendant(d1.id, d2.id)) ); let isCovered = true; let sumTotal = 0; const sumCounts = new Map(); topDescendants.forEach(td => { sumTotal += td.totalFiles; td.fileCounts.forEach((count, hash) => { sumCounts.set(hash, (sumCounts.get(hash) || 0) + count); }); }); if (sumTotal !== node.totalFiles) { isCovered = false; } else { for (const [hash, count] of node.fileCounts) { if (sumCounts.get(hash) !== count) { isCovered = false; break; } } } if (isCovered) { toRemove.add(node.id); } }); const finalIds = g.ids.filter(id => !toRemove.has(id)); if (finalIds.length >= 2) { finalGroups.push({ ids: finalIds, type: g.type, _sim: g._sim }); } }); groups = finalGroups; const partitionedGroups = []; const varianceTolerance = 0.15; groups.forEach(g => { const nodes = g.ids.map(id => folderObjMap.get(id)).filter(Boolean); if (nodes.length <= 2) { const pct = Math.round(g._sim * 100); g.type = `${nodes.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${pct}%`; partitionedGroups.push(g); return; } const simMatrix = []; let maxSim = 0, minSim = 1; for (let i = 0; i < nodes.length; i++) { simMatrix[i] = []; for (let j = 0; j < nodes.length; j++) { if (i === j) { simMatrix[i][j] = 1; continue; } if (j < i) { simMatrix[i][j] = simMatrix[j][i]; continue; } const n1 = nodes[i], n2 = nodes[j]; if (simAlgo === 'sim' && (isDescendant(n2.id, n1.id) || isDescendant(n1.id, n2.id))) { simMatrix[i][j] = 0; minSim = 0; continue; } let tA = n1.weightedTotal, tB = n2.weightedTotal, intS = 0; if (isDescendant(n2.id, n1.id)) tA -= tB; else if (isDescendant(n1.id, n2.id)) tB -= tA; if (tA > 0 && tB > 0) { if (isDescendant(n2.id, n1.id)) { for (let k = 0; k < n2._keys.length; k++) { const h = n2._keys[k], cB = n2.fileCounts.get(h), cA = n1.fileCounts.get(h) || 0, diff = cA - cB; if (diff > 0) intS += (diff < cB ? diff : cB) * weightMap.get(h); } } else if (isDescendant(n1.id, n2.id)) { for (let k = 0; k < n1._keys.length; k++) { const h = n1._keys[k], cA = n1.fileCounts.get(h), cB = n2.fileCounts.get(h) || 0, diff = cB - cA; if (diff > 0) intS += (diff < cA ? diff : cA) * weightMap.get(h); } } else { const fSmall = n1._keys.length < n2._keys.length ? n1 : n2, fLarge = n1._keys.length < n2._keys.length ? n2 : n1; for (let k = 0; k < fSmall._keys.length; k++) { const h = fSmall._keys[k], cLarge = fLarge.fileCounts.get(h); if (cLarge !== undefined) intS += (fSmall.fileCounts.get(h) < cLarge ? fSmall.fileCounts.get(h) : cLarge) * weightMap.get(h); } } const minT = tA < tB ? tA : tB; const un = tA + tB - intS; const s = simAlgo === 'contain' ? (minT > 0 ? (intS / minT) : 0) : (un > 0 ? (intS / un) : 0); simMatrix[i][j] = s; if (s > maxSim) maxSim = s; if (s < minSim) minSim = s; } else { simMatrix[i][j] = 0; minSim = 0; } } } if (maxSim - minSim <= varianceTolerance) { const minPct = Math.round(minSim * 100); const maxPct = Math.round(maxSim * 100); const range = (minPct === maxPct) ? `${minPct}%` : `${minPct}% ~ ${maxPct}%`; g.type = `${nodes.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${range}`; partitionedGroups.push(g); } else { const unassigned = new Set(nodes.map((_, idx) => idx)); const pairs =[]; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) pairs.push({i, j, s: simMatrix[i][j]}); } pairs.sort((a, b) => b.s - a.s); pairs.forEach(pair => { if (unassigned.has(pair.i) && unassigned.has(pair.j) && pair.s >= simThreshold) { const sg = [pair.i, pair.j]; let sgMinSim = pair.s; unassigned.delete(pair.i); unassigned.delete(pair.j); for (const u of Array.from(unassigned)) { let canAdd = true, localMin = 1; for (const mem of sg) { const s = simMatrix[u < mem ? u : mem][u > mem ? u : mem]; if (pair.s - s > varianceTolerance || s < simThreshold) { canAdd = false; break; } if (s < localMin) localMin = s; } if (canAdd) { sg.push(u); unassigned.delete(u); if (localMin < sgMinSim) sgMinSim = localMin; } } if (sg.length >= 2) { let subMin = 1, subMax = 0; for(let x=0; x sg[y] ? sg[x] : sg[y]; const sVal = simMatrix[idx1][idx2]; if (sVal < subMin) subMin = sVal; if (sVal > subMax) subMax = sVal; } } const minPct = Math.round(subMin * 100); const maxPct = Math.round(subMax * 100); const range = (minPct === maxPct) ? `${minPct}%` : `${minPct}% ~ ${maxPct}%`; partitionedGroups.push({ ids: sg.map(idx => nodes[idx].id), type: `${sg.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${range}`, _sim: subMin }); } } }); } }); groups = partitionedGroups; } } catch (e) { console.error("[SimMode] Error:", e); } if (groups.length === 0) { setLoad(false); showToast(L.msg_dup_none); return; } groups.sort((a, b) => (b._sim - a._sim) || (b.ids.length - a.ids.length)); S.analyzeSimGroups = groups; viewItems = Array.from(assigned).map(id => { const node = nodeMap.get(id); return { id: node.id, kind: 'drive#folder', name: node.name, icon_link: node.icon_link, starred: node.starred, tags: node.tags, size: node.size.toString(), _pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home, _lineage: node.lineage, modified_time: new Date(getServerNow()).toISOString(), parent_id: node.parentId }; }); } else { updateLoadTxt(L.str_rendering); const uniqueResults = Array.from(new Set(largeFolders)); const kw = gmGet('pk_analyze_last_keyword', ''); const kwList = kw ? kw.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : []; let filteredResults = uniqueResults.filter(n => n.size >= minBytes && (maxBytes === 0 || n.size <= maxBytes)); if (kwList.length > 0) { filteredResults = filteredResults.filter(node => !kwList.some(k => (node.name || "").toLowerCase().includes(k))); } filteredResults.sort((a, b) => b.size - a.size); if (filteredResults.length === 0) { setLoad(false); let rangeStr = maxBytes > 0 ? `${fmtSize(minBytes)} - ${fmtSize(maxBytes)}` : `≥ ${fmtSize(minBytes)}`; showAlert(L.msg_analyze_no_large_folders.replace('{s}', rangeStr)); return; } viewItems = filteredResults.map(node => ({ id: node.id, kind: 'drive#folder', name: node.name, icon_link: node.icon_link, starred: node.starred, tags: node.tags, size: node.size.toString(), _pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home, _lineage: node.lineage, modified_time: new Date(getServerNow()).toISOString(), parent_id: node.parentId })); } S.analyzeMode = true; S.hasShownAnaWarn = false; S.sort = 'size'; S.dir = 1; S.isFlattened = false; S.dupMode = false; S.analyzeResultItems = [...viewItems]; S.analyzeMap = nodeMap; S.path = [{ id: 'analyze_root', name: L.str_analyze_results }]; renderCrumb(); S.items = viewItems; S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); S.sel.clear(); UI.scan.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; refresh(); updateStat(); setTimeout(() => { setLoad(false); isGUISensitive = false; }, 200); } }; } function showAnalyzeResultModal(list, threshold) { const L = getStrings(); const modalHtml = `

${L.title_analyze_result}

${L.msg_analyze_summary_fmt.replace('{n}', list.length).replace('{s}', threshold)}
${L.col_path_name}
${L.col_size}
${L.col_action}
`; const m = showModal(modalHtml); const modalBox = m.querySelector('.pk-modal'); modalBox.style.width = "800px"; modalBox.style.height = "80vh"; const container = m.querySelector('#pk_analyze_list'); const fragment = document.createDocumentFragment(); list.forEach(item => { const row = document.createElement('div'); row.style.cssText = "display:grid; grid-template-columns: 1fr 100px 80px; padding:10px 12px; border-bottom:1px solid var(--pk-bd); font-size:13px; align-items:center;"; const fullPath = item.path; const name = item.name; const parentPath = fullPath.substring(0, fullPath.lastIndexOf(name)); const sizeNum = Number(item.size); row.innerHTML = `
${esc(name)}
${esc(parentPath || '/')}
${fmtSize(sizeNum)}
`; row.querySelector('button').onclick = () => { m.remove(); const wasAnalyzeMode = S.analyzeMode; const cleanLineage = item.lineage.filter(x => x.id); S.analyzeMode = false; S.isFlattened = false; S.dupMode = false; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; S.path =[{ id: '', name: getStrings().btn_nav_home }, ...cleanLineage]; if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal && wasAnalyzeMode && typeof S.wasGlobalChecked !== 'undefined') { UI.chkGlobal.checked = S.wasGlobalChecked; } if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.scan) UI.scan.style.display = 'flex'; load(); }; fragment.appendChild(row); }); container.appendChild(fragment); m.querySelector('#pk_analyze_close').onclick = () => m.remove(); } if (UI.btnExport) { UI.btnExport.onclick = async () => { if (S.trashMode) return; const curFolderId = S.path[S.path.length - 1].id || ''; if (isPathBusy(curFolderId)) { showAlert(L.msg_op_blocked_exporting); return; } const format = await new Promise((resolve) => { const m = showModal(`

${L.title_export_format}

${L.lbl_export_current}
${L.opt_tree_view}
Root\n├─ Folder 1\n│ ├─ Folder 1-1\n│ └─ Folder 1-2\n└─ Folder 2\n └─ Folder 2-1
${L.opt_list_view}
Root/Folder 1\nRoot/Folder 1/Folder 1-1\nRoot/Folder 1/Folder 1-2\nRoot/Folder 2\nRoot/Folder 2/Folder 2-1
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = '520px'; modalBox.style.height = 'auto'; modalBox.style.minHeight = 'auto'; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' }); } let selectedFormat = 'tree'; m.querySelectorAll('.pk-exp-card').forEach(card => { card.onclick = () => { m.querySelectorAll('.pk-exp-card').forEach(c => { c.style.borderColor = 'var(--pk-bd)'; c.querySelector('.pk-exp-title').style.color = 'var(--pk-fg)'; c.querySelector('.pk-exp-check').style.display = 'none'; c.classList.remove('act'); }); card.style.borderColor = 'var(--pk-pri)'; card.querySelector('.pk-exp-title').style.color = 'var(--pk-pri)'; card.querySelector('.pk-exp-check').style.display = 'flex'; card.classList.add('act'); selectedFormat = card.dataset.fmt; }; }); m.querySelector('#exp_cancel').onclick = () => { m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); }; m.querySelector('#exp_confirm').onclick = () => { m.remove(); resolve(selectedFormat); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#exp_confirm').click(); } }); }); if (!format) return; setLoad(true); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); }; const rootNode = S.path[S.path.length - 1]; const startNodes =[{ id: rootNode.id || '', name: rootNode.name || L.btn_nav_home, lineage: [], retryCount: 0 }]; const itemTree = new Map(); try { await coreRecursiveEngine(startNodes, { signal: signal, onFolder: (folder, filesInFolder) => { itemTree.set(folder.id, [...filesInFolder]); if (typeof globalCache !== 'undefined' && !globalCache.has(folder.id)) { globalCache.set(folder.id, [...filesInFolder]); } indexParents(folder.id, folder.name, filesInFolder); }, onProgress: (st) => { updateLoadTxt(`${L.msg_exporting}\n${L.str_folders}: ${st.folders} | ${L.str_files}: ${st.files}`); } }); if (!isRunning || signal.aborted || myScanId !== S.scanId) return; updateLoadTxt(L.str_processing); await sleep(50); const rootName = startNodes[0].name; let outputLines =[]; const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); const sortItems = (items) => { return items.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1; return collator.compare(a.name, b.name); }); }; if (format === 'tree') { outputLines.push(rootName); const buildTree = (parentId, prefix) => { const children = itemTree.get(parentId); if (!children || children.length === 0) return; const sorted = sortItems(children); for (let i = 0; i < sorted.length; i++) { const child = sorted[i]; const isLast = (i === sorted.length - 1); const connector = isLast ? '└─ ' : '├─ '; outputLines.push(prefix + connector + child.name); if (child.kind === 'drive#folder') { const childPrefix = prefix + (isLast ? ' ' : '│ '); buildTree(child.id, childPrefix); } } }; buildTree(startNodes[0].id, ''); } else { const buildList = (parentId, currentPath) => { const children = itemTree.get(parentId); if (!children || children.length === 0) return; const sorted = sortItems(children); for (let i = 0; i < sorted.length; i++) { const child = sorted[i]; const fullPath = currentPath ? currentPath + '/' + child.name : child.name; outputLines.push(fullPath); if (child.kind === 'drive#folder') { buildList(child.id, fullPath); } } }; buildList(startNodes[0].id, rootName); } const outputText = outputLines.join('\r\n'); const blob = new Blob([outputText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const safeRootName = rootName.replace(/[\\/:*?"<>|]/g, '_'); const a = document.createElement('a'); a.href = url; a.download = `${safeRootName}_${format}.txt`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error}: ${e.message}`); } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; isGUISensitive = false; } } }; } UI.btnNewFolder.onclick = async () => { const name = await showPrompt(L.msg_newfolder_prompt, ''); if (!name) return; isGUISensitive = true; const cur = S.path[S.path.length - 1]; const cacheKey = cur.id || 'root'; try { await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: 'drive#folder', parent_id: cur.id || '', name: name }) }); if (cur.id) gmSet('pk_fmod_' + cur.id, new Date(getServerNow()).toISOString()); await sleep(300); S.cache.delete(cacheKey); if (typeof globalCache !== 'undefined') globalCache.delete(cacheKey); if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(cacheKey === 'root' ? '' : cacheKey); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (cacheKey !== 'root') { scannedFolderIds.delete(cacheKey); } if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); load(false, true); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { isGUISensitive = false; } }; UI.btnCopy.onclick = async () => { if (S.sel.size === 0) return; if (S.sel.size > 10000) { showToast(L.str_copying); await sleep(10); } const itemList = []; let count = 0; for (const id of S.sel) { const item = S.itemMap.get(id); if (item) itemList.push(item); count++; if (count % 10000 === 0) await sleep(0); } S.clipItems = itemList; S.clipType = 'copy'; const curId = S.path[S.path.length - 1].id || ''; S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId; setLoad(false); UI.btnPaste.disabled = false; showToast(L.msg_copy_done); }; UI.btnCut.onclick = async () => { if (S.sel.size === 0) return; const itemList = []; let count = 0; for (const id of S.sel) { const item = S.itemMap.get(id); if (!S.trashMode && isSystemItem(item)) { continue; } itemList.push(item); count++; if (count % 10000 === 0) await sleep(0); } if (itemList.length === 0) return; if (itemList.length > 10000) { showToast(L.str_moving); await sleep(10); } S.clipItems = itemList; S.clipType = 'move'; const curId = S.path[S.path.length - 1].id || ''; S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId; setLoad(false); UI.btnPaste.disabled = false; showToast(L.msg_cut_done); }; UI.btnPaste.onclick = async () => { if (!S.clipItems || S.clipItems.length === 0) { showToast(L.msg_paste_empty, 'error'); return; } if (S.movingIds && S.movingIds.size > 0) { showAlert(L.msg_op_blocked_moving); return; } const items = S.clipItems; const type = S.clipType; const srcId = S.clipSourceParentId; const destId = S.path[S.path.length - 1].id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; S.clipItems = []; S.clipType = ''; updateStat(); const targetFolderName = S.path[S.path.length - 1].name || "Target"; await executeFileTransfer(items, type, normalize(srcId), normalize(destId), targetFolderName); }; UI.btnRename.onclick = async () => { if (S.sel.size !== 1) return; const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (!item || (!S.trashMode && isSystemItem(item))) return; const newName = await showPrompt(L.msg_rename_prompt, item.name, L.modal_rename_title); if (!newName || newName === item.name) return; let progressTask = null; try { progressTask = FloatBarManager.create(L.str_renaming); await apiAction(`/${id}`, { name: newName }); item.name = newName; const nowIso = new Date(getServerNow()).toISOString(); item.modified_time = nowIso; if (item.kind === 'drive#folder') gmSet('pk_fmod_' + item.id, nowIso); const parentIdForFmod = item.parent_id || 'root'; gmSet('pk_fmod_' + parentIdForFmod, nowIso); const row = UI.in.querySelector(`.pk-row[data-id="${id}"]`); if (row && row.lastElementChild) { row.lastElementChild.textContent = fmtDate(nowIso); row.lastElementChild.style.transition = 'color 0.3s'; row.lastElementChild.style.color = 'var(--pk-pri)'; setTimeout(() => { if(row.lastElementChild) row.lastElementChild.style.color = ''; }, 2000); } const pid = item.parent_id || 'root'; if (typeof globalCache !== 'undefined') { if (globalCache.has(pid)) { const list = globalCache.get(pid); const target = list.find(f => f.id === id); if (target) target.name = newName; } globalDirtyFolders.add(pid === 'root' ? '' : pid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (S.lastGlobalResults && S.lastGlobalResults.length > 0) { const globalTarget = S.lastGlobalResults.find(f => f.id === id); if (globalTarget) globalTarget.name = newName; } if (S.analyzeResultItems) { const anaItem = S.analyzeResultItems.find(x => x.id === id); if (anaItem) anaItem.name = newName; } if (S.analyzeMap && S.analyzeMap.has(id)) { S.analyzeMap.get(id).name = newName; } if (S.dupMode) renderDupView(); else refresh(); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { if (progressTask) progressTask.destroy(); } }; UI.btnBulkRename.onclick = () => { if (S.sel.size < 2) return; if (S.movingIds && S.movingIds.size > 0) { const hasConflict = Array.from(S.sel).some(id => S.movingIds.has(id)); if (hasConflict) { showAlert(L.msg_op_blocked_analyzing); return; } } const getSmartDisplayHTML = (fullStr, hlStr) => { if (!hlStr || hlStr.indexOf('§§§MATCH_START§§§') === -1) { return esc(fullStr); } const parts = hlStr.split(/(§§§MATCH_START§§§.*?§§§MATCH_END§§§)/g); let result = ""; const PRE_CTX = 12; const SUF_CTX = 80; parts.forEach((part, index) => { if (part.startsWith('§§§MATCH_START§§§')) { const content = part.replace(/§§§MATCH_START§§§|§§§MATCH_END§§§/g, ''); result += `${esc(content)}`; } else { let text = part; if (index === 0) { if (text.length > PRE_CTX + 3) { text = "..." + text.slice(-PRE_CTX); } } else if (index === parts.length - 1) { if (text.length > SUF_CTX) { text = text.slice(0, SUF_CTX) + "..."; } } else { if (text.length > 20) { text = text.slice(0, 8) + ".." + text.slice(-8); } } result += esc(text); } }); return result; }; const extractKeyword = (fileName) => { const fc2Regex = /(?:FC2|FC)(?:[-_. ]*PPV)?[-_. @]*(\d{5,8})(?:[-_. ]*(?:part|pt|cd)?[-_. ]?(\d{1,2}|[a-e]))?(?![a-z\d])/i; const fc2Match = fileName.match(fc2Regex); if (fc2Match) { const id = fc2Match[1]; const part = fc2Match[2]; return (part && part.trim()) ? `FC2-PPV-${id}-${part.toUpperCase()}` : `FC2-PPV-${id}`; } return null; }; const inputStyle = ` width:100%; height:44px; padding:0 15px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; font-weight:600; outline:none; transition:border-color 0.2s; box-sizing:border-box; `; const labelStyle = ` position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; line-height:1; font-size:11px; color:var(--pk-pri); font-weight:bold; pointer-events:none; z-index:1; `; const m = showModal(`

${L.modal_rename_multi_title}

${L.rn_stat.replace('{n}', S.sel.size).replace('{m}', '0')}
${L.label_replace_find}
${L.label_replace_to}
${L.col_type}
${L.col_old}
${L.col_new}
${L.rn_tip_wait}
${L.lbl_rn_preview_title}
`); const modalBox = m.querySelector('.pk-modal'); Object.assign(modalBox.style, { width: '800px', maxWidth: '95vw', padding: '30px', borderRadius: '12px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); const radios = m.querySelectorAll('input[name="rn_mode"]'); const groupsRaw = { jav: m.querySelector('#group_jav'), pattern: m.querySelector('#group_pattern'), replace: m.querySelector('#group_replace') }; const inpPattern = m.querySelector('#rn_pattern'); const inpFind = m.querySelector('#rn_find'); const inpRep = m.querySelector('#rn_rep'); const chkRegex = m.querySelector('#rn_regex'); const chkCase = m.querySelector('#rn_case_sense'); const chkIncludeExt = m.querySelector('#rn_include_ext'); const btnApply = m.querySelector('#rn_apply'); const rnVp = m.querySelector('#pk-rn-vp'); const rnIn = m.querySelector('#pk-rn-in'); const txtStatNum = m.querySelector('#rn_stat_num'); const bindFocus = (el) => { el.onfocus = () => el.style.borderColor = 'var(--pk-pri)'; el.onblur = () => el.style.borderColor = 'var(--pk-bd)'; }; [inpPattern, inpFind, inpRep].forEach(bindFocus); const updateInputs = () => { const mode = m.querySelector('input[name="rn_mode"]:checked').value; const groups = { 'replace': m.querySelector('#group_replace'), 'pattern': m.querySelector('#group_pattern'), 'jav': m.querySelector('#group_jav'), 'format': m.querySelector('#group_format'), 'ad_remove': m.querySelector('#group_ad_remove'), 'ext_fix': m.querySelector('#group_ext_fix') }; Object.keys(groups).forEach(k => { if (groups[k]) { let displayStyle = 'block'; if (k === 'replace' || k === 'format') displayStyle = 'flex'; groups[k].style.display = (k === mode) ? displayStyle : 'none'; } }); plannedChanges = []; rnDisplay = []; previewSelectedIds.clear(); const initialTip = (mode === 'jav') ? L.rn_tip_jav : L.rn_tip_wait; rnIn.innerHTML = `
${initialTip}
`; txtStatNum.innerText = "0"; btnApply.disabled = true; const cbWrapper = m.querySelector('#rn_cb_wrapper'); const cbAll = m.querySelector('#rn_cb_all'); if (cbWrapper) cbWrapper.style.visibility = 'hidden'; if (cbAll) { cbAll.checked = false; cbAll.indeterminate = false; } generatePreview(); }; radios.forEach(r => r.onchange = updateInputs); const setupInputHistory = (inputEl, storageKey) => { const pop = document.createElement('div'); pop.className = 'pk-hist-pop'; pop.style.cssText = "position:absolute; top:calc(100% + 5px); left:0; right:0; z-index:9999; display:none; flex-direction:column;"; inputEl.parentNode.appendChild(pop); const loadHist = () => { try { return JSON.parse(gmGet(storageKey, '[]')); } catch { return []; } }; const saveHist = (val) => { if (val === null || val === undefined) return; let list = loadHist(); list = list.filter(x => x !== val); list.unshift(val); if (list.length > 3) list = list.slice(0, 3); gmSet(storageKey, JSON.stringify(list)); }; const render = () => { const list = loadHist(); if (list.length === 0) { pop.style.display = 'none'; return; } document.querySelectorAll('.pk-hist-pop').forEach(p => p.style.display = 'none'); let html = `
${L.title_search_hist}${L.btn_clear_hist}
`; list.forEach(txt => { const displayTxt = txt === "" ? "(Empty)" : (txt.replace(/\s/g, '') === "" ? `"${txt}"` : txt); html += `
${esc(displayTxt)}
`; }); pop.innerHTML = html; pop.style.display = 'flex'; pop.querySelector('#pk-hist-del').onclick = (e) => { e.stopPropagation(); gmSet(storageKey, '[]'); pop.style.display = 'none'; }; pop.querySelectorAll('.pk-select-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const val = el.getAttribute('data-raw'); inputEl.value = val; pop.style.display = 'none'; generatePreview(); }; }); }; inputEl.addEventListener('focus', render); inputEl.addEventListener('click', (e) => { e.stopPropagation(); render(); }); const closeHandler = (e) => { if (!inputEl || !pop || !pop.parentNode) return; if (!inputEl.contains(e.target) && !pop.contains(e.target)) pop.style.display = 'none'; }; setTimeout(() => document.addEventListener('click', closeHandler), 0); return { save: saveHist }; }; const findHist = setupInputHistory(inpFind, 'pk_bn_find_hist'); const repHist = setupInputHistory(inpRep, 'pk_bn_rep_hist'); const RN_ROW_HEIGHT = 40; const RN_BUFFER = 15; let rnDisplay = []; let plannedChanges = []; let previewSelectedIds = new Set(); let isRnScrollScheduled = false; let currentPreviewId = 0; const VALID_EXTS_LIST = [ 'mp4', 'mkv', 'avi', 'mov', 'wmv', 'm4v', 'flv', '3gp', 'webm', 'ts', 'm2ts', 'mts', 'vob', 'mpg', 'mpeg', 'rm', 'rmvb', 'asf', 'divx', 'f4v', 'ogv', 'm2v', 'mpe', 'mp3', 'aac', 'flac', 'wav', 'ogg', 'm4a', 'wma', 'opus', 'ape', 'alac', 'aiff', 'mid', 'midi', 'amr', 'mka', 'dts', 'ac3', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'tif', 'tiff', 'heic', 'raw', 'ico', 'psd', 'ai', 'eps', 'cdr', 'srt', 'ass', 'ssa', 'vtt', 'smi', 'sub', 'idx', 'sup', 'lrc', 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso', 'img', 'dmg', 'pkg', 'apk', 'ipa', 'exe', 'msi', 'torrent', 'nzb', 'pdf', 'txt', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'epub', 'mobi', 'azw3', 'cbz', 'cbr', 'md', 'rtf', 'csv', 'log', 'ini', 'cfg', 'html', 'htm', 'css', 'js', 'json', 'xml', 'php', 'py', 'java', 'c', 'cpp' ]; const VALID_EXTS = new Set(VALID_EXTS_LIST); const recalcPatternNames = () => { const mode = m.querySelector('input[name="rn_mode"]:checked').value; if (mode !== 'pattern') return; const pattern = inpPattern.value; let counter = 1; rnDisplay.forEach(item => { const originalItem = S.itemMap.get(item.id); const isFolder = originalItem?.kind === 'drive#folder'; const isSelected = previewSelectedIds.has(item.id); if (isSelected && !isFolder) { let ext = ''; const oldName = item.old; const lastDotIndex = oldName.lastIndexOf('.'); if (lastDotIndex > 0) { const potentialExt = oldName.substring(lastDotIndex + 1).toLowerCase(); if (VALID_EXTS.has(potentialExt)) ext = '.' + potentialExt; } const newName = pattern.replace(/{n}/g, String(counter).padStart(2, '0')) + ext; item.new = newName; item.newNameHTML = `${esc(newName)}`; counter++; } else { item.new = item.old; item.newNameHTML = `${esc(item.old)}`; } }); }; const updateHeaderCheckbox = () => { const cbAll = m.querySelector('#rn_cb_all'); if (!cbAll) return; const total = plannedChanges.length; const count = previewSelectedIds.size; cbAll.checked = total > 0 && count === total; cbAll.indeterminate = count > 0 && count < total; const validCount = rnDisplay.filter(c => c.new !== c.old && previewSelectedIds.has(c.id)).length; btnApply.disabled = validCount === 0; txtStatNum.style.display = 'inline'; txtStatNum.innerHTML = `${validCount}/${total}`; }; const renderRNVisible = () => { const top = rnVp.scrollTop; const h = rnVp.clientHeight; const totalHeight = rnDisplay.length * RN_ROW_HEIGHT; rnIn.style.height = `${totalHeight}px`; const start = Math.max(0, Math.floor(top / RN_ROW_HEIGHT) - RN_BUFFER); const end = Math.min(rnDisplay.length, Math.ceil((top + h) / RN_ROW_HEIGHT) + RN_BUFFER); rnIn.innerHTML = ''; const fragment = document.createDocumentFragment(); const rowStyle = ` display: flex; align-items: center; height: ${RN_ROW_HEIGHT}px; padding-right: 20px; border-bottom: 1px dashed #f0f0f0; box-sizing: border-box; font-size: 13px; color: var(--pk-fg); `; for (let i = start; i < end; i++) { const c = rnDisplay[i]; if (!c) continue; const row = document.createElement('div'); row.style.cssText = `position:absolute; top:${i * RN_ROW_HEIGHT}px; width:100%;` + rowStyle; let finalDisplayHTML = esc(c.old); if (c.hl_old) { finalDisplayHTML = getSmartDisplayHTML(c.old, c.hl_old); } const isChecked = previewSelectedIds.has(c.id); const it = S.itemMap.get(c.id); let iconHtml = ''; let thumbAttr = ''; if (it) { const isFolder = it.kind === 'drive#folder'; const mime = (it.mime_type || '').toLowerCase(); const isMedia = mime.startsWith('video/') || mime.startsWith('image/'); const hasCover = it.thumbnail_link && it.thumbnail_link !== it.icon_link; const fallbackSvg = getIcon(it).replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"'); if (hasCover) { thumbAttr = `data-pk-thumb="${it.thumbnail_link}"`; } if (!isFolder && isMedia && hasCover) { iconHtml = ``; const secondFallback = it.icon_link ? `${fallbackSvg}` : fallbackSvg; iconHtml += `${secondFallback}`; } else { const iconSrc = it.icon_link; iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; } } row.innerHTML = `
${iconHtml}
${finalDisplayHTML}
${c.newNameHTML}
${c.conflict ? `
${L.str_name_conflict}
` : ''}
`; const toggleRow = () => { const targetId = c.id; if (previewSelectedIds.has(targetId)) previewSelectedIds.delete(targetId); else previewSelectedIds.add(targetId); recalcPatternNames(); renderRNVisible(); updateHeaderCheckbox(); }; row.onclick = () => { toggleRow(); }; fragment.appendChild(row); } rnIn.appendChild(fragment); }; rnVp.onscroll = () => { if (!isRnScrollScheduled) { requestAnimationFrame(() => { renderRNVisible(); isRnScrollScheduled = false; }); isRnScrollScheduled = true; } }; const handlePreviewResults = (changes, error) => { if (error) { rnIn.innerHTML = `
❌ ${L.err_worker}: ${esc(error)}
`; return; } plannedChanges = changes; previewSelectedIds = new Set(changes.map(c => c.id)); rnDisplay = changes.map(c => { let newH = esc(c.new); if (c.new !== c.old) { newH = `${newH}`; } return { id: c.id, old: c.old, new: c.new, hl_old: c.hl_old, newNameHTML: newH, conflict: c.conflict }; }); recalcPatternNames(); const cbWrapper = m.querySelector('#rn_cb_wrapper'); const cbAll = m.querySelector('#rn_cb_all'); if (rnDisplay.length === 0) { rnIn.innerHTML = `
${L.rn_tip_none}
`; txtStatNum.innerText = "0"; btnApply.disabled = true; if (cbWrapper) cbWrapper.style.visibility = 'hidden'; if (cbAll) { cbAll.checked = false; cbAll.disabled = true; } } else { rnVp.scrollTop = 0; renderRNVisible(); updateHeaderCheckbox(); if (cbWrapper) cbWrapper.style.visibility = 'visible'; if (cbAll) cbAll.disabled = false; btnApply.disabled = false; } }; const javState = { isRunning: false, runId: 0, signature: '', cache: new Map(), completed: 0, total: 0, ui: null, activePlannedMap: new Map(), activeNames: new Set() }; const generatePreview = async () => { const myPreviewId = ++currentPreviewId; const mode = m.querySelector('input[name="rn_mode"]:checked').value; rnDisplay = []; plannedChanges = []; rnIn.innerHTML = ''; const pattern = inpPattern.value; const findStr = inpFind.value; const repStr = inpRep.value || ''; const useRegex = chkRegex.checked; const useIncludeExt = chkIncludeExt.checked; const caseMode = selectedCase; const widthMode = selectedWidth; const useCaseSense = m.querySelector('#rn_case_sense').checked; let isRuleActive = true; if (mode === 'replace') isRuleActive = !!findStr; else if (mode === 'format') isRuleActive = !!(caseMode || widthMode); else if (mode === 'pattern') isRuleActive = !!pattern; if (!isRuleActive && mode !== 'jav' && mode !== 'ad_remove' && mode !== 'ext_fix') { plannedChanges = []; rnDisplay = []; previewSelectedIds.clear(); rnIn.innerHTML = `
${L.rn_tip_wait}
`; txtStatNum.innerText = "0"; btnApply.disabled = true; const cbWrapper = m.querySelector('#rn_cb_wrapper'); if (cbWrapper) cbWrapper.style.visibility = 'hidden'; return; } if (mode === 'replace') { if (findStr) findHist.save(findStr); if (repStr) repHist.save(repStr); } const cbWrapper = m.querySelector('#rn_cb_wrapper'); if (cbWrapper) cbWrapper.style.visibility = 'hidden'; rnIn.innerHTML = `
${L.str_calc_changes}
`; btnApply.disabled = true; txtStatNum.innerText = "..."; plannedChanges = []; rnDisplay = []; previewSelectedIds = new Set(); const cbAll = m.querySelector('#rn_cb_all'); if(cbAll) { cbAll.checked = false; cbAll.indeterminate = false; } const selectedIds = Array.from(S.sel); const items = S.display.filter(i => !i.isHeader); if (false) { const targetIds =[]; for (let i = 0; i < items.length; i++) { const item = items[i]; const id = item.id; if (!S.sel.has(id)) continue; if (isSystemItem(item)) continue; const isFolder = item.kind === 'drive#folder'; const mime = (item.mime_type || '').toLowerCase(); const duration = (item.params && item.params.duration) || 0; const isVideo = !isFolder && (mime.startsWith('video/') || duration > 0); if ((isFolder || isVideo) && extractKeyword(item.name)) { targetIds.push(id); } } if (targetIds.length === 0) { rnIn.innerHTML = `
${L.rn_tip_none}
`; txtStatNum.innerText = "0"; btnApply.disabled = true; javState.isRunning = false; return; } const sigIds = targetIds.length > 200 ? targetIds.slice(0, 100).concat(targetIds.slice(-100)) : targetIds; const currentSignature = [...sigIds].sort().join('|') + `_len_${targetIds.length}`; const isSameSelection = (currentSignature === javState.signature); if (!isSameSelection) { javState.runId++; javState.signature = currentSignature; javState.cache.clear(); javState.completed = 0; javState.total = targetIds.length; javState.isRunning = false; } const changes = targetIds.map(id => { const item = S.itemMap.get(id); const cached = javState.cache.get(id); if (cached) { return { id: item.id, old: item.name, new: cached.new, hl_old: cached.hlOld, conflict: cached.conflict, parent_id: item.parent_id }; } else { return { id: item.id, old: item.name, new: item.name, hl_old: null, conflict: false, parent_id: item.parent_id }; } }); handlePreviewResults(changes, null); const plannedMap = new Map(); plannedChanges.forEach(c => plannedMap.set(c.id, c)); const displayMap = new Map(); javState.activePlannedMap.clear(); plannedChanges.forEach(c => javState.activePlannedMap.set(c.id, c)); javState.activeNames = new Set(items.map(i => i.name)); rnDisplay.forEach(d => { displayMap.set(d.id, d); const cached = javState.cache.get(d.id); if (cached) { if (cached.new === d.old) { d.newNameHTML = `${L.str_jav_no_match}`; } else { d.newNameHTML = `${esc(cached.new)}`; } if (cached.hlOld) { d.hl_old = cached.hlOld; } } else { d.newNameHTML = ` ${L.str_jav_querying}`; } }); if (!document.getElementById('pk-spin-style')) { const style = document.createElement('style'); style.id = 'pk-spin-style'; style.innerHTML = `@keyframes pk-spin { 100% { transform: rotate(360deg); } }`; document.head.appendChild(style); } const updateProgress = () => { if(!txtStatNum) return; txtStatNum.style.display = 'inline-flex'; txtStatNum.style.alignItems = 'baseline'; txtStatNum.style.gap = '8px'; txtStatNum.innerHTML = `
${javState.completed} / ${javState.total} `; }; javState.ui = { displayMap: displayMap, render: renderRNVisible, updateProgress: updateProgress }; renderRNVisible(); if (isSameSelection && (javState.isRunning || javState.completed === javState.total)) { updateProgress(); if (javState.completed === javState.total) { if (rnVp) rnVp.scrollTop = 0; renderRNVisible(); btnApply.disabled = false; updateHeaderCheckbox(); const validCount = plannedChanges.filter(c => c.new !== c.old && !c.conflict).length; txtStatNum.innerHTML = `${validCount} / ${javState.total}`; } return; } javState.isRunning = true; const currentRunId = javState.runId; btnApply.disabled = true; updateProgress(); const allNames = new Set(items.map(i => i.name)); const queue = targetIds.filter(id => !javState.cache.has(id)); if (queue.length === 0) { javState.isRunning = false; btnApply.disabled = false; updateHeaderCheckbox(); return; } let lastRenderTime = 0; const processItem = async (id) => { if (currentRunId !== javState.runId || !document.body.contains(m)) return; const item = S.itemMap.get(id); if (!item) { javState.completed++; updateProgress(); return; } const oldName = item.name; let ext = ''; if (item.kind !== 'drive#folder') { const extIndex = oldName.lastIndexOf('.'); if (extIndex > 0) { const potentialExt = oldName.substring(extIndex + 1).toLowerCase(); if (VALID_EXTS.has(potentialExt)) { ext = oldName.substring(extIndex); } } } const code = extractKeyword(oldName); let newName = oldName; let hlOld = null; let displayHTML = `${L.str_jav_no_match}`; if (code) { try { const parts = code.split(/[^a-zA-Z0-9]+/).filter(p => p.length > 0); if (parts.length > 0) { const escapedParts = parts.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const pattern = `(${escapedParts.join('|')})`; const re = new RegExp(pattern, 'gi'); hlOld = oldName.replace(re, (m) => '§§§MATCH_START§§§' + m + '§§§MATCH_END§§§'); } } catch(e) {} newName = code + ext; displayHTML = `${esc(newName)}`; } let isConflict = false; if (newName !== oldName) { if (javState.activeNames.has(newName)) isConflict = true; else javState.activeNames.add(newName); } javState.cache.set(id, { new: newName, hlOld, conflict: isConflict }); javState.completed++; const currentMode = m.querySelector('input[name="rn_mode"]:checked').value; if (currentMode === 'jav' && currentRunId === javState.runId && javState.ui) { const displayItem = javState.ui.displayMap.get(id); const planItem = javState.activePlannedMap.get(id); if (displayItem) { displayItem.new = newName; displayItem.hl_old = hlOld; displayItem.newNameHTML = displayHTML; if (isConflict) displayItem.conflict = true; if (planItem) { planItem.new = newName; planItem.hl_old = hlOld; planItem.conflict = isConflict; } javState.ui.updateProgress(); const now = Date.now(); if (now - lastRenderTime > 500) { javState.ui.render(); updateHeaderCheckbox(); lastRenderTime = now; } } } }; const CONCURRENCY = 6; await Promise.all(Array.from({ length: CONCURRENCY }).map(async () => { while (queue.length > 0) { if (currentRunId !== javState.runId) break; const id = queue.shift(); if (id) await processItem(id); } })); if (currentRunId === javState.runId) { javState.isRunning = false; const currentMode = m.querySelector('input[name="rn_mode"]:checked').value; if (currentMode === 'jav') { if (rnVp) rnVp.scrollTop = 0; renderRNVisible(); btnApply.disabled = false; updateHeaderCheckbox(); const validCount = plannedChanges.filter(c => c.new !== c.old && !c.conflict).length; txtStatNum.style.display = 'inline'; txtStatNum.innerHTML = `${validCount}/${javState.total}`; } } return; } const workerFunction = function(e) { try { const { selectedIds, items, mode, pattern, findStr, repStr, useRegex, validExtsList, caseMode, widthMode, useCaseSense, useIncludeExt } = e.data; const changes =[]; const idSet = new Set(selectedIds); const allNames = new Set(); items.forEach(i => allNames.add(i.name)); const VALID_EXTS = new Set(validExtsList); const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const extractKeyword = (fileName) => { const fc2Regex = /(?:FC2|FC)(?:[-_. ]*PPV)?[-_. @]*(\d{5,8})(?:[-_. ]*(?:part|pt|cd)?[-_. ]?(\d{1,2}|[a-e]))?(?![a-z\d])/i; const fc2Match = fileName.match(fc2Regex); if (fc2Match) { const id = fc2Match[1]; const part = fc2Match[2]; return (part && part.trim()) ? `FC2-PPV-${id}-${part.toUpperCase()}` : `FC2-PPV-${id}`; } return null; }; const toHalf = (str) => str.replace(/[!-~]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0)).replace(/ /g, ' '); const toFull = (str) => str.replace(/[!-~]/g, c => String.fromCharCode(c.charCodeAt(0) + 0xFEE0)).replace(/ /g, ' '); const toTitle = (str) => str.replace(/\b\w/g, c => c.toUpperCase()); let regex = null; if (mode === 'replace' && findStr) { try { const flags = useCaseSense ? 'g' : 'gi'; const p = useRegex ? findStr : escapeRegExp(findStr); regex = new RegExp(p, flags); } catch (err) {} } let counter = 1; for (let i = 0; i < items.length; i++) { const item = items[i]; if (!idSet.has(item.id)) continue; const oldName = item.name; let newName = oldName, hlOld = null; if (mode === 'pattern') { if (item.kind === 'drive#folder') continue; let ext = ''; const lastDotIndex = oldName.lastIndexOf('.'); if (lastDotIndex > 0) { const potentialExt = oldName.substring(lastDotIndex + 1).toLowerCase(); if (VALID_EXTS.has(potentialExt)) ext = '.' + potentialExt; } newName = pattern.replace(/{n}/g, String(counter).padStart(2, '0')) + ext; counter++; } else if (mode === 'replace') { if (findStr && regex) { let targetStr = oldName; let extStr = ""; if (!useIncludeExt && item.kind !== 'drive#folder') { const lastDot = oldName.lastIndexOf('.'); if (lastDot > 0) { targetStr = oldName.substring(0, lastDot); extStr = oldName.substring(lastDot); } } if (regex.test(targetStr)) { regex.lastIndex = 0; const newBase = targetStr.replace(regex, repStr); regex.lastIndex = 0; const hlBase = targetStr.replace(regex, (m) => '§§§MATCH_START§§§' + m + '§§§MATCH_END§§§'); newName = newBase + extStr; hlOld = hlBase + extStr; } } } else if (mode === 'format') { let base = newName; let ext = ''; if (item.kind !== 'drive#folder') { const lastDot = newName.lastIndexOf('.'); if (lastDot > 0) { base = newName.substring(0, lastDot); ext = newName.substring(lastDot); } } if (widthMode === 'half') base = toHalf(base); else if (widthMode === 'full') base = toFull(base); if (caseMode === 'upper') base = base.toUpperCase(); else if (caseMode === 'lower') base = base.toLowerCase(); else if (caseMode === 'title') base = toTitle(base); newName = base + ext; } else if (mode === 'jav') { const code = extractKeyword(oldName); if (code) { let ext = '', ext_old = '', base_old = oldName; if (item.kind !== 'drive#folder') { const lastDotIndex = oldName.lastIndexOf('.'); if (lastDotIndex > 0) { const potentialExt = oldName.substring(lastDotIndex + 1).toLowerCase(); if (VALID_EXTS.has(potentialExt)) { ext = oldName.substring(lastDotIndex); ext_old = ext; base_old = oldName.substring(0, lastDotIndex); } } } newName = code + ext; try { const parts = code.split(/[^a-zA-Z0-9]+/).filter(p => p.length > 0); if (parts.length > 0) { const escapedParts = parts.map(p => escapeRegExp(p)); const pat = `(${escapedParts.join('|')})`; const re = new RegExp(pat, 'gi'); hlOld = base_old.replace(re, (m) => '§§§MATCH_START§§§' + m + '§§§MATCH_END§§§') + ext_old; } } catch(e) {} } } else if (mode === 'ad_remove') { let cleanName = oldName; cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, ''); cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, ''); const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])"; const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\s)', 'i'); cleanName = cleanName.replace(atRegex, ''); const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i'); cleanName = cleanName.replace(hyphenRegex, ''); cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, ''); cleanName = cleanName.replace(/^[-_. ,,::;;\p{Extended_Pictographic}]+/u, ''); const idxChnR_Fix = cleanName.indexOf('】'); const idxChnL_Check = cleanName.indexOf('【'); if (idxChnR_Fix > 0 && idxChnR_Fix <= 10 && (idxChnL_Check === -1 || idxChnL_Check > idxChnR_Fix)) { cleanName = '【' + cleanName; } const idxEngR_Fix = cleanName.indexOf(']'); const idxEngL_Check = cleanName.indexOf('['); if (idxEngR_Fix > 0 && idxEngR_Fix <= 10 && (idxEngL_Check === -1 || idxEngL_Check > idxEngR_Fix)) { cleanName = '[' + cleanName; } const idxBkR_Fix = cleanName.indexOf('》'); const idxBkL_Check = cleanName.indexOf('《'); if (idxBkR_Fix > 0 && idxBkR_Fix <= 10 && (idxBkL_Check === -1 || idxBkL_Check > idxBkR_Fix)) { cleanName = '《' + cleanName; } const idxAngR_Fix = cleanName.indexOf('>'); const idxAngL_Check = cleanName.indexOf('<'); if (idxAngR_Fix > 0 && idxAngR_Fix <= 10 && (idxAngL_Check === -1 || idxAngL_Check > idxAngR_Fix)) { cleanName = '<' + cleanName; } const idxChnParR_Fix = cleanName.indexOf(')'); const idxChnParL_Check = cleanName.indexOf('('); if (idxChnParR_Fix > 0 && idxChnParR_Fix <= 10 && (idxChnParL_Check === -1 || idxChnParL_Check > idxChnParR_Fix)) { cleanName = '(' + cleanName; } const idxEngParR_Fix = cleanName.indexOf(')'); const idxEngParL_Check = cleanName.indexOf('('); if (idxEngParR_Fix > 0 && idxEngParR_Fix <= 10 && (idxEngParL_Check === -1 || idxEngParL_Check > idxEngParR_Fix)) { cleanName = '(' + cleanName; } const idxCurR_Fix = cleanName.indexOf('}'); const idxCurL_Check = cleanName.indexOf('{'); if (idxCurR_Fix > 0 && idxCurR_Fix <= 10 && (idxCurL_Check === -1 || idxCurL_Check > idxCurR_Fix)) { cleanName = '{' + cleanName; } const cleanStack = (L, R) => { const chars = cleanName.split(''); const stack = []; const toRemove = new Set(); for (let i = 0; i < chars.length; i++) { const c = chars[i]; if (c === L) { stack.push(i); } else if (c === R) { if (stack.length > 0) stack.pop(); else toRemove.add(i); } } stack.forEach(i => toRemove.add(i)); if (toRemove.size > 0) { cleanName = chars.filter((_, i) => !toRemove.has(i)).join(''); } }; cleanStack('【', '】'); cleanStack('[', ']'); cleanStack('{', '}'); cleanStack('(', ')'); cleanStack('(', ')'); cleanStack('《', '》'); cleanStack('<', '>'); const quote2 = (cleanName.match(/'/g) || []).length; if (quote2 % 2 !== 0) cleanName = cleanName.replace(/"/, ''); const result = cleanName.trim(); const lastDot = oldName.lastIndexOf('.'); if (lastDot !== -1) { const ext = oldName.substring(lastDot); const extNoDot = ext.substring(1); if (!result || result === ext || result === extNoDot) { newName = oldName; } else { newName = result; } } else { if (!result) newName = oldName; else newName = result; } } else if (mode === 'ext_fix') { const mimeMap = { 'video/mp4': ['.mp4', '.m4v', '.f4v', '.mp4v', '.mov', '.avi', '.m4a', '.m4b'], 'video/x-matroska': ['.mkv', '.mk3d', '.mka', '.mks'], 'video/x-msvideo': ['.avi'], 'video/quicktime': ['.mov', '.qt', '.mp4', '.m4v'], 'video/x-flv': ['.flv'], 'video/webm': ['.webm'], 'video/mpeg': ['.mpg', '.mpeg', '.mpe', '.vob'], 'video/3gpp': ['.3gp', '.3g2', '.mp4'], 'video/mp2t': ['.ts', '.m2ts', '.mts'], 'video/x-m4v': ['.m4v', '.mp4'], 'video/x-ms-wmv': ['.wmv'], 'video/x-ms-asf': ['.asf', '.wmv', '.wma'], 'audio/mpeg': ['.mp3', '.mp2'], 'audio/mp4': ['.m4a', '.m4b', '.mp4'], 'audio/x-wav': ['.wav'], 'audio/flac': ['.flac'], 'audio/aac': ['.aac'], 'audio/ogg': ['.ogg', '.opus'], 'audio/x-ms-wma': ['.wma'], 'audio/webm': ['.weba'], 'image/jpeg': ['.jpg', '.jpeg', '.jpe', '.jif', '.jfif'], 'image/png': ['.png'], 'image/gif': ['.gif'], 'image/webp': ['.webp'], 'image/bmp': ['.bmp'], 'image/svg+xml': ['.svg'], 'image/heif': ['.heic', '.heif'], 'image/vnd.adobe.photoshop': ['.psd', '.abr'], 'image/x-icon': ['.ico'], 'image/vnd.microsoft.icon': ['.ico'], 'image/tiff': ['.tif', '.tiff', '.cr2', '.cr3', '.nef', '.dng', '.arw', '.orf', '.rw2', '.pef', '.sr2', '.raf'], 'application/postscript': ['.ps', '.eps', '.ai'], 'application/dicom': ['.dcm'], 'application/zip': [ '.zip', '.exe', '.pt', '.pth', '.apk', '.xapk', '.apks', '.obb', '.aar', '.aab', '.ipa', '.ipsw', '.wgt', '.docx', '.docm', '.dotx', '.dotm', '.xlsx', '.xlsm', '.xltx', '.xltm', '.pptx', '.pptm', '.potx', '.potm', '.vsdx', '.xmind', '.xlam', '.thmx', '.odt', '.ods', '.odp', '.oxps', '.xps', '.pages', '.numbers', '.key', '.xd', '.idml', '.zxp', '.fig', '.sketch', '.brush', '.brushset', '.3mf', '.usdz', '.dwfx', '.ora', '.ufo', '.h5p', '.apkg', '.colpkg', '.mcpack', '.mcworld', '.unitypackage', '.sb3', '.love', '.egg', '.nsp', '.xci', '.cia', '.alfredworkflow', '.sublime-package', '.otf', '.ttf', '.woff', '.woff2', '.epub', '.kmz', '.cbz', '.jar', '.war', '.ear', '.sar', '.whl', '.nupkg', '.wsz', '.crx', '.xpi', '.vsix', '.msix', '.appx', '.msixbundle', '.appxbundle', '.kra', '.appv', '.jks', '.keystore', '.truststore' ], 'application/x-rar-compressed': ['.rar', '.cbr', '.exe'], 'application/x-rar': ['.rar', '.cbr', '.exe'], 'application/vnd.rar': ['.rar', '.cbr', '.exe'], 'application/x-7z-compressed': ['.7z', '.exe', '.cb7', '.wim', '.esd'], 'application/x-tar': ['.tar', '.cbt', '.ova', '.unitypackage', '.gem'], 'application/gzip': ['.gz', '.tgz', '.svgz', '.als', '.schematic', '.litematic', '.tgs', '.unitypackage', '.box'], 'application/x-lzh-compressed': ['.lzh', '.lha'], 'application/x-lha': ['.lzh', '.lha'], 'application/x-iso9660-image': ['.iso', '.img'], 'application/vnd.android.package-archive': ['.apk'], 'application/x-apple-diskimage': ['.dmg'], 'application/x-debian-package': ['.deb'], 'application/x-redhat-package-manager': ['.rpm'], 'application/pdf': ['.pdf', '.ai'], 'text/plain': [ '.txt', '.log', '.md', '.markdown', '.nfo', '.rtf', '.rst', '.adoc', '.org','.mhtml', '.mht', '.dts', '.dtsi', '.ofx', '.qif', '.gnucash', '.tscn', '.tres', '.gd', '.godot', '.out', '.err', '.pid', '.asc', '.md5', '.sha1', '.sha256', '.sha512', '.dockerfile', '.makefile', '.jenkinsfile', '.tf', '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.astro', '.mdx', '.css', '.scss', '.less', '.html', '.htm', '.pug', '.jade', '.coffee', '.wat', '.pac', '.graphql', '.gql', '.prisma', '.py', '.java', '.c', '.cpp', '.h', '.hh', '.hpp', '.cs', '.php', '.go', '.rs', '.rb', '.lua', '.kt', '.swift', '.dart', '.pl', '.pm', '.scala', '.groovy', '.hs', '.asp', '.aspx', '.jsp', '.m', '.mm', '.r', '.rmd', '.jl', '.nb', '.ex', '.exs', '.erl', '.hrl', '.clj', '.lisp', '.ml', '.v', '.sv', '.vhd', '.vhdl', '.sas', '.do', '.sh', '.bat', '.cmd', '.ps1', '.psd1', '.psm1', '.vbs', '.reg', '.vmx', '.lock', '.toml', '.hex', '.gradle', '.cmake', '.editorconfig', '.ini', '.cfg', '.conf', '.rc', '.list', '.yaml', '.yml', '.json', '.xml', '.properties', '.env', '.gitignore', '.sql', '.drawio', '.dio', '.htaccess', '.npmrc', '.eps', '.ps', '.meta', '.asset', '.fbx', '.step', '.stp', '.iges', '.igs', '.gcode', '.stl', '.ply', '.fasta', '.fa', '.eml', '.mbox', '.ics', '.ifb', '.vcf', '.ovpn', '.glsl', '.hlsl', '.shader', '.cginc', '.unity', '.pem', '.key', '.crt', '.csr', '.p7b', '.p7c', '.tex', '.sty', '.cls', '.bib', '.srt', '.ass', '.ssa', '.sub', '.vtt', '.smi', '.lrc', '.sup', '.idx', '.sbv', '.m3u', '.m3u8', '.cue', '.torrent' ], 'text/html': ['.html', '.htm', '.mhtml', '.mht', '.vue', '.svelte', '.astro', '.txt'], 'text/xml': [ '.xml', '.ui', '.opml', '.kml', '.gpx', '.rss', '.nfo', '.txt', '.svg', '.plist', '.mobileconfig', '.webloc', '.ttml', '.musicxml', '.drawio', '.dio', '.csproj', '.vbproj', '.xaml', '.kdenlive', '.fb2', '.xmp', '.dae', '.fods', '.fodt', '.fodp', '.mobileprovision', '.nuspec', '.resx', '.vbox', '.osm', '.application', '.manifest' ], 'application/json': [ '.json', '.txt', '.ipynb', '.gltf', '.geojson', '.map', '.har', '.topojson', '.webmanifest', '.postman_collection', '.tfstate', '.webapp', '.uproject', '.uplugin', '.glyphs' ], 'application/x-hdf': ['.h5', '.hdf5', '.keras'], 'application/x-hdf5': ['.h5', '.hdf5', '.keras'], 'text/calendar': ['.ics', '.ifb'], 'application/x-bittorrent': ['.torrent'], 'message/rfc822': ['.mhtml', '.mht', '.eml'], 'multipart/related': ['.mhtml', '.mht'], 'application/x-mobipocket-ebook': ['.mobi', '.azw3'], 'application/vnd.amazon.ebook': ['.azw3', '.mobi'], 'text/vcard': ['.vcf'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx', '.docm', '.dotx', '.dotm'], 'application/msword': ['.doc'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx', '.xlsm', '.xltx', '.xltm', '.csv'], 'application/vnd.ms-excel': ['.xls', '.csv'], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx', '.pptm', '.potx', '.potm'], 'application/vnd.ms-powerpoint': ['.ppt'], 'application/epub+zip': ['.epub', '.zip'] }; const exactNamesToKeep = new Set([ 'thumbs.db', 'desktop.ini', '.ds_store', 'dockerfile', 'makefile', 'jenkinsfile', 'rakefile', 'gemfile', 'vagrantfile', 'procfile', 'license', 'readme', 'changelog', 'copying', 'authors', 'cmakelists.txt', 'contributors', 'patents', 'security', 'notice', 'version', 'cname', 'owners', 'robots.txt', 'go.mod', 'go.sum', 'podfile', 'podfile.lock', 'yarn.lock', 'package-lock.json' ]); const binaryMimes = ['application/octet-stream', 'binary/octet-stream']; const safeImgExts = ['.jpg', '.jpeg', '.png', '.bmp', '.heic']; const safeVidExts = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.3gp', '.ts', '.mpg', '.mpeg', '.vob', '.rmvb', '.asf']; const highRiskExts = [ '.rar', '.zip', '.7z', '.iso', '.img', '.dmg', '.apk', '.ipa', '.mhtml', '.mht', '.html', '.htm', '.xml', '.json', '.db', '.dat', '.tmp', '.dts', '.dtsi', '.ts', '.3gp', '.mkv', '.avi', '.mp4', '.flv', '.mov', '.wmv' ]; if (item.mimeType === 'application/vnd.google-apps.folder') continue; const pureMimeType = (item.mimeType || '').split(';')[0].trim().toLowerCase(); const lowerName = oldName.toLowerCase(); const lastDotIndex = oldName.lastIndexOf('.'); const currentExt = lastDotIndex !== -1 ? oldName.substring(lastDotIndex).toLowerCase() : ''; const isPartFile = /^\.\d+$/.test(currentExt) || /\.part\d+$/i.test(currentExt); let newName = oldName; let shouldFix = false; if (binaryMimes.includes(pureMimeType) || exactNamesToKeep.has(lowerName) || oldName.startsWith('.') || isPartFile) { shouldFix = false; } else { const validExtensions = mimeMap[pureMimeType]; if (validExtensions && validExtensions.length > 0) { const primaryExt = validExtensions[0]; if (validExtensions.includes(currentExt)) { newName = oldName.substring(0, lastDotIndex) + currentExt; shouldFix = true; } else if ( (safeImgExts.includes(currentExt) && safeImgExts.includes(primaryExt)) || (safeVidExts.includes(currentExt) && safeVidExts.includes(primaryExt)) ) { newName = oldName.substring(0, lastDotIndex) + currentExt; shouldFix = true; } else if ( (pureMimeType === 'application/pdf' && ['.rar', '.zip', '.7z'].includes(currentExt)) || highRiskExts.includes(currentExt) ) { shouldFix = false; } else { shouldFix = true; if (lastDotIndex === -1) { const ambiguousTextExts = ['.svg', '.html', '.htm', '.xml', '.json']; if (ambiguousTextExts.includes(primaryExt) && !oldName.includes(' ')) { shouldFix = false; } else { if (ambiguousTextExts.includes(primaryExt)) newName = oldName + '.txt'; else newName = oldName + primaryExt; } } else { const isSourceText = ['.txt', '.log', '.md', '.ini', '.nfo'].includes(currentExt); const ambiguousTextExts = ['.svg', '.html', '.htm', '.xml', '.json']; if (isSourceText && ambiguousTextExts.includes(primaryExt)) { newName = oldName.substring(0, lastDotIndex) + currentExt; } else { newName = oldName.substring(0, lastDotIndex) + primaryExt; } } } } } if (shouldFix && newName !== oldName) { changes.push({ id: item.id, old: oldName, new: newName, hl_old: null, conflict: false, parent_id: item.parent_id }); } } if (item._isSystem) continue; if (newName !== oldName || (mode === 'replace' && hlOld) || mode === 'pattern') { let isConflict = false; const cleanNewName = newName.trim(); if (!cleanNewName) { isConflict = true; newName = e.data.STR_EMPTY_FILENAME; } else if (allNames.has(cleanNewName)) { isConflict = true; } else { allNames.add(cleanNewName); } changes.push({ id: item.id, old: oldName, new: newName, hl_old: hlOld, conflict: isConflict, parent_id: item.parent_id }); } } self.postMessage({ changes: changes, error: null }); } catch (e) { self.postMessage({ changes: [], error: e.toString() }); } }; const workerCode = 'self.onmessage = ' + workerFunction.toString(); const itemsCopy = items.map(i => ({ id: i.id, name: i.name, kind: i.kind, mimeType: i.mime_type, parent_id: i.parent_id, _isSystem: isSystemItem(i) })); const blob = new Blob([workerCode], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); const previewWorker = new Worker(workerUrl); previewWorker.onmessage = (e) => { const { changes, error } = e.data; previewWorker.terminate(); URL.revokeObjectURL(workerUrl); if (myPreviewId !== currentPreviewId) return; handlePreviewResults(changes, error); }; previewWorker.onerror = (e) => { console.error("Worker Error:", e); rnIn.innerHTML = `
❌ Worker Error
`; previewWorker.terminate(); URL.revokeObjectURL(workerUrl); }; previewWorker.postMessage({ selectedIds, items: itemsCopy, mode, pattern, findStr, repStr, useRegex, STR_INVALID_REGEX: L.err_invalid_regex, STR_EMPTY_FILENAME: L.str_empty_filename, validExtsList: VALID_EXTS_LIST, caseMode, widthMode, useCaseSense, useIncludeExt }); }; m.querySelector('#rn_cancel').onclick = () => m.remove(); [inpPattern, inpFind, inpRep, chkRegex, chkCase, chkIncludeExt].forEach(el => el.onchange = generatePreview); [inpPattern, inpFind, inpRep].forEach(el => el.onkeydown = (e) => { if(e.key==='Enter') generatePreview(); }); let selectedCase = ""; let selectedWidth = ""; const bindRNSelect = (id, onSelect) => { const container = m.querySelector(`#${id}`); if (!container) return; const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('span'); const items = container.querySelectorAll('.pk-select-item'); trigger.onclick = (e) => { e.stopPropagation(); const allMenus = m.querySelectorAll('.pk-select-menu'); const isOpen = menu.style.display === 'block'; allMenus.forEach(om => om.style.display = 'none'); menu.style.display = isOpen ? 'none' : 'block'; }; items.forEach(item => { item.onclick = (e) => { e.stopPropagation(); items.forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val); generatePreview(); }; }); }; bindRNSelect('cs_rn_case', (val) => { selectedCase = val; }); bindRNSelect('cs_rn_width', (val) => { selectedWidth = val; }); const closeDropdowns = () => m.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none'); setTimeout(() => document.addEventListener('click', closeDropdowns), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeDropdowns); _orgRemove(); }; const bindHeaderCheckbox = () => { const cbAll = m.querySelector('#rn_cb_all'); if(cbAll) { cbAll.onclick = (e) => { const isChecked = e.target.checked; if(isChecked) { plannedChanges.forEach(c => previewSelectedIds.add(c.id)); } else { previewSelectedIds.clear(); } recalcPatternNames(); renderRNVisible(); updateHeaderCheckbox(); }; } }; setTimeout(bindHeaderCheckbox, 0); btnApply.onclick = async () => { const validChanges = rnDisplay.filter(c => previewSelectedIds.has(c.id) && c.new !== c.old); const skippedCount = plannedChanges.length - validChanges.length; if (validChanges.length === 0) { showAlert(L.msg_rn_all_skipped); return; } let confirmMsg = L.rn_warn_confirm.replace('{n}', validChanges.length); if (!await showConfirm(confirmMsg)) return; const progressTask = FloatBarManager.create(L.str_renaming); m.remove(); let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; updateLoadTxt(L.str_stopping); }; const USER_LIMIT = parseInt(localStorage.getItem('pk_user_limit') || "200"); let currentLimit = 2; const MIN_LIMIT = 2; const queue = [...validChanges]; const activeTasks = new Set(); const stats = { success: 0, fail: 0, lastUiTime: 0 }; const total = validChanges.length; const runRenameTask = async (task) => { try { await apiAction(`/${task.id}`, { name: task.new }); stats.success++; const item = S.itemMap.get(task.id); if (item) { item.name = task.new; const nowIso = new Date(getServerNow()).toISOString(); item.modified_time = nowIso; if (item.kind === 'drive#folder') gmSet('pk_fmod_' + item.id, nowIso); const parentIdForFmod = item.parent_id || 'root'; gmSet('pk_fmod_' + parentIdForFmod, nowIso); const row = UI.in.querySelector(`.pk-row[data-id="${task.id}"]`); if (row && row.lastElementChild) row.lastElementChild.textContent = fmtDate(nowIso); if (S.analyzeResultItems) { const anaItem = S.analyzeResultItems.find(x => x.id === task.id); if (anaItem) anaItem.name = task.new; } if (S.analyzeMap && S.analyzeMap.has(task.id)) { S.analyzeMap.get(task.id).name = task.new; } if (S.lastGlobalResults && S.lastGlobalResults.length > 0) { const gItem = S.lastGlobalResults.find(x => x.id === task.id); if (gItem) gItem.name = task.new; } if (typeof globalDirtyFolders !== 'undefined') { const pid = item.parent_id || ''; globalDirtyFolders.add(pid); } } if (currentLimit < USER_LIMIT) currentLimit++; } catch (e) { if (!isRunning) return; if ((e.message && e.message.includes('429')) || (e.message && e.message.includes('Network'))) { currentLimit = Math.max(MIN_LIMIT, Math.floor(currentLimit / 2)); task.retryCount = (task.retryCount || 0) + 1; await sleep(Math.min(task.retryCount * 1000, 10000)); queue.push(task); } else { stats.fail++; } } }; try { updateLoadTxt(L.str_init_rename); while ((queue.length > 0 || activeTasks.size > 0) && isRunning) { while (queue.length > 0 && activeTasks.size < currentLimit && isRunning) { const task = queue.shift(); const p = runRenameTask(task).finally(() => activeTasks.delete(p)); activeTasks.add(p); } if (activeTasks.size > 0) await Promise.race(activeTasks); const now = Date.now(); if (now - stats.lastUiTime > 150) { progressTask.update(`${L.str_renaming} ${stats.success + stats.fail}/${total} | ${L.str_speed}: ${activeTasks.size} | ${L.str_success}: ${stats.success}`); stats.lastUiTime = now; } } if (!isRunning) throw new Error('StoppedByUser'); updateLoadTxt(L.str_refreshing_cache); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); if (S.dupMode) renderDupView(); else refresh(); let msgParts = []; if (stats.success > 0) msgParts.push(L.msg_bulkrename_done.replace('{n}', stats.success)); if (stats.fail > 0) msgParts.push(L.msg_rn_fail_count.replace('{n}', stats.fail)); const finalMsg = msgParts.join('\n'); await sleep(300); showAlert(finalMsg); } catch (e) { if (e.message !== 'StoppedByUser') showAlert(`${L.str_error_crit}: ${e.message}`); } finally { if (progressTask) progressTask.destroy(); setLoad(false); } }; }; UI.btnPrune.onclick = async () => { isGUISensitive = true; ensureItemMap(); const selectedFolders = Array.from(S.sel) .map(id => S.itemMap.get(id)) .filter(i => i && i.kind === 'drive#folder'); if (selectedFolders.length === 0) return; const hasConflict = selectedFolders.some(f => isPathBusy(f.id)); if (hasConflict) { showAlert(L.msg_prune_blocked_moving); return; } if (!await showConfirm(L.msg_prune_confirm)) return; setLoad(true); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); setLoad(false); isGUISensitive = false; }; const folderMap = new Map(); updateLoadTxt(L.str_scanning_dir); try { await coreRecursiveEngine(selectedFolders, { signal: signal, onFolder: (folder, filesInFolder, subFolders) => { const hasFiles = filesInFolder.some(f => f.kind !== 'drive#folder'); folderMap.set(folder.id, { id: folder.id, name: folder.name, parent_id: folder.parent_id, depth: folder.depth || 0, hasFiles: hasFiles, subFolderIds: subFolders.map(s => s.id) }); }, onProgress: (st) => { const folderText = `${L.str_scanning_dir} ${st.folders} ${L.unit_folders}`; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo); } }); if (!S.scanning || signal.aborted || myScanId !== S.scanId) return; updateLoadTxt(L.str_analyzing); await sleep(50); const allScanned = Array.from(folderMap.values()).sort((a, b) => b.depth - a.depth); const toDeleteList = []; const toDeleteIds = new Set(); for (let i = 0; i < allScanned.length; i++) { if (!S.scanning) return; const folder = allScanned[i]; const isSystemProtected = isSystemItem({ ...folder, kind: 'drive#folder' }); if (isSystemProtected) continue; if (!folder.hasFiles) { const allSubsWillBeDeleted = folder.subFolderIds.every(subId => toDeleteIds.has(subId)); if (allSubsWillBeDeleted) { toDeleteIds.add(folder.id); toDeleteList.push(folder); } } } if (toDeleteList.length === 0) { setLoad(false); showAlert(L.msg_prune_none); } else { setLoad(false); const cacheHitCount = Array.from(folderMap.keys()).filter(id => globalCache.has(id)).length; let confirmMsg = L.msg_prune_found.replace('{n}', toDeleteList.length); if (await showConfirm(confirmMsg)) { const allIds = toDeleteList.map(f => f.id); await executeBatchDelete(allIds, { silent: true, forceRefresh: false }); if (myScanId === S.scanId) { const deletedSet = new Set(allIds); if (S.lastGlobalResults && S.lastGlobalResults.length > 0) { S.lastGlobalResults = S.lastGlobalResults.filter(x => !deletedSet.has(x.id)); } if (S.analyzeMode && S.analyzeResultItems) { S.analyzeResultItems = S.analyzeResultItems.filter(x => !deletedSet.has(x.id)); } updateLoadTxt(L.str_refreshing); const affectedParentIds = new Set(); affectedParentIds.add(S.path[S.path.length - 1].id || 'root'); toDeleteList.forEach(folder => { if (folder.parent_id) affectedParentIds.add(folder.parent_id); else affectedParentIds.add('root'); }); affectedParentIds.forEach(pid => { if (typeof globalCache !== 'undefined') globalCache.delete(pid); S.cache.delete(pid); }); await load(false, true); showToast(L.str_cleanup_done); } } } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; isGUISensitive = false; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } }; if (UI.btnUpPause) UI.btnUpPause.onclick = () => { const ids = Array.from(S.sel); ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task && S.upMng) S.upMng.pause(task, true); }); if (S.uploadMode) { refresh(); } }; if (UI.btnUpStart) UI.btnUpStart.onclick = () => { const ids = Array.from(S.sel); ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task && S.upMng) S.upMng.resume(task, true); }); if (S.uploadMode) { refresh(); } }; if (UI.btnUpDel) UI.btnUpDel.onclick = () => { if (S.sel.size === 0) return; const count = S.sel.size; const html = `

${L.title_del_task_confirm_fmt.replace('{n}', count)}

`; const m = showModal(html); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => m.remove(); m.querySelector('#del_up_cancel').onclick = close; const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; m.querySelector('#del_up_confirm').onclick = async () => { const isDeleteFile = m.querySelector('#del_up_files').checked; m.remove(); const ids = Array.from(S.sel); const filesToDelete = []; ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task) { task._deleted = true; task._deleteFileIntent = isDeleteFile; if (isDeleteFile && task.file_id) { filesToDelete.push(task.file_id); } if (S.upMng) S.upMng.pause(task, true); } }); const idSet = S.sel; S.uploadTasks = S.uploadTasks.filter(t => !idSet.has(t.id)); S.clearSelection(); load(false, true); if (filesToDelete.length > 0) { try { await executeBatchDelete(filesToDelete, { silent: false, hardDelete: false, forceRefresh: false }); } catch(e) { console.error("Failed to delete uploaded files:", e); showToast(L.msg_file_del_failed + e.message, "error"); } } else { showToast(L.msg_task_del_success_fmt.replace('{n}', ids.length)); } }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#del_up_confirm').click(); } }); }; if (UI.btnUpClearAll) UI.btnUpClearAll.onclick = () => { if (S.uploadTasks.length === 0) return; const count = S.uploadTasks.length; const html = `

${L.title_clear_task_confirm}

`; const m = showModal(html); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => m.remove(); m.querySelector('#clear_up_cancel').onclick = close; const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; m.querySelector('#clear_up_confirm').onclick = async () => { const isDeleteFile = m.querySelector('#clear_all_up_files').checked; m.remove(); const filesToDelete =[]; S.uploadTasks.forEach(task => { task._deleted = true; task._deleteFileIntent = isDeleteFile; if (S.upMng) S.upMng.pause(task, true); if (isDeleteFile && task.file_id) { filesToDelete.push(task.file_id); } }); S.uploadTasks = []; S.clearSelection(); load(false, true); if (filesToDelete.length > 0) { try { await executeBatchDelete(filesToDelete, { silent: false, hardDelete: false, forceRefresh: false }); } catch(e) { console.error("Clear All: Cloud deletion failed", e); } } else { showToast(L.msg_task_clear_success_fmt.replace('{n}', count)); } }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#clear_up_confirm').click(); } }); }; UI.btnExt.onclick = async () => { const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.loading_detail); const targetApiId = ((S.offlineMode && item.kind === 'drive#task') || (S.uploadMode && item.file_id)) ? item.file_id : item.id; let detail = item; try { if (!targetApiId) throw new Error("File ID not ready"); detail = await apiGet(targetApiId); } catch (e) { console.warn("Fetch detail failed, using cached info"); if (!detail.web_content_link && !detail.medias) { setLoad(false); showToast(L.msg_video_fail, 'error'); return; } } setLoad(false); const qualities = generateQualityList(detail); const bestSource = getBestSource(detail); let selectedUrl = bestSource.src; let selectedResName = bestSource.name; const savedPlayer = gmGet('pk_ext_player', 'potplayer'); let selectedPlayer = (savedPlayer === 'potplayer') ? 'potplayer' : 'other'; const initialBtnTxt = (selectedPlayer === 'potplayer') ? L.btn_start_play : L.btn_copy_link; const m = showModal(`

${L.btn_ext}

${L.lbl_resolution}
${selectedResName}
${CONF.crumbIcons.down}
${qualities.map(q => `
${q.name}
`).join('')}
${L.lbl_player}
${selectedPlayer === 'potplayer' ? 'PotPlayer' : L.opt_player_other}
${CONF.crumbIcons.down}
PotPlayer
${L.opt_player_other}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '480px', padding: '30px', height: 'auto', minHeight: 'auto', overflow: 'visible' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const bindSelect = (id, onSelect) => { const container = m.querySelector(`#${id}`); const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('span'); trigger.onclick = (e) => { e.stopPropagation(); const allMenus = m.querySelectorAll('.pk-select-menu'); const isCurrentlyOpen = menu.style.display === 'block'; allMenus.forEach(om => om.style.display = 'none'); menu.style.display = isCurrentlyOpen ? 'none' : 'block'; }; container.querySelectorAll('.pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); container.querySelectorAll('.pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val); }; }); }; const runBtn = m.querySelector('#ext_run'); bindSelect('cs_res', (val) => { selectedUrl = val; }); bindSelect('cs_player', (val) => { selectedPlayer = val; runBtn.textContent = (val === 'potplayer') ? L.btn_start_play : L.btn_copy_link; }); const closeAllMenus = () => m.querySelectorAll('.pk-select-menu').forEach(om => om.style.display = 'none'); setTimeout(() => document.addEventListener('click', closeAllMenus), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeAllMenus); _orgRemove(); }; m.querySelector('#ext_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); runBtn.click(); } }); runBtn.onclick = () => { gmSet('pk_ext_player', selectedPlayer); let cleanUrl = selectedUrl.replace('&ext=.m3u8', ''); if (cleanUrl.includes('ts_downloader') && cleanUrl.includes('url=')) { const urlParam = new URL(cleanUrl).searchParams.get('url'); if (urlParam) cleanUrl = decodeURIComponent(urlParam); } if (selectedPlayer === 'potplayer') { m.remove(); const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanUrl} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; } else { GM_setClipboard(cleanUrl); runBtn.textContent = L.msg_copy_success; runBtn.style.background = "#52c41a"; runBtn.disabled = true; setTimeout(() => m.remove(), 1000); } }; }; UI.win.querySelector('#pk-down').onclick = async () => { const hasConflict = Array.from(S.sel).some(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') return isPathBusy(item.id); return S.movingIds.has(id); }); if (hasConflict) { showAlert(L.msg_resource_locked_download); return; } setLoad(true); isGUISensitive = true; const abortCtrl = new AbortController(); const { signal } = abortCtrl; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; abortCtrl.abort(); updateLoadTxt(L.str_stopping); }; const allFiles = []; const rootNodes = []; const HYDRATE_LIMIT = 20; S.sel.forEach(id => { const item = S.itemMap.get(id); if (item) { if (item.kind === 'drive#folder') rootNodes.push({...item, lineage:[], retryCount: 0}); else allFiles.push(item); } }); let progressTask = null; const fExtStr = gmGet('pk_dl_filter_ext', '').toLowerCase(); const fNameStr = gmGet('pk_dl_filter_name', '').toLowerCase(); const fExts = fExtStr.split(/[,,]/).map(s => s.trim().replace(/^\./, '')).filter(Boolean); const fNames = fNameStr.split(/[,,]/).map(s => s.trim()).filter(Boolean); try { await coreRecursiveEngine(rootNodes, { signal, onFile: (f) => { const lowName = f.name.toLowerCase(); const ext = lowName.split('.').pop(); const isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n)); if (!isBlocked) allFiles.push(f); }, onProgress: (st) => { updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`); } }); if (!isRunning) throw new Error('StoppedByUser'); if (allFiles.length === 0) { setLoad(false); showToast(L.msg_batch_no_files); return; } setLoad(false); if (allFiles.length > 10) { if (!await showConfirm(L.msg_down_confirm_total.replace('{n}', allFiles.length))) return; } progressTask = FloatBarManager.create(L.msg_batch_hydrating); const readyFiles = []; const hydrateQueue = [...allFiles]; const activeTasks = new Set(); while ((hydrateQueue.length > 0 || activeTasks.size > 0) && isRunning) { while (hydrateQueue.length > 0 && activeTasks.size < HYDRATE_LIMIT && isRunning) { const file = hydrateQueue.pop(); const p = (async () => { try { const detail = (file.web_content_link) ? file : await apiGet(file.id); if (detail?.web_content_link) readyFiles.push(detail); } catch (e) {} })().finally(() => activeTasks.delete(p)); activeTasks.add(p); } if (activeTasks.size > 0) await Promise.race(activeTasks); if (progressTask) progressTask.update(`${L.msg_batch_hydrating} ${readyFiles.length} / ${allFiles.length}`); } for (let i = 0; i < readyFiles.length; i++) { if (!isRunning) break; if (progressTask) progressTask.update(`${L.msg_down_progress} ${i + 1} / ${readyFiles.length}`); const link = document.createElement('a'); link.href = readyFiles[i].web_content_link; link.setAttribute('download', readyFiles[i].name); link.style.display = 'none'; document.body.appendChild(link); link.click(); setTimeout(() => { if (link.parentNode) link.remove(); }, 10000); if (i < readyFiles.length - 1) await sleep(2000); } if (isRunning && readyFiles.length > 0) { showToast(L.msg_down_success.replace('{n}', readyFiles.length)); } } catch (e) { if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); isGUISensitive = false; if (progressTask) progressTask.destroy(); } }; UI.win.querySelector('#pk-aria2').onclick = async () => { const hasConflict = Array.from(S.sel).some(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') return isPathBusy(item.id); return S.movingIds.has(id); }); if (hasConflict) { showAlert(L.msg_resource_locked_aria2); return; } let ariaUrl = gmGet('pk_aria2_url', ''); let ariaToken = gmGet('pk_aria2_token', ''); if (!ariaUrl) { const result = await new Promise((resolve) => { const m = showModal(`

${CONF.icons.aria2.replace('width="16"', 'width="22"').replace('height="16"', 'width="22"')} ${L.btn_aria2} - ${L.btn_settings}

${L.msg_aria2_not_set}
${L.label_aria2_url}
Default
${L.lbl_aria2_status}
${L.label_aria2_token}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) Object.assign(modalBox.style, { width: '480px', padding: '30px', height: 'auto', minHeight: 'auto', overflow: 'visible' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); const inpU = m.querySelector('#pop_aria_url'); const inpT = m.querySelector('#pop_aria_token'); const dot = m.querySelector('#pop_aria_test_dot'); const txt = m.querySelector('#pop_aria_test_txt'); const boxRes = m.querySelector('#pop_aria_test_res'); let testTimer = null; const runLiveTest = async () => { const url = inpU.value.trim(); const token = inpT.value.trim(); const showTip = () => showAlert(L.tip_mixed_content, L.lbl_aria2_status); if (!url) { dot.className = 'pk-aria-dot'; txt.textContent = L.lbl_aria2_status; boxRes.onclick = showTip; boxRes.style.cursor = 'pointer'; return; } dot.className = 'pk-aria-dot wait'; txt.textContent = L.str_connecting; boxRes.onclick = showTip; boxRes.style.cursor = 'pointer'; const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: [`token:${token}`] }; let testUrl = url.replace(/^ws/i, 'http'); if (!testUrl.includes('/jsonrpc') && !testUrl.includes('?')) { testUrl = testUrl.endsWith('/') ? testUrl + 'jsonrpc' : testUrl + '/jsonrpc'; } try { await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: testUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 3000, onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error(r.status)); }, onerror: (e) => rejectReq(e) }); }); dot.className = 'pk-aria-dot ok'; txt.textContent = L.str_connected; boxRes.onclick = null; boxRes.style.cursor = 'default'; } catch (e) { dot.className = 'pk-aria-dot err'; txt.textContent = L.str_conn_fail; boxRes.onclick = showTip; boxRes.style.cursor = 'pointer'; } }; const triggerTest = () => { clearTimeout(testTimer); testTimer = setTimeout(runLiveTest, 600); }; inpU.oninput = (e) => { e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; triggerTest(); }; inpT.oninput = (e) => { e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; triggerTest(); }; m.querySelector('#btn_pop_aria_default').onclick = () => { inpU.value = 'http://localhost:6800/jsonrpc'; inpU.style.borderColor = 'var(--pk-pri)'; triggerTest(); }; setTimeout(runLiveTest, 200); m.querySelector('#pop_aria_cancel').onclick = () => { m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); }; m.querySelector('#pop_aria_save').onclick = async () => { let u = inpU.value.trim(); let t = inpT.value.trim(); if (!u) { inpU.style.borderColor = '#d93025'; return; } if (!/^https?:\/\/|^wss?:\/\//i.test(u)) u = 'http://' + u; const saveBtn = m.querySelector('#pop_aria_save'); const originalTxt = saveBtn.textContent; saveBtn.disabled = true; saveBtn.textContent = L.str_saving_dots; try { const testUrl = u.replace(/^ws/i, 'http'); const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: [`token:${t}`] }; await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: testUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 5000, onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error('HTTP ' + r.status)); }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); gmSet('pk_aria2_url', u); gmSet('pk_aria2_token', t); m.remove(); resolve({ url: u, token: t }); } catch (err) { const confirmed = await showConfirm( L.msg_aria2_test_fail, L.title_aria2_fail ); if (confirmed) { gmSet('pk_aria2_url', u); gmSet('pk_aria2_token', t); m.remove(); resolve({ url: u, token: t }); } else { saveBtn.disabled = false; saveBtn.textContent = originalTxt; } } }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#pop_aria_save').click(); } }); }); if (!result) return; ariaUrl = result.url; ariaToken = result.token; } setLoad(true); isGUISensitive = true; const abortCtrl = new AbortController(); const { signal } = abortCtrl; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; abortCtrl.abort(); updateLoadTxt(L.str_stopping); }; if (ariaUrl.startsWith('ws')) ariaUrl = ariaUrl.replace(/^ws/, 'http'); if (!ariaUrl.includes('/jsonrpc') && !ariaUrl.includes('?')) { ariaUrl = ariaUrl.endsWith('/') ? ariaUrl + 'jsonrpc' : ariaUrl + '/jsonrpc'; } const allFiles = []; const rootNodes = []; const HYDRATE_LIMIT = 40; const fExtStr = gmGet('pk_dl_filter_ext', '').toLowerCase(); const fNameStr = gmGet('pk_dl_filter_name', '').toLowerCase(); const fExts = fExtStr.split(/[,,]/).map(s => s.trim().replace(/^\./, '')).filter(Boolean); const fNames = fNameStr.split(/[,,]/).map(s => s.trim()).filter(Boolean); const stats = { hydratedCount: 0, lastUiTime: 0 }; S.sel.forEach(id => { const item = S.itemMap.get(id); if (item) { if (item.kind === 'drive#folder') { rootNodes.push({...item, lineage: [{ id: item.id, name: item.name }], retryCount: 0}); } else { allFiles.push({ ...item, _lineage: [] }); } } }); let progressTask = null; try { await coreRecursiveEngine(rootNodes, { signal, onFile: (f, parent) => { const lowName = f.name.toLowerCase(); const ext = lowName.split('.').pop(); const isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n)); if (!isBlocked) { f._lineage = parent.lineage || []; allFiles.push(f); } }, onProgress: (st) => { const now = Date.now(); if (now - stats.lastUiTime > 150) { updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`); stats.lastUiTime = now; } } }); if (!isRunning) throw new Error('StoppedByUser'); if (allFiles.length === 0) { setLoad(false); showAlert(L.msg_batch_no_files); return; } setLoad(false); progressTask = FloatBarManager.create(L.msg_batch_hydrating); const readyFiles =[]; const failedFiles = []; const hydrateQueue = [...allFiles]; const activeTasks = new Set(); const hydrateWithRetry = async (file, maxRetries = 3) => { if (file.web_content_link) return file; let lastErr = null; for (let i = 0; i < maxRetries; i++) { if (!isRunning) return null; try { const detail = await apiGet(file.id); if (detail && detail.web_content_link) return detail; throw new Error("Link Empty"); } catch (e) { lastErr = e; if (i < maxRetries - 1) await sleep(1000 * (i + 1)); } } throw lastErr; }; while ((hydrateQueue.length > 0 || activeTasks.size > 0) && isRunning) { while (hydrateQueue.length > 0 && activeTasks.size < HYDRATE_LIMIT && isRunning) { const file = hydrateQueue.pop(); const p = (async () => { try { const detail = await hydrateWithRetry(file); if (detail) { detail._lineage = file._lineage; readyFiles.push(detail); } } catch (e) { console.error(`[Hydrate Failed] ${file.name}:`, e); failedFiles.push(file.name + " " + L.str_aria2_fetch_err); } })().finally(() => { activeTasks.delete(p); stats.hydratedCount++; if (progressTask) progressTask.update(`${L.msg_batch_hydrating} ${stats.hydratedCount} / ${allFiles.length}`); }); activeTasks.add(p); } if (activeTasks.size > 0) await Promise.race(activeTasks); } if (!isRunning) throw new Error('StoppedByUser'); if (readyFiles.length > 0 && isRunning) { const BATCH_SIZE = 50; let successCount = 0; let rpcFatalError = false; for (let i = 0; i < readyFiles.length; i += BATCH_SIZE) { if (!isRunning || rpcFatalError) break; const chunk = readyFiles.slice(i, i + BATCH_SIZE); const sanitize = (s) => s.replace(/[\\/:*?"<>|]/g, '_').trim(); const payload = chunk.map(f => { let relativePrefix = ''; if (f._lineage && f._lineage.length > 0) { relativePrefix = f._lineage.map(n => sanitize(n.name)).join('/') + '/'; } const outPath = relativePrefix + sanitize(f.name); return { jsonrpc: '2.0', method: 'aria2.addUri', id: `pk_${Date.now()}_${Math.random().toString(16).slice(2)}`, params:[`token:${ariaToken}`, [f.web_content_link], { out: outPath, header:[`User-Agent: ${navigator.userAgent}`, `Referer: https://mypikpak.com/`] }] }; }); try { await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: ariaUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error(`RPC ${r.status}`)); }, onerror: () => rejectReq(new Error("Network Error")), ontimeout: () => rejectReq(new Error("Timeout")) }); }); successCount += chunk.length; } catch (rpcErr) { console.error("[RPC Batch Error]", rpcErr); chunk.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_rpc_err)); if (rpcErr.message === "Network Error" || rpcErr.message === "Timeout") { console.warn("[RPC Circuit Breaker] Aria2 disconnected. Aborting remaining batches."); rpcFatalError = true; const remainingFiles = readyFiles.slice(i + BATCH_SIZE); remainingFiles.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_aborted)); } } if (progressTask) progressTask.update(`${L.msg_aria2_sending_batch} ${successCount} / ${readyFiles.length}`); } if (failedFiles.length > 0) { let failListText = failedFiles.slice(0, 10).join('\n'); if (failedFiles.length > 10) { failListText += '\n...'; failListText += L.msg_aria2_batch_fail_log; } await showAlert(`${L.str_failed}: ${failedFiles.length}\n\n${failListText}`, L.title_alert); if (failedFiles.length > 10) { try { const blob = new Blob([failedFiles.join('\r\n')], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const now = new Date(); const dateStr = now.toISOString().replace(/[:.]/g, '-').slice(0, 19); a.href = url; a.download = `${L.str_aria2_fail_file_name}_${dateStr}.txt`; document.body.appendChild(a); a.click(); setTimeout(() => { if (a.parentNode) document.body.removeChild(a); URL.revokeObjectURL(url); }, 1000); } catch (exportErr) { console.error("[Export Error] Failed to generate failure log:", exportErr); } } } else { showToast(L.msg_aria2_sent.replace('{n}', successCount)); } } } catch (e) { if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') { showAlert(`${L.msg_aria2_check_fail}\n(${e.message})`); } } finally { setLoad(false); isGUISensitive = false; if (progressTask) progressTask.destroy(); } }; const ensureItemMap = () => { S.itemMap.clear(); const len = S.items.length; for (let i = 0; i < len; i++) { const item = S.items[i]; if (item && item.id) { S.itemMap.set(item.id, item); } } }; const executeBatchDelete = async (ids, options = {}) => { const { silent = false, deleteFiles = false, isTask = false, forceRefresh = false, hardDelete = false, explicitItems = [] } = options; if (!ids || ids.length === 0) return; const tempItemLookup = new Map(); if (S.itemMap) S.itemMap.forEach((v, k) => tempItemLookup.set(k, v)); if (explicitItems && explicitItems.length > 0) { explicitItems.forEach(item => { if (item && item.id) tempItemLookup.set(item.id, item); }); } const BATCH_SIZE = hardDelete ? 1000 : 200; const SKIP_VERIFY = hardDelete; const progressTask = FloatBarManager.create(L.str_deleting); const updateFloat = progressTask.update; S.movingSourceId = S.path[S.path.length - 1].id || 'root'; S.movingDestId = 'trash'; const allLockedIdsArray =[]; ids.forEach(id => { S.movingIds.add(id); allLockedIdsArray.push(id); if (isTask && deleteFiles) { const taskItem = tempItemLookup.get(id); if (taskItem && taskItem.file_id) { S.movingIds.add(taskItem.file_id); allLockedIdsArray.push(taskItem.file_id); } } }); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_ADD', ids: allLockedIdsArray, src: S.movingSourceId, dst: S.movingDestId }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); isGUISensitive = true; S.sel.clear(); S.lastSelIdx = -1; S.activeId = null; try { const BATCH_SIZE = 200; const totalToDelete = ids.length; let deletedCount = 0; const affectedParentIds = new Set(); const deletedSet = new Set(); affectedParentIds.add(S.path[S.path.length - 1].id || 'root'); for (let i = 0; i < totalToDelete; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); let retry = 0; const maxRetries = 3; let success = false; while (retry < maxRetries && !success) { try { if (isTask) { await apiCancelTask(chunk, deleteFiles); } else { const action = hardDelete ? 'files:batchDelete' : 'files:batchTrash'; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/${action}`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }) }); if (!res.ok) throw new Error(`API ${res.status}`); } success = true; } catch (err) { retry++; console.warn(`[Delete] Retry ${retry}/${maxRetries}`); updateFloat(`${L.str_deleting} (Retry ${retry})...`); await sleep(1000 * retry); if (retry >= maxRetries) throw err; } } if (!isTask && chunk.length > 0 && !SKIP_VERIFY) { const lastId = chunk[chunk.length - 1]; let verifyRetries = 0; while (verifyRetries < 20) { try { const meta = await apiGet(lastId); if (meta.trashed) break; } catch (e) { break; } updateFloat(`${L.str_deleting} ${deletedCount}/${totalToDelete}`); verifyRetries++; await sleep(500); } } const chunkSet = new Set(chunk); const chunkLockedIds =[]; chunk.forEach(id => { S.movingIds.delete(id); chunkLockedIds.push(id); const it = tempItemLookup.get(id); let physicalFolderId = null; if (it) { if (it.kind === 'drive#folder') { physicalFolderId = it.id; } else if (isTask && deleteFiles && it.file_id) { const isFolderTask = (it.mime_type && (it.mime_type.includes('folder') || it.mime_type.includes('directory'))) || (it.icon_link && it.icon_link.includes('folder')) || (typeof globalCache !== 'undefined' && globalCache.has(it.file_id)); if (isFolderTask) { physicalFolderId = it.file_id; } } } if (physicalFolderId && typeof globalCache !== 'undefined') { const purgeDescendants = (fid) => { const data = globalCache.get(fid) || (S.cache ? S.cache.get(fid) : null); if (data) { globalTombstoneCache.set(fid, Array.isArray(data) ?[...data] : {...data}); const list = Array.isArray(data) ? data : (data.items ||[]); list.forEach(child => { deletedSet.add(child.id); S.itemMap.delete(child.id); if (child.kind === 'drive#folder') { purgeDescendants(child.id); } }); globalCache.delete(fid); if (S.cache) S.cache.delete(fid); } }; purgeDescendants(physicalFolderId); } if (isTask && deleteFiles && it && it.file_id) { S.movingIds.delete(it.file_id); chunkLockedIds.push(it.file_id); deletedSet.add(it.file_id); if (typeof globalParentIndex !== 'undefined' && globalParentIndex.has(it.file_id)) { const parentInfo = globalParentIndex.get(it.file_id); if (parentInfo && parentInfo.id) { affectedParentIds.add(parentInfo.id === 'root' ? '' : parentInfo.id); } } affectedParentIds.add('root'); affectedParentIds.add(''); } if (it && S.analyzeMap) { const analyzeTargetId = physicalFolderId || id; const analyzeIdsToRemove = new Set([analyzeTargetId, id]); const queue = [analyzeTargetId, id]; while (queue.length > 0) { const currId = queue.shift(); S.analyzeMap.forEach((node, nId) => { if (node.parentId === currId && !analyzeIdsToRemove.has(nId)) { analyzeIdsToRemove.add(nId); queue.push(nId); } }); } analyzeIdsToRemove.forEach(remId => { if (S.analyzeMap.has(remId)) S.analyzeMap.delete(remId); }); const lostSize = parseInt(it.size || 0); if (lostSize > 0) { let currPid = it.parent_id; if (!currPid && isTask && typeof globalParentIndex !== 'undefined') { const pInfo = globalParentIndex.get(it.file_id); if (pInfo) currPid = pInfo.id; } let safety = 50; while (currPid && S.analyzeMap.has(currPid) && safety > 0) { const pNode = S.analyzeMap.get(currPid); pNode.size = Math.max(0, pNode.size - lostSize); currPid = pNode.parentId; safety--; } } if (S.analyzeSimGroups) { S.analyzeSimGroups.forEach(group => { group.ids = group.ids.filter(gid => !analyzeIdsToRemove.has(gid)); }); S.analyzeSimGroups = S.analyzeSimGroups.filter(group => group.ids.length >= 2); if (S.analyzeSimGroups.length === 0) { setTimeout(() => { if (UI.btnExit) UI.btnExit.click(); }, 500); } } if (S.analyzeResultItems) { S.analyzeResultItems = S.analyzeResultItems.filter(x => !analyzeIdsToRemove.has(x.id)); S.analyzeResultItems.forEach(resItem => { if (S.analyzeMap.has(resItem.id)) { resItem.size = S.analyzeMap.get(resItem.id).size.toString(); } }); } } if (it && it.parent_id) affectedParentIds.add(it.parent_id); S.itemMap.delete(id); deletedSet.add(id); }); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: chunkLockedIds }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); S.items = S.items.filter(x => !deletedSet.has(x.id)); if (typeof pkState !== 'undefined' && pkState && pkState.lastGlobalResults) { pkState.lastGlobalResults = pkState.lastGlobalResults.filter(x => !deletedSet.has(x.id)); } if (typeof globalCache !== 'undefined') { const cleanListChunk = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !deletedSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !deletedSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { globalCache.set(key, cleanListChunk(globalCache.get(key))); } for (const key of S.cache.keys()) { S.cache.set(key, cleanListChunk(S.cache.get(key))); } } if (!forceRefresh) { if (S.dupMode) renderDupView(); else refresh(); } await new Promise(r => requestAnimationFrame(r)); deletedCount += chunk.length; updateFloat(`${L.str_deleting} ${Math.min(deletedCount, totalToDelete)} / ${totalToDelete}`); if (deletedCount < totalToDelete) await sleep(50); } const cur = S.path[S.path.length - 1]; if (cur && cur.id) gmSet('pk_fmod_' + cur.id, new Date(getServerNow()).toISOString()); if (typeof globalCache !== 'undefined') { const cleanList = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !deletedSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !deletedSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { globalCache.set(key, cleanList(globalCache.get(key))); } for (const key of S.cache.keys()) { S.cache.set(key, cleanList(S.cache.get(key))); } affectedParentIds.forEach(pid => { const keysToCheck = (pid === 'root' || pid === '') ? ['root', ''] : [pid]; keysToCheck.forEach(key => { if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(key); }); }); globalCache.delete('root_trashed'); S.cache.delete('root_trashed'); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(); if (forceRefresh) { await load(false, true); } else { updateStat(); setTimeout(() => updateQuotaUI(), 2000); } if (!silent && !S.dupMode) { showToast(isTask ? L.msg_task_deleted : L.msg_del_items_done.replace('{n}', deletedCount)); } } catch (e) { console.error(e); showAlert(`${L.str_error}: ${e.message}`); } finally { if (typeof allLockedIdsArray !== 'undefined' && allLockedIdsArray.length > 0) { allLockedIdsArray.forEach(id => S.movingIds.delete(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: allLockedIdsArray }); } else if (ids && ids.length > 0) { ids.forEach(id => S.movingIds.delete(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: ids }); } if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); if (S.movingIds.size === 0) { isGUISensitive = false; } if (progressTask) progressTask.destroy(); } }; UI.btnDel.onclick = async () => { if (!S.sel.size) return; if (S.historyMode) { const count = S.sel.size; if (!await showConfirm(L.warn_clear_history.replace('{n}', count))) return; const selIds = new Set(S.sel); selIds.forEach(id => { gmSet('pk_progress_' + id, null); S.itemMap.delete(id); }); S.items = S.items.filter(it => !selIds.has(it.id)); S.clearSelection(); refresh(); showToast(L.msg_clear_history_done); return; } if (S.offlineMode) { const count = S.sel.size; const html = `

${L.title_del_task_confirm_fmt.replace('{n}', count)}

`; if (typeof m !== 'undefined' && m.remove) m.remove(); const taskModal = showModal(html); const modalBox = taskModal.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => taskModal.remove(); taskModal.querySelector('#del_task_cancel').onclick = close; const closeBtn = taskModal.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; taskModal.querySelector('#del_task_confirm').onclick = async () => { const isDeleteFile = taskModal.querySelector('#del_task_files').checked; taskModal.remove(); await executeBatchDelete(Array.from(S.sel), { isTask: true, deleteFiles: isDeleteFile, forceRefresh: true }); }; taskModal.tabIndex = 0; setTimeout(() => taskModal.focus(), 10); taskModal.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); taskModal.querySelector('#del_task_confirm').click(); } }); return; } ensureItemMap(); setLoad(true); const totalCount = S.sel.size; updateLoadTxt(`${L.str_checking_bl}\n0 / ${totalCount}`); await sleep(16); const blSet = S.blSet; const blFolderSet = S.blFolderSet; const toDeleteIds = []; let blacklistedCount = 0; let processed = 0; let lastYieldTime = performance.now(); let protectedCount = 0; for (let id of S.sel) { const item = S.itemMap.get(id); if (!item) { processed++; continue; } if (!S.trashMode && isSystemItem(item)) { protectedCount++; processed++; continue; } const lowerName = item.name.toLowerCase().trim(); const isFolder = item.kind === 'drive#folder'; const isProtectedMode = gmGet('pk_skip_bl_on_del', true); if (isProtectedMode && (isFolder ? blFolderSet.has(lowerName) : blSet.has(lowerName))) { blacklistedCount++; } else { toDeleteIds.push(id); } processed++; if ((processed % 500) === 0) { const now = performance.now(); if (now - lastYieldTime > 12) { updateLoadTxt(`${L.str_checking_bl}\n${processed} / ${totalCount}`); await sleep(0); lastYieldTime = performance.now(); } } } setLoad(false); if (blacklistedCount > 0 && toDeleteIds.length === 0) { showAlert(L.msg_del_protected.replace('{n}', blacklistedCount)); S.sel.clear(); refresh(); updateStat(); return; } if (toDeleteIds.length === 0) { showAlert(L.msg_del_none); return; } if (!await showConfirm(L.warn_del.replace('{n}', toDeleteIds.length))) return; await executeBatchDelete(toDeleteIds); }; UI.btnDeselect.onclick = () => { S.clearSelection(); refresh(); }; const processBlacklistAction = async (action) => { const totalSelected = S.sel.size; if (totalSelected === 0) return; const isRemove = action === 'remove'; const progressTask = FloatBarManager.create(L.str_init_op); const updateFloat = progressTask.update; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; updateFloat(L.str_stopping); }; await sleep(16); const getCleanKey = (str) => str ? str.toLowerCase().trim() : ""; const parseList = (str) => str ? str.split(/[\r\n]+/).map(s => s.trim()).filter(s => s) : []; const fileListStr = gmGet('pk_blacklist', ''); const folderListStr = gmGet('pk_blacklist_folders', ''); let currentFiles = parseList(fileListStr); let currentFolders = parseList(folderListStr); const targetFileKeys = new Set(); const targetFolderKeys = new Set(); const toAddFiles = []; const toAddFolders = []; let processedCount = 0; let lastYieldTime = performance.now(); const len = S.items.length; for (let i = 0; i < len; i++) { if (!isRunning) break; const item = S.items[i]; if (S.sel.has(item.id)) { const name = item.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim(); const key = getCleanKey(name); if (item.kind === 'drive#folder') { targetFolderKeys.add(key); if (!isRemove) toAddFolders.push(name); } else { targetFileKeys.add(key); if (!isRemove) toAddFiles.push(name); } processedCount++; if ((processedCount & 63) === 0) { const now = performance.now(); if (now - lastYieldTime > 12) { updateFloat(`${L.str_analyzing} ${processedCount} / ${totalSelected}`); await sleep(0); lastYieldTime = performance.now(); } } } } if (!isRunning) { progressTask.destroy(); showAlert(L.msg_bl_stop); return; } updateFloat(L.str_processing); await sleep(10); let finalCount = 0; let dataChanged = false; if (isRemove) { const oldFileCount = currentFiles.length; const oldFolderCount = currentFolders.length; currentFiles = currentFiles.filter(name => !targetFileKeys.has(getCleanKey(name))); currentFolders = currentFolders.filter(name => !targetFolderKeys.has(getCleanKey(name))); if (oldFileCount !== currentFiles.length || oldFolderCount !== currentFolders.length) { dataChanged = true; finalCount = (oldFileCount - currentFiles.length) + (oldFolderCount - currentFolders.length); } } else { const existingFileKeys = new Set(currentFiles.map(s => getCleanKey(s))); const existingFolderKeys = new Set(currentFolders.map(s => getCleanKey(s))); let addedCount = 0; for (const name of toAddFiles) { const key = getCleanKey(name); if (!existingFileKeys.has(key)) { currentFiles.push(name); existingFileKeys.add(key); addedCount++; dataChanged = true; } } for (const name of toAddFolders) { const key = getCleanKey(name); if (!existingFolderKeys.has(key)) { currentFolders.push(name); existingFolderKeys.add(key); addedCount++; dataChanged = true; } } finalCount = addedCount; } if (dataChanged) { updateFloat(L.str_saving); await sleep(10); gmSet('pk_blacklist', currentFiles.join('\n')); gmSet('pk_blacklist_folders', currentFolders.join('\n')); S.updateBlCache(); renderVisible(); } progressTask.destroy(); const msgTemplate = isRemove ? L.msg_bl_remove_done : L.msg_bl_add_done; showToast(msgTemplate.replace('{n}', finalCount)); }; UI.btnBlacklistManager.onclick = showBlacklistModal; if (UI.btnTrashBlacklistManager) UI.btnTrashBlacklistManager.onclick = showBlacklistModal; if (UI.uploadWrap && UI.btnUpload) { UI.btnUpload.onclick = (e) => { e.stopPropagation(); const menu = UI.uploadWrap.querySelector('.pk-dropdown-menu'); const isActive = menu.style.display === 'flex'; document.querySelectorAll('.pk-dropdown-menu, .pk-select-menu').forEach(m => m.style.display = 'none'); document.querySelectorAll('.pk-dropdown-wrap').forEach(w => w.classList.remove('active')); if (!isActive) { menu.style.display = 'flex'; UI.uploadWrap.classList.add('active'); } }; UI.actUpFile.onclick = (e) => { e.stopPropagation(); UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none'; UI.uploadWrap.classList.remove('active'); UI.inpFile.click(); }; UI.actUpFolder.onclick = (e) => { e.stopPropagation(); UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none'; UI.uploadWrap.classList.remove('active'); UI.inpFolder.click(); }; const updateRowUI = (task) => { const row = document.querySelector(`.pk-row[data-id="${task.id}"]`); if (row) { const progBar = row.querySelector('.pk-up-prog-bar'); const progTxt = row.querySelector('.pk-up-prog-txt'); const spdTxt = row.querySelector('.pk-up-spd'); const statusCol = row.children[4]; const msgSpan = statusCol ? statusCol.querySelector('span:first-child') : null; if (progBar) progBar.style.width = task.progress + '%'; if (progTxt) progTxt.textContent = Math.floor(task.progress) + '%'; if (msgSpan) { msgSpan.textContent = task.message; const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; if (activeStatus.includes(task.status)) { msgSpan.style.color = 'var(--pk-pri)'; if (progBar) progBar.style.backgroundColor = 'var(--pk-pri)'; } } if (spdTxt) spdTxt.innerHTML = task.status === 'DONE' ? '${L.lbl_done_check}' : S.upMng.fmtSpeed(task.speed); } }; const resolveTask = (task) => { if (S.uploadMode) updateRowUI(task); }; S.upMng = { limit: 3, running: 0, fmtSpeed: (bytesPerSec) => { if (bytesPerSec === 0) return '0 B/s'; const units = ['B/s', 'KB/s', 'MB/s', 'GB/s']; let i = 0; while (bytesPerSec >= 1024 && i < units.length - 1) { bytesPerSec /= 1024; i++; } return bytesPerSec.toFixed(2) + ' ' + units[i]; }, createTask: (file, parentId) => ({ id: 'up_' + Date.now() + '_' + Math.random().toString(36).substr(2), kind: 'pk#upload', file: file, name: file.name, size: file.size, parentId: parentId, status: 'WAITING', progress: 0, speed: 0, message: L.msg_task_waiting, _xhr: null, _lastCalcTime: 0, _lastCalcLoaded: 0, _lastUiTime: 0 }), scheduler: () => { if (S.upMng.running >= S.upMng.limit) return; const waiting = S.uploadTasks.find(t => t.status === 'WAITING'); if (waiting) S.upMng.start(waiting); }, start: async (task) => { if (S.quota && S.quota.limitRaw > 0) { const remaining = S.quota.limitRaw - S.quota.usedRaw; if (task.size > remaining) { task.status = 'ERROR'; task.message = L.err_quota_exceeded ; if (S.uploadMode) updateRowUI(task); S.upMng.scheduler(); return; } } S.upMng.running++; task.status = 'HASHING'; task.message = L.msg_task_hashing; task._totalUploadedBytes = 0; let _lastPollTime = Date.now(); let _lastPollBytes = 0; const speedTimer = setInterval(() => { if (task.status === 'UPLOADING') { const now = Date.now(); const timeDiff = now - _lastPollTime; const bytesDiff = task._totalUploadedBytes - _lastPollBytes; if (timeDiff > 0) { task.speed = Math.max(0, (bytesDiff / timeDiff) * 1000); } _lastPollTime = now; _lastPollBytes = task._totalUploadedBytes; if (S.uploadMode) updateRowUI(task); } }, 1500); const cryptoSign = async (secret, stringToSign) => { const enc = new TextEncoder(); const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]); const signature = await crypto.subtle.sign("HMAC", key, enc.encode(stringToSign)); return btoa(String.fromCharCode(...new Uint8Array(signature))); }; try { let finalParentId = task.parentId; if (finalParentId === 'root' || finalParentId === 'upload_root' || finalParentId === '') { const UPLOAD_FOLDER_NAME = 'My Upload'; const cacheKey = `lock_root_${UPLOAD_FOLDER_NAME}`; if (!S.upMng._syncLocks) S.upMng._syncLocks = new Map(); if (!S.upMng._syncLocks.has(cacheKey)) { const createAction = (async () => { const checkExisting = async (targetName) => { try { const list = await apiList('', 1000); const found = list.find(f => f.kind === 'drive#folder' && f.name === targetName); return found ? found.id : null; } catch (e) { return null; } }; let existingId = await checkExisting(UPLOAD_FOLDER_NAME); if (existingId) return existingId; let retry = 0; while (retry < 3) { try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: "drive#folder", parent_id: "", name: UPLOAD_FOLDER_NAME }) }); if (res.ok) { const data = await res.json(); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(''); if (typeof globalCache !== 'undefined') globalCache.delete(''); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); return data.file.id; } if (res.status === 400 || res.status === 429) { await new Promise(r => setTimeout(r, 1000)); existingId = await checkExisting(UPLOAD_FOLDER_NAME); if (existingId) return existingId; } } catch (e) {} retry++; } existingId = await checkExisting(UPLOAD_FOLDER_NAME); if (existingId) return existingId; throw new Error("My Upload folder creation failed"); })(); S.upMng._syncLocks.set(cacheKey, createAction); } try { finalParentId = await S.upMng._syncLocks.get(cacheKey); task.parentId = finalParentId; } catch (e) { S.upMng._syncLocks.delete(cacheKey); throw e; } } if (task.relativeFolder) { const folderNames = task.relativeFolder.split('/'); let currentPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (!S.upMng._syncLocks) S.upMng._syncLocks = new Map(); for (const name of folderNames) { const cacheKey = `lock_${currentPid}_${name}`; if (!S.upMng._syncLocks.has(cacheKey)) { const createAction = (async () => { const checkExisting = async (pid, targetName) => { try { const list = await apiList(pid, 1000); const found = list.find(f => f.kind === 'drive#folder' && f.name === targetName); return found ? found.id : null; } catch (e) { return null; } }; let existingId = await checkExisting(currentPid, name); if (existingId) return existingId; let retry = 0; while (retry < 3) { try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: "drive#folder", parent_id: currentPid, name: name }) }); if (res.ok) { const data = await res.json(); return data.file.id; } if (res.status === 400 || res.status === 429) { await new Promise(r => setTimeout(r, 1000)); existingId = await checkExisting(currentPid, name); if (existingId) return existingId; } } catch (e) {} retry++; } existingId = await checkExisting(currentPid, name); if (existingId) return existingId; throw new Error("Folder creation failed"); })(); S.upMng._syncLocks.set(cacheKey, createAction); } try { currentPid = await S.upMng._syncLocks.get(cacheKey); } catch (e) { S.upMng._syncLocks.delete(cacheKey); throw e; } } finalParentId = currentPid; } if (task._deleted) throw new Error("Aborted"); const hash = await calcSha1(task.file); if (task._deleted) throw new Error("Aborted"); task.status = 'UPLOADING'; task.message = L.msg_task_init_upload; _lastPollTime = Date.now(); _lastPollBytes = 0; const safePid = (finalParentId === 'root' || finalParentId === 'upload_root') ? '' : (finalParentId || ''); let res = null; let createRetry = 0; const maxCreateRetries = 5; while (createRetry < maxCreateRetries) { try { res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: "drive#file", parent_id: safePid, name: task.name, size: task.size, hash: hash, upload_type: "UPLOAD_TYPE_RESUMABLE" }) }); if (res.status === 429) { const waitMs = 2000 + Math.random() * 2000 * (createRetry + 1); console.warn(`[Upload] Rate limited (429). Retrying in ${Math.round(waitMs)}ms...`); await new Promise(r => setTimeout(r, waitMs)); createRetry++; continue; } if (!res.ok) { const errData = await res.json().catch(() => ({})); const errMsg = errData.error_description || `HTTP ${res.status}`; const isQuotaExceeded = res.status === 400 && (errData.error_code === 12 || errMsg.toLowerCase().includes('quota')); if (isQuotaExceeded) { const quotaErr = new Error(L.err_quota_exceeded); quotaErr.isFatal = true; throw quotaErr; } if (res.status === 404 || errMsg.toLowerCase().includes('not found') || errMsg.toLowerCase().includes('invalid parent')) { if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); throw new Error(L.err_parent_not_found); } throw new Error(errMsg); } break; } catch (e) { console.warn(`[Upload] Init request failed (${createRetry + 1}/${maxCreateRetries}):`, e.message); if (e.isFatal) throw e; createRetry++; if (createRetry >= maxCreateRetries) throw e; await new Promise(r => setTimeout(r, 1500)); } } const data = await res.json(); let newlyCreatedFileId = null; if (data.file) { if (data.file.id) { task.file_id = data.file.id; newlyCreatedFileId = data.file.id; } if (data.file.name) task.name = data.file.name; if (data.file.thumbnail_link) task.thumbnail_link = data.file.thumbnail_link; if (data.file.icon_link) task.icon_link = data.file.icon_link; } else if (data.task && data.task.file_id) { task.file_id = data.task.file_id; newlyCreatedFileId = data.task.file_id; if (data.task.name) task.name = data.task.name; } else if (data.id) { task.file_id = data.id; newlyCreatedFileId = data.id; if (data.name) task.name = data.name; } if (task._deleted) { console.warn(`[Upload] Task ${task.id} was deleted by user during initialization. Triggering self-destruct.`); if (newlyCreatedFileId && task._deleteFileIntent) { try { await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [newlyCreatedFileId] }) }); console.log(`[Upload] Ghost file ${newlyCreatedFileId} cleaned up.`); } catch (err) { console.error(`[Upload] Failed to cleanup ghost file ${newlyCreatedFileId}`, err); } } throw new Error("Aborted"); } if (data.upload_type === "UPLOAD_TYPE_URL" || data.phase === "PHASE_TYPE_COMPLETE" || (data.file && data.file.phase === "PHASE_TYPE_COMPLETE")) { task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_fast_success; if (S.uploadMode) { updateRowUI(task); requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); } setTimeout(() => updateQuotaUI(), 1000); if (typeof globalCache !== 'undefined') { const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (globalCache.has(targetPid)) { const cacheEntry = globalCache.get(targetPid); const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []); const newFileStub = { id: task.file_id, kind: 'drive#file', name: task.name, size: task.size, parent_id: targetPid, mime_type: task.mime_type || '', thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link, modified_time: new Date().toISOString(), hash: hash }; if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub); } } if (typeof globalDirtyFolders !== 'undefined') { const targetPid = task.parentId === 'root' ? '' : (task.parentId || ''); globalDirtyFolders.add(targetPid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } else if (data.resumable && data.resumable.params) { const p = data.resumable.params; const ossUA = 'aliyun-sdk-js/6.23.0 Microsoft Edge 144.0.0.0 on Windows 10 64-bit'; const objectName = p.key; const host = `https://${p.bucket}.${p.endpoint}`; const totalSize = task.file.size; let PART_SIZE = 5 * 1024 * 1024; if (totalSize > 4 * 1024 * 1024 * 1024) { PART_SIZE = 20 * 1024 * 1024; } else if (totalSize > 1 * 1024 * 1024 * 1024) { PART_SIZE = 10 * 1024 * 1024; } const partCount = Math.ceil(totalSize / PART_SIZE); const getAuth = async (method, subResource = '', contentType = '') => { const dateStr = new Date().toUTCString(); const headersToSign = { 'x-oss-date': dateStr, 'x-oss-security-token': p.security_token, 'x-oss-user-agent': ossUA }; let canonicalResource = `/${p.bucket}/${objectName}`; if (subResource) canonicalResource += `?${subResource}`; const canonicalHeaders = Object.keys(headersToSign).sort().map(k => `${k}:${headersToSign[k]}`).join('\n') + '\n'; const stringToSign = [method, "", contentType, dateStr, canonicalHeaders + canonicalResource].join("\n"); const signature = await cryptoSign(p.access_key_secret, stringToSign); return { date: dateStr, auth: `OSS ${p.access_key_id}:${signature}` }; }; const ossRequest = (method, query, body, contentType, onProgress) => { return new Promise(async (resolve, reject) => { try { const creds = await getAuth(method, query, contentType); const url = `${host}/${objectName.split('/').map(encodeURIComponent).join('/')}${query ? '?' + query : ''}`; const req = GM_xmlhttpRequest({ method: method, url: url, data: body, headers: { 'Authorization': creds.auth, 'x-oss-date': creds.date, 'x-oss-security-token': p.security_token, 'x-oss-user-agent': ossUA, 'Content-Type': contentType || '' }, upload: { onprogress: onProgress }, onload: (res) => { if (res.status >= 200 && res.status < 300) resolve(res); else reject(new Error(`OSS ${method} Error: ${res.status} ${res.statusText}`)); }, onerror: (err) => reject(new Error("Network Error")), onabort: () => reject(new Error("Aborted")) }); if (onProgress) task._xhr = { abort: () => req.abort() }; } catch (e) { reject(e); } }); }; task.message = L.msg_task_uploading; if (partCount <= 1) { await ossRequest('PUT', '', task.file, 'application/octet-stream', (pe) => { task._totalUploadedBytes = pe.loaded; task.progress = (pe.loaded / totalSize) * 100; const now = Date.now(); if (now - task._lastUiTime > 100) { if (S.uploadMode) updateRowUI(task); task._lastUiTime = now; } }); task.progress = 100; } else { task.message = L.msg_task_init_part; if (S.uploadMode) updateRowUI(task); const initRes = await ossRequest('POST', 'uploads', null, ''); const initXml = new DOMParser().parseFromString(initRes.responseText, "text/xml"); const uploadId = initXml.querySelector('UploadId')?.textContent || initXml.getElementsByTagName('UploadId')[0]?.textContent; if (!uploadId) throw new Error("Failed to get UploadId"); const parts = new Array(partCount); const CONCURRENCY = 3; let completedBytes = 0; const activeParts = new Map(); const updateProgress = () => { let activeTotal = 0; for (const bytes of activeParts.values()) activeTotal += bytes; const currentTotal = Math.min(totalSize, completedBytes + activeTotal); task._totalUploadedBytes = currentTotal; task.progress = (currentTotal / totalSize) * 100; const now = Date.now(); if (now - task._lastUiTime > 100) { if (S.uploadMode) updateRowUI(task); task._lastUiTime = now; } }; const pool = Array.from({length: partCount}, (_, k) => k + 1); const worker = async () => { while (pool.length > 0) { if (task.status === 'PAUSED' || !document.body.contains(el)) throw new Error("Aborted"); const i = pool.shift(); const startByte = (i - 1) * PART_SIZE; const endByte = Math.min(i * PART_SIZE, totalSize); const chunk = task.file.slice(startByte, endByte); activeParts.set(i, 0); const query = `partNumber=${i}&uploadId=${uploadId}`; const partRes = await ossRequest('PUT', query, chunk, 'application/octet-stream', (pe) => { activeParts.set(i, pe.loaded); updateProgress(); }); const finalizedSize = chunk.size; completedBytes += finalizedSize; activeParts.delete(i); updateProgress(); const etagHeader = partRes.responseHeaders.match(/etag:\s*"?([^"\r\n]+)"?/i); const etag = etagHeader ? etagHeader[1] : null; if (!etag) throw new Error(`Part ${i} missing ETag`); parts[i - 1] = { partNumber: i, etag: etag }; } }; task.message = L.msg_task_uploading_2; if (S.uploadMode) updateRowUI(task); const workers = Array(Math.min(partCount, CONCURRENCY)).fill(0).map(worker); await Promise.all(workers); if (task._deleted) throw new Error("Aborted"); const xmlBody = `${parts.map(p => `${p.partNumber}${p.etag}`).join('')}`; await ossRequest('POST', `uploadId=${uploadId}`, xmlBody, 'application/xml'); if (task._deleted && task.file_id && task._deleteFileIntent) { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [task.file_id] }) }).catch(()=>{}); throw new Error("Aborted"); } task.progress = 100; task._totalUploadedBytes = totalSize; setTimeout(() => updateQuotaUI(), 1500); const tryFetchMeta = async (isRetry = false) => { if (task._deleted) return true; try { const meta = await apiGet(task.file_id); if (meta) { if (meta.icon_link) task.icon_link = meta.icon_link; if (meta.mime_type) task.mime_type = meta.mime_type; if (meta.medias) task.medias = meta.medias; const isValidThumb = meta.thumbnail_link && meta.thumbnail_link !== meta.icon_link; if (isValidThumb) { const testUrl = meta.thumbnail_link + (meta.thumbnail_link.includes('?') ? '&' : '?') + '_t=' + Date.now(); const isImageReady = await new Promise(resolve => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = testUrl; }); if (isImageReady) { task.thumbnail_link = testUrl; if (typeof globalCache !== 'undefined') { const pid = task.parentId === 'root' ? '' : (task.parentId || ''); if (globalCache.has(pid)) { const list = globalCache.get(pid); const target = Array.isArray(list) ? list.find(f => f.id === task.file_id) : (list.items ? list.items.find(f => f.id === task.file_id) : null); if (target) target.thumbnail_link = testUrl; } } if (isRetry && S.uploadMode) { requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); console.log(`[Upload] Cover synced globally: ${task.name}`); } return true; } else { console.log(`[Upload] Cover CDN not ready (404). Retrying later...`); return false; } } } } catch(e) { console.warn("[Upload] Meta fetch warning", e); } return false; }; (async () => { if (await tryFetchMeta(false)) return; for (let i = 0; i < 8; i++) { await sleep(3500); if (await tryFetchMeta(true)) return; } for (let i = 0; i < 20; i++) { await sleep(15000); if (await tryFetchMeta(true)) return; } console.log(`[Upload] Entering infinite polling mode for: ${task.name}`); while (true) { await sleep(60000); if (await tryFetchMeta(true)) return; } })(); } task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_upload_done; if (S.uploadMode) { updateRowUI(task); requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); } if (typeof globalCache !== 'undefined') { const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (globalCache.has(targetPid)) { const cacheEntry = globalCache.get(targetPid); const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []); const newFileStub = { id: task.file_id, kind: 'drive#file', name: task.name, size: task.size, parent_id: targetPid, mime_type: task.mime_type || '', thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link, modified_time: new Date().toISOString(), hash: hash }; if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub); } } if (typeof globalDirtyFolders !== 'undefined') { const targetPid = task.parentId === 'root' ? '' : (task.parentId || ''); globalDirtyFolders.add(targetPid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } } catch (e) { const isManualAbort = e.message === 'Aborted'; task.status = isManualAbort ? 'PAUSED' : 'ERROR'; task.message = isManualAbort ? L.msg_task_paused : (e.message || L.err_unknown); if (S.uploadMode) updateRowUI(task); } finally { clearInterval(speedTimer); task._xhr = null; S.upMng.running--; if (S.uploadMode) { refresh(); } S.upMng.scheduler(); } }, pause: (task, skipRender = false) => { if (task.status === 'UPLOADING' && task._xhr) { task._xhr.abort(); } else if (task.status === 'WAITING') { task.status = 'PAUSED'; task.message = L.msg_task_paused; if (S.uploadMode && !skipRender) { refresh(); } } }, resume: (task, skipRender = false) => { if (task.status === 'PAUSED' || task.status === 'ERROR') { task.status = 'WAITING'; task.message = L.msg_task_waiting; S.upMng.scheduler(); if (S.uploadMode && !skipRender) { refresh(); } } } }; const handleUploadInput = async (files) => { if (!files || files.length === 0) return; if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); const curPath = S.path[S.path.length - 1]; const isVirtual = curPath.id.startsWith('virtual_') || curPath.id.includes('_root') || curPath.id === 'upload_root'; const safeParentId = (curPath.id && !isVirtual) ? curPath.id : ''; let addedCount = 0; const processEntry = async (entry, parentId) => { if (entry.isFile) { return new Promise(resolve => { entry.file(file => { if (file.name.startsWith('.')) return resolve(); if (S.upMng) { const task = S.upMng.createTask(file, parentId); S.uploadTasks.push(task); addedCount++; } resolve(); }); }); } else if (entry.isDirectory) { } }; const fileList = Array.from(files); for (const file of fileList) { if (file.name.startsWith('.')) continue; let relativeFolder = ""; if (file.webkitRelativePath) { const parts = file.webkitRelativePath.split('/'); if (parts.length > 1) { parts.pop(); relativeFolder = parts.join('/'); } } if (S.upMng) { const task = S.upMng.createTask(file, safeParentId); task.relativeFolder = relativeFolder; S.uploadTasks.unshift(task); addedCount++; } } showToast(L.msg_task_added.replace('{n}', addedCount)); if (!S.uploadMode) { switchTab('upload'); } else { renderVisible(); updateStat(); } if (S.upMng) S.upMng.scheduler(); UI.inpFile.value = ''; UI.inpFolder.value = ''; }; UI.inpFile.onchange = (e) => handleUploadInput(e.target.files); UI.inpFolder.onchange = (e) => handleUploadInput(e.target.files); const dragMask = document.createElement('div'); dragMask.className = 'pk-drag-mask'; UI.win.appendChild(dragMask); const parseEntries = async (entries, relPath = "") => { for (const entry of entries) { if (entry.isFile) { const file = await new Promise(res => entry.file(res)); const curPath = S.path[S.path.length - 1]; const safeParentId = (curPath.id && !curPath.id.includes('_root')) ? curPath.id : ''; const task = S.upMng.createTask(file, safeParentId); task.relativeFolder = relPath; S.uploadTasks.unshift(task); } else if (entry.isDirectory) { const reader = entry.createReader(); const subEntries = await new Promise(res => reader.readEntries(res)); await parseEntries(subEntries, (relPath ? relPath + "/" : "") + entry.name); } } }; const canDragUpload = () => { return !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.uploadMode; }; let dragCounter = 0; const handleDragGuard = (e) => { e.preventDefault(); e.stopPropagation(); if (canDragUpload()) { e.dataTransfer.dropEffect = 'copy'; } else { e.dataTransfer.dropEffect = 'none'; } }; el.addEventListener('dragenter', (e) => { handleDragGuard(e); if (!canDragUpload()) return; dragCounter++; const curPath = S.path[S.path.length - 1]; const isRoot = S.path.length === 1 && (curPath.id === '' || curPath.id === 'root'); let destHtml = ""; if (isRoot) { const homeIcon = CONF.icons.home.replace('width="24"', 'width="16"').replace('height="24"', 'height="16"').replace('
${L.msg_drag_drop_hint}
${L.lbl_upload_to}${destHtml}
`; dragMask.style.display = 'flex'; }); el.addEventListener('dragover', handleDragGuard); el.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); if (!canDragUpload()) return; dragCounter--; if (dragCounter <= 0) { dragMask.style.display = 'none'; dragCounter = 0; } }); el.addEventListener('drop', async (e) => { handleDragGuard(e); if (!canDragUpload()) return; dragMask.style.display = 'none'; dragCounter = 0; const items = e.dataTransfer.items; if (!items) return; if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); const entries =[]; for (let i = 0; i < items.length; i++) { const entry = items[i].webkitGetAsEntry(); if (entry) entries.push(entry); } if (entries.length > 0) { await parseEntries(entries); if (!S.uploadMode) switchTab('upload'); else { refresh(); updateStat(); } if (S.upMng) S.upMng.scheduler(); } }); document.addEventListener('click', (e) => { if (UI.uploadWrap && !UI.uploadWrap.contains(e.target)) { UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none'; UI.uploadWrap.classList.remove('active'); } }); } const switchTab = (mode) => { if (S.abortController) S.abortController.abort(); activeLoadId++; S.sortId++; if (S.loading) { S.loading = false; } S.items = []; S.display = []; S.recentResultItems = null; S.itemMap.clear(); S.sel.clear(); if (UI.in) UI.in.innerHTML = ''; if (UI.vp) UI.vp.scrollTop = 0; S.sort = (mode === 'history') ? 'play_time' : ((mode === 'offline') ? 'created_time' : 'modified_time'); if (mode === 'recent' || mode === 'history') { S.dir = 1; } S.dir = (mode === 'recent' || mode === 'offline') ? 1 : S.dir; S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); if (UI.chkGlobal && !S.trashMode && !S.shareMode && !S.starredMode && !S.offlineMode && !S.historyMode && !S.recentMode && !S.uploadMode) { S.wasGlobalChecked = UI.chkGlobal.checked; } S.trashMode = (mode === 'trash'); S.shareMode = (mode === 'share'); S.starredMode = (mode === 'starred'); S.recentMode = (mode === 'recent'); S.historyMode = (mode === 'history'); S.offlineMode = (mode === 'offline'); S.uploadMode = (mode === 'upload'); S.scanFilter = null; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; let rootName = L.btn_nav_home; if (S.trashMode) rootName = L.btn_nav_trash; if (S.shareMode) rootName = L.btn_nav_share; if (S.starredMode) rootName = L.btn_nav_starred; if (S.recentMode) rootName = L.btn_nav_recent; if (S.historyMode) rootName = L.btn_nav_history; if (S.offlineMode) rootName = L.title_offline; if (S.uploadMode) rootName = L.btn_nav_upload; if (S.offlineMode) { S.path = [{ id: 'offline_root', name: rootName }]; } else if (S.uploadMode) { S.path = [{ id: 'upload_root', name: rootName }]; } else if (S.recentMode) { S.path = [{ id: 'recent_root', name: rootName }]; } else if (S.historyMode) { S.path = [{ id: 'history_root', name: rootName }]; } else { S.path = [{ id: '', name: rootName }]; } UI.btnNavHome.classList.toggle('act', mode === 'home'); UI.btnNavTrash.classList.toggle('act', mode === 'trash'); if (UI.btnNavShare) UI.btnNavShare.classList.toggle('act', mode === 'share'); if (UI.btnNavStarred) UI.btnNavStarred.classList.toggle('act', mode === 'starred'); if (UI.btnNavRecent) UI.btnNavRecent.classList.toggle('act', mode === 'recent'); if (UI.btnNavHistory) UI.btnNavHistory.classList.toggle('act', mode === 'history'); if (UI.btnNavOffline) UI.btnNavOffline.classList.toggle('act', mode === 'offline'); if (UI.btnNavUpload) UI.btnNavUpload.classList.toggle('act', mode === 'upload'); if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.actionBar) UI.actionBar.style.display = 'flex'; if(UI.trashBar) UI.trashBar.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if(UI.crumb) { UI.crumb.style.opacity = '1'; UI.crumb.style.display = 'flex'; } if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; const stdBtns = [UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnBlacklistManager]; const shareBtns = [document.getElementById('pk-cancel-share')]; const upBtns = [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll]; const upSep = document.getElementById('pk-up-sep'); upBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(upSep) upSep.style.display = 'none'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; if (S.historyMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; [UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.scan) UI.scan.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.searchInput && UI.searchInput.parentNode) { UI.searchInput.parentNode.style.display = 'flex'; } if (UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; } else if (S.starredMode || S.recentMode) { UI.win.classList.remove('pk-mode-trash'); [UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; } else if (S.shareMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; } else if (S.offlineMode) { UI.win.classList.remove('pk-mode-trash'); [UI.btnNewFolder, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip].forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnDel, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; } else if (S.uploadMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(upSep) upSep.style.display = 'block'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex';[UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; } else if (S.trashMode) { UI.win.classList.add('pk-mode-trash'); if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.actionBar) UI.actionBar.style.display = 'none'; if(UI.trashBar) UI.trashBar.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; } else { UI.win.classList.remove('pk-mode-trash'); [UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if(UI.scan) UI.scan.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (UI.chkGlobal && typeof S.wasGlobalChecked !== 'undefined') { UI.chkGlobal.checked = S.wasGlobalChecked; } } if (S.starredMode || S.offlineMode || S.uploadMode) { if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; } S.clearSelection(); const isOffline = mode === 'offline'; const isRecent = mode === 'recent'; const realKey = S.getRealCacheKey(isOffline ? 'offline_root' : (isRecent ? 'recent_root' : '')); const hasCache = typeof globalCache !== 'undefined' && globalCache.has(realKey); const session = typeof globalCache !== 'undefined' ? globalCache.get('offline_session') : null; const isResumingOffline = isOffline && session && !session.completed; const recentCache = isRecent && hasCache ? globalCache.get(realKey) : null; const isResumingRecent = isRecent && recentCache && !Array.isArray(recentCache) && recentCache.nextToken; if (!((isOffline && (hasCache || isResumingOffline)) || (isRecent && (hasCache || isResumingRecent)))) { setLoad(true, true); } load(false, !(isOffline || isRecent)); if (window.pkSmartRefreshTrigger) { setTimeout(() => window.pkSmartRefreshTrigger(isOffline || isRecent), 100); } }; const btnCloud = el.querySelector('#pk-btn-cloud'); const showFolderSelector = (initialId, onConfirm, initialPath = null, fileFilter = null, customTitle = null) => { let currentPath = initialPath ? JSON.parse(JSON.stringify(initialPath)) : [{ id: '', name: L.picker_all }]; if (currentPath.length > 0) currentPath[0].name = L.picker_all; let currentList = []; let selectedFile = null; let sortMode = 'new'; const titleStr = customTitle || (fileFilter ? L.title_select_file : L.picker_title); const picker = showModal(`

${titleStr}

${CONF.icons.close}
${L.picker_sort_new}
${L.loading}
${CONF.icons.newfolder} ${L.picker_new}
`); const mContent = picker.querySelector('.pk-modal'); mContent.style.padding = '20px'; mContent.style.width = 'fit-content'; picker.querySelector('.pk-modal-close').style.display = 'none'; const crumbEl = picker.querySelector('#pk_picker_crumb'); const listEl = picker.querySelector('#pk_picker_list'); listEl.onscroll = () => { const oldPop = document.querySelector('.pk-crumb-pop'); if (oldPop && typeof oldPop._cleanup === 'function') oldPop._cleanup(); }; const sortTrigger = picker.querySelector('#pk_sort_trigger'); const sortMenu = picker.querySelector('#pk_sort_menu'); const sortTxt = picker.querySelector('#pk_sort_txt'); const closeBtn = picker.querySelector('#pk_picker_close_btn'); closeBtn.onmouseover = () => closeBtn.style.background = 'var(--pk-hl)'; closeBtn.onmouseout = () => closeBtn.style.background = 'transparent'; closeBtn.onclick = () => picker.remove(); const applySort = () => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); currentList.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1; const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortMode === 'az') return collator.compare(a.name, b.name); if (sortMode === 'za') return collator.compare(b.name, a.name); if (sortMode === 'new') return new Date(b.modified_time) - new Date(a.modified_time); if (sortMode === 'old') return new Date(a.modified_time) - new Date(b.modified_time); return 0; }); }; const renderList = () => { listEl.innerHTML = ''; if (currentList.length === 0) { listEl.innerHTML = `
${CONF.emptySVG}
${L.str_no_files}
`; return; } currentList.forEach(item => { const div = document.createElement('div'); div.style.cssText = "display:flex; align-items:center; padding:10px 8px; cursor:pointer; border-radius:6px; transition:background 0.1s; border-bottom:1px dashed var(--pk-bd);"; const isDir = item.kind === 'drive#folder'; const isSelected = selectedFile && selectedFile.id === item.id; let checkHtml = ""; if (!isDir && fileFilter) { checkHtml = ``; } const iconSrc = item.icon_link || ''; let iconHtml = ''; if (isDir) { const fallbackSvg = CONF.typeIcons.folder.replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"'); iconHtml = iconSrc ? `` : `
${fallbackSvg}
`; } else { const fallbackSvg = getIcon(item).replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"'); iconHtml = iconSrc ? `` : `
${fallbackSvg}
`; } div.innerHTML = ` ${checkHtml} ${iconHtml}
${esc(item.name)}
${!isDir ? `
${fmtSize(item.size)}
` : ''} `; div.onmouseover = () => div.style.background = 'var(--pk-hl)'; div.onmouseout = () => div.style.background = 'transparent'; if (isSelected) div.style.background = 'var(--pk-sel-bg)'; div.onclick = (e) => { if (isDir) { selectedFile = null; loadFolder(item.id, item.name); } else if (fileFilter) { selectedFile = (selectedFile && selectedFile.id === item.id) ? null : item; renderList(); } }; listEl.appendChild(div); }); }; let _pickerCrumbIdx = 0; let _lastPickerScroll = 0; const showPickerDropdown = async (e, parentId, triggerEl) => { const old = document.querySelector('.pk-crumb-pop'); if (old) { const wasSame = old._sourceEl === triggerEl; if (typeof old._cleanup === 'function') old._cleanup(); if (wasSame) return; } triggerEl.style.background = 'transparent'; triggerEl.innerHTML = CONF.crumbIcons.down; const svgD = triggerEl.querySelector('svg'); if (svgD) { svgD.style.width = '14px'; svgD.style.height = '14px'; svgD.style.display = 'block'; } const pop = document.createElement('div'); pop.className = 'pk-crumb-pop pk-scroll pk-show'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.style.zIndex = '2147483647'; pop._sourceEl = triggerEl; document.body.appendChild(pop); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; pop.style.zoom = scale; const rect = triggerEl.getBoundingClientRect(); pop.style.top = ((rect.bottom / scale) + 5) + 'px'; pop.style.left = (rect.left / scale) + 'px'; const cleanup = () => { if (pop.parentNode) pop.remove(); triggerEl.innerHTML = CONF.crumbIcons.right; const svgR = triggerEl.querySelector('svg'); if (svgR) { svgR.style.width = '14px'; svgR.style.height = '14px'; svgR.style.display = 'block'; svgR.style.opacity = '0.6'; } document.removeEventListener('mousedown', closer); window.removeEventListener('resize', cleanup); }; pop._cleanup = cleanup; const closer = (ev) => { if (!pop.contains(ev.target) && ev.target !== triggerEl) cleanup(); }; document.addEventListener('mousedown', closer); window.addEventListener('resize', cleanup); const cacheKey = parentId || 'root'; let folders = null; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) { const raw = globalCache.get(cacheKey); if (Array.isArray(raw) || (raw && raw.items && !raw.nextToken)) { const items = Array.isArray(raw) ? raw : raw.items; folders = items.filter(f => f.kind === 'drive#folder'); } } const renderMenu = (list) => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); list.sort((a, b) => { const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortMode === 'new') return new Date(b.modified_time || 0) - new Date(a.modified_time || 0); return collator.compare(a.name, b.name); }); if (list.length === 0) { cleanup(); return; } pop.innerHTML = ''; list.forEach(f => { const itemDiv = document.createElement('div'); itemDiv.className = 'pk-crumb-item'; const iconSrc = f.icon_link || ''; const fallbackSvg = CONF.typeIcons.folder.replace(/width="\d+"/, 'width="18"').replace(/height="\d+"/, 'height="18"'); const iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; itemDiv.innerHTML = `${iconHtml}${esc(f.name)}`; itemDiv.onclick = (ev) => { ev.stopPropagation(); cleanup(); const idx = currentPath.findIndex(p => p.id === parentId); if (idx !== -1) { currentPath = currentPath.slice(0, idx + 1); loadFolder(f.id, f.name); } }; pop.appendChild(itemDiv); }); }; if (folders !== null) { renderMenu(folders); } else { pop.innerHTML = `
`; try { const listItems = await apiList(parentId || '', 1000); if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, listItems); renderMenu(listItems.filter(f => f.kind === 'drive#folder')); } catch (err) { cleanup(); } } }; const renderCrumb = () => { crumbEl.innerHTML = ''; currentPath.forEach((p, i) => { const sp = document.createElement('span'); const isLast = i === currentPath.length - 1; if (i === 0) { const homeIcon = CONF.icons.home.replace(' { currentPath = currentPath.slice(0, i + 1); loadFolder(p.id, null, true); }; sp.onmouseover = () => sp.style.background = 'var(--pk-hl)'; sp.onmouseout = () => sp.style.background = 'transparent'; } crumbEl.appendChild(sp); if (!isLast) { const sep = document.createElement('span'); sep.className = 'pk-picker-arrow'; sep.innerHTML = CONF.crumbIcons.right; const svg = sep.querySelector('svg'); if(svg) { svg.style.width = '14px'; svg.style.height = '14px'; svg.style.display = 'block'; svg.style.opacity = '0.6'; } sep.style.cssText = "margin:0 2px; display:flex; align-items:center; cursor:pointer; padding:4px; border-radius:4px; transition:all 0.2s; flex-shrink:0; color:var(--pk-icon-c);"; sep.onmouseover = () => { sep.style.background = 'var(--pk-hl)'; sep.style.color = 'var(--pk-pri)'; if(svg) svg.style.opacity='1'; }; sep.onmouseout = () => { sep.style.background = 'transparent'; sep.style.color = 'var(--pk-icon-c)'; if(svg) svg.style.opacity='0.6'; }; sep.onclick = (e) => { e.stopPropagation(); if (typeof showPickerDropdown === 'function') showPickerDropdown(e, p.id, sep); }; crumbEl.appendChild(sep); } }); _pickerCrumbIdx = currentPath.length - 1; requestAnimationFrame(() => { crumbEl.scrollLeft = crumbEl.scrollWidth; }); }; const handlePickerWheel = (e) => { e.preventDefault(); document.querySelectorAll('.pk-crumb-pop').forEach(p => { if (typeof p._cleanup === 'function') p._cleanup(); }); const now = Date.now(); if (now - _lastPickerScroll < 120) return; _lastPickerScroll = now; const nodes = Array.from(crumbEl.children).filter(c => !c.classList.contains('pk-picker-arrow')); if (!nodes.length) return; if (e.deltaY < 0) _pickerCrumbIdx = Math.max(0, _pickerCrumbIdx - 1); else _pickerCrumbIdx = Math.min(nodes.length - 1, _pickerCrumbIdx + 1); const target = nodes[_pickerCrumbIdx]; if (target) { const centerOffset = target.offsetLeft + (target.offsetWidth / 2) - (crumbEl.clientWidth / 2); crumbEl.scrollTo({ left: centerOffset, behavior: 'smooth' }); } }; crumbEl.addEventListener('wheel', handlePickerWheel, { passive: false }); const loadFolder = async (id, name, isBack = false) => { listEl.innerHTML = `
`; if (name && !isBack) currentPath.push({ id, name }); renderCrumb(); const cacheKey = id || 'root'; let items = null; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) { const raw = globalCache.get(cacheKey); const isComplete = Array.isArray(raw) || (raw && raw.items && !raw.nextToken); if (isComplete) { items = Array.isArray(raw) ? raw : raw.items; } } try { if (items === null) { items = await apiList(id || '', 1000); if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, items); indexParents(id, name || L.picker_all, items); } currentList = items.filter(i => { if (i.kind === 'drive#folder') return true; if (fileFilter && fileFilter(i)) return true; return false; }); applySort(); renderList(); } catch (e) { listEl.innerHTML = `
${L.str_error}: ${esc(e.message)}
`; } }; picker.querySelector('#pk_picker_cancel').onclick = () => picker.remove(); picker.querySelector('#pk_picker_ok').onclick = () => { const cur = currentPath[currentPath.length - 1]; const returnPathChain = JSON.parse(JSON.stringify(currentPath)); if (fileFilter && selectedFile) { onConfirm(selectedFile.id, selectedFile.name, selectedFile, returnPathChain); } else { onConfirm(cur.id, cur.name, null, returnPathChain); } picker.remove(); }; if (!fileFilter) { picker.querySelector('#pk_picker_new').onclick = async () => { const cur = currentPath[currentPath.length - 1]; const name = await showPrompt(L.msg_newfolder_prompt, '', L.picker_new); if (name) { try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: 'drive#folder', parent_id: cur.id || '', name: name }) }); if (!res.ok) throw new Error("Create Failed"); const data = await res.json(); const newFolder = data.file || data; const parentId = cur.id || 'root'; if (typeof globalCache !== 'undefined') globalCache.delete(parentId); if (S.cache) S.cache.delete(parentId); globalDirtyFolders.add(parentId); gmSet('pk_fmod_' + parentId, new Date(getServerNow()).toISOString()); if (newFolder && newFolder.id) await loadFolder(newFolder.id, newFolder.name); else await loadFolder(cur.id, null, true); } catch(e) { showAlert(e.message); } } }; } sortTrigger.onclick = (e) => { e.stopPropagation(); const isOpening = sortMenu.style.display !== 'block'; sortMenu.style.display = isOpening ? 'block' : 'none'; sortTrigger.style.background = isOpening ? 'var(--pk-hl)' : 'transparent'; }; picker.querySelectorAll('.pk-sort-opt').forEach(el => { el.onclick = () => { sortMode = el.dataset.val; sortTxt.textContent = el.textContent; sortMenu.style.display = 'none'; sortTrigger.style.background = 'transparent'; applySort(); renderList(); }; el.onmouseover = () => { el.style.background = 'var(--pk-hl)'; el.style.color = 'var(--pk-pri)'; }; el.onmouseout = () => { el.style.background = 'transparent'; el.style.color = 'var(--pk-fg)'; }; }); const closeSortMenu = () => { if (sortMenu) sortMenu.style.display = 'none'; if (sortTrigger) sortTrigger.style.background = 'transparent'; }; setTimeout(() => document.addEventListener('click', closeSortMenu), 0); const _orgRemove = picker.remove.bind(picker); picker.remove = () => { document.removeEventListener('click', closeSortMenu); _orgRemove(); }; loadFolder(initialId || '', null, true); }; if (btnCloud) { btnCloud.onclick = () => { const curFolder = S.path[S.path.length - 1]; const isVirtual = curFolder.id.startsWith('virtual_') || curFolder.id.includes('_root') || curFolder.id === 'analyze_root'; const isHomeSubDir = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.isFlattened && !S.dupMode && S.path.length > 1 && !isVirtual; let saveToId = isHomeSubDir ? (curFolder.id || '') : ''; let saveToName = isHomeSubDir ? (curFolder.name || L.lbl_default_folder) : L.lbl_default_folder; let currentSavePath = isHomeSubDir ? S.path.filter(p => !p.id.startsWith('virtual_')) : null; const m = showModal(`

${L.title_cloud_task}

${L.lbl_save_to}
${CONF.typeIcons.folder.replace('width="30"', 'width="20"').replace('height="30"', 'height="20"')}
${saveToName}
${L.btn_modify}
${L.btn_via_torrent}
`); const modalBox = m.querySelector('.pk-modal'); modalBox.style.width = "560px"; modalBox.style.padding = "30px"; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) { closeBtn.style.top = "36px"; closeBtn.style.right = "30px"; } const input = m.querySelector('#pk_cloud_input'); const submit = m.querySelector('#cloud_submit'); const dirLabel = m.querySelector('#pk_cloud_dir_name'); input.focus(); m.querySelector('#pk_cloud_change_dir').onclick = () => { showFolderSelector(saveToId, (id, name, fullItem, selectedPathChain) => { saveToId = id; saveToName = name; dirLabel.textContent = name; if (selectedPathChain) { currentSavePath = selectedPathChain; } }, currentSavePath); }; const smartFixCheckbox = m.querySelector('#pk_cloud_smart_fix'); if (smartFixCheckbox) smartFixCheckbox.onchange = () => input.dispatchEvent(new Event('input')); const parseAndCleanLinks = (rawString, isSmartFix) => { const formattedVal = rawString.replace(/(https?:\/\/|ftp:\/\/|sftp:\/\/|magnet:\?|ed2k:\/\/|thunder:\/\/)/gi, '\n$1'); const lines = formattedVal.split('\n').map(l => l.trim()).filter(l => l); const uniqueTasks = new Map(); const linkRegex = /^(https?:\/\/|ftp:\/\/|sftp:\/\/|magnet:\?|ed2k:\/\/|thunder:\/\/)/i; const base32ToHex = (b32) => { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bits = ""; const input = b32.toUpperCase(); for (let i = 0; i < input.length; i++) { const val = alphabet.indexOf(input[i]); if (val === -1) return null; bits += val.toString(2).padStart(5, '0'); } let hex = ""; for (let i = 0; i + 4 <= bits.length; i += 4) { const chunk = bits.substring(i, i + 4); const num = parseInt(chunk, 2); if (!isNaN(num)) hex += num.toString(16); } return hex.toUpperCase(); }; const extractHexHash = (url) => { const match = url.match(/urn:btih:([^&]+)/i); if (!match) return null; const hash = match[1].toUpperCase(); if (hash.length === 40) return hash; if (hash.length === 32) return base32ToHex(hash); return null; }; const addResult = (url) => { const hash = extractHexHash(url); if (hash) { if (!uniqueTasks.has(hash)) { uniqueTasks.set(hash, url); } } else { uniqueTasks.set(url, url); } }; for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (linkRegex.test(line)) { addResult(line); continue; } if (isSmartFix) { let cleanedStr = line.replace(/[^a-zA-Z0-9]/g, ''); const hexMatches = cleanedStr.matchAll(/([a-fA-F0-9]{40})/g); for (const match of hexMatches) { addResult(`magnet:?xt=urn:btih:${match[1].toUpperCase()}`); cleanedStr = cleanedStr.replace(match[1], ' '.repeat(40)); } const b32Matches = cleanedStr.matchAll(/([a-zA-Z2-7]{32})/g); for (const match of b32Matches) { const hex = base32ToHex(match[1].toUpperCase()); if (hex) { addResult(`magnet:?xt=urn:btih:${hex}`); } } } } return Array.from(uniqueTasks.values()); }; input.oninput = () => { const rawVal = input.value.trim(); const isSmartFix = smartFixCheckbox ? smartFixCheckbox.checked : false; const linesRaw = rawVal.split('\n').map(l => l.trim()).filter(l => l); const hasInput = linesRaw.length > 0; const finalLinks = parseAndCleanLinks(rawVal, isSmartFix); const allValid = hasInput && (finalLinks.length === linesRaw.length || finalLinks.length > 0); const isError = hasInput && !allValid; m.querySelector('#pk_cloud_error').style.display = isError ? 'block' : 'none'; submit.disabled = !allValid; }; m.querySelector('#cloud_cancel').onclick = () => m.remove(); const torrentTrigger = m.querySelector('#pk_cloud_torrent_trigger'); const torrentFile = m.querySelector('#pk_cloud_torrent_file'); torrentTrigger.onclick = () => torrentFile.click(); const showSnapshotModal = (urlList, defaultId, defaultName) => { return new Promise((resolve) => { let currentSaveId = defaultId; let currentSaveName = defaultName; const displayUrl = urlList[0]; const countSuffix = urlList.length > 1 ? L.str_snap_link_count_suffix.replace('{n}', urlList.length) : ''; const snapIcon = ``; const sm = showModal(`

${L.title_save_method}

${L.msg_save_snapshot_desc}
${snapIcon}
${esc(displayUrl)}${countSuffix}
${L.lbl_save_to}
${CONF.typeIcons.folder.replace('width="30"', 'width="20"').replace('height="30"', 'height="20"')} ${esc(currentSaveName)} ${L.btn_modify}
`); const mBox = sm.querySelector('.pk-modal'); if (mBox) { mBox.style.padding = '24px'; mBox.style.width = 'auto'; } const closeBtn = sm.querySelector('.pk-modal-close'); if (closeBtn) { closeBtn.style.top = '30px'; closeBtn.style.right = '24px'; } let snapCurrentPath = currentSavePath; sm.querySelector('#snap_change_dir').onclick = () => { showFolderSelector(currentSaveId, (id, name, fullItem, selectedPathChain) => { currentSaveId = id; currentSaveName = name; sm.querySelector('#snap_dir_name').textContent = name; if (selectedPathChain) { snapCurrentPath = selectedPathChain; currentSavePath = selectedPathChain; saveToId = id; saveToName = name; dirLabel.textContent = name; } }, snapCurrentPath); }; const doClose = (result) => { sm.remove(); resolve(result ? { confirm: true, targetId: currentSaveId } : { confirm: false }); }; sm.querySelector('#snap_cancel').onclick = () => doClose(false); if (closeBtn) closeBtn.onclick = () => doClose(false); sm.querySelector('#snap_save').onclick = () => doClose(true); sm.tabIndex = 0; setTimeout(() => sm.focus(), 10); sm.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); sm.querySelector('#snap_save').click(); } }); }); }; torrentFile.onchange = async (e) => { const files = e.target.files; if (!files || files.length === 0) return; m.remove(); const fb = FloatBarManager.create(L.str_parsing_torrent); let successCount = 0; let failCount = 0; for (let i = 0; i < files.length; i++) { const file = files[i]; fb.update(`${L.str_parsing_torrent} (${i + 1}/${files.length})`); try { const magnetLink = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async (evt) => { try { const buf = new Uint8Array(evt.target.result); let pos = 0; const decodeSkip = () => { if (pos >= buf.length) return; const c = buf[pos]; if (c === 100 || c === 108) { pos++; while (pos < buf.length && buf[pos] !== 101) decodeSkip(); pos++; } else if (c === 105) { pos++; while (pos < buf.length && buf[pos] !== 101) pos++; pos++; } else if (c >= 48 && c <= 57) { let colon = pos; while (colon < buf.length && buf[colon] !== 58) colon++; const lenStr = new TextDecoder().decode(buf.slice(pos, colon)); const len = parseInt(lenStr, 10); pos = colon + 1 + len; } }; if (buf[0] !== 100) throw new Error(L.err_invalid_torrent); pos = 1; let infoHash = null; while (pos < buf.length && buf[pos] !== 101) { const keyStart = pos; decodeSkip(); let colon = keyStart; while (colon < buf.length && buf[colon] !== 58) colon++; const kLen = parseInt(new TextDecoder().decode(buf.slice(keyStart, colon))); const keyStr = new TextDecoder().decode(buf.slice(colon + 1, colon + 1 + kLen)); if (keyStr === "info") { const infoStart = pos; decodeSkip(); const infoEnd = pos; const infoBuf = buf.slice(infoStart, infoEnd); const hashBuf = await crypto.subtle.digest("SHA-1", infoBuf); infoHash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join(''); break; } else { decodeSkip(); } } if (infoHash) resolve(`magnet:?xt=urn:btih:${infoHash}&dn=${encodeURIComponent(file.name)}`); else reject(new Error(L.err_torrent_no_info)); } catch (err) { reject(err); } }; reader.onerror = () => reject(new Error(L.err_file_read)); reader.readAsArrayBuffer(file); }); fb.update(`${L.msg_creating_cloud_task} (${i + 1}/${files.length})`); let retry = 0; while (retry < 3) { try { await apiAddOfflineTask(magnetLink, saveToId); successCount++; break; } catch (reqErr) { if (reqErr.message && reqErr.message.includes('429')) { retry++; await sleep(2000 * retry); } else { throw reqErr; } } } } catch (e) { console.error(`Torrent parse/upload failed for ${file.name}:`, e); failCount++; } } fb.destroy(); if (failCount > 0) { showToast(L.msg_cloud_task_finish.replace('{s}', successCount).replace('{f}', failCount), 'warning'); } else if (successCount > 0) { showToast(L.msg_cloud_task_success.replace('{n}', successCount)); } if (successCount > 0) { if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; setTimeout(() => updateQuotaUI(), 1000); const curPathId = S.path[S.path.length - 1].id || ''; if (S.offlineMode || (saveToId && curPathId === saveToId)) { load(false, true); } else if (!saveToId && S.path.length === 1 && window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(true); } } }; submit.onclick = async () => { if (submit.disabled) return; const rawVal = input.value.trim(); if (!rawVal) return; const isSmartFix = smartFixCheckbox ? smartFixCheckbox.checked : false; m.style.display = 'none'; const finalLinks = parseAndCleanLinks(rawVal, isSmartFix); const videoPlatformRegex = /(?:youtube\.com|youtu\.be|twitter\.com|x\.com|tiktok\.com|douyin\.com|facebook\.com|fb\.watch|instagram\.com|t\.me|bilibili\.com)/i; const snapshotLinks = finalLinks.filter(l => /^https?:\/\//i.test(l) && !videoPlatformRegex.test(l)); const directLinks = finalLinks.filter(l => !/^https?:\/\//i.test(l) || videoPlatformRegex.test(l)); let snapshotParams = null; const isSingleSnapshotTask = (finalLinks.length === 1 && snapshotLinks.length === 1); if (isSingleSnapshotTask) { const result = await showSnapshotModal(snapshotLinks, saveToId, saveToName); if (!result.confirm) { m.style.display = 'flex'; return; } snapshotParams = { save_as: 'snapshot', targetId: result.targetId }; } m.remove(); const progressTask = FloatBarManager.create(L.msg_creating_cloud_task); let successCount = 0; let failCount = 0; const processQueue =[ ...directLinks.map(u => ({ url: u, isSnap: false })), ...snapshotLinks.map(u => ({ url: u, isSnap: true })) ]; for (let i = 0; i < processQueue.length; i++) { const item = processQueue[i]; progressTask.update(L.str_creating_task_n.replace('{n}', i + 1).replace('{t}', processQueue.length)); try { const pid = (item.isSnap && snapshotParams) ? snapshotParams.targetId : saveToId; const extras = item.isSnap ? { save_as: 'snapshot' } : {}; let retry = 0; while (retry < 3) { try { await apiAddOfflineTask(item.url, pid, extras); successCount++; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; break; } catch (reqErr) { if (reqErr.message && reqErr.message.includes('429')) { retry++; await sleep(2000 * retry); } else { throw reqErr; } } } } catch(e) { console.error(`Task Create Failed[${item.url}]:`, e); failCount++; } await sleep(300); } progressTask.destroy(); if (failCount > 0) { showToast(L.msg_cloud_task_finish.replace('{s}', successCount).replace('{f}', failCount), 'warning'); } else { showToast(L.msg_cloud_task_success.replace('{n}', successCount)); } setTimeout(() => updateQuotaUI(), 1000); const curPathId = S.path[S.path.length - 1].id || ''; if (S.offlineMode || (saveToId && curPathId === saveToId)) { load(false, true); } else if (!saveToId && S.path.length === 1) { if(window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); } }; }; } UI.btnNavHome.onclick = () => switchTab('home'); if(UI.btnNavStarred) UI.btnNavStarred.onclick = () => switchTab('starred'); if(UI.btnNavRecent) UI.btnNavRecent.onclick = () => switchTab('recent'); if(UI.btnNavHistory) UI.btnNavHistory.onclick = () => switchTab('history'); if(UI.btnNavUpload) UI.btnNavUpload.onclick = () => switchTab('upload'); if(UI.btnNavShare) UI.btnNavShare.onclick = () => switchTab('share'); if (UI.btnNavOffline) { UI.btnNavOffline.onclick = () => { switchTab('offline'); }; } UI.btnNavTrash.onclick = () => switchTab('trash'); if (UI.btnTrashRefresh) { UI.btnTrashRefresh.onclick = () => { updateQuotaUI(); load(false, true); }; } UI.btnRestore.onclick = async () => { if (S.sel.size === 0) return; ensureItemMap(); const progressTask = FloatBarManager.create(L.msg_prepare_restore); const updateFloat = progressTask.update; isGUISensitive = true; const ids = Array.from(S.sel); S.sel.clear(); S.lastSelIdx = -1; S.activeId = null; updateStat(); try { const BATCH_SIZE = 500; const total = ids.length; const taskIds =[]; const affectedParentIds = new Set(); const restoredFolders = []; ids.forEach(id => { const item = S.itemMap.get(id); if (item) { if (item.kind === 'drive#folder') restoredFolders.push(item); if (item.parent_id) affectedParentIds.add(item.parent_id); else affectedParentIds.add('root'); } }); updateFloat(L.msg_submit_request.replace('{c}', 0).replace('{t}', total)); for (let i = 0; i < total; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files:batchUntrash`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }) }); if (!res.ok) throw new Error(`Batch Untrash Error ${res.status}`); const data = await res.json(); if (data.task_id) { taskIds.push(data.task_id); } updateFloat(L.msg_submit_request.replace('{c}', Math.min(i + BATCH_SIZE, total)).replace('{t}', total)); await sleep(50); } if (taskIds.length > 0) { updateFloat(L.msg_wait_server.replace('{c}', 0).replace('{t}', taskIds.length)); const pendingTasks = new Set(taskIds); let pollRetries = 0; const maxPollRetries = 120; while (pendingTasks.size > 0 && pollRetries < maxPollRetries) { await sleep(1000); pollRetries++; const currentIdsToCheck = Array.from(pendingTasks).join(','); const filters = { phase: { eq: "PHASE_TYPE_COMPLETE" }, id: { in: currentIdsToCheck } }; const filterStr = encodeURIComponent(JSON.stringify(filters)); const pollUrl = `https://api-drive.mypikpak.com/drive/v1/tasks?with=reference_resource&type=&thumbnail_size=SIZE_SMALL&limit=100&filters=${filterStr}`; try { const tRes = await fetch(pollUrl, { headers: getHeaders() }); if (tRes.ok) { const tData = await tRes.json(); const completedTasks = tData.tasks || []; completedTasks.forEach(t => { if (pendingTasks.has(t.id)) { pendingTasks.delete(t.id); } }); const doneCount = taskIds.length - pendingTasks.size; updateFloat(L.msg_server_processing.replace('{c}', doneCount).replace('{t}', taskIds.length)); } } catch (err) { console.warn("[Restore] Poll failed, retrying...", err); } } if (pendingTasks.size > 0) { console.warn(`[Restore] Timeout waiting for tasks: ${Array.from(pendingTasks)}`); } } const allIdSet = new Set(ids); ids.forEach(id => S.itemMap.delete(id)); S.items = S.items.filter(x => !allIdSet.has(x.id)); ids.forEach(id => { const reviveFromTombstone = (fid) => { if (globalTombstoneCache.has(fid)) { const savedData = globalTombstoneCache.get(fid); globalCache.set(fid, savedData); globalTombstoneCache.delete(fid); const list = Array.isArray(savedData) ? savedData : (savedData.items ||[]); list.forEach(child => { if (child.kind === 'drive#folder') { reviveFromTombstone(child.id); } }); } }; const wipeCrawlerMemory = (fid) => { scannedFolderIds.delete(fid); const children = []; for (const [childId, parentInfo] of globalParentIndex.entries()) { if (parentInfo.id === fid) { children.push(childId); } } children.forEach(childId => wipeCrawlerMemory(childId)); }; reviveFromTombstone(id); wipeCrawlerMemory(id); }); if (typeof globalCache !== 'undefined') { const cleanListChunk = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !allIdSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !allIdSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { if (key && (key.endsWith('_root') || key === 'root_trashed')) { globalCache.set(key, cleanListChunk(globalCache.get(key))); } } for (const key of S.cache.keys()) { if (key && (key.endsWith('_root') || key === 'root_trashed')) { S.cache.set(key, cleanListChunk(S.cache.get(key))); } } } refresh(); affectedParentIds.forEach(pid => { if (pid && pid !== 'root') gmSet('pk_fmod_' + pid, new Date(getServerNow()).toISOString()); }); affectedParentIds.forEach(pid => { const keys = (pid === 'root' || pid === '') ? ['root', ''] : [pid]; keys.forEach(k => { if (typeof globalCache !== 'undefined') globalCache.delete(k); if (S.cache) S.cache.delete(k); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(k); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(k); }); const queueId = (pid === 'root') ? '' : pid; backgroundQueue.unshift({ id: queueId, name: "Refill_Restore", retryCount: 0 }); }); restoredFolders.forEach(folder => { scannedFolderIds.delete(folder.id); backgroundQueue.unshift({ id: folder.id, name: folder.name, retryCount: 0 }); }); runBackgroundCrawler(); if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; showToast(L.msg_restore_done.replace('{n}', total)); } catch(e) { showAlert(`${L.str_error}: ${e.message}`); load(false, true); } finally { if (progressTask) progressTask.destroy(); isGUISensitive = false; } }; UI.btnDelForever.onclick = async () => { if (S.sel.size === 0) return; ensureItemMap(); if (!await showConfirm(L.msg_del_forever_confirm.replace('{n}', S.sel.size))) return; const ids = Array.from(S.sel); await executeBatchDelete(ids, { hardDelete: true, silent: false }); }; UI.btnEmptyTrash.onclick = async () => { if (!await showConfirm(L.msg_empty_trash_confirm)) return; if (S.items.length === 0) return; S._isEmptyingTrash = true; setLoad(true); updateLoadTxt(L.str_deleting); try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files/trash:empty', { method: 'PATCH', headers: getHeaders() }); if (!res.ok) throw new Error(`API Error ${res.status}`); S.items = []; S.display = []; S.itemMap.clear(); S.clearSelection(); if (typeof globalCache !== 'undefined') globalCache.delete('root_trashed'); if (S.cache) S.cache.delete('root_trashed'); refresh(); updateStat(); showToast(L.msg_trash_emptied); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); S._isEmptyingTrash = false; } }; const openSettingsModal = () => { const inputStyle = `width:100%; height:44px; padding:0 15px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; font-weight:600; outline:none; transition:border-color 0.2s; box-sizing:border-box;`; const areaStyle = `width:100%; min-height:60px; max-height:120px; padding:12px 15px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:13px; font-weight:600; outline:none; transition:border-color 0.2s; box-sizing:border-box; resize:vertical; line-height:1.5; font-family:inherit; cursor:auto;`; const labelStyle = `position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; line-height:1; font-size:11px; color:var(--pk-pri); font-weight:bold; pointer-events:none; z-index:1;`; const curLang = gmGet('pk_lang', lang); const curEngine = gmGet('pk_search_engine', 'google'); const curAriaUrl = gmGet('pk_aria2_url', ''); const curAriaToken = gmGet('pk_aria2_token', ''); const curBlur = gmGet('pk_blur_thumb', false); let selectedLang = curLang; let selectedEngine = curEngine; let totalStorageBytes = 0; const keys = typeof GM_listValues !== 'undefined' ? GM_listValues() : Object.keys(localStorage); keys.forEach(k => { if (k.startsWith('pk_')) { const val = typeof GM_getValue !== 'undefined' ? GM_getValue(k) : localStorage.getItem(k); totalStorageBytes += (k.length + JSON.stringify(val || '').length); } }); if (typeof globalCache !== 'undefined') { for (const [k, v] of globalCache.entries()) { try { totalStorageBytes += k.toString().length + JSON.stringify(v).length; } catch(e){} } } const storageDisplay = fmtSize(totalStorageBytes); const m = showModal(`

${L.modal_settings_title}

${L.label_lang}
${CONF.crumbIcons.down}
简体中文
繁體中文
English
한국어
日本語
${L.label_turbo_mode}
${L.label_privacy_mode}
${L.title_blacklist}
${L.lbl_browse_exp}
${L.label_sort_pref}
${L.label_comic_mode}
${L.label_search_engine}
${CONF.crumbIcons.down}
${L.opt_engine_google}
${L.opt_engine_yandex}
${L.opt_engine_saucenao}
${L.opt_engine_tracemoe}
${L.lbl_dl_filter}
${L.label_dl_filter_ext}
${L.label_dl_filter_name}
${L.desc_dl_filter}
${L.label_aria2_url}
Default
${L.lbl_aria2_status}
${L.label_aria2_token}
${L.lbl_pwd_manage}
${CONF.icons.vault.replace('width="16"','width="20"').replace('height="16"','width="20"')} ${L.title_pwd_vault}
${L.lbl_config_manage}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: 'auto', padding: '0', overflow: 'hidden', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const bindSelect = (id, currentVal, onSelect) => { const container = m.querySelector(`#${id}`); const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('span'); const items = container.querySelectorAll('.pk-select-item'); items.forEach(item => { if (item.dataset.val === currentVal) { item.classList.add('act'); txt.textContent = item.textContent; } item.onclick = (e) => { e.stopPropagation(); items.forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val); }; }); trigger.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('.pk-select-menu').forEach(om => { if(om !== menu) om.style.display = 'none'; }); menu.style.display = menu.style.display === 'block' ? 'none' : 'block'; }; }; bindSelect('cs_set_lang', curLang, (val) => { selectedLang = val; }); bindSelect('cs_set_engine', curEngine, (val) => { selectedEngine = val; }); const ariaInp = m.querySelector('#set_aria_url'); const ariaTok = m.querySelector('#set_aria_token'); const ariaDot = m.querySelector('#aria_test_dot'); const ariaTxt = m.querySelector('#aria_test_txt'); const ariaBox = m.querySelector('#aria_test_res'); let ariaTimer = null; const runAriaTest = async () => { const url = ariaInp.value.trim(); const token = ariaTok.value.trim(); const showTip = () => showAlert(L.tip_mixed_content, L.lbl_aria2_status); if (!url) { ariaDot.className = 'pk-aria-dot'; ariaTxt.textContent = L.lbl_aria2_status; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; return; } ariaDot.className = 'pk-aria-dot wait'; ariaTxt.textContent = L.str_connecting; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_live_test', params: [`token:${token}`] }; let testUrl = url.replace(/^ws/i, 'http'); if (!testUrl.includes('/jsonrpc') && !testUrl.includes('?')) { testUrl = testUrl.endsWith('/') ? testUrl + 'jsonrpc' : testUrl + '/jsonrpc'; } try { await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: testUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 3000, onload: (r) => { if (r.status === 200) resolve(); else reject(new Error(r.status)); }, onerror: (e) => reject(e) }); }); ariaDot.className = 'pk-aria-dot ok'; ariaTxt.textContent = L.str_connected; ariaBox.onclick = null; ariaBox.style.cursor = 'default'; } catch (e) { ariaDot.className = 'pk-aria-dot err'; ariaTxt.textContent = L.str_conn_fail; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; } }; const debouncedTest = () => { clearTimeout(ariaTimer); ariaTimer = setTimeout(runAriaTest, 600); }; ariaInp.oninput = (e) => { if (e.target.oninput) e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; debouncedTest(); }; ariaTok.oninput = (e) => { if (e.target.oninput) e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; debouncedTest(); }; m.querySelector('#btn_aria_default').onclick = () => { ariaInp.value = 'http://localhost:6800/jsonrpc'; ariaInp.style.borderColor = 'var(--pk-pri)'; debouncedTest(); }; setTimeout(runAriaTest, 200); const clickAway = () => m.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none'); setTimeout(() => document.addEventListener('click', clickAway), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', clickAway); _orgRemove(); }; const extInp = m.querySelector('#set_dl_filter_ext'); const nameInp = m.querySelector('#set_dl_filter_name'); const groupEl = m.querySelector('#pk_dl_group'); const updateDlBorders = () => { const hasE = extInp.value.trim() !== ''; const hasN = nameInp.value.trim() !== ''; extInp.classList.toggle('pk-active-border', hasE); nameInp.classList.toggle('pk-active-border', hasN); groupEl.classList.toggle('pk-typing-active', hasE || hasN); }; extInp.oninput = nameInp.oninput = updateDlBorders; updateDlBorders(); m.querySelector('#btn_open_vault').onclick = (e) => { e.stopPropagation(); const subM = document.createElement('div'); subM.className = 'pk-modal-ov'; subM.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) subM.classList.add('pk-dark'); const savedCount = gmGet('pk_pwd_try_count', 10); const savedPwds = (() => { try { return JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x.p : x).join('\n'); } catch { return ''; } })(); subM.innerHTML = `
${CONF.icons.close}

${CONF.icons.vault.replace('width="16"','width="22"').replace('height="16"','width="22"')} ${L.title_pwd_vault}

${L.lbl_pwd_try_count}
${L.tip_pwd_manual}
${L.btn_cancel}
`; document.body.appendChild(subM); const cntInp = subM.querySelector('#vault_cnt_val'); subM.querySelector('#vault_cnt_dec').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(cntInp.value)) || 10; cntInp.value = Math.max(10, v - 1); }; subM.querySelector('#vault_cnt_inc').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(cntInp.value)) || 0; cntInp.value = Math.min(50, v + 1); }; cntInp.oninput = () => { let raw = cntInp.value; if (raw === "") return; let v = parseInt(raw); if (isNaN(v)) { cntInp.value = 10; return; } if (v > 50) cntInp.value = 50; }; cntInp.onblur = () => { let v = parseInt(cntInp.value); if (isNaN(v) || v < 10) cntInp.value = 10; }; const area = subM.querySelector('#vault_pwd_area'); area.onfocus = () => area.style.borderColor = 'var(--pk-pri)'; area.onblur = () => area.style.borderColor = 'var(--pk-bd)'; area.value = savedPwds; area.oninput = () => { let lines = area.value.split('\n'); let changed = false; let msg = ""; if (lines.length > 50) { lines = lines.slice(0, 50); changed = true; msg = L.err_vault_max; } for (let i = 0; i < lines.length; i++) { if (lines[i].length > 127) { lines[i] = lines[i].substring(0, 127); changed = true; msg = L.err_pwd_len; } } if (changed) { const cursor = area.selectionStart; area.value = lines.join('\n'); area.setSelectionRange(cursor, cursor); showToast(msg, 'error'); area.style.borderColor = '#d93025'; setTimeout(() => { if(area) area.style.borderColor = 'var(--pk-pri)'; }, 1000); } }; const doSave = () => { let cnt = Math.floor(Number(cntInp.value)); if (isNaN(cnt) || cnt < 10) cnt = 10; if (cnt > 50) cnt = 50; const inputPwds = area.value.split('\n').map(s => s.trim()).filter(s => s); if (inputPwds.length > 50) { showToast(L.err_vault_max, 'error'); area.style.borderColor = '#d93025'; return; } try { const oldList = JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x : {p: x, h: 0}); const hitMap = new Map(oldList.map(x => [x.p, x.h])); const newList = [...new Set(inputPwds)].map(p => ({ p: p, h: hitMap.get(p) || 0 })); newList.sort((a, b) => b.h - a.h); gmSet('pk_pwd_try_count', cnt); gmSet('pk_pwd_vault', JSON.stringify(newList)); } catch(e) { gmSet('pk_pwd_vault', JSON.stringify([...new Set(inputPwds)])); } subM.remove(); showToast(L.msg_settings_saved); }; subM.querySelector('#vault_save').onclick = doSave; subM.querySelector('#vault_cancel').onclick = () => subM.remove(); subM.querySelector('.pk-modal-close').onclick = () => subM.remove(); }; m.querySelector('#btn_cfg_clean').onclick = async () => { const keys = typeof GM_listValues !== 'undefined' ? GM_listValues() : Object.keys(localStorage); const sizes = { index: 0, pref: 0, rules: 0, vault: 0, history: 0, cache: 0 }; if (typeof globalCache !== 'undefined') { for (const [k, v] of globalCache.entries()) { try { sizes.index += k.toString().length + JSON.stringify(v).length; } catch(e){} } } const getCat = (k) => { if (!k.startsWith('pk_')) return null; if (k.startsWith('pk_archive_pwd_') || k === 'pk_pwd_vault') return 'vault'; if (k.startsWith('pk_progress_') || k.startsWith('pk_duration_')) return 'history'; if (k.startsWith('pk_fmod_') || k === 'pk_captured_captcha') return 'cache'; const ruleKeys =['pk_blacklist', 'pk_blacklist_folders', 'pk_aria2_url', 'pk_aria2_token', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist']; if (ruleKeys.includes(k) || k.startsWith('pk_scan_last_') || k.startsWith('pk_analyze_last_') || k === 'pk_dup_strictness') return 'rules'; return 'pref'; }; keys.forEach(k => { const cat = getCat(k); if (cat) { const val = typeof GM_getValue !== 'undefined' ? GM_getValue(k) : localStorage.getItem(k); sizes[cat] += (k.length + (val ? JSON.stringify(val).length : 0)); } }); const renderLbl = (cat, txt, isChecked = false, isMandatory = false) => { const sz = sizes[cat]; if (sz === 0 && cat !== 'index') return ''; const szStr = fmtSize(sz); const checkAttr = (isChecked || isMandatory) ? 'checked' : ''; const disAttr = isMandatory ? 'disabled' : ''; const cursor = isMandatory ? 'not-allowed' : 'pointer'; const opacity = isMandatory ? '0.7' : '1'; return ``; }; const htmlOptions =[ renderLbl('index', L.opt_cfg_index, true, true), renderLbl('pref', L.opt_cfg_pref), renderLbl('rules', L.opt_cfg_rules), renderLbl('vault', L.opt_cfg_vault), renderLbl('history', L.opt_cfg_history), renderLbl('cache', L.opt_cfg_cache) ].filter(Boolean).join(''); if (!htmlOptions) return; const cleanM = showModal(`

${L.title_clean_data}

${htmlOptions}
`); cleanM.querySelector('#clean_cancel').onclick = () => cleanM.remove(); cleanM.querySelector('#clean_confirm').onclick = async () => { const selected = Array.from(cleanM.querySelectorAll('.clean-opt:checked')).map(el => el.value); if (selected.length === 0) { cleanM.remove(); return; } if (!await showConfirm(L.msg_clean_confirm)) return; if (selected.includes('index')) { if (typeof globalCache !== 'undefined') globalCache.clear(); if (typeof S !== 'undefined' && S.cache) S.cache.clear(); if (typeof globalLineageMap !== 'undefined') globalLineageMap.clear(); if (typeof globalParentIndex !== 'undefined') globalParentIndex.clear(); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.clear(); } keys.forEach(k => { const cat = getCat(k); if (cat && selected.includes(cat)) { try { if (typeof GM_deleteValue !== 'undefined') { GM_deleteValue(k); } else if (typeof GM_setValue !== 'undefined') { GM_setValue(k, ''); } localStorage.removeItem(k); } catch (e) { console.warn("Delete config error:", e); } } }); showToast(L.msg_clean_success); setTimeout(() => location.reload(), 1500); }; }; m.querySelector('#btn_cfg_export').onclick = () => { const keys = typeof GM_listValues !== 'undefined' ? GM_listValues() : Object.keys(localStorage); const config = { "_pk_metadata": { "signature": "PIKPAK_ENHANCEMENT_MASTER", "version": version, "export_at": new Date().toISOString(), "author": "digbug82" } }; const pkKeys = keys.filter(k => k.startsWith('pk_') && k !== 'pk_captured_captcha'); const getCatWeight = (k) => { if (k.startsWith('pk_archive_pwd_') || k === 'pk_pwd_vault' || k === 'pk_share_limits') return 3; if (k.startsWith('pk_progress_') || k.startsWith('pk_duration_')) return 4; if (k.startsWith('pk_fmod_')) return 5; const ruleKeys =['pk_blacklist', 'pk_blacklist_folders', 'pk_aria2_url', 'pk_aria2_token', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_bn_find_hist', 'pk_bn_rep_hist']; if (ruleKeys.includes(k) || k.startsWith('pk_scan_last_') || k.startsWith('pk_analyze_last_') || k === 'pk_dup_strictness') return 2; return 1; }; pkKeys.sort((a, b) => { const wA = getCatWeight(a); const wB = getCatWeight(b); if (wA !== wB) return wA - wB; return a.localeCompare(b); }); pkKeys.forEach(k => { config[k] = typeof GM_getValue !== 'undefined' ? GM_getValue(k) : localStorage.getItem(k); }); const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const domEl = document.querySelector('.name.ellipsis'); let rawUserName = domEl ? (domEl.title || domEl.innerText) : 'Default'; const userName = rawUserName.trim().replace(/[\\/:*?"<>|]/g, '_'); const now = new Date(); const dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); const timeStr = now.getHours().toString().padStart(2,'0') + now.getMinutes().toString().padStart(2,'0'); const fileName = `PKM_Backup_${userName}_${dateStr}_${timeStr}.json`; const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); }; const fileInput = m.querySelector('#cfg_import_input'); m.querySelector('#btn_cfg_import').onclick = () => fileInput.click(); fileInput.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; if (!await showConfirm(L.msg_import_confirm)) { fileInput.value = ''; return; } const reader = new FileReader(); reader.onload = (ev) => { try { const config = JSON.parse(ev.target.result); if (!config._pk_metadata || config._pk_metadata.signature !== "PIKPAK_ENHANCEMENT_MASTER") { throw new Error("INVALID_SIGNATURE"); } Object.keys(config).forEach(k => { if (!k.startsWith('pk_')) return; let importedVal = config[k]; let localVal = typeof GM_getValue !== 'undefined' ? GM_getValue(k, null) : localStorage.getItem(k); if (localVal === null || localVal === undefined || localVal === '') { GM_setValue(k, importedVal); return; } try { if (k === 'pk_blacklist' || k === 'pk_blacklist_folders') { const localSet = new Set(localVal.split('\n').map(s => s.trim()).filter(s => s)); const importedSet = new Set(importedVal.split('\n').map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); GM_setValue(k, Array.from(localSet).join('\n')); } else if (k === 'pk_dl_filter_ext' || k === 'pk_dl_filter_name') { const localSet = new Set(localVal.split(/[,,\n]/).map(s => s.trim()).filter(s => s)); const importedSet = new Set(importedVal.split(/[,,\n]/).map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); GM_setValue(k, Array.from(localSet).join(', ')); } else if (typeof importedVal === 'string' && (importedVal.startsWith('[') || importedVal.startsWith('{'))) { const localObj = JSON.parse(localVal); const importedObj = JSON.parse(importedVal); if (Array.isArray(localObj) && Array.isArray(importedObj)) { if (k === 'pk_pwd_vault') { const map = new Map(); localObj.forEach(x => { if (typeof x === 'object') map.set(x.p, x); else map.set(x, {p: x, h: 0}); }); importedObj.forEach(x => { let p = typeof x === 'object' ? x.p : x; let h = typeof x === 'object' ? x.h : 0; if (map.has(p)) map.get(p).h += h; else map.set(p, {p, h}); }); let merged = Array.from(map.values()).sort((a,b) => b.h - a.h).slice(0, 50); GM_setValue(k, JSON.stringify(merged)); } else if (k === 'pk_expired_shares') { const map = new Map(); localObj.forEach(x => map.set(x.id, x)); importedObj.forEach(x => map.set(x.id, x)); GM_setValue(k, JSON.stringify(Array.from(map.values()))); } else { const mergedSet = new Set([...importedObj, ...localObj]); GM_setValue(k, JSON.stringify(Array.from(mergedSet).slice(0, 100))); } } else if (typeof localObj === 'object' && typeof importedObj === 'object') { const mergedObj = Object.assign({}, localObj, importedObj); GM_setValue(k, JSON.stringify(mergedObj)); } else { GM_setValue(k, importedVal); } } else { GM_setValue(k, importedVal); } } catch (e) { GM_setValue(k, importedVal); } }); showToast(L.msg_import_success); setTimeout(() => location.reload(), 1500); } catch (err) { let errorTip = ""; if (err.message === "INVALID_SIGNATURE") { errorTip = L.err_invalid_config; } else { errorTip = L.err_json_format; console.error("[Config Import]", err); } showAlert(errorTip, L.str_error); fileInput.value = ''; } }; reader.readAsText(file); }; m.querySelector('#set_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#set_save').click(); } }); m.querySelector('#set_save').onclick = async () => { const newTurbo = m.querySelector('#set_turbo').checked; const oldTurbo = gmGet('pk_turbo_mode', false); const newUrl = m.querySelector('#set_aria_url').value.trim(); const newToken = m.querySelector('#set_aria_token').value.trim(); const newBlur = m.querySelector('#set_thumb').checked; const newKeepPos = m.querySelector('#set_keep_pos').checked; const newSkipBl = m.querySelector('#set_skip_bl').checked; const newComicMode = m.querySelector('#set_comic_mode').checked; const sortPref = m.querySelector('input[name="set_sort_pref"]:checked').value; const saveBtn = m.querySelector('#set_save'); gmSet('pk_blur_thumb', newBlur); gmSet('pk_keep_pos', newKeepPos); gmSet('pk_comic_mode', newComicMode); gmSet('pk_skip_bl_on_del', newSkipBl); const wasIndep = gmGet('pk_sort_independent', false); const isIndep = (sortPref === 'indep'); gmSet('pk_sort_independent', isIndep); if (wasIndep && !isIndep) { gmSet('pk_folder_sort_prefs', '{}'); gmSet('pk_global_sort_pref', JSON.stringify({ sort: 'modified_time', dir: 1 })); S.sort = 'modified_time'; S.dir = 1; } gmSet('pk_lang', selectedLang); gmSet('pk_search_engine', selectedEngine); gmSet('pk_turbo_mode', newTurbo); gmSet('pk_dl_filter_ext', m.querySelector('#set_dl_filter_ext').value.trim()); gmSet('pk_dl_filter_name', m.querySelector('#set_dl_filter_name').value.trim()); const applyChangesAndClose = () => { m.remove(); const savedMsg = (T[selectedLang] || T['en']).msg_settings_saved; showToast(savedMsg); if (newTurbo !== oldTurbo) { setTimeout(() => location.reload(), 300); return; } if (curLang !== selectedLang) { let safePath = [...S.path]; if (safePath.some(n => n.id === 'virtual_search_root' || n.id === 'analyze_root')) { safePath = S.preSearchPath || [{ id: '', name: L.btn_nav_home }]; } globalSavedState = { path: safePath, trashMode: S.trashMode, shareMode: S.shareMode, starredMode: S.starredMode, recentMode: S.recentMode, historyMode: S.historyMode, offlineMode: S.offlineMode, uploadMode: S.uploadMode, isMaximized: UI.win.classList.contains('pk-maximized'), scrollTop: UI.vp ? UI.vp.scrollTop : 0, uploadTasks: S.uploadTasks }; document.removeEventListener('keydown', keyHandler); document.removeEventListener('mouseup', mouseHandler); if (typeof destroyTooltip === 'function') destroyTooltip(); if (visibilityListener && visibilityListener.abort) visibilityListener.abort(); el.remove(); openManager(S.cache, S.preLoadPromise); } else { renderVisible(); } }; if (!newUrl && !newToken) { gmSet('pk_aria2_url', ''); gmSet('pk_aria2_token', ''); applyChangesAndClose(); return; } saveBtn.disabled = true; saveBtn.textContent = L.str_saving_dots; try { const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_test', params: [`token:${newToken}`] }; let fetchUrl = (newUrl || "http://localhost:6800/jsonrpc").replace('ws', 'http'); await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: fetchUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 5000, onload: (r) => { if(r.status === 200) resolveReq(); else rejectReq(new Error('HTTP ' + r.status)); }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); gmSet('pk_aria2_url', newUrl); gmSet('pk_aria2_token', newToken); applyChangesAndClose(); } catch (e) { if (await showConfirm(L.msg_aria2_test_fail, L.title_aria2_fail)) { gmSet('pk_aria2_url', newUrl); gmSet('pk_aria2_token', newToken); applyChangesAndClose(); } else { saveBtn.disabled = false; saveBtn.textContent = L.btn_save; } } }; }; UI.btnSettings.onclick = (e) => { if (e) e.stopPropagation(); const existing = document.getElementById('pk-settings-pop'); if (existing) { existing.remove(); return; } const isMax = UI.win.classList.contains('pk-maximized'); const pop = document.createElement('div'); pop.id = 'pk-settings-pop'; if (isMax) pop.className = 'pk-pop-max'; pop.style.cssText = ` position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; padding: 4px 0; box-shadow: 0 4px 15px rgba(0,0,0,0.15); z-index: 2147483647; min-width: 140px; display: flex; flex-direction: column; zoom: var(--pk-zoom, 1); `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.innerHTML = `
${CONF.icons.settings} ${L.btn_settings}
${CONF.icons.logout} ${L.btn_logout}
`; document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = UI.btnSettings.getBoundingClientRect(); const winEl = document.querySelector('.pk-win'); const isMax = winEl && winEl.classList.contains('pk-maximized'); let popLeft, popBottom; if (isMax) { popLeft = (rect.left / scale); popBottom = (window.innerHeight - rect.top + 8) / scale; } else { popLeft = (rect.right / scale) + 10; popBottom = (window.innerHeight - rect.bottom) / scale; } pop.style.bottom = popBottom + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; const closer = (ev) => { if (!pop.contains(ev.target) && !UI.btnSettings.contains(ev.target)) cleanup(); }; setTimeout(() => document.addEventListener('mousedown', closer), 10); pop.querySelector('#pk-set-menu-settings').onclick = (ev) => { ev.stopPropagation(); cleanup(); openSettingsModal(); }; pop.querySelector('#pk-set-menu-logout').onclick = async (ev) => { ev.stopPropagation(); cleanup(); if (await showConfirm(L.msg_logout_confirm)) { const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && (k.startsWith('credentials') || k.startsWith('captcha') || k === 'pk_captured_captcha')) { keysToRemove.push(k); } } keysToRemove.forEach(k => localStorage.removeItem(k)); if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); window.location.href = 'https://mypikpak.com/drive/login'; } }; }; const ctx = el.querySelector('#pk-ctx'); if (!window.pkPropCache) window.pkPropCache = new Map(); ctx.querySelector('#ctx-property').onclick = async () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; if (!id) return; let item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.loading_detail); const isFolder = item.kind === 'drive#folder'; try { const freshData = await apiGet(id); item = { ...item, ...freshData }; } catch(e) { console.warn("Fetch detail failed, using cached info"); } if (isFolder) { const localFMod = gmGet('pk_fmod_' + item.id); if (localFMod) item.modified_time = localFMod; } setLoad(false); let cachedProp = window.pkPropCache.get(item.id); if (!cachedProp) { cachedProp = { size: BigInt(0), fileCount: 0, folderCount: 0, isDone: false, scanned: new Set(), isRunning: false }; window.pkPropCache.set(item.id, cachedProp); } let realSize = isFolder ? cachedProp.size : BigInt(item.size || 0); let fileCount = isFolder ? cachedProp.fileCount : 0; let folderCount = isFolder ? cachedProp.folderCount : 0; if (isFolder && item.usage && item.usage.size && item.usage.size !== "0") { realSize = BigInt(item.usage.size); fileCount = item.usage.file_count; folderCount = item.usage.folder_count; cachedProp.size = realSize; cachedProp.fileCount = fileCount; cachedProp.folderCount = folderCount; cachedProp.isDone = true; } const countStr = isFolder ? L.fmt_prop_count.replace('{f}', fileCount).replace('{d}', folderCount) : "-"; const formatTime = (iso) => fmtDate ? fmtDate(iso) : iso; let sourceStr = L.str_prop_unknown; let magnetLink = item.params?.url || item.audit?.source_url || ""; if (magnetLink) { sourceStr = L.str_prop_cloud; } else { sourceStr = L.str_prop_user; } const btnStyle = "margin-left:10px; padding:2px 8px; font-size:12px; background:var(--pk-pri); color:#fff; border:none; border-radius:4px; cursor:pointer; height:24px; white-space:nowrap;"; const rowStyle = "display:flex; align-items:center; margin-bottom:12px; font-size:13px; line-height:1.5;"; const labelStyle = "width:80px; color:#888; flex-shrink:0;"; const valStyle = "color:var(--pk-fg); flex:1; word-break:break-all;"; const mkRow = (lbl, val, copyVal = null, isHtml = false, valId = null) => { if (!val && val !== 0 && val !== "-") return ""; let btnHtml = copyVal ? `` : ""; return `
${lbl}:
${isHtml ? val : esc(val)}
${btnHtml}
`; }; let pathRowHtml = ""; const isSpecialView = S.shareMode || S.offlineMode || S.recentMode || S.historyMode || S.trashMode || S.starredMode; const curPathNode = S.path[S.path.length - 1]; const shouldHidePath = isSpecialView || (S.analyzeMode && curPathNode && curPathNode.id !== 'analyze_root'); if (!shouldHidePath) { const homeText = L.btn_nav_home; let parts = (item._lineage && Array.isArray(item._lineage)) ? item._lineage.filter(p => p.id !== item.id).map(p => p.name) : S.path.map(p => p.name); parts = parts.filter(n => n && n !== 'Root' && n !== L.str_root_dir_cn); if (parts[0] !== homeText) parts.unshift(homeText); const pathStr = parts.join('/'); let pathDisplayHtml = esc(pathStr); if (pathStr.startsWith(homeText)) { const homeSvg = CONF.icons.home.replace('width="24"','width="15"').replace('height="24"','height="15"').replace('viewBox="0 0 24 24"','viewBox="0 0 24 24" style="margin-right:4px;flex-shrink:0;vertical-align:-2.5px;"'); const restPath = pathStr.substring(homeText.length); const homeGroup = `${homeSvg}${esc(homeText)}`; pathDisplayHtml = `
${homeGroup}${esc(restPath)}
`; } pathRowHtml = mkRow(L.lbl_prop_path, pathDisplayHtml, pathStr, true); } const html = `
${mkRow(L.lbl_prop_name, item.name)} ${mkRow(L.lbl_prop_size, fmtSize(realSize.toString()) || "0 B", null, false, 'pk_prop_size_val')} ${isFolder ? mkRow(L.lbl_prop_count, countStr, null, false, 'pk_prop_count_val') : ''} ${mkRow(L.lbl_prop_ctime, formatTime(item.created_time))} ${mkRow(L.lbl_prop_mtime, formatTime(item.modified_time))} ${mkRow(L.lbl_prop_source, sourceStr)} ${magnetLink ? mkRow(L.lbl_prop_link, magnetLink, magnetLink) : ''} ${pathRowHtml}
`; const m = showModal(`

${L.title_property}

${html} `); m.querySelectorAll('.pk-btn-copy-prop').forEach(btn => { btn.onclick = (e) => { const txt = e.target.getAttribute('data-val'); GM_setClipboard(txt); const oldTxt = e.target.textContent; e.target.textContent = "OK"; e.target.style.background = "#4CAF50"; setTimeout(() => { e.target.textContent = oldTxt; e.target.style.background = "var(--pk-pri)"; }, 1500); }; }); if (isFolder && !cachedProp.isDone) { const sizeEl = m.querySelector('#pk_prop_size_val'); const countEl = m.querySelector('#pk_prop_count_val'); let lastUiUpdateTime = performance.now(); const updateUI = () => { const now = performance.now(); if (now - lastUiUpdateTime > 80) { if (sizeEl && sizeEl.isConnected) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl && countEl.isConnected) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); lastUiUpdateTime = now; } }; if (!cachedProp.isRunning) { cachedProp.isRunning = true; const localAbortCtrl = new AbortController(); const rootNodes =[{ id: item.id, name: item.name, lineage: [], retryCount: 0 }]; coreRecursiveEngine(rootNodes, { signal: localAbortCtrl.signal, onFolder: (f) => { if (f.id !== item.id && !cachedProp.scanned.has(f.id)) { cachedProp.folderCount++; cachedProp.scanned.add(f.id); } updateUI(); }, onFile: (f) => { if (!cachedProp.scanned.has(f.id)) { cachedProp.fileCount++; cachedProp.size += BigInt(f.size || 0); cachedProp.scanned.add(f.id); } updateUI(); }, onProgress: () => {} }).then(() => { cachedProp.isDone = true; cachedProp.isRunning = false; if (sizeEl && sizeEl.isConnected) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl && countEl.isConnected) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); }).catch(e => { cachedProp.isRunning = false; }); } else { const timer = setInterval(() => { if (!document.contains(m)) { clearInterval(timer); return; } if (sizeEl) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); if (cachedProp.isDone) clearInterval(timer); }, 200); } } }; const btnLocate = ctx.querySelector('#ctx-locate'); if (btnLocate) { btnLocate.onclick = async () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; if (!id) return; let item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.str_loc_tracing); try { if (item.kind === 'drive#task' || S.recentMode || S.uploadMode) { const lookupId = (item.kind === 'drive#task' || S.uploadMode) ? item.file_id : item.id; if (!lookupId) { showToast(L.err_folder_not_ready, 'error'); setLoad(false); return; } try { item = await apiGet(lookupId); } catch (e) { const errText = e.message || ""; if (errText.includes('404') || errText.includes('400')) { showToast(L.err_item_deleted, 'error'); } else { showToast(`${L.str_error}: ${e.message}`, 'error'); } setLoad(false); return; } } let pathChain = []; if (item._lineage && Array.isArray(item._lineage)) { pathChain = item._lineage.filter(p => p.id !== item.id && p.id !== 'virtual_search_root' && p.id !== 'analyze_root' && p.id !== 'recent_root' ); } else if (item.parent_id && typeof globalLineageMap !== 'undefined' && globalLineageMap.has(item.parent_id)) { pathChain = [...globalLineageMap.get(item.parent_id)]; } else { let currParentId = item.parent_id; for (let i = 0; i < 15; i++) { if (!currParentId || currParentId === 'root' || currParentId === '') break; if (typeof globalLineageMap !== 'undefined' && globalLineageMap.has(currParentId)) { const cachedLineage = globalLineageMap.get(currParentId); pathChain.unshift(...cachedLineage); break; } try { const res = await apiGet(currParentId); pathChain.unshift({ id: res.id, name: res.name }); currParentId = res.parent_id; } catch (e) { console.warn("Trace broken:", e); break; } } } if (pathChain.length > 0 && (pathChain[0].id === '' || pathChain[0].id === 'root')) { pathChain[0].id = ''; pathChain[0].name = L.btn_nav_home; } else { pathChain.unshift({ id: '', name: L.btn_nav_home }); } const targetContextId = (item.parent_id === 'root' || !item.parent_id) ? '' : item.parent_id; const needsRestoreGlobalCheck = S.starredMode || S.recentMode || S.historyMode || S.offlineMode || S.uploadMode || S.shareMode || S.isFlattened || S.dupMode || S.analyzeMode; S.starredMode = false; S.trashMode = false; S.shareMode = false; S.offlineMode = false; S.recentMode = false; S.historyMode = false; S.uploadMode = false; S.dupMode = false; S.isFlattened = false; S.analyzeMode = false; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; if (UI.btnNavStarred) UI.btnNavStarred.classList.remove('act'); if (UI.btnNavRecent) UI.btnNavRecent.classList.remove('act'); if (UI.btnNavHistory) UI.btnNavHistory.classList.remove('act'); if (UI.btnNavShare) UI.btnNavShare.classList.remove('act'); if (UI.btnNavOffline) UI.btnNavOffline.classList.remove('act'); if (UI.btnNavUpload) UI.btnNavUpload.classList.remove('act'); if (UI.btnNavHome) UI.btnNavHome.classList.add('act'); [UI.btnIdm, UI.btnBc, UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; [UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; S.path = pathChain; if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal && needsRestoreGlobalCheck && typeof S.wasGlobalChecked !== 'undefined') { UI.chkGlobal.checked = S.wasGlobalChecked; } if (UI.scan) UI.scan.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'inline-flex'; if (UI.btnPaste) UI.btnPaste.style.display = S.clipItems && S.clipItems.length > 0 ? 'inline-flex' : 'inline-flex'; S.sort = 'modified_time'; S.dir = 1; await load(); let trackCount = 0; const maxTracks = 10; let trackerInterval = null; const stopTracking = () => { if (trackerInterval) { clearInterval(trackerInterval); trackerInterval = null; } }; let hasTriedRecovery = false; const performLocate = () => { const currentPathNode = S.path[S.path.length - 1]; const currentContextId = currentPathNode ? (currentPathNode.id || '') : ''; if (currentContextId !== targetContextId) { stopTracking(); return; } if (!S.loading && !S.itemMap.has(item.id) && !hasTriedRecovery) { hasTriedRecovery = true; console.warn(`[Locate] Target item ${item.id} not found in cache. Forcing network sync...`); const cacheKey = S.getRealCacheKey(currentContextId); S.cache.delete(cacheKey); if (typeof globalCache !== 'undefined') globalCache.delete(cacheKey); globalDirtyFolders.add(currentContextId || 'root'); updateLoadTxt(L.str_loc_stale); load(false, true); return; } if (S.loading || !S.itemMap.has(item.id)) return; S.sel.clear(); S.sel.add(item.id); S.activeId = item.id; const targetIdx = S.display.findIndex(x => x.id === item.id); if (targetIdx !== -1) { const vpHeight = UI.vp.clientHeight; const rowTop = targetIdx * CONF.rowHeight; const centerScroll = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); if (Math.abs(UI.vp.scrollTop - centerScroll) > (CONF.rowHeight / 2)) { UI.vp.scrollTop = centerScroll; renderVisible(); } const row = UI.in.querySelector(`.pk-row[data-id="${item.id}"]`); if (row) { if (!row.dataset.flashing) { row.dataset.flashing = "true"; row.style.transition = "none"; row.style.backgroundColor = "rgba(255, 193, 7, 0.5)"; void row.offsetWidth; requestAnimationFrame(() => { row.style.transition = "background-color 1.5s ease-out"; row.style.backgroundColor = ""; setTimeout(() => { if(row) delete row.dataset.flashing; }, 1500); }); } } } }; performLocate(); trackerInterval = setInterval(() => { trackCount++; const limit = hasTriedRecovery ? 25 : 10; if (S.loading || trackCount <= limit) performLocate(); else stopTracking(); }, 200); } catch (e) { console.error(e); showAlert(`${L.str_error}: ${e.message}`); if (UI.vp) UI.vp.style.opacity = '1'; } finally { setLoad(false); setTimeout(() => { if (UI.vp) UI.vp.style.opacity = '1'; }, 100); } }; } ctx.querySelector('#ctx-ext-play').onclick = () => { ctx.style.display = 'none'; UI.btnExt.click(); }; ctx.querySelector('#ctx-open').onclick = () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; if (!id) return; const item = S.items.find(x => x.id === id); if (!item) return; if (item.kind === 'drive#folder') { if (S.loading) return; S.path.push({ id: item.id, name: item.name }); load(); } else { const mime = (item.mime_type || "").toLowerCase(); const name = (item.name || "").toLowerCase(); if (name.endsWith('.torrent')) { handleTorrentFile(item); } else if (mime.includes('zip') || mime.includes('rar') || mime.includes('7z') || mime.includes('compressed') || mime.includes('archive') || name.endsWith('.zip') || name.endsWith('.rar') || name.endsWith('.7z') || name.endsWith('.tar') || name.endsWith('.gz')) { handleOpenArchive(item); } else if (mime.startsWith('video')) { playVideo(item); } else if (mime.startsWith('image')) { showImage(item); } else { UI.btnExt.click(); } } }; const starBtnCtx = ctx.querySelector('#ctx-star'); if (starBtnCtx) { starBtnCtx.onclick = async (e) => { ctx.style.display = 'none'; const action = e.target.getAttribute('data-action'); const isStar = (action === 'star'); const rawIds = Array.from(S.sel); const ids = rawIds.filter(id => { const it = S.itemMap.get(id); if (!it) return false; if (!S.trashMode && isSystemItem(it)) return false; const isCurrentlyStarred = !!(it.starred || (it.tags && it.tags.some(t => t.name === 'STAR'))); if (isStar && isCurrentlyStarred) return false; if (!isStar && !isCurrentlyStarred) return false; return true; }); if (ids.length === 0) { showToast(isStar ? L.msg_star_added : L.msg_unstar_done); return; } const starTask = FloatBarManager.create(isStar ? L.msg_starring : L.msg_unstarring); const total = ids.length; let successCount = 0; try { const url = `https://api-drive.mypikpak.com/drive/v1/files:${action}`; const headers = getHeaders(); const BATCH_SIZE = 100; for (let i = 0; i < total; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); const chunkSet = new Set(chunk); starTask.update(`${isStar ? L.msg_starring : L.msg_unstarring} ${Math.min(i + BATCH_SIZE, total)} / ${total}`); const res = await fetch(url, { method: 'POST', headers: headers, body: JSON.stringify({ "ids": chunk }) }); if (!res.ok) { const errText = await res.text(); if (res.status === 400 && errText.includes('captcha')) throw new Error(L.err_captcha_simple); throw new Error(`API ${res.status}`); } chunk.forEach(id => { if (isStar) S.starredSet.add(id); else S.starredSet.delete(id); const syncObject = (o) => { if (!o) return; o.starred = isStar; if (!o.tags) o.tags = []; if (isStar) { if (!o.tags.some(t => t.name === 'STAR')) o.tags.push({name: 'STAR', type: 0}); } else { o.tags = o.tags.filter(t => t.name !== 'STAR'); } }; syncObject(S.itemMap.get(id)); const deepSync = (cacheMap) => { if (!cacheMap) return; cacheMap.forEach((data) => { const list = Array.isArray(data) ? data : (data?.items || []); const target = list.find(f => f.id === id); if (target) syncObject(target); }); }; deepSync(globalCache); deepSync(S.cache); }); if (!isStar && S.starredMode && S.path.length === 1) { S.items = S.items.filter(it => !chunkSet.has(it.id)); S.display = S.display.filter(d => !d.isHeader && !chunkSet.has(d.id)); renderVisible(); updateStat(); } else { renderVisible(); } successCount += chunk.length; if (total > BATCH_SIZE) await sleep(50); } starTask.destroy(); showToast(isStar ? L.msg_star_added : L.msg_unstar_done); } catch (err) { console.error(err); if (starTask) starTask.destroy(); ids.forEach(id => { const revertStatus = !isStar; if (revertStatus) S.starredSet.add(id); else S.starredSet.delete(id); const item = S.itemMap.get(id); if (item) { item.starred = revertStatus; if (!item.tags) item.tags = []; if (revertStatus) { if (!item.tags.some(t => t.name === 'STAR')) item.tags.push({name: 'STAR', type: 0}); } else { item.tags = item.tags.filter(t => t.name !== 'STAR'); } } }); renderVisible(); showAlert(err.message); } }; } const observeUnzipTask = (taskId, folderId, fileId, skipUiRefresh = false) => { const checkStatus = async () => { try { const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/progress?task_id=${taskId}`, { headers: getHeaders() }); if (!res.ok) return; const data = await res.json(); if (data.phase === 'PHASE_TYPE_COMPLETE') { console.log(`[Unzip] Task finished: ${taskId}`); if (S && S.sel) S.clearSelection(); let physicalParentId = folderId || 'root'; if (fileId && S.itemMap.has(fileId)) { const it = S.itemMap.get(fileId); if (!it.params) it.params = {}; it.params.global_file_kind = '1'; if (it.parent_id) physicalParentId = it.parent_id; else if (it.parent_id === '') physicalParentId = 'root'; } S.items.forEach(it => { if ((it.kind === 'drive#task' || S.offlineMode || S.uploadMode) && it.file_id === fileId) { if (!it.params) it.params = {}; it.params.global_file_kind = '1'; } }); if (skipUiRefresh) return; if (S && S.sel && S.sel.size > 0) S.clearSelection(); refresh(); updateStat(); const dirtyTargets = new Set(); if (folderId) dirtyTargets.add(folderId); else dirtyTargets.add('root'); dirtyTargets.add(physicalParentId); dirtyTargets.forEach(target => { globalDirtyFolders.add(target); if (target === 'root') globalDirtyFolders.add(''); if (typeof globalCache !== 'undefined') globalCache.delete(target); if (typeof pkState !== 'undefined' && pkState && pkState.cache) pkState.cache.delete(target); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(target === 'root' ? '' : target); backgroundQueue.unshift({ id: target === 'root' ? '' : target, name: 'Unzipped_Update', retryCount: 0 }); }); runBackgroundCrawler(); const curPathNode = S.path[S.path.length - 1]; const curId = curPathNode.id || 'root'; if (dirtyTargets.has(curId) || curId === 'virtual_search_root' || S.isFlattened || S.dupMode) { if (window.pkSmartRefreshTrigger) { console.log(`[Unzip] Triggering force sync for ${curId}`); setTimeout(() => window.pkSmartRefreshTrigger(true), 1200); } } else { dirtyTargets.forEach(target => { apiList(target === 'root' ? '' : target, 500, null, null, false, true).then(newFiles => { if (typeof globalCache !== 'undefined') globalCache.set(target, newFiles); }).catch(()=>{}); }); } return; } if (data.phase === 'PHASE_TYPE_RUNNING' || data.phase === 'PHASE_TYPE_PENDING') { setTimeout(checkStatus, 4000); } } catch (e) { setTimeout(checkStatus, 8000); } }; checkStatus(); }; const askForPassword = (fileName, errorMsg, isBatch = false) => { return new Promise((resolve) => { const txtCancel = isBatch ? L.btn_skip : L.btn_cancel; const txtConfirm = isBatch ? L.btn_ok : L.btn_view_file; const m = showModal(`
${esc(fileName)}
${CONF.typeIcons.archive.replace(/width="\d+"/, 'width="64"').replace(/height="\d+"/, 'height="64"')}
${L.title_input_pwd}
${esc(errorMsg) || L.err_pwd_simple}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.padding = '0'; modalBox.style.width = "420px"; modalBox.style.height = "420px"; modalBox.style.display = "flex"; modalBox.style.flexDirection = "column"; modalBox.style.overflow = "hidden"; } const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) { closeBtn.style.top = "21px"; closeBtn.style.right = "24px"; closeBtn.style.color = "#999"; } const inp = m.querySelector('#retry_pwd'); const clearBtn = m.querySelector('#pwd_clear_btn'); setTimeout(() => inp.focus(), 50); inp.addEventListener('input', () => { clearBtn.style.display = inp.value ? 'flex' : 'none'; }); clearBtn.onclick = () => { inp.value = ''; clearBtn.style.display = 'none'; inp.focus(); }; clearBtn.onmouseover = () => clearBtn.style.color = '#666'; clearBtn.onmouseout = () => clearBtn.style.color = '#999'; const doResolve = (val) => { m.remove(); resolve(val); }; m.querySelector('#ask_skip').onclick = () => doResolve(null); m.querySelector('.pk-modal-close').onclick = () => doResolve(null); m.querySelector('#ask_retry').onclick = () => doResolve(inp.value); inp.onkeydown = (e) => { if(e.key === 'Enter') doResolve(inp.value); }; }); }; const handleOpenArchive = async (file) => { if (S.trashMode) return; setLoad(true); updateLoadTxt(L.loading); const Vault = { get: () => { try { const raw = JSON.parse(gmGet('pk_pwd_vault', '[]')); return raw.map(x => typeof x === 'object' ? x.p : x); } catch { return []; } }, save: (p) => { if (!p) return; try { let list = JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x : {p: x, h: 0}); let item = list.find(x => x.p === p); if (item) item.h = (item.h || 0) + 1; else list.push({p: p, h: 1}); list.sort((a, b) => b.h - a.h); if (list.length > 50) list = list.slice(0, 50); gmSet('pk_pwd_vault', JSON.stringify(list)); } catch(e) {} } }; try { let detail = file; if (!detail.gcid && !detail.hash) detail = await apiGet(file.id); const gcid = detail.gcid || detail.hash || detail.md5_checksum || ""; let currentPwd = ""; let isVerified = false; let hasTriedAuto = false; let serverBusyRetry = 0; while (!isVerified) { const payload = { gcid, file_id: file.id, password: currentPwd, path: "" }; const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); const data = await res.json().catch(() => ({})); const errStr = (data.status_text || data.error_description || "").toLowerCase(); const isPwdRequired = data.error_code === 10023 || data.status === 'PASS_WORD_ERROR' || errStr.includes('password') || errStr.includes('密码'); if (data.status === 'OK' && res.ok) { isVerified = true; if (currentPwd) Vault.save(currentPwd); } else if (isPwdRequired) { if (!currentPwd && !hasTriedAuto) { hasTriedAuto = true; const tryLimit = gmGet('pk_pwd_try_count', 10); const candidates = Vault.get().slice(0, tryLimit); if (candidates.length > 0) { console.log(`[Archive] Parallel-Bruteforce starting: ${candidates.length} candidates`); setLoad(false); const matchingTask = FloatBarManager.create(L.msg_smart_matching_n.replace('{n}', candidates.length)); const checkTask = async (pwd, idx) => { const tieredDelay = Math.floor(idx / 5) * 1200; await sleep((idx * 150) + tieredDelay); if (isVerified) return Promise.reject("Aborted"); try { const autoRes = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ gcid, file_id: file.id, password: pwd, path: "" }) }); const autoData = await autoRes.json(); if (autoData.status === 'OK') return pwd; } catch(e) {} throw new Error("Wrong Pwd"); }; try { const correctPwd = await Promise.any(candidates.map((p, i) => checkTask(p, i))); if (correctPwd) { currentPwd = correctPwd; isVerified = true; Vault.save(correctPwd); } } catch (e) { console.log("[Archive] All cached passwords failed."); } finally { matchingTask.destroy(); if (!isVerified) setLoad(true); } if (isVerified) break; } } setLoad(false); const promptMsg = currentPwd ? L.err_pwd_simple : ""; const userPwd = await askForPassword(file.name, promptMsg); if (userPwd === null) { setLoad(false); return; } currentPwd = userPwd; setLoad(true); updateLoadTxt(L.str_verifying); } else { if (res.status === 500 || res.status === 502) { if (serverBusyRetry < 5) { serverBusyRetry++; console.warn(`[Archive] Server 500 Error. Retry ${serverBusyRetry}/5...`); updateLoadTxt(L.str_server_indexing.replace('{n}', serverBusyRetry)); await sleep(1500); continue; } } throw new Error(errStr || `API Error ${res.status}`); } } setLoad(false); const result = await showArchivePreview(file, currentPwd); if (result && result.confirm) { if (result.password) Vault.save(result.password); handleUnzip([file], result.password, true, result.taskId); } } catch (e) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); } }; const handleTorrentFile = async (file) => { if (parseInt(file.size) > 10 * 1024 * 1024) { showAlert(`${L.str_error_crit}: ${L.err_invalid_links}`); return; } const fb = FloatBarManager.create(L.str_processing); try { const physicalId = (file.file_id || (file.params && file.params.file_id)) || file.id; let detail = file; if (!detail.web_content_link) detail = await apiGet(physicalId); const res = await fetch(detail.web_content_link); const buffer = await res.arrayBuffer(); const buf = new Uint8Array(buffer); if (buf[0] !== 100) throw new Error(L.err_invalid_torrent); let pos = 0; let safetyCounter = 0; const MAX_ITERATIONS = 100000; const decodeSkip = () => { if (++safetyCounter > MAX_ITERATIONS) throw new Error(L.err_torrent_complex); if (pos >= buf.length) return; const c = buf[pos]; if (c === 100 || c === 108) { pos++; while (pos < buf.length && buf[pos] !== 101) { decodeSkip(); if (pos > buf.length) break; } pos++; } else if (c === 105) { pos++; while (pos < buf.length && buf[pos] !== 101) pos++; pos++; } else if (c >= 48 && c <= 57) { let colon = pos; while (colon < buf.length && buf[colon] !== 58) colon++; if (colon >= buf.length) throw new Error(L.err_torrent_format); const len = parseInt(new TextDecoder().decode(buf.slice(pos, colon))); if (isNaN(len)) throw new Error(L.err_torrent_len); pos = colon + 1 + len; } else { throw new Error(L.err_torrent_char); } }; let infoHash = null; pos = 1; while (pos < buf.length && buf[pos] !== 101) { if (++safetyCounter > MAX_ITERATIONS) break; const keyStart = pos; decodeSkip(); let colon = keyStart; while (buf[colon] !== 58 && colon < buf.length) colon++; const kLen = parseInt(new TextDecoder().decode(buf.slice(keyStart, colon))); const keyStr = new TextDecoder().decode(buf.slice(colon + 1, colon + 1 + kLen)); if (keyStr === "info") { const infoStart = pos; decodeSkip(); const infoEnd = pos; const infoBuf = buf.slice(infoStart, infoEnd); const hashBuf = await crypto.subtle.digest("SHA-1", infoBuf); infoHash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join(''); break; } } if (!infoHash) throw new Error("Invalid Torrent File"); const magnet = `magnet:?xt=urn:btih:${infoHash}&dn=${encodeURIComponent(file.name)}`; fb.update(L.msg_creating_cloud_task); await apiAddOfflineTask(magnet, file.parent_id || ""); showToast(L.msg_cloud_task_success.replace('{n}', 1)); if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (S.offlineMode) load(false, true); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { fb.destroy(); } }; const sendUnzipRequest = async (file, password) => { let detail = file; if (!detail.gcid && !detail.hash) { try { detail = await apiGet(file.id); } catch(e) {} } const gcid = detail.gcid || detail.hash || detail.md5_checksum || ""; const payload = { file_id: file.id, files: [], password: password || "", default_parent: true, gcid: gcid }; const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/decompress`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); const data = await res.json().catch(() => ({})); const rawMsg = data.msg || data.error || data.error_description || data.status_text || ""; const errStr = rawMsg.toLowerCase(); const isPwdError = data.error_code === 10023 || data.status === 'PASS_WORD_ERROR' || errStr.includes('password') || errStr.includes('密码'); if (isPwdError) { throw { isError: true, isPwd: true, code: 10023, msg: L.err_pwd_simple }; } if (!res.ok) { throw { isError: true, code: res.status, msg: rawMsg || `HTTP ${res.status}` }; } if (data.status && data.status !== 'OK' && data.code !== 0) { throw { isError: true, code: data.code || -1, msg: rawMsg || data.status }; } return data; }; const showArchivePreview = async (file, initialPassword = "") => { let currentPath = ""; let pathNodes = [{ name: L.picker_all, path: '' }]; let currentPwd = initialPassword; let preCheckData = null; setLoad(true); updateLoadTxt(L.loading); try { let detail = file; if (!detail.gcid && !detail.hash) { try { detail = await apiGet(file.id); } catch(e) {} } const gcid = detail.gcid || detail.hash || detail.md5_checksum || ""; let isVerified = false; let isFirstTry = true; while(!isVerified) { const payload = { gcid, file_id: file.id, password: currentPwd, path: "" }; const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); const data = await res.json().catch(() => ({})); const errStr = (data.status_text || data.error_description || "").toLowerCase(); const isPwdErr = data.error_code === 10023 || data.status === 'PASS_WORD_ERROR' || errStr.includes('password') || errStr.includes('密码'); if (isPwdErr) { setLoad(false); const newPwd = await askForPassword(file.name, isFirstTry ? "" : L.err_pwd_simple); if (newPwd !== null) { currentPwd = newPwd; isFirstTry = false; setLoad(true); } else { return { confirm: false }; } } else if (!res.ok || (data.status && data.status !== 'OK')) { throw new Error(data.status_text || `API Error ${res.status}`); } else { if (currentPwd) { if (gcid) gmSet('pk_archive_pwd_' + gcid, currentPwd); if (typeof updateGlobalPool === 'function') updateGlobalPool(currentPwd); } preCheckData = data; isVerified = true; } } } catch (e) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); return { confirm: false }; } finally { setLoad(false); } return new Promise(async (resolve) => { const m = showModal(`
${esc(file.name)}
`); const modalBox = m.querySelector('.pk-modal'); modalBox.style.padding = '0'; modalBox.style.width = "600px"; modalBox.style.height = "500px"; modalBox.style.maxHeight = "85vh"; modalBox.style.display = "flex"; modalBox.style.flexDirection = "column"; modalBox.style.overflow = "hidden"; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) { closeBtn.style.top = "17px"; closeBtn.style.right = "20px"; closeBtn.style.color = "#999"; closeBtn.style.zIndex = "10"; } const listContainer = m.querySelector('#arc_list_container'); const crumbContainer = m.querySelector('#arc_crumb'); const loading = m.querySelector('#arc_loading'); let arcCrumbIdx = 0; let lastArcScroll = 0; crumbContainer.onwheel = (e) => { e.preventDefault(); const now = Date.now(); if (now - lastArcScroll < 120) return; lastArcScroll = now; const nodes = [...crumbContainer.children].filter(c => c.textContent.trim() !== ""); if (!nodes.length) return; if (e.deltaY < 0) arcCrumbIdx = Math.max(0, arcCrumbIdx - 1); else arcCrumbIdx = Math.min(nodes.length - 1, arcCrumbIdx + 1); const target = nodes[arcCrumbIdx]; const containerWidth = crumbContainer.offsetWidth; const centerOffset = target.offsetLeft + (target.offsetWidth / 2) - (containerWidth / 2); crumbContainer.scrollTo({ left: centerOffset, behavior: 'smooth' }); }; const renderData = (data, path) => { crumbContainer.innerHTML = ''; pathNodes.forEach((node, idx) => { const isLast = idx === pathNodes.length - 1; const span = document.createElement('span'); span.textContent = node.name; span.style.cssText = isLast ? "font-weight:bold; color:var(--pk-fg); cursor:default;" : "color:#888; cursor:pointer; transition:color 0.2s;"; if (!isLast) { span.onmouseover = () => span.style.color = 'var(--pk-pri)'; span.onmouseout = () => span.style.color = '#888'; span.onclick = () => { pathNodes = pathNodes.slice(0, idx + 1); updateView(node.path); }; } crumbContainer.appendChild(span); if (!isLast) { const sep = document.createElement('span'); sep.innerHTML = ``; sep.style.margin = "0 6px"; crumbContainer.appendChild(sep); } }); arcCrumbIdx = pathNodes.length - 1; requestAnimationFrame(() => { crumbContainer.scrollTo({ left: crumbContainer.scrollWidth, behavior: 'smooth' }); }); listContainer.innerHTML = ''; listContainer.appendChild(loading); const items = data.files || data.list || []; if (items.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.style.cssText = "padding:50px; text-align:center; color:#999; font-size:13px;"; emptyMsg.textContent = L.str_empty_dir; listContainer.appendChild(emptyMsg); } else { items.forEach(f => { const itemName = f.filename || f.file_name || f.name || "Unknown"; const itemSize = f.filesize || f.size || 0; const isDir = f.kind === 'drive#folder' || (itemSize == 0 && !f.mime_type); const fullPath = (path || "") + itemName + (isDir ? "/" : ""); const row = document.createElement('div'); row.style.cssText = "display:flex; align-items:center; height:48px; padding:0 24px; cursor:default; transition:background 0.1s; border-bottom:1px solid rgba(0,0,0,0.03);"; if (isDir) { row.style.cursor = "pointer"; row.onmouseover = () => row.style.background = 'var(--pk-hl)'; row.onmouseout = () => row.style.background = 'transparent'; row.onclick = (e) => { e.stopPropagation(); pathNodes.push({ name: itemName, path: fullPath }); updateView(fullPath); }; } else { row.onmouseover = () => row.style.background = 'var(--pk-hl)'; row.onmouseout = () => row.style.background = 'transparent'; } const iconSrc = f.icon_link || ''; const fallbackSvg = (isDir ? CONF.typeIcons.folder : getIcon({ name: itemName, mime_type: f.mime_type })) .replace(/width="\d+"/, 'width="28"').replace(/height="\d+"/, 'height="28"'); const iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; row.innerHTML = `
${iconHtml}
${esc(itemName)}
${isDir ? '-' : fmtSize(itemSize)}
`; listContainer.appendChild(row); }); } }; const updateView = async (path) => { loading.style.display = 'flex'; try { let detail = file; if (!detail.gcid && !detail.hash) detail = await apiGet(file.id); const gcid = detail.gcid || detail.hash || detail.md5_checksum || ""; const payload = { gcid, file_id: file.id, password: currentPwd, path: path }; const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); const data = await res.json().catch(() => ({})); if (res.ok && data.status === 'OK') renderData(data, path); else throw new Error(data.status_text || L.str_load_failed_simple); } catch (e) { const errDiv = document.createElement('div'); errDiv.style.cssText = "padding:50px; text-align:center; color:#d93025; font-size:13px;"; errDiv.textContent = esc(e.message); listContainer.innerHTML = ''; listContainer.appendChild(loading); listContainer.appendChild(errDiv); } finally { loading.style.display = 'none'; } }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve({ confirm: false }); }; m.querySelector('#unzip_cancel').onclick = () => { m.remove(); resolve({ confirm: false }); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#unzip_confirm').click(); } }); m.querySelector('#unzip_confirm').onclick = async () => { const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isVirtual = S.isFlattened || S.dupMode || isStarredRoot || isRecentRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; if (isVirtual) { const userConfirmed = await new Promise(res => { const vm = showModal(`

${L.title_confirm}

${L.msg_unzip_virtual_view_warn}
`); vm.querySelector('#vc_cancel').onclick = () => { vm.remove(); res(false); }; vm.querySelector('.pk-modal-close').onclick = () => { vm.remove(); res(false); }; vm.querySelector('#vc_ok').onclick = () => { vm.remove(); res(true); }; vm.tabIndex = 0; setTimeout(() => vm.focus(), 10); vm.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); vm.querySelector('#vc_ok').click(); } }); }); if (!userConfirmed) return; } const btn = m.querySelector('#unzip_confirm'); const progTxt = m.querySelector('#unzip_progress_text'); btn.disabled = true; btn.style.opacity = '0.6'; btn.style.cursor = 'not-allowed'; btn.textContent = L.str_unzipping_state; progTxt.textContent = L.str_unzipping_prog_0; progTxt.style.opacity = '1'; try { const resp = await sendUnzipRequest(file, currentPwd); if (resp && resp.task_id) { const taskId = resp.task_id; let isPolling = true; let progressTask = null; const currentFolderId = S.path[S.path.length - 1].id || ''; observeUnzipTask(taskId, currentFolderId, file.id); const pollProgress = async () => { if (!isPolling) return; if (!document.contains(m) && !progressTask) { progressTask = FloatBarManager.create(L.str_unzipping.replace('{n}', file.name)); } try { const tRes = await fetch(`https://api-drive.mypikpak.com/decompress/v1/progress?task_id=${taskId}`, { headers: getHeaders() }); if (!tRes.ok) { finishUnzip(); return; } const tData = await tRes.json(); if (tData.phase === 'PHASE_TYPE_COMPLETE') { finishUnzip(); return; } if (tData.progress !== undefined) { const text = L.str_unzipping_prog_fmt.replace('{n}', tData.progress); if (progressTask) progressTask.update(`${L.str_unzipping_state} ${tData.progress}%`); else progTxt.textContent = text; if (tData.progress >= 100) { finishUnzip(); return; } } } catch(e) { if (progressTask) progressTask.destroy(); isPolling = false; resolve({ confirm: true, password: currentPwd, taskId: taskId, alreadyStarted: true }); return; } if (isPolling) setTimeout(pollProgress, 800); }; const finishUnzip = async () => { isPolling = false; if (progressTask) progressTask.destroy(); if (document.contains(m)) { progTxt.textContent = L.str_unzipping_prog_100; await sleep(500); m.remove(); } }; resolve({ confirm: true, password: currentPwd, taskId: taskId, alreadyStarted: true, alreadyObserved: true }); pollProgress(); } else { m.remove(); resolve({ confirm: true, password: currentPwd }); } } catch (e) { btn.disabled = false; btn.style.opacity = '1'; btn.style.cursor = 'pointer'; btn.textContent = L.btn_unzip_all; progTxt.style.color = '#d93025'; progTxt.textContent = e.msg || e.message || L.str_action_failed; console.error(e); } }; renderData(preCheckData, ""); }); }; const handleUnzip = async (items, verifiedPwd = "", skipPreview = false, externalTaskId = null) => { if (!items || items.length === 0) return; const currentFolderId = S.path[S.path.length - 1].id || ''; const Vault = { get: () => { try { const raw = JSON.parse(gmGet('pk_pwd_vault', '[]')); return raw.map(x => typeof x === 'object' ? x.p : x); } catch { return []; } }, save: (p) => { if (!p) return; try { let list = JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x : {p: x, h: 0}); let item = list.find(x => x.p === p); if (item) item.h = (item.h || 0) + 1; else list.push({p: p, h: 1}); list.sort((a, b) => b.h - a.h); if (list.length > 50) list = list.slice(0, 50); gmSet('pk_pwd_vault', JSON.stringify(list)); } catch(e) {} } }; const curPathNode = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isVirtual = S.isFlattened || S.dupMode || isStarredRoot || isRecentRoot || curPathNode.id === 'analyze_root' || curPathNode.id === 'virtual_search_root'; let startFromPreviewTaskId = externalTaskId; let previewResult = null; if (!skipPreview && !verifiedPwd) { if (items.length === 1) { previewResult = await showArchivePreview(items[0], ""); if (!previewResult || !previewResult.confirm) return; verifiedPwd = previewResult.password; if (previewResult.alreadyStarted && previewResult.taskId) { startFromPreviewTaskId = previewResult.taskId; } } else { if (isVirtual) { const userConfirmed = await new Promise(res => { const vm = showModal(`

${L.title_confirm}

${L.msg_unzip_virtual_view_warn}
`); vm.querySelector('#vc_cancel').onclick = () => { vm.remove(); res(false); }; vm.querySelector('.pk-modal-close').onclick = () => { vm.remove(); res(false); }; vm.querySelector('#vc_ok').onclick = () => { vm.remove(); res(true); }; }); if (!userConfirmed) return; } else { if (!await showConfirm(L.msg_unzip_confirm_n.replace('{n}', items.length))) return; } } } if (S.sel.size > 0) { S.clearSelection(); refresh(); } const isSilentMode = (items.length === 1 && !!startFromPreviewTaskId); let progressTask = null; if (!isSilentMode) { progressTask = FloatBarManager.create(L.str_preparing); } let successCount = 0; let failCount = 0; let lastBatchPwd = ""; const MAX_CONCURRENCY = 3; const activePromises = []; let promptMutex = Promise.resolve(); const waitForTaskDone = async (taskId) => { for (let i = 0; i < 300; i++) { try { const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/progress?task_id=${taskId}`, { headers: getHeaders() }); if (res.ok) { const d = await res.json(); if (d.phase === 'PHASE_TYPE_COMPLETE' || d.phase === 'PHASE_TYPE_ERROR') return; } else if (res.status === 404) { return; } } catch(e) {} await sleep(2000); } }; const processSingleItem = async (file, index) => { try { let detail = file; if (!detail.gcid && !detail.hash) { for(let w = 0; w < 5; w++) { try { detail = await Promise.race([ apiGet(file.id), new Promise((_, r) => setTimeout(() => r(new Error("Timeout")), 2000)) ]); } catch(e) {} if (detail.gcid || detail.hash || detail.md5_checksum) break; await sleep(1000); } } const gcid = detail.gcid || detail.hash || detail.md5_checksum || ""; let isDone = false; let triedVerified = false; let triedBatch = false; let triedEmpty = false; let hasTriedVault = false; let isFirstManualInput = true; let systemRetry = 0; while (!isDone) { let pwdToTry = ""; let currentSource = ""; if (verifiedPwd && !triedVerified) { pwdToTry = verifiedPwd; currentSource = "VERIFIED"; } else if (lastBatchPwd && !triedBatch) { pwdToTry = lastBatchPwd; currentSource = "BATCH"; } else if (!triedEmpty) { pwdToTry = ""; currentSource = "EMPTY"; } else if (!hasTriedVault) { hasTriedVault = true; const tryLimit = gmGet('pk_pwd_try_count', 10); const candidates = Vault.get().slice(0, tryLimit); if (candidates.length > 0) { if (progressTask) progressTask.update(L.msg_smart_matching_file.replace('{n}', file.name)); const checkTask = async (pwd, idx) => { const tieredDelay = Math.floor(idx / 5) * 1200; await sleep((idx * 150) + tieredDelay); try { const autoRes = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ gcid, file_id: file.id, password: pwd, path: "" }), signal: AbortSignal.timeout(5000) }); const autoData = await autoRes.json(); if (autoData.status === 'OK') return pwd; } catch(e) {} throw new Error("Wrong"); }; try { const correctPwd = await Promise.any(candidates.map((p, i) => checkTask(p, i))); if (correctPwd) { lastBatchPwd = correctPwd; Vault.save(correctPwd); triedBatch = false; continue; } } catch (e) {} } continue; } else { currentSource = "MANUAL"; } if (currentSource === "MANUAL") { const pwdBeforeWait = lastBatchPwd; const previousMutex = promptMutex; let releaseMutex; promptMutex = new Promise(r => releaseMutex = r); await previousMutex; try { if (lastBatchPwd !== pwdBeforeWait && lastBatchPwd !== "") { continue; } setLoad(false); const errorMsg = isFirstManualInput ? "" : L.err_pwd_simple; const userPwd = await askForPassword(file.name, errorMsg, true); isFirstManualInput = false; setLoad(true); if (userPwd !== null) { lastBatchPwd = userPwd; Vault.save(userPwd); verifiedPwd = userPwd; triedVerified = false; continue; } else { failCount++; isDone = true; continue; } } finally { releaseMutex(); } } try { if (progressTask) { const doneCount = successCount + failCount; progressTask.update(`${L.str_unzipping.replace('{n}', file.name)} (${doneCount + 1} / ${items.length})`); } let taskId = null; let needsObserve = true; if (index === 0 && startFromPreviewTaskId) { taskId = startFromPreviewTaskId; startFromPreviewTaskId = null; if (items.length === 1 && previewResult && previewResult.alreadyObserved) { needsObserve = false; } } else { const preRes = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ gcid, file_id: file.id, password: pwdToTry, path: "" }), signal: AbortSignal.timeout(8000) }); const preData = await preRes.json().catch(() => ({})); const preErrStr = (preData.status_text || preData.error_description || "").toLowerCase(); if (preData.error_code === 10023 || preData.status === 'PASS_WORD_ERROR' || preErrStr.includes('password') || preErrStr.includes('密码')) { throw { isPwd: true, code: 10023, msg: L.err_pwd_simple }; } const resp = await sendUnzipRequest(file, pwdToTry); taskId = resp?.task_id; } if (taskId) { if (needsObserve) observeUnzipTask(taskId, currentFolderId, file.id, true); await waitForTaskDone(taskId); } if (pwdToTry) { lastBatchPwd = pwdToTry; Vault.save(pwdToTry); } successCount++; isDone = true; if (!isSilentMode) { const doneCount = successCount + failCount; updateLoadTxt(`${L.str_unzipping.replace('{n}', file.name)}\n(${doneCount} / ${items.length})`); } } catch (err) { const errText = (err.msg || "").toLowerCase(); const isDefinitePwdErr = err.isPwd || err.code === 10023 || err.status === 'PASS_WORD_ERROR' || errText.includes('password') || errText.includes('密码'); const isAlreadyRunning = errText.includes('正在解压') || errText.includes('decompressing'); const isSystemErr = !isDefinitePwdErr && !isAlreadyRunning && (err.name === 'TimeoutError' || err.code === 400 || err.code === 429 || err.code >= 500 || errText.includes('limit') || errText.includes('busy') || errText.includes('task creation failed')); if (isAlreadyRunning) { console.warn(`[Unzip] Server Lock detected for: ${file.name}. Task is running in background or zombie.`); if (!isSilentMode) { showToast(L.msg_unzip_running_bg.replace('{n}', esc(file.name)), 'warning', 6000); } successCount++; isDone = true; } else if (isSystemErr) { systemRetry++; if (systemRetry < 6) { const delay = 1500 * systemRetry + (Math.random() * 500); updateLoadTxt(L.msg_system_busy_retry.replace('{n}', systemRetry)); await sleep(delay); continue; } failCount++; isDone = true; if (progressTask) progressTask.update(`${L.str_unzipping.replace('{n}', file.name)} (${successCount + failCount} / ${items.length})`); } else if (isDefinitePwdErr) { if (currentSource === "VERIFIED") { triedVerified = true; verifiedPwd = ""; } else if (currentSource === "BATCH") { triedBatch = true; } else if (currentSource === "EMPTY") { triedEmpty = true; } } else { console.error(err); failCount++; isDone = true; if (progressTask) progressTask.update(`${L.str_unzipping.replace('{n}', file.name)} (${successCount + failCount} / ${items.length})`); } } } } catch (fatal) { console.error("Fatal Worker Error:", fatal); failCount++; } }; for (let i = 0; i < items.length; i++) { while (activePromises.length >= MAX_CONCURRENCY) { await Promise.race(activePromises); } const p = processSingleItem(items[i], i); const wrappedP = p.then(() => { const idx = activePromises.indexOf(wrappedP); if (idx > -1) activePromises.splice(idx, 1); }); activePromises.push(wrappedP); } await Promise.all(activePromises); if (successCount > 0 && !isSilentMode) { S.clearSelection(); refresh(); updateStat(); if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); } if (progressTask) progressTask.destroy(); if (!isSilentMode) { setLoad(false); if (successCount > 0) { let msg = L.msg_unzip_batch_submitted.replace('{n}', successCount); if (failCount > 0) msg += " " + L.msg_unzip_batch_skipped.replace('{n}', failCount); if (isVirtual) msg += L.msg_unzip_check_source; showToast(msg); } else if (failCount > 0) { showToast(L.msg_unzip_fail, 'error'); } } }; const renderCalendar = (anchorEl, onSelect) => { const old = document.querySelector('.pk-cal-pop'); if (old) old.remove(); const pop = document.createElement('div'); pop.className = 'pk-cal-pop'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) { pop.classList.add('pk-dark'); } pop.style.cssText = "opacity: 0; animation: none; visibility: hidden; transition: opacity 0.15s ease; z-index: 2147483647 !important; zoom: var(--pk-zoom, 1);"; const now = new Date(getServerNow()); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); let viewYear = now.getFullYear(); let viewMonth = now.getMonth(); const sideOpts = [ { lbl: L.share_perm, val: -1 }, { lbl: `7 ${L.share_days}`, val: 7 }, { lbl: `14 ${L.share_days}`, val: 14 }, { lbl: `30 ${L.share_days}`, val: 30 } ]; const buildHTML = () => { const navLeft = ``; const navRight = ``; let sideHtml = sideOpts.map(o => `
${o.lbl}
` ).join(''); const monthStr = (L.cal_months || [])[viewMonth] || (viewMonth + 1 + L.unit_month); const headerHtml = `
${navLeft}
${navLeft}
${viewYear} ${monthStr}
${navRight}
${navRight}
`; const weeksHtml = (L.cal_week_days || ["S","M","T","W","T","F","S"]).map(w => `
${w}
` ).join(''); const firstDay = new Date(viewYear, viewMonth, 1).getDay(); const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); let daysHtml = ''; for(let i=0; i`; for(let d=1; d<=daysInMonth; d++) { const currentTs = new Date(viewYear, viewMonth, d).getTime(); const isToday = currentTs === todayStart; const isPast = currentTs < todayStart; const isDisabled = isPast; let cls = 'pk-cal-td'; if (isDisabled) cls += ' disabled'; if (isToday) cls += ' today'; daysHtml += `
${d}
`; } return `
${sideHtml}
${L.cal_custom_title}
${headerHtml}
${weeksHtml} ${daysHtml}
`; }; const refresh = () => { pop.innerHTML = buildHTML(); pop.querySelectorAll('.pk-cal-side-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const days = parseInt(el.dataset.val); onSelect({ days: days, ts: null }); pop.remove(); document.removeEventListener('mousedown', onClickOutside); }; }); pop.querySelectorAll('.pk-cal-td:not(.disabled)').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const ts = parseInt(el.dataset.ts); const diff = (ts - todayStart) / (1000 * 3600 * 24); const days = Math.max(1, Math.ceil(diff)); onSelect({ days: days, ts: ts }); pop.remove(); document.removeEventListener('mousedown', onClickOutside); }; }); pop.querySelector('#cal_prev_y').onclick = (e) => { e.stopPropagation(); viewYear--; refresh(); }; pop.querySelector('#cal_next_y').onclick = (e) => { e.stopPropagation(); viewYear++; refresh(); }; pop.querySelector('#cal_prev_m').onclick = (e) => { e.stopPropagation(); viewMonth--; if(viewMonth<0){viewMonth=11;viewYear--;} refresh(); }; pop.querySelector('#cal_next_m').onclick = (e) => { e.stopPropagation(); viewMonth++; if(viewMonth>11){viewMonth=0;viewYear++;} refresh(); }; }; refresh(); document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; if (anchorEl.offsetParent === null) { cleanup(); return; } const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = anchorEl.getBoundingClientRect(); let top = (rect.bottom / scale) + 5; let left = rect.left / scale; if (top + 320 > window.innerHeight / scale) top = (rect.top / scale) - 320 - 5; if (left + 400 > window.innerWidth / scale) left = (window.innerWidth / scale) - 400 - 10; if (left < 10) left = 10; pop.style.top = top + 'px'; pop.style.left = left + 'px'; }; updatePosition(); requestAnimationFrame(() => { pop.style.visibility = 'visible'; void pop.offsetHeight; pop.style.opacity = '1'; }); window.addEventListener('resize', updatePosition); window.addEventListener('scroll', updatePosition, true); const onClickOutside = (e) => { if (!pop || !anchorEl || !pop.parentNode) return; if (!pop.contains(e.target) && !anchorEl.contains(e.target)) { cleanup(); } }; const cleanup = () => { window.removeEventListener('resize', updatePosition); window.removeEventListener('scroll', updatePosition, true); document.removeEventListener('mousedown', onClickOutside); pop.remove(); }; setTimeout(() => document.addEventListener('mousedown', onClickOutside), 10); }; const showShareDetail = (item) => { const checkIcon = ``; const copyIcon = ``; let statusColor = '#52c41a'; let statusText = L.share_perm; if (item.share_status === 'EXPIRED') { statusColor = '#faad14'; statusText = L.str_share_expired; } else if (item.share_status === 'DELETED') { statusColor = '#ff4d4f'; statusText = L.str_share_deleted; } else if (item.expiration_at && item.expiration_at !== "-1") { statusColor = 'inherit'; const d = new Date(item.expiration_at); const pad = n => String(n).padStart(2, '0'); statusText = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; } else if (item.share_status_text) { statusText = item.share_status_text; } const labelStyle = "font-size:14px; font-weight:bold; color:var(--pk-fg); margin-bottom:8px;"; const inputWrapStyle = "display:flex; align-items:center; border:1px solid var(--pk-bd); border-radius:4px; padding:0 12px; background:var(--pk-bg); height:40px; transition:border-color 0.2s;"; const inputStyle = "flex:1; border:none; background:transparent; outline:none; font-size:14px; color:var(--pk-fg);"; const copyBtnStyle = "margin-left:10px; display:flex; align-items:center; justify-content:center; transition:color 0.2s;"; const m = showModal(`

${L.title_share_detail}

${esc(item.name || item.title)}
${item.view_count || 0}
${L.lbl_share_view}
${item.save_count || 0}
${L.lbl_share_save}
${L.lbl_share_link_title}
${item.share_url}
${copyIcon}
${L.lbl_share_pwd_title}
${copyIcon}
${L.share_count_ed}
${CONF.crumbIcons.right}
${L.lbl_share_expire_title}
${CONF.crumbIcons.right}
${L.lbl_share_code_title}
${L.btn_add_share_code}
`); const modalContainer = m.querySelector('.pk-modal'); if (modalContainer) { modalContainer.style.padding = '0'; modalContainer.style.width = 'fit-content'; modalContainer.style.display = 'flex'; modalContainer.style.flexDirection = 'column'; modalContainer.style.overflow = 'hidden'; modalContainer.style.maxHeight = '600px'; } m.querySelectorAll('.pk-copy-btn').forEach(btn => { btn.onclick = (e) => { const val = btn.getAttribute('data-val'); if(!val) return; GM_setClipboard(val); const originalSvg = btn.innerHTML; btn.innerHTML = ``; setTimeout(() => btn.innerHTML = originalSvg, 1500); }; }); const pwdField = m.querySelector('#pk_share_pwd_edit'); const pwdCopyBtn = m.querySelector('#pk_copy_pwd'); pwdField.readOnly = true; pwdField.style.cursor = 'pointer'; pwdField.onclick = () => { const subM = document.createElement('div'); subM.className = 'pk-modal-ov'; subM.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) subM.classList.add('pk-dark'); subM.innerHTML = `
${CONF.icons.close}

${L.title_edit_pwd}

${L.btn_close_pwd}
${L.btn_cancel}
`; document.body.appendChild(subM); const input = subM.querySelector('#pk_new_pwd_input'); const saveBtn = subM.querySelector('#pk_edit_pwd_save'); input.focus(); const validate = () => { const val = input.value.trim(); const isValid = /^[a-zA-Z0-9]{4,10}$/.test(val); saveBtn.disabled = !isValid; saveBtn.style.opacity = isValid ? '1' : '0.4'; saveBtn.style.cursor = isValid ? 'pointer' : 'not-allowed'; return isValid; }; input.oninput = validate; validate(); const closeBtn = subM.querySelector('#pk_close_pwd_row'); closeBtn.onclick = async (e) => { e.stopPropagation(); closeBtn.style.pointerEvents = 'none'; closeBtn.style.opacity = '0.5'; try { await apiUpdateShare(item.id, { pass_code_option: 'NOT_REQUIRED' }); item.pass_code = ""; pwdField.value = L.str_no_pwd; pwdCopyBtn.setAttribute('data-val', ""); pwdCopyBtn.style.display = 'none'; const mainCopyBtn = m.querySelector('#pk_detail_copy_all'); if (mainCopyBtn) mainCopyBtn.textContent = L.btn_copy_link; renderVisible(); subM.remove(); const toast = document.createElement('div'); toast.style.cssText = "position:absolute; top:180px; left:50%; transform:translateX(-50%); width:max-content; background:var(--pk-toast-bg); color:var(--pk-toast-fg); padding:10px 24px; border-radius:8px; font-size:14px; font-weight:600; z-index:10007; pointer-events:none; box-shadow:0 8px 24px var(--pk-tip-sd); border:1px solid var(--pk-toast-bd);"; toast.textContent = L.msg_pwd_updated; m.querySelector('.pk-modal').appendChild(toast); setTimeout(() => toast.remove(), 1200); } catch (err) { showAlert(err.message); closeBtn.style.pointerEvents = 'auto'; closeBtn.style.opacity = '1'; } }; const doSave = async () => { const newPwd = input.value.trim(); const oldPwd = item.pass_code || ''; if (!validate() || newPwd === oldPwd) { if(newPwd === oldPwd) subM.remove(); return; } saveBtn.disabled = true; saveBtn.style.opacity = '0.7'; saveBtn.textContent = "..."; try { await apiUpdateShare(item.id, { pass_code_option: 'REQUIRED', custom_pass_code: newPwd }); item.pass_code = newPwd; pwdField.value = newPwd; pwdCopyBtn.setAttribute('data-val', newPwd); pwdCopyBtn.style.setProperty('display', 'flex', 'important'); const mainCopyBtn = m.querySelector('#pk_detail_copy_all'); if (mainCopyBtn) mainCopyBtn.textContent = L.btn_copy_link_pwd; renderVisible(); subM.remove(); const toast = document.createElement('div'); toast.style.cssText = "position:absolute; top:180px; left:50%; transform:translateX(-50%); width:max-content; background:var(--pk-toast-bg); color:var(--pk-toast-fg); padding:10px 24px; border-radius:8px; font-size:14px; font-weight:600; z-index:10007; pointer-events:none; box-shadow:0 8px 24px var(--pk-tip-sd); border:1px solid var(--pk-toast-bd);"; toast.textContent = L.msg_pwd_updated; m.querySelector('.pk-modal').appendChild(toast); setTimeout(() => toast.remove(), 1200); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); saveBtn.disabled = false; saveBtn.style.opacity = '1'; saveBtn.textContent = L.btn_save; } }; saveBtn.onclick = doSave; subM.querySelector('#pk_edit_pwd_cancel').onclick = () => subM.remove(); subM.querySelector('.pk-modal-close').onclick = () => subM.remove(); input.onkeydown = (e) => { if(e.key === 'Enter') doSave(); if(e.key === 'Escape') { e.stopPropagation(); subM.remove(); } }; }; const phraseRowsContainer = m.querySelector('#pk_phrase_rows'); const btnAddPhrase = m.querySelector('#pk_btn_add_phrase'); let localPhrases = []; const renderPhraseRows = () => { if (!phraseRowsContainer) return; phraseRowsContainer.innerHTML = localPhrases.map(p => `
${esc(p)}
`).join(''); phraseRowsContainer.querySelectorAll('.pk-phrase-row').forEach(row => { row.onclick = (e) => { const copyBtn = e.target.closest('.pk-copy-btn'); if (copyBtn) { const val = copyBtn.dataset.val; GM_setClipboard(val); const originalHtml = copyBtn.innerHTML; copyBtn.innerHTML = ``; setTimeout(() => { if (copyBtn.isConnected) { copyBtn.innerHTML = originalHtml; } }, 1500); return; } openPhraseEditModal(row.dataset.val); }; row.onmouseenter = () => row.style.background = 'var(--pk-hl)'; row.onmouseleave = () => row.style.background = 'var(--pk-bg)'; }); }; const openPhraseEditModal = (oldVal = "") => { const subM = document.createElement('div'); subM.className = 'pk-modal-ov'; subM.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) subM.classList.add('pk-dark'); subM.innerHTML = `
${CONF.icons.close}

${oldVal ? L.title_edit_share_code : L.btn_add_share_code}

${L.btn_del_share_code}
${L.btn_cancel}
`; document.body.appendChild(subM); const input = subM.querySelector('#pk_new_phrase_input'); const saveBtn = subM.querySelector('#pk_edit_phrase_save'); input.focus(); const validate = () => { const val = input.value.trim(); const isValid = val.length >= 5 && val.length <= 18; saveBtn.disabled = !isValid; saveBtn.style.opacity = isValid ? '1' : '0.4'; saveBtn.style.cursor = isValid ? 'pointer' : 'not-allowed'; return isValid; }; input.oninput = validate; validate(); const doRequest = async (newVal) => { saveBtn.disabled = true; saveBtn.style.opacity = '0.7'; saveBtn.textContent = "..."; try { await apiUpdateSharePhrase(item.id, newVal, oldVal); if (!newVal) { localPhrases = localPhrases.filter(x => x !== oldVal); } else if (!oldVal) { localPhrases.push(newVal); } else { localPhrases = localPhrases.map(x => x === oldVal ? newVal : x); } subM.remove(); renderPhraseRows(); renderVisible(); const toast = document.createElement('div'); toast.style.cssText = "position:absolute; top:180px; left:50%; transform:translateX(-50%); width:max-content; background:var(--pk-toast-bg); color:var(--pk-toast-fg); padding:10px 24px; border-radius:8px; font-size:14px; font-weight:600; z-index:10007; pointer-events:none; box-shadow:0 8px 24px var(--pk-tip-sd); border:1px solid var(--pk-toast-bd);"; toast.textContent = L.msg_share_code_updated; m.querySelector('.pk-modal').appendChild(toast); setTimeout(() => toast.remove(), 1200); } catch (err) { showAlert(err.message); saveBtn.disabled = false; saveBtn.style.opacity = '1'; saveBtn.textContent = L.btn_save; } }; saveBtn.onclick = () => { if(validate()) doRequest(input.value.trim()); }; const delBtn = subM.querySelector('#pk_del_phrase_row'); if (delBtn) delBtn.onclick = () => doRequest(""); subM.querySelector('#pk_edit_phrase_cancel').onclick = () => subM.remove(); subM.querySelector('.pk-modal-close').onclick = () => subM.remove(); input.onkeydown = (ev) => { if(ev.key === 'Enter') { if(validate()) saveBtn.onclick(); } if(ev.key === 'Escape') { ev.stopPropagation(); subM.remove(); } }; }; if (btnAddPhrase) btnAddPhrase.onclick = () => openPhraseEditModal(""); apiGetSharePhrases(item.id).then(list => { localPhrases = list; renderPhraseRows(); }); const limitField = m.querySelector('#pk_share_limit_edit'); const limitInput = m.querySelector('#pk_share_limit_val'); limitField.onclick = () => { const currentSave = parseInt(item.save_count || 0); const currentLimit = parseInt(item.limit_count || 0); const subM = document.createElement('div'); subM.className = 'pk-modal-ov'; subM.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) subM.classList.add('pk-dark'); subM.innerHTML = `
${CONF.icons.close}

${L.share_count_ed}

${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${L.btn_cancel}
`; document.body.appendChild(subM); const radios = subM.querySelectorAll('input[name="sh_mod_cnt"]'); const input = subM.querySelector('#sh_mod_cnt_val'); const saveBtn = subM.querySelector('#pk_mod_cnt_save'); const ctrl = subM.querySelector('.pk-num-ctrl'); const updateCtrlState = () => { ctrl.style.opacity = input.disabled ? '0.3' : '1'; ctrl.style.pointerEvents = input.disabled ? 'none' : 'auto'; ctrl.style.cursor = input.disabled ? 'not-allowed' : 'default'; }; radios.forEach(r => r.onchange = () => { input.disabled = r.value !== 'custom'; if (!input.disabled) input.focus(); updateCtrlState(); }); updateCtrlState(); subM.querySelector('#sh_cnt_inc').onclick = (e) => { if (input.disabled) return; e.stopPropagation(); input.value = (parseInt(input.value) || currentSave) + 1; validate(); }; subM.querySelector('#sh_cnt_dec').onclick = (e) => { if (input.disabled) return; e.stopPropagation(); input.value = Math.max(currentSave + 1, (parseInt(input.value) || (currentSave + 2)) - 1); validate(); }; const doSave = async () => { const rVal = subM.querySelector('input[name="sh_mod_cnt"]:checked').value; let newLimit = 0; if (rVal === 'custom') { const val = parseInt(input.value); if (isNaN(val) || val <= 0) { input.style.borderColor = '#d93025'; return; } newLimit = val; } else { newLimit = parseInt(rVal); if (newLimit === -1) newLimit = 0; } if (newLimit > 0 && newLimit <= currentSave) { showToast(L.err_limit_too_low.replace('{n}', newLimit).replace('{s}', currentSave), 'error'); if(input.disabled) input.value = currentSave + 1; return; } saveBtn.textContent = "..."; saveBtn.disabled = true; item.limit_count = newLimit; const store = JSON.parse(gmGet('pk_share_limits', '{}')); if (newLimit > 0) { store[item.id] = newLimit; } else { delete store[item.id]; } gmSet('pk_share_limits', JSON.stringify(store)); if (newLimit === 0) { limitInput.value = L.share_unlimit; } else { limitInput.value = `${newLimit} ${L.share_times}`; } limitInput.style.color = 'var(--pk-fg)'; renderVisible(); if (window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(true); } setTimeout(() => { subM.remove(); showToast(L.msg_limit_updated); }, 200); }; subM.querySelector('#pk_mod_cnt_cancel').onclick = () => subM.remove(); subM.querySelector('.pk-modal-close').onclick = () => subM.remove(); saveBtn.onclick = doSave; input.onkeydown = (e) => { if(e.key === 'Enter') doSave(); }; }; const expField = m.querySelector('#pk_share_exp_edit'); const expInput = m.querySelector('#pk_share_exp_val'); expField.onclick = (e) => { e.stopPropagation(); const existing = document.querySelector('.pk-cal-pop'); if (existing) { existing.remove(); return; } renderCalendar(expField, async (res) => { const selectedDays = res.days; if (selectedDays === null) return; const originalText = expInput.value; expInput.value = "..."; try { const payload = {}; let newText = ""; let newStatusLeft = ""; if (selectedDays === -1) { payload.expiration_at = "-1"; newText = L.share_perm; newStatusLeft = "-1"; } else { const d = res.ts ? new Date(res.ts) : new Date(getServerNow() + selectedDays * 24 * 3600 * 1000); const pad = n => String(n).padStart(2, '0'); const ds = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; payload.expiration_at = `${ds}T23:59:59.000+08:00`; newText = res.ts ? ds : (selectedDays + L.share_days); if (res.ts) { const diff = Math.max(1, Math.ceil((res.ts - new Date(getServerNow()).setHours(0,0,0,0)) / (1000 * 3600 * 24))); newStatusLeft = diff + L.unit_days.trim(); } else { newStatusLeft = selectedDays + L.share_days; } } await apiUpdateShare(item.id, payload); item.expiration_days = selectedDays; item.expiration_at = payload.expiration_at; item.expiration_left = newStatusLeft; expInput.value = newText; expInput.style.color = 'var(--pk-fg)'; renderVisible(); const toast = document.createElement('div'); toast.style.cssText = "position:absolute; top:180px; left:50%; transform:translateX(-50%); width:max-content; background:var(--pk-toast-bg); color:var(--pk-toast-fg); padding:10px 24px; border-radius:8px; font-size:14px; font-weight:600; z-index:10007; pointer-events:none; box-shadow:0 8px 24px var(--pk-tip-sd); border:1px solid var(--pk-toast-bd);"; toast.textContent = L.msg_exp_updated; m.querySelector('.pk-modal').appendChild(toast); setTimeout(() => toast.remove(), 1200); } catch (err) { showAlert(`${L.str_error}: ${err.message}`); expInput.value = originalText; } }); }; m.querySelector('#pk_detail_cancel').onclick = async () => { if (await showConfirm(L.msg_cancel_share_confirm.replace('{n}', 1))) { setLoad(true); try { await apiCancelShare([item.id]); m.remove(); showAlert(L.msg_cancel_share_done.replace('{n}', 1)); load(false, true); } catch (e) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); } } }; m.querySelector('#pk_detail_copy_all').onclick = () => { const url = item.share_url; const pwd = item.pass_code || ''; const title = item.name || item.title; let text = url + '\n'; if (pwd) text += `${L.share_copy_pwd}: ${pwd}\n`; text += `${title}\n${L.share_copy_suffix}`; GM_setClipboard(text); const btn = m.querySelector('#pk_detail_copy_all'); const orgTxt = btn.textContent; btn.textContent = L.msg_copy_success; const originalColor = btn.style.backgroundColor; const originalBorder = btn.style.borderColor; btn.style.backgroundColor = "#52c41a"; btn.style.borderColor = "#52c41a"; setTimeout(() => { btn.textContent = orgTxt; btn.style.backgroundColor = originalColor; btn.style.borderColor = originalBorder; }, 1500); }; }; UI.btnCancelShare.onclick = async () => { const n = S.sel.size; if (n === 0) return; if (!await showConfirm(L.msg_cancel_share_confirm.replace('{n}', n))) return; setLoad(true); updateLoadTxt(L.str_processing); try { const selectedItems = Array.from(S.sel).map(id => S.itemMap.get(id)).filter(Boolean); const serverIds = selectedItems.filter(it => !it._is_local_phantom).map(it => it.id); const phantomIds = selectedItems.filter(it => it._is_local_phantom).map(it => it.id); if (serverIds.length > 0) { await apiCancelShare(serverIds); } if (phantomIds.length > 0) { const graveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); const newGraveyard = graveyard.filter(x => !phantomIds.includes(x.id)); gmSet('pk_expired_shares', JSON.stringify(newGraveyard)); } S.clearSelection(); await load(false, true); showToast(L.msg_cancel_share_done.replace('{n}', n)); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); } }; if (UI.btnCopyLinkOffline) { UI.btnCopyLinkOffline.onclick = () => { const ids = Array.from(S.sel); if (ids.length === 0) return; const tasks = ids.map(id => S.itemMap.get(id)).filter(t => t && (t.source_url || (t.params && t.params.url))); if (tasks.length === 0) return; const urls = tasks.map(t => t.source_url || t.params.url).join('\n'); GM_setClipboard(urls); showToast(L.msg_copy_success); }; } if (UI.btnRetryTask) { UI.btnRetryTask.onclick = async () => { const ids = Array.from(S.sel); if (ids.length === 0) return; const targets = ids.map(id => S.itemMap.get(id)) .filter(t => t && t.phase === 'PHASE_TYPE_ERROR' && (t.source_url || (t.params && t.params.url))); if (targets.length === 0) { showToast(L.err_no_failed_task, "error"); return; } setLoad(true); updateLoadTxt(L.str_processing); let successCount = 0; try { for (const task of targets) { const url = task.source_url || task.params.url; try { await apiAddOfflineTask(url); await apiCancelTask([task.id]); successCount++; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } catch (e) { console.error(`[Retry] Failed for ${task.name}:`, e); if (e.message && e.message.includes(L.err_task_exists)) { await apiCancelTask([task.id]).catch(()=>{}); successCount++; } } await sleep(150); } await load(false, true); showToast(L.msg_retry_submitted.replace('{n}', successCount)); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); S.clearSelection(); updateStat(); } }; } UI.btnUnzip.onclick = async () => { ensureItemMap(); const isArchive = (it) => { if (!it || it.kind === 'drive#folder') return false; const n = (it.name || '').toLowerCase(); const m = (it.mime_type || '').toLowerCase(); return m.includes('zip') || m.includes('rar') || m.includes('7z') || m.includes('compressed') || m.includes('archive') || n.endsWith('.zip') || n.endsWith('.rar') || n.endsWith('.7z') || n.endsWith('.tar') || n.endsWith('.gz'); }; const rawTargets = Array.from(S.sel).map(id => S.itemMap.get(id)).filter(i => isArchive(i)); const existingFolderNames = new Set( S.items.filter(i => i.kind === 'drive#folder').map(i => i.name) ); const targets = rawTargets.filter(item => { if (rawTargets.length === 1) return true; const isMarkedUnzipped = item.params && (item.params.global_file_kind === '1' || item.params.global_file_root); if (!isMarkedUnzipped) return true; let targetFolderName = item.name; const lastDot = targetFolderName.lastIndexOf('.'); if (lastDot > 0) targetFolderName = targetFolderName.substring(0, lastDot); return !existingFolderNames.has(targetFolderName); }); const skippedItems = rawTargets.filter(item => !targets.includes(item)); const skippedCount = skippedItems.length; if (skippedCount > 0) { if (await showConfirm(L.msg_unzip_skip_del_confirm.replace('{n}', skippedCount))) { const idsToDelete = skippedItems.map(i => i.id); await executeBatchDelete(idsToDelete, { silent: true, forceRefresh: false }); showToast(L.msg_del_items_done.replace('{n}', skippedCount)); } else { showToast(L.msg_skip_unzipped.replace('{n}', skippedCount)); } } if (targets.length === 1) { handleOpenArchive(targets[0]); } else if (targets.length > 1) { handleUnzip(targets); } }; ctx.querySelector('#ctx-share').onclick = async () => { ctx.style.display = 'none'; const ids = Array.from(S.sel); if (ids.length === 0) return; const item = S.itemMap.get(ids[0]); if (!item) return; const m = showModal(`

${L.share_title}

${L.share_mode}
${L.share_public}
${L.share_encrypted}
${L.share_expiry}
${L.share_custom}
${L.share_pass}
${L.share_count}
`); const tabs = m.querySelectorAll('.pk-s-tab'); const passSec = m.querySelector('#sh_pass_sec'); const passVal = m.querySelector('#sh_pass_val'); const passRadios = m.querySelectorAll('input[name="sh_pass_type"]'); const btnGo = m.querySelector('#sh_go'); const validate = () => { const mode = m.querySelector('.pk-s-tab.act').dataset.val; const passType = m.querySelector('input[name="sh_pass_type"]:checked').value; const pass = passVal.value.trim(); let isValid = true; if (mode === 'encrypted' && passType === 'custom') { const reg = /^[a-zA-Z0-9]{4,10}$/; isValid = reg.test(pass); } btnGo.disabled = !isValid; btnGo.style.opacity = isValid ? '1' : '0.4'; btnGo.style.cursor = isValid ? 'pointer' : 'not-allowed'; }; tabs.forEach(t => t.onclick = () => { tabs.forEach(x => x.classList.remove('act')); t.classList.add('act'); const isEnc = t.dataset.val === 'encrypted'; passSec.style.opacity = isEnc ? '1' : '0.3'; passSec.style.pointerEvents = isEnc ? 'auto' : 'none'; validate(); }); passRadios.forEach(r => r.onchange = () => { passVal.disabled = r.value !== 'custom'; if (!passVal.disabled) passVal.focus(); validate(); }); const cntRadios = m.querySelectorAll('input[name="sh_cnt"]'); const cntVal = m.querySelector('#sh_cnt_val'); cntRadios.forEach(r => r.onchange = () => { const isCustom = r.value === 'custom'; cntVal.disabled = !isCustom; if (isCustom) { setTimeout(() => cntVal.focus(), 10); } else { cntVal.value = ''; } validate(); }); cntVal.onfocus = () => { if(!cntVal.disabled) cntBox.style.borderColor = 'var(--pk-pri)'; }; cntVal.onblur = () => { if(cntVal.value === '') cntBox.style.borderColor = 'var(--pk-bd)'; }; cntVal.oninput = () => { cntVal.value = cntVal.value.replace(/[^\d]/g, ''); validate(); }; const modalContainer = m.querySelector('.pk-modal'); if (modalContainer) { modalContainer.style.padding = '0'; modalContainer.style.width = 'fit-content'; } const btnCustomExp = m.querySelector('#sh_exp_custom_btn'); const txtCustom = m.querySelector('#sh_custom_txt'); let customDays = null; m.querySelectorAll('input[name="sh_exp"]').forEach(r => { r.addEventListener('change', (e) => { if (e.target.value !== 'custom') { btnCustomExp.style.borderColor = 'var(--pk-bd)'; btnCustomExp.style.color = 'var(--pk-fg)'; txtCustom.textContent = L.share_custom; customDays = null; } }); }); btnCustomExp.onclick = (e) => { e.stopPropagation(); e.preventDefault(); const existing = document.querySelector('.pk-cal-pop'); if (existing) { existing.remove(); return; } renderCalendar(btnCustomExp, (res) => { const presetRadio = m.querySelector(`input[name="sh_exp"][value="${res.days}"]`); if (presetRadio && res.ts === null) { presetRadio.checked = true; presetRadio.dispatchEvent(new Event('change')); } else { customDays = res.days; const radio = btnCustomExp.querySelector('input'); radio.checked = true; btnCustomExp.style.borderColor = 'var(--pk-pri)'; btnCustomExp.style.color = 'var(--pk-pri)'; if (res.ts) { const d = new Date(res.ts); const dateStr = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; txtCustom.textContent = dateStr; } else if (res.days === -1) { txtCustom.textContent = L.share_perm; } else { txtCustom.textContent = `${res.days} ${L.share_days}`; } } }); }; passVal.oninput = validate; m.querySelector('#sh_cancel').onclick = () => m.remove(); btnGo.onclick = async () => { if (btnGo.disabled) return; const mode = m.querySelector('.pk-s-tab.act').dataset.val; const cntRadio = m.querySelector('input[name="sh_cnt"]:checked'); let cnt = parseInt(cntRadio.value); if (cntRadio.value === 'custom') { const customInput = m.querySelector('#sh_cnt_val').value.trim(); cnt = (customInput === "") ? -1 : (parseInt(customInput) || 1); } let exp = -1; const expRadio = m.querySelector('input[name="sh_exp"]:checked'); if (expRadio.value === 'custom') { if (customDays === null) { exp = 7; } else { exp = customDays; } } else { exp = parseInt(expRadio.value); } const isCustomPass = m.querySelector('input[name="sh_pass_type"]:checked').value === 'custom'; const pass = isCustomPass ? passVal.value.trim() : ""; if (mode === 'encrypted' && isCustomPass) { if (pass.length < 4 || pass.length > 10) { showAlert(L.err_share_pass); return; } } m.remove(); setLoad(true); updateLoadTxt(L.msg_creating_share); try { const payload = { file_ids: ids, share_to: mode === 'encrypted' ? 'encryptedlink' : 'publiclink', expiration_days: parseInt(exp), pass_code_option: mode === 'encrypted' ? 'REQUIRED' : 'NOT_REQUIRED' }; if (expRadio.value === 'custom' && customDays !== null) { payload.expiration_days = customDays; } if (cnt > 0) payload.limit_count = cnt; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/share`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) throw new Error(`Create API Error ${res.status}`); let data = await res.json(); let finalPassCode = data.pass_code; if (cnt > 0) { const store = JSON.parse(gmGet('pk_share_limits', '{}')); store[data.share_id] = cnt; gmSet('pk_share_limits', JSON.stringify(store)); } if (mode === 'encrypted' && isCustomPass && pass) { updateLoadTxt(L.str_saving); const patchPayload = { share_id: data.share_id, pass_code_option: 'REQUIRED', custom_pass_code: pass }; const patchRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/share`, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify(patchPayload) }); if (patchRes.ok) { finalPassCode = pass; } else { console.warn("[Share] Custom password patch failed, using fallback."); } } const fullText = data.share_url + (finalPassCode ? ` ${L.lbl_share_code}: ${finalPassCode}` : ''); const resM = showModal(`
${L.title_share_result}
${finalPassCode ? `
${L.lbl_share_code}: ${finalPassCode}
` : ''}
`); const modalBox = resM.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = 'auto'; modalBox.style.minWidth = '460px'; modalBox.style.overflow = 'visible'; modalBox.style.padding = '30px'; } resM.querySelector('#res_copy').onclick = () => { GM_setClipboard(fullText); const b = resM.querySelector('#res_copy'); b.textContent = L.msg_copy_success; setTimeout(() => resM.remove(), 1000); }; } catch (e) { showAlert(e.message); } finally { setLoad(false); } }; }; ctx.querySelector('#ctx-copy-name').onclick = () => { ctx.style.display = 'none'; const names = []; S.sel.forEach(id => { const item = S.itemMap.get(id); if (item && item.name) { const name = item.name; if (item.kind !== 'drive#folder' && name.includes('.') && name.lastIndexOf('.') > 0) { names.push(name.substring(0, name.lastIndexOf('.'))); } else { names.push(name); } } }); if (names.length > 0) { GM_setClipboard(names.join('\n')); showToast(L.msg_copy_success); } }; ctx.querySelector('#ctx-down').onclick = () => { ctx.style.display = 'none'; UI.win.querySelector('#pk-down').click(); }; ctx.querySelector('#ctx-add-bl').onclick = (e) => { ctx.style.display = 'none'; const action = e.target.getAttribute('data-action'); processBlacklistAction(action); }; ctx.querySelector('#ctx-copy').onclick = () => { ctx.style.display = 'none'; UI.btnCopy.click(); }; ctx.querySelector('#ctx-cut').onclick = () => { ctx.style.display = 'none'; UI.btnCut.click(); }; ctx.querySelector('#ctx-prune').onclick = () => { ctx.style.display = 'none'; UI.btnPrune.click(); }; ctx.querySelector('#ctx-rename').onclick = () => { ctx.style.display = 'none'; if (S.sel.size > 1) { UI.btnBulkRename.click(); } else { UI.btnRename.click(); } }; const ctxDel = ctx.querySelector('#ctx-del'); ctxDel.onclick = () => { ctx.style.display = 'none'; UI.btnDel.click(); }; if (S.historyMode) { const ctxDelTxt = ctxDel.childNodes[1]; if (ctxDelTxt) ctxDelTxt.textContent = " " + L.btn_clear_history; } const ctxShCancel = ctx.querySelector('#ctx-sh-cancel'); if (ctxShCancel) { ctxShCancel.onclick = () => { ctx.style.display = 'none'; if (UI.btnCancelShare) UI.btnCancelShare.click(); }; } const ctxShDetail = ctx.querySelector('#ctx-sh-detail'); if (ctxShDetail) { ctxShDetail.onclick = () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (item) showShareDetail(item); }; } const ctxShCopy = ctx.querySelector('#ctx-sh-copy'); if (ctxShCopy) { ctxShCopy.onclick = () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (!item) return; const url = item.share_url || ""; const pwd = item.pass_code || ""; const title = item.name || item.title || ""; let text = url + '\n'; if (pwd) text += `${L.share_copy_pwd}: ${pwd}\n`; text += `${title}\n${L.share_copy_suffix}`; GM_setClipboard(text); showToast(L.msg_copy_success); }; } const ctxRestore = ctx.querySelector('#ctx-restore'); if (ctxRestore) { ctxRestore.onclick = () => { ctx.style.display = 'none'; UI.btnRestore.click(); }; } const ctxDelForever = ctx.querySelector('#ctx-del-forever'); if (ctxDelForever) { ctxDelForever.onclick = () => { ctx.style.display = 'none'; UI.btnDelForever.click(); }; } updateStat(); const restoreUIState = () => { const navs =[UI.btnNavHome, UI.btnNavTrash, UI.btnNavShare, UI.btnNavStarred, UI.btnNavRecent, UI.btnNavHistory, UI.btnNavOffline, UI.btnNavUpload]; navs.forEach(n => { if(n) n.classList.remove('act'); }); const stdBtns =[UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnBlacklistManager]; const shareBtns =[UI.btnCancelShare]; const upBtns =[UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll]; const upSep = el.querySelector('#pk-up-sep'); upBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(upSep) upSep.style.display = 'none'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; if (S.trashMode) { UI.win.classList.add('pk-mode-trash'); if(UI.btnNavTrash) UI.btnNavTrash.classList.add('act'); if(UI.actionBar) UI.actionBar.style.display = 'none'; if(UI.trashBar) UI.trashBar.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.trash_title; } else if (S.shareMode) { if(UI.btnNavShare) UI.btnNavShare.classList.add('act'); if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.btn_nav_share; stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); } else if (S.offlineMode) { if(UI.btnNavOffline) UI.btnNavOffline.classList.add('act'); if (S.path[0]) S.path[0].name = L.title_offline; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex';[UI.btnNewFolder, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip].forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnDel, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); } else if (S.uploadMode) { if(UI.btnNavUpload) UI.btnNavUpload.classList.add('act'); if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.btn_nav_upload; stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(upSep) upSep.style.display = 'block'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; } else if (S.historyMode) { if(UI.btnNavHistory) UI.btnNavHistory.classList.add('act'); if (S.path[0]) S.path[0].name = L.btn_nav_history; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; } else if (S.recentMode || S.starredMode) { if (S.recentMode && UI.btnNavRecent) UI.btnNavRecent.classList.add('act'); if (S.starredMode && UI.btnNavStarred) UI.btnNavStarred.classList.add('act'); if (S.recentMode && S.path[0]) S.path[0].name = L.btn_nav_recent; if (S.starredMode && S.path[0]) S.path[0].name = L.btn_nav_starred; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; } else { if(UI.btnNavHome) UI.btnNavHome.classList.add('act'); if (S.path.length > 0 && S.path[0].id === '') S.path[0].name = L.btn_nav_home; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; } if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.crumb) { UI.crumb.style.opacity = '1'; UI.crumb.style.display = 'flex'; } refresh(); }; restoreUIState(); load(); const AUTO_REFRESH_INTERVAL = 30000; const startSmartRefresh = () => { if (UI.win.dataset.autoRefresh) return null; UI.win.dataset.autoRefresh = "true"; let silentAbortController = null; let retryTimer = null; let lastUserInteractionTime = 0; const updateInteractionTime = () => { lastUserInteractionTime = Date.now(); }; el.addEventListener('mousedown', updateInteractionTime, true); el.addEventListener('keydown', updateInteractionTime, true); const diffWorkerBlob = new Blob([` self.onmessage = function(e) { const { oldList, newList } = e.data; if (oldList.length !== newList.length) { self.postMessage(true); return; } for (let i = 0; i < newList.length; i++) { const a = oldList[i]; const b = newList[i]; if (a.id !== b.id || a.modified_time !== b.modified_time || a.size !== b.size || a.hash !== b.hash || a.starred !== b.starred) { self.postMessage(true); return; } } self.postMessage(false); }; `], { type: 'application/javascript' }); const diffWorker = new Worker(URL.createObjectURL(diffWorkerBlob)); const runWatchdogAudit = async () => { if (document.hidden) return; try { const list = await apiShareList(); const toAutoCancel = list.filter(it => it.kind === 'pikpak#share' && it.limit_count > 0 && it.save_count >= it.limit_count && it.share_status === 'OK'); if (toAutoCancel.length > 0) { const cancelIds = toAutoCancel.map(x => x.id); const expiredGraveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); toAutoCancel.forEach(it => { it.share_status = 'EXPIRED'; it._is_local_phantom = true; expiredGraveyard.push(it); }); if (expiredGraveyard.length > 50) expiredGraveyard.splice(0, expiredGraveyard.length - 50); gmSet('pk_expired_shares', JSON.stringify(expiredGraveyard)); await apiCancelShare(cancelIds).catch(() => {}); const store = JSON.parse(gmGet('pk_share_limits', '{}')); cancelIds.forEach(id => delete store[id]); gmSet('pk_share_limits', JSON.stringify(store)); console.log(`[Watchdog] Autonomous Audit: Auto-canceled ${cancelIds.length} shares.`); if (S.shareMode && window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); } } catch (e) { console.warn("[Watchdog] Audit failed", e); } }; const checkAndRefresh = async (isRetry = false, bypassLock = false) => { if (document.hidden) return; const isTyping = ['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName); const hasUIState = S.sel.size > 0 || (UI.ctx && UI.ctx.style.display === 'block'); const isInteracting = !bypassLock && (Date.now() - lastUserInteractionTime < 1500); const isBusy = S._isEmptyingTrash || S.loading || S.scanning || S.dupMode || S.isFlattened || hasUIState || isTyping || isInteracting; if (isBusy) { if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => checkAndRefresh(true, bypassLock), 2500); return; } if (S.historyMode || S.uploadMode) return; const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const cacheKey = S.shareMode ? 'share_root' : (isStarredRoot ? 'starred_root' : (isRecentRoot ? 'recent_root' : (cur.id || 'root'))); if (cur.id === 'virtual_search_root' || cur.id === 'analyze_root' || cur.id === 'upload_root') return; if (silentAbortController) silentAbortController.abort(); silentAbortController = new AbortController(); const signal = silentAbortController.signal; try { const runWatchdogAudit = async (targetList) => { const toAutoCancel = targetList.filter(it => it.kind === 'pikpak#share' && it.limit_count > 0 && it.save_count >= it.limit_count && it.share_status === 'OK'); if (toAutoCancel.length > 0) { const cancelIds = toAutoCancel.map(x => x.id); const expiredGraveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); toAutoCancel.forEach(it => { it.share_status = 'EXPIRED'; it._is_local_phantom = true; expiredGraveyard.push(it); }); if (expiredGraveyard.length > 50) expiredGraveyard.splice(0, expiredGraveyard.length - 50); gmSet('pk_expired_shares', JSON.stringify(expiredGraveyard)); await apiCancelShare(cancelIds).catch(() => {}); const store = JSON.parse(gmGet('pk_share_limits', '{}')); cancelIds.forEach(id => delete store[id]); gmSet('pk_share_limits', JSON.stringify(store)); console.log(`[Watchdog] Background Audit: Auto-canceled ${cancelIds.length} shares.`); return true; } return false; }; let allFetchedItems = []; if (!S.shareMode) { apiShareList().then(list => runWatchdogAudit(list)); } if (S.shareMode) { allFetchedItems = await apiShareList(); await runWatchdogAudit(allFetchedItems); } else if (S.offlineMode) { const rawTasks =[]; await apiTaskList(1000, (batch) => { if (batch && batch.length) rawTasks.push(...batch); }); allFetchedItems = rawTasks.map(t => { const ref = t.reference_resource || {}; return { id: t.id, kind: 'drive#task', name: ref.name || t.name || t.file_name || 'Untitled Task', size: t.file_size, phase: t.phase, progress: parseInt(t.progress || 0), message: t.message, icon_link: t.icon_link, thumbnail_link: ref.thumbnail_link ? ref.thumbnail_link : t.icon_link, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || '', file_id: t.file_id || '', source_url: (t.params && t.params.url) ? t.params.url : '', params: Object.assign({}, t.params || {}, ref.params || {}), mime_type: ref.mime_type || '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))) }; }); } else if (S.recentMode) { let nextToken = null; const limit = 500; do { if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.sel.size > 0) return; const filters = encodeURIComponent('{"phase":{"in":"PHASE_TYPE_COMPLETE"}}'); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?limit=${limit}&filters=${filters}&thumbnail_size=SIZE_MEDIUM&with_reference_resource=true&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const netPriority = bypassLock ? 'high' : 'low'; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: netPriority }); if (!res.ok) throw new Error("Recent SWR fetch error"); const json = await res.json(); const validTasks = (json.tasks ||[]).filter(t => t.phase === 'PHASE_TYPE_COMPLETE' && (t.type === 'offline' || t.type === 'upload') && t.file_id !== "" ); const mapped = validTasks.map(t => { const ref = t.reference_resource || {}; const mime = ref.mime_type || ''; const isFolder = (ref.kind === 'drive#folder') || (mime === 'application/x-directory') || (t.icon_link && t.icon_link.includes('folder')); return { id: t.file_id || t.id, kind: isFolder ? 'drive#folder' : 'drive#file', name: ref.name || t.file_name || t.name, size: t.file_size, thumbnail_link: ref.thumbnail_link || t.icon_link || '', icon_link: t.icon_link || '', web_content_link: t.file_id ? null : null, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || t.created_time, mime_type: mime, parent_id: '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))), trashed: false, params: Object.assign({}, t.params || {}, ref.params || {}), _sourceTaskId: t.id }; }); allFetchedItems.push(...mapped); nextToken = json.next_page_token; if (allFetchedItems.length >= 2000) break; } while (nextToken); const seen = new Set(); allFetchedItems = allFetchedItems.filter(f => { if (seen.has(f.id)) return false; seen.add(f.id); return true; }); } else { let nextToken = null; const limit = 500; const now = Date.now(); do { if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.sel.size > 0) return; const currentIsStarredRoot = S.starredMode && S.path.length === 1; const targetParentId = (S.trashMode || currentIsStarredRoot) ? '*' : (cur.id || ''); const filterObj = { "trashed": { "eq": S.trashMode } }; if (currentIsStarredRoot) { filterObj.trashed = { "eq": false }; filterObj.system_tag = { "in": "STAR" }; } else if (!S.trashMode) { filterObj.phase = { "eq": "PHASE_TYPE_COMPLETE" }; } const filters = `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`; const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&parent_id=${targetParentId}&_t=${now}${nextToken ? `&page_token=${nextToken}` : ''}`; const netPriority = bypassLock ? 'high' : 'low'; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: netPriority }); if (!res.ok) throw new Error("Silent fetch error"); const data = await res.json(); if (data.files) { allFetchedItems.push(...data.files.map(f => minifyFile(f, true))); } nextToken = data.next_page_token; } while (nextToken); } if (S.analyzeMode && S.analyzeMap) { allFetchedItems.forEach(item => { if (item.kind === 'drive#folder' && S.analyzeMap.has(item.id)) { item.size = S.analyzeMap.get(item.id).size.toString(); } }); } if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.sel.size > 0) return; const nowCur = S.path[S.path.length - 1]; const nowStarredRoot = S.starredMode && S.path.length === 1; let currentExpectedKey = 'root'; if (S.shareMode) currentExpectedKey = 'share_root'; else if (S.offlineMode) currentExpectedKey = 'offline_root'; else if (nowStarredRoot) currentExpectedKey = 'starred_root'; else currentExpectedKey = nowCur.id || 'root'; if (currentExpectedKey === cacheKey) { diffWorker.onmessage = (e) => { const hasChanges = e.data; if (!hasChanges) { console.log("[SmartRefresh] Cache verified. No changes."); return; } if ((!bypassLock && Date.now() - lastUserInteractionTime < 1500) || S.sel.size > 0) { if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => checkAndRefresh(true, bypassLock), 2000); return; } if (allFetchedItems.length > 0) { const sample = allFetchedItems[0]; if (S.shareMode) { } else if (S.trashMode && !sample.trashed) { console.warn("[SmartRefresh] Dirty data blocked: Home data trying to enter Trash view."); return; } else if (!S.trashMode && sample.trashed) { console.warn("[SmartRefresh] Dirty data blocked: Trash data trying to enter Home view."); return; } } S.cache.set(cacheKey, allFetchedItems); if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, allFetchedItems); const newItemMap = new Map(); const newStarredSet = new Set(); for (let i = 0; i < allFetchedItems.length; i++) { const item = allFetchedItems[i]; newItemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { newStarredSet.add(item.id); } } requestAnimationFrame(() => { if (S.sel.size > 0 || S.loading || S.scanning) return; S.items = allFetchedItems; S.itemMap = newItemMap; S.starredSet = newStarredSet; const scrollTop = UI.vp.scrollTop; refresh(); if (UI.vp) UI.vp.scrollTop = scrollTop; console.log(`[SmartRefresh] SWR Sync Complete. Updated ${allFetchedItems.length} items.`); }); }; const simplify = (list) => list.map(x => { let diffHash = x.hash; if (x.kind === 'drive#task') { diffHash = `${x.phase}_${x.progress}_${x.params?.global_file_kind || ''}_${x.hash || ''}`; } else if (x.params?.global_file_kind) { diffHash = `${x.params.global_file_kind}_${x.hash || ''}`; } return { id: x.id, modified_time: x.modified_time, size: x.size, hash: diffHash, starred: !!(x.starred || (x.tags && x.tags.some(t => t.name === 'STAR'))) }; }); if (S.shareMode || cacheKey === 'share_root') { const toAutoCancel = allFetchedItems.filter(it => it.kind === 'pikpak#share' && it.limit_count > 0 && it.save_count >= it.limit_count && it.share_status === 'OK'); if (toAutoCancel.length > 0) { const cancelIds = toAutoCancel.map(x => x.id); const expiredGraveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); toAutoCancel.forEach(it => { it.share_status = 'EXPIRED'; it._is_local_phantom = true; expiredGraveyard.push(it); }); if (expiredGraveyard.length > 50) expiredGraveyard.splice(0, expiredGraveyard.length - 50); gmSet('pk_expired_shares', JSON.stringify(expiredGraveyard)); await apiCancelShare(cancelIds).catch(() => {}); console.log(`[Watchdog] Auto-canceled and archived ${cancelIds.length} shares.`); const store = JSON.parse(gmGet('pk_share_limits', '{}')); cancelIds.forEach(id => delete store[id]); gmSet('pk_share_limits', JSON.stringify(store)); allFetchedItems = allFetchedItems.filter(it => !cancelIds.includes(it.id)); } } diffWorker.postMessage({ oldList: simplify(S.items), newList: simplify(allFetchedItems) }); } } catch (e) { } }; window.pkSmartRefreshTrigger = (isForce = false) => { const session = globalCache.get('offline_session'); if (S.offlineMode && (!session || !session.completed) && !isForce) { console.log(`[SmartRefresh] Pagination in progress (${S.items.length} items loaded). SWR standby...`); return; } if (S.recentMode) { const cached = globalCache.get('recent_root'); if (cached && !Array.isArray(cached) && cached.nextToken && !isForce) { console.log(`[SmartRefresh] Pagination in progress (${S.items.length} items loaded). SWR standby...`); return; } } checkAndRefresh(false, isForce); }; const onVisibilityChange = () => { if (retryTimer) clearTimeout(retryTimer); checkAndRefresh(false, false); runWatchdogAudit(); }; const uiSyncTimer = setInterval(() => checkAndRefresh(false, true), 60000); const watchdogTimer = setInterval(runWatchdogAudit, 60000); document.addEventListener('visibilitychange', onVisibilityChange); return { handler: onVisibilityChange, abort: () => { if(silentAbortController) silentAbortController.abort(); if(retryTimer) clearTimeout(retryTimer); if(uiSyncTimer) clearInterval(uiSyncTimer); if(watchdogTimer) clearInterval(watchdogTimer); document.removeEventListener('visibilitychange', onVisibilityChange); el.removeEventListener('mousedown', updateInteractionTime, true); el.removeEventListener('keydown', updateInteractionTime, true); if (diffWorker) diffWorker.terminate(); delete window.pkSmartRefreshTrigger; } }; }; const visibilityListener = startSmartRefresh(); const handleClose = () => { let safePath = [...S.path]; if (safePath.some(n => n.id === 'virtual_search_root' || n.id === 'analyze_root')) { safePath = S.preSearchPath || [{ id: '', name: L.btn_nav_home }]; } globalSavedState = { path: safePath, trashMode: S.trashMode, isMaximized: UI.win.classList.contains('pk-maximized') }; if (S.offlineMode && S.items.length > 0) { const cacheKey = 'offline_root'; const cacheData = { items: [...S.items], nextToken: null }; if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, cacheData); } pkState = null; delete window.pkUpdateCrawlerUI; if (S && S.broadcast) S.broadcast.close(); if (visibilityListener && visibilityListener.abort) { visibilityListener.abort(); } if (typeof destroyTooltip === 'function') destroyTooltip(); el.remove(); document.removeEventListener('keydown', keyHandler); document.removeEventListener('mouseup', mouseHandler); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }; UI.btnClose.addEventListener('click', () => { document.body.classList.remove('pk-body-max'); el.style.display = 'none'; }); updateCrawlerUI(); S.updateBlCache(); if (S.items && S.items.length > 0) { S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); } async function syncGlobalStarredStatus() { let nextToken = null; try { const latestStarredIds = new Set(); do { const filter = encodeURIComponent('{"starred":{"eq":true},"trashed":{"eq":false}}'); const url = `https://api-drive.mypikpak.com/drive/v1/files?filters=${filter}&limit=1000${nextToken ? `&page_token=${nextToken}` : ''}`; const res = await fetch(url, { headers: getHeaders() }); if (!res.ok) break; const data = await res.json(); if (data.files) { data.files.forEach(f => latestStarredIds.add(f.id)); } nextToken = data.next_page_token; } while (nextToken); S.pendingMap.forEach((targetStatus, id) => { if (targetStatus) { latestStarredIds.add(id); } else { latestStarredIds.delete(id); } }); if (latestStarredIds.size > 0 || S.items.length > 0) { S.starredSet = latestStarredIds; let updatedCount = 0; if (typeof renderVisible === 'function') renderVisible(); } } catch (e) { console.warn(L.err_star_sync_fail + ":", e); } } const refreshQuotaText = () => { const txt = el.querySelector('#pk-quota-txt'); if (!txt || !S.quota) return; const isMaxNow = UI.win.classList.contains('pk-maximized'); txt.textContent = isMaxNow ? `${S.quota.usedStr} / ${S.quota.limitStr}` : `${S.quota.pct}%`; }; const updateQuotaUI = async () => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/about?_t=${Date.now()}`, { headers: getHeaders() }); if (!res.ok) { if (res.status === 401 || res.status === 403) { setTimeout(updateQuotaUI, 2000); } return; } const data = await res.json(); const q = data.quota; if (!q) return; const used = parseInt(q.usage); const limit = parseInt(q.limit); const pct = Math.min(100, (used / limit) * 100).toFixed(1); const fQ = (v, d) => { let n = v, i = 0, units = ['B','KB','MB','GB','TB']; while(n >= 1024 && i < 4) { n /= 1024; i++; } return n.toFixed(d) + ' ' + units[i]; }; S.quota = { usedStr: fQ(used, 2), limitStr: fQ(limit, 0), pct: pct, usedRaw: used, limitRaw: limit }; const bar = el.querySelector('#pk-quota-bar'); const panel = el.querySelector('#pk-quota-panel'); if (bar) bar.style.width = pct + '%'; refreshQuotaText(); if (panel) panel.setAttribute('data-pk-tip', `${L.lbl_storage}: ${pct}% (${S.quota.usedStr} / ${S.quota.limitStr})`); if (bar) { if (parseFloat(pct) > 90) bar.style.background = '#d93025'; else if (parseFloat(pct) > 70) bar.style.background = '#faad14'; else bar.style.background = 'var(--pk-pri)'; } } catch (e) { console.warn("[Quota] Sync failed"); } }; updateQuotaUI(); const quotaTimer = setInterval(updateQuotaUI, 300000); const originalClose = UI.btnClose.onclick; UI.btnClose.onclick = (e) => { clearInterval(quotaTimer); if(originalClose) originalClose.call(UI.btnClose, e); }; await load(false, true); if (globalSavedState && globalSavedState.scrollTop !== undefined) { setTimeout(() => { if (typeof UI !== 'undefined' && UI.vp) { UI.vp.scrollTop = globalSavedState.scrollTop; delete globalSavedState.scrollTop; } }, 50); } syncGlobalStarredStatus(); if (gmGet('pk_turbo_mode', false)) { setTimeout(() => { if (typeof showToast === 'function') { showToast(getStrings().msg_turbo_activated, 'success', 5000); } }, 800); } } let backgroundQueue = []; let isBackgroundRunning = false; let scannedFolderIds = new Set(); let globalPreloadPromise = null; let globalCache = new Map(); let globalLineageMap = new Map(); let globalParentIndex = new Map(); let globalTombstoneCache = new Map(); let globalDirtyFolders = new Set(); let globalNeedsSync = false; let isGlobalIndexReady = false; let hasShownGlobalWarnSession = false; let serverClockOffset = 0; let hasSyncedTime = false; let globalSavedState = null; const syncTime = (headers) => { if (!headers) return; const serverDate = headers.get('Date') || headers.get('date'); if (serverDate) { const remoteTime = new Date(serverDate).getTime(); const localTime = Date.now(); serverClockOffset = remoteTime - localTime; hasSyncedTime = true; } }; const getServerNow = () => Date.now() + serverClockOffset; let isGUISensitive = false; let pkState = null; const indexParents = (parentId, parentName, files) => { if (!files || !Array.isArray(files)) return; const pId = parentId || 'root'; const pName = parentName || 'Root'; for (const f of files) { if (f.kind === 'drive#folder') { globalParentIndex.set(f.id, { id: pId, name: pName }); } } }; const DurationProber = (() => { let queue = []; let isRunning = false; let probeVideo = null; let loopTimer = null; let runToken = 0; const startNext = async () => { if (!isRunning || queue.length === 0) { isRunning = false; return; } const currentToken = runToken; if (document.hidden) { console.log(`[Prober] Running in background... Queue: ${queue.length}`); } const isUserWatching = !!document.getElementById('pk-player-ov'); const isSystemScanning = typeof pkState !== 'undefined' && pkState && pkState.scanning; if (isUserWatching || isSystemScanning) { loopTimer = setTimeout(startNext, 2000); return; } const item = queue.shift(); if (!probeVideo) { probeVideo = document.createElement('video'); probeVideo.muted = true; probeVideo.style.display = 'none'; } console.log(`[Prober] Probing duration for: ${item.name}`); let watchdog = null; const currentVideo = probeVideo; const cleanup = () => { if (watchdog) clearTimeout(watchdog); if (currentVideo) { currentVideo.removeEventListener('loadedmetadata', onLoaded); currentVideo.removeEventListener('error', onError); currentVideo.src = ""; currentVideo.load(); } if (isRunning && currentToken === runToken) { loopTimer = setTimeout(startNext, 1500); } }; const saveAndNotify = (targetItem, dur) => { gmSet('pk_duration_' + targetItem.id, dur); if (typeof pkState !== 'undefined' && pkState) { [pkState.items, pkState.display].forEach(list => { const found = list.find(i => i.id === targetItem.id); if (found && found.params) found.params.duration = dur; }); const row = document.querySelector(`.pk-row[data-id="${targetItem.id}"]`); if (row) { const cols = row.children; const durCol = cols.length > 1 ? cols[cols.length - 2] : null; if (durCol) { durCol.style.color = 'var(--pk-pri)'; durCol.textContent = fmtDur(dur); setTimeout(() => { if(durCol) durCol.style.color = ''; }, 2000); } } } }; const onLoaded = () => { if (currentToken === runToken) { const dur = Math.round(currentVideo.duration); if (dur > 0) { console.log(`[Prober] Success: ${item.name} -> ${dur}s`); saveAndNotify(item, dur); } } cleanup(); }; const onError = () => { cleanup(); }; currentVideo.addEventListener('loadedmetadata', onLoaded); currentVideo.addEventListener('error', onError); try { const res = await apiGet(item.id); if (currentToken !== runToken) { cleanup(); return; } if (!res || !res.web_content_link) { cleanup(); return; } const url = res.web_content_link; const nameLower = (item.name || '').toLowerCase(); const urlLower = url.toLowerCase(); const isM3u8 = nameLower.endsWith('.m3u8') || urlLower.includes('.m3u8'); const isWmv = nameLower.endsWith('.wmv') || urlLower.includes('.wmv') || nameLower.endsWith('.asf') || urlLower.includes('.asf'); const isAvi = nameLower.endsWith('.avi') || urlLower.includes('.avi') || nameLower.endsWith('.divx') || urlLower.includes('.divx'); const isFlv = nameLower.endsWith('.flv') || urlLower.includes('.flv'); const isMkv = nameLower.endsWith('.mkv') || urlLower.includes('.mkv'); const isRmvb = nameLower.endsWith('.rmvb') || urlLower.includes('.rmvb') || nameLower.endsWith('.rm') || urlLower.includes('.rm'); if (isM3u8) { const fetchOptions = window.AbortSignal ? { signal: AbortSignal.timeout(15000) } : {}; const text = await fetch(url, fetchOptions).then(r => r.text()); if (currentToken === runToken) { const matches = text.matchAll(/#EXTINF:([\d.]+)/g); let total = 0; for (const m of matches) total += parseFloat(m[1]); if (total > 0) saveAndNotify(item, Math.round(total)); } cleanup(); } else if (isWmv || isAvi || isFlv || isMkv || isRmvb) { const rangeEnd = isMkv ? 65535 : 8191; const fetchOptions = { headers: { 'Range': `bytes=0-${rangeEnd}` }, ...(window.AbortSignal ? { signal: AbortSignal.timeout(15000) } : {}) }; try { const response = await fetch(url, fetchOptions); if (currentToken === runToken && (response.ok || response.status === 206)) { const buffer = await response.arrayBuffer(); const view = new DataView(buffer); const bytes = new Uint8Array(buffer); let seconds = 0; let formatName = ''; if (isWmv) { formatName = nameLower.endsWith('.asf') ? 'ASF' : 'WMV'; const guid = [0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11, 0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65]; let foundIdx = -1; for (let i = 0; i < bytes.length - 88; i++) { let match = true; for (let j = 0; j < 16; j++) { if (bytes[i + j] !== guid[j]) { match = false; break; } } if (match) { foundIdx = i; break; } } if (foundIdx !== -1) { const playDur = view.getBigUint64(foundIdx + 64, true); const preroll = view.getBigUint64(foundIdx + 80, true); seconds = Number(playDur - preroll) / 10000000; } } else if (isAvi) { formatName = nameLower.endsWith('.divx') ? 'DIVX' : 'AVI'; const avih = [0x61, 0x76, 0x69, 0x68]; let foundIdx = -1; for (let i = 0; i < bytes.length - 28; i++) { if (bytes[i] === avih[0] && bytes[i+1] === avih[1] && bytes[i+2] === avih[2] && bytes[i+3] === avih[3]) { foundIdx = i; break; } } if (foundIdx !== -1) { const microSecPerFrame = view.getUint32(foundIdx + 8, true); const totalFrames = view.getUint32(foundIdx + 24, true); seconds = (microSecPerFrame * totalFrames) / 1000000; } } else if (isFlv) { formatName = 'FLV'; const durKey = [0x00, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00]; let foundIdx = -1; for (let i = 0; i < bytes.length - 19; i++) { let match = true; for (let j = 0; j < 11; j++) { if (bytes[i + j] !== durKey[j]) { match = false; break; } } if (match) { foundIdx = i; break; } } if (foundIdx !== -1) seconds = view.getFloat64(foundIdx + 11, false); } else if (isRmvb) { formatName = 'RM/RMVB'; const prop = [0x50, 0x52, 0x4F, 0x50]; let foundIdx = -1; for (let i = 0; i < bytes.length - 36; i++) { if (bytes[i] === prop[0] && bytes[i+1] === prop[1] && bytes[i+2] === prop[2] && bytes[i+3] === prop[3]) { foundIdx = i; break; } } if (foundIdx !== -1) { const ms = view.getUint32(foundIdx + 32, false); seconds = ms / 1000; } } else if (isMkv) { formatName = 'MKV'; let timecodeScale = 1000000; let durationVal = 0; for (let i = 0; i < bytes.length - 10; i++) { if (bytes[i] === 0x2A && bytes[i+1] === 0xD7 && bytes[i+2] === 0xB1) { let len = bytes[i+3] & 0x7F; if (len === 3) timecodeScale = (bytes[i+4]<<16) | (bytes[i+5]<<8) | bytes[i+6]; if (len === 4) timecodeScale = (bytes[i+4]<<24) | (bytes[i+5]<<16) | (bytes[i+6]<<8) | bytes[i+7]; break; } } for (let i = 0; i < bytes.length - 10; i++) { if (bytes[i] === 0x44 && bytes[i+1] === 0x89) { let lenByte = bytes[i+2]; if (lenByte === 0x84) { durationVal = view.getFloat32(i+3, false); break; } else if (lenByte === 0x88) { durationVal = view.getFloat64(i+3, false); break; } } } if (durationVal > 0) { seconds = (durationVal * timecodeScale) / 1000000000; } } if (seconds > 0) { console.log(`[Prober] Success (${formatName} Binary Parsing): ${item.name} -> ${Math.round(seconds)}s`); saveAndNotify(item, Math.round(seconds)); } else { console.warn(`[Prober] Binary parsed but got 0 duration for ${item.name}`); } } } catch (e) { console.warn(`[Prober] Failed to parse binary header for ${item.name}`); } cleanup(); } else { if (document.hidden) { console.log(`[Prober] Video tag throttled by browser, pausing prober: ${item.name}`); queue.unshift(item); isRunning = false; cleanup(); return; } watchdog = setTimeout(() => { console.warn(`[Prober] Watchdog timeout fetching metadata for: ${item.name}`); cleanup(); }, 15000); currentVideo.src = url; currentVideo.load(); } } catch(e) { cleanup(); } }; return { add: (item, isBackground = false) => { if (queue.some(i => i.id === item.id)) return; if (isBackground) { queue.push(item); } else { queue.unshift(item); } if (!isRunning) { isRunning = true; startNext(); } }, checkAndRun: () => { if (!isRunning && queue.length > 0) { isRunning = true; startNext(); } }, reset: () => { console.log(`[Prober] Resetting queue (Dropped ${queue.length} tasks).`); runToken++; queue = []; isRunning = false; if (loopTimer) clearTimeout(loopTimer); if (probeVideo) { probeVideo.removeAttribute('src'); probeVideo.load(); probeVideo = null; } } }; })(); const minifyFile = (f, isBackground = false) => { if (f._minified) return f; const { id, kind, name, parent_id, size, mime_type, thumbnail_link, icon_link, web_content_link, hash, gcid, md5_checksum } = f; const trashed = !!f.trashed; const tags = f.tags ? [...f.tags] : []; const lineage = f._lineage ? [...f._lineage] : undefined; const isStarred = !!(f.starred || f.star || f.is_star || (tags.some(t => t.name === 'STAR'))); let duration = 0; const parse = (v) => { if (!v) return 0; const n = parseInt(v, 10); return isNaN(n) ? 0 : n; }; if (f.video_media_metadata?.duration) duration = parse(f.video_media_metadata.duration); if (!duration && f.audio_media_metadata?.duration) duration = parse(f.audio_media_metadata.duration); if (!duration && f.medias && Array.isArray(f.medias)) { for (const m of f.medias) { const d = m.duration ? parse(m.duration) : (m.video?.duration ? parse(m.video.duration) : 0); if (d > 0) { duration = d; break; } } } if (!duration && f.params?.duration) duration = parse(f.params.duration); let final_modified_time = f.modified_time; if (!duration && id) duration = gmGet('pk_duration_' + id, 0); if (kind === 'drive#folder' && id) { const localFMod = gmGet('pk_fmod_' + id); if (localFMod) final_modified_time = localFMod; } if (!duration && kind === 'drive#file') { const ext = (name || '').split('.').pop().toLowerCase(); const mime = (mime_type || '').toLowerCase(); if (['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'ts'].includes(ext) || mime.startsWith('video/')) { const probeItem = { id, name, kind, mime_type, parent_id, size }; setTimeout(() => DurationProber.add(probeItem, isBackground), 3000); } } return { id, kind, name, parent_id, size, file_count: (function() { if (f.file_count !== undefined) return f.file_count; if (f.usage && f.usage.file_count !== undefined) return f.usage.file_count; if (f.params && f.params.file_count !== undefined) return f.params.file_count; if (f.audit && f.audit.file_count !== undefined) return f.audit.file_count; return undefined; })(), modified_time: final_modified_time, thumbnail_link, icon_link, mime_type, trashed, web_content_link, tags, starred: isStarred, params: { duration, width: f.video_media_metadata?.width || f.params?.width, height: f.video_media_metadata?.height || f.params?.height, global_file_kind: f.params?.global_file_kind, global_file_root: f.params?.global_file_root }, hash: hash || md5_checksum || gcid, _lineage: lineage, _minified: true }; }; async function runBackgroundCrawler() { if (isBackgroundRunning) return; isBackgroundRunning = true; if (window.pkUpdateCrawlerUI) window.pkUpdateCrawlerUI(); const homeBtn = document.querySelector('#pk-nav-home'); if (homeBtn) homeBtn.classList.add('pk-status-dot'); const userSetLimit = parseInt(localStorage.getItem('pk_user_limit') || "50"); const BACKGROUND_MAX_CONCURRENCY = Math.min(userSetLimit, 32); let currentConcurrencyLimit = 5; const MIN_CONCURRENCY = 2; let activeRequests = 0; let pendingRetries = 0; const fetchFolderContents = async (folder) => { activeRequests++; try { let files; if (globalCache.has(folder.id)) { files = globalCache.get(folder.id); } else { files = await apiList(folder.id, 1000, null, null, false, true); if (!isGUISensitive) { globalCache.set(folder.id, files); } } if (files && Array.isArray(files)) { for (let i = 0; i < files.length; i++) { const f = files[i]; if (f.kind === 'drive#folder') { if (!scannedFolderIds.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name, retryCount: 0 }); scannedFolderIds.add(f.id); } } } } if (currentConcurrencyLimit < BACKGROUND_MAX_CONCURRENCY) { currentConcurrencyLimit += 0.2; } } catch (err) { currentConcurrencyLimit = MIN_CONCURRENCY; folder.retryCount = (folder.retryCount || 0) + 1; const backoffTime = Math.min(folder.retryCount * 5000, 30000); pendingRetries++; try { await sleep(backoffTime); if (!isGUISensitive) backgroundQueue.unshift(folder); } finally { pendingRetries--; } } finally { activeRequests--; } }; while (backgroundQueue.length > 0 || activeRequests > 0 || pendingRetries > 0 || (typeof globalDirtyFolders !== 'undefined' && globalDirtyFolders.size > 0)) { const isUserBusy = pkState && (pkState.scanning || pkState.loading || document.getElementById('pk-player-ov')); if (isUserBusy) { if (homeBtn) homeBtn.classList.remove('pk-status-dot'); await sleep(2000); continue; } if (homeBtn) homeBtn.classList.add('pk-status-dot'); if (backgroundQueue.length > 0 && activeRequests < Math.floor(currentConcurrencyLimit)) { const folder = backgroundQueue.pop(); fetchFolderContents(folder); await sleep(50); } else if (activeRequests > 0 || pendingRetries > 0) { await sleep(500); } else if (typeof globalDirtyFolders !== 'undefined' && globalDirtyFolders.size > 0) { const dirtyId = Array.from(globalDirtyFolders)[0]; globalDirtyFolders.delete(dirtyId); if (typeof globalCache !== 'undefined') { for (const k of globalCache.keys()) { if (k && k.startsWith('__analyze_nodeMap_')) { globalCache.delete(k); } } } const normalizedId = dirtyId === 'root' ? '' : dirtyId; backgroundQueue.unshift({ id: normalizedId, name: "Dirty_Reval", retryCount: 0 }); continue; } else { let discovered = 0; if (typeof globalCache !== 'undefined') { for (const [parentFid, files] of globalCache) { if (!files) continue; for (let i = 0; i < files.length; i++) { const f = files[i]; if (f.kind === 'drive#folder' && !scannedFolderIds.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name, retryCount: 0 }); scannedFolderIds.add(f.id); discovered++; } } if (discovered > 0) break; } } if (discovered === 0) break; } } isBackgroundRunning = false; if (window.pkUpdateCrawlerUI) window.pkUpdateCrawlerUI(); if (homeBtn) homeBtn.classList.remove('pk-status-dot'); } async function preLoadRootFiles(onProgress) { if (globalPreloadPromise) return globalPreloadPromise; console.log("Initiating background pre-load..."); globalPreloadPromise = new Promise(async (resolve) => { try { const isAuthReady = await waitForAuth(15000); if (!isAuthReady) { console.warn("Background Crawler: Auth token wait timeout."); } const rootFiles = await apiList('', 1000, onProgress); globalCache.set('root', rootFiles); console.log("Background pre-load (Root) successful."); const rootFolders = rootFiles.filter(f => f.kind === 'drive#folder'); rootFolders.forEach(f => { if (!scannedFolderIds.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name }); scannedFolderIds.add(f.id); } }); runBackgroundCrawler(); } catch (e) { console.error("Background pre-load failed:", e); } finally { resolve(globalCache.has('root')); } }); return globalPreloadPromise; } async function tryInject() { if (location.href.includes('/login') || location.pathname.includes('login')) return; console.log("🚀 PikPak Script: Attempting inject..."); if (document.getElementById('pk-launch')) { console.log("🚀 PikPak Script: Already injected."); return; } if (!document.body) { console.log("🚀 PikPak Script: Body not ready, retrying..."); setTimeout(tryInject, 500); return; } inject(); const isTurbo = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; if (isTurbo) { console.log("🚀[Turbo Mode] Fast-track rendering..."); const startTurbo = async () => { const preload = preLoadRootFiles(); if (!document.querySelector('.pk-ov')) { await openManager(globalCache, preload); } }; setTimeout(startTurbo, 100); } else { setTimeout(() => { preLoadRootFiles(); }, 1500); } document.addEventListener('visibilitychange', () => { if (!document.hidden) { if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); if (typeof isBackgroundRunning !== 'undefined' && !isBackgroundRunning) runBackgroundCrawler(); } }); console.log("🚀 PikPak Script: INJECT SUCCESS! Background pre-load started."); } function inject() { if (document.getElementById('pk-launch')) return; const b = document.createElement('button'); b.id = 'pk-launch'; const isTurbo = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; const displayStyle = isTurbo ? 'none!important' : 'flex!important'; b.style.cssText = `position:fixed;bottom:20px;right:20px;width:50px;height:50px;border-radius:50%;background:#1a5eff;border:none;cursor:pointer;z-index:2147483647;box-shadow:0 4px 12px rgba(0,0,0,0.3);padding:0;overflow:hidden;transition:transform 0.1s;display:${displayStyle};align-items:center!important;justify-content:center!important;`; b.innerHTML = ` `; const savedLeft = gmGet('pk_pos_left', null); const savedTop = gmGet('pk_pos_top', null); if (savedLeft !== null && savedTop !== null) { b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = savedLeft; b.style.top = savedTop; } else { b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = '10px'; b.style.top = '430px'; } let isDragging = false; let dragStartX, dragStartY; const constrainBall = () => { const rect = b.getBoundingClientRect(); let newLeft = rect.left; let newTop = rect.top; const maxL = window.innerWidth - 50; const maxT = window.innerHeight - 50; if (newLeft > maxL) newLeft = maxL; if (newTop > maxT) newTop = maxT; if (newLeft < 0) newLeft = 0; if (newTop < 0) newTop = 0; b.style.left = newLeft + 'px'; b.style.top = newTop + 'px'; }; window.addEventListener('resize', constrainBall); const blockNativeDrag = (e) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; }; b.addEventListener('dragenter', blockNativeDrag); b.addEventListener('dragover', blockNativeDrag); b.addEventListener('drop', (e) => { blockNativeDrag(e); if (!document.querySelector('.pk-ov') || document.querySelector('.pk-ov').style.display === 'none') { const clickEvt = new MouseEvent('mousedown', { clientX: e.clientX, clientY: e.clientY }); b.dispatchEvent(clickEvt); const upEvt = new MouseEvent('mouseup', { clientX: e.clientX, clientY: e.clientY }); document.dispatchEvent(upEvt); } }); b.onmousedown = (e) => { isDragging = false; dragStartX = e.clientX; dragStartY = e.clientY; const rect = b.getBoundingClientRect(); b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = rect.left + 'px'; b.style.top = rect.top + 'px'; b.style.transition = 'none'; const offsetX = e.clientX - rect.left; const offsetY = e.clientY - rect.top; const onMove = (em) => { if (!isDragging && (Math.abs(em.clientX - dragStartX) > 3 || Math.abs(em.clientY - dragStartY) > 3)) { isDragging = true; } if (isDragging) { b.style.left = (em.clientX - offsetX) + 'px'; b.style.top = (em.clientY - offsetY) + 'px'; } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); b.style.transition = 'transform 0.1s'; if (!isDragging) { if (window.innerWidth < 720 || window.innerHeight < 340) { return; } const existingWin = document.querySelector('.pk-ov'); if (existingWin) { if (existingWin.style.display === 'none') { const currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) return; if (existingWin.querySelector('.pk-win.pk-maximized')) { document.body.classList.add('pk-body-max'); } existingWin.style.display = 'flex'; existingWin.focus(); } else { existingWin.style.display = 'none'; } } else { const currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) return; openManager(globalCache, globalPreloadPromise); } } else { constrainBall(); gmSet('pk_pos_left', b.style.left); gmSet('pk_pos_top', b.style.top); } }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }; document.body.appendChild(b); setTimeout(constrainBall, 0); console.log("🚀 Button Created!"); } const startObserver = () => { if (!document.body) return; const obs = new MutationObserver(() => { if (!document.getElementById('pk-launch')) { tryInject(); } }); obs.observe(document.body, { childList: true, subtree: true }); }; if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', () => { tryInject(); startObserver(); }); } else { tryInject(); startObserver(); } })() ;