// ==UserScript== // @name PikPak 增强大师 // @name:zh-CN PikPak 增强大师 // @name:zh-TW PikPak 增強大師 // @name:en PikPak Enhancement Master // @name:ko PikPak Enhancement Master // @name:ja PikPak Enhancement Master // @name:id PikPak Enhancement Master // @name:ms PikPak Enhancement Master // @namespace https://github.com/digbug82/ // @version 2.3.0 // @author digbug82 // @license CC-BY-NC-SA-4.0 // @description 桌面级PikPak网盘管家!包含Aria2/Motrix带目录结构推送、文件查重(哈希/时长/名称)、文件夹查重(名称/相似度/包含率)、批量重命名(正则替换/连续编号/文本格式化/FC2名称清洗/前缀去广告/后缀智能修复)、清理空文件夹、内置解压密码库的批量解压、夹杂无关文字或“去头”的污染磁链智能识别、自定义资源黑白名单:清理垃圾文件/文件夹、多账号数据迁移、分享提取次数限制、导出目录树等。沉浸式媒体播放引擎:以图搜图、高级字幕加载、跳过片头尾及进度条缩略图预览。叫“增强大师”是有原因的,何不进来看看? // @description:zh-CN 桌面级PikPak网盘管家!包含Aria2/Motrix带目录结构推送、文件查重(哈希/时长/名称)、文件夹查重(名称/相似度/包含率)、批量重命名(正则替换/连续编号/文本格式化/FC2名称清洗/前缀去广告/后缀智能修复)、清理空文件夹、内置解压密码库的批量解压、夹杂无关文字或“去头”的污染磁链智能识别、自定义资源黑白名单:清理垃圾文件/文件夹、多账号数据迁移、分享提取次数限制、导出目录树等。沉浸式媒体播放引擎:以图搜图、高级字幕加载、跳过片头尾及进度条缩略图预览。叫“增强大师”是有原因的,何不进来看看? // @description:zh-TW 桌面級PikPak網盤管家!包含Aria2/Motrix帶目錄結構推送、檔案查重(雜湊/時長/名稱)、資料夾查重(名稱/相似度/包含率)、批次重新命名(正規替換/連續編號/文字格式化/FC2名稱清洗/前綴去廣告/後綴智慧修復)、清理空資料夾、內建解壓縮密碼庫的批次解壓縮、夾雜無關文字或「去頭」的污染磁鏈智慧識別、自訂資源黑白名單:清理垃圾檔案/資料夾、多帳號資料遷移、分享提取次數限制、匯出目錄樹等。沉浸式媒體播放引擎:以圖搜圖、進階字幕載入、跳過片頭尾及進度條縮圖預覽。叫「增強大師」是有原因的,何不進來看看? // @description:en Desktop-grade PikPak manager! Features Aria2/Motrix push with folders, file dedupe (hash/time/name), folder dedupe (name/similarity/inclusion), bulk rename (regex/sequential/format/FC2/ad-free/ext-fix), empty folder pruning, batch extract via password vault, corrupted magnet recognition, custom trash black/whitelists, multi-account migration, share limit restrictions, tree export. Immersive player: image search, advanced subs, intro/outro skip, thumbnails. Why not take a look? // @description:ko 데스크톱급 PikPak 관리자! 구조 유지 Aria2/Motrix 푸시, 파일(해시/시간/이름)·폴더(이름/유사도/포함율) 중복 검사, 일괄 이름 변경(정규식/순번/포맷/FC2/광고제거/확장자복구), 빈 폴더 정리, 내장 암호 일괄 압축 해제, 오염 마그넷 인식, 지정 흑백명단 쓰레기 정리, 다중 계정 마이그레이션, 공유 횟수 제한, 트리 내보내기. 몰입형 플레이어: 이미지 검색, 고급 자막, 오프/엔딩 스킵, 썸네일. "인핸서 마스터"인 이유를 확인해보세요! // @description:ja デスクトップ級PikPakマネージャー!階層保持Aria2/Motrix転送、ファイル(ハッシュ/時間/名前)・フォルダ(名前/類似度/包含率)重複チェック、一括リネーム(正規表現/連番/書式/FC2/広告削除/拡張子修復)、空フォルダ削除、内蔵パス庫の一括解凍、破損マグネット認識、白/黒リストによる不要ファイル整理、複数アカウント移行、共有回数制限、ツリー出力。没入型プレーヤー:画像検索、高度な字幕、OP/EDスキップ、サムネイル。なぜ「拡張マスター」なのか、ぜひお試しを! // @description:id Pengelola PikPak kelas desktop! Mendukung push Aria2/Motrix dengan struktur folder, duplikat file/folder, ganti nama massal, bersih folder kosong, ekstraksi massal dengan vault kata sandi, identifikasi magnet tercemar, daftar hitam/putih kustom, migrasi multi-akun, batas ekstraksi berbagi, ekspor pohon direktori, dan lainnya. Mesin media imersif: cari gambar, subtitle lanjutan, lewati intro/outro, dan pratinjau thumbnail progres. // @description:ms Pengurus PikPak bertaraf desktop! Menyokong push Aria2/Motrix dengan struktur folder, semakan pendua fail/folder, penamaan semula pukal, pembersihan folder kosong, nyahmampat pukal dengan peti kata laluan, pengecaman magnet tercemar, senarai hitam/putih tersuai, migrasi berbilang akaun, had pengekstrakan perkongsian, eksport pepohon direktori dan banyak lagi. Enjin media imersif: carian imej, sari kata lanjutan, langkau intro/outro, dan pratonton lakaran kecil kemajuan. // @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 upload.pikpak.site // @connect *.upload.pikpak.site // @connect whatslink.info // @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 https://update.greasyfork.icu/scripts/570993/PikPak%20%E5%A2%9E%E5%BC%BA%E5%A4%A7%E5%B8%88.user.js // @updateURL https://update.greasyfork.icu/scripts/570993/PikPak%20%E5%A2%9E%E5%BC%BA%E5%A4%A7%E5%B8%88.meta.js // ==/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"; if (window.self !== window.top) return; const parseCloudLinks = (rawString, isSmartFix) => { const formattedVal = String(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 = String(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 = String(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()); }; const NativeTokenSniffer = { init: () => { const inject = () => { const s = document.createElement('script'); s.textContent = `(function(){ const _f = window.fetch; window.fetch = async function(...args) { const url = args[0] ? args[0].toString() : ''; if (url.includes('area_accessible')) { return new Promise((_, reject) => { reject(new Error("Bypassed area_accessible")); }); } const isTurbo = localStorage.getItem('pk_turbo_mode') === 'true'; if (isTurbo) { if (location.href.includes('/login') || location.pathname.includes('login')) { return _f.apply(this, args); } 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; let auth = null; if (opts.headers) { if (opts.headers instanceof Headers) { cap = opts.headers.get('x-captcha-token') || opts.headers.get('X-Captcha-Token'); auth = opts.headers.get('authorization') || opts.headers.get('Authorization'); } else if (typeof opts.headers === 'object') { const capKey = Object.keys(opts.headers).find(k => k.toLowerCase() === 'x-captcha-token'); if (capKey) cap = opts.headers[capKey]; const authKey = Object.keys(opts.headers).find(k => k.toLowerCase() === 'authorization'); if (authKey) auth = opts.headers[authKey]; } } if (cap && cap.length > 20) localStorage.setItem('pk_captured_captcha', cap); if (auth && auth.length > 20) document.dispatchEvent(new CustomEvent('pk-token-captured', { detail: auth })); } catch (e) {} } return _f.apply(this, args); }; const _W = window.Worker; window.Worker = function(url, opts) { const isTurbo = localStorage.getItem('pk_turbo_mode') === 'true'; if (!isTurbo) return new _W(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); }; })()`; (document.head || document.documentElement).appendChild(s).remove(); }; 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; } }); window.pkAddGhostFile = function(id) { try { const ghosts = JSON.parse(localStorage.getItem('pk_ghost_files') || '[]'); if (!ghosts.includes(id)) { ghosts.push(id); localStorage.setItem('pk_ghost_files', JSON.stringify(ghosts)); } } catch(e){} }; window.pkRemoveGhostFile = function(id) { try { const ghosts = JSON.parse(localStorage.getItem('pk_ghost_files') || '[]'); const updated = ghosts.filter(x => x !== id); localStorage.setItem('pk_ghost_files', JSON.stringify(updated)); } catch(e){} }; window.pkCleanupGhostFiles = function() { try { const ghosts = JSON.parse(localStorage.getItem('pk_ghost_files') || '[]'); if (ghosts.length > 0) { console.log(`[GhostCleanup] Found ${ghosts.length} interrupted uploads. Cleaning up...`); const BATCH_SIZE = 100; for (let i = 0; i < ghosts.length; i += BATCH_SIZE) { const chunk = ghosts.slice(i, i + BATCH_SIZE); fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }) }).catch(()=>{}); } localStorage.setItem('pk_ghost_files', '[]'); } } catch(e){} try { const phases = "PHASE_TYPE_UNKNOW,PHASE_TYPE_PENDING,PHASE_TYPE_RUNNING,PHASE_TYPE_PAUSED,PHASE_TYPE_ERROR"; const filters = encodeURIComponent(JSON.stringify({ "phase": { "in": phases } })); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?type=upload&limit=100&filters=${filters}&_t=${Date.now()}`; fetch(url, { headers: getHeaders() }) .then(res => res.json()) .then(cloudData => { const cloudTasks = cloudData.tasks || []; const ghostIds = []; cloudTasks.forEach(ct => { if (ct.file_id) { const ref = ct.reference_resource || {}; const taskName = ref.name || ct.name || ct.file_name || 'Ghost Task'; if (taskName === 'upload' || taskName === 'Ghost Task') { ghostIds.push(ct.file_id); } } }); if (ghostIds.length > 0) { console.log(`[Reconcile] Auto-cleaning orphaned cloud ghost files on startup: ${ghostIds.length} files`); fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ghostIds }) }).catch(()=>{}); } }).catch(()=>{}); } catch (e) {} }; window.addEventListener('beforeunload', () => { const tasks = pkState?.uploadTasks || []; const filesToDelete = tasks.filter(t => t.status !== 'DONE' && t.file_id && !t._deleted).map(t => t.file_id); if (filesToDelete.length > 0) { const BATCH_SIZE = 100; for (let i = 0; i < filesToDelete.length; i += BATCH_SIZE) { const chunk = filesToDelete.slice(i, i + BATCH_SIZE); try { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }), keepalive: true }).catch(()=>{}); } catch(e) {} } if (typeof window.pkRemoveGhostFile === 'function') { filesToDelete.forEach(id => window.pkRemoveGhostFile(id)); } } }); ; const CONF = { rowHeight: 40, buffer: 20, dupGridHeaderHeight: { normal: 40, max: 60 }, dupGridSectionGap: { normal: 14, max: 18 }, dupGridBodyGapY: { normal: 16, max: 20 }, SYSTEM_FOLDER_NAME: 'My Pack', browserDownloadConfirmFileCount: 10, browserDownloadConfirmTotalBytes: 10 * 1024 * 1024 * 1024, uploadPartSizeOfficial: 1 * 1024 * 1024, uploadPartMaxCount: 9999, uploadPartConcurrencyOfficial: 5, mouseSideNavHistoryMax: 50, mouseSideNavDebug: false, clipboardMagnetPaste: true, clipboardMagnetFocusCooldown: 2500, clipboardMagnetDenyCooldown: 60000, clipboardMagnetPromptGap: 10 * 60 * 1000, clipboardMagnetIgnoreTTL: 10 * 60 * 1000, clipboardMagnetMaxChars: 200000, magnetPreviewApi: 'https://whatslink.info/api/v1/link', magnetPreviewTimeout: 9000, magnetPreviewCacheTTL: 60 * 60 * 1000, magnetPreviewErrorCacheTTL: 10 * 60 * 1000, magnetPreviewCircuitTTL: 5 * 60 * 1000, magnetPreviewMaxShots: 5, potplayerLaunchStateKey: 'pk_potplayer_launch_state', potplayerLaunchStateSchemaVersion: 1, potplayerProtocolStateKey: 'pk_potplayer_protocol_state', potplayerProtocolStateSchemaVersion: 1, potplayerProtocolRepairVersion: '20260501', potplayerRegDeleteFileName: 'pikpak-potplayer-delete.reg', potplayerRegCustomInstallFileName: 'pikpak-potplayer-install-custom.reg', potplayerBrowserPolicyRegFileName: 'pikpak-potplayer-browser-policy.reg', potplayerAutoRepairFailThreshold: 2, potplayerPromptCooldown: 10 * 60 * 1000, potplayerLikelyOpenTrustTTL: 24 * 60 * 60 * 1000, potplayerSuppressTodayTTL: 24 * 60 * 60 * 1000, potplayerPostRepairConfirmDelay: 6000, logoSVG: ``, emptySVG: ``, dupHashSVG: ``, dupSimSVG: ``, dupNameSVG: ``, dupContainSVG: ``, crumbIcons: { right: ``, down: ``, sortAZ: ``, sortZA: ``, sortNew: ``, sortOld: ``, sortLarge: ``, sortSmall: ``, sortStarNew: ``, sortStarOld: ``, sortTypeDurAsc: ``, sortTypeDurDesc: ``, sortPathAsc: ``, sortPathDesc: `` }, icons: { offline: ``, navShare: ``, unshare: ``, refresh: ``, retry: ``, settings: ``, home: ``, recent: ``, history: ``, trash: ``, emptyTrash: ``, restore: ``, delForever: ``, newfolder: ``, del: ``, deselect: ``, copy: ``, cut: ``, paste: ``, rename: ``, bulkrename: ``, unzip: ``, migrate: ``, prune: ``, blacklist: ``, invert: ``, folderFirst: ``, viewList: ``, viewGrid: ``, analyze: ``, scanDup: ``, export: ``, stop: ``, ext: ``, download: ``, aria2:``, info: ``, moon: ``, sun: ``, cloudDownload: ``, share: ``, maximize: ``, minimize: ``, close: ``, help: ``, warning: ``, eyeOff: ``, eye: ``, 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-magnet-preview-wrap { width:420px; max-width:86vw; display:flex; flex-direction:column; color:var(--pk-fg); overflow:hidden; } .pk-magnet-hero { width:100%; height:210px; background:var(--pk-hl); display:flex; align-items:center; justify-content:center; overflow:hidden; border-radius:14px 14px 0 0; } .pk-magnet-hero img { width:100%; height:100%; object-fit:cover; display:block; } .pk-magnet-empty { width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:14px; opacity:0.62; } .pk-magnet-body { padding:18px 20px 20px 20px; display:flex; flex-direction:column; gap:12px; } .pk-magnet-title { font-size:16px; font-weight:800; line-height:1.45; overflow:hidden; word-break:break-all; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; text-overflow:ellipsis; } .pk-magnet-desc { font-size:12px; line-height:1.5; opacity:0.68; } .pk-magnet-meta { display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; } .pk-magnet-meta-item { border:1px solid var(--pk-bd); border-radius:10px; padding:8px 9px; background:var(--pk-bg); min-width:0; } .pk-magnet-meta-label { font-size:11px; opacity:0.58; margin-bottom:4px; } .pk-magnet-meta-value { font-size:13px; font-weight:700; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; } .pk-magnet-hash { font-size:11px; line-height:1.45; opacity:0.62; word-break:break-all; background:var(--pk-hl); border-radius:8px; padding:8px 10px; } .pk-magnet-save-row { display:flex; align-items:center; gap:5px; min-width:0; height:26px; margin-top:-2px; font-size:12px; color:var(--pk-fg); } .pk-magnet-save-label { opacity:.72; flex-shrink:0; } .pk-magnet-save-icon { width:16px; height:16px; display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; } .pk-magnet-save-icon svg { width:16px !important; height:16px !important; display:block; } .pk-magnet-save-name { font-weight:700; max-width:150px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pk-magnet-save-help { color:#aaa; display:flex; align-items:center; flex-shrink:0; } .pk-magnet-save-change { color:var(--pk-pri); cursor:pointer; font-size:12px; font-weight:700; flex-shrink:0; } .pk-magnet-save-change:hover { text-decoration:underline; } .pk-magnet-source { font-size:11px; line-height:1.35; opacity:.58; display:flex; align-items:center; gap:4px; } .pk-magnet-source a { color:var(--pk-pri); text-decoration:none; font-weight:700; } .pk-magnet-source a:hover { text-decoration:underline; } .pk-magnet-warn { font-size:12px; line-height:1.45; color:#d93025; background:rgba(217,48,37,0.08); border:1px solid rgba(217,48,37,0.16); border-radius:8px; padding:8px 10px; } .pk-magnet-shots { display:flex; gap:7px; overflow-x:auto; padding-bottom:2px; } .pk-magnet-thumb-wrap { width:68px; height:42px; border-radius:7px; overflow:hidden; flex:0 0 auto; position:relative; border:1px solid var(--pk-bd); cursor:pointer; user-select:none; transition:border-color .16s ease, box-shadow .16s ease; } .pk-magnet-thumb-wrap:hover { } .pk-magnet-thumb-wrap.active { border-color:var(--pk-pri); box-shadow:0 0 0 2px rgba(0,103,192,18); } .pk-magnet-thumb-wrap .pk-magnet-thumb { width:100%; height:100%; border:none; border-radius:0; display:block; object-fit:cover; } .pk-magnet-shot-time { position:absolute; right:3px; bottom:3px; padding:1px 4px; border-radius:4px; background:rgba(0,0,0,.62); color:#fff; font-size:10px; line-height:1.25; pointer-events:none; } .pk-magnet-thumb { width:68px; height:42px; border-radius:7px; object-fit:cover; flex:0 0 auto; border:1px solid var(--pk-bd); cursor:pointer; user-select:none; -webkit-user-drag:none; transition:border-color .16s ease, box-shadow .16s ease; } .pk-magnet-thumb:hover { } .pk-magnet-thumb.active { border-color:var(--pk-pri); box-shadow:0 0 0 2px rgba(0,103,192,18); } .pk-magnet-hero img { user-select:none; -webkit-user-drag:none; } .pk-magnet-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:2px; } .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-copy-success-freeze, .pk-copy-success-freeze:hover, .pk-copy-success-freeze:disabled, .pk-copy-success-freeze:disabled:hover, .pk-btn.pk-copy-success-freeze, .pk-btn.pk-copy-success-freeze:hover, .pk-btn.pk-copy-success-freeze:disabled, .pk-btn.pk-copy-success-freeze:disabled:hover, #pk_err_launch_btn.pk-copy-success-freeze, #pk_err_launch_btn.pk-copy-success-freeze:hover, #pk_err_launch_btn.pk-copy-success-freeze:disabled, #pk_err_launch_btn.pk-copy-success-freeze:disabled:hover { background:#52c41a !important; color:#fff !important; filter:none !important; opacity:1 !important; cursor:default !important; pointer-events:none !important; } .pk-potfix-entry { align-self:flex-start; height:30px; padding:0 2px; border:none; background:transparent; color:var(--pk-pri); font-size:12px; font-weight:700; cursor:pointer; display:inline-flex; align-items:center; justify-content:flex-start; } .pk-potfix-entry:hover { text-decoration:underline; background:transparent !important; } .pk-potfix-desc { white-space: pre-line;line-height:1.62; font-size:12px; color:var(--pk-fg); opacity:0.72; } .pk-potfix-linkbox { width:100%; min-height:72px; max-height:110px; resize:none; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-hl); color:var(--pk-fg); padding:10px 12px; box-sizing:border-box; font-size:12px; line-height:1.45; font-family:"SF Mono","Consolas",monospace; word-break:break-all; outline:none; } .pk-potfix-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:0; } .pk-potfix-actions .pk-btn, .pk-potfix-repair-actions .pk-btn { height:40px; justify-content:center; border-radius:6px; border:1px solid var(--pk-bd); background:var(--pk-bg); color:var(--pk-fg); box-shadow:0 1px 4px rgba(0,0,0,.06); } .pk-potfix-repair-panel { border:1px solid var(--pk-bd); border-radius:6px; background:rgba(var(--pk-bg-rgb),0.76); padding:12px; display:flex; flex-direction:column; gap:10px; flex:0 0 auto; } .pk-potfix-repair-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; } .pk-potfix-secondary { grid-column:1 / -1; position:static; display:flex; justify-content:flex-start; gap:8px; flex-wrap:wrap; margin:0; min-width:0; background:transparent; padding-right:0; } .pk-potfix-secondary .pk-btn { height:30px; padding:0 12px; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); opacity:.72; font-size:12px; box-shadow:0 2px 8px rgba(0,0,0,.08); } .pk-potfix-secondary .pk-btn:hover:not(:disabled) { opacity:1; color:var(--pk-pri); border-color:var(--pk-pri); background:var(--pk-hl) !important; } .pk-potfix-footer { position:relative; display:grid; grid-template-columns:1fr minmax(220px,236px) auto; align-items:center; gap:10px 12px; margin-top:8px; padding:12px 0 2px; border-top:1px solid var(--pk-bd); } .pk-potfix-retry { grid-column:2; width:100%; height:42px !important; border:none !important; border-radius:6px !important; background:var(--pk-pri) !important; color:#fff !important; font-weight:700 !important; justify-content:center !important; box-shadow:0 2px 10px rgba(0,103,192,.28); } .pk-potfix-foot { grid-column:3; display:flex; justify-content:flex-end; min-width:56px; } .pk-potfix-version-note { font-size:12px; line-height:1.45; color:#b26a00; background:rgba(250,173,20,0.10); border:1px solid rgba(250,173,20,0.22); border-radius:8px; padding:8px 10px; } .pk-modal.pk-potfix-modal { overflow-x:hidden !important; overflow-y:auto !important; max-height:92vh !important; align-items:stretch; gap:12px !important; } .pk-modal.pk-potfix-modal > * { flex-shrink:0; } .pk-potfix-adv-desc { font-size:12px; line-height:1.55; color:var(--pk-fg); opacity:.66; } .pk-potfix-path-label { font-size:12px; font-weight:700; color:var(--pk-fg); opacity:.82; } .pk-potfix-path-row { display:grid; grid-template-columns:1fr auto; gap:8px; align-items:center; } .pk-potfix-path-input { height:36px; min-width:0; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); padding:0 10px; font-size:12px; outline:none; box-sizing:border-box; } .pk-potfix-path-input:focus { border-color:var(--pk-pri); box-shadow:0 0 0 2px rgba(0,103,192,.12); } .pk-potfix-path-row .pk-btn { height:34px; border:1px solid var(--pk-bd); border-radius:6px; background:transparent; color:var(--pk-fg); font-size:12px; justify-content:center; opacity:.86; } .pk-potfix-repair-actions .pk-btn:hover:not(:disabled), .pk-potfix-path-row .pk-btn:hover:not(:disabled) { opacity:1; color:var(--pk-pri); border-color:var(--pk-pri); background:var(--pk-hl) !important; } .pk-potfix-status { font-size:12px; line-height:1.45; border-radius:6px; padding:8px 10px; background:var(--pk-hl); color:var(--pk-fg); opacity:.86; } .pk-potfix-status.warn { color:#b26a00; background:rgba(250,173,20,0.10); border:1px solid rgba(250,173,20,0.18); opacity:1; } @media (max-width:520px) { .pk-modal.pk-potfix-modal { width:calc(100vw - 32px) !important; padding:22px !important; } .pk-potfix-actions, .pk-potfix-repair-actions, .pk-potfix-path-row, .pk-potfix-footer { grid-template-columns:1fr; } .pk-potfix-secondary { grid-column:auto; padding-right:0; margin-bottom:2px; } .pk-potfix-retry, .pk-potfix-foot { grid-column:auto; } .pk-potfix-foot { justify-content:center; } } .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: 1360px) { .pk-maximized .pk-btn:not(#pk-btn-folder-first):not(#pk-grid-folder-first):not(#pk-btn-invert):not(#pk-grid-invert):not(#pk-filter-btn):not(#pk-btn-exit):not(#pk-scan-dup):not(#pk-analyze):not(#pk-export):not(#pk-migrate):not(#pk-ext):not(#pk-img-search):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-grid-folder-first):not(#pk-btn-invert):not(#pk-grid-invert):not(#pk-filter-btn):not(#pk-btn-exit):not(#pk-scan-dup):not(#pk-analyze):not(#pk-export):not(#pk-migrate):not(#pk-ext):not(#pk-img-search):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-migrate span, #pk-ext span, #pk-img-search 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-wrap { position: relative; display: inline-flex; align-items: center; width: 220px; min-width: 0; flex-shrink: 0; } #pk-dup-folder-sel { position: absolute !important; inset: 0 !important; width: 0 !important; height: 0 !important; opacity: 0 !important; pointer-events: none !important; } #pk-dup-folder-btn { width: 100%; height: 30px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); padding: 0 28px 0 10px; font-size: 12px; text-align: left; cursor: pointer; position: relative; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #pk-dup-folder-btn:hover { background: var(--pk-btn-hov); border-color: var(--pk-pri); } #pk-dup-folder-btn::after { content: ""; position: absolute; right: 10px; top: 50%; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-65%) rotate(45deg); opacity: 0.7; pointer-events: none; } .pk-dup-folder-pop { position:fixed; min-width:220px; max-width:520px; max-height:320px; background:#ffffff; color:#1f1f1f; border:1px solid rgba(0,0,0,0.12); border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,0.18); z-index:2147483647; overflow:hidden; zoom:var(--pk-zoom,1); display:flex; flex-direction:column; } .pk-dup-folder-pop.pk-dark { background:rgba(28,28,28,0.98); color:#f5f5f5; border-color:rgba(255,255,255,0.12); box-shadow:0 10px 28px rgba(0,0,0,0.45); } .pk-dup-folder-head { flex:0 0 auto; padding:4px; border-bottom:1px solid rgba(0,0,0,0.08); background:#ffffff; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-head { background:rgba(28,28,28,0.98); border-bottom:1px solid rgba(255,255,255,0.10); } .pk-dup-folder-list { flex:1 1 auto; min-height:0; max-height:272px; overflow-y:auto; overflow-x:hidden; padding:4px; } .pk-dup-folder-item { width:100%; min-height:30px; border:0; background:transparent; color:inherit; border-radius:6px; padding:0 10px; font-size:12px; text-align:left; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pk-dup-folder-item:hover { background:rgba(0,0,0,0.06); } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item:hover { background:rgba(255,255,255,0.08); } .pk-dup-folder-item.act { background:rgba(26,115,232,0.12); color:#1a73e8; font-weight:600; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item.act { background:rgba(138,180,248,0.18); color:#8ab4f8; } .pk-dup-folder-item.pk-reset { position:relative; top:auto; z-index:auto; color:#1a73e8; font-weight:700; background:transparent; border-bottom:0; border-radius:6px; } .pk-dup-folder-item.pk-reset:hover { background:#f7f9fc; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item.pk-reset { color:#8ab4f8; background:transparent; border-bottom:0; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item.pk-reset:hover { background:rgba(255,255,255,0.06); } @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-wrap { 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: pointer; } .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-view-switch { display: inline-flex; align-items: center; gap: 4px; margin-left: 12px; padding: 3px; border: 1px solid var(--pk-bd); border-radius: 999px; background: var(--pk-bg); flex-shrink: 0; } .pk-view-switch[style*="display: none"] { margin-left: 0; } .pk-view-btn { width: 34px; height: 28px; border: none; border-radius: 999px; background: transparent; color: #777; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s,color 0.2s,transform 0.2s; padding: 0; } .pk-view-btn:hover { color: var(--pk-fg); background: var(--pk-hl); } .pk-view-btn.active { background: var(--pk-pri); color: #fff; box-shadow: 0 3px 10px rgba(0,103,192,0.22); } .pk-grid-hd.pk-grid-view-hd { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 18px 0 16px; overflow: visible; position: relative; z-index: 30; } .pk-grid-hd.pk-grid-view-hd > div { overflow: visible; min-width: 0; } .pk-grid-hd.pk-grid-view-hd > div:first-child { width: auto; flex: 0 1 auto; } .pk-grid-check-tools { display:flex; align-items:center; gap:10px; min-width:0; flex:0 1 auto; overflow:visible !important; padding-left:9px; box-sizing:border-box; } .pk-grid-check-tools > input[type="checkbox"] { flex:0 0 auto; margin:0; } .pk-grid-check-tools > #pk-grid-folder-first { flex:0 0 auto; } .pk-grid-check-tools { display:flex; align-items:center; gap:8px; min-width:0; overflow:visible !important; padding-left:9px; box-sizing:border-box; } .pk-grid-sort-wrap { position: relative; display: inline-flex; align-items: center; flex: 0 0 auto; flex-shrink: 0; overflow: visible !important; z-index: 2; } .pk-grid-sort-trigger { height: 30px; border: 1px solid var(--pk-bd); border-radius: 999px; background: var(--pk-bg); color: var(--pk-fg); padding: 0 12px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer; font-size: 12px; font-weight: 600; transition: border-color 0.2s,background 0.2s,color 0.2s; max-width: 100%; } .pk-grid-sort-trigger:hover, .pk-grid-sort-wrap.open .pk-grid-sort-trigger { border-color: var(--pk-pri); color: var(--pk-pri); background: var(--pk-hl); } .pk-grid-sort-trigger svg { flex-shrink: 0; } .pk-grid-sort-trigger span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .pk-grid-sort-menu { position: absolute; top: calc(100% + 8px); right: 0; min-width: 156px; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 12px; box-shadow: 0 12px 36px rgba(0,0,0,0.16); padding: 6px; display: none; z-index: 10020; } .pk-grid-sort-wrap.open .pk-grid-sort-menu { display: block; } .pk-grid-sort-opt { border-radius: 10px; padding: 9px 12px; display: flex; align-items: center; gap: 10px; color: var(--pk-fg); cursor: pointer; transition: background 0.15s,color 0.15s; font-size: 13px; font-weight: 500; white-space: nowrap; } .pk-grid-sort-opt:hover, .pk-grid-sort-opt.active { background: var(--pk-hl); color: var(--pk-pri); } .pk-grid-view .pk-vp { padding: 14px 0 18px; } .pk-grid-view .pk-in { left: 0; right: 0; } .pk-view-switching .pk-row, .pk-view-switching .pk-row::before, .pk-view-switching .pk-row::after, .pk-view-switching .pk-row *, .pk-grid-resizing .pk-row, .pk-grid-resizing .pk-row::before, .pk-grid-resizing .pk-row::after, .pk-grid-resizing .pk-row *, .pk-grid-scrolling .pk-row, .pk-grid-scrolling .pk-row::before, .pk-grid-scrolling .pk-row::after, .pk-grid-scrolling .pk-row * { transition: none !important; animation: none !important; } .pk-view-switching.pk-grid-view .pk-row, .pk-view-switching.pk-grid-view .pk-row:hover, .pk-view-switching.pk-grid-view .pk-row.sel, .pk-view-switching.pk-grid-view .pk-row.sel.pk-focused, .pk-dark .pk-view-switching.pk-grid-view .pk-row, .pk-dark .pk-view-switching.pk-grid-view .pk-row:hover, .pk-dark .pk-view-switching.pk-grid-view .pk-row.sel, .pk-dark .pk-view-switching.pk-grid-view .pk-row.sel.pk-focused, .pk-grid-resizing.pk-grid-view .pk-row, .pk-grid-resizing.pk-grid-view .pk-row:hover, .pk-grid-resizing.pk-grid-view .pk-row.sel, .pk-grid-resizing.pk-grid-view .pk-row.sel.pk-focused, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row:hover, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row.sel, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row.sel.pk-focused { border-color: transparent !important; box-shadow: none !important; outline: none !important; } .pk-grid-view .pk-row { box-sizing: border-box; border-radius: 16px; border: 1px solid transparent; background: #e3e9f2; box-shadow: none; overflow: hidden; transition: background 0.2s, border-color 0.2s, box-shadow 0.2s; } .pk-grid-view .pk-row:hover { background: #d9e1ec; transform: none; box-shadow: none; } .pk-grid-view .pk-row.sel { background: #d4deeb; border-color: rgba(0,103,192,0.15); box-shadow: 0 0 0 1px rgba(0,103,192,0.15); } .pk-grid-view .pk-row.sel.pk-focused { border-color: var(--pk-pri); box-shadow: 0 0 0 1px var(--pk-pri); } .pk-grid-scrolling.pk-grid-view .pk-row.pk-focused:not(.sel), .pk-dark .pk-grid-scrolling.pk-grid-view .pk-row.pk-focused:not(.sel) { background: var(--pk-sel-bg) !important; border-color: var(--pk-pri) !important; box-shadow: none !important; outline: none !important; } .pk-grid-card-body { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; box-sizing: border-box; padding: 10px; gap: 10px; } .pk-gv-check { position: absolute; top: 12px; left: 12px; width: 24px; height: 24px; background: #fff; border-radius: 6px; z-index: 4; display: flex; align-items: center; justify-content: center; cursor: pointer; box-sizing: border-box; opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(-2px); transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease; } .pk-gv-check::before { content: ''; position: absolute; left: 50%; top: 50%; width: 18px; height: 18px; margin-left: -9px; margin-top: -9px; border-radius: 50%; border: 1.5px solid #c7cdd6; background: transparent; box-sizing: border-box; transition: all 0.15s; } .pk-gv-check::after { content: ''; position: absolute; left: 50%; top: 50%; width: 4px; height: 8px; margin-left: -2px; margin-top: -5px; border: solid #fff; border-width: 0 1.5px 1.5px 0; opacity: 0; transform: rotate(45deg) scale(0.8); transform-origin: center; transition: opacity 0.15s, transform 0.15s; box-sizing: border-box; } .pk-grid-view .pk-row:hover .pk-gv-check, .pk-grid-view .pk-row.sel .pk-gv-check, .pk-grid-view .pk-row:focus-within .pk-gv-check { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); } .pk-grid-view .pk-row:hover .pk-gv-check::before { border-color: #818c9b; } .pk-grid-view .pk-row.sel .pk-gv-check::before { background: var(--pk-pri); border-color: var(--pk-pri); } .pk-grid-view .pk-row.sel .pk-gv-check::after { opacity: 1; transform: rotate(45deg) scale(1); } .pk-grid-view .pk-row input[type="checkbox"] { position: absolute; inset: 0; width: 100% !important; height: 100% !important; margin: 0 !important; opacity: 0; cursor: pointer; z-index: 2; } .pk-gv-more { position: absolute; top: 12px; right: 12px; width: 24px; height: 24px; border: none; border-radius: 6px; background: #fff; color: #8a94a4; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; z-index: 5; padding: 0; opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(-2px); transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease, color 0.2s; } .pk-grid-view .pk-row:hover .pk-gv-more, .pk-grid-view .pk-row:focus-within .pk-gv-more, .pk-grid-view .pk-row.sel.pk-focused .pk-gv-more, .pk-grid-view .pk-row.pk-sel-single .pk-gv-more { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); } .pk-gv-more:hover { color: var(--pk-pri); } .pk-gv-more svg { width: 16px; height: 16px; flex-shrink: 0; } .pk-gv-cover { position: relative; height: calc(100% - 56px); min-height: 140px; background: transparent; display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 12px; } .pk-gv-cover:not(.pk-gv-file) { align-items: flex-end; padding-bottom: 8px; box-sizing: border-box; } .pk-gv-cover.pk-gv-file { width: auto; max-width: 100%; aspect-ratio: 1 / 1; align-self: center; border-radius: 18px; flex: 0 0 auto; isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount { width: 100%; height: 100%; display: block; position: relative; isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount > div { width: 100%; height: 100%; position: relative; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount .pk-max-thumb { width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important; object-fit: cover !important; border-radius: 0 !important; transform: translateZ(0); transform-origin: center center; backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform, opacity; } .pk-gv-cover.pk-gv-has-thumb { background: transparent; } .pk-gv-cover img { max-width: 100%; max-height: 100%; object-fit: cover; display: block; border-radius: 12px; } .pk-gv-cover.pk-gv-has-thumb > img { width: 100%; height: 100%; } .pk-gv-cover.pk-gv-blur > img, .pk-gv-cover.pk-gv-blur .pk-gv-media-mount [data-pk-thumb-ready="1"] > .pk-max-thumb, .pk-gv-cover.pk-gv-blur .pk-gv-media-mount .pk-max-thumb[data-pk-frozen-cover="1"], .pk-gv-cover.pk-gv-blur .pk-gv-folder-preview img[data-pk-id][data-pk-src], .pk-gv-cover.pk-gv-blur .pk-gv-folder-preview img[data-pk-frozen-cover="1"] { filter: blur(8px); transform: scale(1.04); } .pk-gv-cover.pk-gv-file .pk-gv-media-mount { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount > div { width: 100%; height: 100%; } .pk-gv-cover .pk-gv-icon { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } .pk-gv-cover .pk-gv-icon svg, .pk-gv-cover .pk-gv-icon img { width: 108px !important; height: 108px !important; max-width: 108px !important; max-height: 108px !important; object-fit: contain !important; } .pk-gv-cover .pk-gv-icon img { border-radius: 10px; } .pk-gv-folder-preview.pk-gv-folder-has-thumb { background: transparent; isolation: isolate; transform: translateZ(0); } .pk-gv-folder-preview.pk-gv-folder-has-thumb img { width: 100%; height: 100%; display: block; object-fit: cover; border-radius: 0; transform: translateZ(0) scale(1.02); transform-origin: center center; backface-visibility: hidden; will-change: transform; } .pk-gv-folder-shell { position: relative; width: 176px; height: 138px; margin: 12px auto 0; display: flex; align-items: flex-end; justify-content: center; transform: translateY(12px); } .pk-gv-folder-back { position: absolute; top: 19px; left: 0; right: 0; bottom: 0; background: #fbc645; border-radius: 12px; } .pk-gv-folder-tab { position: absolute; top: 6px; left: 15px; width: 62px; height: 23px; background: #fbc645; border-radius: 9px 9px 0 0; } .pk-gv-folder-preview { position: absolute; top: 0; left: 15px; right: 15px; bottom: 23px; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #fff7e6; z-index: 1; display: flex; align-items: center; justify-content: center; isolation: isolate; transform: translateZ(0); } .pk-gv-folder-preview { position: absolute; top: 0; left: 15px; right: 15px; bottom: 23px; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #fff7e6; z-index: 1; display: flex; align-items: center; justify-content: center; isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-folder-fallback { width: 102px !important; height: 102px !important; max-width: 102px !important; max-height: 102px !important; object-fit: contain !important; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; } .pk-gv-folder-front { position: absolute; left: -1px; right: -1px; bottom: -1px; height: 86px; background: #f9b126; border-radius: 12px; z-index: 2; box-shadow: 0 -2px 12px rgba(249, 177, 38, 0.2); transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-folder-shell { isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; } .pk-gv-folder-back, .pk-gv-folder-tab { transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; } .pk-gv-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 48px; height: 48px; border-radius: 50%; background: rgba(0,0,0,0.4); display: inline-flex; align-items: center; justify-content: center; pointer-events: none; z-index: 3; } .pk-gv-play svg { width: 28px !important; height: 28px !important; color: #fff; margin-left: 3px; } .pk-gv-fav { position: absolute; right: 14px; bottom: 14px; width: 24px; height: 24px; border-radius: 50%; background: rgba(255,255,255,0.95); color: #f4b400; display: grid; place-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.1); pointer-events: none; z-index: 3; line-height: 0; } .pk-dark .pk-gv-fav { background: rgba(39,45,54,0.96); box-shadow: 0 6px 14px rgba(0,0,0,0.24); } .pk-gv-fav .pk-star-icon { width: 14px !important; height: 14px !important; display: block !important; margin: 0 !important; padding: 0 !important; transform: none !important; transform-origin: center center !important; } .pk-gv-fav .pk-star-icon svg { width: 14px !important; height: 14px !important; display: block !important; margin: 0 !important; padding: 0 !important; transform: none !important; } .pk-gv-bl { position: absolute; left: 14px; bottom: 14px; width: 24px; height: 24px; border-radius: 50%; background: rgba(255,255,255,0.95); display: grid; place-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.1); pointer-events: none; z-index: 3; line-height: 0; } .pk-gv-bl svg { width: 14px !important; height: 14px !important; display: block !important; margin: 0 !important; padding: 0 !important; transform: none !important; transform-origin: center center !important; } .pk-dark .pk-gv-bl { background: rgba(39,45,54,0.96); box-shadow: 0 6px 14px rgba(0,0,0,0.24); } .pk-gv-info { display:grid; grid-template-rows:20px 16px; row-gap:2px; align-content:end; align-self:stretch; width:100%; min-width:0; padding:0 2px; height:38px; min-height:38px; box-sizing:border-box; overflow:hidden; text-align:left; } .pk-grid-view .pk-name { display:flex; align-items:center; justify-content:flex-start; align-self:stretch; width:100%; min-width:0; height:20px; overflow:hidden; text-align:left; } .pk-grid-view .pk-name .pk-name-main { display:inline-flex; align-items:center; min-width:0; max-width:100%; overflow:hidden; } .pk-grid-view .pk-name .pk-name-txt { display:block; flex:0 1 auto; width:auto; min-width:0; max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; line-height:20px; font-size:14px; font-weight:500; color:#1a1a1a; word-break:normal; text-align:left; } .pk-grid-view .pk-name .pk-tag-default { flex:0 0 auto; align-self:center; margin-left:6px; margin-top:0 !important; margin-bottom:0 !important; padding:0 6px; min-width:auto; height:16px; border-radius:10px; font-size:11px; line-height:1 !important; white-space:nowrap; display:inline-flex !important; align-items:center !important; justify-content:center !important; text-align:center; box-sizing:border-box; vertical-align:middle; transform:none !important; position:relative; top:-2px; } .pk-gv-meta { display:flex; align-items:center; justify-content:flex-start; align-self:stretch; width:100%; min-width:0; height:16px; gap:0; color:#1a1a1a; font-size:12px; line-height:16px; overflow:hidden; text-align:left; } .pk-gv-meta-item { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:0 1 auto; text-align:left; } .pk-gv-date-wrap { display:flex; align-items:center; min-width:0; flex:0 1 auto; overflow:hidden; gap:0; } .pk-gv-date { flex:0 1 auto; min-width:0; text-align:left; } .pk-gv-size { flex:0 0 auto; max-width:40%; text-align:left; } .pk-gv-dur { flex:0 0 auto; max-width:34%; text-align:left; } .pk-gv-fcount { flex:0 0 auto; max-width:34%; text-align:left; } .pk-gv-meta-sep { display:block; width:3px; height:3px; margin:0 6px; border-radius:50%; background:currentColor; opacity:0.5; flex:0 0 auto; align-self:center; } .pk-gv-date, .pk-gv-size, .pk-gv-dur, .pk-gv-fcount { color: inherit; opacity: 1; } .pk-dark .pk-grid-view .pk-row { background: rgba(39,45,54,0.96); border-color: transparent; box-shadow: 0 12px 26px rgba(0,0,0,0.22); outline: none; background-clip: padding-box; } .pk-dark .pk-grid-view .pk-row:hover { background: rgba(45,52,62,0.98); border-color: transparent; outline: none; } .pk-dark .pk-grid-view .pk-row.sel { background: rgba(34,60,96,0.9); border-color: rgba(92,169,255,0.42); box-shadow: 0 12px 28px rgba(18,44,82,0.32); outline: none; background-clip: padding-box; } .pk-dark .pk-grid-view .pk-row.sel:not(.pk-sel-single), .pk-dark .pk-grid-view .pk-row.sel:not(.pk-sel-single):hover, .pk-dark .pk-grid-view .pk-row.sel:not(.pk-sel-single).pk-focused { transition: none !important; animation: none !important; } .pk-dark .pk-grid-view .pk-row.pk-sel-single, .pk-dark .pk-grid-view .pk-row.pk-sel-single:hover, .pk-dark .pk-grid-view .pk-row.pk-sel-single.pk-focused { transition: none !important; animation: none !important; } .pk-dark .pk-gv-check::before { background: rgba(37,42,50,0.92); border-color: rgba(124,137,156,0.9); } .pk-dark .pk-gv-more { background: rgba(34,40,48,0.94); color: #c0c7d2; box-shadow: 0 6px 14px rgba(0,0,0,0.24); } .pk-dark .pk-gv-more:hover { background: rgba(46,54,64,0.98); color: #fff; } .pk-dark .pk-gv-cover.pk-gv-file { background: transparent; } .pk-dark .pk-gv-cover.pk-gv-file.pk-gv-has-thumb:has(.pk-gv-media-mount > [data-pk-thumb-ready="1"]) { background: linear-gradient(180deg,#313844 0%,#2a313b 100%); } .pk-dark .pk-gv-cover.pk-gv-folder { background: transparent; } .pk-dark .pk-gv-folder-preview { background: #fff7e6; } .pk-dark .pk-grid-view .pk-name .pk-name-txt, .pk-dark .pk-gv-meta, .pk-dark .pk-gv-meta-item, .pk-dark .pk-gv-date, .pk-dark .pk-gv-size { color:#fff; } .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-grid-view .pk-row.sel.pk-focused { border-color: var(--pk-pri); box-shadow: 0 0 0 1px var(--pk-pri); border-radius: 16px; } .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-group-hd .pk-name { height:100%; align-items:center !important; } .pk-group-hd .pk-name .pk-name-txt, .pk-group-hd .pk-name span { display:flex !important; align-items:center !important; height:100% !important; line-height:1.2 !important; padding:0 !important; margin-top:0 !important; } .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; overscroll-behavior: none; overflow: hidden; padding: 20px; box-sizing: border-box; } .pk-modal { position: relative; background: var(--pk-bg); padding: 25px; border-radius: 12px; width: 500px; max-height: 100%; 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; margin: auto; flex-shrink: 1; } .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: 16px !important; height: 16px !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-ph { position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; overflow:hidden; background:#3a3a3a; } .pk-p-plist-ph img, .pk-p-plist-ph svg { width:100% !important; height:100% !important; max-width:none !important; max-height:none !important; display:block; border-radius:0 !important; } .pk-p-plist-ph img { object-fit:cover !important; } .pk-p-plist-ph svg { object-fit:fill !important; } .pk-p-plist-item > img { position:relative; z-index:2; width:100%; height:100%; object-fit:cover; opacity:1; transition:opacity 0.3s ease-in-out; display:block; } .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; justify-content: flex-start; gap: 8px; color: var(--pk-fg); } .pk-crumb-item:hover { background: var(--pk-hl); } .pk-crumb-item.pk-moving { opacity: 0.4; filter: grayscale(1); cursor: wait; pointer-events: none; } .pk-crumb-item svg { flex-shrink: 0; } .pk-crumb-name-wrap { display: inline-flex; align-items: center; justify-content: flex-start; min-width: 0; max-width: calc(100% - 26px); flex: 0 1 auto; overflow: hidden; } .pk-crumb-name { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; padding-bottom: 2px; margin-bottom: -2px; } .pk-crumb-name-wrap .pk-tag-default { flex: 0 0 auto !important; margin-left: 6px; min-width: auto; height: 16px; padding: 0 6px; line-height: 1 !important; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap !important; overflow: visible !important; text-overflow: clip !important; padding-bottom: 0 !important; margin-bottom: 0 !important; } @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_edit_pwd_cancel, #pk_edit_phrase_cancel, #pk_mod_cnt_cancel { display:inline-flex; align-items:center; justify-content:center; min-width:64px; height:32px; padding:0 14px; border-radius:8px; color:#666; transition:background 0.18s ease, box-shadow 0.18s ease, color 0.18s ease; } .pk-modal-ov:not(.pk-dark) #pk_edit_pwd_cancel:hover, .pk-modal-ov:not(.pk-dark) #pk_edit_phrase_cancel:hover, .pk-modal-ov:not(.pk-dark) #pk_mod_cnt_cancel:hover { background:rgba(0,0,0,0.06); box-shadow:0 2px 8px rgba(0,0,0,0.08); color:#444; } .pk-modal-ov.pk-dark #pk_edit_pwd_cancel:hover, .pk-modal-ov.pk-dark #pk_edit_phrase_cancel:hover, .pk-modal-ov.pk-dark #pk_mod_cnt_cancel:hover { background:rgba(255,255,255,0.08); box-shadow:0 2px 8px rgba(0,0,0,0.22); color:#ddd; } .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; display:flex; align-items:center; justify-content:center; line-height:0; overflow:visible; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; margin:0 !important; } .pk-share-icon-wrap > div:not(.pk-share-lock) { width:100%; height:100%; display:flex; align-items:center; justify-content:center; margin:0 !important; flex:0 0 100%; } .pk-share-icon-wrap > div:not(.pk-share-lock) > svg, .pk-share-icon-wrap > svg { width:100%; height:100%; display:block !important; margin:0 !important; transform:scale(0.94) !important; transform-origin:center center !important; } .pk-share-icon-disabled { transform:none !important; } .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-down svg { width: 16px !important; height: 16px !important; display: block !important; transform: none !important; transform-origin: center center !important; shape-rendering: geometricPrecision; } #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:not(.pk-grid-view) .pk-row, .pk-maximized:not(.pk-grid-view) .pk-group-hd { height: 60px !important; font-size: 16px !important; padding: 0 20px !important; } .pk-maximized:not(.pk-grid-view) .pk-group-hd { border-top-width: 10px !important; border-bottom-width: 10px !important; } .pk-maximized.pk-grid-view .pk-group-hd { height: 60px !important; font-size: 16px !important; padding: 0 20px !important; border-top-width: 10px !important; border-bottom-width: 10px !important; box-sizing: border-box !important; align-items: center !important; } .pk-maximized.pk-grid-view .pk-group-hd > div { height: 100% !important; display: flex !important; align-items: center !important; } .pk-maximized.pk-grid-view .pk-group-hd .pk-name { height: 100% !important; display: flex !important; align-items: center !important; min-height: 0 !important; } .pk-maximized.pk-grid-view .pk-group-hd .pk-name .pk-name-txt, .pk-maximized.pk-grid-view .pk-group-hd .pk-name span { display: inline-flex !important; align-items: center !important; height: 100% !important; line-height: 1 !important; padding-top: 0 !important; padding-bottom: 0 !important; margin-top: 0 !important; } .pk-maximized:not(.pk-grid-view) .pk-name svg { width: 60px !important; height: 60px !important; margin-right: 20px !important; } .pk-maximized:not(.pk-grid-view) .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-maximized.pk-grid-view .pk-row { padding: 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-view-switch { padding: 4px !important; } .pk-maximized .pk-view-btn { width: 38px !important; height: 32px !important; } .pk-maximized .pk-grid-hd { height: 58px !important; } .pk-maximized .pk-grid-hd.pk-grid-view-hd { padding: 0 24px 0 20px !important; gap: 14px !important; overflow: visible !important; } .pk-maximized .pk-grid-check-tools { padding-left: 5px !important; box-sizing: border-box !important; } .pk-maximized .pk-grid-sort-trigger { height: 36px !important; font-size: 13px !important; padding: 0 14px !important; min-width: 122px !important; } .pk-maximized .pk-grid-view .pk-row { border-radius: 22px !important; } .pk-maximized .pk-grid-card-body { padding: 12px 12px 14px !important; gap: 12px !important; } .pk-maximized .pk-gv-check { top: 18px !important; left: 18px !important; width: 22px !important; height: 22px !important; } .pk-maximized .pk-gv-more { top: 16px !important; right: 16px !important; width: 28px !important; height: 28px !important; } .pk-maximized .pk-gv-cover { height: calc(100% - 86px) !important; min-height: 180px !important; border-radius: 18px !important; } .pk-maximized .pk-gv-cover .pk-gv-icon svg, .pk-maximized .pk-gv-cover .pk-gv-icon img { width: 122px !important; height: 122px !important; max-width: 122px !important; max-height: 122px !important; } .pk-maximized .pk-gv-folder-shell { width: 216px !important; max-width: 216px !important; height: 152px !important; min-height: 152px !important; margin-top: 12px !important; transform: translateY(12px) !important; } .pk-maximized .pk-gv-folder-back { top: 19px !important; border-radius: 12px !important; } .pk-maximized .pk-gv-folder-tab { top: 6px !important; left: 17px !important; width: 68px !important; height: 25px !important; border-radius: 10px 10px 0 0 !important; } .pk-maximized .pk-gv-folder-preview { top: 0 !important; left: 17px !important; right: 17px !important; bottom: 25px !important; border-radius: 10px !important; } .pk-maximized .pk-gv-folder-fallback, .pk-maximized .pk-gv-folder-fallback svg { width: 116px !important; height: 116px !important; max-width: 116px !important; max-height: 116px !important; } .pk-maximized .pk-gv-folder-front { height: 90px !important; border-radius: 12px !important; } .pk-maximized .pk-gv-info { grid-template-rows:22px 18px !important; row-gap:4px !important; align-self:stretch !important; width:100% !important; min-width:0 !important; height:44px !important; min-height:44px !important; padding:0 2px !important; text-align:left !important; } .pk-maximized.pk-grid-view .pk-name .pk-name-txt { font-size:16px !important; line-height:22px !important; text-align:left !important; } .pk-maximized.pk-grid-view .pk-name { display:flex !important; align-items:center !important; justify-content:flex-start !important; align-self:stretch !important; width:100% !important; min-width:0 !important; height:22px !important; overflow:hidden !important; text-align:left !important; } .pk-maximized.pk-grid-view .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-grid-view .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-grid-view .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:not(.pk-grid-view) .pk-name { display:flex !important; align-items:center !important; min-width:0 !important; width:100% !important; height:auto !important; overflow:visible !important; text-align:left !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap { position:relative !important; width:50px !important; height:50px !important; min-width:50px !important; min-height:50px !important; margin-right:20px !important; display:flex !important; align-items:center !important; justify-content:center !important; flex:0 0 50px !important; transform:none !important; overflow:visible !important; background:transparent !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap > svg { width:100% !important; height:100% !important; min-width:100% !important; min-height:100% !important; margin:0 !important; transform:scale(1.16) !important; transform-origin:center center !important; } .pk-maximized:not(.pk-grid-view) .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:not(.pk-grid-view) .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.16) !important; transform-origin:center center !important; margin:0 !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap img { width:100% !important; height:100% !important; min-width:100% !important; min-height:100% !important; object-fit:contain !important; margin:0 !important; transform:none !important; } .pk-maximized:not(.pk-grid-view) .pk-share-lock { width:18px !important; height:18px !important; bottom:-2px !important; right:-6px !important; z-index:15 !important; } .pk-maximized:not(.pk-grid-view) .pk-share-lock svg { width:100% !important; height:100% !important; min-width:100% !important; min-height:100% !important; margin:0 !important; transform:none !important; display:block !important; color:#fff !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-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-token-eye { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 28px; height: 28px; border: none; border-radius: 6px; background: transparent; color: #8a94a4; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; transition: background 0.15s, color 0.15s; z-index: 20; } .pk-token-eye:hover { background: rgba(0,103,192,0.1); color: var(--pk-pri); } .pk-token-eye svg { width: 18px; height: 18px; display: block; } .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; display:flex; align-items:center; justify-content:center; transform:none !important; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; } .pk-share-icon-wrap > div:not(.pk-share-lock) { width:100%; height:100%; display:flex; align-items:center; justify-content:center; transform:none !important; margin:0 !important; } .pk-share-icon-wrap > div:not(.pk-share-lock) > svg, .pk-share-icon-wrap > svg { transform:scale(0.94) !important; transform-origin:center center !important; } .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)); function getLogicalRect(el) { const rect = el.getBoundingClientRect(); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; if (scale === 1) return rect; if (window._isRectScaledByZoom === undefined) { const dummy = document.createElement('div'); dummy.style.cssText = 'position:fixed;top:0;left:0;width:100px;height:100px;zoom:0.5;pointer-events:none;opacity:0;border:none;margin:0;padding:0;'; document.body.appendChild(dummy); const dRect = dummy.getBoundingClientRect(); document.body.removeChild(dummy); window._isRectScaledByZoom = Math.abs(dRect.width - 50) < 1; } if (window._isRectScaledByZoom) { return { top: rect.top / scale, right: rect.right / scale, bottom: rect.bottom / scale, left: rect.left / scale, width: rect.width / scale, height: rect.height / scale }; } return rect; } 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); } function getDefaultPotPlayerProtocolState() { return { schemaVersion: CONF.potplayerProtocolStateSchemaVersion, repairVersion: "", confirmedRepairVersion: "", installRegDownloadedAt: 0, deleteRegDownloadedAt: 0, browserPolicyRegDownloadedAt: 0, customPlayerPath: "", customPlayerPathUpdatedAt: 0, pathBoundRepairVersion: "", userConfirmedFixedAt: 0, lastPromptAt: 0, neverAutoPrompt: false, suppressUntil: 0 }; } function normalizePotPlayerProtocolState(rawState) { const def = getDefaultPotPlayerProtocolState(); let data = rawState; if (typeof data === 'string') { try { data = data ? JSON.parse(data) : null; } catch (e) { data = null; } } if (!data || typeof data !== 'object' || Array.isArray(data)) return def; const toTime = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 0; }; return { schemaVersion: CONF.potplayerProtocolStateSchemaVersion, repairVersion: typeof data.repairVersion === 'string' ? data.repairVersion : "", confirmedRepairVersion: typeof data.confirmedRepairVersion === 'string' ? data.confirmedRepairVersion : "", installRegDownloadedAt: toTime(data.installRegDownloadedAt), deleteRegDownloadedAt: toTime(data.deleteRegDownloadedAt), browserPolicyRegDownloadedAt: toTime(data.browserPolicyRegDownloadedAt), customPlayerPath: typeof data.customPlayerPath === 'string' ? data.customPlayerPath : "", customPlayerPathUpdatedAt: toTime(data.customPlayerPathUpdatedAt), pathBoundRepairVersion: typeof data.pathBoundRepairVersion === 'string' ? data.pathBoundRepairVersion : "", userConfirmedFixedAt: toTime(data.userConfirmedFixedAt), lastPromptAt: toTime(data.lastPromptAt), neverAutoPrompt: data.neverAutoPrompt === true, suppressUntil: toTime(data.suppressUntil) }; } function readPotPlayerProtocolState() { try { return normalizePotPlayerProtocolState(gmGet(CONF.potplayerProtocolStateKey, '')); } catch (e) { return getDefaultPotPlayerProtocolState(); } } function writePotPlayerProtocolState(state) { try { gmSet(CONF.potplayerProtocolStateKey, normalizePotPlayerProtocolState(state)); } catch (e) {} } function updatePotPlayerProtocolState(patch = {}) { const state = readPotPlayerProtocolState(); Object.assign(state, patch, { repairVersion: CONF.potplayerProtocolRepairVersion }); writePotPlayerProtocolState(state); return state; } function recordPotPlayerProtocolRegDownload(kind) { const now = Date.now(); let patch; if (kind === 'delete') patch = { deleteRegDownloadedAt: now }; else if (kind === 'browser_policy') patch = { browserPolicyRegDownloadedAt: now }; else patch = { installRegDownloadedAt: now }; return updatePotPlayerProtocolState(patch); } function recordPotPlayerProtocolUserFixed() { const now = Date.now(); const state = readPotPlayerProtocolState(); const patch = { confirmedRepairVersion: CONF.potplayerProtocolRepairVersion, userConfirmedFixedAt: now }; if (state.customPlayerPath) { patch.pathBoundRepairVersion = CONF.potplayerProtocolRepairVersion; } return updatePotPlayerProtocolState(patch); } function recordPotPlayerProtocolAutoPrompt() { return updatePotPlayerProtocolState({ lastPromptAt: Date.now() }); } function suppressPotPlayerAutoPromptToday() { return updatePotPlayerProtocolState({ suppressUntil: Date.now() + CONF.potplayerSuppressTodayTTL }); } function disablePotPlayerAutoPrompt() { return updatePotPlayerProtocolState({ neverAutoPrompt: true }); } function shouldAutoPromptPotPlayerRepair(playUrl) { const cleanUrl = String(playUrl || '').trim(); if (!cleanUrl) return false; const now = Date.now(); const launchState = readPotPlayerLaunchState(); const protocolState = readPotPlayerProtocolState(); if (launchState.consecutiveFailCount < CONF.potplayerAutoRepairFailThreshold) return false; if (launchState.lastLikelyOpenAt > 0 && now - launchState.lastLikelyOpenAt < CONF.potplayerLikelyOpenTrustTTL) return false; if (protocolState.suppressUntil > now) return false; if (protocolState.neverAutoPrompt === true) return false; if (protocolState.lastPromptAt > 0 && now - protocolState.lastPromptAt < CONF.potplayerPromptCooldown) return false; return true; } function schedulePotPlayerAutoRepairPrompt(playUrl, opts = {}) { const cleanUrl = String(playUrl || '').trim(); if (!cleanUrl) return; if (opts.autoRepairPrompt === false) return; const source = normalizePotPlayerLaunchSource(opts.source); const autoPromptDelay = Number.isFinite(Number(opts.autoRepairPromptDelay)) ? Math.max(0, Number(opts.autoRepairPromptDelay)) : 180; setTimeout(() => { if (!shouldAutoPromptPotPlayerRepair(cleanUrl)) return; recordPotPlayerProtocolAutoPrompt(); openPotPlayerProtocolRepairHelper(cleanUrl, { source, autoPrompt: true, button: null }); }, autoPromptDelay); } function clearPotPlayerLaunchFailCount() { const state = readPotPlayerLaunchState(); state.consecutiveFailCount = 0; writePotPlayerLaunchState(state); return state; } function escapeRegString(value) { return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } function normalizePotPlayerCustomPath(input) { return String(input || '').trim().replace(/^["']+|["']+$/g, '').trim(); } function validatePotPlayerCustomPath(input) { const path = normalizePotPlayerCustomPath(input); if (!path) return { ok: false, path }; if (!/\.exe$/i.test(path)) return { ok: false, path }; if (!(/[A-Za-z]:\\/.test(path) || path.includes('\\'))) return { ok: false, path }; if (/[\r\n]/.test(path)) return { ok: false, path }; return { ok: true, path }; } function savePotPlayerCustomPath(input) { const checked = validatePotPlayerCustomPath(input); if (!checked.ok) return { ok: false, changed: false, path: checked.path }; const state = readPotPlayerProtocolState(); const oldPath = state.customPlayerPath || ""; const changed = oldPath !== checked.path; const patch = { customPlayerPath: checked.path }; if (changed) { patch.customPlayerPathUpdatedAt = Date.now(); patch.confirmedRepairVersion = ""; patch.pathBoundRepairVersion = ""; } updatePotPlayerProtocolState(patch); return { ok: true, changed, wasConfigured: !!oldPath, path: checked.path }; } function getValidSavedPotPlayerPath() { const state = readPotPlayerProtocolState(); const checked = validatePotPlayerCustomPath(state.customPlayerPath); return checked.ok ? checked.path : ""; } function buildPotPlayerInstallReg(customPlayerPath = "") { const checked = validatePotPlayerCustomPath(customPlayerPath); const hasCustomPath = checked.ok; const command = hasCustomPath ? `"${checked.path}" "%1"` : 'cmd.exe /d /s /c "start "" "PotPlayerMini64.exe" "%1" || start "" "PotPlayerMini.exe" "%1" || start "" "PotPlayer.exe" "%1""'; const defaultIcon = hasCustomPath ? `"${checked.path}",1` : 'PotPlayerMini64.exe,1'; return [ 'Windows Registry Editor Version 5.00', '', '; PikPak Enhancement Master - PotPlayer protocol repair', '; User-level protocol registration only. No browser policy is written.', hasCustomPath ? '; This file binds potplayer:// to the custom PotPlayer executable path below.' : '; This file uses the PotPlayer executable discoverable from the Windows path.', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer]', '@="URL:PotPlayer Protocol"', '"URL Protocol"=""', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\DefaultIcon]', `@="${escapeRegString(defaultIcon)}"`, '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\shell]', '@="open"', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\shell\\open]', '@="open"', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\shell\\open\\command]', `@="${escapeRegString(command)}"`, '' ].join('\r\n'); } function buildPotPlayerDeleteReg() { return [ 'Windows Registry Editor Version 5.00', '', '; PikPak Enhancement Master - PotPlayer protocol cleanup', '; User-level cleanup only. System-level registrations are not modified.', '', '[-HKEY_CURRENT_USER\\Software\\Classes\\potplayer]', '' ].join('\r\n'); } function buildPotPlayerBrowserPolicyReg() { return [ 'Windows Registry Editor Version 5.00', '', '; PikPak Enhancement Master - optional browser external protocol helper', '; Advanced option only. These HKLM policy keys may require administrator rights.', '; Enterprise, school, or company devices may be controlled by existing policies.', '', '[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Google\\Chrome]', '"ExternalProtocolDialogShowAlwaysOpenCheckbox"=dword:00000001', '', '[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Edge]', '"ExternalProtocolDialogShowAlwaysOpenCheckbox"=dword:00000001', '' ].join('\r\n'); } function makeUtf16LeBlob(text) { const input = String(text || ''); const buffer = new ArrayBuffer(2 + input.length * 2); const view = new DataView(buffer); view.setUint16(0, 0xFEFF, true); for (let i = 0; i < input.length; i++) { view.setUint16(2 + i * 2, input.charCodeAt(i), true); } return new Blob([buffer], { type: 'application/octet-stream' }); } function downloadPotPlayerRegFile(kind) { const isDelete = kind === 'delete'; const isBrowserPolicy = kind === 'browser_policy'; const savedPath = getValidSavedPotPlayerPath(); if (!isDelete && !isBrowserPolicy && !savedPath) return false; const content = isBrowserPolicy ? buildPotPlayerBrowserPolicyReg() : (isDelete ? buildPotPlayerDeleteReg() : buildPotPlayerInstallReg(savedPath)); const blob = makeUtf16LeBlob(content); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = isBrowserPolicy ? CONF.potplayerBrowserPolicyRegFileName : (isDelete ? CONF.potplayerRegDeleteFileName : CONF.potplayerRegCustomInstallFileName); a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); recordPotPlayerProtocolRegDownload(isBrowserPolicy ? 'browser_policy' : (isDelete ? 'delete' : 'install')); return true; } function getDefaultPotPlayerLaunchState() { return { schemaVersion: CONF.potplayerLaunchStateSchemaVersion, lastLikelyOpenAt: 0, lastLikelyFailAt: 0, consecutiveFailCount: 0, lastFailSource: "", lastCopiedAt: 0, lastLaunchAt: 0 }; } function normalizePotPlayerLaunchState(rawState) { const def = getDefaultPotPlayerLaunchState(); let data = rawState; if (typeof data === 'string') { try { data = data ? JSON.parse(data) : null; } catch (e) { data = null; } } if (!data || typeof data !== 'object' || Array.isArray(data)) return def; const toTime = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 0; }; const toCount = (v) => { const n = parseInt(v, 10); return Number.isFinite(n) && n > 0 ? n : 0; }; return { schemaVersion: CONF.potplayerLaunchStateSchemaVersion, lastLikelyOpenAt: toTime(data.lastLikelyOpenAt), lastLikelyFailAt: toTime(data.lastLikelyFailAt), consecutiveFailCount: toCount(data.consecutiveFailCount), lastFailSource: typeof data.lastFailSource === 'string' ? data.lastFailSource : "", lastCopiedAt: toTime(data.lastCopiedAt), lastLaunchAt: toTime(data.lastLaunchAt) }; } function readPotPlayerLaunchState() { try { return normalizePotPlayerLaunchState(gmGet(CONF.potplayerLaunchStateKey, '')); } catch (e) { return getDefaultPotPlayerLaunchState(); } } function writePotPlayerLaunchState(state) { try { gmSet(CONF.potplayerLaunchStateKey, normalizePotPlayerLaunchState(state)); } catch (e) {} } function normalizePotPlayerLaunchSource(source) { return ['normal', 'error_fallback', 'unknown'].includes(source) ? source : 'unknown'; } function recordPotPlayerLaunchAttempt(source, launchAt = Date.now()) { const state = readPotPlayerLaunchState(); const time = Number.isFinite(Number(launchAt)) && Number(launchAt) > 0 ? Number(launchAt) : Date.now(); state.lastLaunchAt = time; writePotPlayerLaunchState(state); return state; } function recordPotPlayerLaunchResult(result, opts = {}) { const now = Date.now(); const launchAt = Number.isFinite(Number(opts.launchAt)) && Number(opts.launchAt) > 0 ? Number(opts.launchAt) : now; const copiedAt = Number.isFinite(Number(opts.copiedAt)) && Number(opts.copiedAt) > 0 ? Number(opts.copiedAt) : now; const source = normalizePotPlayerLaunchSource(opts.source); const state = readPotPlayerLaunchState(); if (result === 'likely_open') { state.lastLikelyOpenAt = now; state.lastLaunchAt = launchAt; state.consecutiveFailCount = 0; state.lastFailSource = ""; } else if (result === 'likely_fail') { state.lastLikelyFailAt = now; state.lastLaunchAt = launchAt; state.lastCopiedAt = copiedAt; state.consecutiveFailCount += 1; state.lastFailSource = source; } else { return state; } writePotPlayerLaunchState(state); return state; } function getBlurScope() { const legacyBlur = gmGet('pk_blur_thumb', false); const scope = gmGet('pk_blur_scope', legacyBlur ? 'list' : 'off'); return ['off', 'list', 'grid', 'both'].includes(scope) ? scope : (legacyBlur ? 'list' : 'off'); } function isBlurEnabledForView(view) { const scope = getBlurScope(); return scope === 'both' || scope === view; } const calcSha1 = async (file) => { if (!window.crypto || !window.crypto.subtle) return ""; const hexFromBytes = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); const sha1Bytes = async (buffer) => new Uint8Array(await crypto.subtle.digest('SHA-1', buffer)); const calcXunleiCid = async () => { const CID_SAMPLE_SIZE = 0x5000; if (file.size < 0xF000) return hexFromBytes(await sha1Bytes(await file.arrayBuffer())); const head = file.slice(0, CID_SAMPLE_SIZE); const midStart = Math.floor(file.size / 3); const mid = file.slice(midStart, midStart + CID_SAMPLE_SIZE); const tail = file.slice(Math.max(0, file.size - CID_SAMPLE_SIZE), file.size); return hexFromBytes(await sha1Bytes(await new Blob([head, mid, tail]).arrayBuffer())); }; const calcXunleiGcid = async () => { let partSize = 0x40000; while ((file.size / partSize) > 0x200 && partSize < 0x200000) partSize <<= 1; const digests = []; if (file.size === 0) digests.push(await sha1Bytes(new ArrayBuffer(0))); for (let offset = 0; offset < file.size; offset += partSize) { const buffer = await file.slice(offset, Math.min(file.size, offset + partSize)).arrayBuffer(); digests.push(await sha1Bytes(buffer)); } const merged = new Uint8Array(digests.length * 20); digests.forEach((bytes, index) => merged.set(bytes, index * 20)); return hexFromBytes(await sha1Bytes(merged.buffer)); }; const queryGcidByCid = async (cid) => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/resource/cid?cid=${encodeURIComponent(String(cid || '').toLowerCase())}&file_size=${encodeURIComponent(String(file.size || 0))}`, { method: 'GET', headers: getHeaders() }); if (!res.ok) return ""; const data = await res.json().catch(() => null); return data && data.gcid ? String(data.gcid).toUpperCase() : ""; } catch (e) { console.warn('[Upload] CID precheck failed:', e); return ""; } }; const cid = await calcXunleiCid(); const gcid = await queryGcidByCid(cid); return gcid || await calcXunleiGcid(); }; 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('id')||n.startsWith('in'))?'id':n.startsWith('ms')?'ms':n.startsWith('ko')?'ko':n.startsWith('ja')?'ja':'en';} const I18N_CONF={defaultLang:'zh',remoteLangs:['tc','en','ko','ja','id','ms'],manifestUrl:'https://cdn.jsdelivr.net/gh/digbug82/PikPak_Enhancement_Master@main/i18n/manifest.json',baseUrl:'https://cdn.jsdelivr.net/gh/digbug82/PikPak_Enhancement_Master@main/i18n/',cachePrefix:'pk_i18n_',manifestKey:'pk_i18n_manifest',cacheTTL:7*24*60*60*1000,manifestTTL:24*60*60*1000,requiredKeys:['title','modal_settings_title','label_lang','msg_settings_saved','btn_nav_home']}; let pkRemoteI18n=null; let pkI18nReadyPromise=null; let pkI18nReadyLang=''; function getI18nCacheKey(lang){return `${I18N_CONF.cachePrefix}${lang}`;} function readI18nCache(lang,expectedVersion=''){try{const raw=gmGet(getI18nCacheKey(lang),'');if(!raw)return null;const parsed=typeof raw==='string'?JSON.parse(raw):raw;if(!parsed||parsed.lang!==lang||!parsed.data||typeof parsed.data!=='object'||Array.isArray(parsed.data))return null;if(parsed.expiresAt&&parsed.expiresAtObject.prototype.hasOwnProperty.call(data,k)));} async function loadI18nManifest(force=false){if(!force){const cached=readI18nManifestCache();if(isValidI18nManifest(cached))return cached;}const manifestUrl=`${I18N_CONF.manifestUrl}${I18N_CONF.manifestUrl.includes('?')?'&':'?'}_t=${Date.now()}`;const manifest=await fetchI18nJson(manifestUrl);if(!isValidI18nManifest(manifest))throw new Error('Invalid i18n manifest');writeI18nManifestCache(manifest);return manifest;} async function loadRemoteLangPack(force=false,langOverride=''){const lang=langOverride||getLang();if(!I18N_CONF.remoteLangs.includes(lang)||!I18N_CONF.manifestUrl){pkRemoteI18n=null;return null;}const manifest=await loadI18nManifest(force);const version=String(manifest.version||'');if(!force){const cached=readI18nCache(lang,version);if(cached){pkRemoteI18n={lang,version:cached.version,data:cached.data};return pkRemoteI18n;}}const file=manifest.files&&manifest.files[lang];if(!file){pkRemoteI18n=null;return null;}const baseUrl=manifest.baseUrl||I18N_CONF.baseUrl;const fileUrl=/^https?:\/\//.test(file)?file:`${baseUrl}${file}`;const data=await fetchI18nJson(`${fileUrl}${fileUrl.includes('?')?'&':'?'}_v=${encodeURIComponent(version)}`);if(!isValidI18nPayload(data))throw new Error('Invalid i18n payload');writeI18nCache(lang,version,data);pkRemoteI18n={lang,version,data};return pkRemoteI18n;} function ensureI18nReady(force=false,langOverride=''){const lang=langOverride||getLang();if(force||!pkI18nReadyPromise||pkI18nReadyLang!==lang){pkI18nReadyLang=lang;pkI18nReadyPromise=loadRemoteLangPack(force,lang).catch(()=>null);}return pkI18nReadyPromise;} async function ensureI18nReadyBeforeOpen(langOverride=''){const lang=langOverride||getLang();if(lang==='zh')return null;try{return await Promise.race([ensureI18nReady(false,lang),sleep(4000).then(()=>null)]);}catch(e){return null;}} const T_LOCAL = { 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_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]", tip_help: "帮助 [Alt] + [H]", btn_view_file: "查看文件", btn_jump: "跳转", btn_copy_text: "复制", btn_stop: "停止", btn_exit_script: "返回登录界面", btn_settings: "设置", btn_logout: "退出 PikPak", msg_logout_confirm: "确定要退出登录吗?", tip_settings: "设置和更多 [Alt] + [S]", lbl_upload_to: "上传文件至: ", msg_move_done: "移动完成。", msg_upload_dup_confirm: "发现 {n} 个名称与大小相同的文件,是否跳过?", /* --- 离线、上传与云下载 --- */ 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_code: "提取码", btn_copy_share: "复制全部", str_share_expired: "已过期", str_share_deleted: "文件已删", title_edit_pwd: "密码修改", lbl_share_code_title: "分享代码", btn_migrate: "数据迁移", tip_migrate: "打包选中项并准备迁移至新账号 [Alt] + [M]", msg_migrate_confirm: "确定要将选中的 {n} 个项目迁移至其他账号吗?\n\n打包成功后,会自动退出。\n登录【目标账号】以完成接收。", msg_migrate_packing: "正在构建加密迁移包...", msg_migrate_ready: "✅ 迁移包已就绪!\n\n即将退出当前账号,请登录您的【目标账号】。\n重新登录后系统将自动接管转存工作。\n(注:迁移数据包将在 1 天后自动过期失效)", err_migrate_ban: "打包失败:选中的内容中包含违规或被官方限制分享的资源。\n请剔除违规文件后重试。", msg_migrate_detect: "📦 检测到来自另一个账号的迁移数据包!\n\n共包含 {n} 个项目。\n接收后将保存到以下路径:\n主页 / Pack From Shared\n若该文件夹不存在,将自动创建。\n\n是否立即开始迁移?\n选择“是”将执行迁移;选择“否”将取消本次迁移。", msg_migrate_same_account: "迁移已取消:您登录的仍然是原账号,存根已清除。", msg_migrate_saving: "正在从加密通道高速转存数据...", msg_migrate_success: "🎉 跨账号迁移完成!\n所有文件已成功转存至当前账号。", 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_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}个重复)", 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: "目录列表", opt_grid_view: "网格视图", msg_exporting: "正在生成目录树...", str_analyze_results: "匹配结果", 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: "批量重命名", 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_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: "输入关键词,下方链接自动更新...", 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: "复制链接", msg_potplayer_launching: "正在唤起 PotPlayer...", msg_potplayer_maybe_failed: "可能未唤起 PotPlayer,播放链接已复制,可手动粘贴到播放器。", msg_potplayer_link_copied: "链接已复制", potplayer_fix_entry: "PotPlayer 协议修复", potplayer_fix_title: "PotPlayer 协议修复助手", potplayer_fix_desc: "如果点击 PotPlayer 后没有响应,通常是系统没有正确注册 potplayer:// 协议,或协议指向旧路径。请按下面步骤填写 PotPlayer 程序路径并生成修复文件。", potplayer_fix_manual_tip: "如果曾经修复过错误路径,请先下载并执行清理旧协议文件。\n推荐流程:填写 PotPlayer 程序路径 → 保存路径 → 下载协议修复文件 → 手动执行 .reg → 点击“已执行修复文件,重新尝试”。", potplayer_fix_copy_link: "复制播放链接", potplayer_fix_download_delete: "下载清理旧协议文件", potplayer_fix_i_have_fixed: "已执行修复文件,重新尝试", potplayer_fix_close: "关闭", potplayer_fix_copy_success: "播放链接已复制", potplayer_fix_install_downloaded: "协议修复文件已下载,请双击执行后再点击重新尝试", potplayer_fix_delete_downloaded: "旧协议清理文件已下载,请双击执行后再重新生成修复文件", potplayer_fix_confirmed: "已确认执行修复文件,正在重新尝试", potplayer_fix_skip_today: "今日不再提示", potplayer_fix_never_auto: "不再自动提示", potplayer_fix_reason_auto: "已连续多次未检测到 PotPlayer 打开信号", potplayer_fix_skip_today_done: "今日不再自动提示", potplayer_fix_never_auto_done: "已关闭自动提示", potplayer_fix_auto_title: "PotPlayer 可能需要协议修复", potplayer_fix_advanced_desc: "请填写 PotPlayer.exe、PotPlayerMini64.exe 或 PotPlayerPortable.exe 的完整路径,不要填写其他程序。", potplayer_fix_path_label: "PotPlayer 程序路径", potplayer_fix_path_placeholder: "例如 C:\\PotPlayer\\PotPlayerMini64.exe", potplayer_fix_save_path: "保存路径", potplayer_fix_path_saved: "路径已保存,请重新下载并执行协议修复文件", potplayer_fix_path_invalid: "请输入有效的 PotPlayer exe 路径", potplayer_fix_retry_need_path: "请先填写 PotPlayer 程序路径,并下载执行协议修复文件", potplayer_fix_path_changed: "路径已变更,修复状态已失效,请重新下载并执行协议修复文件", potplayer_fix_download_custom_install: "下载协议修复文件", potplayer_fix_version_outdated: "已保存的修复记录较旧,建议重新生成协议修复文件", potplayer_fix_browser_policy: "下载兜底修复文件", potplayer_fix_browser_policy_desc: "兜底修复文件仅当已执行协议修复文件、PotPlayer 本体能正常打开,但浏览器中点击 PotPlayer 仍无响应时使用。可能需要管理员权限。", potplayer_fix_browser_policy_downloaded: "浏览器协议确认修复文件已下载", potplayer_fix_path_status_empty: "请先填写 PotPlayer 程序路径", potplayer_fix_path_status_confirmed: "已记录当前路径,请确认已执行修复文件后重新尝试", potplayer_fix_path_status_unconfirmed: "路径已保存,请下载并执行协议修复文件", 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: "RPC 测试", ph_aria2_secret: "密钥 (选填)", str_connected: "连接成功", str_conn_fail: "连接失败", str_connecting: "正在测试...", tip_mixed_content: "常用 RPC 端口:\n• 6800 (Aria2 默认)\n• 16800 (Motrix 默认)", picker_title: "选择文件夹", picker_all: "全部文件", picker_new: "新建文件夹", picker_sort_new: "更新", picker_sort_star_new: "星标·更新", picker_sort_star_old: "星标·更旧", picker_sort_type_dur_asc: "类型/时长·顺序", picker_sort_type_dur_desc: "类型/时长·逆序", picker_sort_old: "更旧", picker_sort_large: "更大", picker_sort_small: "更小", picker_sort_path_asc: "路径顺序", grid_folder_count: "{n}项", picker_sort_path_desc: "路径逆序", 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_keep_pos: "保持浏览位置 (返回时定位)", label_sort_pref: "排序偏好", opt_sort_indep: "每个文件夹独立", opt_sort_global: "全部相同", desc_sort_indep: "调整排序方式仅针对当前文件夹生效", desc_sort_global: "全部文件夹使用相同的排序 (时间倒序)", label_view_pref: "视图模式偏好", opt_view_indep: "每个文件夹独立", opt_view_global: "全部相同", desc_view_indep: "调整视图仅对当前文件夹生效", desc_view_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: "模糊媒体封面缩略图", opt_privacy_off: "关闭", opt_privacy_list: "仅列表视图", opt_privacy_grid: "仅网格视图", opt_privacy_both: "列表和网格视图", label_dl_filter_ext: "下载后缀过滤 (例: .txt, .jpg)", label_dl_filter_name: "下载名称过滤 (关键词或全名)", lbl_dl_filter: "文件夹下载过滤", desc_dl_filter: "文件夹下载/Aria2推送时自动排除匹配的文件", 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: "一键勾选", btn_in_group_sort: "组内排序", opt_sort_time: "时间", opt_sort_size: "大小", opt_sort_path: "路径", opt_sort_name: "名称", opt_keep_new: "保留最新的", opt_keep_old: "保留最旧的", opt_keep_large: "保留最大的", opt_keep_small: "保留最小的", opt_keep_short: "保留名称最短的", opt_keep_long: "保留名称最长的", label_clipboard_magnet_focus: "剪贴板磁链识别", desc_clipboard_magnet_focus: "回到前台时检测剪贴板磁链", msg_magnet_preview_desc: "已识别到剪贴板磁链,确认后将直接创建云下载任务。", msg_magnet_preview_fail: "未获取到公开预览信息,仍可继续添加。", str_magnet_unknown_name: "未知资源", str_no_preview: "暂无预览图", lbl_magnet_count: "文件数", lbl_magnet_size: "总大小", lbl_magnet_type: "类型", lbl_magnet_hash: "磁链", btn_magnet_continue: "高速云下载", lbl_magnet_preview_source: "预览信息来自", msg_magnet_preview_rate_limited: "预览服务请求过于频繁,已临时降级。仍可继续添加。", msg_magnet_preview_timeout: "预览服务响应超时,仍可继续添加。", msg_magnet_preview_network: "预览服务暂不可用,仍可继续添加。", /* --- 状态、进度与加载短语 --- */ 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_copying: "复制到剪贴板...", str_moving: "准备移动...", str_sorting: "正在排序...", str_refreshing: "刷新中...", str_refreshing_cache: "刷新缓存中...", str_group: "组", str_init_rename: "初始化重命名...", str_renaming: "重命名中...", str_calc_changes: "计算变更中...", str_scanning_dir: "扫描目录结构...", str_init_op: "初始化操作...", str_init_scan: "正在初始化全盘扫描...", str_upload_1: "正在上传 (节点 1/3)...", str_upload_2: "节点1超时,切换节点 2...", str_upload_3: "节点2超时,尝试最后节点...", str_upload_fail_copy: "上传失败,准备写入剪贴板...", 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: "提取次数已更新", str_selected_items: "已选项目", /* --- 提示、确认与交互消息 --- */ 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_clear_history_done: "已从历史记录中移除", msg_skip_unzipped: "已跳过 {n} 个已解压的项目。", msg_unzip_skip_del_confirm: "检测到 {n} 个压缩包在当前路径存在同名文件夹且状态标记为已解压,大概率已完成解压。是否将其移入回收站?\n\n(建议:请先检查同名文件夹内容是否完整)", msg_cancel_share_confirm: "确定要取消选中的 {n} 个分享吗?\n链接将立即失效。", msg_pwd_updated: "密码已更新", msg_exp_updated: "有效期已更新", msg_cancel_share_done: "已取消 {n} 个分享。", msg_cancel_share_ing: "正在取消分享...", msg_drag_drop_hint: "将文件拖拽到此处并释放", str_drag_files: " 等 {n} 个文件", msg_creating_share: "正在创建分享...", title_share_result: "分享成功", msg_no_files: "选中的项目为空。", warn_del: "确定要删除选中的 {n} 项吗?", msg_clear_sel_confirm: "已选中 {n} 个重复文件,确认要取消当前的勾选吗?", str_bl_stat: "匹配: {n} 项 | 已选中: {m} 项", str_hits: "命中", msg_settings_saved: "设置已保存。页面将刷新。", 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_restore_done: "已成功还原 {n} 个项目。", msg_auto_sub_load: "已自动加载字幕:{n}", msg_dl_sub: "正在下载字幕...", 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_ghost_task_resume: "云端未完成 (点击恢复重选文件)", msg_parsing_files: "正在解析文件,请稍候...", msg_token_expired_retry: "登录过期,正在重试...", str_empty_str: "(空)", err_clipboard_failed: "写入剪贴板失败", err_cors_blocked: "安全拦截: 跨域访问被拒绝", err_worker_failed: "工作线程遇到错误", str_target_folder: "目标文件夹", str_unknown_name: "未知名称", btn_default: "默认", 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_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_down_progress: "正在调用浏览器下载...", msg_down_confirm_total: "✅ 扫描完毕,共找到 {n} 个文件,总大小约 {s}。\n\n⚠️ 警告:当前任务规模较大,浏览器批量下载可能导致页面卡顿或被浏览器拦截。\n建议当文件数超过 {fc} 个或总大小超过 {fs} 时,优先使用 Aria2。\n\n是否仍然使用浏览器下载?", msg_aria2_sending_batch: "🚀 正在分批发送任务至 Aria2...", msg_aria2_check_fail: "Aria2 连接失败!\n请检查 URL 和 Token。", 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_batch_filtered: "下载过滤规则已跳过 {n} 个文件。", msg_batch_all_filtered: "已全部过滤:{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_bl_run_limit: "⚠️ 模式限制\n\n清理操作涉及物理文件递归操作。目前处于非标准目录,无法准确定位物理扫描范围。\n\n请返回主页常规文件夹后再执行清理。", msg_del_protected: "资源管理器中已记录的 {n} 个文件,已保护并跳过删除。", msg_del_none: "没有可删除的文件。", rn_tip_wait: "请设置规则", rn_tip_jav: "点击上方按钮开始智能匹配", rn_tip_none: "没有匹配的项目或名称", rn_tip_series_folder_only: "选中项均为文件夹,没有匹配的文件", 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_global_warn: "即将开始全盘文件同步。\n\n文件同步后缓存到本地内存中,网页刷新前持续存在。\n\n是否继续?", msg_init_scan_sel: "正在初始化选中项扫描...", warn_clear_history: "确定要从历史记录中移除选中的 {n} 项吗?\n(这不会删除您的云端文件)", 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_migrate_too_many: "⚠️ 选中项过多\n\n当前请求数据量 ({s}MB) 已超过服务器 4MB 的硬性限制。\n\n【解决方案】:\n请在常规目录中直接选中【文件夹】进行迁移,而不是在全盘透视模式下选中海量独立文件。", msg_migrate_quota_err: "⚠️ 目标账号空间不足!\n\n系统提示:{d}\n\n是否保留迁移记录?\n(选\"是\"保留记录,清理空间后刷新页面可重试;选\"否\"则终止本次迁移)", msg_migrate_err_keep: "迁移异常:\n{e}\n\n是否保留迁移记录以便稍后重试?\n(选\"否\"将清除记录,不再弹窗)", title_migrate_fail: "迁移失败", /* --- 错误提示 --- */ err_invalid_links: "请输入正确的链接", 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_clipboard_denied: "剪贴板访问被拒绝", err_worker: "工作线程错误", 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_video_fail: "无法获取视频链接。", err_star_sync_fail: "星标同步失败", err_paste_descendant: "不能移动或复制到当前或当前子目录下", err_quota_exceeded: "存储空间不足", err_name_exists: "文件名称不能重复", err_share_pass: "自定义提取码需为 4-10 位字符", err_share_limit: "分享失败:单次最多只能分享 100 个项目", err_migrate_limit: "迁移失败:单次最多只能迁移 100 个项目", lbl_hard_delete: "彻底删除 (不进入回收站)", str_error: "错误", str_error_crit: "严重错误", str_action_failed: "操作失败", str_scan_error: "扫描错误", err_limit_too_low: "修改失败:新次数 ({n}) 必须大于当前已保存次数 ({s})", err_vault_max: "密码金库最多仅支持存储 50 个常用密码", err_pwd_len: "单个密码长度不能超过 127 个字符", err_operation_failed: "操作失败", /* --- 帮助文档 --- */ modal_help_title: "帮助", help_desc: `
✨ 体验与导航引擎
交互重构:在官方功能基础上,界面按 Windows 文件资源管理器 逻辑深度重构,并支持列表视图 / 网格视图一键切换
极速模式:开启后接管原生逻辑,彻底解决海量文件下的卡顿与崩溃问题。
高级路径栏:支持滚轮滑动,并通过下拉菜单进行同级切换与定位。全盘搜索、分析套件均集成路径栏,支持路径回显与溯源跳转。
体验增强:支持星标、类型等多维排序,一键模糊封面、暗黑皮肤切换,并通过 SWR 策略静默刷新视图,尽量减少闪烁与打断感。
后台索引与保护:主页蓝点闪烁表示正在同步全盘索引。系统内置并发操作物理锁,主动拦截冲突操作,降低脏数据与误操作风险。
* 注:默认文件夹(My Pack)受官方保护,严防误删、复制、移动及重命名。
📂 批量与空间管理
批量重命名:支持正则替换/删除剧集流水号、文本格式化FC2 纯净命名前缀去广告及基于 MIME 的后缀修复
分析套件文件分析整合了筛选与查重(哈希/时长/名称三模态);文件夹分析整合了筛选与查重(名称/相似度/包含率三模态);并支持导出当前目录的树状列表
智能整理:删除时支持彻底删除(跳过回收站);一键清理空文件夹;批量解压集成密码自动记忆与智能填充,支持跳过并删除已解压项。
资源管理器:自定义文件黑名单一键清理垃圾资源;或作为文件白名单,在批量删除时自动保护。
* 注:为避免数据同步冲突,处理期间请勿在其他客户端同时改动同一批文件。
🌐 传输与分享中心
分享管理:支持设定提取次数上限,次数达标后链接自动失效并取消分享。
极速上传:支持将本地文件 / 文件夹直接拖拽到网页上传,突破官方部分交互限制,并显著降低大量小文件场景下的传输中断率。
上传残留清理:脚本会在异常退出或中断上传后尝试清理残留云端“幽灵文件”,减少目录污染与重复占位。
云下载增强:批量离线链接支持自动去重;内置磁链智能清洗引擎,自动抽取 Base32 / Hex 哈希去除污染字符;支持解析 .torrent 种子,并为受限链接提供网页快照兜底方案
* 注:提取次数拦截仅在网页保持开启且设备未休眠时生效。
🎬 沉浸式媒体增强
播放引擎:支持 0.5x-3.0x 倍速、旋转翻转、比例切换、子母画面、从头播放、上一集/下一集、跳过片头片尾清单循环 / 单集循环 / 播完暂停等增强控制。
兼容回退:内置兼容模式与画质回退逻辑,遇到黑屏、当前清晰度不可用或编码兼容性异常时,可自动切换到可播放画质。
字幕系统:支持云端字幕本地字幕文件在线字幕搜索三路加载;支持字幕显示开关、位置切换、背景透明、大小调整与毫秒级进度偏移。
外部直连:支持一键唤起 PotPlayer 外部播放,或直接导出当前视频流链接并复制使用。
视觉辅助:支持对图片或视频当前帧执行以图搜图,快速反查封面、演员、番剧或素材来源。
* 注:播放历史列表持续记录在脚本环境内产生的播放进度。
⚙️ 配置与数据管理
配置备份:支持将偏好设置、管理规则、密码金库等导出为带数字指纹的 JSON 备份文件;导入时支持智能合并去重,避免重复覆盖。
数据清理:支持按需清除全盘索引偏好设置管理规则密码金库缓存,释放本地空间并提升隐私安全。
密码金库:内置常用解压密码集中管理,供批量解压场景自动调用与快速填充。
数据迁移:支持将选中项目加密打包,通过自动接管机制在其他账号登录后直接识别并转存,实现数据跨账号无缝迁移。
* 注:全盘索引会在网页关闭后清空;偏好设置、管理规则与密码金库等数据则会持久化保存。
⚡ 下载与分发
外部下载:支持通过 RPC 协议将文件一键推送到 Aria2 / Motrix 等下载节点,并可实时检测连接状态。
目录结构还原:RPC 推送整个文件夹时可自动恢复云盘中的树状目录结构,避免扁平化后难以整理。
分发增强:支持长连接监控、失败后自动导出错误清单,并支持文件夹下载过滤,便于大批量任务精细化下发。
本项目严格遵循 CC-BY-NC-SA-4.0 协议,严禁用于任何商业用途
` }, }; function getStrings() { const lang = getLang(); const base = T_LOCAL.zh || {}; const local = T_LOCAL[lang] || {}; const remote = pkRemoteI18n && pkRemoteI18n.lang === lang ? pkRemoteI18n.data : null; return mergeI18nPack(mergeI18nPack(base, remote), local); }; ensureI18nReady(); let cachedCredKey = null; let cachedCaptchaKey = null; let memoryCapturedToken = ''; document.addEventListener('pk-token-captured', (e) => { memoryCapturedToken = e.detail; }); function resetHeaderCache() { cachedCredKey = null; cachedCaptchaKey = null; memoryCapturedToken = ''; } 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; } async function confirmedLogout(reason = 'unknown', graceMs = 5000, waitMs = 5200) { console.warn(`[Auth] Confirmed logout requested: ${reason}`); if (typeof window.pkEnterAuthRecoveryWindow === 'function') { window.pkEnterAuthRecoveryWindow(`confirmed-logout:${reason}`, graceMs); } await sleep(800); const isAuthReady = await waitForAuth(waitMs); if (isAuthReady) { console.warn(`[Auth] Logout aborted, auth recovered: ${reason}`); if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); return false; } console.warn(`[Auth] Logout confirmed after recheck: ${reason}`); if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); if (!location.href.includes('/login')) window.location.href = 'https://mypikpak.com/drive/login'; return true; } (() => { let authCheckTimer = null; let authRouteGraceTimer = null; let authRecoveryUntil = 0; let pageHiddenAt = document.hidden ? Date.now() : 0; const AUTH_PURGE_DELAY_MS = 1200; const AUTH_ROUTE_GRACE_MS = 4000; const AUTH_RESUME_GRACE_MS = 5000; const AUTH_RESUME_MIN_HIDDEN_MS = 45000; const isLoginRoute = () => location.href.includes('/login') || location.pathname.includes('login'); const hasCredentialSnapshot = () => { if (memoryCapturedToken && memoryCapturedToken.length > 10) return true; 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) return true; } catch {} } } return false; }; const markAuthRecovered = () => { authRecoveryUntil = 0; if (authRouteGraceTimer) { clearTimeout(authRouteGraceTimer); authRouteGraceTimer = null; } }; const enterAuthRecoveryWindow = (reason = 'unknown', graceMs = AUTH_ROUTE_GRACE_MS) => { authRecoveryUntil = Math.max(authRecoveryUntil, Date.now() + graceMs); if (authRouteGraceTimer) clearTimeout(authRouteGraceTimer); authRouteGraceTimer = setTimeout(() => { authRouteGraceTimer = null; if (isLoginRoute() && !hasCredentialSnapshot()) { console.log(`[Auth Sync] Recovery grace expired (${reason}), auth still missing.`); checkAndPurgeAuth(`grace-expired:${reason}`); } else { console.log(`[Auth Sync] Recovery grace ended (${reason}), auth restored.`); markAuthRecovered(); } }, graceMs + 50); }; const checkAndPurgeAuth = (reason = 'unknown') => { if (authCheckTimer) clearTimeout(authCheckTimer); const extraWait = authRecoveryUntil > Date.now() ? (authRecoveryUntil - Date.now() + 50) : 0; authCheckTimer = setTimeout(() => { if (hasCredentialSnapshot()) { console.log(`[Auth Sync] Auth recovered before purge (${reason}).`); markAuthRecovered(); return; } console.log(`[Auth Sync] No credentials found after delay (${reason}), executing deep purge.`); if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); if (!isLoginRoute()) window.location.href = 'https://mypikpak.com/drive/login'; }, Math.max(AUTH_PURGE_DELAY_MS, extraWait)); }; const handlePageResume = (reason = 'resume') => { const hiddenFor = pageHiddenAt ? (Date.now() - pageHiddenAt) : 0; pageHiddenAt = 0; if (hiddenFor < AUTH_RESUME_MIN_HIDDEN_MS && reason !== 'pageshow-bfcache') return; console.log(`[Auth Sync] Page resumed (${reason}, hidden ${hiddenFor}ms), entering recovery grace.`); enterAuthRecoveryWindow(`resume:${reason}`, AUTH_RESUME_GRACE_MS); checkAndPurgeAuth(`resume:${reason}`); }; document.addEventListener('pk-token-captured', () => { const wasRecovering = authRecoveryUntil > Date.now(); markAuthRecovered(); if (wasRecovering && typeof window.pkForceManagerReloadAfterAuth === 'function') { window.pkForceManagerReloadAfterAuth('token-captured'); } }); document.addEventListener('visibilitychange', () => { if (document.hidden) { pageHiddenAt = Date.now(); return; } handlePageResume('visibility'); }); window.addEventListener('pageshow', (e) => { if (e.persisted) { handlePageResume('pageshow-bfcache'); return; } if (pageHiddenAt) handlePageResume('pageshow'); }); window.addEventListener('focus', () => { if (pageHiddenAt) handlePageResume('focus'); }); 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) { enterAuthRecoveryWindow(`storage-remove:${e.key}`); checkAndPurgeAuth(`storage-remove:${e.key}`); } else { resetHeaderCache(); markAuthRecovered(); } } }); 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(); markAuthRecovered(); } }; 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}), entering recovery grace.`); enterAuthRecoveryWindow(`remove:${key}`); checkAndPurgeAuth(`remove:${key}`); } }; const checkAuthRoute = () => { if (isLoginRoute()) { console.log(`[Auth Sync] SPA route changed to /login, entering recovery grace.`); enterAuthRecoveryWindow('route-login'); checkAndPurgeAuth('route-login'); } else { markAuthRecovered(); } }; 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.pkEnterAuthRecoveryWindow = enterAuthRecoveryWindow; window.pkMarkAuthRecovered = markAuthRecovered; window.pkIsAuthRecoveryActive = () => authRecoveryUntil > Date.now(); window.addEventListener('popstate', checkAuthRoute); })(); function getHeaders() { const recoveryActive = typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive(); let token = memoryCapturedToken || '', captcha = ''; if (recoveryActive) { cachedCredKey = null; cachedCaptchaKey = null; } if (!token && 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; break; } } catch {} } } } } const headers = { 'Content-Type': 'application/json', 'Authorization': token, 'x-device-id': localStorage.getItem('deviceid') || '', 'x-captcha-token': captcha }; if (headers.Authorization && headers.Authorization.length > 10 && typeof window.pkMarkAuthRecovered === 'function') { window.pkMarkAuthRecovered(); } return headers; } async function waitForAuth(timeout = 10000) { const start = Date.now(); let probeCount = 0; let hardTimeout = timeout + ((typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 3000 : 0); while (Date.now() - start < hardTimeout) { const h = getHeaders(); if (h.Authorization && h.Authorization.length > 10) { if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); return true; } if ((probeCount++ % 5) === 0 && typeof resetHeaderCache === 'function') { resetHeaderCache(); } if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { hardTimeout = Math.max(hardTimeout, timeout + 3000); } await sleep((typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 250 : 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 === 403) { console.warn(`[API] ${res.status} Unauthorized. Throwing hard auth error...`); localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); throw new Error(`API Error ${res.status}`); } if (res.status === 400) { console.warn(`[API] 400 Error. Possible captcha intercept.`); localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); try { if (typeof showToast !== 'undefined') showToast(getStrings().err_captcha_simple, 'error'); } catch(e){} throw new Error('CAPTCHA_INTERCEPT'); } 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 === 401 || res.status === 403) throw new Error(`API Error ${res.status}`); if (res.status === 429) await sleep(2000); } catch (e) { if (e.message.includes('401') || e.message.includes('403')) throw 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, preferFresh = false } = 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 = []; const folderId = current.id === 'root' ? '' : (current.id || ''); if (!preferFresh && typeof globalCache !== 'undefined' && globalCache.has(folderId)) { const cachedData = globalCache.get(folderId); if (Array.isArray(cachedData)) { files = cachedData; stats.cacheHits++; } } let isFromNetwork = false; let start = 0; if (files.length === 0) { start = performance.now(); files = await apiList(folderId, 1000, null, signal, false, true); isFromNetwork = true; if (typeof globalCache !== 'undefined') globalCache.set(folderId, files); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.add(folderId); } 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(); function normalizeAriaRpcUrl(url) { let u = (url || '').trim(); if (!u) u = 'http://localhost:6800/jsonrpc'; if (!/^https?:\/\//i.test(u) && !/^wss?:\/\//i.test(u)) u = 'http://' + u; if (!u.includes('/jsonrpc') && !u.includes('?')) { u = u.endsWith('/') ? u + 'jsonrpc' : u + '/jsonrpc'; } return u; } function buildAriaRpcParams(token, params = []) { const t = (token || '').trim(); return t ? [`token:${t}`, ...params] : params; } function isAriaWsRpcUrl(url) { return /^wss?:\/\//i.test(url || ''); } function getAriaRpcError(data) { const list = Array.isArray(data) ? data : [data]; const hit = list.find(x => x && x.error); if (!hit) return null; const err = hit.error || {}; return new Error(err.message || `RPC ${err.code || 'Error'}`); } function aria2RpcHttpRequest(url, payload, timeout = 5000) { return new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout, onload: (r) => { if (r.status !== 200) { rejectReq(new Error('HTTP ' + r.status)); return; } try { const data = r.responseText ? JSON.parse(r.responseText) : null; const rpcErr = getAriaRpcError(data); if (rpcErr) rejectReq(rpcErr); else resolveReq(data); } catch (e) { rejectReq(new Error('Invalid JSON')); } }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); } function aria2RpcWsRequest(url, payload, timeout = 5000) { return new Promise((resolveReq, rejectReq) => { let ws = null; let settled = false; const requests = Array.isArray(payload) ? payload : [payload]; const pending = new Set(requests.map(req => String(req.id))); const results = []; const finish = (ok, value) => { if (settled) return; settled = true; clearTimeout(timer); try { if (ws && ws.readyState <= 1) ws.close(); } catch (e) {} ok ? resolveReq(value) : rejectReq(value); }; const timer = setTimeout(() => finish(false, new Error('Timeout')), timeout); try { ws = new WebSocket(url); } catch (e) { finish(false, e); return; } ws.onopen = () => { try { requests.forEach(req => ws.send(JSON.stringify(req))); } catch (e) { finish(false, e); } }; ws.onmessage = (ev) => { try { const data = JSON.parse(ev.data); const list = Array.isArray(data) ? data : [data]; const rpcErr = getAriaRpcError(list); if (rpcErr) { finish(false, rpcErr); return; } list.forEach(item => { if (!item || item.id === undefined || item.id === null) return; const id = String(item.id); if (!pending.has(id)) return; pending.delete(id); results.push(item); }); if (pending.size === 0) { finish(true, Array.isArray(payload) ? results : results[0]); } } catch (e) { finish(false, new Error('Invalid JSON')); } }; ws.onerror = () => finish(false, new Error('Network Error')); ws.onclose = () => { if (!settled) finish(false, new Error('Network Error')); }; }); } function aria2RpcRequest(url, payload, timeout = 5000) { const rpcUrl = normalizeAriaRpcUrl(url); return isAriaWsRpcUrl(rpcUrl) ? aria2RpcWsRequest(rpcUrl, payload, timeout) : aria2RpcHttpRequest(rpcUrl, payload, timeout); } 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(), selMode: 'explicit', selEx: 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: [], dupGridMeta: null, dupGridMetaKey: '', 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: '', viewMode: (globalSavedState && globalSavedState.viewMode) ? globalSavedState.viewMode : (() => { if (gmGet('pk_view_independent', false)) { try { const prefs = JSON.parse(gmGet('pk_folder_view_prefs', '{}')); const saved = prefs.root; const mode = typeof saved === 'string' ? saved : (saved && saved.viewMode); return mode === 'list' ? 'list' : 'grid'; } catch(e) { return 'grid'; } } return gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'; })(), sortId: 0, isFlattened: false, filterState: { active: false, cat: 'all', ext: 'all' }, suppressClearConfirm: false, preSearchPath: null, lastGlobalResults: [], folderLineageMap: globalLineageMap, strictFolderFirstSnapshot: null, 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, navBuckets: { home: { backStack: [], current: null, forwardStack: [] }, starred: { backStack: [], current: null, forwardStack: [] }, recent: { backStack: [], current: null, forwardStack: [] } }, navScrollStore: { home: Object.create(null), starred: Object.create(null), recent: Object.create(null) }, navActiveBucket: null, navFrozenBucket: null, navContext: 'none', navSyncSig: '', navMaxHistory: CONF.mouseSideNavHistoryMax, navSuspendRecord: false, navTransitionBusy: false, navLastModalType: '', navVideoBackArmed: 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'), getNavBucketKey: () => { if (S.starredMode) return 'starred'; if (S.recentMode) return 'recent'; if (S.trashMode || S.shareMode || S.historyMode || S.offlineMode || S.uploadMode) return null; if (S.isFlattened || S.dupMode || S.analyzeMode) return null; const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; if (cur && typeof cur.id === 'string' && (cur.id.startsWith('virtual_') || cur.id === 'analyze_root')) return null; return 'home'; }, getActiveNavBucket: () => { const key = S.getNavBucketKey(); return key ? S.navBuckets[key] : null; }, cloneNavPath: (path = S.path) => (Array.isArray(path) ? path : []).map(n => ({ id: n.id, name: n.name })), getNavPathSig: (path = S.path) => { const arr = Array.isArray(path) ? path : []; return arr.map(n => n && n.id || '').join('>'); }, saveNavScrollTop: (bucketKey = null, path = S.path, scrollTop = null) => { const key = bucketKey || S.getNavBucketKey() || S.navActiveBucket || null; if (!key || !S.navScrollStore[key] || !UI.vp) return; const sig = S.getNavPathSig(path); if (!sig) return; S.navScrollStore[key][sig] = Math.max(0, Math.round(scrollTop == null ? UI.vp.scrollTop : scrollTop)); }, getNavScrollTop: (bucketKey = null, path = S.path) => { const key = bucketKey || S.getNavBucketKey() || S.navActiveBucket || null; if (!key || !S.navScrollStore[key]) return null; const sig = S.getNavPathSig(path); if (!sig) return null; const v = S.navScrollStore[key][sig]; return Number.isFinite(v) ? v : null; }, restoreNavScrollTop: (bucketKey = null, path = S.path) => { if (!gmGet('pk_keep_pos', true) || !UI.vp) return; const savedTop = S.getNavScrollTop(bucketKey, path); if (!Number.isFinite(savedTop)) return; requestAnimationFrame(() => { if (!UI.vp) return; const maxTop = Math.max(0, UI.vp.scrollHeight - UI.vp.clientHeight); UI.vp.scrollTop = Math.max(0, Math.min(savedTop, maxTop)); if (typeof renderVisible === 'function') renderVisible(); }); }, isSameNavPath: (a, b) => { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if ((a[i] && a[i].id) !== (b[i] && b[i].id)) return false; } return true; }, trimNavStack: (stack) => { if (!Array.isArray(stack) || stack.length <= S.navMaxHistory) return; stack.splice(0, stack.length - S.navMaxHistory); }, getNavContext: () => { if (S.isStrictVirtualNavMode()) return 'virtual'; const key = S.getNavBucketKey(); return key || 'none'; }, recordNavSnapshot: (bucketKey, path = S.path) => { const bucket = bucketKey ? S.navBuckets[bucketKey] : null; if (!bucket) return; const snap = S.cloneNavPath(path); if (!snap.length) return; if (bucket.current && S.isSameNavPath(bucket.current, snap)) return; if (bucket.current && bucket.current.length) { bucket.backStack.push(S.cloneNavPath(bucket.current)); S.trimNavStack(bucket.backStack); } bucket.current = snap; bucket.forwardStack = []; }, restoreFrozenNavBucketPath: () => { const key = S.navFrozenBucket; const bucket = key ? S.navBuckets[key] : null; if (!bucket || !bucket.current || !bucket.current.length) return false; S.navSuspendRecord = true; S.path = S.cloneNavPath(bucket.current); S.navActiveBucket = key; S.navContext = key; S.navSyncSig = ''; queueMicrotask(() => { S.navSuspendRecord = false; }); return true; }, loadNavPath: async (path, bucketKey = null, opts = null) => { const targetPath = S.cloneNavPath(path); if (!targetPath.length) return false; if (S.navTransitionBusy) return false; const key = bucketKey || S.getNavBucketKey() || S.navActiveBucket || null; const skipNavSync = !!(opts && opts.skipNavSync); const prevPath = S.cloneNavPath(S.path); const prevActiveBucket = S.navActiveBucket; const prevContext = S.navContext; const prevSyncSig = S.navSyncSig; const prevSuspendRecord = S.navSuspendRecord; const prevScrollTop = UI.vp ? UI.vp.scrollTop : 0; const prevSelMode = S.selMode; const prevSel = new Set(S.sel); const prevSelEx = new Set(S.selEx); const prevLastSelIdx = S.lastSelIdx; const prevActiveIdForRollback = S.activeId; const targetSig = S.getNavPathSig(targetPath); S.navTransitionBusy = true; S.navSuspendRecord = true; S.saveNavScrollTop(prevActiveBucket || S.getNavBucketKey(), prevPath, prevScrollTop); S.path = targetPath; S.navActiveBucket = key; S.navContext = key || 'none'; S.navSyncSig = ''; try { updateQuotaUI(); const pendingLoad = Promise.resolve(load()); await new Promise(r => requestAnimationFrame(r)); S.clearSelection(); pendingLoad.then(() => { if (S.getNavPathSig() === targetSig) { S.restoreNavScrollTop(key, targetPath); } }).catch(err => { S.sideNavLog('loadNavPath async error', err); }); return true; } catch (err) { S.sideNavLog('loadNavPath rollback', err); S.path = prevPath; S.navActiveBucket = prevActiveBucket; S.navContext = prevContext; S.navSyncSig = prevSyncSig || ''; S.selMode = prevSelMode; S.sel = new Set(prevSel); S.selEx = new Set(prevSelEx); S.lastSelIdx = prevLastSelIdx; S.activeId = prevActiveIdForRollback; if (typeof updateStat === 'function') updateStat(); try { updateQuotaUI(); await load(); S.restoreNavScrollTop(prevActiveBucket || S.getNavBucketKey(), prevPath); } catch (restoreErr) { S.sideNavLog('loadNavPath restore failed', restoreErr); } return false; } finally { S.navSuspendRecord = prevSuspendRecord; S.navTransitionBusy = false; if (!skipNavSync) { S.navSyncSig = ''; S.syncNavContextState(); } } }, navGoBack: async () => { const key = S.getNavBucketKey(); const bucket = key ? S.navBuckets[key] : null; if (!bucket || !bucket.backStack.length || S.navTransitionBusy) return false; const prevPath = S.cloneNavPath(bucket.backStack[bucket.backStack.length - 1]); if (!prevPath.length) return false; const currentPath = bucket.current && bucket.current.length ? S.cloneNavPath(bucket.current) : S.cloneNavPath(S.path); const ok = await S.loadNavPath(prevPath, key, { skipNavSync: true }); if (!ok) return false; bucket.backStack.pop(); if (currentPath.length) { bucket.forwardStack.push(currentPath); S.trimNavStack(bucket.forwardStack); } bucket.current = S.cloneNavPath(prevPath); S.navActiveBucket = key; S.navContext = key; S.navFrozenBucket = null; S.navSyncSig = ''; S.syncNavContextState(); return true; }, navGoForward: async () => { const key = S.getNavBucketKey(); const bucket = key ? S.navBuckets[key] : null; if (!bucket || !bucket.forwardStack.length || S.navTransitionBusy) return false; const nextPath = S.cloneNavPath(bucket.forwardStack[bucket.forwardStack.length - 1]); if (!nextPath.length) return false; const currentPath = bucket.current && bucket.current.length ? S.cloneNavPath(bucket.current) : S.cloneNavPath(S.path); const ok = await S.loadNavPath(nextPath, key, { skipNavSync: true }); if (!ok) return false; bucket.forwardStack.pop(); if (currentPath.length) { bucket.backStack.push(currentPath); S.trimNavStack(bucket.backStack); } bucket.current = S.cloneNavPath(nextPath); S.navActiveBucket = key; S.navContext = key; S.navFrozenBucket = null; S.navSyncSig = ''; S.syncNavContextState(); return true; }, exitVirtualNavMode: async () => { const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; const isAnalyzeGroupedGrid = S.analyzeMode && !!S.analyzeSimGroups && !!cur && cur.id === 'analyze_root' && S.viewMode === 'grid'; const shouldSyncGroupedGridExit = (S.dupMode && S.viewMode === 'grid') || isAnalyzeGroupedGrid; 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' }; if (shouldSyncGroupedGridExit) S._groupedGridExitSyncPending = true; S._sortAppliedForId = null; S._comicApplied = false; if (S.analyzeMode) { S.analyzeMode = false; S.analyzeResultItems = null; S.analyzeSimGroups = null; S.analyzeMap = null; } S.dupRawGroups = []; S.dupBuckets = null; S.dupItemMap = null; 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 = ''; const frozenKey = S.navFrozenBucket; const frozenBucket = frozenKey ? S.navBuckets[frozenKey] : null; const fallbackPath = [{ id: '', name: L.btn_nav_home }]; const targetPath = (frozenBucket && frozenBucket.current && frozenBucket.current.length) ? frozenBucket.current : fallbackPath; S.navFrozenBucket = null; return await S.loadNavPath(targetPath, frozenBucket ? frozenKey : null); }, getTopModalOverlay: () => { const list = Array.from(document.querySelectorAll('.pk-modal-ov')).filter(m => { if (!m || !m.isConnected) return false; const st = window.getComputedStyle(m); return st.display !== 'none' && st.visibility !== 'hidden'; }); return list.length ? list[list.length - 1] : null; }, hasFrontOverlayForSideNav: () => { return !!(document.getElementById('pk-player-ov') || document.querySelector('.pk-img-ov') || S.getTopModalOverlay()); }, isManagerSideNavScope: () => { const win = document.querySelector('.pk-ov'); return !!(win && win.style.display !== 'none'); }, sideNavLog: (...args) => { if (!CONF.mouseSideNavDebug) return; console.log('[PK SideNav]', ...args); }, closePlayerOverlay: () => { const player = document.getElementById('pk-player-ov'); if (!player) return false; S.navVideoBackArmed = false; const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; if (typeof player._pkDestroyPlayer === 'function') { player._pkDestroyPlayer(); S.sideNavLog('player close: unified destroy'); return true; } const v = player.querySelector('#pk_video'); if (v) { try { v.pause(); v.removeAttribute('src'); v.load(); } catch (e) {} } player.remove(); S.sideNavLog('player close: unified'); return true; }, closeImageOverlay: () => { const d = document.querySelector('.pk-img-ov'); if (!d) return false; const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; const cleanup = d._pkResizeHandler; if (cleanup) { window.removeEventListener('resize', cleanup); d._pkResizeHandler = null; } const imgList = d._pkImgList || []; const curIdx = Number.isInteger(d._pkCurIdx) ? d._pkCurIdx : -1; const currentItem = curIdx >= 0 ? imgList[curIdx] : null; 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 = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); } } d.remove(); S.sideNavLog('image close: unified'); return true; }, closeTopModalOverlay: () => { const openModal = S.getTopModalOverlay(); if (!openModal) return false; const closeBtn = openModal.querySelector('.pk-modal-close'); if (closeBtn) closeBtn.click(); else openModal.remove(); return true; }, handleOverlayMouseSideBack: () => { const player = document.getElementById('pk-player-ov'); if (player) { S.sideNavLog('back: video close'); return S.closePlayerOverlay(); } const imgPlayer = document.querySelector('.pk-img-ov'); if (imgPlayer) { S.sideNavLog('back: image close'); return S.closeImageOverlay(); } if (S.closeTopModalOverlay()) { S.sideNavLog('back: modal close'); return true; } return false; }, canHandleMouseSideBack: () => S.isManagerSideNavScope(), canHandleMouseSideForward: () => S.isManagerSideNavScope(), handleMouseSideBack: async () => { if (!S.isManagerSideNavScope() || S.navTransitionBusy) return false; if (S.handleOverlayMouseSideBack()) return true; if (S.isStrictVirtualNavMode()) { await S.exitVirtualNavMode(); return true; } if (S.getNavBucketKey() !== null) { await S.navGoBack(); return true; } return true; }, handleMouseSideForward: async () => { if (!S.isManagerSideNavScope() || S.navTransitionBusy) return false; const player = document.getElementById('pk-player-ov'); if (player) { const v = player.querySelector('#pk_video'); if (v) { if (v.ended) { S.sideNavLog('forward: video restart'); try { v.currentTime = 0; } catch (e) {} v.play().catch(()=>{}); return true; } if (v.paused) { S.sideNavLog('forward: video play'); v.play().catch(()=>{}); return true; } S.sideNavLog('forward: video pause'); v.pause(); return true; } return true; } if (S.hasFrontOverlayForSideNav()) return true; if (S.isStrictVirtualNavMode()) return true; if (S.getNavBucketKey() !== null) { await S.navGoForward(); return true; } return true; }, syncNavContextState: () => { const ctx = S.getNavContext(); const sig = `${ctx}|${(Array.isArray(S.path) ? S.path.map(n => n.id).join('>') : '')}`; if (S.navSyncSig === sig) return; const prevCtx = S.navContext; const prevBucketKey = (prevCtx === 'home' || prevCtx === 'starred' || prevCtx === 'recent') ? prevCtx : null; const curBucketKey = (ctx === 'home' || ctx === 'starred' || ctx === 'recent') ? ctx : null; if (S.navSuspendRecord) { S.navContext = ctx; S.navSyncSig = sig; if (curBucketKey) S.navActiveBucket = curBucketKey; return; } if (ctx === 'virtual') { if (prevBucketKey) S.navFrozenBucket = prevBucketKey; S.navContext = 'virtual'; S.navSyncSig = sig; return; } if (ctx === 'none') { if (prevBucketKey) S.navFrozenBucket = prevBucketKey; S.navContext = 'none'; S.navSyncSig = sig; return; } S.navActiveBucket = curBucketKey; S.recordNavSnapshot(curBucketKey); if (prevCtx === 'virtual' && S.navFrozenBucket === curBucketKey) { S.navFrozenBucket = null; } else if (prevCtx !== 'virtual') { S.navFrozenBucket = null; } S.navContext = curBucketKey; S.navSyncSig = sig; }, canUseMouseSideNav: () => S.canHandleMouseSideBack() || S.canHandleMouseSideForward(), isStrictVirtualNavMode: () => { if (S.isFlattened || S.dupMode || S.analyzeMode) return true; const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; return !!(cur && typeof cur.id === 'string' && (cur.id.startsWith('virtual_') || cur.id === 'analyze_root')); }, getSelectableItems: () => { const out = []; const list = Array.isArray(S.display) ? S.display : []; for (let i = 0; i < list.length; i++) { const item = list[i]; if (!item || item.isHeader) continue; if (S.movingIds && S.movingIds.has(item.id)) continue; out.push(item); } return out; }, getSelectableIds: () => { const out = []; const list = Array.isArray(S.display) ? S.display : []; for (let i = 0; i < list.length; i++) { const item = list[i]; if (!item || item.isHeader) continue; if (S.movingIds && S.movingIds.has(item.id)) continue; out.push(item.id); } return out; }, getSelectableCount: () => { let count = 0; const list = Array.isArray(S.display) ? S.display : []; for (let i = 0; i < list.length; i++) { const item = list[i]; if (!item || item.isHeader) continue; if (S.movingIds && S.movingIds.has(item.id)) continue; count++; } return count; }, isAllSelectionMode: () => S.selMode === 'all', isSelected: (id) => { if (!id) return false; return S.selMode === 'all' ? !S.selEx.has(id) : S.sel.has(id); }, getSelectedCount: () => { if (S.selMode !== 'all') return S.sel.size; const total = S.getSelectableCount(); return Math.max(0, total - S.selEx.size); }, getSelectedIds: () => { if (S.selMode !== 'all') return Array.from(S.sel); const ids = []; const list = S.getSelectableIds(); for (let i = 0; i < list.length; i++) { const id = list[i]; if (!S.selEx.has(id)) ids.push(id); } return ids; }, setSelected: (id, selected) => { if (!id) return; if (S.selMode === 'all') { if (selected) S.selEx.delete(id); else S.selEx.add(id); return; } if (selected) S.sel.add(id); else S.sel.delete(id); }, toggleSelected: (id) => { if (!id) return false; const next = !S.isSelected(id); S.setSelected(id, next); return next; }, setExplicitSelection: (ids) => { S.selMode = 'explicit'; S.selEx.clear(); S.sel = new Set(ids || []); }, setAllSelection: (enabled = true) => { if (enabled) { S.selMode = 'all'; S.sel.clear(); S.selEx.clear(); } else { S.selMode = 'explicit'; S.sel.clear(); S.selEx.clear(); } }, invertSelection: () => { if (S.loading) return; const total = S.getSelectableCount(); if (total <= 0) { S.clearSelection(); return; } if (S.selMode === 'all') { S.setExplicitSelection(Array.from(S.selEx)); } else { const nextExcluded = new Set(S.sel); S.selMode = 'all'; S.sel.clear(); S.selEx = nextExcluded; } S.lastSelIdx = -1; S.activeId = null; }, clearSelection: () => { S.selMode = 'explicit'; S.sel.clear(); S.selEx.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 === 'history_root') return 'history_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.historyMode) return 'history_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: fixed; 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: 2147483647 !important; 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)}`; const isDark = document.querySelector('.pk-ov')?.classList.contains('pk-dark'); if (isDark) el.classList.add('pk-dark'); const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (fsEl) fsEl.appendChild(el); else document.body.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) => { const tipAttr = tip ? ` data-pk-tip="${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)} ${mkLbl('pk-chk-sim', L.tag_sim, L.tag_sim_short)} ${mkLbl('pk-chk-name', L.tag_name, L.tag_name_short)}
${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; if (tipEl.parentNode && tipEl.nextSibling) { document.body.appendChild(tipEl); } 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'); const isForceTip = target.classList.contains('pk-force-tip'); if (!isName && !isPath && !isThumb && !isUI && !isForceTip) { 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 = getLogicalRect(tipEl); const w = rect.width; const h = rect.height; 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; } if (target.closest('.pk-grid-card-row')) { 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'); const isForceTip = target.classList.contains('pk-force-tip'); if (!isName && !isPath && !isThumb && !isUI && !isForceTip) { 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'), btnDupSort: el.querySelector('#pk-dup-sort-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'), btnMigrate: el.querySelector('#pk-migrate'), 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'), btnImgSearch: el.querySelector('#pk-img-search'), 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 parent = el.querySelector('#pk-search-path-con'); const b = document.createElement('button'); b.className = 'pk-ana-select-btn'; b.id = 'pk-ana-select-btn'; b.style.marginRight = '4px'; b.innerHTML = ` ${L.btn_ana_select}`; if (parent) parent.parentNode.insertBefore(b, parent); return b; })(), btnAnaSort: (function() { const parent = el.querySelector('#pk-search-path-con'); const b = document.createElement('button'); b.className = 'pk-ana-select-btn'; b.id = 'pk-ana-sort-btn'; b.innerHTML = ` ${L.btn_in_group_sort}`; 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 isRealHomeUploadView = () => { const path = Array.isArray(S.path) ? S.path : []; const cur = path[path.length - 1] || { id: '' }; const pathStartsAtHome = path.length > 0 && path[0] && path[0].id === ''; const hasVirtualNode = path.some(p => { const id = String((p && p.id) || ''); return id === 'virtual_search_root' || id === 'analyze_root' || id.startsWith('virtual_'); }); return pathStartsAtHome && !S.trashMode && !S.shareMode && !S.offlineMode && !S.uploadMode && !S.historyMode && !S.recentMode && !S.starredMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.scanning && !S.search && !hasVirtualNode && cur.id !== 'virtual_search_root' && cur.id !== 'analyze_root'; }; const closeLocalUploadMenu = () => { if (typeof window.__pkCloseUploadMenu === 'function') { try { window.__pkCloseUploadMenu(); } catch(e) {} } if (UI.uploadWrap) UI.uploadWrap.classList.remove('active'); const menus = []; if (UI.uploadWrap) { const localMenu = UI.uploadWrap.querySelector('.pk-dropdown-menu'); if (localMenu) menus.push(localMenu); } document.querySelectorAll('.pk-dropdown-menu[data-pk-portal="1"]').forEach(m => menus.push(m)); menus.forEach(menu => { if (!menu) return; if (menu._pkPlaceRaf1) cancelAnimationFrame(menu._pkPlaceRaf1); if (menu._pkPlaceRaf2) cancelAnimationFrame(menu._pkPlaceRaf2); menu._pkPlaceRaf1 = 0; menu._pkPlaceRaf2 = 0; menu.style.display = 'none'; if (menu._pkOriginParent && menu.parentNode !== menu._pkOriginParent) { menu._pkOriginParent.appendChild(menu); } menu.style.position = ''; menu.style.top = ''; menu.style.left = ''; menu.style.right = ''; menu.style.bottom = ''; menu.style.marginTop = ''; menu.style.zIndex = ''; menu.style.minWidth = ''; menu.style.visibility = ''; menu.style.pointerEvents = ''; menu.style.zoom = ''; menu.style.transformOrigin = ''; delete menu.dataset.pkPortal; }); }; const syncLocalUploadVisibility = () => { if (!UI.uploadWrap) return; const shouldShow = isRealHomeUploadView(); UI.uploadWrap.style.display = shouldShow ? 'inline-flex' : 'none'; if (!shouldShow) closeLocalUploadMenu(); }; const getBlacklistCleanKey = (str) => String(str || '').replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const getBlacklistItemName = (item) => { if (!item) return ''; return String(item.name || item.title || '').replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim(); }; const getSelectedBlacklistItems = () => { const selectedIds = S.getSelectedIds(); const items = []; for (const id of selectedIds) { const item = S.itemMap.get(id) || (Array.isArray(S.items) ? S.items.find(x => x && x.id === id) : null) || (Array.isArray(S.display) ? S.display.find(x => x && x.id === id) : null); if (item && !item.isHeader && getBlacklistItemName(item)) items.push(item); } return items; }; const isBlacklistFolderItem = (item) => item && item.kind === 'drive#folder'; const isItemInBlacklist = (item) => { const key = getBlacklistCleanKey(getBlacklistItemName(item)); if (!key) return false; return isBlacklistFolderItem(item) ? S.blFolderSet.has(key) : S.blSet.has(key); }; const getBlacklistContextAction = () => { if (S.updateBlCache) S.updateBlCache(); const items = getSelectedBlacklistItems(); if (items.length === 0) return 'add'; return items.every(isItemInBlacklist) ? 'remove' : 'add'; }; const setBlacklistContextItemState = (el) => { if (!el) return; const action = getBlacklistContextAction(); el.innerHTML = action === 'remove' ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; el.setAttribute('data-action', action); }; const isAnalyzeGroupedRoot = () => { const cur = S.path && S.path.length ? S.path[S.path.length - 1] : null; return !!(S.analyzeMode && cur && cur.id === 'analyze_root' && S.analyzeSimGroups); }; const isGroupedResultMode = () => !!(S.dupMode || isAnalyzeGroupedRoot()); const canUseGridView = () => !S.shareMode && !S.offlineMode && !S.uploadMode && !S.historyMode && !S.trashMode; const isGridView = () => S.viewMode === 'grid' && canUseGridView(); const getListRowHeight = () => UI.win && UI.win.classList.contains('pk-maximized') ? 60 : 40; const getGridCardHeight = () => UI.win && UI.win.classList.contains('pk-maximized') ? 324 : 286; const getGridCardRadius = () => UI.win && UI.win.classList.contains('pk-maximized') ? 22 : 18; const getGridLayout = () => { const vpRect = getLogicalRect(UI.vp || UI.in); const vpWidth = Math.max(0, Math.floor((vpRect && vpRect.width) || UI.vp?.clientWidth || 0)); const isMax = UI.win && UI.win.classList.contains('pk-maximized'); const padX = isMax ? 20 : 16; const gapX = isMax ? 18 : 16; const gapY = isMax ? 20 : 16; const minCardWidth = isMax ? 236 : 208; const innerWidth = Math.max(minCardWidth, vpWidth - padX * 2); const cols = Math.max(1, Math.floor((innerWidth + gapX) / (minCardWidth + gapX))); const cardWidth = Math.max(188, Math.floor((innerWidth - gapX * (cols - 1)) / cols)); const cardHeight = Math.max(220, CONF.rowHeight - gapY); return { padX, gapX, gapY, cols, cardWidth, cardHeight }; }; const isGroupedGridView = () => !!(isGroupedResultMode() && isGridView()); const getGroupedGridSectionMetrics = () => { const isMax = !!(UI.win && UI.win.classList.contains('pk-maximized')); return { headerHeight: isMax ? CONF.dupGridHeaderHeight.max : CONF.dupGridHeaderHeight.normal, sectionGap: isMax ? CONF.dupGridSectionGap.max : CONF.dupGridSectionGap.normal, bodyGapY: isMax ? CONF.dupGridBodyGapY.max : CONF.dupGridBodyGapY.normal }; }; const buildGroupedGridMetaKey = () => { const baseKey = getGridLayoutKey(); const display = Array.isArray(S.display) ? S.display : []; const displayLen = display.length; if (!displayLen) return `${baseKey}::grouped-grid::empty`; let firstId = ''; let lastId = ''; let runSig = ''; let prevRunKey = null; let runCount = 0; for (let i = 0; i < displayLen; i++) { const item = display[i]; if (!item) continue; const itemId = item.id || ''; if (!firstId) firstId = itemId; lastId = itemId || lastId; const runKey = item.isHeader ? `h:${itemId || i}` : `i:${S.dupGroups.has(itemId) ? S.dupGroups.get(itemId) : -1}`; if (runKey !== prevRunKey) { runSig += `${prevRunKey === null ? '' : '|'}${runKey}`; prevRunKey = runKey; runCount++; } } return `${baseKey}::grouped-grid::${displayLen}::${runCount}::${firstId}::${lastId}::${runSig}`; }; const getGroupedGridMeta = (force = false) => { if (!isGroupedGridView()) { S.dupGridMeta = null; S.dupGridMetaKey = ''; return null; } const metaKey = buildGroupedGridMetaKey(); if (!force && S.dupGridMeta && S.dupGridMetaKey === metaKey) return S.dupGridMeta; const display = Array.isArray(S.display) ? S.display : []; const gridLayout = getGridLayout(); const sectionMetrics = getGroupedGridSectionMetrics(); const cols = Math.max(1, gridLayout.cols); const sectionGapY = Math.max(0, gridLayout.gapY || 0); const vpRect = getLogicalRect(UI.vp || UI.in); const fullWidth = Math.max(0, Math.floor((vpRect && vpRect.width) || UI.vp?.clientWidth || 0)); const sections = []; const indexLayout = new Map(); const indexToSection = new Map(); let current = null; let totalHeight = 0; const pushSection = () => { if (!current) return; const itemCount = current.itemIndices.length; const rows = itemCount > 0 ? Math.ceil(itemCount / cols) : 0; const headerTop = totalHeight; const bodyTop = headerTop + sectionMetrics.headerHeight + (itemCount > 0 ? sectionGapY : 0); const bodyHeight = rows * CONF.rowHeight; const sectionHeight = sectionMetrics.headerHeight + (itemCount > 0 ? (sectionGapY + bodyHeight) : 0); const section = { key: current.key, groupIndex: current.groupIndex, reason: current.reason, title: current.title, headerIndex: current.headerIndex, itemIndices: [...current.itemIndices], itemIds: [...current.itemIds], itemCount, rows, cols, top: headerTop, headerHeight: sectionMetrics.headerHeight, bodyTop, bodyHeight, totalHeight: sectionHeight, bottom: headerTop + sectionHeight }; if (current.headerIndex !== null) { indexLayout.set(current.headerIndex, { kind: 'header', sectionKey: current.key, top: headerTop, left: 0, width: fullWidth, height: sectionMetrics.headerHeight }); indexToSection.set(current.headerIndex, section); } for (let localIdx = 0; localIdx < current.itemIndices.length; localIdx++) { const displayIndex = current.itemIndices[localIdx]; const rowIdx = Math.floor(localIdx / cols); const colIdx = localIdx % cols; indexLayout.set(displayIndex, { kind: 'item', sectionKey: current.key, localIndex: localIdx, rowIdx, colIdx, top: bodyTop + rowIdx * CONF.rowHeight, left: gridLayout.padX + colIdx * (gridLayout.cardWidth + gridLayout.gapX), width: gridLayout.cardWidth, height: gridLayout.cardHeight }); indexToSection.set(displayIndex, section); } sections.push(section); totalHeight += sectionHeight; current = null; }; for (let i = 0; i < display.length; i++) { const d = display[i]; if (!d) continue; if (d.isHeader) { pushSection(); const parsedGroupIndex = /^grp_\d+$/.test(d.id || '') ? parseInt(String(d.id).replace('grp_', ''), 10) : -1; current = { key: d.id || `grp_auto_${i}`, headerIndex: i, groupIndex: parsedGroupIndex, reason: d.type || '', title: d.name || '', itemIndices: [], itemIds: [] }; continue; } if (!current) { current = { key: `loose_${i}`, headerIndex: null, groupIndex: S.dupGroups.has(d.id) ? S.dupGroups.get(d.id) : -1, reason: S.dupReasons.get(d.id) || '', title: '', itemIndices: [], itemIds: [] }; } current.itemIndices.push(i); if (d.id) current.itemIds.push(d.id); } pushSection(); if (sections.length > 0) totalHeight = Math.max(0, totalHeight); S.dupGridMetaKey = metaKey; S.dupGridMeta = { key: metaKey, cols, gridLayout, sectionMetrics, fullWidth, totalHeight: Math.max(0, totalHeight), sections, indexLayout, indexToSection }; return S.dupGridMeta; }; const findDupGridHitIndex = (logicalOffsetX, logicalOffsetY) => { if (!isGroupedGridView()) return -1; const dupGridMeta = getGroupedGridMeta(); if (!dupGridMeta || !dupGridMeta.sections || !dupGridMeta.sections.length) return -1; const gridLayout = dupGridMeta.gridLayout || getGridLayout(); if (logicalOffsetX < gridLayout.padX || logicalOffsetY < 0) return -1; const colStride = gridLayout.cardWidth + gridLayout.gapX; const relX = logicalOffsetX - gridLayout.padX; const colIdx = Math.floor(relX / Math.max(1, colStride)); const withinColX = relX - colIdx * colStride; if (colIdx < 0 || colIdx >= gridLayout.cols || withinColX < 0 || withinColX > gridLayout.cardWidth) return -1; for (const section of dupGridMeta.sections) { if (logicalOffsetY < section.top) break; if (logicalOffsetY >= section.bottom) continue; if (logicalOffsetY < section.bodyTop || logicalOffsetY >= section.bodyTop + section.bodyHeight) return -1; const relY = logicalOffsetY - section.bodyTop; const rowIdx = Math.floor(relY / Math.max(1, CONF.rowHeight)); const withinRowY = relY - rowIdx * CONF.rowHeight; if (rowIdx < 0 || rowIdx >= section.rows || withinRowY < 0 || withinRowY > gridLayout.cardHeight) return -1; const localIdx = rowIdx * gridLayout.cols + colIdx; if (localIdx < 0 || localIdx >= section.itemIndices.length) return -1; return section.itemIndices[localIdx]; } return -1; }; const getDupGridAnchorIndex = (logicalCenterX, logicalCenterY) => { if (!isGroupedGridView()) return -1; const dupGridMeta = getGroupedGridMeta(); if (!dupGridMeta) return -1; const hitIdx = findDupGridHitIndex(logicalCenterX, logicalCenterY); if (hitIdx >= 0) return hitIdx; let bestIdx = -1; let bestScore = Infinity; dupGridMeta.indexLayout.forEach((layout, idx) => { if (!layout || layout.kind !== 'item') return; const cx = layout.left + (layout.width / 2); const cy = layout.top + (Math.min(layout.height, CONF.rowHeight) / 2); const score = Math.abs(cy - logicalCenterY) * 4 + Math.abs(cx - logicalCenterX); if (score < bestScore) { bestScore = score; bestIdx = idx; } }); return bestIdx; }; const getItemScrollTopByIndex = (targetIdx) => { if (targetIdx < 0) return 0; if (isGridView()) { if (isGroupedGridView()) { const dupGridMeta = getGroupedGridMeta(); const layout = dupGridMeta && dupGridMeta.indexLayout ? dupGridMeta.indexLayout.get(targetIdx) : null; if (layout) return Math.max(0, layout.top); } const gridLayout = getGridLayout(); const visualRow = Math.floor(targetIdx / Math.max(1, gridLayout.cols)); return gridLayout.gapY + visualRow * CONF.rowHeight; } return targetIdx * CONF.rowHeight; }; const getViewportAnchorId = (preferSelection = false) => { const selectedIds = S.getSelectedIds(); const fallbackId = S.activeId || (selectedIds.length ? selectedIds[selectedIds.length - 1] : null); if (preferSelection && fallbackId && S.display.some(x => x.id === fallbackId)) return fallbackId; if (!UI.vp || !S.display.length) return fallbackId; const centerY = UI.vp.scrollTop + (UI.vp.clientHeight / 2); if (isGridView()) { const gridLayout = getGridLayout(); if (isGroupedGridView()) { const usableWidth = Math.max(1, UI.vp.clientWidth - gridLayout.padX * 2); const logicalCenterX = gridLayout.padX + (usableWidth / 2); const targetIdx = getDupGridAnchorIndex(logicalCenterX, centerY); if (targetIdx >= 0 && targetIdx < S.display.length) { return S.display[targetIdx]?.id || fallbackId; } return fallbackId; } const cols = Math.max(1, gridLayout.cols); const totalRows = Math.max(1, Math.ceil(S.display.length / cols)); const adjustedCenterY = Math.max(0, centerY - gridLayout.gapY); const visualRow = Math.max(0, Math.min(totalRows - 1, Math.floor(adjustedCenterY / Math.max(1, CONF.rowHeight)))); const usableWidth = Math.max(1, UI.vp.clientWidth - gridLayout.padX * 2); const centerX = usableWidth / 2; const visualCol = Math.max(0, Math.min(cols - 1, Math.floor(centerX / Math.max(1, gridLayout.cardWidth + gridLayout.gapX)))); const targetIdx = Math.min(S.display.length - 1, visualRow * cols + visualCol); return S.display[targetIdx]?.id || fallbackId; } const targetIdx = Math.max(0, Math.min(S.display.length - 1, Math.floor(centerY / Math.max(1, CONF.rowHeight)))); return S.display[targetIdx]?.id || fallbackId; }; const syncLayoutMetrics = () => { CONF.rowHeight = isGridView() ? getGridCardHeight() : getListRowHeight(); }; const getGridLayoutKey = () => { const vpRect = getLogicalRect(UI.vp || UI.in); const vpWidth = Math.max(0, Math.floor((vpRect && vpRect.width) || UI.vp?.clientWidth || 0)); const vpHeight = Math.max(0, Math.floor(UI.vp?.clientHeight || 0)); const gridLayout = getGridLayout(); const isMax = UI.win && UI.win.classList.contains('pk-maximized') ? 1 : 0; return [vpWidth, vpHeight, gridLayout.cols, gridLayout.cardWidth, gridLayout.cardHeight, isMax].join(':'); }; const resetViewportRenderState = () => { if (S._gridRelayoutRaf1) { cancelAnimationFrame(S._gridRelayoutRaf1); S._gridRelayoutRaf1 = 0; } if (S._gridRelayoutRaf2) { cancelAnimationFrame(S._gridRelayoutRaf2); S._gridRelayoutRaf2 = 0; } if (!UI.in) return; const pool = UI.in._pkVisibleRowPool || []; for (let i = 0; i < pool.length; i++) { const row = pool[i]; if (!row) continue; if (typeof resetPooledRow === 'function') resetPooledRow(row); if (row.parentNode === UI.in) UI.in.removeChild(row); } if (typeof cleanupNonPooledChildren === 'function') { cleanupNonPooledChildren(UI.in); } }; const beginFolderViewSync = () => { if (S._folderViewSyncing) return; S._folderViewSyncing = true; resetViewportRenderState(); if (UI.vp) { UI.vp.style.visibility = 'hidden'; UI.vp.style.pointerEvents = 'none'; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = 'hidden'; }; const endFolderViewSync = (force = false) => { if (!S._folderViewSyncing) return; if (S._folderViewSyncHold && !force) return; requestAnimationFrame(() => { if (S._folderViewSyncHold && !force) return; if (UI.vp) { UI.vp.style.visibility = ''; UI.vp.style.pointerEvents = ''; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = ''; S._folderViewSyncing = false; }); }; const scheduleGridRelayout = (forceFull = false) => { if (!isGridView()) { endFolderViewSync(); return; } if (S._gridRelayoutRaf1) cancelAnimationFrame(S._gridRelayoutRaf1); if (S._gridRelayoutRaf2) cancelAnimationFrame(S._gridRelayoutRaf2); const prevKey = S._gridLayoutKey || ''; S._gridRelayoutRaf1 = requestAnimationFrame(() => { S._gridRelayoutRaf2 = requestAnimationFrame(() => { if (!UI.vp || !UI.in || !isGridView()) { endFolderViewSync(); return; } const nextKey = getGridLayoutKey(); if (forceFull || nextKey !== prevKey) { S._gridLayoutKey = nextKey; renderList(); } else if (typeof renderVisible === 'function') { renderVisible(); endFolderViewSync(); } else { endFolderViewSync(); } }); }); }; const ensureGridMediaStore = () => { if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); if (!window.pkThumbTriState) window.pkThumbTriState = { folder: Object.create(null), file: Object.create(null) }; if (!window.pkThumbTriState.folder) window.pkThumbTriState.folder = Object.create(null); if (!window.pkThumbTriState.file) window.pkThumbTriState.file = Object.create(null); if (!window.pkGridMediaStore) window.pkGridMediaStore = { folder: Object.create(null), file: Object.create(null) }; if (!window.pkGridMediaStore.folder) window.pkGridMediaStore.folder = Object.create(null); if (!window.pkGridMediaStore.file) window.pkGridMediaStore.file = Object.create(null); return window.pkGridMediaStore; }; const getStableFolderMediaState = (item) => { ensureGridMediaStore(); const tri = window.pkThumbTriState.folder; const store = window.pkGridMediaStore.folder; const id = item && item.id; if (!id) return { state: 'none', src: '' }; const state = tri[id] || 'unknown'; const cached = store[id] || ''; if (state === 'ok' && cached) return { state: 'ok', src: cached }; if (state === 'ok' && item.thumbnail_link) { store[id] = item.thumbnail_link; return { state: 'ok', src: item.thumbnail_link }; } if ((state === 'unknown' || state === 'probing') && cached) return { state: 'ok', src: cached }; return { state, src: '' }; }; const markStableFolderMediaState = (id, state, src = '') => { ensureGridMediaStore(); if (!id) return; window.pkThumbTriState.folder[id] = state; if (state === 'ok' && src) { window.pkGridMediaStore.folder[id] = src; window.pkGlobalThumbCache.add(id); } else if (state === 'fail' && window.pkGridMediaStore.folder[id]) { delete window.pkGridMediaStore.folder[id]; } }; const getStableFileMediaState = (item) => { ensureGridMediaStore(); const tri = window.pkThumbTriState.file; const store = window.pkGridMediaStore.file; const id = item && item.id; if (!id) return { state: 'none', src: '' }; const state = tri[id] || 'unknown'; const cached = store[id] || ''; if (state === 'ok' && cached) return { state: 'ok', src: cached }; if (state === 'ok' && item.thumbnail_link) { store[id] = item.thumbnail_link; return { state: 'ok', src: item.thumbnail_link }; } if ((state === 'unknown' || state === 'probing') && cached) return { state: 'ok', src: cached }; return { state, src: '' }; }; const markStableFileMediaState = (id, state, src = '') => { ensureGridMediaStore(); if (!id) return; window.pkThumbTriState.file[id] = state; if (state === 'ok' && src) { window.pkGridMediaStore.file[id] = src; window.pkGlobalThumbCache.add(id); } else if (state === 'fail' && window.pkGridMediaStore.file[id]) { delete window.pkGridMediaStore.file[id]; } }; window.ensureGridMediaStore = ensureGridMediaStore; window.getStableFolderMediaState = getStableFolderMediaState; window.markStableFolderMediaState = markStableFolderMediaState; window.getStableFileMediaState = getStableFileMediaState; window.markStableFileMediaState = markStableFileMediaState; const ensureGridMediaDomCache = () => { if (!window.pkGridMediaDomCache) window.pkGridMediaDomCache = { folder: Object.create(null), file: Object.create(null) }; return window.pkGridMediaDomCache; }; const isTrustedChildRealThumb = (child) => { if (!child || !child.thumbnail_link || child.thumbnail_link === child.icon_link) return false; const mime = (child.mime_type || '').toLowerCase(); const duration = Number((child.params && child.params.duration) || (child.medias && child.medias[0] && child.medias[0].duration) || 0); const hasMediaMeta = Array.isArray(child.medias) && child.medias.length > 0; const stableState = child.id && typeof getStableFileMediaState === 'function' ? getStableFileMediaState(child).state : 'unknown'; return stableState === 'ok' || mime.startsWith('video/') || mime.startsWith('image/') || duration > 0 || hasMediaMeta; }; const getFolderEmbedCacheStamp = (item) => { if (!item || item.kind !== 'drive#folder') return ''; if (typeof globalCache === 'undefined') return 'nogc'; const normalize = (data) => (data && !Array.isArray(data) && data.items) ? data.items : data; const raw = globalCache.get(item.id); if (!raw) return 'miss'; if (!Array.isArray(raw) && raw.nextToken) { const pending = normalize(raw); const len = Array.isArray(pending) ? pending.filter(Boolean).length : 0; return `loading:${len}`; } const children = normalize(raw); if (!Array.isArray(children)) return 'invalid'; const visibleChildren = children.filter(Boolean); if (!visibleChildren.length) return 'empty'; const nonFolders = visibleChildren.filter(child => child.kind !== 'drive#folder'); if (!nonFolders.length) { return `folders:${visibleChildren.length}:${visibleChildren.slice(0, 3).map(child => child.id || '').join(',')}`; } const hasRealChildThumb = (child) => isTrustedChildRealThumb(child); const isFallbackPlaceholderChild = (child) => { const mime = (child.mime_type || '').toLowerCase(); const duration = (child.params && child.params.duration) || 0; return !hasRealChildThumb(child) && (mime.startsWith('video/') || mime.startsWith('image/') || duration > 0); }; const realThumbChild = nonFolders.find(hasRealChildThumb); if (realThumbChild) { const stableChildMedia = typeof getStableFileMediaState === 'function' ? getStableFileMediaState(realThumbChild) : { state: 'unknown', src: '' }; const realThumbSrc = stableChildMedia.src || realThumbChild.thumbnail_link || ''; return ['real', realThumbChild.id || '', realThumbSrc || '', realThumbChild.icon_link || '', realThumbChild.mime_type || '', realThumbChild.name || ''].join(':'); } if (nonFolders.every(isFallbackPlaceholderChild)) { const target = nonFolders[0] || {}; return ['fallback', target.id || '', target.icon_link || '', target.thumbnail_link || '', target.mime_type || '', target.name || ''].join(':'); } const ordinaryPlaceholderChild = nonFolders.find(child => !hasRealChildThumb(child) && !isFallbackPlaceholderChild(child)); if (ordinaryPlaceholderChild) { return ['ordinary', ordinaryPlaceholderChild.id || '', ordinaryPlaceholderChild.icon_link || '', ordinaryPlaceholderChild.thumbnail_link || '', ordinaryPlaceholderChild.mime_type || '', ordinaryPlaceholderChild.name || ''].join(':'); } return `blank:${visibleChildren.length}:${nonFolders.length}`; }; const makeGridMediaCacheKey = (item) => [item?.id || '', item?.kind || '', item?.thumbnail_link || '', item?.icon_link || '', getFolderEmbedCacheStamp(item)].join('::'); const stashFrozenGridMediaNode = (itemId, mediaKey, node, kind = '') => { if (!itemId || !mediaKey || !node) return; const cache = ensureGridMediaDomCache(); const bucket = kind === 'drive#folder' ? cache.folder : cache.file; bucket[itemId] = { key: mediaKey, node }; }; const takeFrozenGridMediaNode = (item) => { if (!item || !item.id) return null; const cache = ensureGridMediaDomCache(); const bucket = item.kind === 'drive#folder' ? cache.folder : cache.file; const hit = bucket[item.id]; const mediaKey = makeGridMediaCacheKey(item); if (!hit || hit.key !== mediaKey || !hit.node) return null; delete bucket[item.id]; return hit.node; }; window.ensureGridMediaDomCache = ensureGridMediaDomCache; window.stashFrozenGridMediaNode = stashFrozenGridMediaNode; window.takeFrozenGridMediaNode = takeFrozenGridMediaNode; const buildGridFolderPreview = (folderItem, folderIconHtml) => { const hasThumb = !!(folderItem.thumbnail_link && folderItem.thumbnail_link !== folderItem.icon_link); const stableFolderMedia = hasThumb ? getStableFolderMediaState(folderItem) : { state: 'none', src: '' }; const folderThumbState = stableFolderMedia.state; const folderResolvedSrc = stableFolderMedia.src || folderItem.thumbnail_link || ''; const folderHasCoverThumb = hasThumb && folderThumbState === 'ok' && !!folderResolvedSrc; if (hasThumb && folderThumbState === 'unknown' && !folderResolvedSrc && folderItem.id && window.pkThumbTriState && window.pkThumbTriState.folder) { window.pkThumbTriState.folder[folderItem.id] = 'probing'; } const makePreviewBaseHtml = (display = 'flex', opacity = '1', iconSrc = folderItem.icon_link, svgHtml = folderIconHtml) => iconSrc ? `` : `${svgHtml}`; const makeItemPlaceholderHtml = (child) => { const childIconHtml = getIcon(child).replace(/width="\d+"/g, 'width="108"').replace(/height="\d+"/g, 'height="108"'); return makePreviewBaseHtml('flex', '1', child.icon_link, childIconHtml); }; const resolveCachedPreview = () => { const normalize = (data) => (data && !Array.isArray(data) && data.items) ? data.items : data; if (typeof globalCache === 'undefined') return { decided: false, hasThumb: false, hidePreview: false, html: '' }; const raw = globalCache.get(folderItem.id); if (!raw) return { decided: false, hasThumb: false, hidePreview: false, html: '' }; if (!Array.isArray(raw) && raw.nextToken) { return { decided: false, hasThumb: false, hidePreview: false, html: '' }; } const children = normalize(raw); if (!Array.isArray(children)) return { decided: false, hasThumb: false, hidePreview: false, html: '' }; if (children.length === 0) return { decided: true, hasThumb: false, hidePreview: true, html: '' }; const visibleChildren = children.filter(Boolean); if (!visibleChildren.length) return { decided: true, hasThumb: false, hidePreview: true, html: '' }; const nonFolders = visibleChildren.filter(child => child.kind !== 'drive#folder'); if (!nonFolders.length) { return { decided: true, hasThumb: false, hidePreview: false, html: makePreviewBaseHtml('flex', '1') }; } const hasRealChildThumb = (child) => isTrustedChildRealThumb(child); const isFallbackPlaceholderChild = (child) => { const mime = (child.mime_type || '').toLowerCase(); const duration = (child.params && child.params.duration) || 0; return !hasRealChildThumb(child) && (mime.startsWith('video/') || mime.startsWith('image/') || duration > 0); }; const makeChildRealThumbHtml = (child) => { const stableChildMedia = typeof getStableFileMediaState === 'function' ? getStableFileMediaState(child) : { state: 'unknown', src: '' }; const childThumbState = stableChildMedia.state; const childResolvedSrc = stableChildMedia.src || child.thumbnail_link || ''; if (!childResolvedSrc || childThumbState === 'fail') return ''; if (childThumbState === 'unknown' && child.id && window.pkThumbTriState && window.pkThumbTriState.file) { window.pkThumbTriState.file[child.id] = 'probing'; } const imgOpacity = childThumbState === 'ok' ? '1' : '0'; const baseOpacity = childThumbState === 'ok' ? '0' : '1'; return `
${makePreviewBaseHtml('flex', baseOpacity, folderItem.icon_link, folderIconHtml)}
`; }; const realThumbChild = nonFolders.find(hasRealChildThumb); if (realThumbChild) { const realThumbHtml = makeChildRealThumbHtml(realThumbChild); if (realThumbHtml) { return { decided: true, hasThumb: true, hidePreview: false, html: realThumbHtml }; } } if (nonFolders.every(isFallbackPlaceholderChild)) { return { decided: true, hasThumb: false, hidePreview: false, html: makeItemPlaceholderHtml(nonFolders[0]) }; } const ordinaryPlaceholderChild = nonFolders.find(child => !hasRealChildThumb(child) && !isFallbackPlaceholderChild(child)); if (ordinaryPlaceholderChild) { return { decided: true, hasThumb: false, hidePreview: false, html: makeItemPlaceholderHtml(ordinaryPlaceholderChild) }; } return { decided: true, hasThumb: false, hidePreview: false, html: '' }; }; if (folderHasCoverThumb) { return { hasThumb: true, hidePreview: false, html: `${makePreviewBaseHtml('none', '1')}` }; } const cachedPreview = resolveCachedPreview(); const probeBasePreview = cachedPreview.decided ? cachedPreview : { hasThumb: false, hidePreview: false, html: '' }; if (hasThumb && folderThumbState !== 'fail') { return { hasThumb: false, hidePreview: false, html: `${probeBasePreview.html}` }; } if (cachedPreview.decided) { return { hasThumb: cachedPreview.hasThumb, hidePreview: cachedPreview.hidePreview, html: cachedPreview.html }; } return { hasThumb: false, hidePreview: false, html: '' }; }; const renderFrozenGridMedia = (item, iconFallback, gridFileFallbackHtml) => { const isFolder = item.kind === 'drive#folder'; const hasThumb = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); if (isFolder) { const folderEmbed = buildGridFolderPreview(item, iconFallback); return `
`; } const makeFileFallbackHtml = (opacity = '1') => item.icon_link ? `
${iconFallback}
` : `
${iconFallback}
`; if (hasThumb) { const stableFileMedia = getStableFileMediaState(item); const fileThumbState = stableFileMedia.state; const fileResolvedSrc = stableFileMedia.src || item.thumbnail_link || ''; if (fileThumbState === 'fail' || !fileResolvedSrc) { return makeFileFallbackHtml(); } if (fileThumbState === 'unknown') { window.pkThumbTriState.file[item.id] = 'probing'; } const imgOpacity = fileThumbState === 'ok' ? '1' : '0'; const baseOpacity = fileThumbState === 'ok' ? '0' : '1'; return `
${makeFileFallbackHtml(baseOpacity)}
`; } return makeFileFallbackHtml(); }; const isGridVideoItem = (item) => { const mime = ((item && item.mime_type) || '').toLowerCase(); const duration = Number((item && item.params && item.params.duration) || (item && item.medias && item.medias[0] && item.medias[0].duration) || 0); return mime.startsWith('video/') || duration > 0; }; const syncGridVideoPlayState = (scope, item = null) => { const root = scope && scope.nodeType === 1 ? (scope.matches('.pk-grid-card-row, .pk-row') ? scope : (scope.closest('.pk-grid-card-row, .pk-row') || scope)) : null; if (!root) return; const mount = root.querySelector('.pk-gv-media-mount'); const play = root.querySelector('.pk-gv-play'); if (!play) return; const isVideo = item ? isGridVideoItem(item) : !!(mount && mount.dataset && mount.dataset.pkIsVideo === '1'); const hasThumb = item ? !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link) : !!(mount && mount.dataset && mount.dataset.pkHasThumb === '1'); if (!mount || !isVideo || !hasThumb) { play.style.display = 'none'; return; } const wrap = mount.firstElementChild; const thumb = mount.querySelector('img.pk-max-thumb'); const wrapperReady = !!(wrap && wrap.dataset && wrap.dataset.pkThumbReady === '1'); const imgReady = !!(thumb && thumb.complete && (thumb.naturalWidth > 1 || thumb.naturalHeight > 1)); play.style.display = (wrapperReady || imgReady) ? 'flex' : 'none'; }; window.syncGridVideoPlayState = syncGridVideoPlayState; const patchFrozenGridMedia = (row, item, prevMediaNode = null, prevMediaKey = '') => { const mount = row.querySelector('.pk-gv-media-mount'); if (!mount || !item) return; const mediaKey = makeGridMediaCacheKey(item); const iconFallback = mount.dataset.pkIconFallback || ''; const gridFileFallbackHtml = mount.dataset.pkFileFallback || ''; mount.dataset.pkIsVideo = isGridVideoItem(item) ? '1' : '0'; mount.dataset.pkHasThumb = (item.thumbnail_link && item.thumbnail_link !== item.icon_link) ? '1' : '0'; const syncGridVideoPlay = () => { if (typeof syncGridVideoPlayState === 'function') { syncGridVideoPlayState(row, item); } }; const queueSyncGridVideoPlay = () => requestAnimationFrame(syncGridVideoPlay); const cachedMediaNode = typeof takeFrozenGridMediaNode === 'function' ? takeFrozenGridMediaNode(item) : null; if (cachedMediaNode) { mount.innerHTML = ''; mount.appendChild(cachedMediaNode); mount.dataset.pkMediaKey = mediaKey; queueSyncGridVideoPlay(); return; } if (prevMediaNode && prevMediaKey === mediaKey) { mount.innerHTML = ''; mount.appendChild(prevMediaNode); mount.dataset.pkMediaKey = mediaKey; queueSyncGridVideoPlay(); return; } if (mount.dataset.pkMediaKey === mediaKey && mount.firstElementChild) { queueSyncGridVideoPlay(); return; } mount.dataset.pkMediaKey = mediaKey; mount.innerHTML = renderFrozenGridMedia(item, iconFallback, gridFileFallbackHtml); queueSyncGridVideoPlay(); }; window.renderFrozenGridMedia = renderFrozenGridMedia; window.patchFrozenGridMedia = patchFrozenGridMedia; const getRowClassName = (isSelected, isFocused, isMoving) => { let cls = 'pk-row'; if (isGridView()) cls += ' pk-grid-card-row'; if (isSelected) cls += ' sel'; if (isSelected && isGridView() && S.getSelectedCount() === 1) cls += ' pk-sel-single'; if (isFocused) cls += ' pk-focused'; if (isMoving) cls += ' pk-moving'; return cls; }; const ensureVisibleRowPool = (container, need) => { if (!container._pkVisibleRowPool) container._pkVisibleRowPool = []; const pool = container._pkVisibleRowPool; while (pool.length < need) { const row = document.createElement('div'); row._pkPooledRow = true; row.style.position = 'absolute'; pool.push(row); } return pool; }; const resetPooledRow = (row) => { row.className = ''; row.innerHTML = ''; row.onclick = null; row.ondblclick = null; row.oncontextmenu = null; row.onmouseover = null; row.onmouseout = null; row.onmouseenter = null; row.onmouseleave = null; row.onmousedown = null; row.onmouseup = null; row.style.cssText = 'position:absolute;'; row.removeAttribute('data-id'); row.removeAttribute('data-pk-media-key'); row.removeAttribute('data-pk-thumb'); row.removeAttribute('data-pk-bound-id'); row.removeAttribute('data-pk-bound-kind'); delete row.dataset.pkBoundId; delete row.dataset.pkBoundKind; return row; }; const cleanupNonPooledChildren = (container) => { Array.from(container.children).forEach(node => { if (!node._pkPooledRow) container.removeChild(node); }); }; const flushVisibleRowPool = (container, usedCount) => { const pool = container._pkVisibleRowPool || []; for (let i = 0; i < pool.length; i++) { const row = pool[i]; if (i < usedCount) { if (row.parentNode !== container) container.appendChild(row); } else if (row.parentNode === container) { container.removeChild(row); } } }; const refreshVisibleSelectionState = () => { const pool = (UI.in && UI.in._pkVisibleRowPool) || []; const singleGridSel = isGridView() && S.getSelectedCount() === 1; for (const row of pool) { if (!row || row.parentNode !== UI.in) continue; if (row.classList.contains('pk-group-hd')) { const grpChk = row.querySelector('.pk-grp-chk'); if (!grpChk) continue; const gIds = Array.isArray(row._pkGroupIds) ? row._pkGroupIds : []; let selCount = 0; gIds.forEach(id => { if (S.isSelected(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; continue; } const boundId = row.dataset.pkBoundId || ''; if (!boundId) continue; const isSelected = S.isSelected(boundId); const isFocused = S.activeId === boundId; const isMoving = S.movingIds.has(boundId); row.className = getRowClassName(isSelected, isFocused, isMoving); const chk = row.querySelector('input[type="checkbox"]'); if (chk && chk.checked !== isSelected) chk.checked = isSelected; if (singleGridSel && isSelected) row.classList.add('pk-sel-single'); else row.classList.remove('pk-sel-single'); if (isFocused && !isSelected) { row.style.backgroundColor = 'var(--pk-sel-bg)'; row.style.border = '1px solid var(--pk-pri)'; row.style.borderRadius = isGridView() ? `${getGridCardRadius()}px` : '4px'; } else { row.style.backgroundColor = ''; row.style.border = ''; row.style.borderRadius = ''; } } }; const resolvePreferredViewMode = (folderId = null) => { const curNode = folderId !== null ? { id: folderId } : (S.path[S.path.length - 1] || { id: 'root' }); const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curNode.id.startsWith('virtual_') || curNode.id === 'virtual_search_root'); if (!isStandard) return gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'; if (gmGet('pk_view_independent', false)) { const safeId = folderId !== null ? (folderId || 'root') : (curNode.id || 'root'); try { const prefStore = JSON.parse(gmGet('pk_folder_view_prefs', '{}')); const saved = prefStore[safeId]; const mode = typeof saved === 'string' ? saved : (saved && saved.viewMode); return mode === 'list' ? 'list' : 'grid'; } catch(e) { return 'grid'; } } return gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'; }; const persistViewPreference = () => { const nextMode = S.viewMode === 'grid' ? 'grid' : 'list'; gmSet('pk_file_view_mode', nextMode); const curNode = S.path[S.path.length - 1]; const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curNode.id.startsWith('virtual_') || curNode.id === 'virtual_search_root'); if (!isStandard) return; if (gmGet('pk_view_independent', false)) { const folderId = curNode.id || 'root'; try { const prefStore = JSON.parse(gmGet('pk_folder_view_prefs', '{}')); prefStore[folderId] = nextMode; gmSet('pk_folder_view_prefs', JSON.stringify(prefStore)); } catch(e) {} } }; const persistSortPreference = () => { 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) return; if (gmGet('pk_sort_independent', false)) { const folderId = curNode.id || 'root'; try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); const prev = prefStore[folderId] || {}; prefStore[folderId] = { ...prev, sort: S.sort, dir: S.dir }; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } catch(e) {} } else { try { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); globalPref.sort = S.sort; globalPref.dir = S.dir; globalPref.folderFirst = S.folderFirst; gmSet('pk_global_sort_pref', JSON.stringify(globalPref)); } catch(e) { gmSet('pk_global_sort_pref', JSON.stringify({ sort: S.sort, dir: S.dir, folderFirst: S.folderFirst })); } } try { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); globalPref.folderFirst = S.folderFirst; gmSet('pk_global_sort_pref', JSON.stringify(globalPref)); } catch(e) { gmSet('pk_global_sort_pref', JSON.stringify({ sort: S.sort, dir: S.dir, folderFirst: S.folderFirst })); } gmSet('pk_folder_first', S.folderFirst); }; const getGridSortOptions = () => { const cur = S.path[S.path.length - 1] || { id: 'root' }; 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; const supportsDurationTypeSort = !isAnalyzeRoot; const opts = [ { sort: 'starred', ascLabel: L.picker_sort_star_new, descLabel: L.picker_sort_star_old, ascIcon: CONF.crumbIcons.sortStarNew, descIcon: CONF.crumbIcons.sortStarOld }, { sort: 'name', ascLabel: 'A-Z', descLabel: 'Z-A', ascIcon: CONF.crumbIcons.sortAZ, descIcon: CONF.crumbIcons.sortZA }, { sort: 'size', ascLabel: L.picker_sort_large, descLabel: L.picker_sort_small, ascIcon: CONF.crumbIcons.sortLarge, descIcon: CONF.crumbIcons.sortSmall }, { sort: 'modified_time', ascLabel: L.picker_sort_new, descLabel: L.picker_sort_old, ascIcon: CONF.crumbIcons.sortNew, descIcon: CONF.crumbIcons.sortOld } ]; if (supportsPathSort) { opts.splice(2, 0, { sort: 'path', ascLabel: L.picker_sort_path_asc, descLabel: L.picker_sort_path_desc, ascIcon: CONF.crumbIcons.sortPathAsc, descIcon: CONF.crumbIcons.sortPathDesc } ); } if (supportsDurationTypeSort) { opts.splice(opts.length - 1, 0, { sort: 'duration', ascLabel: L.picker_sort_type_dur_asc, descLabel: L.picker_sort_type_dur_desc, ascIcon: CONF.crumbIcons.sortTypeDurAsc, descIcon: CONF.crumbIcons.sortTypeDurDesc } ); } return opts; }; const resolveGridSortOptionState = (opt) => { const isActive = opt.sort === S.sort; const dir = isActive ? S.dir : 1; return { ...opt, dir, active: isActive, label: dir === -1 ? opt.descLabel : opt.ascLabel, icon: dir === -1 ? opt.descIcon : opt.ascIcon }; }; const getGridSortMeta = () => { const opts = getGridSortOptions(); const target = opts.find(opt => opt.sort === S.sort) || opts[0]; return resolveGridSortOptionState(target); }; const closeGridSortMenu = (evt) => { if (!UI.gridSortWrap) return; if (!evt) { UI.gridSortWrap.classList.remove('open'); S.gridSortMenuOpen = false; return; } const target = evt.target; const isNodeTarget = !!target && typeof target === 'object' && typeof target.nodeType === 'number'; if (typeof evt.composedPath === 'function') { const path = evt.composedPath(); if (Array.isArray(path) && path.includes(UI.gridSortWrap)) return; } else if (isNodeTarget && UI.gridSortWrap.contains(target)) { return; } UI.gridSortWrap.classList.remove('open'); S.gridSortMenuOpen = false; }; const applySortSelection = (sort) => { if (S.sort === sort) S.dir *= -1; else { S.sort = sort; S.dir = 1; } S.gridSortMenuOpen = true; persistSortPreference(); refresh(); }; const rememberFolderFirstBeforeStrictMode = () => { if (S.strictFolderFirstSnapshot === null) { S.strictFolderFirstSnapshot = S.folderFirst === true; } }; const restoreFolderFirstAfterStrictMode = () => { if (S.strictFolderFirstSnapshot === null) return; S.folderFirst = S.strictFolderFirstSnapshot === true; S.strictFolderFirstSnapshot = null; if (S.renderFolderFirst) S.renderFolderFirst(); }; const toggleFolderFirst = () => { S.folderFirst = !S.folderFirst; gmSet('pk_folder_first', S.folderFirst); try { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); globalPref.sort = S.sort; globalPref.dir = S.dir; globalPref.folderFirst = S.folderFirst; gmSet('pk_global_sort_pref', JSON.stringify(globalPref)); } catch(e) { gmSet('pk_global_sort_pref', JSON.stringify({ sort: S.sort, dir: S.dir, folderFirst: S.folderFirst })); } closeGridSortMenu(); if (S.renderFolderFirst) S.renderFolderFirst(); refresh(); }; const ensureViewSwitch = () => { if (!UI.actionBar || UI.actionBar.querySelector('#pk-view-switch')) return; const wrap = document.createElement('div'); wrap.className = 'pk-view-switch'; wrap.id = 'pk-view-switch'; wrap.innerHTML = ``; if (UI.btnCancelShare) UI.btnCancelShare.insertAdjacentElement('afterend', wrap); else UI.actionBar.appendChild(wrap); UI.viewSwitch = wrap; UI.btnViewList = wrap.querySelector('#pk-view-list'); UI.btnViewGrid = wrap.querySelector('#pk-view-grid'); UI.btnViewList.onclick = (e) => { e.stopPropagation(); if (S.viewMode === 'list') return; const anchorId = getViewportAnchorId(true); const finishViewSwitch = () => requestAnimationFrame(() => UI.win.classList.remove('pk-view-switching')); UI.win.classList.add('pk-view-switching'); S.viewMode = 'list'; persistViewPreference(); UI.win.classList.remove('pk-grid-view'); syncLayoutMetrics(); renderViewSwitch(); renderList(); if (anchorId) { requestAnimationFrame(() => { const targetIdx = S.display.findIndex(x => x.id === anchorId); if (targetIdx !== -1) { const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); } finishViewSwitch(); }); } else { finishViewSwitch(); } }; UI.btnViewGrid.onclick = (e) => { e.stopPropagation(); if (!canUseGridView() || S.viewMode === 'grid') return; const anchorId = getViewportAnchorId(true); const finishViewSwitch = () => requestAnimationFrame(() => UI.win.classList.remove('pk-view-switching')); UI.win.classList.add('pk-view-switching'); S.viewMode = 'grid'; persistViewPreference(); UI.win.classList.add('pk-grid-view'); syncLayoutMetrics(); renderViewSwitch(); renderList(); if (anchorId) { requestAnimationFrame(() => { const targetIdx = S.display.findIndex(x => x.id === anchorId); if (targetIdx !== -1) { const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); } finishViewSwitch(); }); } else { finishViewSwitch(); } }; }; const renderViewSwitch = () => { const usable = canUseGridView(); if (!usable) { if (UI.viewSwitch) { UI.viewSwitch.style.display = 'none'; UI.viewSwitch.style.visibility = 'hidden'; UI.viewSwitch.style.pointerEvents = 'none'; } return; } ensureViewSwitch(); if (!UI.viewSwitch) return; UI.viewSwitch.style.display = 'inline-flex'; UI.viewSwitch.style.visibility = ''; UI.viewSwitch.style.pointerEvents = ''; if (UI.btnViewList) UI.btnViewList.classList.toggle('active', !isGridView()); if (UI.btnViewGrid) UI.btnViewGrid.classList.toggle('active', isGridView()); }; 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; const isGuiVisible = el.style.display !== 'none'; const isTurboCurrent = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; let z = 1; if (screenW <= 1600 || width <= 1600) z = 0.8; if (screenW <= 1280 || width <= 1280) z = 0.7; const MIN_WIDTH = 940 * z; const MIN_HEIGHT = 450; const isTooSmall = width < MIN_WIDTH || height < MIN_HEIGHT; const keepScaledContext = isGuiVisible || isForcedHidden; const shouldKeepZoom = keepScaledContext || (!isGuiVisible && !isForcedHidden && !!document.getElementById('pk-launch')); document.documentElement.style.setProperty('--pk-zoom', String(shouldKeepZoom ? z : 1)); if (isTooSmall && !isTurboCurrent && keepScaledContext) { 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; } }; const syncDupFolderButtonText = () => { const sel = UI.selDupFolder; const btn = document.getElementById('pk-dup-folder-btn'); const txt = document.getElementById('pk-dup-folder-btn-txt'); if (!sel || !btn || !txt) return; const placeholder = window.innerWidth <= 1200 ? L.lbl_dup_select_folder_short : L.lbl_dup_select_folder; let label = placeholder; const opt = sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex] : null; if (opt && opt.value && opt.value !== "__RESET__") label = (opt.textContent || placeholder).trim(); txt.textContent = label; btn.removeAttribute('title'); btn.removeAttribute('data-pk-tip'); }; (() => { const sel = UI.selDupFolder; const btn = document.getElementById('pk-dup-folder-btn'); const wrap = document.getElementById('pk-dup-folder-sel-wrap'); if (!sel || !btn || !wrap) return; let pop = null; const closePop = () => { if (pop && pop.parentNode) pop.remove(); pop = null; btn.classList.remove('act'); }; const openPop = () => { closePop(); const opts = Array.from(sel.options).filter(o => !(o.disabled && !o.value)); opts.sort((a, b) => (b.value === "__RESET__") - (a.value === "__RESET__")); if (!opts.length) return; pop = document.createElement('div'); pop.className = 'pk-dup-folder-pop'; pop.classList.toggle('pk-dark', !!document.querySelector('.pk-ov')?.classList.contains('pk-dark')); pop.innerHTML = '
'; const head = pop.firstElementChild; const list = pop.lastElementChild; opts.forEach(o => { const item = document.createElement('button'); item.type = 'button'; item.className = `pk-dup-folder-item${o.value === sel.value ? ' act' : ''}${o.value === "__RESET__" ? ' pk-reset' : ''}`; const itemText = (o.textContent || '').trim(); item.dataset.value = o.value; item.textContent = itemText; item.removeAttribute('title'); if (itemText && o.value !== "__RESET__") item.setAttribute('data-pk-tip', itemText); if (o.value === "__RESET__") head.appendChild(item); else list.appendChild(item); }); document.body.appendChild(pop); pop.addEventListener('mousedown', (e) => e.stopPropagation(), true); pop.addEventListener('click', (e) => e.stopPropagation(), true); pop.addEventListener('wheel', (e) => e.stopPropagation(), { passive: true }); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = typeof getLogicalRect === 'function' ? getLogicalRect(btn) : btn.getBoundingClientRect(); const popRect = pop.getBoundingClientRect(); const logicalWinW = window.innerWidth / scale; const logicalWinH = window.innerHeight / scale; let left = rect.left; let top = rect.bottom + 4; if (left + popRect.width > logicalWinW - 8) left = Math.max(8, logicalWinW - popRect.width - 8); if (top + popRect.height > logicalWinH - 8) top = Math.max(8, rect.top - popRect.height - 4); pop.style.left = `${left}px`; pop.style.top = `${top}px`; btn.classList.add('act'); const activeItem = list.querySelector('.pk-dup-folder-item.act'); if (activeItem) activeItem.scrollIntoView({ block: 'nearest' }); }; btn.addEventListener('click', (e) => { e.stopPropagation(); syncDupFolderButtonText(); if (pop) closePop(); else openPop(); }); document.addEventListener('click', (e) => { if (!pop) return; const t = e.target; if (pop.contains(t)) { const item = t.closest('.pk-dup-folder-item'); if (!item) return; const pickedValue = item.dataset.value || ''; S._dupFolderPickedScrollTop = !!(pickedValue && pickedValue !== "__RESET__"); sel.value = pickedValue; syncDupFolderButtonText(); closePop(); sel.dispatchEvent(new Event('change', { bubbles: true })); return; } if (!wrap.contains(t)) closePop(); }, true); const handleDupFolderViewportChange = (e) => { if (!pop) return; const t = e && e.target; if (t && pop.contains(t)) return; closePop(); }; window.addEventListener('resize', handleDupFolderViewportChange, true); window.addEventListener('scroll', handleDupFolderViewportChange, true); syncDupFolderButtonText(); })(); window.addEventListener('resize', () => { checkGuiResponsiveness(); syncDupFolderButtonText(); if (el.style.display === 'none') return; const isAuthManagerReloading = !!( typeof window.pkIsAuthManagerReloading === 'function' && window.pkIsAuthManagerReloading() ); if (isAuthManagerReloading) { if (typeof window.pkDeferAuthManagerRelayout === 'function') { window.pkDeferAuthManagerRelayout(); } return; } const isDupAnalyzeMaskBusy = !!( S.loading && S.dupMode && S.dupRunning && UI.loader && UI.loader.style.display !== 'none' ); if (isDupAnalyzeMaskBusy) return; if (isGridView() && typeof scheduleGridRelayout === 'function') { scheduleGridRelayout(true); } else if (typeof renderList === 'function') { requestAnimationFrame(() => { if (typeof syncLayoutMetrics === 'function') syncLayoutMetrics(); renderList(); }); } else if (typeof renderVisible === 'function') { requestAnimationFrame(() => { if (typeof syncLayoutMetrics === 'function') syncLayoutMetrics(); if (UI.in) UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; renderVisible(); }); } }); checkGuiResponsiveness(); if (UI.btnClose) { UI.btnClose.addEventListener('click', () => { isForcedHidden = false; requestAnimationFrame(checkGuiResponsiveness); }); } 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'); } const uploadMenu = (UI.uploadWrap && UI.uploadWrap.querySelector('.pk-dropdown-menu')) || document.querySelector('.pk-dropdown-menu[data-pk-portal="1"]'); if (uploadMenu) uploadMenu.classList.toggle('pk-dark', !wasDark); 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; let pkSyncHideMenuLock = false; const syncHideNativeExitDropdown = () => { if (pkSyncHideMenuLock) return; pkSyncHideMenuLock = true; try { document.querySelectorAll('.pk-dropdown-menu[data-pk-portal="1"]').forEach(n => n.remove()); const keyInit = { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true }; document.dispatchEvent(new KeyboardEvent('keydown', keyInit)); document.dispatchEvent(new KeyboardEvent('keyup', keyInit)); document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, button: 0 })); document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, button: 0 })); document.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, button: 0 })); } catch (e) {} setTimeout(() => { pkSyncHideMenuLock = false; }, 0); }; const pkWinHideObserver = new MutationObserver(() => { if (!el.isConnected) return; const cs = getComputedStyle(el); if (el.hidden || cs.display === 'none' || cs.visibility === 'hidden') { requestAnimationFrame(syncHideNativeExitDropdown); } }); pkWinHideObserver.observe(el, { attributes: true, attributeFilter: ['style', 'class', 'hidden'] }); 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 = 2147483640; 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(); } }); }); } function showDeleteConfirm(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({ confirm: false }); }; m.querySelector('#cfm_yes').onclick = () => { const isHard = m.querySelector('#pk_hard_delete_chk').checked; m.remove(); resolve({ confirm: true, hardDelete: isHard }); }; m.querySelector('.pk-modal-close').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('#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.getSelectedCount())).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, preferFresh: true, 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 = getLogicalRect(listDiv); 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 = getLogicalRect(listDiv); 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; const isHardDelete = m.querySelector('#bl_prev_hard_delete').checked; 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, hardDelete: isHardDelete }); }; } function updateLoadAction(isExitMode = false) { if (!UI.stopBtn) return; UI.stopBtn.removeAttribute('data-pk-tip'); const txtEl = UI.stopBtn.querySelector('span'); if (txtEl) txtEl.textContent = isExitMode ? L.btn_exit_script : L.btn_stop; } 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; updateLoadAction(false); } } else { UI.loader.style.display = 'none'; updateLoadAction(false); } } function updateLoadTxt(txt) { if (UI.loadTxt) UI.loadTxt.innerText = txt; updateLoadAction(txt === L.str_waiting_token); } 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; const activeStages = ['hash', ...(cfg.video ? ['video'] : []), 'name']; const stageSpan = 100 / activeStages.length; const setDupProgress = (pct) => { const safePct = Math.max(0, Math.min(100, Math.round(pct))); updateLoadTxt(L.loading_dup.replace('{p}', safePct)); }; let lastDupPct = 0; const setStageProgress = (stageKey, processed, total, stageStartRatio = 0, stageEndRatio = 1) => { const stageIndex = activeStages.indexOf(stageKey); if (stageIndex < 0) return; const start = stageIndex * stageSpan; const end = (stageIndex + 1) * stageSpan; const safeStartRatio = Math.max(0, Math.min(1, stageStartRatio)); const safeEndRatio = Math.max(safeStartRatio, Math.min(1, stageEndRatio)); let innerRatio = safeEndRatio; if (total > 0) { const raw = Math.max(0, Math.min(1, processed / total)); innerRatio = safeStartRatio + ((safeEndRatio - safeStartRatio) * raw); } const nextPct = start + ((end - start) * innerRatio); if (nextPct >= lastDupPct) { lastDupPct = nextPct; setDupProgress(nextPct); } }; setDupProgress(0); if (isRunningFn()) { const hashMap = new Map(); let hashProcessed = 0; const hashBuildTotal = candidates.length; 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); } hashProcessed++; if (hashProcessed % 500 === 0) { if (!isRunningFn()) break; setStageProgress('hash', hashProcessed, hashBuildTotal, 0, 0.7); await sleep(0); } } if (!isRunningFn()) return groups; const hashEntries = Array.from(hashMap.entries()); const hashTotal = hashBuildTotal + hashEntries.length; for (let i = 0; i < hashEntries.length; i++) { const [key, items] = hashEntries[i]; 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 ((i + 1) % 200 === 0) { if (!isRunningFn()) break; setStageProgress('hash', i + 1, hashEntries.length, 0.7, 1); await sleep(0); } } if (!isRunningFn()) return groups; setStageProgress('hash', 1, 1, 1, 1); } if (isRunningFn() && cfg.video) { 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; if (totalCount <= 0) { setStageProgress('video', 0, 0); } for (let i = 0; i < totalCount; i++) { processedCount++; if (processedCount % 500 === 0) { if (!isRunningFn()) break; setStageProgress('video', processedCount, totalCount); 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') ? 3.0 : 2.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 (strictness === 'loose' || ratio <= 0.10) { 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()) return groups; setStageProgress('video', totalCount, totalCount); } if (isRunningFn()) { 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(); let nameBuildProcessed = 0; const nameBuildTotal = remainingItems.length; for (const item of remainingItems) { const tGroup = getTypeGroup(item.mime_type); const cleaned = cleanNameAd(item.name); if (cleaned) { const key = tGroup + '|' + cleaned; if (!typeNameMap.has(key)) typeNameMap.set(key,[]); typeNameMap.get(key).push(item); } nameBuildProcessed++; if (nameBuildProcessed % 500 === 0) { if (!isRunningFn()) break; setStageProgress('name', nameBuildProcessed, nameBuildTotal, 0, 0.7); await sleep(0); } } if (!isRunningFn()) return groups; const typeEntries = Array.from(typeNameMap.entries()); const nameTotal = nameBuildTotal + typeEntries.length; for (let i = 0; i < typeEntries.length; i++) { const [key, items] = typeEntries[i]; 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 j = 1; j < sortedForAlgo.length; j++) { const target = sortedForAlgo[j]; 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 }); }); } if ((i + 1) % 200 === 0) { if (!isRunningFn()) break; setStageProgress('name', i + 1, typeEntries.length, 0.7, 1); await sleep(0); } } if (!isRunningFn()) return groups; setStageProgress('name', 1, 1, 1, 1); } setDupProgress(100); 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 (S.strictFolderFirstSnapshot !== null) { S.folderFirst = S.strictFolderFirstSnapshot === true; if (S.renderFolderFirst) S.renderFolderFirst(); } } 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; } if (UI.viewSwitch) { const usableViewSwitch = canUseGridView(); UI.viewSwitch.style.display = usableViewSwitch ? 'inline-flex' : 'none'; UI.viewSwitch.style.visibility = usableViewSwitch ? '' : 'hidden'; UI.viewSwitch.style.pointerEvents = usableViewSwitch ? '' : 'none'; } 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 || S.dupMode) ? '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 (S.historyMode) { const session = globalCache.get('history_session'); if (session && Array.isArray(session.targetIds) && !session.completed && (session.cursor || 0) < session.targetIds.length) { console.log(`[Resuming] History cache snapshot rendered. Continuing detail fetch from ${session.cursor || 0}/${session.targetIds.length}...`); isResuming = true; nextToken = `history:${session.cursor || 0}`; S.items.forEach(it => fetchedIds.add(it.id)); } else if (!nextToken) { setLoad(false); if (window.pkSmartRefreshTrigger) { console.log(`[SWR] Intent detected: Validating ${realCacheKey} in background...`); window.pkSmartRefreshTrigger(true); } 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); } if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('load-missing-token', 5000); const initialWait = (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 6500 : 5000; let isAuthReady = await waitForAuth(initialWait); if (!isAuthReady) { console.warn("PikPak Master: Auth token wait timeout. Entering recovery recheck before logout."); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('load-missing-token-recheck', 5000); await sleep(1200); isAuthReady = await waitForAuth(5000); } if (!isAuthReady) { console.warn("PikPak Master: Auth token still missing after recovery recheck."); const didLogout = await confirmedLogout('load-missing-token-final', 5000, 5200); if (didLogout) return; setLoad(false); if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); return; } if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); } if (!isResuming && el) { el.focus(); } UI.stopBtn.onclick = () => { const isAuthSyncMode = (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) || (UI.loadTxt && UI.loadTxt.innerText === L.str_waiting_token); if (isAuthSyncMode) { handleClose(); window.location.href = `${window.location.origin}/login`; return; } 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); const sourceSig = targetIds.join('|'); const fetchDetail = async (id) => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${id}?thumbnail_size=SIZE_MEDIUM`, { headers: getHeaders(), signal: signal }); if (!res.ok) return null; const f = await res.json(); if (f && !f.trashed) return f; return null; } catch (e) { return null; } }; let historySession = (typeof globalCache !== 'undefined') ? globalCache.get('history_session') : null; const canResumeSession = !!(historySession && historySession.sourceSig === sourceSig && Array.isArray(historySession.targetIds)); if (!canResumeSession) { historySession = { sourceSig, targetIds: [...targetIds], cursor: 0, completed: false, updatedAt: Date.now() }; } else { historySession.cursor = Math.max(0, Math.min(historySession.cursor || 0, targetIds.length)); historySession.completed = !!historySession.completed && historySession.cursor >= targetIds.length; historySession.updatedAt = Date.now(); } let historyList = (canResumeSession && Array.isArray(S.items) && S.items.length > 0) ? [...S.items] : []; if (!historyList.length && typeof globalCache !== 'undefined') { const cachedHistory = globalCache.get('history_root'); if (cachedHistory && !Array.isArray(cachedHistory) && Array.isArray(cachedHistory.items)) { historyList = [...cachedHistory.items]; } else if (Array.isArray(cachedHistory)) { historyList = [...cachedHistory]; } } const existingIds = new Set(); historyList.forEach(f => { if (f && f.id) existingIds.add(f.id); }); const syncHistoryCache = () => { const hasPending = !historySession.completed && historySession.cursor < targetIds.length; const snapshot = { items: [...historyList], nextToken: hasPending ? `history:${historySession.cursor}` : null }; S.cache.set('history_root', snapshot); if (typeof globalCache !== 'undefined') { globalCache.set('history_root', snapshot); globalCache.set('history_session', { ...historySession, targetIds: [...historySession.targetIds] }); } }; const syncHistoryUI = () => { if (signal.aborted || currentId !== activeLoadId) return; S.items = [...historyList]; S.itemMap.clear(); S.items.forEach(f => S.itemMap.set(f.id, f)); if (UI.loader.style.display !== 'none') setLoad(false); refresh(); updateStat(); }; if (targetIds.length === 0) { historySession.cursor = 0; historySession.completed = true; historySession.updatedAt = Date.now(); syncHistoryCache(); data = { files: [], next_page_token: null }; break; } syncHistoryCache(); syncHistoryUI(); const CONCURRENCY = 6; let cursor = historySession.completed ? targetIds.length : (historySession.cursor || 0); for (let i = cursor; i < targetIds.length; i += CONCURRENCY) { if (signal.aborted || currentId !== activeLoadId) { historySession.cursor = i; historySession.completed = false; historySession.updatedAt = Date.now(); syncHistoryCache(); return; } const chunk = targetIds.slice(i, i + CONCURRENCY); const results = await Promise.all(chunk.map(id => fetchDetail(id))); let appended = 0; results.forEach(rawF => { if (!rawF) return; 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; if (existingIds.has(f.id)) { const idx = historyList.findIndex(x => x.id === f.id); if (idx !== -1) historyList[idx] = f; } else { existingIds.add(f.id); historyList.push(f); appended++; } }); historyList.sort((a, b) => (b._history_ts || 0) - (a._history_ts || 0)); historySession.cursor = Math.min(targetIds.length, i + chunk.length); historySession.completed = historySession.cursor >= targetIds.length; historySession.updatedAt = Date.now(); syncHistoryCache(); syncHistoryUI(); if (appended === 0 || historySession.cursor % 24 === 0) await sleep(10); } if (signal.aborted || currentId !== activeLoadId) return; historySession.cursor = targetIds.length; historySession.completed = true; historySession.updatedAt = Date.now(); syncHistoryCache(); syncHistoryUI(); if (window.pkSmartRefreshTrigger) { console.log(`[SWR] History cache completed. Validating history_root in background...`); window.pkSmartRefreshTrigger(true); } data = { files: [], next_page_token: null }; break; } 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; try { const phases = "PHASE_TYPE_UNKNOW,PHASE_TYPE_PENDING,PHASE_TYPE_RUNNING,PHASE_TYPE_PAUSED,PHASE_TYPE_ERROR"; const filters = encodeURIComponent(JSON.stringify({ "phase": { "in": phases } })); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?type=upload&limit=100&filters=${filters}&_t=${Date.now()}`; const res = await fetch(url, { headers: getHeaders(), signal: signal }); if (res.ok) { const cloudData = await res.json(); const cloudTasks = cloudData.tasks || []; const cloudIds = new Set(cloudTasks.map(ct => ct.id)); const cloudFileIds = new Set(cloudTasks.map(ct => ct.file_id).filter(Boolean)); S.uploadTasks = S.uploadTasks.filter(lt => { if (lt.status === 'DONE' || (!lt.file_id && !lt._uploadId)) return true; if ((lt.name === 'upload' || lt.name === 'Ghost Task') && !lt.file && lt.file_id) { console.log(`[Reconcile] Auto-cleaning orphaned local ghost file: ${lt.file_id}`); fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [lt.file_id] }) }).catch(()=>{}); if (S.upMng) S.upMng.removeTask(lt.id); return false; } const existsInCloud = cloudIds.has(lt.id) || (lt.file_id && cloudFileIds.has(lt.file_id)) || (lt._sourceTaskId && cloudIds.has(lt._sourceTaskId)); if (!existsInCloud) { console.log(`[Reconcile] 发现死任务,云端已清理,同步清理本地库: ${lt.name}`); if (S.upMng) S.upMng.removeTask(lt.id); return false; } return true; }); cloudTasks.forEach(ct => { const existsLocally = S.uploadTasks.some(lt => lt.id === ct.id || lt.file_id === ct.file_id || lt._sourceTaskId === ct.id); if (!existsLocally && ct.file_id) { const ref = ct.reference_resource || {}; const taskName = ref.name || ct.name || ct.file_name || 'Ghost Task'; if (taskName === 'upload' || taskName === 'Ghost Task') { console.log(`[Reconcile] Auto-cleaning orphaned cloud ghost file: ${ct.file_id}`); fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [ct.file_id] }) }).catch(()=>{}); return; } const ghostTask = { id: 'up_' + Date.now() + '_' + Math.random().toString(36).substr(2), _sourceTaskId: ct.id, kind: 'pk#upload', file: null, name: taskName, size: ct.file_size, parentId: '', status: 'PAUSED', progress: parseInt(ct.progress || 0), speed: 0, message: L.msg_ghost_task_resume, file_id: ct.file_id, hash: (ct.params && ct.params.hash) || '', _xhr: null, _lastCalcTime: 0, _lastCalcLoaded: 0, _lastUiTime: 0, icon_link: ct.icon_link || '', thumbnail_link: ct.thumbnail_link || '' }; S.uploadTasks.push(ghostTask); if (S.upMng) S.upMng.saveTask(ghostTask); } }); } } catch (e) { console.warn("[Upload] Failed to sync cloud upload tasks:", e); } 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); S._networkRetryCount = 0; } } 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') || e.message.includes('AUTH_RETRY'); 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); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('auth-error', 5000); await sleep(1000); try { let h = getHeaders(); if (!h.Authorization || h.Authorization.length < 10) { const isAuthReady = await waitForAuth(5000); if (!isAuthReady) { if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('auth-error-missing-token-recheck', 5000); await sleep(1200); const isAuthReadyAgain = await waitForAuth(5000); if (!isAuthReadyAgain) { const didLogout = await confirmedLogout('auth-error-missing-token-recheck-final', 5000, 5200); if (didLogout) return; S._isRetrying = false; setLoad(false); return; } } h = getHeaders(); } let testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token rejected by server, entering recovery recheck before logout."); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('auth-error-about-recheck', 5000); await sleep(1200); const isAuthReady = await waitForAuth(5000); if (!isAuthReady) { const didLogout = await confirmedLogout('auth-error-about-first-reject', 5000, 5200); if (didLogout) return; h = getHeaders(); } else { h = getHeaders(); } testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token still rejected by server after recovery recheck."); const didLogout = await confirmedLogout('auth-error-about-second-reject', 5000, 5200); if (didLogout) return; h = getHeaders(); testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token still unavailable after confirmed logout recheck, keeping current view."); S._isRetrying = false; setLoad(false); return; } } } if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); const testData = await testRes.json(); if (testData.error) { console.warn("[Auth] API returned error inside response."); const didLogout = await confirmedLogout('auth-error-about-payload-error', 5000, 5200); if (didLogout) return; S._isRetrying = false; setLoad(false); return; } if (!testData.sub || testData.sub === '') { console.warn("[Auth] Token valid but no user info."); const didLogout = await confirmedLogout('auth-error-about-empty-sub', 5000, 5200); if (didLogout) return; S._isRetrying = false; setLoad(false); return; } } catch(testErr) { console.warn("[Auth] Token verification failed (CORS/Network/Parse):", testErr); const didLogout = await confirmedLogout('auth-error-about-verify-exception', 5000, 5200); if (didLogout) return; S._isRetrying = false; setLoad(false); return; } 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) { S._networkRetryCount = (S._networkRetryCount || 0) + 1; if (S._networkRetryCount > 5) { console.error("[Network] Max retries reached."); const didLogout = await confirmedLogout('network-retry-exhausted', 5000, 5200); if (didLogout) return; S._isRetrying = false; S._networkRetryCount = 0; setLoad(false); showAlert(L.msg_network_unstable); return; } 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... (${S._networkRetryCount}/5)`); 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; syncLocalUploadVisibility(); const prevViewMode = S.viewMode === 'grid' ? 'grid' : 'list'; const nextCur = S.path[S.path.length - 1] || { id: '' }; const nextIsStandardView = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!nextCur.id.startsWith('virtual_') || nextCur.id === 'virtual_search_root'); const groupedGridExitSyncPending = !!S._groupedGridExitSyncPending; if (nextIsStandardView) { const nextFolderId = nextCur.id || 'root'; const nextViewMode = (typeof resolvePreferredViewMode === 'function') ? resolvePreferredViewMode(nextFolderId) : (gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'); if (prevViewMode !== nextViewMode || (groupedGridExitSyncPending && nextViewMode === 'grid')) { beginFolderViewSync(); } S._groupedGridExitSyncPending = false; S.viewMode = nextViewMode; } else { S._groupedGridExitSyncPending = false; endFolderViewSync(); } renderCrumb(); if (!S.dupMode) S.dupItemMap = null; const isStrictVirtual = S.isFlattened || S.dupMode || S.analyzeMode; const cur = S.path[S.path.length - 1]; S.syncNavContextState(); if (!isStrictVirtual) { restoreFolderFirstAfterStrictMode(); } 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'; const showAnalyzeGroupTools = isAnalyzeRoot && !S.search && S.analyzeSimGroups; const isAnalyzeDupView = S.analyzeMode && S.analyzeSimGroups; UI.lblSearchPath.style.display = (S.dupMode || S.isFlattened || isAnalyzeRoot) ? 'flex' : 'none'; if (UI.btnAnaSelect) UI.btnAnaSelect.style.display = showAnalyzeGroupTools ? 'flex' : 'none'; if (UI.btnAnaSort) UI.btnAnaSort.style.display = showAnalyzeGroupTools ? 'flex' : 'none'; if (UI.vp) UI.vp.style.overflowX = (S.dupMode || isAnalyzeDupView) ? 'hidden' : ''; } 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; const visibleSet = new Set(S.display.map(i => i.id)); if (S.selMode === 'all') { const nextExcluded = new Set(); for (const id of S.selEx) { if (visibleSet.has(id)) nextExcluded.add(id); } S.selEx = nextExcluded; } else { const nextSelected = S.getSelectedIds().filter(id => visibleSet.has(id)); S.setExplicitSelection(nextSelected); } 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); const folderFirstViewKey = S.starredMode ? '__starred__' : (S.recentMode ? '__recent__' : ''); if (isStandardView) { const folderId = cur.id || 'root'; if (S._viewAppliedForId !== folderId) { S.viewMode = resolvePreferredViewMode(folderId); S._viewAppliedForId = folderId; } 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; } else { S.sort = 'modified_time'; S.dir = 1; } } catch(e) { S.sort = 'modified_time'; S.dir = 1; } } else { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); S.sort = globalPref.sort; S.dir = globalPref.dir; } try { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); if (globalPref.folderFirst !== undefined) S.folderFirst = globalPref.folderFirst === true; else S.folderFirst = gmGet('pk_folder_first', false); } catch(e) { S.folderFirst = gmGet('pk_folder_first', false); } S._sortAppliedForId = folderId; S._comicApplied = false; if (S.renderFolderFirst) S.renderFolderFirst(); } if (gmGet('pk_comic_mode', true) && !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; } } } else if (folderFirstViewKey) { if (S._sortAppliedForId !== folderFirstViewKey) { try { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); if (globalPref.folderFirst !== undefined) S.folderFirst = globalPref.folderFirst === true; else S.folderFirst = gmGet('pk_folder_first', false); } catch(e) { S.folderFirst = gmGet('pk_folder_first', false); } S._sortAppliedForId = folderFirstViewKey; if (S.renderFolderFirst) S.renderFolderFirst(); } } 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; }); const dupItemMap = new Map(); for (let i = 0, len = S.items.length; i < len; i++) { dupItemMap.set(S.items[i].id, S.items[i]); } S.dupItemMap = dupItemMap; const dupBuckets = { all: [], hash: [], sim: [], name: [] }; for (let i = 0, len = groups.length; i < len; i++) { const g = groups[i]; const validIds = []; for (let k = 0, idsLen = g.ids.length; k < idsLen; k++) { const id = g.ids[k]; if (dupItemMap.has(id)) validIds.push(id); } if (validIds.length <= 1) continue; const normalizedGroup = { ...g, ids: validIds }; dupBuckets.all.push(normalizedGroup); if (g.type === L.tag_hash) dupBuckets.hash.push(normalizedGroup); else if (g.type === L.tag_sim) dupBuckets.sim.push(normalizedGroup); else if (g.type === L.tag_name) dupBuckets.name.push(normalizedGroup); } if (dupBuckets.all.length === 0) { S.dupRunning = false; setLoad(false); showToast(L.msg_dup_none); if (UI.btnExit) UI.btnExit.click(); return; } S.dupBuckets = dupBuckets; S.dupRawGroups = dupBuckets.all; 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 > 0) { if (!isInvert) { filesInTarget.forEach(f => S.sel.add(f.id)); } } }); renderDupView(); updateStat(); if (S._dupFolderPickedScrollTop) { S._dupFolderPickedScrollTop = false; if (S._dupFolderPickedTopRaf) cancelAnimationFrame(S._dupFolderPickedTopRaf); S._dupFolderPickedTopRaf = requestAnimationFrame(() => { S._dupFolderPickedTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } }; 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', true) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { const targetTop = getItemScrollTopByIndex(targetIdx); UI.vp.scrollTop = Math.max(0, targetTop - (UI.vp.clientHeight / 2) + (CONF.rowHeight / 2)); S.clearSelection(); S.activeId = S.latestChildId; 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, pg: finalDur > 0 ? Math.min(100, Math.max(0, ((item._history_progress || 0) / finalDur) * 100)) : -1, 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 === 'progress') { const pctA = typeof a.pg === 'number' ? a.pg : -1; const pctB = typeof b.pg === 'number' ? b.pg : -1; if (pctA !== pctB) return (pctB - pctA) * 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') { if (a.k !== b.k) return folderFirst ? (b.k - a.k) : (a.k - b.k); 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','id':'id','ms':'ms'})[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; const currentIds = new Set(S.display.map(i => i.id)); if (S.selMode === 'all') { const nextExcluded = new Set(); for (const id of S.selEx) { if (currentIds.has(id)) nextExcluded.add(id); } S.selEx = nextExcluded; } else { const nextSelected = S.getSelectedIds().filter(id => currentIds.has(id)); S.setExplicitSelection(nextSelected); } renderList(); if (gmGet('pk_keep_pos', true) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { const rowTop = getItemScrollTopByIndex(targetIdx); 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.clearSelection(); S.activeId = S.latestChildId; 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(); let dupBuckets = S.dupBuckets; if (!dupBuckets || !Array.isArray(dupBuckets.all)) { dupBuckets = { all: Array.isArray(S.dupRawGroups) ? S.dupRawGroups : [], hash: [], sim: [], name: [] }; const allLen = dupBuckets.all.length; for (let i = 0; i < allLen; i++) { const g = dupBuckets.all[i]; if (g.type === L.tag_hash) dupBuckets.hash.push(g); else if (g.type === L.tag_sim) dupBuckets.sim.push(g); else if (g.type === L.tag_name) dupBuckets.name.push(g); } S.dupBuckets = dupBuckets; } if (dupBuckets.all.length === 0 && !S.dupRunning) { if (UI.btnExit) { UI.btnExit.click(); } else { S.dupMode = false; S.isFlattened = false; refresh(); } return; } let groupsToRender = []; if (showHash) groupsToRender.push(...dupBuckets.hash); if (showSim) groupsToRender.push(...dupBuckets.sim); if (showName) groupsToRender.push(...dupBuckets.name); if (S.pinnedDupPath) { const pinnedGroups = []; const normalGroups = []; for (const g of groupsToRender) { 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, groupIds: ids.slice() }); const selectedDupPath = S.pinnedDupPath || ""; const compareDupGroupData = (a, b) => { const op = S.groupSortOp || 'path'; const dir = S.groupSortDir || 1; let res = 0; if (op === 'time') { res = new Date(b.it.modified_time) - new Date(a.it.modified_time); } else if (op === 'size') { const sa = BigInt(a.it.size || 0), sb = BigInt(b.it.size || 0); res = sa < sb ? 1 : (sa > sb ? -1 : 0); } else if (op === 'path') { if (a.path.length !== b.path.length) res = a.path.length - b.path.length; else res = a.path.localeCompare(b.path); } else if (op === 'name') { res = a.it.name.localeCompare(b.it.name); } return res * dir; }; const sortDupGroupChunk = arr => arr.slice().sort(compareDupGroupData); const groupData = groupItems.map((it, idx) => ({ it, path: groupPaths[idx] })); let sortedGroupData; if (selectedDupPath) { const groupA = []; const groupB = []; for (let i = 0; i < groupData.length; i++) { const row = groupData[i]; if (row.path === selectedDupPath) groupA.push(row); else groupB.push(row); } if (groupA.length > 0) { sortedGroupData = sortDupGroupChunk(groupA).concat(sortDupGroupChunk(groupB)); } else { sortedGroupData = sortDupGroupChunk(groupData); } } else { sortedGroupData = sortDupGroupChunk(groupData); } let lastFullPath = null; for (let idx = 0; idx < sortedGroupData.length; idx++) { const { it: item, path: fullPath } = sortedGroupData[idx]; item._dupFullPath = fullPath; if (idx > 0 && fullPath === lastFullPath) { item._isSameFolder = true; } else { item._isSameFolder = false; } lastFullPath = fullPath; newDisplay.push(item); } } const dupFolderWrap = document.getElementById('pk-dup-folder-sel-wrap'); if (dupFolderWrap) { dupFolderWrap.style.width = "220px"; dupFolderWrap.style.flexShrink = "0"; } 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) { if (dupFolderWrap) dupFolderWrap.style.display = 'none'; UI.selDupFolder.style.display = 'none'; if (invertLabel) invertLabel.style.display = 'none'; } else { if (dupFolderWrap) dupFolderWrap.style.display = 'inline-flex'; 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','id':'id','ms':'ms'})[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; } UI.selDupFolder.value = ""; } syncDupFolderButtonText(); 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() { syncLayoutMetrics(); UI.win.classList.toggle('pk-grid-view', isGridView()); renderViewSwitch(); if (isGridView()) { const gridLayout = getGridLayout(); S._gridLayoutKey = getGridLayoutKey(); if (isGroupedGridView()) { const dupGridMeta = getGroupedGridMeta(true); UI.in.style.height = `${Math.max(0, dupGridMeta ? dupGridMeta.totalHeight : 0)}px`; } else { S.dupGridMeta = null; S.dupGridMetaKey = ''; UI.in.style.height = `${Math.ceil((S.display.length || 0) / gridLayout.cols) * CONF.rowHeight}px`; } scheduleGridRelayout(); } else { S._gridLayoutKey = ''; S.dupGridMeta = null; S.dupGridMetaKey = ''; if (UI.win) UI.win.classList.remove('pk-grid-view', 'pk-grid-resizing', 'pk-grid-scrolling'); 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) { if (isGridView()) { const gridMeta = getGridSortMeta(); const hideGroupedRootGridHeadTools = S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups); const hideGridFolderFirst = S.isFlattened || S.dupMode || isAnalyzeRoot; hd.classList.add('pk-grid-view-hd'); hd.style.gridTemplateColumns = ''; hd.innerHTML = `
${CONF.icons.invert} ${L.btn_invert}
${getGridSortOptions().map(opt => { const state = resolveGridSortOptionState(opt); return `
${state.icon}${state.label}
`; }).join('')}
`; UI.chkAll = hd.querySelector('#pk-all'); if (UI.chkAll) UI.chkAll.onclick = S.handleSelectAll; UI.btnGridFolderFirst = hd.querySelector('#pk-grid-folder-first'); if (UI.btnGridFolderFirst) { UI.btnGridFolderFirst.onclick = (e) => { e.stopPropagation(); toggleFolderFirst(); }; } const btnGridInv = hd.querySelector('#pk-grid-invert'); if (btnGridInv) { btnGridInv.onclick = (e) => { e.stopPropagation(); if (S.loading || S.display.length === 0) return; S.invertSelection(); renderVisible(); updateStat(); }; btnGridInv.onmouseenter = () => btnGridInv.style.color = 'var(--pk-pri)'; btnGridInv.onmouseleave = () => btnGridInv.style.color = 'var(--pk-fg)'; } UI.gridSortWrap = hd.querySelector('.pk-grid-sort-wrap'); if (UI.gridSortWrap && S.gridSortMenuOpen) UI.gridSortWrap.classList.add('open'); const sortTrigger = hd.querySelector('#pk-grid-sort-trigger'); if (sortTrigger) { sortTrigger.onclick = (e) => { e.stopPropagation(); if (!UI.gridSortWrap) return; const willOpen = !UI.gridSortWrap.classList.contains('open'); S.gridSortMenuOpen = willOpen; UI.gridSortWrap.classList.toggle('open', willOpen); }; } hd.querySelectorAll('.pk-grid-sort-opt').forEach(opt => { opt.onclick = (e) => { e.stopPropagation(); applySortSelection(opt.dataset.sort); }; }); if (!S.gridSortDismissBound) { document.addEventListener('mousedown', closeGridSortMenu, true); document.addEventListener('scroll', closeGridSortMenu, true); window.addEventListener('resize', closeGridSortMenu); S.gridSortDismissBound = true; } } else { hd.classList.remove('pk-grid-view-hd'); hd.style.gridTemplateColumns = colDef; UI.gridSortWrap = null; closeGridSortMenu(); } if (!isGridView() && 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 (!isGridView() && 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 (!isGridView() && 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 (!isGridView() && 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 = []; for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader && !S.movingIds.has(item.id) && !S.isSelected(item.id)) newSel.push(item.id); } S.setExplicitSelection(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 if (!isGridView()) { 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 && !S.isFlattened && !S.dupMode) ? `
${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(); } else { UI.btnFolderFirst = null; 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 = []; for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader && !S.movingIds.has(item.id) && !S.isSelected(item.id)) newSel.push(item.id); } S.setExplicitSelection(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 = isGridView() ? [] : 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; const totalSelectable = S.getSelectableCount(); const selectedCount = S.getSelectedCount(); UI.chkAll.checked = totalSelectable > 0 && selectedCount === totalSelectable; UI.chkAll.indeterminate = selectedCount > 0 && selectedCount < totalSelectable; } requestAnimationFrame(() => { renderVisible(); if (!isGridView()) { requestAnimationFrame(() => { endFolderViewSync(); if (UI.win) UI.win.classList.remove('pk-view-switching'); }); } }); } const getStarIcon = (isStarred) => { const color = isStarred ? '#FFC107' : '#ccc'; const fill = isStarred ? '#FFC107' : 'none'; return ` `; }; function renderVisible() { const L = getStrings(); const isGridMode = isGridView(); const gridLayout = isGridMode ? getGridLayout() : null; 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; } const dupGridMeta = isGroupedGridView() ? getGroupedGridMeta() : null; if (isGridMode && gridLayout) { UI.in.style.height = `${Math.max(0, dupGridMeta ? dupGridMeta.totalHeight : Math.ceil(S.display.length / gridLayout.cols) * CONF.rowHeight + gridLayout.gapY)}px`; } else { 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; let start = 0; let end = 0; let visibleIndices = null; if (dupGridMeta) { const sectionBuffer = Math.max(CONF.rowHeight * 2, buffer * CONF.rowHeight); visibleIndices = []; for (const section of dupGridMeta.sections) { if (section.bottom < top - sectionBuffer) continue; if (section.top > top + h + sectionBuffer) break; if (section.headerIndex !== null) visibleIndices.push(section.headerIndex); const itemIndices = section.itemIndices || []; for (let k = 0; k < itemIndices.length; k++) visibleIndices.push(itemIndices[k]); } end = visibleIndices.length; } else { start = isGridMode && gridLayout ? Math.max(0, (Math.floor(top / CONF.rowHeight) - Math.ceil(buffer / 2)) * gridLayout.cols) : Math.max(0, Math.floor(top / CONF.rowHeight) - buffer); end = isGridMode && gridLayout ? Math.min(S.display.length, (Math.ceil((top + h) / CONF.rowHeight) + Math.ceil(buffer / 2)) * gridLayout.cols) : Math.min(S.display.length, Math.ceil((top + h) / CONF.rowHeight) + buffer); } const isBlur = isBlurEnabledForView(isGridMode ? 'grid' : 'list'); 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 = ''; cleanupNonPooledChildren(UI.in); const visibleCount = dupGridMeta ? visibleIndices.length : Math.max(0, end - start); const rowPool = ensureVisibleRowPool(UI.in, visibleCount); let poolPtr = 0; 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; }; const renderIndices = dupGridMeta ? visibleIndices : null; const renderCount = dupGridMeta ? renderIndices.length : Math.max(0, end - start); for (let seqIdx = 0; seqIdx < renderCount; seqIdx++) { const i = dupGridMeta ? renderIndices[seqIdx] : (start + seqIdx); const d = S.display[i]; if (!d) continue; const row = rowPool[poolPtr++]; const prevBoundId = row.dataset.pkBoundId || ''; const prevBoundKind = row.dataset.pkBoundKind || ''; const prevGridMediaMount = isGridMode ? row.querySelector('.pk-gv-media-mount') : null; const prevGridMediaNode = prevGridMediaMount ? prevGridMediaMount.firstElementChild : null; const prevGridMediaKey = prevGridMediaMount ? (prevGridMediaMount.dataset.pkMediaKey || '') : ''; const suppressGridRebindTransition = isGridMode && prevBoundId && prevBoundId !== d.id; if (isGridMode && prevBoundId && prevGridMediaNode && typeof stashFrozenGridMediaNode === 'function') { stashFrozenGridMediaNode(prevBoundId, prevGridMediaKey, prevGridMediaNode, prevBoundKind); } resetPooledRow(row); if (suppressGridRebindTransition) { row.style.transition = 'none'; row.style.animation = 'none'; } const dupLayout = dupGridMeta ? dupGridMeta.indexLayout.get(i) : null; if (dupLayout) { row.style.top = `${dupLayout.top}px`; row.style.left = `${dupLayout.left}px`; row.style.width = `${dupLayout.width}px`; row.style.height = `${dupLayout.height}px`; if (dupLayout.kind === 'header') { row.style.padding = isMax ? '0 20px' : '0 16px'; row.style.display = 'grid'; row.style.gridTemplateColumns = colDef; row.style.zIndex = '2'; row.style.boxSizing = 'border-box'; } else { row.style.padding = '0'; row.style.display = 'block'; row.style.gridTemplateColumns = ''; row.style.zIndex = '1'; } } else if (isGridMode && gridLayout) { const rowIdx = Math.floor(i / gridLayout.cols); const colIdx = i % gridLayout.cols; row.style.top = `${gridLayout.gapY + rowIdx * CONF.rowHeight}px`; row.style.left = `${gridLayout.padX + colIdx * (gridLayout.cardWidth + gridLayout.gapX)}px`; row.style.width = `${gridLayout.cardWidth}px`; row.style.height = `${gridLayout.cardHeight}px`; row.style.padding = '0'; row.style.display = 'block'; } else { row.style.top = `${i * CONF.rowHeight}px`; row.style.left = '0'; row.style.width = '100%'; row.style.gridTemplateColumns = colDef; row.style.height = ''; row.style.padding = ''; row.style.display = 'grid'; } 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 (dupGridMeta) row.style.gridColumn = ""; else 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 = Array.isArray(d.groupIds) ? d.groupIds.slice() : (groupData ? groupData.ids : []); row._pkGroupIds = groupItemIds.slice(); let selectedInGroup = 0; groupItemIds.forEach(id => { if (S.isSelected(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 => { S.setSelected(id, targetState); }); renderVisible(); updateStat(); }; } } else { const isSel = S.isSelected(d.id); const isFocused = S.activeId === d.id; const isMoving = S.movingIds && S.movingIds.has(d.id); row.className = getRowClassName(isSel, isFocused, isMoving); 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 = isGridMode ? `${getGridCardRadius()}px` : '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.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) { 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; if (typeof window.markStableFolderMediaState === 'function') { window.markStableFolderMediaState(item.id || lookupId, 'ok', foundThumb); } else { if (!window.pkThumbTriState) window.pkThumbTriState = { folder: Object.create(null) }; if (!window.pkGridMediaStore) window.pkGridMediaStore = { folder: Object.create(null) }; if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); window.pkThumbTriState.folder[item.id || lookupId] = 'ok'; window.pkGridMediaStore.folder[item.id || lookupId] = foundThumb; window.pkGlobalThumbCache.add(item.id || lookupId); } } else if (item._coverResolved || (isFolder && globalCache.has(lookupId))) { item.thumbnail_link = null; item._coverResolved = false; item._isFolderLike = false; hasValidCover = false; if (typeof window.markStableFolderMediaState === 'function') { window.markStableFolderMediaState(item.id || lookupId, 'fail'); } else if (window.pkThumbTriState && window.pkThumbTriState.folder) { window.pkThumbTriState.folder[item.id || lookupId] = 'fail'; if (window.pkGridMediaStore && window.pkGridMediaStore.folder) delete window.pkGridMediaStore.folder[item.id || lookupId]; } } 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 = isBlurEnabledForView(isGridView() ? 'grid' : 'list'); 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 && (typeof isGridVideoItem === 'function' ? isGridVideoItem(item) : (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 (isGridMode && gridLayout) { getDynamicIcon(d); const isFolder = d.kind === 'drive#folder'; const displayDate = fmtDate(d.modified_time); const isVideo = !isFolder && (((d.mime_type || '').toLowerCase().startsWith('video/')) || (d.params && d.params.duration > 0)); const displayDur = isVideo ? (d._history_duration || d.params?.duration || 0) : 0; let folderItemCount = null; if (isFolder && !S.trashMode) { let count = d.file_count; if (count === undefined || count === null) count = d.usage?.file_count; if (count === undefined || count === null) count = d.params?.file_count; if (count === undefined || count === null) count = d.audit?.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; } } if (count !== undefined && count !== null && count !== '') { const parsed = parseInt(count, 10); if (!Number.isNaN(parsed)) folderItemCount = Math.max(0, parsed); } } const displayFolderCount = folderItemCount === null ? '' : L.grid_folder_count.replace('{n}', folderItemCount); const hasThumb = !!(d.thumbnail_link && d.thumbnail_link !== d.icon_link); const isStarred = S.starredSet.has(d.id) || !!(d.starred || (d.tags && d.tags.some(t => t.name === 'STAR'))); const nameDisplay = (S.search && shouldShowHl) ? getSearchHlHTML(d.name, S.search, 160) : esc(d.name); const coverCls = `pk-gv-cover ${isFolder ? 'pk-gv-folder' : 'pk-gv-file'}${hasThumb ? ' pk-gv-has-thumb' : ''}${isBlur && hasThumb ? ' pk-gv-blur' : ''}`; const iconFallback = getIcon(d).replace(/width="\d+"/g, 'width="108"').replace(/height="\d+"/g, 'height="108"'); const menuIcon = ``; ensureGridMediaStore(); const gridFileFallbackHtml = `
${iconFallback}
`.replace(/"/g, '"').replace(/\n/g, ''); const stableFolderMedia = hasThumb ? getStableFolderMediaState(d) : { state: 'none', src: '' }; let folderThumbState = stableFolderMedia.state; let folderResolvedSrc = stableFolderMedia.src || d.thumbnail_link || ''; if (hasThumb && d.kind === 'drive#folder' && folderResolvedSrc && d._coverResolved && folderThumbState !== 'ok') { if (typeof window.markStableFolderMediaState === 'function') { window.markStableFolderMediaState(d.id, 'ok', folderResolvedSrc); } else { if (!window.pkThumbTriState) window.pkThumbTriState = { folder: Object.create(null) }; if (!window.pkGridMediaStore) window.pkGridMediaStore = { folder: Object.create(null) }; if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); window.pkThumbTriState.folder[d.id] = 'ok'; window.pkGridMediaStore.folder[d.id] = folderResolvedSrc; window.pkGlobalThumbCache.add(d.id); } folderThumbState = 'ok'; } const folderHasCoverThumb = hasThumb && folderThumbState === 'ok' && !!folderResolvedSrc; if (hasThumb && folderThumbState === 'unknown' && !folderResolvedSrc) { window.pkThumbTriState.folder[d.id] = 'probing'; } const makeFolderFallbackHtml = (display = 'flex', opacity = '1') => d.icon_link ? `` : `${iconFallback}`; const folderPreviewHtml = folderHasCoverThumb ? `${makeFolderFallbackHtml('none','1')}` : (hasThumb ? `${makeFolderFallbackHtml('flex','1')}` : makeFolderFallbackHtml('flex','1')); const gridIconFallbackHtml = iconFallback.replace(/"/g, '"').replace(/\n/g, ''); const showFolderAnalyzeSize = isFolder && S.analyzeMode && !S.analyzeSimGroups && d.size !== undefined && d.size !== null && d.size !== ''; const showFolderDedupeSize = isFolder && S.analyzeMode && !!S.analyzeSimGroups && d.size !== undefined && d.size !== null && d.size !== ''; const folderSizeHtml = (showFolderAnalyzeSize || showFolderDedupeSize) ? `${fmtSize(d.size)}` : ''; const metaHtml = isFolder ? `
${folderSizeHtml}${displayDate}${displayFolderCount ? `${displayFolderCount}` : ''}
` : `
${fmtSize(d.size)}${displayDate}${displayDur > 0 ? `${fmtDur(displayDur)}` : ''}
`; const cleanName = (d.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const isBlMarked = isFolder ? (S.blFolderSet && S.blFolderSet.has(cleanName)) : (S.blSet && S.blSet.has(cleanName)); const gridBlHtml = isBlMarked ? `${CONF.icons.blMarker}` : ''; const videoPlayDisplay = 'none'; html = `
${isVideo && hasThumb ? `
` : ''} ${isStarred ? `${_internalGetStarIcon(true).replace('pk-star-toggle', 'pk-star-icon')}` : ''} ${gridBlHtml}
${nameDisplay} ${isProtected ? `${L.tag_default}` : ''}
${metaHtml}
`; } else 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 += `
${esc(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}` : ''; const saveClass = d.limit_count > 0 ? "pk-force-tip" : ""; 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 (i > 0) { const prevItem = S.display[i - 1]; if (!prevItem.isHeader) { const prevPStr = prevItem._pathStr || prevItem.path || ""; if (prevPStr === pStr) isSameAsPrev = true; else isSameAsPrev = false; } else { isSameAsPrev = false; } } else { isSameAsPrev = false; } 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) { let isSameAsPrev = d._isSameFolder || false; const rawPath = d._dupFullPath || ""; if (i > 0) { const prevItem = S.display[i - 1]; if (!prevItem.isHeader) { const prevRawPath = prevItem._dupFullPath || ""; if (prevRawPath === rawPath) isSameAsPrev = true; else isSameAsPrev = false; } else { isSameAsPrev = false; } } else { isSameAsPrev = false; } if (isSameAsPrev) { pathHtml = `${L.str_same_folder}`; } else { 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 (i > 0) { const prevItem = S.display[i - 1]; if (!prevItem.isHeader) { 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; row.dataset.pkBoundId = d.id || ''; row.dataset.pkBoundKind = d.kind || ''; if (suppressGridRebindTransition) { const reboundId = d.id || ''; requestAnimationFrame(() => { if (row.dataset.pkBoundId === reboundId) { row.style.transition = ''; row.style.animation = ''; } }); } if (isGridMode && typeof patchFrozenGridMedia === 'function') { patchFrozenGridMedia(row, d, prevGridMediaNode, prevGridMediaKey); } 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'; } if (typeof syncGridVideoPlayState === 'function') { syncGridVideoPlayState(row, d); } if (S.sel.has(d.id) && typeof updateStat === 'function') updateStat(); }; 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 (typeof syncGridVideoPlayState === 'function') { syncGridVideoPlayState(row, d); } if (S.sel.has(d.id) && typeof updateStat === 'function') updateStat(); }; 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 gridMoreBtn = row.querySelector('.pk-gv-more'); if (gridMoreBtn) { gridMoreBtn.onclick = (evt) => { evt.preventDefault(); evt.stopPropagation(); const openMenu = () => { const liveRow = UI.in.querySelector(`.pk-row[data-id="${d.id}"]`) || row; const liveBtn = liveRow.querySelector('.pk-gv-more') || gridMoreBtn; const btnRect = liveBtn.getBoundingClientRect(); liveRow.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: Math.max(btnRect.left + 8, btnRect.right - 8), clientY: btnRect.bottom + 6, button: 2 })); }; if (!S.isSelected(d.id) || S.activeId !== d.id) { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; S.lastSelIdx = i; renderVisible(); updateStat(); requestAnimationFrame(openMenu); return; } openMenu(); }; } 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') { if (d.file_id) { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); } 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.setAllSelection(false); S.setSelected(d.id, true); 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; if (d.phase !== 'PHASE_TYPE_COMPLETE') { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); 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.setAllSelection(false); S.setSelected(d.id, true); 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; if (S.analyzeMode && S.strictFolderFirstSnapshot !== null) { S.folderFirst = S.strictFolderFirstSnapshot === true; } else { S.folderFirst = false; } if (S.renderFolderFirst) S.renderFolderFirst(); S.saveNavScrollTop(S.getNavBucketKey(), S.path); 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) { const selectedCount = S.getSelectedCount(); const isCurrentSelected = S.isSelected(d.id); if (isProtectedView && selectedCount > 5) { if (selectedCount > 1 || !isCurrentSelected) { if (!await confirmSelectionClear()) return; } } S.setAllSelection(false); S.setSelected(d.id, true); 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)) { S.setSelected(item.id, targetState); } } } else { S.setSelected(d.id, chk.checked); } } 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.movingIds.has(item.id)) S.setSelected(item.id, true); } } else if (e.ctrlKey || e.metaKey) { S.toggleSelected(d.id); } else { const selectedCount = S.getSelectedCount(); const isCurrentSelected = S.isSelected(d.id); if (isProtectedView && selectedCount > 5) { if (selectedCount > 1 || !isCurrentSelected) { if (!await confirmSelectionClear()) { renderVisible(); return; } } } S.setAllSelection(false); S.setSelected(d.id, true); } } 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.isSelected(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 = isGridView() ? `${getGridCardRadius()}px` : '4px'; } const cls = getRowClassName(isSelected, isFocused, r.classList.contains('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 = Array.isArray(gNode.groupIds) ? gNode.groupIds : (S.dupMode ? (S.dupRawGroups[gIdx]?.ids || []) : (S.analyzeMode ? (S.analyzeSimGroups[gIdx]?.ids || []) : [])); let selCount = 0; gIds.forEach(id => { if (S.isSelected(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.saveNavScrollTop(S.getNavBucketKey(), S.path); 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.isSelected(d.id)) { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = 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'), detail: UI.ctx.querySelector('#ctx-sh-detail'), copy: UI.ctx.querySelector('#ctx-sh-copy') }; const selectedIds = S.getSelectedIds(); const isSingle = (selectedIds.length === 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'; 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') }; const uploadSelectedIds = S.getSelectedIds(); if (uploadSelectedIds.length > 1) { const items = uploadSelectedIds.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 ids = S.getSelectedIds(); const isSingle = ids.length === 1; 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 isFolderLike = (firstItem.mime_type && (firstItem.mime_type.includes('folder') || firstItem.mime_type.includes('directory'))) || (firstItem.icon_link && firstItem.icon_link.includes('folder')); const isReadyMedia = firstItem.phase === 'PHASE_TYPE_COMPLETE' && (isVid || isImg); const canOpen = isSingle && firstItem?.file_id && firstItem.phase !== 'PHASE_TYPE_ERROR' && (isReadyMedia || isFolderLike); 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_COMPLETE' && 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'; setBlacklistContextItemState(els.bl); 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'; setBlacklistContextItemState(tr.addBl); } }else { if(itms.share) itms.share.style.display = 'flex'; if(seps[0]) seps[0].style.display = 'block'; let hasGroup2 = false; const ctxSelectedIds = S.getSelectedIds(); const ctxSelectedCount = ctxSelectedIds.length; if (ctxSelectedCount === 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 ctxSelectedIds) { 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; const selectedCount = S.getSelectedCount(); if((S.starredMode || S.recentMode || S.historyMode || isSearchRoot || S.dupMode || S.isFlattened || S.analyzeMode || canLocateUpload) && selectedCount === 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 = (selectedCount > 1) ? `${ctxIcons.renameBulk} ${L.btn_bulkrename}` : `${ctxIcons.rename} ${L.ctx_rename}`; } } if(itms.copy) itms.copy.style.display = 'flex'; let hasFolder = false; const selectedIds = S.getSelectedIds(); for (const id of selectedIds) { 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'; setBlacklistContextItemState(itms.addBl); } 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'; }; } } flushVisibleRowPool(UI.in, poolPtr); if (window.pkRefreshTooltip) { requestAnimationFrame(() => { window.pkRefreshTooltip(); }); } } let isMarquee = false, mqStartX = 0, mqStartY = 0, startScroll = 0, lastMouseX = 0, lastMouseY = 0, scrollSpeed = 0, scrollRaf = null, marqueeRenderRaf = null, marqueeAutoScrollTs = 0; let lastRngS = -1, lastRngE = -1, cachedVpRect = null; const mqBox = document.createElement('div'); mqBox.className = 'pk-selection-box'; el.appendChild(mqBox); const scheduleMarqueeRefresh = () => { if (marqueeRenderRaf) return; marqueeRenderRaf = requestAnimationFrame(() => { marqueeRenderRaf = null; refreshVisibleSelectionState(); updateStat(); }); }; 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; const cssRectBottom = cachedVpRect.bottom; const cssRectLeft = cachedVpRect.left; 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 drawR = drawL + drawW; const logicLeft = Math.max(0, drawL - cssRectLeft); const logicRight = Math.max(logicLeft, drawR - cssRectLeft); const sIdx = Math.floor(logTop / CONF.rowHeight), eIdx = Math.min(S.display.length - 1, Math.floor(logBot / CONF.rowHeight)); if (sIdx !== lastRngS || eIdx !== lastRngE || isGridView()) { lastRngS = sIdx; lastRngE = eIdx; if (!window.event?.ctrlKey && !window.event?.metaKey) { S.selMode = 'explicit'; S.sel.clear(); S.selEx.clear(); } if (isGridView()) { const gridLayout = getGridLayout(); const dupGridMeta = isGroupedGridView() ? getGroupedGridMeta() : null; if (dupGridMeta && dupGridMeta.indexLayout) { dupGridMeta.indexLayout.forEach((layout, itemIdx) => { if (!layout || layout.kind !== 'item') return; const item = S.display[itemIdx]; if (!item || item.isHeader || S.movingIds.has(item.id)) return; const cardLeft = layout.left; const cardRight = cardLeft + layout.width; const cardTop = layout.top; const cardBottom = cardTop + layout.height; if (cardLeft < logicRight && cardRight > logicLeft && cardTop < logBot && cardBottom > logTop) { S.setSelected(item.id, true); } }); } else { const adjustedTop = Math.max(0, logTop - gridLayout.gapY); const adjustedBot = Math.max(adjustedTop, logBot - gridLayout.gapY); const startRow = Math.max(0, Math.floor(adjustedTop / CONF.rowHeight)); const endRow = Math.max(startRow, Math.floor(adjustedBot / CONF.rowHeight)); for (let rowIdx = startRow; rowIdx <= endRow; rowIdx++) { for (let colIdx = 0; colIdx < gridLayout.cols; colIdx++) { const itemIdx = rowIdx * gridLayout.cols + colIdx; if (itemIdx >= S.display.length) break; const item = S.display[itemIdx]; if (!item || item.isHeader || S.movingIds.has(item.id)) continue; const cardLeft = gridLayout.padX + colIdx * (gridLayout.cardWidth + gridLayout.gapX); const cardRight = cardLeft + gridLayout.cardWidth; const cardTop = gridLayout.gapY + rowIdx * CONF.rowHeight; const cardBottom = cardTop + gridLayout.cardHeight; if (cardLeft < logicRight && cardRight > logicLeft && cardTop < logBot && cardBottom > logTop) { S.setSelected(item.id, true); } } } } } else { for (let k = sIdx; k <= eIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) S.setSelected(item.id, true); } } scheduleMarqueeRefresh(); } }; const runAutoScroll = (ts = 0) => { if (!isMarquee || scrollSpeed === 0) { scrollRaf = null; marqueeAutoScrollTs = 0; return; } const dt = marqueeAutoScrollTs ? Math.min(32, ts - marqueeAutoScrollTs) : 16; marqueeAutoScrollTs = ts || performance.now(); const prevTop = UI.vp.scrollTop; const maxTop = Math.max(0, UI.vp.scrollHeight - UI.vp.clientHeight); const nextTop = Math.max(0, Math.min(maxTop, prevTop + (scrollSpeed * (dt / 16)))); if (nextTop === prevTop) { scrollSpeed = 0; scrollRaf = null; marqueeAutoScrollTs = 0; return; } UI.vp.scrollTop = nextTop; 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; S.activeId = null; UI.win.classList.add('pk-is-seeking'); refreshVisibleSelectionState(); } else { return; } } lastMouseX = e.clientX; lastMouseY = e.clientY; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const logicalClientY = e.clientY / scale; const edgeZone = 28; if (logicalClientY > cachedVpRect.bottom - edgeZone) scrollSpeed = Math.min(26, 2 + Math.pow((logicalClientY - (cachedVpRect.bottom - edgeZone)) / 6, 1.2)); else if (logicalClientY < cachedVpRect.top + edgeZone) scrollSpeed = -Math.min(26, 2 + Math.pow(((cachedVpRect.top + edgeZone) - logicalClientY) / 6, 1.2)); else scrollSpeed = 0; if (scrollSpeed !== 0 && !scrollRaf) scrollRaf = requestAnimationFrame(runAutoScroll); updateMarqueeUIAndSelection(e.clientX, e.clientY); }; const stopMarquee = () => { isMarquee = false; scrollSpeed = 0; marqueeAutoScrollTs = 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); if (marqueeRenderRaf) cancelAnimationFrame(marqueeRenderRaf); scrollRaf = null; marqueeRenderRaf = 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 selectedIds = S.getSelectedIds(); const count = selectedIds.length; const firstId = selectedIds[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); } else if (!firstItem) { text = L.str_selected_items || 'Selected Items'; } 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.isSelected(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 || L.str_target_folder)}`); 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 => { const normalizedK = (k === '' || k === 'root') ? 'root' : k; const altK = (normalizedK === 'root') ? '' : normalizedK; S.cache.delete(normalizedK); S.cache.delete(altK); if (typeof globalCache !== 'undefined') { globalCache.delete(normalizedK); globalCache.delete(altK); if (typeof scannedFolderIds !== 'undefined') { scannedFolderIds.delete(altK); } } if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(altK); } }); 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 = L.lbl_type_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 || L.lbl_type_folder; } else if (dropTargetType === 'crumb') { const pathNode = S.path.find(p => (p.id || 'root') === (dropTargetId || 'root')); targetName = pathNode ? pathNode.name : L.str_target_folder; } 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; const dragSelectedIds = S.getSelectedIds(); for (const id of dragSelectedIds) { 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; }; dragSelectedIds.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 || 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.isSelected(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 = getLogicalRect(UI.vp); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const safeRight = cachedVpRect.left + UI.vp.clientWidth; const safeBottom = cachedVpRect.top + UI.vp.clientHeight; const logicalClientX = e.clientX / scale; const logicalClientY = e.clientY / scale; if (logicalClientX > safeRight || logicalClientY > safeBottom) return; let startItem = null; if (isGridView()) { const gridLayout = getGridLayout(); const logicalOffsetX = logicalClientX - cachedVpRect.left; const logicalOffsetY = logicalClientY - cachedVpRect.top + UI.vp.scrollTop; let startIdx = -1; if (isGroupedGridView()) { startIdx = findDupGridHitIndex(logicalOffsetX, logicalOffsetY); } else { const totalRows = Math.max(1, Math.ceil(S.display.length / Math.max(1, gridLayout.cols))); if (logicalOffsetX >= gridLayout.padX && logicalOffsetY >= 0 && logicalOffsetY < totalRows * CONF.rowHeight) { const colStride = gridLayout.cardWidth + gridLayout.gapX; const relX = logicalOffsetX - gridLayout.padX; const rowIdx = Math.floor(logicalOffsetY / CONF.rowHeight); const colIdx = Math.floor(relX / Math.max(1, colStride)); const withinColX = relX - colIdx * colStride; const withinRowY = logicalOffsetY - rowIdx * CONF.rowHeight; if (colIdx >= 0 && colIdx < gridLayout.cols && withinColX >= 0 && withinColX <= gridLayout.cardWidth && withinRowY >= 0 && withinRowY <= gridLayout.cardHeight) { startIdx = rowIdx * gridLayout.cols + colIdx; } } } if (startIdx >= 0 && startIdx < S.display.length) { const hitItem = S.display[startIdx]; startItem = (hitItem && !hitItem.isHeader) ? hitItem : null; } } else { const startOffsetY = logicalClientY - cachedVpRect.top + UI.vp.scrollTop; const startIdx = Math.floor(startOffsetY / CONF.rowHeight); if (startIdx >= 0 && startIdx < S.display.length) { const hitItem = S.display[startIdx]; startItem = (hitItem && !hitItem.isHeader) ? hitItem : null; } } mqStartX = e.clientX; mqStartY = e.clientY; const prevActiveId = S.activeId; S.activeId = startItem ? startItem.id : null; if (!startItem && prevActiveId && S.getSelectedCount() === 0) { renderVisible(); requestAnimationFrame(() => { if (UI.chkAll) UI.chkAll.checked = false; }); } 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; let gridScrollEndTimer = 0; const markGridScrolling = () => { if (!isGridView() || !UI.win) return; UI.win.classList.add('pk-grid-scrolling'); if (gridScrollEndTimer) clearTimeout(gridScrollEndTimer); gridScrollEndTimer = setTimeout(() => { gridScrollEndTimer = 0; if (UI.win) UI.win.classList.remove('pk-grid-scrolling'); }, 90); }; UI.vp.onscroll = () => { if (UI.ctx && UI.ctx.style.display !== 'none') UI.ctx.style.display = 'none'; markGridScrolling(); if (!isScrollScheduled) { requestAnimationFrame(() => { renderVisible(); if (typeof isMarquee !== 'undefined' && isMarquee && !scrollRaf) { updateMarqueeUIAndSelection(lastMouseX, lastMouseY); } isScrollScheduled = false; }); isScrollScheduled = true; } }; UI.vp.addEventListener('wheel', () => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } markGridScrolling(); }, { 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; const selectedCount = S.getSelectedCount(); if (selectedCount > 0 || S.activeId) { const isAnalyzeRoot = S.analyzeMode && S.path[S.path.length - 1].id === 'analyze_root'; if ((S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups)) && selectedCount > 5) { if (!await confirmSelectionClear()) return; } S.clearSelection(); S.activeId = null; 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; el.addEventListener('click', (e) => { if (!S.activeId) return; if (e.target.closest('.pk-row')) return; if (e.target.closest('#pk-theme')) return; if (e.target.closest('#pk-view-switch') || e.target.closest('.pk-view-switch') || e.target.closest('#pk-view-list') || e.target.closest('#pk-view-grid') || e.target.closest('.pk-view-btn')) return; S.activeId = null; renderVisible(); }, { capture: true }); if (UI.vp) { UI.vp.addEventListener('click', (e) => { if (e.target.closest('.pk-row')) return; const rect = getLogicalRect(UI.vp); 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) => { const getLiveTriggerEl = () => { if (triggerEl && triggerEl.isConnected) return triggerEl; if (!UI.crumb) return triggerEl; const idx = S.path.findIndex(p => p.id === parentId); const seps = UI.crumb.querySelectorAll('.pk-crumb-sep'); return (idx >= 0 && seps[idx]) ? seps[idx] : triggerEl; }; const activeTriggerEl = getLiveTriggerEl(); if (activeTriggerEl && activeTriggerEl.classList.contains('pk-active')) { const oldPop = document.getElementById('pk-main-crumb-pop'); if (oldPop) oldPop.remove(); activeTriggerEl.classList.remove('pk-active'); activeTriggerEl.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 = []; const isAnalyzeSimMode = targetId === 'analyze_root' && S.analyzeSimGroups; if (isAnalyzeSimMode) { sourceItems = S.display.filter(item => !item.isHeader); } else 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}
`; document.body.appendChild(pop); const liveTriggerEl = getLiveTriggerEl(); if (!liveTriggerEl || !liveTriggerEl.isConnected) { pop.remove(); return; } const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; pop.style.zoom = scale; const rect = getLogicalRect(liveTriggerEl); 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 listId = targetId === 'root' ? '' : targetId; const cacheKey = targetId === 'root' ? 'root' : targetId; const isDirty = typeof globalDirtyFolders !== 'undefined' && globalDirtyFolders.has(listId); const rawCached = !isDirty ? ((typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) ? globalCache.get(cacheKey) : S.cache.get(cacheKey)) : null; const cachedItems = (rawCached && !Array.isArray(rawCached) && rawCached.items) ? rawCached.items : (Array.isArray(rawCached) ? rawCached : null); const cachedFolders = cachedItems ? cachedItems.filter(f => f.kind === 'drive#folder') : null; if (cachedFolders && cachedFolders.length > 0) { folders = cachedFolders; } else { folders = [{ id: 'loading', name: L.loading, kind: 'drive#folder' }]; apiList(listId).then(res => { if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, res); if (S.cache) S.cache.set(cacheKey, res); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.delete(listId); const freshFolders = Array.isArray(res) ? res.filter(f => f.kind === 'drive#folder') : []; const pop = document.getElementById('pk-main-crumb-pop'); if (!pop || pop.dataset.targetId !== targetId) return; renderMenu(freshFolders.length > 0 ? freshFolders : [{ id: 'error', name: L.str_no_files, kind: 'drive#folder' }]); }).catch(() => { const pop = document.getElementById('pk-main-crumb-pop'); if (pop && pop.dataset.targetId === targetId) { renderMenu([{ id: 'error', name: L.str_load_failed, kind: 'drive#folder' }]); } }); } } const renderMenu = (list) => { let s = S.sort, d = S.dir; const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.path.some(p => p.id === 'virtual_search_root'); if (isStandard) { 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 isAnalyzeSimMode = S.analyzeMode && targetId === 'analyze_root' && S.analyzeSimGroups; if (!isAnalyzeSimMode) { const collator = new Intl.Collator(({ 'zh': 'zh-CN', 'tc': 'zh-TW', 'ja': 'ja', 'ko': 'ko' })[getLang()] || 'en', { numeric: true, sensitivity: 'base' }); const parseSize = n => parseInt(n || 0); const parseTime = t => t ? new Date(t).getTime() : 0; 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); }; 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 (s === 'play_time') { const tA = a._history_ts || 0; const tB = b._history_ts || 0; if (tA !== tB) return (tB - tA) * d; } else if (s === 'modified_time') { const tA = parseTime(a.modified_time); const tB = parseTime(b.modified_time); if (tA !== tB) return (tB - tA) * d; } else if (s === 'created_time') { const tA = parseTime(a.created_time); const tB = parseTime(b.created_time); if (tA !== tB) return (tB - tA) * d; } else if (s === 'size') { const sizeA = parseSize(a.size); const sizeB = parseSize(b.size); if (sizeA !== sizeB) return (sizeB - sizeA) * d; } else if (s === 'play_time') { const ptA = a._history_ts || 0; const ptB = b._history_ts || 0; if (ptA !== ptB) return (ptB - ptA) * d; } else if (s === 'view_count' || s === 'save_count') { const valA = s === 'view_count' ? parseSize(a.view_count) : parseSize(a.save_count); const valB = s === 'view_count' ? parseSize(b.view_count) : parseSize(b.save_count); if (valA !== valB) return (valB - valA) * d; } else if (s === 'starred') { const isItemStarred = (item) => S.starredSet.has(item.id) || !!(item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))); const stA = isItemStarred(a) ? 1 : 0; const stB = isItemStarred(b) ? 1 : 0; if (stA !== stB) return stB - stA; const tA = parseTime(a.modified_time); const tB = parseTime(b.modified_time); return (tB - tA) * d; } else if (s === 'path') { const getPath = (item) => { if (S.analyzeMode) return item._pathStr || ""; if (item._lineage) return item._lineage.map(x => x.name).join('/'); return ""; }; const pA = getPath(a); const pB = getPath(b); const pathCmp = compareNames(pA, pB); if (pathCmp !== 0) return pathCmp * d; } return compareNames(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 = ''; const movingIds = (typeof pkState !== 'undefined') ? pkState.movingIds : new Set(); list.forEach(f => { const item = document.createElement('div'); const isMoving = movingIds.has(f.id); item.className = 'pk-crumb-item' + (isMoving ? ' pk-moving' : ''); 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; const isInPath = S.path.some(pathNode => pathNode.id === f.id); const textStyle = isInPath ? 'font-weight:bold; color:var(--pk-pri);' : ''; const isProtected = typeof isSystemItem === 'function' && isSystemItem(f); const tagHtml = isProtected ? `${L.tag_default}` : ''; item.innerHTML = `${iconHtml}${esc(f.name)}${tagHtml}`; 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 liveTriggerEl = getLiveTriggerEl(); if (!liveTriggerEl || !liveTriggerEl.isConnected) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; pop.style.zoom = scale; const rect = getLogicalRect(liveTriggerEl); const popRect = pop.getBoundingClientRect(); let left = rect.left - 4; const popW = popRect.width / scale; const winW = window.innerWidth / scale; 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'); liveTriggerEl.classList.add('pk-active'); liveTriggerEl.innerHTML = CONF.crumbIcons.down; }); const closeMenu = () => { if (pop.parentNode) pop.remove(); const liveTriggerEl = getLiveTriggerEl(); if (liveTriggerEl) { liveTriggerEl.classList.remove('pk-active'); liveTriggerEl.innerHTML = CONF.crumbIcons.right; const svg = liveTriggerEl.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) => { const liveTriggerEl = getLiveTriggerEl(); if (!pop.contains(evt.target) && !(liveTriggerEl && liveTriggerEl.contains(evt.target))) closeMenu(); }; window.addEventListener('resize', closeMenu); UI.vp.addEventListener('scroll', closeMenu, { passive: true }); setTimeout(() => document.addEventListener('mousedown', onOutsideClick), 10); }; 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(); } else { if (UI.btnRefresh && !UI.btnRefresh.disabled) UI.btnRefresh.click(); } }; UI.crumb.appendChild(s); const isLast = i === S.path.length - 1; let showArrow = !isLast; if (isLast) { const isUnsupportedVirtual = p.id === 'history_root' || p.id === 'offline_root' || p.id === 'upload_root'; if (!S.isFlattened && !S.dupMode && !isUnsupportedVirtual) { if (S.items && S.items.some(item => item.kind === 'drive#folder')) { showArrow = true; } } } if (showArrow) { 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') { if (typeof isSearchHistOpen === 'function' && isSearchHistOpen()) { closeSearchHist(); return; } const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; if (S.closePlayerOverlay()) return; if (S.closeImageOverlay()) return; if (S.closeTopModalOverlay()) return; if (UI.ctx.style.display === 'block') { UI.ctx.style.display = 'none'; return; } if (S.getSelectedCount() > 0) { S.clearSelection(); refresh(); } else if (UI.win.classList.contains('pk-maximized')) { if (!isTurbo && btnMax) btnMax.click(); } else { if (!isTurbo) UI.btnClose.click(); } return; } const hasMediaOverlay = document.querySelector('.pk-img-ov, #pk-player-ov'); if (hasMediaOverlay) return; if (!e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { if (e.key === 'f' || e.key === 'F') { if (UI.btnImgSearch && !UI.btnImgSearch.disabled && UI.btnImgSearch.style.display !== 'none') { e.preventDefault(); UI.btnImgSearch.click(); return; } } } if (e.key === 'Delete' && e.altKey) { e.preventDefault(); if (S.shareMode || 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'); if (hasActiveOverlay) return; if (e.key === 'F2') { e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; const selectedCount = S.getSelectedCount(); if (selectedCount === 1) { if (UI.btnRename.disabled) return; UI.btnRename.click(); } else if (selectedCount > 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.altKey && (e.key === 'm' || e.key === 'M')) { e.preventDefault(); if (UI.btnMigrate && !UI.btnMigrate.disabled && UI.btnMigrate.style.display !== 'none') { UI.btnMigrate.click(); } return; } if ((e.key === 'm' || e.key === 'M') && !e.altKey && !e.ctrlKey && !e.metaKey) { 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); if (document._pkPlayerCloseClickHandler) { document.removeEventListener('click', document._pkPlayerCloseClickHandler, true); } document._pkPlayerCloseClickHandler = (e) => { const btn = e.target && e.target.closest ? e.target.closest('#pk_p_close') : null; if (!btn) return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); S.closePlayerOverlay(); }; document.addEventListener('click', document._pkPlayerCloseClickHandler, true); if (document._pkImageCloseClickHandler) { document.removeEventListener('click', document._pkImageCloseClickHandler, true); } document._pkImageCloseClickHandler = (e) => { const btn = e.target && e.target.closest ? e.target.closest('#pk_img_close') : null; if (!btn) return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); S.closeImageOverlay(); }; document.addEventListener('click', document._pkImageCloseClickHandler, true); let sideMouseBusy = false; let lastSideMouseBtn = -1; let lastSideMouseTs = 0; const mouseSideNavHandler = async (e) => { const btn = e.button; if (btn !== 3 && btn !== 4) return; const win = document.querySelector('.pk-ov'); if (!win || win.style.display === 'none') return; S.sideNavLog('event', { type: e.type, button: btn, target: e.target }); const now = Date.now(); if (lastSideMouseBtn === btn && (now - lastSideMouseTs) < 250) { S.sideNavLog('blocked: duplicate'); e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); return; } lastSideMouseBtn = btn; lastSideMouseTs = now; const hasFrontOverlay = !!document.querySelector('.pk-modal-ov, .pk-img-ov, #pk-player-ov'); if (!hasFrontOverlay) { const t = e.target; if (t && (t.closest('textarea') || t.closest('input:not([type="checkbox"]):not([type="button"]):not([type="submit"])') || t.closest('[contenteditable="true"]'))) { S.sideNavLog('pass-through: editing target'); return; } } e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); if (sideMouseBusy) { S.sideNavLog('blocked: busy'); return; } sideMouseBusy = true; try { if (btn === 3) { S.sideNavLog('dispatch: back'); await S.handleMouseSideBack(); } else { S.sideNavLog('dispatch: forward'); await S.handleMouseSideForward(); } } catch (err) { S.sideNavLog('error', err); throw err; } finally { setTimeout(() => { sideMouseBusy = false; }, 0); } }; if (window._pkMouseSideNavHandler) { window.removeEventListener('mousedown', window._pkMouseSideNavHandler, true); window.removeEventListener('mouseup', window._pkMouseSideNavHandler, true); window.removeEventListener('auxclick', window._pkMouseSideNavHandler, true); window.removeEventListener('click', window._pkMouseSideNavHandler, true); } window._pkMouseSideNavHandler = mouseSideNavHandler; window.addEventListener('mousedown', mouseSideNavHandler, { capture: true, passive: false }); window.addEventListener('mouseup', mouseSideNavHandler, { capture: true, passive: false }); window.addEventListener('auxclick', mouseSideNavHandler, { capture: true, passive: false }); window.addEventListener('click', mouseSideNavHandler, { capture: true, passive: false }); 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.getSelectedCount() === 1) { const id = S.getSelectedIds()[0]; const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder' && item.name === CONF.SYSTEM_FOLDER_NAME) { S.clearSelection(); renderVisible(); updateStat(); } } } }; document.addEventListener('mouseup', mouseHandler); function updateStat() { syncLocalUploadVisibility(); let total = 0; const visibleIdSet = new Set(); const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; if (item && !item.isHeader && !(S.movingIds && S.movingIds.has(item.id))) { total++; visibleIdSet.add(item.id); } } if (S.selMode === 'all') { const validExcludedIds = new Set(); for (const id of S.selEx) { if (visibleIdSet.has(id)) validExcludedIds.add(id); } if (validExcludedIds.size !== S.selEx.size) { S.selEx = validExcludedIds; } } else { const validSelectedIds = S.getSelectedIds().filter(id => visibleIdSet.has(id)); const hadShrink = validSelectedIds.length !== S.getSelectedCount(); if (hadShrink) { S.setExplicitSelection(validSelectedIds); } } const selectedIds = S.getSelectedIds(); let n = selectedIds.length; const btnInvert = document.getElementById('pk-btn-invert'); if (btnInvert) { btnInvert.style.display = (!S.dupMode && n > 0) ? 'flex' : 'none'; btnInvert.style.color = 'var(--pk-fg)'; } const btnGridInvert = document.getElementById('pk-grid-invert'); if (btnGridInvert) { btnGridInvert.style.display = (!S.dupMode && n > 0) ? 'flex' : 'none'; btnGridInvert.style.color = 'var(--pk-fg)'; } let hasProtectedItem = false; let hasSelFolder = false; let selSize = 0; if (n > 0) { for (const id of selectedIds) { 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; 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; let isSingleVideo = false; let isSingleMediaWithCover = false; if (n === 1) { const id = selectedIds[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); const isImg = mime.startsWith('image/') || ['jpg','jpeg','png','gif','bmp','webp','heic'].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 ((isVid || isImg) && isTaskReady) { if (item.thumbnail_link && item.thumbnail_link !== item.icon_link) { const isMax = UI.win.classList.contains('pk-maximized'); const isBlur = typeof isBlurEnabledForView === 'function' ? isBlurEnabledForView(isGridView() ? 'grid' : 'list') : (typeof gmGet !== 'undefined' ? gmGet('pk_blur_thumb', false) : false); if (isMax && isBlur) { isSingleMediaWithCover = true; } else if (window.pkGlobalThumbCache && window.pkGlobalThumbCache.has(item.id)) { isSingleMediaWithCover = true; } } } } } if (UI.btnExt) UI.btnExt.disabled = S.trashMode || !isSingleVideo; if (UI.btnImgSearch) UI.btnImgSearch.disabled = S.trashMode || !isSingleMediaWithCover; if (isUpload) { if (hasSel) { let hasRunning = false; let hasStopped = false; for (const id of selectedIds) { 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 = selectedIds.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; } if (UI.btnMigrate) { UI.btnMigrate.disabled = !hasSel; UI.btnMigrate.style.cursor = UI.btnMigrate.disabled ? 'not-allowed' : 'pointer'; UI.btnMigrate.style.opacity = UI.btnMigrate.disabled ? '0.4' : '1'; } 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 = selectedIds.some(id => isArchive(S.itemMap.get(id))); UI.btnUnzip.disabled = S.trashMode || !hasArchive; } } async function getLinks() { const res = []; for (const id of S.getSelectedIds()) { 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 }; }; const launchPotPlayerWithFallback = (cleanUrl, opts = {}) => { const L = getStrings(); const button = opts.button || null; const timeout = opts.timeout || 3500; const source = normalizePotPlayerLaunchSource(opts.source); const launchAt = Date.now(); let copiedAt = launchAt; let likelyOpened = false; let finished = false; let timer = null; let confirmTimer = null; let focusProbe = null; const markOpened = () => { likelyOpened = true; }; const onVisibility = () => { if (document.hidden) markOpened(); }; const cleanup = () => { window.removeEventListener('blur', markOpened); document.removeEventListener('visibilitychange', onVisibility, true); if (timer) clearTimeout(timer); if (confirmTimer) clearTimeout(confirmTimer); if (focusProbe) clearInterval(focusProbe); }; recordPotPlayerLaunchAttempt(source, launchAt); try { GM_setClipboard(cleanUrl); copiedAt = Date.now(); } catch (e) {} if (button) { button.disabled = true; button.textContent = L.msg_potplayer_launching; } window.addEventListener('blur', markOpened, { once: true }); document.addEventListener('visibilitychange', onVisibility, true); if (typeof document.hasFocus === 'function') { focusProbe = setInterval(() => { if (!document.hasFocus()) markOpened(); }, 120); } try { const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanUrl} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; } catch (e) {} const finishLikelyOpen = () => { if (finished) return; finished = true; cleanup(); recordPotPlayerLaunchResult('likely_open', { source, launchAt }); if (typeof opts.onLikelyOpen === 'function') opts.onLikelyOpen(); }; const finishLikelyFail = () => { if (finished) return; finished = true; cleanup(); recordPotPlayerLaunchResult('likely_fail', { source, launchAt, copiedAt }); const failToastDuration = opts.failToastDuration || 6500; const failCloseDelay = opts.failCloseDelay || 3200; showToast(L.msg_potplayer_maybe_failed, 'warning', failToastDuration); const notifyLikelyFail = () => { if (typeof opts.onLikelyFail === 'function') { opts.onLikelyFail({ source, launchAt, copiedAt, cleanUrl, autoRepairPrompt: opts.autoRepairPrompt, autoRepairPromptDelay: opts.autoRepairPromptDelay }); } }; if (button) { button.disabled = true; button.textContent = L.msg_potplayer_link_copied; button.classList.add('pk-copy-success-freeze'); button.style.setProperty('background', '#52c41a', 'important'); button.style.setProperty('color', '#fff', 'important'); } setTimeout(notifyLikelyFail, failCloseDelay); }; timer = setTimeout(() => { if (finished) return; if (likelyOpened) { finishLikelyOpen(); return; } const confirmDelay = Number.isFinite(Number(opts.confirmFailDelay)) ? Math.max(0, Number(opts.confirmFailDelay)) : 0; if (confirmDelay > 0) { confirmTimer = setTimeout(() => { if (finished) return; if (likelyOpened) { finishLikelyOpen(); return; } finishLikelyFail(); }, confirmDelay); return; } finishLikelyFail(); }, timeout); return cleanup; }; const openPotPlayerProtocolRepairHelper = (playUrl, opts = {}) => { const L = getStrings(); const cleanUrl = String(playUrl || ''); const isAutoPrompt = opts.autoPrompt === true; const title = isAutoPrompt ? L.potplayer_fix_auto_title : L.potplayer_fix_title; const autoReason = isAutoPrompt ? `
${L.potplayer_fix_reason_auto}
` : ''; const protocolState = readPotPlayerProtocolState(); const savedCustomPath = normalizePotPlayerCustomPath(protocolState.customPlayerPath); const versionOutdated = !!protocolState.confirmedRepairVersion && protocolState.confirmedRepairVersion !== CONF.potplayerProtocolRepairVersion; const getPathStatusText = () => { const state = readPotPlayerProtocolState(); const checked = validatePotPlayerCustomPath(state.customPlayerPath); if (!checked.ok) return L.potplayer_fix_path_status_empty; return (state.confirmedRepairVersion === CONF.potplayerProtocolRepairVersion && state.pathBoundRepairVersion === CONF.potplayerProtocolRepairVersion) ? L.potplayer_fix_path_status_confirmed : L.potplayer_fix_path_status_unconfirmed; }; const m = showModal(`

${title}

${autoReason}
${L.potplayer_fix_desc}
${L.potplayer_fix_manual_tip}
${versionOutdated ? `
${L.potplayer_fix_version_outdated}
` : ''}
${L.potplayer_fix_advanced_desc}
${getPathStatusText()}
${L.potplayer_fix_browser_policy_desc}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.classList.add('pk-potfix-modal'); Object.assign(modalBox.style, { width: '480px', padding: '24px', height: 'auto', minHeight: 'auto' }); modalBox.style.setProperty('max-height', '92vh', 'important'); modalBox.style.setProperty('overflow-x', 'hidden', 'important'); modalBox.style.setProperty('overflow-y', 'auto', 'important'); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '18px', right: '18px' }); } const pathInput = m.querySelector('#pk_potfix_path'); const pathStatus = m.querySelector('#pk_potfix_path_status'); const refreshPathStatus = () => { if (pathStatus) pathStatus.textContent = getPathStatusText(); }; m.querySelector('#pk_potfix_copy').onclick = () => { try { GM_setClipboard(cleanUrl); } catch (e) {} showToast(L.potplayer_fix_copy_success || L.msg_copy_success); }; m.querySelector('#pk_potfix_delete').onclick = () => { downloadPotPlayerRegFile('delete'); showToast(L.potplayer_fix_delete_downloaded); }; m.querySelector('#pk_potfix_save_path').onclick = () => { const result = savePotPlayerCustomPath(pathInput ? pathInput.value : ''); if (!result.ok) { showToast(L.potplayer_fix_path_invalid, 'warning'); return; } if (pathInput) pathInput.value = result.path; refreshPathStatus(); showToast(result.changed && result.wasConfigured ? L.potplayer_fix_path_changed : L.potplayer_fix_path_saved, result.changed && result.wasConfigured ? 'warning' : 'success'); }; m.querySelector('#pk_potfix_custom_install').onclick = () => { const result = savePotPlayerCustomPath(pathInput ? pathInput.value : ''); if (!result.ok) { showToast(L.potplayer_fix_path_invalid, 'warning'); return; } if (pathInput) pathInput.value = result.path; refreshPathStatus(); if (!downloadPotPlayerRegFile('install')) { showToast(L.potplayer_fix_path_invalid, 'warning'); return; } showToast(L.potplayer_fix_install_downloaded); }; m.querySelector('#pk_potfix_browser_policy').onclick = () => { downloadPotPlayerRegFile('browser_policy'); showToast(L.potplayer_fix_browser_policy_downloaded); }; m.querySelector('#pk_potfix_retry').onclick = () => { const savedPath = getValidSavedPotPlayerPath(); if (!savedPath) { const result = savePotPlayerCustomPath(pathInput ? pathInput.value : ''); if (!result.ok) { showToast(L.potplayer_fix_retry_need_path || L.potplayer_fix_path_invalid, 'warning'); return; } if (pathInput) pathInput.value = result.path; refreshPathStatus(); showToast(L.potplayer_fix_retry_need_path || L.potplayer_fix_path_saved, 'warning'); return; } recordPotPlayerProtocolUserFixed(); clearPotPlayerLaunchFailCount(); showToast(L.potplayer_fix_confirmed); m.remove(); launchPotPlayerWithFallback(cleanUrl, { button: opts.button || null, source: typeof opts.source === 'string' ? opts.source : 'normal', failToastDuration: 6500, failCloseDelay: 3200, confirmFailDelay: CONF.potplayerPostRepairConfirmDelay, autoRepairPrompt: false, onLikelyOpen: () => { if (typeof opts.onRetryLikelyOpen === 'function') opts.onRetryLikelyOpen(); }, onLikelyFail: () => { if (typeof opts.onRetryLikelyFail === 'function') opts.onRetryLikelyFail(); } }); }; m.querySelector('#pk_potfix_skip_today').onclick = () => { suppressPotPlayerAutoPromptToday(); m.remove(); showToast(L.potplayer_fix_skip_today_done); }; m.querySelector('#pk_potfix_never_auto').onclick = () => { disablePotPlayerAutoPrompt(); m.remove(); showToast(L.potplayer_fix_never_auto_done); }; m.querySelector('#pk_potfix_close').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); m.remove(); } }); return m; }; 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; let mediaSessionToken = 0; let activeHealthTimer = null; let activeHlsObjectUrl = null; const isStaleMediaSession = (token) => token !== mediaSessionToken || isPlayerDestroyed; 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; let selectedErrUrl = recommendedUrl; let selectedErrName = recommended.name; let selectedErrPlayer = (savedPlayer === 'other') ? 'other' : 'potplayer'; const resOptions = qualityList.map(q => { const qUrl = q.link || q.url; const isSelected = qUrl === recommendedUrl; return `
${q.name}
`; }).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}
${selectedErrName}
${CONF.crumbIcons.down}
${resOptions}
${L.lbl_player}
${selectedErrPlayer === 'potplayer' ? 'PotPlayer' : L.opt_player_other}
${CONF.crumbIcons.down}
PotPlayer
${L.opt_player_other}
`; 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(); }; const launchBtn = dialog.querySelector('#pk_err_launch_btn'); const fixBtn = dialog.querySelector('#pk_err_potplayer_fix'); const closeErrMenus = () => dialog.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none'); const removeErrDialog = () => { document.removeEventListener('click', closeErrMenus); dialog.remove(); }; const getCleanErrSelectedUrl = () => { let cleanLink = String(selectedErrUrl || '').replace('&ext=.m3u8', ''); if (cleanLink.includes('ts_downloader') && cleanLink.includes('url=')) { try { const urlParam = new URL(cleanLink).searchParams.get('url'); if (urlParam) cleanLink = decodeURIComponent(urlParam); } catch (e) {} } return cleanLink; }; const updateErrPotPlayerFixEntry = () => { if (fixBtn) fixBtn.style.display = selectedErrPlayer === 'potplayer' ? 'inline-flex' : 'none'; }; const bindErrSelect = (id, onSelect) => { const container = dialog.querySelector(`#${id}`); if (!container) return; const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('.pk-select-trigger > span'); if (!trigger || !menu || !txt) return; trigger.onclick = (e) => { e.stopPropagation(); const isOpen = menu.style.display === 'block'; closeErrMenus(); menu.style.display = isOpen ? '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, item.textContent); }; }); }; dialog.querySelector('.pk-err-close').onclick = (e) => { e.stopPropagation(); removeErrDialog(); }; dialog.onclick = (e) => e.stopPropagation(); bindErrSelect('pk_err_res_cs', (val, text) => { selectedErrUrl = val; selectedErrName = text; }); bindErrSelect('pk_err_player_cs', (val) => { selectedErrPlayer = val; launchBtn.textContent = (val === 'other') ? L.btn_copy_link : L.btn_start_play; updateErrPotPlayerFixEntry(); }); launchBtn.textContent = (selectedErrPlayer === 'other') ? L.btn_copy_link : L.btn_start_play; setTimeout(() => document.addEventListener('click', closeErrMenus), 0); if (fixBtn) { fixBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openPotPlayerProtocolRepairHelper(getCleanErrSelectedUrl(), { button: launchBtn, source: 'error_fallback', onRetryLikelyOpen: () => { removeErrDialog(); destroyPlayer(); }, onRetryLikelyFail: () => { removeErrDialog(); destroyPlayer(); } }); }; } launchBtn.onclick = (e) => { e.stopPropagation(); const selPlayer = selectedErrPlayer; const selResName = selectedErrName; gmSet('pk_ext_player', selPlayer); const cleanLink = getCleanErrSelectedUrl(); if (selPlayer === 'other') { GM_setClipboard(cleanLink); launchBtn.textContent = L.msg_copy_success; launchBtn.style.background = "#52c41a"; launchBtn.style.color = "#fff"; setTimeout(() => { removeErrDialog(); if (typeof destroyPlayer === 'function') destroyPlayer(); }, 800); } else if (selPlayer === 'potplayer') { launchPotPlayerWithFallback(cleanLink, { button: launchBtn, source: 'error_fallback', restoreText: L.btn_start_play, restoreBg: launchBtn.style.background, restoreColor: launchBtn.style.color, failToastDuration: 6500, failCloseDelay: 3200, onLikelyOpen: () => { removeErrDialog(); destroyPlayer(); }, onLikelyFail: (ctx) => { removeErrDialog(); destroyPlayer(); schedulePotPlayerAutoRepairPrompt(cleanLink, { source: 'error_fallback', autoRepairPrompt: ctx && ctx.autoRepairPrompt, autoRepairPromptDelay: ctx && ctx.autoRepairPromptDelay }); } }); } 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(); } removeErrDialog(); 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 coverSrc = (v.thumbnail_link && v.thumbnail_link !== v.icon_link) ? v.thumbnail_link : ''; const phSrc = v.icon_link || v.thumbnail_link || ''; const phSvg = getIcon(v); return `
${phSrc ? `` : phSvg}
${coverSrc ? `` : ``}
`; }).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++; mediaSessionToken++; const myReqId = switchReqId; isSwitching = true; lastWorkingLink = null; lastWorkingResName = null; failedUrls.clear(); 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 (activeHealthTimer) { clearInterval(activeHealthTimer); activeHealthTimer = null; } if (pkHls) { try { pkHls.stopLoad(); } catch (e) {} try { pkHls.detachMedia(); } catch (e) {} try { pkHls.destroy(); } catch (e) {} pkHls = null; } if (activeHlsObjectUrl) { try { URL.revokeObjectURL(activeHlsObjectUrl); } catch (e) {} activeHlsObjectUrl = null; } if (v) { v.pause(); v._bufferingSince = null; v._blackScreenCount = 0; 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(); } 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) { syncVideoPlistItems( scrollMode === 'instant' ? 'instant' : (box.classList.contains('plist-active') ? 'smooth' : false) ); } 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'; isSwitching = false; } } }; 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) { const knownDur = (item.params && item.params.duration) || S.durationMap.get(pId) || gmGet('pk_duration_' + pId, 0); if (knownDur > 0 && skipOp >= knownDur) { console.warn(`[AutoSkip] Intro skip (${skipOp}s) exceeds total duration (${knownDur}s), ignored.`); startTime = 0; } else { 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'); } const currentMediaToken = ++mediaSessionToken; if (activeHealthTimer) { clearInterval(activeHealthTimer); activeHealthTimer = null; } if (pkHls) { try { pkHls.stopLoad(); } catch (e) {} try { pkHls.detachMedia(); } catch (e) {} try { pkHls.destroy(); } catch (e) {} pkHls = null; } if (activeHlsObjectUrl) { try { URL.revokeObjectURL(activeHlsObjectUrl); } catch (e) {} activeHlsObjectUrl = null; } v._pkMediaToken = currentMediaToken; v._bufferingSince = null; v._blackScreenCount = 0; 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); activeHlsObjectUrl = finalPlayUrl; 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 (isStaleMediaSession(currentMediaToken) || !pkHls) { clearInterval(healthTimer); if (activeHealthTimer === healthTimer) activeHealthTimer = null; 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); if (activeHealthTimer === healthTimer) activeHealthTimer = null; handleVideoError({ force: true, target: v, mediaToken: currentMediaToken }); } } 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); if (activeHealthTimer === healthTimer) activeHealthTimer = null; handleVideoError({ force: true, target: v, mediaToken: currentMediaToken }); } } }, 1000); activeHealthTimer = healthTimer; pkHls.on(window.Hls.Events.LEVEL_LOADED, function (event, data) { if (isStaleMediaSession(currentMediaToken)) return; 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) { if (isStaleMediaSession(currentMediaToken)) return; 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 (isStaleMediaSession(currentMediaToken)) return; 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, mediaToken: currentMediaToken }); } else { console.log("[Hls] Network error, trying to recover..."); pkHls.startLoad(); } break; case window.Hls.ErrorTypes.MEDIA_ERROR: pkHls.recoverMediaError(); setTimeout(() => { if (isStaleMediaSession(currentMediaToken)) return; if (v.paused && !box.querySelector('.pk-err-dialog')) { handleVideoError({ target: v, mediaToken: currentMediaToken }); } }, 1500); break; default: handleVideoError({ target: v, mediaToken: currentMediaToken }); break; } } else if (data.details === 'bufferStalledError' && v.currentTime < 1) { handleVideoError({ target: v, mediaToken: currentMediaToken }); } }); } else { if (isStaleMediaSession(currentMediaToken)) return; 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
${posterUrl ? `` : ''}
${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' }); } }; const bindVideoPlistItems = () => { 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, 10); if (idx === curListIdx) return; softSwitch(idx); }; }); }; const syncVideoPlistItems = (scrollMode = 'smooth') => { const RANGE = 150; const desiredStart = Math.max(0, curListIdx - RANGE); const desiredEnd = Math.min(videoPlaylist.length, curListIdx + RANGE + 1); const prevStart = parseInt(pScroll.dataset.pkStart || '-1', 10); const prevEnd = parseInt(pScroll.dataset.pkEnd || '-1', 10); const needRebuild = !Number.isFinite(prevStart) || !Number.isFinite(prevEnd) || curListIdx < prevStart || curListIdx >= prevEnd || pScroll.childElementCount === 0 || pScroll.dataset.pkTotal !== String(videoPlaylist.length); if (needRebuild) { pScroll.innerHTML = renderPlaylistItems(); pScroll.dataset.pkStart = String(desiredStart); pScroll.dataset.pkEnd = String(desiredEnd); pScroll.dataset.pkTotal = String(videoPlaylist.length); bindVideoPlistItems(); } pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { const idx = parseInt(el.dataset.idx, 10); el.classList.toggle('active', idx === curListIdx); }); const activeItem = pScroll.querySelector('.pk-p-plist-item.active'); if (activeItem && plist.classList.contains('open') && scrollMode !== false) { 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); } } if (typeof updatePlistNav === 'function') updatePlistNav(); }; bindVideoPlistItems(); 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(() => syncVideoPlistItems(false), 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 (e && e.mediaToken && e.mediaToken !== mediaSessionToken) 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 initReqId = switchReqId; const initItem = item; const targetApiId = getPhysicalId(initItem); const newData = await apiGet(targetApiId); if (isPlayerDestroyed || initReqId !== switchReqId || item !== initItem) return; 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 = getLogicalRect(box); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const logicalX = clientX / scale; const pos = Math.max(0, Math.min(1, (logicalX - rect.left) / rect.width)); const targetTime = pos * v.duration; if (!isFinite(targetTime)) return; let left = logicalX - boxRect.left; const halfWidth = (imgBox.offsetWidth / 2) || 80; const minX = halfWidth + 10; const maxX = boxRect.width - 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; const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; document.removeEventListener('keydown', playerKeyHandler); if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); } window.removeEventListener('resize', onResizeTransform); if (transcodeTimer) { clearInterval(transcodeTimer); transcodeTimer = null; } if (activeHealthTimer) { clearInterval(activeHealthTimer); activeHealthTimer = null; } if (activeHlsObjectUrl) { try { URL.revokeObjectURL(activeHlsObjectUrl); } catch (e) {} activeHlsObjectUrl = 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 = getItemScrollTopByIndex(targetIdx); 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._pkDestroyPlayer = null; d.remove(); }; d._pkDestroyPlayer = destroyPlayer; v.addEventListener('play', () => { S.navVideoBackArmed = false; updateState(); }); v.addEventListener('pause', updateState); const markStarted = () => { if (isPlayerDestroyed || isSwitching) 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 blockMediaDragWhenErr = (e) => { if (!box.querySelector('.pk-err-dialog')) return; e.preventDefault(); e.stopPropagation(); }; v.addEventListener('dragstart', blockMediaDragWhenErr); 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'); let seekIndicatorTimer = null; const showSeekIndicatorAt = (targetTime) => { if (!seekIndicator || !v.duration || isNaN(v.duration)) return; seekIndicator.textContent = `${fmtFullT(targetTime)} / ${fmtFullT(v.duration)}`; seekIndicator.style.display = 'flex'; box.classList.add('pk-is-seeking'); clearTimeout(seekIndicatorTimer); seekIndicatorTimer = setTimeout(() => { if (!isDragSeek) { seekIndicator.style.display = 'none'; box.classList.remove('pk-is-seeking'); } }, 900); }; const updateVisualOnly = (clientX) => { if (!progRectCache || !v.duration) return 0; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const logicalX = clientX / scale; const pos = Math.max(0, Math.min(1, (logicalX - 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 = getLogicalRect(topBar); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; if ((e.clientY / scale) >= barRect.top && (e.clientY / scale) <= 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 = getLogicalRect(progArea); 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 = getLogicalRect(progArea); 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 = getLogicalRect(box); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const lx = e.clientX / scale, ly = e.clientY / scale; if (lx <= rect.left || lx >= rect.right || ly <= rect.top || ly >= 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) { initPosterImg.addEventListener('dragstart', blockMediaDragWhenErr); 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 if (curLang === 'id') { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/id/search2/sublanguageid-ind/moviename-${plusQ}` }, { name: 'Samehadaku', url: `https://samehadaku.li/?s=${plusQ}` }, { name: 'Anoboy', url: `https://anoboy.be/?s=${plusQ}` } ]; } else if (curLang === 'ms') { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ms/search2/sublanguageid-may/moviename-${plusQ}` }, { name: 'PencuriMovie', url: `https://pencurimovie.my/?s=${plusQ}` }, { name: 'Oh Flix', url: `https://vvww.ohflix.my.id/?s=${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, Math.min(3600, valOp + delta)); opVal.textContent = valOp + " " + L.unit_sec; gmSet('pk_skip_intro', valOp); } else { valEd = Math.max(0, Math.min(3600, 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; const skipOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; if (skipEd > 0 && v.duration > 0 && !hasTriggeredEnd) { if (skipEd >= v.duration || (skipOp + skipEd) >= v.duration || v.currentTime < (v.duration / 2)) return; 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 = Math.min(3600, val); gmSet('pk_skip_intro', valOp); d.querySelector('#pk_op_val').textContent = valOp + " " + L.unit_sec; }); makeEditable(d.querySelector('#pk_ed_val'), 'int', (val) => { valEd = Math.min(3600, val); gmSet('pk_skip_outro', valEd); d.querySelector('#pk_ed_val').textContent = valEd + " " + 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 || 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'); showSeekIndicatorAt(targetTime); } 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'); showSeekIndicatorAt(targetTime); } 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 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); 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 coverSrc = (v.thumbnail_link && v.thumbnail_link !== v.icon_link) ? v.thumbnail_link : ''; const phSrc = v.icon_link || v.thumbnail_link || ''; const phSvg = getIcon(v); return `
${phSrc ? `` : phSvg}
${coverSrc ? `` : ``}
`; }).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'); } }; d.addEventListener('wheel', (e) => { if (isLongImageMode) return; e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; let newScale = Math.max(0.1, Math.min(10, scale + delta)); 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 * newScale; let curH = ih * baseRatio * newScale; if (Math.abs(rotation % 180) === 90) [curW, curH] = [curH, curW]; const limitX = curW > vw ? (curW - vw) / 2 : 0; const limitY = curH > vh ? (curH - vh) / 2 : 0; transX = Math.max(-limitX, Math.min(limitX, transX)); transY = Math.max(-limitY, Math.min(limitY, transY)); } scale = newScale; 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 * z; startY = e.clientY - transY * z; }); document.addEventListener('mousemove', (e) => { if (!isDrag || isLongImageMode) return; const z = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let tx = (e.clientX - startX) / z; let ty = (e.clientY - startY) / 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 : 0; const limitY = curH > vh ? (curH - vh) / 2 : 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._pkResizeHandler = resizeHandler; d._pkImgList = imgList; d._pkGetCurIdx = () => curIdx; Object.defineProperty(d, '_pkCurIdx', { get: () => curIdx, configurable: true }); d.querySelector('#pk_img_close').onclick = (e) => { e.stopPropagation(); S.closeImageOverlay(); }; 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}`; const RANGE = 150; const desiredStart = Math.max(0, curIdx - RANGE); const desiredEnd = Math.min(imgList.length, curIdx + RANGE + 1); const prevStart = parseInt(pScroll.dataset.pkStart || '-1', 10); const prevEnd = parseInt(pScroll.dataset.pkEnd || '-1', 10); const needRebuild = !Number.isFinite(prevStart) || !Number.isFinite(prevEnd) || curIdx < prevStart || curIdx >= prevEnd || pScroll.childElementCount === 0 || pScroll.dataset.pkTotal !== String(imgList.length); if (needRebuild) { pScroll.innerHTML = renderImgListItems(); pScroll.dataset.pkStart = String(desiredStart); pScroll.dataset.pkEnd = String(desiredEnd); pScroll.dataset.pkTotal = String(imgList.length); pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const idx = parseInt(e.currentTarget.dataset.idx, 10); 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, 10); 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') { S.closeImageOverlay(); } 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; } `; let isImageSearchRunning = false; async function startImageSearch(mediaElement, fileName, containerElement, originalLink) { if (isImageSearchRunning) return; isImageSearchRunning = true; 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; let progressTask = null; if (typeof FloatBarManager !== 'undefined') { progressTask = FloatBarManager.create(L.str_processing); } 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 FAST_TIMEOUT = 4000; const uploadTask = (url, formData, parseType, stageText) => { return new Promise((resolve, reject) => { if (progressTask) progressTask.update(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; } if (progressTask) { progressTask.update(L.str_redirecting.replace('Google Lens', engineName)); const el = document.querySelector('.pk-float-bar-item .pk-spin-lg'); if (el) el.style.borderColor = '#4CAF50'; } await sleep(600); window.open(jumpUrl, '_blank'); } catch (err) { console.warn("Auto upload failed, switching to manual fallback:", err); if (progressTask) progressTask.update(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 (progressTask) progressTask.update(L.err_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}`); if (progressTask) progressTask.destroy(); const fallbackOv = document.createElement('div'); fallbackOv.className = 'pk-search-running-mask'; fallbackOv.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;'; fallbackOv.innerHTML = `
⚠️
${L.msg_copy_success}
${hintText}
`; const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (fsEl) { fsEl.appendChild(fallbackOv); } else if (containerElement) { containerElement.appendChild(fallbackOv); } else { document.body.appendChild(fallbackOv); } await sleep(2500); fallbackOv.remove(); 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); if (progressTask) progressTask.destroy(); const errorMsg = e.message.includes('Tainted') ? L.err_cors_blocked : e.message; if (typeof showToast !== 'undefined') { showToast(`${errorMsg}`, 'error'); } } finally { if (progressTask) progressTask.destroy(); isImageSearchRunning = false; } } 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 = getLogicalRect(triggerEl); let popLeft = rect.left; if (popLeft + 420 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 430; pop.style.top = (rect.bottom + 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 = getLogicalRect(UI.filterExtsWrap); let popLeft = rect.left; if (popLeft + 340 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 350; pop.style.top = (rect.bottom + 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; syncLocalUploadVisibility(); if (S.dupMode) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.clearSelection(); 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(); } }; const searchHist = UI.searchHist; const searchWrap = UI.searchInput.closest('.pk-search') || UI.searchInput.parentNode; let searchHistRaf = 0; let searchHistOpen = false; if (searchHist) { searchHist.style.display = 'none'; searchHist.style.visibility = ''; searchHist.style.pointerEvents = ''; } const isSearchHistOpen = () => { return !!searchHist && searchHistOpen && getHistory()?.length > 0; }; const requestPlaceSearchHist = () => { if (!isSearchHistOpen()) return; if (searchHistRaf) cancelAnimationFrame(searchHistRaf); searchHistRaf = requestAnimationFrame(() => { searchHistRaf = requestAnimationFrame(() => { searchHistRaf = 0; if (isSearchHistOpen()) placeSearchHist(); }); }); }; const placeSearchHist = () => { if (!searchHist || !searchWrap) return; if (!searchHist._pkOriginParent) searchHist._pkOriginParent = searchWrap; if (searchHist.parentNode !== document.body) { document.body.appendChild(searchHist); } searchHist.classList.toggle('pk-dark', el.classList.contains('pk-dark')); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const wrapRect = typeof getLogicalRect === 'function' ? getLogicalRect(searchWrap) : searchWrap.getBoundingClientRect(); const pad = 8; searchHist.dataset.pkPortal = '1'; searchHist.style.position = 'fixed'; searchHist.style.left = '0'; searchHist.style.top = '0'; searchHist.style.right = 'auto'; searchHist.style.bottom = 'auto'; searchHist.style.marginTop = '0'; searchHist.style.zIndex = '10080'; searchHist.style.zoom = scale; searchHist.style.transformOrigin = 'top left'; searchHist.style.width = Math.max(Math.round(wrapRect.width), 220) + 'px'; searchHist.style.display = 'flex'; searchHist.style.visibility = 'hidden'; searchHist.style.pointerEvents = 'none'; const histRect = searchHist.getBoundingClientRect(); const popW = histRect.width / scale; const popH = histRect.height / scale; const winW = window.innerWidth / scale; const winH = window.innerHeight / scale; let left = wrapRect.left; let top = wrapRect.bottom + 6; if (left + popW > winW - pad) left = winW - pad - popW; if (left < pad) left = pad; if (top + popH > winH - pad) { top = wrapRect.top - popH - 6; } if (top < pad) top = pad; searchHist.style.left = Math.round(left) + 'px'; searchHist.style.top = Math.round(top) + 'px'; searchHist.style.visibility = ''; searchHist.style.pointerEvents = ''; }; const closeSearchHist = () => { if (!searchHist) return; searchHistOpen = false; if (searchHistRaf) cancelAnimationFrame(searchHistRaf); searchHistRaf = 0; searchHist.style.display = 'none'; searchHist.style.visibility = ''; searchHist.style.pointerEvents = ''; }; UI.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isSearchHistOpen()) { e.preventDefault(); e.stopPropagation(); closeSearchHist(); } }); UI.searchInput.onfocus = () => { renderHistory(); if (getHistory()?.length > 0) { searchHistOpen = true; searchHist.style.display = 'flex'; requestPlaceSearchHist(); } else { closeSearchHist(); } }; document.addEventListener('mousedown', (e) => { if (!UI || !UI.searchInput || !searchHist) return; if (!searchHist.contains(e.target) && !searchWrap.contains(e.target)) { closeSearchHist(); } }, true); if (!window.__pkSearchHistPortalBound) { window.addEventListener('resize', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, { passive: true }); document.addEventListener('scroll', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, true); if (window.visualViewport) { window.visualViewport.addEventListener('resize', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, { passive: true }); window.visualViewport.addEventListener('scroll', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, { passive: true }); } window.__pkSearchHistPortalBound = true; } if (window.ResizeObserver && !searchWrap.__pkSearchHistRO) { searchWrap.__pkSearchHistRO = new ResizeObserver(() => { if (!searchHistOpen) return; if (isSearchHistOpen()) requestPlaceSearchHist(); }); searchWrap.__pkSearchHistRO.observe(searchWrap); } 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 = []; syncLocalUploadVisibility(); 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.clearSelection(); 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(); if (S._flattenEnterTopRaf) cancelAnimationFrame(S._flattenEnterTopRaf); if (S.isFlattened && isGridView()) { S._flattenEnterTopRaf = requestAnimationFrame(() => { S._flattenEnterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } } 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; syncLocalUploadVisibility(); 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) { const didBuildFullGlobalIndex = isSyncOnly && !isPartialScan; if (didBuildFullGlobalIndex) { 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; if (!isSyncOnly && !isSilent && total === 0) { setLoad(false); S.scanning = false; showToast(L.msg_no_files); 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; return; } 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(); } } rememberFolderFirstBeforeStrictMode(); S.isFlattened = true; S.sort = 'modified_time'; S.dir = 1; UI.chkAll.checked = false; S.clearSelection(); 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(); if (S._flattenEnterTopRaf) cancelAnimationFrame(S._flattenEnterTopRaf); if (S.isFlattened && isGridView()) { S._flattenEnterTopRaf = requestAnimationFrame(() => { S._flattenEnterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } const msg = L.msg_scan_done.replace('{n}', total).replace('{f}', processedFolders); if (!isSilent) showToast(msg.replace(/\n+/g, ' '), 'success', 3200); } } } 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 = S.getSelectedIds() .map(id => S.itemMap.get(id)) .filter(Boolean); 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; syncLocalUploadVisibility(); 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; syncLocalUploadVisibility(); 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 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(); } } rememberFolderFirstBeforeStrictMode(); S.dupMode = true; S.groupSortOp = 'path'; S.groupSortDir = 1; S.isFlattened = false; S.sort = 'modified_time'; S.dir = 1; UI.chkAll.checked = false; S.clearSelection(); S.search = ''; S.lastGlobalResults = []; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.searchHist) UI.searchHist.style.display = 'none'; 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(); if (S._enterTopRaf) cancelAnimationFrame(S._enterTopRaf); S._enterTopRaf = requestAnimationFrame(() => { S._enterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } } 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.clearSelection(); 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 () => { await S.exitVirtualNavMode(); 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; gmSet('pk_folder_first', S.folderFirst); try { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); globalPref.sort = S.sort; globalPref.dir = S.dir; globalPref.folderFirst = S.folderFirst; gmSet('pk_global_sort_pref', JSON.stringify(globalPref)); } catch(e) { gmSet('pk_global_sort_pref', JSON.stringify({ sort: S.sort, dir: S.dir, folderFirst: S.folderFirst })); } if (S.renderFolderFirst) 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; S.invertSelection(); 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; const totalVisible = S.getSelectableCount(); const selectedCount = S.getSelectedCount(); const isSelectAllAction = selectedCount < totalVisible; if (UI.chkAll) { UI.chkAll.checked = isSelectAllAction; UI.chkAll.indeterminate = false; } if (isSelectAllAction) { S.setAllSelection(true); } else { S.setAllSelection(false); } requestAnimationFrame(() => { renderVisible(); updateStat(); if (UI.chkAll) { UI.chkAll.checked = isSelectAllAction; UI.chkAll.indeterminate = false; } }); }; if (UI.btnAnaSelect || UI.btnAnaSort || UI.btnDupSmart || UI.btnDupSort) { 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}
`; const sortPop = document.createElement('div'); sortPop.className = 'pk-ana-pop'; sortPop.innerHTML = `
${L.opt_sort_time}
${L.opt_sort_size}
${L.opt_sort_path}
${L.opt_sort_name}
`; UI.win.appendChild(pop); UI.win.appendChild(sortPop); let activeTargetBtn = null; let activePop = null; const updatePopPos = () => { if (!activePop || activePop.style.display !== 'flex' || !activeTargetBtn) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const winRect = getLogicalRect(UI.win); const btnRect = getLogicalRect(activeTargetBtn); let left = btnRect.left - winRect.left; const top = btnRect.bottom - winRect.top; const winWidth = winRect.width; const popWidth = activePop === sortPop ? 200 : 340; if (left + popWidth > winWidth - 10) { left = btnRect.right - winRect.left - popWidth; } activePop.style.left = Math.max(10, left) + 'px'; activePop.style.top = (top + 5) + 'px'; }; const togglePop = (e, btn, targetPop) => { e.stopPropagation(); const isVisible = (targetPop.style.display === 'flex' && activeTargetBtn === btn); if (activePop && activePop !== targetPop) activePop.style.display = 'none'; if (isVisible) { targetPop.style.display = 'none'; activeTargetBtn = null; activePop = null; } else { if (targetPop === pop && S.analyzeMode && !S.hasShownAnaWarn) { showToast(L.msg_ana_warn, 'warning', 6000); S.hasShownAnaWarn = true; } if (targetPop === sortPop) { if (!S.groupSortOp) { S.groupSortOp = 'path'; S.groupSortDir = 1; } sortPop.querySelectorAll('.pk-sort-opt').forEach(el => { let baseTxt = ""; if (el.dataset.op === 'time') baseTxt = L.opt_sort_time; if (el.dataset.op === 'size') baseTxt = L.opt_sort_size; if (el.dataset.op === 'path') baseTxt = L.opt_sort_path; if (el.dataset.op === 'name') baseTxt = L.opt_sort_name; if (el.dataset.op === S.groupSortOp) { el.textContent = baseTxt + (S.groupSortDir === 1 ? ' ▼' : ' ▲'); el.style.color = 'var(--pk-pri)'; } else { el.textContent = baseTxt; el.style.color = ''; } }); } targetPop.style.display = 'flex'; activeTargetBtn = btn; activePop = targetPop; updatePopPos(); } }; if (UI.btnAnaSelect) UI.btnAnaSelect.onclick = (e) => togglePop(e, UI.btnAnaSelect, pop); if (UI.btnAnaSort) UI.btnAnaSort.onclick = (e) => togglePop(e, UI.btnAnaSort, sortPop); if (UI.btnDupSmart) UI.btnDupSmart.onclick = (e) => togglePop(e, UI.btnDupSmart, pop); if (UI.btnDupSort) UI.btnDupSort.onclick = (e) => togglePop(e, UI.btnDupSort, sortPop); window.addEventListener('resize', updatePopPos); pop.querySelectorAll('.pk-ana-opt').forEach(opt => { opt.onclick = () => { const op = opt.dataset.op; const nextSel = []; 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) nextSel.push(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) nextSel.push(m.id); }); }); } S.setExplicitSelection(nextSel); pop.style.display = 'none'; activeTargetBtn = null; activePop = null; renderVisible(); updateStat(); }; }); sortPop.querySelectorAll('.pk-sort-opt').forEach(opt => { opt.onclick = (e) => { e.stopPropagation(); const op = opt.dataset.op; if (S.groupSortOp === op) { S.groupSortDir *= -1; } else { S.groupSortOp = op; S.groupSortDir = 1; } sortPop.querySelectorAll('.pk-sort-opt').forEach(el => { let baseTxt = ""; if (el.dataset.op === 'time') baseTxt = L.opt_sort_time; if (el.dataset.op === 'size') baseTxt = L.opt_sort_size; if (el.dataset.op === 'path') baseTxt = L.opt_sort_path; if (el.dataset.op === 'name') baseTxt = L.opt_sort_name; if (el.dataset.op === S.groupSortOp) { el.textContent = baseTxt + (S.groupSortDir === 1 ? ' ▼' : ' ▲'); el.style.color = 'var(--pk-pri)'; } else { el.textContent = baseTxt; el.style.color = ''; } }); const cmp = (a, b) => { let res = 0; if (S.groupSortOp === 'time') { res = new Date(b.modified_time) - new Date(a.modified_time); } else if (S.groupSortOp === 'size') { const sa = BigInt(a.size || 0), sb = BigInt(b.size || 0); res = sa < sb ? 1 : (sa > sb ? -1 : 0); } else if (S.groupSortOp === 'path') { const pa = a._dupFullPath || a._pathStr || a.path || ""; const pb = b._dupFullPath || b._pathStr || b.path || ""; res = pa.localeCompare(pb); } else if (S.groupSortOp === 'name') { res = a.name.localeCompare(b.name); } return res * S.groupSortDir; }; const newDisplay = []; let currentGroupHeader = null; let currentGroupMembers = []; const flushGroup = () => { if (currentGroupHeader) newDisplay.push(currentGroupHeader); if (currentGroupMembers.length > 0) { const getDupPath = it => it._dupFullPath || it._pathStr || it.path || ""; let orderedMembers = currentGroupMembers; if (S.pinnedDupPath) { const pinnedMembers = []; const otherMembers = []; currentGroupMembers.forEach(it => { if (getDupPath(it) === S.pinnedDupPath) pinnedMembers.push(it); else otherMembers.push(it); }); if (pinnedMembers.length > 0) { pinnedMembers.sort(cmp); otherMembers.sort(cmp); orderedMembers = pinnedMembers.concat(otherMembers); } else { orderedMembers = currentGroupMembers.slice().sort(cmp); } } else { orderedMembers = currentGroupMembers.slice().sort(cmp); } let lastPath = null; orderedMembers.forEach((it, idx) => { const curPath = getDupPath(it); if (idx > 0 && curPath === lastPath && curPath !== "") { it._isSameFolder = true; } else { it._isSameFolder = false; } lastPath = curPath; }); newDisplay.push(...orderedMembers); } currentGroupHeader = null; currentGroupMembers = []; }; S.display.forEach(d => { if (d.isHeader) { flushGroup(); currentGroupHeader = d; } else { currentGroupMembers.push(d); } }); flushGroup(); S.display = newDisplay; renderVisible(); }; }); document.addEventListener('mousedown', (e) => { const isClickInsideBtn = (UI.btnAnaSelect && UI.btnAnaSelect.contains(e.target)) || (UI.btnAnaSort && UI.btnAnaSort.contains(e.target)) || (UI.btnDupSmart && UI.btnDupSmart.contains(e.target)) || (UI.btnDupSort && UI.btnDupSort.contains(e.target)); if (!pop.contains(e.target) && !sortPop.contains(e.target) && !isClickInsideBtn) { pop.style.display = 'none'; sortPop.style.display = 'none'; activeTargetBtn = null; activePop = 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 selectedCount = S.getSelectedCount(); const analyzeTargetDesc = selectedCount > 0 ? L.lbl_analyze_selected.replace('{n}', selectedCount) : L.lbl_analyze_current; const anaSimTargetDesc = selectedCount > 0 ? L.lbl_ana_sim_selected.replace('{n}', selectedCount) : 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(() => m.focus(), 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 }]; }; const analyzeSelectedIds = S.getSelectedIds(); if (analyzeSelectedIds.length > 0) { analyzeSelectedIds.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; syncLocalUploadVisibility(); 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)}`; showToast(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 })); } rememberFolderFirstBeforeStrictMode(); 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(); if (S._enterTopRaf) cancelAnimationFrame(S._enterTopRaf); S._enterTopRaf = requestAnimationFrame(() => { S._enterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); 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 () => { const ids = S.getSelectedIds(); const totalSelected = ids.length; if (totalSelected === 0) return; if (totalSelected > 10000) { showToast(L.str_copying); await sleep(10); } const itemList = []; let count = 0; for (const id of ids) { 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 () => { const ids = S.getSelectedIds(); if (ids.length === 0) return; const itemList = []; let count = 0; for (const id of ids) { 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 || L.str_target_folder; await executeFileTransfer(items, type, normalize(srcId), normalize(destId), targetFolderName); }; UI.btnRename.onclick = async () => { if (S.getSelectedCount() !== 1) return; const id = S.getSelectedIds()[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 = () => { const selectedIds = S.getSelectedIds(); const selectedCount = selectedIds.length; if (selectedCount < 2) return; if (S.movingIds && S.movingIds.size > 0) { const hasConflict = selectedIds.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}', selectedCount).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 === "" ? L.str_empty_str : (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) { const mode = m.querySelector('input[name="rn_mode"]:checked').value; const selectedIds = S.getSelectedIds(); const selectedIdSet = new Set(selectedIds); const selectedItems = S.display.filter(i => !i.isHeader && selectedIdSet.has(i.id)); const isSeriesFolderOnly = mode === 'pattern' && selectedItems.length > 0 && selectedItems.every(i => i.kind === 'drive#folder'); rnIn.innerHTML = `
${isSeriesFolderOnly ? L.rn_tip_series_folder_only : 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 = S.getSelectedIds(); const selectedIdSet = new Set(selectedIds); 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 (!selectedIdSet.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 = `
❌ ${L.err_worker_failed}
`; 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 = S.getSelectedIds() .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); const delRes = await showDeleteConfirm(confirmMsg); if (delRes.confirm) { const allIds = toDeleteList.map(f => f.id); await executeBatchDelete(allIds, { silent: true, forceRefresh: false, hardDelete: delRes.hardDelete }); 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 = S.getSelectedIds(); 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 = S.getSelectedIds(); 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 = () => { const count = S.getSelectedCount(); if (count === 0) return; 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 = S.getSelectedIds(); const filesToTrash = []; const ghostsToHardDelete = []; ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task) { task._deleted = true; const forceDelete = task.status !== 'DONE'; task._deleteFileIntent = forceDelete || isDeleteFile; if (task.file_id) { if (forceDelete) ghostsToHardDelete.push(task.file_id); else if (isDeleteFile) filesToTrash.push(task.file_id); } if (S.upMng) S.upMng.pause(task, true); } }); const idSet = S.sel; S.uploadTasks = S.uploadTasks.filter(t => { if(idSet.has(t.id)) { if (S.upMng) S.upMng.removeTask(t.id); return false; } return true; }); S.clearSelection(); load(false, true); if (filesToTrash.length > 0) { try { await executeBatchDelete(filesToTrash, { silent: false, hardDelete: false, forceRefresh: false }); } catch(e) { console.error("Failed to trash uploaded files:", e); } } if (ghostsToHardDelete.length > 0) { try { await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ghostsToHardDelete }) }); ghostsToHardDelete.forEach(id => { if (typeof window.pkRemoveGhostFile === 'function') window.pkRemoveGhostFile(id); }); } catch (e) { console.error("Failed to hard delete ghost files:", e); } } 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 filesToTrash = []; const ghostsToHardDelete = []; const count = S.uploadTasks.length; S.uploadTasks.forEach(task => { task._deleted = true; const forceDelete = task.status !== 'DONE'; task._deleteFileIntent = forceDelete || isDeleteFile; if (S.upMng) { S.upMng.pause(task, true); S.upMng.removeTask(task.id); } if (task.file_id) { if (forceDelete) ghostsToHardDelete.push(task.file_id); else if (isDeleteFile) filesToTrash.push(task.file_id); } }); S.uploadTasks = []; S.clearSelection(); load(false, true); if (filesToTrash.length > 0) { try { await executeBatchDelete(filesToTrash, { silent: false, hardDelete: false, forceRefresh: false }); } catch(e) {} } if (ghostsToHardDelete.length > 0) { try { await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ghostsToHardDelete }) }); ghostsToHardDelete.forEach(id => { if (typeof window.pkRemoveGhostFile === 'function') window.pkRemoveGhostFile(id); }); } catch (e) {} } 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 = S.getSelectedIds()[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'); const fixBtn = m.querySelector('#ext_potplayer_fix'); const getCleanSelectedUrl = () => { let cleanUrl = String(selectedUrl || '').replace('&ext=.m3u8', ''); if (cleanUrl.includes('ts_downloader') && cleanUrl.includes('url=')) { try { const urlParam = new URL(cleanUrl).searchParams.get('url'); if (urlParam) cleanUrl = decodeURIComponent(urlParam); } catch (e) {} } return cleanUrl; }; const updatePotPlayerFixEntry = () => { if (fixBtn) fixBtn.style.display = selectedPlayer === 'potplayer' ? 'inline-flex' : 'none'; }; bindSelect('cs_res', (val) => { selectedUrl = val; }); bindSelect('cs_player', (val) => { selectedPlayer = val; runBtn.textContent = (val === 'potplayer') ? L.btn_start_play : L.btn_copy_link; updatePotPlayerFixEntry(); }); 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') { if (e.target && e.target.id === 'ext_potplayer_fix') return; e.preventDefault(); e.stopPropagation(); runBtn.click(); } }); if (fixBtn) { fixBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openPotPlayerProtocolRepairHelper(getCleanSelectedUrl(), { button: runBtn, source: 'normal', onRetryLikelyOpen: () => m.remove(), onRetryLikelyFail: () => m.remove() }); }; } runBtn.onclick = () => { gmSet('pk_ext_player', selectedPlayer); const cleanUrl = getCleanSelectedUrl(); if (selectedPlayer === 'potplayer') { launchPotPlayerWithFallback(cleanUrl, { button: runBtn, source: 'normal', restoreText: L.btn_start_play, restoreBg: runBtn.style.background, restoreColor: runBtn.style.color, failToastDuration: 6500, failCloseDelay: 3200, onLikelyOpen: () => m.remove(), onLikelyFail: (ctx) => { m.remove(); schedulePotPlayerAutoRepairPrompt(cleanUrl, { source: 'normal', autoRepairPrompt: ctx && ctx.autoRepairPrompt, autoRepairPromptDelay: ctx && ctx.autoRepairPromptDelay }); } }); } else { GM_setClipboard(cleanUrl); runBtn.textContent = L.msg_copy_success; runBtn.style.background = "#52c41a"; runBtn.disabled = true; setTimeout(() => m.remove(), 1000); } }; }; if (UI.btnImgSearch) { UI.btnImgSearch.onclick = () => { if (UI.btnImgSearch.disabled) return; const id = S.getSelectedIds()[0]; const item = S.itemMap.get(id); if (!item || !item.thumbnail_link) return; const thumbUrl = item.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE'); const thumbImg = new Image(); thumbImg.crossOrigin = 'anonymous'; thumbImg.src = thumbUrl; startImageSearch(thumbImg, item.name, document.body, thumbUrl); }; } UI.win.querySelector('#pk-down').onclick = async () => { const selectedIds = S.getSelectedIds(); const hasConflict = selectedIds.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; selectedIds.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); const filterStats = { scanned: 0, blocked: 0 }; const fSizeMinStr = gmGet('pk_dl_filter_size_min', ''); const fSizeMaxStr = gmGet('pk_dl_filter_size_max', ''); const fSizeUnitStr = gmGet('pk_dl_filter_size_unit', 'MB'); let fSizeMin = fSizeMinStr ? parseFloat(fSizeMinStr) : -1; let fSizeMax = fSizeMaxStr ? parseFloat(fSizeMaxStr) : -1; if (fSizeUnitStr === 'GB') { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024 * 1024; } else if (fSizeUnitStr === 'TB') { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024 * 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024 * 1024 * 1024; } else { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024; } const hasDownloadFilterRules = fExts.length > 0 || fNames.length > 0 || (Number.isFinite(fSizeMin) && fSizeMin >= 0) || (Number.isFinite(fSizeMax) && fSizeMax >= 0); try { await coreRecursiveEngine(rootNodes, { signal, onFile: (f) => { filterStats.scanned++; const lowName = f.name.toLowerCase(); const ext = lowName.split('.').pop(); let isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n)); if (!isBlocked && (fSizeMin >= 0 || fSizeMax >= 0)) { const sz = parseInt(f.size || 0); if (fSizeMin >= 0 && sz < fSizeMin) isBlocked = true; if (fSizeMax >= 0 && sz > fSizeMax) isBlocked = true; } if (isBlocked) { filterStats.blocked++; return; } 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); if (hasDownloadFilterRules && filterStats.scanned > 0 && filterStats.blocked === filterStats.scanned) { showToast(L.msg_batch_all_filtered.replace('{n}', filterStats.blocked)); } else { showToast(L.msg_batch_no_files); } return; } if (hasDownloadFilterRules && filterStats.blocked > 0) { showToast(L.msg_batch_filtered.replace('{n}', filterStats.blocked)); } let totalBytes = 0; for (let i = 0; i < allFiles.length; i++) { totalBytes += parseInt((allFiles[i] && allFiles[i].size) || 0, 10) || 0; } const needsSoftConfirm = allFiles.length > CONF.browserDownloadConfirmFileCount || totalBytes > CONF.browserDownloadConfirmTotalBytes; setLoad(false); if (needsSoftConfirm) { const confirmText = L.msg_down_confirm_total .replace('{n}', allFiles.length) .replace('{s}', fmtSize(totalBytes)) .replace('{fc}', CONF.browserDownloadConfirmFileCount) .replace('{fs}', fmtSize(CONF.browserDownloadConfirmTotalBytes)); if (!await showConfirm(confirmText)) return; } progressTask = FloatBarManager.create(L.msg_batch_hydrating); const readyFiles = []; const hydrateQueue = [...allFiles]; const activeTasks = new Set(); const hydrateWithRetry = async (file, maxRetries = 3) => { if (file.web_content_link) return file; if (file.phase === "PHASE_TYPE_PENDING" || file.phase === "PHASE_TYPE_RUNNING" || file.trashed) return null; 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; if (detail && (detail.phase === "PHASE_TYPE_PENDING" || detail.phase === "PHASE_TYPE_RUNNING")) return null; 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) { readyFiles.push(detail); } else if (detail === null) { console.warn(`[Hydrate Skipped] ${file.name}: File is pending/running or trashed`); } } catch (e) { console.error(`[Hydrate Failed] ${file.name}:`, 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); } else { await sleep(1500); } } 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 selectedIds = S.getSelectedIds(); const hasConflict = selectedIds.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}
${L.btn_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 inpTEye = m.querySelector('#btn_pop_aria_token_eye'); 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; let popAriaTokenVisible = false; 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 testUrl = normalizeAriaRpcUrl(url); const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: buildAriaRpcParams(token) }; try { await aria2RpcRequest(testUrl, payload, 3000); 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(); }; if (inpTEye) { inpTEye.onclick = (e) => { e.preventDefault(); e.stopPropagation(); popAriaTokenVisible = !popAriaTokenVisible; inpT.style.webkitTextSecurity = popAriaTokenVisible ? 'none' : 'disc'; inpTEye.innerHTML = popAriaTokenVisible ? CONF.icons.eyeOff : CONF.icons.eye; }; } 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 { u = normalizeAriaRpcUrl(u); const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: buildAriaRpcParams(t) }; await aria2RpcRequest(u, payload, 5000); 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); }; ariaUrl = normalizeAriaRpcUrl(ariaUrl); 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 }; const filterStats = { scanned: 0, blocked: 0 }; const fSizeMinStr = gmGet('pk_dl_filter_size_min', ''); const fSizeMaxStr = gmGet('pk_dl_filter_size_max', ''); const fSizeUnitStr = gmGet('pk_dl_filter_size_unit', 'MB'); let fSizeMin = fSizeMinStr ? parseFloat(fSizeMinStr) : -1; let fSizeMax = fSizeMaxStr ? parseFloat(fSizeMaxStr) : -1; if (fSizeUnitStr === 'GB') { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024 * 1024; } else if (fSizeUnitStr === 'TB') { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024 * 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024 * 1024 * 1024; } else { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024; } const hasDownloadFilterRules = fExts.length > 0 || fNames.length > 0 || (Number.isFinite(fSizeMin) && fSizeMin >= 0) || (Number.isFinite(fSizeMax) && fSizeMax >= 0); const selectedIdsForAria2 = S.getSelectedIds(); selectedIdsForAria2.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) => { filterStats.scanned++; const lowName = f.name.toLowerCase(); const ext = lowName.split('.').pop(); let isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n)); if (!isBlocked && (fSizeMin >= 0 || fSizeMax >= 0)) { const sz = parseInt(f.size || 0); if (fSizeMin >= 0 && sz < fSizeMin) isBlocked = true; if (fSizeMax >= 0 && sz > fSizeMax) isBlocked = true; } if (isBlocked) { filterStats.blocked++; return; } 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); if (hasDownloadFilterRules && filterStats.scanned > 0 && filterStats.blocked === filterStats.scanned) { showToast(L.msg_batch_all_filtered.replace('{n}', filterStats.blocked)); } else { showToast(L.msg_batch_no_files); } return; } if (hasDownloadFilterRules && filterStats.blocked > 0) { showToast(L.msg_batch_filtered.replace('{n}', filterStats.blocked)); } 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; if (file.phase === "PHASE_TYPE_PENDING" || file.phase === "PHASE_TYPE_RUNNING" || file.trashed) return null; 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; if (detail && (detail.phase === "PHASE_TYPE_PENDING" || detail.phase === "PHASE_TYPE_RUNNING")) return null; 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); } else if (detail === null) { console.warn(`[Hydrate Skipped] ${file.name}: File is pending/running or trashed`); } } 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: buildAriaRpcParams(ariaToken, [[f.web_content_link], { out: outPath, header: [`User-Agent: ${navigator.userAgent}`, `Referer: https://mypikpak.com/`] }]) }; }); try { await aria2RpcRequest(ariaUrl, payload, Math.max(10000, chunk.length * 800)); 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} ${L.str_items}\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 = 200; const SKIP_VERIFY = false; 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 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 (!hardDelete && 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 () => { const selectedIds = S.getSelectedIds(); const count = selectedIds.length; if (!count) return; if (S.historyMode) { if (!await showConfirm(L.warn_clear_history.replace('{n}', count))) return; const selIds = new Set(selectedIds); 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 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(selectedIds, { 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 = selectedIds.length; 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 (const id of selectedIds) { 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) { showToast(L.msg_del_protected.replace('{n}', blacklistedCount)); } if (blacklistedCount > 0 && toDeleteIds.length === 0) { S.clearSelection(); refresh(); updateStat(); return; } if (toDeleteIds.length === 0) { showAlert(L.msg_del_none); return; } const delRes = await showDeleteConfirm(L.warn_del.replace('{n}', toDeleteIds.length)); if (!delRes.confirm) return; await executeBatchDelete(toDeleteIds, { hardDelete: delRes.hardDelete }); }; UI.btnDeselect.onclick = () => { S.clearSelection(); refresh(); }; const processBlacklistAction = async (action) => { if (S.updateBlCache) S.updateBlCache(); const selectedItems = getSelectedBlacklistItems(); const totalSelected = selectedItems.length; if (totalSelected === 0) return; const allSelectedAlreadyAdded = selectedItems.every(isItemInBlacklist); const isRemove = allSelectedAlreadyAdded && action !== 'add'; 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 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 = selectedItems.length; for (let i = 0; i < len; i++) { if (!isRunning) break; const item = selectedItems[i]; const name = getBlacklistItemName(item); const key = getBlacklistCleanKey(name); if (isBlacklistFolderItem(item)) { 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(getBlacklistCleanKey(name))); currentFolders = currentFolders.filter(name => !targetFolderKeys.has(getBlacklistCleanKey(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 => getBlacklistCleanKey(s))); const existingFolderKeys = new Set(currentFolders.map(s => getBlacklistCleanKey(s))); let addedCount = 0; for (const name of toAddFiles) { const key = getBlacklistCleanKey(name); if (!existingFileKeys.has(key)) { currentFiles.push(name); existingFileKeys.add(key); addedCount++; dataChanged = true; } } for (const name of toAddFolders) { const key = getBlacklistCleanKey(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) { const uploadMenu = UI.uploadWrap.querySelector('.pk-dropdown-menu'); const restoreUploadMenu = () => { if (!uploadMenu) return; if (uploadMenu._pkOriginParent && uploadMenu.parentNode !== uploadMenu._pkOriginParent) { uploadMenu._pkOriginParent.appendChild(uploadMenu); } uploadMenu.style.position = ''; uploadMenu.style.top = ''; uploadMenu.style.left = ''; uploadMenu.style.right = ''; uploadMenu.style.bottom = ''; uploadMenu.style.marginTop = ''; uploadMenu.style.zIndex = ''; uploadMenu.style.minWidth = ''; uploadMenu.style.visibility = ''; uploadMenu.style.pointerEvents = ''; uploadMenu.style.zoom = ''; uploadMenu.style.transformOrigin = ''; delete uploadMenu.dataset.pkPortal; }; const closeUploadMenu = () => { if (!uploadMenu) return; if (uploadMenu._pkPlaceRaf1) cancelAnimationFrame(uploadMenu._pkPlaceRaf1); if (uploadMenu._pkPlaceRaf2) cancelAnimationFrame(uploadMenu._pkPlaceRaf2); uploadMenu._pkPlaceRaf1 = 0; uploadMenu._pkPlaceRaf2 = 0; uploadMenu.style.display = 'none'; restoreUploadMenu(); UI.uploadWrap.classList.remove('active'); }; window.__pkCloseUploadMenu = closeUploadMenu; const placeUploadMenu = () => { if (!uploadMenu) return; if (!uploadMenu._pkOriginParent) uploadMenu._pkOriginParent = UI.uploadWrap; const isGridView = !!(UI.win && UI.win.classList.contains('pk-grid-view')); if (!isGridView) { if (uploadMenu.parentNode !== uploadMenu._pkOriginParent) { uploadMenu._pkOriginParent.appendChild(uploadMenu); } uploadMenu.classList.toggle('pk-dark', !!(el && el.classList.contains('pk-dark'))); uploadMenu.style.position = 'absolute'; uploadMenu.style.top = '100%'; uploadMenu.style.right = '0'; uploadMenu.style.left = 'auto'; uploadMenu.style.bottom = 'auto'; uploadMenu.style.marginTop = '6px'; uploadMenu.style.zIndex = ''; uploadMenu.style.minWidth = ''; uploadMenu.style.visibility = ''; uploadMenu.style.pointerEvents = ''; delete uploadMenu.dataset.pkPortal; return; } if (uploadMenu.parentNode !== document.body) { document.body.appendChild(uploadMenu); } uploadMenu.classList.toggle('pk-dark', !!(el && el.classList.contains('pk-dark'))); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const btnRect = typeof getLogicalRect === 'function' ? getLogicalRect(UI.btnUpload) : UI.btnUpload.getBoundingClientRect(); const pad = 8; uploadMenu.dataset.pkPortal = '1'; uploadMenu.style.position = 'fixed'; uploadMenu.style.left = '0'; uploadMenu.style.top = '0'; uploadMenu.style.right = 'auto'; uploadMenu.style.bottom = 'auto'; uploadMenu.style.marginTop = '0'; uploadMenu.style.zIndex = '10060'; uploadMenu.style.zoom = scale; uploadMenu.style.transformOrigin = 'top right'; uploadMenu.style.minWidth = Math.max(Math.round(btnRect.width), 140) + 'px'; uploadMenu.style.display = 'flex'; uploadMenu.style.visibility = 'hidden'; uploadMenu.style.pointerEvents = 'none'; const menuRect = uploadMenu.getBoundingClientRect(); const menuW = menuRect.width / scale; const menuH = menuRect.height / scale; const winW = window.innerWidth / scale; const winH = window.innerHeight / scale; let left = btnRect.right - menuW; let top = btnRect.bottom + 6; if (left < pad) left = pad; if (left + menuW > winW - pad) { left = winW - pad - menuW; } if (top + menuH > winH - pad) { top = btnRect.top - menuH - 6; } if (top < pad) top = pad; uploadMenu.style.left = Math.round(left) + 'px'; uploadMenu.style.top = Math.round(top) + 'px'; uploadMenu.style.visibility = ''; uploadMenu.style.pointerEvents = ''; }; const requestPlaceUploadMenu = () => { if (!uploadMenu) return; if (uploadMenu.style.display !== 'flex') return; if (!UI.uploadWrap.classList.contains('active')) return; if (uploadMenu._pkPlaceRaf1) cancelAnimationFrame(uploadMenu._pkPlaceRaf1); if (uploadMenu._pkPlaceRaf2) cancelAnimationFrame(uploadMenu._pkPlaceRaf2); uploadMenu._pkPlaceRaf1 = requestAnimationFrame(() => { uploadMenu._pkPlaceRaf2 = requestAnimationFrame(() => { uploadMenu._pkPlaceRaf1 = 0; uploadMenu._pkPlaceRaf2 = 0; if (!uploadMenu || uploadMenu.style.display !== 'flex') return; if (!UI.uploadWrap.classList.contains('active')) return; placeUploadMenu(); }); }); }; window.__pkRequestPlaceUploadMenu = requestPlaceUploadMenu; if (uploadMenu && !uploadMenu.dataset.pkBindStop) { uploadMenu.addEventListener('click', (e) => e.stopPropagation()); uploadMenu.dataset.pkBindStop = '1'; } if (!window.__pkUploadMenuPortalBound) { window.addEventListener('resize', () => { requestPlaceUploadMenu(); }, { passive: true }); document.addEventListener('scroll', () => { closeUploadMenu(); }, true); if (window.visualViewport) { window.visualViewport.addEventListener('resize', () => { requestPlaceUploadMenu(); }, { passive: true }); window.visualViewport.addEventListener('scroll', () => { requestPlaceUploadMenu(); }, { passive: true }); } document.addEventListener('mousedown', (e) => { if (!uploadMenu || uploadMenu.style.display !== 'flex') return; const target = e.target; if (uploadMenu.contains(target)) return; if (UI.btnUpload.contains(target)) return; if (UI.uploadWrap.contains(target) && !uploadMenu.dataset.pkPortal) return; closeUploadMenu(); }, true); window.__pkUploadMenuPortalBound = true; } if (window.ResizeObserver && !UI.uploadWrap.__pkUploadMenuRO) { UI.uploadWrap.__pkUploadMenuRO = new ResizeObserver(() => { requestPlaceUploadMenu(); }); UI.uploadWrap.__pkUploadMenuRO.observe(UI.uploadWrap); } UI.btnUpload.onclick = (e) => { e.stopPropagation(); if (!isRealHomeUploadView()) { syncLocalUploadVisibility(); return; } const isActive = uploadMenu && uploadMenu.style.display === 'flex' && UI.uploadWrap.classList.contains('active'); 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) { closeUploadMenu(); return; } if (!uploadMenu) return; uploadMenu.style.display = 'flex'; placeUploadMenu(); UI.uploadWrap.classList.add('active'); }; UI.actUpFile.onclick = (e) => { e.stopPropagation(); closeUploadMenu(); UI.inpFile.click(); }; UI.actUpFolder.onclick = (e) => { e.stopPropagation(); closeUploadMenu(); UI.inpFolder.click(); }; const updateRowUI = (task) => { if (document.hidden) return; 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, store: null, initStore: () => {}, saveTask: (task) => {}, removeTask: (id) => {}, 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) => { const task = { 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 }; S.upMng.saveTask(task); return task; }, 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 && !task.file_id) { 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++; if (!task.hash) { task.status = 'HASHING'; task.message = L.msg_task_hashing; if (S.uploadMode) updateRowUI(task); task.hash = await calcSha1(task.file); } const hash = task.hash; S.upMng.saveTask(task); if (!task._totalUploadedBytes) task._totalUploadedBytes = 0; let _lastPollTime = Date.now(); let _lastPollBytes = task._totalUploadedBytes; 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"); task.status = 'UPLOADING'; task.message = L.msg_task_init_upload; _lastPollTime = Date.now(); _lastPollBytes = 0; const safePid = (finalParentId === 'root' || finalParentId === 'upload_root') ? '' : (finalParentId || ''); let data = null; if (task._initData && task.file_id) { data = task._initData; } else { 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({ hash: hash, name: task.name, size: String(task.size), kind: "drive#file", id: "", parent_id: safePid, upload_type: "UPLOAD_TYPE_RESUMABLE", folder_type: "NORMAL", resumable: { provider: "PROVIDER_ALIYUN" } }) }); 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)); } } data = await res.json(); task._initData = data; } 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; } const uploadTaskId = (data.task && data.task.id) || data.task_id || data.upload_task_id || (data.resumable && data.resumable.task_id) || ''; if (uploadTaskId) task._uploadTaskId = uploadTaskId; if (newlyCreatedFileId && !task._ghostAdded) { if (typeof window.pkAddGhostFile === 'function') window.pkAddGhostFile(newlyCreatedFileId); task._ghostAdded = true; } S.upMng.saveTask(task); const waitOfficialUploadTaskComplete = async () => { if (!task._uploadTaskId) return true; const maxPoll = 20; for (let i = 1; i <= maxPoll; i++) { if (task._deleted) throw new Error("Aborted"); task.status = 'RUNNING'; task.speed = 0; task.progress = Math.min(99.8, Math.max(99, Number(task.progress) || 99)); task.message = L.msg_wait_server.replace('{c}', i).replace('{t}', maxPoll); S.upMng.saveTask(task); if (S.uploadMode) updateRowUI(task); try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${encodeURIComponent(task._uploadTaskId)}`, { headers: getHeaders() }); if (res.ok) { const taskData = await res.json().catch(() => null); const phase = (taskData && taskData.phase) || (taskData && taskData.task && taskData.task.phase) || ''; const ref = (taskData && taskData.reference_resource) || (taskData && taskData.task && taskData.task.reference_resource) || {}; if (ref.name) task.name = ref.name; if (ref.mime_type) task.mime_type = ref.mime_type; if (ref.icon_link) task.icon_link = ref.icon_link; if (ref.thumbnail_link) task.thumbnail_link = ref.thumbnail_link; if (phase === 'PHASE_TYPE_COMPLETE') return true; if (phase === 'PHASE_TYPE_ERROR') throw new Error((taskData && (taskData.message || taskData.error_description)) || L.err_unknown); } } catch (e) { if (e.message === "Aborted") throw e; console.warn('[Upload] Task completion poll warning:', e); } await sleep(i < 6 ? 1000 : 2000); } console.warn(`[Upload] Task completion poll timeout, fallback to local completion: ${task.name}`); return true; }; 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 (typeof window.pkRemoveGhostFile === 'function' && task.file_id) window.pkRemoveGhostFile(task.file_id); S.upMng.removeTask(task.id); if (S.uploadMode) { updateRowUI(task); if (!S._upRenderScheduled) { S._upRenderScheduled = true; requestAnimationFrame(() => { S._upRenderScheduled = false; if (typeof renderVisible === 'function' && !document.hidden) 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 isOssCname = p.cname === true || String(p.cname || '').toLowerCase() === 'true'; const ossEndpoint = String(p.endpoint || '').replace(/^https?:\/\//i, '').replace(/\/+$/g, ''); const host = isOssCname ? `https://${ossEndpoint}` : `https://${p.bucket}.${ossEndpoint}`; const totalSize = task.file.size; const officialPartSize = Number(CONF.uploadPartSizeOfficial) || (1 * 1024 * 1024); const maxPartCount = Number(CONF.uploadPartMaxCount) || 9999; const PART_SIZE = Math.max(officialPartSize, Math.ceil(totalSize / maxPartCount)); 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 : ''}`; if (!task._xhrs) task._xhrs = new Set(); 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) => { task._xhrs.delete(req); if (res.status >= 200 && res.status < 300) resolve(res); else { const err = new Error(`OSS ${method} Error: ${res.status} ${res.statusText}`); err.status = res.status; reject(err); } }, onerror: (err) => { task._xhrs.delete(req); reject(new Error("Network Error")); }, onabort: () => { task._xhrs.delete(req); reject(new Error("Aborted")); } }); task._xhrs.add(req); 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; }); task.progress = 100; } else { task.message = L.msg_task_init_part; if (S.uploadMode) updateRowUI(task); let uploadId = task._uploadId; let isResuming = !!uploadId; if (!uploadId) { const initRes = await ossRequest('POST', 'uploads', null, ''); const initXml = new DOMParser().parseFromString(initRes.responseText, "text/xml"); uploadId = initXml.querySelector('UploadId')?.textContent || initXml.getElementsByTagName('UploadId')[0]?.textContent; if (!uploadId) throw new Error("Failed to get UploadId"); task._uploadId = uploadId; } const parts = new Array(partCount); const CONCURRENCY = Math.max(1, Number(CONF.uploadPartConcurrencyOfficial) || 5); let completedBytes = 0; const activeParts = new Map(); const uploadedPartNumbers = new Set(); if (isResuming) { try { let nextMarker = ''; let isTruncated = true; while (isTruncated) { const query = `uploadId=${uploadId}` + (nextMarker ? `&part-number-marker=${nextMarker}` : ''); const listRes = await ossRequest('GET', query, null, ''); const listXml = new DOMParser().parseFromString(listRes.responseText, "text/xml"); const partNodes = listXml.querySelectorAll('Part') || listXml.getElementsByTagName('Part'); Array.from(partNodes).forEach(node => { const pNum = parseInt(node.querySelector('PartNumber')?.textContent || node.getElementsByTagName('PartNumber')[0]?.textContent); const etag = node.querySelector('ETag')?.textContent || node.getElementsByTagName('ETag')[0]?.textContent; if (pNum && etag && !uploadedPartNumbers.has(pNum)) { parts[pNum - 1] = { partNumber: pNum, etag: etag }; uploadedPartNumbers.add(pNum); const pSize = (pNum === partCount) ? (totalSize - (partCount - 1) * PART_SIZE) : PART_SIZE; completedBytes += pSize; } }); const truncNode = listXml.querySelector('IsTruncated') || listXml.getElementsByTagName('IsTruncated')[0]; isTruncated = truncNode ? (truncNode.textContent === 'true') : false; if (isTruncated) { const markerNode = listXml.querySelector('NextPartNumberMarker') || listXml.getElementsByTagName('NextPartNumberMarker')[0]; nextMarker = markerNode ? markerNode.textContent : ''; } } task._totalUploadedBytes = completedBytes; task.progress = (completedBytes / totalSize) * 100; console.log(`[Upload] Resume Check: Found ${uploadedPartNumbers.size}/${partCount} parts. Total: ${fmtSize(completedBytes)}`); } catch (e) { console.warn(`[Upload] Failed to fetch existing parts. Overwriting.`, e); } } 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 pool = Array.from({length: partCount}, (_, k) => k + 1).filter(num => !uploadedPartNumbers.has(num)); const worker = async () => { while (pool.length > 0) { if (task.status === 'PAUSED' || !document.body.contains(el)) { activeParts.clear(); 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}`; try { 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 }; if (!task._parts) task._parts = []; task._parts[i - 1] = parts[i - 1]; S.upMng.saveTask(task); } catch (err) { activeParts.delete(i); pool.unshift(i); throw err; } } }; task.message = L.msg_task_uploading_2; if (S.uploadMode) updateRowUI(task); if (pool.length > 0) { const workers = Array(Math.min(pool.length, 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) { if (!S._upRenderScheduled) { S._upRenderScheduled = true; requestAnimationFrame(() => { S._upRenderScheduled = false; if (typeof renderVisible === 'function' && !document.hidden) 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; } })(); } await waitOfficialUploadTaskComplete(); task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_upload_done; if (typeof window.pkRemoveGhostFile === 'function' && task.file_id) window.pkRemoveGhostFile(task.file_id); S.upMng.removeTask(task.id); if (S.uploadMode) { updateRowUI(task); if (!S._upRenderScheduled) { S._upRenderScheduled = true; requestAnimationFrame(() => { S._upRenderScheduled = false; if (typeof renderVisible === 'function' && !document.hidden) 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 (e.status === 403 && task.file_id) { task.message = L.msg_token_expired_retry; task._initData = null; task._uploadId = null; task.progress = 0; task._totalUploadedBytes = 0; fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [task.file_id] }) }).catch(()=>{}); if (typeof window.pkRemoveGhostFile === 'function') window.pkRemoveGhostFile(task.file_id); task.file_id = null; } if (S.uploadMode) updateRowUI(task); S.upMng.saveTask(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.status = 'PAUSED'; task.message = L.msg_task_paused; if (task._xhrs && task._xhrs.size > 0) { task._xhrs.forEach(req => req.abort()); task._xhrs.clear(); } if (task._xhr) { task._xhr.abort(); task._xhr = null; } S.upMng.saveTask(task); if (S.uploadMode && !skipRender) { refresh(); } } else if (task.status === 'WAITING') { task.status = 'PAUSED'; task.message = L.msg_task_paused; S.upMng.saveTask(task); 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.saveTask(task); S.upMng.scheduler(); if (S.uploadMode && !skipRender) { refresh(); } } } }; S.upMng.initStore(); 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 fileList = Array.from(files); let existingFiles = []; if (typeof globalCache !== 'undefined' && globalCache.has(safeParentId)) { const raw = globalCache.get(safeParentId); existingFiles = Array.isArray(raw) ? raw : (raw.items || []); } else if (!isVirtual && S.items && S.items.length > 0) { existingFiles = S.items; } if (existingFiles.length > 0) { const existingMap = new Set(); existingFiles.forEach(f => { if (f.kind !== 'drive#folder') { existingMap.add(`${f.name}_${f.size}`); } }); let dupCount = 0; const duplicateIndices = new Set(); fileList.forEach((file, index) => { if (file.name.startsWith('.')) return; let relativeFolder = ""; if (file.webkitRelativePath) { const parts = file.webkitRelativePath.split('/'); if (parts.length > 1) { parts.pop(); relativeFolder = parts.join('/'); } } if (!relativeFolder) { const key = `${file.name}_${file.size}`; if (existingMap.has(key)) { dupCount++; duplicateIndices.add(index); } } }); if (dupCount > 0) { const skipDups = await showConfirm(L.msg_upload_dup_confirm.replace('{n}', dupCount)); if (skipDups) { fileList = fileList.filter((_, idx) => !duplicateIndices.has(idx)); } } } if (fileList.length === 0) { UI.inpFile.value = ''; UI.inpFolder.value = ''; return; } let addedCount = 0; const BATCH_SIZE = 50; if (fileList.length > BATCH_SIZE) { showToast(L.msg_parsing_files, 'info', 2000); } for (let i = 0; i < fileList.length; i += BATCH_SIZE) { const batch = fileList.slice(i, i + BATCH_SIZE); await new Promise(resolve => setTimeout(resolve, 0)); for (const file of batch) { 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 = "") => { const BATCH_SIZE = 10; for (let i = 0; i < entries.length; i += BATCH_SIZE) { const batch = entries.slice(i, i + BATCH_SIZE); await Promise.all(batch.map(async (entry) => { if (entry.isFile) { return new Promise((res) => { entry.file(file => { if (!file.name.startsWith('.')) { 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); } res(); }, () => res()); }); } else if (entry.isDirectory) { return new Promise((res) => { const reader = entry.createReader(); const readAllEntries = async () => { let allSubEntries = []; let readBatch = async () => { return new Promise((r) => { reader.readEntries((sub) => { if (sub.length > 0) { allSubEntries = allSubEntries.concat(sub); readBatch().then(r); } else { r(); } }, () => r()); }); }; await readBatch(); await parseEntries(allSubEntries, (relPath ? relPath + "/" : "") + entry.name); res(); }; readAllEntries(); }); } })); } }; 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) { 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 existingFiles = []; if (typeof globalCache !== 'undefined' && globalCache.has(safeParentId)) { const raw = globalCache.get(safeParentId); existingFiles = Array.isArray(raw) ? raw : (raw.items || []); } else if (!isVirtual && S.items && S.items.length > 0) { existingFiles = S.items; } if (existingFiles.length > 0) { const existingMap = new Set(); existingFiles.forEach(f => { if (f.kind !== 'drive#folder') { existingMap.add(`${f.name}_${f.size}`); } }); let dupCount = 0; const duplicateIndices = new Set(); const topLevelFiles = []; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (entry.isFile) { const file = await new Promise((res) => entry.file(res, () => res(null))); if (file && !file.name.startsWith('.')) { topLevelFiles.push({ file, index: i }); } } } topLevelFiles.forEach(tf => { const key = `${tf.file.name}_${tf.file.size}`; if (existingMap.has(key)) { dupCount++; duplicateIndices.add(tf.index); } }); if (dupCount > 0) { const skipDups = await showConfirm(L.msg_upload_dup_confirm.replace('{n}', dupCount)); if (skipDups) { for (let i = entries.length - 1; i >= 0; i--) { if (duplicateIndices.has(i)) { entries.splice(i, 1); } } } } } if (entries.length === 0) return; 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.historyMode && mode !== 'history' && typeof globalCache !== 'undefined') { const historySession = globalCache.get('history_session'); if (historySession) { const hasPending = !historySession.completed && (historySession.cursor || 0) < (((historySession.targetIds || []).length) || 0); const historySnapshot = { items: Array.isArray(S.items) ? [...S.items] : [], nextToken: hasPending ? `history:${historySession.cursor || 0}` : null }; S.cache.set('history_root', historySnapshot); globalCache.set('history_root', historySnapshot); } } if (S.abortController) S.abortController.abort(); activeLoadId++; S.sortId++; if (S.loading) { S.loading = false; } const isForcedListTab = mode === 'offline' || mode === 'upload' || mode === 'history' || mode === 'trash' || mode === 'share' || mode === 'starred' || mode === 'recent'; const needListTabSync = isForcedListTab && (isGridView() || (UI.win && UI.win.classList.contains('pk-grid-view')) || CONF.rowHeight !== getListRowHeight() || (UI.vp && UI.vp.scrollTop > 0)); if (needListTabSync) { beginFolderViewSync(); if (UI.win) { UI.win.classList.add('pk-view-switching'); UI.win.classList.remove('pk-grid-view', 'pk-grid-resizing', 'pk-grid-scrolling'); } S.viewMode = 'list'; CONF.rowHeight = getListRowHeight(); S._gridLayoutKey = ''; S.dupGridMeta = null; S.dupGridMetaKey = ''; if (UI.in) { UI.in.style.height = '0px'; UI.in.style.transform = 'none'; } if (UI.vp) { UI.vp.scrollTop = 0; UI.vp.scrollLeft = 0; } } S.items = []; S.display = []; S.recentResultItems = null; S.itemMap.clear(); S.sel.clear(); if (UI.in) { UI.in.innerHTML = ''; UI.in.style.height = '0px'; UI.in.style.transform = 'none'; } if (UI.vp) { UI.vp.scrollTop = 0; UI.vp.scrollLeft = 0; } if (mode === 'history') { S.sort = 'play_time'; S.dir = 1; } else if (mode === 'offline') { S.sort = 'created_time'; S.dir = 1; } else if (mode === 'recent') { S.sort = 'modified_time'; S.dir = 1; } else if (mode === 'home') { S._sortAppliedForId = null; } else { S.sort = 'modified_time'; } 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.btnMigrate, 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 !== UI.btnMigrate) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnMigrate) UI.btnMigrate.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, UI.btnImgSearch].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, UI.btnImgSearch].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 = 'none'; 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.style.display = 'none'; }); 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, UI.btnMigrate].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'; if(UI.btnImgSearch) UI.btnImgSearch.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'; if(UI.btnImgSearch) UI.btnImgSearch.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, UI.btnImgSearch].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; } syncLocalUploadVisibility(); 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' }); 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); }; 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 compareNames(a.name, b.name); if (sortMode === 'za') return compareNames(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; const isProtected = isDir && ((typeof isSystemItem === 'function') ? isSystemItem(item) : false); const tagHtml = isProtected ? `${L.tag_default}` : ''; 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)} ${tagHtml}
${!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 = getLogicalRect(triggerEl); pop.style.top = (rect.bottom + 5) + 'px'; pop.style.left = rect.left + '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' }); 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); }; 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 === 'az') return compareNames(a.name, b.name); if (sortMode === 'za') return compareNames(b.name, a.name); if (sortMode === 'new') return new Date(b.modified_time || 0) - new Date(a.modified_time || 0); if (sortMode === 'old') return new Date(a.modified_time || 0) - new Date(b.modified_time || 0); return 0; }); 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; const isInPath = S.path.some(pathNode => pathNode.id === f.id); const textStyle = isInPath ? 'font-weight:bold; color:var(--pk-pri);' : ''; const isProtected = (typeof isSystemItem === 'function') ? isSystemItem(f) : false; const tagHtml = isProtected ? `${L.tag_default}` : ''; itemDiv.innerHTML = `${iconHtml}${esc(f.name)}${tagHtml}`; 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(' { if (!isLast) { 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); let showArrow = !isLast; if (isLast) { if (currentList && currentList.some(item => item.kind === 'drive#folder')) { showArrow = true; } } if (showArrow) { 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(); renderCrumb(); } 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); }; window.pkPendingCloudPresetLinks = ''; window.pkOpenCloudTaskWithLinks = (links) => { const text = Array.isArray(links) ? links.map(x => String(x || '').trim()).filter(Boolean).join('\n') : String(links || '').trim(); if (!text) return false; window.pkPendingCloudPresetLinks = text; if (btnCloud) { btnCloud.click(); return true; } return false; }; const initClipboardMagnetFocusWatcher = () => { if (window.__pkClipboardMagnetFocusWatcherBound) return; window.__pkClipboardMagnetFocusWatcherBound = true; const state = { lastCheckAt: 0, lastDeniedAt: 0, lastPromptAt: 0, lastSignature: '', prompting: false, processing: false, ignored: new Map(), queue: [], queued: new Set(), previewCache: new Map(), previewCircuitUntil: 0 }; const getClipText = () => getStrings(); const normalizeHash = (link) => { const match = String(link || '').match(/urn:btih:([^&]+)/i); return match ? match[1].toUpperCase() : String(link || '').trim().toLowerCase(); }; const makeSignature = (links) => links.map(normalizeHash).filter(Boolean).sort().join('\n'); const isIgnoredSignature = (signature) => { const until = state.ignored.get(signature) || 0; if (!until) return false; if (Date.now() < until) return true; state.ignored.delete(signature); return false; }; const markIgnoredSignature = (signature) => { state.ignored.set(signature, Date.now() + CONF.clipboardMagnetIgnoreTTL); }; const getDefaultMagnetSaveTarget = () => { const curFolder = S.path[S.path.length - 1] || { id: '', name: L.lbl_default_folder }; const curId = curFolder.id || ''; const isVirtual = curId.startsWith('virtual_') || curId.includes('_root') || curId === 'analyze_root'; const isHomeSubDir = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.isFlattened && !S.dupMode && S.path.length > 1 && !isVirtual; return { id: isHomeSubDir ? curId : '', name: isHomeSubDir ? (curFolder.name || L.lbl_default_folder) : L.lbl_default_folder, path: isHomeSubDir ? S.path.filter(p => !p.id.startsWith('virtual_')) : null }; }; const createMagnetCloudTasks = async (links, targetId) => { const progressTask = FloatBarManager.create(L.msg_creating_cloud_task); let successCount = 0; let failCount = 0; for (let i = 0; i < links.length; i++) { progressTask.update(L.str_creating_task_n.replace('{n}', i + 1).replace('{t}', links.length)); try { let retry = 0; let created = false; while (retry < 3) { try { await apiAddOfflineTask(links[i], targetId || '', {}); successCount++; created = true; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; break; } catch (reqErr) { const isRateLimited = String((reqErr && (reqErr.message || reqErr.status || reqErr.code)) || '').includes('429'); if (isRateLimited) { retry++; if (retry >= 3) throw reqErr; await sleep(2000 * retry); } else { throw reqErr; } } } if (!created) throw new Error('Magnet task create failed after retry'); } catch (e) { console.error(`Magnet Task Create Failed[${links[i]}]:`, 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 || (targetId && curPathId === targetId)) { load(false, true); } else if (!targetId && S.path.length === 1) { if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); } }; const extractMagnetLinks = (rawText) => { const text = String(rawText || '').trim(); if (!text || text.length > CONF.clipboardMagnetMaxChars) return []; const unique = new Map(); const addMagnet = (value) => { let link = String(value || '').trim(); if (!link) return; const urnOnly = link.match(/^urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})(?:[^\s"'<>`]*)?$/i); if (urnOnly) link = `magnet:?xt=urn:btih:${urnOnly[1].toUpperCase()}`; const pureHash = link.match(/^([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})$/i); if (pureHash) link = `magnet:?xt=urn:btih:${pureHash[1].toUpperCase()}`; if (!/^magnet:\?/i.test(link)) return; const hashMatch = link.match(/[?&]xt=urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})/i); if (!hashMatch) return; const signature = hashMatch[1].toUpperCase(); if (!unique.has(signature)) unique.set(signature, link); }; if (!/(magnet:\?|urn:btih:)/i.test(text)) { const lines = text.split(/\r?\n/).map(x => x.trim()).filter(Boolean); if (lines.length === 1) addMagnet(lines[0]); return Array.from(unique.values()); } const magnetRegex = /magnet:\?[^\s"'<>`]+/gi; let match; while ((match = magnetRegex.exec(text))) addMagnet(match[0]); const urnRegex = /urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})(?:[^\s"'<>`]*)?/gi; while ((match = urnRegex.exec(text))) addMagnet(match[0]); return Array.from(unique.values()); }; const requestMagnetPreview = (link) => { return new Promise((resolve) => { const hash = normalizeHash(link); const now = Date.now(); const cached = state.previewCache.get(hash); if (cached && now - cached.at < cached.ttl) { resolve(cached.data); return; } const finish = (data) => { const ttl = data && data.ok ? CONF.magnetPreviewCacheTTL : CONF.magnetPreviewErrorCacheTTL; state.previewCache.set(hash, { at: Date.now(), ttl, data }); resolve(data); }; if (state.previewCircuitUntil && now < state.previewCircuitUntil) { finish({ ok: false, code: 'rate_limited' }); return; } const url = `${CONF.magnetPreviewApi}?url=${encodeURIComponent(link)}`; const handleStatus = (status) => { if (status === 429) { state.previewCircuitUntil = Date.now() + CONF.magnetPreviewCircuitTTL; finish({ ok: false, code: 'rate_limited', status }); return true; } if (status >= 500) { state.previewCircuitUntil = Date.now() + CONF.magnetPreviewCircuitTTL; finish({ ok: false, code: 'network', status }); return true; } if (status && (status < 200 || status >= 300)) { finish({ ok: false, code: 'network', status }); return true; } return false; }; if (typeof GM_xmlhttpRequest !== 'function') { fetch(url, { cache: 'no-store' }).then(r => { if (handleStatus(r.status)) return null; return r.json(); }).then(data => { if (data) finish({ ok: true, code: 'ok', data }); }).catch(() => finish({ ok: false, code: 'network' })); return; } GM_xmlhttpRequest({ method: 'GET', url, responseType: 'json', timeout: CONF.magnetPreviewTimeout, onload: (res) => { if (handleStatus(res.status)) return; try { const data = res.response && typeof res.response === 'object' ? res.response : JSON.parse(res.responseText || '{}'); finish({ ok: true, code: 'ok', data }); } catch (e) { finish({ ok: false, code: 'network' }); } }, onerror: () => finish({ ok: false, code: 'network' }), ontimeout: () => finish({ ok: false, code: 'timeout' }) }); }); }; const showMagnetPreviewModal = (links, preview) => { return new Promise((resolve) => { const TXT = getClipText(); const data = preview && preview.ok && preview.data ? preview.data : {}; let saveTarget = getDefaultMagnetSaveTarget(); const toWhatslinkImageUrl = (value) => { let url = String(value || '').trim(); if (!url) return ''; if (/^\/\//.test(url)) url = `https:${url}`; else if (/^\//.test(url)) url = `https://whatslink.info${url}`; else if (!/^https?:\/\//i.test(url) && /\.(webp|png|jpe?g|gif)(\?|#|$)/i.test(url)) url = `https://whatslink.info/${url.replace(/^\.?\//, '')}`; return /^https?:\/\//i.test(url) ? url : ''; }; const pickShot = (item) => { if (!item) return null; if (typeof item === 'string') { const src = toWhatslinkImageUrl(item); return src ? { src, time: 0 } : null; } if (Array.isArray(item)) { for (const sub of item) { const shot = pickShot(sub); if (shot) return shot; } return null; } if (typeof item === 'object') { const officialSrc = toWhatslinkImageUrl(item.screenshot); if (officialSrc) return { src: officialSrc, time: Number(item.time || 0) || 0 }; const keys = ['url', 'src', 'image', 'img', 'thumbnail', 'thumb', 'preview', 'poster', 'file', 'path']; for (const key of keys) { const shot = pickShot(item[key]); if (shot) return { src: shot.src, time: Number(item.time || shot.time || 0) || 0 }; } for (const val of Object.values(item)) { const shot = pickShot(val); if (shot) return { src: shot.src, time: Number(item.time || shot.time || 0) || 0 }; } } return null; }; const collectShots = (source) => { const list = []; const seen = new Set(); const push = (value) => { const shot = pickShot(value); if (shot && shot.src && !seen.has(shot.src)) { seen.add(shot.src); list.push(shot); } }; if (Array.isArray(source.screenshots)) source.screenshots.forEach(push); if (source.screenshot) push({ screenshot: source.screenshot, time: source.time }); if (Array.isArray(source.thumbnails)) source.thumbnails.forEach(push); if (Array.isArray(source.images)) source.images.forEach(push); if (Array.isArray(source.files)) source.files.forEach(file => { if (file && (file.screenshots || file.screenshot || file.thumbnail || file.thumb || file.preview || file.poster || file.image || file.url || file.path)) push(file); }); return list.slice(0, CONF.magnetPreviewMaxShots); }; const fmtShotTime = (seconds) => { const total = Math.floor(Number(seconds || 0)); if (!Number.isFinite(total) || total <= 0) return ''; const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; }; const shots = collectShots(data); const name = data.name || TXT.str_magnet_unknown_name; const size = Number(data.size || 0) > 0 ? fmtSize(data.size) : TXT.str_magnet_unknown_name; const count = Number(data.count || 0) > 0 ? String(data.count) : String(links.length); const type = data.file_type || data.type || TXT.str_magnet_unknown_name; const hashText = normalizeHash(links[0]); const heroHtml = shots[0] ? `` : `
${esc(TXT.str_no_preview)}
`; const shotsHtml = shots.length > 1 ? `
${shots.map((shot, idx) => `
${fmtShotTime(shot.time) ? `${esc(fmtShotTime(shot.time))}` : ''}
`).join('')}
` : ''; const getPreviewFailText = () => { if (preview && preview.ok) return ''; if (preview && preview.code === 'rate_limited') return TXT.msg_magnet_preview_rate_limited; if (preview && preview.code === 'timeout') return TXT.msg_magnet_preview_timeout; if (preview && preview.code === 'network') return TXT.msg_magnet_preview_network; return TXT.msg_magnet_preview_fail; }; const failText = getPreviewFailText(); const warnHtml = failText ? `
${esc(failText)}
` : ''; const m = showModal(`
${heroHtml}
${esc(name)}
${esc(TXT.msg_magnet_preview_desc)}
${warnHtml} ${shotsHtml}
${esc(TXT.lbl_magnet_count)}
${esc(count)}
${esc(TXT.lbl_magnet_size)}
${esc(size)}
${esc(TXT.lbl_magnet_type)}
${esc(type)}
${esc(TXT.lbl_magnet_hash)}:${esc(hashText)}
${esc(L.lbl_save_to)} ${CONF.typeIcons.folder} ${esc(saveTarget.name)} ${L.tip_cloud_save_path ? `` : ''} ${esc(L.btn_modify)}
${esc(TXT.lbl_magnet_preview_source)} whatslink.info
`); const box = m.querySelector('.pk-modal'); if (box) { box.style.width = '420px'; box.style.padding = '0'; box.style.borderRadius = '16px'; } const heroBox = m.querySelector('.pk-magnet-hero'); const setHeroShot = (src, thumb) => { if (!heroBox || !src) return; let heroImg = heroBox.querySelector('img'); if (!heroImg) { heroBox.innerHTML = ``; heroImg = heroBox.querySelector('img'); } heroImg.src = src; heroImg.setAttribute('draggable', 'false'); heroImg.setAttribute('referrerpolicy', 'no-referrer'); m.querySelectorAll('.pk-magnet-thumb-wrap').forEach(x => x.classList.remove('active')); if (thumb) thumb.classList.add('active'); }; m.querySelectorAll('.pk-magnet-thumb-wrap').forEach(thumb => { thumb.addEventListener('dragstart', e => e.preventDefault()); thumb.addEventListener('click', () => setHeroShot(thumb.dataset.shotSrc || thumb.querySelector('img')?.getAttribute('src'), thumb)); }); const heroImg = m.querySelector('.pk-magnet-hero img'); if (heroImg) heroImg.addEventListener('dragstart', e => e.preventDefault()); const saveNameEl = m.querySelector('#pk_magnet_save_name'); const changeDirEl = m.querySelector('#pk_magnet_change_dir'); if (changeDirEl) { changeDirEl.onclick = () => { showFolderSelector(saveTarget.id, (id, name, fullItem, selectedPathChain) => { saveTarget.id = id || ''; saveTarget.name = name || L.lbl_default_folder; saveTarget.path = selectedPathChain || null; if (saveNameEl) saveNameEl.textContent = saveTarget.name; }, saveTarget.path); }; } const close = () => { m.remove(); resolve({ confirm: false }); }; m.querySelector('.pk-modal-close').onclick = close; m.querySelector('#pk_magnet_cancel').onclick = close; m.querySelector('#pk_magnet_continue').onclick = () => { m.remove(); resolve({ confirm: true, targetId: saveTarget.id, targetName: saveTarget.name }); }; }); }; const processMagnetQueue = async () => { if (state.processing) return; state.processing = true; try { while (state.queue.length > 0) { const task = state.queue.shift(); if (!task || !task.link || !task.signature) continue; if (isIgnoredSignature(task.signature)) { state.queued.delete(task.signature); continue; } if (state.lastSignature === task.signature && Date.now() - state.lastPromptAt < CONF.clipboardMagnetPromptGap) { state.queued.delete(task.signature); continue; } state.prompting = true; state.lastSignature = task.signature; state.lastPromptAt = Date.now(); let result = null; try { const preview = await requestMagnetPreview(task.link); result = await showMagnetPreviewModal([task.link], preview); } catch (e) { console.error('Magnet preview queue failed:', e); result = { confirm: false }; } finally { state.prompting = false; } if (!result || !result.confirm) { markIgnoredSignature(task.signature); state.queued.delete(task.signature); continue; } try { await createMagnetCloudTasks([task.link], result.targetId || ''); } catch (e) { console.error('Magnet cloud task failed:', e); if (L.err_operation_failed) showToast(L.err_operation_failed, 'error'); } state.queued.delete(task.signature); await sleep(80); } } finally { state.processing = false; if (state.queue.length > 0) processMagnetQueue(); } }; const handleMagnetLinks = (links) => { if (!links || links.length === 0) return; let added = false; const now = Date.now(); links.forEach(link => { const cleanLink = String(link || '').trim(); if (!/^magnet:\?/i.test(cleanLink)) return; const signature = normalizeHash(cleanLink); if (!signature) return; if (isIgnoredSignature(signature)) return; if (state.queued.has(signature)) return; if (state.lastSignature === signature && now - state.lastPromptAt < CONF.clipboardMagnetPromptGap) return; state.queue.push({ link: cleanLink, signature }); state.queued.add(signature); added = true; }); if (added) processMagnetQueue(); }; const handleRawText = (rawText) => { const magnetLinks = extractMagnetLinks(rawText); if (magnetLinks.length === 0) return; handleMagnetLinks(magnetLinks); }; const shouldSkipFocusCheck = () => { if (gmGet('pk_clipboard_magnet_focus', false) !== true) return true; if (document.hidden) return true; if (!navigator.clipboard || typeof navigator.clipboard.readText !== 'function') return true; if (document.querySelector('#pk_cloud_input')) return true; if (document.querySelector('.pk-modal-ov') && !state.processing && !state.prompting) return true; const now = Date.now(); if (now - state.lastCheckAt < CONF.clipboardMagnetFocusCooldown) return true; if (now - state.lastDeniedAt < CONF.clipboardMagnetDenyCooldown) return true; return false; }; const checkClipboardMagnet = async () => { if (shouldSkipFocusCheck()) return; state.lastCheckAt = Date.now(); let rawText = ''; try { rawText = await navigator.clipboard.readText(); } catch (e) { state.lastDeniedAt = Date.now(); return; } handleRawText(rawText); }; const scheduleClipboardMagnetCheck = () => { if (window.__pkClipboardMagnetFocusTimer) clearTimeout(window.__pkClipboardMagnetFocusTimer); window.__pkClipboardMagnetFocusTimer = setTimeout(checkClipboardMagnet, 350); }; document.addEventListener('paste', (e) => { if (gmGet('pk_clipboard_magnet_paste', CONF.clipboardMagnetPaste) === false) return; if (document.querySelector('#pk_cloud_input')) return; const active = document.activeElement; if (active && (active.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(active.tagName))) return; const rawText = e.clipboardData ? e.clipboardData.getData('text/plain') : ''; handleRawText(rawText); }, true); document.addEventListener('visibilitychange', () => { if (!document.hidden) scheduleClipboardMagnetCheck(); }); window.addEventListener('focus', scheduleClipboardMagnetCheck); window.addEventListener('pageshow', scheduleClipboardMagnetCheck); }; initClipboardMagnetFocusWatcher(); 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'); 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 = parseCloudLinks; 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; }; const presetLinks = String(window.pkPendingCloudPresetLinks || '').trim(); window.pkPendingCloudPresetLinks = ''; if (presetLinks) { input.value = presetLinks; if (smartFixCheckbox) smartFixCheckbox.checked = true; input.dispatchEvent(new Event('input')); setTimeout(() => input.focus(), 0); } 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 () => { const ids = S.getSelectedIds(); if (ids.length === 0) return; ensureItemMap(); const progressTask = FloatBarManager.create(L.msg_prepare_restore); const updateFloat = progressTask.update; isGUISensitive = true; S.clearSelection(); 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 () => { const ids = S.getSelectedIds(); const count = ids.length; if (count === 0) return; ensureItemMap(); if (!await showConfirm(L.msg_del_forever_confirm.replace('{n}', count))) return; 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); const curBlurScope = gmGet('pk_blur_scope', curBlur ? 'list' : 'off'); 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
한국어
日本語
Indonesia
Bahasa Melayu
${L.label_turbo_mode}
${L.label_privacy_mode}
${L.label_blur_cover} ${curBlurScope === 'off' ? L.opt_privacy_off : curBlurScope === 'list' ? L.opt_privacy_list : curBlurScope === 'grid' ? L.opt_privacy_grid : L.opt_privacy_both}
${CONF.crumbIcons.down}
${L.opt_privacy_off}
${L.opt_privacy_list}
${L.opt_privacy_grid}
${L.opt_privacy_both}
${L.title_blacklist}
${L.label_clipboard_magnet_focus}
${L.lbl_browse_exp}
${L.label_sort_pref}
${L.label_view_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.lbl_ana_min}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
-
${L.lbl_ana_max}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${CONF.crumbIcons.down}
MB
GB
TB
${L.label_dl_filter_ext}
${L.label_dl_filter_name}
${L.desc_dl_filter}
${L.label_aria2_url}
${L.btn_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 ariaEye = m.querySelector('#btn_aria_token_eye'); const ariaDot = m.querySelector('#aria_test_dot'); const ariaTxt = m.querySelector('#aria_test_txt'); const ariaBox = m.querySelector('#aria_test_res'); let ariaTimer = null; let ariaTokenVisible = false; 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 testUrl = normalizeAriaRpcUrl(url); const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_live_test', params: buildAriaRpcParams(token) }; try { await aria2RpcRequest(testUrl, payload, 3000); 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(); }; if (ariaEye) { ariaEye.onclick = (e) => { e.preventDefault(); e.stopPropagation(); ariaTokenVisible = !ariaTokenVisible; ariaTok.style.webkitTextSecurity = ariaTokenVisible ? 'none' : 'disc'; ariaEye.innerHTML = ariaTokenVisible ? CONF.icons.eyeOff : CONF.icons.eye; }; } 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 sizeMinInp = m.querySelector('#set_dl_filter_size_min'); const sizeMaxInp = m.querySelector('#set_dl_filter_size_max'); const groupEl = m.querySelector('#pk_dl_group'); const updateDlBorders = () => { const hasE = extInp.value.trim() !== ''; const hasN = nameInp.value.trim() !== ''; const hasSMin = sizeMinInp.value.trim() !== ''; const hasSMax = sizeMaxInp.value.trim() !== ''; extInp.classList.toggle('pk-active-border', hasE); nameInp.classList.toggle('pk-active-border', hasN); sizeMinInp.classList.toggle('pk-active-border', hasSMin); sizeMaxInp.classList.toggle('pk-active-border', hasSMax); groupEl.classList.toggle('pk-typing-active', hasE || hasN || hasSMin || hasSMax); }; extInp.oninput = nameInp.oninput = sizeMinInp.oninput = sizeMaxInp.oninput = updateDlBorders; updateDlBorders(); const curDlSizeUnit = gmGet('pk_dl_filter_size_unit', 'MB'); bindSelect('cs_set_dl_size_unit', curDlSizeUnit, (val) => {}); m.querySelector('#dl_size_dec_min').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMinInp.value)) || 0; if (v > 0) sizeMinInp.value = v - 1; ensureDlSizeRange(); updateDlBorders(); }; m.querySelector('#dl_size_inc_min').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMinInp.value)) || 0; sizeMinInp.value = v + 1; ensureDlSizeRange(); updateDlBorders(); }; m.querySelector('#dl_size_dec_max').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMaxInp.value)) || 0; if (v > 0) sizeMaxInp.value = v - 1; ensureDlSizeRange(); updateDlBorders(); }; m.querySelector('#dl_size_inc_max').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMaxInp.value)) || 0; sizeMaxInp.value = v + 1; ensureDlSizeRange(); updateDlBorders(); }; const ensureDlSizeRange = () => { let minVal = sizeMinInp.value.trim() === '' ? -1 : Math.floor(Number(sizeMinInp.value)); let maxVal = sizeMaxInp.value.trim() === '' ? -1 : Math.floor(Number(sizeMaxInp.value)); if (minVal >= 0 && maxVal >= 0 && minVal > maxVal) { if (document.activeElement === sizeMinInp) { sizeMaxInp.value = minVal; } else if (document.activeElement === sizeMaxInp) { sizeMinInp.value = maxVal; } else { sizeMaxInp.value = minVal; } } }; sizeMinInp.addEventListener('blur', ensureDlSizeRange); sizeMaxInp.addEventListener('blur', ensureDlSizeRange); sizeMinInp.addEventListener('change', ensureDlSizeRange); sizeMaxInp.addEventListener('change', ensureDlSizeRange); 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_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', '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 gmKeys = typeof GM_listValues !== 'undefined' ? GM_listValues() : []; const lsKeys = Object.keys(localStorage).filter(k => typeof k === 'string' && k.startsWith('pk_')); const keys = Array.from(new Set([...(Array.isArray(gmKeys) ? gmKeys : []), ...lsKeys])); const config = { "_pk_metadata": { "signature": "PIKPAK_ENHANCEMENT_MASTER", "version": version, "export_at": new Date().toISOString(), "author": "digbug82", "scope": "whitelist" } }; const exportExactKeys = new Set([ 'pk_lang', 'pk_theme', 'pk_turbo_mode', 'pk_file_view_mode', 'pk_view_independent', 'pk_folder_view_prefs', 'pk_keep_pos', 'pk_pos_left', 'pk_pos_top', 'pk_blur_thumb', 'pk_blur_scope', 'pk_comic_mode', 'pk_clipboard_magnet_focus', 'pk_sort_independent', 'pk_folder_first', 'pk_folder_sort_prefs', 'pk_global_sort_pref', 'pk_ext_player', 'pk_play_mode', 'pk_skip_intro', 'pk_skip_outro', 'pk_vol_muted', 'pk_vol_level', 'pk_suppress_global_warn', 'pk_blacklist', 'pk_blacklist_folders', 'pk_aria2_url', 'pk_aria2_token', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist', 'pk_dup_strictness', 'pk_skip_bl_on_del', 'pk_pwd_vault', 'pk_pwd_try_count' ]); const exportPrefixKeys = [ 'pk_scan_last_', 'pk_analyze_last_', 'pk_archive_pwd_', 'pk_progress_', 'pk_duration_' ]; const isWhitelistedExportKey = (k) => exportExactKeys.has(k) || exportPrefixKeys.some(prefix => k.startsWith(prefix)); const readStoredValue = (k) => { let v = typeof GM_getValue !== 'undefined' ? GM_getValue(k, null) : null; if ((v === null || v === undefined || v === '') && localStorage.getItem(k) !== null) v = localStorage.getItem(k); return v; }; const pkKeys = keys.filter(k => isWhitelistedExportKey(k)); const getCatWeight = (k) => { if (k.startsWith('pk_archive_pwd_') || k === 'pk_pwd_vault' || k === 'pk_pwd_try_count' || k === 'pk_share_limits') return 3; if (k.startsWith('pk_progress_') || k.startsWith('pk_duration_')) return 4; const ruleKeys = ['pk_blacklist', 'pk_blacklist_folders', 'pk_aria2_url', 'pk_aria2_token', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist', 'pk_dup_strictness', 'pk_skip_bl_on_del', 'pk_clipboard_magnet_focus']; if (ruleKeys.includes(k) || k.startsWith('pk_scan_last_') || k.startsWith('pk_analyze_last_')) 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 => { const v = readStoredValue(k); if (v !== null && v !== undefined && v !== '') config[k] = v; }); config._pk_metadata.exported_keys = pkKeys.length; 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"); } const importExactKeys = new Set([ 'pk_lang', 'pk_theme', 'pk_turbo_mode', 'pk_file_view_mode', 'pk_view_independent', 'pk_folder_view_prefs', 'pk_keep_pos', 'pk_pos_left', 'pk_pos_top', 'pk_blur_thumb', 'pk_blur_scope', 'pk_comic_mode', 'pk_clipboard_magnet_focus', 'pk_sort_independent', 'pk_folder_first', 'pk_folder_sort_prefs', 'pk_global_sort_pref', 'pk_ext_player', 'pk_play_mode', 'pk_skip_intro', 'pk_skip_outro', 'pk_vol_muted', 'pk_vol_level', 'pk_suppress_global_warn', 'pk_blacklist', 'pk_blacklist_folders', 'pk_aria2_url', 'pk_aria2_token', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist', 'pk_dup_strictness', 'pk_skip_bl_on_del', 'pk_pwd_vault', 'pk_pwd_try_count' ]); const importPrefixKeys = [ 'pk_scan_last_', 'pk_analyze_last_', 'pk_archive_pwd_', 'pk_progress_', 'pk_duration_' ]; const isWhitelistedImportKey = (k) => importExactKeys.has(k) || importPrefixKeys.some(prefix => k.startsWith(prefix)); const readStoredValue = (k) => { let v = typeof GM_getValue !== 'undefined' ? GM_getValue(k, null) : null; if ((v === null || v === undefined || v === '') && localStorage.getItem(k) !== null) v = localStorage.getItem(k); return v; }; const writeStoredValue = (k, v) => { if (typeof GM_setValue !== 'undefined') GM_setValue(k, v); else localStorage.setItem(k, typeof v === 'string' ? v : JSON.stringify(v)); if (k === 'pk_turbo_mode') localStorage.setItem(k, String(v)); }; const parseMaybeJson = (v) => { if (typeof v === 'string') { const s = v.trim(); if ((s.startsWith('[') && s.endsWith(']')) || (s.startsWith('{') && s.endsWith('}'))) { try { return JSON.parse(s); } catch {} } } return v; }; const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v); const mergeArrayUnique = (localArr, importedArr, limit = 100) => { const seen = new Set(); const merged = []; [...localArr, ...importedArr].forEach(item => { const key = (item && typeof item === 'object') ? JSON.stringify(item) : `v:${String(item)}`; if (seen.has(key)) return; seen.add(key); merged.push(item); }); return merged.slice(0, limit); }; const importKeys = Object.keys(config).filter(k => isWhitelistedImportKey(k)); importKeys.forEach(k => { const importedVal = config[k]; const localVal = readStoredValue(k); if (localVal === null || localVal === undefined || localVal === '') { writeStoredValue(k, importedVal); return; } try { if (k === 'pk_blacklist' || k === 'pk_blacklist_folders') { const localSet = new Set(String(localVal || '').split('\n').map(s => s.trim()).filter(s => s)); const importedSet = new Set(String(importedVal || '').split('\n').map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); writeStoredValue(k, Array.from(localSet).join('\n')); return; } if (k === 'pk_dl_filter_ext' || k === 'pk_dl_filter_name') { const localSet = new Set(String(localVal || '').split(/[,,\n]/).map(s => s.trim()).filter(s => s)); const importedSet = new Set(String(importedVal || '').split(/[,,\n]/).map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); writeStoredValue(k, Array.from(localSet).join(', ')); return; } const localObj = parseMaybeJson(localVal); const importedObj = parseMaybeJson(importedVal); if (k === 'pk_pwd_vault') { const localArr = Array.isArray(localObj) ? localObj : []; const importedArr = Array.isArray(importedObj) ? importedObj : []; const map = new Map(); localArr.forEach(x => { const p = typeof x === 'object' ? x.p : x; const h = typeof x === 'object' ? (Number(x.h) || 0) : 0; if (p) map.set(p, { p, h }); }); importedArr.forEach(x => { const p = typeof x === 'object' ? x.p : x; const h = typeof x === 'object' ? (Number(x.h) || 0) : 0; if (!p) return; if (map.has(p)) map.get(p).h += h; else map.set(p, { p, h }); }); writeStoredValue(k, JSON.stringify(Array.from(map.values()).sort((a, b) => b.h - a.h).slice(0, 50))); return; } if (k === 'pk_expired_shares') { const localArr = Array.isArray(localObj) ? localObj : []; const importedArr = Array.isArray(importedObj) ? importedObj : []; const map = new Map(); localArr.forEach(x => { if (x && x.id) map.set(x.id, x); }); importedArr.forEach(x => { if (x && x.id) map.set(x.id, x); }); writeStoredValue(k, JSON.stringify(Array.from(map.values()))); return; } if (Array.isArray(localObj) && Array.isArray(importedObj)) { writeStoredValue(k, JSON.stringify(mergeArrayUnique(localObj, importedObj, 100))); return; } if (isPlainObject(localObj) && isPlainObject(importedObj)) { writeStoredValue(k, JSON.stringify(Object.assign({}, localObj, importedObj))); return; } writeStoredValue(k, importedVal); } catch (e2) { writeStoredValue(k, importedVal); } }); showToast(L.msg_import_success); fileInput.value = ''; 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); }; const thumbScopeWrap = m.querySelector('#cs_thumb_scope'); const thumbScopeInput = m.querySelector('#set_thumb_scope'); const thumbScopeTrigger = m.querySelector('#cs_thumb_scope .pk-select-trigger'); const thumbScopeMenu = m.querySelector('#cs_thumb_scope .pk-select-menu'); const thumbScopeTxt = m.querySelector('#txt_thumb_scope'); if (thumbScopeWrap && thumbScopeInput && thumbScopeTrigger && thumbScopeMenu && thumbScopeTxt) { thumbScopeTrigger.onclick = (e) => { e.stopPropagation(); thumbScopeMenu.style.display = thumbScopeMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('#cs_thumb_scope .pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('#cs_thumb_scope .pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); thumbScopeInput.value = item.dataset.val; thumbScopeTxt.textContent = item.textContent; thumbScopeMenu.style.display = 'none'; }; }); m.addEventListener('click', () => { thumbScopeMenu.style.display = 'none'; }); } 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 newBlurScope = m.querySelector('#set_thumb_scope').value; const newKeepPos = m.querySelector('#set_keep_pos').checked; const newSkipBl = m.querySelector('#set_skip_bl').checked; const newClipboardMagnetFocus = m.querySelector('#set_clipboard_magnet_focus').checked; const newComicMode = m.querySelector('#set_comic_mode').checked; const sortPref = m.querySelector('input[name="set_sort_pref"]:checked').value; const viewPref = m.querySelector('input[name="set_view_pref"]:checked').value; const saveBtn = m.querySelector('#set_save'); const applyChangesAndClose = async () => { gmSet('pk_blur_scope', newBlurScope); gmSet('pk_blur_thumb', newBlurScope !== 'off'); gmSet('pk_keep_pos', newKeepPos); gmSet('pk_comic_mode', newComicMode); gmSet('pk_clipboard_magnet_focus', newClipboardMagnetFocus); 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) { const keepFolderFirst = gmGet('pk_folder_first', false); gmSet('pk_folder_sort_prefs', '{}'); gmSet('pk_global_sort_pref', JSON.stringify({ sort: 'modified_time', dir: 1, folderFirst: keepFolderFirst })); S.sort = 'modified_time'; S.dir = 1; S.folderFirst = keepFolderFirst; } const isViewIndep = (viewPref === 'indep'); gmSet('pk_view_independent', isViewIndep); persistViewPreference(); 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_size_min', m.querySelector('#set_dl_filter_size_min').value.trim()); gmSet('pk_dl_filter_size_max', m.querySelector('#set_dl_filter_size_max').value.trim()); gmSet('pk_dl_filter_size_unit', m.querySelector('#cs_set_dl_size_unit .pk-select-item.act') ? m.querySelector('#cs_set_dl_size_unit .pk-select-item.act').dataset.val : 'MB'); gmSet('pk_dl_filter_name', m.querySelector('#set_dl_filter_name').value.trim()); gmSet('pk_aria2_url', newUrl); gmSet('pk_aria2_token', newToken); if (curLang !== selectedLang) { await ensureI18nReady(true, selectedLang); } m.remove(); const savedPack = (pkRemoteI18n && pkRemoteI18n.lang === selectedLang) ? pkRemoteI18n.data : T_LOCAL.zh; showToast((savedPack && savedPack.msg_settings_saved) || T_LOCAL.zh.msg_settings_saved); 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: (savedPack && savedPack.btn_nav_home) || 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(); await ensureI18nReadyBeforeOpen(selectedLang); await openManager(S.cache, S.preLoadPromise); } else { renderVisible(); } }; if (!newUrl && !newToken) { await applyChangesAndClose(); return; } saveBtn.disabled = true; saveBtn.textContent = L.str_saving_dots; try { const fetchUrl = normalizeAriaRpcUrl(newUrl || 'http://localhost:6800/jsonrpc'); const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_test', params: buildAriaRpcParams(newToken) }; await aria2RpcRequest(fetchUrl, payload, 5000); await applyChangesAndClose(); } catch (e) { if (await showConfirm(L.msg_aria2_test_fail, L.title_aria2_fail)) { await 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 = getLogicalRect(UI.btnSettings); const winEl = document.querySelector('.pk-win'); const isMax = winEl && winEl.classList.contains('pk-maximized'); let popLeft, popBottom; if (isMax) { popLeft = rect.left; popBottom = (window.innerHeight / scale) - rect.top + 8; } else { popLeft = rect.right + 10; popBottom = (window.innerHeight / scale) - rect.bottom; } 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(); if (!location.href.includes('/login')) 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 = S.getSelectedIds()[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 = S.getSelectedIds()[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.btnAria2, UI.btnDown, UI.btnExt, UI.btnImgSearch].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.btnMigrate, 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; syncLocalUploadVisibility(); 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; const locateTargetViewMode = (typeof resolvePreferredViewMode === 'function') ? resolvePreferredViewMode(targetContextId || 'root') : (gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'); const shouldHoldLocateGridSync = locateTargetViewMode === 'grid'; if (shouldHoldLocateGridSync) { S._folderViewSyncHold = true; beginFolderViewSync(); } await load(); let trackCount = 0; const maxTracks = 10; let trackerInterval = null; const releaseLocateViewSync = () => { if (!S._folderViewSyncHold) return; S._folderViewSyncHold = false; endFolderViewSync(true); }; const stopTracking = (releaseViewSync = true) => { if (trackerInterval) { clearInterval(trackerInterval); trackerInterval = null; } if (releaseViewSync) releaseLocateViewSync(); }; 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; let rowTop = targetIdx * CONF.rowHeight; if (isGridView()) { syncLayoutMetrics(); const gridLayout = getGridLayout(); const cols = Math.max(1, gridLayout.cols || 1); rowTop = Math.floor(targetIdx / cols) * 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 (isGridView()) { delete row.dataset.flashing; row.style.transition = ''; row.style.backgroundColor = ''; row.style.border = ''; row.style.borderRadius = ''; row.style.boxShadow = ''; row.style.outline = ''; row.className = getRowClassName(true, true, S.movingIds.has(item.id)); const chk = row.querySelector('input[type="checkbox"]'); if (chk && !chk.checked) chk.checked = true; } else 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); }); } } } requestAnimationFrame(() => { releaseLocateViewSync(); }); }; 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 = S.getSelectedIds()[0]; if (!id) return; const item = S.items.find(x => x.id === id); if (!item) return; if (S.offlineMode && item.phase !== 'PHASE_TYPE_COMPLETE') { const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); return; } if (S.uploadMode && item.status !== 'DONE') { if (item.file_id) { const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); } 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 = S.getSelectedIds(); 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.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.getSelectedCount() > 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 || L.str_unknown_name; 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 = getLogicalRect(anchorEl); let top = rect.bottom + 5; let left = rect.left; if (top + 320 > window.innerHeight / scale) top = rect.top - 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 selectedIds = S.getSelectedIds(); const n = selectedIds.length; if (n === 0) return; if (!await showConfirm(L.msg_cancel_share_confirm.replace('{n}', n))) return; setLoad(true); updateLoadTxt(L.msg_cancel_share_ing); try { const selectedItems = selectedIds.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 = S.getSelectedIds(); 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 = S.getSelectedIds(); 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.btnMigrate.onclick = async () => { const selectedIds = S.getSelectedIds(); const selectedCount = selectedIds.length; if (selectedCount === 0) return; const L = getStrings(); const hasConflict = selectedIds.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_op_blocked_moving); return; } if (selectedCount > 100) { showToast(L.err_migrate_limit, 'error'); return; } const estimatedSize = JSON.stringify(selectedIds).length; if (estimatedSize > 3.8 * 1024 * 1024) { const mb = (estimatedSize / 1024 / 1024).toFixed(2); showAlert(L.err_migrate_too_many.replace('{s}', mb)); return; } if (!await showConfirm(L.msg_migrate_confirm.replace('{n}', selectedCount))) return; setLoad(true); updateLoadTxt(L.msg_migrate_packing); try { let sourceUid = ""; 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.sub) { sourceUid = v.sub; break; } } catch {} } } if (!sourceUid) sourceUid = "UNKNOWN_UID_" + Date.now(); const randPass = Math.random().toString(36).slice(-5) + Math.random().toString(36).slice(-5).toUpperCase(); const payload = { file_ids: selectedIds, share_to: 'encryptedlink', expiration_days: 1, pass_code_option: 'REQUIRED', custom_pass_code: randPass, limit_count: 1 }; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/share`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 403 || res.status === 400) { throw new Error(L.err_migrate_ban); } throw new Error(err.error_description || `API Error ${res.status}`); } const data = await res.json(); const stub = { source_uid: sourceUid, share_id: data.share_id, pass_code: randPass, file_count: selectedCount, file_ids: selectedIds, timestamp: Date.now() }; gmSet('pk_migration_stub', JSON.stringify(stub)); try { const store = JSON.parse(gmGet('pk_share_limits', '{}')); store[data.share_id] = 1; gmSet('pk_share_limits', JSON.stringify(store)); } catch(e) {} setLoad(false); await showAlert(L.msg_migrate_ready, L.btn_migrate); 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(); if (!location.href.includes('/login')) window.location.href = 'https://mypikpak.com/drive/login'; } catch (e) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); } }; 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 = S.getSelectedIds().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) { const delRes = await showDeleteConfirm(L.msg_unzip_skip_del_confirm.replace('{n}', skippedCount)); if (delRes.confirm) { const idsToDelete = skippedItems.map(i => i.id); await executeBatchDelete(idsToDelete, { silent: true, forceRefresh: false, hardDelete: delRes.hardDelete }); 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 = S.getSelectedIds(); if (ids.length === 0) return; if (ids.length > 100) { showToast(L.err_share_limit, 'error'); 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 = []; const ids = S.getSelectedIds(); ids.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.currentTarget.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.getSelectedCount() > 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 = S.getSelectedIds()[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 = S.getSelectedIds()[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.btnMigrate, UI.btnBlacklistManager]; const shareBtns =[UI.btnCancelShare]; const upBtns =[UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll]; const upSep = el.querySelector('#pk-up-sep'); const downBtns = [UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnImgSearch]; 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.style.display = 'none'; }); 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, UI.btnMigrate].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'; }); [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; if(UI.btnImgSearch) UI.btnImgSearch.style.display = 'inline-flex'; } else if (S.uploadMode) { if(UI.btnNavUpload) UI.btnNavUpload.classList.add('act'); if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (S.path[0]) S.path[0].name = L.btn_nav_upload; [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; if(UI.btnImgSearch) UI.btnImgSearch.style.display = 'inline-flex'; 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 !== UI.btnMigrate) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnMigrate) UI.btnMigrate.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; downBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); } 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 = 'none'; downBtns.forEach(b => { if(b) b.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'; downBtns.forEach(b => { if(b) b.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'; } syncLocalUploadVisibility(); refresh(); }; restoreUIState(); load(); window.pkForceManagerReloadAfterAuth = (() => { let timer = null; let reloadSeq = 0; let authReloading = false; let pendingRelayout = false; const flushDeferredRelayout = () => { if (!pendingRelayout) return; pendingRelayout = false; if (!el || el.style.display === 'none') return; requestAnimationFrame(() => { if (!el || el.style.display === 'none') return; if (typeof syncLayoutMetrics === 'function') syncLayoutMetrics(); if (isGridView() && typeof scheduleGridRelayout === 'function') { scheduleGridRelayout(true); } else if (typeof renderList === 'function') { renderList(); } else if (typeof renderVisible === 'function') { if (UI.in) UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; renderVisible(); } }); }; const finishAuthReload = (seq) => { if (seq !== reloadSeq) return; authReloading = false; requestAnimationFrame(flushDeferredRelayout); }; window.pkIsAuthManagerReloading = () => authReloading; window.pkDeferAuthManagerRelayout = () => { pendingRelayout = true; }; return (reason = 'auth-recovered') => { if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; try { const ov = document.querySelector('.pk-ov'); if (!ov || ov.style.display === 'none') return; if (!S || !Array.isArray(S.path) || S.path.length === 0) return; const curNode = S.path[S.path.length - 1] || { id: '' }; const folderId = curNode.id || 'root'; const cacheKey = S.getRealCacheKey ? S.getRealCacheKey(folderId) : folderId; console.log(`[Auth Sync] Forcing manager reload after auth recovery: ${reason}, folder=${folderId}`); if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (typeof globalCache !== 'undefined') { globalCache.delete(cacheKey); if (folderId === 'root') { globalCache.delete(''); globalCache.delete('root'); } } if (S.cache) { S.cache.delete(cacheKey); if (folderId === 'root') { S.cache.delete(''); S.cache.delete('root'); } } if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(folderId === 'root' ? '' : folderId); if (folderId === 'root') globalDirtyFolders.add('root'); } if (typeof scannedFolderIds !== 'undefined') { scannedFolderIds.delete(folderId === 'root' ? '' : folderId); } const seq = ++reloadSeq; authReloading = true; pendingRelayout = false; let reloadTask = Promise.resolve(); if (typeof load === 'function') { reloadTask = Promise.resolve(load(false, true)).catch(e => console.warn('[Auth Sync] Forced reload failed:', e)); } else if (typeof window.pkSmartRefreshTrigger === 'function') { window.pkSmartRefreshTrigger(true); } else if (typeof refresh === 'function') { refresh(); } if (typeof runBackgroundCrawler === 'function' && typeof isBackgroundRunning !== 'undefined' && !isBackgroundRunning) { runBackgroundCrawler(); } reloadTask.finally(() => finishAuthReload(seq)); } catch (e) { authReloading = false; requestAnimationFrame(flushDeferredRelayout); console.warn('[Auth Sync] Force reload error:', e); } }, 260); }; })(); 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.getSelectedCount() > 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.historyMode) { 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 targetIds = historyItems.slice(0, 1000).map(x => x.id); const CONCURRENCY = 6; const fetchDetail = async (id) => { try { const netPriority = bypassLock ? 'high' : 'low'; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${id}?thumbnail_size=SIZE_MEDIUM`, { headers: getHeaders(), signal: signal, priority: netPriority }); if (!res.ok) return null; const f = await res.json(); if (f && !f.trashed) return f; return null; } catch (e) { return null; } }; for (let i = 0; i < targetIds.length; i += CONCURRENCY) { if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.getSelectedCount() > 0) return; const chunk = targetIds.slice(i, i + CONCURRENCY); const results = await Promise.all(chunk.map(id => fetchDetail(id))); results.forEach(rawF => { if (!rawF) return; 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; allFetchedItems.push(f); }); if (allFetchedItems.length > 0 && i > 0 && i % 24 === 0) await sleep(10); } allFetchedItems.sort((a, b) => (b._history_ts || 0) - (a._history_ts || 0)); } 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.getSelectedCount() > 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) { if (res.status === 401 || res.status === 403) { console.warn("[Recent] SWR fetch auth rejected."); const didLogout = await confirmedLogout('recent-swr-fetch-auth', 5000, 5200); if (didLogout) return; return; } 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.getSelectedCount() > 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) { if (res.status === 401 || res.status === 403) { console.warn("[Recent] Silent fetch auth rejected."); const didLogout = await confirmedLogout('recent-silent-fetch-auth', 5000, 5200); if (didLogout) return; return; } 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.getSelectedCount() > 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.getSelectedCount() > 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.getSelectedCount() > 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(); function 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(); if (pkWinHideObserver) pkWinHideObserver.disconnect(); el.remove(); document.removeEventListener('keydown', keyHandler); document.removeEventListener('mouseup', mouseHandler); if (window._pkMouseSideNavHandler) { window.removeEventListener('mousedown', window._pkMouseSideNavHandler, true); window.removeEventListener('mouseup', window._pkMouseSideNavHandler, true); window.removeEventListener('auxclick', window._pkMouseSideNavHandler, true); window.removeEventListener('click', window._pkMouseSideNavHandler, true); } 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) { console.warn("[Quota] Auth rejected."); const didLogout = await confirmedLogout('quota-auth-rejected', 5000, 5200); if (didLogout) return; } 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 (location.href.includes('/login') || location.pathname.includes('login')) return; if (typeof showToast === 'function') { showToast(getStrings().msg_turbo_activated, 'success', 5000); } }, 800); } setTimeout(async () => { const stubStr = gmGet('pk_migration_stub', ''); if (!stubStr) return; try { const stub = JSON.parse(stubStr); if (Date.now() - stub.timestamp > 86400000) { console.log("[Migration] Stub expired, clearing silently."); gmSet('pk_migration_stub', ''); return; } const isAuthReady = await waitForAuth(5000); if (!isAuthReady) { console.warn("[Migration] Auth not ready, aborting detection."); return; } const aboutRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/about?_t=${Date.now()}`, { headers: getHeaders() }); if (!aboutRes.ok) throw new Error(`About API Error ${aboutRes.status}`); const aboutData = await aboutRes.json(); const currentUid = aboutData.sub; if (currentUid === stub.source_uid) { gmSet('pk_migration_stub', ''); showToast(L.msg_migrate_same_account, 'warning'); return; } if (await showConfirm(L.msg_migrate_detect.replace('{n}', stub.file_count), L.btn_migrate)) { setLoad(true); updateLoadTxt(L.msg_migrate_saving); const infoRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/share?share_id=${stub.share_id}&pass_code=${stub.pass_code}`, { method: 'GET', headers: getHeaders() }); if (!infoRes.ok) throw new Error(`Share Info Fetch Error ${infoRes.status}`); const infoData = await infoRes.json(); const passCodeToken = infoData.pass_code_token || ""; const savePayload = { share_id: stub.share_id, pass_code_token: passCodeToken, params: { trace_file_ids: stub.file_ids.join(',') } }; const saveRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/share/restore`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(savePayload) }); if (!saveRes.ok) { const errData = await saveRes.json().catch(() => ({})); if (errData.error === 'file_restore_own' || errData.error_code === 9) { gmSet('pk_migration_stub', ''); setLoad(false); showToast(L.msg_migrate_same_account, 'warning'); return; } if (errData.error === 'file_space_not_enough' || errData.error_code === 8) { setLoad(false); const keep = await showConfirm(L.msg_migrate_quota_err.replace('{d}', errData.error_description), L.title_migrate_fail); if (!keep) gmSet('pk_migration_stub', ''); return; } throw new Error(errData.error_description || `Save API Error ${saveRes.status}`); } const saveData = await saveRes.json(); if (saveData.task_id) { let isDone = false; let pollCount = 0; while(!isDone && pollCount < 300) { await sleep(2000); pollCount++; const tRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${saveData.task_id}`, { headers: getHeaders() }); if (!tRes.ok) continue; const tData = await tRes.json(); if (tData.phase === 'PHASE_TYPE_COMPLETE') isDone = true; else if (tData.phase === 'PHASE_TYPE_ERROR') throw new Error("Transfer task failed on server."); } if (!isDone) console.warn("[Migration] Task polling timed out, but might still be running."); } gmSet('pk_migration_stub', ''); setLoad(false); await showAlert(L.msg_migrate_success, "🎉 " + L.title_alert); UI.btnRefresh.click(); } else { gmSet('pk_migration_stub', ''); } } catch(e) { setLoad(false); const keep = await showConfirm(L.msg_migrate_err_keep.replace('{e}', e.message), L.title_alert); if (!keep) gmSet('pk_migration_stub', ''); } }, 1500); } 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(2500); if (!isAuthReady) { console.warn("Background Crawler: Auth token wait timeout. Halting preload."); resolve(false); return; } if (typeof window.pkCleanupGhostFiles === 'function') window.pkCleanupGhostFiles(); 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(); window.pkScheduleResumeTasks = (reason = 'visibility') => { if (window.__pkResumeTaskTimer) clearTimeout(window.__pkResumeTaskTimer); const waitMs = (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 900 : 120; window.__pkResumeTaskTimer = setTimeout(() => { if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { console.log(`🚀 [Resume Sync] Auth recovery active, deferring resume tasks (${reason}).`); window.pkScheduleResumeTasks(`auth-wait:${reason}`); return; } if (location.href.includes('/login') || location.pathname.includes('login')) return; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); if (typeof isBackgroundRunning !== 'undefined' && !isBackgroundRunning) runBackgroundCrawler(); if (typeof pkState !== 'undefined' && pkState && pkState.uploadMode) { if (typeof refresh === 'function') refresh(); } }, waitMs); }; 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 () => { if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { console.log("🚀[Turbo Mode] Auth recovery active, delaying fast-track render."); setTimeout(startTurbo, 900); return; } const preload = preLoadRootFiles(); if (!document.querySelector('.pk-ov')) { await ensureI18nReadyBeforeOpen(); await openManager(globalCache, preload); } }; setTimeout(startTurbo, 100); } else { const startPreload = () => { if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { console.log("🚀 Preload deferred during auth recovery."); setTimeout(startPreload, 900); return; } preLoadRootFiles(); }; setTimeout(startPreload, 1500); } if (!window.__pkResumeHooksBound) { window.__pkResumeHooksBound = true; document.addEventListener('visibilitychange', () => { if (!document.hidden && typeof window.pkScheduleResumeTasks === 'function') { window.pkScheduleResumeTasks('visibility'); } }); window.addEventListener('focus', () => { if (!document.hidden && typeof window.pkScheduleResumeTasks === 'function') { window.pkScheduleResumeTasks('focus'); } }); window.addEventListener('pageshow', () => { if (!document.hidden && typeof window.pkScheduleResumeTasks === 'function') { window.pkScheduleResumeTasks('pageshow'); } }); } 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 - rect.width; const maxT = window.innerHeight - rect.height; 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 = async () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); b.style.transition = 'transform 0.1s'; if (!isDragging) { if (window.innerWidth < 720 || window.innerHeight < 340) { return; } let currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) { console.warn("PikPak Master: No auth token on button click. Entering recovery recheck."); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('button-click-missing-token', 4000); let isAuthReady = await waitForAuth(4500); if (!isAuthReady) { await sleep(800); isAuthReady = await waitForAuth(4200); } if (!isAuthReady) { console.warn("PikPak Master: Button click auth wait timeout."); const didLogout = await confirmedLogout('button-click-missing-token-final', 4000, 4500); if (didLogout) return; return; } currentHeaders = getHeaders(); } if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); const existingWin = document.querySelector('.pk-ov'); if (existingWin) { if (existingWin.style.display === 'none') { const needGridReopenRelayout = !!existingWin.querySelector('.pk-win.pk-grid-view'); if (existingWin.querySelector('.pk-win.pk-maximized')) { document.body.classList.add('pk-body-max'); } existingWin.style.display = 'flex'; existingWin.focus(); if (needGridReopenRelayout) { requestAnimationFrame(() => { requestAnimationFrame(() => { window.dispatchEvent(new Event('resize')); }); }); } } else { existingWin.style.display = 'none'; } } else { await ensureI18nReadyBeforeOpen(); 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(); } })() ;