Greasy Fork is available in English.
桌面级PikPak网盘管家!包含多模态文件查重(哈希/时长/名称)、多模态文件夹查重(名称/相似度/包含率)、多模态批量重命名(正则替换/剧集流水号生成/文本格式化/FC2名称清洗/前缀去广告/后缀智能修复)、清理空文件夹、内置解压密码库的批量解压、Aria2/Motrix带目录结构推送、夹杂无关文字或“去头”的污染磁链智能识别、自定义资源黑白名单:清理垃圾文件/文件夹、分享提取次数限制、导出目录树等。沉浸式媒体播放引擎:以图搜图、高级字幕加载、跳过片头尾及进度条缩略图预览。叫“增强大师”是有原因的,何不进来看看?
当前为
// ==UserScript== // @name PikPak 增强大师 // @name:zh-CN PikPak 增强大师 // @name:zh-TW PikPak 增強大師 // @name:en PikPak Enhancement Master // @name:ko PikPak 인핸서 마스터 // @name:ja PikPak 拡張マスター // @namespace https://github.com/digbug82/ // @version 1.2.0 // @author digbug82 // @license CC-BY-NC-SA-4.0 // @description 桌面级PikPak网盘管家!包含多模态文件查重(哈希/时长/名称)、多模态文件夹查重(名称/相似度/包含率)、多模态批量重命名(正则替换/剧集流水号生成/文本格式化/FC2名称清洗/前缀去广告/后缀智能修复)、清理空文件夹、内置解压密码库的批量解压、Aria2/Motrix带目录结构推送、夹杂无关文字或“去头”的污染磁链智能识别、自定义资源黑白名单:清理垃圾文件/文件夹、分享提取次数限制、导出目录树等。沉浸式媒体播放引擎:以图搜图、高级字幕加载、跳过片头尾及进度条缩略图预览。叫“增强大师”是有原因的,何不进来看看? // @description:zh-CN 桌面级PikPak网盘管家!包含多模态文件查重(哈希/时长/名称)、多模态文件夹查重(名称/相似度/包含率)、多模态批量重命名(正则替换/剧集流水号生成/文本格式化/FC2名称清洗/前缀去广告/后缀智能修复)、清理空文件夹、内置解压密码库的批量解压、Aria2/Motrix带目录结构推送、夹杂无关文字或“去头”的污染磁链智能识别、自定义资源黑白名单:清理垃圾文件/文件夹、分享提取次数限制、导出目录树等。沉浸式媒体播放引擎:以图搜图、高级字幕加载、跳过片头尾及进度条缩略图预览。叫“增强大师”是有原因的,何不进来看看? // @description:zh-TW 桌面級PikPak網盤管家!包含多模態檔案重複檢查(雜湊/時長/名稱)、多模態資料夾重複檢查(名稱/相似度/包含率)、多模態批次重新命名(正規替換/劇集流水號產生/文字格式化/FC2名稱清洗/前綴去廣告/副檔名智慧修復)、清理空資料夾、內建解壓縮密碼庫的批次解壓縮、Aria2/Motrix帶目錄結構推送、夾雜無關文字或「去頭」的污染磁鏈智慧識別、自訂資源黑白名單:清理垃圾檔案/資料夾、分享提取次數限制、匯出目錄樹等。沉浸式媒體播放引擎:以圖搜圖、進階字幕載入、跳過片頭尾及進度列縮圖預覽。叫「增強大師」是有原因的,何不進來看看? // @description:en Desktop-grade PikPak file manager! Features multi-modal file deduplication (hash/duration/name), folder deduplication (name/similarity/containment), bulk renaming (regex/serialization/formatting/FC2 cleaning/ad-removal/MIME-fix), empty folder pruning, batch extraction with password vault, Aria2/Motrix push with directory structure, smart corrupted magnet link recognition, custom resource black/whitelist, share limits, directory tree export, etc. Immersive media player: reverse image search, advanced subtitles, intro/outro skipping, and thumbnail previews. There's a reason it's called the "Enhancement Master", why not take a look? // @description:ko 데스크톱 수준의 PikPak 클라우드 관리자! 다중 모드 파일 중복 체크(해시/시간/이름), 폴더 중복 체크(이름/유사도/포함율), 다중 모드 일괄 이름 변경(정규식/에피소드 번호/포맷팅/FC2 정리/광고 제거/확장자 복구), 빈 폴더 정리, 비밀번호 금고 기반 일괄 압축 해제, 디렉토리 구조 유지 Aria2/Motrix 푸시, 손상된 마그넷 링크 스마트 인식, 리소스 블랙/화이트리스트, 공유 횟수 제한, 디렉토리 트리 내보내기 등을 제공합니다. 몰입형 미디어 플레이어: 이미지 검색, 고급 자막, 오프닝/엔딩 건너뛰기, 진행률 썸네일 미리보기. "인핸서 마스터"라고 불리는 데에는 이유가 있습니다. 한번 확인해 보세요! // @description:ja デスクトップクラスのPikPakマネージャー!マルチモーダルなファイル重複チェック(ハッシュ/時間/名前)、フォルダ重複チェック(名前/類似度/包含率)、一括リネーム(正規表現/連番/フォーマット化/FC2クリーンアップ/広告削除/拡張子修復)、空フォルダのクリーンアップ、パスワード庫連携の自動一括解凍、ディレクトリ構造を保持したAria2/Motrixプッシュ、破損マグネットリンクのスマート認識、リソースのブラック/ホワイトリスト、共有回数制限、ディレクトリツリーのエクスポートなどを備えています。没入型メディアプレーヤー:画像検索、高度な字幕、OP/EDスキップ、プログレスバーのサムネイル。なぜ「拡張マスター」と呼ばれるのか、ぜひお試しください! // @match https://mypikpak.com/drive/* // @match https://app.mypikpak.com/* // @match https://drive.mypikpak.com/* // @icon https://raw.githubusercontent.com/digbug82/PikPak_Enhancement_Master/main/img/logo.svg // @homepage https://github.com/digbug82/PikPak_Enhancement_Master // @supportURL https://github.com/digbug82/PikPak_Enhancement_Master/issues // @compatible chrome // @compatible edge // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_xmlhttpRequest // @connect catbox.moe // @connect litterbox.catbox.moe // @connect uguu.se // @connect mypikpak.com // @connect localhost // @run-at document-start // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/localforage.min.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"; const NativeTokenSniffer = { init: () => { const isTurbo = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; if (!isTurbo) { console.log('🚀 [PikPak Master] Turbo Mode OFF. Native Hijacking Disabled.'); return; } if (location.href.includes('/login') || location.pathname.includes('login')) { console.log('🚀 [PikPak Master] Login page detected. OOM-Guard suspended.'); return; } const inject = () => { const s = document.createElement('script'); s.textContent = `(function(){ const _W = window.Worker; window.Worker = function(url, opts) { if (location.href.includes('/login') || location.pathname.includes('login')) { return new _W(url, opts); } const u = url ? url.toString() : ''; const blockList = ['query_db', 'sync', 'database', 'index', 'calc_sha1', 'query_docs']; if (blockList.some(k => u.includes(k))) { console.log('🚫 [PikPak Master] OOM-Guard blocked worker:', u); return new _W('data:application/javascript,self.onmessage=()=>{}'); } return new _W(url, opts); }; const _f = window.fetch; window.fetch = async function(...args) { if (location.href.includes('/login') || location.pathname.includes('login')) { return _f.apply(this, args); } const url = args[0] ? args[0].toString() : ''; if (url.includes(':incremental_sync') || url.includes(':sync')) { return new Response(JSON.stringify({ error_code: 0, data: [], files: [], tasks: [], next_page_token: '' }), {status: 200, headers: {'Content-Type': 'application/json'}}); } try { const opts = args[1] || {}; let cap = null; if (opts.headers) { if (opts.headers instanceof Headers) cap = opts.headers.get('x-captcha-token') || opts.headers.get('X-Captcha-Token'); else if (typeof opts.headers === 'object') { const key = Object.keys(opts.headers).find(k => k.toLowerCase() === 'x-captcha-token'); if (key) cap = opts.headers[key]; } } if (cap && cap.length > 20) localStorage.setItem('pk_captured_captcha', cap); } catch (e) {} return _f.apply(this, args); }; })()`; (document.head || document.documentElement).appendChild(s).remove(); }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', inject) : inject(); } }; NativeTokenSniffer.init(); window.addEventListener('beforeunload', (e) => { const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; const tasks = pkState?.uploadTasks || []; if (tasks.some(t => activeStatus.includes(t.status))) { e.preventDefault(); return e.returnValue; } }); ; const CONF = { rowHeight: 40, buffer: 20, SYSTEM_FOLDER_NAME: 'My Pack', logoSVG: `<svg viewBox="0 0 238 200" style="width:24px;height:24px;border-radius:4px;flex-shrink:0;"><path d="M0 0 C1.82724609 0.01353516 1.82724609 0.01353516 3.69140625 0.02734375 C4.59761719 0.03894531 5.50382812 0.05054688 6.4375 0.0625 C5.95097979 7.11704304 4.33696858 12.90149479 1.6875 19.4375 C1.35234375 20.32566406 1.0171875 21.21382812 0.671875 22.12890625 C0.3315625 22.98097656 -0.00875 23.83304688 -0.359375 24.7109375 C-0.66198242 25.47583496 -0.96458984 26.24073242 -1.27636719 27.02880859 C-3.01571023 29.77913653 -4.60880008 30.70366989 -7.5625 32.0625 C-10.93383789 32.72265625 -10.93383789 32.72265625 -14.78515625 33.125 C-15.47874237 33.20142731 -16.17232849 33.27785461 -16.88693237 33.3565979 C-18.36660067 33.51855298 -19.84685768 33.67520381 -21.3276062 33.82696533 C-25.19232303 34.22318595 -29.05286739 34.65697538 -32.9140625 35.0859375 C-33.67180466 35.16903168 -34.42954681 35.25212585 -35.21025085 35.33773804 C-40.99791882 35.97875931 -46.74864414 36.77615252 -52.5 37.6875 C-61.81496788 39.10080547 -71.19269316 40.07620454 -80.5625 41.0625 C-19.8425 41.0625 40.8775 41.0625 103.4375 41.0625 C91.8875 39.7425 80.3375 38.4225 68.4375 37.0625 C63.8175 36.4025 59.1975 35.7425 54.4375 35.0625 C49.17221542 34.42736314 43.90722683 33.79696512 38.63671875 33.20703125 C37.62996094 33.08714844 36.62320313 32.96726563 35.5859375 32.84375 C34.69052246 32.74126953 33.79510742 32.63878906 32.87255859 32.53320312 C30.35601376 32.0467485 28.59527547 31.44037784 26.4375 30.0625 C23.38532266 24.97553776 21.3341425 19.45473677 19.1875 13.9375 C18.91695801 13.25671387 18.64641602 12.57592773 18.36767578 11.87451172 C16.82394482 7.78804812 16.13851057 4.42502757 16.4375 0.0625 C33.20320897 -0.76054389 50.04132 2.04640823 66.578125 4.53515625 C70.96365446 5.13439358 75.35589707 5.627565 79.75488281 6.11669922 C97.85972043 8.13836316 97.85972043 8.13836316 106.6875 9.4375 C107.39487305 9.52700928 108.10224609 9.61651855 108.83105469 9.70874023 C113.96714941 10.51808328 116.87598017 12.31623275 120.4375 16.0625 C121.69830294 18.53927732 122.67025259 20.7202309 123.5625 23.3125 C124.02136126 24.56846882 124.48232815 25.8236702 124.9453125 27.078125 C125.27250149 28.00288179 125.27250149 28.00288179 125.60630035 28.94632053 C126.38750394 31.05750635 126.38750394 31.05750635 127.44002533 32.93062496 C131.07482517 39.83448151 131.00351579 46.31795394 130.95507812 53.99243164 C130.96050802 55.37978344 130.96763552 56.76712947 130.97631836 58.15446472 C130.99445028 61.89829685 130.98752708 65.6416848 130.97480202 69.38552403 C130.96462344 73.31622656 130.97408092 77.24689291 130.98034668 81.17759705 C130.98760817 87.77544941 130.97807403 94.37312221 130.95898438 100.97094727 C130.93720936 108.58452515 130.94427739 116.19767461 130.96629 123.81124216 C130.98447611 130.36524706 130.98698696 136.91912344 130.97653532 143.47314543 C130.97031913 147.38014362 130.96941296 151.2869408 130.98268127 155.19392586 C130.99428653 158.8672447 130.9861299 162.54001414 130.96310425 166.213274 C130.95534421 168.19404482 130.96713242 170.17486244 130.97961426 172.15560913 C130.90049754 180.52230774 129.95755225 186.09535704 124.25390625 192.5234375 C123.51011719 193.15507812 122.76632813 193.78671875 122 194.4375 C121.25878906 195.08460938 120.51757812 195.73171875 119.75390625 196.3984375 C114.7661098 199.98157627 110.22842399 200.35421576 104.22135925 200.32992554 C103.39785408 200.33445665 102.5743489 200.33898776 101.72588903 200.34365618 C98.968488 200.35630894 96.21128426 200.35467924 93.45385742 200.35302734 C91.475975 200.35901206 89.49809491 200.36581748 87.5202179 200.37338257 C82.14823484 200.39105594 76.77631549 200.39573853 71.40430617 200.39701414 C66.91878502 200.39891354 62.4332787 200.40627158 57.94776326 200.41335833 C47.36384951 200.42964512 36.77996977 200.43452703 26.19604492 200.43310547 C15.28118177 200.43190408 4.36651636 200.45300486 -6.54829675 200.4845928 C-15.92170288 200.51075235 -25.29504442 200.52147289 -34.66848677 200.52019465 C-40.26569836 200.51968491 -45.86273424 200.52537507 -51.45990944 200.54655075 C-56.725388 200.56592749 -61.99052314 200.5660613 -67.25601387 200.55151749 C-69.1861191 200.54942757 -71.11624579 200.55414114 -73.04631424 200.5662384 C-75.68641426 200.58171127 -78.32533312 200.57236959 -80.96540833 200.55697632 C-81.72466655 200.56726344 -82.48392478 200.57755057 -83.26619083 200.58814943 C-90.327556 200.49750269 -96.39704041 197.82485418 -101.375 192.75 C-102.18904297 191.95142578 -102.18904297 191.95142578 -103.01953125 191.13671875 C-108.29053612 184.05088689 -108.01804154 177.09915158 -108.0300293 168.55004883 C-108.04229625 167.18245883 -108.05575106 165.81487905 -108.07029724 164.4473114 C-108.10523797 160.74401042 -108.12059214 157.04088761 -108.13013434 153.33744264 C-108.13673436 151.01403475 -108.14708893 148.69067299 -108.15863991 146.36728477 C-108.19836069 138.23287671 -108.22038571 130.09860956 -108.22827148 121.96411133 C-108.23610728 114.43116961 -108.28516577 106.89925647 -108.35333699 99.36664182 C-108.41007964 92.86514961 -108.43519788 86.36399446 -108.43721896 79.86225718 C-108.43904166 75.9947118 -108.45309089 72.1282487 -108.50003624 68.26096535 C-108.72797687 48.29049317 -107.52961567 30.83210742 -95.5625 14.0625 C-92.23797604 10.732487 -88.44904231 10.20048941 -83.953125 9.5 C-83.20613342 9.37633057 -82.45914185 9.25266113 -81.68951416 9.12524414 C-74.04584045 7.901492 -66.3645662 7.06662299 -58.66394043 6.29776001 C-54.62860447 5.8940274 -50.59547976 5.46951727 -46.5625 5.04296875 C-45.77776306 4.96008102 -44.99302612 4.8771933 -44.18450928 4.79179382 C-36.33754684 3.9513441 -28.53467892 2.87051571 -20.734375 1.67578125 C-13.79617508 0.63078847 -7.03103815 -0.06826251 0 0 Z M-47 131 L-15 106 L-47 81 L-47 91 L-27 106 L-47 121 Z M45.4375 89.0625 C43.16309531 93.61130937 44.11732026 99.81887268 44.0625 104.8125 C44.02511719 106.08867188 43.98773438 107.36484375 43.94921875 108.6796875 C43.6563417 116.25277258 43.6563417 116.25277258 46.7109375 122.91015625 C50.0632924 125.55649945 51.41007501 125.90713502 55.50390625 125.58984375 C58.83921214 124.68021487 60.4149221 122.75927054 62.4375 120.0625 C64.03299443 115.26404894 63.62174204 110.1852134 63.625 105.1875 C63.64336914 103.71603516 63.64336914 103.71603516 63.66210938 102.21484375 C63.77173933 93.57358621 63.77173933 93.57358621 59.75 86.1875 C54.01325068 83.39664894 49.78182352 84.71817648 45.4375 89.0625 Z M-18.5625 155.0625 C-20.89546251 157.88967213 -20.89546251 157.88967213 -20.3125 161.125 C-19.8031756 164.161959 -19.8031756 164.161959 -17.5625 166.0625 C-15.5023267 166.81656896 -13.41368556 167.49416461 -11.3125 168.125 C-10.19359375 168.46660156 -9.0746875 168.80820313 -7.921875 169.16015625 C-1.62436639 170.85169635 4.26860909 171.24487637 10.75 171.25 C11.9555957 171.26836914 11.9555957 171.26836914 13.18554688 171.28710938 C21.14907742 171.30632948 28.31945463 169.57146397 35.875 167.125 C36.88433594 166.80660156 37.89367187 166.48820313 38.93359375 166.16015625 C41.73511224 165.200361 41.73511224 165.200361 43.4375 162.0625 C43.1133631 158.74009676 42.82973697 157.45473697 40.4375 155.0625 C35.63637087 154.61062902 31.50016124 155.74460874 26.9375 157.0625 C14.69655136 160.31686985 0.09246916 160.8899845 -11.5625 155.0625 C-15.0625 154.72916667 -15.0625 154.72916667 -18.5625 155.0625 Z " fill="currentColor" transform="translate(107.5625,-0.0625)"/></svg>`, emptySVG: `<svg viewBox="-2 -2 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5 10L7 5H17L19 10H5Z" fill="#E2E8F0" stroke="#94A3B8" stroke-width="1.2" stroke-linejoin="round"/><path d="M4 10V18C4 19.1 4.9 20 6 20H18C19.1 20 20 19.1 20 18V10" stroke="#334155" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 10L1 6.5" stroke="#334155" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 10L23 6.5" stroke="#334155" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#64748B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 13L10 14L9 15"/><path d="M15 13L14 14L15 15"/><path d="M11 17.5H13"/></g>`, dupHashSVG: `<svg style="width:24px;height:24px;margin-right:8px;flex-shrink:0;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M798 322.42A308.78 308.78 0 0 0 676.73 211.1a17.5 17.5 0 1 0-15.94 31.16 272.73 272.73 0 0 1 148.71 243v63.83c0 25.58-3.14 134.1-8.62 159.68a17.5 17.5 0 0 0 13.44 20.78 17.94 17.94 0 0 0 3.69 0.39 17.5 17.5 0 0 0 17.09-13.83c6.81-31.76 9.4-148.75 9.4-167v-63.88A307 307 0 0 0 798 322.42zM365.68 272.82a273.38 273.38 0 0 1 231.18-53.68 17.5 17.5 0 1 0 7.68-34.14 307.93 307.93 0 0 0-367.72 231.18 17.5 17.5 0 1 0 34.11 7.82 273.89 273.89 0 0 1 94.75-151.18zM246.54 467.73a17.49 17.49 0 0 0-17.5 17.5v69c0 50.29-14.45 87.61-44.18 114.11a17.5 17.5 0 0 0 23.28 26.13c22.56-20.11 38.52-45.63 47.43-75.85 5.7-19.34 8.47-40.4 8.47-64.39v-69a17.5 17.5 0 0 0-17.5-17.5zM743.42 636.35v-0.17l-0.5-52.83a17.5 17.5 0 1 0-35 0.34l0.5 52.74c0 4.2 0 8.79 0.05 13.68 0.21 34.94 0.53 87.74-9.16 116.81a17.5 17.5 0 1 0 33.2 11.08c11.52-34.56 11.2-88.62 11-128.09-0.07-4.85-0.09-9.4-0.09-13.56z" fill="currentColor"></path><path d="M707.92 527.26a17.5 17.5 0 0 0 35 0v-45c0-114.17-92.89-207-207.06-207a207.35 207.35 0 0 0-58.49 8.38 17.5 17.5 0 0 0 9.87 33.58 172.24 172.24 0 0 1 48.62-7c94.87 0 172.06 77.18 172.06 172.05zM363.81 482.22A172.4 172.4 0 0 1 437 341.4a17.5 17.5 0 1 0-20.14-28.62 207.45 207.45 0 0 0-88 169.44v108.39a203 203 0 0 1-6.86 55.17 162.05 162.05 0 0 1-47.22 77.75 17.5 17.5 0 1 0 23.65 25.8c27.84-25.53 47.13-57.24 57.32-94.26a236.32 236.32 0 0 0 8.09-64.46zM440.83 566a17.5 17.5 0 0 0-17.5 17.47l-0.11 56.86c0 12.5-2.7 77.59-56 131.85a17.5 17.5 0 1 0 25 24.53 229.06 229.06 0 0 0 56.17-94.59c8.93-29.25 9.89-53 9.89-61.75l0.11-56.84A17.5 17.5 0 0 0 440.83 566z" fill="currentColor"></path><path d="M604.17 419.76a17.5 17.5 0 0 0-4.71-24.3 113 113 0 0 0-176.16 93.68v38.12a17.5 17.5 0 0 0 35 0v-38.12a78 78 0 0 1 121.57-64.68 17.49 17.49 0 0 0 24.3-4.7zM618.85 438.05a17.51 17.51 0 0 0-9.92 22.68 77.55 77.55 0 0 1 5.33 28.41v206.29c0 33.49-6.45 66.07-19.71 99.61a17.5 17.5 0 1 0 32.55 12.87c14.9-37.71 22.16-74.51 22.16-112.48V489.14a112.38 112.38 0 0 0-7.74-41.14 17.5 17.5 0 0 0-22.67-9.95z" fill="currentColor"></path><path d="M549.91 488a17.5 17.5 0 0 0-35 0v174.37c0 0.51 0 1 0.06 1.52 0.08 0.88 7 89.15-51.16 152.8a17.5 17.5 0 0 0 25.83 23.62c66-72.15 61-168 60.27-178.62z" fill="currentColor"></path></svg>`, dupSimSVG: `<svg style="width:18px;height:18px;margin-right:8px;flex-shrink:0;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M956.416 348.864a328.512 328.512 0 0 1-245.12 363.2 328.576 328.576 0 0 1-643.712-36.928 328.512 328.512 0 0 1 245.12-363.2 328.576 328.576 0 0 1 643.712 36.928zM534.336 639.808a263.04 263.04 0 0 0 121.92 16.96c1.28-12.736 1.664-25.728 1.024-38.848l-122.88 21.888z m-75.136-45.12l189.056-33.664a263.488 263.488 0 0 0-14.272-39.808l-211.072 35.648c10.88 13.824 23.04 26.432 36.288 37.76zM390.528 503.936l211.456-35.712a265.28 265.28 0 0 0-34.176-36.288l-192.256 30.336c3.84 14.464 8.96 28.416 14.976 41.6z m-23.808-98.56l126.08-19.84a263.04 263.04 0 0 0-125.056-18.304 263.68 263.68 0 0 0-1.024 38.144z m351.744 180.48c2.56 18.944 3.52 37.76 2.88 56.32a264.576 264.576 0 0 0-126.336-510.72A264.448 264.448 0 0 0 382.72 302.144a328.512 328.512 0 0 1 335.744 283.712zM305.536 438.144a330.624 330.624 0 0 1-2.88-56.32 264.576 264.576 0 0 0 126.336 510.72 264.448 264.448 0 0 0 212.288-170.688 328.512 328.512 0 0 1-335.744-283.712z" fill="currentColor" fill-opacity=".9"></path></svg>`, dupNameSVG: `<svg style="width:18px;height:18px;margin-right:8px;flex-shrink:0;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M478.144 947.328c-120.448 0-176.96 0.128-297.408-0.064-104.384-0.128-146.432-42.56-146.496-147.264 0-195.008-0.128-389.952 0-584.96 0.064-92.224 44.48-137.792 137.152-137.984 247.04-0.576 430.144-0.64 677.184 0.064 95.104 0.256 140.992 47.36 141.056 141.696 0.128 196.032 0.128 392 0 588.032-0.064 95.872-44.16 140.096-140.992 140.416-123.456 0.384-246.976 0.064-370.496 0.064z m375.296-51.52c62.016-0.128 84.224-22.72 84.288-85.248v-596.992c0-62.528-22.208-85.312-84.16-85.312-248.96-0.128-433.984-0.128-682.944 0-62.016 0.064-84.224 22.72-84.288 85.248v596.992c0 62.592 22.208 85.248 84.16 85.312 123.456 0.192 182.976 0.064 306.432 0.064 125.44 0 251.008 0.128 376.512-0.064z" fill="currentColor"></path><path d="M594.176 358.912c0.576-26.24-2.432-50.88 8.768-74.048 27.264-56.576 77.952-56.896 130.624-57.024 49.024-0.128 94.272 7.232 114.304 57.6 19.776 49.664 20.864 103.296-0.576 152.256-25.984 59.456-82.368 58.048-136.64 57.28-46.912-0.704-86.464-13.632-108.224-59.584-11.968-25.216-6.912-52.032-8.256-76.48z m51.84 1.024c-0.384 76.544 6.144 82.944 83.328 82.944 7.104 0 14.208 0.128 21.312-0.064 32.832-1.024 53.44-14.656 56.128-44.096 5.12-56.512 6.208-99.84-26.752-114.048a58.112 58.112 0 0 0-20.736-3.84 1113.856 1113.856 0 0 0-67.008 0c-29.184 1.152-44.16 16.32-46.144 45.632-0.768 11.136-0.128 22.336-0.128 33.472z m-319.232 416.768c-58.176 0-52.288 0.064-110.464-0.064-9.152 0-18.496 0.256-27.456-1.28-13.12-2.24-23.488-9.6-23.872-23.808-0.384-15.744 10.24-25.6 24.896-25.664 135.68-0.576 207.296-0.576 342.976 0 13.312 0.064 23.104 8.768 23.424 23.488 0.384 16.32-10.112 24.832-24.576 25.92-23.36 1.664-46.848 1.28-70.336 1.344-44.8 0.192-89.728 0.064-134.592 0.064z m-121.792-109.376c-19.2 0-40.256-1.216-39.552-26.496 0.576-22.976 21.376-24.512 38.976-24.576 125.248-0.32 186.56-0.32 311.808-0.128 18.88 0.064 39.872 1.728 39.232 27.328-0.64 24.064-20.928 23.808-38.656 23.808-63.168 0.128-126.272 0.064-189.44 0.064h-122.368z" fill="currentColor"></path></svg>`, dupContainSVG: `<svg style="width:18px;height:18px;margin-right:8px;flex-shrink:0;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M784 64a144 144 0 0 1 144 144v96A144 144 0 0 1 784 448H320v200.32c0 44.16 35.84 80 80 80H448v-8.32A144 144 0 0 1 592 576h192a144 144 0 0 1 144 144v96A144 144 0 0 1 784 960h-192a144 144 0 0 1-143.744-135.68H400a176 176 0 0 1-176-176V447.104a144 144 0 0 1-128-143.104v-96A144 144 0 0 1 240 64h544z m0 608h-192a48 48 0 0 0-48 48v96a48 48 0 0 0 48 48h192a48 48 0 0 0 48-48v-96a48 48 0 0 0-48-48z m0-512h-544a48 48 0 0 0-48 48v96c0 26.496 21.504 48 48 48h544a48 48 0 0 0 48-48v-96a48 48 0 0 0-48-48z" fill="currentColor"></path></svg>`, crumbIcons: { right: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`, down: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`, sortAZ: `<svg viewBox="0 0 800 800" fill="currentColor" style="width:35px;height:35px;flex-shrink:0;"><g transform="translate(21,843) scale(0.09,-0.09)"><path d="M2566 6494 c-32 -10 -63 -31 -95 -62 -40 -41 -74 -107 -283 -552 -131 -278 -281 -597 -334 -710 -113 -237 -118 -265 -68 -348 69 -113 229 -123 308 -20 13 18 49 87 79 153 30 66 65 141 78 168 l23 47 355 -2 355 -3 76 -168 c42 -93 89 -184 104 -203 69 -87 220 -83 289 7 31 42 50 109 41 146 -6 24 -425 976 -582 1323 -65 144 -110 197 -187 223 -58 20 -94 20 -159 1z m117 -639 c20 -44 58 -131 86 -194 28 -62 51 -115 51 -117 0 -2 -83 -4 -185 -4 -102 0 -185 2 -185 5 0 11 190 398 194 394 2 -2 19 -40 39 -84z"/><path d="M4965 6436 c-37 -16 -92 -77 -102 -114 -5 -15 -8 -696 -8 -1515 0 -818 -1 -1487 -1 -1487 -1 0 -120 127 -265 283 -145 155 -284 303 -309 328 -77 77 -173 89 -254 32 -75 -53 -100 -157 -57 -240 18 -35 243 -280 723 -788 115 -121 216 -220 240 -232 54 -30 136 -31 193 -3 25 12 71 53 111 99 38 42 129 142 203 222 654 702 660 710 661 789 0 20 -10 56 -22 80 -48 93 -140 131 -233 95 -45 -17 -49 -20 -567 -580 l-37 -40 -1 1451 c0 1585 2 1519 -57 1576 -56 54 -151 73 -218 44z"/><path d="M1865 4166 c-78 -34 -119 -103 -113 -187 5 -74 40 -129 100 -158 43 -20 56 -21 507 -21 l463 0 -169 -123 c-92 -68 -276 -204 -408 -303 -132 -99 -276 -206 -320 -238 -105 -77 -133 -103 -156 -148 -37 -72 -18 -190 37 -239 59 -51 27 -50 832 -47 l749 3 37 29 c111 84 95 258 -29 324 -26 14 -87 16 -467 16 -241 1 -438 3 -438 6 1 3 135 105 298 228 655 491 684 514 709 562 35 66 29 163 -12 221 -17 23 -50 53 -74 66 l-44 23 -736 0 c-590 -1 -742 -3 -766 -14z"/></g></svg>`, sortZA: `<svg viewBox="0 0 800 800" fill="currentColor" style="width:35px;height:35px;flex-shrink:0;"><g transform="translate(35,808) scale(0.09,-0.09)"><path d="M2433 6180 c-51 -12 -122 -59 -152 -101 -16 -21 -56 -103 -89 -181 -33 -79 -141 -336 -240 -573 -99 -236 -202 -479 -228 -540 -70 -163 -70 -236 0 -313 33 -37 71 -52 131 -52 118 0 156 39 239 243 32 78 65 152 72 165 l14 22 325 0 c179 0 325 -1 325 -3 0 -13 126 -311 146 -345 39 -67 120 -100 201 -82 59 14 107 50 133 100 34 66 27 105 -56 309 -41 102 -165 411 -276 686 -110 275 -213 520 -229 545 -63 99 -194 149 -316 120z m163 -745 l80 -205 -172 0 -173 0 16 38 c8 20 25 60 37 87 12 28 45 103 72 168 27 64 51 117 54 117 3 -1 41 -93 86 -205z"/><path d="M4828 6116 c-28 -10 -139 -116 -426 -407 -491 -499 -574 -586 -588 -612 -34 -66 -23 -144 28 -203 54 -61 123 -84 199 -64 39 10 76 43 351 323 l308 312 0 -1481 c0 -1106 3 -1490 12 -1515 29 -86 91 -129 184 -129 70 0 131 32 166 87 l23 38 5 1498 5 1498 310 -311 c335 -336 339 -339 431 -325 122 18 202 167 146 272 -26 49 -950 990 -992 1010 -47 22 -117 26 -162 9z"/><path d="M1745 3866 c-68 -29 -124 -131 -111 -199 10 -51 38 -97 74 -125 l35 -27 510 -5 509 -5 -523 -394 c-288 -217 -537 -406 -552 -420 -65 -59 -90 -169 -54 -236 20 -40 75 -91 110 -104 19 -8 272 -11 775 -11 708 0 749 1 788 19 138 63 143 256 9 333 l-40 23 -459 -4 c-298 -2 -457 0 -455 7 2 5 234 182 514 392 281 210 527 398 548 418 105 101 70 280 -65 337 -33 13 -135 15 -810 15 -616 -1 -779 -3 -803 -14z"/></g></svg>`, sortNew: `<svg viewBox="0 0 800 800" fill="currentColor" style="width:35px;height:35px;flex-shrink:0;"><g transform="translate(9,813) scale(0.1,-0.1)"><path d="M3545 6019 c-792 -100 -1452 -629 -1696 -1360 -67 -199 -89 -333 -96 -564 -9 -360 47 -638 192 -942 164 -343 395 -611 705 -818 334 -222 670 -329 1076 -342 315 -10 579 36 860 152 736 303 1230 1022 1238 1804 1 124 0 128 -27 170 -70 106 -224 106 -293 1 -24 -37 -27 -51 -35 -193 -13 -250 -56 -427 -154 -634 -222 -468 -651 -800 -1185 -914 -91 -20 -135 -23 -325 -24 -253 0 -343 13 -538 76 -350 114 -649 333 -859 629 -77 108 -172 293 -217 424 -94 266 -114 598 -55 882 86 420 374 823 754 1057 236 145 575 247 825 247 97 0 174 41 211 111 18 36 18 112 0 147 -19 36 -65 78 -99 91 -33 13 -181 12 -282 0z"/><path d="M4942 5844 c-43 -22 -79 -66 -92 -112 -6 -22 -10 -180 -10 -397 l0 -360 -164 165 c-98 99 -180 172 -203 182 -150 68 -300 -93 -224 -240 25 -47 634 -654 681 -678 45 -23 125 -24 170 -1 20 9 185 164 367 345 298 294 333 331 343 371 21 81 -11 156 -85 198 -47 26 -101 29 -152 8 -21 -9 -104 -83 -202 -180 -92 -91 -169 -165 -172 -165 -2 0 -4 170 -4 378 0 421 1 413 -71 468 -46 35 -132 44 -182 18z"/><path d="M3693 5021 c-28 -13 -49 -33 -67 -64 l-27 -45 3 -564 c3 -544 4 -564 23 -594 32 -50 557 -579 592 -598 41 -21 115 -21 160 0 74 36 111 133 84 217 -9 25 -85 110 -261 289 l-249 253 -1 500 0 501 -27 42 c-50 76 -144 102 -230 63z"/></g></svg>`, sortOld: `<svg viewBox="0 0 800 800" fill="currentColor" style="width:35px;height:35px;flex-shrink:0;"><g transform="translate(55,832) scale(0.1,-0.1)"><path d="M3165 6093 c-239 -30 -413 -70 -574 -130 -484 -180 -885 -542 -1106 -998 -99 -204 -158 -410 -186 -650 -18 -156 -6 -441 26 -594 79 -380 252 -716 508 -988 308 -326 736 -548 1175 -608 147 -20 418 -20 563 1 215 30 420 94 621 195 294 148 567 392 750 670 199 302 301 631 315 1014 5 149 4 163 -15 200 -79 155 -271 155 -343 0 -14 -31 -19 -64 -19 -145 -1 -565 -289 -1067 -777 -1356 -332 -196 -747 -265 -1130 -188 -575 115 -1043 533 -1227 1096 -60 183 -71 257 -71 493 0 182 3 224 23 319 97 453 364 820 772 1064 195 116 485 207 725 227 102 8 157 28 194 70 51 59 63 133 32 200 -43 93 -120 126 -256 108z"/><path d="M4382 6058 c-53 -29 -650 -641 -669 -685 -33 -81 -4 -180 69 -233 34 -25 51 -30 95 -30 30 0 70 7 87 15 18 9 95 79 171 155 76 77 140 140 142 140 2 0 3 -150 3 -332 0 -194 4 -348 10 -369 12 -42 85 -115 125 -125 55 -14 133 1 173 33 72 58 72 56 72 451 0 193 2 352 5 352 2 0 73 -66 156 -147 83 -82 163 -154 177 -162 14 -8 50 -14 80 -14 128 0 217 139 167 261 -19 46 -632 664 -687 693 -51 26 -124 25 -176 -3z"/><path d="M3177 5075 c-50 -17 -85 -50 -108 -100 -17 -37 -19 -76 -19 -553 0 -439 2 -518 15 -550 20 -47 583 -614 639 -644 85 -44 203 -2 246 86 23 49 26 113 6 159 -7 18 -129 147 -270 287 l-256 255 0 463 c0 442 -1 465 -20 502 -24 47 -71 84 -125 99 -50 14 -61 13 -108 -4z"/></g></svg>` }, icons: { offline: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"></path><path d="M12 12v9"></path><path d="m8 17 4 4 4-4"></path></svg>`, navShare: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>`, unshare: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M392.2 517.2c0-13.8-2.4-26.9-5.9-39.6l280-168c27.6 34.6 69.6 57.1 117.2 57.1 83.1 0 150.4-67.4 150.4-150.4S866.5 65.8 783.5 65.8s-150.4 67.4-150.4 150.4c0 13.8 2.4 26.9 5.9 39.6l-280 168c-27.6-34.6-69.6-57.1-117.2-57.1-83.1 0-150.4 67.4-150.4 150.4s67.4 150.4 150.4 150.4c47.6 0 89.6-22.6 117.2-57.1l160.2 96.1 30.9-51.6-163.8-98.2c3.5-12.7 5.9-25.8 5.9-39.5z m391.3-391.3c49.8 0 90.3 40.5 90.3 90.3s-40.5 90.3-90.3 90.3-90.3-40.5-90.3-90.3 40.5-90.3 90.3-90.3zM241.8 607.4c-49.8 0-90.3-40.5-90.3-90.3s40.5-90.3 90.3-90.3 90.3 40.5 90.3 90.3c0 49.9-40.5 90.3-90.3 90.3z m640.7 69l-99 99-99.1-99-42.6 42.5 99.1 99.1-99.1 99.1 42.6 42.6 99.1-99.1 99 99.1 42.6-42.6L826 818l99-99.1-42.5-42.5z" fill="currentColor"></path></svg>`, refresh: `<svg width="18" height="18" viewBox="80 80 864 864" fill="currentColor" stroke="currentColor" stroke-width="45" stroke-linejoin="round" version="1.1" xmlns="http://www.w3.org/2000/svg" style="flex-shrink: 0;"><path d="M950.371072 532.795629l-84.398202-84.393085c-6.101975-6.096858-14.093996-9.145287-22.087041-9.143241-7.995091-0.001023-15.988136 3.047406-22.079878 9.148357l-84.519975 84.530209c-12.20088 12.195763-12.20088 31.971156 0 44.171012 6.099928 6.094812 14.09195 9.145287 22.082948 9.145287s15.993253-3.050476 22.082948-9.150404l33.171494-33.175587c-16.019859 175.330214-163.813926 313.145-343.250668 313.145-190.096523 0-344.749812-154.653289-344.749812-344.749812s154.653289-344.754928 344.749812-344.754928c92.084255 0 178.658006 35.859719 243.779166 100.975762 12.20088 12.20088 31.966039 12.20088 44.166919 0 12.20088-12.195763 12.20088-31.971156 0-44.166919-76.914764-76.91988-179.176822-119.27657-287.946085-119.27657-224.543056 0-407.217539 182.679599-407.217539 407.222655 0 224.537939 182.674483 407.217539 407.217539 407.217539 212.604142 0 387.574153-163.800623 405.591505-371.808074l29.239951 29.238928c6.099928 6.094812 14.09195 9.145287 22.082948 9.145287 7.990998 0 15.98302-3.050476 22.082948-9.150404C962.571952 564.770877 962.571952 544.995485 950.371072 532.795629zM411.244248 429.099918l22.082948-22.082948c12.20088-12.195763 12.20088-31.971156 0-44.166919-12.20088-12.20088-31.966039-12.20088-44.166919 0l-22.082948 22.082948c-12.20088 12.195763-12.20088 31.971156 0 44.166919 6.099928 6.099928 14.09195 9.150404 22.082948 9.150404S405.143297 435.199847 411.244248 429.099918zM565.846372 539.536146l-22.082948 22.082948c-12.20088 12.195763-12.20088 31.971156 0 44.166919 6.099928 6.099928 14.09195 9.150404 22.082948 9.150404s15.98302-3.050476 22.082948-9.150404l22.082948-22.082948c12.20088-12.195763 12.20088-31.971156 0-44.165896C597.812411 527.335267 578.047252 527.335267 565.846372 539.536146zM336.453868 521.093099c-4.869914 20.679995-4.809539 63.99757 26.373671 95.175663 22.663162 22.658046 51.944046 29.03222 74.18049 29.03222 8.194636 0 15.433504-0.868787 21.025872-2.104941 16.694217-3.691065 27.115568-20.070104 23.638373-36.810371-3.477194-16.730033-19.968797-27.578102-36.754089-24.258497-0.25378 0.035816-23.5166 4.376681-37.923728-10.030447-14.010085-14.020318-9.953699-35.539424-9.658987-37.003775 3.741207-16.673751-6.639211-33.302477-23.312962-37.231973C357.26587 493.89055 340.408947 504.296551 336.453868 521.093099z" /></svg>`, retry: `<svg width="18" height="18" viewBox="0 0 1024 1024" fill="currentColor" version="1.1" xmlns="http://www.w3.org/2000/svg" style="transform: scale(1.1); vertical-align: -4px;"><path d="M233.088 189.141333A425.002667 425.002667 0 0 1 512 85.333333c235.648 0 426.666667 191.018667 426.666667 426.666667 0 91.136-28.586667 175.616-77.226667 244.906667L725.333333 512h128A341.333333 341.333333 0 0 0 275.626667 265.728l-42.538667-76.586667z m557.824 645.717334A425.002667 425.002667 0 0 1 512 938.666667C276.352 938.666667 85.333333 747.648 85.333333 512c0-91.136 28.586667-175.616 77.226667-244.906667L298.666667 512H170.666667a341.333333 341.333333 0 0 0 577.706666 246.272l42.538667 76.586667z" /></svg>`, settings: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`, home: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`, recent: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="12" y1="11" x2="12" y2="17"></line><line x1="9" y1="14" x2="15" y2="14"></line></svg>`, history: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>`, trash: `<svg width="24" height="24" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M870.453928 231.233432l-139.824559 0L730.629369 119.372761c0-30.890544-25.040303-55.929824-55.929824-55.929824L339.118558 63.442938c-30.890544 0-55.929824 25.040303-55.929824 55.929824l0 111.859647-139.824559 0c-15.442714 0-27.964912 12.522198-27.964912 27.964912s12.522198 27.964912 27.964912 27.964912l27.964912 0 0 559.300282c0 61.780065 50.079582 111.859647 111.859647 111.859647l447.439612 0c61.780065 0 111.859647-50.079582 111.859647-111.859647L842.487993 287.163255l27.964912 0c15.442714 0 27.964912-12.522198 27.964912-27.964912S885.896642 231.233432 870.453928 231.233432zM339.118558 119.372761l335.579965 0 0 111.859647L339.118558 231.232408 339.118558 119.372761zM786.559193 846.463538c0 30.890544-25.040303 55.929824-55.929824 55.929824L283.188734 902.393361c-30.890544 0-55.929824-25.040303-55.929824-55.929824L227.25891 287.163255l559.300282 0L786.559193 846.463538zM590.803787 734.602867c15.442714 0 27.964912-12.522198 27.964912-27.964912L618.768699 426.987814c0-15.442714-12.522198-27.964912-27.964912-27.964912s-27.964912 12.523221-27.964912 27.964912l0 279.650141C562.838875 722.080669 575.362096 734.602867 590.803787 734.602867zM423.014316 734.602867c15.442714 0 27.964912-12.522198 27.964912-27.964912L450.979228 426.987814c0-15.442714-12.523221-27.964912-27.964912-27.964912s-27.964912 12.523221-27.964912 27.964912l0 279.650141C395.049405 722.080669 407.571602 734.602867 423.014316 734.602867z" stroke="currentColor" stroke-width="40" stroke-linejoin="round"></path></svg>`, emptyTrash: `<svg width="16" height="24" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M870.45 231.23h-139.82l-0.01-111.86c0-30.89-25.04-55.93-55.93-55.93h-335.58c-30.89 0-55.93 25.04-55.93 55.93l0 111.86h-139.82c-15.44 0-27.96 12.52-27.96 27.96s12.52 27.96 27.96 27.96l27.96 0 0 559.3c0 61.78 50.08 111.86 111.86 111.86h447.44c61.78 0 111.86-50.08 111.86-111.86l0-559.3 27.96 0c15.44 0 27.96-12.52 27.96-27.96s-12.52-27.96-27.96-27.96zM339.12 119.37h335.58v111.86h-335.58V119.37zM786.56 846.46c0 30.89-25.04 55.93-55.93 55.93h-447.44c-30.89 0-55.93-25.04-55.93-55.93v-559.3h559.3v559.3z" /><path d="M620 440l-216 216m216 0l-216-216" stroke="currentColor" stroke-width="70" stroke-linecap="round"/></svg>`, restore: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>`, delForever: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`, newfolder: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform: scale(1.2); transform-origin: center;"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/><line x1="12" x2="12" y1="10" y2="16"/><line x1="9" x2="15" y1="13" y2="13"/></svg>`, del: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`, deselect: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="m9 9 6 6"/><path d="m15 9-6 6"/></svg>`, copy: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`, cut: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" x2="9" y1="12" y2="12" /></svg>`, paste: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>`, rename: `<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor" version="1.1"><path d="M56.925091 777.495273v189.579636h189.579636L805.655273 407.924364l-189.579637-189.579637L56.925091 777.495273zM952.32 261.306182a50.315636 50.315636 0 0 0 0-71.354182L834.048 71.68a50.315636 50.315636 0 0 0-71.400727 0L670.254545 164.165818 859.787636 353.745455l92.532364-92.439273v-0.093091z" fill="currentColor"></path></svg>`, bulkrename: `<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor" version="1.1" style="transform: scale(1.2);"><path d="M882.88 280.64l42.88-45.76a13.76 13.76 0 0 0 0-19.2L829.12 128a13.12 13.12 0 0 0-18.88 0L768 173.12zM739.84 202.56l-218.88 234.88a17.28 17.28 0 0 0-3.52 8.64L512 547.84a13.44 13.44 0 0 0 15.04 14.08l102.08-12.48a13.76 13.76 0 0 0 7.68-5.44l218.88-233.6z" fill="currentColor"></path><path d="M864 381.12a24 24 0 0 0-24 24v317.76H304.96V189.12h317.76a24 24 0 0 0 0-48H296.96A40 40 0 0 0 256 181.12v82.56H174.72a40 40 0 0 0-40 40v549.44a40 40 0 0 0 40 40h549.44a40 40 0 0 0 40-40v-75.84a20.8 20.8 0 0 0 0-6.4h83.84a40.32 40.32 0 0 0 40-40V405.12a24.32 24.32 0 0 0-24-24z m-147.84 396.16v67.84H182.72V311.68H256v419.2a40 40 0 0 0 40 40h421.44a20.8 20.8 0 0 0-1.28 6.4z" fill="currentColor" stroke="currentColor" stroke-width="35"></path></svg>`, unzip: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M1024 927.168V237.696c0-37.12-29.248-74.112-73.152-74.112H526.72L394.88 30.08H65.92C29.248 30.08 0 59.776 0 96.832v830.336c0 37.12 29.248 66.688 65.856 66.688h892.352c36.48 0 65.792-29.632 65.792-66.688zM943.552 245.12v667.2H80.448V111.68H358.4L497.344 245.12h146.304v96.384h95.104v96.384h-95.104v96.384h95.104v96.32h-95.104v185.344h190.208V534.272h-95.104V437.888h95.104V341.504h-95.104V245.12h204.8z" fill="currentColor"></path></svg>`, prune: `<svg width="16" height="16" viewBox="0 0 1064 1024" fill="currentColor" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M155.648 419.84H909.312l15.40096 369.90976a215.04 215.04 0 0 1-214.8352 224.01024H355.1232a215.04 215.04 0 0 1-214.8352-224.01024L155.648 419.84z m98.26304 102.4l-11.34592 271.81056A112.64 112.64 0 0 0 355.1232 911.36h354.7136a112.64 112.64 0 0 0 112.51712-117.30944l-11.30496-271.81056H253.91104z" /><path d="M358.4 184.32a174.08 174.08 0 0 1 348.16 0v30.72h133.12a174.08 174.08 0 0 1 174.08 174.08V450.56A71.68 71.68 0 0 1 942.08 522.24H122.88A71.68 71.68 0 0 1 51.2 450.56V389.12A174.08 174.08 0 0 1 225.28 215.04h133.12V184.32zM532.48 112.64a71.68 71.68 0 0 0-71.68 71.68v81.92c0 28.2624-22.9376 51.2-51.2 51.2H225.28A71.68 71.68 0 0 0 153.6 389.12v30.72h757.76V389.12a71.68 71.68 0 0 0-71.68-71.68H655.36c-28.2624 0-51.2-22.9376-51.2-51.2v-81.92A71.68 71.68 0 0 0 532.48 112.64zM442.90048 686.16192a51.2 51.2 0 0 1 48.5376 53.6576l-10.24 204.8a51.2 51.2 0 0 1-102.23616-5.07904l10.24-204.8a51.2 51.2 0 0 1 53.6576-48.57856z" /></svg>`, blacklist: `<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor" version="1.1"><path d="M134.8 86.7H261c25.1 0 46.6 17.9 51.2 42.6l20.9 111.9c11.7 62.6 66.4 108 130.1 108h385.6c29.9 0 54.2 24.3 54.2 54.3v45.8c0 22.2 18 40.3 40.3 40.3s40.3-18 40.3-40.3v-45.8c0-74.3-60.5-134.8-134.8-134.8H463.3c-24.9 0-46.3-17.8-50.9-42.3l-20.9-111.9C379.7 51.8 324.9 6.2 261 6.2H134.8C60.5 6.2 0 66.7 0 141v633.6C0 847 58.9 905.9 131.3 905.9h279.4c22.2 0 40.3-18 40.3-40.3 0-22.2-18-40.3-40.3-40.3H131.3c-28 0-50.7-22.8-50.7-50.7V141c-0.1-29.9 24.3-54.3 54.2-54.3z" p-id="32404"></path><path d="M554.2 140.4h338.3c22.2 0 40.3-18 40.3-40.3 0-22.2-18-40.3-40.3-40.3H554.2c-22.2 0-40.3 18-40.3 40.3 0.1 22.3 18.1 40.3 40.3 40.3zM792.7 555.1c-127.6 0-231.4 103.8-231.4 231.4 0 127.6 103.8 231.3 231.4 231.3 127.6 0 231.3-103.8 231.3-231.3 0-127.6-103.8-231.4-231.3-231.4z m0 64.4c34.4 0 66.4 10.5 93 28.4L654.1 879.4c-17.9-26.6-28.4-58.6-28.4-93 0-92 74.9-166.9 167-166.9z m0 333.9c-34.4 0-66.4-10.5-93-28.4l231.5-231.5c17.9 26.6 28.4 58.6 28.4 93 0 92-74.9 166.9-166.9 166.9z" p-id="32405"></path></svg>`, invert: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M910.69 235.796H788.834V112.482c0-61.833-50.136-112.01-112.01-112.01H113.232C51.318 0.473 1.18 50.61 1.18 112.483v563.673c0 61.873 50.137 112.05 112.05 112.05h121.895v123.273c0 61.873 50.137 112.05 112.05 112.05H910.69c61.913 0 112.09-50.137 112.09-112.05V347.845c0-61.873-50.177-112.05-112.09-112.05zM235.126 347.845V712.9h-91.412c-37.14 0-67.23-30.13-67.23-67.23V143.006c0-37.1 30.09-67.23 67.23-67.23H646.38c37.061 0 67.151 30.13 67.151 67.23v92.79H347.175c-61.912 0-112.049 50.176-112.049 112.049zM844.25 533.937L598.016 780.091c-4.923 4.923-11.146 7.365-17.605 8.192-11.618 6.499-26.466 5.238-36.352-4.687L413.735 653.273c-12.012-12.013-12.012-31.469 0-43.481l14.455-14.454c12.012-12.012 31.468-12.012 43.48 0l97.595 97.634 217.088-217.009c11.934-12.012 31.39-12.012 43.402 0l14.533 14.494c11.855 12.012 11.855 31.468-0.04 43.48z" fill="currentColor"></path></svg>`, folderFirst: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="17" height="17"><path d="M496.439 270.671l5.909 4.937 256 256c16.662 16.662 16.662 43.677 0 60.34-14.811 14.811-37.802 16.457-54.431 4.937l-5.909-4.937L514.981 408.929l0.041 494.182c0 23.564-19.103 42.667-42.667 42.667-21.422 0-39.157-15.787-42.204-36.362l-0.463-6.305-0.041-494.592-183.3 183.429c-14.811 14.811-37.802 16.457-54.431 4.937l-5.909-4.937c-14.811-14.811-16.457-37.802-4.937-54.431l4.937-5.909 256-256c14.812-14.811 37.803-16.457 54.432-4.937z m231.739-178.227c23.564 0 42.667 19.103 42.667 42.667 0 21.422-15.787 39.157-36.362 42.204l-6.305 0.463h-512c-23.564 0-42.667-19.103-42.667-42.667 0-21.422 15.787-39.157 36.362-42.204l6.305-0.463h512z" fill="currentColor" stroke="currentColor" stroke-width="35" stroke-linejoin="round"></path></svg>`, analyze: `<svg width="18" height="18" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M529.664 213.333333H896a42.666667 42.666667 0 0 1 42.666667 42.666667v597.333333a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V170.666667a42.666667 42.666667 0 0 1 42.666667-42.666667h316.330667zM170.666667 213.333333v597.333334h682.666666V298.666667h-358.997333l-85.333333-85.333334z m341.333333 170.666667v170.666667h170.666667a170.666667 170.666667 0 1 1-170.666667-170.666667z"></path></svg>`, scanDup: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="transform: scale(1.2); transform-origin: center;"><path d="M200.1408 123.3408A115.2 115.2 0 0 1 281.6 89.6h326.4a38.4 38.4 0 0 1 27.136 11.264l211.2 211.2c7.2192 7.168 11.264 16.9472 11.264 27.136V819.2a115.2 115.2 0 0 1-115.2 115.2H614.4a38.4 38.4 0 0 1 0-76.8h128a38.4 38.4 0 0 0 38.4-38.4V355.1232L592.0768 166.4H281.6a38.4 38.4 0 0 0-38.4 38.4v204.8a38.4 38.4 0 0 1-76.8 0V204.8c0-30.5664 12.1344-59.8528 33.7408-81.4592z" fill="currentColor"></path><path d="M588.8 89.6a38.4 38.4 0 0 1 38.4 38.4v192H819.2a38.4 38.4 0 0 1 0 76.8h-230.4a38.4 38.4 0 0 1-38.4-38.4V128a38.4 38.4 0 0 1 38.4-38.4zM201.0112 537.856a38.4 38.4 0 0 1-3.584 54.2208 166.4 166.4 0 0 0-56.5248 119.04 165.5296 165.5296 0 0 0 48.128 122.6752 167.2704 167.2704 0 0 0 122.88 49.3568 167.936 167.936 0 0 0 120.4224-55.04 38.4 38.4 0 0 1 56.9344 51.456 243.968 243.968 0 0 1-175.5136 80.384 244.7872 244.7872 0 0 1-179.2-72.0384 243.0464 243.0464 0 0 1-70.4-179.4048 242.432 242.432 0 0 1 82.6368-174.1824 38.4 38.4 0 0 1 54.2208 3.584z" fill="currentColor"></path><path d="M280.064 484.864A38.4 38.4 0 0 1 307.2 473.6 243.2 243.2 0 0 1 550.4 716.8a38.4 38.4 0 0 1-38.4 38.4H307.2a38.4 38.4 0 0 1-38.4-38.4v-204.8a38.4 38.4 0 0 1 11.264-27.136z m65.536 70.0416v123.4944h123.4944a166.4 166.4 0 0 0-123.4944-123.4944z" fill="currentColor"></path></svg>`, export: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M392.843947 286.762667h508.16a122.538667 122.538667 0 0 0 0-244.096H392.843947a122.538667 122.538667 0 0 0 0 244.096z m0-151.04h508.16a29.098667 29.098667 0 0 1 0 57.984H392.843947a29.098667 29.098667 0 0 1 0-57.984z" fill="currentColor"></path><path d="M425.099947 576.384a112.384 112.384 0 0 0 103.253333 75.52h372.650667a122.538667 122.538667 0 0 0 0-244.138667h-372.650667a112.298667 112.298667 0 0 0-103.253333 75.562667H163.211947v-203.093333a121.088 121.088 0 0 0 77.909333-115.712 117.418667 117.418667 0 0 0-111.786667-122.026667h-17.408A117.418667 117.418667 0 0 0 0.09728 164.522667a121.130667 121.130667 0 0 0 77.909333 115.712v539.050666a117.418667 117.418667 0 0 0 111.658667 122.282667h235.306667a112.341333 112.341333 0 0 0 103.253333 75.52h372.650667a122.538667 122.538667 0 0 0 0-244.138667h-372.650667a112.256 112.256 0 0 0-103.253333 75.52H189.66528a27.904 27.904 0 0 1-26.581333-28.970666v-243.2z m103.253333-75.52h372.650667a29.098667 29.098667 0 0 1 0 57.941333h-372.650667a29.098667 29.098667 0 0 1 0-57.941333z m0 365.098667h372.650667a29.098667 29.098667 0 0 1 0 57.984h-372.650667a29.098667 29.098667 0 0 1 0-57.984zM111.926613 135.722667h17.408a29.098667 29.098667 0 0 1 0 57.984h-17.408a29.098667 29.098667 0 0 1 0-57.984z" fill="currentColor"></path></svg>`, stop: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><rect x="9" y="9" width="6" height="6"/></svg>`, ext: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform: scale(1.1);"><polygon points="6 3 20 12 6 21 6 3" fill="var(--pk-bg)" stroke="currentColor"></polygon></svg>`, download: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform: scale(1.1);"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>`, aria2:`<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor" version="1.1" xmlns="http://www.w3.org/2000/svg" style="transform: scale(1.0); margin-right: 1px;"><path d="M512 0C229.233705 0 0 229.216503 0 511.982798s229.233705 512 512 512 512-229.216503 512-512S794.731891 0 512 0z m269.262734 881.584733q-48.853649 0-84.995028-125.677731-14.105631-47.735519-35.625319-167.100121-53.721812 7.603279-140.505846 26.594275l-140.144604 29.140169q-26.594275 69.410026-90.774896 202.98347c-11.404919 19.696277-26.594275 29.656229-46.118533 29.656229-14.105631 0-26.577073-5.074587-37.844375-15.378578a50.143798 50.143798 0 0 1-16.634324-38.154012q0-25.493348 79.920441-193.677194a57.041795 57.041795 0 0 1-9.048246-31.823679c0-28.572504 17.374009-47.013036 51.915603-55.149577q60.206961-113.378309 152.254804-260.403709 125.57452-200.712807 156.245666-200.730009c28.02204 0 47.013036 19.352238 57.317027 58.228732l33.268647 178.126596 79.215159 368.122564 30.189491 84.083322c10.321193 28.572504 15.378578 47.752721 15.378578 57.33423a50.350222 50.350222 0 0 1-16.462304 38.446445c-11.026475 10.321193-23.497917 15.378578-37.603547 15.378578zM593.915872 275.249026l43.572638 204.153205q-130.459884 23.325897-194.382476 39.7882z" fill="currentColor"></path></svg>`, info: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`, moon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`, sun: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>`, cloudDownload: `<svg viewBox="0 0 800 800" fill="currentColor" style="overflow:visible;"><g transform="translate(-45,845) scale(0.12,-0.12)"><path d="M3895 5919 c-480 -46 -922 -309 -1159 -689 -90 -145 -146 -288 -185 -476 l-17 -86 -90 -23 c-383 -97 -709 -356 -881 -700 -151 -300 -176 -664 -67 -991 63 -190 204 -408 352 -544 312 -289 736 -424 1217 -390 114 9 312 36 323 45 3 2 -55 286 -63 308 -5 10 -14 16 -24 13 -47 -15 -267 -36 -373 -36 -445 0 -801 177 -1016 505 -56 85 -102 193 -129 303 -24 97 -24 312 0 416 95 403 435 704 872 772 50 7 113 14 141 14 l51 0 6 118 c8 148 27 242 71 366 126 354 429 613 833 713 94 23 129 27 273 27 176 1 233 -8 395 -61 235 -76 495 -267 621 -457 l36 -55 38 32 c33 29 194 170 213 187 11 10 -112 160 -208 253 -316 308 -795 477 -1230 436z"/><path d="M5728 4880 c-75 -12 -199 -54 -257 -87 -111 -65 -190 -137 -362 -334 -46 -53 -178 -202 -294 -333 -115 -130 -211 -243 -213 -250 -2 -11 101 -111 224 -218 20 -18 22 -16 135 114 339 390 591 666 635 695 96 63 140 77 244 78 107 0 158 -15 234 -69 184 -131 223 -409 89 -634 -31 -53 -227 -282 -432 -507 -25 -28 -88 -99 -140 -158 l-93 -108 24 -22 c14 -12 70 -64 125 -115 l101 -92 84 93 c284 317 530 599 571 656 309 422 195 1009 -238 1222 -44 21 -107 46 -140 54 -75 19 -222 27 -297 15z"/><path d="M5688 4207 c-98 -114 -466 -533 -633 -722 -83 -93 -216 -244 -296 -334 -79 -91 -197 -226 -263 -300 l-118 -135 33 -29 c19 -16 78 -69 131 -118 l97 -89 19 25 c25 31 369 427 477 550 45 49 177 200 295 334 248 283 316 359 467 531 88 99 108 128 99 139 -6 8 -63 60 -128 117 l-116 104 -64 -73z"/><path d="M4224 3453 c-270 -300 -314 -360 -367 -503 -123 -330 -29 -694 233 -903 229 -183 537 -216 806 -87 44 21 109 60 143 86 62 47 505 538 499 553 -4 10 -241 221 -249 221 -3 0 -75 -80 -161 -177 -226 -259 -289 -324 -349 -360 -244 -146 -552 -21 -623 252 -31 119 -12 257 52 363 20 33 82 112 137 173 55 62 142 159 193 216 50 56 92 107 92 111 0 9 -65 72 -171 165 l-77 67 -158 -177z"/></g></svg>`, share: `<svg width="16" height="16" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M288 373.333333c-82.432 0-149.333333 66.922667-149.333333 149.333334a149.333333 149.333333 0 0 0 149.333333 149.333333c82.496 0 149.333333-66.816 149.333333-149.333333 0-82.410667-66.88-149.333333-149.333333-149.333334z m0 64c47.104 0 85.333333 38.250667 85.333333 85.333334 0 47.146667-38.186667 85.333333-85.333333 85.333333a85.333333 85.333333 0 1 1 0-170.666667zM757.333333 672a128.021333 128.021333 0 1 0 128 128c0-70.656-57.344-128-128-128z m0 64a64.021333 64.021333 0 1 1-64 64c0-35.328 28.672-64 64-64zM757.333333 117.333333a128.021333 128.021333 0 1 0 128 128c0-70.656-57.344-128-128-128z m0 64a64.021333 64.021333 0 1 1-64 64c0-35.328 28.672-64 64-64z" ></path><path d="M356.565333 580.864a32 32 0 0 1 43.904-10.965333l266.666667 160a32 32 0 0 1-32.938667 54.869333l-266.666666-160a32 32 0 0 1-10.965334-43.904zM643.050667 264.789333a32 32 0 0 1 36.565333 52.522667l-256 178.282667a32 32 0 0 1-36.565333-52.522667l256-178.282667z" ></path></svg>`, maximize: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>`, minimize: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="M14 10l7-7"/><path d="M3 21l7-7"/></svg>`, close: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`, help: `<svg width="19" height="19" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M878.08 731.274667a32 32 0 0 1-54.88-32.938667A360.789333 360.789333 0 0 0 874.666667 512c0-200.298667-162.368-362.666667-362.666667-362.666667S149.333333 311.701333 149.333333 512s162.368 362.666667 362.666667 362.666667a360.789333 360.789333 0 0 0 186.314667-51.445334 32 32 0 0 1 32.928 54.88A424.778667 424.778667 0 0 1 512 938.666667C276.362667 938.666667 85.333333 747.637333 85.333333 512S276.362667 85.333333 512 85.333333s426.666667 191.029333 426.666667 426.666667c0 78.293333-21.152 153.568-60.586667 219.274667zM650.666667 437.333333c0 65.898667-46.72 120.853333-109.194667 135.082667V608a32 32 0 0 1-64 0v-64a32 32 0 0 1 32-32C552.266667 512 586.666667 478.4 586.666667 437.333333s-34.4-74.666667-77.194667-74.666666c-26.773333 0-51.082667 13.248-65.173333 34.624a73.088 73.088 0 0 0-8.522667 17.717333 32 32 0 0 1-60.885333-19.690667c3.797333-11.754667 9.173333-22.933333 15.978666-33.237333 25.856-39.253333 70.186667-63.413333 118.613334-63.413333C587.274667 298.666667 650.666667 360.576 650.666667 437.333333zM512 736a32 32 0 1 1 0-64 32 32 0 0 1 0 64z" fill="currentColor"></path></svg>`, warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:18px;height:18px;margin-right:8px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`, lock: `<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`, uploadBtn: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:translateY(-1px);"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M12 12v9"/><path d="m16 16-4-4-4 4"/></svg>`, upFile: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>`, upFolder: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 2H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>`, navUpload: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`, taskStart: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>`, taskPause: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>`, cleanAll: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>`, logout: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>`, blMarker: `<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" style="width:100% !important; height:100% !important; display:block;"><path d="M512.3 64.2c-247.4 0-448 200.6-448 448s200.6 448 448 448 448-200.6 448-448-200.6-448-448-448z m0 61.4c95.6 0 183.2 34.9 250.8 92.6L218.2 763c-57.7-67.6-92.6-155.2-92.6-250.8 0-213.2 173.5-386.6 386.7-386.6z m0 773.3c-95.6 0-183.2-34.9-250.7-92.6l544.9-544.9c57.7 67.6 92.6 155.2 92.6 250.7-0.2 213.3-173.6 386.8-386.8 386.8z" fill="#d93025"/></svg>`, vault: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512 981.333c-211.2 0-384-172.8-384-384V563.2c0-89.6 72.533-164.267 164.267-164.267H755.2c78.933 0 142.933 64 142.933 142.934v55.466c-2.133 211.2-174.933 384-386.133 384z m-221.867-518.4c-55.466 0-100.266 44.8-100.266 100.267v34.133c0 177.067 142.933 320 320 320s320-142.933 320-320V544c0-44.8-36.267-78.933-78.934-78.933h-460.8z" fill="currentColor"></path><path d="M697.6 422.4c-17.067 0-32-14.933-32-32V260.267c0-85.334-68.267-153.6-153.6-153.6s-153.6 68.266-153.6 153.6V390.4c0 17.067-14.933 32-32 32s-32-14.933-32-32V260.267c0-119.467 98.133-217.6 217.6-217.6s217.6 98.133 217.6 217.6V390.4c0 17.067-14.933 32-32 32z" fill="currentColor"></path><path d="M512 759.467c-17.067 0-32-14.934-32-32V588.8c0-17.067 14.933-32 32-32s32 14.933 32 32v138.667c0 17.066-14.933 32-32 32z" fill="currentColor"></path></svg>` }, typeIcons: { folder: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V8C22 6.9 21.1 6 20 6H12L10 4Z" fill="#E8A723"/><rect x="4.5" y="7" width="15" height="9" rx="1.5" fill="white" fill-opacity="0.95"/><path d="M2 11C2 9.895 2.895 9 4 9H20C21.105 9 22 9.895 22 11V18C22 19.105 21.105 20 20 20H4C2.895 20 2 19.105 2 18V11Z" fill="#FFC107"/></svg>`, systemFolder: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" fill="#E8A723"/><rect x="4.5" y="7" width="15" height="9" rx="1.5" fill="white" fill-opacity="0.95"/><path d="M2 11c0-1.1.9-2 2-2h16c1.1 0 2 .9 2 2v7c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2v-7z" fill="#FFC107"/><path d="M7.1 12L8.6 10.6c.1-.1.3-.1.4-.1h2.4c.2 0 .3.1.2.3l-1.4 1.2H7.1ZM16.9 12l-1.5-1.4c-.1-.1-.3-.1-.4-.1h-2.4c-.2 0-.3.1-.2.3l1.4 1.2h3.1z" fill="#D68C09"/><path d="M7 12h10v5.2c0 .4-.4.8-.9.8H7.9c-.5 0-.9-.4-.9-.8V12z" fill="#D68C09"/><rect x="9.5" y="13.5" width="1.5" height="2.5" rx="0.75" fill="#FFC107"/><rect x="13" y="13.5" width="1.5" height="2.5" rx="0.75" fill="#FFC107"/></svg>`, video: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#647EFF"/><rect x="6.5" y="8.5" width="8" height="7" rx="1" fill="white"/><path d="M15.5 10.5L18 9v6l-2.5-1.5v-3z" fill="white"/><path d="M9.5 10.5v3l2.5-1.5-2.5-1.5z" fill="#647EFF"/></svg>`, image: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#48C78E"/><rect x="5.5" y="5.5" width="13" height="13" rx="2" fill="white"/><circle cx="8.5" cy="9" r="1.5" fill="#48C78E"/><path d="M5.5 16.5l3.5-4.5 2.5 2.5 4-7.5 3 9.5h-13z" fill="#48C78E"/></svg>`, audio: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#D883FF"/><path d="M13 6h4v3h-2v6.5c0 2.21-1.79 4-4 4s-4-1.79-4-4 1.79-4 4-4c.75 0 1.45.21 2 .58V6z" fill="white"/></svg>`, archive: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#FFC107"/><rect x="7" y="2" width="3" height="3" fill="white"/><rect x="10" y="5" width="3" height="3" fill="white"/><rect x="7" y="8" width="3" height="3" fill="white"/><rect x="10" y="11" width="3" height="3" fill="white"/><rect x="7" y="14" width="3" height="3" fill="white"/></svg>`, archiveUnzipped: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#FFC107"/><rect x="7" y="2" width="3" height="3" fill="white"/><rect x="10" y="5" width="3" height="3" fill="white"/><rect x="7" y="8" width="3" height="3" fill="white"/><rect x="10" y="11" width="3" height="3" fill="white"/><rect x="7" y="14" width="3" height="3" fill="white"/><path d="M19.5 13l-4 6h2.5v4l4.5-6h-3l1.5-4z" stroke="white" stroke-width="2" stroke-linejoin="round" fill="white"/><path d="M19.5 13l-4 6h2.5v4l4.5-6h-3l1.5-4z" fill="#FFC107"/></svg>`, text: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#60C5F1"/><path d="M6 6.5h12v3h-4.5v8h-3v-8h-4.5v-3z" fill="white"/></svg>`, pdf: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#FF5252"/><g transform="translate(4,4) scale(0.015625)"><path d="M974.848 647.168c-28.672-30.72-86.016-48.128-167.936-48.128-44.032 0-94.208 4.096-149.504 14.336-30.72-30.72-62.464-66.56-92.16-108.544-21.504-29.696-39.936-60.416-56.32-91.136 32.768-101.376 48.128-183.296 48.128-242.688 0-66.56-23.552-136.192-93.184-136.192-21.504 0-41.984 13.312-53.248 31.744-30.72 56.32-17.408 179.2 36.864 300.032-20.48 60.416-40.96 118.784-67.584 183.296-22.528 54.272-49.152 111.616-76.8 162.816-155.648 63.488-256 137.216-265.216 194.56-4.096 21.504 3.072 41.984 18.432 57.344 5.12 4.096 25.6 21.504 59.392 21.504 103.424 0 211.968-169.984 267.264-273.408 41.984-14.336 84.992-27.648 126.976-39.936 46.08-13.312 93.184-23.552 135.168-30.72C753.664 741.376 849.92 757.76 898.048 757.76c59.392 0 80.896-24.576 88.064-45.056 11.264-25.6 3.072-54.272-10.24-69.632l-1.024 4.096z m-55.296 41.984c-4.096 21.504-25.6 35.84-55.296 35.84-8.192 0-15.36-1.024-23.552-3.072-54.272-13.312-104.448-40.96-155.648-83.968 50.176-8.192 92.16-10.24 118.784-10.24 29.696 0 55.296 1.024 71.68 6.144 19.456 4.096 50.176 17.408 44.032 55.296z m-300.032-67.584c-36.864 7.168-75.776 16.384-116.736 27.648-32.768 9.216-66.56 18.432-100.352 30.72 18.432-35.84 33.792-70.656 48.128-103.424 17.408-40.96 30.72-81.92 45.056-120.832 14.336 24.576 29.696 49.152 45.056 70.656 25.6 33.792 52.224 66.56 78.848 95.232zM434.176 83.968c6.144-11.264 17.408-17.408 26.624-17.408 29.696 0 34.816 34.816 34.816 62.464 0 46.08-14.336 116.736-37.888 197.632-40.96-112.64-44.032-205.824-23.552-242.688zM279.552 756.736c-71.68 120.832-141.312 196.608-183.296 196.608-8.192 0-15.36-3.072-21.504-7.168-8.192-8.192-12.288-18.432-10.24-30.72 8.192-43.008 89.088-103.424 215.04-158.72z" fill="white"/></g></svg>`, subtitle: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#48C78E"/><g transform="translate(4,4) scale(0.015625)"><path d="M512 150.97856V274.0224H364.544C242.40128 274.0224 143.36 384.24576 143.36 520.192c0 135.9872 99.04128 246.1696 221.184 246.1696H512v123.0848H371.54816C177.68448 889.4464 20.48 724.13184 20.48 520.192c0-203.93984 157.20448-369.21344 351.06816-369.21344H512z m491.52 0V274.0224h-147.456c-122.14272 0-221.184 110.22336-221.184 246.1696 0 135.9872 99.04128 246.1696 221.184 246.1696H1003.52v123.0848h-140.45184C669.20448 889.4464 512 724.13184 512 520.192c0-203.93984 157.20448-369.21344 351.06816-369.21344H1003.52z" fill="white"/></g></svg>`, executable: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#48C78E"/><g transform="translate(5.85,5.85) scale(0.012)"><path d="M1005.65035 611.129741c-43.944112-14.307385-70.51497-54.163673-70.51497-100.151697s29.636727-84.822355 70.51497-100.151697c12.263473-4.087824 20.439122-18.39521 16.351298-31.680638-12.263473-48.031936-31.680639-91.976048-56.207585-134.898204-6.131737-12.263473-22.483034-16.351297-35.768463-10.219561-14.307385 8.175649-33.724551 12.263473-50.075849 12.263473-58.251497 0-104.239521-48.031936-104.239521-104.239521 0-16.351297 4.087824-35.768463 12.263474-50.075848 6.131737-12.263473 2.043912-26.570858-10.219561-35.768463C737.897855 32.702595 691.909831 14.307385 643.877895 2.043912c-12.263473-4.087824-26.570858 4.087824-31.680639 16.351298-14.307385 43.944112-54.163673 70.51497-100.151696 70.51497s-84.822355-29.636727-100.151697-70.51497C407.806039 6.131737 393.498654-2.043912 380.213224 2.043912c-48.031936 12.263473-91.976048 31.680639-134.898203 56.207585-12.263473 6.131737-16.351297 22.483034-10.219561 35.768463 8.175649 14.307385 12.263473 33.724551 12.263473 50.075848 0 58.251497-48.031936 104.239521-104.239521 104.239521-16.351297 0-35.768463-4.087824-50.075848-12.263473-12.263473-6.131737-26.570858-2.043912-35.768463 10.219561C32.748155 288.191617 13.330989 334.179641 1.067516 381.189621c-4.087824 12.263473 4.087824 26.570858 16.351297 31.680638 43.944112 14.307385 70.51497 54.163673 70.51497 100.151697S58.297057 597.844311 17.418813 613.173653c-12.263473 4.087824-20.439122 18.39521-16.351297 31.680638 12.263473 48.031936 31.680639 91.976048 56.207585 134.898204 6.131737 12.263473 22.483034 16.351297 35.768463 10.219561 14.307385-8.175649 33.724551-12.263473 50.075848-12.263473 58.251497 0 104.239521 48.031936 104.239521 104.239521 0 16.351297-4.087824 35.768463-12.263473 50.075848-6.131737 12.263473-2.043912 26.570858 10.219561 35.768463 41.9002 24.526946 87.888224 43.944112 134.898203 56.207585h6.131737c10.219561 0 20.439122-6.131737 24.526946-18.39521 14.307385-43.944112 54.163673-70.51497 100.151697-70.51497s84.822355 29.636727 100.151696 70.51497c4.087824 12.263473 18.39521 20.439122 31.680639 16.351298 48.031936-12.263473 91.976048-31.680639 134.898204-56.207585 12.263473-6.131737 16.351297-22.483034 10.219561-35.768463-8.175649-14.307385-12.263473-33.724551-12.263474-50.075848 0-58.251497 48.031936-104.239521 104.239521-104.239521 16.351297 0 35.768463 4.087824 50.075849 12.263473 12.263473 6.131737 26.570858 2.043912 35.768463-10.219561 24.526946-41.9002 43.944112-87.888224 56.207585-134.898204 5.10978-12.263473-1.021956-27.592814-16.351298-31.680638z m-490.538922 58.251497c-87.888224 0-158.403194-70.51497-158.403194-158.403194s70.51497-158.403194 158.403194-158.403194S673.514622 423.08982 673.514622 510.978044s-71.536926 158.403194-158.403194 158.403194z" fill="white"/></g></svg>`, web: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#6F88FC"/><g transform="translate(4,4) scale(0.015625)"><path d="M631.296 188.416c-40.96 17.664-79.616 40.3968-114.944 67.584-15.872-10.1376-68.352-38.4-132.352-51.2l52.6336-23.296a352.4608 352.4608 0 0 1 85.3504-10.4448c38.1952 0 74.9568 6.144 109.312 17.3056z m227.072 232.448c-3.072 7.168-6.4512 14.1824-9.984 21.1456-10.9056 29.5936-34.6112 64.3072-71.0144 104.192-9.216 10.3936-18.944 20.3776-29.0304 29.952a537.9072 537.9072 0 0 0-186.0096-287.232 485.0176 485.0176 0 0 1 131.9936-66.9184l6.656-2.048a350.6176 350.6176 0 0 1 157.3888 200.96z m-537.6-186.624l12.3392 2.6624-0.0512 1.4336c42.496 11.776 82.3296 26.0608 119.5008 42.8032 7.0144 3.7376 13.9776 7.6288 20.7872 11.6736a537.7024 537.7024 0 0 0-143.2064 229.9392c-15.3088 44.032-22.8352 84.0704-22.6816 120.064-1.28 18.0736-1.6896 36.1472-1.1776 54.272a483.328 483.328 0 0 1-106.496-37.5296 348.8256 348.8256 0 0 1 114.944-421.0176l6.0416-4.3008z m381.0304 380.7232a484.864 484.864 0 0 1-287.744 94.0544c-18.0736 0-35.84-0.9728-53.4016-2.8672a490.6496 490.6496 0 0 1 0.2048-52.48c6.912-51.7632 14.592-92.3648 23.04-121.7024a483.328 483.328 0 0 1 121.1392-194.816l9.216-8.8576 5.0176-4.608a483.7888 483.7888 0 0 1 182.528 291.2768z m58.88 162.1504c5.8368-46.336 5.6832-93.184-0.4608-139.4688C783.4624 618.2912 844.8 550.4 870.4 512c0 17.0496-0.512 34.3552-1.536 51.8656-11.264 101.7344-47.3088 157.0304-108.2368 213.248z m-515.1744-41.0112c21.1968 6.912 42.9056 12.544 65.1776 16.896 2.4576 18.3808 5.7856 36.4544 10.0352 54.272a353.3312 353.3312 0 0 1-72.5504-67.84l-2.6624-3.328z m464.2816-61.1328a483.7888 483.7888 0 0 1-12.5952 148.7872 350.464 350.464 0 0 1-175.2064 46.6432 350.976 350.976 0 0 1-134.144-26.4704 478.3104 478.3104 0 0 1-21.4528-83.2c15.7184 1.3824 31.5904 2.048 47.616 2.048a539.136 539.136 0 0 0 284.416-80.5376l11.3664-7.2704z" fill="white"/></g></svg>`, apk: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#48C78E"/><g transform="translate(4,4) scale(0.015625)"><path d="M512 161.87392a292.57728 292.57728 0 0 1 292.57728 292.57728v19.49696H219.42272v-19.49696A292.57728 292.57728 0 0 1 512 161.87392zM219.42272 493.48608h585.15456V942.08H219.42272z" fill="white"/><path d="M394.97728 317.93152h58.49088V376.4224H394.97728zM590.0288 317.93152h58.49088V376.4224h-58.49088z" fill="#48C78E"/><path d="M724.86912 141.7216l41.3696 41.3696-96.54272 96.50176-41.3696-41.3696zM299.13088 141.7216l-41.3696 41.3696L354.304 279.552l41.3696-41.3696zM102.4 512.98304h78.0288v273.03936H102.4zM843.5712 512.98304H921.6v273.03936h-78.0288z" fill="white"/></g></svg>`, file: `<svg width="30" height="30" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="20" height="20" rx="6" fill="#B2B2B2"/><g transform="translate(4,4) scale(0.015625)"><path d="M939.303655 84.62284a289.780202 289.780202 0 0 1 0 409.290765l-446.080124 446.080124a289.487642 289.487642 0 0 1-408.486226-0.731399 289.487642 289.487642 0 0 1-0.731399-408.486226l236.24181-236.24181a178.315026 178.315026 0 0 1 251.820605 0c69.482885 69.482885 69.482885 182.118299 0 251.966884l-223.076632 223.003492a48.27232 48.27232 0 1 1-68.312647-68.239507L503.828814 478.334811a81.843525 81.843525 0 0 0 0-115.48787 81.916665 81.916665 0 0 0-115.41473 0L152.172274 598.869332a193.308701 193.308701 0 0 0 0 272.884889 193.235561 193.235561 0 0 0 272.88489 0l446.080123-446.080123a193.016141 193.016141 0 0 0-272.884889-272.81175l-13.165179 13.165178a48.27232 48.27232 0 1 1-68.166367-68.312647l13.092038-13.165179a289.926482 289.926482 0 0 1 397.368965-11.263541l11.9218 11.263541z" fill="white"/></g></svg>` } }; ; const CSS = ` :root { --pk-zoom: 1; --pk-bg: #ffffff; --pk-bg-rgb: 255, 255, 255; --pk-fg: #1a1a1a; --pk-bd: #e5e5e5; --pk-hl: #f0f0f0; --pk-sel-bg: #e6f3ff; --pk-sel-bd: #cce8ff; --pk-pri: #0067c0; --pk-btn-hov: #e0e0e0; --pk-gh: #f5f5f5; --pk-gh-fg: #333; --pk-sb-bg: transparent; --pk-sb-th: #ccc; --pk-sb-hov: #aaa; --pk-icon-c: #888; --pk-tip-bg: rgba(255, 255, 255, 0.95); --pk-tip-fg: #1a1a1a; --pk-tip-bd: rgba(0, 0, 0, 0.06); --pk-tip-sd: rgba(0, 0, 0, 0.12); --pk-toast-bg: rgba(255, 255, 255, 0.95); --pk-toast-fg: #1a1a1a; --pk-toast-bd: rgba(0, 0, 0, 0.08); --pk-match-bg: #fff2cc; --pk-match-fg: #d93025; --pk-v-line: #d1d1d1; } .pk-no-transition, .pk-no-transition * { transition: none !important; } .pk-dark { --pk-bg: #202020; --pk-bg-rgb: 32, 32, 32; --pk-fg: #f5f5f5; --pk-bd: #333333; --pk-hl: #2d2d2d; --pk-sel-bg: #2b3a4a; --pk-sel-bd: #0067c0; --pk-pri: #4cc2ff; --pk-btn-hov: #3a3a3a; --pk-gh: #2a2a2a; --pk-gh-fg: #eee; --pk-sb-th: #555; --pk-sb-hov: #777; --pk-icon-c: #aaa; --pk-tip-bg: rgba(20, 20, 20, 0.95); --pk-tip-fg: #ffffff; --pk-tip-bd: rgba(255, 255, 255, 0.1); --pk-tip-sd: rgba(0, 0, 0, 0.4); --pk-toast-bg: rgba(45, 45, 45, 0.95); --pk-toast-fg: #ffffff; --pk-toast-bd: rgba(255, 255, 255, 0.15); --pk-v-line: rgba(255, 255, 255, 0.25); } .pk-dark .pk-loading-ov { background: rgba(0,0,0,0.8); } .pk-ov { position: fixed; top: 0; left: 0; width: calc(100vw / var(--pk-zoom, 1)); height: calc(100vh / var(--pk-zoom, 1)); zoom: var(--pk-zoom, 1); transform-origin: top left; z-index: 10000; background: rgba(0,0,0,0.4); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; font-family: inherit; outline: none; overscroll-behavior: none; -webkit-user-select: none; user-select: none; } .pk-ov input, .pk-ov textarea { -webkit-user-select: text !important; user-select: text !important; cursor: text; } .pk-win { width: 90%; max-width: calc(1600px / var(--pk-zoom, 1)); min-width: 720px; min-height: 340px; height: 80%; background: var(--pk-bg); color: var(--pk-fg); border-radius: 8px; box-shadow: 0 25px 50px rgba(0,0,0,0.25); display: flex; flex-direction: row; overflow: hidden; border: 1px solid var(--pk-bd); position: relative; } .pk-sidebar { width: 68px; background: var(--pk-bg); border-right: 1px solid var(--pk-bd); display: flex; flex-direction: column; align-items: center; padding: 16px 0; flex-shrink: 0; z-index: 10; gap: 0; } .pk-nav-btn { width: 44px !important; height: 44px !important; border-radius: 10px; color: var(--pk-icon-c); padding: 0 !important; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; margin-bottom: 0; transition: background-color 0.1s ease, color 0.1s ease; position: relative !important; } .pk-nav-btn:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-nav-btn.act { background: var(--pk-sel-bg); color: var(--pk-pri); } #pk-btn-cloud { background: var(--pk-pri) !important; color: #fff !important; border-radius: 50% !important; margin-bottom: 12px !important; padding: 0 !important; overflow: visible !important; display: flex !important; align-items: center !important; justify-content: center !important; } #pk-btn-cloud:hover { filter: brightness(1.1); } #pk-btn-cloud svg { width: 24px !important; height: 24px !important; transform: scale(1.3); transform-origin: center center; margin: 0 !important; transition: transform 0.2s; } .pk-nav-btn svg { width: 24px !important; height: 24px !important; } .pk-maximized #pk-btn-cloud { background: var(--pk-pri) !important; border-radius: 8px !important; width: calc(100% - 20px) !important; height: 48px !important; padding: 0 15px !important; margin-bottom: 20px !important; } .pk-maximized #pk-btn-cloud svg { width: 24px !important; height: 24px !important; transform: scale(1.4); margin-right: 6px; } .pk-maximized #pk-btn-cloud:hover { filter: brightness(1.1); } .pk-sidebar #pk-settings { margin-top: auto; } .pk-btn-danger { background: #d93025 !important; color: #fff !important; border: none !important; } .pk-btn-danger:hover, #pk-empty-trash:hover { background: #d93025 !important; color: #fff !important; filter: brightness(1.15); opacity: 1 !important; } .pk-sidebar #pk-settings:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-sidebar #pk-settings svg { width: 26px !important; height: 26px !important; } .pk-main-col { flex: 1; display: flex; flex-direction: column; overflow: hidden; position: relative; min-width: 0; } .pk-hd { height: 48px; border-bottom: 1px solid var(--pk-bd); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; background: var(--pk-bg); } .pk-tt { font-weight: 700; font-size: 20px; display: flex; align-items: center; gap: 10px; } .pk-tt svg { color: #333; margin-right: 10px; transition: color 0.2s ease; } .pk-ov.pk-dark .pk-tt svg { color: #fff; } .pk-tb { height: 50px !important; padding: 0 8px !important; border-bottom: 1px solid var(--pk-bd); display: flex; gap: clamp(4px, 0.6vw, 8px) !important; align-items: center !important; background: var(--pk-bg); overflow: visible !important; flex-wrap: nowrap !important; position: relative; } #pk-top-bar { z-index: 30; } #pk-actionbar, #pk-trash-bar { z-index: 20; } .pk-btn { height: 32px; padding: 0 12px; border-radius: 4px; border: 1px solid transparent; background: transparent; color: var(--pk-fg); cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 6px; transition: background-color 0.1s; position: relative; font-weight: 500; white-space: nowrap; flex-shrink: 0; backface-visibility: hidden; } #pk-theme svg { transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } #pk-theme:active svg { transform: rotate(90deg) scale(0.8); } .pk-btn:hover:not(:disabled) { background: var(--pk-btn-hov) !important; } .pk-btn.pri:hover:not(:disabled) { background: var(--pk-pri) !important; filter: brightness(1.15); color: #fff !important; } .pk-btn:disabled { opacity: 0.4; cursor: not-allowed; } .pk-btn.pri { color: var(--pk-pri); font-weight: 600; } .pk-btn svg { width: 18px; height: 18px; flex-shrink: 0; display: inline-block; vertical-align: -4px; } .pk-btn span { white-space: nowrap; pointer-events: none; } @media (max-width: 1550px) { .pk-maximized .pk-btn:not(#pk-btn-folder-first):not(#pk-btn-invert):not(#pk-filter-btn):not(#pk-btn-exit):not(#pk-scan-dup):not(#pk-analyze):not(#pk-export):not(#pk-ext):not(#pk-aria2):not(#pk-down) span { display: none !important; } .pk-maximized .pk-btn { padding: 0 8px !important; } } @media (max-width: 1360px) { .pk-btn:not(#pk-btn-folder-first):not(#pk-btn-invert):not(#pk-filter-btn):not(#pk-btn-exit):not(#pk-scan-dup):not(#pk-analyze):not(#pk-export):not(#pk-ext):not(#pk-aria2):not(#pk-down):not(#btn_cfg_clean):not(#btn_cfg_export):not(#btn_cfg_import) span { display: none !important; } .pk-btn { padding: 0 8px !important; } } @media (max-width: 1150px) { #pk-btn-folder-first span, #pk-btn-invert span, #pk-filter-btn span, #pk-btn-exit span, #pk-scan-dup span, #pk-analyze span, #pk-export span { display: none !important; } } @media (max-width: 1000px) { #pk-ext span, #pk-aria2 span, #pk-down span { display: none !important; } } .pk-blacklist-area { display: flex; align-items: center; gap: 8px; margin-left: auto; max-width: 300px; min-width: 150px; flex-shrink: 1; } .pk-blacklist-area input { height: 32px; padding: 0 8px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); font-size: 13px; width: 100%; transition: border-color 0.2s; } .pk-blacklist-area input:focus { border-color: var(--pk-pri); outline: none; } .pk-global-chk { display: flex; align-items: center; cursor: pointer; margin-right: 8px; font-size: 13px; color: var(--pk-fg); user-select: none; white-space: nowrap; } .pk-global-chk input { margin: 0 4px 0 0; width: 16px; height: 16px; accent-color: var(--pk-pri); } .pk-search { position: relative; display: flex !important; align-items: center !important; margin: 0 !important; flex: 1 1 auto; min-width: 100px; max-width: 300px; transition: all 0.2s; z-index: 100; } .pk-search input { height: 32px; padding: 0 56px 0 10px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); font-size: 13px; width: 100% !important; margin: 0 !important; transition: border-color 0.2s; box-sizing: border-box; } .pk-search input:focus { border-color: var(--pk-pri); outline: none; } #pk-search-btn { position: absolute; right: 10px; left: auto; width: 14px; height: 14px; color: #888; cursor: pointer; transition: color 0.2s; } .pk-search input:focus + svg { color: var(--pk-pri); } .pk-f-ext { cursor: pointer; color: var(--pk-fg); padding: 4px 8px; border-radius: 4px; transition: background 0.2s, color 0.2s; font-weight: 500; font-size: 13px; } .pk-f-ext:hover { background: var(--pk-hl); } .pk-f-ext.act { color: var(--pk-pri); font-weight: bold; } .pk-fc-btn { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; background: var(--pk-hl); color: var(--pk-fg); transition: all 0.2s; border: 1px solid transparent; white-space: nowrap; } .pk-fc-btn:hover { background: rgba(0, 103, 192, 0.05); color: var(--pk-pri); border-color: var(--pk-pri); } .pk-fc-btn.act { background: var(--pk-pri); color: #fff; border: none; font-weight: bold; } .pk-fc-btn.act:hover { background: var(--pk-pri); color: #fff; } .pk-search-clear { position: absolute; right: 32px; top: 50%; transform: translateY(-50%); width: 20px; height: 20px; display: none; align-items: center; justify-content: center; cursor: pointer; color: #999; border-radius: 50%; transition: background 0.2s; } .pk-search-clear:hover { background: var(--pk-hl); color: #666; } .pk-search-clear svg { position: static !important; width: 14px !important; height: 14px !important; color: inherit !important; } .pk-hist-pop { position: absolute; top: 100%; left: 0; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 10010 !important; display: none; flex-direction: column; overflow: hidden; padding: 4px; margin-top: 4px; } .pk-hist-hd { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; font-size: 11px; color: var(--pk-pri); font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--pk-bd); margin-bottom: 4px; } .pk-hist-clear-btn { cursor: pointer; } .pk-hist-clear-btn:hover { color: var(--pk-pri); } .pk-hist-item { padding: 8px 12px; font-size: 13px; cursor: pointer; color: var(--pk-fg); display: flex; align-items: center; gap: 8px; border-bottom: 1px solid transparent; } .pk-hist-item:hover { background: var(--pk-sel-bg); } .pk-hist-item svg { position: static !important; width: 14px !important; height: 14px !important; color: #999; margin: 0 !important; } .pk-dup-toolbar { display: none; align-items: center; gap: 4px; padding: 0 8px; height: 100%; margin-left: 8px; background: transparent; border: none; flex-shrink: 0; } .pk-dup-lbl, .pk-dup-chk { white-space: nowrap !important; font-weight: 500; color: var(--pk-fg); font-size: 13px; margin-right: 6px; opacity: 0.8; flex-shrink: 0; cursor: pointer; display: flex; align-items: center; } .pk-dup-chk input { margin: 0 4px 0 0; vertical-align: middle; accent-color: var(--pk-pri); width: 16px; height: 16px; } .pk-txt-short { display: none; } .pk-txt-long { display: inline; } #pk-search-path-con .pk-txt-short { font-weight: 500; color: var(--pk-fg); } #pk-dup-folder-sel { max-width: 300px !important; min-width: auto; height: 30px !important; transition: max-width 0.2s; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; text-align: left; padding-right: 20px; } @media (max-width: 1150px) { .pk-txt-long { display: none !important; } .pk-txt-short { display: inline !important; } #pk-btn-exit, #pk-scan-dup { padding: 0 8px !important; } #pk-dup-folder-sel { max-width: 100px !important; } } .pk-btn-toggle { border: 1px solid var(--pk-bd); background: var(--pk-bg); color: var(--pk-fg); height: 30px; border-radius: 4px; padding: 0 10px; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; flex-shrink: 0; } .pk-btn-toggle:hover { background: var(--pk-btn-hov); border-color: var(--pk-pri); } .pk-btn-toggle span { font-weight: 700; color: var(--pk-pri); } #pk-dup-exit { flex-shrink: 0; } .pk-nav { display: flex !important; align-items: center !important; gap: 4px; overflow: hidden; white-space: nowrap; font-size: 13px; color: #666; margin: 0 8px; height: 100%; max-width: 60%; } .pk-nav span { cursor: pointer; padding: 2px 6px; border-radius: 4px; } .pk-nav span:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-nav span.act { font-weight: 600; color: var(--pk-fg); cursor: default; } .pk-grid-hd, .pk-row { display: grid; column-gap: 10px; align-items: center; font-size: 14px; color: var(--pk-fg); box-sizing: border-box; width: 100%; } .pk-grid-hd > div, .pk-row > div { display: flex; align-items: center; justify-content: flex-start !important; overflow: hidden; white-space: nowrap; text-align: left; } .pk-grid-hd > div:first-child, .pk-row > div:first-child { justify-content: center !important; overflow: visible !important; } .pk-grid-hd { height: 36px; border-bottom: 1px solid var(--pk-bd); font-size: 13px; color: #666; user-select: none; padding: 0 22px 0 16px; } .pk-row { padding: 0 16px; } .pk-col { cursor: pointer; font-weight: 600; display: flex; align-items: center; justify-content: flex-start; } .pk-col:hover { color: var(--pk-fg); } .pk-modal::-webkit-scrollbar, .pk-vp::-webkit-scrollbar, .pk-prev-list::-webkit-scrollbar, .pk-scroll::-webkit-scrollbar, #pk-rn-vp::-webkit-scrollbar, .pk-bl-area::-webkit-scrollbar, textarea::-webkit-scrollbar, .pk-sub-pane::-webkit-scrollbar, #pk_sub_search_list::-webkit-scrollbar, .pk-p-pop::-webkit-scrollbar, .pk-share-modal::-webkit-scrollbar { width: 6px; height: 6px; } .pk-no-scrollbar::-webkit-scrollbar { display: none !important; } .pk-no-scrollbar { -ms-overflow-style: none !important; scrollbar-width: none !important; } .pk-vp::-webkit-scrollbar-track, .pk-modal::-webkit-scrollbar-track, .pk-prev-list::-webkit-scrollbar-track, .pk-scroll::-webkit-scrollbar-track, #pk-rn-vp::-webkit-scrollbar-track, .pk-bl-area::-webkit-scrollbar-track, textarea::-webkit-scrollbar-track, .pk-sub-pane::-webkit-scrollbar-track, #pk_sub_search_list::-webkit-scrollbar-track, .pk-p-pop::-webkit-scrollbar-track { background: var(--pk-sb-bg); } .pk-vp::-webkit-scrollbar-thumb, .pk-modal::-webkit-scrollbar-thumb, .pk-prev-list::-webkit-scrollbar-thumb, .pk-scroll::-webkit-scrollbar-thumb, #pk-rn-vp::-webkit-scrollbar-thumb, .pk-bl-area::-webkit-scrollbar-thumb, textarea::-webkit-scrollbar-thumb, .pk-sub-pane::-webkit-scrollbar-thumb, #pk_sub_search_list::-webkit-scrollbar-thumb, .pk-p-pop::-webkit-scrollbar-thumb { background: var(--pk-sb-th); border-radius: 3px; } .pk-vp::-webkit-scrollbar-thumb:hover, .pk-modal::-webkit-scrollbar-thumb:hover, .pk-prev-list::-webkit-scrollbar-thumb:hover, .pk-scroll::-webkit-scrollbar-thumb:hover { background: var(--pk-sb-hov); } ::-webkit-scrollbar { cursor: default; } .pk-vp { flex: 1; overflow-y: auto; position: relative; background: var(--pk-bg); scrollbar-gutter: stable; } .pk-in { position: absolute; width: 100%; top: 0; } .pk-row { height: 40px; border: 1px solid transparent; cursor: default; padding: 0 16px; border-radius: 4px; } .pk-row:hover { background: var(--pk-hl); } .pk-row.sel { background: var(--pk-sel-bg); border: 1px solid transparent; } .pk-row.sel.pk-focused { border: 1px solid var(--pk-pri); border-radius: 4px; } .pk-name { display: flex; align-items: center; overflow: visible; min-width: 0; cursor: default; } .pk-name .pk-name-txt { transition: color 0.1s; border-bottom: 1px solid transparent; } .pk-name svg { flex-shrink: 0; margin-right: 8px; cursor: default; } .pk-win:not(.pk-mode-trash) .pk-name .pk-name-txt { cursor: pointer; } .pk-win:not(.pk-mode-trash) .pk-name .pk-name-txt:hover { color: var(--pk-pri); } .pk-tag-default { cursor: default !important; color: #999 !important; border: 1px solid #ccc !important; font-weight: normal !important; } .pk-tag-default:hover { color: #999 !important; } .pk-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; line-height: 1.5; padding: 2px 0; margin-top: -2px; } .pk-group-hd { display: flex; background: var(--pk-gh); color: var(--pk-gh-fg); font-weight: bold; align-items: center; padding: 0 16px; border-top: 4px solid var(--pk-bg) !important; border-bottom: 4px solid var(--pk-bg) !important; background-clip: padding-box; height: 40px !important; box-sizing: border-box; margin-top: 0 !important; } .pk-group-hd .pk-tag { margin-left: auto; background: #666; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; border: 1px solid #555; } .pk-group-hd .pk-cnt { margin-left: 10px; color: var(--pk-fg); font-size: 12px; opacity: 0.9; } .pk-loading-ov { position: absolute; inset: 0; background: rgba(var(--pk-bg-rgb), 0.75); z-index: 999; display: none; flex-direction: column; align-items: center; justify-content: center; color: var(--pk-fg); gap: 28px; backdrop-filter: blur(10px) saturate(180%); -webkit-backdrop-filter: blur(10px) saturate(180%); transition: all 0.3s ease; } .pk-spin-lg { width: 56px; height: 56px; border: 4px solid rgba(136, 136, 136, 0.25); border-top-color: var(--pk-pri); border-radius: 50%; position: relative; animation: pk-ultra-spin 1s linear infinite; transform: translateZ(0); } .pk-loading-txt { font-size: 15px; font-weight: 600; text-align: center; white-space: pre-line; line-height: 1.6; letter-spacing: 1px; } @keyframes pk-ultra-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes pk-text-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(0.98); } } .pk-stop-btn { display: flex !important; align-items: center !important; justify-content: center !important; gap: 6px; padding: 8px 24px; background: #d93025; color: white; border: none; border-radius: 20px; font-size: 14px; cursor: pointer; font-weight: bold; box-shadow: 0 4px 10px rgba(217, 48, 37, 0.3); transition: transform 0.1s; } .pk-stop-btn svg { display: block; } .pk-stop-btn:hover { background: #b02a20; transform: scale(1.05); } .pk-stop-btn:active { transform: scale(0.95); } .pk-ft { height: 48px; border-top: 1px solid var(--pk-bd); background: var(--pk-bg); display: flex; align-items: center; padding: 0 16px; justify-content: space-between; font-size: 13px; } .pk-stat { color: var(--pk-fg); font-size: 13px; } .pk-grp { display: flex; gap: 8px; } .pk-pop { position: fixed; pointer-events: none; z-index: 2147483647 !important; background: #000; border: 1px solid #333; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); border-radius: 6px; display: none; overflow: hidden; } .pk-pop img { display: block; max-width: 320px; max-height: 240px; object-fit: contain; } .pk-ctx { position: fixed; z-index: 2147483647 !important; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); min-width: 150px; padding: 4px 0; display: none; } .pk-ctx-item { padding: 8px 16px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px; color: var(--pk-fg); } .pk-ctx-item:hover { background: var(--pk-hl); } .pk-ctx-sep { height: 1px; background: var(--pk-bd); margin: 4px 0; } .pk-modal-ov { position: fixed; top: 0; left: 0; width: calc(100vw / var(--pk-zoom, 1)); height: calc(100vh / var(--pk-zoom, 1)); zoom: var(--pk-zoom, 1); transform-origin: top left; background: rgba(0, 0, 0, 0.5); z-index: 10001; display: flex; align-items: center; justify-content: center; } .pk-modal { position: relative; background: var(--pk-bg); padding: 25px; border-radius: 12px; width: 500px; max-height: 85vh; overflow: hidden !important; display: flex; flex-direction: column; gap: 15px; border: 1px solid var(--pk-bd); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); overscroll-behavior: none; } .pk-modal-ov { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10001; display: flex; align-items: center; justify-content: center; overscroll-behavior: none; overflow: hidden; } .pk-modal h3 { margin: 0 0 5px 0; font-size: 16px; border-bottom: 1px solid var(--pk-bd); padding-bottom: 10px; padding-right: 40px; color: var(--pk-fg); } .pk-modal-close { position: absolute; top: 15px; right: 15px; cursor: pointer; color: var(--pk-icon-c); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background 0.1s, color: 0.1s; } .pk-modal-close:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-field { display: flex; flex-direction: column; gap: 5px; font-size: 13px; } .pk-field input, .pk-field select { padding: 6px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); } .pk-field select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; background-size: 16px; padding-right: 32px !important; cursor: pointer; } .pk-modal-act { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } .pk-credit { font-size: 11px; color: #888; text-align: center; margin-top: 20px; border-top: 1px solid var(--pk-bd); padding-top: 10px; } .pk-credit a { color: #888; text-decoration: none; } .pk-credit a:hover { text-decoration: underline; } .pk-prev-list { flex: 1; overflow-y: auto; border: 1px solid var(--pk-bd); max-height: 300px; } .pk-prev-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 5px 10px; border-bottom: 1px solid var(--pk-bd); font-size: 12px; } .pk-prev-row:nth-child(odd) { background: var(--pk-hl); } .pk-sep { width: 1px; height: 16px; background: var(--pk-bd); margin: 0 4px; display: none; flex-shrink: 0; } .pk-sep-sm { width: 1px; height: 16px; background: var(--pk-bd); margin: 0 8px; flex-shrink: 0; } #pk-refresh, #pk-trash-refresh { width: auto !important; justify-content: center !important; flex-shrink: 0; } #pk-refresh svg, #pk-trash-refresh svg { width: 18px !important; height: 18px !important; transform: none !important; stroke-width: 2 !important; } .pk-grid-hd input[type="checkbox"], .pk-row input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; margin: 0 auto !important; flex-shrink: 0; accent-color: var(--pk-pri); box-sizing: content-box; transform: translateZ(0); display: block; position: relative; } .pk-player-box { position: relative; width: 100%; height: 100%; background: #000; display: flex; flex-direction: column; user-select: none; overflow: hidden; } .pk-player-video { width: 100%; height: 100%; object-fit: contain; outline: none; transform: translateZ(0); backface-visibility: hidden; image-rendering: -webkit-optimize-contrast; -webkit-font-smoothing: antialiased; } .pk-player-top { position: absolute; top: 0; left: 0; right: 0; height: 64px; background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.3) 60%, transparent 100%); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; z-index: 100 !important; opacity: 1; transition: opacity 0.3s; pointer-events: auto; } .pk-player-box.ui-hidden .pk-player-top { opacity: 0 !important; pointer-events: none !important; } .pk-player-title { color: #fff; font-size: 16px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); letter-spacing: 0.5px; } .pk-player-controls { position: absolute; bottom: 0; left: 0; right: 0; height: 64px; background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.35) 60%, transparent 100%); display: flex; align-items: center; padding: 0 16px; z-index: 80 !important; gap: 4px; opacity: 0; transition: opacity 0.3s; } .pk-player-box:hover .pk-player-controls, .pk-player-box.paused .pk-player-controls { opacity: 1; } .pk-player-progress-container { position: absolute; bottom: 64px; left: 12px; right: 12px; height: 14px; cursor: pointer; z-index: 11; display: flex; align-items: center; } .pk-player-progress-bg { width: 100%; height: 4px; background: rgba(255, 255, 255, 0.25); position: relative; border-radius: 2px; transition: height 0.1s, transform 0.1s; backdrop-filter: blur(2px); } .pk-player-progress-container:hover .pk-player-progress-bg { height: 6px; transform: scaleY(1.1); } .pk-player-progress-filled { height: 100%; background: var(--pk-pri); width: 0; position: relative; border-radius: 2px; transition: none !important; will-change: width; } .pk-player-progress-thumb { position: absolute; right: -7px; top: 50%; transform: translateY(-50%) scale(0); width: 14px; height: 14px; border-radius: 50%; background: #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .pk-player-progress-container:hover .pk-player-progress-thumb { transform: translateY(-50%) scale(1); } .pk-p-btn { color: #eee; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 50px; height: 40px; border-radius: 4px; transition: all 0.2s ease; position: relative; flex-shrink: 0; } .pk-p-btn:hover { color: var(--pk-pri); background: transparent; transform: scale(1.05); } .pk-p-btn:active { color: var(--pk-pri); background: transparent; transform: scale(0.95); } #pk_p_play, #pk_p_vol, #pk_p_close { display: inline-flex !important; width: 40px !important; height: 40px !important; border-radius: 50% !important; margin: 0 4px !important; padding: 0 !important; box-sizing: border-box !important; justify-content: center !important; align-items: center !important; } #pk_p_play svg, #pk_p_vol svg, #pk_p_close svg { margin: 0 !important; padding: 0 !important; } #pk_p_play:hover, #pk_p_vol:hover, #pk_p_close:hover { color: #fff !important; filter: brightness(1.5); background: rgba(255, 255, 255, 0.15) !important; transform: none !important; } .pk-p-btn svg { width: 24px; height: 24px; fill: currentColor; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); } #pk_sub_trigger .pk-p-btn svg { width: 21px; height: 21px; } #pk_p_full svg { width: 28px; height: 28px; } .pk-p-time { color: #ddd; font-size: 13px; font-family: "Segoe UI", Roboto, monospace; min-width: 90px; text-align: center; font-variant-numeric: tabular-nums; margin: 0 5px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); } .pk-p-menu-con { position: relative; display: flex; align-items: center; justify-content: center; height: 40px; cursor: pointer; font-size: 15px; color: #ddd; font-weight: 600; padding: 0; min-width: 50px; transition: color 0.2s; border-radius: 4px; } #pk_sub_trigger .pk-p-btn { width: 50px; height: 40px; } .pk-p-menu-con:hover { color: var(--pk-pri); background: transparent; } .pk-p-pop { position: absolute; bottom: 45px; left: 50%; transform: translateX(-50%); background: rgba(20, 20, 20, 0.9); border-radius: 8px; padding: 6px 0; display: none; flex-direction: column-reverse; min-width: 100px; text-align: center; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); z-index: 20; backdrop-filter: blur(10px); } .pk-p-pop::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: 45px; background: transparent; } .pk-p-menu-con:hover .pk-p-pop { display: flex; animation: pkFadeIn 0.2s ease; } @keyframes pkFadeIn { from { opacity: 0; transform: translate(-50%, 10px); } to { opacity: 1; transform: translate(-50%, 0); } } .pk-p-item { padding: 8px 16px; color: #ccc; cursor: pointer; font-size: 13px; position: relative; z-index: 2; transition: background 0.1s; text-align: center; display: flex; align-items: center; justify-content: center; } .pk-p-item:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } .pk-p-item.active { color: var(--pk-pri); font-weight: bold; } .pk-p-vol-wrap { display: flex; align-items: center; height: 100%; gap: 0px; margin-right: 5px; } .pk-p-vol-slider { -webkit-appearance: none; width: 70px; height: 4px; background: rgba(255, 255, 255, 0.3); border-radius: 2px; outline: none; cursor: pointer; transition: height 0.1s; } .pk-p-vol-slider:hover { height: 6px; } .pk-p-vol-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff; cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.1s; } .pk-p-vol-slider::-webkit-slider-thumb:hover { transform: scale(1.2); } .pk-p-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; display: none; } .pk-player-box.buffering:not(.pk-is-seeking) .pk-p-loading { display: block; } .pk-player-box.pk-is-seeking .pk-p-loading { display: none !important; } .pk-p-center-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) translateZ(0); width: 72px; height: 72px; background: rgba(0, 0, 0, 0.35); border-radius: 50%; display: none; align-items: center; justify-content: center; z-index: 36; pointer-events: none; border: 1px solid rgba(255, 255, 255, 0.25); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); backface-visibility: hidden; will-change: transform, opacity; } .pk-p-center-play svg { width: 36px; height: 36px; fill: #fff; margin-left: 4px; filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.3)); } .pk-player-box.paused.pk-v-started:not(.buffering):not(.pk-is-seeking) .pk-p-center-play { display: flex !important; animation: pkPlayPop 0.35s cubic-bezier(0.2, 0, 0.2, 1) forwards; } .pk-player-box.buffering .pk-p-center-play { display: none !important; opacity: 0 !important; } .pk-player-box.pk-is-seeking .pk-p-center-play { display: none !important; opacity: 0 !important; } @keyframes pkPlayPop { from { opacity: 0; transform: translate(-50%, -50%) scale(0.8) translateZ(0); } to { opacity: 1; transform: translate(-50%, -50%) scale(1) translateZ(0); } } .pk-p-seek-indicator { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) translateZ(0); height: 54px; min-width: 190px; background: rgba(0, 0, 0, 0.65); color: #fff; padding: 0 22px; border-radius: 12px; font-size: 22px; font-weight: 300; font-family: "Inter", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", sans-serif; z-index: 50; display: none; align-items: center; justify-content: center; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); font-variant-numeric: tabular-nums; pointer-events: none; white-space: nowrap; letter-spacing: 0px; } body.pk-dragging { cursor: pointer !important; user-select: none !important; -webkit-user-select: none !important; } .pk-player-box.pk-is-seeking .pk-player-progress-thumb { transform: translateY(-50%) scale(1) !important; opacity: 1 !important; transition: none !important; } .pk-player-box.pk-is-seeking .pk-player-progress-bg { height: 6px !important; transform: scaleY(1.1) !important; transition: none !important; } .pk-player-box.pk-is-seeking .pk-player-progress-filled { transition: none !important; will-change: width; } .pk-p-resume-toast { position: absolute; bottom: 85px; left: 24px; background: rgba(28, 28, 28, 0.95); color: #fff; padding: 8px 16px; border-radius: 99px; font-size: 13px; display: flex; align-items: center; gap: 8px; z-index: 100; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); animation: pkFadeInUp 0.2s ease; } .pk-p-plist-ov { position: absolute; top: 100%; left: 0; right: 0; z-index: 25; display: flex; flex-direction: column; pointer-events: none; } .pk-p-plist-strip { height: 84px; background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); display: flex; align-items: center; position: relative; border-top: none; pointer-events: auto; } .pk-p-plist-tab { position: absolute !important; bottom: 100%; left: 50%; transform: translateX(-50%); height: 25px; pointer-events: auto !important; z-index: 70; margin-bottom: -1px; } .pk-p-plist-tab { align-self: center; position: relative; z-index: 26; color: rgba(255, 255, 255, 0.8); padding: 0 20px; height: 30px; font-size: 12px; font-weight: 600; cursor: pointer; border: none; display: flex; align-items: center; justify-content: center; gap: 4px; background: transparent; margin-bottom: -1px; letter-spacing: 0.5px; transition: opacity 0.3s ease; opacity: 0; } .pk-p-plist-tab:hover, .pk-player-box.plist-active .pk-p-plist-tab { opacity: 1; } .pk-p-plist-tab:hover, .pk-player-box.plist-active .pk-p-plist-tab, .pk-img-box.plist-active .pk-p-plist-tab { opacity: 1; } .pk-p-plist-tab::before { content: ''; position: absolute; inset: 0; z-index: -1; left: -50px; right: -50px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='20' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20 Z' fill='rgba(20, 20, 20, 0.9)'/%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20' fill='none' stroke='rgba(255,255,255,0.1)' stroke-width='1' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E"); background-size: 100% 100%; background-repeat: no-repeat; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); transform: translateZ(0); backface-visibility: hidden; -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='20' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20 Z' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='20' viewBox='0 0 100 20' preserveAspectRatio='none'%3E%3Cpath d='M0 20 C 25 20, 25 0, 40 0 H 60 C 75 0, 75 20, 100 20 Z' fill='black'/%3E%3C/svg%3E"); -webkit-mask-size: 100% 100%; mask-size: 100% 100%; } #pk_p_box:fullscreen, #pk_p_box:-webkit-full-screen, #pk_p_box:-moz-full-screen { width: 100vw !important; height: 100vh !important; top: 0 !important; left: 0 !important; transform: none !important; margin: 0 !important; border-radius: 0 !important; overflow: hidden !important; } #pk_p_box:fullscreen #pk_video, #pk_p_box:-webkit-full-screen #pk_video, #pk_p_box.full #pk_video, #pk_p_box:fullscreen #pk_p_poster, #pk_p_box:-webkit-full-screen #pk_p_poster, #pk_p_box.full #pk_p_poster { height: 100% !important; bottom: auto !important; top: 0 !important; transform: translateZ(0); } #pk_p_box:fullscreen.plist-active #pk_video, #pk_p_box:-webkit-full-screen.plist-active #pk_video, #pk_p_box.full.plist-active #pk_video, #pk_p_box:fullscreen.plist-active #pk_p_poster, #pk_p_box:-webkit-full-screen.plist-active #pk_p_poster, #pk_p_box.full.plist-active #pk_p_poster { height: calc(100% - 84px) !important; } #pk_p_box:fullscreen.plist-active .pk-p-side-nav, #pk_p_box:fullscreen.plist-active .pk-p-center-play, #pk_p_box:fullscreen.plist-active .pk-p-seek-indicator, #pk_p_box:-webkit-full-screen.plist-active .pk-p-side-nav, #pk_p_box:-webkit-full-screen.plist-active .pk-p-center-play, #pk_p_box:-webkit-full-screen.plist-active .pk-p-seek-indicator { top: calc(50% - 42px) !important; } #pk_p_box:fullscreen #pk_p_plist, #pk_p_box:-webkit-full-screen #pk_p_plist { top: auto !important; bottom: 0 !important; transform: translateY(100%) translateZ(0); backface-visibility: hidden; perspective: 1000px; transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); } #pk_p_box:fullscreen .pk-p-plist-tab, #pk_p_box:-webkit-full-screen .pk-p-plist-tab { bottom: auto !important; top: 0 !important; transform: translateY(-100%) translateZ(0) !important; margin-top: 1px !important; margin-bottom: 0 !important; backface-visibility: hidden; z-index: 100 !important; } #pk_p_box:fullscreen.plist-active #pk_p_plist, #pk_p_box:-webkit-full-screen.plist-active #pk_p_plist { transform: translateY(0); } #pk_p_box:fullscreen.plist-active .pk-player-controls, #pk_p_box:-webkit-full-screen.plist-active .pk-player-controls { bottom: 84px !important; } #pk_p_box:fullscreen.plist-active .pk-p-prog-wrap, #pk_p_box:-webkit-full-screen.plist-active .pk-p-prog-wrap { bottom: 148px !important; } .pk-p-plist-tab:hover { color: rgba(255, 255, 255, 0.8); } .pk-p-plist-tab:hover::before { opacity: 1; filter: none; } .pk-p-plist-tab svg { transition: transform 0.3s; } .pk-p-plist-ov.open .pk-p-plist-tab svg { transform: rotate(180deg); } .pk-p-plist-strip { height: 110px; background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); display: flex; align-items: center; position: relative; border-top: 1px solid rgba(255, 255, 255, 0.1); } .pk-p-plist-scroll { flex: 1; display: flex; overflow-x: auto; gap: 2px; padding: 0 50px; height: 100%; align-items: center; } .pk-p-plist-scroll::-webkit-scrollbar { display: none; } .pk-p-plist-item { flex-shrink: 0; width: 140px; height: 80px; background: #333; cursor: pointer; position: relative; overflow: hidden; border: 2px solid transparent; border-radius: 4px; will-change: opacity, transform; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .pk-p-plist-item.active { border-color: var(--pk-pri); box-shadow: 0 0 10px var(--pk-pri); } .pk-p-plist-item img { width: 100%; height: 100%; object-fit: cover; opacity: 1; transition: opacity 0.3s ease-in-out; } .pk-p-plist-item img:error { opacity: 0 !important; } .pk-p-plist-nav { position: absolute; top: 0; bottom: 0; width: 50px; background: transparent !important; color: rgba(255, 255, 255, 0.7); cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 35; transition: all 0.2s; border: none; } .pk-p-plist-nav:hover { background: rgba(255, 255, 255, 0.15) !important; color: #fff; } .pk-p-plist-nav.L { left: 0; } .pk-p-plist-nav.R { right: 0; } .pk-p-plist-nav svg { width: 28px; height: 28px; stroke-width: 3; } .pk-p-plist-tip { position: fixed; background: rgba(0, 0, 0, 0.9); color: #fff; padding: 8px 12px; border-radius: 6px; font-size: 12px; pointer-events: none; z-index: 2147483647; max-width: 340px; line-height: 1.4; display: none; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-p-resume-btn { color: #4aa1ff; cursor: pointer; font-weight: bold; text-decoration: none; } .pk-p-resume-btn:hover { text-decoration: underline; } .pk-p-resume-close { cursor: pointer; color: #888; margin-left: 4px; display: flex; align-items: center; } @keyframes pkFadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .pk-img-ov { position: fixed; top: 0; left: 0; width: calc(100vw / var(--pk-zoom, 1)); height: calc(100vh / var(--pk-zoom, 1)); zoom: var(--pk-zoom, 1); transform-origin: top left; z-index: 2147483640; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; outline: none; user-select: none; } .pk-img-box { position: absolute !important; top: 10%; left: 50%; transform: translateX(-50%); background: #000; border-radius: 8px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); overflow: visible !important; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 90%; max-width: calc(1600px / var(--pk-zoom, 1)); min-width: 480px; height: 80%; transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1), height 0.2s cubic-bezier(0.4, 0, 0.2, 1), border-radius 0.2s, top 0.2s, left 0.2s, transform 0.2s; z-index: 10; box-sizing: border-box; border: none; } .pk-img-box.plist-active { height: calc(80% - 84px); border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .pk-img-box.full { width: 100%; max-width: none; height: 100%; border-radius: 0; top: 0 !important; left: 0 !important; transform: none !important; } .pk-img-box.full.plist-active { height: calc(100vh - 84px); } .pk-img-obj { flex: 1; width: 100%; height: 100%; min-height: 0; object-fit: contain; cursor: grab; transition: transform 0.1s linear; transform-origin: center center; } #pk_img_plist { position: absolute; top: 100%; left: 0; right: 0; z-index: 25; height: 84px; display: flex; flex-direction: column; pointer-events: none; } #pk_img_plist .pk-p-plist-strip { pointer-events: none; opacity: 0; transition: opacity 0.2s; } .pk-img-box.plist-active #pk_img_plist .pk-p-plist-strip { display: flex !important; opacity: 1 !important; pointer-events: auto !important; animation: pkFadeInOnly 0.2s ease; } #pk_img_plist_tab { pointer-events: auto !important; z-index: 30; cursor: pointer; } #pk_img_plist_scroll { pointer-events: auto !important; } .pk-img-obj:active { cursor: grabbing; } .pk-img-bar { position: absolute; top: 0; left: 0; right: 0; height: 50px; background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; z-index: 20; opacity: 0; transition: opacity 0.3s; pointer-events: none; } .pk-img-bar > * { pointer-events: auto; } .pk-img-box:hover .pk-img-bar { opacity: 1; } .pk-img-title { color: #fff; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: bold; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); } .pk-img-actions { display: flex; gap: 10px; } .pk-img-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 50px; height: 100px; display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, 0.5); cursor: pointer; z-index: 5; transition: background 0.2s, color 0.2s; border-radius: 4px; opacity: 0; } .pk-img-box:hover .pk-img-nav { opacity: 1; } .pk-img-nav:hover { background: rgba(0, 0, 0, 0.3); color: #fff; } .pk-img-prev { left: 10px; } .pk-img-next { right: 10px; } .pk-img-nav svg { width: 36px; height: 36px; stroke-width: 3; } .pk-img-btn { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; color: #eee; cursor: pointer; border-radius: 50%; background: transparent; transition: all 0.2s ease; flex-shrink: 0; } .pk-img-btn:not(#pk_img_close):hover { color: var(--pk-pri) !important; background: transparent !important; } #pk_img_close:hover { background: rgba(255, 255, 255, 0.15) !important; color: #fff !important; } .pk-img-btn svg { width: 20px; height: 20px; } .pk-tag-default { margin-top: -1px; margin-left: 10px; flex-shrink: 0; min-width: 32px; box-sizing: border-box; font-size: 10px; height: 18px; padding: 1px 6px 0 6px !important; border-radius: 20px; font-weight: normal; white-space: nowrap; cursor: default; display: inline-flex; align-items: center; justify-content: center; user-select: none; background-color: transparent; color: #999; border: 1px solid #ccc; } .pk-ov.pk-dark .pk-tag-default { background-color: transparent; color: #888; border-color: #555; text-shadow: none; } #pk-scan-dup, #pk-btn-exit { align-items: center !important; margin: 0 !important; flex-shrink: 0 !important; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .pk-status-dot::after { content: ''; position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; background: #1a5eff; border-radius: 50%; border: 2px solid var(--pk-bg); box-shadow: 0 0 5px rgba(26, 94, 255, 0.5); animation: pk-pulse 2s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; z-index: 11; pointer-events: none; } @keyframes pk-pulse { 0% { transform: scale(0.9); opacity: 0.6; box-shadow: 0 0 0 0 rgba(26, 94, 255, 0.7); } 50% { transform: scale(1.1); opacity: 1; box-shadow: 0 0 0 4px rgba(26, 94, 255, 0); } 100% { transform: scale(0.9); opacity: 0.6; box-shadow: 0 0 0 0 rgba(26, 94, 255, 0); } } .pk-tooltip { position: fixed; z-index: 2147483647 !important; background: var(--pk-tip-bg); color: var(--pk-tip-fg); border: 1px solid var(--pk-tip-bd); padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 500; line-height: 1.4; pointer-events: none; opacity: 0; transform: translateY(5px) scale(0.95); transition: opacity 0.15s ease, transform 0.15s ease; box-shadow: 0 4px 16px var(--pk-tip-sd); backdrop-filter: blur(4px); max-width: 300px; white-space: pre-wrap; } .pk-tooltip.show { opacity: 1; transform: translateY(0) scale(1); } .pk-drag-ghost { position: fixed; z-index: 2147483647; background: var(--pk-bg); border: 1px solid var(--pk-pri); color: var(--pk-fg); padding: 8px 12px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); pointer-events: none; font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 8px; opacity: 0.9; transform: translate(15px, 15px); } .pk-row.pk-drop-target { background: var(--pk-sel-bg) !important; outline: 2px dashed var(--pk-pri); outline-offset: -2px; z-index: 10; } #pk-crumb span.pk-drop-target { background: var(--pk-sel-bg) !important; color: var(--pk-pri) !important; outline: 2px dashed var(--pk-pri); outline-offset: -2px; border-radius: 4px; z-index: 10; opacity: 1 !important; } .pk-crumb-item.pk-drop-target { background: var(--pk-sel-bg) !important; color: var(--pk-pri) !important; outline: 2px dashed var(--pk-pri); outline-offset: -2px; } .pk-empty { position: absolute; inset: 0; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; padding-bottom: 10vh; z-index: 1; opacity: 0; animation: pkFadeInOnly 0.4s ease forwards; } .pk-empty svg { width: 35vmin; max-width: 220px; height: auto; margin-bottom: 3vmin; filter: drop-shadow(0 15px 25px rgba(0, 0, 0, 0.05)); } .pk-empty-txt { font-size: clamp(13px, 3vmin, 16px); color: #94a3b8; font-weight: 500; letter-spacing: 1px; } .pk-ov.pk-dark .pk-empty-txt { color: #666e75; } .pk-selection-box { position: fixed; inset: 0 auto auto 0; background: rgba(0, 103, 192, 0.1); border: 1px solid rgba(0, 103, 192, 0.4); z-index: 2147483647 !important; pointer-events: none; display: none; will-change: transform; box-sizing: border-box; } .pk-p-vol-indicator { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.75); color: #fff; padding: 12px 24px; border-radius: 12px; font-size: 20px; font-weight: bold; z-index: 120; display: none; align-items: center; gap: 10px; backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.15); pointer-events: none; font-family: "Inter", sans-serif; } .pk-p-vol-indicator svg { fill: #fff !important; width: 100%; height: 100%; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); } .pk-is-vol-active .pk-p-center-play, .pk-is-vol-active .pk-p-loading { display: none !important; opacity: 0 !important; } .pk-input-err-msg { color: #ff4d4f; font-size: 12px; margin-top: 8px; min-height: 18px; visibility: hidden; transition: opacity 0.2s; } .pk-ana-select { position: relative; width: 85px; flex-shrink: 0; } #an_val_min::-webkit-outer-spin-button, #an_val_min::-webkit-inner-spin-button, #an_val_max::-webkit-outer-spin-button, #an_val_max::-webkit-inner-spin-button, #sc_val_min::-webkit-outer-spin-button, #sc_val_min::-webkit-inner-spin-button, #sc_val_max::-webkit-outer-spin-button, #sc_val_max::-webkit-inner-spin-button, #sh_mod_cnt_val::-webkit-outer-spin-button, #sh_mod_cnt_val::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } #an_val_min, #an_val_max, #sc_val_min, #sc_val_max, #sh_mod_cnt_val { -moz-appearance: textfield; } #an_val_min, #an_val_max { padding-right:32px !important; } .pk-num-ctrl { position: absolute; right: 8px; top: 4px; bottom: 4px; display: flex; flex-direction: column; width: 24px; gap: 1px; z-index: 5; } .pk-num-btn { flex: 1; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-icon-c); border-radius: 3px; transition: all 0.1s; } .pk-num-btn:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-num-btn:active { transform: scale(0.9); } .pk-num-btn svg { width: 14px; height: 14px; stroke-width: 3; } .pk-ana-trigger { width: 100%; height: 42px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; border: 2px solid var(--pk-bd); border-radius: 8px; background: var(--pk-bg); color: var(--pk-fg); cursor: pointer; font-weight: 700; font-size: 14px; transition: border-color 0.2s; box-sizing: border-box; } .pk-ana-trigger:hover { border-color: var(--pk-pri); } .pk-ana-menu { position: absolute; top: 100%; left: 0; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; margin-top: 6px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); z-index: 10010; display: none; overflow: hidden; } .pk-ana-item { padding: 10px 12px; cursor: pointer; font-size: 13px; color: var(--pk-fg); transition: background 0.1s; } .pk-ana-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-ana-item.act { color: var(--pk-pri); font-weight: 700; background: rgba(0, 103, 192, 0.05); } .pk-msg-toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--pk-toast-bg); color: var(--pk-toast-fg); padding: 12px 28px; border-radius: 12px; z-index: 20000; font-size: 14px; font-weight: 600; pointer-events: none; opacity: 0; transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); text-align: left; display: flex; align-items: center; justify-content: center; gap: 12px; backdrop-filter: blur(8px); border: 1px solid var(--pk-toast-bd); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .pk-msg-toast.show { opacity: 1; transform: translate(-50%, -60%); } .pk-crumb-sep { width: 22px; height: 22px; margin: 0 2px; cursor: pointer; border-radius: 4px; color: #aaa; transition: all 0.2s; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } .pk-crumb-sep:hover, .pk-crumb-sep.pk-active { background: var(--pk-hl); color: currentColor; } .pk-crumb-sep svg { width: 14px; height: 14px; stroke-width: 3.5; transition: transform 0.2s ease; } .pk-crumb-pop { position: fixed; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); z-index: 2147483647 !important; min-width: 180px; max-width: 320px; max-height: 50vh; overflow-y: auto; padding: 6px 0; opacity: 0; pointer-events: none; transition: opacity 0.1s ease; border-top: 1px solid rgba(255, 255, 255, 0.05); } .pk-crumb-pop.pk-show { opacity: 1; pointer-events: auto; } .pk-crumb-item { padding: 8px 12px; font-size: 13px; line-height: 1.5; cursor: pointer; display: flex; align-items: center; gap: 8px; color: var(--pk-fg); } .pk-crumb-item:hover { background: var(--pk-hl); } .pk-crumb-item svg { flex-shrink: 0; } .pk-crumb-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; padding-bottom: 2px; margin-bottom: -2px; } @keyframes pkFadeInOnly { from { opacity: 0; } to { opacity: 1; } } .pk-share-modal { width: 540px !important; box-sizing: border-box; display: flex; flex-direction: column; gap: 20px; padding: 0 !important; overflow: visible !important; } .pk-s-sec { display: flex; flex-direction: column; gap: 12px; padding: 0 28px; box-sizing: border-box; } .pk-detail-cancel-btn { cursor: pointer; color: var(--pk-pri); font-size: 15px; padding: 8px 12px; border-radius: 6px; transition: background 0.2s; margin-left: -12px; } .pk-detail-cancel-btn:hover { background: var(--pk-hl); } .pk-share-stat-box { display: flex; background: var(--pk-hl); border-radius: 10px; padding: 10px 0; margin-bottom: 18px; border: 1px solid var(--pk-bd); } .pk-share-stat-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; } .pk-share-stat-item:first-child::after { content: ''; position: absolute; right: 0; top: 25%; bottom: 25%; width: 1px; background: var(--pk-bd); } .pk-share-stat-val { font-size: 20px; font-weight: 700; color: var(--pk-fg); line-height: 1.1; font-family: "Inter", system-ui, sans-serif; } .pk-share-stat-lbl { font-size: 11px; color: #888; margin-top: 2px; font-weight: 500; } .pk-s-lbl { font-size: 13px; font-weight: 700; color: var(--pk-fg); opacity: 0.6; text-transform: uppercase; letter-spacing: 0.5px; } .pk-s-tabs { display: flex; background: var(--pk-hl); border-radius: 8px; padding: 4px; gap: 4px; border: 1px solid var(--pk-bd); } .pk-s-tab { flex: 1; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-fg); font-size: 14px; border-radius: 6px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0.7; } .pk-s-tab.act { background: var(--pk-bg); color: var(--pk-pri); opacity: 1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); font-weight: 700; } .pk-s-opts { display: flex; flex-flow: row nowrap; gap: 20px; align-items: center; justify-content: flex-start; } .pk-s-opt { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; color: var(--pk-fg); white-space: nowrap; flex-shrink: 0; } .pk-s-lbl { font-size: 13px; font-weight: 600; color: var(--pk-fg); opacity: 0.8; } .pk-s-tabs { display: flex; background: var(--pk-hl); border-radius: 6px; padding: 3px; gap: 2px; border: 1px solid var(--pk-bd); } .pk-s-tab { flex: 1; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-fg); font-size: 13px; border-radius: 4px; transition: all 0.2s; opacity: 0.7; } .pk-s-tab.act { background: var(--pk-bg); color: var(--pk-pri); opacity: 1; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); font-weight: bold; } .pk-s-opts { display: flex; flex-wrap: nowrap; gap: 12px; align-items: center; justify-content: space-between; } .pk-s-opt { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; color: var(--pk-fg); white-space: nowrap; flex-shrink: 0; } .pk-s-opt input[type="radio"] { appearance: none; width: 16px; height: 16px; border: 1px solid var(--pk-icon-c); border-radius: 50%; position: relative; cursor: pointer; margin: 0; background: var(--pk-bg); transition: all 0.2s; } .pk-s-opt input:checked { border-color: var(--pk-pri); border-width: 5px; } .pk-s-input { flex: 1; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 4px; color: var(--pk-fg); padding: 5px 10px; font-size: 13px; outline: none; transition: border-color 0.2s; min-width: 0; } .pk-s-input:focus { border-color: var(--pk-pri); } .pk-s-input:disabled { opacity: 0.3; cursor: not-allowed; background: var(--pk-hl); } .pk-share-res-val { font-family: "SF Mono", "Consolas", monospace; font-weight: 600; color: var(--pk-pri) !important; } .pk-cal-pop { position: absolute; background: var(--pk-bg); color: var(--pk-fg); border: 1px solid var(--pk-bd); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); border-radius: 8px; z-index: 10005; display: flex; font-size: 13px; overflow: hidden; animation: pkFadeIn 0.2s; user-select: none; } .pk-cal-side { width: 110px; background: var(--pk-hl); border-right: 1px solid var(--pk-bd); display: flex; flex-direction: column; padding: 8px 0; } .pk-cal-side-item { padding: 8px 16px; cursor: pointer; color: var(--pk-fg); transition: background 0.2s; } .pk-cal-side-item:hover { background: rgba(0, 0, 0, 0.05); } .pk-cal-side-item.active { color: var(--pk-pri); font-weight: bold; background: var(--pk-bg); } .pk-cal-main { width: 280px; padding: 10px; display: flex; flex-direction: column; } .pk-cal-hd { display: flex; justify-content: space-between; align-items: center; padding: 0 8px 10px 8px; border-bottom: 1px solid var(--pk-bd); } .pk-cal-nav-btn { cursor: pointer; padding: 4px; border-radius: 4px; color: #888; } .pk-cal-nav-btn:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-top: 10px; } .pk-cal-th { text-align: center; color: #888; font-size: 12px; padding-bottom: 6px; } .pk-cal-td { height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; transition: all 0.2s; position: relative; color: var(--pk-fg); } .pk-cal-td:hover:not(.disabled):not(.selected) { background: var(--pk-hl); color: var(--pk-pri); } .pk-cal-td.selected { background: var(--pk-pri); color: #fff; font-weight: bold; } .pk-cal-td.disabled { color: #888; cursor: not-allowed; opacity: 0.7; } .pk-share-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; padding: 16px 24px 24px 24px; border-top: 1px solid var(--pk-bd); background: transparent; } .pk-btn-quiet-red { color: #d93025; font-size: 14px; font-weight: 500; cursor: pointer; padding: 8px 12px; border-radius: 6px; transition: background 0.2s; margin-left: -8px; } .pk-btn-quiet-red:hover { background: rgba(217, 48, 37, 0.08); } .pk-btn-primary-action { background: var(--pk-pri); color: #fff; border: none; height: 38px; padding: 0 20px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .pk-btn-primary-action:hover { filter: brightness(1.08); box-shadow: 0 2px 8px rgba(0, 103, 192, 0.25); } .pk-cal-td.today::after { content: ''; position: absolute; bottom: 4px; width: 4px; height: 4px; background: currentColor; border-radius: 50%; opacity: 0.5; } .pk-share-icon-wrap { position: relative; width: 30px; height: 30px; margin-right: 12px; flex-shrink: 0; transition: transform 0.2s ease; transform-origin: center center; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; } .pk-share-lock { position: absolute; bottom: -2px; right: -4px; width: 14px; height: 14px; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 10; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); border: none; box-shadow: none; pointer-events: none; } .pk-ov.pk-dark .pk-share-lock { filter: drop-shadow(0 0 2px rgba(0, 0, 0, 1)) drop-shadow(0 2px 5px rgba(0, 0, 0, 1)); } .pk-name { display: flex !important; align-items: center; min-width: 0; width: 100%; overflow: hidden; } .pk-name-txt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .pk-select-item { padding: 10px 12px; border-radius: 5px; cursor: pointer; color: var(--pk-fg); font-size: 14px; transition: background 0.1s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-select-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-select-item.act { background: rgba(0, 103, 192, 0.1); color: var(--pk-pri); font-weight: 700; } .pk-select-label { position: absolute; top: 0; transform: translate3d(0, -50%, 0); -webkit-transform: translate3d(0, -50%, 0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; left: 10px; background: var(--pk-bg); padding: 0 5px; font-size: 11px; color: var(--pk-pri); font-weight: bold; pointer-events: none; z-index: 10; line-height: 1; pointer-events: none; } .pk-field input, .pk-field select, .pk-share-modal input, .pk-ov input { transform: translateZ(0); will-change: transform; } .pk-maximized { position: fixed !important; width: 100% !important; height: 100% !important; max-width: none !important; top: 0 !important; left: 0 !important; border-radius: 0 !important; border: none !important; z-index: 2147483647 !important; } .pk-maximized .pk-sidebar { width: 190px !important; align-items: flex-start !important; padding: 20px 10px !important; } .pk-maximized .pk-nav-btn { width: 100% !important; justify-content: flex-start !important; padding: 0 15px !important; gap: 10px; height: 54px !important; border-radius: 8px !important; } .pk-nav-btn span { display: none; font-size: 14px; font-weight: 600; white-space: nowrap; } .pk-maximized .pk-nav-btn span { display: inline-block; } .pk-maximized #pk-quota-panel { align-items: flex-start !important; padding: 0 10px !important; margin-bottom: 6px !important; opacity: 0.9 !important; gap: 4px !important; transition: none !important; } .pk-cloud-area { width: 100%; height: 180px; background: #f1f3f5; border: none; border-radius: 8px; padding: 15px; font-size: 14px; line-height: 1.6; color: #1a1a1a; resize: none; outline: none; font-family: inherit; cursor: auto; } .pk-dark .pk-cloud-area { background: #2d2d2d !important; color: #f5f5f5 !important; caret-color: #fff !important; } .pk-dark .pk-cloud-area::placeholder { color: #666 !important; } .pk-cloud-area::placeholder { color: #adb5bd; } #pk_cloud_torrent_trigger:hover, #pk_cloud_change_dir:hover { text-decoration: underline; } .pk-maximized #pk-quota-bar-box { width: 100% !important; height: 6px !important; transition: none !important; border-radius: 3px !important; } .pk-maximized #pk-quota-txt { font-size: 12px !important; transform: none !important; opacity: 1; font-weight: normal !important; white-space: nowrap; } .pk-maximized .pk-hd { height: 60px !important; padding: 0 20px !important; } .pk-maximized .pk-tt { font-size: 24px !important; } .pk-maximized .pk-tt svg { width: 32px !important; height: 32px !important; } .pk-maximized .pk-tb { height: 60px !important; padding: 0 16px !important; gap: 10px !important; } .pk-maximized .pk-btn { height: 40px !important; font-size: 15px !important; padding: 0 16px !important; } .pk-btn svg { width: auto; height: auto; } #pk-theme svg, #pk-maximize svg { width: 16px !important; height: 16px !important; } #pk-help svg { width: 19px !important; height: 19px !important; } #pk-close svg { width: 19px !important; height: 19px !important; } .pk-maximized #pk-theme svg, .pk-maximized #pk-maximize svg { width: 24px !important; height: 24px !important; } .pk-maximized #pk-help svg { width: 26px !important; height: 29px !important; } .pk-maximized #pk-close svg { width: 28px !important; height: 28px !important; } .pk-maximized .pk-grid-hd { height: 50px !important; font-size: 15px !important; padding: 0 26px 0 20px !important; } .pk-maximized .pk-row, .pk-maximized .pk-group-hd { height: 60px !important; font-size: 16px !important; padding: 0 20px !important; } .pk-maximized .pk-group-hd { border-top-width: 10px !important; border-bottom-width: 10px !important; } .pk-maximized .pk-name svg { width: 60px !important; height: 60px !important; margin-right: 20px !important; } .pk-maximized .pk-row .pk-name .pk-name-txt { white-space: normal !important; display: -webkit-box !important; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden !important; text-overflow: ellipsis !important; line-height: 1.35 !important; word-break: break-all !important; margin-top: 0 !important; } .pk-max-icon-box { position: relative !important; width: 50px !important; height: 50px !important; margin-right: 20px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; flex-shrink: 0 !important; vertical-align: middle !important; overflow: visible !important; } .pk-placeholder-icon { position: absolute !important; inset: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; z-index: 1 !important; transition: opacity 0.2s, visibility 0.2s; } .pk-placeholder-icon svg { width: 44px !important; height: 44px !important; object-fit: contain !important; } .pk-max-icon-box .pk-max-thumb { position: absolute !important; top: 0 !important; left: 0 !important; width: 50px !important; height: 50px !important; object-fit: cover !important; border-radius: 4px !important; opacity: 0; z-index: 2 !important; transition: opacity 0.25s ease-in-out; background: transparent; } .pk-maximized .pk-share-icon-wrap { width: 50px !important; height: 50px !important; margin-right: 20px !important; background: transparent !important; overflow: visible !important; } .pk-maximized .pk-row:has(.pk-max-thumb) .pk-name-txt { padding-left: 0 !important; } .pk-maximized .pk-nav-btn svg { width: 28px !important; height: 28px !important; } body:not(.pk-body-max):has(.pk-modal-ov) #pk-launch { filter: grayscale(1) brightness(0.6) !important; opacity: 0.5 !important; pointer-events: none !important; cursor: not-allowed !important; transition: filter 0.3s, opacity 0.3s; } body:has(.pk-ov:not([style*="display: none"])), body:has(.pk-img-ov), body:has(#pk-player-ov) { #pk-launch { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } } .pk-maximized .pk-search input { height: 40px !important; font-size: 15px !important; padding: 0 70px 0 15px !important; } .pk-maximized #pk-search-btn { width: 18px !important; height: 18px !important; right: 14px !important; } .pk-maximized .pk-search-clear { width: 24px !important; height: 24px !important; right: 40px !important; } .pk-maximized input[type="checkbox"] { width: 18px !important; height: 18px !important; transform: none !important; margin: 0 8px 0 0 !important; } .pk-maximized .pk-star-icon, .pk-maximized .pk-star-toggle, .pk-maximized .pk-col[data-k="starred"] svg { width: 16px !important; height: 16px !important; } .pk-maximized .pk-name { overflow: visible !important; } .pk-maximized .pk-share-icon-wrap { width: 40px !important; height: 40px !important; margin-right: 16px !important; position: relative !important; display: block !important; overflow: visible !important; } .pk-maximized .pk-share-icon-wrap { width: 50px !important; height: 50px !important; margin-right: 20px !important; display: flex !important; align-items: center !important; justify-content: center !important; transform: none !important; overflow: visible !important; } .pk-maximized .pk-share-icon-wrap > svg { width: 100% !important; height: 100% !important; min-width: 100% !important; min-height: 100% !important; transform: scale(1.25) !important; transform-origin: center center !important; margin: 0 !important; } .pk-maximized .pk-share-icon-wrap > div:not(.pk-share-lock) { width: 100% !important; height: 100% !important; display: flex !important; align-items: center !important; justify-content: center !important; transform: none !important; margin: 0 !important; } .pk-maximized .pk-share-icon-wrap > div:not(.pk-share-lock) > svg { width: 100% !important; height: 100% !important; min-width: 100% !important; min-height: 100% !important; transform: scale(1.25) !important; transform-origin: center center !important; margin: 0 !important; } .pk-maximized .pk-share-icon-wrap img { width: 100% !important; height: 100% !important; min-width: 100% !important; min-height: 100% !important; object-fit: contain !important; transform: none !important; } .pk-maximized .pk-share-lock { width: 18px !important; height: 18px !important; bottom: -2px !important; right: -12px !important; z-index: 15 !important; } .pk-maximized .pk-row > div { font-size: 16px !important; font-weight: normal !important; } .pk-maximized .pk-row div[style*="font-size:12px"], .pk-maximized .pk-row div[style*="font-size: 12px"] { font-size: 15px !important; font-weight: 400 !important; opacity: 0.9; } .pk-maximized .pk-row div[style*="tabular-nums"] { font-weight: 400 !important; } .pk-maximized .pk-share-lock svg { width: 100% !important; height: 100% !important; color: #fff !important; } .pk-maximized .pk-ft { height: 50px !important; font-size: 15px !important; padding: 0 20px !important; } .pk-maximized .pk-stat { font-size: 15px !important; } .pk-maximized .pk-ft .pk-btn { height: 36px !important; font-size: 15px !important; } .pk-maximized #pk-filter-cat-label { height: 40px !important; font-size: 15px !important; padding: 0 16px !important; } .pk-maximized #pk-filter-exts-wrap { height: 40px !important; } .pk-maximized .pk-f-ext { font-size: 15px !important; padding: 6px 12px !important; } .pk-maximized #pk-filter-exit-btn { height: 40px !important; font-size: 15px !important; padding: 0 20px !important; } .pk-maximized .pk-bl-area { font-size: 15px !important; line-height: 1.6 !important; } .pk-maximized #pk-crumb span { font-size: 16px !important; display: inline-flex !important; align-items: center !important; height: 32px !important; padding: 0 8px !important; border-radius: 4px !important; margin: auto 2px !important; } .pk-maximized #pk-crumb div span { font-size: 18px !important; } .pk-maximized #pk-crumb div span[style*="font-size:11px"], .pk-maximized #pk-crumb div span[style*="font-size: 11px"] { font-size: 14px !important; margin-left: 12px !important; } .pk-maximized #pk-crumb svg { width: 18px !important; height: 18px !important; vertical-align: middle !important; margin-top: -1px !important; } .pk-maximized .pk-crumb-sep { width: 26px !important; height: 26px !important; margin: auto 4px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; } .pk-maximized .pk-crumb-sep svg { width: 20px !important; height: 20px !important; } .pk-custom-select { position: relative; width: 100%; } .pk-select-trigger { display: flex; align-items: center; justify-content: space-between; height: 44px; padding: 0 15px; border: 2px solid var(--pk-bd); border-radius: 8px; background: var(--pk-bg); color: var(--pk-fg); font-size: 14px; font-weight: 600; cursor: pointer; box-sizing: border-box; transition: all 0.2s; } .pk-select-trigger:hover { border-color: var(--pk-pri); } .pk-select-trigger svg { width: 16px !important; height: 16px !important; min-width: 16px; min-height: 16px; color: #999; flex-shrink: 0; } .pk-dropdown-wrap { position: relative; display: inline-flex; height: 32px; align-items: center; } .pk-dropdown-menu { position: absolute; top: 100%; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); display: none; z-index: 10010; min-width: 140px; padding: 4px 0; margin-top: 6px; flex-direction: column; overflow: hidden; } .pk-dropdown-item { padding: 10px 16px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--pk-fg); font-size: 13px; transition: background 0.1s; white-space: nowrap; } .pk-dropdown-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-pop-max { width: 170px !important; min-width: 170px !important; padding: 6px 0 !important; border-radius: 10px !important; box-shadow: 0 8px 30px rgba(0,0,0,0.3) !important; } .pk-pop-max .pk-dropdown-item { padding: 12px 15px !important; font-size: 15px !important; gap: 10px !important; font-weight: 600 !important; } .pk-pop-max .pk-dropdown-item svg { width: 22px !important; height: 22px !important; } .pk-btn-arrow { margin-left: 2px; opacity: 0.6; transition: transform 0.2s; } .pk-aria-status-box { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: bold; margin-top: 6px; transform: translateZ(0); -webkit-transform: translateZ(0); backface-visibility: hidden; will-change: transform; cursor: default; } .pk-aria-dot { width: 8px; height: 8px; border-radius: 50%; background: #ccc; transform: translateZ(0); } .pk-aria-dot.ok { background: #52c41a; box-shadow: 0 0 8px rgba(82, 196, 26, 0.5); } .pk-aria-dot.err { background: #ff4d4f; } .pk-aria-dot.wait { background: var(--pk-pri); animation: pk-pulse 1.5s infinite; } .pk-dropdown-wrap.active .pk-btn-arrow { transform: rotate(180deg); } .pk-select-menu { position: absolute; top: 100%; left: 0; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; margin-top: 6px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); z-index: 10010; display: none; overflow: hidden; max-height: 240px; } .pk-share-icon-wrap { position: relative; width: 30px; height: 30px; margin-right: 12px; flex-shrink: 0; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; } .pk-share-lock { position: absolute; bottom: -3px; right: -6px; width: 17px; height: 17px; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 10; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); border: none; box-shadow: none; pointer-events: none; } .pk-ov.pk-dark .pk-share-lock { filter: drop-shadow(0 0 2px rgba(0, 0, 0, 1)) drop-shadow(0 2px 5px rgba(0, 0, 0, 1)); } .pk-bl-marker { position: absolute !important; bottom: 0px !important; left: 4px !important; width: 10px !important; height: 10px !important; display: flex !important; align-items: center; justify-content: center; z-index: 99 !important; pointer-events: none; filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 2px rgba(255,255,255,0.5)); } .pk-maximized .pk-bl-marker { width: 18px !important; height: 18px !important; bottom: -1px !important; left: 9px !important; } .pk-min-icon, .pk-max-icon-box { position: relative !important; overflow: visible !important; display: inline-flex !important; } .pk-active-border { border-color: var(--pk-pri) !important; } #pk_dl_group { transform: translateZ(0); backface-visibility: hidden; will-change: border-color; } #pk_dl_group:hover { border-color: var(--pk-pri) !important; } #pk_dl_group.pk-typing-active:hover { border-color: var(--pk-bd) !important; } .pk-row > div.pk-name, .pk-min-icon, .pk-max-icon-box, .pk-min-media-box { overflow: visible !important; } .pk-maximized .pk-row .pk-name>img[style*="width:24px"] { width: 48px !important; height: 48px !important; margin-right: 20px !important; margin-left: -4px !important; } .pk-maximized .pk-row .pk-name>div[style*="width:24px"] { width: 48px !important; height: 48px !important; margin-right: 20px !important; margin-left: -4px !important; } .pk-maximized .pk-row .pk-name>div[style*="width:24px"] svg { width: 36px !important; height: 36px !important; } .pk-maximized .pk-row div[style*="width:100px"] { width: 200px !important; height: 8px !important; } .pk-min-icon-box { position: relative; width: 24px; height: 24px; margin-right: 12px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; } .pk-min-placeholder { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 1; transition: opacity 0.2s; } .pk-min-thumb { position: absolute; inset: 0; width: 24px; height: 24px; object-fit: cover; border-radius: 4px; z-index: 2; opacity: 0; transition: opacity 0.2s; } .pk-drag-mask { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.92); z-index: 9999; display: none; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; backdrop-filter: blur(4px); } .pk-dark .pk-drag-mask { background: rgba(32, 32, 32, 0.92); } .pk-drag-icon { width: 80px; height: 80px; background: var(--pk-pri); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; margin-bottom: 24px; box-shadow: 0 10px 30px rgba(26, 94, 255, 0.3); } .pk-drag-hint { font-size: 20px; font-weight: 700; color: var(--pk-fg); margin-bottom: 12px; } .pk-drag-path { font-size: 14px; color: #888; display: flex; align-items: center; gap: 6px; } .pk-help-scroll { max-height: 380px; overflow-y: auto; overscroll-behavior: contain; } .pk-body-max .pk-help-scroll { max-height: 75vh; } body.pk-hide-all-ui #pk-launch, body.pk-hide-all-ui .pk-ov, body.pk-hide-all-ui .pk-modal-ov, body.pk-hide-all-ui .pk-img-ov, body.pk-hide-all-ui #pk-player-ov, body.pk-hide-all-ui .pk-cal-pop, body.pk-hide-all-ui .pk-crumb-pop, body.pk-hide-all-ui .pk-hist-pop, body.pk-hide-all-ui .pk-tooltip, body.pk-hide-all-ui .pk-msg-toast, body.pk-hide-all-ui .pk-float-bar-item, body.pk-hide-all-ui .pk-selection-box, body.pk-hide-all-ui .pk-drag-ghost { display: none !important; opacity: 0 !important; pointer-events: none !important; } .pk-ana-select-btn { border: 1px solid var(--pk-bd); background: var(--pk-bg); color: var(--pk-fg); height: 32px; border-radius: 6px; padding: 0 10px; font-size: 13px; cursor: pointer; display: none; align-items: center; gap: 6px; transition: all 0.2s; white-space: nowrap; margin-right: 8px; } .pk-ana-select-btn:hover { border-color: var(--pk-pri); color: var(--pk-pri); background: rgba(var(--pk-bg-rgb), 0.05); } .pk-ana-pop { position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 1000; display: none; flex-direction: column; padding: 8px; width: 340px; gap: 4px; pointer-events: auto; } .pk-ana-pop-row { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } .pk-ana-opt { padding: 8px 4px; border-radius: 4px; cursor: pointer; font-size: 12px; color: var(--pk-fg); transition: background 0.1s; text-align: center; border: 1px solid transparent; background: var(--pk-hl); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-ana-opt:hover { background: var(--pk-sel-bg); color: var(--pk-pri); border-color: var(--pk-sel-bd); } `; ; const sleep = ms => new Promise(r => setTimeout(r, ms)); const esc = s => (s || '').replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); const fmtSize = n => { n = parseInt(n || 0, 10); if (isNaN(n)) return ''; if (n === 0) return '0 KB'; const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return (n < 10 ? n.toFixed(2) : n.toFixed(1)) + ' ' + u[i]; }; const fmtDate = t => { if (!t) return '-'; const d = new Date(new Date(t).getTime() + (8 * 60 * 60 * 1000)); const pad = n => String(n).padStart(2, '0'); return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`; }; const fmtDur = s => { s = Math.max(0, parseInt(s, 10) || 0); if (s <= 0) return "00:00"; const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sc = s % 60; const mmss = String(m).padStart(2, '0') + ':' + String(sc).padStart(2, '0'); return h > 0 ? String(h).padStart(2, '0') + ':' + mmss : mmss; }; function gmGet(key, def) { if (typeof GM_getValue !== 'undefined') { let v = GM_getValue(key, def); return (v === null) ? def : v; } return def; } function gmSet(key, val) { if (typeof GM_setValue !== 'undefined') GM_setValue(key, val); } const calcSha1 = async (file) => { if (!window.crypto || !window.crypto.subtle) return ""; const CHUNK_SIZE = 20 * 1024 * 1024; const chunks = Math.ceil(file.size / CHUNK_SIZE); if (file.size > 100 * 1024 * 1024) { const head = file.slice(0, 1024 * 1024); const mid = file.slice(file.size / 2, file.size / 2 + 1024 * 1024); const tail = file.slice(file.size - 1024 * 1024); const combined = new Blob([head, mid, tail]); const buffer = await combined.arrayBuffer(); const hash = await crypto.subtle.digest('SHA-1', buffer); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); } const buffer = await file.arrayBuffer(); const hash = await crypto.subtle.digest('SHA-1', buffer); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); }; function getLang(){const u=gmGet('pk_lang','');if(u)return u;const n=navigator.language.toLowerCase();return (n==='zh'||n.startsWith('zh-cn')||n.startsWith('zh-sg'))?'zh':(n.startsWith('zh-tw')||n.startsWith('zh-hk')||n.startsWith('zh-mo'))?'tc':n.startsWith('ko')?'ko':n.startsWith('ja')?'ja':'en';} const T = { zh: { /* --- 通用与基础UI --- */ title: "PikPak 增强大师", str_original: "原画", str_original_fast: "原画 (高速)", str_folders: "文件夹", str_files: "文件", unit_folders: "个文件夹", unit_days: "天", unit_month: "月", unit_sec: "秒", str_no_files: "暂无文件", str_items: "项", col_name: "名称", col_size: "大小", col_dur: "类型/时长", col_duration_only: "时长", col_progress: "播放进度", col_play_time: "播放时间", col_date: "修改日期", col_remaining: "剩余时长", col_path: "路径", col_old: "原名称", col_new: "新名称", col_type: "类型", col_path_name: "路径 / 名称", col_action: "操作", lbl_folder_first: "文件夹置顶", tag_default: "默认", current_dir: "当前目录", str_same_folder: "(同文件夹)", lbl_dont_show: "不再提醒", lbl_dont_show_session: "本次查重不再提示", str_empty_filename: "(空文件名)", str_empty_dir: "(空目录)", btn_filter: "筛选", title_file_filter: "文件筛选", cat_all: "全部", cat_video: "视频", cat_audio: "音频", cat_image: "图片", cat_document: "文档", cat_software: "软件", cat_archive: "压缩包", cat_torrent: "BT种子", cat_other: "其他", btn_exit_filter: "退出筛选", /* --- 属性面板 --- */ ctx_property: "属性", title_property: "文件属性", lbl_prop_name: "文件名称", lbl_prop_size: "文件大小", lbl_prop_count: "文件数量", lbl_prop_ctime: "创建时间", lbl_prop_mtime: "修改时间", lbl_prop_source: "添加来源", lbl_prop_link: "资源链接", lbl_prop_path: "文件位置", str_prop_cloud: "云添加", str_prop_share: "来自分享", str_prop_user: "用户上传", str_prop_unknown: "未知来源", fmt_prop_count: "包含 {f} 个文件,{d} 个文件夹", str_prop_offline: "离线任务", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "主页", btn_nav_share: "我的分享", btn_nav_offline: "离线下载", btn_nav_recent: "最近添加", btn_nav_history: "播放历史", btn_nav_starred: "收藏夹", btn_nav_trash: "回收站", btn_nav_upload: "我的上传", title_offline: "我的离线", trash_title: "回收站", trash_notice: "回收站的文件最多保存15天", history_notice: "仅记录在脚本环境内产生的播放进度", ctx_open: "打开", ctx_add_bl: "添加到资源管理器", ctx_remove_bl: "从资源管理器移除", ctx_rename: "重命名", ctx_copy: "复制", ctx_copy_name: "复制文件名", ctx_copy_link: "复制链接", ctx_cut: "移动", ctx_del: "删除", ctx_down: "下载", ctx_star: "添加星标", ctx_unstar: "取消星标", ctx_locate: "在文件夹中查看", ctx_share: "分享", /* --- 通用文件操作按钮 --- */ btn_down: "下载", tip_down: "下载 [Alt] + [D]", btn_aria2: "发送 Aria2", tip_aria2: "发送 Aria2 [Alt] + [A]", btn_refresh_short: "刷新", tip_refresh: "刷新 [F5]", btn_newfolder: "新建文件夹", tip_newfolder: "新建文件夹 [F8]", btn_del: "删除", tip_del: "删除 [Delete]", btn_deselect: "取消选择", tip_deselect: "取消选择 [Esc]", btn_invert: "反选", btn_copy: "复制", tip_copy: "复制 [Ctrl] + [C]", btn_cut: "移动", tip_cut: "移动 [Ctrl] + [X]", btn_paste: "粘贴", tip_paste: "粘贴 [Ctrl] + [V]", btn_clear_history: "删除历史", tip_clear_history: "删除历史 [Delete]", btn_restore: "还原", tip_restore: "还原 [R]", btn_del_forever: "永久删除", tip_del_forever: "永久删除 [Delete]", btn_empty_trash: "清空回收站", tip_empty_trash: "清空回收站 [Shift] + [Delete]", btn_exit: "退出", btn_close: "关闭", tip_close: "关闭 [Esc]", tip_theme: "切换主题 [Alt] + [T]", tip_rotate: "旋转 [R]", tip_mirror: "镜像翻转 [H]", tip_flip_v: "垂直翻转 [V]", tip_maximize: "最大化 [M]", tip_minimize: "最小化 [M]", tip_full_screen: "全屏 [Enter]", btn_help: "帮助", tip_help: "帮助 [Alt] + [H]", btn_view_file: "查看文件", btn_jump: "跳转", btn_copy_text: "复制", btn_stop: "停止", tip_stop: "立即停止当前操作", btn_settings: "设置", btn_logout: "退出 PikPak", msg_logout_confirm: "确定要退出登录吗?", tip_settings: "设置和更多 [Alt] + [S]", lbl_upload_to: "上传文件至: ", msg_move_done: "移动完成。", /* --- 离线、上传与云下载 --- */ btn_upload: "本地上传", btn_up_file: "上传文件", btn_up_folder: "上传文件夹", btn_cloud_download: "云下载", btn_up_pause: "暂停任务", tip_up_pause: "暂停任务 [Alt] + [P]", btn_up_start: "开始任务", tip_up_start: "开始任务 [Alt] + [G]", btn_up_del: "删除任务", tip_up_del: "删除任务 [Delete]", btn_up_clear_all: "清空任务", tip_up_clear_all: "清空任务 [Shift] + [Delete]", btn_retry_task: "重试任务", tip_retry_task: "重试任务 [R]", col_task_status: "任务状态", col_task_progress: "离线进度", col_up_speed: "速度", col_up_status: "状态", lbl_task_run: "进行中", lbl_task_fail: "已失败", lbl_task_ok: "已完成", lbl_up_run: "进行中", lbl_up_pause: "已中断", lbl_up_downloading: "下载中", lbl_up_done: "已完成", tip_up_pause_desc: "包含手动暂停及报错的任务", title_cloud_task: "创建云下载任务", ph_cloud_links: "支持链接格式:\n- 各种下载链接,如magnet。\n- YouTube、X (Twitter)、TikTok、Facebook等分享链接。\n通过换行可一次性添加多条链接。", lbl_save_to: "文件将被保存至:", lbl_default_folder: "默认文件夹", btn_via_torrent: "通过 Torrent 创建", tip_cloud_save_path: "常规云下载文件会保存在 My Pack 目录,来自其他 App 的云下载文件会保存至 My [XYZ] 目录,[XYZ] 为 App 名称。", lbl_smart_fix: "自动修复防屏蔽磁链 (提取特征码/剔除文字干扰)", title_save_method: "保存方式", msg_save_snapshot_desc: "此链接只能被存储为网页快照。", tip_snapshot_details: "PikPak 不能从此链接中直接采集媒体文件,您可以保存网页快照。PikPak 将尽可能保存完整的网页内容到快照文件中。", btn_save_snapshot: "保存快照", btn_create_now: "立即创建", btn_modify: "修改", str_snap_link_count_suffix: " 等 {n} 个链接", /* --- 分享管理 --- */ btn_cancel_share: "取消分享", share_copy_suffix: "复制这段内容后打开 PikPak-App,畅享极速秒播", share_copy_pwd: "密码", title_share_detail: "分享详情", ctx_share_detail: "查看分享详情", ctx_share_copy: "复制链接和密码", col_view: "浏览", col_save: "保存", col_share_time: "分享时间", col_share_status: "分享状态", lbl_limit_reached: "次数已满", lbl_limit_tip: "限制次数", lbl_share_view: "浏览", lbl_share_save: "保存", lbl_share_link_title: "分享链接", lbl_share_pwd_title: "密码", lbl_share_expire_title: "有效期", btn_copy_link_pwd: "复制链接和密码", str_expire_suffix: "天后过期", ph_edit_pwd: "输入分享密码,支持 4-10 位", btn_close_pwd: "关闭密码", str_no_pwd: "无密码", title_edit_share_code: "分享代码修改", ph_edit_share_code: "支持 5-18 位文字、数字、及符号等", btn_add_share_code: "添加分享代码", btn_del_share_code: "删除分享代码", share_title: "分享文件", share_mode: "分享方式", share_public: "公开链接", share_encrypted: "加密链接", share_expiry: "设置有效期", share_pass: "设置提取码", share_count: "设置提取次数", share_count_ed: "提取次数", share_perm: "永久有效", share_unlimit: "不限", share_rand: "系统随机", share_custom: "自定义", share_days: "天", share_times: "次", btn_share_start: "立即分享", cal_custom_title: "自定义有效期", lbl_share_link: "链接", lbl_share_code: "提取码", btn_copy_share: "复制全部", str_share_expired: "已过期", str_share_deleted: "文件已删", title_edit_pwd: "密码修改", lbl_share_code_title: "分享代码", ph_password: "密码", ph_pass_range: "4-10位字符", cal_week_days:["日", "一", "二", "三", "四", "五", "六"], cal_months:["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "文件分析", btn_scan: "文件透视", lbl_scan_selected: "对选中的 {n} 个项目执行文件透视", lbl_keyword_filter: "排除关键词", ph_keyword_filter: "排除包含的关键词,多个用逗号分隔", lbl_scan_current: "对当前路径下所有项目执行文件透视", tip_dup: "文件查重", lbl_dup_selected: "对选中的 {n} 个项目执行文件查重", lbl_dup_current: "对当前路径下所有项目执行文件查重", tip_scan_dup: "筛选或查重文件", lbl_dup_tool: "选择删除对象:", lbl_dup_reset: "↺ 复原 (取消置顶 & 清空选择)", lbl_dup_select_folder: "📂 按文件夹选择", lbl_dup_select_folder_short: "📂 文件夹", lbl_dup_invert: "反选模式", lbl_dup_invert_short: "反选", tip_dup_invert_limit: "仅按文件夹选择可用", fmt_dup_count: "({n}个重复)", btn_start_scan: "开始扫描", tag_hash: "精准匹配", tag_hash_short: "精准", tag_name: "名称相似", tag_name_short: "名称", tag_sim: "时长相似", tag_sim_short: "时长", label_dup_video: "视频文件 (精准匹配 + 时长相似 + 名称相似)", label_dup_image: "图片文件 (精准匹配 + 名称相似)", label_dup_other: "其他文件 (精准匹配 + 名称相似)", btn_analyze: "文件夹分析", tip_analyze: "筛选或查重文件夹", btn_export: "导出目录", tip_export: "生成并下载当前路径的文件树列表", title_export_format: "导出目录样式", lbl_export_current: "对当前路径下所有项目执行目录导出", opt_tree_view: "目录树", opt_list_view: "目录列表", msg_exporting: "正在生成目录树...", str_analyze_results: "匹配结果", lbl_size_threshold: "检测阈值", title_analyze_result: "文件夹分析结果", opt_ana_large: "文件夹透视", lbl_analyze_selected: "对选中的 {n} 个项目执行文件夹透视", lbl_analyze_current: "对当前路径下所有项目执行文件夹透视", opt_ana_sim: "文件夹查重", lbl_ana_sim_selected: "对选中的 {n} 个项目执行文件夹查重", lbl_ana_sim_current: "对当前路径下所有项目执行文件夹查重", title_algo_help: "查重算法说明", algo_help_content: "名称匹配:查找名称和体积相近的文件夹群组。\n相似度匹配:查找内部文件高度重合的文件夹群组。\n包含率匹配:查找小文件夹的内容被大文件夹完全覆盖的子集冗余。\n\n精度:相似度匹配 > 包含率匹配 > 名称匹配\n范围:包含率匹配 > 相似度匹配", lbl_threshold: "阈值", lbl_sim_score: "相似度", lbl_containment: "包含率", lbl_name_match: "名称匹配", lbl_sim_match: "相似度匹配", lbl_contain_match: "包含率匹配", lbl_ana_min: "下限", lbl_ana_max: "上限", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "清理空文件夹", tip_prune: "清理空文件夹 [Ctrl] + [Delete]", btn_rename: "重命名", tip_rename: "重命名 [F2]", btn_bulkrename: "批量重命名", tip_bulkrename: "批量重命名 [F2]", title_blacklist: "资源管理器", btn_blacklist_run: "立即运行清理", btn_clear_list: "清空列表", tip_bl_desc: "下列项目在【删除】时会跳过,仅通过【立即运行清理】查找删除", tip_blacklist_input: "资源管理器 [Alt] +[Delete]", label_bl_folder: "文件夹名单 (精准查找)", label_bl_file: "文件名单 (精准查找)", lbl_type_folder: "文件夹", lbl_type_file: "文件", ph_bl_folder: "请通过“粘贴”或文件右键菜单“添加到资源管理器”导入。", ph_bl_file: "请通过“粘贴”或文件右键菜单“添加到资源管理器”导入。", modal_bl_preview: "检索结果", btn_bl_delete: "删除选中项", modal_rename_title: "重命名", modal_rename_multi_title: "批量重命名", btn_preview: "预览", modal_preview_title: "确认更改", label_pattern: "模式 (例: Video {n})", label_replace: "替换/删除", label_replace_note: "区分大小写", label_include_ext: "包含后缀", label_regex: "正则 (Regex)", placeholder_find: "查找内容", placeholder_replace: "替换为 (留空删除)", label_jav: "FC2 纯净命名", lbl_rn_pattern: "命名模板", lbl_rn_case_convert: "大小写转换", opt_rn_keep_origin: "(保持原样)", opt_rn_lower: "全部小写 (abc)", lbl_rn_mode_series: "剧集模式", lbl_rn_mode_format: "格式化", lbl_rn_mode_ad: "前缀去广告", lbl_rn_mode_ext: "后缀修复", opt_rn_upper: "全部大写 (ABC)", opt_rn_title: "首字母大写 (Abc)", lbl_rn_width_convert: "全半角转换", opt_rn_width_half: "全角转半角 (A->A)", opt_rn_width_full: "半角转全角 (A->A)", lbl_rn_preview_title: "变更预览", tip_jav_mode_desc: "✨ 智能提取FC2并去除无关字符", tip_ad_remove_desc: "🧹 智能滤除头部广告、网址及垃圾符号,自动清洗Emoji并修复括号格式", tip_ext_fix_desc: "🧩 根据文件真实类型 (MIME) 智能修正后缀", label_replace_find: "查找内容", label_replace_to: "替换为", /* --- 解压相关 --- */ btn_unzip: "批量解压", tip_unzip: "批量解压 [Alt] + [U]", btn_unzip_all: "全部解压", btn_understand_unzip: "理解并解压", title_input_pwd: "需要解压密码", lbl_pwd_prompt: "请输入密码:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "外部播放", tip_ext: "使用PotPlayer播放或获取播放链接 [Alt] +[E]", btn_img_search: "以图搜图 [F]", tip_play_search: "以图搜图 [F]", tip_pip: "画中画 [P]", str_no_sub: "无字幕", lbl_sub_sel: "字幕选择", lbl_show_sub: "显示字幕", btn_sub_search: "搜索在线字幕", btn_sub_cloud: "打开云盘字幕", btn_sub_local: "打开本地字幕", lbl_sub_pos: "字幕位置", lbl_sub_bottom: "底部", lbl_sub_top: "顶部", lbl_sub_bg_op: "背景透明", lbl_sub_size: "字幕大小", lbl_sub_offset: "字幕进度", title_sel_sub: "选择字幕", ph_sub_search: "输入关键词,下方链接自动更新...", btn_force_play: "尝试强行播放", str_compat_mode: "兼容模式", lang_code: "zh", btn_go_search: "🔍 去 {n} 手动搜", btn_restart: "从头播放", btn_prev_video: "上一个 [Ctrl + ←]", btn_next_video: "下一个 [Ctrl + →]", tip_plist_open: "展开 [E]", tip_plist_close: "收起 [E]", tab_sub: "字幕", tab_size: "尺寸", tab_more: "更多", lbl_ratio: "比例", lbl_direction: "方向", opt_ratio_def: "默认", btn_rot_l: "向左旋转", btn_rot_r: "向右旋转", btn_flip_h: "水平翻转", btn_flip_v: "垂直翻转", lbl_play_end: "当播放结束", opt_list_loop: "列表循环", opt_single_loop: "单集循环", opt_play_stop: "播完暂停", lbl_skip_op: "跳过片头", lbl_skip_ed: "跳过片尾", export_link_title: "导出视频串流链接", btn_start_play: "开始播放", btn_copy_link: "复制链接", tip_copy_link: "复制链接 [Alt] + [C]", opt_player_other: "其他 (导出链接)", lbl_player: "播放器", btn_mark: "标记", lbl_resolution: "清晰度", str_switch_compat: "当前清晰度不可用,已为您恢复至 {n}", type_img: "图像", type_doc: "文档", type_archive: "压缩文件", type_sub: "字幕文件", type_app: "应用程序", type_suffix: "文件", /* --- 设置与搜索 --- */ label_turbo_mode: "极速模式", desc_turbo_mode: "自动开启并替代网页界面 (推荐)", lbl_aria2_status: "连接状态", ph_aria2_secret: "密钥 (选填)", str_connected: "连接成功", str_conn_fail: "连接失败", str_connecting: "正在测试...", tip_mixed_content: "常用端口参考:\n• 6800 (Aria2 标准版)\n• 16800 (Motrix 默认)\n• 6881 (其他集成版)", picker_title: "选择文件夹", picker_all: "全部文件", picker_new: "新建文件夹", picker_sort_new: "最新", picker_sort_old: "最旧", title_select_file: "选择文件", placeholder_search: "搜索文件...", placeholder_search_short: "搜索", title_search_hist: "搜索历史", btn_clear_hist: "清空", lbl_global_search: "全盘搜索", lbl_search_path: "搜索包含路径", lbl_search_path_short: "路径", str_search_results: "搜索结果", modal_settings_title: "设置", label_lang: "语言 (Language)", label_thumb: "模糊略缩图 (隐私模式)", label_keep_pos: "保持浏览位置 (返回时定位)", label_sort_pref: "排序偏好", opt_sort_indep: "每个文件夹独立", opt_sort_global: "全部相同", desc_sort_indep: "调整排序方式仅针对当前文件夹生效", desc_sort_global: "全部文件夹使用相同的排序 (时间倒序)", label_search_engine: "搜图引擎", opt_engine_google: "Google Lens (综合)", opt_engine_yandex: "Yandex (综合)", opt_engine_saucenao: "SauceNAO (Pixiv/插画)", opt_engine_tracemoe: "trace.moe (动漫截图)", label_dup_strictness: "相似匹配阈值", opt_strict: "严格", opt_loose: "宽松", label_comic_mode: "媒体模式", desc_comic_mode: "纯图片或纯视频文件夹默认 A-Z 排序", label_aria2_url: "Aria2 地址", label_aria2_token: "Aria2 密钥", label_privacy_mode: "隐私模式", label_blur_cover: "模糊封面图", label_dl_filter_ext: "下载后缀过滤 (例: .txt, .jpg)", label_dl_filter_name: "下载名称过滤 (关键词或全名)", lbl_dl_filter: "文件夹下载过滤", desc_dl_filter: "文件夹下载/推送时自动排除匹配的文件", lbl_config_manage: "配置管理", btn_export_data: "导出备份", btn_import_data: "导入备份", btn_clean_data: "清除本地数据", title_clean_data: "选择清理项目", msg_clean_confirm: "确定要彻底删除选中的本地数据吗?此操作无法撤销。", msg_clean_success: "本地数据已清理,页面即将刷新...", opt_cfg_index: "全盘索引 (已同步的目录结构/文件快照)", opt_cfg_pref: "偏好设置 (UI 外观/操作习惯/排序偏好)", opt_cfg_rules: "管理规则 (资源管理器/分享次数限制/搜索记录/下载规则)", opt_cfg_vault: "密码金库 (解压密码记忆)", opt_cfg_history: "视频缓存 (视频播放进度/视频时长缓存)", opt_cfg_cache: "运行缓存 (文件夹修改时间/最后浏览位置/指纹)", msg_import_confirm: "导入的配置将与当前设置合并(合并名单/记录,覆盖冲突的基础设置),是否继续?", msg_import_success: "配置导入成功,页面即将刷新...", err_invalid_config: "无效的配置文件:未检测到指纹标识或格式错误", err_json_format: "文件解析失败:JSON 语法错误或文件已损坏", lbl_storage: "存储空间", lbl_browse_exp: "浏览体验", lbl_skip_bl_on_del: "删除时跳过管理器中记录资源", lbl_pwd_manage: "解压密码管理", title_pwd_vault: "密码金库", lbl_pwd_try_count: "单个压缩包密码匹配上限", tip_pwd_manual: "每行记录一个密码,回车换行", str_root_dir_cn: "根目录", btn_ana_select: "一键勾选", opt_keep_new: "保留最新的", opt_keep_old: "保留最旧的", opt_keep_large: "保留最大的", opt_keep_small: "保留最小的", opt_keep_short: "保留名称最短的", opt_keep_long: "保留名称最长的", /* --- 状态、进度与加载短语 --- */ loading: "加载中...", loading_detail: "正在全速索引目录结构...", loading_fetch: "获取中... ({n})", loading_dup: "分析重复项... ({p}%)", str_loading_placeholder: "加载中...", str_load_failed: " (加载失败)", str_load_failed_simple: "加载失败", str_waiting_token: "正在同步登录状态...", str_speed: "速度", status_scanning_selection: "扫描选中项... {n}", status_ready: "{n} 个项目", sel_count: "已选择 {n} 个项目", str_cached: "已缓存:", str_retries: "重试:", str_failed: "失败:", str_success: "成功:", str_stopping: "正在停止...", str_merging: "合并数据中...", str_rendering: "渲染列表中...", str_scanning: "扫描中...", str_analyzing: "分析中...", str_deleting: "删除中...", str_saving: "保存中...", str_saving_dots: "保存中...", str_checking_bl: "匹配名单记录中...", str_processing: "系统正在全速处理中...", str_cleanup_done: "清理完成。", str_waiting_preload: "等待预加载...", str_copying: "复制到剪贴板...", str_moving: "准备移动...", str_sorting: "正在排序...", str_refreshing: "刷新中...", str_refreshing_cache: "刷新缓存中...", str_syncing_stars: "同步星标状态...", str_updating_view: "更新视图中...", str_generating_view: "生成视图中...", str_group: "组", str_init_rename: "初始化重命名...", str_renaming: "重命名中...", str_calc_changes: "计算变更中...", str_scanning_dir: "扫描目录结构...", str_init_op: "初始化操作...", str_init_scan: "正在初始化全盘扫描...", str_rebuilding: "正在重建索引...", str_upload_1: "正在上传 (节点 1/3)...", str_upload_2: "节点1超时,切换节点 2...", str_upload_3: "节点2超时,尝试最后节点...", str_upload_fail_copy: "上传失败,准备写入剪贴板...", msg_transcoding: "云端转码中...", msg_transcoding_wait: "服务器正在处理此视频,请稍候", str_preparing: "准备解压...", str_unzipping: "正在解压: {n}", str_unzipping_state: "解压中...", str_unzipping_prog_0: "解压中: 0%", str_unzipping_prog_100: "解压中: 100%", str_unzipping_prog_fmt: "解压中: {n}%", msg_task_waiting: "等待中...", msg_task_hashing: "校验文件中...", msg_task_init_upload: "初始化上传...", msg_task_uploading: "正在上传...", msg_task_init_part: "初始化分片...", msg_task_uploading_2: "正在上传...", str_loc_tracing: "正在回溯路径...", str_loc_stale: "缓存已过期,正在同步云端...", str_verifying: "正在验证...", str_server_indexing: "服务器索引中... ({n}/5)", str_creating_task_n: "创建任务 ({n}/{t})...", msg_submit_request: "正在提交请求... {c}/{t}", msg_wait_server: "等待服务器处理... ({c}/{t})", msg_server_processing: "服务器处理中... ({c}/{t})", str_jav_querying: "正在查询...", lbl_done_check: "✔ 完成", msg_limit_updated: "提取次数已更新", /* --- 提示、确认与交互消息 --- */ title_alert: "提示", title_confirm: "确认", title_prompt: "输入", btn_ok: "确定", btn_yes: "是", btn_no: "否", btn_save: "保存配置", btn_cancel: "取消", btn_create: "创建", btn_skip: "跳过", msg_down_success: "已成功调用浏览器下载 {n} 个文件。", msg_batch_txt: "已生成下载列表 (.txt)。", msg_clear_history_done: "已从历史记录中移除", msg_skip_unzipped: "已跳过 {n} 个已解压的项目。", msg_unzip_skip_del_confirm: "检测到 {n} 个已解压的压缩包,是否将其移入回收站?", msg_cancel_share_confirm: "确定要取消选中的 {n} 个分享吗?\n链接将立即失效。", msg_pwd_updating: "正在更新密码...", msg_pwd_updated: "密码已更新", msg_exp_updated: "有效期已更新", msg_cancel_share_done: "已取消 {n} 个分享。", msg_drag_drop_hint: "将文件拖拽到此处并释放", str_drag_files: " 等 {n} 个文件", msg_creating_share: "正在创建分享...", title_share_result: "分享成功", msg_no_files: "没有项目。", msg_no_selection: "请先选择项目。", warn_del: "确定要删除选中的 {n} 项吗?", msg_clear_sel_confirm: "已选中 {n} 个重复文件,确认要取消当前的勾选吗?", str_bl_stat: "匹配: {n} 项 | 已选中: {m} 项", str_hits: "命中", msg_settings_saved: "设置已保存。页面将刷新。", msg_name_exists: "名称已存在: {n}", str_name_conflict: "(可能重名)", msg_newfolder_prompt: "新文件夹名称:", msg_rename_prompt: "输入新名称:", msg_copy_done: "已复制。请选择粘贴位置。", msg_cut_done: "准备移动。请选择粘贴位置。", msg_paste_empty: "没有可粘贴的项目。", msg_copy_empty: "剪贴板为空或无文本数据。", msg_add_success: "已追加 {n} 行数据。", msg_del_done: "已删除选中行。", msg_del_select: "请先点击选中要删除的行!", msg_del_items_done: "已删除 {n} 个项目。", msg_copy_success: "复制成功", str_redirecting: "正在跳转 Google Lens...", msg_manual_paste: "图床上传超时。已复制截图,请在新窗口按 {cmd}", msg_starring: "正在添加星标...", msg_unstarring: "正在取消星标...", msg_star_added: "已添加星标", msg_unstar_done: "已取消星标", msg_empty_trash_confirm: "确定要清空回收站吗?此操作无法撤销!", msg_trash_emptied: "回收站已清空。", msg_del_forever_confirm: "确定要彻底删除这 {n} 个项目吗?此操作无法撤销!", msg_del_forever_done: "已彻底删除 {n} 个项目。", msg_restore_done: "已成功还原 {n} 个项目。", msg_auto_sub_load: "已自动加载字幕:{n}", msg_dl_sub: "正在下载字幕...", msg_transcode_done: "✅ 转码完成,开始播放", msg_fallback_report: "⚠️ 原画不可播放(MPEG4/HEVC),已自动切换至 {n}", tip_manual_sub: "提示:下载 .srt 或 .vtt 后,直接拖入播放器即可加载。", msg_sub_drop_load: "已通过拖拽加载字幕:{n}", msg_resume_hint: "已为您从 {t} 继续播放,点击这里 ", msg_unzip_confirm_n: "确定要在当前目录解压这 {n} 个文件吗?", msg_task_paused: "已暂停", msg_task_added: "已添加 {n} 个上传任务", msg_task_fast_success: "秒传成功", msg_task_upload_done: "上传完成", log_dirty_data: "拦截到脏数据入侵!请求模式与当前视图不符,已物理熔断。", msg_network_unstable: "网络连接波动,正在自动恢复...", msg_skip_locked: "{n} 个被锁定", msg_skip_self: "{n} 个原地移动", msg_skip_conflict: "{n} 个子路径忙碌", msg_skip_invalid: "已自动跳过无效项: ", msg_creating_cloud_task: "正在创建云下载任务...", str_parsing_torrent: "正在解析种子文件...", err_torrent_no_info: "解析失败:未发现有效信息", err_file_read: "文件读取失败", msg_cloud_task_finish: "创建完成:{s} 成功,{f} 失败", msg_cloud_task_success: "🎉 已成功创建 {n} 个任务", msg_prepare_restore: "准备还原...", msg_smart_matching_n: "正在智能匹配密码 ({n}个)...", msg_system_busy_retry: "系统繁忙,等待重试 ({n}/5)...", msg_unzip_running_bg: "[{n}] 已在云端解压中,请稍后刷新查看", msg_share_code_updated: "分享代码已更新", msg_retry_submitted: "已重试提交 {n} 个任务", msg_aria2_batch_fail_log: "\n\n检测到失败项较多,已为您自动导出完整错误清单 (.txt)", str_aria2_fetch_err: "(获取链接失败)", str_aria2_rpc_err: "(投递失败)", str_aria2_aborted: "(已取消)", str_aria2_fail_file_name: "Aria2_失败清单", msg_op_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运,请等待当前任务完成后再操作。", msg_op_blocked_analyzing: "⚠️ 操作拦截\n\n后台正在执行文件搬运。为确保文件夹分析数据准确,请等待当前任务完成后再操作。", msg_op_blocked_exporting: "⚠️ 操作拦截\n\n后台正在执行文件搬运。为确保导出目录文本准确,请等待当前任务完成后再操作。", msg_analyze_only_normal_dir: "请选择文件夹。", msg_analyze_no_large_folders: "没有发现符合阈值范围的文件夹 ({s})", msg_analyze_summary_fmt: "共发现 <b>{n}</b> 个大于 {s}GB 的文件夹 (已按大小降序排列)", msg_prune_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运,暂时无法执行清理。", msg_global_index_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运。全盘索引需要稳定的目录结构,请等待当前任务完成后再开启全盘搜索。", msg_resource_locked_download: "⚠️ 操作拦截\n\n选中项包含正在移动的文件或文件夹。请等待搬运完成后再发起下载。", msg_resource_locked_aria2: "⚠️ 操作拦截\n\n选中项包含正在移动的文件或文件夹。请等待搬运完成后再推送至 Aria2。", msg_flatten_blocked_moving: "⚠️ 操作拦截\n\n后台正在执行文件搬运。此时执行文件分析会导致文件列表缺失或重复,请稍后再试。", err_task_conflict: "⚠️ 操作拦截\n\n后台正在执行文件搬运。全局清理需要稳定的目录结构,请等待搬运完成后再执行。", title_del_task_confirm_fmt: "确认删除 {n} 条传输任务?", lbl_del_cloud_files_too: "同时删除云盘内的文件", msg_file_del_failed: "文件删除失败: ", msg_task_del_success_fmt: "已删除 {n} 个任务", title_clear_task_confirm: "确认清空所有上传任务?", msg_task_clear_success_fmt: "已清空 {n} 个上传任务", msg_unzip_virtual_view_warn: "您正在虚拟视图中操作。解压后的文件将存放在<b>各压缩包所在的原始文件夹</b>中,而<b>不会</b>直接出现在当前列表中。<br><br>是否继续?", msg_smart_matching_file: "智能匹配密码中... ({n})", msg_unzip_batch_submitted: "✅ 已完成 {n} 个解压任务", msg_unzip_batch_skipped: " ({n} 个跳过)", msg_unzip_check_source: "。请前往源目录查看结果。", tip_jump_to_folder: "跳转到此文件夹", msg_task_deleted: "任务已删除", msg_scan_done: "扫描完成!\n共发现 {n} 个文件,遍历了 {f} 个文件夹。", msg_scan_fail: "\n\n❌ 有 {n} 个失败。", msg_scan_fix: "\n\n✅ 自动修复了 {n} 次网络错误。", msg_down_scanning: "正在解析文件夹内容...", msg_down_progress: "正在调用浏览器下载...", msg_down_confirm_total: "✅ 扫描完毕,共找到 {n} 个文件。\n\n⚠️ 警告:浏览器直接下载大量文件极易导致页面卡死或被拦截。\n建议超过 10 个文件使用 Aria2 导出。\n\n是否坚持使用浏览器下载?", msg_aria2_sending_batch: "🚀 正在分批发送任务至 Aria2...", msg_aria2_check_fail: "Aria2 连接失败!\n请检查 URL 和 Token。", msg_aria2_check_ok: "Aria2 连接成功!", msg_aria2_sent: "已将 {n} 个文件发送到 Aria2。", msg_aria2_test_fail: "Aria2 连接失败。\n仍然保存设置吗?", title_aria2_fail: "连接测试失败", msg_batch_scanning: "🚀 正在高速扫描目录结构...", msg_batch_hydrating: "⚡ 正在并行提取下载链路...", msg_batch_no_files: "未发现可下载的文件。", msg_dup_warn: "是否开始搜索重复文件?", msg_dup_result: "发现 {n} 组重复项。", msg_dup_none: "未发现重复文件。", msg_bl_stop: "操作已停止。", msg_bl_add_done: "已将 {n} 个项目添加到记录。", msg_bl_remove_done: "已从记录中移除 {n} 个项目。", msg_bl_empty: "名单列表为空,无法运行。", msg_bl_clear_confirm: "确定要清空所有记录条目吗?此操作不可恢复。", msg_blacklist_run_none: "网盘中未发现符合名单条件的项目。", msg_blacklist_run_confirm: "在网盘中发现了 {n} 个已记录项目。\n\n是否立即移入回收站?", msg_bl_run_limit: "⚠️ 模式限制\n\n清理操作涉及物理文件递归操作。目前处于非标准目录,无法准确定位物理扫描范围。\n\n请返回主页常规文件夹后再执行清理。", msg_del_protected: "已保护 {n} 个已记录文件不被删除。", msg_del_none: "没有可删除的文件。", msg_bl_scanning: "全盘搜索中... \n已扫描目录: {d} | 命中: {f}", rn_tip_wait: "请设置规则", rn_tip_jav: "点击上方按钮开始智能匹配", rn_tip_none: "没有匹配的项目或名称", rn_stat: "匹配: {n} 项 | 有效变更: {m} 项", rn_warn_confirm: "确定要重命名 {n} 个文件吗?", msg_bulkrename_done: "已重命名 {n} 个项目。", msg_rn_all_skipped: "❌ 所有项目均因重名被跳过,未执行任何操作。", msg_rn_fail_count: "跳过已重名 {n} 个项目", msg_prune_confirm: "是否开始搜索当前列表中的空文件夹?", msg_prune_none: "未发现空文件夹。", msg_prune_found: "发现了 {n} 个空文件夹。\n是否立即删除?", msg_deleting_folders: "正在删除 {n} 个文件夹...", msg_global_warn: "即将开始全盘文件同步。\n\n文件同步后缓存到本地内存中,网页刷新前持续存在。\n\n是否继续?", msg_init_scan_sel: "正在初始化选中项扫描...", warn_clear_history: "确定要从历史记录中移除选中的 {n} 项吗?\n(这不会删除您的云端文件)", msg_img_copy_hint: "在新窗口中,请按下 {cmd} 即可搜索。", msg_aria2_not_set: "检测到您尚未配置 Aria2,请填写后继续:", str_jav_no_match: "(未匹配到番号)", msg_unzip_fail: "解压请求失败", msg_jszip_fail: "JSZip 加载失败,请检查网络。", msg_turbo_activated: "极速模式已激活:脚本已深度接管网页逻辑,确保稳定流畅。", msg_console_legal: "严禁商业用途:本项目仅供个人学习与交流使用。", msg_ana_warn: "文件夹查重提示:判定基于算法推测,删除前请务必人工核对,以防误删。", /* --- 错误提示 --- */ err_invalid_links: "请输入正确的链接", err_pwd_format: "密码必须为 4-10 位字母或数字", err_invalid_torrent: "无效的种子文件格式", err_torrent_complex: "解析复杂度过高,可能是非法文件", err_torrent_format: "种子文件结构损坏", err_torrent_len: "字段长度解析异常", err_torrent_char: "解析遇到非法字符", err_share_code_exists: "该分享代码已被占用", err_folder_not_ready: "云端文件夹正在创建中,请稍后再试", err_item_deleted: "该项目不存在", err_network: "网络错误", err_clipboard_denied: "剪贴板访问被拒绝", err_worker: "工作线程错误", err_api: "API 错误", err_capture: "截图失败。", err_captcha_simple: "验证失败。请在网页列表手动收藏一次文件以完成验证。", err_sub_dl_fail: "字幕下载失败: ", err_req_blocked: "网络请求失败 (可能被拦截)", err_req_timeout: "请求超时", err_sub_drop_type: "只解析字幕类型文件", err_codec_t1: "无法播放视频编码 ({c})", err_codec_t2: "您的浏览器不支持该视频格式。<br>请点击下方按钮调用外部播放器。", err_pwd_simple: "密码错误", err_task_exists: "任务已存在", err_network_break: "图片节点网络断流,请再次点击重试", err_no_failed_task: "未选中失败的任务", err_unknown: "未知错误", err_invalid_regex: "无效的正则表达式", err_parent_not_found: "文件夹不存在", msg_sys_error: "不允许操作系统文件夹", msg_download_fail: "无法获取下载链接。", msg_video_fail: "无法获取视频链接。", err_star_sync_fail: "星标同步失败", err_paste_descendant: "不能移动或复制到当前或当前子目录下", err_quota_exceeded: "存储空间不足", err_name_exists: "文件名称不能重复", err_share_pass: "自定义提取码需为 4-10 位字符", str_error: "错误", str_error_crit: "严重错误", str_error_paste: "粘贴错误", str_action_failed: "操作失败", str_scan_error: "扫描错误", err_limit_too_low: "修改失败:新次数 ({n}) 必须大于当前已保存次数 ({s})", err_vault_max: "密码金库最多仅支持存储 50 个常用密码", err_pwd_len: "单个密码长度不能超过 127 个字符", /* --- 帮助文档 --- */ modal_help_title: "帮助", help_desc: ` <div class="pk-no-scrollbar pk-help-scroll" style="font-size:13px;line-height:1.6;color:var(--pk-fg);text-align:justify;text-justify:inter-ideograph;word-break:break-all;pointer-events:auto;display:block;"> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">✨ 体验与导航引擎</b><br> • <b>交互重构</b>:在官方功能基础上,界面仿 <b>Windows 文件资源管理器</b> 重构。<br> • <b>极速模式</b>:开启后接管原生逻辑,彻底解决海量文件下的卡顿与崩溃问题。<br> • <b>高级路径栏</b>:支持滚轮滑动、下拉菜单同级切换定位。全盘搜索、分析套件均集成路径栏,支持路径回显与溯源跳转。<br> • <b>体验增强</b>:支持星标等多维度排序,及一键<b>模糊封面</b>与暗黑皮肤切换。后台采用 <b>SWR 策略</b>静默无感刷新视图。<br> • <b>后台索引与保护</b>:主页蓝点闪烁表示正同步目录树。系统自带并发操作物理锁,拦截冲突操作,严防脏数据产生。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:默认文件夹(My Pack)受官方保护,严防误删、复制、移动及重命名。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">📂 批量与空间管理</b><br> • <b>批量重命名</b>:支持<b>正则替换/删除</b>、<b>剧集流水号</b>、文本<b>格式化</b>、<b>FC2 规范命名</b>、<b>前缀去广告</b>及基于 MIME 的<b>后缀修复</b>。<br> • <b>分析套件</b>:<b>文件分析</b>整合了筛选与查重(哈希/时长/名称三模态);<b>文件夹分析</b>整合了筛选与查重(名称/相似度/包含率三模态);并支持导出当前<b>目录树</b>列表。<br> • <b>智能整理</b>:一键清理空文件夹;<b>批量解压</b>集成密码自动记忆与智能填充,支持跳过并删除已解压项。<br> • <b>资源管理器</b>:自定义<b>文件黑名单</b>一键清理垃圾资源;或作为<b>文件白名单</b>,在批量删除时自动保护。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:为避免数据同步冲突,处理期间请勿在其他客户端修改文件。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🌐 传输与分享中心</b><br> • <b>分享管理</b>:支持设定提取次数上限,次数达标后链接自动失效取消分享。<br> • <b>极速上传</b>:支持全局将本地文件/文件夹拖拽至网页直传,突破官方限制并<b>大幅降低小文件传输中断率</b>。<br> • <b>云下载增强</b>:批量离线链接<b>自动去重</b>。内置<b>磁链智能清洗引擎</b>(自动提取 Base32/Hex 哈希去干扰);支持解析 <b>.torrent</b> 种子文件;针对受限链接提供<b>保存网页快照</b>兜底方案。 <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:提取次数拦截仅在网页保持开启且电脑未休眠时生效。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🎬 沉浸式媒体增强</b><br> • <b>播放引擎</b>:支持 0.5x-3.0x 倍速、旋转翻转、强制比例、自动跳过片头片尾及<b>连播/循环</b>模式,进度条支持缩略图预览。内置<b>看门狗</b>,遇黑屏或不支持编码自动回退兼容画质。<br> • <b>字幕系统</b>:支持加载云端同名字幕、本地文件及跨站在线搜索。支持字幕轴毫秒级偏移微调,及本地文本直接<b>拖拽解析</b>挂载。<br> • <b>视觉辅助</b>:内嵌多引擎支持图片或视频当前帧<b>以图搜图</b>;设置中可激活“媒体模式”,使剧集/漫画文件夹自动按名称 A-Z 顺序排列。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:播放历史列表持续记录在脚本环境内产生的播放进度。</div> </div> <div style="margin-bottom:12px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚙️ 配置与数据管理</b><br> • <b>配置备份</b>:支持将偏好设置、管理规则、密码金库等导出为带数字指纹的 JSON 备份文件,导入时支持<b>智能合并去重</b>。<br> • <b>数据清理</b>:支持对全盘索引、偏好设置、管理规则、密码金库与缓存按需清除,释放本地空间并保障隐私。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:全盘索引在网页关闭后清空,而偏好设置、密码金库等则持久化保存。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚡ 下载与分发</b><br> • <b>外部直连</b>:支持一键获取视频流直链,或唤起 PotPlayer 播放。支持将文件通过 RPC 协议一键推送到 <b>Aria2</b> 节点。<br> • <b>分发增强</b>:推送文件夹至 Aria2 时<b>自动还原云盘树状目录结构</b>。支持长连接监控,遇错自动导出错误清单。支持设置<b>文件夹下载过滤</b>。 </div> <div style="margin-top:16px; color:#d93025; font-weight:bold; text-align:center; font-size:11px; border-top:1px dashed rgba(217,48,37,0.2); padding-top:12px; letter-spacing:0.5px; opacity:0.9;"> 本项目严格遵循 CC-BY-NC-SA-4.0 协议,严禁用于任何商业用途 </div> </div>` }, tc: { /* --- 通用与基础UI --- */ title: "PikPak 增強大師", str_original: "原畫", str_original_fast: "原畫 (高速)", str_folders: "資料夾", str_files: "檔案", unit_folders: "個資料夾", unit_days: "天", unit_month: "月", unit_sec: "秒", str_no_files: "暫無檔案", str_items: "項", col_name: "名稱", col_size: "大小", col_dur: "類型/時長", col_duration_only: "時長", col_progress: "播放進度", col_play_time: "播放時間", col_date: "修改日期", col_remaining: "剩餘時長", col_path: "路徑", col_old: "原名稱", col_new: "新名稱", col_type: "類型", col_path_name: "路徑 / 名稱", col_action: "操作", lbl_folder_first: "資料夾置頂", tag_default: "預設", current_dir: "目前目錄", str_same_folder: "(同資料夾)", lbl_dont_show: "不再提醒", lbl_dont_show_session: "本次查重不再提示", str_empty_filename: "(空檔名)", str_empty_dir: "(空目錄)", btn_filter: "篩選", title_file_filter: "檔案篩選", cat_all: "全部", cat_video: "影片", cat_audio: "音訊", cat_image: "圖片", cat_document: "文件", cat_software: "軟體", cat_archive: "壓縮檔", cat_torrent: "BT種子", cat_other: "其他", btn_exit_filter: "退出篩選", /* --- 属性面板 --- */ ctx_property: "屬性", title_property: "檔案屬性", lbl_prop_name: "檔案名稱", lbl_prop_size: "檔案大小", lbl_prop_count: "檔案數量", lbl_prop_ctime: "建立時間", lbl_prop_mtime: "修改時間", lbl_prop_source: "加入來源", lbl_prop_link: "資源連結", lbl_prop_path: "檔案位置", str_prop_cloud: "雲端加入", str_prop_share: "來自分享", str_prop_user: "使用者上傳", str_prop_unknown: "未知來源", fmt_prop_count: "包含 {f} 個檔案,{d} 個資料夾", str_prop_offline: "離線任務", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "首頁", btn_nav_share: "我的分享", btn_nav_offline: "離線下載", btn_nav_recent: "最近加入", btn_nav_history: "播放歷史", btn_nav_starred: "收藏夾", btn_nav_trash: "回收站", btn_nav_upload: "我的上傳", title_offline: "我的離線", trash_title: "回收站", trash_notice: "資源回收筒的檔案最多儲存 15 天", history_notice: "僅記錄在腳本環境內產生的播放進度", ctx_open: "開啟", ctx_add_bl: "添加到資源管理器", ctx_remove_bl: "從資源管理器移除", ctx_rename: "重新命名", ctx_copy: "複製", ctx_copy_name: "複製檔名", ctx_copy_link: "複製連結", ctx_cut: "移動", ctx_del: "刪除", ctx_down: "下載", ctx_star: "加入星號", ctx_unstar: "取消星號", ctx_locate: "在資料夾中檢視", ctx_share: "分享", /* --- 通用文件操作按钮 --- */ btn_down: "下載", tip_down: "下載 [Alt] + [D]", btn_aria2: "傳送至 Aria2", tip_aria2: "傳送至 Aria2 [Alt] + [A]", btn_refresh_short: "刷新", tip_refresh: "刷新 [F5]", btn_newfolder: "新增資料夾", tip_newfolder: "新增資料夾 [F8]", btn_del: "刪除", tip_del: "刪除 [Delete]", btn_deselect: "取消選取", tip_deselect: "取消選取 [Esc]", btn_invert: "反向選取", btn_copy: "複製", tip_copy: "複製 [Ctrl] + [C]", btn_cut: "移動", tip_cut: "移動 [Ctrl] + [X]", btn_paste: "貼上", tip_paste: "貼上 [Ctrl] + [V]", btn_clear_history: "刪除歷史", tip_clear_history: "刪除歷史 [Delete]", btn_restore: "還原", tip_restore: "還原 [R]", btn_del_forever: "永久刪除", tip_del_forever: "永久刪除 [Delete]", btn_empty_trash: "清空資源回收筒", tip_empty_trash: "清空資源回收筒 [Shift] + [Delete]", btn_exit: "退出", btn_close: "關閉", tip_close: "關閉 [Esc]", tip_theme: "切換主題 [Alt] + [T]", tip_rotate: "旋轉 [R]", tip_mirror: "鏡像翻轉 [H]", tip_flip_v: "垂直翻轉 [V]", tip_maximize: "最大化 [M]", tip_minimize: "最小化 [M]", tip_full_screen: "全螢幕 [Enter]", btn_help: "說明", tip_help: "說明 [Alt] + [H]", btn_view_file: "檢視檔案", btn_jump: "跳轉", btn_copy_text: "複製", btn_stop: "停止", tip_stop: "立即停止目前操作", btn_settings: "設定", btn_logout: "登出 PikPak", msg_logout_confirm: "確定要登出嗎?", tip_settings: "設定與更多 [Alt] + [S]", lbl_upload_to: "上傳檔案至: ", msg_move_done: "移動完成。", /* --- 离线、上传与云下载 --- */ btn_upload: "本機上傳", btn_up_file: "上傳檔案", btn_up_folder: "上傳資料夾", btn_cloud_download: "雲下載", btn_up_pause: "暫停任務", tip_up_pause: "暫停任務 [Alt] + [P]", btn_up_start: "開始任務", tip_up_start: "開始任務 [Alt] + [G]", btn_up_del: "刪除任務", tip_up_del: "刪除任務 [Delete]", btn_up_clear_all: "清空任務", tip_up_clear_all: "清空任務 [Shift] + [Delete]", btn_retry_task: "重試任務", tip_retry_task: "重試任務 [R]", col_task_status: "任務狀態", col_task_progress: "離線進度", col_up_speed: "速度", col_up_status: "狀態", lbl_task_run: "進行中", lbl_task_fail: "已失敗", lbl_task_ok: "已完成", lbl_up_run: "進行中", lbl_up_pause: "已中斷", lbl_up_downloading: "下載中", lbl_up_done: "已完成", tip_up_pause_desc: "包含手動暫停及發生錯誤的任務", title_cloud_task: "建立雲端下載任務", ph_cloud_links: "支援連結格式:\n- 各種下載連結,如 magnet。\n- YouTube、X (Twitter)、TikTok、Facebook 等分享連結。\n透過換行可一次加入多條連結。", lbl_save_to: "檔案將被儲存至:", lbl_default_folder: "預設資料夾", btn_via_torrent: "透過 Torrent 建立", tip_cloud_save_path: "正規雲端下載檔案會儲存在 My Pack 目錄,來自其他 App 的雲端下載檔案會儲存至 My [XYZ] 目錄,[XYZ] 為 App 名稱。", lbl_smart_fix: "自動修復防屏蔽磁鏈 (提取特徵碼/剔除文字干擾)", title_save_method: "儲存方式", msg_save_snapshot_desc: "此連結只能被儲存為網頁快照。", tip_snapshot_details: "PikPak 無法從此連結中直接擷取媒體檔案,您可以儲存網頁快照。PikPak 將盡可能儲存完整的網頁內容到快照檔案中。", btn_save_snapshot: "儲存快照", btn_create_now: "立即建立", btn_modify: "修改", str_snap_link_count_suffix: " 等 {n} 個連結", /* --- 分享管理 --- */ btn_cancel_share: "取消分享", share_copy_suffix: "複製這段內容後開啟 PikPak App,暢享極速秒播", share_copy_pwd: "密碼", title_share_detail: "分享詳情", ctx_share_detail: "檢視分享詳情", ctx_share_copy: "複製連結和密碼", col_view: "瀏覽", col_save: "儲存", col_share_time: "分享時間", col_share_status: "分享狀態", lbl_limit_reached: "次數已滿", lbl_limit_tip: "限制次數", lbl_share_view: "瀏覽", lbl_share_save: "儲存", lbl_share_link_title: "分享連結", lbl_share_pwd_title: "密碼", lbl_share_expire_title: "有效期限", btn_copy_link_pwd: "複製連結和密碼", str_expire_suffix: "天後過期", ph_edit_pwd: "輸入分享密碼,支援 4-10 個字元", btn_close_pwd: "關閉密碼", str_no_pwd: "無密碼", title_edit_share_code: "分享代碼修改", ph_edit_share_code: "支援 5-18 位文字、數字及符號等", btn_add_share_code: "加入分享代碼", btn_del_share_code: "刪除分享代碼", share_title: "分享檔案", share_mode: "分享方式", share_public: "公開連結", share_encrypted: "加密連結", share_expiry: "設定有效期限", share_pass: "設定提取碼", share_count: "設定提取次數", share_count_ed: "提取次數", share_perm: "永久有效", share_unlimit: "不限", share_rand: "系統隨機", share_custom: "自訂", share_days: "天", share_times: "次", btn_share_start: "立即分享", cal_custom_title: "自訂有效期限", lbl_share_link: "連結", lbl_share_code: "提取碼", btn_copy_share: "複製全部", str_share_expired: "已過期", str_share_deleted: "檔案已刪", title_edit_pwd: "密碼修改", lbl_share_code_title: "分享代碼", ph_password: "密碼", ph_pass_range: "4-10位字元", cal_week_days:["日", "一", "二", "三", "四", "五", "六"], cal_months:["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "檔案分析", btn_scan: "文件透視", lbl_scan_selected: "對選取的 {n} 個項目執行文件透視", lbl_keyword_filter: "排除關鍵字", ph_keyword_filter: "排除包含的關鍵字,多個用逗號分隔", lbl_scan_current: "對目前路徑下所有項目執行文件透視", tip_dup: "檔案查重", lbl_dup_selected: "對選取的 {n} 個項目執行文件查重", lbl_dup_current: "對目前路徑下所有項目執行文件查重", tip_scan_dup: "篩選或查重檔案", lbl_dup_tool: "選擇刪除對象:", lbl_dup_reset: "↺ 復原 (取消置頂 & 清空選取)", lbl_dup_select_folder: "📂 依資料夾選擇", lbl_dup_select_folder_short: "📂 資料夾", lbl_dup_invert: "反向選取模式", lbl_dup_invert_short: "反向選取", tip_dup_invert_limit: "僅依資料夾選擇時可用", fmt_dup_count: "({n}個重複)", btn_start_scan: "開始掃描", tag_hash: "精準比對", tag_hash_short: "精準", tag_name: "名稱相似", tag_name_short: "名稱", tag_sim: "時長相似", tag_sim_short: "時長", label_dup_video: "影片檔案 (精準比對 + 時長相似 + 名稱相似)", label_dup_image: "圖片檔案 (精準比對 + 名稱相似)", label_dup_other: "其他檔案 (精準比對 + 名稱相似)", btn_analyze: "資料夾分析", tip_analyze: "篩選或查重資料夾", btn_export: "匯出目錄", tip_export: "產生並下載目前路徑的檔案樹狀清單", title_export_format: "匯出目錄樣式", lbl_export_current: "對目前路徑下所有項目執行目錄匯出", opt_tree_view: "目錄樹", opt_list_view: "目錄列表", msg_exporting: "正在產生目錄樹...", str_analyze_results: "比對結果", lbl_size_threshold: "偵測閾值", title_analyze_result: "資料夾分析結果", opt_ana_large: "資料夾透視", lbl_analyze_selected: "對選取的 {n} 個項目執行資料夾透視", lbl_analyze_current: "對目前路徑下所有項目執行資料夾透視", opt_ana_sim: "資料夾查重", lbl_ana_sim_selected: "對選取的 {n} 個項目執行資料夾查重", lbl_ana_sim_current: "對目前路徑下所有項目執行資料夾查重", title_algo_help: "查重演算法說明", algo_help_content: "名稱比對:查找名稱和體積相近的資料夾群組。\n相似度比對:查找內部檔案高度重合的資料夾群組。\n包含率比對:查找小資料夾的內容被大資料夾完全覆蓋的子集冗餘。\n\n精度:相似度比對 > 包含率比對 > 名稱比對\n範圍:包含率比對 > 相似度比對", lbl_threshold: "閾值", lbl_sim_score: "相似度", lbl_containment: "包含率", lbl_name_match: "名稱比對", lbl_sim_match: "相似度比對", lbl_contain_match: "包含率比對", lbl_ana_min: "下限", lbl_ana_max: "上限", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "清理空資料夾", tip_prune: "清理空資料夾 [Ctrl] + [Delete]", btn_rename: "重新命名", tip_rename: "重新命名 [F2]", btn_bulkrename: "批次重新命名", tip_bulkrename: "批次重新命名 [F2]", title_blacklist: "資源管理器", btn_blacklist_run: "立即執行清理", btn_clear_list: "清空清單", tip_bl_desc: "下列項目在【刪除】時會跳過,僅透過【立即執行清理】尋找並刪除", tip_blacklist_input: "資源管理器 [Alt] +[Delete]", label_bl_folder: "資料夾名單 (精準查找)", label_bl_file: "檔案名單 (精準查找)", lbl_type_folder: "資料夾", lbl_type_file: "檔案", ph_bl_folder: "請透過「貼上」或檔案右鍵選單「添加到資源管理器」匯入。", ph_bl_file: "請透過「貼上」或檔案右鍵選單「添加到資源管理器」匯入。", modal_bl_preview: "搜尋結果", btn_bl_delete: "刪除選取項目", modal_rename_title: "重新命名", modal_rename_multi_title: "批次重新命名", btn_preview: "預覽", modal_preview_title: "確認變更", label_pattern: "模式 (例:Video {n})", label_replace: "取代/刪除", label_replace_note: "區分大小寫", label_include_ext: "包含副檔名", label_regex: "正規表示式 (Regex)", placeholder_find: "尋找內容", placeholder_replace: "取代為 (留空則刪除)", label_jav: "FC2 純淨命名", lbl_rn_pattern: "命名範本", lbl_rn_case_convert: "大小寫轉換", opt_rn_keep_origin: "(保持原樣)", opt_rn_lower: "全部小寫 (abc)", lbl_rn_mode_series: "影集模式", lbl_rn_mode_format: "格式化", lbl_rn_mode_ad: "前綴去廣告", lbl_rn_mode_ext: "副檔名修復", opt_rn_upper: "全部大寫 (ABC)", opt_rn_title: "首字母大寫 (Abc)", lbl_rn_width_convert: "全形半形轉換", opt_rn_width_half: "全形轉半形 (A->A)", opt_rn_width_full: "半形轉全形 (A->A)", lbl_rn_preview_title: "變更預覽", tip_jav_mode_desc: "✨ 智慧提取 FC2 並去除無關字元", tip_ad_remove_desc: "🧹 智慧過濾頭部廣告、網址及垃圾符號,自動清除 Emoji 並修復括號格式", tip_ext_fix_desc: "🧩 根據檔案真實類型 (MIME) 智慧修正副檔名", label_replace_find: "尋找內容", label_replace_to: "取代為", /* --- 解压相关 --- */ btn_unzip: "批次解壓縮", tip_unzip: "批次解壓縮 [Alt] + [U]", btn_unzip_all: "全部解壓縮", btn_understand_unzip: "理解並解壓縮", title_input_pwd: "需要解壓縮密碼", lbl_pwd_prompt: "請輸入密碼:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "外部播放", tip_ext: "使用PotPlayer播放或獲取播放連結 [Alt] +[E]", btn_img_search: "以圖搜圖 [F]", tip_play_search: "以圖搜圖 [F]", tip_pip: "子母畫面 [P]", str_no_sub: "無字幕", lbl_sub_sel: "字幕選擇", lbl_show_sub: "顯示字幕", btn_sub_search: "搜尋線上字幕", btn_sub_cloud: "開啟雲端字幕", btn_sub_local: "開啟本機字幕", lbl_sub_pos: "字幕位置", lbl_sub_bottom: "底部", lbl_sub_top: "頂部", lbl_sub_bg_op: "背景透明", lbl_sub_size: "字幕大小", lbl_sub_offset: "字幕進度", title_sel_sub: "選擇字幕", ph_sub_search: "輸入關鍵字,下方連結將自動更新...", btn_force_play: "嘗試強制播放", str_compat_mode: "相容模式", lang_code: "zh-TW", btn_go_search: "🔍 去 {n} 手動搜尋", btn_restart: "從頭播放", btn_prev_video: "上一個 [Ctrl + ←]", btn_next_video: "下一個 [Ctrl + →]", tip_plist_open: "展開 [E]", tip_plist_close: "收合 [E]", tab_sub: "字幕", tab_size: "尺寸", tab_more: "更多", lbl_ratio: "比例", lbl_direction: "方向", opt_ratio_def: "預設", btn_rot_l: "向左旋轉", btn_rot_r: "向右旋轉", btn_flip_h: "水平翻轉", btn_flip_v: "垂直翻轉", lbl_play_end: "當播放結束", opt_list_loop: "清單循環", opt_single_loop: "單集循環", opt_play_stop: "播完暫停", lbl_skip_op: "跳過片頭", lbl_skip_ed: "跳過片尾", export_link_title: "匯出影片串流連結", btn_start_play: "開始播放", btn_copy_link: "複製連結", tip_copy_link: "複製連結 [Alt] + [C]", opt_player_other: "其他 (匯出連結)", lbl_player: "播放器", btn_mark: "標記", lbl_resolution: "畫質", str_switch_compat: "目前畫質無法使用,已為您恢復至 {n}", type_img: "影像", type_doc: "文件", type_archive: "壓縮檔", type_sub: "字幕檔", type_app: "應用程式", type_suffix: "檔案", /* --- 设置与搜索 --- */ label_turbo_mode: "極速模式", desc_turbo_mode: "自動開啟並替代網頁介面 (推薦)", lbl_aria2_status: "連線狀態", ph_aria2_secret: "密鑰 (選填)", str_connected: "連線成功", str_conn_fail: "連線失敗", str_connecting: "正在測試...", tip_mixed_content: "常用連接埠參考:\n• 6800 (Aria2 標準版)\n• 16800 (Motrix 預設)\n• 6881 (其他整合版)", picker_title: "選擇資料夾", picker_all: "全部檔案", picker_new: "新增資料夾", picker_sort_new: "最新", picker_sort_old: "最舊", title_select_file: "選擇檔案", placeholder_search: "搜尋檔案...", placeholder_search_short: "搜尋", title_search_hist: "搜尋歷史", btn_clear_hist: "清空", lbl_global_search: "全盤搜尋", lbl_search_path: "搜尋包含路徑", lbl_search_path_short: "路徑", str_search_results: "搜尋結果", modal_settings_title: "設定", label_lang: "語言 (Language)", label_thumb: "模糊縮圖 (隱私模式)", label_keep_pos: "保持瀏覽位置 (返回時定位)", label_sort_pref: "排序偏好", opt_sort_indep: "每個資料夾獨立", opt_sort_global: "全部相同", desc_sort_indep: "調整排序方式僅對目前資料夾生效", desc_sort_global: "所有資料夾使用相同的排序 (時間倒序)", label_search_engine: "搜圖引擎", opt_engine_google: "Google Lens (綜合)", opt_engine_yandex: "Yandex (綜合)", opt_engine_saucenao: "SauceNAO (Pixiv/插畫)", opt_engine_tracemoe: "trace.moe (動漫截圖)", label_dup_strictness: "相似比對閾值", opt_strict: "嚴格", opt_loose: "寬鬆", label_comic_mode: "媒體模式", desc_comic_mode: "純圖片或純影片資料夾預設依 A-Z 排序", label_aria2_url: "Aria2 位址", label_aria2_token: "Aria2 密鑰", label_privacy_mode: "隱私模式", label_blur_cover: "模糊封面圖", label_dl_filter_ext: "下載字尾過濾 (例: .txt, .jpg)", label_dl_filter_name: "下載名稱過濾 (關鍵字或全名)", lbl_dl_filter: "資料夾下載過濾", desc_dl_filter: "資料夾下載/推送時自動排除匹配的檔案", lbl_config_manage: "配置管理", btn_export_data: "匯出備份", btn_import_data: "匯入備份", btn_clean_data: "清除本機資料", title_clean_data: "選擇清理項目", msg_clean_confirm: "確定要徹底刪除選取的本機資料嗎?此操作無法還原。", msg_clean_success: "本機資料已清理,頁面即將重新整理...", opt_cfg_index: "全盤索引 (已同步的目錄結構/檔案快照)", opt_cfg_pref: "偏好設定 (UI 外觀/操作習慣/排序偏好)", opt_cfg_rules: "管理規則 (資源管理器/分享次數限制/搜尋紀錄/下載規則)", opt_cfg_vault: "密碼金庫 (解壓縮密碼記憶)", opt_cfg_history: "影片快取 (影片播放進度/影片時長快取)", opt_cfg_cache: "運行快取 (資料夾修改時間/最後瀏覽位置/指紋)", msg_import_confirm: "匯入的配置將與目前設定合併(合併名單/紀錄,覆蓋衝突的基礎設定),是否繼續?", msg_import_success: "配置匯入成功,頁面即將重新整理...", err_invalid_config: "無效的設定檔:未偵測到指紋標識或格式錯誤", err_json_format: "檔案解析失敗:JSON 語法錯誤或檔案已損壞", lbl_storage: "儲存空間", lbl_browse_exp: "瀏覽體驗", lbl_skip_bl_on_del: "刪除時跳過管理器中記錄資源", lbl_pwd_manage: "解壓密碼管理", title_pwd_vault: "密碼金庫", lbl_pwd_try_count: "單個壓縮檔密碼匹配上限", tip_pwd_manual: "每行記錄一個密碼,回車換行", str_root_dir_cn: "根目錄", btn_ana_select: "一鍵勾選", opt_keep_new: "保留最新的", opt_keep_old: "保留最舊的", opt_keep_large: "保留最大的", opt_keep_small: "保留最小的", opt_keep_short: "保留名稱最短的", opt_keep_long: "保留名稱最長的", /* --- 状态、进度与加载短语 --- */ loading: "載入中...", loading_detail: "正在全速索引目錄結構...", loading_fetch: "取得中... ({n})", loading_dup: "分析重複項目... ({p}%)", str_loading_placeholder: "載入中...", str_load_failed: " (載入失敗)", str_load_failed_simple: "載入失敗", str_waiting_token: "正在同步登入狀態...", str_speed: "速度", status_scanning_selection: "掃描選取項目... {n}", status_ready: "{n} 個項目", sel_count: "已選取 {n} 個項目", str_cached: "已快取:", str_retries: "重試:", str_failed: "失敗:", str_success: "成功:", str_stopping: "正在停止...", str_merging: "合併資料中...", str_rendering: "渲染清單中...", str_scanning: "掃描中...", str_analyzing: "分析中...", str_deleting: "刪除中...", str_saving: "儲存中...", str_saving_dots: "儲存中...", str_checking_bl: "匹配名單記錄中...", str_processing: "系統正在全速處理中...", str_cleanup_done: "清理完成。", str_waiting_preload: "等待預先載入...", str_copying: "複製到剪貼簿...", str_moving: "準備移動...", str_sorting: "正在排序...", str_refreshing: "重新整理中...", str_refreshing_cache: "重新整理快取中...", str_syncing_stars: "同步星號狀態...", str_updating_view: "更新視圖中...", str_generating_view: "產生視圖中...", str_group: "組", str_init_rename: "初始化重新命名...", str_renaming: "重新命名中...", str_calc_changes: "計算變更中...", str_scanning_dir: "掃描目錄結構...", str_init_op: "初始化操作...", str_init_scan: "正在初始化全盤掃描...", str_rebuilding: "正在重建索引...", str_upload_1: "正在上傳 (節點 1/3)...", str_upload_2: "節點1超時,切換節點 2...", str_upload_3: "節點2超時,嘗試最後節點...", str_upload_fail_copy: "上傳失敗,準備寫入剪貼簿...", msg_transcoding: "雲端轉碼中...", msg_transcoding_wait: "伺服器正在處理此影片,請稍候", str_preparing: "準備解壓縮...", str_unzipping: "正在解壓縮:{n}", str_unzipping_state: "解壓縮中...", str_unzipping_prog_0: "解壓縮中:0%", str_unzipping_prog_100: "解壓縮中:100%", str_unzipping_prog_fmt: "解壓縮中:{n}%", msg_task_waiting: "等待中...", msg_task_hashing: "校驗檔案中...", msg_task_init_upload: "初始化上傳...", msg_task_uploading: "正在上傳...", msg_task_init_part: "初始化分片...", msg_task_uploading_2: "正在上傳...", str_loc_tracing: "正在回溯路徑...", str_loc_stale: "快取已過期,正在同步雲端...", str_verifying: "正在驗證...", str_server_indexing: "伺服器索引中... ({n}/5)", str_creating_task_n: "建立任務 ({n}/{t})...", msg_submit_request: "正在提交請求... {c}/{t}", msg_wait_server: "等待伺服器處理... ({c}/{t})", msg_server_processing: "伺服器處理中... ({c}/{t})", str_jav_querying: "正在查詢...", lbl_done_check: "✔ 完成", msg_limit_updated: "提取次數已更新", /* --- 提示、确认与交互消息 --- */ title_alert: "提示", title_confirm: "確認", title_prompt: "輸入", btn_ok: "確定", btn_yes: "是", btn_no: "否", btn_save: "儲存設定", btn_cancel: "取消", btn_create: "建立", btn_skip: "跳過", msg_down_success: "已成功呼叫瀏覽器下載 {n} 個檔案。", msg_batch_txt: "已產生下載清單 (.txt)。", msg_clear_history_done: "已從歷史紀錄中移除", msg_skip_unzipped: "已跳過 {n} 個已解壓縮的項目。", msg_unzip_skip_del_confirm: "偵測到 {n} 個已解壓縮的壓縮檔,是否將其移入資源回收筒?", msg_cancel_share_confirm: "確定要取消選取的 {n} 個分享嗎?\n連結將立即失效。", msg_pwd_updating: "正在更新密碼...", msg_pwd_updated: "密碼已更新", msg_exp_updated: "有效期限已更新", msg_cancel_share_done: "已取消 {n} 個分享。", msg_drag_drop_hint: "將檔案拖曳到此處並放開", str_drag_files: " 等 {n} 個檔案", msg_creating_share: "正在建立分享...", title_share_result: "分享成功", msg_no_files: "沒有項目。", msg_no_selection: "請先選擇項目。", warn_del: "確定要刪除選取的 {n} 項嗎?", msg_clear_sel_confirm: "已選取 {n} 個重複檔案,確認要取消目前的勾選嗎?", str_bl_stat: "比對:{n} 項 | 已選取:{m} 項", str_hits: "命中", msg_settings_saved: "設定已儲存。頁面將重新整理。", msg_name_exists: "名稱已存在:{n}", str_name_conflict: "(可能同名)", msg_newfolder_prompt: "新資料夾名稱:", msg_rename_prompt: "輸入新名稱:", msg_copy_done: "已複製。請選擇貼上位置。", msg_cut_done: "準備移動。請選擇貼上位置。", msg_paste_empty: "沒有可貼上的項目。", msg_copy_empty: "剪貼簿為空或無文字資料。", msg_add_success: "已附加 {n} 列資料。", msg_del_done: "已刪除選取列。", msg_del_select: "請先點擊選取要刪除的列!", msg_del_items_done: "已刪除 {n} 個項目。", msg_copy_success: "複製成功", str_redirecting: "正在跳轉 Google Lens...", msg_manual_paste: "圖床上傳超時。已複製截圖,請在新視窗按 {cmd}", msg_starring: "正在加入星號...", msg_unstarring: "正在取消星號...", msg_star_added: "已加入星號", msg_unstar_done: "已取消星號", msg_empty_trash_confirm: "確定要清空資源回收筒嗎?此操作無法還原!", msg_trash_emptied: "資源回收筒已清空。", msg_del_forever_confirm: "確定要徹底刪除這 {n} 個項目嗎?此操作無法還原!", msg_del_forever_done: "已徹底刪除 {n} 個項目。", msg_restore_done: "已成功還原 {n} 個項目。", msg_auto_sub_load: "已自動載入字幕:{n}", msg_dl_sub: "正在下載字幕...", msg_transcode_done: "✅ 轉碼完成,開始播放", msg_fallback_report: "⚠️ 原畫無法播放 (MPEG4/HEVC),已自動切換至 {n}", tip_manual_sub: "提示:下載 .srt 或 .vtt 後,直接拖入播放器即可載入。", msg_sub_drop_load: "已透過拖曳載入字幕:{n}", msg_resume_hint: "已為您從 {t} 繼續播放,點擊這裡 ", msg_unzip_confirm_n: "確定要在目前目錄解壓縮這 {n} 個檔案嗎?", msg_task_paused: "已暫停", msg_task_added: "已加入 {n} 個上傳任務", msg_task_fast_success: "秒傳成功", msg_task_upload_done: "上傳完成", log_dirty_data: "攔截到髒資料入侵!請求模式與目前視圖不符,已強制中斷。", msg_network_unstable: "網路連線波動,正在自動恢復...", msg_skip_locked: "{n} 個被鎖定", msg_skip_self: "{n} 個原地移動", msg_skip_conflict: "{n} 個子路徑忙碌", msg_skip_invalid: "已自動跳過無效項目:", msg_creating_cloud_task: "正在建立雲端下載任務...", str_parsing_torrent: "正在解析種子檔案...", err_torrent_no_info: "解析失敗:未發現有效資訊", err_file_read: "檔案讀取失敗", msg_cloud_task_finish: "建立完成:{s} 成功,{f} 失敗", msg_cloud_task_success: "🎉 已成功建立 {n} 個任務", msg_prepare_restore: "準備還原...", msg_smart_matching_n: "正在智慧比對密碼 ({n}個)...", msg_system_busy_retry: "系統忙碌,等待重試 ({n}/5)...", msg_unzip_running_bg: "[{n}] 已在雲端解壓縮中,請稍後重新整理檢視", msg_share_code_updated: "分享代碼已更新", msg_retry_submitted: "已重試提交 {n} 個任務", msg_aria2_batch_fail_log: "\n\n檢測到失敗項較多,已為您自動匯出完整錯誤清單 (.txt)", str_aria2_fetch_err: "(獲取連結失敗)", str_aria2_rpc_err: "(投遞失敗)", str_aria2_aborted: "(已取消)", str_aria2_fail_file_name: "Aria2_失敗清單", msg_op_blocked_moving: "⚠️ 操作攔截\n\n背景正在執行檔案搬移,請等待目前任務完成後再操作。", msg_op_blocked_analyzing: "⚠️ 操作攔截\n\n後台正在執行檔案搬移。為確保資料夾分析數據準確,請等待目前任務完成後再操作。", msg_op_blocked_exporting: "⚠️ 操作攔截\n\n後台正在執行檔案搬移。為確保匯出目錄文本準確,請等待目前任務完成後再操作。", msg_analyze_only_normal_dir: "請選擇資料夾。", msg_analyze_no_large_folders: "沒有發現符合閾值範圍的資料夾 ({s})", msg_analyze_summary_fmt: "共發現 <b>{n}</b> 個大於 {s}GB 的資料夾 (已依大小降序排列)", msg_prune_blocked_moving: "⚠️ 操作攔截\n\n背景正在執行檔案搬移,暫時無法執行清理。", msg_global_index_blocked_moving: "⚠️ 操作攔截\n\n背景正在執行檔案搬移。全盤索引需要穩定的目錄結構,請等待目前任務完成後再開啟全盤搜尋。", msg_resource_locked_download: "⚠️ 操作攔截\n\n選取項目包含正在移動的檔案或資料夾。請等待搬移完成後再發起下載。", msg_resource_locked_aria2: "⚠️ 操作攔截\n\n選取項目包含正在移動的檔案或資料夾。請等待搬移完成後再推送至 Aria2。", msg_flatten_blocked_moving: "⚠️ 操作攔截\n\n後台正在執行檔案搬移。此時執行文件分析會導致檔案列表缺失或重複,請稍後再試。", err_task_conflict: "⚠️ 操作攔截\n\n背景正在執行檔案搬移。全域清理需要穩定的目錄結構,請等待搬移完成後再執行。", title_del_task_confirm_fmt: "確認刪除 {n} 條傳輸任務?", lbl_del_cloud_files_too: "同時刪除雲端硬碟內的檔案", msg_file_del_failed: "檔案刪除失敗:", msg_task_del_success_fmt: "已刪除 {n} 個任務", title_clear_task_confirm: "確認清空所有上傳任務?", msg_task_clear_success_fmt: "已清空 {n} 個上傳任務", msg_unzip_virtual_view_warn: "您正在虛擬視圖中操作。解壓縮後的檔案將存放在<b>各壓縮檔所在的原始資料夾</b>中,而<b>不會</b>直接出現在目前清單中。<br><br>是否繼續?", msg_smart_matching_file: "智慧比對密碼中... ({n})", msg_unzip_batch_submitted: "✅ 已完成 {n} 個解壓縮任務", msg_unzip_batch_skipped: " ({n} 個跳過)", msg_unzip_check_source: "。請前往來源目錄檢視結果。", tip_jump_to_folder: "跳轉到此資料夾", msg_task_deleted: "任務已刪除", msg_scan_done: "掃描完成!\n共發現 {n} 個檔案,遍歷了 {f} 個資料夾。", msg_scan_fail: "\n\n❌ 有 {n} 個失敗。", msg_scan_fix: "\n\n✅ 自動修復了 {n} 次網路錯誤。", msg_down_scanning: "正在解析資料夾內容...", msg_down_progress: "正在呼叫瀏覽器下載...", msg_down_confirm_total: "✅ 掃描完畢,共找到 {n} 個檔案。\n\n⚠️ 警告:瀏覽器直接下載大量檔案極易導致頁面卡死或被攔截。\n建議超過 10 個檔案使用 Aria2 匯出。\n\n是否堅持使用瀏覽器下載?", msg_aria2_sending_batch: "🚀 正在分批傳送任務至 Aria2...", msg_aria2_check_fail: "Aria2 連線失敗!\n請檢查 URL 和 Token。", msg_aria2_check_ok: "Aria2 連線成功!", msg_aria2_sent: "已將 {n} 個檔案傳送到 Aria2。", msg_aria2_test_fail: "Aria2 連線失敗。\n仍然要儲存設定嗎?", title_aria2_fail: "連線測試失敗", msg_batch_scanning: "🚀 正在高速掃描目錄結構...", msg_batch_hydrating: "⚡ 正在平行擷取下載連結...", msg_batch_no_files: "未發現可下載的檔案。", msg_dup_warn: "是否開始搜尋重複檔案?", msg_dup_result: "發現 {n} 組重複項目。", msg_dup_none: "未發現重複檔案。", msg_bl_stop: "操作已停止。", msg_bl_add_done: "已將 {n} 個項目添加到記錄。", msg_bl_remove_done: "已從記錄中移除 {n} 個項目。", msg_bl_empty: "名單列表為空,無法運行。", msg_bl_clear_confirm: "確定要清空所有記錄條目嗎?此操作不可恢復。", msg_blacklist_run_none: "網盤中未發現符合名單條件的項目。", msg_blacklist_run_confirm: "在網盤中發現了 {n} 個已記錄項目。\n\n是否立即移入回收站?", msg_bl_run_limit: "⚠️ 模式限制\n\n清理操作涉及物理文件遞歸操作。目前處於非標準目錄,無法準確定位物理掃描範圍。\n\n請返回主頁常規文件夾後再執行清理。", msg_del_protected: "已保護 {n} 個已記錄文件不被刪除。", msg_del_none: "沒有可刪除的檔案。", msg_bl_scanning: "全盤搜尋中...\n已掃描目錄:{d} | 命中:{f}", rn_tip_wait: "請設定規則", rn_tip_jav: "點擊上方按鈕開始智慧匹配", rn_tip_none: "沒有符合的項目或名稱", rn_stat: "比對:{n} 項 | 有效變更:{m} 項", rn_warn_confirm: "確定要重新命名 {n} 個檔案嗎?", msg_bulkrename_done: "已重新命名 {n} 個項目。", msg_rn_all_skipped: "❌ 所有項目均因同名被跳過,未執行任何操作。", msg_rn_fail_count: "跳過已重名 {n} 個項目", msg_prune_confirm: "是否開始搜尋目前清單中的空資料夾?", msg_prune_none: "未發現空資料夾。", msg_prune_found: "發現了 {n} 個空資料夾。\n是否立即刪除?", msg_deleting_folders: "正在刪除 {n} 個資料夾...", msg_global_warn: "即將開始全盤檔案同步。\n\n檔案同步後快取到本機記憶體中,網頁重新整理前持續存在。\n\n是否繼續?", msg_init_scan_sel: "正在初始化選取項目掃描...", warn_clear_history: "確定要從歷史紀錄中移除選取的 {n} 項嗎?\n(這不會刪除您的雲端檔案)", msg_img_copy_hint: "在新視窗中,請按下 {cmd} 即可搜尋。", msg_aria2_not_set: "偵測到您尚未設定 Aria2,請填寫後繼續:", str_jav_no_match: "(未比對到番號)", msg_unzip_fail: "解壓縮請求失敗", msg_jszip_fail: "JSZip 載入失敗,請檢查網路。", msg_turbo_activated: "極速模式已激活:腳本已深度接管網頁邏輯,確保穩定流暢。", msg_console_legal: "嚴禁商業用途:本項目僅供個人學習與交流使用。", msg_ana_warn: "資料夾查重提示:判定基於演算法推測,刪除前請務必人工核對,以防誤刪。", /* --- 错误提示 --- */ err_invalid_links: "請輸入正確的連結", err_pwd_format: "密碼必須為 4-10 位字母或數字", err_invalid_torrent: "無效的種子檔案格式", err_torrent_complex: "解析複雜度過高,可能是非法檔案", err_torrent_format: "種子檔案結構損壞", err_torrent_len: "欄位長度解析異常", err_torrent_char: "解析遇到非法字符", err_share_code_exists: "該分享代碼已被佔用", err_folder_not_ready: "雲端資料夾正在建立中,請稍後再試", err_item_deleted: "該項目不存在", err_network: "網路錯誤", err_clipboard_denied: "剪貼簿存取被拒絕", err_worker: "工作執行緒錯誤", err_api: "API 錯誤", err_capture: "截圖失敗。", err_captcha_simple: "驗證失敗。請在網頁清單手動收藏一次檔案以完成驗證。", err_sub_dl_fail: "字幕下載失敗:", err_req_blocked: "網路請求失敗 (可能被攔截)", err_req_timeout: "請求超時", err_sub_drop_type: "僅解析字幕類型檔案", err_codec_t1: "無法播放影片編碼 ({c})", err_codec_t2: "您的瀏覽器不支援該影片格式。<br>請點擊下方按鈕呼叫外部播放器。", err_pwd_simple: "密碼錯誤", err_task_exists: "任務已存在", err_network_break: "圖片節點網路斷流,請再次點擊重試", err_no_failed_task: "未選取失敗的任務", err_unknown: "未知錯誤", err_invalid_regex: "無效的正規表示式", err_parent_not_found: "資料夾不存在", msg_sys_error: "不允許作業系統資料夾", msg_download_fail: "無法取得下載連結。", msg_video_fail: "無法取得影片連結。", err_star_sync_fail: "星號同步失敗", err_paste_descendant: "不能移動或複製到目前或目前子目錄下", err_quota_exceeded: "儲存空間不足", err_name_exists: "檔案名稱不能重複", err_share_pass: "自訂提取碼需為 4-10 個字元", str_error: "錯誤", str_error_crit: "嚴重錯誤", str_error_paste: "貼上錯誤", str_action_failed: "操作失敗", str_scan_error: "掃描錯誤", err_limit_too_low: "修改失敗:新次數 ({n}) 必須大於目前已儲存次數 ({s})", err_vault_max: "密碼金庫最多僅支援儲存 50 個常用密碼", err_pwd_len: "單個密碼長度不能超過 127 個字元", /* --- 帮助文档 --- */ modal_help_title: "說明", help_desc: ` <div class="pk-no-scrollbar pk-help-scroll" style="font-size:13px;line-height:1.6;color:var(--pk-fg);text-align:justify;text-justify:inter-ideograph;word-break:break-all;pointer-events:auto;display:block;"> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">✨ 體驗與導覽引擎</b><br> • <b>互動重構</b>:在官方功能基礎上,介面仿 <b>Windows 檔案總管</b> 重構。<br> • <b>極速模式</b>:開啟後接管原生邏輯,徹底解決海量檔案下的卡頓與崩潰問題。<br> • <b>進階路徑列</b>:支援滾輪滑動、下拉選單同級切換定位。全盤搜尋、分析套件均整合於路徑列,支援路徑回顯與溯源跳轉。<br> • <b>體驗增強</b>:支援星號等多維度排序,及一鍵<b>模糊封面</b>與深色佈景切換。後台採用 <b>SWR 策略</b>靜默無感重新整理視圖。<br> • <b>後台索引與保護</b>:首頁藍點閃爍表示正同步目錄樹。系統自帶併發操作物理鎖,攔截衝突操作,嚴防髒資料產生。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 註:預設資料夾(My Pack)受官方保護,嚴防誤刪、複製、移動及重新命名。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">📂 批次與空間管理</b><br> • <b>批次重新命名</b>:支援<b>正規替換/刪除</b>、<b>劇集流水號</b>、文字<b>格式化</b>、<b>FC2 規範命名</b>、<b>前綴去廣告</b>及基於 MIME 的<b>副檔名修復</b>。<br> • <b>分析套件</b>:<b>檔案分析</b>整合了篩選與查重(雜湊/時長/名稱三模態);<b>資料夾分析</b>整合了篩選與查重(名稱/相似度/包含率三模態);並支援匯出目前<b>目錄樹</b>清單。<br> • <b>智慧整理</b>:一鍵清理空資料夾;<b>批次解壓縮</b>整合密碼自動記憶與智慧填入,支援跳過並刪除已解壓縮項目。<br> • <b>資源管理器</b>:自訂<b>檔案黑名單</b>一鍵清理垃圾資源;或作為<b>檔案白名單</b>,在批次刪除時自動保護。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 註:為避免資料同步衝突,處理期間請勿在其他用戶端修改檔案。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🌐 傳輸與分享中心</b><br> • <b>分享管理</b>:支援設定提取次數上限,次數達標後連結自動失效取消分享。<br> • <b>極速上傳</b>:支援全域將本機檔案/資料夾拖曳至網頁直傳,突破官方限制並<b>大幅降低小檔案傳輸中斷率</b>。<br> • <b>雲下載增強</b>:批次離線連結<b>自動去重</b>。內建<b>磁鏈智慧清洗引擎</b>(自動提取 Base32/Hex 雜湊去干擾);支援解析 <b>.torrent</b> 種子檔案;針對受限連結提供<b>儲存網頁快照</b>兜底方案。 <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 註:提取次數攔截僅在網頁保持開啟且電腦未休眠時生效。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🎬 沉浸式媒體增強</b><br> • <b>播放引擎</b>:支援 0.5x-3.0x 倍速、旋轉翻轉、強制比例、自動跳過片頭片尾及<b>連播/循環</b>模式,進度列支援縮圖預覽。內建<b>看門狗</b>,遇黑螢幕或不支援編碼自動回退相容畫質。<br> • <b>字幕系統</b>:支援載入雲端同名字幕、本機檔案及跨站線上搜尋。支援字幕軸毫秒級偏移微調,及本機文字直接<b>拖曳解析</b>掛載。<br> • <b>視覺輔助</b>:內嵌多引擎支援圖片或影片當前影格<b>以圖搜圖</b>;設定中可啟動「媒體模式」,使劇集/漫畫資料夾自動按名稱 A-Z 順序排列。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 註:播放歷史列表持續記錄在腳本環境內產生的播放進度。</div> </div> <div style="margin-bottom:12px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚙️ 配置與資料管理</b><br> • <b>配置備份</b>:支援將偏好設定、管理規則、密碼金庫等匯出為帶數位指紋的 JSON 備份檔案,匯入時支援<b>智慧合併去重</b>。<br> • <b>資料清理</b>:支援對全盤索引、偏好設定、管理規則、密碼金庫與快取按需清除,釋放本機空間並保障隱私。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 註:全盤索引在網頁關閉後清空,而偏好設定、密碼金庫等則持久化保存。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚡ 下載與分發</b><br> • <b>外部直連</b>:支援一鍵獲取影片流直鏈,或喚起 PotPlayer 播放。支援將檔案透過 RPC 協定一鍵推播到 <b>Aria2</b> 節點。<br> • <b>分發增強</b>:推播資料夾至 Aria2 時<b>自動還原雲端樹狀目錄結構</b>。支援長連線監控,遇錯自動匯出錯誤清單。支援設定<b>資料夾下載過濾</b>。 </div> <div style="margin-top:16px; color:#d93025; font-weight:bold; text-align:center; font-size:11px; border-top:1px dashed rgba(217,48,37,0.2); padding-top:12px; letter-spacing:0.5px; opacity:0.9;"> 本項目嚴格遵循 CC-BY-NC-SA-4.0 協議,嚴禁於任何形式的商業用途 </div> </div>` }, en: { /* --- 通用与基础UI --- */ title: "PikPak Master Enhancer", str_original: "Original", str_original_fast: "Original (High Speed)", str_folders: "Folders", str_files: "Files", unit_folders: "folders", unit_days: "days", unit_month: "month", unit_sec: "sec", str_no_files: "No files found", str_items: "items", col_name: "Name", col_size: "Size", col_dur: "Type/Duration", col_duration_only: "Duration", col_progress: "Progress", col_play_time: "Play Time", col_date: "Date Modified", col_remaining: "Remaining", col_path: "Path", col_old: "Original Name", col_new: "New Name", col_type: "Type", col_path_name: "Path / Name", col_action: "Action", lbl_folder_first: "Folders on top", tag_default: "Default", current_dir: "Current Directory", str_same_folder: "(Same Folder)", lbl_dont_show: "Don't show again", lbl_dont_show_session: "Don't remind me this session", str_empty_filename: "(Empty Filename)", str_empty_dir: "(Empty Directory)", btn_filter: "Filter", title_file_filter: "File Filter", cat_all: "All", cat_video: "Videos", cat_audio: "Audio", cat_image: "Images", cat_document: "Documents", cat_software: "Software", cat_archive: "Archives", cat_torrent: "Torrent", cat_other: "Others", btn_exit_filter: "Exit Filter", /* --- 属性面板 --- */ ctx_property: "Properties", title_property: "File Properties", lbl_prop_name: "File Name", lbl_prop_size: "File Size", lbl_prop_count: "File Count", lbl_prop_ctime: "Created", lbl_prop_mtime: "Modified", lbl_prop_source: "Source", lbl_prop_link: "Resource Link", lbl_prop_path: "Location", str_prop_cloud: "Cloud Download", str_prop_share: "From Share", str_prop_user: "User Upload", str_prop_unknown: "Unknown Source", fmt_prop_count: "Contains {f} files, {d} folders", str_prop_offline: "Offline Task", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "Home", btn_nav_share: "My Shares", btn_nav_offline: "Offline Transfers", btn_nav_recent: "Recent Added", btn_nav_history: "Watch History", btn_nav_starred: "Starred", btn_nav_trash: "Trash", btn_nav_upload: "My Uploads", title_offline: "My Transfers", trash_title: "Trash", trash_notice: "Files in Trash will be deleted after 15 days", history_notice: "Only records progress generated within the script environment", ctx_open: "Open", ctx_add_bl: "Add to Resource Manager", ctx_remove_bl: "Remove from Resource Manager", ctx_rename: "Rename", ctx_copy: "Copy", ctx_copy_name: "Copy Name", ctx_copy_link: "Copy Link", ctx_cut: "Move", ctx_del: "Delete", ctx_down: "Download", ctx_star: "Add Star", ctx_unstar: "Remove Star", ctx_locate: "Open file location", ctx_share: "Share", /* --- 通用文件操作按钮 --- */ btn_down: "Download", tip_down: "Download [Alt] + [D]", btn_aria2: "Send to Aria2", tip_aria2: "Send to Aria2 [Alt] + [A]", btn_refresh_short: "Refresh", tip_refresh: "Refresh [F5]", btn_newfolder: "New Folder", tip_newfolder: "New Folder [F8]", btn_del: "Delete", tip_del: "Delete [Delete]", btn_deselect: "Deselect", tip_deselect: "Deselect [Esc]", btn_invert: "Invert Selection", btn_copy: "Copy", tip_copy: "Copy [Ctrl] + [C]", btn_cut: "Move", tip_cut: "Cut [Ctrl] + [X]", btn_paste: "Paste", tip_paste: "Paste [Ctrl] + [V]", btn_clear_history: "Delete History", tip_clear_history: "Delete History [Delete]", btn_restore: "Restore", tip_restore: "Restore [R]", btn_del_forever: "Delete Permanently", tip_del_forever: "Delete Permanently [Delete]", btn_empty_trash: "Empty Trash", tip_empty_trash: "Empty Trash [Shift] + [Delete]", btn_exit: "Exit", btn_close: "Close", tip_close: "Close [Esc]", tip_theme: "Switch Theme [Alt] + [T]", tip_rotate: "Rotate [R]", tip_mirror: "Mirror [H]", tip_flip_v: "Vertical Flip [V]", tip_maximize: "Maximize [M]", tip_minimize: "Minimize [M]", tip_full_screen: "Fullscreen [Enter]", btn_help: "Help", tip_help: "Help [Alt] + [H]", btn_view_file: "View File", btn_jump: "Jump", btn_copy_text: "Copy", btn_stop: "Stop", tip_stop: "Stop current operation", btn_settings: "Settings", btn_logout: "Logout PikPak", msg_logout_confirm: "Are you sure you want to logout?", tip_settings: "Settings and more [Alt] + [S]", lbl_upload_to: "Upload to: ", msg_move_done: "Move completed.", /* --- 离线、上传与云下载 --- */ btn_upload: "Local Upload", btn_up_file: "Upload File", btn_up_folder: "Upload Folder", btn_cloud_download: "Cloud Download", btn_up_pause: "Pause", tip_up_pause: "Pause [Alt] + [P]", btn_up_start: "Start", tip_up_start: "Start [Alt] + [G]", btn_up_del: "Remove", tip_up_del: "Remove [Delete]", btn_up_clear_all: "Clear All", tip_up_clear_all: "Clear All [Shift] + [Delete]", btn_retry_task: "Retry", tip_retry_task: "Retry [R]", col_task_status: "Task Status", col_task_progress: "Cloud Progress", col_up_speed: "Speed", col_up_status: "Status", lbl_task_run: "In Progress", lbl_task_fail: "Failed", lbl_task_ok: "Completed", lbl_up_run: "Uploading", lbl_up_pause: "Paused", lbl_up_downloading: "Downloading", lbl_up_done: "Completed", tip_up_pause_desc: "Includes manually paused and error tasks", title_cloud_task: "Create Cloud Download", ph_cloud_links: "Supported links:\n- Magnet, HTTP, FTP, etc.\n- YouTube, X (Twitter), TikTok, Facebook, etc.\nAdd multiple links by starting a new line.", lbl_save_to: "Save to:", lbl_default_folder: "Default Folder", btn_via_torrent: "Upload Torrent", tip_cloud_save_path: "Standard cloud downloads are saved in the My Pack folder. Downloads from other apps are saved to the My [XYZ] folder, where [XYZ] is the app name.", lbl_smart_fix: "Auto-fix masked magnets (Extract hash / Remove text noise)", title_save_method: "Save Method", msg_save_snapshot_desc: "This link can only be saved as a Web Snapshot.", tip_snapshot_details: "PikPak cannot extract media from this link directly. It will save the complete web content as a snapshot file instead.", btn_save_snapshot: "Save Snapshot", btn_create_now: "Create Now", btn_modify: "Modify", str_snap_link_count_suffix: " and {n} other links", /* --- 分享管理 --- */ btn_cancel_share: "Stop Sharing", share_copy_suffix: "Copy this content and open the PikPak app to watch instantly.", share_copy_pwd: "Password", title_share_detail: "Share Details", ctx_share_detail: "View Share Details", ctx_share_copy: "Copy Link & Password", col_view: "Views", col_save: "Saves", col_share_time: "Shared on", col_share_status: "Status", lbl_limit_reached: "Limit Reached", lbl_limit_tip: "Limit Count", lbl_share_view: "Views", lbl_share_save: "Saves", lbl_share_link_title: "Share Link", lbl_share_pwd_title: "Password", lbl_share_expire_title: "Expires", btn_copy_link_pwd: "Copy Link & Password", str_expire_suffix: " days left", ph_edit_pwd: "Enter password (4-10 characters)", btn_close_pwd: "Disable Password", str_no_pwd: "No Password", title_edit_share_code: "Edit Share Code", ph_edit_share_code: "5-18 characters (letters, numbers, symbols)", btn_add_share_code: "Add Share Code", btn_del_share_code: "Delete Share Code", share_title: "Share Files", share_mode: "Share Method", share_public: "Public Link", share_encrypted: "Encrypted Link", share_expiry: "Expiration", share_pass: "Access Code", share_count: "Access Limit", share_count_ed: "Limit", share_perm: "Permanent", share_unlimit: "Unlimited", share_rand: "Random", share_custom: "Custom", share_days: "days", share_times: "times", btn_share_start: "Share Now", cal_custom_title: "Custom Expiry", lbl_share_link: "Link", lbl_share_code: "Code", btn_copy_share: "Copy All", str_share_expired: "Expired", str_share_deleted: "File Deleted", title_edit_pwd: "Change Password", lbl_share_code_title: "Share Code", ph_password: "Password", ph_pass_range: "4-10 characters", cal_week_days:["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], cal_months:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "File Analysis", btn_scan: "File Perspective", lbl_scan_selected: "Execute File Perspective on {n} selected items", lbl_keyword_filter: "Exclude Keywords", ph_keyword_filter: "Exclude keywords, comma-separated", lbl_scan_current: "Execute File Perspective on all items in the current path", tip_dup: "Deduplicate Files", lbl_dup_selected: "Execute File Deduplication on {n} selected items", lbl_dup_current: "Execute File Deduplication on all items in the current path", tip_scan_dup: "Filter or Deduplicate Files", lbl_dup_tool: "Selection Criteria:", lbl_dup_reset: "↺ Reset (Clear selection)", lbl_dup_select_folder: "📂 Select by Folder", lbl_dup_select_folder_short: "📂 Folder", lbl_dup_invert: "Invert Mode", lbl_dup_invert_short: "Invert", tip_dup_invert_limit: "Only available for Folder selection", fmt_dup_count: "({n} duplicates)", btn_start_scan: "Start Scan", tag_hash: "Exact Match", tag_hash_short: "Exact", tag_name: "Name Sim", tag_name_short: "Name", tag_sim: "Duration Sim", tag_sim_short: "Duration", label_dup_video: "Videos (Exact Match + Duration Sim + Name Sim)", label_dup_image: "Images (Exact Match + Name Sim)", label_dup_other: "Others (Exact Match + Name Sim)", btn_analyze: "Folder Analysis", tip_analyze: "Filter or Deduplicate Folders", btn_export: "Export Directory", tip_export: "Generate and download the file tree of the current path", title_export_format: "Export Format", lbl_export_current: "Export directory structure for all items in the current path", opt_tree_view: "Tree View", opt_list_view: "List View", msg_exporting: "Generating directory tree...", str_analyze_results: "Match Results", lbl_size_threshold: "Size Threshold", title_analyze_result: "Folder Analysis Results", opt_ana_large: "Folder Perspective", lbl_analyze_selected: "Execute Folder Perspective on {n} selected items", lbl_analyze_current: "Execute Folder Perspective on all items in the current path", opt_ana_sim: "Folder Deduplication", lbl_ana_sim_selected: "Execute Folder Deduplication on {n} selected items", lbl_ana_sim_current: "Execute Folder Deduplication on all items in the current path", title_algo_help: "Algorithm Guide", algo_help_content: "Name Match: Finds folder groups with similar names and sizes.\nSimilarity Match: Finds folder groups with highly overlapping internal files.\nContainment Match: Finds subset redundancies where small folder content is fully covered by a large folder.\n\nAccuracy: Similarity > Containment > Name\nScope: Containment > Similarity", lbl_threshold: "Threshold", lbl_sim_score: "Similarity", lbl_containment: "Containment", lbl_name_match: "Name Match", lbl_sim_match: "Similarity Match", lbl_contain_match: "Containment Match", lbl_ana_min: "Min", lbl_ana_max: "Max", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "Prune Empty Folders", tip_prune: "Delete empty folders [Ctrl] + [Delete]", btn_rename: "Rename", tip_rename: "Rename [F2]", btn_bulkrename: "Bulk Rename", tip_bulkrename: "Bulk Rename [F2]", title_blacklist: "Resource Manager", btn_blacklist_run: "Run Cleanup Now", btn_clear_list: "Clear List", tip_bl_desc: "Blacklisted items only removed via 'Run Cleanup Now'.", tip_blacklist_input: "Resource Manager [Alt] + [Delete]", label_bl_folder: "Folder List (Exact)", label_bl_file: "File List (Exact)", lbl_type_folder: "Folder", lbl_type_file: "File", ph_bl_folder: "Paste or use right-click menu to add folders.", ph_bl_file: "Paste or use right-click menu to add files.", modal_bl_preview: "Scan Results", btn_bl_delete: "Delete Selected", modal_rename_title: "Rename", modal_rename_multi_title: "Bulk Rename", btn_preview: "Preview", modal_preview_title: "Confirm Changes", label_pattern: "Pattern (e.g., Video {n})", label_replace: "Replace/Delete", label_replace_note: "Case sensitive", label_include_ext: "Include Extension", label_regex: "Regex", placeholder_find: "Search for...", placeholder_replace: "Replace with... (leave empty to delete)", label_jav: "FC2 Clean Naming", lbl_rn_pattern: "Naming Template", lbl_rn_case_convert: "Case Conversion", opt_rn_keep_origin: "(No change)", opt_rn_lower: "lowercase (abc)", lbl_rn_mode_series: "TV Show Mode", lbl_rn_mode_format: "Formatting", lbl_rn_mode_ad: "Ad-Cleaner", lbl_rn_mode_ext: "Fix Extension", opt_rn_upper: "UPPERCASE (ABC)", opt_rn_title: "Title Case (Abc)", lbl_rn_width_convert: "Width Conversion", opt_rn_width_half: "Full-width to Half (A->A)", opt_rn_width_full: "Half-width to Full (A->A)", lbl_rn_preview_title: "Change Preview", tip_jav_mode_desc: "✨ Smartly extract FC2 and remove irrelevant characters", tip_ad_remove_desc: "🧹 Removes ads, URLs, junk symbols, cleans Emojis, and fixes bracket formats.", tip_ext_fix_desc: "🧩 Corrects extensions based on actual file type (MIME).", label_replace_find: "Find", label_replace_to: "Replace With", /* --- 解压相关 --- */ btn_unzip: "Bulk Extract", tip_unzip: "Bulk Extract [Alt] + [U]", btn_unzip_all: "Extract All", btn_understand_unzip: "I Understand & Extract", title_input_pwd: "Password Required", lbl_pwd_prompt: "Enter password:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "External Play", tip_ext: "Play with PotPlayer or get stream link [Alt] + [E]", btn_img_search: "Image Search [F]", tip_play_search: "Search by Image [F]", tip_pip: "Picture-in-Picture [P]", str_no_sub: "No Subtitles", lbl_sub_sel: "Subtitles", lbl_show_sub: "Show Subtitles", btn_sub_search: "Search Online", btn_sub_cloud: "Cloud", btn_sub_local: "Local File", lbl_sub_pos: "Subtitle Position", lbl_sub_bottom: "Bottom", lbl_sub_top: "Top", lbl_sub_bg_op: "BG Opacity", lbl_sub_size: "Subtitle Size", lbl_sub_offset: "Subtitle Sync", title_sel_sub: "Select Subtitles", ph_sub_search: "Type keywords, links will update below...", btn_force_play: "Force Playback", str_compat_mode: "Compatibility Mode", lang_code: "en", btn_go_search: "🔍 Manual Search on {n}", btn_restart: "Restart", btn_prev_video: "Previous [Ctrl + ←]", btn_next_video: "Next [Ctrl + →]", tip_plist_open: "Expand [E]", tip_plist_close: "Collapse [E]", tab_sub: "Subtitles", tab_size: "Aspect", tab_more: "More", lbl_ratio: "Ratio", lbl_direction: "Rotation", opt_ratio_def: "Default", btn_rot_l: "Rot L", btn_rot_r: "Rot R", btn_flip_h: "Flip H", btn_flip_v: "Flip V", lbl_play_end: "On End", opt_list_loop: "Repeat List", opt_single_loop: "Repeat One", opt_play_stop: "Pause", lbl_skip_op: "Skip Intro", lbl_skip_ed: "Skip Outro", export_link_title: "Export Video Stream Link", btn_start_play: "Start Play", btn_copy_link: "Copy Link", tip_copy_link: "Copy Link [Alt] + [C]", opt_player_other: "Others (Export Link)", lbl_player: "Player", btn_mark: "Mark", lbl_resolution: "Quality", str_switch_compat: "Current quality unavailable, reverted to {n}", type_img: "Image", type_doc: "Document", type_archive: "Archive", type_sub: "Subtitle", type_app: "Application", type_suffix: "File", /* --- 设置与搜索 --- */ label_turbo_mode: "Turbo Mode", desc_turbo_mode: "Auto-replace native UI (Recommended)", lbl_aria2_status: "Connection", ph_aria2_secret: "Secret (Optional)", str_connected: "Connected", str_conn_fail: "Failed", str_connecting: "Testing...", tip_mixed_content: "Common Ports:\n• 6800 (Aria2 Standard)\n• 16800 (Motrix Default)\n• 6881 (Others)", picker_title: "Select Folder", picker_all: "All Files", picker_new: "New Folder", picker_sort_new: "Newer", picker_sort_old: "Older", title_select_file: "Select File", placeholder_search: "Search files...", placeholder_search_short: "Search", title_search_hist: "Search History", btn_clear_hist: "Clear", lbl_global_search: "Global Search", lbl_search_path: "Include path in search", lbl_search_path_short: "Path", str_search_results: "Search Results", modal_settings_title: "Settings", label_lang: "Language", label_thumb: "Privacy Mode (Blur Thumbnails)", label_keep_pos: "Restore scroll position on return", label_sort_pref: "Sorting Preference", opt_sort_indep: "Independent for each folder", opt_sort_global: "Same for all folders", desc_sort_indep: "Sorting changes only apply to the current folder.", desc_sort_global: "All folders use the same sorting (Newest first).", label_search_engine: "Image Search Engine", opt_engine_google: "Google Lens", opt_engine_yandex: "Yandex", opt_engine_saucenao: "SauceNAO", opt_engine_tracemoe: "trace.moe", label_dup_strictness: "Similarity Threshold", opt_strict: "Strict", opt_loose: "Loose", label_comic_mode: "Media Mode", desc_comic_mode: "Default A-Z sorting for Image/Video only folders.", label_aria2_url: "Aria2 RPC URL", label_aria2_token: "Aria2 RPC Token", label_privacy_mode: "Privacy Mode", label_blur_cover: "Blur Cover Images", label_dl_filter_ext: "Download Extension Filter (e.g. .txt, .jpg)", label_dl_filter_name: "Download Name Filter (Keyword or Full Name)", lbl_dl_filter: "Folder Download Filter", desc_dl_filter: "Auto-exclude matching files when downloading/pushing folders", lbl_config_manage: "Config Management", btn_export_data: "Export Backup", btn_import_data: "Import Backup", btn_clean_data: "Clear Local Data", title_clean_data: "Select Items to Clear", msg_clean_confirm: "Are you sure you want to permanently delete the selected local data? This cannot be undone.", msg_clean_success: "Local data cleared. Page will refresh...", opt_cfg_index: "Global Index (Synced Directory Structure/File Snapshots)", opt_cfg_pref: "Preferences (UI Looks/Habits/Sort Order)", opt_cfg_rules: "Rules (Resource Manager/Share Limits/Search/Download Rules)", opt_cfg_vault: "Vault (Extract Password Memory)", opt_cfg_history: "Video Cache (Play Progress / Duration Cache)", opt_cfg_cache: "Cache (Folder Mod-Time/Last Position/Fingerprint)", msg_import_confirm: "The imported config will be merged with current settings (lists/records combined, conflicting basic settings overwritten). Continue?", msg_import_success: "Config imported successfully. Page will refresh...", err_invalid_config: "Invalid config file: Missing fingerprint or format error", err_json_format: "Parse failed: JSON syntax error or file corrupted", lbl_storage: "Storage", lbl_browse_exp: "Browsing Experience", lbl_skip_bl_on_del: "Skip resources recorded in manager on delete", lbl_pwd_manage: "Extraction Password Management", title_pwd_vault: "Password Vault", lbl_pwd_try_count: "Max password retries per archive", tip_pwd_manual: "One password per line, Enter to wrap", str_root_dir_cn: "Root", btn_ana_select: "Smart Select", opt_keep_new: "Keep Newest", opt_keep_old: "Keep Oldest", opt_keep_large: "Keep Largest", opt_keep_small: "Keep Smallest", opt_keep_short: "Keep Shortest Name", opt_keep_long: "Keep Longest Name", /* --- 状态、进度与加载短语 --- */ loading: "Loading...", loading_detail: "Full-speed indexing directory...", loading_fetch: "Fetching... ({n})", loading_dup: "Analyzing duplicates... ({p}%)", str_loading_placeholder: "Loading...", str_load_failed: " (Load Failed)", str_load_failed_simple: "Load Failed", str_waiting_token: "Syncing login status...", str_speed: "Speed", status_scanning_selection: "Scanning selection... {n}", status_ready: "{n} items", sel_count: "{n} items selected", str_cached: "Cached:", str_retries: "Retries:", str_failed: "Failed:", str_success: "Success:", str_stopping: "Stopping...", str_merging: "Merging data...", str_rendering: "Rendering list...", str_scanning: "Scanning...", str_analyzing: "Analyzing...", str_deleting: "Deleting...", str_saving: "Saving...", str_saving_dots: "Saving...", str_checking_bl: "Matching records...", str_processing: "System processing at full speed...", str_cleanup_done: "Cleanup completed.", str_waiting_preload: "Waiting for preload...", str_copying: "Copying to clipboard...", str_moving: "Preparing to move...", str_sorting: "Sorting...", str_refreshing: "Refreshing...", str_refreshing_cache: "Refreshing cache...", str_syncing_stars: "Syncing favorites...", str_updating_view: "Updating view...", str_generating_view: "Generating view...", str_group: "Group", str_init_rename: "Initializing rename...", str_renaming: "Renaming...", str_calc_changes: "Calculating changes...", str_scanning_dir: "Scanning directory...", str_init_op: "Initializing operation...", str_init_scan: "Initializing global scan...", str_rebuilding: "Rebuilding index...", str_upload_1: "Uploading (Node 1/3)...", str_upload_2: "Node 1 timeout, switching to Node 2...", str_upload_3: "Node 2 timeout, trying last node...", str_upload_fail_copy: "Upload failed, copying to clipboard...", msg_transcoding: "Transcoding in cloud...", msg_transcoding_wait: "Server is processing this video, please wait.", str_preparing: "Preparing extraction...", str_unzipping: "Extracting: {n}", str_unzipping_state: "Extracting...", str_unzipping_prog_0: "Extracting: 0%", str_unzipping_prog_100: "Extracting: 100%", str_unzipping_prog_fmt: "Extracting: {n}%", msg_task_waiting: "Waiting...", msg_task_hashing: "Verifying checksum...", msg_task_init_upload: "Initializing upload...", msg_task_uploading: "Uploading...", msg_task_init_part: "Initializing parts...", msg_task_uploading_2: "Uploading...", str_loc_tracing: "Tracing path...", str_loc_stale: "Stale cache, syncing with cloud...", str_verifying: "Verifying...", str_server_indexing: "Server indexing... ({n}/5)", str_creating_task_n: "Creating tasks ({n}/{t})...", msg_submit_request: "Submitting request... {c}/{t}", msg_wait_server: "Waiting for server... ({c}/{t})", msg_server_processing: "Server processing... ({c}/{t})", str_jav_querying: "Querying...", lbl_done_check: "✔ Done", msg_limit_updated: "Access limit updated", /* --- 提示、确认与交互消息 --- */ title_alert: "Notice", title_confirm: "Confirm", title_prompt: "Input", btn_ok: "OK", btn_yes: "Yes", btn_no: "No", btn_save: "Save Config", btn_cancel: "Cancel", btn_create: "Create", btn_skip: "Skip", msg_down_success: "Browser download called for {n} files.", msg_batch_txt: "Download list (.txt) generated.", msg_clear_history_done: "Removed from history.", msg_skip_unzipped: "Skipped {n} already extracted items.", msg_unzip_skip_del_confirm: "Detected {n} already extracted archives. Move them to Trash?", msg_cancel_share_confirm: "Are you sure you want to stop {n} shares?\nLinks will expire immediately.", msg_pwd_updating: "Updating password...", msg_pwd_updated: "Password updated.", msg_exp_updated: "Expiry updated.", msg_cancel_share_done: "Stopped {n} shares.", msg_drag_drop_hint: "Drop files here to upload", str_drag_files: " and {n} other files", msg_creating_share: "Creating share...", title_share_result: "Share Success", msg_no_files: "No items.", msg_no_selection: "Please select items first.", warn_del: "Are you sure you want to delete {n} items?", msg_clear_sel_confirm: "You have {n} duplicates selected. Clear selection?", str_bl_stat: "Matches: {n} | Selected: {m}", str_hits: "Hits", msg_settings_saved: "Settings saved. Page will refresh.", msg_name_exists: "Name already exists: {n}", str_name_conflict: "(Conflict)", msg_newfolder_prompt: "Folder name:", msg_rename_prompt: "Enter new name:", msg_copy_done: "Copied. Navigate to destination and paste.", msg_cut_done: "Ready to move. Navigate to destination and paste.", msg_paste_empty: "Nothing to paste.", msg_copy_empty: "Clipboard is empty.", msg_add_success: "Added {n} rows.", msg_del_done: "Selected rows deleted.", msg_del_select: "Select rows to delete first!", msg_del_items_done: "Deleted {n} items.", msg_copy_success: "Copied to clipboard", str_redirecting: "Redirecting to Google Lens...", msg_manual_paste: "Upload timeout. Screenshot copied to clipboard, press {cmd} in new window.", msg_starring: "Adding to favorites...", msg_unstarring: "Removing from favorites...", msg_star_added: "Added to Favorites", msg_unstar_done: "Removed from Favorites", msg_empty_trash_confirm: "Empty the trash? This cannot be undone!", msg_trash_emptied: "Trash emptied.", msg_del_forever_confirm: "Permanently delete these {n} items? This cannot be undone!", msg_del_forever_done: "{n} items deleted permanently.", msg_restore_done: "Successfully restored {n} items.", msg_auto_sub_load: "Auto-loaded subtitle: {n}", msg_dl_sub: "Downloading subtitles...", msg_transcode_done: "✅ Transcode finished. Starting playback.", msg_fallback_report: "⚠️ Original quality unplayable (MPEG4/HEVC). Switched to {n}.", tip_manual_sub: "Tip: Drag and drop .srt or .vtt files into the player to load.", msg_sub_drop_load: "Subtitle loaded via drag-and-drop: {n}", msg_resume_hint: "Resumed from {t}. Click here to ", msg_unzip_confirm_n: "Extract {n} files to current directory?", msg_task_paused: "Paused", msg_task_added: "Added {n} upload tasks", msg_task_fast_success: "Instant upload successful", msg_task_upload_done: "Upload complete", log_dirty_data: "Dirty data detected! View mismatch intercepted.", msg_network_unstable: "Network unstable, attempting to recover...", msg_skip_locked: "{n} items locked", msg_skip_self: "{n} items skipped (already at destination)", msg_skip_conflict: "{n} paths busy", msg_skip_invalid: "Auto-skipped invalid items: ", msg_creating_cloud_task: "Creating cloud download...", str_parsing_torrent: "Parsing torrent file...", err_torrent_no_info: "Parse failed: No valid info found", err_file_read: "File read error", msg_cloud_task_finish: "Finished: {s} successful, {f} failed", msg_cloud_task_success: "🎉 Successfully created {n} tasks", msg_prepare_restore: "Preparing to restore...", msg_smart_matching_n: "Smart-matching passwords ({n})...", msg_system_busy_retry: "System busy, retrying ({n}/5)...", msg_unzip_running_bg: "[{n}] is being extracted in the cloud. Refresh later.", msg_share_code_updated: "Share code updated.", msg_retry_submitted: "Resubmitted {n} tasks.", msg_aria2_batch_fail_log: "\n\nToo many failures. A complete log (.txt) has been exported.", str_aria2_fetch_err: "(Fetch Error)", str_aria2_rpc_err: "(RPC Error)", str_aria2_aborted: "(Aborted)", str_aria2_fail_file_name: "Aria2_Failed_List", msg_op_blocked_moving: "⚠️ Operation Blocked\n\nFile transfer in progress. Please wait.", msg_op_blocked_analyzing: "⚠️ Operation Blocked\n\nFile transfer in progress. For accurate Folder Analysis data, please wait for the current task to finish.", msg_op_blocked_exporting: "⚠️ Operation Blocked\n\nFile transfer in progress. To ensure accurate export text, please wait for the current task to finish.", msg_analyze_only_normal_dir: "Please select folder(s)", msg_analyze_no_large_folders: "No folders found within the threshold range ({s})", msg_analyze_summary_fmt: "Found <b>{n}</b> folders larger than {s}GB (sorted by size)", msg_prune_blocked_moving: "⚠️ Operation Blocked\n\nCannot prune folders during a transfer.", msg_global_index_blocked_moving: "⚠️ Operation Blocked\n\nGlobal indexing requires a stable structure. Please wait for the transfer.", msg_resource_locked_download: "⚠️ Operation Blocked\n\nSelected items are being moved. Please wait.", msg_resource_locked_aria2: "⚠️ Operation Blocked\n\nCannot send to Aria2 while files are moving.", msg_flatten_blocked_moving: "⚠️ Operation Blocked\n\nFile transfer in progress. Performing File Analysis at this time may result in missing or duplicate file lists, please try again later.", err_task_conflict: "⚠️ Operation Blocked\n\nGlobal cleanup requires a stable structure. Please wait.", title_del_task_confirm_fmt: "Delete {n} transfer tasks?", lbl_del_cloud_files_too: "Also delete files in cloud drive", msg_file_del_failed: "File deletion failed: ", msg_task_del_success_fmt: "Deleted {n} tasks", title_clear_task_confirm: "Clear all upload tasks?", msg_task_clear_success_fmt: "Cleared {n} upload tasks", msg_unzip_virtual_view_warn: "You are in a Virtual View. Extracted files will appear in the <b>original folders</b> of the archives, <b>not</b> in this list.<br><br>Continue?", msg_smart_matching_file: "Matching password... ({n})", msg_unzip_batch_submitted: "✅ Completed {n} extraction tasks", msg_unzip_batch_skipped: " ({n} skipped)", msg_unzip_check_source: ". Please check source directory for results.", tip_jump_to_folder: "Go to folder", msg_task_deleted: "Task deleted", msg_scan_done: "Scan complete!\nFound {n} files across {f} folders.", msg_scan_fail: "\n\n❌ {n} failures.", msg_scan_fix: "\n\n✅ Auto-fixed {n} network errors.", msg_down_scanning: "Parsing folder contents...", msg_down_progress: "Calling browser download...", msg_down_confirm_total: "✅ Scan complete. {n} files found.\n\n⚠️ Warning: Downloading many files via browser may freeze the page or get blocked.\nSuggest using Aria2 for >10 files.\n\nProceed anyway?", msg_aria2_sending_batch: "🚀 Sending tasks to Aria2 in batches...", msg_aria2_check_fail: "Aria2 connection failed!\nCheck URL and Token.", msg_aria2_check_ok: "Aria2 connection successful!", msg_aria2_sent: "Sent {n} files to Aria2.", msg_aria2_test_fail: "Aria2 test failed. Save settings anyway?", title_aria2_fail: "Connection Failed", msg_batch_scanning: "🚀 Scanning directory structure...", msg_batch_hydrating: "⚡ Extracting download links...", msg_batch_no_files: "No downloadable files found.", msg_dup_warn: "Search for duplicate files?", msg_dup_result: "Found {n} duplicate groups.", msg_dup_none: "No duplicates found.", msg_bl_stop: "Operation stopped.", msg_bl_add_done: "Added {n} items to records.", msg_bl_remove_done: "Removed {n} items from records.", msg_bl_empty: "Record list is empty, cannot run.", msg_bl_clear_confirm: "Clear all records? This cannot be undone.", msg_blacklist_run_none: "No matching records found in cloud drive.", msg_blacklist_run_confirm: "Found {n} recorded items.\n\nMove to trash now?", msg_bl_run_limit: "⚠️ Mode Restriction\n\nCleanup requires physical recursive scanning. Please run from Home or a standard folder.", msg_del_protected: "Protected {n} recorded files from deletion.", msg_del_none: "No files to delete.", msg_bl_scanning: "Global scanning... \nFolders: {d} | Hits: {f}", rn_tip_wait: "Please set rules", rn_tip_jav: "Click the button above to start matching", rn_tip_none: "No matching items", rn_stat: "Matches: {n} | Valid changes: {m}", rn_warn_confirm: "Rename {n} files?", msg_bulkrename_done: "Renamed {n} items.", msg_rn_all_skipped: "❌ All items skipped due to name conflicts.", msg_rn_fail_count: "Skipped {n} items (name conflict)", msg_prune_confirm: "Search for empty folders in the current list?", msg_prune_none: "No empty folders found.", msg_prune_found: "Found {n} empty folders.\nDelete them now?", msg_deleting_folders: "Deleting {n} folders...", msg_global_warn: "Start full drive sync?\n\nFiles will be cached in memory until page refresh.\n\nContinue?", msg_init_scan_sel: "Initializing scan for selected items...", warn_clear_history: "Remove {n} items from history?\n(This will not delete the cloud files)", msg_img_copy_hint: "Press {cmd} in the new window to search.", msg_aria2_not_set: "Aria2 not configured. Please fill in details:", str_jav_no_match: "(No match found)", msg_unzip_fail: "Extraction request failed.", msg_jszip_fail: "JSZip failed to load. Check your network.", msg_turbo_activated: "Turbo Mode Active: Script has taken over web logic for peak performance.", msg_console_legal: "Strictly Non-Commercial: This project is strictly for personal study and communication only.", msg_ana_warn: "Folder Deduplication Hint: Based on algorithmic inference. Please double-check manually before deleting.", /* --- 错误提示 --- */ err_invalid_links: "Please enter valid links.", err_pwd_format: "Password must be 4-10 alphanumeric characters.", err_invalid_torrent: "Invalid torrent file format", err_torrent_complex: "Parsing complexity too high (possibly invalid file)", err_torrent_format: "Torrent structure corrupted", err_torrent_len: "Field length parsing error", err_torrent_char: "Unexpected character encountered", err_share_code_exists: "Share code already taken.", err_folder_not_ready: "Cloud folder is being created, please try again later.", err_item_deleted: "Item does not exist.", err_network: "Network error.", err_clipboard_denied: "Clipboard access denied.", err_worker: "Worker thread error.", err_api: "API error.", err_capture: "Screenshot failed.", err_captcha_simple: "Verification failed. Manually save a file once to pass verification.", err_sub_dl_fail: "Subtitle download failed: ", err_req_blocked: "Request blocked (Check firewall/adblock).", err_req_timeout: "Request timeout.", err_sub_drop_type: "Only subtitle file types are supported.", err_codec_t1: "Codec not supported ({c}).", err_codec_t2: "Browser cannot play this format.<br>Please use an external player.", err_pwd_simple: "Incorrect password.", err_task_exists: "Task already exists.", err_network_break: "Image node disconnected, click again to retry.", err_no_failed_task: "No failed tasks selected.", err_unknown: "Unknown error.", err_invalid_regex: "Invalid Regular Expression.", err_parent_not_found: "Folder not found.", msg_sys_error: "System folders cannot be modified.", msg_download_fail: "Could not retrieve download link.", msg_video_fail: "Could not retrieve video link.", err_star_sync_fail: "Favorites sync failed.", err_paste_descendant: "Cannot move/copy into the same or a sub-folder.", err_quota_exceeded: "Storage quota exceeded.", err_name_exists: "File name already exists.", err_share_pass: "Access code must be 4-10 characters.", str_error: "Error", str_error_crit: "Critical Error", str_error_paste: "Paste Error", str_action_failed: "Action Failed", str_scan_error: "Scan Error", err_limit_too_low: "Failed: New limit ({n}) must be higher than current ({s}).", err_vault_max: "Password vault supports storing up to 50 common passwords", err_pwd_len: "A single password length cannot exceed 127 characters", /* --- 帮助文档 --- */ modal_help_title: "Help", help_desc: ` <div class="pk-no-scrollbar pk-help-scroll" style="font-size:13px;line-height:1.6;color:var(--pk-fg);text-align:justify;text-justify:inter-ideograph;word-break:break-all;pointer-events:auto;display:block;"> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">✨ Experience & Navigation Engine</b><br> • <b>UI Refactoring</b>: Interface redesigned to resemble <b>Windows File Explorer</b>.<br> • <b>Turbo Mode</b>: Takes over native logic completely to resolve lag and crashes with massive files.<br> • <b>Advanced Path Bar</b>: Supports scroll wheel navigation and dropdown peer-level switching. Global search and analysis suites are integrated, supporting path echoing and source backtracking.<br> • <b>Enhanced UX</b>: Supports multi-dimensional sorting, one-click <b>cover blurring</b>, and dark theme switching. Background uses <b>SWR strategy</b> for silent view updates.<br> • <b>Background Indexing & Protection</b>: A blinking blue dot on the home icon indicates directory tree syncing. Features a physical concurrency lock to intercept conflicting operations and prevent dirty data.<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* Note: The default folder (My Pack) is officially protected against accidental deletion, copying, moving, and renaming.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">📂 Batch & Space Management</b><br> • <b>Batch Rename</b>: Supports <b>Regex replace/delete</b>, <b>episode serialization</b>, text <b>formatting</b>, <b>AV/FC2 standard naming</b>, <b>ad prefix removal</b>, and MIME-based <b>extension fixing</b>.<br> • <b>Analysis Suite</b>: <b>File Analysis</b> integrates filtering and deduplication (hash/duration/name tri-modal); <b>Folder Analysis</b> integrates filtering and deduplication (name/similarity/containment tri-modal); and supports exporting the <b>directory tree</b>.<br> • <b>Smart Organizing</b>: One-click empty folder cleanup; <b>Batch Unzip</b> integrates automatic password memory and smart auto-filling, supporting auto-skip and deletion of extracted items.<br> • <b>Resource Manager</b>: Can be used as a <b>File Blacklist</b> to clean up junk resources; or as a <b>File Whitelist</b> to auto-skip and protect items during batch deletion.<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* Note: To avoid data sync conflicts, please do not modify files via other clients during processing.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🌐 Transfer & Sharing Hub</b><br> • <b>Share Management</b>: Supports setting extraction limits. Shares are automatically canceled once the limit is reached.<br> • <b>Ultra-fast Upload</b>: Supports global drag-and-drop direct upload, bypassing official limits and <b>significantly reducing the interruption rate of small file transfers</b>.<br> • <b>Cloud Download Enhancements</b>: <b>Auto-deduplicates</b> batch offline links. Built-in <b>magnet smart cleaning engine</b> (extracts Base32/Hex hash to remove noise); supports parsing <b>.torrent</b> files; provides <b>web snapshot saving</b> as a fallback for restricted links. <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* Note: Extraction limit interception only works when the webpage is kept open and the computer is awake.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🎬 Immersive Media Enhancements</b><br> • <b>Playback Engine</b>: Supports 0.5x-3.0x speed, rotation/mirroring, forced aspect ratios, auto-skip intro/outro, and <b>autoplay/loop</b> modes. The progress bar supports thumbnail previews. Built-in <b>Watchdog</b> automatically falls back to compatible quality upon black screens or unsupported codecs.<br> • <b>Subtitle System</b>: Supports loading cloud subtitles, local files, and cross-site online search. Supports millisecond-level subtitle offset tweaking and direct <b>drag-and-drop parsing</b> of local text.<br> • <b>Visual Aids</b>: Built-in multi-engine reverse image search; "Media Mode" can be activated to auto-sort series or manga folders alphabetically (A-Z).<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* Note: The playback history list continuously records progress generated within the script environment.</div> </div> <div style="margin-bottom:12px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚙️ Configuration & Data Management</b><br> • <b>Config Backup</b>: Supports exporting preferences, rules, password vaults as JSON backup files with digital fingerprints. Supports <b>smart merge & deduplication</b> upon importing.<br> • <b>Data Cleanup</b>: Supports on-demand clearing of global index, preferences, rules, password vaults, and caches to free up local storage and protect privacy.<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* Note: The global index is cleared when the webpage is closed, while preferences, password vaults, etc., are saved persistently.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚡ Download & Distribution</b><br> • <b>External Direct Connection</b>: Supports one-click video stream link extraction, or launching PotPlayer. Supports pushing files to <b>Aria2</b> nodes via RPC protocol.<br> • <b>Distribution Enhancements</b>: <b>Auto-reconstructs cloud tree directory structures</b> when pushing folders to Aria2. Supports persistent connection monitoring and auto-exports error lists upon failure. Supports setting <b>folder download filters</b>. </div> <div style="margin-top:16px; color:#d93025; font-weight:bold; text-align:center; font-size:11px; border-top:1px dashed rgba(217,48,37,0.2); padding-top:12px; letter-spacing:0.5px; opacity:0.9;"> This project strictly adheres to the CC-BY-NC-SA-4.0 license and is strictly prohibited for any commercial use. </div> </div>` }, ko: { /* --- 通用与基础UI --- */ title: "PikPak 인핸서 마스터", str_original: "원본 화질", str_original_fast: "원본 (고속)", str_folders: "폴더", str_files: "파일", unit_folders: "개 폴더", unit_days: "일", unit_month: "개월", unit_sec: "초", str_no_files: "파일이 없습니다", str_items: "개 항목", col_name: "이름", col_size: "크기", col_dur: "유형/길이", col_duration_only: "길이", col_progress: "재생 진행도", col_play_time: "재생 시간", col_date: "수정 날짜", col_remaining: "남은 시간", col_path: "경로", col_old: "기존 이름", col_new: "새 이름", col_type: "유형", col_path_name: "경로 / 이름", col_action: "작업", lbl_folder_first: "폴더 상단 고정", tag_default: "기본", current_dir: "현재 디렉토리", str_same_folder: "(동일 폴더)", lbl_dont_show: "다시 보지 않기", lbl_dont_show_session: "이번 검사에서 다시 표시 안 함", str_empty_filename: "(파일 이름 없음)", str_empty_dir: "(빈 디렉토리)", btn_filter: "필터", title_file_filter: "파일 필터", cat_all: "전체", cat_video: "비디오", cat_audio: "오디오", cat_image: "이미지", cat_document: "문서", cat_software: "앱", cat_archive: "압축 파일", cat_torrent: "BT 토렌트", cat_other: "기타", btn_exit_filter: "필터 종료", /* --- 属性面板 --- */ ctx_property: "속성", title_property: "파일 속성", lbl_prop_name: "파일 이름", lbl_prop_size: "파일 크기", lbl_prop_count: "파일 개수", lbl_prop_ctime: "생성 시간", lbl_prop_mtime: "수정 시간", lbl_prop_source: "추가 출처", lbl_prop_link: "리소스 링크", lbl_prop_path: "파일 위치", str_prop_cloud: "클라우드 추가", str_prop_share: "공유 링크", str_prop_user: "사용자 업로드", str_prop_unknown: "알 수 없는 출처", fmt_prop_count: "파일 {f}개, 폴더 {d}개 포함", str_prop_offline: "오프라인 작업", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "홈", btn_nav_share: "내 공유", btn_nav_offline: "오프라인 전송", btn_nav_recent: "최근 항목", btn_nav_history: "재생 기록", btn_nav_starred: "즐겨찾기", btn_nav_trash: "휴지통", btn_nav_upload: "내 업로드", title_offline: "내 오프라인", trash_title: "휴지통", trash_notice: "휴지통의 파일은 최대 15일 동안 보관됩니다", history_notice: "스크립트 환경 내에서 발생한 재생 진행률만 기록됩니다", ctx_open: "열기", ctx_add_bl: "리소스 관리자에 추가", ctx_remove_bl: "리소스 관리자에서 제거", ctx_rename: "이름 바꾸기", ctx_copy: "복사", ctx_copy_name: "파일명 복사", ctx_copy_link: "링크 복사", ctx_cut: "이동", ctx_del: "삭제", ctx_down: "다운로드", ctx_star: "즐겨찾기 추가", ctx_unstar: "즐겨찾기 취소", ctx_locate: "폴더 위치 열기", ctx_share: "공유", /* --- 通用文件操作按钮 --- */ btn_down: "다운로드", tip_down: "다운로드 [Alt] + [D]", btn_aria2: "Aria2 전송", tip_aria2: "Aria2로 보내기 [Alt] + [A]", btn_refresh_short: "새로고침", tip_refresh: "새로고침 [F5]", btn_newfolder: "새 폴더", tip_newfolder: "새 폴더 만들기 [F8]", btn_del: "삭제", tip_del: "삭제 [Delete]", btn_deselect: "선택 해제", tip_deselect: "선택 해제 [Esc]", btn_invert: "선택 반전", btn_copy: "복사", tip_copy: "복사 [Ctrl] + [C]", btn_cut: "이동", tip_cut: "이동 [Ctrl] + [X]", btn_paste: "붙여넣기", tip_paste: "붙여넣기 [Ctrl] + [V]", btn_clear_history: "히스토리 삭제", tip_clear_history: "히스토리 삭제 [Delete]", btn_restore: "복원", tip_restore: "복원 [R]", btn_del_forever: "영구 삭제", tip_del_forever: "영구 삭제 [Delete]", btn_empty_trash: "휴지통 비우기", tip_empty_trash: "휴지통 비우기 [Shift] + [Delete]", btn_exit: "나가기", btn_close: "닫기", tip_close: "닫기 [Esc]", tip_theme: "테마 전환 [Alt] + [T]", tip_rotate: "회전 [R]", tip_mirror: "좌우 반전 [H]", tip_flip_v: "상하 반전 [V]", tip_maximize: "최대화 [M]", tip_minimize: "최소화 [M]", tip_full_screen: "전체 화면 [Enter]", btn_help: "도움말", tip_help: "도움말 [Alt] + [H]", btn_view_file: "파일 보기", btn_jump: "이동", btn_copy_text: "복사", btn_stop: "중지", tip_stop: "현재 작업 즉시 중지", btn_settings: "설정", btn_logout: "PikPak 로그아웃", msg_logout_confirm: "로그아웃 하시겠습니까?", tip_settings: "설정 및 기타 [Alt] + [S]", lbl_upload_to: "업로드 위치: ", msg_move_done: "이동 완료.", /* --- 离线、上传与云下载 --- */ btn_upload: "로컬 업로드", btn_up_file: "파일 업로드", btn_up_folder: "폴더 업로드", btn_cloud_download: "링크 저장", btn_up_pause: "작업 일시정지", tip_up_pause: "작업 일시정지 [Alt] + [P]", btn_up_start: "작업 시작", tip_up_start: "작업 시작 [Alt] + [G]", btn_up_del: "작업 삭제", tip_up_del: "작업 삭제 [Delete]", btn_up_clear_all: "목록 비우기", tip_up_clear_all: "모든 작업 삭제 [Shift] + [Delete]", btn_retry_task: "작업 재시도", tip_retry_task: "재시도 [R]", col_task_status: "작업 상태", col_task_progress: "오프라인 진행도", col_up_speed: "속도", col_up_status: "상태", lbl_task_run: "진행 중", lbl_task_fail: "실패", lbl_task_ok: "완료", lbl_up_run: "업로드 중", lbl_up_pause: "중단됨", lbl_up_downloading: "다운로드 중", lbl_up_done: "완료됨", tip_up_pause_desc: "수동 일시정지 및 오류가 발생한 작업 포함", title_cloud_task: "클라우드 다운로드 작업 생성", ph_cloud_links: "지원 링크 형식:\n- 마그넷(magnet) 등 각종 다운로드 링크\n- YouTube, X(Twitter), TikTok, Facebook 등 공유 링크\n줄바꿈을 통해 여러 개의 링크를 동시에 추가할 수 있습니다.", lbl_save_to: "파일 저장 위치:", lbl_default_folder: "기본 폴더", btn_via_torrent: "토렌트 파일로 생성", tip_cloud_save_path: "일반 클라우드 다운로드 파일은 My Pack 디렉토리에 저장되며, 다른 앱의 클라우드 다운로드 파일은 My [XYZ] 디렉토리에 저장됩니다([XYZ]는 앱 이름).", lbl_smart_fix: "차단 방지 마그넷 자동 복구 (해시 추출 / 텍스트 노이즈 제거)", title_save_method: "저장 방식", msg_save_snapshot_desc: "이 링크는 웹페이지 스냅샷으로만 저장할 수 있습니다.", tip_snapshot_details: "PikPak이 이 링크에서 미디어 파일을 직접 수집할 수 없습니다. 대신 웹페이지 스냅샷으로 저장하며, 최대한 전체 내용을 보존합니다.", btn_save_snapshot: "스냅샷 저장", btn_create_now: "지금 생성", btn_modify: "수정", str_snap_link_count_suffix: " 외 {n}개 링크", /* --- 分享管理 --- */ btn_cancel_share: "공유 취소", share_copy_suffix: "이 내용을 복사한 후 PikPak 앱을 열면 초고속 재생을 즐길 수 있습니다", share_copy_pwd: "비밀번호", title_share_detail: "공유 상세 정보", ctx_share_detail: "공유 상세 보기", ctx_share_copy: "링크 및 비번 복사", col_view: "조회", col_save: "저장", col_share_time: "공유 시간", col_share_status: "공유 상태", lbl_limit_reached: "횟수 초과", lbl_limit_tip: "제한 횟수", lbl_share_view: "조회", lbl_share_save: "저장", lbl_share_link_title: "공유 링크", lbl_share_pwd_title: "비밀번호", lbl_share_expire_title: "유효 기간", btn_copy_link_pwd: "링크 및 비밀번호 복사", str_expire_suffix: "일 후 만료", ph_edit_pwd: "공유 비밀번호 입력 (4-10자)", btn_close_pwd: "비밀번호 해제", str_no_pwd: "비밀번호 없음", title_edit_share_code: "공유 코드 수정", ph_edit_share_code: "5~18자 문자, 숫자, 기호 등 지원", btn_add_share_code: "공유 코드 추가", btn_del_share_code: "공유 코드 삭제", share_title: "파일 공유", share_mode: "공유 방식", share_public: "공개 링크", share_encrypted: "암호화 링크", share_expiry: "유효 기간 설정", share_pass: "추출 코드 설정", share_count: "추출 횟수 설정", share_count_ed: "추출 횟수", share_perm: "영구적", share_unlimit: "제한 없음", share_rand: "랜덤 생성", share_custom: "사용자 지정", share_days: "일", share_times: "회", btn_share_start: "지금 공유", cal_custom_title: "유효 기간 직접 선택", lbl_share_link: "링크", lbl_share_code: "추출 코드", btn_copy_share: "모두 복사", str_share_expired: "만료됨", str_share_deleted: "파일 삭제됨", title_edit_pwd: "비밀번호 변경", lbl_share_code_title: "공유 코드", ph_password: "비밀번호", ph_pass_range: "4-10자", cal_week_days:["일", "월", "화", "수", "목", "금", "토"], cal_months:["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "파일 분석", btn_scan: "파일 투시", lbl_scan_selected: "선택한 {n}개 항목에 대해 파일 투시를 실행합니다", lbl_keyword_filter: "키워드 제외", ph_keyword_filter: "제외할 키워드, 쉼표로 구분", lbl_scan_current: "현재 경로의 모든 항목에 대해 파일 투시를 실행합니다", tip_dup: "파일 중복 확인", lbl_dup_selected: "선택한 {n}개 항목에 대해 파일 중복 확인을 실행합니다", lbl_dup_current: "현재 경로의 모든 항목에 대해 파일 중복 확인을 실행합니다", tip_scan_dup: "파일 필터링 또는 중복 확인", lbl_dup_tool: "삭제 대상 선택:", lbl_dup_reset: "↺ 초기화 (고정 해제 및 선택 취소)", lbl_dup_select_folder: "📂 폴더별 선택", lbl_dup_select_folder_short: "📂 폴더", lbl_dup_invert: "반전 모드", lbl_dup_invert_short: "반전", tip_dup_invert_limit: "폴더별 선택 시에만 사용 가능", fmt_dup_count: "({n}개 중복)", btn_start_scan: "스캔 시작", tag_hash: "정밀 일치", tag_hash_short: "정밀", tag_name: "이름 유사", tag_name_short: "이름", tag_sim: "재생시간 유사", tag_sim_short: "재생시간", label_dup_video: "비디오 파일 (정밀 일치 + 재생시간 유사 + 이름 유사)", label_dup_image: "이미지 파일 (정밀 일치 + 이름 유사)", label_dup_other: "기타 파일 (정밀 일치 + 이름 유사)", btn_analyze: "폴더 분석", tip_analyze: "폴더 필터링 또는 중복 확인", btn_export: "디렉토리 내보내기", tip_export: "현재 경로의 파일 트리 목록 생성 및 다운로드", title_export_format: "내보내기 스타일", lbl_export_current: "현재 경로의 모든 항목에 대해 디렉토리 내보내기를 실행합니다", opt_tree_view: "트리 뷰", opt_list_view: "리스트 뷰", msg_exporting: "디렉토리 트리 생성 중...", str_analyze_results: "일치 결과", lbl_size_threshold: "감지 임계값", title_analyze_result: "폴더 분석 결과", opt_ana_large: "폴더 투시", lbl_analyze_selected: "선택한 {n}개 항목에 대해 폴더 투시를 실행합니다", lbl_analyze_current: "현재 경로의 모든 항목에 대해 폴더 투시를 실행합니다", opt_ana_sim: "폴더 중복 확인", lbl_ana_sim_selected: "선택한 {n}개 항목에 대해 폴더 중복 확인을 실행합니다", lbl_ana_sim_current: "현재 경로의 모든 항목에 대해 폴더 중복 확인을 실행합니다", title_algo_help: "알고리즘 설명", algo_help_content: "이름 일치: 이름과 크기가 유사한 폴더 그룹을 찾습니다.\n유사도 일치: 내부 파일이 고도로 중복되는 폴더 그룹을 찾습니다.\n포함율 일치: 작은 폴더의 내용이 큰 폴더에 완전히 포함된 부분 집합 중복을 찾습니다.\n\n정확도: 유사도 일치 > 포함율 일치 > 이름 일치\n범위: 포함율 일치 > 유사도 일치", lbl_threshold: "임계값", lbl_sim_score: "유사도", lbl_containment: "포함율", lbl_name_match: "이름 일치", lbl_sim_match: "유사도 일치", lbl_contain_match: "포함율 일치", lbl_ana_min: "최소", lbl_ana_max: "최대", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "빈 폴더 정리", tip_prune: "빈 폴더 정리 [Ctrl] + [Delete]", btn_rename: "이름 바꾸기", tip_rename: "이름 바꾸기 [F2]", btn_bulkrename: "일괄 이름 바꾸기", tip_bulkrename: "일괄 이름 바꾸기 [F2]", title_blacklist: "리소스 관리자", btn_blacklist_run: "정리 즉시 실행", btn_clear_list: "목록 비우기", tip_bl_desc: "아래 항목들은 일반 '삭제' 시 건너뛰며, '정리 즉시 실행'을 통해서만 삭제됩니다.", tip_blacklist_input: "리소스 관리자 [Alt] + [Delete]", label_bl_folder: "폴더 목록 (정밀 검색)", label_bl_file: "파일 목록 (정밀 검색)", lbl_type_folder: "폴더", lbl_type_file: "파일", ph_bl_folder: "'붙여넣기' 또는 우클릭 메뉴의 '리소스 관리자에 추가'를 통해 가져오세요.", ph_bl_file: "'붙여넣기' 또는 우클릭 메뉴의 '리소스 관리자에 추가'를 통해 가져오세요.", modal_bl_preview: "검색 결과", btn_bl_delete: "선택 항목 삭제", modal_rename_title: "이름 바꾸기", modal_rename_multi_title: "일괄 이름 바꾸기", btn_preview: "미리보기", modal_preview_title: "변경 사항 확인", label_pattern: "패턴 (예: 비디오 {n})", label_replace: "바꾸기/삭제", label_replace_note: "대소문자 구분", label_include_ext: "확장자 포함", label_regex: "정규식 (Regex)", placeholder_find: "찾을 내용", placeholder_replace: "바꿀 내용 (비워두면 삭제)", label_jav: "FC2 클린 네이밍", lbl_rn_pattern: "이름 템플릿", lbl_rn_case_convert: "대소문자 변환", opt_rn_keep_origin: "(변경 없음)", opt_rn_lower: "모두 소문자 (abc)", lbl_rn_mode_series: "에피소드 모드", lbl_rn_mode_format: "포맷팅", lbl_rn_mode_ad: "접두사 광고 제거", lbl_rn_mode_ext: "확장자 복구", opt_rn_upper: "모두 대문자 (ABC)", opt_rn_title: "첫 글자만 대문자 (Abc)", lbl_rn_width_convert: "전각/반각 변환", opt_rn_width_half: "전각을 반각으로 (A->A)", opt_rn_width_full: "반각을 전각으로 (A->A)", lbl_rn_preview_title: "변경 미리보기", tip_jav_mode_desc: "✨ FC2 스마트 추출 및 불필요한 문자 제거", tip_ad_remove_desc: "🧹 제목 앞부분의 광고, URL, 불필요한 기호를 제거하고 이모지 정리 및 괄호 형식 수정", tip_ext_fix_desc: "🧩 실제 파일 유형(MIME)에 기반하여 확장자를 지능적으로 수정", label_replace_find: "찾을 내용", label_replace_to: "바꿀 내용", /* --- 解压相关 --- */ btn_unzip: "일괄 압축 해제", tip_unzip: "일괄 압축 해제 [Alt] + [U]", btn_unzip_all: "전체 압축 해제", btn_understand_unzip: "확인 및 해제", title_input_pwd: "압축 해제 비밀번호 필요", lbl_pwd_prompt: "비밀번호를 입력하세요:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "외부 재생", tip_ext: "PotPlayer 재생 / 링크 가져오기 [Alt] + [E]", btn_img_search: "이미지로 검색 [F]", tip_play_search: "이미지로 검색 [F]", tip_pip: "PIP 모드 [P]", str_no_sub: "자막 없음", lbl_sub_sel: "자막 선택", lbl_show_sub: "자막 표시", btn_sub_search: "자막 검색", btn_sub_cloud: "클라우드", btn_sub_local: "로컬 자막", lbl_sub_pos: "자막 위치", lbl_sub_bottom: "하단", lbl_sub_top: "상단", lbl_sub_bg_op: "배경 투명도", lbl_sub_size: "자막 크기", lbl_sub_offset: "자막 싱크", title_sel_sub: "자막 선택", ph_sub_search: "키워드 입력 시 아래 링크가 자동 업데이트됩니다...", btn_force_play: "강제 재생 시도", str_compat_mode: "호환 모드", lang_code: "ko", btn_go_search: "🔍 {n}에서 직접 검색", btn_restart: "처음부터 재생", btn_prev_video: "이전 영상 [Ctrl + ←]", btn_next_video: "다음 영상 [Ctrl + →]", tip_plist_open: "목록 펼치기 [E]", tip_plist_close: "목록 접기 [E]", tab_sub: "자막", tab_size: "크기", tab_more: "기타", lbl_ratio: "비율", lbl_direction: "방향", opt_ratio_def: "기본", btn_rot_l: "좌측회전", btn_rot_r: "우측회전", btn_flip_h: "좌우반전", btn_flip_v: "상하반전", lbl_play_end: "재생 종료 시", opt_list_loop: "목록 반복", opt_single_loop: "한 곡 반복", opt_play_stop: "재생 후 중지", lbl_skip_op: "오프닝 건너뛰기", lbl_skip_ed: "엔딩 건너뛰기", export_link_title: "비디오 스트리밍 링크 내보내기", btn_start_play: "재생 시작", btn_copy_link: "링크 복사", tip_copy_link: "링크 복사 [Alt] + [C]", opt_player_other: "기타 (링크 내보내기)", lbl_player: "플레이어", btn_mark: "마크", lbl_resolution: "해상도", str_switch_compat: "현재 해상도를 사용할 수 없어 {n} 화질로 전환되었습니다", type_img: "이미지", type_doc: "문서", type_archive: "압축", type_sub: "자막", type_app: "앱", type_suffix: "파일", /* --- 设置与搜索 --- */ label_turbo_mode: "초고속 모드", desc_turbo_mode: "순정 UI 자동 대체 (권장)", lbl_aria2_status: "연결 상태", ph_aria2_secret: "보안 비밀 (선택 사항)", str_connected: "연결됨", str_conn_fail: "실패", str_connecting: "테스트 중...", tip_mixed_content: "상용 포트 참고:\n• 6800 (Aria2 기본)\n• 16800 (Motrix 기본)\n• 6881 (기타 통합판)", picker_title: "폴더 선택", picker_all: "모든 파일", picker_new: "새 폴더", picker_sort_new: "최신", picker_sort_old: "오래된", title_select_file: "파일 선택", placeholder_search: "파일 검색...", placeholder_search_short: "검색", title_search_hist: "검색 기록", btn_clear_hist: "비우기", lbl_global_search: "전체 검색", lbl_search_path: "경로 포함 검색", lbl_search_path_short: "경로", str_search_results: "검색 결과", modal_settings_title: "설정", label_lang: "언어 (Language)", label_thumb: "썸네일 흐리게 (프라이버시 모드)", label_keep_pos: "탐색 위치 기억 (뒤로 가기 시 위치 고정)", label_sort_pref: "정렬 방식 설정", opt_sort_indep: "폴더별 개별 설정", opt_sort_global: "전체 동일 적용", desc_sort_indep: "정렬 방식 변경이 현재 폴더에만 적용됩니다", desc_sort_global: "모든 폴더에 동일한 정렬 방식(날짜 역순)을 적용합니다", label_search_engine: "이미지 검색 엔진", opt_engine_google: "Google 렌즈 (종합)", opt_engine_yandex: "Yandex (종합)", opt_engine_saucenao: "SauceNAO (Pixiv/일러스트)", opt_engine_tracemoe: "trace.moe (애니메이션 스크린샷)", label_dup_strictness: "유사 일치 임계값", opt_strict: "엄격", opt_loose: "느슨하게", label_comic_mode: "미디어 모드", desc_comic_mode: "이미지/동영상 전용 폴더는 기본 A-Z 정렬", label_aria2_url: "Aria2 주소", label_aria2_token: "Aria2 RPC Token", label_privacy_mode: "프라이버시 모드", label_blur_cover: "커버 이미지 흐리게", label_dl_filter_ext: "다운로드 확장자 필터 (예: .txt, .jpg)", label_dl_filter_name: "다운로드 이름 필터 (키워드 또는 전체 이름)", lbl_dl_filter: "폴더 다운로드 필터링", desc_dl_filter: "폴더 다운로드/전송 시 매칭되는 파일을 자동으로 제외합니다", lbl_config_manage: "설정 관리", btn_export_data: "백업 내보내기", btn_import_data: "백업 가져오기", btn_clean_data: "로컬 데이터 지우기", title_clean_data: "정리할 항목 선택", msg_clean_confirm: "선택한 로컬 데이터를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", msg_clean_success: "로컬 데이터가 정리되었습니다. 페이지가 새로고침됩니다...", opt_cfg_index: "전체 인덱스 (동기화된 디렉토리 구조/파일 스냅샷)", opt_cfg_pref: "기본 설정 (UI 외형/작업 습관/정렬 방식)", opt_cfg_rules: "관리 규칙 (리소스 관리자/공유 횟수 제한/검색 기록/다운로드 규칙)", opt_cfg_vault: "비밀번호 금고 (압축 해제 비밀번호 기억)", opt_cfg_history: "비디오 캐시 (비디오 재생 진행도/비디오 길이 캐시)", opt_cfg_cache: "실행 캐시 (폴더 수정 시간/최근 탐색 위치/지문)", msg_import_confirm: "가져온 설정이 현재 설정과 병합됩니다(목록/기록 병합, 충돌하는 기본 설정 덮어쓰기). 계속하시겠습니까?", msg_import_success: "설정을 성공적으로 가져왔습니다. 페이지가 새로고침됩니다...", err_invalid_config: "잘못된 설정 파일: 지문 식별이 없거나 형식 오류입니다", err_json_format: "파일 분석 실패: JSON 구문 오류 또는 파일이 손상되었습니다", lbl_storage: "저장 공간", lbl_browse_exp: "탐색 경험", lbl_skip_bl_on_del: "삭제 시 관리 도구 기록 리소스 건너뛰기", lbl_pwd_manage: "압축 해제 암호 관리", title_pwd_vault: "비밀번호 금고", lbl_pwd_try_count: "압축 파일당 암호 매칭 시도 제한", tip_pwd_manual: "한 줄에 암호 하나씩, Enter로 줄바꿈", str_root_dir_cn: "루트 디렉토리", btn_ana_select: "일괄 선택", opt_keep_new: "최신 항목 유지", opt_keep_old: "오래된 항목 유지", opt_keep_large: "가장 큰 항목 유지", opt_keep_small: "가장 작은 항목 유지", opt_keep_short: "이름이 가장 짧은 항목 유지", opt_keep_long: "이름이 가장 긴 항목 유지", /* --- 状态、进度与加载短语 --- */ loading: "로딩 중...", loading_detail: "디렉토리 구조를 최고 속도로 인덱싱 중...", loading_fetch: "가져오는 중... ({n})", loading_dup: "중복 항목 분석 중... ({p}%)", str_loading_placeholder: "로딩 중...", str_load_failed: " (로딩 실패)", str_load_failed_simple: "로딩 실패", str_waiting_token: "로그인 상태 동기화 중...", str_speed: "속도", status_scanning_selection: "선택 항목 스캔 중... {n}", status_ready: "{n}개 항목", sel_count: "{n}개 항목 선택됨", str_cached: "캐시됨:", str_retries: "재시도:", str_failed: "실패:", str_success: "성공:", str_stopping: "중지 중...", str_merging: "데이터 병합 중...", str_rendering: "목록 렌더링 중...", str_scanning: "스캔 중...", str_analyzing: "분석 중...", str_deleting: "삭제 중...", str_saving: "저장 중...", str_saving_dots: "저장 중...", str_checking_bl: "명단 기록 매칭 중...", str_processing: "시스템이 최고 속도로 처리 중입니다...", str_cleanup_done: "정리 완료.", str_waiting_preload: "프리로드 대기 중...", str_copying: "클립보드에 복사 중...", str_moving: "이동 준비 중...", str_sorting: "정렬 중...", str_refreshing: "새로고침 중...", str_refreshing_cache: "캐시 새로고침 중...", str_syncing_stars: "즐겨찾기 상태 동기화 중...", str_updating_view: "뷰 업데이트 중...", str_generating_view: "뷰 생성 중...", str_group: "그룹", str_init_rename: "이름 바꾸기 초기화 중...", str_renaming: "이름 바꾸는 중...", str_calc_changes: "변경 사항 계산 중...", str_scanning_dir: "디렉토리 구조 스캔 중...", str_init_op: "작업 초기화 중...", str_init_scan: "전체 스캔 초기화 중...", str_rebuilding: "인덱스 재구성 중...", str_upload_1: "업로드 중 (노드 1/3)...", str_upload_2: "노드 1 타임아웃, 노드 2로 전환...", str_upload_3: "노드 2 타임아웃, 마지막 노드 시도...", str_upload_fail_copy: "업로드 실패, 클립보드 작성을 준비합니다...", msg_transcoding: "클라우드 인코딩 중...", msg_transcoding_wait: "서버에서 비디오를 처리 중입니다. 잠시만 기다려 주세요.", str_preparing: "압축 해제 준비 중...", str_unzipping: "압축 해제 중: {n}", str_unzipping_state: "압축 해제 중...", str_unzipping_prog_0: "압축 해제 중: 0%", str_unzipping_prog_100: "압축 해제 중: 100%", str_unzipping_prog_fmt: "압축 해제 중: {n}%", msg_task_waiting: "대기 중...", msg_task_hashing: "파일 체크섬 확인 중...", msg_task_init_upload: "업로드 초기화 중...", msg_task_uploading: "업로드 중...", msg_task_init_part: "파티션 초기화 중...", msg_task_uploading_2: "업로드 중...", str_loc_tracing: "경로 추적 중...", str_loc_stale: "캐시 만료됨, 클라우드와 동기화 중...", str_verifying: "확인 중...", str_server_indexing: "서버 인덱싱 중... ({n}/5)", str_creating_task_n: "작업 생성 중 ({n}/{t})...", msg_submit_request: "요청 제출 중... {c}/{t}", msg_wait_server: "서버 응답 대기 중... ({c}/{t})", msg_server_processing: "서버 처리 중... ({c}/{t})", str_jav_querying: "조회 중...", lbl_done_check: "✔ 완료", msg_limit_updated: "추출 횟수가 업데이트되었습니다", /* --- 提示、确认与交互消息 --- */ title_alert: "알림", title_confirm: "확인", title_prompt: "입력", btn_ok: "확인", btn_yes: "예", btn_no: "아니요", btn_save: "설정 저장", btn_cancel: "취소", btn_create: "생성", btn_skip: "건너뛰기", msg_down_success: "브라우저를 통해 {n}개의 파일 다운로드를 시작했습니다.", msg_batch_txt: "다운로드 목록(.txt)이 생성되었습니다.", msg_clear_history_done: "기록에서 제거되었습니다.", msg_skip_unzipped: "이미 압축 해제된 항목 {n}개를 건너뛰었습니다.", msg_unzip_skip_del_confirm: "이미 압축 해제된 {n}개의 압축 파일을 감지했습니다. 휴지통으로 이동하시겠습니까?", msg_cancel_share_confirm: "선택한 {n}개의 공유를 취소하시겠습니까?\n링크가 즉시 무효화됩니다.", msg_pwd_updating: "비밀번호 업데이트 중...", msg_pwd_updated: "비밀번호가 업데이트되었습니다", msg_exp_updated: "유효 기간이 업데이트되었습니다", msg_cancel_share_done: "{n}개의 공유가 취소되었습니다.", msg_drag_drop_hint: "파일을 여기에 끌어다 놓으세요", str_drag_files: " 외 {n}개 파일", msg_creating_share: "공유 링크 생성 중...", title_share_result: "공유 성공", msg_no_files: "항목이 없습니다.", msg_no_selection: "먼저 항목을 선택해 주세요.", warn_del: "선택한 {n}개 항목을 삭제하시겠습니까?", msg_clear_sel_confirm: "{n}개의 중복 파일이 선택되었습니다. 선택을 취소하시겠습니까?", str_bl_stat: "일치: {n}개 | 선택됨: {m}개", str_hits: "적중", msg_settings_saved: "설정이 저장되었습니다. 페이지가 새로고침됩니다.", msg_name_exists: "이미 존재하는 이름입니다: {n}", str_name_conflict: "(이름 중복 가능성)", msg_newfolder_prompt: "새 폴더 이름:", msg_rename_prompt: "새 이름 입력:", msg_copy_done: "복사되었습니다. 붙여넣을 위치를 선택해 주세요.", msg_cut_done: "이동 준비 완료. 붙여넣을 위치를 선택해 주세요.", msg_paste_empty: "붙여넣을 항목이 없습니다.", msg_copy_empty: "클립보드가 비어 있거나 텍스트 데이터가 없습니다.", msg_add_success: "{n}행의 데이터가 추가되었습니다.", msg_del_done: "선택한 행이 삭제되었습니다.", msg_del_select: "삭제할 행을 먼저 클릭하여 선택하세요!", msg_del_items_done: "{n}개 항목이 삭제되었습니다.", msg_copy_success: "복사 성공", str_redirecting: "Google 렌즈로 이동 중...", msg_manual_paste: "이미지 업로드 시간 초과. 스크린샷이 복사되었습니다. 새 창에서 {cmd}를 눌러주세요.", msg_starring: "즐겨찾기 추가 중...", msg_unstarring: "즐겨찾기 취소 중...", msg_star_added: "즐겨찾기에 추가되었습니다", msg_unstar_done: "즐겨찾기가 취소되었습니다", msg_empty_trash_confirm: "휴지통을 비우시겠습니까? 이 작업은 되돌릴 수 없습니다!", msg_trash_emptied: "휴지통을 비웠습니다.", msg_del_forever_confirm: "선택한 {n}개 항목을 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다!", msg_del_forever_done: "{n}개 항목이 영구적으로 삭제되었습니다.", msg_restore_done: "{n}개 항목이 성공적으로 복원되었습니다.", msg_auto_sub_load: "자막이 자동으로 로드되었습니다: {n}", msg_dl_sub: "자막 다운로드 중...", msg_transcode_done: "✅ 인코딩 완료, 재생을 시작합니다", msg_fallback_report: "⚠️ 원본 재생 불가(MPEG4/HEVC), 자동으로 {n} 화질로 전환되었습니다", tip_manual_sub: "팁: .srt 또는 .vtt 파일을 플레이어에 드래그하여 로드할 수 있습니다.", msg_sub_drop_load: "드래그를 통해 자막이 로드되었습니다: {n}", msg_resume_hint: "{t} 지점부터 이어서 재생합니다. 여기를 클릭하세요 ", msg_unzip_confirm_n: "현재 폴더에 {n}개의 파일을 압축 해제하시겠습니까?", msg_task_paused: "일시정지됨", msg_task_added: "{n}개의 업로드 작업이 추가되었습니다", msg_task_fast_success: "순식간에 전송 완료(초고속 업로드)", msg_task_upload_done: "업로드 완료", log_dirty_data: "잘못된 데이터 감지! 요청 모드가 현재 뷰와 일치하지 않아 작업을 중단했습니다.", msg_network_unstable: "네트워크가 불안정합니다. 자동으로 복구를 시도 중...", msg_skip_locked: "{n}개 항목이 잠겨 있음", msg_skip_self: "{n}개 항목이 현재 위치와 동일함", msg_skip_conflict: "{n}개 하위 경로가 사용 중임", msg_skip_invalid: "유효하지 않은 항목을 건너뛰었습니다: ", msg_creating_cloud_task: "클라우드 다운로드 작업 생성 중...", str_parsing_torrent: "토렌트 파일 분석 중...", err_torrent_no_info: "분석 실패: 유효한 정보를 찾을 수 없음", err_file_read: "파일 읽기 오류", msg_cloud_task_finish: "생성 완료: {s} 성공, {f} 실패", msg_cloud_task_success: "🎉 {n}개의 작업이 성공적으로 생성되었습니다", msg_prepare_restore: "복원 준비 중...", msg_smart_matching_n: "비밀번호 스마트 매칭 중 ({n}개)...", msg_system_busy_retry: "시스템이 바쁩니다. 재시도 중 ({n}/5)...", msg_unzip_running_bg: "[{n}] 클라우드에서 압축 해제 중입니다. 잠시 후 새로고침하여 확인하세요.", msg_share_code_updated: "공유 코드가 업데이트되었습니다", msg_retry_submitted: "{n}개의 작업을 다시 제출했습니다", msg_aria2_batch_fail_log: "\n\n실패 항목이 많아 전체 오류 목록(.txt)을 자동으로 내보냈습니다.", str_aria2_fetch_err: "(링크 가져오기 실패)", str_aria2_rpc_err: "(전송 실패)", str_aria2_aborted: "(취소됨)", str_aria2_fail_file_name: "Aria2_실패_목록", msg_op_blocked_moving: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 완료 후 시도해 주세요.", msg_op_blocked_analyzing: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 정확한 폴더 분석 데이터를 위해 현재 작업이 완료된 후 다시 시도해 주세요.", msg_op_blocked_exporting: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 정확한 디렉토리 내보내기를 위해 현재 작업이 완료된 후 다시 시도해 주세요.", msg_analyze_only_normal_dir: "폴더를 선택해 주세요", msg_analyze_no_large_folders: "설정된 임계값 범위 내의 폴더를 찾을 수 없습니다 ({s})", msg_analyze_summary_fmt: "용량이 <b>{n}</b>개인 {s}GB 이상의 폴더를 발견했습니다 (크기순 정렬)", msg_prune_blocked_moving: "⚠️ 작업 차단\n\n파일 이동 중에는 정리를 수행할 수 없습니다.", msg_global_index_blocked_moving: "⚠️ 작업 차단\n\n파일 이동 중에는 전체 검색 인덱스를 생성할 수 없습니다. 완료 후 검색해 주세요.", msg_resource_locked_download: "⚠️ 작업 차단\n\n선택 항목에 이동 중인 파일이 포함되어 있습니다. 이동 완료 후 다운로드해 주세요.", msg_resource_locked_aria2: "⚠️ 작업 차단\n\n선택 항목에 이동 중인 파일이 포함되어 있습니다. 완료 후 Aria2로 전송해 주세요.", msg_flatten_blocked_moving: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 작업이 진행 중입니다. 현재 파일 분석을 실행하면 파일 목록이 누락되거나 중복될 수 있으니 잠시 후 다시 시도해 주세요.", err_task_conflict: "⚠️ 작업 차단\n\n백그라운드에서 파일 이동 중입니다. 완료 후 정리를 실행해 주세요.", title_del_task_confirm_fmt: "{n}개의 전송 작업을 삭제하시겠습니까?", lbl_del_cloud_files_too: "클라우드 내 실제 파일도 함께 삭제", msg_file_del_failed: "파일 삭제 실패: ", msg_task_del_success_fmt: "{n}개의 작업이 삭제되었습니다", title_clear_task_confirm: "모든 업로드 작업을 삭제하시겠습니까?", msg_task_clear_success_fmt: "{n}개의 업로드 작업이 삭제되었습니다", msg_unzip_virtual_view_warn: "현재 가상 뷰에서 작업 중입니다. 압축 해제된 파일은 <b>각 압축 파일이 위치한 실제 폴더</b>에 저장되며, 현재 목록에 즉시 나타나지 않을 수 있습니다.<br><br>계속하시겠습니까?", msg_smart_matching_file: "비밀번호 스마트 매칭 중... ({n})", msg_unzip_batch_submitted: "✅ {n}개의 압축 해제 완료", msg_unzip_batch_skipped: " ({n}개 건너뜀)", msg_unzip_check_source: ". 원본 폴더에서 결과를 확인하세요.", tip_jump_to_folder: "이 폴더로 이동", msg_task_deleted: "작업이 삭제되었습니다", msg_scan_done: "스캔 완료!\n총 {n}개의 파일 발견, {f}개의 폴더 탐색.", msg_scan_fail: "\n\n❌ {n}개 실패.", msg_scan_fix: "\n\n✅ {n}회의 네트워크 오류를 자동 복구했습니다.", msg_down_scanning: "폴더 내용 분석 중...", msg_down_progress: "브라우저 다운로드 호출 중...", msg_down_confirm_total: "✅ 스캔 완료, 총 {n}개의 파일을 찾았습니다.\n\n⚠️ 주의: 다량의 파일을 브라우저로 다운로드하면 페이지가 멈추거나 차단될 수 있습니다.\n10개 이상의 파일은 Aria2 내보내기를 권장합니다.\n\n브라우저 다운로드를 강행하시겠습니까?", msg_aria2_sending_batch: "🚀 Aria2로 작업을 분할 전송 중입니다...", msg_aria2_check_fail: "Aria2 연결 실패!\nURL 및 토큰을 확인해 주세요.", msg_aria2_check_ok: "Aria2 연결 성공!", msg_aria2_sent: "{n}개의 파일을 Aria2로 전송했습니다.", msg_aria2_test_fail: "Aria2 연결에 실패했습니다.\n설정을 그대로 저장하시겠습니까?", title_aria2_fail: "연결 테스트 실패", msg_batch_scanning: "🚀 디렉토리 구조 고속 스캔 중...", msg_batch_hydrating: "⚡ 다운로드 링크 병렬 추출 중...", msg_batch_no_files: "다운로드 가능한 파일을 찾을 수 없습니다.", msg_dup_warn: "중복 파일 검색을 시작하시겠습니까?", msg_dup_result: "{n}그룹의 중복 항목을 발견했습니다.", msg_dup_none: "중복 파일을 찾지 못했습니다.", msg_bl_stop: "작업이 중지되었습니다.", msg_bl_add_done: "{n}개의 항목이 기록에 추가되었습니다.", msg_bl_remove_done: "{n}개의 항목이 기록에서 제거되었습니다.", msg_bl_empty: "명단 목록이 비어 있어 실행할 수 없습니다.", msg_bl_clear_confirm: "모든 기록 항목을 지우시겠습니까? 이 작업은 복구할 수 없습니다.", msg_blacklist_run_none: "클라우드 내에 명단 조건에 맞는 항목이 없습니다.", msg_blacklist_run_confirm: "클라우드 내에서 {n}개의 기록된 항목을 발견했습니다.\n\n지금 휴지통으로 이동하시겠습니까?", msg_bl_run_limit: "⚠️ 모드 제한\n\n정리 작업은 실제 파일 재귀 작업이 필요합니다. 홈 폴더의 일반 폴더로 돌아가서 실행해 주세요.", msg_del_protected: "{n}개의 기록된 파일이 삭제되지 않도록 보호되었습니다.", msg_del_none: "삭제할 파일이 없습니다.", msg_bl_scanning: "전체 검색 중... \n스캔한 폴더: {d} | 일치: {f}", rn_tip_wait: "규칙을 설정해 주세요", rn_tip_jav: "위 버튼을 클릭하여 매칭을 시작하세요", rn_tip_none: "일치하는 항목이나 이름이 없습니다", rn_stat: "일치: {n}개 | 유효 변경: {m}개", rn_warn_confirm: "{n}개 파일의 이름을 바꾸시겠습니까?", msg_bulkrename_done: "{n}개 항목의 이름을 바꿨습니다.", msg_rn_all_skipped: "❌ 이름이 중복되어 모든 작업을 건너뛰었습니다.", msg_rn_fail_count: "이름 중복으로 {n}개 항목 건너뜀", msg_prune_confirm: "현재 목록에서 빈 폴더 검색을 시작하시겠습니까?", msg_prune_none: "빈 폴더가 없습니다.", msg_prune_found: "{n}개의 빈 폴더를 발견했습니다.\n지금 삭제하시겠습니까?", msg_deleting_folders: "{n}개의 폴더 삭제 중...", msg_global_warn: "전체 파일 동기화를 시작합니다.\n\n동기화된 파일은 로컬 메모리에 캐싱되며 페이지 새로고침 전까지 유지됩니다.\n\n계속하시겠습니까?", msg_init_scan_sel: "선택 항목 스캔 초기화 중...", warn_clear_history: "선택한 {n}개 항목을 재생 기록에서 제거하시겠습니까?\n(클라우드 파일은 삭제되지 않습니다)", msg_img_copy_hint: "새 창에서 {cmd}를 누르면 검색이 시작됩니다.", msg_aria2_not_set: "Aria2가 설정되지 않았습니다. 정보를 입력하고 계속하세요:", str_jav_no_match: "(품번 매칭 실패)", msg_unzip_fail: "압축 해제 요청 실패", msg_jszip_fail: "JSZip 로드 실패. 네트워크 상태를 확인하세요.", msg_turbo_activated: "터보 모드 활성화: 스크립트가 웹 로직을 제어하여 최상의 성능을 유지합니다.", msg_console_legal: "상업적 이용 엄격 금지: 이 프로젝트는 개인 학습 및 교류 목적으로만 사용됩니다.", msg_ana_warn: "폴더 중복 확인 팁: 알고리즘 추측을 바탕으로 합니다. 삭제 전 반드시 직접 확인하여 오삭제를 방지하세요。", /* --- 错误提示 --- */ err_invalid_links: "올바른 링크를 입력해 주세요", err_pwd_format: "비밀번호는 4-10자의 영문 또는 숫자여부야 합니다", err_invalid_torrent: "유효하지 않은 토렌트 파일 형식", err_torrent_complex: "구문 분석 복잡도가 너무 높음 (잘못된 파일 가능성)", err_torrent_format: "토렌트 구조가 손상됨", err_torrent_len: "필드 길이 분석 오류", err_torrent_char: "잘못된 문자 발견", err_share_code_exists: "이미 사용 중인 공유 코드입니다", err_folder_not_ready: "클라우드 폴더를 생성 중입니다. 잠시 후 다시 시도해 주세요", err_item_deleted: "항목이 존재하지 않습니다", err_network: "네트워크 오류", err_clipboard_denied: "클립보드 액세스가 거부되었습니다", err_worker: "워커 스레드 오류", err_api: "API 오류", err_capture: "캡처 실패.", err_captcha_simple: "인증 실패. 웹 리스트에서 수동으로 파일을 한 번 저장하여 인증을 완료해 주세요.", err_sub_dl_fail: "자막 다운로드 실패: ", err_req_blocked: "네트워크 요청 실패 (차단되었을 수 있음)", err_req_timeout: "요청 시간 초과", err_sub_drop_type: "자막 형식의 파일만 지원합니다", err_codec_t1: "비디오 코덱을 재생할 수 없습니다 ({c})", err_codec_t2: "브라우저에서 지원하지 않는 비디오 형식입니다.<br>아래 버튼을 눌러 외부 플레이어를 사용하세요.", err_pwd_simple: "비밀번호 오류", err_task_exists: "이미 존재하는 작업입니다", err_network_break: "이미지 노드 네트워크 단절. 다시 클릭하여 시도하세요.", err_no_failed_task: "실패한 작업이 선택되지 않았습니다", err_unknown: "알 수 없는 오류", err_invalid_regex: "유효하지 않은 정규식입니다", err_parent_not_found: "폴더를 찾을 수 없습니다", msg_sys_error: "시스템 폴더는 조작할 수 없습니다", msg_download_fail: "다운로드 링크를 가져올 수 없습니다.", msg_video_fail: "비디오 링크를 가져올 수 없습니다.", err_star_sync_fail: "즐겨찾기 동기화 실패", err_paste_descendant: "현재 폴더 또는 하위 폴더로 이동/복사할 수 없습니다", err_quota_exceeded: "저장 공간 부족", err_name_exists: "파일 이름이 중복될 수 없습니다", err_share_pass: "추출 코드는 4-10자여야 합니다", str_error: "오류", str_error_crit: "치명적 오류", str_error_paste: "붙여넣기 오류", str_action_failed: "작업 실패", str_scan_error: "스캔 오류", err_limit_too_low: "수정 실패: 새 횟수({n})는 현재 저장된 횟수({s})보다 커야 합니다", err_vault_max: "비밀번호 금고는 최대 50개의 자주 사용하는 비밀번호만 저장할 수 있습니다", err_pwd_len: "단일 비밀번호의 길이는 127자를 초과할 수 없습니다", /* --- 帮助文档 --- */ modal_help_title: "도움말", help_desc: ` <div class="pk-no-scrollbar pk-help-scroll" style="font-size:13px;line-height:1.6;color:var(--pk-fg);text-align:justify;text-justify:inter-ideograph;word-break:break-all;pointer-events:auto;display:block;"> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">✨ 경험 및 탐색 엔진</b><br> • <b>인터랙션 재구성</b>: 인터페이스를 <b>Windows 파일 탐색기</b> 스타일로 재구성했습니다.<br> • <b>초고속 모드</b>: 활성화 시 기본 웹 로직을 완전히 제어하여 대량 파일 환경에서의 렉과 충돌을 해결합니다.<br> • <b>고급 경로 표시줄</b>: 마우스 휠 스크롤 및 드롭다운 메뉴 동급 전환을 지원합니다. 전체 검색 및 분석 도구가 통합되어 경로 복원 및 상위 이동을 지원합니다.<br> • <b>사용자 경험 향상</b>: 즐겨찾기 등 다차원 정렬, 원클릭 <b>커버 블러 처리</b> 및 다크 테마 전환을 지원합니다. 백그라운드에서는 <b>SWR 전략</b>을 사용하여 무감각하게 뷰를 새로 고칩니다.<br> • <b>백그라운드 인덱싱 및 보호</b>: 홈 아이콘의 파란색 점멸은 디렉토리 트리 동기화를 나타냅니다. 충돌하는 작업을 차단하고 데이터 손상을 방지하는 동시성 물리적 잠금을 제공합니다.<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 참고: 기본 폴더(My Pack)는 시스템 보호를 받아 삭제, 복사, 이동 및 이름 변경이 제한됩니다.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">📂 일괄 처리 및 공간 관리</b><br> • <b>일괄 이름 변경</b>: <b>정규식 치환/삭제</b>, <b>에피소드 번호</b> 생성, 텍스트 <b>포맷팅</b>, <b>FC2 표준 네이밍</b>, <b>광고 제거</b> 및 MIME 기반 <b>확장자 복구</b>를 지원합니다.<br> • <b>분석 도구</b>: <b>파일 분석</b>(필터링 및 해시/시간/이름 3가지 모드 중복 검사)과 <b>폴더 분석</b>(필터링 및 이름/유사도/포함율 3가지 모드 중복 검사)을 통합 제공하며, 현재 경로의 <b>디렉토리 트리</b> 내보내기를 지원합니다.<br> • <b>스마트 정리</b>: 원클릭 빈 폴더 정리; <b>일괄 압축 해제</b> 기능은 암호 자동 기억 및 스마트 입력을 지원하며, 해제된 항목 건너뛰기 및 삭제를 지원합니다.<br> • <b>리소스 관리자</b>: <b>파일 블랙리스트</b>를 설정하여 스팸 리소스를 한 번에 정리하거나, <b>화이트리스트</b>로 설정하여 일괄 삭제 시 해당 파일을 보호할 수 있습니다.<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 참고: 데이터 동기화 충돌을 방지하기 위해 처리 중에는 다른 클라이언트에서 파일을 수정하지 마십시오.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🌐 전송 및 공유 센터</b><br> • <b>공유 관리</b>: 추출 횟수 상한 설정을 지원하며, 횟수 도달 시 링크가 자동으로 무효화됩니다.<br> • <b>초고속 업로드</b>: 로컬 대용량 파일 및 폴더를 웹으로 드래그 앤 드롭하여 바로 업로드할 수 있으며, 공식 제한을 돌파하고 <b>소용량 파일 전송 중단율을 대폭 낮췄습니다</b>.<br> • <b>클라우드 다운로드 향상</b>: 일괄 오프라인 링크 <b>자동 중복 제거</b>. 내장형 <b>마그넷 스마트 정리 엔진</b>(Base32/Hex 해시를 자동 추출하여 간섭 제거); <b>.torrent</b> 시드 파일 파싱 지원; 제한된 링크에 대한 <b>웹 스냅샷 저장</b> 기능 제공. <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 참고: 추출 횟수 차단은 웹페이지가 열려 있고 컴퓨터가 절전 모드가 아닐 때만 작동합니다.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🎬 몰입형 미디어 기능 향상</b><br> • <b>재생 엔진</b>: 0.5x~3.0x 배속, 화면 회전/미러링, 강제 비율 조절, 오프닝/엔딩 건너뛰기 및 <b>연속 재생/루프</b> 모드를 지원하며 진행률 바 썸네일 미리보기를 지원합니다. 내장된 <b>감시견(Watchdog)</b>을 통해 블랙 스크린이나 지원되지 않는 코덱 발생 시 호환 화질로 자동 폴백합니다.<br> • <b>자막 시스템</b>: 클라우드 내 동일 이름 자막, 로컬 파일 및 온라인 자막 검색을 지원합니다. 자막 싱크 미세 조정 및 로컬 텍스트 <b>드래그 앤 드롭 파싱</b> 마운트를 지원합니다.<br> • <b>시각 보조</b>: 다중 엔진 기반 <b>이미지로 검색</b> 기능을 지원합니다. "미디어 모드" 활성화 시 시리즈/만화 폴더가 자동으로 A-Z 순서로 정렬됩니다.<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 참고: 재생 기록 목록은 스크립트 환경 내에서 발생한 재생 진행도만 지속적으로 기록합니다.</div> </div> <div style="margin-bottom:12px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚙️ 설정 및 데이터 관리</b><br> • <b>설정 백업</b>: 개인 설정, 관리 규칙, 암호 금고 등을 디지털 지문이 포함된 JSON 파일로 내보낼 수 있으며, 가져오기 시 <b>스마트 병합 및 중복 제거</b>를 지원합니다.<br> • <b>데이터 정리</b>: 전체 인덱스, 설정, 규칙, 암호 금고 및 캐시를 필요에 따라 지워 로컬 공간을 확보하고 개인 정보를 보호할 수 있습니다.<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 참고: 전체 인덱스는 웹페이지 종료 시 삭제되지만, 설정 및 비밀번호 금고 데이터는 영구 보관됩니다.</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚡ 다운로드 및 배포</b><br> • <b>외부 직접 연결</b>: 클릭 한 번으로 비디오 스트리밍 직링크를 얻거나 PotPlayer 재생을 호출합니다. RPC 프로토콜을 통해 파일을 즉시 <b>Aria2</b> 노드로 전송할 수 있습니다.<br> • <b>배포 향상</b>: 폴더를 Aria2로 푸시할 때 <b>클라우드 트리 디렉토리 구조를 자동 복원</b>합니다. 장기 연결 모니터링을 지원하며 오류 발생 시 오류 목록을 자동 내보냅니다. <b>폴더 다운로드 필터링</b> 설정을 지원합니다. </div> <div style="margin-top:16px; color:#d93025; font-weight:bold; text-align:center; font-size:11px; border-top:1px dashed rgba(217,48,37,0.2); padding-top:12px; letter-spacing:0.5px; opacity:0.9;"> 이 프로젝트는 CC-BY-NC-SA-4.0 라이선스를 엄격히 준수하며, 상업적 이용을 금지합니다. </div> </div>` }, ja: { /* --- 通用与基础UI --- */ title: "PikPak 拡張マスター", str_original: "原画", str_original_fast: "原画 (高速)", str_folders: "フォルダ", str_files: "ファイル", unit_folders: "個のフォルダ", unit_days: "日", unit_month: "ヶ月", unit_sec: "秒", str_no_files: "ファイルがありません", str_items: "項目", col_name: "名前", col_size: "サイズ", col_dur: "種別/長さ", col_duration_only: "長さ", col_progress: "再生進捗", col_play_time: "再生時間", col_date: "更新日時", col_remaining: "残り時間", col_path: "パス", col_old: "元の名前", col_new: "新しい名前", col_type: "種類", col_path_name: "パス / 名前", col_action: "操作", lbl_folder_first: "フォルダを先頭に表示", tag_default: "既定", current_dir: "現在のディレクトリ", str_same_folder: "(同一フォルダ内)", lbl_dont_show: "次回から表示しない", lbl_dont_show_session: "このセッションで表示しない", str_empty_filename: "(空のファイル名)", str_empty_dir: "(空のディレクトリ)", btn_filter: "フィルタ", title_file_filter: "ファイルフィルタ", cat_all: "すべて", cat_video: "動画", cat_audio: "音声", cat_image: "画像", cat_document: "文書", cat_software: "ソフト", cat_archive: "圧縮画庫", cat_torrent: "BT種子", cat_other: "その他", btn_exit_filter: "フィルタ解除", /* --- 属性面板 --- */ ctx_property: "プロパティ", title_property: "ファイルプロパティ", lbl_prop_name: "ファイル名", lbl_prop_size: "容量", lbl_prop_count: "ファイル数", lbl_prop_ctime: "作成日時", lbl_prop_mtime: "更新日時", lbl_prop_source: "追加ソース", lbl_prop_link: "リソースリンク", lbl_prop_path: "場所", str_prop_cloud: "クラウド追加", str_prop_share: "共有から", str_prop_user: "ユーザーアップロード", str_prop_unknown: "不明なソース", fmt_prop_count: "{f} 個のファイル、{d} 個のフォルダ", str_prop_offline: "オフラインタスク", /* --- 导航、视图模式与右键菜单 --- */ btn_nav_home: "ホーム", btn_nav_share: "マイ共有", btn_nav_offline: "リンク追加", btn_nav_recent: "最近の追加", btn_nav_history: "再生履歴", btn_nav_starred: "スター付き", btn_nav_trash: "ゴミ箱", btn_nav_upload: "ローカル", title_offline: "マイオフライン", trash_title: "ゴミ箱", trash_notice: "ゴミ箱のファイルは最大15日間保存されます", history_notice: "スクリプト環境内で発生した再生進捗のみを記録します", ctx_open: "開く", ctx_add_bl: "リソース管理に追加", ctx_remove_bl: "リソース管理から削除", ctx_rename: "名前の変更", ctx_copy: "コピー", ctx_copy_name: "名前をコピー", ctx_copy_link: "リンクをコピー", ctx_cut: "移動", ctx_del: "削除", ctx_down: "ダウンロード", ctx_star: "スターを追加", ctx_unstar: "スターを外す", ctx_locate: "ファイルの場所を開く", ctx_share: "共有", /* --- 通用文件操作按钮 --- */ btn_down: "ダウンロード", tip_down: "ダウンロード [Alt] + [D]", btn_aria2: "Aria2 へ送信", tip_aria2: "Aria2 へ送信 [Alt] + [A]", btn_refresh_short: "更新", tip_refresh: "更新 [F5]", btn_newfolder: "新規フォルダ", tip_newfolder: "新規フォルダ [F8]", btn_del: "削除", tip_del: "削除 [Delete]", btn_deselect: "選択解除", tip_deselect: "選択解除 [Esc]", btn_invert: "選択反転", btn_copy: "コピー", tip_copy: "コピー [Ctrl] + [C]", btn_cut: "移動", tip_cut: "移動 [Ctrl] + [X]", btn_paste: "貼り付け", tip_paste: "貼り付け [Ctrl] + [V]", btn_clear_history: "履歴を削除", tip_clear_history: "履歴を削除 [Delete]", btn_restore: "元に戻す", tip_restore: "元に戻す [R]", btn_del_forever: "完全に削除", tip_del_forever: "完全に削除 [Delete]", btn_empty_trash: "ゴミ箱を空にする", tip_empty_trash: "ゴミ箱を空にする [Shift] + [Delete]", btn_exit: "終了", btn_close: "閉じる", tip_close: "閉じる [Esc]", tip_theme: "テーマ切替 [Alt] + [T]", tip_rotate: "回転 [R]", tip_mirror: "左右反転 [H]", tip_flip_v: "上下反転 [V]", tip_maximize: "最大化 [M]", tip_minimize: "最小化 [M]", tip_full_screen: "全画面 [Enter]", btn_help: "ヘルプ", tip_help: "ヘルプ [Alt] + [H]", btn_view_file: "ファイルを表示", btn_jump: "ジャンプ", btn_copy_text: "コピー", btn_stop: "停止", tip_stop: "現在の操作を停止", btn_settings: "設定", btn_logout: "サインアウト", msg_logout_confirm: "ログアウトしますか?", tip_settings: "設定とその他 [Alt] + [S]", lbl_upload_to: "アップロード先: ", msg_move_done: "移動が完了しました。", /* --- 离线、上传与云下载 --- */ btn_upload: "アップロード", btn_up_file: "ファイル選択", btn_up_folder: "フォルダ選択", btn_cloud_download: "クラウドDL", btn_up_pause: "一時停止", tip_up_pause: "一時停止 [Alt] + [P]", btn_up_start: "再開", tip_up_start: "再開 [Alt] + [G]", btn_up_del: "タスク削除", tip_up_del: "タスク削除 [Delete]", btn_up_clear_all: "タスクをクリア", tip_up_clear_all: "タスクをクリア [Shift] + [Delete]", btn_retry_task: "リトライ", tip_retry_task: "リトライ [R]", col_task_status: "タスク状態", col_task_progress: "転送進捗", col_up_speed: "速度", col_up_status: "状態", lbl_task_run: "実行中", lbl_task_fail: "失敗", lbl_task_ok: "完了", lbl_up_run: "実行中", lbl_up_pause: "中断", lbl_up_downloading: "ダウンロード中", lbl_up_done: "完了", tip_up_pause_desc: "手動停止およびエラーが発生したタスク", title_cloud_task: "クラウドタスク作成", ph_cloud_links: "対応リンク形式:\n- magnetなどの各種ダウンロードリンク\n- YouTube、X (Twitter)、TikTok、Facebookなどの共有リンク\n改行で複数リンクの一括追加が可能です。", lbl_save_to: "保存先:", lbl_default_folder: "デフォルトフォルダ", btn_via_torrent: "Torrent経由", tip_cloud_save_path: "通常のクラウドダウンロードファイルは My Pack ディレクトリに保存されます。他アプリからのクラウドダウンロードファイルは My [XYZ] ディレクトリ([XYZ] はアプリ名)に保存されます。", lbl_smart_fix: "規制回避磁気リンクの自動修復 (ハッシュ抽出 / 文字ノイズ削除)", title_save_method: "保存方法", msg_save_snapshot_desc: "このリンクはウェブスナップショットとしてのみ保存可能です。", tip_snapshot_details: "PikPakはこのリンクからメディアファイルを直接抽出できませんが、ウェブページの内容をスナップショットとして保存できます。", btn_save_snapshot: "スナップショットを保存", btn_create_now: "作成", btn_modify: "変更", str_snap_link_count_suffix: " 他 {n} 件のリンク", /* --- 分享管理 --- */ btn_cancel_share: "共有を解除", share_copy_suffix: "この内容をコピーして PikPak アプリを開くと、高速再生を楽しめます", share_copy_pwd: "パスワード", title_share_detail: "共有の詳細", ctx_share_detail: "共有の詳細を表示", ctx_share_copy: "リンクとパスワードをコピー", col_view: "閲覧数", col_save: "保存数", col_share_time: "共有日時", col_share_status: "共有状態", lbl_limit_reached: "制限到達", lbl_limit_tip: "回数制限", lbl_share_view: "閲覧", lbl_share_save: "保存", lbl_share_link_title: "共有リンク", lbl_share_pwd_title: "パスワード", lbl_share_expire_title: "有効期限", btn_copy_link_pwd: "リンクとパスワードをコピー", str_expire_suffix: "日で失効", ph_edit_pwd: "パスワードを入力 (4-10桁)", btn_close_pwd: "パスワードを無効化", str_no_pwd: "パスワードなし", title_edit_share_code: "共有コードの変更", ph_edit_share_code: "5-18桁の英数字・記号に対応", btn_add_share_code: "共有コードを追加", btn_del_share_code: "共有コードを削除", share_title: "ファイルを共有", share_mode: "共有方法", share_public: "公開リンク", share_encrypted: "非公開リンク", share_expiry: "有効期限を設定", share_pass: "抽出コードを設定", share_count: "抽出回数を設定", share_count_ed: "抽出回数", share_perm: "無期限", share_unlimit: "無制限", share_rand: "ランダム生成", share_custom: "カスタム", share_days: "日", share_times: "回", btn_share_start: "共有を開始", cal_custom_title: "有効期限の選択", lbl_share_link: "リンク", lbl_share_code: "コード", btn_copy_share: "すべてコピー", str_share_expired: "期限切れ", str_share_deleted: "ファイル削除済み", title_edit_pwd: "パスワード変更", lbl_share_code_title: "共有コード", ph_password: "パスワード", ph_pass_range: "4-10文字", cal_week_days: ["日", "月", "火", "水", "木", "金", "土"], cal_months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], /* --- 文件分析与文件夹分析 --- */ title_file_analysis: "ファイル分析", btn_scan: "ファイル透視", lbl_scan_selected: "選択した {n} 項目にファイル透視を実行します", lbl_keyword_filter: "除外キーワード", ph_keyword_filter: "除外するキーワード、カンマ区切り", lbl_scan_current: "現在のパスのすべての項目にファイル透視を実行します", tip_dup: "重複チェック", lbl_dup_selected: "選択した {n} 項目にファイル重複チェックを実行します", lbl_dup_current: "現在のパスのすべての項目にファイル重複チェックを実行します", tip_scan_dup: "ファイルの絞り込みまたは重複チェック", lbl_dup_tool: "削除対象の選択:", lbl_dup_reset: "↺ リセット (固定解除 & 選択クリア)", lbl_dup_select_folder: "📂 フォルダごとに選択", lbl_dup_select_folder_short: "📂 フォルダ", lbl_dup_invert: "反転モード", lbl_dup_invert_short: "反転", tip_dup_invert_limit: "「フォルダごとに選択」時のみ有効", fmt_dup_count: "({n}個の重複)", btn_start_scan: "スキャン開始", tag_hash: "完全一致", tag_hash_short: "完全", tag_name: "名前類似", tag_name_short: "名前", tag_sim: "長さ類似", tag_sim_short: "長さ", label_dup_video: "動画ファイル (完全一致 + 長さ類似 + 名前類似)", label_dup_image: "画像ファイル (完全一致 + 名前類似)", label_dup_other: "その他のファイル (完全一致 + 名前類似)", btn_analyze: "フォルダ分析", tip_analyze: "フォルダのフィルタまたは重複チェック", btn_export: "ディレクトリ出力", tip_export: "現在のパスのファイルツリーを生成してダウンロード", title_export_format: "エクスポート形式", lbl_export_current: "現在のパスのすべての項目にディレクトリ出力を実行します", opt_tree_view: "ツリービュー", opt_list_view: "リストビュー", msg_exporting: "ディレクトリツリーを作成中...", str_analyze_results: "分析結果", lbl_size_threshold: "検出しきい値", title_analyze_result: "フォルダ分析結果", opt_ana_large: "フォルダ透視", lbl_analyze_selected: "選択した {n} 項目にフォルダ透視を実行します", lbl_analyze_current: "現在のパスのすべての項目にフォルダ透視を実行します", opt_ana_sim: "フォルダ重複チェック", lbl_ana_sim_selected: "選択した {n} 項目にフォルダ重複チェックを実行します", lbl_ana_sim_current: "現在のパスのすべての項目にフォルダ重複チェックを実行します", title_algo_help: "アルゴリズム解説", algo_help_content: "名前一致:名称と容量が近いフォルダ群を検索します。\n類似度一致:内部ファイルが高度に重複するフォルダ群を検索します。\n包含率一致:小フォルダの内容が大フォルダに完全に含まれているサブセット冗余を検索します。\n\n精度:類似度一致 > 包含率一致 > 名前一致\n範囲:包含率一致 > 類似度一致", lbl_threshold: "しきい値", lbl_sim_score: "類似度", lbl_containment: "包含率", lbl_name_match: "名前一致", lbl_sim_match: "類似度一致", lbl_contain_match: "包含率一致", lbl_ana_min: "下限", lbl_ana_max: "上限", /* --- 重命名、清理与资源管理器 --- */ btn_prune: "空フォルダ削除", tip_prune: "空フォルダ削除 [Ctrl] + [Delete]", btn_rename: "名前の変更", tip_rename: "名前の変更 [F2]", btn_bulkrename: "一括リネーム", tip_bulkrename: "一括リネーム [F2]", title_blacklist: "リソース管理", btn_blacklist_run: "今すぐクリーンアップ", btn_clear_list: "リストをクリア", tip_bl_desc: "以下の項目は通常の削除ではスキップされ、「今すぐクリーンアップ」でのみ削除されます", tip_blacklist_input: "リソース管理 [Alt] + [Delete]", label_bl_folder: "フォルダリスト (完全一致)", label_bl_file: "ファイルリスト (完全一致)", lbl_type_folder: "フォルダ", lbl_type_file: "ファイル", ph_bl_folder: "ペーストするか、右クリックで追加", ph_bl_file: "ペーストするか、右クリックで追加", modal_bl_preview: "検索結果", btn_bl_delete: "選択項目を削除", modal_rename_title: "リネーム", modal_rename_multi_title: "一括リネーム", btn_preview: "プレビュー", modal_preview_title: "変更の確認", label_pattern: "パターン (例: Video {n})", label_replace: "置換 / 削除", label_replace_note: "大文字・小文字を区別", label_include_ext: "拡張子を含める", label_regex: "正規表現 (Regex)", placeholder_find: "検索する文字列", placeholder_replace: "置換後の文字列 (空で削除)", label_jav: "FC2 クリーン命名", lbl_rn_pattern: "命名テンプレート", lbl_rn_case_convert: "大文字・小文字変換", opt_rn_keep_origin: "(そのまま)", opt_rn_lower: "すべて小写 (abc)", lbl_rn_mode_series: "シリーズモード", lbl_rn_mode_format: "フォーマット化", lbl_rn_mode_ad: "広告タグ削除", lbl_rn_mode_ext: "拡張子修復", opt_rn_upper: "すべて大写 (ABC)", opt_rn_title: "先頭のみ大写 (Abc)", lbl_rn_width_convert: "全角・半角変換", opt_rn_width_half: "全角から半角 (A->A)", opt_rn_width_full: "半角から全角 (A->A)", lbl_rn_preview_title: "変更プレビュー", tip_jav_mode_desc: "✨ FC2をスマートに抽出し、無関係な文字を削除", tip_ad_remove_desc: "🧹 プレフィックス広告、URL、ゴミ記号を自動除去し、絵文字や括弧の書式を修正", tip_ext_fix_desc: "🧩 ファイルの真のタイプ (MIME) に基づいて拡張子を自動修正", label_replace_find: "検索内容", label_replace_to: "置換先", /* --- 解压相关 --- */ btn_unzip: "一括解凍", tip_unzip: "一括解凍 [Alt] + [U]", btn_unzip_all: "すべて解凍", btn_understand_unzip: "理解して解凍", title_input_pwd: "解凍パスワードが必要", lbl_pwd_prompt: "パスワードを入力してください:", /* --- 媒体播放器与以图搜图 --- */ btn_ext: "外部プレーヤーで再生", tip_ext: "PotPlayerで再生 / リンク取得 [Alt] + [E]", btn_img_search: "画像で検索 [F]", tip_play_search: "画像で検索 [F]", tip_pip: "PiP [P]", str_no_sub: "字幕なし", lbl_sub_sel: "字幕選択", lbl_show_sub: "字幕を表示", btn_sub_search: "オンライン", btn_sub_cloud: "クラウド", btn_sub_local: "ローカル", lbl_sub_pos: "字幕の位置", lbl_sub_bottom: "下部", lbl_sub_top: "上部", lbl_sub_bg_op: "背景の透過", lbl_sub_size: "字幕のサイズ", lbl_sub_offset: "字幕のズレ調整", title_sel_sub: "字幕の選択", ph_sub_search: "キーワードを入力すると、下のリンクが自動更新されます...", btn_force_play: "強制再生を試行", str_compat_mode: "互換モード", lang_code: "ja", btn_go_search: "🔍 {n} で手動検索", btn_restart: "最初から再生", btn_prev_video: "前へ [Ctrl + ←]", btn_next_video: "次へ [Ctrl + →]", tip_plist_open: "リスト表示 [E]", tip_plist_close: "リストを閉じる [E]", tab_sub: "字幕", tab_size: "サイズ", tab_more: "詳細", lbl_ratio: "比率", lbl_direction: "方向", opt_ratio_def: "デフォルト", btn_rot_l: "左に回転", btn_rot_r: "右に回転", btn_flip_h: "左右反転", btn_flip_v: "上下反転", lbl_play_end: "再生終了時", opt_list_loop: "リストをループ", opt_single_loop: "1曲ループ", opt_play_stop: "停止", lbl_skip_op: "OPをスキップ", lbl_skip_ed: "EDをスキップ", export_link_title: "ストリーミングリンクをエクスポート", btn_start_play: "再生開始", btn_copy_link: "リンクをコピー", tip_copy_link: "リンクをコピー [Alt] + [C]", opt_player_other: "その他 (リンク出力)", lbl_player: "プレーヤー", btn_mark: "マーク", lbl_resolution: "画質", str_switch_compat: "現在の画質は利用できません。{n} に戻しました", type_img: "画像", type_doc: "文書", type_archive: "圧縮", type_sub: "字幕", type_app: "アプリ", type_suffix: "ファイル", /* --- 设置与搜索 --- */ label_turbo_mode: "極速モード", desc_turbo_mode: "標準UIを自動代替 (推奨)", lbl_aria2_status: "接続状態", ph_aria2_secret: "シークレット (任意)", str_connected: "接続完了", str_conn_fail: "接続失敗", str_connecting: "テスト中...", tip_mixed_content: "一般的なポート:\n• 6800 (Aria2 標準)\n• 16800 (Motrix 既定)\n• 6881 (その他)", picker_title: "フォルダの選択", picker_all: "すべてのファイル", picker_new: "新規フォルダ", picker_sort_new: "新しい", picker_sort_old: "古い", title_select_file: "ファイルの選択", placeholder_search: "ファイルを検索...", placeholder_search_short: "検索", title_search_hist: "検索履歴", btn_clear_hist: "クリア", lbl_global_search: "全ドライブ検索", lbl_search_path: "検索パスを含める", lbl_search_path_short: "パス", str_search_results: "検索結果", modal_settings_title: "設定", label_lang: "言語 (Language)", label_thumb: "サムネイルをぼかす (プライバシーモード)", label_keep_pos: "閲覧位置を記憶 (戻った時に復元)", label_sort_pref: "ソート設定", opt_sort_indep: "フォルダごとに独立", opt_sort_global: "すべて共通", desc_sort_indep: "現在のフォルダのみ適用", desc_sort_global: "全フォルダ共通の並び順 (新しい順)", label_search_engine: "画像検索エンジン", opt_engine_google: "Google レンズ (総合)", opt_engine_yandex: "Yandex (総合)", opt_engine_saucenao: "SauceNAO (イラスト/Pixiv)", opt_engine_tracemoe: "trace.moe (アニメ)", label_dup_strictness: "類似一致のしきい値", opt_strict: "厳密", opt_loose: "緩め", label_comic_mode: "メディアモード", desc_comic_mode: "画像/動画のみのフォルダはデフォルトA-Z順", label_aria2_url: "Aria2 RPC URL", label_aria2_token: "Aria2 RPC Token", label_privacy_mode: "プライバシーモード", label_blur_cover: "カバー画像をぼかす", label_dl_filter_ext: "ダウンロード拡張子フィルタ (例: .txt, .jpg)", label_dl_filter_name: "ダウンロード名前フィルタ (キーワードまたはフルネーム)", lbl_dl_filter: "フォルダのダウンロードフィルタ", desc_dl_filter: "転送時に一致するファイルを自動除外", lbl_config_manage: "設定管理", btn_export_data: "データ出力", btn_import_data: "データ読込", btn_clean_data: "ローカルデータをクリア", title_clean_data: "クリアする項目を選択", msg_clean_confirm: "選択したローカルデータを完全に削除しますか?この操作は元に戻せません。", msg_clean_success: "ローカルデータをクリアしました。ページをリロードします...", opt_cfg_index: "全検索インデックス (同期済みディレクトリ構成/ファイルスナップショット)", opt_cfg_pref: "基本設定 (UI 外観/操作習慣/ソート順)", opt_cfg_rules: "管理ルール (リソース管理/共有回数制限/検索履歴/ダウンロードルール)", opt_cfg_vault: "パスワード庫 (解凍パスワード記憶)", opt_cfg_history: "動画キャッシュ (再生進捗/動画の長さキャッシュ)", opt_cfg_cache: "実行キャッシュ (フォルダ更新時刻/閲覧位置/指紋)", msg_import_confirm: "インポートされた設定は現在の設定とマージされます(リスト/記録は統合され、競合する基本設定は上書きされます)。続行しますか?", msg_import_success: "設定のインポートに成功しました。ページをリロードします...", err_invalid_config: "無効な設定ファイル:指紋識別がないか、フォーマットエラーです", err_json_format: "ファイルの解析に失敗:JSON 構文エラーまたはファイルが破損しています", lbl_storage: "ストレージ", lbl_browse_exp: "閲覧体験", lbl_skip_bl_on_del: "削除時に記録済みリソースをスキップ", lbl_pwd_manage: "解凍パスワード管理", title_pwd_vault: "パスワード庫", lbl_pwd_try_count: "パスワード試行上限", tip_pwd_manual: "1行に1つのパスワード、Enterで改行", str_root_dir_cn: "ルート", btn_ana_select: "一括選択", opt_keep_new: "最新を保持", opt_keep_old: "最古を保持", opt_keep_large: "最大を保持", opt_keep_small: "最小を保持", opt_keep_short: "名前が最短のものを保持", opt_keep_long: "名前が最長のものを保持", /* --- 状态、进度与加载短语 --- */ loading: "読み込み中...", loading_detail: "ディレクトリ構造を全速でインデックス中...", loading_fetch: "取得中... ({n})", loading_dup: "重複を分析中... ({p}%)", str_loading_placeholder: "読み込み中...", str_load_failed: " (読み込み失敗)", str_load_failed_simple: "読み込み失敗", str_waiting_token: "ログイン状態を同期中...", str_speed: "速度", status_scanning_selection: "選択範囲をスキャン中... {n}", status_ready: "{n} 個の項目", sel_count: "{n} 個の項目を選択中", str_cached: "キャッシュ済み:", str_retries: "リトライ:", str_failed: "失敗:", str_success: "成功:", str_stopping: "停止中...", str_merging: "データを統合中...", str_rendering: "リストをレンダリング中...", str_scanning: "スキャン中...", str_analyzing: "分析中...", str_deleting: "削除中...", str_saving: "保存中...", str_saving_dots: "保存中...", str_checking_bl: "リスト照合中...", str_processing: "システムが全速で処理しています...", str_cleanup_done: "クリーンアップ完了。", str_waiting_preload: "プリロード待機中...", str_copying: "クリップボードにコピー中...", str_moving: "移動の準備中...", str_sorting: "ソート中...", str_refreshing: "更新中...", str_refreshing_cache: "キャッシュを更新中...", str_syncing_stars: "スター状態を同期中...", str_updating_view: "表示を更新中...", str_generating_view: "ビューを生成中...", str_group: "グループ", str_init_rename: "リネームを初期化中...", str_renaming: "リネーム中...", str_calc_changes: "変更を計算中...", str_scanning_dir: "ディレクトリ構造をスキャン中...", str_init_op: "操作を初期化中...", str_init_scan: "全ドライブスキャンを初期化中...", str_rebuilding: "インデックスを再構築中...", str_upload_1: "アップロード中 (ノード 1/3)...", str_upload_2: "ノード1タイムアウト、ノード2へ切替...", str_upload_3: "ノード2タイムアウト、最終ノードを試行...", str_upload_fail_copy: "アップロード失敗、クリップボードへ書き込み中...", msg_transcoding: "クラウドでトランスコード中...", msg_transcoding_wait: "サーバーで処理中です。少々お待ちください", str_preparing: "解凍準備中...", str_unzipping: "解凍中: {n}", str_unzipping_state: "解凍中...", str_unzipping_prog_0: "解凍中: 0%", str_unzipping_prog_100: "解凍中: 100%", str_unzipping_prog_fmt: "解凍中: {n}%", msg_task_waiting: "待機中...", msg_task_hashing: "ファイルを検証中...", msg_task_init_upload: "アップロード初期化中...", msg_task_uploading: "アップロード中...", msg_task_init_part: "分割処理の初期化中...", msg_task_uploading_2: "アップロード中...", str_loc_tracing: "パスを追跡中...", str_loc_stale: "キャッシュが古いため、クラウドと同期中...", str_verifying: "検証中...", str_server_indexing: "サーバーでインデックス作成中... ({n}/5)", str_creating_task_n: "タスク作成中 ({n}/{t})...", msg_submit_request: "リクエスト送信中... {c}/{t}", msg_wait_server: "サーバーの応答待機中... ({c}/{t})", msg_server_processing: "サーバー処理中... ({c}/{t})", str_jav_querying: "照会中...", lbl_done_check: "✔ 完了", msg_limit_updated: "抽出回数が更新されました", /* --- 提示、确认与交互消息 --- */ title_alert: "ヒント", title_confirm: "確認", title_prompt: "入力", btn_ok: "OK", btn_yes: "はい", btn_no: "いいえ", btn_save: "設定を保存", btn_cancel: "キャンセル", btn_create: "作成", btn_skip: "スキップ", msg_down_success: "ブラウザで {n} 個のファイルのダウンロードを開始しました。", msg_batch_txt: "ダウンロードリスト (.txt) を作成しました。", msg_clear_history_done: "履歴から削除しました", msg_skip_unzipped: "解凍済みの {n} 個の項目をスキップしました。", msg_unzip_skip_del_confirm: "解凍済みの {n} 個の圧縮ファイルを検出しました。ゴミ箱に移動しますか?", msg_cancel_share_confirm: "選択した {n} 個の共有を解除しますか?\nリンクは即座に無効になります。", msg_pwd_updating: "パスワードを更新中...", msg_pwd_updated: "パスワードを更新しました", msg_exp_updated: "有効期限を更新しました", msg_cancel_share_done: "{n} 個の共有を解除しました。", msg_drag_drop_hint: "ファイルをここにドラッグ&ドロップ", str_drag_files: " 他 {n} 個のファイル", msg_creating_share: "共有を作成中...", title_share_result: "共有成功", msg_no_files: "項目がありません。", msg_no_selection: "項目を選択してください。", warn_del: "選択した {n} 項目を削除しますか?", msg_clear_sel_confirm: "{n} 個の重複ファイルが選択されています。選択を解除しますか?", str_bl_stat: "一致: {n} | 選択中: {m}", str_hits: "ヒット", msg_settings_saved: "設定を保存しました。ページをリロードします。", msg_name_exists: "名前が既に存在します: {n}", str_name_conflict: "(名前衝突の可能性)", msg_newfolder_prompt: "新規フォルダ名:", msg_rename_prompt: "新しい名前を入力してください:", msg_copy_done: "コピーしました。貼り付け先を選択してください。", msg_cut_done: "移動準備完了。貼り付け先を選択してください。", msg_paste_empty: "貼り付ける項目がありません。", msg_copy_empty: "クリップボードが空、またはデータがありません。", msg_add_success: "{n} 行のデータを追加しました。", msg_del_done: "選択した行を削除しました。", msg_del_select: "削除する行を選択してください。", msg_del_items_done: "{n} 個の項目を削除しました。", msg_copy_success: "コピー成功", str_redirecting: "Google レンズへ転送中...", msg_manual_paste: "画像アップロードがタイムアウトしました。コピーした画像を新しいウィンドウで {cmd} を押して貼り付けてください", msg_starring: "スターを追加中...", msg_unstarring: "スターを解除中...", msg_star_added: "スターを追加しました", msg_unstar_done: "スターを解除しました", msg_empty_trash_confirm: "ゴミ箱を空にしますか?この操作は取り消せません!", msg_trash_emptied: "ゴミ箱を空にしました。", msg_del_forever_confirm: "{n} 個の項目を完全に削除しますか?この操作は取り消せません!", msg_del_forever_done: "{n} 個の項目を完全に削除しました。", msg_restore_done: "{n} 個の項目を復元しました。", msg_auto_sub_load: "字幕を自動読み込みしました:{n}", msg_dl_sub: "字幕をダウンロード中...", msg_transcode_done: "✅ トランスコード完了。再生を開始します", msg_fallback_report: "⚠️ 原画が再生不可 (MPEG4/HEVC) なため、{n} に切り替えました", tip_manual_sub: "ヒント: .srt または .vtt ファイルをプレーヤーにドラッグ&ドロップして読み込めます。", msg_sub_drop_load: "ドラッグ&ドロップで字幕を読み込みました:{n}", msg_resume_hint: "{t} から再生を再開します。ここをクリック ", msg_unzip_confirm_n: "現在のディレクトリに {n} 個のファイルを解凍しますか?", msg_task_paused: "停止中", msg_task_added: "{n} 個のアップロードタスクを追加しました", msg_task_fast_success: "高速アップロード成功", msg_task_upload_done: "アップロード完了", log_dirty_data: "異常データを検出しました。リクエストを遮断します。", msg_network_unstable: "ネットワークが不安定です。自動復旧を試みています...", msg_skip_locked: "{n} 個がロックされています", msg_skip_self: "{n} 個を同じ場所に移動しようとしています", msg_skip_conflict: "{n} 個のサブパスが使用中です", msg_skip_invalid: "無効な項目をスキップしました: ", msg_creating_cloud_task: "クラウドタスクを作成中...", str_parsing_torrent: "種子ファイルを解析中...", err_torrent_no_info: "解析失敗:有効な情報が見つかりません", err_file_read: "ファイルの読み取りに失敗しました", msg_cloud_task_finish: "作成完了:成功 {s}、失敗 {f}", msg_cloud_task_success: "🎉 {n} 個のタスクを正常に作成しました", msg_prepare_restore: "復元準備中...", msg_smart_matching_n: "パスワードを自動照合中 ({n}個)...", msg_system_busy_retry: "システム混雑中。リトライしています ({n}/5)...", msg_unzip_running_bg: "[{n}] クラウドで解凍中です。後で更新して確認してください", msg_share_code_updated: "共有コードが更新されました", msg_retry_submitted: "{n} 個のタスクを再送信しました", msg_aria2_batch_fail_log: "\n\n失敗項目が多いため、詳細なエラーリスト(.txt)を自動的に書き出しました。", str_aria2_fetch_err: "(リンク取得失敗)", str_aria2_rpc_err: "(送信失敗)", str_aria2_aborted: "(取り消し済み)", str_aria2_fail_file_name: "Aria2_失敗リスト", msg_op_blocked_moving: "⚠️ 操作ブロック\n\nファイル移動タスクが進行中です。完了までお待ちください。", msg_op_blocked_analyzing: "⚠️ 操作ブロック\n\nバックグラウンドでファイル移動が進行中です。フォルダ分析の精度を保つため、現在のタスクが完了してから操作してください。", msg_op_blocked_exporting: "⚠️ 操作ブロック\n\nバックグラウンドでファイル移動が進行中です。正確なディレクトリ出力を確保するため、現在のタスクが完了してから操作してください。", msg_analyze_only_normal_dir: "フォルダを選択してください", msg_analyze_no_large_folders: "設定されたしきい値の範囲内にフォルダが見つかりませんでした ({s})", msg_analyze_summary_fmt: "<b>{n}</b> 個の {s}GB 以上のフォルダが見つかりました(サイズ順)", msg_prune_blocked_moving: "⚠️ 操作ブロック\n\nファイル移動タスクが進行中です。クリーンアップを実行できません。", msg_global_index_blocked_moving: "⚠️ 操作ブロック\n\nファイル移動中です。構造が安定してから全検索を実行してください。", msg_resource_locked_download: "⚠️ 操作ブロック\n\n移動中のファイルが含まれています。完了までお待ちください。", msg_resource_locked_aria2: "⚠️ 操作ブロック\n\n移動中のファイルが含まれています。完了までお待ちください。", msg_flatten_blocked_moving: "⚠️ 操作ブロック\n\nバックグラウンドでファイル移動が進行中です。このタイミングでファイル分析を実行すると、ファイルリストの欠損や重複が発生する可能性があるため、後ほど再試行してください。", err_task_conflict: "⚠️ 操作ブロック\n\nファイル移動中です。完了までお待ちください。", title_del_task_confirm_fmt: "{n} 件の転送タスクを削除しますか?", lbl_del_cloud_files_too: "クラウド上のファイルも同時に削除する", msg_file_del_failed: "ファイル削除失敗: ", msg_task_del_success_fmt: "{n} 個のタスクを削除しました", title_clear_task_confirm: "すべてのアップロードタスクをクリアしますか?", msg_task_clear_success_fmt: "{n} 個のアップロードタスクをクリアしました", msg_unzip_virtual_view_warn: "仮想ビューで操作しています。解凍されたファイルは<b>各圧縮ファイルの元のフォルダ</b>に保存され、現在のリストには表示されません。<br><br>続行しますか?", msg_smart_matching_file: "パスワードを照合中... ({n})", msg_unzip_batch_submitted: "✅ {n} 個の解凍が完了しました", msg_unzip_batch_skipped: " ({n} 個スキップ)", msg_unzip_check_source: "。元のディレクトリで結果を確認してください。", tip_jump_to_folder: "このフォルダにジャンプ", msg_task_deleted: "タスクを削除しました", msg_scan_done: "スキャン完了!\n{n} 個のファイル、{f} 個のフォルダを確認しました。", msg_scan_fail: "\n\n❌ {n} 件のエラーが発生しました。", msg_scan_fix: "\n\n✅ {n} 回のネットワークエラーを自動修復しました。", msg_down_scanning: "フォルダの内容を解析中...", msg_down_progress: "ブラウザでダウンロード中...", msg_down_confirm_total: "✅ スキャン完了。{n} 個のファイルが見つかりました。\n\n⚠️ 警告:大量のファイルを一度にダウンロードするとブラウザがフリーズする可能性があります。10個以上の場合は Aria2 を推奨します。\n\nブラウザで続行しますか?", msg_aria2_sending_batch: "🚀 Aria2 へタスクを送信中...", msg_aria2_check_fail: "Aria2 への接続に失敗しました!\nURLとTokenを確認してください。", msg_aria2_check_ok: "Aria2 への接続に成功しました!", msg_aria2_sent: "{n} 個のファイルを Aria2 へ送信しました。", msg_aria2_test_fail: "Aria2 への接続に失敗しました。\n設定を保存しますか?", title_aria2_fail: "接続テスト失敗", msg_batch_scanning: "🚀 ディレクトリ構造を高速スキャン中...", msg_batch_hydrating: "⚡ ダウンロードリンクを並列抽出中...", msg_batch_no_files: "ダウンロード可能なファイルが見つかりませんでした。", msg_dup_warn: "重複ファイルの検索を開始しますか?", msg_dup_result: "{n} 組の重複が見つかりました。", msg_dup_none: "重複ファイルは見つかりませんでした。", msg_bl_stop: "操作を停止しました。", msg_bl_add_done: "{n} 個の項目を記録しました。", msg_bl_remove_done: "{n} 個の項目を記録から削除しました。", msg_bl_empty: "リストが空です。", msg_bl_clear_confirm: "すべての記録を削除しますか?この操作は取り消せません。", msg_blacklist_run_none: "該当する記録は見つかりませんでした。", msg_blacklist_run_confirm: "{n} 個の該当項目が見つかりました。\n\n今すぐゴミ箱へ移動しますか?", msg_bl_run_limit: "⚠️ 制限事項\n\nクリーンアップは物理的な再帰操作を伴います。ホーム画面に戻ってから実行してください。", msg_del_protected: "{n} 個の記録済みファイルを保護しました。", msg_del_none: "削除可能なファイルはありません。", msg_bl_scanning: "全検索中... \nスキャン済み: {d} | ヒット: {f}", rn_tip_wait: "ルールを設定してください", rn_tip_jav: "上のボタンをクリックして照合を開始します", rn_tip_none: "一致する項目はありません", rn_stat: "一致: {n} | 有効な変更: {m}", rn_warn_confirm: "{n} 個のファイルをリネームしますか?", msg_bulkrename_done: "{n} 個の項目をリネームしました。", msg_rn_all_skipped: "❌ すべての名前が重複しているため、変更されませんでした。", msg_rn_fail_count: "同名のため {n} 件スキップしました", msg_prune_confirm: "空フォルダの検索を開始しますか?", msg_prune_none: "空フォルダは見つかりませんでした。", msg_prune_found: "{n} 個の空フォルダが見つかりました。\n今すぐ削除しますか?", msg_deleting_folders: "{n} 個のフォルダを削除中...", msg_global_warn: "全ドライブの同期を開始します。\n同期されたデータはブラウザを更新するまでメモリに保持されます。\n続行しますか?", msg_init_scan_sel: "選択項目のスキャンを初期化中...", warn_clear_history: "選択した {n} 項目を履歴から削除しますか?\n(クラウド上のファイルは削除されません)", msg_img_copy_hint: "新しいウィンドウで {cmd} を押して検索してください。", msg_aria2_not_set: "Aria2 が設定されていません。設定を入力してください:", str_jav_no_match: "(品番が見つかりません)", msg_unzip_fail: "解凍リクエストが失敗しました", msg_jszip_fail: "JSZip の読み込みに失敗しました。ネットワークを確認してください。", msg_turbo_activated: "ターボモード起動:スクリプトがウェブ論理を完全制御し、最高の快適さを提供。", msg_console_legal: "商用利用厳禁:このプロジェクトは個人の学習および交流目的のみに使用されます。", msg_ana_warn: "フォルダ重複チェックのヒント:判定はアルゴリズムに基づいています。誤削除を防ぐため、削除前に必ず目視で確認してください。", /* --- 错误提示 --- */ err_invalid_links: "有効なリンクを入力してください", err_pwd_format: "パスワードは4-10桁の英数字である必要があります", err_invalid_torrent: "無効な種子ファイルの形式", err_torrent_complex: "解析の複雑度が高すぎます(不正なファイルの可能性)", err_torrent_format: "種子ファイルの構造が破損しています", err_torrent_len: "フィールド長の解析エラー", err_torrent_char: "不正な文字を検出しました", err_share_code_exists: "この共有コードは既に使用されています", err_folder_not_ready: "クラウドフォルダを作成中です。後で再試行してください", err_item_deleted: "項目が見つかりません", err_network: "ネットワークエラー", err_clipboard_denied: "クリップボードへのアクセスが拒否されました", err_worker: "ワーカーエラー", err_api: "APIエラー", err_capture: "スクリーンショットに失敗しました。", err_captcha_simple: "検証に失敗しました。Web版で一度ファイルをお気に入りに追加して、認証を完了させてください。", err_sub_dl_fail: "字幕のダウンロードに失敗しました: ", err_req_blocked: "リクエストがブロックされました (ファイアウォールなど)", err_req_timeout: "リクエストがタイムアウトしました", err_sub_drop_type: "字幕形式のファイルのみ対応しています", err_codec_t1: "再生不可能なビデオコーデックです ({c})", err_codec_t2: "ブラウザがこのビデオ形式をサポートしていません。<br>下のボタンから外部プレーヤーを呼び出してください。", err_pwd_simple: "パスワードが間違っています", err_task_exists: "タスクは既に存在します", err_network_break: "画像ノードの接続が切れました。もう一度クリックしてください", err_no_failed_task: "失敗したタスクは選択されていません", err_unknown: "不明なエラー", err_invalid_regex: "無効な正規表現", err_parent_not_found: "フォルダが見つかりません", msg_sys_error: "システムフォルダの操作は許可されていません", msg_download_fail: "ダウンロードリンクを取得できませんでした。", msg_video_fail: "ビデオリンクを取得できませんでした。", err_star_sync_fail: "スターの同期に失敗しました", err_paste_descendant: "自身または自身の子フォルダにはコピー・移動できません", err_quota_exceeded: "ストレージ容量が不足しています", err_name_exists: "同名のファイルが既に存在します", err_share_pass: "抽出コードは4-10文字である必要があります", str_error: "エラー", str_error_crit: "致命的なエラー", str_error_paste: "貼り付けエラー", str_action_failed: "操作に失敗しました", str_scan_error: "スキャンエラー", err_limit_too_low: "更新失敗:新しい回数 ({n}) は現在の保存数 ({s}) より大きい必要があります", err_vault_max: "パスワード保管庫は最大50個のよく使うパスワードのみ保存できます", err_pwd_len: "単一のパスワードの長さは127文字を超えることはできません", /* --- 帮助文档 --- */ modal_help_title: "ヘルプ", help_desc: ` <div class="pk-no-scrollbar pk-help-scroll" style="font-size:13px;line-height:1.6;color:var(--pk-fg);text-align:justify;text-justify:inter-ideograph;word-break:break-all;pointer-events:auto;display:block;"> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">✨ エクスペリエンス&ナビゲーション</b><br> • <b>インタラクションの再構築</b>:公式機能をベースに、インターフェースを <b>Windows エクスプローラー</b> 風に刷新しました。<br> • <b>ターボモード</b>:有効にするとウェブ版のロジックを完全に引き継ぎ、大量のファイルによるフリーズやクラッシュを根本から解決します。<br> • <b>高機能パスバー</b>:マウスホイールのスクロールやドロップダウンでの同階層切り替えに対応。全体検索や分析スイートが統合され、パスの履歴表示や遡及ジャンプが可能です。<br> • <b>UXの強化</b>:スター等の多角的なソート、ワンクリックでの<b>サムネイルぼかし</b>、ダークテーマ切り替えをサポート。バックグラウンドでは <b>SWR 戦略</b> を採用し、シームレスにビューを更新します。<br> • <b>バックグラウンドインデックスと保護</b>:ホームアイコンの青い点滅はディレクトリツリーの同期中を示します。競合する操作をブロックし、データの破損を防ぐ同時実行の物理的ロックを備えています。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:デフォルトフォルダ(My Pack)はシステムにより保護されており、誤削除、コピー、移動、リネームが制限されています。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">📂 一括処理&ストレージ管理</b><br> • <b>一括リネーム</b>:<b>正規表現による置換/削除</b>、<b>エピソード番号</b>生成、テキスト<b>整形</b>、<b>FC2 標準命名</b>、<b>広告プレフィックスの削除</b>、MIMEに基づく<b>拡張子の修復</b>に対応しています。<br> • <b>分析スイート</b>:<b>ファイル分析</b>(フィルタおよびハッシュ/時間/名前による重複チェック)と<b>フォルダ分析</b>(フィルタおよび名前/類似度/包含率による重複チェック)を統合。現在のパスの<b>ディレクトリツリー</b>出力もサポートします。<br> • <b>スマート整理</b>:ワンクリックで空フォルダを削除。<b>一括解凍</b>はパスワード自動記憶とスマート入力を統合し、解凍済み項目のスキップや削除も可能です。<br> • <b>リソースマネージャー</b>:カスタム<b>ブラックリスト</b>として不要ファイルを一括クリーンアップ、または<b>ホワイトリスト</b>として一括削除時に自動保護します。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:データの同期競合を避けるため、処理中は他のクライアントでファイルを変更しないでください。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🌐 転送&共有センター</b><br> • <b>共有管理</b>:抽出回数の上限設定に対応。制限に達すると自動的に共有が解除されます。<br> • <b>高速アップロード</b>:ローカルファイルやフォルダのドラッグ&ドロップによる直接アップロードに対応。公式の制限を突破し、<b>小容量ファイルの転送中断率を大幅に低減</b>しました。<br> • <b>クラウドダウンロード強化</b>:一括オフラインリンクの<b>自動重複排除</b>。内蔵の<b>マグネットスマートクリーンエンジン</b>(Base32/Hex ハッシュを自動抽出してノイズを除去)。<b>.torrent</b> シードファイルの解析をサポート。制限付きリンクに対する<b>ウェブスナップショット保存</b>のフォールバック機能を提供。 <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:抽出回数による自動解除は、ページを開いており、コンピュータがスリープ状態でない場合にのみ動作します。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">🎬 没入型メディア拡張</b><br> • <b>再生エンジン</b>:0.5x〜3.0xの倍速再生、画面の回転/反転、アスペクト比の強制調整、OP/EDスキップ、<b>連続再生/ループ</b>モードをサポートし、プログレスバーのサムネイルプレビューに対応。内蔵の<b>ウォッチドッグ</b>により、ブラックスクリーンや非対応コーデック時に互換画質へ自動フォールバックします。<br> • <b>字幕システム</b>:クラウド内の同名字幕、ローカルファイル、およびオンライン字幕検索の読み込みに対応。字幕のズレをミリ秒単位で微調整でき、ローカルテキストの<b>ドラッグ&ドロップ解析</b>も可能です。<br> • <b>ビジュアルアシスト</b>:複数のエンジンによる<b>画像検索</b>(逆引き)を内蔵。「メディアモード」を有効にすると、マンガやアニメのフォルダが自動的に名前順(A-Z)でソートされます。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:再生履歴リストは、スクリプト環境内で発生した再生進捗のみを記録します。</div> </div> <div style="margin-bottom:12px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚙️ 設定&データ管理</b><br> • <b>設定のバックアップ</b>:個人設定、管理ルール、パスワード庫等をデジタル指紋付きの JSON ファイルとしてエクスポートでき、インポート時の<b>スマート結合と重複排除</b>をサポートします。<br> • <b>データの削除</b>:インデックス、設定、ルール、パスワード、キャッシュデータを必要に応じて個別に削除し、ストレージの解放とプライバシー保護を行えます。<br> <div style="color:var(--pk-fg); opacity:0.6; font-size:12px; margin-top:6px;">* 注:インデックスはページを閉じると消去されますが、設定やパスワード庫などのデータは永続的に保存されます。</div> </div> <div style="margin-bottom:24px;"> <b style="font-size:14px; color:var(--pk-pri); display:inline-block; margin-bottom:4px;">⚡ ダウンロード&配布</b><br> • <b>外部ダイレクト接続</b>:ワンクリックでビデオストリームの直リンクを取得、または PotPlayer を起動。RPC プロトコルを通じてファイルを <b>Aria2</b> ノードへ即座に転送できます。<br> • <b>配布の強化</b>:フォルダを Aria2 にプッシュする際、<b>クラウドのツリーディレクトリ構造を自動復元</b>します。持続的な接続監視をサポートし、エラー時にエラーリストを自動出力します。<b>フォルダダウンロードフィルタ</b>の設定をサポート。 </div> <div style="margin-top:16px; color:#d93025; font-weight:bold; text-align:center; font-size:11px; border-top:1px dashed rgba(217,48,37,0.2); padding-top:12px; letter-spacing:0.5px; opacity:0.9;"> このプロジェクトは CC-BY-NC-SA-4.0 ライセンスに厳格に従っており、あらゆる形式の商用利用を禁止します。 </div> </div>` }, }; function getStrings() { return T[getLang()] || T.zh; }; let cachedCredKey = null; let cachedCaptchaKey = null; function resetHeaderCache() { cachedCredKey = null; cachedCaptchaKey = null; } function purgeAllCachesOnLogout() { resetHeaderCache(); if (typeof globalCache !== 'undefined') globalCache.clear(); if (typeof globalLineageMap !== 'undefined') globalLineageMap.clear(); if (typeof globalParentIndex !== 'undefined') globalParentIndex.clear(); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.clear(); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.clear(); if (typeof backgroundQueue !== 'undefined') backgroundQueue.length = 0; if (typeof isBackgroundRunning !== 'undefined') isBackgroundRunning = false; if (typeof DurationProber !== 'undefined') DurationProber.reset(); const ui = document.querySelector('.pk-ov'); if (ui) { ui.remove(); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; } const btn = document.getElementById('pk-launch'); if (btn) btn.remove(); if (typeof pkState !== 'undefined' && pkState) pkState = null; if (typeof globalSavedState !== 'undefined') globalSavedState = null; if (typeof globalPreloadPromise !== 'undefined') globalPreloadPromise = null; } (() => { window.addEventListener('storage', (e) => { if (e.key && (e.key.startsWith('credentials') || e.key.startsWith('captcha') || e.key === 'pk_captured_captcha')) { console.log(`[Auth Sync] Token storage change detected (${e.key}), flushing auth cache.`); if (!e.newValue) purgeAllCachesOnLogout(); else resetHeaderCache(); } }); const _origSetItem = Storage.prototype.setItem; Storage.prototype.setItem = function(key, value) { _origSetItem.apply(this, arguments); if (key && (key.startsWith('credentials') || key.startsWith('captcha') || key === 'pk_captured_captcha')) { resetHeaderCache(); } }; const _origRemoveItem = Storage.prototype.removeItem; Storage.prototype.removeItem = function(key) { _origRemoveItem.apply(this, arguments); if (key && (key.startsWith('credentials') || key.startsWith('captcha'))) { console.log(`[Auth Sync] Local credential removed (${key}), triggering deep purge.`); purgeAllCachesOnLogout(); } }; const checkAuthRoute = () => { if (location.href.includes('/login') || location.pathname.includes('login')) { console.log(`[Auth Sync] SPA route changed to /login, triggering deep purge.`); purgeAllCachesOnLogout(); } }; const _origPushState = history.pushState; history.pushState = function() { _origPushState.apply(this, arguments); checkAuthRoute(); }; const _origReplaceState = history.replaceState; history.replaceState = function() { _origReplaceState.apply(this, arguments); checkAuthRoute(); }; window.addEventListener('popstate', checkAuthRoute); })(); function getHeaders() { let token = '', captcha = ''; if (cachedCredKey) { try { const v = JSON.parse(localStorage.getItem(cachedCredKey)); if (v && v.access_token) token = v.token_type + ' ' + v.access_token; } catch {} } if (!token) { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('credentials')) { try { const v = JSON.parse(localStorage.getItem(k)); if (v && v.access_token) { token = v.token_type + ' ' + v.access_token; cachedCredKey = k; break; } } catch { } } } } if (cachedCaptchaKey) { try { const v = JSON.parse(localStorage.getItem(cachedCaptchaKey)); if (v) captcha = v.captcha_token; } catch {} } if (!captcha) { captcha = localStorage.getItem('pk_captured_captcha') || ''; if (!captcha) { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('captcha')) { try { const v = JSON.parse(localStorage.getItem(k)); if(v) { captcha = v.captcha_token; cachedCaptchaKey = k; } } catch { } } } } } return { 'Content-Type': 'application/json', 'Authorization': token, 'x-device-id': localStorage.getItem('deviceid') || '', 'x-captcha-token': captcha }; } async function waitForAuth(timeout = 10000) { const start = Date.now(); while (Date.now() - start < timeout) { const h = getHeaders(); if (h.Authorization && h.Authorization.length > 10) { return true; } await sleep(200); } return false; } async function apiList(parentId, limit = 1000, onProgress, signal, trashed = false, isBackground = false) { let all = [], next = null, safe = 5000; const TIMEOUT_MS = 25000; const filters = trashed ? `&filters=${encodeURIComponent('{"trashed":{"eq":true}}')}&parent_id=*` : `&parent_id=${parentId || ''}`; do { let pageRetries = 0; const MAX_PAGE_RETRIES = 5; let pageSuccess = false; while (pageRetries < MAX_PAGE_RETRIES && !pageSuccess) { if (signal?.aborted) throw new DOMException('Aborted by user', 'AbortError'); const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&with_audit=true&_t=${Date.now()}${next ? `&page_token=${next}` : ''}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true }); try { const res = await fetch(url, { headers: getHeaders(), signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) { if (res.status === 404) { console.warn(`[API] 404 Not Found (Skipped): ${parentId || 'Root'}`); return []; } if (res.status === 401 || res.status === 400) { console.warn(`[API] ${res.status} Error. Flushing auth cache...`); localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); if (res.status === 400) { try { if (typeof showToast !== 'undefined') showToast(getStrings().err_captcha_simple, 'error'); } catch(e){} throw new Error('CAPTCHA_INTERCEPT'); } throw new Error('AUTH_RETRY'); } if (res.status === 429) { await sleep(3000 * (pageRetries + 1)); throw new Error('RATE_LIMIT'); } throw new Error(`HTTP_${res.status}`); } syncTime(res.headers); const data = await res.json(); if (!data.files && data.next_page_token) { throw new Error('PAGINATION_INCOMPLETE'); } if (data.files) { const validFiles = data.files.filter(f => trashed ? f.trashed : !f.trashed).map(f => minifyFile(f, isBackground)); all.push(...validFiles); if (onProgress) onProgress(all.length); if (isBackground && parentId !== undefined && typeof globalCache !== 'undefined') { globalCache.set(parentId, { items: [...all], nextToken: data.next_page_token }); } } next = data.next_page_token; pageSuccess = true; if (next) await sleep(50); } catch (e) { clearTimeout(timeoutId); pageRetries++; safe--; let isTimeout = false; let errMsg = e.message; if (e.name === 'AbortError' && !(signal && signal.aborted)) { isTimeout = true; e = new Error('FETCH_TIMEOUT'); errMsg = 'Local Timeout'; } const isNetworkError = e.name === 'TypeError' || errMsg.includes('fetch') || errMsg.includes('PAGINATION') || isTimeout; if ((isNetworkError || errMsg === 'AUTH_RETRY') && safe > 0) { const backoff = pageRetries === 1 ? 500 : Math.min(pageRetries * 2000, 10000); console.warn(`[API] Retry ${pageRetries}/${MAX_PAGE_RETRIES} for ${parentId || 'Root'} due to ${errMsg}. Wait ${backoff}ms`); await sleep(backoff); continue; } throw e; } } } while (next && safe > 0); return all; } async function apiGet(id) { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${id}?thumbnail_size=SIZE_MEDIUM&_t=${Date.now()}`, { headers: getHeaders() }); if (!res.ok) throw new Error(`API Error ${res.status}`); return res.json(); } async function apiAction(action, data) { const method = action.includes('batch') ? 'POST' : 'PATCH'; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files${action}`, { method: method, headers: getHeaders(), body: JSON.stringify(data) }); syncTime(res.headers); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error_description || `API Error ${res.status}`); } return res.json(); }; async function apiStar(ids, star = true) { const action = star ? 'star' : 'unstar'; const url = `https://api-drive.mypikpak.com/drive/v1/files:${action}`; const res = await fetch(url, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ "ids": ids }) }); syncTime(res.headers); if (!res.ok) throw new Error(`API Error ${res.status}`); return true; } async function apiShareList(limit = 100) { let all =[], next = null, safe = 50; do { const url = `https://api-drive.mypikpak.com/drive/v1/share/list?limit=${limit}&thumbnail_size=SIZE_SMALL&_t=${Date.now()}${next ? `&page_token=${next}` : ''}`; const res = await fetch(url, { headers: getHeaders() }); if (!res.ok) throw new Error(`Share API Error ${res.status}`); const json = await res.json(); const list = json.data || []; const normalized = list.map(item => ({ id: item.share_id, kind: 'pikpak#share', name: item.title, size: item.file_size, modified_time: item.create_time, icon_link: item.icon_link, view_count: item.view_count, save_count: item.restore_count, limit_count: (function(){ const store = JSON.parse(gmGet('pk_share_limits', '{}')); return store[item.share_id] || parseInt(item.limit_count || 0); })(), share_status: item.share_status, share_status_text: item.share_status_text, expiration_days: item.expiration_days, expiration_left: item.expiration_left, expiration_at: item.expiration_at, phrase: item.phrase || "", share_url: item.share_url, pass_code: item.pass_code, parent_id: 'share_root' })); all.push(...normalized); if (!next) { const graveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); const serverIds = new Set(all.map(x => x.id)); const phantoms = graveyard.filter(x => !serverIds.has(x.id)); all.push(...phantoms); } next = json.next_page_token; safe--; } while (next && safe > 0); const graveyard = JSON.parse(gmGet("pk_expired_shares", "[]")); const graveyardIds = new Set(graveyard.map(x => x.id)); const filteredServerList = all.filter(it => !graveyardIds.has(it.id)); return [...filteredServerList, ...graveyard]; } async function apiTaskList(limit = 500, onBatch = null, session = null, signal = null) { let totalFetched = 0; const state = session || { phase: 'active', nextToken: null, completed: false }; const fetchList = async (phases, maxCount, startToken = null) => { let next = startToken; let page = 0; const maxPages = 100; do { if (signal && signal.aborted) break; const filters = encodeURIComponent(JSON.stringify({ "phase": { "in": phases } })); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?type=offline&limit=${limit}&filters=${filters}&thumbnail_size=SIZE_SMALL&with_reference_resource=true&_t=${Date.now()}${next ? `&page_token=${next}` : ''}`; let res = null; for (let i = 0; i < 3; i++) { try { res = await fetch(url, { headers: getHeaders() }); if (res.ok) break; if (res.status === 429) await sleep(2000); } catch (e) { await sleep(1000); } } if (!res || !res.ok) break; const data = await res.json(); const tasks = data.tasks || []; if (tasks.length > 0) { state.nextToken = data.next_page_token; if (onBatch) onBatch(tasks, data.next_page_token, phases.includes('COMPLETE') ? 'done' : 'active'); totalFetched += tasks.length; } next = data.next_page_token; page++; if (totalFetched >= maxCount || page >= maxPages) break; if (next) await sleep(50); } while (next); return next; }; if (state.phase === 'active') { const activePhases = "PHASE_TYPE_UNKNOW,PHASE_TYPE_PENDING,PHASE_TYPE_RUNNING,PHASE_TYPE_PAUSED,PHASE_TYPE_ERROR"; const lastToken = await fetchList(activePhases, 2000, state.nextToken); if (!lastToken) { state.phase = 'done'; state.nextToken = null; } else { return totalFetched; } } if (state.phase === 'done') { const donePhases = "PHASE_TYPE_COMPLETE"; const lastToken = await fetchList(donePhases, 15000, state.nextToken); if (!lastToken) { state.completed = true; state.nextToken = null; } } return totalFetched; } async function apiCancelTask(ids, deleteFiles = false) { let filesToDelete = []; if (deleteFiles && typeof pkState !== 'undefined' && pkState.itemMap) { ids.forEach(taskId => { const task = pkState.itemMap.get(taskId); if (task && task.id === taskId && task.file_id) { filesToDelete.push(task.file_id); } }); } const BATCH_SIZE = 50; for (let i = 0; i < ids.length; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); const idStr = chunk.join(','); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?task_ids=${idStr}&delete_files=${deleteFiles}&_t=${Date.now()}`; try { const res = await fetch(url, { method: 'DELETE', headers: getHeaders() }); if (!res.ok && res.status !== 404) throw new Error(`Task Del Err ${res.status}`); } catch(e) { console.warn("Task delete warning:", e); } } if (filesToDelete.length > 0) { console.log(`[Task] Also deleting ${filesToDelete.length} associated files...`); const trashUrl = `https://api-drive.mypikpak.com/drive/v1/files:batchTrash`; for (let i = 0; i < filesToDelete.length; i += 100) { const fileChunk = filesToDelete.slice(i, i + 100); await fetch(trashUrl, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: fileChunk }) }); } } return true; } async function apiAddOfflineTask(url, parentId = '', extraParams = {}) { const apiUrl = `https://api-drive.mypikpak.com/drive/v1/files`; const payload = { kind: "drive#file", upload_type: "UPLOAD_TYPE_URL", url: { "url": url }, params: { from: 'manual', with_thumbnail: 'true', ...extraParams } }; if (parentId) { payload.parent_id = parentId; } else { payload.folder_type = 'DOWNLOAD'; } const res = await fetch(apiUrl, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 400 && err.error_code === 9) throw new Error(getStrings().err_task_exists); throw new Error(err.error_description || `Add Task Error ${res.status}`); } return await res.json(); } async function apiGetSharePhrases(shareId) { const url = `https://api-drive.mypikpak.com/phrase/v1/content/info/share?id=${shareId}`; const res = await fetch(url, { headers: getHeaders() }); if (!res.ok) return []; const json = await res.json(); return (json.data || []).map(x => x.phrase); } async function apiUpdateSharePhrase(shareId, newPhrase, oldPhrase = "") { const url = `https://api-drive.mypikpak.com/phrase/v1/content/share`; const isDelete = newPhrase === ""; const isCreate = !oldPhrase && !isDelete; const payload = { content: shareId, type: "share" }; if (isCreate) { payload.phrase = newPhrase; } else { payload.old_phrase = oldPhrase; payload.new_phrase = newPhrase; } const res = await fetch(url, { method: isCreate ? 'POST' : 'PATCH', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 400) throw new Error(getStrings().err_share_code_exists); throw new Error(err.error_description || `API Error ${res.status}`); } return res.json(); } async function apiUpdateShare(shareId, data) { const url = `https://api-drive.mypikpak.com/drive/v1/share`; const payload = { share_id: shareId, ...data }; const res = await fetch(url, { method: 'PATCH', headers: getHeaders(), body: JSON.stringify(payload) }); syncTime(res.headers); if (!res.ok) { const err = await res.json().catch(()=>({})); throw new Error(err.error_description || `API Error ${res.status}`); } return await res.json(); } async function apiCancelShare(ids) { const url = `https://api-drive.mypikpak.com/drive/v1/share:batchDelete`; const res = await fetch(url, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ids }) }); if (!res.ok) { console.warn("[Share] Batch delete failed, trying sequential delete..."); for (const id of ids) { await fetch(`https://api-drive.mypikpak.com/drive/v1/share/${id}`, { method: 'DELETE', headers: getHeaders() }); } return true; } return true; } // Original Logic by digbug82. Modification does not grant ownership. async function coreRecursiveEngine(roots, options) { const { signal, onFile, onFolder, onProgress } = options; const L = getStrings(); let queue = [...roots]; let activeTasks = new Set(); let inFlight = new Set(); let pendingRetries = 0; const stats = { folders: 0, files: 0, retries: 0, cacheHits: 0, currentConcurrency: 10 }; const USER_LIMIT = parseInt(localStorage.getItem('pk_user_limit') || "200"); const ABSOLUTE_MAX = 128; const MIN_CONCURRENCY = 5; const processFolder = async (current) => { inFlight.add(current.id); try { let files = []; if (typeof globalCache !== 'undefined' && globalCache.has(current.id)) { const cachedData = globalCache.get(current.id); if (Array.isArray(cachedData)) { files = cachedData; stats.cacheHits++; } } let isFromNetwork = false; let start = 0; if (files.length === 0) { start = performance.now(); files = await apiList(current.id, 1000, null, signal, false, true); isFromNetwork = true; if (typeof globalCache !== 'undefined') globalCache.set(current.id, files); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.add(current.id); } if (signal && signal.aborted) return; const nextSubFolders = []; for (const f of files) { if (f.kind === 'drive#folder') { const myLineage = [...(current.lineage || []), { id: f.id, name: f.name }]; nextSubFolders.push({ ...f, lineage: myLineage, depth: (current.depth || 0) + 1, retryCount: 0 }); } else { stats.files++; if (onFile) onFile(f, current); } } if (onFolder) onFolder(current, files, nextSubFolders); queue.push(...nextSubFolders); stats.folders++; if (isFromNetwork) { const rtt = performance.now() - start; const DYNAMIC_MAX = Math.min(USER_LIMIT, ABSOLUTE_MAX); if (rtt < 800) { if (stats.currentConcurrency < DYNAMIC_MAX) stats.currentConcurrency += 1; } else if (rtt > 3000) { stats.currentConcurrency = Math.max(MIN_CONCURRENCY, Math.floor(stats.currentConcurrency * 0.8)); } } } catch (err) { if (err.name === 'AbortError' || (signal && signal.aborted)) return; stats.currentConcurrency = Math.max(MIN_CONCURRENCY, Math.floor(stats.currentConcurrency * 0.5)); stats.retries++; current.retryCount = (current.retryCount || 0) + 1; pendingRetries++; try { const backoff = current.retryCount === 1 ? 1000 : Math.min(current.retryCount * 5000, 60000); const reason = err.message || "Unknown Error"; console.warn(`[ZeroLoss] Folder: ${current.name} | Reason: ${reason} | Attempt: ${current.retryCount} | Re-queueing...`); stats.isRetrying = true; if (onProgress) onProgress(stats); await sleep(backoff); if (signal && !signal.aborted) { queue.unshift(current); } } finally { stats.isRetrying = false; pendingRetries--; } } finally { inFlight.delete(current.id); if (onProgress) onProgress(stats); } }; while ((queue.length > 0 || inFlight.size > 0 || pendingRetries > 0) && (!signal || !signal.aborted)) { while (queue.length > 0 && activeTasks.size < stats.currentConcurrency && (!signal || !signal.aborted)) { const folder = queue.pop(); if (inFlight.has(folder.id) && folder.retryCount === 0) continue; const p = processFolder(folder); const pWrapper = p.finally(() => activeTasks.delete(pWrapper)); activeTasks.add(pWrapper); } if (activeTasks.size > 0) { await Promise.race(activeTasks).catch(() => {}); } else if (pendingRetries > 0 || inFlight.size > 0) { await sleep(100); } } } const version = (typeof GM_info !== 'undefined' && GM_info.script) ? GM_info.script.version : "1.0.0"; console.log("%c PikPak Enhancement Master %c v" + version + " %c digbug82 %c CC-BY-NC-SA-4.0 ", "color:#fff; background:#1a5eff; padding:3px 0; border-radius:4px 0 0 4px; font-weight:bold;", "color:#fff; background:#333; padding:3px 8px;", "color:#fff; background:#f57c00; padding:3px 8px; font-weight:bold;", "color:#fff; background:#d93025; padding:3px 8px; border-radius:0 4px 4px 0; font-weight:bold;"); console.log("%c" + getStrings().msg_console_legal + "%c", "color:#d93025; font-weight:bold;", ""); function getIcon(item) { const isFolder = item.kind === 'drive#folder' || (item.kind === 'drive#task' && ( (item.mime_type && item.mime_type.includes('folder')) || (item.icon_link && item.icon_link.includes('folder')) )); if (isFolder) { const isRootMyPack = (item.name === CONF.SYSTEM_FOLDER_NAME) && (!item.parent_id || item.parent_id === '' || item.parent_id === 'root' || item._isSystemRoot); if (isRootMyPack) return CONF.typeIcons.systemFolder; return CONF.typeIcons.folder; } const name = (item.name || '').toLowerCase(); const ext = name.split('.').pop(); const mime = (item.mime_type || '').toLowerCase(); const duration = (item.params && item.params.duration) || 0; if (ext === 'apk') return CONF.typeIcons.apk; if (ext === 'txt') return CONF.typeIcons.text; if (ext === 'html' || ext === 'htm') return CONF.typeIcons.web; if (['srt', 'vtt', 'ass', 'ssa'].includes(ext)) return CONF.typeIcons.subtitle; if (['exe', 'msi', 'bat', 'cmd', 'elf', 'dmg', 'pkg', 'app'].includes(ext)) return CONF.typeIcons.executable; const isArchiveExt = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'iso'].includes(ext); if (mime.startsWith('video/') || duration > 0) return CONF.typeIcons.video; if (mime.startsWith('image/')) return CONF.typeIcons.image; if (mime.startsWith('audio/')) return CONF.typeIcons.audio; if (mime === 'application/pdf') return CONF.typeIcons.pdf; if (mime.startsWith('text/') || mime.includes('word') || mime.includes('excel') || mime.includes('powerpoint') || mime.includes('officedocument') || mime === 'application/rtf') return CONF.typeIcons.text; if (isArchiveExt || mime.includes('zip') || mime.includes('rar') || mime.includes('7z') || mime.includes('tar') || mime.includes('archive') || mime.includes('compressed')) { const isUnzipped = item.params && (item.params.global_file_kind === '1' || item.params.global_file_root); return isUnzipped ? CONF.typeIcons.archiveUnzipped : CONF.typeIcons.archive; } return CONF.typeIcons.file; } async function openManager(initialCache, preloadPromise) { const L = getStrings(); const lang = getLang(); if (document.querySelector('.pk-ov')) return; document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; const S = { path: (globalSavedState && globalSavedState.path) ? [...globalSavedState.path] : [{ id: '', name: L.btn_nav_home }], items: [], itemMap: new Map(), display: [], sel: new Set(), cache: initialCache || new Map(), sort: (globalSavedState && globalSavedState.sort) ? globalSavedState.sort : 'modified_time', dir: (globalSavedState && globalSavedState.dir !== undefined) ? globalSavedState.dir : 1, scanning: false, dupMode: false, dupRunning: false, folderFirst: false, dupReasons: new Map(), dupGroups: new Map(), dupRawGroups: [], offlineFilters: { running: true, failed: true, complete: true }, uploadFilters: { running: true, paused: true, complete: true }, activeId: null, clipItems: [], clipType: '', clipSourceParentId: null, loading: false, lastSelIdx: -1, dupConfig: { video: true, image: true, other: true }, search: '', sortId: 0, isFlattened: false, filterState: { active: false, cat: 'all', ext: 'all' }, suppressClearConfirm: false, preSearchPath: null, lastGlobalResults: [], folderLineageMap: globalLineageMap, trashMode: (globalSavedState && globalSavedState.trashMode) || false, shareMode: (globalSavedState && globalSavedState.shareMode) || false, starredMode: (globalSavedState && globalSavedState.starredMode) || false, recentMode: (globalSavedState && globalSavedState.recentMode) || false, historyMode: (globalSavedState && globalSavedState.historyMode) || false, offlineMode: (globalSavedState && globalSavedState.offlineMode) || false, uploadMode: (globalSavedState && globalSavedState.uploadMode) || false, scanId: 0, preloaded: (globalSavedState || (initialCache && initialCache.has('root'))) ? true : false, preLoadPromise: preloadPromise || null, blSet: new Set(), blFolderSet: new Set(), starredSet: new Set(), pendingMap: new Map(), durationMap: new Map(), loadedThumbs: new Set(), movingIds: new Set(), movingSourceId: null, movingDestId: null, uploadTasks: (globalSavedState && globalSavedState.uploadTasks) ? globalSavedState.uploadTasks : [], broadcast: new BroadcastChannel('pk_act_sync'), clearSelection: () => { S.sel.clear(); S.lastSelIdx = -1; S.activeId = null; if (typeof updateStat === 'function') updateStat(); }, updateBlCache: () => { const parse = (key) => new Set(gmGet(key, '').toLowerCase().split('\n').map(s => s.trim()).filter(s => s)); S.blSet = parse('pk_blacklist'); S.blFolderSet = parse('pk_blacklist_folders'); }, getRealCacheKey: (id) => { if (id === 'offline_root') return 'offline_root'; if (id === 'recent_root') return 'recent_root'; if (id === 'root' || id === '' || !id) { if (S.shareMode) return 'share_root'; if (S.starredMode) return 'starred_root'; if (S.recentMode) return 'recent_root'; if (S.trashMode) return 'root_trashed'; if (S.offlineMode) return 'offline_root'; return 'root'; } return id; } }; pkState = S; if (S.uploadTasks.length > 0) { S.uploadTasks.forEach(task => { if (task.status === 'WAITING') task.message = L.msg_task_waiting; else if (task.status === 'DONE') task.message = L.msg_task_upload_done; else if (task.status === 'PAUSED') task.message = L.msg_task_paused; else if (task.status === 'HASHING') task.message = L.msg_task_hashing; else if (task.status === 'UPLOADING') task.message = L.msg_task_uploading; }); } const FloatBarManager = (() => { const activeBars = []; const BASE_BOTTOM = 30; const GAP = 10; const reposition = () => { let currentBottom = BASE_BOTTOM; activeBars.forEach(item => { item.el.style.bottom = `${currentBottom}px`; currentBottom += (item.el.offsetHeight || 40) + GAP; }); }; const create = (initialText) => { const id = 'pk_fb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const el = document.createElement('div'); el.className = 'pk-float-bar-item'; el.style.cssText = ` position: absolute; left: 50%; transform: translateX(-50%); background: var(--pk-toast-bg); color: var(--pk-toast-fg); padding: 10px 24px; border-radius: 30px; font-size: 13px; font-weight: 600; box-shadow: 0 10px 30px var(--pk-tip-sd); z-index: 20000; border: 1px solid var(--pk-toast-bd); display: flex; align-items: center; gap: 12px; transition: bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; opacity: 0; pointer-events: none; white-space: nowrap; backdrop-filter: blur(8px); `; el.innerHTML = `<div class="pk-spin-lg" style="width:16px; height:16px; border-width:2px;"></div><span class="pk-float-txt">${esc(initialText)}</span>`; if (UI.win) UI.win.appendChild(el); else { const container = document.querySelector('.pk-ov') || document.body; container.appendChild(el); } const entry = { id, el }; activeBars.push(entry); requestAnimationFrame(() => { reposition(); el.style.opacity = '1'; }); return { update: (text) => { const span = el.querySelector('.pk-float-txt'); if (span) span.textContent = text; }, destroy: () => { const idx = activeBars.findIndex(x => x.id === id); if (idx !== -1) { activeBars.splice(idx, 1); el.style.opacity = '0'; el.style.transform = 'translateX(-50%) scale(0.9)'; reposition(); setTimeout(() => el.remove(), 300); } } }; }; return { create }; })(); S.broadcast.onmessage = (e) => { const { type, ids, src, dst } = e.data; if (!ids) return; if (type === 'LOCK_ADD') { ids.forEach(id => S.movingIds.add(id)); if (src) S.movingSourceId = src; if (dst) S.movingDestId = dst; } else if (type === 'LOCK_REM') { ids.forEach(id => S.movingIds.delete(id)); } else if (type === 'LOCK_CLR') { S.movingIds.clear(); S.movingSourceId = null; S.movingDestId = null; } if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); if (typeof updateStat === 'function') updateStat(); }; const updateGlobalLockCSS = () => { let style = document.getElementById('pk-lock-css'); if (!style) { style = document.createElement('style'); style.id = 'pk-lock-css'; document.head.appendChild(style); } if (!S.movingIds || S.movingIds.size === 0 || !S.movingSourceId) { style.textContent = ''; return; } const selector = Array.from(S.movingIds) .map(id => `.pk-win .pk-row[data-id="${id}"]`) .join(','); style.textContent = `${selector} { opacity: 0.4 !important; filter: grayscale(100%) !important; pointer-events: none !important; cursor: wait !important; }`; }; const isPathBusy = (targetFolderId) => { if (!S.movingIds || S.movingIds.size === 0) return false; const sourceId = S.movingSourceId; const destId = S.movingDestId; const tid = targetFolderId === 'root' ? '' : (targetFolderId || ''); if (tid === '') return true; if (tid === sourceId || tid === destId) return true; const checkAncestry = (startId, forbiddenId) => { let curr = startId; let safety = 50; while (curr && curr !== 'root' && safety > 0) { if (curr === forbiddenId) return true; const parent = globalParentIndex.get(curr); curr = parent ? parent.id : null; safety--; } return false; }; if (checkAncestry(sourceId, tid) || checkAncestry(destId, tid)) return true; if (checkAncestry(tid, sourceId) || checkAncestry(tid, destId)) return true; return false; }; const resumeBackgroundDiscovery = () => { if (S.scanning || S.loading) { return; } const isVirtual = S.isFlattened || S.dupMode || S.path.some(p => p.id === 'virtual_search_root'); if (isVirtual) { return; } let addedCount = 0; if (typeof globalCache !== 'undefined') { for (const [parentFolderId, files] of globalCache) { if (!files || !Array.isArray(files)) continue; for (let i = 0; i < files.length; i++) { const f = files[i]; if (f.kind === 'drive#folder' && !scannedFolderIds.has(f.id) && !globalCache.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name, retryCount: 0 }); scannedFolderIds.add(f.id); addedCount++; } } if (addedCount > 500) break; } } if (addedCount === 0) { const visibleSubFolders = S.items.filter(f => f.kind === 'drive#folder'); for (let i = visibleSubFolders.length - 1; i >= 0; i--) { const sub = visibleSubFolders[i]; if (!scannedFolderIds.has(sub.id) && !globalCache.has(sub.id)) { backgroundQueue.push({ id: sub.id, name: sub.name, retryCount: 0 }); scannedFolderIds.add(sub.id); addedCount++; } } } if (addedCount > 0 || backgroundQueue.length > 0) { console.log(`♻️ Background crawler resumed: ${addedCount} new tasks found in map.`); runBackgroundCrawler(); } }; S.updateBlCache(); if (S.items && S.items.length > 0) { S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); } const el = document.createElement('div'); el.className = 'pk-ov'; let siteFont = window.getComputedStyle(document.body).fontFamily || ''; siteFont = siteFont.replace(/,?\s*sans-serif\s*$/i, ''); el.style.fontFamily = siteFont ? `${siteFont}, "Noto Sans", sans-serif` : '"Noto Sans", sans-serif'; el.addEventListener('wheel', (e) => { e.stopPropagation(); const scrollTarget = e.target.closest('.pk-vp, .pk-modal, #pk-rn-vp, .pk-prev-list, textarea, .pk-scroll, .pk-help-scroll'); if (!scrollTarget) { e.preventDefault(); return; } const st = scrollTarget.scrollTop; const sh = scrollTarget.scrollHeight; const ch = scrollTarget.clientHeight; const isUp = e.deltaY < 0; const isDown = e.deltaY > 0; const isAtTop = st <= 0.5; const isAtBottom = (st + ch) >= (sh - 1.5); if ((isUp && isAtTop) || (isDown && isAtBottom)) { if (e.cancelable) e.preventDefault(); } }, { passive: false }); const mkLbl = (id, txt, shortTxt, tip) => { return `<label class="pk-dup-chk" data-pk-tip="${tip || txt}"><input type="checkbox" id="${id}" checked> <span class="pk-txt-long">${txt}</span><span class="pk-txt-short">${shortTxt}</span></label>`; }; 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: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>`, info: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`, open: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11V21H3V11"/><path d="M3 11L6 5"/><path d="M21 11L18 5"/><line x1="12" y1="17" x2="12" y2="3"/><polyline points="8 7 12 3 16 7"/></svg>`, star: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`, unstar: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/><line x1="2" y1="2" x2="22" y2="22"/></svg>`, download: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>`, copy: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`, move: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>`, rename: `<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor" version="1.1"><path d="M56.925091 777.495273v189.579636h189.579636L805.655273 407.924364l-189.579637-189.579637L56.925091 777.495273zM952.32 261.306182a50.315636 50.315636 0 0 0 0-71.354182L834.048 71.68a50.315636 50.315636 0 0 0-71.400727 0L670.254545 164.165818 859.787636 353.745455l92.532364-92.439273v-0.093091z" fill="currentColor"></path></svg>`, renameBulk: `<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor" version="1.1" style="transform: scale(1.2);"><path d="M882.88 280.64l42.88-45.76a13.76 13.76 0 0 0 0-19.2L829.12 128a13.12 13.12 0 0 0-18.88 0L768 173.12zM739.84 202.56l-218.88 234.88a17.28 17.28 0 0 0-3.52 8.64L512 547.84a13.44 13.44 0 0 0 15.04 14.08l102.08-12.48a13.76 13.76 0 0 0 7.68-5.44l218.88-233.6z" fill="currentColor"></path><path d="M864 381.12a24 24 0 0 0-24 24v317.76H304.96V189.12h317.76a24 24 0 0 0 0-48H296.96A40 40 0 0 0 256 181.12v82.56H174.72a40 40 0 0 0-40 40v549.44a40 40 0 0 0 40 40h549.44a40 40 0 0 0 40-40v-75.84a20.8 20.8 0 0 0 0-6.4h83.84a40.32 40.32 0 0 0 40-40V405.12a24.32 24.32 0 0 0-24-24z m-147.84 396.16v67.84H182.72V311.68H256v419.2a40 40 0 0 0 40 40h421.44a20.8 20.8 0 0 0-1.28 6.4z" fill="currentColor" stroke="currentColor" stroke-width="35"></path></svg>`, blAdd: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`, blRem: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="m9 12 2 2 4-4"/></svg>`, share: CONF.icons.share, copyLink: `<svg width="16" height="16" viewBox="0 0 1024 1024"><path d="M232.6 1023.8c-61.2 0-118.6-23.7-161.6-66.6l-4.2-4.2c-89.1-89.1-89.1-234.1 0-323.2l181.1-181.1c3.5 25.4 9.6 50.4 18.1 74.6l-152.8 152.8c-63.5 63.5-63.5 166.9 0 230.5l4.3 4.3c30.7 30.7 71.6 47.6 115.2 47.6s84.5-16.9 115.3-47.7l228.9-228.9c63.5-63.5 63.5-166.9 0-230.5l-4.3-4.2c-2.1-2.1-4.2-4.1-6.4-6l46.4-46.4c2.2 2 4.3 4 6.4 6.1l4.3 4.2c89.1 89.1 89.1 234.1 0 323.2l-228.9 228.9c-43 42.9-100.4 66.6-161.6 66.6zM411.5 629.3c-2.2-2-4.3-4.1-6.4-6.1l-4.2-4.2c-43.1-43.1-66.8-100.5-66.8-161.6 0-61.1 23.7-118.5 66.8-161.6l228.9-228.9c43.1-43.1 100.5-66.8 161.6-66.8 61.3 0 118.7 23.7 161.6 66.6l4.2 4.2c89 89.1 89 234.1 0 323.2l-181.1 181.1c-3.5-25.4-9.6-50.5-18.1-74.6l152.8-152.8c63.5-63.5 63.5-166.9 0-230.5l-4.3-4.3c-30.7-30.7-71.6-47.6-115.2-47.6s-84.5 16.9-115.2 47.7l-228.9 228.9c-30.7 30.7-47.7 71.7-47.7 115.3 0 43.6 16.9 84.5 47.7 115.2l4.2 4.2c2 2 4.2 4.1 6.4 6.1l-46.4 46.4z" fill="currentColor"></path></svg>`, copyName: `<svg width="16" height="16" viewBox="0 0 1024 1024" fill="currentColor" version="1.1" xmlns="http://www.w3.org/2000/svg" style="transform: scale(1.22); vertical-align: -3px;"><path d="M852.411679 250.1876a85.155752 85.155752 0 0 0-84.942863-78.811649h-340.623009v85.155753h340.623009v340.623009h85.155752v-340.623009l-0.212889-6.386682zM171.37855 767.466217v-340.623009a85.155752 85.155752 0 0 1 85.155752-85.155752h287.400664l138.378098 138.378097v287.400664a85.155752 85.155752 0 0 1-85.155752 85.155753h-340.62301a85.155752 85.155752 0 0 1-85.155752-85.155753z m425.778762-252.146182l-88.476827-88.476827H256.534302v340.623009h340.62301v-252.146182z"></path></svg>`, trash: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`, restore: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>`, delForever: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>` }; el.innerHTML = ` <style>${CSS}</style> <div class="${winClass}"> <div class="pk-loading-ov" id="pk-loader"> <div class="pk-spin-lg"></div> <div class="pk-loading-txt" id="pk-load-txt">${L.loading_detail}</div> <button class="pk-stop-btn" id="pk-stop-load" data-pk-tip="${L.tip_stop}">${CONF.icons.stop} <span>${L.btn_stop}</span></button> </div> <div class="pk-sidebar"> <div class="pk-nav-btn" id="pk-btn-cloud" data-pk-tip="${L.btn_cloud_download}">${CONF.icons.cloudDownload}<span>${L.btn_cloud_download}</span></div> <div class="pk-nav-btn act" id="pk-nav-home" data-pk-tip="${L.btn_nav_home}">${CONF.icons.home}<span>${L.btn_nav_home}</span></div> <div class="pk-nav-btn" id="pk-nav-upload" data-pk-tip="${L.btn_nav_upload}">${CONF.icons.navUpload}<span>${L.btn_nav_upload}</span></div> <div class="pk-nav-btn" id="pk-nav-offline" data-pk-tip="${L.btn_nav_offline}">${CONF.icons.offline}<span>${L.btn_nav_offline}</span></div> <div class="pk-nav-btn" id="pk-nav-recent" data-pk-tip="${L.btn_nav_recent}">${CONF.icons.recent}<span>${L.btn_nav_recent}</span></div> <div class="pk-nav-btn" id="pk-nav-history" data-pk-tip="${L.btn_nav_history}">${CONF.icons.history}<span>${L.btn_nav_history}</span></div> <div class="pk-nav-btn" id="pk-nav-share" data-pk-tip="${L.btn_nav_share}">${CONF.icons.navShare}<span>${L.btn_nav_share}</span></div> <div class="pk-nav-btn" id="pk-nav-starred" data-pk-tip="${L.btn_nav_starred}"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg> <span>${L.btn_nav_starred}</span> </div> <div class="pk-nav-btn" id="pk-nav-trash" data-pk-tip="${L.btn_nav_trash}">${CONF.icons.trash}<span>${L.btn_nav_trash}</span></div> <div id="pk-quota-panel" style="margin-top:auto; width:100%; display:flex; flex-direction:column; align-items:center; padding:0; gap:4px; cursor:default; opacity:0.6; transition:all 0.2s; margin-bottom:2px;"> <span id="pk-quota-txt" style="font-size:10px; color:var(--pk-fg); font-weight:700; transform:scale(0.8); font-variant-numeric:tabular-nums; line-height:1;">--%</span> <div style="width:44px; height:4px; background:var(--pk-bd); border-radius:2px; overflow:hidden; position:relative;" id="pk-quota-bar-box"> <div id="pk-quota-bar" style="position:absolute; left:0; top:0; height:100%; width:0%; background:var(--pk-pri); transition:width 0.5s ease-out;"></div> </div> </div> <div class="pk-nav-btn" id="pk-settings" data-pk-tip="${L.tip_settings}" style="margin-top:0 !important;">${CONF.icons.settings}<span>${L.btn_settings}</span></div> </div> <div class="pk-main-col"> <div class="pk-hd"> <div class="pk-tt"> ${CONF.logoSVG} <div style="display: inline-flex; align-items: baseline; gap: 10px;"> <span style="font-weight: 700;">${L.title}</span> <span style="font-size: 13px; font-weight: 600; color: var(--pk-fg); cursor: default;"> by <a href="https://github.com/digbug82/PikPak_Enhancement_Master" target="_blank" style="color: inherit; text-decoration: none; transition: color 0.2s; font-weight: inherit; cursor: pointer;" onmouseover="this.style.color='var(--pk-pri)'" onmouseout="this.style.color='inherit'">digbug82</a> </span> </div> </div> <div style="display:flex; gap:4px;"> <div class="pk-btn" id="pk-theme" style="width:32px;padding:0;justify-content:center;" data-pk-tip="${L.tip_theme}">${themeIcon}</div> <div class="pk-btn" id="pk-help" style="width:32px;padding:0;justify-content:center;" data-pk-tip="${L.tip_help}">${CONF.icons.help}</div> <div class="pk-btn" id="pk-maximize" style="width:32px;padding:0;justify-content:center;" data-pk-tip="${L.tip_maximize}">${CONF.icons.maximize}</div> <div class="pk-btn" id="pk-close" style="width:32px;padding:0;justify-content:center;" data-pk-tip="${L.tip_close}">${CONF.icons.close}</div> </div> </div> <div class="pk-tb" id="pk-top-bar"> <div class="pk-nav" id="pk-crumb"></div> <div id="pk-filter-bar" style="display:none; align-items:center; gap:8px; height:100%; margin-left:10px;"> <button class="pk-btn" id="pk-filter-btn" style="border:1px solid var(--pk-bd); background:var(--pk-bg); padding:0 12px; height:32px; border-radius:6px; box-shadow: 0 1px 2px rgba(0,0,0,0.05);"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg> <span style="font-weight:500; color:var(--pk-fg);">${L.btn_filter}</span> </button> <div id="pk-filter-active-ui" style="display:none; align-items:center; gap:8px; height:100%;"> <div id="pk-filter-cat-label" style="background:var(--pk-bg); border: 1px solid var(--pk-bd); padding:0 12px; height:32px; display:flex; align-items:center; border-radius:4px; font-size:13px; font-weight:500; color:var(--pk-fg); cursor:pointer; transition:all 0.2s;" onmouseover="this.style.borderColor='var(--pk-pri)';this.style.color='var(--pk-pri)';" onmouseout="this.style.borderColor='var(--pk-bd)';this.style.color='var(--pk-fg)';"></div> <div id="pk-filter-exts-wrap" style="display:flex; align-items:center; background:var(--pk-bg); border:1px solid var(--pk-bd); border-radius:4px; padding:0 4px; height:32px; position:relative;"> <div id="pk-filter-exts-main" style="display:flex; align-items:center; gap:4px; padding:0 4px;"></div> <div id="pk-filter-exts-more-btn" style="cursor:pointer; padding:0 8px; display:flex; align-items:center; color:#888;"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="6 9 12 15 18 9"></polyline></svg> </div> </div> <button class="pk-btn pri" id="pk-filter-exit-btn" style="background:var(--pk-pri); color:#fff; border:none; height:32px; padding:0 15px; font-size:13px; font-weight:500; border-radius:4px;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14L4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/></svg> <span>${L.btn_exit_filter}</span> </button> </div> </div> <div class="pk-dup-toolbar" id="pk-offline-tools" style="display:none; margin-left:10px; border-right:1px solid var(--pk-bd); padding-right:10px; margin-right:10px;"> <label class="pk-dup-chk" style="margin-right:10px;cursor:pointer;display:flex;align-items:center;"> <input type="checkbox" id="pk-off-run" checked style="margin:0 4px 0 0;accent-color:var(--pk-pri);"> <span style="font-size:13px;color:var(--pk-fg);">${L.lbl_task_run}</span> </label> <label class="pk-dup-chk" style="margin-right:10px;cursor:pointer;display:flex;align-items:center;"> <input type="checkbox" id="pk-off-fail" checked style="margin:0 4px 0 0;accent-color:var(--pk-pri);"> <span style="font-size:13px;color:#d93025;">${L.lbl_task_fail}</span> </label> <label class="pk-dup-chk" style="margin-right:0;cursor:pointer;display:flex;align-items:center;"> <input type="checkbox" id="pk-off-ok" checked style="margin:0 4px 0 0;accent-color:var(--pk-pri);"> <span style="font-size:13px;color:#52c41a;">${L.lbl_task_ok}</span> </label> </div> <div class="pk-dup-toolbar" id="pk-upload-tools" style="display:none; margin-left:10px; border-right:1px solid var(--pk-bd); padding-right:10px; margin-right:10px;"> <label class="pk-dup-chk" style="margin-right:10px;cursor:pointer;display:flex;align-items:center;"> <input type="checkbox" id="pk-chk-up-run" checked style="margin:0 4px 0 0;accent-color:var(--pk-pri);"> <span style="font-size:13px;color:var(--pk-fg);">${L.lbl_up_run}</span> </label> <label class="pk-dup-chk" style="margin-right:10px;cursor:pointer;display:flex;align-items:center;" data-pk-tip="${L.tip_up_pause_desc}"> <input type="checkbox" id="pk-chk-up-pause" checked style="margin:0 4px 0 0;accent-color:var(--pk-pri);"> <span style="font-size:13px;color:#faad14;">${L.lbl_up_pause}</span> </label> <label class="pk-dup-chk" style="margin-right:0;cursor:pointer;display:flex;align-items:center;"> <input type="checkbox" id="pk-chk-up-done" checked style="margin:0 4px 0 0;accent-color:var(--pk-pri);"> <span style="font-size:13px;color:#52c41a;">${L.lbl_up_done}</span> </label> </div> <div class="pk-dup-toolbar" id="pk-dup-filters" style="margin-left:10px; border-right:1px solid var(--pk-bd); padding-right:10px; margin-right:10px;"> ${mkLbl('pk-chk-hash', L.tag_hash, L.tag_hash_short, L.tag_hash)} ${mkLbl('pk-chk-sim', L.tag_sim, L.tag_sim_short, L.tag_sim)} ${mkLbl('pk-chk-name', L.tag_name, L.tag_name_short, L.tag_name)} </div> <div class="pk-dup-toolbar" id="pk-dup-tools" style="display:none; align-items:center; gap:4px; padding:0 10px; height:100%; border-right:1px solid var(--pk-bd); margin-right:10px;"> <select id="pk-dup-folder-sel" style="height:28px; border:1px solid var(--pk-bd); border-radius:4px; font-size:12px; background:var(--pk-bg); color:var(--pk-fg); margin-right:5px;"> <option value="">${L.lbl_dup_select_folder}</option> </select> <label style="display:inline-flex; align-items:center; margin-right:8px; cursor:pointer; user-select:none; opacity:0.5;" data-pk-tip="${L.tip_dup_invert_limit}"> <input type="checkbox" id="pk-dup-invert" disabled style="margin:0 4px 0 0;"> <span class="pk-txt-long" style="font-size:12px; color:var(--pk-fg);">${L.lbl_dup_invert}</span> <span class="pk-txt-short" style="font-size:12px; color:var(--pk-fg);">${L.lbl_dup_invert_short}</span> </label> <button class="pk-ana-select-btn" id="pk-dup-smart-btn" style="display:flex; margin-right:0;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg> <span>${L.btn_ana_select}</span> </button> </div> <div style="flex:1"></div> <label class="pk-global-chk" id="pk-lbl-global"> <input type="checkbox" id="pk-chk-global" ${S.wasGlobalChecked ? 'checked' : ''}> <span>${L.lbl_global_search}</span> </label> <label class="pk-global-chk" id="pk-search-path-con" style="display:none; margin-right:8px;" data-pk-tip="${L.lbl_search_path}"> <input type="checkbox" id="pk-chk-search-path"> <span class="pk-txt-long">${L.lbl_search_path}</span> <span class="pk-txt-short">${L.lbl_search_path_short}</span> </label> <div class="pk-search"> <input type="text" id="pk-search-input" placeholder="${L.placeholder_search}" autocomplete="off"> <div class="pk-search-clear" id="pk-search-clear">${CONF.icons.close}</div> <svg id="pk-search-btn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg> <div class="pk-hist-pop" id="pk-search-hist"></div> </div> <button class="pk-btn" id="pk-export" data-pk-tip="${L.tip_export}"> ${CONF.icons.export} <span>${L.btn_export}</span> </button> <button class="pk-btn" id="pk-scan-dup" data-pk-tip="${L.tip_scan_dup}"> ${CONF.icons.scanDup} <span>${L.title_file_analysis}</span> </button> <button class="pk-btn" id="pk-analyze" data-pk-tip="${L.tip_analyze}"> ${CONF.icons.analyze} <span>${L.btn_analyze}</span> </button> <button class="pk-btn" id="pk-btn-exit" style="display:none; color:#d93025; border-color:transparent; margin-left:10px; flex-shrink:0;"> ${CONF.icons.close} <span>${L.btn_exit}</span> </button> </div> <div class="pk-tb" id="pk-trash-bar" style="display:none;"> <button class="pk-btn" id="pk-trash-refresh" data-pk-tip="${L.tip_refresh}">${CONF.icons.refresh} <span>${L.btn_refresh_short}</span></button> <div class="pk-sep"></div> <button class="pk-btn" id="pk-restore" data-pk-tip="${L.tip_restore}" disabled> ${CONF.icons.restore} <span>${L.btn_restore}</span> </button> <button class="pk-btn" id="pk-del-forever" data-pk-tip="${L.tip_del_forever}" disabled> ${CONF.icons.delForever} <span>${L.btn_del_forever}</span> </button> <button class="pk-btn pk-btn-danger" id="pk-empty-trash" data-pk-tip="${L.tip_empty_trash}" style="margin-left: 4px;"> ${CONF.icons.emptyTrash} <span>${L.btn_empty_trash}</span> </button> <div style="flex:1;"></div> <button class="pk-btn" id="pk-trash-blacklist-manager" data-pk-tip="${L.tip_blacklist_input}" style="color:#d93025"> ${CONF.icons.blacklist} <span>${L.title_blacklist}</span> </button> </div> <div class="pk-tb" id="pk-actionbar"> <button class="pk-btn" id="pk-refresh" data-pk-tip="${L.tip_refresh}">${CONF.icons.refresh} <span>${L.btn_refresh_short}</span></button> <button class="pk-btn" id="pk-off-copy-link" style="display:none;" data-pk-tip="${L.tip_copy_link}">${ctxIcons.copyLink} <span>${L.btn_copy_link}</span></button> <button class="pk-btn" id="pk-retry-task" style="display:none;" data-pk-tip="${L.tip_retry_task}">${CONF.icons.retry} <span>${L.btn_retry_task}</span></button> <button class="pk-btn" id="pk-up-pause" style="display:none;" data-pk-tip="${L.tip_up_pause}">${CONF.icons.taskPause} <span>${L.btn_up_pause}</span></button> <button class="pk-btn" id="pk-up-start" style="display:none;" data-pk-tip="${L.tip_up_start}">${CONF.icons.taskStart} <span>${L.btn_up_start}</span></button> <button class="pk-btn" id="pk-up-del" style="display:none;" data-pk-tip="${L.tip_up_del}">${CONF.icons.del} <span>${L.btn_up_del}</span></button> <button class="pk-btn" id="pk-up-clear-all" style="display:none;" data-pk-tip="${L.tip_up_clear_all}">${CONF.icons.cleanAll} <span>${L.btn_up_clear_all}</span></button> <button class="pk-btn" id="pk-newfolder" data-pk-tip="${L.tip_newfolder}">${CONF.icons.newfolder} <span>${L.btn_newfolder}</span></button> <button class="pk-btn" id="pk-del" data-pk-tip="${L.tip_del}">${CONF.icons.del} <span>${L.btn_del}</span></button> <button class="pk-btn" id="pk-deselect" data-pk-tip="${L.tip_deselect}" style="display:none">${CONF.icons.deselect} <span>${L.btn_deselect}</span></button> <div class="pk-sep"></div> <button class="pk-btn" id="pk-copy" data-pk-tip="${L.tip_copy}">${CONF.icons.copy} <span>${L.btn_copy}</span></button> <button class="pk-btn" id="pk-cut" data-pk-tip="${L.tip_cut}">${CONF.icons.cut} <span>${L.btn_cut}</span></button> <button class="pk-btn" id="pk-paste" data-pk-tip="${L.tip_paste}" disabled>${CONF.icons.paste} <span>${L.btn_paste}</span></button> <div class="pk-sep"></div> <button class="pk-btn" id="pk-rename" data-pk-tip="${L.tip_rename}">${CONF.icons.rename} <span>${L.btn_rename}</span></button> <button class="pk-btn" id="pk-bulkrename" data-pk-tip="${L.tip_bulkrename}">${CONF.icons.bulkrename} <span>${L.btn_bulkrename}</span></button> <button class="pk-btn" id="pk-prune" data-pk-tip="${L.tip_prune}">${CONF.icons.prune} <span>${L.btn_prune}</span></button> <button class="pk-btn" id="pk-unzip" data-pk-tip="${L.tip_unzip}">${CONF.icons.unzip} <span>${L.btn_unzip}</span></button> <button class="pk-btn" id="pk-cancel-share" data-pk-tip="${L.btn_cancel_share} [Delete]" style="display:none;">${CONF.icons.unshare} <span>${L.btn_cancel_share}</span></button> <div style="flex:1"></div> <div class="pk-dropdown-wrap" id="pk-upload-wrap"> <button class="pk-btn pri" id="pk-btn-upload" style="background:var(--pk-pri); color:#fff; border:none; margin-right:8px;"> ${CONF.icons.uploadBtn} <span>${L.btn_upload}</span> <svg class="pk-btn-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="margin-left:4px;"><polyline points="6 9 12 15 18 9"/></svg> </button> <div class="pk-dropdown-menu"> <div class="pk-dropdown-item" id="pk-act-upload-file"> ${CONF.icons.upFile} ${L.btn_up_file} </div> <div class="pk-dropdown-item" id="pk-act-upload-folder"> ${CONF.icons.upFolder} ${L.btn_up_folder} </div> </div> <input type="file" id="pk-file-selector" multiple style="display:none;"> <input type="file" id="pk-folder-selector" webkitdirectory directory multiple style="display:none;"> </div> <button class="pk-btn" id="pk-blacklist-manager" data-pk-tip="${L.tip_blacklist_input}" style="color:#d93025"> ${CONF.icons.blacklist} <span>${L.title_blacklist}</span> </button> </div> <div class="pk-grid-hd" style="grid-template-columns: 36px 30px 1fr 80px 105px 130px;"> <div><input type="checkbox" id="pk-all"></div> <div class="pk-col" data-k="starred" style="justify-content:center; display:flex; align-items:center;"> <svg viewBox="0 0 1024 1024" width="16" height="16" style="margin-top:-2px;"> <path d="M953.107692 425.353846c3.938462-19.692308-11.815385-39.384615-31.507692-43.323077l-259.938462-39.384615-118.153846-244.184616c-3.938462-7.876923-7.876923-11.815385-15.753846-15.753846-7.876923-3.938462-15.753846-3.938462-19.692308-3.938461h-3.938461c-3.938462 0-7.876923 3.938462-11.815385 3.938461 0 0-3.938462 0-3.938461 3.938462-3.938462 3.938462-3.938462 7.876923-7.876923 11.815384v3.938462l-118.153846 244.184615-259.938462 39.384616c-3.938462 0-7.876923 3.938462-11.815385 3.938461 0 0-3.938462 0-3.938461 3.938462 0 0-3.938462 0-3.938462 3.938461v3.938462c0 3.938462-3.938462 3.938462-3.938461 7.876923 0 0 0 3.938462-3.938462 3.938462v15.753846c0 3.938462 0 3.938462 3.938462 7.876923 0 3.938462 3.938462 7.876923 7.876923 11.815384L275.692308 638.030769 232.369231 905.846154V917.661538c0 3.938462 0 3.938462 3.938461 7.876924v3.938461c0 3.938462 3.938462 3.938462 7.876923 7.876923l3.938462 3.938462c3.938462 0 3.938462 3.938462 7.876923 3.938461H275.692308c3.938462 0 7.876923 0 11.815384-3.938461l78.769231-43.323077 149.661539-78.769231 3.938461-3.938462 232.369231 126.03077c7.876923 3.938462 15.753846 3.938462 23.630769 3.938461 19.692308-3.938462 31.507692-23.630769 27.569231-43.323077l-43.323077-267.815384 189.046154-189.046154c0-7.876923 0-11.815385 3.938461-19.692308z" fill="#FFC107"></path> </svg> <span style="font-size:10px; margin-left:0;"></span> </div> <div class="pk-col" data-k="name" style="display:flex; align-items:center;"> <div id="pk-name-text-wrap" style="display:flex; align-items:center; transition:color 0.2s;"> ${L.col_name}<span style="display:inline-block; min-width:18px; text-align:center;"></span> </div> <div id="pk-btn-folder-first" data-pk-tip="${L.lbl_folder_first}" style="margin-left:8px; cursor:pointer; display:flex; align-items:center; color:#666; transition:color 0.2s;"> ${CONF.icons.folderFirst} <span style="margin-left:2px;">${L.lbl_folder_first}</span> </div> <div id="pk-btn-invert" data-pk-tip="${L.btn_invert}" style="margin-left:12px; cursor:pointer; display:none; align-items:center; color:var(--pk-fg); transition:color 0.2s;"> ${CONF.icons.invert} <span style="margin-left:2px; color:var(--pk-fg);">${L.btn_invert}</span> </div> </div> <div class="pk-col" data-k="path" style="display:none; cursor:default; color:#888;">${L.col_path} <span></span></div> <div class="pk-col" data-k="size">${L.col_size} <span></span></div> <div class="pk-col" data-k="duration">${L.col_dur} <span></span></div> <div class="pk-col" data-k="modified_time">${L.col_date} <span></span></div> </div> <div class="pk-vp" id="pk-vp"> <div class="pk-in" id="pk-in"></div> </div> <div class="pk-ft"> <div class="pk-stat" id="pk-stat">${L.status_ready.replace('{n}', 0)}</div> <div class="pk-grp"> <button class="pk-btn" id="pk-ext" data-pk-tip="${L.tip_ext}">${CONF.icons.ext} <span>${L.btn_ext}</span></button> <button class="pk-btn" id="pk-aria2" data-pk-tip="${L.tip_aria2}">${CONF.icons.aria2} <span>${L.btn_aria2}</span></button> <button class="pk-btn" id="pk-down" data-pk-tip="${L.tip_down}">${CONF.icons.download} <span>${L.btn_down}</span></button> </div> </div> </div> </div> <div class="pk-pop" id="pk-pop"></div> <div class="pk-ctx" id="pk-ctx"> <div class="pk-ctx-item" id="ctx-share">${CONF.icons.share} ${L.ctx_share}</div> <div class="pk-ctx-sep" id="sep-1"></div> <div class="pk-ctx-item" id="ctx-open">${ctxIcons.open} ${L.ctx_open}</div> <div class="pk-ctx-item" id="ctx-ext-play" style="display:none;">${CONF.icons.ext} ${L.btn_ext}</div> <div class="pk-ctx-item" id="ctx-star">${ctxIcons.star} ${L.ctx_star}</div> <div class="pk-ctx-item" id="ctx-property">${ctxIcons.info} ${L.ctx_property}</div> <div class="pk-ctx-item" id="ctx-locate" style="display:none;">${ctxIcons.locate} ${L.ctx_locate}</div> <div class="pk-ctx-sep" id="sep-2"></div> <div class="pk-ctx-item" id="ctx-down">${ctxIcons.download} ${L.ctx_down}</div> <div class="pk-ctx-item" id="ctx-copy-name">${ctxIcons.copyName} ${L.ctx_copy_name}</div> <div class="pk-ctx-item" id="ctx-copy-link" style="display:none;">${ctxIcons.copyLink} ${L.ctx_copy_link}</div> <div class="pk-ctx-sep" id="sep-3"></div> <div class="pk-ctx-item" id="ctx-cut">${ctxIcons.move} ${L.btn_cut}</div> <div class="pk-ctx-item" id="ctx-copy">${ctxIcons.copy} ${L.ctx_copy}</div> <div class="pk-ctx-item" id="ctx-rename">${ctxIcons.rename} ${L.ctx_rename}</div> <div class="pk-ctx-item" id="ctx-prune">${CONF.icons.prune} ${L.btn_prune}</div> <div class="pk-ctx-sep" id="sep-4"></div> <div class="pk-ctx-item" id="ctx-sh-cancel" style="display:none; color:#d93025;">${CONF.icons.unshare} ${L.btn_cancel_share}</div> <div class="pk-ctx-sep" id="sep-trash-1" style="display:none;"></div> <div class="pk-ctx-item" id="ctx-restore" style="display:none">${ctxIcons.restore} ${L.btn_restore}</div> <div class="pk-ctx-sep" id="sep-trash-2" style="display:none;"></div> <div class="pk-ctx-item" id="ctx-del-forever" style="display:none; color:#d93025;">${ctxIcons.delForever} ${L.btn_del_forever}</div> <div class="pk-ctx-item" id="ctx-del" style="color:#d93025">${ctxIcons.trash} ${L.ctx_del}</div> <div class="pk-ctx-item" id="ctx-add-bl" style="color:#d93025">${ctxIcons.blAdd} ${L.ctx_add_bl}</div> <div class="pk-ctx-sep" id="sep-share-extra" style="display:none;"></div> <div class="pk-ctx-item" id="ctx-sh-detail" style="display:none;">${ctxIcons.info} ${L.ctx_share_detail}</div> <div class="pk-ctx-item" id="ctx-sh-copy" style="display:none;">${ctxIcons.copy} ${L.ctx_share_copy}</div> </div> `; document.body.appendChild(el); const destroyTooltip = (() => { const tipEl = document.createElement('div'); tipEl.className = 'pk-tooltip'; const style = document.createElement('style'); style.textContent = ` .pk-tooltip { zoom: var(--pk-zoom, 1); width: max-content; max-width: 280px; min-width: auto; background: var(--pk-tip-bg); color: var(--pk-tip-fg); border: 1px solid var(--pk-tip-bd); padding: 6px; box-sizing: border-box; border-radius: 8px; font-size: 12px; line-height: 1.4; position: fixed; z-index: 2147483647 !important; pointer-events: none; box-shadow: 0 4px 16px var(--pk-tip-sd); backdrop-filter: blur(4px); display: flex; flex-direction: column; gap: 4px; white-space: normal; word-break: break-all; text-align: justify; opacity: 0; transform: translateY(5px) scale(0.95); transition: opacity 0.15s ease, transform 0.15s ease; } .pk-tooltip.pk-dark { --pk-tip-bg: rgba(20, 20, 20, 0.95); --pk-tip-fg: #ffffff; --pk-tip-bd: rgba(255, 255, 255, 0.1); --pk-tip-sd: rgba(0, 0, 0, 0.4); } .pk-tooltip.show { opacity: 1; transform: translateY(0) scale(1); } body.pk-dragging .pk-tooltip { display: none !important; opacity: 0 !important; } .pk-tooltip img { display: none; width: 100%; max-width: 100%; max-height: 320px; height: auto; object-fit: cover; border-radius: 4px; margin-bottom: 0 !important; display: block; } `; document.head.appendChild(style); document.body.appendChild(tipEl); let activeTarget = null; let isMenuOpen = false; let lastMouseX = 0, lastMouseY = 0; const hideTip = () => { tipEl.style.display = 'none'; tipEl.classList.remove('show'); tipEl.style.left = '-9999px'; activeTarget = null; }; const renderTip = (target) => { const isDark = document.querySelector('.pk-ov')?.classList.contains('pk-dark'); tipEl.classList.toggle('pk-dark', !!isDark); const text = target.getAttribute('data-pk-tip'); const thumb = target.getAttribute('data-pk-thumb'); if (!text && !thumb) { hideTip(); return; } activeTarget = target; let html = ''; if (thumb && thumb !== 'undefined' && thumb !== 'null' && (thumb.startsWith('http') || thumb.startsWith('blob:'))) { const isBlur = gmGet('pk_blur_thumb', false); html += `<img src="${thumb}" style="${isBlur ? 'filter:blur(8px);' : ''}" onload="this.style.display='block'; this.style.marginBottom='6px';" onError="this.remove()">`; } if (text) { html += `<div>${text}</div>`; } tipEl.innerHTML = html; tipEl.style.display = 'block'; requestAnimationFrame(() => tipEl.classList.add('show')); }; document.addEventListener('mouseover', (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; if (isMenuOpen) return; if (document.body.classList.contains('pk-dragging')) { hideTip(); return; } if (document.querySelector('#pk-ctx')?.style.display === 'block') return; const target = e.target.closest('[data-pk-tip], [data-pk-thumb]'); if (!target) return; if (target === activeTarget) return; const isName = target.classList.contains('pk-name') || target.closest('.pk-name'); const isPath = target.classList.contains('pk-path') || target.closest('.pk-path'); const isThumb = target.hasAttribute('data-pk-thumb'); const isUI = target.closest('.pk-tb, .pk-sidebar, .pk-hd, .pk-ft, .pk-player-box, .pk-modal, .pk-img-box'); if (!isName && !isPath && !isThumb && !isUI) { if (target.scrollWidth <= target.clientWidth + 1) return; } renderTip(target); updatePos({ clientX: lastMouseX, clientY: lastMouseY }); }); const updatePos = (e) => { if (!tipEl || isMenuOpen || tipEl.style.display === 'none') return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let left = (e.clientX / scale) + 15; let top = (e.clientY / scale) + 15; const rect = tipEl.getBoundingClientRect(); const w = rect.width / scale; const h = rect.height / scale; const winW = window.innerWidth / scale; const winH = window.innerHeight / scale; if (left + w > winW - 10) left = (e.clientX / scale) - w - 15; if (top + h > winH - 10) top = (e.clientY / scale) - h - 15; tipEl.style.left = `${left}px`; tipEl.style.top = `${top}px`; }; const onGlobalMouseMove = (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; if (document.body.classList.contains('pk-dragging')) { hideTip(); return; } updatePos(e); if (activeTarget && tipEl.style.display !== 'none') { const actualTarget = e.target.closest('[data-pk-tip],[data-pk-thumb]'); if (actualTarget !== activeTarget) { hideTip(); } } }; document.addEventListener('mousemove', onGlobalMouseMove); const onGlobalMouseOut = (e) => { const target = e.target.closest('[data-pk-tip],[data-pk-thumb]'); if (target && target === activeTarget) { if (target.contains(e.relatedTarget)) return; hideTip(); } }; document.addEventListener('mouseout', onGlobalMouseOut); const onGlobalCtxMenu = () => { isMenuOpen = true; hideTip(); }; const onGlobalClick = () => { isMenuOpen = false; if(activeTarget) hideTip(); }; const onGlobalWheel = () => { isMenuOpen = false; hideTip(); }; window.addEventListener('contextmenu', onGlobalCtxMenu, true); window.addEventListener('click', onGlobalClick, true); window.addEventListener('wheel', onGlobalWheel, { passive: true, capture: true }); window.pkRefreshTooltip = () => { if (isMenuOpen || document.body.classList.contains('pk-dragging')) { hideTip(); return; } const elUnder = document.elementFromPoint(lastMouseX, lastMouseY); if (!elUnder) { hideTip(); return; } const target = elUnder.closest('[data-pk-tip], [data-pk-thumb]'); if (!target) { hideTip(); return; } const isName = target.classList.contains('pk-name') || target.closest('.pk-name'); const isPath = target.classList.contains('pk-path') || target.closest('.pk-path'); const isThumb = target.hasAttribute('data-pk-thumb'); const isUI = target.closest('.pk-tb, .pk-sidebar, .pk-hd, .pk-ft, .pk-player-box, .pk-modal, .pk-img-box'); if (!isName && !isPath && !isThumb && !isUI) { if (target.scrollWidth <= target.clientWidth + 1) { hideTip(); return; } } renderTip(target); updatePos({ clientX: lastMouseX, clientY: lastMouseY }); }; return () => { document.removeEventListener('mousemove', onGlobalMouseMove); document.removeEventListener('mouseout', onGlobalMouseOut); window.removeEventListener('contextmenu', onGlobalCtxMenu, true); window.removeEventListener('click', onGlobalClick, true); window.removeEventListener('wheel', onGlobalWheel, { passive: true, capture: true }); delete window.pkRefreshTooltip; if (tipEl && tipEl.parentNode) tipEl.remove(); if (style && style.parentNode) style.remove(); }; })(); const UI = { win: el.querySelector('.pk-win'), vp: el.querySelector('#pk-vp'), in: el.querySelector('#pk-in'), loader: el.querySelector('#pk-loader'), loadTxt: el.querySelector('#pk-load-txt'), stopBtn: el.querySelector('#pk-stop-load'), crumb: el.querySelector('#pk-crumb'), stat: el.querySelector('#pk-stat'), chkAll: el.querySelector('#pk-all'), scan: el.querySelector('#pk-scan-dup'), dupTools: el.querySelector('#pk-dup-tools'), dupFilters: el.querySelector('#pk-dup-filters'), chkName: el.querySelector('#pk-chk-name'), chkSim: el.querySelector('#pk-chk-sim'), chkHash: el.querySelector('#pk-chk-hash'), selDupFolder: el.querySelector('#pk-dup-folder-sel'), offTools: el.querySelector('#pk-offline-tools'), chkOffRun: el.querySelector('#pk-off-run'), chkOffFail: el.querySelector('#pk-off-fail'), chkOffOk: el.querySelector('#pk-off-ok'), upTools: el.querySelector('#pk-upload-tools'), chkUpRun: el.querySelector('#pk-chk-up-run'), chkUpPause: el.querySelector('#pk-chk-up-pause'), chkUpDone: el.querySelector('#pk-chk-up-done'), btnAnalyze: el.querySelector('#pk-analyze'), btnExport: el.querySelector('#pk-export'), btnFolderFirst: el.querySelector('#pk-btn-folder-first'), btnNavHome: el.querySelector('#pk-nav-home'), btnNavOffline: el.querySelector('#pk-nav-offline'), btnNavUpload: el.querySelector('#pk-nav-upload'), btnNavRecent: el.querySelector('#pk-nav-recent'), btnNavHistory: el.querySelector('#pk-nav-history'), btnNavShare: el.querySelector('#pk-nav-share'), btnNavStarred: el.querySelector('#pk-nav-starred'), btnNavTrash: el.querySelector('#pk-nav-trash'), trashBar: el.querySelector('#pk-trash-bar'), btnTrashRefresh: el.querySelector('#pk-trash-refresh'), bottomGrp: el.querySelector('.pk-ft .pk-grp'), actionBar: el.querySelector('#pk-actionbar'), btnRestore: el.querySelector('#pk-restore'), btnDelForever: el.querySelector('#pk-del-forever'), btnEmptyTrash: el.querySelector('#pk-empty-trash'), btnTrashBlacklistManager: el.querySelector('#pk-trash-blacklist-manager'), btnDupSmart: el.querySelector('#pk-dup-smart-btn'), btnExit: el.querySelector('#pk-btn-exit'), btnCopy: el.querySelector('#pk-copy'), btnCut: el.querySelector('#pk-cut'), btnDel: el.querySelector('#pk-del'), btnDeselect: el.querySelector('#pk-deselect'), btnRename: el.querySelector('#pk-rename'), btnBulkRename: el.querySelector('#pk-bulkrename'), btnPrune: el.querySelector('#pk-prune'), btnUnzip: el.querySelector('#pk-unzip'), btnBlacklistManager: el.querySelector('#pk-blacklist-manager'), uploadWrap: el.querySelector('#pk-upload-wrap'), btnUpload: el.querySelector('#pk-btn-upload'), actUpFile: el.querySelector('#pk-act-upload-file'), actUpFolder: el.querySelector('#pk-act-upload-folder'), inpFile: el.querySelector('#pk-file-selector'), inpFolder: el.querySelector('#pk-folder-selector'), btnCancelShare: el.querySelector('#pk-cancel-share'), btnRetryTask: el.querySelector('#pk-retry-task'), btnCopyLinkOffline: el.querySelector('#pk-off-copy-link'), btnUpPause: el.querySelector('#pk-up-pause'), btnUpStart: el.querySelector('#pk-up-start'), btnUpDel: el.querySelector('#pk-up-del'), btnUpClearAll: el.querySelector('#pk-up-clear-all'), btnPaste: el.querySelector('#pk-paste'), btnPaste: el.querySelector('#pk-paste'), btnRefresh: el.querySelector('#pk-refresh'), btnNewFolder: el.querySelector('#pk-newfolder'), btnSettings: el.querySelector('#pk-settings'), btnClose: el.querySelector('#pk-close'), btnHelp: el.querySelector('#pk-help'), btnTheme: el.querySelector('#pk-theme'), btnExt: el.querySelector('#pk-ext'), btnAria2: el.querySelector('#pk-aria2'), btnDown: el.querySelector('#pk-down'), pop: el.querySelector('#pk-pop'), ctx: el.querySelector('#pk-ctx'), cols: el.querySelectorAll('.pk-col'), searchInput: el.querySelector('#pk-search-input'), chkGlobal: el.querySelector('#pk-chk-global'), lblGlobal: el.querySelector('#pk-lbl-global'), chkSearchPath: el.querySelector('#pk-chk-search-path'), lblSearchPath: el.querySelector('#pk-search-path-con'), btnAnaSelect: (function() { const b = document.createElement('button'); b.className = 'pk-ana-select-btn'; b.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg> <span>${L.btn_ana_select}</span>`; const parent = el.querySelector('#pk-search-path-con'); if (parent) parent.parentNode.insertBefore(b, parent); return b; })(), topBar: el.querySelector('#pk-top-bar'), searchClear: el.querySelector('#pk-search-clear'), searchBtn: el.querySelector('#pk-search-btn'), searchHist: el.querySelector('#pk-search-hist'), filterBar: el.querySelector('#pk-filter-bar'), filterBtn: el.querySelector('#pk-filter-btn'), filterActiveUI: el.querySelector('#pk-filter-active-ui'), filterCatLabel: el.querySelector('#pk-filter-cat-label'), filterExtsWrap: el.querySelector('#pk-filter-exts-wrap'), filterExtsMain: el.querySelector('#pk-filter-exts-main'), filterExtsMoreBtn: el.querySelector('#pk-filter-exts-more-btn'), filterExitBtn: el.querySelector('#pk-filter-exit-btn') }; const invokeExternalPlayer = async (item) => { const player = gmGet('pk_ext_player', 'system'); const L = getStrings(); let link = item.web_content_link; if (!link) { try { const detail = await apiGet(item.id); link = detail.web_content_link; } catch (e) { showAlert(L.msg_video_fail); return; } } if (!link) { showAlert(L.msg_video_fail); return; } if (player === 'potplayer') { let cleanLink = link.replace('&ext=.m3u8', ''); if (cleanLink.includes('ts_downloader') && cleanLink.includes('url=')) { const urlParam = new URL(cleanLink).searchParams.get('url'); if (urlParam) cleanLink = decodeURIComponent(urlParam); } const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanLink} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; } else { playVideo(item); } }; const isSystemItem = (item) => { if (!item) return false; if (item.kind !== 'drive#folder') return false; if (item._isSystemRoot) return true; const isRootLocation = S.path.length === 1 && S.path[0].id === ''; if (isRootLocation && item.name === CONF.SYSTEM_FOLDER_NAME) return true; if (!item.parent_id && item.name === CONF.SYSTEM_FOLDER_NAME) return true; return false; }; const updateCrawlerUI = () => { if (!UI.btnNavHome) return; if (typeof isBackgroundRunning !== 'undefined' && isBackgroundRunning) { UI.btnNavHome.classList.add('pk-status-dot'); } else { UI.btnNavHome.classList.remove('pk-status-dot'); } }; window.pkUpdateCrawlerUI = updateCrawlerUI; let isForcedHidden = false; const checkGuiResponsiveness = () => { const width = window.innerWidth; const height = window.innerHeight; const screenW = window.screen.width; let z = 1; if (screenW <= 1600 || width <= 1600) z = 0.8; if (screenW <= 1280 || width <= 1280) z = 0.7; document.documentElement.style.setProperty('--pk-zoom', z); const isTurboCurrent = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; const MIN_WIDTH = 940 * z; const MIN_HEIGHT = 450; const isTooSmall = width < MIN_WIDTH || height < MIN_HEIGHT; const isGuiVisible = el.style.display !== 'none'; if (isTooSmall && !isTurboCurrent) { document.body.classList.add('pk-hide-all-ui'); if (isGuiVisible) { el.style.display = 'none'; isForcedHidden = true; } } else { document.body.classList.remove('pk-hide-all-ui'); if (isForcedHidden) { el.style.display = 'flex'; if (el.focus) el.focus(); isForcedHidden = false; } } const isCompact = width <= 1200; if (UI.searchInput) { UI.searchInput.placeholder = isCompact ? L.placeholder_search_short : L.placeholder_search; } const folderSelPlaceholder = document.querySelector('#pk-dup-folder-sel option[value=""]'); if (folderSelPlaceholder) { folderSelPlaceholder.textContent = isCompact ? L.lbl_dup_select_folder_short : L.lbl_dup_select_folder; } }; window.addEventListener('resize', () => { checkGuiResponsiveness(); if (typeof renderVisible === 'function') requestAnimationFrame(renderVisible); }); checkGuiResponsiveness(); if (UI.btnClose) { UI.btnClose.addEventListener('click', () => { isForcedHidden = false; }); } if (UI.btnTheme) { UI.btnTheme.onclick = (e) => { if (e) e.stopPropagation(); el.classList.add('pk-no-transition'); void el.offsetHeight; const wasDark = el.classList.contains('pk-dark'); const newTheme = wasDark ? 'light' : 'dark'; const newIcon = wasDark ? CONF.icons.moon : CONF.icons.sun; UI.btnTheme.innerHTML = newIcon; if (wasDark) { el.classList.remove('pk-dark'); } else { el.classList.add('pk-dark'); } gmSet('pk_theme', newTheme); void el.offsetHeight; requestAnimationFrame(() => { el.classList.remove('pk-no-transition'); }); }; } const btnMax = el.querySelector('#pk-maximize'); const isTurbo = gmGet('pk_turbo_mode', false); let isWinMaximized = (globalSavedState && typeof globalSavedState.isMaximized !== 'undefined') ? globalSavedState.isMaximized : isTurbo; if (isTurbo) { if (btnMax) btnMax.style.display = 'none'; if (UI.btnClose) UI.btnClose.style.display = 'none'; } const win = el.querySelector('.pk-win'); if (isWinMaximized) { if (win) win.classList.add('pk-maximized'); document.body.classList.add('pk-body-max'); CONF.rowHeight = 60; if (btnMax) { btnMax.innerHTML = CONF.icons.minimize; btnMax.setAttribute('data-pk-tip', L.tip_minimize); } } else { if (win) win.classList.remove('pk-maximized'); document.body.classList.remove('pk-body-max'); CONF.rowHeight = 40; if (btnMax) { btnMax.innerHTML = CONF.icons.maximize; btnMax.setAttribute('data-pk-tip', L.tip_maximize); } } if (btnMax) { btnMax.onclick = (e) => { if (e) e.stopPropagation(); el.classList.add('pk-no-transition'); const vp = UI.vp; const oldRowHeight = CONF.rowHeight; const centerIndex = (vp.scrollTop + vp.clientHeight / 2) / oldRowHeight; isWinMaximized = !isWinMaximized; btnMax.innerHTML = isWinMaximized ? CONF.icons.minimize : CONF.icons.maximize; btnMax.setAttribute('data-pk-tip', isWinMaximized ? L.tip_minimize : L.tip_maximize); const win = el.querySelector('.pk-win'); if (isWinMaximized) { win.classList.add('pk-maximized'); document.body.classList.add('pk-body-max'); CONF.rowHeight = 60; } else { win.classList.remove('pk-maximized'); document.body.classList.remove('pk-body-max'); CONF.rowHeight = 40; } if (typeof renderList === 'function') { renderList(); } else if (typeof renderVisible === 'function') { if(UI.in) UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; renderVisible(); } if (typeof refreshQuotaText === 'function') refreshQuotaText(); requestAnimationFrame(() => { void el.offsetHeight; const newRowHeight = CONF.rowHeight; const newVpHeight = vp.clientHeight; const targetScrollTop = (centerIndex * newRowHeight) - (newVpHeight / 2); vp.scrollTop = Math.max(0, targetScrollTop); renderVisible(); el.classList.remove('pk-no-transition'); }); }; } let modalZIndexCounter = 100000; function showModal(html) { let container = document.getElementById('pk-toast-container'); if (container) document.body.appendChild(container); const m = document.createElement('div'); m.className = 'pk-modal-ov'; m.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) { m.classList.add('pk-dark'); } m.innerHTML = `<div class="pk-modal" style="height:auto; min-height:auto; max-height:85vh; overflow:visible;"><div class="pk-modal-close" style="z-index:10;">${CONF.icons.close}</div>${html}</div>`; 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(` <h3 style="border:none; margin-bottom:16px; font-size:18px; font-weight:700; color:var(--pk-fg);">${title}</h3> <div style="margin-bottom:32px; line-height:1.6; font-size:14px; color:var(--pk-fg); opacity:0.9; word-break:break-all;">${msg.replace(/\n/g, '<br>')}</div> <div class="pk-modal-act" style="justify-content: flex-end;"> <button class="pk-btn pri" id="alert_ok" style="height:40px; min-width:86px; padding:0 30px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold; font-size:14px; justify-content:center;"> ${L.btn_ok} </button> </div> `); 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(` <h3 style="border:none; margin-bottom:16px; font-size:18px; font-weight:700; color:var(--pk-fg);">${title}</h3> <div style="margin-bottom:30px; line-height:1.6; font-size:14px; color:var(--pk-fg);">${esc(msg).replace(/\n/g, '<br>')}</div> <div class="pk-modal-act" style="display:flex; justify-content:flex-end; gap:12px; align-items:center;"> <button class="pk-btn" id="cfm_no" style="height:40px; min-width:86px; padding:0 24px; border-radius:8px; font-weight:500; justify-content:center; background:transparent;">${L.btn_no}</button> <button class="pk-btn pri" id="cfm_yes" style="height:40px; min-width:86px; padding:0 24px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold; justify-content:center;">${L.btn_yes}</button> </div> `); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '420px', height: 'auto', minHeight: 'auto', padding: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelector('#cfm_no').onclick = () => { m.remove(); resolve(false); }; m.querySelector('#cfm_yes').onclick = () => { m.remove(); resolve(true); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(false); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#cfm_yes').click(); } }); }); } const confirmSelectionClear = async () => { if (S.suppressClearConfirm) return true; return new Promise((resolve) => { const m = showModal(` <h3 style="border:none; margin-bottom:16px; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.title_confirm}</h3> <div style="margin-bottom:20px; line-height:1.6; font-size:14px; color:var(--pk-fg);">${esc(L.msg_clear_sel_confirm.replace('{n}', S.sel.size)).replace(/\n/g, '<br>')}</div> <div style="margin-bottom:20px; display:flex; align-items:center;"> <label style="display:flex; align-items:center; cursor:pointer; font-size:12px; color:#666; user-select:none;"> <input type="checkbox" id="pk_session_suppress" style="margin-right:6px; width:14px; height:14px; accent-color:var(--pk-pri);"> <span>${L.lbl_dont_show_session}</span> </label> </div> <div class="pk-modal-act" style="display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="cfm_no" style="height:40px; min-width:86px; border-radius:8px; font-weight:500; background:transparent;">${L.btn_no}</button> <button class="pk-btn pri" id="cfm_yes" style="height:40px; min-width:86px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold;">${L.btn_yes}</button> </div> `); 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(` <h3 style="border:none; margin-bottom:24px; font-size:18px; font-weight:700; color:var(--pk-fg);">${cleanTitle}</h3> <div style="position:relative;"> <input type="text" id="prm_input" value="${esc(val)}" style="width:100%; height:44px; padding:0 12px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:15px; font-weight:600; outline:none; transition:border-color 0.2s; box-sizing:border-box;"> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; line-height:1;">${title}</div> <div id="prm_err" class="pk-input-err-msg" style="color:#ff4d4f; font-size:12px; margin-top:8px; min-height:18px; visibility:hidden; font-weight:500;">${L.err_name_exists}</div> </div> <div class="pk-modal-act" style="margin-top:15px; display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="prm_cancel" style="height:40px; min-width:86px; padding:0 24px; border-radius:8px; justify-content:center; background:transparent;">${L.btn_cancel}</button> <button class="pk-btn pri" id="prm_ok" style="height:40px; min-width:86px; padding:0 24px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold; justify-content:center;">${okBtnText}</button> </div> `); 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}<span style="flex: 1; line-height: 1.4;">${msg}</span>`; 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: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`, delRow: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`, trash: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`, rocket: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.1 4 0 4 0"/><path d="M12 15v5s3.03-.55 4-2c1.1-1.62 0-4 0-4"/></svg>`, save: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>` }; 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 = ` <style> .pk-bl-btn { border: none; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); font-size: 12px; font-weight: 600; padding: 0 16px; height: 32px; } .pk-bl-btn:active { transform: scale(0.96); } .pk-tool-btn { border: 1px solid var(--pk-bd); background: var(--pk-bg); color: var(--pk-fg); padding: 0 12px; height: 32px; border-radius: 6px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px; } .pk-tool-btn:hover { border-color: var(--pk-pri); color: var(--pk-pri); background: var(--pk-sel-bg); } .pk-main-clear { background: transparent; color: #d93025; padding: 0 10px; font-size: 13px; } .pk-main-clear:hover { background: rgba(217, 48, 37, 0.08); } .pk-main-run { background: #d93025; color: #fff; box-shadow: 0 4px 12px rgba(217, 48, 37, 0.25); border-radius: 8px; padding: 0 20px; font-size: 13px; } .pk-main-run:hover { filter: brightness(1.15); transform: translateY(-1px); box-shadow: 0 6px 16px rgba(217, 48, 37, 0.35); } .pk-main-save { background: var(--pk-pri); color: #fff; border: none; border-radius: 8px; padding: 0 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-size: 13px; } .pk-main-save:hover { filter: brightness(1.1); transform: translateY(-1px); } .pk-bl-area:focus { background: var(--pk-bg) !important; border-color: var(--pk-pri) !important; outline: none !important; } .pk-ov.pk-dark .pk-bl-area:focus { border-color: #ffffff !important; box-shadow: 0 0 0 1px #ffffff, 0 0 10px rgba(255,255,255,0.2) !important; outline: none !important; } .pk-bl-area::placeholder { color: #999; font-style: normal; opacity: 0.8; } </style> `; const m = showModal(` ${modalInnerStyle} <div style="display:flex; flex-direction:column; height:100%; padding: 10px 0;"> <div style="flex-shrink:0;"> <h3 style="margin:0; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.title_blacklist}</h3> <div style="margin-top:12px; display:${gmGet('pk_skip_bl_on_del', true) ? 'flex' : 'none'}; align-items:center; gap:8px; font-size:12px; color:var(--pk-pri); background:rgba(0,103,192,0.06); padding:8px 12px; border-radius:6px; line-height:1.4;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> <span>${L.tip_bl_desc}</span> </div> <div style="display:flex; gap:20px; margin-top:16px; align-items:center;"> <label style="display:flex; align-items:center; cursor:pointer; user-select:none;"> <input type="radio" name="bl_mode" value="folder" checked style="accent-color:var(--pk-pri); transform:scale(1.1); margin:0;"> <div style="display:flex; align-items:center; margin-left:8px; color:var(--pk-fg);"> ${CONF.icons.upFolder.replace('width="16"', 'width="18"').replace('height="16"', 'height="18"')} <span style="margin-left:6px; font-weight:600; font-size:13px;">${L.label_bl_folder}</span> </div> </label> <div style="width:1px; height:14px; background:var(--pk-bd);"></div> <label style="display:flex; align-items:center; cursor:pointer; user-select:none;"> <input type="radio" name="bl_mode" value="file" style="accent-color:var(--pk-pri); transform:scale(1.1); margin:0;"> <div style="display:flex; align-items:center; margin-left:8px; color:var(--pk-fg);"> ${CONF.icons.upFile.replace('width="16"', 'width="18"').replace('height="16"', 'height="18"')} <span style="margin-left:6px; font-weight:600; font-size:13px;">${L.label_bl_file}</span> </div> </label> </div> </div> <div id="pk_bl_toast" style="${toastStyle}"></div> <div style="flex:1; display:flex; flex-direction:column; min-height:0; position:relative; margin-top:15px;"> <div id="group_bl_folder" style="display:flex; flex-direction:column; height:100%;"> <div style="display:flex; justify-content:flex-end; gap:10px; margin-bottom:10px;"> <button type="button" id="btn_paste_folder" class="pk-tool-btn"> ${icons.paste} <span>${L.btn_paste}</span> </button> <button type="button" id="btn_del_folder" class="pk-tool-btn"> ${icons.delRow} <span>${L.btn_del}</span> </button> </div> <textarea id="bl_folder_input" class="pk-bl-area" readonly spellcheck="false" wrap="off" placeholder="${L.ph_bl_folder}" style="${textareaStyle}"></textarea> </div> <div id="group_bl_file" style="display:none; flex-direction:column; height:100%;"> <div style="display:flex; justify-content:flex-end; gap:10px; margin-bottom:10px;"> <button type="button" id="btn_paste_file" class="pk-tool-btn"> ${icons.paste} <span>${L.btn_paste}</span> </button> <button type="button" id="btn_del_file" class="pk-tool-btn"> ${icons.delRow} <span>${L.btn_del}</span> </button> </div> <textarea id="bl_file_input" class="pk-bl-area" readonly spellcheck="false" wrap="off" placeholder="${L.ph_bl_file}" style="${textareaStyle}"></textarea> </div> </div> <div class="pk-modal-act" style="justify-content: space-between; align-items: center; margin-top:20px; padding-top:15px; border-top:1px solid var(--pk-bd); flex-shrink:0;"> <button class="pk-bl-btn pk-main-clear" id="bl_clear"> ${icons.trash} <span>${L.btn_clear_list}</span> </button> <div style="display:flex; gap:12px;"> <button class="pk-bl-btn pk-main-run" id="bl_run"> ${icons.rocket} <span>${L.btn_blacklist_run}</span> </button> <button class="pk-bl-btn pk-main-save" id="bl_save"> ${icons.save} <span>${L.btn_save}</span> </button> </div> </div> </div> `); const modalEl = m.querySelector('.pk-modal'); Object.assign(modalEl.style, { width: '600px', maxWidth: '90vw', padding: '30px', height: '600px', maxHeight: '85vh' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); const areaFolder = m.querySelector('#bl_folder_input'); const areaFile = m.querySelector('#bl_file_input'); const radios = m.querySelectorAll('input[name="bl_mode"]'); const groupFolder = m.querySelector('#group_bl_folder'); const groupFile = m.querySelector('#group_bl_file'); radios.forEach(r => { r.onchange = () => { const mode = r.value; groupFolder.style.display = mode === 'folder' ? 'flex' : 'none'; groupFile.style.display = mode === 'file' ? 'flex' : 'none'; }; }); const toast = m.querySelector('#pk_bl_toast'); const showToast = (msg) => { toast.textContent = msg; toast.style.opacity = '1'; setTimeout(() => { toast.style.opacity = '0'; }, 2000); }; const loadLargeText = async (el, storageKey, originalPlaceholder) => { el.placeholder = L.str_loading_placeholder; await sleep(350); const data = await new Promise(r => setTimeout(() => r(gmGet(storageKey, '')), 0)); if (data) { const prevDisplay = el.style.display; el.style.display = 'none'; el.value = data; el.style.display = prevDisplay; } el.placeholder = originalPlaceholder; el.setSelectionRange(0, 0); el.blur(); }; loadLargeText(areaFolder, 'pk_blacklist_folders', L.ph_bl_folder); loadLargeText(areaFile, 'pk_blacklist', L.ph_bl_file); const highlightLine = (el) => { if (el.selectionStart !== el.selectionEnd) return; const val = el.value; if (!val) return; const cursor = el.selectionStart; let start = val.lastIndexOf('\n', cursor - 1); start = start === -1 ? 0 : start + 1; let end = val.indexOf('\n', cursor); if (end === -1) end = val.length; el.setSelectionRange(start, end); }; const enableLineSnap = (el) => { el.addEventListener('click', () => highlightLine(el)); el.addEventListener('keyup', (e) => { if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) highlightLine(el); }); }; enableLineSnap(areaFolder); enableLineSnap(areaFile); const setupSafeControls = (el, btnPaste, btnDel) => { btnPaste.onclick = async () => { try { const text = await navigator.clipboard.readText(); if (!text || !text.trim()) return showToast(L.msg_copy_empty); const cleanText = text.split(/\r?\n/).map(line => line.trim()).filter(line => line).join('\n'); const oldVal = el.value; const prefix = (oldVal && !oldVal.endsWith('\n')) ? '\n' : ''; el.value = oldVal + prefix + cleanText; el.scrollTop = el.scrollHeight; const addedCount = cleanText.split('\n').length; showToast(L.msg_add_success.replace('{n}', addedCount)); } catch (err) { showToast(L.err_clipboard_denied); } }; btnDel.onclick = () => { const val = el.value; if (!val) return; const selStart = el.selectionStart; const selEnd = el.selectionEnd; if (selStart === selEnd) { return showToast(L.msg_del_select); } let lineStart = val.lastIndexOf('\n', selStart - 1); lineStart = (lineStart === -1) ? 0 : lineStart + 1; let lineEnd = val.indexOf('\n', selEnd); if (lineEnd === -1) lineEnd = val.length; else lineEnd += 1; const newVal = val.substring(0, lineStart) + val.substring(lineEnd); el.value = newVal; el.setSelectionRange(lineStart, lineStart); highlightLine(el); el.focus(); showToast(L.msg_del_done); }; }; setupSafeControls(areaFolder, m.querySelector('#btn_paste_folder'), m.querySelector('#btn_del_folder')); setupSafeControls(areaFile, m.querySelector('#btn_paste_file'), m.querySelector('#btn_del_file')); m.querySelector('#bl_save').onclick = async () => { const fDir = areaFolder.value.trim(); const fFile = areaFile.value.trim(); gmSet('pk_blacklist_folders', fDir); gmSet('pk_blacklist', fFile); S.updateBlCache(); m.remove(); renderVisible(); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); e.stopPropagation(); m.querySelector('#bl_save').click(); } }); m.querySelector('#bl_clear').onclick = async () => { if (areaFolder.value.trim() === '' && areaFile.value.trim() === '') return; if (await showConfirm(L.msg_bl_clear_confirm, L.title_confirm)) { areaFolder.value = ""; areaFile.value = ""; gmSet('pk_blacklist_folders', ""); gmSet('pk_blacklist', ""); S.updateBlCache(); renderVisible(); showToast(L.str_cleanup_done); } }; m.querySelector('#bl_run').onclick = async () => { if (S.movingIds && S.movingIds.size > 0) { showAlert(L.err_task_conflict); return; } const fDir = areaFolder.value.trim(); const fFile = areaFile.value.trim(); gmSet('pk_blacklist_folders', fDir); gmSet('pk_blacklist', fFile); S.updateBlCache(); const isGlobalSearch = S.path.some(p => p.id === 'virtual_search_root'); if (S.trashMode || S.shareMode || S.offlineMode || S.starredMode || S.recentMode || S.historyMode || S.isFlattened || S.dupMode || S.analyzeMode || isGlobalSearch) { m.remove(); showAlert(L.msg_bl_run_limit); return; } m.remove(); setLoad(true); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; const parseBigTextAsync = async (text, typeLabel) => { const set = new Set(); if (!text) return set; const len = text.length; let start = 0; let end = 0; let count = 0; let lastYieldTime = performance.now(); updateLoadTxt(`${L.str_analyzing}\n${typeLabel}... 0%`); await sleep(16); while (start < len) { if (!S.scanning || signal.aborted || myScanId !== S.scanId) return set; end = text.indexOf('\n', start); if (end === -1) end = len; const line = text.substring(start, end).trim().toLowerCase(); if (line) set.add(line); start = end + 1; count++; if (count % 2000 === 0) { const now = performance.now(); if (now - lastYieldTime > 16) { const progress = Math.min(100, Math.round((start / len) * 100)); updateLoadTxt(`${L.str_analyzing}\n${typeLabel}... ${progress}% (${set.size})`); await sleep(0); lastYieldTime = performance.now(); } } } return set; }; UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); }; try { const folderText = areaFolder.value || ''; const fileText = areaFile.value || ''; if (folderText) S.blFolderSet = await parseBigTextAsync(folderText, L.lbl_type_folder); else S.blFolderSet = new Set(); if (S.scanning && !signal.aborted && myScanId === S.scanId) { if (fileText) S.blSet = await parseBigTextAsync(fileText, L.lbl_type_file); else S.blSet = new Set(); } if (!S.scanning || signal.aborted || myScanId !== S.scanId) { if (myScanId === S.scanId) setLoad(false); return; } if (S.blSet.size === 0 && S.blFolderSet.size === 0) { setLoad(false); showAlert(L.msg_bl_empty); return; } updateLoadTxt(L.str_init_scan); await sleep(50); const foundMatches = []; const rootNodes = [{ id: '', name: 'Root', lineage: [], retryCount: 0 }]; await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { const cleanName = f.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (S.blSet.has(cleanName)) { f._lineage = parent.lineage || []; foundMatches.push({ item: f, type: 'FILE' }); } }, onFolder: (folder, filesInFolder, nextSubFolders) => { const cleanName = folder.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const isSystemRootFolder = (folder.parent_id === '' || folder.parent_id === 'root') && folder.name === CONF.SYSTEM_FOLDER_NAME; if (S.blFolderSet.has(cleanName) && !isSystemRootFolder) { folder._lineage = (folder.lineage || []).slice(0, -1); foundMatches.push({ item: folder, type: 'FOLDER' }); nextSubFolders.length = 0; } }, onProgress: (st) => { const folderText = `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const statusInfo = ` | ${L.str_hits}: ${foundMatches.length} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo); } }); if (!S.scanning || signal.aborted || myScanId !== S.scanId) { if (myScanId === S.scanId) setLoad(false); return; } setLoad(false); if (foundMatches.length === 0) { showAlert(L.msg_blacklist_run_none); return; } showPreviewModal(foundMatches); } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { setLoad(false); showAlert(`${L.str_scan_error}: ${e.message}`); } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } }; } function showPreviewModal(matches) { const modalHtml = ` <style> .pk-bl-prev-ov { position: relative; flex: 1; overflow: hidden; display: flex; flex-direction: column; border: 2px solid var(--pk-bd); border-radius: 8px; background: var(--pk-bg); } #bl_prev_list { position: relative; flex: 1; overflow-y: auto; overflow-x: hidden; user-select: none; } #bl_prev_del:hover:not(:disabled) { background: #d93025 !important; filter: brightness(1.15) !important; } .pk-bl-mq { position: absolute; background: rgba(0, 103, 192, 0.1); border: 1px solid rgba(0, 103, 192, 0.4); pointer-events: none; display: none; z-index: 10; } #bl_prev_list::-webkit-scrollbar { width: 6px; } #bl_prev_list::-webkit-scrollbar-thumb { background: var(--pk-sb-th); border-radius: 3px; } </style> <div style="display:flex; flex-direction:column; height:100%; overflow:hidden;"> <div style="padding: 24px 30px 15px 30px; flex-shrink:0;"> <h3 style="margin:0; font-size:18px; font-weight:700; color:var(--pk-fg); line-height:1.2;">${L.modal_bl_preview.replace('{n}', matches.length)}</h3> <div id="bl_top_stat" style="font-size:12px; color:#888; margin-top:4px; font-weight:500;"></div> </div> <div style="flex:1; padding: 0 30px; overflow:hidden; display:flex; flex-direction:column;"> <div class="pk-bl-prev-ov"> <div style="display:grid; grid-template-columns: 40px 50px 1fr 1fr; font-weight:600; padding:10px 0; border-bottom:1px dashed var(--pk-bd); background:var(--pk-bg); font-size:12px; position:sticky; top:0; align-items:center; z-index: 11; color:#888;"> <div style="display:flex; align-items:center; justify-content:center;"><input type="checkbox" id="bl_prev_all" checked style="cursor:pointer; margin:0; width:16px; height:16px; accent-color:var(--pk-pri);"></div> <div style="display:flex; align-items:center; justify-content:center;">${L.col_type}</div> <div>${L.col_name}</div> <div>${L.col_path}</div> </div> <div id="bl_prev_list"><div id="bl_prev_in" style="position:relative; width:100%;"></div></div> </div> </div> <div class="pk-modal-act" style="padding: 20px 30px 24px 30px; margin-top:0; display:flex; align-items:center; justify-content:flex-end; flex-shrink:0;"> <div style="display:flex; gap:12px;"> <button class="pk-btn" id="bl_prev_cancel" style="height:40px; padding:0 20px; border-radius:8px; background:transparent; font-weight:600; color:var(--pk-fg);">${L.btn_cancel}</button> <button class="pk-btn pri" id="bl_prev_del" style="height:40px; padding:0 24px; border-radius:8px; background:#d93025; border:none; color:#fff; font-weight:bold; box-shadow:0 4px 12px rgba(217,48,37,0.3); transition:all 0.2s;">${L.btn_bl_delete}</button> </div> </div> </div> `; 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}', `<b style="color:var(--pk-pri); margin:0 2px;">${c}</b>`); } 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 = `<img src="${it.thumbnail_link}" style="width:24px;height:24px;object-fit:cover;border-radius:4px;flex-shrink:0;" onerror="this.style.display='none';this.nextElementSibling.style.display='inline-flex';">`; const secondFallback = it.icon_link ? `<img src="${it.icon_link}" style="width:24px;height:24px;object-fit:contain;flex-shrink:0;" onerror="this.style.display='none';this.nextElementSibling.style.display='inline-flex';"><span style="display:none;align-items:center;">${fallbackSvg}</span>` : fallbackSvg; iconHtml += `<span style="display:none;align-items:center;justify-content:center;">${secondFallback}</span>`; } else { const iconSrc = it.icon_link; iconHtml = iconSrc ? `<img src="${iconSrc}" style="width:24px;height:24px;object-fit:contain;flex-shrink:0;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display='inline-flex';"><span style="display:none;align-items:center;flex-shrink:0;">${fallbackSvg}</span>` : fallbackSvg; } const lineage = mMatch.item._lineage ||[]; const relativePath = lineage.map(x => x.name).join('/'); const homeIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;flex-shrink:0;vertical-align:-1px;"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`; row.ondragstart = (e) => { e.preventDefault(); return false; }; let pathHtml = `<div style="display:flex;align-items:center;overflow:hidden;white-space:nowrap;"> <span style="display:flex;align-items:center;flex-shrink:0;">${homeIcon}${esc(L.btn_nav_home)}</span>`; if (relativePath) { pathHtml += `<span style="margin:0 4px;">/</span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(relativePath)}</span>`; } pathHtml += `</div>`; const homeGroupTip = `<span style="display:inline-flex;align-items:center;vertical-align:bottom;">${homeIcon}${esc(L.btn_nav_home)}</span>`; const fullPathTip = `<div style="line-height:1.6;word-break:break-all;">${homeGroupTip}${relativePath ? '<span style="margin:0 4px;opacity:0.5;">/</span>' + esc(relativePath) : ''}</div>`; const thumbAttr = (typeof hasCover !== 'undefined' && hasCover) ? `data-pk-thumb="${it.thumbnail_link}"` : ''; const isChecked = selectedIds.has(mMatch.item.id); row.innerHTML = ` <div style="display:flex; align-items:center; justify-content:center;"> <input type="checkbox" ${isChecked ? 'checked' : ''} data-id="${mMatch.item.id}" style="cursor:pointer; margin:0; display:block; width:16px; height:16px; accent-color:var(--pk-pri);"> </div> <div style="display:flex; align-items:center; justify-content:center;" ${thumbAttr}>${iconHtml}</div> <div style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-right:10px; font-weight:normal; color:var(--pk-fg);" ${thumbAttr} data-pk-tip="${esc(mMatch.item.name)}">${esc(mMatch.item.name)}</div> <div style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--pk-fg);" data-pk-tip="${fullPathTip.replace(/"/g, '"')}">${pathHtml}</div> `; const chk = row.querySelector('input'); row.onclick = (e) => { const curIdx = i; const id = mMatch.item.id; if (e.target === chk && !e.shiftKey && !e.ctrlKey && !e.metaKey) return; if (e.shiftKey && lastIdx !== -1) { const s = Math.min(lastIdx, curIdx); const eIdx = Math.max(lastIdx, curIdx); if (!e.ctrlKey && !e.metaKey) selectedIds.clear(); for (let k = s; k <= eIdx; k++) selectedIds.add(matches[k].item.id); } else if (e.ctrlKey || e.metaKey) { if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id); } else { selectedIds.clear(); selectedIds.add(id); } lastIdx = curIdx; updateCount(); renderPreviewList(); }; chk.onchange = (e) => { if(e.target.checked) { selectedIds.add(mMatch.item.id); lastIdx = i; } else selectedIds.delete(mMatch.item.id); updateCount(); renderPreviewList(); }; fragment.appendChild(row); } listIn.appendChild(fragment); }; listDiv.onscroll = () => { if (!isRenderScheduled) { requestAnimationFrame(() => { renderPreviewList(); isRenderScheduled = false; }); isRenderScheduled = true; } }; renderPreviewList(); m.addEventListener('click', (e) => { if (m._blockClick) return; if (e.target.closest('[data-id]') || e.target.closest('button') || e.target.closest('input') || e.target.closest('label')) return; if (selectedIds.size > 0) { selectedIds.clear(); lastIdx = -1; updateCount(); renderPreviewList(); } }); chkAll.onchange = (e) => { const checked = e.target.checked; if(checked) { matches.forEach(m => selectedIds.add(m.item.id)); } else { selectedIds.clear(); } updateCount(); renderPreviewList(); }; const mqBox = document.createElement('div'); mqBox.className = 'pk-bl-mq'; listDiv.appendChild(mqBox); let isDragging = false, isMarquee = false, startX = 0, startY = 0; let blScrollSpeed = 0, blScrollRaf = null, blLastX = 0, blLastY = 0, blLastCtrl = false; listDiv.onmousedown = (e) => { if (e.button !== 0 || e.target.closest('input')) return; const rect = listDiv.getBoundingClientRect(); isDragging = true; isMarquee = false; startX = e.clientX - rect.left; startY = e.clientY - rect.top + listDiv.scrollTop; const rawStartX = e.clientX, rawStartY = e.clientY; const updateMarqueeBox = () => { const curRect = listDiv.getBoundingClientRect(); const curX = blLastX - curRect.left; const maxScrollHeight = listIn.offsetHeight; const curY = Math.max(0, Math.min(maxScrollHeight, blLastY - curRect.top + listDiv.scrollTop)); const top = Math.min(startY, curY); const left = Math.min(startX, curX); const width = Math.abs(curX - startX); const height = Math.abs(curY - startY); mqBox.style.display = 'block'; mqBox.style.top = top + 'px'; mqBox.style.left = left + 'px'; mqBox.style.width = width + 'px'; mqBox.style.height = height + 'px'; const sIdx = Math.floor(top / ROW_HEIGHT); const eIdx = Math.floor((top + height) / ROW_HEIGHT); for (let i = 0; i < matches.length; i++) { const isInside = i >= sIdx && i <= eIdx; if (isInside) { selectedIds.add(matches[i].item.id); } else if (!blLastCtrl) { selectedIds.delete(matches[i].item.id); } } updateCount(); if (!isRenderScheduled) { requestAnimationFrame(() => { renderPreviewList(); isRenderScheduled = false; }); isRenderScheduled = true; } }; const runBlScroll = () => { if (!isMarquee || blScrollSpeed === 0) { blScrollRaf = null; return; } listDiv.scrollTop += blScrollSpeed; updateMarqueeBox(); blScrollRaf = requestAnimationFrame(runBlScroll); }; const onMouseMove = (me) => { if (!isDragging) return; if (!isMarquee) { if (Math.abs(me.clientX - rawStartX) > 5 || Math.abs(me.clientY - rawStartY) > 5) { isMarquee = true; if (!me.ctrlKey && !me.metaKey && !me.shiftKey) { selectedIds.clear(); lastIdx = -1; updateCount(); renderPreviewList(); } } else return; } blLastX = me.clientX; blLastY = me.clientY; blLastCtrl = me.ctrlKey || me.metaKey; const curRect = listDiv.getBoundingClientRect(); if (blLastY > curRect.bottom - 5) blScrollSpeed = Math.min(45, 2 + Math.pow((blLastY - curRect.bottom + 5) / 5, 1.3)); else if (blLastY < curRect.top + 5) blScrollSpeed = -Math.min(45, 2 + Math.pow((curRect.top + 5 - blLastY) / 5, 1.3)); else blScrollSpeed = 0; if (blScrollSpeed !== 0 && !blScrollRaf) blScrollRaf = requestAnimationFrame(runBlScroll); updateMarqueeBox(); }; const onMouseUp = () => { if (isDragging && mqBox.style.display === 'block') { m._blockClick = true; setTimeout(() => m._blockClick = false, 100); } isDragging = false; blScrollSpeed = 0; if (blScrollRaf) { cancelAnimationFrame(blScrollRaf); blScrollRaf = null; } mqBox.style.display = 'none'; window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }; m.querySelector('#bl_prev_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 50); m.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) { e.preventDefault(); e.stopPropagation(); matches.forEach(m => selectedIds.add(m.item.id)); updateCount(); renderPreviewList(); } }, true); btnDel.onclick = async () => { if (selectedIds.size === 0) return; const confirmed = await showConfirm(L.warn_del.replace('{n}', selectedIds.size)); if (!confirmed) return; m.remove(); const allIds = Array.from(selectedIds).filter(id => { const match = matches.find(m => m.item.id === id); if (match && isSystemItem(match.item)) { console.warn(`[Security] Prevented deletion of system folder: ${match.item.name}`); return false; } return true; }); if (allIds.length < selectedIds.size) { console.log("System items were removed from deletion list."); } const itemsToDelete = matches.filter(m => allIds.includes(m.item.id)).map(m => m.item); await executeBatchDelete(allIds, { silent: false, explicitItems: itemsToDelete }); }; } function setLoad(b, isInline = false) { S.loading = b; if (b) { if (isInline) { UI.loader.style.display = 'none'; } else { UI.loader.style.display = 'flex'; if (UI.loadTxt) UI.loadTxt.textContent = L.loading_detail; } } else { UI.loader.style.display = 'none'; } } function updateLoadTxt(txt) { if (UI.loadTxt) UI.loadTxt.innerText = txt; } let activeLoadId = 0; const computeDuplicateGroups = async (candidates, cfg, isRunningFn) => { const groups =[]; const assigned = new Set(); const strictness = gmGet('pk_dup_strictness', 'strict'); const sizeRatioLimit = (strictness === 'loose') ? 0.10 : 0.05; if (isRunningFn()) { updateLoadTxt(L.str_analyzing); const hashMap = new Map(); for (const item of candidates) { const hash = item.gcid || item.md5_checksum || item.hash; const key = hash ? `${hash}|${item.size}` : null; if (key) { if (!hashMap.has(key)) hashMap.set(key,[]); hashMap.get(key).push(item); } } for (const[key, items] of hashMap) { if (items.length > 1) { const ids = items.map(i => i.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_hash); }); groups.push({ ids: ids, type: L.tag_hash }); } } } if (isRunningFn() && cfg.video) { updateLoadTxt(L.str_analyzing); const simCandidates = candidates.filter(i => i.mime_type.startsWith('video') && !assigned.has(i.id)); const validVideos = simCandidates.filter(item => (parseFloat(item.params?.duration || 0) > 0)); validVideos.sort((a, b) => parseFloat(a.params?.duration || 0) - parseFloat(b.params?.duration || 0)); let processedCount = 0; const totalCount = validVideos.length; for (let i = 0; i < totalCount; i++) { processedCount++; if (processedCount % 500 === 0) { if (!isRunningFn()) break; const pct = Math.round(processedCount / totalCount * 100); updateLoadTxt(`${L.str_analyzing} (${pct}%)`); await sleep(0); } if (assigned.has(validVideos[i].id)) continue; const root = validVideos[i]; const rootDur = parseFloat(root.params?.duration || 0); const rootSize = parseInt(root.size || 0); const groupItems = [root]; for (let j = i + 1; j < totalCount; j++) { const target = validVideos[j]; if (assigned.has(target.id)) continue; const durThreshold = (strictness === 'loose') ? 2.0 : 1.0; const targetDur = parseFloat(target.params?.duration || 0); const durDiff = Math.abs(targetDur - rootDur); if (durDiff > durThreshold) break; const targetSize = parseInt(target.size || 0); if (rootSize > 0 && targetSize > 0) { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); const ratio = sizeDiff / maxBase; if (ratio <= sizeRatioLimit) { groupItems.push(target); } } } if (groupItems.length > 1) { const ids = groupItems.map(x => x.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_sim); }); groups.push({ ids: ids, type: L.tag_sim }); } } } if (isRunningFn()) { updateLoadTxt(L.str_analyzing); const cleanNameAd = (oldName) => { let cleanName = oldName; cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, ''); cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, ''); const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])"; const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\\\s)', 'i'); cleanName = cleanName.replace(atRegex, ''); const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i'); cleanName = cleanName.replace(hyphenRegex, ''); cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, ''); cleanName = cleanName.replace(/^[-_. ,,::;;\\p{Extended_Pictographic}]+/u, ''); const pairs = [['【','】'], ['[',']'], ['《','》'],['<','>'], ['(',')'],['(',')'], ['{','}']]; pairs.forEach(([L_char, R_char]) => { const idxR_Fix = cleanName.indexOf(R_char); const idxL_Check = cleanName.indexOf(L_char); if (idxR_Fix > 0 && idxR_Fix <= 10 && (idxL_Check === -1 || idxL_Check > idxR_Fix)) { cleanName = L_char + cleanName; } const chars = cleanName.split(''); const stack =[]; const toRemove = new Set(); for (let i = 0; i < chars.length; i++) { const c = chars[i]; if (c === L_char) { stack.push(i); } else if (c === R_char) { if (stack.length > 0) stack.pop(); else toRemove.add(i); } } stack.forEach(i => toRemove.add(i)); if (toRemove.size > 0) { cleanName = chars.filter((_, i) => !toRemove.has(i)).join(''); } }); const quote2 = (cleanName.match(/'/g) ||[]).length; if (quote2 % 2 !== 0) cleanName = cleanName.replace(/"/, ''); let result = cleanName.trim(); const lastDot = result.lastIndexOf('.'); if (lastDot > 0) result = result.substring(0, lastDot); let finalResult = result ? result.toLowerCase() : oldName.replace(/\.[^/.]+$/, "").toLowerCase().trim(); if (strictness === 'loose') { finalResult = finalResult.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ''); } return finalResult; }; const remainingItems = candidates.filter(i => !assigned.has(i.id)); const getTypeGroup = (mime) => { if (!mime) return 'other'; if (mime.startsWith('video')) return 'video'; if (mime.startsWith('image')) return 'image'; return 'other'; }; const typeNameMap = new Map(); for (const item of remainingItems) { const tGroup = getTypeGroup(item.mime_type); const cleaned = cleanNameAd(item.name); if (!cleaned) continue; const key = tGroup + '|' + cleaned; if (!typeNameMap.has(key)) typeNameMap.set(key,[]); typeNameMap.get(key).push(item); } for (const [key, items] of typeNameMap) { if (items.length > 1) { const sortedForAlgo = [...items].sort((a, b) => parseInt(a.size || 0) - parseInt(b.size || 0)); let currentGroup = [sortedForAlgo[0]]; const tempGroups =[]; for (let i = 1; i < sortedForAlgo.length; i++) { const target = sortedForAlgo[i]; const root = currentGroup[0]; const rootSize = parseInt(root.size || 0); const targetSize = parseInt(target.size || 0); let isMatch = false; if (rootSize === 0 && targetSize === 0) { isMatch = true; } else if (rootSize > 0 && targetSize > 0) { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); if ((sizeDiff / maxBase) <= sizeRatioLimit) { isMatch = true; } } if (isMatch) currentGroup.push(target); else { if (currentGroup.length > 1) tempGroups.push(currentGroup); currentGroup = [target]; } } if (currentGroup.length > 1) tempGroups.push(currentGroup); tempGroups.forEach(grp => { const ids = grp.map(i => i.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_name); }); groups.push({ ids: ids, type: L.tag_name }); }); } } } return groups; }; async function deepPreload(currentItems) { if (S.scanning) return; const folderItems = currentItems.filter(item => item.kind === 'drive#folder'); if (folderItems.length === 0) return; const preloadTasks = folderItems.map(folder => { if (S.cache.has(folder.id)) return Promise.resolve(); if (typeof globalCache !== 'undefined' && globalCache.has(folder.id)) { S.cache.set(folder.id, globalCache.get(folder.id)); return Promise.resolve(); } return apiList(folder.id, 1000) .then(files => { S.cache.set(folder.id, files); if (typeof globalCache !== 'undefined') globalCache.set(folder.id, files); }) .catch(e => { }); }); const MAX_CONCURRENT_PRELOADS = 5; let activePromises = []; for(let i = 0; i < preloadTasks.length; i++) { activePromises.push(preloadTasks[i]); if(activePromises.length >= MAX_CONCURRENT_PRELOADS) { await Promise.race(activePromises.map(p => p.then(() => 0).catch(() => 0))); activePromises = activePromises.filter(p => p.isResolved); } } await Promise.allSettled(activePromises); console.log("Deep preload finished."); } async function load(isHistoryNav = false, forceUpdate = false) { if (S.scanning) return; const snapshot = { trash: S.trashMode, share: S.shareMode, starred: S.starredMode, recent: S.recentMode, pathId: S.path[S.path.length - 1]?.id || '' }; const cur = S.path[S.path.length - 1]; const folderId = cur.id || 'root'; if (S.clipType === 'move' && S.movingIds.size > 0 && folderId === S.movingSourceId) { console.log(`[Cache Guard] Detected active move from this source: ${folderId}. Forcing network sync.`); forceUpdate = true; } let realCacheKey = S.getRealCacheKey(folderId); const isAnalyzeSub = S.path.some(p => p.id === 'analyze_root'); if (isAnalyzeSub) { S.analyzeMode = true; S.sort = 'size'; S.dir = 1; } if (folderId === 'analyze_root') { renderCrumb(); if (UI.scan) UI.scan.style.display = 'none'; S.items = [...(S.analyzeResultItems || [])]; S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); setLoad(false); refresh(); updateStat(); return; } if (globalDirtyFolders.has(folderId) || (folderId === 'root' && globalDirtyFolders.has(''))) { console.log(`[Load] ${folderId} is dirty. Forcing network fetch.`); forceUpdate = true; globalDirtyFolders.delete(folderId); globalDirtyFolders.delete(''); globalDirtyFolders.delete('root'); } S.clearSelection(); if (typeof UI !== 'undefined' && UI.vp && !forceUpdate) { UI.vp.scrollTop = 0; } if (UI.win) UI.win.setAttribute('data-cur-pid', folderId); activeLoadId++; const currentId = activeLoadId; let nextToken = null; const fetchedIds = new Set(); let isResuming = false; if (S.abortController) { S.abortController.abort(); } S.abortController = new AbortController(); const signal = S.abortController.signal; const isInVirtualSearch = S.path.some(p => p.id === 'virtual_search_root'); if (!isInVirtualSearch) { S.search = ''; if (UI.searchInput) { UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; } } if (cur.id === 'virtual_search_root') { renderCrumb(); if (UI.scan) UI.scan.style.display = 'none'; setLoad(false); refresh(); return; } const isInVirtualStack = S.path.some(p => p.id === 'virtual_search_root'); if (isInVirtualStack) { if (UI.chkGlobal) UI.chkGlobal.checked = true; } UI.scan.style.display = (S.trashMode || S.shareMode || S.offlineMode) ? 'none' : 'flex'; UI.btnExit.style.display = 'none'; if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (UI.offTools) UI.offTools.style.display = S.offlineMode ? 'flex' : 'none'; if (UI.upTools) UI.upTools.style.display = S.uploadMode ? 'flex' : 'none'; if (UI.crumb) UI.crumb.style.display = ''; if (UI.searchInput && UI.searchInput.parentNode) { UI.searchInput.parentNode.style.display = S.trashMode ? 'none' : 'flex'; } if (UI.cntFolderFirst) { UI.cntFolderFirst.style.display = S.trashMode ? 'none' : 'flex'; } if (UI.btnAnalyze) { UI.btnAnalyze.style.display = (S.trashMode || S.shareMode || S.offlineMode) ? 'none' : 'flex'; } if (UI.lblGlobal) { UI.lblGlobal.style.display = (S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode || S.offlineMode || S.uploadMode || S.isFlattened || S.dupMode || S.analyzeMode) ? 'none' : 'flex'; if (S.trashMode && UI.chkGlobal) { UI.chkGlobal.checked = false; } if (isInVirtualSearch) { UI.chkGlobal.checked = true; } } S.dupMode = false; S.lastSelIdx = -1; if (!isAnalyzeSub) { if (S.analyzeMode && UI.chkSearchPath) UI.chkSearchPath.checked = false; S.analyzeMode = false; } if (UI.btnNewFolder) { UI.btnNewFolder.style.display = (S.trashMode || S.shareMode) ? 'none' : 'flex'; } S.isFlattened = false; renderCrumb(); if (folderId === 'root' && !S.preloaded) { if (typeof globalCache !== 'undefined' && globalCache.has('root')) { S.cache.set('root', globalCache.get('root')); S.preloaded = true; } else if (S.preLoadPromise) { let preLoadSuccess = false; try { const TIMEOUT_LIMIT = isTurbo ? 1000 : 200; const raceResult = await Promise.race([ S.preLoadPromise, new Promise(r => setTimeout(() => r('TIMEOUT'), TIMEOUT_LIMIT)) ]); if (globalCache.has('root') || raceResult !== 'TIMEOUT') { S.cache.set('root', globalCache.get('root')); S.preloaded = true; preLoadSuccess = true; } } catch(e) {} if (!preLoadSuccess) { console.log("[Load] Preload too slow or failed, forcing direct network fetch."); forceUpdate = true; } } else { forceUpdate = true; } } const syncStars = () => { for (let k = 0; k < S.items.length; k++) { const it = S.items[k]; if (it.starred || (it.tags && it.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(it.id); } } }; if (!forceUpdate) { let cachedData = null; if (S.cache.has(realCacheKey)) { cachedData = S.cache.get(realCacheKey); } else if (typeof globalCache !== 'undefined') { let lookupKey = realCacheKey; if (realCacheKey === 'root' && globalCache.has('')) lookupKey = ''; if (globalCache.has(lookupKey)) { cachedData = globalCache.get(lookupKey); S.cache.set(realCacheKey, cachedData); } } if (cachedData) { if (cachedData && !Array.isArray(cachedData) && cachedData.items) { S.items = [...cachedData.items]; nextToken = cachedData.nextToken; isResuming = !!nextToken; if (isResuming) S.items.forEach(it => fetchedIds.add(it.id)); } else { S.items = Array.isArray(cachedData) ? [...cachedData] : []; nextToken = null; } if (S.analyzeMode && S.analyzeMap) { S.items.forEach(item => { if (item.kind === 'drive#folder' && S.analyzeMap.has(item.id)) { item.size = S.analyzeMap.get(item.id).size.toString(); } }); } S.itemMap.clear(); for (let k = 0; k < S.items.length; k++) { S.itemMap.set(S.items[k].id, S.items[k]); } syncStars(); refresh(); updateStat(); if (S.offlineMode) { const session = globalCache.get('offline_session'); if (session && session.nextToken && !session.completed) { console.log(`[Resuming] Cache snapshot rendered. Continuing pagination from ${S.items.length}...`); isResuming = true; S.items.forEach(it => fetchedIds.add(it.id)); } else { setLoad(false); return; } } else if (!nextToken) { setLoad(false); if (window.pkSmartRefreshTrigger) { console.log(`[SWR] Intent detected: Validating ${realCacheKey} in background...`); window.pkSmartRefreshTrigger(true); } const visibleSubFolders = S.items.filter(f => f.kind === 'drive#folder'); for (let i = visibleSubFolders.length - 1; i >= 0; i--) { const sub = visibleSubFolders[i]; if (!scannedFolderIds.has(sub.id) && !globalCache.has(sub.id)) { backgroundQueue.push({ id: sub.id, name: sub.name, retryCount: 0 }); scannedFolderIds.add(sub.id); } } runBackgroundCrawler(); return; } else { console.log(`[Resuming] Cache snapshot rendered. Continuing pagination from ${S.items.length}...`); } } } if (!nextToken && !isResuming) { S.items = []; S.display = []; S.itemMap.clear(); refresh(); setLoad(true, true); updateLoadTxt(L.loading_detail); } isGUISensitive = false; const currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) { if (!isResuming) { setLoad(true); updateLoadTxt(L.str_waiting_token); } const isAuthReady = await waitForAuth(15000); if (!isAuthReady) console.warn("PikPak Master: Auth token wait timeout."); } if (!isResuming && el) { el.focus(); } UI.stopBtn.onclick = () => { S.abortController.abort(); if (S.loading && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.shareMode && !S.offlineMode && S.path.length > 1) { const canceledNode = S.path.pop(); console.log(`[Navigation] Cancelled entry to: ${canceledNode.name}, rolling back path.`); renderCrumb(); const parentNode = S.path[S.path.length - 1]; const parentKey = S.getRealCacheKey(parentNode.id); const cached = (typeof globalCache !== 'undefined') ? globalCache.get(parentKey) : null; if (cached) { S.items = Array.isArray(cached) ? [...cached] : (cached.items || []); ensureItemMap(); refresh(); } } setLoad(false); isGUISensitive = false; }; try { let pageCount = 0; const limit = 500; let totalItemsLoaded = S.items.length; do { if (currentId !== activeLoadId) return; if (signal.aborted) throw new DOMException('Aborted', 'AbortError'); const isStarredRoot = S.starredMode && S.path.length === 1; let data = {}; if (S.offlineMode) { if (pageCount > 0) break; if (!isResuming) { S.items = []; S.itemMap.clear(); fetchedIds.clear(); totalItemsLoaded = 0; } let session = isResuming ? globalCache.get('offline_session') : { phase: 'active', nextToken: null, completed: false }; if (!isResuming) globalCache.set('offline_session', session); const handleBatch = (batchTasks, nextToken, currentPhase) => { if (signal.aborted || currentId !== activeLoadId) return; const uniqueTasks = batchTasks.filter(t => !fetchedIds.has(t.id) && !S.itemMap.has(t.id)); if (uniqueTasks.length === 0) return; const newItems = uniqueTasks.map(t => { const ref = t.reference_resource || {}; return { id: t.id, kind: 'drive#task', name: ref.name || t.name || t.file_name || 'Untitled Task', size: t.file_size, phase: t.phase, progress: parseInt(t.progress || 0), message: t.message, icon_link: t.icon_link, thumbnail_link: ref.thumbnail_link ? ref.thumbnail_link : t.icon_link, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || '', file_id: t.file_id || '', source_url: (t.params && t.params.url) ? t.params.url : '', params: Object.assign({}, t.params || {}, ref.params || {}), mime_type: ref.mime_type || '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))) }; }); S.items.push(...newItems); newItems.forEach(f => { fetchedIds.add(f.id); S.itemMap.set(f.id, f); }); totalItemsLoaded = S.items.length; session.phase = currentPhase; session.nextToken = nextToken; session.completed = false; globalCache.set('offline_session', session); globalCache.set('offline_root', { items: [...S.items], fetchedIds: fetchedIds }); S.items.sort((a, b) => new Date(b.created_time || 0) - new Date(a.created_time || 0)); if (UI.loader.style.display !== 'none') setLoad(false); refresh(); updateStat(); }; await apiTaskList(500, handleBatch, session, signal); if (signal.aborted || currentId !== activeLoadId) return; const finalSession = { phase: 'done', nextToken: null, completed: true }; globalCache.set('offline_session', finalSession); data = { files: [], next_page_token: null }; break; } else if (S.historyMode && S.path.length === 1) { if (pageCount > 0) break; let keys = []; if (typeof GM_listValues !== 'undefined') { keys = GM_listValues(); } else { keys = Object.keys(localStorage); } const historyItems = []; const historyMap = new Map(); keys.forEach(k => { if (k.startsWith('pk_progress_')) { const id = k.replace('pk_progress_', ''); const raw = gmGet(k); let data = { t: 0, d: 0, ts: 0 }; if (typeof raw === 'number') { data.t = raw; data.ts = 0; } else if (typeof raw === 'object' && raw !== null) { data = raw; } if (data.t > 1 || data.ts > 0) { historyItems.push({ id, ...data }); historyMap.set(id, data); } } }); historyItems.sort((a, b) => b.ts - a.ts); const topItems = historyItems.slice(0, 1000); const targetIds = topItems.map(x => x.id); if (targetIds.length > 0) { const fetchDetail = async (id) => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${id}?thumbnail_size=SIZE_MEDIUM`, { headers: getHeaders() }); if (!res.ok) return null; const f = await res.json(); if (f && !f.trashed) return f; return null; } catch (e) { return null; } }; const validFiles = []; const CONCURRENCY = 6; for (let i = 0; i < targetIds.length; i += CONCURRENCY) { if (signal.aborted) break; const chunk = targetIds.slice(i, i + CONCURRENCY); const results = await Promise.all(chunk.map(id => fetchDetail(id))); results.forEach(rawF => { if (rawF) { const f = minifyFile(rawF); const local = historyMap.get(f.id) || {}; f._history_progress = local.t || 0; const cloudDur = f.params?.duration || 0; f._history_duration = cloudDur || local.d || 0; f._history_ts = local.ts || 0; validFiles.push(f); } else { } }); if (validFiles.length % 20 === 0) await sleep(10); } data = { files: validFiles, next_page_token: null }; } else { data = { files: [], next_page_token: null }; } } else if (S.recentMode && S.path.length === 1) { const filters = encodeURIComponent('{"phase":{"in":"PHASE_TYPE_COMPLETE"}}'); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?limit=${limit}&filters=${filters}&thumbnail_size=SIZE_MEDIUM&with_reference_resource=true&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const res = await fetch(url, { headers: getHeaders(), signal: signal }); if (!res.ok) throw new Error("Recent API " + res.status); const json = await res.json(); const validTasks = (json.tasks || []).filter(t => t.phase === 'PHASE_TYPE_COMPLETE' && (t.type === 'offline' || t.type === 'upload') && t.file_id !== "" ); data = { files: validTasks.map(t => { const ref = t.reference_resource || {}; const mime = ref.mime_type || ''; const isFolder = (ref.kind === 'drive#folder') || (mime === 'application/x-directory') || (t.icon_link && t.icon_link.includes('folder')); return { id: t.file_id || t.id, kind: isFolder ? 'drive#folder' : 'drive#file', name: ref.name || t.file_name || t.name, size: t.file_size, thumbnail_link: ref.thumbnail_link || t.icon_link || '', icon_link: t.icon_link || '', web_content_link: t.file_id ? null : null, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || t.created_time, mime_type: mime, parent_id: '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))), trashed: false, params: Object.assign({}, t.params || {}, ref.params || {}), _sourceTaskId: t.id }; }).filter(f => f.id && !fetchedIds.has(f.id)), next_page_token: json.next_page_token }; } else if (S.shareMode) { if (pageCount > 0) break; const shares = await apiShareList(); data = { files: shares, next_page_token: null }; } else if (S.uploadMode) { if (pageCount > 0) break; data = { files: [...S.uploadTasks], next_page_token: null }; } else { const isStarredRoot = S.starredMode && S.path.length === 1; const filterObj = { "trashed": { "eq": S.trashMode } }; if (isStarredRoot) { filterObj.trashed = { "eq": false }; filterObj.system_tag = { "in": "STAR" }; } else if (!S.trashMode) { filterObj.phase = { "eq": "PHASE_TYPE_COMPLETE" }; } const filters = `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`; const targetParentId = (S.trashMode || isStarredRoot) ? '*' : (cur.id || ''); const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&parent_id=${targetParentId}&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: 'high' }); if (currentId !== activeLoadId) return; if (!res.ok) { if (res.status === 429) { await sleep(1000); continue; } throw new Error("API " + res.status); } data = await res.json(); } if (data.files && data.files.length >= 0) { if (currentId !== activeLoadId) return; const currentPathId = S.path[S.path.length - 1]?.id || ''; if (S.trashMode !== snapshot.trash || S.shareMode !== snapshot.share || S.starredMode !== snapshot.starred || S.recentMode !== snapshot.recent || currentPathId !== snapshot.pathId) { console.warn("[Load Guard] Context Drift Detected. Aborting render."); return; } const hasDirtyData = data.files.some(f => f && (S.trashMode ? !f.trashed : f.trashed)); if (!S.shareMode && !S.offlineMode && hasDirtyData) { console.warn(L.log_dirty_data); return; } let validFiles = []; if (S.shareMode || S.offlineMode || S.uploadMode) { validFiles = data.files; } else { validFiles = data.files.map(f => minifyFile(f, false)); } const newUniqueFiles = validFiles.filter(f => !fetchedIds.has(f.id)); if (S.analyzeMode && S.analyzeMap) { newUniqueFiles.forEach(item => { if (item.kind === 'drive#folder' && S.analyzeMap.has(item.id)) { item.size = S.analyzeMap.get(item.id).size.toString(); } }); } if (newUniqueFiles.length > 0 || (pageCount === 0 && !isResuming)) { if (pageCount === 0 && !isResuming) { S.items = newUniqueFiles; S.itemMap.clear(); } else { S.items.push(...newUniqueFiles); } newUniqueFiles.forEach(f => { fetchedIds.add(f.id); S.itemMap.set(f.id, f); }); totalItemsLoaded = S.items.length; if (S.recentMode && S.path.length === 1) { S.recentResultItems = [...S.items]; } if (S.offlineMode) { S.cache.set(realCacheKey, [...S.items]); if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, [...S.items]); } else { const tempCache = { items: [...S.items], nextToken: data.next_page_token }; S.cache.set(realCacheKey, tempCache); if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, tempCache); } if (pageCount === 0 || UI.loader.style.display !== 'none') { refresh(); if (UI.loader.style.display !== 'none') { setLoad(false); } if (S.items.length === 0) { UI.in.innerHTML = ''; renderVisible(); } } else { if (pageCount % 4 === 0) refresh(); } } } nextToken = data.next_page_token; pageCount++; if (currentId === activeLoadId) { UI.stat.textContent = `${L.status_ready.replace('{n}', totalItemsLoaded)} (${L.loading_fetch.replace('{n}', pageCount)})`; } if (nextToken) await sleep(0); } while (nextToken && currentId === activeLoadId); if (currentId === activeLoadId) { if (S.trashMode && S.items.length === 0 && pageCount > 1) { } else { S.cache.set(realCacheKey, [...S.items]); } if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, [...S.items]); if (window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(); } indexParents(folderId, cur.name, S.items); S.itemMap.clear(); for (let k = 0; k < S.items.length; k++) S.itemMap.set(S.items[k].id, S.items[k]); syncStars(); if (folderId === 'root' && !S.preloaded) S.preloaded = true; const visibleSubFolders = S.items.filter(f => f.kind === 'drive#folder'); for (let i = visibleSubFolders.length - 1; i >= 0; i--) { const sub = visibleSubFolders[i]; if (!scannedFolderIds.has(sub.id) && !globalCache.has(sub.id)) { backgroundQueue.push({ id: sub.id, name: sub.name, retryCount: 0 }); scannedFolderIds.add(sub.id); } } isGUISensitive = false; runBackgroundCrawler(); refresh(); updateStat(); setLoad(false); } } catch (e) { isGUISensitive = false; if (currentId === activeLoadId) { if (!S._isRetrying) setLoad(false); if (e.name === 'AbortError') { console.log("Load aborted by user."); } else { console.error("API Error encountered:", e); if (typeof resetHeaderCache === 'function') resetHeaderCache(); const isAuthError = e.message.includes('401') || e.message.includes('403') || e.message.includes('400') || e.message.includes('CAPTCHA'); const isNotFoundError = e.message.includes('404'); const isNetworkError = e.name === 'TypeError' || e.message.includes('Failed to fetch') || e.message.includes('NetworkError'); if (!S._isRetrying || isNetworkError) { S._isRetrying = true; setLoad(true); if (isAuthError) { resetHeaderCache(); updateLoadTxt(L.str_waiting_token); await sleep(2000); try { const h = getHeaders(); if (!h.Authorization || h.Authorization.length < 10) { location.reload(); return; } const testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token still rejected by server, forcing page reload to recover..."); location.reload(); return; } } catch(testErr) {} console.log("[Auth] Auth state recovered, resuming load..."); load(false, true).finally(() => { S._isRetrying = false; }); return; } if (isNotFoundError) { if (S.path.length > 1 || S.path[0].id !== '') { S.path = [{ id: '', name: L.btn_nav_home }]; load(false, true).finally(() => { S._isRetrying = false; }); return; } } if (isNetworkError) { const delay = 1000 + Math.random() * 2000; const retryMsg = L.msg_network_unstable; updateLoadTxt(retryMsg); console.warn(`[Network] Connection drop detected. Retrying in ${Math.round(delay)}ms...`); await sleep(delay); load(false, true).finally(() => { }); return; } S._isRetrying = false; } if (S.items.length === 0) { setLoad(false); showAlert(L.str_failed + " " + e.message); } } } } } async function refresh() { S.sortId++; const currentReqId = S.sortId; if (!S.dupMode) S.dupItemMap = null; const isStrictVirtual = S.isFlattened || S.dupMode || S.analyzeMode; const cur = S.path[S.path.length - 1]; const isInSearchContext = S.path.some(p => p.id === 'virtual_search_root'); const shouldHideHeavyOps = isStrictVirtual || isInSearchContext || S.offlineMode || S.uploadMode; if (UI.btnFolderFirst) { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isAnalyzeSub = S.analyzeMode && cur.id !== 'analyze_root'; const shouldHideFF = S.isFlattened || S.dupMode || isAnalyzeRoot; if (UI.btnFolderFirst) { UI.btnFolderFirst.style.display = shouldHideFF ? 'none' : 'flex'; if (isAnalyzeSub) S.renderFolderFirst(); } } if (UI.btnAnalyze) { UI.btnAnalyze.style.display = (shouldHideHeavyOps || S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode) ? 'none' : 'flex'; } if (UI.btnExport) { UI.btnExport.style.display = (shouldHideHeavyOps || S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode) ? 'none' : 'flex'; } if (UI.scan) { UI.scan.style.display = (shouldHideHeavyOps || S.trashMode || S.shareMode || S.starredMode || S.recentMode || S.historyMode) ? 'none' : 'flex'; } if (UI.btnExit) { UI.btnExit.style.display = isStrictVirtual ? 'flex' : 'none'; } if (UI.lblSearchPath) { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; UI.lblSearchPath.style.display = (S.dupMode || S.isFlattened || isAnalyzeRoot) ? 'flex' : 'none'; if (UI.btnAnaSelect) UI.btnAnaSelect.style.display = (isAnalyzeRoot && !S.search && S.analyzeSimGroups) ? 'flex' : 'none'; } if (UI.filterBar) { if (S.isFlattened && !S.dupMode) { UI.filterBar.style.display = 'flex'; if (S.filterState.active) { UI.filterBtn.style.display = 'none'; UI.filterActiveUI.style.display = 'flex'; if (typeof renderActiveFilterUI === 'function') renderActiveFilterUI(); } else { UI.filterBtn.style.display = 'flex'; UI.filterActiveUI.style.display = 'none'; } } else { UI.filterBar.style.display = 'none'; } } if (UI.cntFolderFirst) { } const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; if (UI.btnNewFolder) { const isNewFolderBlocked = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || isStarredRoot || isRecentRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; UI.btnNewFolder.style.display = isNewFolderBlocked ? 'none' : (S.trashMode ? 'none' : 'flex'); } if (UI.btnPaste) { const isPasteBlocked = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || isStarredRoot || isRecentRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; UI.btnPaste.style.display = isPasteBlocked ? 'none' : (S.trashMode ? 'none' : 'inline-flex'); } const isInVirtualSearch = S.path.some(p => p.id === 'virtual_search_root'); const isInsideSearchResult = isInVirtualSearch && cur.id !== 'virtual_search_root'; let listToProcess = []; if (S.search && !S.dupMode && !isInsideSearchResult) { const q = S.search.toLowerCase(); const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; if (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups) { const newDisplay = []; S.analyzeSimGroups.forEach((g, gIdx) => { const groupItems = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); const isGroupHit = g.type.toLowerCase().includes(q) || groupItems.some(it => { const nameMatch = it.name.toLowerCase().includes(q); let pathMatch = false; if (includePath && it._pathStr) { const homeText = L.btn_nav_home; const parentPath = (it._pathStr === homeText || it._pathStr.startsWith(homeText + '/')) ? it._pathStr : (homeText + '/' + it._pathStr); const fullItemPath = parentPath.endsWith('/') ? (parentPath + it.name) : (parentPath + '/' + it.name); pathMatch = fullItemPath.toLowerCase().includes(q); } return nameMatch || pathMatch; }); if (isGroupHit) { newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: g.type, count: g.ids.length, type: g.type || L.str_group }); groupItems.sort((a, b) => parseInt(b.size || 0) - parseInt(a.size || 0)); groupItems.forEach(it => { if (it.name.toLowerCase().includes(q)) { const name = it.name; const idx = name.toLowerCase().indexOf(q); const len = q.length; const start = Math.max(0, idx - 15); const end = Math.min(name.length, idx + len + 30); let pre = start > 0 ? "..." : ""; let suf = end < name.length ? "..." : ""; it._hlNameHTML = `${pre}${esc(name.substring(start, idx))}<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">${esc(name.substring(idx, idx + len))}</b>${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)}<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">${esc(m)}</b>${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)}<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">${esc(partMatch)}</b>${esc(partAfter)}${suffix}`; } }); S.display = filtered; } } else { if (S.path.length > 0 && S.path[0].id === 'starred') { S.display = S.items.filter(i => (i.starred || (i.tags && i.tags.some(t => t.name === 'STAR'))) && !i.trashed); } else if (S.isFlattened && S.filterState && S.filterState.active && S.filterState.cat !== 'all') { const cat = S.filterState.cat; const ext = S.filterState.ext; const fExts = { video: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mpg','mpeg','rm','rmvb','asf','vob','dat','divx','f4v','m2ts','mts','tp','trp','ogv','mpe','m2v','m3u8'], audio: ['mp3','wav','flac','aac','ogg','wma','ape','m4a','amr','opus','m4b','alac','aiff','mid','midi','ra','dts','ac3','dsf','dff'], image: ['jpg','jpeg','png','gif','bmp','webp','svg','tif','tiff','ico','heic','heif','raw','cr2','nef','arw','dng','orf','avif','psd','ai','eps','jfif','jpe'], document: ['txt','html','pdf','pptx','chm','docx','xlsx','htm','doc','dwg','mdb','ppt','xls','rtf','odt','ods','odp','epub','mobi','azw3','djvu','cbz','cbr','md','log','csv','xml','json'], software: ['apk','exe','ipa','dmg','rpm','deb','msi','pkg','xapk','apks','aab','jar','bin','sh','bat','cmd'], archive: ['zip','rar','7z','tar','gz','iso','cab','bz2','xz','tgz','wim','esd','img','zst','lzh'], torrent: ['torrent'] }; let matchExts =[]; if (cat === 'other') { const definedExts = new Set([...fExts.video, ...fExts.audio, ...fExts.image, ...fExts.document, ...fExts.software, ...fExts.archive, ...fExts.torrent]); S.display = S.items.filter(i => { if (i.kind === 'drive#folder') return false; const n = (i.name || '').toLowerCase(); const e = n.split('.').pop(); return !definedExts.has(e); }); } else { if (ext === 'all') { matchExts = fExts[cat] ||[]; } else { matchExts = [ext]; } S.display = S.items.filter(i => { if (i.kind === 'drive#folder') return false; const n = (i.name || '').toLowerCase(); const e = n.split('.').pop(); return matchExts.includes(e); }); } } else if (S.offlineMode) { const cfg = S.offlineFilters; S.display = S.items.filter(i => { const p = i.phase; if (p === 'PHASE_TYPE_COMPLETE') return cfg.complete; if (['PHASE_TYPE_RUNNING', 'PHASE_TYPE_PENDING', 'PHASE_TYPE_PAUSED'].includes(p)) return cfg.running; return cfg.failed; }); } else if (S.uploadMode) { const cfg = S.uploadFilters; S.display = S.items.filter(i => { const s = i.status; if (s === 'DONE') return cfg.complete; if (s === 'PAUSED' || s === 'ERROR') return cfg.paused; return cfg.running; }); } else if (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups) { const newDisplay =[]; S.analyzeSimGroups.forEach((g, gIdx) => { newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: g.type, count: g.ids.length, type: g.type || L.str_group }); const groupItems = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); groupItems.sort((a, b) => { const pA = a._pathStr || ""; const pB = b._pathStr || ""; if (pA.length !== pB.length) return pA.length - pB.length; return pA.localeCompare(pB); }); let lastPathInGroup = null; groupItems.forEach(it => { const currentPath = it._pathStr || ""; if (currentPath === lastPathInGroup && currentPath !== "") { it._isSameFolder = true; } else { it._isSameFolder = false; lastPathInGroup = currentPath; } newDisplay.push(it); }); }); S.display = newDisplay; } else { S.display = [...S.items]; } } if (currentReqId !== S.sortId) return; if (S.sel.size > 0) { const visibleSet = new Set(S.display.map(i => i.id)); Array.from(S.sel).forEach(id => { if (!visibleSet.has(id)) S.sel.delete(id); }); } S.dupReasons.clear(); S.dupGroups.clear(); if (!S.dupMode) S.pinnedDupPath = null; const isStandardView = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!cur.id.startsWith('virtual_') || cur.id === 'virtual_search_root'); const isSortIndep = gmGet('pk_sort_independent', false); if (isStandardView) { const folderId = cur.id || 'root'; if (S._sortAppliedForId !== folderId) { if (isSortIndep) { try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); if (prefStore[folderId]) { S.sort = prefStore[folderId].sort; S.dir = prefStore[folderId].dir; S.folderFirst = prefStore[folderId].folderFirst === true; } else { S.sort = 'modified_time'; S.dir = 1; S.folderFirst = false; } } catch(e) {} } else { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); S.sort = globalPref.sort; S.dir = globalPref.dir; if (globalPref.folderFirst !== undefined) S.folderFirst = globalPref.folderFirst; else S.folderFirst = gmGet('pk_folder_first', false); } S._sortAppliedForId = folderId; S._comicApplied = false; if(S.renderFolderFirst) S.renderFolderFirst(); } if (gmGet('pk_comic_mode', false) && !S._comicApplied) { const hasSubFolders = S.items.some(i => i.kind === 'drive#folder'); if (!hasSubFolders && S.items.length > 0) { const isAllImages = S.items.every(i => i.mime_type && i.mime_type.startsWith('image/')); const isAllVideos = S.items.every(i => i.mime_type && i.mime_type.startsWith('video/')); if (isAllImages || isAllVideos) { S.sort = 'name'; S.dir = 1; } S._comicApplied = true; } } } const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isGlobalSearchRoot = cur.id === 'virtual_search_root' && UI.chkGlobal && UI.chkGlobal.checked; const supportsPathSort = S.dupMode || isAnalyzeRoot || S.isFlattened || isGlobalSearchRoot; if (S.sort === 'path' && !supportsPathSort) { S.sort = 'modified_time'; S.dir = 1; } if (S.dupMode) { setLoad(true); S.dupRunning = true; UI.stopBtn.onclick = () => { S.dupRunning = false; }; updateLoadTxt(L.loading_dup.replace('{p}', 0)); await sleep(20); const cfg = S.dupConfig || { video: true, image: false, other: false }; let candidates = S.display.filter(i => { if (!i.mime_type) return false; const isVideo = i.mime_type.startsWith('video'); const isImage = i.mime_type.startsWith('image'); const isOther = !isVideo && !isImage; if (isVideo && cfg.video) return true; if (isImage && cfg.image) return true; if (isOther && cfg.other) return true; return false; }); const groups = await computeDuplicateGroups(candidates, cfg, () => S.dupRunning); groups.sort((a, b) => { const getPriority = (t) => { if (t === L.tag_hash) return 3; if (t === L.tag_sim) return 2; if (t === L.tag_name) return 1; return 0; }; const pA = getPriority(a.type); const pB = getPriority(b.type); if (pA !== pB) return pB - pA; return b.ids.length - a.ids.length; }); if (groups.length === 0) { S.dupRunning = false; setLoad(false); showToast(L.msg_dup_none); if (UI.btnExit) UI.btnExit.click(); return; } S.dupRawGroups = groups; if (UI.chkName) UI.chkName.checked = true; if (UI.chkSim) UI.chkSim.checked = true; if (UI.chkHash) UI.chkHash.checked = true; const applyDupSelection = () => { const targetPath = UI.selDupFolder.value; const invertChk = document.getElementById('pk-dup-invert'); if (!targetPath || targetPath === "__RESET__") { S.pinnedDupPath = null; S.sel.clear(); if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } renderDupView(); updateStat(); if(targetPath) UI.selDupFolder.value = ""; return; } if (invertChk) { invertChk.disabled = false; invertChk.parentNode.style.opacity = '1'; } const isInvert = invertChk ? invertChk.checked : false; S.pinnedDupPath = targetPath; const itemMap = new Map(); for(const item of S.items) itemMap.set(item.id, item); S.sel.clear(); S.dupRawGroups.forEach(g => { const filesInTarget = []; const filesInOther = []; g.ids.forEach(id => { const item = itemMap.get(id); if (item) { let path = ""; if (item._cachedPath !== undefined) { path = item._cachedPath; } else { if (item._lineage && item._lineage.length > 0) { path = item._lineage.map(p => p.name).join('/'); } else { const curFolder = S.path[S.path.length - 1]; path = curFolder ? curFolder.name : L.current_dir; } item._cachedPath = path; } if (path === targetPath) filesInTarget.push(item); else filesInOther.push(item); } }); if (filesInOther.length > 0) { if (isInvert) { filesInOther.forEach(f => S.sel.add(f.id)); } else { filesInTarget.forEach(f => S.sel.add(f.id)); } } else if (filesInTarget.length > 1) { for (let k = 1; k < filesInTarget.length; k++) S.sel.add(filesInTarget[k].id); } }); renderDupView(); updateStat(); }; UI.selDupFolder.onchange = applyDupSelection; setTimeout(() => { const invertChk = document.getElementById('pk-dup-invert'); if (invertChk) { invertChk.onchange = () => { if (UI.selDupFolder.value && UI.selDupFolder.value !== "__RESET__") { applyDupSelection(); } }; } const smartBtn = document.getElementById('pk-dup-smart-btn'); if (smartBtn) { smartBtn.addEventListener('click', () => { if (S.pinnedDupPath) { S.pinnedDupPath = null; UI.selDupFolder.value = ""; if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } renderDupView(); } }, { capture: true }); } }, 0); S.dupRunning = false; setLoad(false); if (UI.dupTools) UI.dupTools.style.display = 'flex'; if (UI.dupFilters) { UI.dupFilters.style.display = 'flex'; const simChkWrapper = UI.dupFilters.querySelector('#pk-chk-sim')?.closest('.pk-dup-chk'); if (simChkWrapper) { simChkWrapper.style.display = S.dupConfig.video ? 'flex' : 'none'; } } renderDupView(); } else { if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (S.uploadMode || (S.analyzeMode && S.analyzeSimGroups && cur.id === 'analyze_root')) { renderList(); if (gmGet('pk_keep_pos', false) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { UI.vp.scrollTop = Math.max(0, (targetIdx * CONF.rowHeight) - (UI.vp.clientHeight / 2) + (CONF.rowHeight / 2)); S.activeId = S.latestChildId; S.sel.clear(); renderVisible(); } S.latestChildId = null; } updateStat(); return; } if (S.display.length > 5000 && !S.offlineMode) { setLoad(true); updateLoadTxt(L.str_sorting); await sleep(10); } const sortedList = await new Promise(resolve => { const isRoot = S.path.length === 1 && S.path[0].id === ''; const proxyList = new Array(S.display.length); const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; const isStarred = S.starredSet.has(item.id) || !!(item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))); const isMyPack = item._isSystemRoot || (!S.trashMode && item.kind === 'drive#folder' && ( (item.id === '' || item.id === 'root') || (isRoot && item.name === CONF.SYSTEM_FOLDER_NAME) )); const ext = item.name.split('.').pop().toLowerCase(); const isVid = ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'ts', 'm4v', '3gp'].includes(ext); const isImg = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff', 'ico'].includes(ext); const mimeGroup = isVid ? 1 : (isImg ? 2 : 3); let finalDur = S.durationMap.get(item.id) || parseFloat(item.params?.duration || 0); if (finalDur > 0 && item.params) item.params.duration = finalDur; let pathStr = ""; if (S.analyzeMode) pathStr = item._pathStr || ""; else if (item._lineage) pathStr = item._lineage.map(x=>x.name).join('/'); proxyList[i] = { i: i, n: item.name, k: item.kind === 'drive#folder' ? 1 : 0, s: item.size || 0, t: item.modified_time, d: finalDur, st: isStarred ? 1 : 0, mp: isMyPack ? 1 : 0, m: mimeGroup, p: pathStr, ct: item.created_time, pt: item._history_ts || 0, v: parseInt(item.view_count || 0), sc: parseInt(item.save_count || 0), ss: item.share_status || 'OK', ed: parseInt(item.expiration_days || -1), lc: parseInt(item.limit_count || 0) }; } const workerCode = ` self.onmessage = function(e) { const { proxy, sort, dir, reqId, folderFirst, locale } = e.data; const parseSize = n => parseInt(n || 0); const parseTime = t => t ? new Date(t).getTime() : 0; const collator = new Intl.Collator(locale, { numeric: true, sensitivity: 'base', caseFirst: 'lower' }); const getCharWeight = (str) => { if (!str) return 0; const c = str.charAt(0); if (/[0-9]/.test(c)) return 20; if (/[\\u4e00-\\u9fa5]/.test(c)) return 30; if (/[a-zA-Z]/.test(c)) return 40; return 10; }; const compareNames = (nameA, nameB) => { const rankA = getCharWeight(nameA); const rankB = getCharWeight(nameB); if (rankA !== rankB) { return rankA - rankB; } return collator.compare(nameA, nameB); }; proxy.sort((a, b) => { if (a.mp !== b.mp) return b.mp - a.mp; if (folderFirst && a.k !== b.k) { return b.k - a.k; } if (sort === 'view_count' || sort === 'save_count') { const valA = sort === 'view_count' ? (a.v || 0) : (a.sc || 0); const valB = sort === 'view_count' ? (b.v || 0) : (b.sc || 0); if (valA !== valB) return (valB - valA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'share_status') { const getStatusRank = (item) => { if (item.lc > 0 && item.sc >= item.lc) return 5; if (item.ss === 'DELETED') return 4; if (item.ss === 'EXPIRED') return 3; if (item.ed === -1) return 1; return 2; }; const rA = getStatusRank(a), rB = getStatusRank(b); if (rA !== rB) return (rA - rB) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'modified_time') { const tA = parseTime(a.t), tB = parseTime(b.t); if (tA !== tB) return (tB - tA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'created_time') { const tA = parseTime(a.ct), tB = parseTime(b.ct); if (tA !== tB) return (tB - tA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'play_time') { if (a.pt !== b.pt) return (b.pt - a.pt) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'path') { const pathCmp = compareNames(a.p, b.p); if (pathCmp !== 0) return pathCmp * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'size') { const sizeA = parseSize(a.s), sizeB = parseSize(b.s); if (sizeA !== sizeB) return (sizeB - sizeA) * dir; return compareNames(a.n, b.n) * dir; } if (sort === 'duration') { if (a.k !== b.k) return folderFirst ? (b.k - a.k) : (a.k - b.k); if (a.k) return compareNames(a.n, b.n) * dir; const hasDurA = a.d > 0; const hasDurB = b.d > 0; if (hasDurA !== hasDurB) return hasDurA ? -1 : 1; if (hasDurA) { if (a.d !== b.d) return (b.d - a.d) * dir; return compareNames(a.n, b.n) * dir; } const isVidA = (a.m === 1); const isVidB = (b.m === 1); if (isVidA !== isVidB) return isVidA ? -1 : 1; if (isVidA) { return compareNames(a.n, b.n); } else { const getExt = s => { const i = s.lastIndexOf('.'); return i > 0 ? s.slice(i).toLowerCase() : ''; }; const extA = getExt(a.n); const extB = getExt(b.n); const extCmp = compareNames(extA, extB); if (extCmp !== 0) { return extCmp * dir; } return compareNames(a.n, b.n) * dir; } } if (sort === 'starred') { if (a.st !== b.st) return b.st - a.st; return (parseTime(b.t) - parseTime(a.t)) * dir; } return compareNames(a.n, b.n) * dir; }); const sortedIndices = new Uint32Array(proxy.length); for (let i = 0; i < proxy.length; i++) sortedIndices[i] = proxy[i].i; self.postMessage({ indices: sortedIndices, reqId: reqId }, [sortedIndices.buffer]); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); const sortWorker = new Worker(workerUrl); sortWorker.onmessage = (e) => { const { indices, reqId } = e.data; URL.revokeObjectURL(workerUrl); sortWorker.terminate(); if (reqId === S.sortId) { resolve(Array.from(indices).map(idx => S.display[idx])); } else resolve(null); }; const sortLocale = ({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en'})[getLang()] || 'en'; sortWorker.postMessage({ proxy: proxyList, sort: S.sort, dir: S.dir, reqId: currentReqId, folderFirst: S.folderFirst, locale: sortLocale }); }); if (S.display.length > 5000 && !S.offlineMode) setLoad(false); if (sortedList === null) return; S.display = sortedList; } if (currentReqId !== S.sortId) return; if (S.sel.size > 0) { const currentIds = new Set(S.display.map(i => i.id)); for (let id of S.sel) if (!currentIds.has(id)) S.sel.delete(id); } renderList(); if (gmGet('pk_keep_pos', false) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { const rowTop = targetIdx * CONF.rowHeight; const vpHeight = UI.vp.clientHeight; const centerScroll = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); UI.vp.scrollTop = centerScroll; S.activeId = S.latestChildId; S.sel.clear(); renderVisible(); } S.latestChildId = null; } updateStat(); } const getCommonPathPrefix = (pathStrings) => { if (!pathStrings || pathStrings.length < 2) return ""; const splitPaths = pathStrings.map(p => p.split('/')); const firstPath = splitPaths[0]; let commonLen = 0; for (let i = 0; i < firstPath.length; i++) { const segment = firstPath[i]; const isAllSame = splitPaths.every(p => p[i] === segment); if (isAllSame) { commonLen++; } else { break; } } if (commonLen <= 1) return ""; return firstPath.slice(0, commonLen).join('/'); }; function renderDupView() { if (!S.dupMode) return; const L = getStrings(); const showName = UI.chkName.checked; const showSim = UI.chkSim.checked; const showHash = UI.chkHash.checked; const newDisplay = []; if (!S.dupItemMap || S.dupItemMap.size !== S.items.length) { S.dupItemMap = new Map(); const len = S.items.length; for (let i = 0; i < len; i++) { S.dupItemMap.set(S.items[i].id, S.items[i]); } } const itemMap = S.dupItemMap; const getCachedPath = (item) => { if (item._cachedPath !== undefined) return item._cachedPath; let pStr = ""; if (item._lineage && item._lineage.length > 0) { pStr = item._lineage.map(node => node.name).join('/'); } else { const curFolder = S.path[S.path.length - 1]; pStr = curFolder ? curFolder.name : L.current_dir; } item._cachedPath = pStr; return pStr; }; S.dupGroups.clear(); if (S.dupRawGroups && S.dupRawGroups.length > 0) { const activeRawGroups =[]; for (const g of S.dupRawGroups) { const validIds = g.ids.filter(id => itemMap.has(id)); if (validIds.length > 1) { activeRawGroups.push({ ...g, ids: validIds }); } } S.dupRawGroups = activeRawGroups; } if (S.dupRawGroups.length === 0 && !S.dupRunning) { if (UI.btnExit) { UI.btnExit.click(); } else { S.dupMode = false; S.isFlattened = false; refresh(); } return; } let groupsToRender = S.dupRawGroups; if (S.pinnedDupPath) { const pinnedGroups = []; const normalGroups = []; for (const g of S.dupRawGroups) { let isPinned = false; for (const id of g.ids) { const item = itemMap.get(id); if (item) { const path = getCachedPath(item); if (path === S.pinnedDupPath) { isPinned = true; break; } } } if (isPinned) pinnedGroups.push(g); else normalGroups.push(g); } groupsToRender = [...pinnedGroups, ...normalGroups]; } const dynamicFolderStats = new Map(); const groupsLen = groupsToRender.length; const searchKey = S.search ? S.search.toLowerCase().trim() : null; const includePath = S.search && UI.chkSearchPath ? UI.chkSearchPath.checked : false; for (let gIdx = 0; gIdx < groupsLen; gIdx++) { const g = groupsToRender[gIdx]; if (g.type === L.tag_name && !showName) continue; if (g.type === L.tag_sim && !showSim) continue; if (g.type === L.tag_hash && !showHash) continue; const ids = g.ids; const idsLen = ids.length; for (let k = 0; k < idsLen; k++) { S.dupGroups.set(ids[k], gIdx); } const groupItems = []; const groupPaths = []; for (let k = 0; k < idsLen; k++) { const item = itemMap.get(ids[k]); if (item) { groupItems.push(item); groupPaths.push(getCachedPath(item)); } } if (searchKey) { const isMatch = groupItems.some((item, idx) => { const nameHit = item.name.toLowerCase().includes(searchKey); let pathHit = false; if (includePath) { const rawPath = groupPaths[idx] || ""; const homeText = L.btn_nav_home; const parentPath = (rawPath === homeText || rawPath.startsWith(homeText + '/')) ? rawPath : (homeText + '/' + rawPath); const fullItemPath = parentPath.endsWith('/') ? (parentPath + (item.name || "")) : (parentPath + '/' + (item.name || "")); pathHit = fullItemPath.toLowerCase().includes(searchKey); } return nameHit || pathHit; }); if (!isMatch) continue; } const pathsLen = groupPaths.length; for (let p = 0; p < pathsLen; p++) { const pStr = groupPaths[p]; dynamicFolderStats.set(pStr, (dynamicFolderStats.get(pStr) || 0) + 1); } const firstItem = itemMap.get(ids[0]); let featureStr = ""; if (firstItem) { if (g.type === L.tag_hash) { featureStr = `[ ${fmtSize(firstItem.size)} ] `; } else if (g.type === L.tag_sim) { const dur = firstItem.params?.duration || 0; featureStr = `[ ${dur > 0 ? fmtDur(dur) : '--:--'} ] `; } else if (g.type === L.tag_name) { featureStr = `[ ${L.tag_name_short} ] `; } } const baseName = firstItem ? (g.type === L.tag_name ? firstItem.name.replace(/\.[^/.]+$/, "") : firstItem.name) : `${L.str_group} ${gIdx}`; const countStr = `${idsLen} ${L.str_items}`; const smartTitle = `${countStr} | ${featureStr}${baseName}`; newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: smartTitle, count: idsLen, type: g.type || L.str_group }); const sortedGroupData = groupItems.map((it, idx) => ({ it, path: groupPaths[idx] })).sort((a, b) => { if (a.path.length !== b.path.length) return a.path.length - b.path.length; return a.path.localeCompare(b.path); }); let lastFullPath = null; for (let idx = 0; idx < sortedGroupData.length; idx++) { const { it: item, path: fullPath } = sortedGroupData[idx]; if (idx > 0 && fullPath === lastFullPath) { item._isSameFolder = true; item._dupFullPath = L.str_same_folder; } else { item._isSameFolder = false; item._dupFullPath = fullPath; } lastFullPath = fullPath; newDisplay.push(item); } } UI.selDupFolder.style.width = "220px"; UI.selDupFolder.style.flexShrink = "0"; const currentSelection = UI.selDupFolder.value; const invertChk = document.getElementById('pk-dup-invert'); const invertLabel = invertChk ? invertChk.parentNode : null; if (dynamicFolderStats.size <= 1) { UI.selDupFolder.style.display = 'none'; if (invertLabel) invertLabel.style.display = 'none'; } else { UI.selDupFolder.style.display = 'inline-block'; if (invertLabel) invertLabel.style.display = 'inline-flex'; } let dropdownHtml = `<option value="" disabled selected style="display:none">${L.lbl_dup_select_folder}</option>`; if (dynamicFolderStats.size > 0) { dropdownHtml += `<option value="__RESET__" style="font-weight:bold;color:var(--pk-pri);">${L.lbl_dup_reset}</option>`; } const truncateMiddle = (str, len = 30) => { if (!str || str.length <= len * 2 + 3) return str; return str.substring(0, len) + ' ... ' + str.substring(str.length - len); }; const sortLocale = ({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en'})[getLang()] || 'en'; const collator = new Intl.Collator(sortLocale, { numeric: true }); const sortedFolders = Array.from(dynamicFolderStats.entries()) .sort((a, b) => { const arrA = a[0].split('/'); const arrB = b[0].split('/'); const len = Math.min(arrA.length, arrB.length); for (let i = 0; i < len; i++) { const cmp = collator.compare(arrA[i], arrB[i]); if (cmp !== 0) return cmp; } return arrA.length - arrB.length; }); const optionsArr = sortedFolders.map(([path, count]) => { return `<option value="${esc(path)}" title="${esc(path)}">${esc(truncateMiddle(path, 35))} ${L.fmt_dup_count.replace('{n}', count)}</option>`; }); dropdownHtml += optionsArr.join(''); UI.selDupFolder.innerHTML = dropdownHtml; if (currentSelection && dynamicFolderStats.has(currentSelection)) { UI.selDupFolder.value = currentSelection; } else if (currentSelection === "__RESET__") { UI.selDupFolder.value = "__RESET__"; } else { if (S.pinnedDupPath && !dynamicFolderStats.has(S.pinnedDupPath)) { S.pinnedDupPath = null; } } S.display = newDisplay; const visibleIds = new Set(newDisplay.map(d => d.id)); for(let id of S.sel) { if (!visibleIds.has(id)) S.sel.delete(id); } if (UI.chkAll) UI.chkAll.checked = false; renderList(); updateStat(); } function renderList() { UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; let colDef; const cur = S.path[S.path.length - 1]; const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isGlobalSearchRoot = cur.id === 'virtual_search_root' && UI.chkGlobal && UI.chkGlobal.checked; const showPathCol = S.dupMode || isAnalyzeRoot || S.isFlattened || isGlobalSearchRoot; const isMax = UI.win.classList.contains('pk-maximized'); if (S.offlineMode) { colDef = "36px 1fr 120px 240px 180px"; } else if (S.uploadMode) { colDef = "36px 1fr 100px 120px 180px"; } else if (S.shareMode) { colDef = isMax ? "36px 1fr 80px 80px 120px 130px" : "36px 1fr 80px 80px 80px 130px"; } else if (S.historyMode) { colDef = "36px 30px 1fr 80px 80px 120px 160px"; } else if (S.trashMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (S.recentMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (isAnalyzeRoot) { colDef = "36px 30px 2fr 1fr 80px 130px"; } else if (showPathCol) { colDef = "36px 30px 2fr 1fr 80px 105px 130px"; } else { colDef = "36px 30px 1fr 80px 105px 130px"; } const hd = UI.win.querySelector('.pk-grid-hd'); if (hd) { hd.style.gridTemplateColumns = colDef; if (S.offlineMode) { if (!hd.querySelector('[data-k="offline_status"]')) { hd.innerHTML = ` <div><input type="checkbox" id="pk-all"></div> <div class="pk-col" data-k="name" style="justify-content:flex-start;">${L.col_name}<span></span></div> <div class="pk-col" data-k="size" style="justify-content:flex-start;">${L.col_size}<span></span></div> <div class="pk-col" data-k="offline_status" style="justify-content:flex-start;">${L.col_task_status}<span></span></div> <div class="pk-col" data-k="offline_progress" style="justify-content:flex-start;">${L.col_task_progress}<span></span></div> `; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (S.historyMode) { if (!hd.querySelector('[data-k="play_time"]')) { hd.innerHTML = ` <div><input type="checkbox" id="pk-all"></div> <div class="pk-col" data-k="starred" style="justify-content:center;"> <svg width="16" height="16" viewBox="0 0 1024 1024" style="margin-top:-2px;"><path d="M953.107692 425.353846c3.938462-19.692308-11.815385-39.384615-31.507692-43.323077l-259.938462-39.384615-118.153846-244.184616c-3.938462-7.876923-7.876923-11.815385-15.753846-15.753846-7.876923-3.938462-15.753846-3.938462-19.692308-3.938461h-3.938461c-3.938462 0-7.876923 3.938462-11.815385 3.938461 0 0-3.938462 0-3.938461 3.938462-3.938462 3.938462-3.938462 7.876923-7.876923 11.815384v3.938462l-118.153846 244.184615-259.938462 39.384616c-3.938462 0-7.876923 3.938462-11.815385 3.938461 0 0-3.938462 0-3.938461 3.938462 0 0-3.938462 0-3.938461 3.938461v3.938462c0 3.938462-3.938462 3.938462-3.938461 7.876923 0 0 0 3.938462-3.938462 3.938462v15.753846c0 3.938462 0 3.938462 3.938462 7.876923 0 3.938462 3.938462 7.876923 7.876923 11.815384L275.692308 638.030769 232.369231 905.846154V917.661538c0 3.938462 0 3.938462 3.938461 7.876924v3.938461c0 3.938462 3.938462 3.938462 7.876923 7.876923l3.938462 3.938462c3.938462 0 3.938462 3.938462 7.876923 3.938461H275.692308c3.938462 0 7.876923 0 11.815384-3.938461l78.769231-43.323077 149.661539-78.769231 3.938461-3.938462 232.369231 126.03077c7.876923 3.938462 15.753846 3.938462 23.630769 3.938461 19.692308-3.938462 31.507692-23.630769 27.569231-43.323077l-43.323077-267.815384 189.046154-189.046154c0-7.876923 0-11.815385 3.938461-19.692308z" fill="#FFC107"></path></svg> <span></span> </div> <div class="pk-col" data-k="name" style="justify-content:flex-start;"> <div id="pk-name-text-wrap" style="display:flex;align-items:center;">${L.col_name}<span></span></div> </div> <div class="pk-col" data-k="size">${L.col_size}<span></span></div> <div class="pk-col" data-k="duration">${L.col_duration_only}<span></span></div> <div class="pk-col" data-k="progress" style="justify-content:flex-start;">${L.col_progress}<span></span></div> <div class="pk-col" data-k="play_time" style="justify-content:flex-start;">${L.col_play_time}<span></span></div> `; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (S.uploadMode) { if (!hd.querySelector('[data-k="upload_speed"]')) { hd.innerHTML = ` <div><input type="checkbox" id="pk-all"></div> <div class="pk-col" data-k="name" style="justify-content:flex-start;">${L.col_name}<span></span></div> <div class="pk-col" data-k="size" style="justify-content:flex-start;">${L.col_size}<span></span></div> <div class="pk-col" data-k="upload_speed" style="justify-content:flex-start;">${L.col_up_speed}<span></span></div> <div class="pk-col" data-k="upload_status" style="justify-content:flex-start;">${L.col_up_status}<span></span></div> `; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (S.shareMode) { if (!hd.querySelector('[data-k="share_status"]')) { hd.innerHTML = ` <div><input type="checkbox" id="pk-all"></div> <div class="pk-col" data-k="name" style="justify-content:flex-start;"> <div id="pk-name-text-wrap" style="display:flex;align-items:center;">${L.col_name}<span style="display:inline-block; min-width:18px; text-align:center;"></span></div> <div id="pk-btn-invert" class="pk-btn" data-pk-tip="${L.btn_invert}" style="margin-left:12px; cursor:pointer; display:none; align-items:center; color:var(--pk-fg); padding:0;"> ${CONF.icons.invert} <span style="margin-left:2px;">${L.btn_invert}</span> </div> </div> <div class="pk-col" data-k="view_count" style="justify-content:flex-start;">${L.col_view}<span></span></div> <div class="pk-col" data-k="save_count" style="justify-content:flex-start;">${L.col_save}<span></span></div> <div class="pk-col" data-k="share_status" style="justify-content:center;">${L.col_share_status}<span></span></div> <div class="pk-col" data-k="modified_time" style="justify-content:flex-end;">${L.col_share_time}<span></span></div> `; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; const btnInv = hd.querySelector('#pk-btn-invert'); if(btnInv) { btnInv.onclick = (e) => { e.stopPropagation(); if (S.loading || S.display.length === 0) return; const newSel = new Set(); for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader && !S.sel.has(item.id)) newSel.add(item.id); } S.sel = newSel; S.lastSelIdx = -1; S.activeId = null; renderVisible(); updateStat(); }; btnInv.onmouseenter = () => btnInv.style.color = 'var(--pk-pri)'; btnInv.onmouseleave = () => btnInv.style.color = 'var(--pk-fg)'; } } } else { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const hasDurationCol = !!hd.querySelector('[data-k="duration"]'); const hasHistoryCol = !!hd.querySelector('[data-k="play_time"]'); const hasOfflineCol = !!hd.querySelector('[data-k="offline_status"]'); if (hasDurationCol === isAnalyzeRoot || !hd.querySelector('[data-k="name"]') || hasHistoryCol || hasOfflineCol) { hd.innerHTML = ` <div><input type="checkbox" id="pk-all"></div> <div class="pk-col" data-k="starred" style="justify-content:center;"> <svg width="16" height="16" viewBox="0 0 1024 1024" style="margin-top:-2px;"><path d="M953.107692 425.353846c3.938462-19.692308-11.815385-39.384615-31.507692-43.323077l-259.938462-39.384615-118.153846-244.184616c-3.938462-7.876923-7.876923-11.815385-15.753846-15.753846-7.876923-3.938462-15.753846-3.938462-19.692308-3.938461h-3.938461c-3.938462 0-7.876923 3.938462-11.815385 3.938461 0 0-3.938462 0-3.938461 3.938462-3.938462 3.938462-3.938462 7.876923-7.876923 11.815384v3.938462l-118.153846 244.184615-259.938462 39.384616c-3.938462 0-7.876923 3.938462-11.815385 3.938461 0 0-3.938462 0-3.938461 3.938462 0 0-3.938462 0-3.938462 3.938461v3.938462c0 3.938462-3.938462 3.938462-3.938461 7.876923 0 0 0 3.938462-3.938462 3.938462v15.753846c0 3.938462 0 3.938462 3.938462 7.876923 0 3.938462 3.938462 7.876923 7.876923 11.815384L275.692308 638.030769 232.369231 905.846154V917.661538c0 3.938462 0 3.938462 3.938461 7.876924v3.938461c0 3.938462 3.938462 3.938462 7.876923 7.876923l3.938462 3.938462c3.938462 0 3.938462 3.938462 7.876923 3.938461H275.692308c3.938462 0 7.876923 0 11.815384-3.938461l78.769231-43.323077 149.661539-78.769231 3.938461-3.938462 232.369231 126.03077c7.876923 3.938462 15.753846 3.938462 23.630769 3.938461 19.692308-3.938462 31.507692-23.630769 27.569231-43.323077l-43.323077-267.815384 189.046154-189.046154c0-7.876923 0-11.815385 3.938461-19.692308z" fill="#FFC107"></path></svg> <span></span> </div> <div class="pk-col" data-k="name" style="justify-content:flex-start;"> <div id="pk-name-text-wrap" style="display:flex;align-items:center;">${L.col_name}<span></span></div> ${!isAnalyzeRoot ? ` <div id="pk-btn-folder-first" class="pk-btn" data-pk-tip="${L.lbl_folder_first}" style="margin-left:8px; cursor:pointer; display:flex; align-items:center; color:#666; font-size:12px; flex-shrink:0; padding:0;"> ${CONF.icons.folderFirst} <span style="margin-left:2px; white-space:nowrap;">${L.lbl_folder_first}</span> </div>` : ''} <div id="pk-btn-invert" class="pk-btn" data-pk-tip="${L.btn_invert}" style="margin-left:12px; cursor:pointer; display:none; align-items:center; color:var(--pk-fg); padding:0;"> ${CONF.icons.invert} <span style="margin-left:2px; white-space:nowrap;">${L.btn_invert}</span> </div> </div> <div class="pk-col" data-k="path" style="display:none;color:#888;">${L.col_path} <span></span></div> <div class="pk-col" data-k="size">${L.col_size}<span></span></div> ${!isAnalyzeRoot ? `<div class="pk-col" data-k="duration">${L.col_dur}<span></span></div>` : ''} <div class="pk-col" data-k="modified_time">${L.col_date}<span></span></div> `; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; const btnFF = hd.querySelector('#pk-btn-folder-first'); if (btnFF) { UI.btnFolderFirst = btnFF; btnFF.onclick = (e) => { e.stopPropagation(); S.folderFirst = !S.folderFirst; gmSet('pk_folder_first', S.folderFirst); if(S.renderFolderFirst) S.renderFolderFirst(); refresh(); }; if(S.renderFolderFirst) S.renderFolderFirst(); } const btnInv = hd.querySelector('#pk-btn-invert'); if(btnInv) { btnInv.onclick = (e) => { e.stopPropagation(); if (S.loading || S.display.length === 0) return; const newSel = new Set(); for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader && !S.sel.has(item.id)) newSel.add(item.id); } S.sel = newSel; S.lastSelIdx = -1; S.activeId = null; renderVisible(); updateStat(); }; btnInv.onmouseenter = () => btnInv.style.color = 'var(--pk-pri)'; btnInv.onmouseleave = () => btnInv.style.color = 'var(--pk-fg)'; } } const colPaths = hd.querySelector('[data-k="path"]'); const colStar = hd.querySelector('[data-k="starred"]'); const colDur = hd.querySelector('[data-k="duration"]'); const colSize = hd.querySelector('[data-k="size"]'); if (colPaths) colPaths.style.display = showPathCol ? 'flex' : 'none'; if (colStar) { colStar.style.display = 'flex'; colStar.style.opacity = '1'; colStar.style.pointerEvents = 'auto'; } if (colDur) { colDur.style.display = isAnalyzeRoot ? 'none' : 'flex'; colDur.style.opacity = '1'; colDur.style.pointerEvents = 'auto'; } if (colSize) colSize.style.display = 'flex'; const dateHeader = hd.querySelector('[data-k="modified_time"]'); if (dateHeader) { dateHeader.childNodes[0].textContent = S.trashMode ? L.col_remaining : L.col_date; } } } const currentCols = hd.querySelectorAll('.pk-col'); currentCols.forEach(c => { const span = c.querySelector('span'); const isSimFolderView = (isAnalyzeRoot && S.analyzeSimGroups); if (S.dupMode || S.offlineMode || S.uploadMode || isSimFolderView) { c.style.cursor = 'default'; c.style.pointerEvents = 'none'; c.style.color = 'var(--pk-fg)'; if(span) span.textContent = ''; if (c.dataset.k === 'name') { const nameWrap = c.querySelector('#pk-name-text-wrap'); if (nameWrap) nameWrap.style.color = 'var(--pk-fg)'; } } else { c.style.cursor = 'pointer'; c.style.pointerEvents = 'auto'; c.onclick = () => { const k = c.dataset.k; if (S.sort === k) S.dir *= -1; else { S.sort = k; S.dir = 1; } const curNode = S.path[S.path.length - 1]; const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curNode.id.startsWith('virtual_') || curNode.id === 'virtual_search_root'); if (isStandard) { if (gmGet('pk_sort_independent', false)) { const folderId = curNode.id || 'root'; try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); prefStore[folderId] = { sort: S.sort, dir: S.dir, folderFirst: S.folderFirst }; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } catch(e) {} } else { gmSet('pk_global_sort_pref', JSON.stringify({ sort: S.sort, dir: S.dir, folderFirst: S.folderFirst })); } } refresh(); }; if (span) span.textContent = (c.dataset.k === S.sort) ? (S.dir === 1 ? ' ▼' : ' ▲') : ''; if (c.dataset.k === 'name') { c.style.color = ''; const nameWrap = c.querySelector('#pk-name-text-wrap'); if (nameWrap) { nameWrap.style.color = (S.sort === 'name') ? 'var(--pk-pri)' : '#666'; } } else { c.style.color = (c.dataset.k === S.sort) ? 'var(--pk-pri)' : ''; } } }); UI.chkAll = hd.querySelector('#pk-all'); if (UI.chkAll) UI.chkAll.onclick = S.handleSelectAll; requestAnimationFrame(renderVisible); } const getStarIcon = (isStarred) => { const color = isStarred ? '#FFC107' : '#ccc'; const fill = isStarred ? '#FFC107' : 'none'; return `<svg width="18" height="18" viewBox="0 0 24 24" fill="${fill}" stroke="${color}" stroke-width="2" style="cursor:pointer;" class="pk-star-toggle"> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path></svg>`; }; function renderVisible() { const L = getStrings(); const winEl = el.querySelector('.pk-win'); const listIn = el.querySelector('#pk-in'); if (winEl && listIn) { const isMax = winEl.classList.contains('pk-maximized'); const cur = S.path[S.path.length - 1]; const isGroupedView = S.dupMode || (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups); const gap = isGroupedView ? (isMax ? 10 : 4) : 0; listIn.style.transform = gap > 0 ? `translateY(-${gap}px)` : 'none'; } if (S.display.length === 0) { if (S.loading) { UI.in.style.height = '100%'; UI.in.innerHTML = ` <div style="position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; color: var(--pk-fg); opacity: 0.7; z-index: 10; pointer-events: none;"> <div class="pk-spin-lg" style="width: 36px; height: 36px; border-width: 3px; border-color: rgba(136, 136, 136, 0.2); border-top-color: var(--pk-pri);"></div> <div style="font-size: 13px; font-weight: 500; letter-spacing: 0.5px;">${L.loading_detail}</div> </div> `; 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 = `<div class="pk-empty">${CONF.emptySVG}<div class="pk-empty-txt">${L.str_no_files}</div></div>`; UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; if (UI.ctx) UI.ctx.style.display = 'none'; return; } UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; const top = UI.vp.scrollTop; const h = UI.vp.clientHeight; const buffer = CONF.buffer || 20; const start = Math.max(0, Math.floor(top / CONF.rowHeight) - buffer); const end = Math.min(S.display.length, Math.ceil((top + h) / CONF.rowHeight) + buffer); const isBlur = gmGet('pk_blur_thumb', false); const rootPathStr = S.path.map(p => p.name).join('/'); const lang = getLang(); let colDef; const cur = S.path[S.path.length - 1]; const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const isGlobalSearchRoot = cur.id === 'virtual_search_root' && UI.chkGlobal && UI.chkGlobal.checked; const showPathCol = S.dupMode || isAnalyzeRoot || S.isFlattened || isGlobalSearchRoot; const isMax = UI.win.classList.contains('pk-maximized'); if (S.offlineMode) { colDef = "36px 1fr 120px 240px 180px"; } else if (S.uploadMode) { colDef = "36px 1fr 100px 120px 180px"; } else if (S.shareMode) { colDef = isMax ? "36px 1fr 80px 80px 120px 130px" : "36px 1fr 80px 80px 80px 130px"; } else if (S.historyMode) { colDef = "36px 30px 1fr 80px 80px 120px 160px"; } else if (S.trashMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (S.recentMode) { colDef = "36px 30px 1fr 80px 105px 130px"; } else if (isAnalyzeRoot) { colDef = "36px 30px 2fr 1fr 80px 130px"; } else if (showPathCol) { colDef = "36px 30px 2fr 1fr 80px 105px 130px"; } else { colDef = "36px 30px 1fr 80px 105px 130px"; } UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; const fragment = document.createDocumentFragment(); let nameColWidth = 400; let charCapacity = 50; const pathIds = S.path.map(p => p.id); const isAtGlobalSearchRoot = pathIds[pathIds.length - 1] === 'virtual_search_root'; const isGlobalSearchHistoryPresent = pathIds.includes('virtual_search_root'); const shouldShowHl = (!!S.search && (!isGlobalSearchHistoryPresent || isAtGlobalSearchRoot)); let pathColWidth = 200, pathCharCapacity = 30; if (UI.win) { const charWidth = isMax ? 9.2 : 8; const nameColEl = UI.win.querySelector('.pk-grid-hd .pk-col[data-k="name"]'); if (nameColEl) { nameColWidth = nameColEl.offsetWidth; charCapacity = Math.floor((nameColWidth - 80) / charWidth); } const pathColEl = UI.win.querySelector('.pk-grid-hd .pk-col[data-k="path"]'); if (pathColEl) { pathColWidth = pathColEl.offsetWidth; pathCharCapacity = Math.floor((pathColWidth - 20) / charWidth); } } const getTooltipHlHTML = (text, query) => { if (!query || !shouldShowHl) return esc(text); const lowText = text.toLowerCase(); const q = query.toLowerCase(); const idx = lowText.indexOf(q); if (idx === -1) return esc(text); return esc(text.substring(0, idx)) + `<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">${esc(text.substring(idx, idx + q.length))}</b>` + 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 `<svg class="pk-star-toggle" width="16" height="16" viewBox="0 0 24 24" fill="${fill}" stroke="${color}" stroke-width="2" style="cursor:pointer; transition: all 0.2s;"> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path> </svg>`; }; const getRemainingDays = (dateStr) => { if (!dateStr) return "-"; const deleteTime = new Date(dateStr).getTime(); const now = getServerNow(); const diffMs = now - deleteTime; const daysPassed = diffMs / (1000 * 60 * 60 * 24); let left = Math.max(0, Math.min(15, Math.ceil(15 - daysPassed))); return left + " " + L.unit_days; }; for (let i = start; i < end; i++) { const d = S.display[i]; if (!d) continue; const row = document.createElement('div'); row.style.position = 'absolute'; row.style.top = `${i * CONF.rowHeight}px`; row.style.width = '100%'; row.style.gridTemplateColumns = colDef; if (d.isHeader) { row.className = 'pk-group-hd'; if (i === 0) { row.style.setProperty('border-top', 'none', 'important'); const borderW = isMax ? 10 : 4; row.style.setProperty('padding-top', borderW + 'px', 'important'); } else { row.style.removeProperty('border-top'); row.style.removeProperty('padding-top'); } if (S.trashMode) row.style.gridColumn = "1 / 7"; else if (S.dupMode || S.analyzeMode) row.style.gridColumn = "1 / 8"; else row.style.gridColumn = "1 / 7"; const gIdx = parseInt(d.id.replace('grp_', '')); const groupData = S.dupMode ? S.dupRawGroups[gIdx] : (S.analyzeMode ? S.analyzeSimGroups[gIdx] : null); const groupItemIds = groupData ? groupData.ids :[]; let selectedInGroup = 0; groupItemIds.forEach(id => { if (S.sel.has(id)) selectedInGroup++; }); const isAllSelected = groupItemIds.length > 0 && selectedInGroup === groupItemIds.length; const isIndeterminate = selectedInGroup > 0 && selectedInGroup < groupItemIds.length; let groupIcon = CONF.dupHashSVG; const isContain = d.name.includes(L.lbl_containment); const isSimLike = d.type === L.tag_sim || d.name.includes(L.lbl_sim_score); const isNameLike = d.type === L.tag_name || (S.analyzeMode && d.name.includes(' | ') && !isSimLike && !isContain); if (isContain) { groupIcon = CONF.dupContainSVG; } else if (isSimLike) { groupIcon = CONF.dupSimSVG; } else if (isNameLike) { groupIcon = CONF.dupNameSVG; } groupIcon = groupIcon.replace(/width:\s*\d+px;?/, 'width:18px;').replace(/height:\s*\d+px;?/, 'height:18px;').replace(/margin-right:\s*\d+px;?/, 'margin:0;'); let headerName = d.name; const headerTip = getTooltipHlHTML(d.name, S.search).replace(/"/g, '"'); row.style.display = 'flex'; row.innerHTML = ` <div style="width:36px; flex-shrink:0; display:flex; justify-content:center; align-items:center; margin-right:10px;"> <input type="checkbox" class="pk-grp-chk" ${isAllSelected ? 'checked' : ''} style="width:18px; height:18px; cursor:pointer; margin:0;"> </div> <div style="width:30px; flex-shrink:0; display:flex; justify-content:flex-start; align-items:center; margin-right:10px;"> <div style="display:flex; color:var(--pk-pri); opacity:0.8; margin-left:-1px;">${groupIcon}</div> </div> <div class="pk-name" style="display:flex; align-items:center; overflow:hidden; white-space:nowrap; flex:1; min-width:0;" data-pk-tip="${headerTip}"> <span style="font-weight:600; color:var(--pk-fg); opacity:0.9; overflow:hidden; text-overflow:ellipsis;">${esc(headerName)}</span> </div> <div style="margin-left:auto; display:flex; align-items:center; flex-shrink:0; gap:8px;"></div> </div> `; const grpChk = row.querySelector('.pk-grp-chk'); if (grpChk) { grpChk.indeterminate = isIndeterminate; grpChk.onclick = (e) => { e.stopPropagation(); const targetState = grpChk.checked; groupItemIds.forEach(id => { if (targetState) S.sel.add(id); else S.sel.delete(id); }); renderVisible(); updateStat(); }; } } else { const isSel = S.sel.has(d.id); const isFocused = S.activeId === d.id; const isMoving = S.movingIds && S.movingIds.has(d.id); row.className = `pk-row ${isSel ? 'sel' : ''} ${isFocused ? 'pk-focused' : ''} ${isMoving ? 'pk-moving' : ''}`; row.ondragstart = (e) => e.preventDefault(); if (isFocused && !isSel) { row.style.backgroundColor = 'var(--pk-sel-bg)'; row.style.border = '1px solid var(--pk-pri)'; row.style.borderRadius = '4px'; } if (isMoving) { row.style.opacity = '0.4'; row.style.filter = 'grayscale(100%)'; row.style.pointerEvents = 'none'; row.style.cursor = 'wait'; } else { row.style.opacity = ''; row.style.filter = ''; row.style.pointerEvents = ''; row.style.cursor = ''; } row.style.display = 'grid'; row.dataset.id = d.id; const isProtected = !S.trashMode && isSystemItem(d); const isMax = UI.win.classList.contains('pk-maximized'); const nameTip = getTooltipHlHTML(d.name, S.search).replace(/"/g, '"'); const getDynamicIcon = (item) => { let isBlacklisted = false; const cleanName = (item.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (item.kind === 'drive#folder') { isBlacklisted = S.blFolderSet && S.blFolderSet.has(cleanName); } else { isBlacklisted = S.blSet && S.blSet.has(cleanName); } const blHtml = isBlacklisted ? `<div class="pk-bl-marker" data-bl="active">${CONF.icons.blMarker}</div>` : ''; 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 `<div class="pk-max-icon-box" style="${boxStyle}"><div style="transform: translateX(-6px); display:flex;">${scriptIcon}</div>${blHtml}</div>`; } return `<div class="pk-min-icon" style="width:24px; height:24px; display:inline-flex; align-items:center; justify-content:center; vertical-align:middle; margin-right:12px; flex-shrink:0; position:relative;"> <div style="display:flex; transform: translateX(4px) scale(0.96);">${scriptIcon}</div> ${blHtml} </div>`; } } if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); const isFolder = item.kind === 'drive#folder'; const isTask = item.kind === 'drive#task'; const isUploadTask = item.kind === 'pk#upload'; const lookupId = (isTask || isUploadTask) ? item.file_id : item.id; let hasValidCover = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); if (!window.pkRecentMetaCache) window.pkRecentMetaCache = new Map(); if (S.recentMode && !isFolder) { if (window.pkRecentMetaCache.has(item.id)) { const meta = window.pkRecentMetaCache.get(item.id); item.thumbnail_link = meta.thumbnail_link || item.thumbnail_link; item.icon_link = meta.icon_link || item.icon_link; item.mime_type = meta.mime_type || item.mime_type; if (meta.medias) item.medias = meta.medias; item.params = Object.assign(item.params || {}, meta.params || {}); hasValidCover = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); } else if (!hasValidCover && !item._metaFetching) { item._metaFetching = true; const ext = (item.name || '').split('.').pop().toLowerCase(); const mime = (item.mime_type || '').toLowerCase(); const isLikelyMedia = mime.startsWith('video/') || mime.startsWith('image/') ||['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','jpg','jpeg','png','gif','bmp','webp','heic','svg','tif','tiff','ico'].includes(ext); if (isLikelyMedia) { apiGet(item.id).then(meta => { if (meta) { window.pkRecentMetaCache.set(item.id, meta); item.thumbnail_link = meta.thumbnail_link || item.thumbnail_link; item.icon_link = meta.icon_link || item.icon_link; item.mime_type = meta.mime_type || item.mime_type; if (meta.medias) item.medias = meta.medias; item.params = Object.assign(item.params || {}, meta.params || {}); requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); } }).catch(()=>{}); } } } let forceDeepScan = false; if (item._coverResolved) { hasValidCover = false; forceDeepScan = true; } else if (isFolder && hasValidCover && typeof globalCache !== 'undefined' && globalCache.has(lookupId)) { forceDeepScan = true; } if ((isFolder || isTask || isUploadTask) && lookupId && (!hasValidCover || forceDeepScan) && typeof globalCache !== 'undefined') { const normalize = (data) => (data && !Array.isArray(data) && data.items) ? data.items : data; const scanDeepCover = (targetId, depth) => { if (depth > 5) return null; const raw = globalCache.get(targetId); if (!raw) return null; const files = normalize(raw); if (!files || files.length === 0) return null; const vid = files.find(f => f.mime_type?.startsWith('video/') && f.thumbnail_link); if (vid) return vid.thumbnail_link; const img = files.find(f => f.mime_type?.startsWith('image/') && f.thumbnail_link); if (img) return img.thumbnail_link; const subFolders = files.filter(f => f.kind === 'drive#folder'); for (const sub of subFolders) { if (globalCache.has(sub.id)) { const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; continue; } if (sub.thumbnail_link && sub.thumbnail_link !== sub.icon_link && !sub._coverResolved) { return sub.thumbnail_link; } const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; } return null; }; const foundThumb = scanDeepCover(lookupId, 0); if (foundThumb) { item.thumbnail_link = foundThumb; item._coverResolved = true; item._isFolderLike = true; hasValidCover = true; } else if (item._coverResolved || (isFolder && globalCache.has(lookupId))) { item.thumbnail_link = null; item._coverResolved = false; item._isFolderLike = false; hasValidCover = false; } else if ((isFolder || isTask || isUploadTask) && typeof scannedFolderIds !== 'undefined' && !scannedFolderIds.has(lookupId)) { if (!S.loading && !S.scanning) { scannedFolderIds.add(lookupId); backgroundQueue.push({ id: lookupId, name: 'DeepCoverProbe', retryCount: 0 }); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } } } const iconHtml = getIcon(item); const boxStyle = "width:54px; min-width:54px; height:100%; display:flex; align-items:center; justify-content:flex-start !important; margin-right:12px; position:relative;"; const placeholderStyle = `position: absolute; left: 0; top: 50%; transform: translateY(-50%); z-index: 1; width: 100%; display: flex; align-items: center; transition: opacity 0.3s; pointer-events: none;`; const imgStyle = "width: 48px; height: 48px; object-fit: cover; border-radius: 4px; margin: 0 !important; background: transparent; position: relative; z-index: 2; transition: opacity 0.3s;"; const isDarkTheme = document.body.classList.contains('dark') || document.documentElement.getAttribute('data-theme') === 'dark'; const badgeBg = isDarkTheme ? '#303134' : '#FFFFFF'; const badgeBorder = isDarkTheme ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.08)'; const badgeShadow = isDarkTheme ? '0 2px 6px rgba(0,0,0,0.4)' : '0 2px 6px rgba(0,0,0,0.15)'; const badgeStyle = `position: absolute; bottom: -5px; right: -5px; z-index: 3; width: 24px; height: 24px; border-radius: 50%; background-color: ${badgeBg}; border: ${badgeBorder}; box-shadow: ${badgeShadow}; box-sizing: border-box; transition: opacity 0.3s; pointer-events: none; display: grid; place-items: center; line-height: 0;`; const isBlur = gmGet('pk_blur_thumb', false); if (isMax && isBlur) { const fallbackSvg = iconHtml.replace(/"/g, """).replace(/\n/g, ""); return `<div class="pk-max-icon-box" style="${boxStyle}"> <img src="${item.icon_link}" style="${imgStyle} opacity:1; object-fit:contain;" draggable="false" onerror="this.outerHTML='<div class="pk-placeholder-layer" style="${placeholderStyle} opacity:1;">${fallbackSvg}</div>'"> ${blHtml} </div>`; } 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 = `<img src="${item.icon_link}" style="width:44px; height:44px; object-fit:contain;">`; 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 = `<img src="${item.icon_link}" style="width:16px; height:16px; object-fit:contain; display:block;">`; badgeHtml = `<div class="pk-folder-badge" style="${badgeStyle} opacity: ${badgeOp};">${innerContent}</div>`; } const isVideo = !isFolder && item.mime_type && item.mime_type.startsWith('video/'); let videoOvHtml = ''; if (isVideo) { const vOp = isCached ? '1' : '0'; videoOvHtml = ` <div class="pk-video-ov" style="position:absolute; top:0; bottom:0; left:0; right:0; z-index:4; pointer-events:none; opacity:${vOp}; transition:opacity 0.3s; display:flex !important; align-items:center !important; justify-content:center !important;"> <svg viewBox="0 0 24 24" fill="rgba(255, 255, 255, 0.6)" style="width:24px !important; height:24px !important; flex-shrink:0; display:block; filter:drop-shadow(0 1px 3px rgba(0,0,0,0.5)); transform:translateX(-1.5px); margin:0 !important; padding:0 !important;"> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/> </svg> </div>`; } return ` <div class="pk-max-icon-box" style="${boxStyle}"> <div class="pk-placeholder-layer" style="${placeholderStyle} opacity: ${phOp};"><img src="${item.icon_link}" style="width:50px; height:50px; object-fit:contain;"></div> <img src="${item.thumbnail_link}" class="pk-max-thumb" style="${imgStyle} opacity: ${imgOp};" draggable="false"> ${badgeHtml} ${videoOvHtml} ${blHtml} </div> `; } if (isMax) { if (hasValidCover) { const showBadge = isFolder || item._isFolderLike || (item.icon_link && item.icon_link.includes('folder')); let badgeHtml = ''; if (showBadge) { const innerIcon = `<img src="${item.icon_link}" style="width:16px; height:16px; object-fit:contain; display:block;">`; badgeHtml = `<div class="pk-folder-badge" style="${badgeStyle} opacity:1;">${innerIcon}</div>`; } return ` <div class="pk-max-icon-box" style="${boxStyle}"> <div class="pk-placeholder-layer" style="${placeholderStyle} opacity:0;"><img src="${item.icon_link}" style="width:50px;height:50px;object-fit:contain;"></div> <img src="${item.thumbnail_link}" class="pk-max-thumb" style="${imgStyle} opacity:1;" draggable="false"> ${badgeHtml} ${blHtml} </div> `; } const finalIconSrc = item.icon_link || item.thumbnail_link; if (finalIconSrc) { return `<div class="pk-max-icon-box" style="${boxStyle}"><img src="${finalIconSrc}" style="width:50px; height:50px; object-fit:contain;" onerror="this.outerHTML='<div style="transform:translateX(-6px);display:flex;">${iconHtml.replace(/"/g, """).replace(/\n/g, "")}</div>'">${blHtml}</div>`; } return `<div class="pk-max-icon-box" style="${boxStyle}"><div style="transform: translateX(-6px); display:flex;">${iconHtml}</div>${blHtml}</div>`; } 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 ? `<img src="${item.icon_link}" style="width:100%;height:100%;object-fit:contain;border-radius:4px;pointer-events:none;">` : iconHtml; return `<div class="pk-min-media-box" style="width:24px;height:24px;margin-right:12px;position:relative;flex-shrink:0;display:inline-flex;vertical-align:middle;overflow:visible !important;border-radius:4px;"> <div style="position:absolute;inset:0;overflow:hidden;border-radius:4px;z-index:1;"> <div class="pk-min-ph" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:1;transition:opacity 0.2s;opacity:${phOp};">${placeholder}</div> <img src="${item.thumbnail_link}" class="pk-min-thumb" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:${imgOp};z-index:2;transition:opacity 0.2s;" onload="if(window.pkGlobalThumbCache){window.pkGlobalThumbCache.add('${item.id}');}this.style.opacity='1';this.previousElementSibling.style.opacity='0';" onerror="this.previousElementSibling.style.opacity='1';this.remove();"> </div> ${blHtml} </div>`; } if (item.icon_link) { return `<div class="pk-min-icon" style="position:relative; display:inline-flex; align-items:center; justify-content:center; vertical-align:middle; margin-right:12px; flex-shrink:0; overflow:visible;"><img draggable="false" src="${item.icon_link}" style="width:24px; height:24px; object-fit:contain; border-radius:4px;" onerror="this.style.display='none'; if(this.nextElementSibling) this.nextElementSibling.style.display='inline-flex';"><span style="display:none; align-items:center; justify-content:center;">${iconHtml}</span>${blHtml}</div>`; } return `<div class="pk-min-icon" style="position:relative; display:inline-flex; align-items:center; justify-content:center; vertical-align:middle; margin-right:12px; flex-shrink:0; overflow:visible;">${iconHtml}${blHtml}</div>`; }; const checkboxHtml = `<input type="checkbox" ${isSel ? 'checked' : ''}>`; let html = `<div>${checkboxHtml}</div>`; 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 += `<div style="display:flex; align-items:center; justify-content:center;"><svg class="pk-star-icon" width="16" height="16" viewBox="0 0 24 24" fill="${starFill}" stroke="${starColor}" stroke-width="2" style="cursor:default; opacity:0.8;"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path></svg></div>`; 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 += `<div class="pk-name" ${d.thumbnail_link ? `data-pk-thumb="${d.thumbnail_link}"` : ''} data-pk-tip="${nameTip}"> ${iconImg} <span class="pk-name-txt">${nameDisplay}</span> </div>`; html += `<div>${fmtSize(d.size)}</div>`; const displayDur = d._history_duration || d.params?.duration || 0; html += `<div>${displayDur > 0 ? fmtDur(displayDur) : '-'}</div>`; 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 = ` <div style="width:100%; display:flex; flex-direction:column; justify-content:center; gap:2px;"> <div style="display:flex; justify-content:space-between; font-size:13px; color:var(--pk-fg); line-height:1.2; font-weight:normal;"> <span>${fmtDur(curT)}</span> <span>${parseInt(pct)}%</span> </div> <div style="width:100%; height:4px; background:var(--pk-bd); border-radius:2px; overflow:hidden;"> <div style="width:${pct}%; height:100%; background:var(--pk-pri);"></div> </div> </div> `; } html += `<div style="padding-right:10px;">${progressHtml}</div>`; let playTimeStr = '-'; if (d._history_ts > 0) { playTimeStr = fmtDate(new Date(d._history_ts).toISOString()); } else { playTimeStr = "-"; } html += `<div style="color:var(--pk-fg); font-weight:normal;">${playTimeStr}</div>`; } 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 += `<div class="pk-name" ${thumbAttr} data-pk-tip="${nameTip}"> ${iconImg} <span class="pk-name-txt" style="${nameStyle}">${nameDisplay}</span> </div>`; html += `<div>${fmtSize(d.size)}</div>`; 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 += `<div style="color:${statusColor}; font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" data-pk-tip="${esc(statusText)}">${statusText}</div>`; let progressHtml = ''; if (d.phase === 'PHASE_TYPE_COMPLETE') { progressHtml = `<div style="color:#52c41a; font-weight:bold;">100%</div>`; } else if (d.phase === 'PHASE_TYPE_ERROR') { progressHtml = `<div style="color:#ff4d4f;">-</div>`; } else { const pct = d.progress || 0; progressHtml = ` <div style="width:100px; height:6px; background:var(--pk-bd); border-radius:3px; overflow:hidden; margin-right:8px;"> <div style="width:${pct}%; height:100%; background:var(--pk-pri); transition:width 0.3s;"></div> </div> <div style="font-size:12px; font-variant-numeric:tabular-nums;">${pct}%</div> `; } html += `<div style="display:flex; align-items:center;">${progressHtml}</div>`; } 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 += `<div class="pk-name" ${thumbAttr} data-pk-tip="${nameTip}" style="display:flex; align-items:center; height:100%; overflow:visible;"> ${getDynamicIcon(d)} <span class="pk-name-txt" style="margin:0!important; padding:0!important; line-height:1.5; ${nameStyle}">${nameDisplay}</span> </div>`; html += `<div>${fmtSize(d.size)}</div>`; html += `<div class="pk-up-spd">${(d.status === 'UPLOADING' && S.upMng) ? S.upMng.fmtSpeed(d.speed) : '-'}</div>`; 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 += ` <div style="display:flex; flex-direction:column; justify-content:center; gap:4px; width:100%;"> <div style="display:flex; justify-content:space-between; font-size:12px;"> <span style="color:${statusColor}">${d.message}</span> <span class="pk-up-prog-txt">${Math.floor(d.progress)}%</span> </div> <div style="width:100%; height:4px; background:var(--pk-bd); border-radius:2px; overflow:hidden;"> <div class="pk-up-prog-bar" style="width:${d.progress}%; height:100%; background:${statusColor}; transition:width 0.2s;"></div> </div> </div> `; } 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 ? `<div class="pk-share-lock">${CONF.icons.lock}</div>` : ''; if (isShareDisabled) { iconImg = `<div class="pk-share-icon-wrap" draggable="false" style="transform:scale(1.1); opacity:0.6;">${CONF.typeIcons.file}</div>`; } else { const currentIcon = getDynamicIcon(d); const isUsingThumb = currentIcon.includes('pk-max-thumb'); if (isUsingThumb) { iconImg = `<div class="pk-share-icon-wrap" style="margin-right:20px !important;">${currentIcon}${lockHtml}</div>`; } else if (iconUrl) { iconImg = `<div class="pk-share-icon-wrap" draggable="false" style="display:flex;align-items:center;justify-content:center;"><img draggable="false" src="${iconUrl}" style="width:24px;height:24px;object-fit:contain;flex-shrink:0;">${lockHtml}</div>`; } else { const isFolder = d.kind === 'drive#folder'; iconImg = `<div class="pk-share-icon-wrap" draggable="false"><div style="width:100%;height:100%;${isFolder?'':'transform:scale(1.08);'}">${getIcon(d)}</div>${lockHtml}</div>`; } } const nameDisplay = shouldShowHl ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `<div class="pk-name" data-pk-tip="${nameTip}"> ${iconImg} <span class="pk-name-txt" style="${isShareDisabled ? 'cursor:default; pointer-events:none;' : ''}">${nameDisplay}</span> </div>`; html += `<div style="text-align:left; padding-left:2px; font-variant-numeric:tabular-nums;">${d.view_count || 0}</div>`; const saveVal = d.limit_count > 0 ? `${d.save_count || 0}/${d.limit_count}` : (d.save_count || 0); const saveTip = d.limit_count > 0 ? `${L.lbl_limit_tip}: ${d.limit_count}` : ''; html += `<div style="text-align:left; padding-left:2px; font-variant-numeric:tabular-nums;" data-pk-tip="${saveTip}">${saveVal}</div>`; 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 += `<div style="text-align:center;color:${statusColor};font-size:12px;" data-pk-tip="${esc(statusText)}">${statusText}</div>`; const shareDate = fmtDate(d.modified_time); html += `<div style="text-align:right;font-size:12px;" data-pk-tip="${shareDate}">${shareDate}</div>`; } 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 += `<div style="display:flex; align-items:center; justify-content:center;"><svg class="pk-star-icon" width="16" height="16" viewBox="0 0 24 24" fill="${starFill}" stroke="${starColor}" stroke-width="2" style="cursor:default; opacity:0.8;"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path></svg></div>`; } else { if (isProtected) html += `<div></div>`; else html += `<div style="display:flex; align-items:center; justify-content:center;"><svg class="pk-star-icon" width="16" height="16" viewBox="0 0 24 24" fill="${starFill}" stroke="${starColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="cursor:default; opacity:0.8;"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg></div>`; } 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 += `<div class="pk-name" ${thumbAttr} data-pk-tip="${nameTip}" style="${isProtected ? 'opacity:1;' : ''}">${getDynamicIcon(d)}<span class="pk-name-txt">${nameDisplay}</span>${isProtected ? `<span class="pk-tag-default">${L.tag_default}</span>` : ''}</div>`; if (showPathCol) { let pathHtml = ''; const homeIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;flex-shrink:0;vertical-align:-4px;"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`; const homeText = L.btn_nav_home; let homeQuery = S.search; let restQuery = S.search; let isSlashMatched = false; if (S.search) { const qLower = S.search.toLowerCase(); const hLower = homeText.toLowerCase(); if (qLower.startsWith(hLower + '/')) { homeQuery = S.search.substring(0, homeText.length); restQuery = S.search.substring(homeText.length + 1); isSlashMatched = true; } } const isHomeMatched = shouldShowHl && homeQuery && homeText.toLowerCase().includes(homeQuery.toLowerCase()); const homeDisplay = isHomeMatched ? getSearchHlHTML(homeText, homeQuery, 20) : esc(homeText); const prefix = homeText + '/'; const rootStyle = isMax? "display:inline-flex;align-items:baseline;flex-shrink:0;margin-right:2px;": "display:inline-flex;align-items:baseline;flex-shrink:0;line-height:1.2;padding-bottom:0;"; const contentStyle = isMax ? '' : 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.5;padding-bottom:2px;'; const containerStyle = isMax? "display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-all;line-height:1.4;white-space:normal;color:var(--pk-fg);": "display:flex;align-items:center;overflow:hidden;white-space:nowrap;color:var(--pk-fg);line-height:1.5;padding-bottom:2px;"; if (isAnalyzeRoot) { const pStr = d._pathStr || d.path || ""; displayPath = pStr; let isSameAsPrev = d._isSameFolder || false; if (!isSameAsPrev && S.sort === 'path' && i > 0) { const prevItem = S.display[i - 1]; const prevPStr = prevItem._pathStr || prevItem.path || ""; if (prevPStr === pStr) isSameAsPrev = true; } if (isSameAsPrev) { pathHtml = `<span style="color:var(--pk-fg);">${L.str_same_folder}</span>`; } 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) ? `<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">/</b>` : '/'; const sHtml = `<span style="margin:0 1px;line-height:1.5;padding-bottom:2px;">${sDisp}</span>`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && content) ? getSearchHlHTML(content, hq, pathCharCapacity) : esc(content); let innerHtml = `<span style="${rootStyle}">${homeIcon}<span style="${isMax ? '' : 'transform:translateY(-2px);'}">${homeDisplay}</span></span>`; if (content) { innerHtml += `${sHtml}<span style="${contentStyle}">${pathDisplay}</span>`; } pathHtml = `<div style="${containerStyle}">${innerHtml}</div>`; } } else if (S.dupMode) { if (d._isSameFolder) { pathHtml = `<span style="color:var(--pk-fg);">${L.str_same_folder}</span>`; } else { const rawPath = d._dupFullPath || ""; let realPath = rawPath; if (rawPath === L.current_dir) { realPath = rootPathStr; } else { if (!rawPath.startsWith(rootPathStr)) { realPath = rootPathStr + '/' + rawPath; } } let contentStr = realPath; if (realPath === homeText) { contentStr = ""; } else if (realPath.startsWith(prefix)) { contentStr = realPath.substring(prefix.length); } let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && contentStr.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const sDisp = (shouldShowHl && sm) ? `<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">/</b>` : '/'; const sHtml = `<span style="margin:0 1px;line-height:1.5;padding-bottom:2px;">${sDisp}</span>`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && contentStr) ? getSearchHlHTML(contentStr, hq, pathCharCapacity) : esc(contentStr); let innerHtml = `<span style="${rootStyle}">${homeIcon}<span style="${isMax ? '' : 'transform:translateY(-2px);'}">${homeDisplay}</span></span>`; if (contentStr) { innerHtml += `${sHtml}<span style="${contentStyle}">${pathDisplay}</span>`; } pathHtml = `<div style="${containerStyle}">${innerHtml}</div>`; } } else { let fullPathStr = ""; if (d._lineage === null && d.parent_id && d.parent_id !== 'root') { pathHtml = `<span style="color:#ccc; font-size:12px;">...</span>`; displayPath = null; } else if (isGlobalSearchRoot) { fullPathStr = shortPath; } else { if (displayPath && displayPath.startsWith(prefix)) { fullPathStr = displayPath.substring(prefix.length); } else { fullPathStr = shortPath; } } let isSameAsPrev = false; if (S.sort === 'path' && i > 0) { const prevItem = S.display[i - 1]; let prevPathStr = ""; if (S.analyzeMode) { prevPathStr = prevItem._pathStr || ""; } else { if (prevItem._lineage) prevPathStr = prevItem._lineage.map(x=>x.name).join('/'); } if (prevPathStr === shortPath) isSameAsPrev = true; } if (displayPath !== null) { if (isSameAsPrev) { pathHtml = `<span style="color:var(--pk-fg);">${L.str_same_folder}</span>`; } 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) ? `<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">/</b>` : '/'; const sHtml = `<span style="margin:0 1px;line-height:1.5;padding-bottom:2px;">${sDisp}</span>`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && fullPathStr) ? getSearchHlHTML(fullPathStr, hq, pathCharCapacity) : esc(fullPathStr); let innerHtml = `<span style="${rootStyle}">${homeIcon}<span style="${isMax ? '' : 'transform:translateY(-2px);'}">${homeDisplay}</span></span>`; if (fullPathStr) { innerHtml += `${sHtml}<span style="${contentStyle}">${pathDisplay}</span>`; } pathHtml = `<div style="${containerStyle}">${innerHtml}</div>`; } } } 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) ? `<b style="color:var(--pk-match-fg); background:var(--pk-match-bg); border-radius:2px; padding:0 2px;">/</b>` : '/'; restDisplayTip = slashTip + getTooltipHlHTML(pureRest, hq); } else { restDisplayTip = getTooltipHlHTML(rest, restQuery); } const homeGroup = `<span style="display:inline-flex;align-items:center;vertical-align:bottom;">${homeIcon}${homeDisplayTip}</span>`; pathTipHtml = `<div style="line-height:1.6;word-break:break-all;">${homeGroup}${restDisplayTip}</div>`; } html += `<div class="pk-path" style="font-size:12px; overflow:hidden; text-overflow:ellipsis; ${isMax ? '' : 'white-space:nowrap;'} padding-right:10px; padding-bottom:2px;" data-pk-tip="${pathTipHtml.replace(/"/g, '"')}">${pathHtml}</div>`; } const displaySize = (d.kind === 'drive#folder' && !S.analyzeMode) ? '-' : fmtSize(d.size); html += `<div>${displaySize}</div>`; 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 += `<div data-pk-tip="${durHtml}">${durHtml}</div>`; } const dateTxt = S.trashMode ? getRemainingDays(d.modified_time) : fmtDate(d.modified_time); html += `<div data-pk-tip="${dateTxt}">${dateTxt}</div>`; } row.innerHTML = html; const thumbImg = row.querySelector('.pk-max-thumb, .pk-min-thumb'); if (thumbImg) { const toggleThumb = () => { if(window.pkGlobalThumbCache) window.pkGlobalThumbCache.add(d.id); thumbImg.style.opacity = '1'; const placeholder = thumbImg.previousElementSibling; if (placeholder && (placeholder.classList.contains('pk-placeholder-layer') || placeholder.classList.contains('pk-min-ph'))) { placeholder.style.opacity = '0'; } const parent = thumbImg.parentElement; const badge = parent ? parent.querySelector('.pk-folder-badge') : null; if (badge) { badge.style.opacity = '1'; } const vidOv = parent ? parent.querySelector('.pk-video-ov') : null; if (vidOv) { vidOv.style.opacity = '1'; } }; const handleThumbError = () => { thumbImg.style.display = 'none'; if (window.pkGlobalThumbCache) window.pkGlobalThumbCache.delete(d.id); const placeholder = thumbImg.previousElementSibling; if (placeholder && (placeholder.classList.contains('pk-placeholder-layer') || placeholder.classList.contains('pk-min-ph'))) { placeholder.style.opacity = '1'; } const parent = thumbImg.parentElement; const badge = parent ? parent.querySelector('.pk-folder-badge') : null; if (badge) { badge.style.opacity = '0'; } }; if (thumbImg.complete) { if (thumbImg.naturalWidth > 0) toggleThumb(); else handleThumbError(); } else { thumbImg.onload = toggleThumb; thumbImg.onerror = handleThumbError; } } if (!d.isHeader && d.thumbnail_link) { const minIcon = row.querySelector('.pk-min-icon'); if (minIcon && !minIcon.querySelector('.pk-proxy-thumb')) { const proxyImg = document.createElement('img'); proxyImg.className = 'pk-proxy-thumb'; proxyImg.src = d.thumbnail_link; proxyImg.style.cssText = 'position: absolute; top: 0; left: 0; width: 0; height: 0; opacity: 0; pointer-events: none; visibility: hidden;'; minIcon.appendChild(proxyImg); } } const chk = row.querySelector('input'); const triggerOpen = () => { if (S.movingIds.has(d.id)) return; if (S.trashMode) return; const checkType = (it) => { const m = (it.mime_type || '').toLowerCase(); const n = (it.name || '').toLowerCase(); const dur = (it.params && it.params.duration) || 0; const vExts = ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp']; const aExts = ['zip','rar','7z','tar','gz','bz2','xz']; return { isVideo: m.startsWith('video/') || dur > 0 || vExts.some(e => n.endsWith('.' + e)), isImage: m.startsWith('image/'), isTorrent: n.endsWith('.torrent'), isArchive: m.includes('zip') || m.includes('rar') || m.includes('archive') || aExts.some(e => n.endsWith('.' + e)) }; }; if (S.uploadMode) { if (d.status !== 'DONE') return; const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isVideo || t.isImage) { if (t.isVideo) playVideo(d); else showImage(d); } else if (d.file_id) { S.sel.clear(); S.sel.add(d.id); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); } return; } if (S.offlineMode) { if (!d.file_id || d.phase === 'PHASE_TYPE_ERROR') return; const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isVideo || t.isImage) { if (t.isVideo) playVideo(d); else showImage(d); } else { S.sel.clear(); S.sel.add(d.id); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) { locateBtn.click(); } } return; } if (S.shareMode) { const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if (isShareDisabled) return; showShareDetail(d); return; } if (d.kind === 'drive#folder') { if (!S.trashMode) { const isExitMode = S.isFlattened || S.dupMode; if (isExitMode) { S.isFlattened = false; S.dupMode = false; UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if(UI.dupTools) UI.dupTools.style.display = 'none'; if(UI.btnFolderFirst) UI.btnFolderFirst.style.display = 'flex'; if(UI.btnNewFolder) UI.btnNewFolder.style.display = 'flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if (d._lineage && d._lineage.length > 0) { const newPath = [{ id: '', name: L.btn_nav_home }]; d._lineage.forEach(n => { if (n.id && n.id !== 'root') newPath.push({ id: n.id, name: n.name }); }); if (newPath.length === 0 || newPath[newPath.length-1].id !== d.id) newPath.push({ id: d.id, name: d.name }); S.path = newPath; } else { S.path = [{ id: '', name: L.btn_nav_home }, { id: d.id, name: d.name }]; } load(); } else { const lastNode = S.path[S.path.length - 1]; if (lastNode && lastNode.id === d.id) return; S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); S.path.push(d); load(); } } } else { const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isArchive) handleOpenArchive(d); else if (t.isVideo) playVideo(d); else if (t.isImage) showImage(d); } }; row.onclick = async (e) => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } const nameText = e.target.closest('.pk-name .pk-name-txt'); const isAnalyzeRoot = S.analyzeMode && S.path[S.path.length - 1].id === 'analyze_root'; const isProtectedView = S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups); if (!S.trashMode && nameText) { if (isProtectedView && S.sel.size > 5) { if (S.sel.size > 1 || !S.sel.has(d.id)) { if (!await confirmSelectionClear()) return; } } S.sel.clear(); S.sel.add(d.id); S.activeId = d.id; triggerOpen(); renderVisible(); updateStat(); return; } S.activeId = d.id; if (e.target === chk) { if (e.shiftKey && S.lastSelIdx !== -1) { const startIdx = Math.min(S.lastSelIdx, i); const endIdx = Math.max(S.lastSelIdx, i); const targetState = chk.checked; for (let k = startIdx; k <= endIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) { if (targetState) S.sel.add(item.id); else S.sel.delete(item.id); } } } else { if (chk.checked) S.sel.add(d.id); else S.sel.delete(d.id); } } else { if (e.shiftKey && S.lastSelIdx !== -1) { const startIdx = Math.min(S.lastSelIdx, i); const endIdx = Math.max(S.lastSelIdx, i); for (let k = startIdx; k <= endIdx; k++) { const item = S.display[k]; if (item && !item.isHeader) S.sel.add(item.id); } } else if (e.ctrlKey || e.metaKey) { if (S.sel.has(d.id)) S.sel.delete(d.id); else S.sel.add(d.id); } else { if (isProtectedView && S.sel.size > 5) { if (S.sel.size > 1 || !S.sel.has(d.id)) { if (!await confirmSelectionClear()) { renderVisible(); return; } } } S.sel.clear(); S.sel.add(d.id); } } S.lastSelIdx = i; const rows = UI.in.children; for (let k = 0; k < rows.length; k++) { const r = rows[k]; if (r.dataset.id) { const rid = r.dataset.id; const isSelected = S.sel.has(rid); const isActiveRow = (S.activeId === rid); const isFocused = isSelected && isActiveRow; if (!(isActiveRow && !isSelected)) { r.style.backgroundColor = ''; r.style.border = ''; r.style.borderRadius = ''; } if (isActiveRow && !isSelected) { r.style.backgroundColor = 'var(--pk-sel-bg)'; r.style.border = '1px solid var(--pk-pri)'; r.style.borderRadius = '4px'; } let cls = 'pk-row'; if (isSelected) cls += ' sel'; if (isFocused) cls += ' pk-focused'; if (r.classList.contains('pk-moving')) cls += ' pk-moving'; if (r.className !== cls) r.className = cls; const cb = r.querySelector('input[type="checkbox"]'); if (cb && cb.checked !== isSelected) cb.checked = isSelected; } else if (r.classList.contains('pk-group-hd')) { const grpChk = r.querySelector('.pk-grp-chk'); if (grpChk) { const gNode = S.display[start + k]; if (gNode && gNode.isHeader) { const gIdx = parseInt(gNode.id.replace('grp_', '')); const gIds = S.dupMode ? (S.dupRawGroups[gIdx]?.ids || []) : (S.analyzeMode ? (S.analyzeSimGroups[gIdx]?.ids || []) : []); let selCount = 0; gIds.forEach(id => { if(S.sel.has(id)) selCount++; }); const isAll = gIds.length > 0 && selCount === gIds.length; const isInd = selCount > 0 && selCount < gIds.length; if (grpChk.checked !== isAll) grpChk.checked = isAll; if (grpChk.indeterminate !== isInd) grpChk.indeterminate = isInd; } } } } updateStat(); }; row.ondblclick = (e) => { e.preventDefault(); if (S.trashMode) return; if (S.uploadMode) { if (d.status !== 'DONE') return; triggerOpen(); return; } if (S.shareMode) { const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if (!isShareDisabled) { showShareDetail(d); } return; } const isNameTxt = e.target.closest('.pk-name .pk-name-txt'); if (e.target === chk || isNameTxt) return; if (S.offlineMode) { triggerOpen(); return; } if (d.kind === 'drive#folder') { const isExitMode = S.isFlattened || S.dupMode; if (isExitMode) { S.isFlattened = false; S.dupMode = false; if (S.filterState) S.filterState = { active: false, cat: 'all', ext: 'all' }; UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if(UI.dupTools) UI.dupTools.style.display = 'none'; if(UI.dupFilters) UI.dupFilters.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if(UI.btnNewFolder) UI.btnNewFolder.style.display = 'flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if (d._lineage && d._lineage.length > 0) { const newPath = [{ id: '', name: L.btn_nav_home }]; d._lineage.forEach(n => { if (n.id && n.id !== 'root') newPath.push({ id: n.id, name: n.name }); }); if (newPath.length === 0 || newPath[newPath.length-1].id !== d.id) { newPath.push({ id: d.id, name: d.name }); } S.path = newPath; } else { S.path = [{ id: '', name: L.btn_nav_home }, { id: d.id, name: d.name }]; } load(); } else { S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); S.path.push(d); load(); } } else { const mime = (d.mime_type || "").toLowerCase(); const name = (d.name || "").toLowerCase(); if (name.endsWith('.torrent')) { handleTorrentFile(d); } else if (mime.includes('zip') || mime.includes('rar') || mime.includes('7z') || mime.includes('compressed') || mime.includes('archive') || name.endsWith('.zip') || name.endsWith('.rar') || name.endsWith('.7z') || name.endsWith('.tar') || name.endsWith('.gz')) { handleOpenArchive(d); } else if (mime.startsWith('video')) { playVideo(d); } else if (mime.startsWith('image')) { showImage(d); } } }; row.oncontextmenu = (e) => { e.preventDefault(); if (!S.sel.has(d.id)) { S.sel.clear(); S.sel.add(d.id); S.lastSelIdx = i; renderVisible(); updateStat(); } const itms = { share: UI.ctx.querySelector('#ctx-share'), open: UI.ctx.querySelector('#ctx-open'), extPlay: UI.ctx.querySelector('#ctx-ext-play'), star: UI.ctx.querySelector('#ctx-star'), prop: UI.ctx.querySelector('#ctx-property'), locate: UI.ctx.querySelector('#ctx-locate'), down: UI.ctx.querySelector('#ctx-down'), cpName: UI.ctx.querySelector('#ctx-copy-name'), cut: UI.ctx.querySelector('#ctx-cut'), copy: UI.ctx.querySelector('#ctx-copy'), rename: UI.ctx.querySelector('#ctx-rename'), prune: UI.ctx.querySelector('#ctx-prune'), addBl: UI.ctx.querySelector('#ctx-add-bl'), del: UI.ctx.querySelector('#ctx-del'), restore: UI.ctx.querySelector('#ctx-restore'), delF: UI.ctx.querySelector('#ctx-del-forever'), shCancel: UI.ctx.querySelector('#ctx-share-cancel'), shDetail: UI.ctx.querySelector('#ctx-share-detail'), shCopy: UI.ctx.querySelector('#ctx-share-copy'), taskRetry: UI.ctx.querySelector('#ctx-task-retry') }; const seps = Array.from(UI.ctx.querySelectorAll('.pk-ctx-sep')); if (!itms.taskRetry) { const retryDiv = document.createElement('div'); retryDiv.className = 'pk-ctx-item'; retryDiv.id = 'ctx-task-retry'; retryDiv.innerHTML = `${CONF.icons.refresh} ${L.btn_retry_task}`; const ref = document.getElementById('ctx-down'); if (ref) ref.parentNode.insertBefore(retryDiv, ref); itms.taskRetry = retryDiv; } if (!UI.ctx.querySelector('#ctx-up-pause')) { const pBtn = document.createElement('div'); pBtn.className = 'pk-ctx-item'; pBtn.id = 'ctx-up-pause'; pBtn.innerHTML = `${CONF.icons.taskPause} ${L.btn_up_pause}`; const delRef = document.getElementById('ctx-del'); if (delRef) delRef.parentNode.insertBefore(pBtn, delRef); } if (!UI.ctx.querySelector('#ctx-up-start')) { const sBtn = document.createElement('div'); sBtn.className = 'pk-ctx-item'; sBtn.id = 'ctx-up-start'; sBtn.innerHTML = `${CONF.icons.taskStart} ${L.btn_up_start}`; const delRef = document.getElementById('ctx-del'); if (delRef) delRef.parentNode.insertBefore(sBtn, delRef); } const allCtxItems = UI.ctx.querySelectorAll('.pk-ctx-item'); const allCtxSeps = UI.ctx.querySelectorAll('.pk-ctx-sep'); allCtxItems.forEach(el => el.style.display = 'none'); allCtxSeps.forEach(el => el.style.display = 'none'); const sepShareExtra = UI.ctx.querySelector('#sep-share-extra'); if (S.shareMode) { const sh = { name: UI.ctx.querySelector('#ctx-copy-name'), cancel: UI.ctx.querySelector('#ctx-sh-cancel'), bl: UI.ctx.querySelector('#ctx-add-bl'), detail: UI.ctx.querySelector('#ctx-sh-detail'), copy: UI.ctx.querySelector('#ctx-sh-copy') }; const isSingle = (S.sel.size === 1); const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if(seps[0]) seps[0].style.display = 'none'; if(seps[1]) seps[1].style.display = 'none'; if(sh.name) sh.name.style.display = 'flex'; if(seps[2]) seps[2].style.display = 'block'; if(sh.cancel) sh.cancel.style.display = 'flex'; if(sh.bl) { sh.bl.style.display = 'flex'; let isRemoveMode = false; for (const id of S.sel) { const target = S.itemMap.get(id); if (target && S.blSet.has((target.name || target.title || "").toLowerCase().trim())) { isRemoveMode = true; break; } } sh.bl.innerHTML = isRemoveMode ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; sh.bl.setAttribute('data-action', isRemoveMode ? 'remove' : 'add'); } const showDetails = isSingle && !isShareDisabled; if (showDetails) { if(sepShareExtra) sepShareExtra.style.display = 'block'; if(sh.detail) sh.detail.style.display = 'flex'; if(sh.copy) sh.copy.style.display = 'flex'; } if(seps[3]) seps[3].style.display = 'none'; } else if (S.uploadMode) { const upEls = { open: UI.ctx.querySelector('#ctx-open'), ext: UI.ctx.querySelector('#ctx-ext-play'), loc: UI.ctx.querySelector('#ctx-locate'), name: UI.ctx.querySelector('#ctx-copy-name'), del: UI.ctx.querySelector('#ctx-del'), pause: UI.ctx.querySelector('#ctx-up-pause'), start: UI.ctx.querySelector('#ctx-up-start'), sep1: UI.ctx.querySelector('#sep-1'), sep2: UI.ctx.querySelector('#sep-2'), sep3: UI.ctx.querySelector('#sep-3') }; if (S.sel.size > 1) { const items = Array.from(S.sel).map(id => S.itemMap.get(id)).filter(Boolean); let hasActive = false, hasPaused = false, hasDone = false; items.forEach(t => { if (['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(t.status)) hasActive = true; else if (['PAUSED', 'ERROR'].includes(t.status)) hasPaused = true; else if (t.status === 'DONE') hasDone = true; }); upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; if (hasActive && !hasPaused && !hasDone) { if (upEls.sep1) { upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; } if (upEls.pause) { upEls.pause.style.order = '20'; upEls.pause.style.display = 'flex'; upEls.pause.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpPause) UI.btnUpPause.click(); }; } } else if (!hasActive && hasPaused && !hasDone) { if (upEls.sep1) { upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; } if (upEls.start) { upEls.start.style.order = '20'; upEls.start.style.display = 'flex'; upEls.start.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpStart) UI.btnUpStart.click(); }; } } else { } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else { const mime = (d.mime_type || '').toLowerCase(); const isImg = mime.startsWith('image/'); const isVideo = mime.startsWith('video/') || ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp'].some(e => (d.name||'').toLowerCase().endsWith('.'+e)); const isActive = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(d.status); const isDone = d.status === 'DONE'; if (isDone) { upEls.open.style.order = '10'; upEls.open.style.display = (isVideo || isImg) ? 'flex' : 'none'; upEls.ext.style.order = '11'; upEls.ext.style.display = isVideo ? 'flex' : 'none'; upEls.loc.style.order = '12'; upEls.loc.style.display = d.file_id ? 'flex' : 'none'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; upEls.name.style.order = '20'; upEls.name.style.display = 'flex'; upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else if (isActive) { upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; if (upEls.pause) { upEls.pause.style.order = '20'; upEls.pause.style.display = 'flex'; upEls.pause.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpPause) UI.btnUpPause.click(); }; } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else { upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; if (upEls.start) { upEls.start.style.order = '20'; upEls.start.style.display = 'flex'; upEls.start.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpStart) UI.btnUpStart.click(); }; } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } } } else if (S.offlineMode) { const els = { open: UI.ctx.querySelector('#ctx-open'), ext: UI.ctx.querySelector('#ctx-ext-play'), loc: UI.ctx.querySelector('#ctx-locate'), prop: UI.ctx.querySelector('#ctx-property'), name: UI.ctx.querySelector('#ctx-copy-name'), retry: UI.ctx.querySelector('#ctx-task-retry'), link: UI.ctx.querySelector('#ctx-copy-link'), del: UI.ctx.querySelector('#ctx-del'), bl: UI.ctx.querySelector('#ctx-add-bl'), sep1: UI.ctx.querySelector('#sep-1'), sep2: UI.ctx.querySelector('#sep-2'), sep3: UI.ctx.querySelector('#sep-3') }; const isSingle = S.sel.size === 1; const ids = Array.from(S.sel); const firstItem = S.itemMap.get(ids[0]); const hasFailed = ids.some(id => S.itemMap.get(id)?.phase === 'PHASE_TYPE_ERROR'); let isVid = false, isImg = false; if (firstItem && firstItem.file_id) { const m = (firstItem.mime_type || '').toLowerCase(); const n = (firstItem.name || '').toLowerCase(); isVid = m.startsWith('video/') || ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp'].some(e => n.endsWith('.'+e)); isImg = m.startsWith('image/'); } let g1 = 0; if (els.open) { els.open.style.order = '10'; const canOpen = isSingle && firstItem?.file_id && firstItem.phase !== 'PHASE_TYPE_ERROR' && (isVid || isImg); els.open.style.display = canOpen ? 'flex' : 'none'; if(canOpen) g1++; } if (els.ext) { els.ext.style.order = '11'; const canExt = isSingle && firstItem?.file_id && firstItem.phase !== 'PHASE_TYPE_ERROR' && isVid; els.ext.style.display = canExt ? 'flex' : 'none'; if(canExt) g1++; } if (els.prop) { els.prop.style.order = '12'; els.prop.style.display = isSingle ? 'flex' : 'none'; if(isSingle) g1++; } if (els.loc) { els.loc.style.order = '13'; const canLocate = isSingle && firstItem?.file_id && firstItem.phase !== 'PHASE_TYPE_ERROR'; els.loc.style.display = canLocate ? 'flex' : 'none'; if(canLocate) g1++; } let g2 = 0; if (els.name) { els.name.style.order = '20'; els.name.style.display = 'flex'; g2++; } let g3 = 0; if (els.retry) { els.retry.style.order = '30'; els.retry.innerHTML = `${CONF.icons.retry} ${L.btn_retry_task}`; els.retry.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if (UI.btnRetryTask) UI.btnRetryTask.click(); }; els.retry.style.display = hasFailed ? 'flex' : 'none'; if(hasFailed) g3++; } if (els.link) { els.link.style.order = '31'; els.link.style.display = 'flex'; els.link.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; const tasks = ids.map(id => S.itemMap.get(id)).filter(t => t && (t.source_url || t.params?.url)); if (tasks.length) { GM_setClipboard(tasks.map(t => t.source_url || t.params.url).join('\n')); showToast(L.msg_copy_success); } }; g3++; } let g4 = 0; if (els.del) { els.del.style.order = '40'; els.del.style.display = 'flex'; els.del.innerHTML = `${CONF.icons.del} ${L.btn_del}`; els.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; UI.btnDel.click(); }; g4++; } if (els.bl) { els.bl.style.order = '41'; els.bl.style.display = 'flex'; let isRemove = false; for (const id of ids) { const t = S.itemMap.get(id); if (t && S.blSet.has((t.name||'').toLowerCase().trim())) { isRemove = true; break; } } els.bl.innerHTML = isRemove ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; els.bl.setAttribute('data-action', isRemove ? 'remove' : 'add'); g4++; } if (els.sep1) { els.sep1.style.order = '15'; els.sep1.style.display = (g1 > 0 && (g2 > 0 || g3 > 0 || g4 > 0)) ? 'block' : 'none'; } if (els.sep2) { els.sep2.style.order = '25'; els.sep2.style.display = (g2 > 0 && (g3 > 0 || g4 > 0)) ? 'block' : 'none'; } if (els.sep3) { els.sep3.style.order = '35'; els.sep3.style.display = (g3 > 0 && g4 > 0) ? 'block' : 'none'; } } else { if(sepShareExtra) sepShareExtra.style.display = 'none'; if (S.trashMode) { const tr = { cpName: UI.ctx.querySelector('#ctx-copy-name'), restore: UI.ctx.querySelector('#ctx-restore'), delF: UI.ctx.querySelector('#ctx-del-forever'), addBl: UI.ctx.querySelector('#ctx-add-bl'), sep1: UI.ctx.querySelector('#sep-trash-1'), sep2: UI.ctx.querySelector('#sep-trash-2') }; if(tr.cpName) tr.cpName.style.display = 'flex'; if(tr.sep1) tr.sep1.style.display = 'block'; if(tr.restore) tr.restore.style.display = 'flex'; if(tr.sep2) tr.sep2.style.display = 'block'; if(tr.delF) tr.delF.style.display = 'flex'; if(tr.addBl) { tr.addBl.style.display = 'flex'; let isRemoveMode = false; for (const id of S.sel) { const item = S.itemMap.get(id); if (item && (item.kind === 'drive#folder' ? S.blFolderSet.has(item.name.toLowerCase().trim()) : S.blSet.has(item.name.toLowerCase().trim()))) { isRemoveMode = true; break; } } tr.addBl.innerHTML = isRemoveMode ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; tr.addBl.setAttribute('data-action', isRemoveMode ? 'remove' : 'add'); } }else { if(itms.share) itms.share.style.display = 'flex'; if(seps[0]) seps[0].style.display = 'block'; let hasGroup2 = false; if (S.sel.size === 1) { [itms.open, itms.prop].forEach(el => { if(el) el.style.display = 'flex'; }); const isVideo = (d.mime_type || '').startsWith('video/') || (d.params && d.params.duration > 0); if (itms.extPlay) itms.extPlay.style.display = isVideo ? 'flex' : 'none'; hasGroup2 = true; } if (itms.star) { let hasUnstarred = false; let hasValidItem = false; for (const id of S.sel) { const it = S.itemMap.get(id); if (it && !isSystemItem(it)) { hasValidItem = true; if (!(it.starred || (it.tags && it.tags.some(t => t.name === 'STAR')))) { hasUnstarred = true; break; } } } itms.star.style.display = hasValidItem ? 'flex' : 'none'; if (hasValidItem) { hasGroup2 = true; itms.star.innerHTML = hasUnstarred ? `${ctxIcons.star} ${L.ctx_star}` : `${ctxIcons.unstar} ${L.ctx_unstar}`; itms.star.setAttribute('data-action', hasUnstarred ? 'star' : 'unstar'); } } const isSearchRoot = S.path.length > 0 && S.path[S.path.length - 1].id === 'virtual_search_root'; const canLocateUpload = S.uploadMode && d.status === 'DONE' && d.file_id; if((S.starredMode || S.recentMode || S.historyMode || isSearchRoot || S.dupMode || S.isFlattened || S.analyzeMode || canLocateUpload) && S.sel.size === 1) { itms.locate.style.display = 'flex'; hasGroup2 = true; } if(seps[1]) seps[1].style.display = hasGroup2 ? 'block' : 'none'; [itms.down, itms.cpName].forEach(el => { if(el) el.style.display = 'flex'; }); if (S.historyMode) { if(seps[2]) seps[2].style.display = 'none'; [itms.cut, itms.copy, itms.rename].forEach(el => { if(el) el.style.display = 'none'; }); } else { if(seps[2]) seps[2].style.display = 'block'; } if (S.historyMode) { if(seps[3]) seps[3].style.display = 'block'; } else { if (!isProtected) { if(itms.cut) itms.cut.style.display = 'flex'; if(itms.rename) { itms.rename.style.display = 'flex'; itms.rename.innerHTML = (S.sel.size > 1) ? `${ctxIcons.renameBulk} ${L.btn_bulkrename}` : `${ctxIcons.rename} ${L.ctx_rename}`; } } if(itms.copy) itms.copy.style.display = 'flex'; let hasFolder = false; for (const id of S.sel) { if (S.itemMap.get(id)?.kind === 'drive#folder') { hasFolder = true; break; } } if(itms.prune && hasFolder) itms.prune.style.display = 'flex'; if(seps[3]) seps[3].style.display = 'block'; } if (itms.addBl) { itms.addBl.style.display = 'flex'; S.updateBlCache(); let isRemoveMode = false; for (const id of S.sel) { const item = S.itemMap.get(id); if (!item) continue; const cleanName = (item.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (item.kind === 'drive#folder') { if (S.blFolderSet.has(cleanName)) { isRemoveMode = true; break; } } else { if (S.blSet.has(cleanName)) { isRemoveMode = true; break; } } } itms.addBl.innerHTML = isRemoveMode ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; itms.addBl.setAttribute('data-action', isRemoveMode ? 'remove' : 'add'); } if (itms.del) itms.del.style.display = 'flex'; } } const useFlexOrder = S.offlineMode || S.uploadMode; UI.ctx.style.display = useFlexOrder ? 'flex' : 'block'; if (useFlexOrder) UI.ctx.style.flexDirection = 'column'; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let x = e.clientX / scale, y = e.clientY / scale, w = 160, h = UI.ctx.offsetHeight || 280; let winW = window.innerWidth / scale, winH = window.innerHeight / scale; if (x + w > winW) x = winW - w - 10; if (y + h > winH) y = winH - h - 10; UI.ctx.style.left = x + 'px'; UI.ctx.style.top = y + 'px'; }; } fragment.appendChild(row); } UI.in.innerHTML = ''; UI.in.appendChild(fragment); if (window.pkRefreshTooltip) { requestAnimationFrame(() => { window.pkRefreshTooltip(); }); } } let isMarquee = false, mqStartX = 0, mqStartY = 0, startScroll = 0, lastMouseX = 0, lastMouseY = 0, scrollSpeed = 0, scrollRaf = null; let lastRngS = -1, lastRngE = -1, cachedVpRect = null; const mqBox = document.createElement('div'); mqBox.className = 'pk-selection-box'; el.appendChild(mqBox); const updateMarqueeUIAndSelection = (targetX, targetY) => { if (!cachedVpRect) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const cssTargetX = targetX / scale; const cssTargetY = targetY / scale; const cssRectTop = cachedVpRect.top / scale; const cssRectBottom = cachedVpRect.bottom / scale; const cssRectLeft = cachedVpRect.left / scale; const cssMqStartX = mqStartX / scale; const cssMqStartY = mqStartY / scale; const curScroll = UI.vp.scrollTop; const clampedY = Math.max(cssRectTop, Math.min(cssRectBottom, cssTargetY)); const logicA = cssMqStartY - cssRectTop + startScroll; const logicB = clampedY - cssRectTop + curScroll; const logTop = Math.max(0, Math.min(logicA, logicB)); const logBot = Math.max(logicA, logicB); const visTop = logTop - curScroll + cssRectTop; const visBot = logBot - curScroll + cssRectTop; const clipT = Math.max(cssRectTop, visTop), clipB = Math.min(cssRectBottom, visBot); const drawH = Math.max(0, clipB - clipT); const safeRight = cssRectLeft + UI.vp.clientWidth; const clampedX = Math.max(cssRectLeft, Math.min(safeRight, cssTargetX)); const drawL = Math.min(cssMqStartX, clampedX); const drawW = Math.abs(clampedX - cssMqStartX); mqBox.style.borderTopWidth = (visTop < cssRectTop) ? '0' : '1px'; mqBox.style.borderBottomWidth = (visBot > cssRectBottom) ? '0' : '1px'; mqBox.style.borderLeftWidth = (cssTargetX < cssRectLeft) ? '0' : '1px'; mqBox.style.borderRightWidth = (cssTargetX > safeRight) ? '0' : '1px'; Object.assign(mqBox.style, { display: drawH > 0 ? 'block' : 'none', width: drawW + 'px', height: drawH + 'px', transform: `translate3d(${drawL}px, ${clipT}px, 0)` }); const sIdx = Math.floor(logTop / CONF.rowHeight), eIdx = Math.min(S.display.length - 1, Math.floor(logBot / CONF.rowHeight)); if (sIdx !== lastRngS || eIdx !== lastRngE) { lastRngS = sIdx; lastRngE = eIdx; if (!window.event?.ctrlKey && !window.event?.metaKey) S.sel.clear(); for (let k = sIdx; k <= eIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) S.sel.add(item.id); } renderVisible(); updateStat(); } }; const runAutoScroll = () => { if (!isMarquee || scrollSpeed === 0) { scrollRaf = null; return; } UI.vp.scrollTop += scrollSpeed; updateMarqueeUIAndSelection(lastMouseX, lastMouseY); scrollRaf = requestAnimationFrame(runAutoScroll); }; const handleMarqueeMove = (e) => { if (!cachedVpRect) return; if (!isMarquee) { const moveX = Math.abs(e.clientX - mqStartX); const moveY = Math.abs(e.clientY - mqStartY); if (moveX > 5 || moveY > 5) { isMarquee = true; UI.win.classList.add('pk-is-seeking'); } else { return; } } lastMouseX = e.clientX; lastMouseY = e.clientY; if (e.clientY > cachedVpRect.bottom - 5) scrollSpeed = Math.min(45, 2 + Math.pow((e.clientY - cachedVpRect.bottom + 5) / 5, 1.3)); else if (e.clientY < cachedVpRect.top + 5) scrollSpeed = -Math.min(45, 2 + Math.pow((cachedVpRect.top + 5 - e.clientY) / 5, 1.3)); else scrollSpeed = 0; if (scrollSpeed !== 0 && !scrollRaf) scrollRaf = requestAnimationFrame(runAutoScroll); updateMarqueeUIAndSelection(e.clientX, e.clientY); }; const stopMarquee = () => { isMarquee = false; scrollSpeed = 0; if (UI.win) UI.win.classList.remove('pk-is-seeking'); lastRngS = -1; lastRngE = -1; cachedVpRect = null; if (mqBox) { mqBox.style.display = 'none'; mqBox.style.transform = 'none'; mqBox.style.width = '0px'; mqBox.style.height = '0px'; } if (scrollRaf) cancelAnimationFrame(scrollRaf); scrollRaf = null; window.removeEventListener('mousemove', handleMarqueeMove); window.removeEventListener('mouseup', stopMarquee); }; let isFileDragging = false; let fileDragGhost = null; let dropTargetId = null; let dropTargetType = null; let autoOpenTimer = null; let lastHoverSep = null; const handleFileDragMove = (e) => { if (!isFileDragging) { const moveX = Math.abs(e.clientX - mqStartX); const moveY = Math.abs(e.clientY - mqStartY); if (moveX > 5 || moveY > 5) { isFileDragging = true; document.body.classList.add('pk-dragging'); fileDragGhost = document.createElement('div'); fileDragGhost.className = 'pk-drag-ghost'; if (el.classList.contains('pk-dark')) fileDragGhost.classList.add('pk-dark'); const count = S.sel.size; const firstId = Array.from(S.sel)[0]; const firstItem = S.itemMap.get(firstId); let text = firstItem ? firstItem.name : 'Selected Items'; if (count > 1) { text += L.str_drag_files.replace('{n}', count); } let dragIcon = CONF.typeIcons.folder.replace('30','20').replace('30','20'); if (firstItem && firstItem.icon_link) { dragIcon = `<img src="${firstItem.icon_link}" style="width:24px; height:24px; object-fit:contain; margin-right:8px; pointer-events:none;">`; } fileDragGhost.innerHTML = `${dragIcon}<span>${esc(text)}</span>`; document.body.appendChild(fileDragGhost); } else { return; } } if (fileDragGhost) { const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; fileDragGhost.style.zoom = 'var(--pk-zoom, 1)'; fileDragGhost.style.left = (e.clientX / scale) + 'px'; fileDragGhost.style.top = (e.clientY / scale) + 'px'; } const prevTargets = document.querySelectorAll('.pk-drop-target'); prevTargets.forEach(el => el.classList.remove('pk-drop-target')); dropTargetId = null; dropTargetType = null; if (fileDragGhost) fileDragGhost.style.display = 'none'; const elUnder = document.elementFromPoint(e.clientX, e.clientY); if (fileDragGhost) fileDragGhost.style.display = 'flex'; if (!elUnder) return; const menuItem = elUnder.closest('.pk-crumb-item'); if (menuItem && menuItem.dataset.id) { const tid = menuItem.dataset.id; const currentFolderId = S.path[S.path.length - 1]?.id || ''; if (tid !== currentFolderId && tid !== 'loading' && tid !== 'error') { dropTargetId = tid; dropTargetType = 'menu_item'; menuItem.classList.add('pk-drop-target'); return; } } const sep = elUnder.closest('.pk-crumb-sep'); if (sep) { if (lastHoverSep !== sep) { lastHoverSep = sep; if (autoOpenTimer) clearTimeout(autoOpenTimer); autoOpenTimer = setTimeout(() => { if (isFileDragging) sep.click(); }, 500); } } else { lastHoverSep = null; if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } } const row = elUnder.closest('.pk-row'); if (row && row.dataset.id) { const tid = row.dataset.id; const item = S.itemMap.get(tid); if (item && item.kind === 'drive#folder' && !S.sel.has(tid)) { dropTargetId = tid; dropTargetType = 'row'; row.classList.add('pk-drop-target'); return; } } const crumbItem = elUnder.closest('#pk-crumb span'); if (crumbItem && !crumbItem.classList.contains('pk-crumb-sep') && crumbItem.dataset.id) { const tid = crumbItem.dataset.id === 'root' ? '' : crumbItem.dataset.id; const currentFolderId = S.path[S.path.length - 1]?.id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; if (normalize(tid) !== normalize(currentFolderId)) { dropTargetId = tid; dropTargetType = 'crumb'; crumbItem.classList.add('pk-drop-target'); } } }; const executeFileTransfer = async (items, opType, sourcePid, targetPid, targetNameForLog) => { if (!items || items.length === 0) return; if (sourcePid !== '__VIRTUAL__' && sourcePid === targetPid) { showToast(L.err_paste_descendant, 'error'); return; } const foldersToMoveIds = items.filter(it => it.kind === 'drive#folder').map(it => it.id); if (foldersToMoveIds.length > 0) { const isDescendant = (targetId, ancestorIds) => { let currentId = targetId; let safety = 100; while (currentId && currentId !== 'root' && safety > 0) { if (ancestorIds.includes(currentId)) return true; const parent = globalParentIndex.get(currentId); currentId = parent ? parent.id : null; safety--; } return false; }; if (isDescendant(targetPid, foldersToMoveIds)) { showToast(L.err_paste_descendant, 'error'); return; } } S.movingSourceId = sourcePid || 'root'; S.movingDestId = targetPid || 'root'; const allIds = items.map(it => it.id); allIds.forEach(id => S.movingIds.add(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_ADD', ids: allIds, src: S.movingSourceId, dst: S.movingDestId }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); isGUISensitive = true; const progressTask = FloatBarManager.create(`${opType === 'move' ? L.str_moving : L.str_copying}...`); const updateFloat = progressTask.update; const BATCH_SIZE = 500; let processedCount = 0; const total = items.length; const endpoint = opType === 'move' ? 'files:batchMove' : 'files:batchCopy'; const actionText = opType === 'move' ? L.str_moving : L.str_copying; refresh(); try { for (let i = 0; i < total; i += BATCH_SIZE) { const chunk = items.slice(i, i + BATCH_SIZE); const chunkIds = chunk.map(it => it.id); updateFloat(`${actionText} ${processedCount}/${total} -> ${esc(targetNameForLog || 'Target')}`); const apiTargetPid = (targetPid === 'root') ? '' : targetPid; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/${endpoint}`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunkIds, to: { parent_id: apiTargetPid } }) }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error_description || `API ${res.status}`); if (data.task_id) { await new Promise(resolve => { const checkTask = async () => { try { const tRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${data.task_id}`, { headers: getHeaders() }); const tData = await tRes.json(); if (tData.phase === 'PHASE_TYPE_COMPLETE' || tData.phase === 'PHASE_TYPE_ERROR') resolve(); else setTimeout(checkTask, 800); } catch { resolve(); } }; checkTask(); }); } else if (opType === 'move' && chunkIds.length > 0) { const lastId = chunkIds[chunkIds.length - 1]; let retry = 0; while (retry < 20) { try { const meta = await apiGet(lastId); if (meta.parent_id === targetPid || meta.trashed) break; } catch(e) { break; } await sleep(500); retry++; } } chunkIds.forEach(id => S.movingIds.delete(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: chunkIds }); if (opType === 'move') { const movedSet = new Set(chunkIds); S.items = S.items.filter(it => !movedSet.has(it.id)); if (typeof globalCache !== 'undefined') { const cleanListChunk = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !movedSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !movedSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { globalCache.set(key, cleanListChunk(globalCache.get(key))); } for (const key of S.cache.keys()) { S.cache.set(key, cleanListChunk(S.cache.get(key))); } } if (typeof pkState !== 'undefined' && pkState && pkState.lastGlobalResults) { pkState.lastGlobalResults = pkState.lastGlobalResults.filter(x => !movedSet.has(x.id)); } } else { if (targetPid === (S.path[S.path.length-1].id||'')) { const newItems = (data.files || []).map(f => minifyFile(f, false)); S.items.unshift(...newItems); } } if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); refresh(); processedCount += chunk.length; } if (targetPid) gmSet('pk_fmod_' + targetPid, new Date(getServerNow()).toISOString()); if (S.analyzeMap) { const deltaSize = items.reduce((acc, it) => acc + parseInt(it.size || 0), 0); if (deltaSize > 0) { const updateAnalyzeChain = (startId, isAdd) => { let currId = startId; let safety = 50; while (currId && S.analyzeMap.has(currId) && safety > 0) { const node = S.analyzeMap.get(currId); const oldSize = parseInt(node.size || 0); node.size = isAdd ? (oldSize + deltaSize) : Math.max(0, oldSize - deltaSize); currId = node.parentId; safety--; } }; if (S.analyzeMap.has(targetPid)) updateAnalyzeChain(targetPid, true); if (opType === 'move' && S.analyzeMap.has(sourcePid)) updateAnalyzeChain(sourcePid, false); if (S.analyzeResultItems) { S.analyzeResultItems.forEach(resItem => { if (S.analyzeMap.has(resItem.id)) { resItem.size = S.analyzeMap.get(resItem.id).size.toString(); } }); } } } const keysToClear = [sourcePid || 'root', targetPid || 'root']; keysToClear.forEach(k => { S.cache.delete(k); if (typeof globalCache !== 'undefined') globalCache.delete(k); if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(k); if (k === 'root') globalDirtyFolders.add(''); } }); if (opType === 'move') { const purgeMovedDescendants = (fid) => { if (typeof globalLineageMap !== 'undefined') globalLineageMap.delete(fid); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(fid); const data = (typeof globalCache !== 'undefined' ? globalCache.get(fid) : null) || (S.cache ? S.cache.get(fid) : null); if (data) { const list = Array.isArray(data) ? data : (data.items || []); list.forEach(child => { if (child.kind === 'drive#folder') { purgeMovedDescendants(child.id); } }); if (typeof globalCache !== 'undefined') globalCache.delete(fid); if (S.cache) S.cache.delete(fid); } }; items.forEach(it => { if (it.kind === 'drive#folder') purgeMovedDescendants(it.id); }); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); showToast(opType === 'move' ? L.msg_move_done : L.msg_copy_success); } catch (e) { showToast(`${L.str_error}: ${e.message}`, 'error'); load(false, true); } finally { S.movingIds.clear(); S.movingSourceId = null; S.movingDestId = null; if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_CLR', ids: [] }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); isGUISensitive = false; if (progressTask) progressTask.destroy(); updateStat(); } }; const stopFileDrag = async () => { if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } lastHoverSep = null; window.removeEventListener('mousemove', handleFileDragMove); window.removeEventListener('mouseup', stopFileDrag); document.body.classList.remove('pk-dragging'); if (fileDragGhost) fileDragGhost.remove(); fileDragGhost = null; let targetName = "Folder"; if (dropTargetType === 'menu_item') { const activeItem = document.querySelector('.pk-crumb-item.pk-drop-target span'); if (activeItem) targetName = activeItem.textContent; } else if (dropTargetType === 'row') { targetName = S.itemMap.get(dropTargetId)?.name || "Folder"; } else if (dropTargetType === 'crumb') { const pathNode = S.path.find(p => (p.id || 'root') === (dropTargetId || 'root')); targetName = pathNode ? pathNode.name : "Parent"; } const targets = document.querySelectorAll('.pk-drop-target'); targets.forEach(t => t.classList.remove('pk-drop-target')); document.querySelectorAll('.pk-crumb-pop').forEach(pop => { if (typeof pop._cleanup === 'function') pop._cleanup(); else pop.remove(); }); UI.crumb.querySelectorAll('.pk-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); sep.innerHTML = CONF.crumbIcons.right; }); if (!isFileDragging || dropTargetId === null) { isFileDragging = false; return; } isFileDragging = false; const currentFolderId = S.path[S.path.length - 1]?.id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; if (normalize(dropTargetId) === normalize(currentFolderId)) return; let hasSystem = false; for (const id of S.sel) { const item = S.itemMap.get(id); if (item && isSystemItem(item)) { hasSystem = true; break; } } if (hasSystem) { showToast(`${L.msg_sys_error}`, 'error'); return; } const rawItems = []; const validItems = []; let skipLocked = 0; let skipSelf = 0; let skipConflict = 0; const isDescendantConflict = (candidateId) => { if (!S.movingIds || S.movingIds.size === 0) return false; const hotspots = [S.movingSourceId, S.movingDestId].filter(x => x && x !== 'root'); if (hotspots.length === 0) return false; for (const startPoint of hotspots) { let curr = startPoint; let safety = 50; while (curr && curr !== 'root' && safety > 0) { if (curr === candidateId) return true; const parentInfo = globalParentIndex.get(curr); curr = parentInfo ? parentInfo.id : null; safety--; } } return false; }; S.sel.forEach(id => { const item = S.itemMap.get(id); if (!item) return; rawItems.push(item); if (S.movingIds.has(id)) { skipLocked++; return; } const targetId = normalize(dropTargetId); const selfId = normalize(item.id); const parentId = normalize(item.parent_id); if (targetId === selfId || targetId === parentId) { skipSelf++; return; } if (item.kind === 'drive#folder' && isDescendantConflict(item.id)) { skipConflict++; return; } validItems.push(item); }); if (skipLocked > 0 || skipSelf > 0 || skipConflict > 0) { const reasons = []; if (skipLocked) reasons.push(L.msg_skip_locked.replace('{n}', skipLocked)); if (skipSelf) reasons.push(L.msg_skip_self.replace('{n}', skipSelf)); if (skipConflict) reasons.push(L.msg_skip_conflict.replace('{n}', skipConflict)); showToast(`${L.msg_skip_invalid} ${reasons.join(', ')}`, 'warning'); } S.clearSelection(); refresh(); if (validItems.length > 0) { await executeFileTransfer( validItems, 'move', normalize(currentFolderId), normalize(dropTargetId), targetName ); } }; UI.vp.addEventListener('mousedown', (e) => { if (e.button !== 0 || S.loading || e.detail > 1) return; if (e.target.closest('.pk-btn, .pk-star-icon, input')) return; const row = e.target.closest('.pk-row'); const isClickingSelected = row && row.dataset.id && S.sel.has(row.dataset.id); if (isClickingSelected && !S.trashMode && !S.shareMode && !S.offlineMode && !S.historyMode && !S.starredMode && !S.recentMode && !S.isFlattened && !S.dupMode && !S.analyzeMode) { isFileDragging = false; mqStartX = e.clientX; mqStartY = e.clientY; window.addEventListener('mousemove', handleFileDragMove); window.addEventListener('mouseup', stopFileDrag); return; } cachedVpRect = UI.vp.getBoundingClientRect(); const safeRight = cachedVpRect.left + UI.vp.clientWidth; const safeBottom = cachedVpRect.top + UI.vp.clientHeight; if (e.clientX > safeRight || e.clientY > safeBottom) return; mqStartX = Math.max(cachedVpRect.left, Math.min(safeRight, e.clientX)); mqStartY = e.clientY; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const startOffsetY = (e.clientY - cachedVpRect.top) / scale + UI.vp.scrollTop; const startIdx = Math.floor(startOffsetY / CONF.rowHeight); if (startIdx >= 0 && startIdx < S.display.length) { const startItem = S.display[startIdx]; S.activeId = (startItem && !startItem.isHeader) ? startItem.id : null; } else { S.activeId = null; } isMarquee = false; mqStartX = lastMouseX = e.clientX; mqStartY = lastMouseY = e.clientY; startScroll = UI.vp.scrollTop; window.addEventListener('mousemove', handleMarqueeMove); window.addEventListener('mouseup', stopMarquee); }); let isScrollScheduled = false; UI.vp.onscroll = () => { if (UI.ctx && UI.ctx.style.display !== 'none') UI.ctx.style.display = 'none'; if (!isScrollScheduled) { requestAnimationFrame(() => { renderVisible(); if (typeof isMarquee !== 'undefined' && isMarquee) { updateMarqueeUIAndSelection(lastMouseX, lastMouseY); } isScrollScheduled = false; }); isScrollScheduled = true; } }; UI.vp.addEventListener('wheel', () => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } }, { capture: true, passive: true }); const handleBlankClick = async (e) => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } if (e.target.closest('.pk-nav-btn') || e.target.closest('.pk-btn') || e.target.closest('.pk-btn-toggle') || e.target.closest('input') || e.target.closest('textarea') || e.target.closest('select') || e.target.closest('.pk-nav span') || e.target.closest('.pk-crumb-sep') || e.target.closest('.pk-search') || e.target.closest('label') || e.target.closest('.pk-dropdown-wrap') || e.target.closest('.pk-sort-opt') || e.target.closest('a') ) return; if (S.sel.size > 0 || S.activeId) { const isAnalyzeRoot = S.analyzeMode && S.path[S.path.length - 1].id === 'analyze_root'; if ((S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups)) && S.sel.size > 5) { if (!await confirmSelectionClear()) return; } S.clearSelection(); renderVisible(); requestAnimationFrame(() => { if (UI.chkAll) UI.chkAll.checked = false; }); } }; const sidebarArea = el.querySelector('.pk-sidebar'); if (sidebarArea) sidebarArea.onclick = handleBlankClick; const footerArea = el.querySelector('.pk-ft'); if (footerArea) footerArea.onclick = handleBlankClick; const headerArea = el.querySelector('.pk-hd'); if (headerArea) headerArea.onclick = handleBlankClick; const topBarArea = el.querySelector('#pk-top-bar'); if (topBarArea) topBarArea.onclick = handleBlankClick; const actionArea = el.querySelector('#pk-actionbar'); if (actionArea) actionArea.onclick = handleBlankClick; const trashArea = el.querySelector('#pk-trash-bar'); if (trashArea) trashArea.onclick = handleBlankClick; if (UI.vp) { UI.vp.addEventListener('click', (e) => { if (e.target.closest('.pk-row')) return; const rect = UI.vp.getBoundingClientRect(); if (e.clientX > rect.left + UI.vp.clientWidth || e.clientY > rect.top + UI.vp.clientHeight) return; if (Math.abs(e.clientX - mqStartX) > 5 || Math.abs(e.clientY - mqStartY) > 5) return; handleBlankClick(e); }); } const showCrumbDropdown = async (e, parentId, triggerEl) => { if (triggerEl.classList.contains('pk-active')) { const oldPop = document.getElementById('pk-main-crumb-pop'); if (oldPop) oldPop.remove(); triggerEl.classList.remove('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.right; return; } let targetId = parentId || 'root'; if (targetId === 'root' || targetId === '') { if (S.shareMode) targetId = 'share_root'; else if (S.starredMode) targetId = 'starred_root'; else targetId = 'root'; } let folders = []; if (targetId === 'virtual_search_root' || targetId === 'analyze_root' || targetId === 'recent_root') { let sourceItems = []; if (targetId === 'analyze_root') { sourceItems = (S.analyzeResultItems && S.analyzeResultItems.length > 0) ? S.analyzeResultItems : S.items; } else if (targetId === 'recent_root') { if (S.recentResultItems && S.recentResultItems.length > 0) { sourceItems = S.recentResultItems; } else if (typeof globalCache !== 'undefined' && globalCache.has('recent_root')) { const cached = globalCache.get('recent_root'); sourceItems = Array.isArray(cached) ? cached : (cached.items || []); } else { sourceItems = S.items || []; } } else { sourceItems = (S.lastGlobalResults && S.lastGlobalResults.length > 0) ? S.lastGlobalResults : S.items; } folders = sourceItems.filter(f => f.kind === 'drive#folder'); if (folders.length === 0) { const pop = document.createElement('div'); pop.className = 'pk-crumb-pop'; pop.innerHTML = `<div class="pk-crumb-item" style="color:#888;justify-content:center;">${L.str_no_files}</div>`; el.appendChild(pop); const rect = triggerEl.getBoundingClientRect(); pop.style.left = (rect.left - 4) + 'px'; pop.style.top = (rect.bottom + 6) + 'px'; pop.classList.add('pk-show'); setTimeout(() => { const closer = (evt) => { pop.remove(); document.removeEventListener('mousedown', closer); }; document.addEventListener('mousedown', closer); }, 10); return; } } else { const rawCached = (typeof globalCache !== 'undefined' && globalCache.has(targetId)) ? globalCache.get(targetId) : S.cache.get(targetId); const cachedItems = (rawCached && !Array.isArray(rawCached) && rawCached.items) ? rawCached.items : (Array.isArray(rawCached) ? rawCached : null); if (cachedItems && cachedItems.length > 0) { folders = cachedItems.filter(f => f.kind === 'drive#folder'); } else { folders = [{ id: 'loading', name: L.loading, kind: 'drive#folder' }]; apiList(targetId).then(res => { if (typeof globalCache !== 'undefined') globalCache.set(targetId, res); const pop = el.querySelector('.pk-crumb-pop'); if (pop && pop.dataset.targetId === targetId) renderMenu(res.filter(f => f.kind === 'drive#folder')); }).catch(() => { renderMenu([{ id: 'error', name: L.str_load_failed, kind: 'drive#folder' }]); }); } } const renderMenu = (list) => { let s = S.sort, d = S.dir; const isNormalFolder = targetId && !targetId.includes('_root'); if (isNormalFolder) { if (gmGet('pk_sort_independent', false)) { const prefs = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); if (prefs[targetId]) { s = prefs[targetId].sort; d = prefs[targetId].dir; } else { s = 'modified_time'; d = 1; } } else { const g = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); s = g.sort; d = g.dir; } } const sortKey = (s === 'modified_time') ? 'modified_time' : 'name'; const collator = new Intl.Collator(({ 'zh': 'zh-CN', 'tc': 'zh-TW', 'ja': 'ja', 'ko': 'ko' })[getLang()] || 'en', { numeric: true, sensitivity: 'base' }); list.sort((a, b) => { const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortKey === 'modified_time') { const tA = a.modified_time ? new Date(a.modified_time).getTime() : 0; const tB = b.modified_time ? new Date(b.modified_time).getTime() : 0; return (tB - tA) * d; } return collator.compare(a.name, b.name) * d; }); let pop = document.getElementById('pk-main-crumb-pop'); if (!pop) { pop = document.createElement('div'); pop.id = 'pk-main-crumb-pop'; pop.className = 'pk-crumb-pop pk-scroll'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.style.top = '-9999px'; document.body.appendChild(pop); } pop.dataset.targetId = targetId; pop.innerHTML = ''; list.forEach(f => { const item = document.createElement('div'); item.className = 'pk-crumb-item'; item.dataset.id = f.id; const iconSrc = f.icon_link || ''; const fallbackSvg = CONF.typeIcons.folder.replace(/width="\d+"/, 'width="18"').replace(/height="\d+"/, 'height="18"'); const iconHtml = iconSrc ? `<img src="${iconSrc}" style="width:18px;height:18px;object-fit:contain;flex-shrink:0;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display='inline-flex';"><span style="display:none;align-items:center;flex-shrink:0;">${fallbackSvg}</span>` : fallbackSvg; item.innerHTML = `${iconHtml}<span>${esc(f.name)}</span>`; if (f.id !== 'loading' && f.id !== 'error') { item.onclick = (ev) => { ev.stopPropagation(); closeMenu(); if (targetId === 'virtual_search_root' || targetId === 'analyze_root' || targetId === 'recent_root') { let rootName = L.str_search_results; if (targetId === 'analyze_root') rootName = L.str_analyze_results; else if (targetId === 'recent_root') rootName = L.btn_nav_recent; const newPath = []; if (targetId === 'virtual_search_root') { newPath.push({ id: '', name: L.btn_nav_home }); } newPath.push({ id: targetId, name: rootName }); newPath.push({ id: f.id, name: f.name }); S.path = newPath; } else { const idx = S.path.findIndex(p => p.id === parentId); if (idx !== -1) { S.path = S.path.slice(0, idx + 1); S.path.push({ id: f.id, name: f.name }); } } load(); }; } pop.appendChild(item); }); requestAnimationFrame(() => { const rect = triggerEl.getBoundingClientRect(); const popRect = pop.getBoundingClientRect(); let left = rect.left - 4; const popW = popRect.width; const winW = window.innerWidth; if (left + popW > winW) left = winW - popW - 15; pop.style.left = Math.max(10, left) + 'px'; pop.style.top = (rect.bottom + 6) + 'px'; pop.classList.add('pk-show'); triggerEl.classList.add('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.down; }); const closeMenu = () => { if (pop.parentNode) pop.remove(); if (triggerEl) { triggerEl.classList.remove('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.right; const svg = triggerEl.querySelector('svg'); if (svg) { svg.style.width = '14px'; svg.style.height = '14px'; svg.style.display = 'block'; svg.style.opacity = '0.6'; } } document.removeEventListener('mousedown', onOutsideClick); window.removeEventListener('resize', closeMenu); UI.vp.removeEventListener('scroll', closeMenu); }; const onOutsideClick = (evt) => { if (!pop.contains(evt.target) && !triggerEl.contains(evt.target)) closeMenu(); }; window.addEventListener('resize', closeMenu); UI.vp.addEventListener('scroll', closeMenu, { passive: true }); setTimeout(() => document.addEventListener('mousedown', onOutsideClick), 10); }; if (globalDirtyFolders.has(targetId === 'root' ? '' : targetId)) { folders = null; globalDirtyFolders.delete(targetId === 'root' ? '' : targetId); } renderMenu(folders || []); }; function renderCrumb() { UI.crumb.innerHTML = ''; if (S.offlineMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = ` <div style="display:flex; align-items:center; color:var(--pk-fg); margin-left: -6px;"> <span style="font-weight:bold; font-size:15px; cursor:default;">${L.title_offline}</span> </div> `; return; } if (S.uploadMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = ` <div style="display:flex; align-items:center; color:var(--pk-fg); margin-left: -6px;"> <span style="font-weight:bold; font-size:15px; cursor:default;">${L.btn_nav_upload}</span> </div> `; return; } if (S.shareMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = ` <div style="display:flex; align-items:center; color:var(--pk-fg); margin-left: -6px;"> <span style="font-weight:bold; font-size:15px; cursor:default;">${L.btn_nav_share}</span> </div> `; return; } if (S.historyMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = ` <div style="display:flex; align-items:baseline; color:var(--pk-fg); margin-left: -6px;"> <span style="font-weight:bold; font-size:15px; cursor:default;">${L.btn_nav_history}</span> <span style="color:#888; font-size:11px; margin-left:10px; cursor:default; font-weight:normal; opacity:0.8;">${L.history_notice}</span> </div> `; return; } if (S.trashMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = ` <div style="display:flex; align-items:baseline; color:var(--pk-fg); margin-left: -6px;"> <span style="font-weight:bold; font-size:15px; cursor:default;">${L.trash_title}</span> <span style="color:#888; font-size:11px; margin-left:10px; cursor:default; font-weight:normal; opacity:0.8;">${L.trash_notice}</span> </div> `; return; } UI.crumb.style.pointerEvents = 'auto'; const svgStyle = 'width:14px;height:14px;vertical-align:-4px;margin-right:4px;display:inline-block;'; const ICON_HOME = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="${svgStyle}"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`; const ICON_SEARCH = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="${svgStyle}"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`; const ICON_TRASH = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="${svgStyle}"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`; const ICON_STAR = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="${svgStyle}"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`; 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('<svg', `<svg style="${svgStyle}"`); s.innerHTML = `${ICON_RECENT}${p.name}`; } else { s.textContent = p.name; } s.dataset.id = (p.id === '' || !p.id) ? 'root' : p.id; s.className = i === S.path.length - 1 ? 'act' : ''; s.style.cssText = "display:inline-flex; align-items:center; padding:2px 6px; border-radius:4px; flex-shrink:0; transition:background 0.2s; white-space:nowrap; margin: auto 2px;"; s.onclick = (e) => { e.stopPropagation(); if (i !== S.path.length - 1) { if (i < S.path.length - 1) { S.latestChildId = S.path[i + 1].id; } S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); S.path = S.path.slice(0, i + 1); load(); } }; UI.crumb.appendChild(s); if (i < S.path.length - 1) { const sep = document.createElement('span'); sep.className = 'pk-crumb-sep'; sep.innerHTML = CONF.crumbIcons.right; sep.onclick = (e) => { e.stopPropagation(); showCrumbDropdown(e, p.id, sep); }; UI.crumb.appendChild(sep); } }); S._crumbIdx = S.path.length - 1; UI.crumb.onwheel = (e) => { e.preventDefault(); const existingPop = document.getElementById('pk-main-crumb-pop'); if (existingPop) { existingPop.remove(); UI.crumb.querySelectorAll('.pk-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); sep.innerHTML = CONF.crumbIcons.right; }); } const nodes = [...UI.crumb.querySelectorAll('span:not(.pk-crumb-sep)')]; if (!nodes.length) return; const now = Date.now(); if (now - (S._lastCrumbScroll || 0) < 120) return; S._lastCrumbScroll = now; if (e.deltaY < 0) S._crumbIdx = Math.max(0, S._crumbIdx - 1); else S._crumbIdx = Math.min(nodes.length - 1, S._crumbIdx + 1); const targetNode = nodes[S._crumbIdx]; const containerWidth = UI.crumb.offsetWidth; const centerOffset = targetNode.offsetLeft + (targetNode.offsetWidth / 2) - (containerWidth / 2); UI.crumb.scrollTo({ left: centerOffset, behavior: 'smooth' }); }; setTimeout(() => { if (UI.crumb) { UI.crumb.scrollTo({ left: UI.crumb.scrollWidth, behavior: 'smooth' }); } }, 0); } el.tabIndex = 0; el.focus(); const keyHandler = (e) => { const win = document.querySelector('.pk-ov'); if (!win || win.style.display === 'none') return; const tag = e.target.tagName; if (tag === 'TEXTAREA' || (tag === 'INPUT' && e.target.type !== 'checkbox' && e.target.type !== 'button' && e.target.type !== 'submit')) return; if (e.key === 'Escape') { const player = document.getElementById('pk-player-ov'); if (player) { player.remove(); return; } const openModal = document.querySelector('.pk-modal-ov'); if (openModal) { openModal.remove(); return; } if (UI.ctx.style.display === 'block') { UI.ctx.style.display = 'none'; return; } const isAtRoot = S.path.length === 1 || (S.path.length === 2 && S.path[1].id === 'virtual_search_root'); if (S.sel.size > 0) { S.sel.clear(); refresh(); } else if (UI.win.classList.contains('pk-maximized')) { if (!isTurbo && btnMax) btnMax.click(); } else if (isAtRoot) { if (!isTurbo) UI.btnClose.click(); } return; } if (e.key === 'Delete' && e.altKey) { e.preventDefault(); if (S.uploadMode) return; const existingModal = Array.from(document.querySelectorAll('.pk-modal-ov')).find(m => { const title = m.querySelector('h3'); return title && title.textContent.includes(L.title_blacklist); }); if (existingModal) { existingModal.remove(); } else { showBlacklistModal(); } } if (e.altKey && (e.key === 'h' || e.key === 'H')) { e.preventDefault(); const existingHelp = Array.from(document.querySelectorAll('.pk-modal-ov')).find(m => { const title = m.querySelector('h3'); return title && title.textContent === L.modal_help_title; }); if (existingHelp) { existingHelp.remove(); } else if (UI.btnHelp) { UI.btnHelp.click(); } return; } const hasActiveOverlay = document.querySelector('.pk-modal-ov, .pk-img-ov, #pk-player-ov'); if (hasActiveOverlay) return; if (e.key === 'F2') { e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; if (S.sel.size === 1) { if (UI.btnRename.disabled) return; UI.btnRename.click(); } else if (S.sel.size > 1) { if (UI.btnBulkRename.disabled) return; UI.btnBulkRename.click(); } } if (e.key === 'F5') { e.preventDefault(); if (S.uploadMode) load(false, true); else if (S.trashMode) load(false, true); else UI.btnRefresh.click(); } if (e.key === 'F8') { e.preventDefault(); const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isHistoryRoot = S.historyMode && S.path.length === 1; const isBlocked = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || isStarredRoot || isRecentRoot || isHistoryRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; if (isBlocked) return; UI.btnNewFolder.click(); } if (e.key === 'r' || e.key === 'R') { if (S.trashMode && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); if (UI.btnRestore && !UI.btnRestore.disabled) UI.btnRestore.click(); return; } if (S.offlineMode && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); if (UI.btnRetryTask && !UI.btnRetryTask.disabled) UI.btnRetryTask.click(); return; } } if (e.key === 'm' || e.key === 'M') { const imgPlayer = document.querySelector('.pk-img-ov'); const vidPlayer = document.getElementById('pk-player-ov'); if (!imgPlayer && !vidPlayer) { const btnMax = document.querySelector('#pk-maximize'); if (!isTurbo && btnMax) btnMax.click(); } } if (e.key === 'Delete' && e.shiftKey) { if (S.trashMode && UI.btnEmptyTrash) { e.preventDefault(); UI.btnEmptyTrash.click(); return; } if (S.uploadMode && UI.btnUpClearAll) { e.preventDefault(); UI.btnUpClearAll.click(); return; } } if (e.key === 'Delete' && !e.ctrlKey && !e.altKey && !e.shiftKey) { if (S.shareMode) { if (UI.btnCancelShare && !UI.btnCancelShare.disabled) UI.btnCancelShare.click(); return; } if (S.uploadMode) { if (UI.btnUpDel && !UI.btnUpDel.disabled) UI.btnUpDel.click(); return; } if (S.trashMode) { if(UI.btnDelForever && !UI.btnDelForever.disabled) UI.btnDelForever.click(); } else { if (!UI.btnDel.disabled) UI.btnDel.click(); } } if (e.key === 'Delete' && e.ctrlKey) { e.preventDefault(); if (S.trashMode) return; if (S.isFlattened || S.dupMode || S.uploadMode) return; if (!UI.btnPrune.disabled) UI.btnPrune.click(); } if (e.ctrlKey || e.metaKey) { if (e.key === 'a' || e.key === 'A') { e.preventDefault(); if (S.handleSelectAll) S.handleSelectAll(); } if (e.key === 'c' || e.key === 'C') { if (window.getSelection().toString()) return; e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; if (!UI.btnCopy.disabled) UI.btnCopy.click(); } if (e.key === 'x' || e.key === 'X') { e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; if (!UI.btnCut.disabled) UI.btnCut.click(); } if (e.key === 'v' || e.key === 'V') { e.preventDefault(); if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isHistoryRoot = S.historyMode && S.path.length === 1; const isPasteBlocked = S.isFlattened || S.dupMode || S.offlineMode || S.historyMode || isStarredRoot || isRecentRoot || isHistoryRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; if (isPasteBlocked) return; if (!UI.btnPaste.disabled) UI.btnPaste.click(); } } if (e.altKey) { if (S.offlineMode && (e.key === 'c' || e.key === 'C')) { e.preventDefault(); if (UI.btnCopyLinkOffline && !UI.btnCopyLinkOffline.disabled) UI.btnCopyLinkOffline.click(); return; } if (e.key === 's' || e.key === 'S') { e.preventDefault(); openSettingsModal(); } if (e.key === 'g' || e.key === 'G') { e.preventDefault(); if (S.uploadMode) { if (UI.btnUpStart && !UI.btnUpStart.disabled) UI.btnUpStart.click(); } } if (e.key === 'p' || e.key === 'P') { e.preventDefault(); if (S.uploadMode) { if (UI.btnUpPause && !UI.btnUpPause.disabled) UI.btnUpPause.click(); } } if (e.key === 'u' || e.key === 'U') { e.preventDefault(); const cur = S.path[S.path.length - 1]; const isRoot = S.path.length === 1; const isUnzipBlocked = S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || (S.analyzeMode && cur.id === 'analyze_root') || (cur.id === 'virtual_search_root') || (S.recentMode && isRoot) || (S.starredMode && isRoot); if (!isUnzipBlocked && !UI.btnUnzip.disabled) UI.btnUnzip.click(); } if (e.key === 'd' || e.key === 'D') { e.preventDefault(); if (S.shareMode || S.uploadMode || S.trashMode || S.offlineMode) return; if (UI.btnDown && !UI.btnDown.disabled) UI.btnDown.click(); } if (e.key === 'a' || e.key === 'A') { e.preventDefault(); if (S.shareMode || S.uploadMode || S.trashMode || S.offlineMode) return; if (UI.btnAria2 && !UI.btnAria2.disabled) UI.btnAria2.click(); } if (e.key === 'e' || e.key === 'E') { e.preventDefault(); if (S.shareMode || S.trashMode) return; if (UI.btnExt && !UI.btnExt.disabled) UI.btnExt.click(); } if (e.key === 't' || e.key === 'T') { e.preventDefault(); if (UI.btnTheme) UI.btnTheme.click(); } } }; document.addEventListener('keydown', keyHandler); const showProperty = (item) => { const L = getStrings(); let rows = []; rows.push({ k: L.lbl_prop_name, v: item.name }); rows.push({ k: L.lbl_prop_size, v: fmtSize(item.size) }); rows.push({ k: L.lbl_prop_ctime, v: fmtDate(item.created_time) }); if (item.modified_time) rows.push({ k: L.lbl_prop_mtime, v: fmtDate(item.modified_time) }); let source = L.str_prop_unknown; if (item.kind === 'drive#task') source = L.str_prop_offline; else if (item.kind === 'drive#folder') source = L.lbl_type_folder; else if (item.from === 'share') source = L.str_prop_share; else source = L.str_prop_user; rows.push({ k: L.lbl_prop_source, v: source }); let link = item.source_url || (item.params && item.params.url) || item.web_content_link || item.url; if (!link && item.params && typeof item.params === 'object') { for (const key in item.params) { if (key.toLowerCase() === 'url') { link = item.params[key]; break; } } } if (link) { rows.push({ k: L.lbl_prop_link, v: link, isLink: true }); } let pathStr = "Root"; if (S.offlineMode) pathStr = L.title_offline; else if (S.shareMode) pathStr = L.btn_nav_share; else if (item._lineage) pathStr = item._lineage.map(x => x.name).join('/'); rows.push({ k: L.lbl_prop_path, v: pathStr }); let html = `<div style="display:flex; flex-direction:column; gap:16px; padding-top:10px;">`; rows.forEach(r => { let valHtml = `<div style="flex:1; word-break:break-all; color:var(--pk-fg); user-select:text; line-height:1.5;">${esc(r.v)}</div>`; if (r.isLink) { valHtml = ` <div style="flex:1; display:flex; gap:8px; align-items:flex-start; min-width:0;"> <div style="flex:1; word-break:break-all; color:var(--pk-pri); cursor:pointer; overflow:hidden; text-overflow:ellipsis; display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical;" title="${esc(r.v)}" onclick="navigator.clipboard.writeText(this.title); showToast('${L.msg_copy_success}')"> ${esc(r.v)} </div> <button class="pk-btn" style="padding:0 10px; height:26px; font-size:12px; flex-shrink:0; margin-top:-2px;" onclick="navigator.clipboard.writeText('${esc(r.v)}'); showToast('${L.msg_copy_success}')"> ${L.btn_copy_text} </button> </div> `; } html += ` <div style="display:flex; align-items:baseline; font-size:13px;"> <div style="width:70px; color:#888; flex-shrink:0; text-align:right; margin-right:15px;">${r.k}:</div> ${valHtml} </div> `; }); html += `</div>`; const m = showModal(html); m.querySelector('.pk-modal h3').textContent = L.title_property; m.querySelector('.pk-modal').style.width = "520px"; }; const btnProp = document.getElementById('ctx-property'); if (btnProp) { btnProp.onclick = () => { const item = S.itemMap.get(S.activeId); if (item) showProperty(item); UI.ctx.style.display = 'none'; }; } const mouseHandler = (e) => { if (!document.querySelector('.pk-ov') || !UI || !UI.ctx) return; if (UI.ctx.style.display !== 'none' && !UI.ctx.contains(e.target)) { UI.ctx.style.display = 'none'; UI.ctx.style.flexDirection = ''; const isRoot = S.path.length === 1 && S.path[0].id === ''; if (!S.trashMode && isRoot && S.sel.size === 1) { const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder' && item.name === CONF.SYSTEM_FOLDER_NAME) { S.sel.clear(); renderVisible(); updateStat(); } } } }; document.addEventListener('mouseup', mouseHandler); function updateStat() { let total = 0; const validSelectedIds = new Set(); const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; if (item && !item.isHeader) { total++; if (S.sel.has(item.id)) { validSelectedIds.add(item.id); } } } if (validSelectedIds.size !== S.sel.size) { S.sel = validSelectedIds; } let n = S.sel.size; const btnInvert = document.getElementById('pk-btn-invert'); if (btnInvert) { btnInvert.style.display = (!S.dupMode && n > 0) ? 'flex' : 'none'; btnInvert.style.color = 'var(--pk-fg)'; } let hasProtectedItem = false; let hasSelFolder = false; let selSize = 0; if (n > 0) { for (let id of S.sel) { const item = S.itemMap.get(id); if (!item) continue; const sz = parseInt(item.size || 0); if (item.kind === 'drive#folder') { hasSelFolder = true; if (S.analyzeMode) { selSize += sz; } } else { selSize += sz; } if (!S.trashMode && isSystemItem(item)) hasProtectedItem = true; } } let statText = L.status_ready.replace('{n}', total); if (n > 0) { statText += `\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${L.sel_count.replace('{n}', n)}`; if (!S.shareMode && (S.analyzeMode || !hasSelFolder)) { statText += `\u00A0\u00A0${fmtSize(selSize)}`; } } UI.stat.textContent = statText; const hasSel = n > 0; let isSingleVideo = false; if (n === 1) { const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (item && item.kind !== 'drive#folder') { const mime = (item.mime_type || '').toLowerCase(); const ext = (item.name || '').split('.').pop().toLowerCase(); const isVid = mime.startsWith('video/') ||['mp4','mkv','avi','mov','wmv','flv','webm','ts'].includes(ext); let isTaskReady = true; if (S.offlineMode) isTaskReady = item.phase === 'PHASE_TYPE_COMPLETE'; if (S.uploadMode) isTaskReady = item.status === 'DONE'; if (isVid && isTaskReady) isSingleVideo = true; } } if (UI.btnExt) UI.btnExt.disabled = S.trashMode || !isSingleVideo; if (UI.btnAria2) UI.btnAria2.disabled = S.trashMode || !hasSel; if (UI.btnDown) UI.btnDown.disabled = S.trashMode || !hasSel; if (UI.chkAll) { let isAll = false; let isInd = false; if (n > 0 && total > 0) { isAll = (n === total); isInd = (n < total); } else { isAll = false; isInd = false; } UI.chkAll.checked = isAll; UI.chkAll.indeterminate = isInd; } const isShare = S.shareMode; const isOffline = S.offlineMode; const isHistory = S.historyMode; const isUpload = S.uploadMode; if (isUpload) { if (hasSel) { let hasRunning = false; let hasStopped = false; for (const id of S.sel) { const task = S.itemMap.get(id); if (task) { const s = task.status; if (['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(s)) hasRunning = true; if (['PAUSED', 'ERROR'].includes(s)) hasStopped = true; } if (hasRunning && hasStopped) break; } if (UI.btnUpPause) UI.btnUpPause.disabled = !hasRunning; if (UI.btnUpStart) UI.btnUpStart.disabled = !hasStopped; if (UI.btnUpDel) UI.btnUpDel.disabled = false; } else { if (UI.btnUpPause) UI.btnUpPause.disabled = true; if (UI.btnUpStart) UI.btnUpStart.disabled = true; if (UI.btnUpDel) UI.btnUpDel.disabled = true; } if (UI.btnUpClearAll) UI.btnUpClearAll.disabled = (S.uploadTasks.length === 0); } if (UI.btnRetryTask) { UI.btnRetryTask.style.display = isOffline ? 'inline-flex' : 'none'; const hasFailedTask = Array.from(S.sel).some(id => S.itemMap.get(id)?.phase === 'PHASE_TYPE_ERROR'); UI.btnRetryTask.disabled = !hasFailedTask; } if (UI.btnCopyLinkOffline) { UI.btnCopyLinkOffline.style.display = isOffline ? 'inline-flex' : 'none'; UI.btnCopyLinkOffline.disabled = !hasSel; } UI.btnCopy.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnCut.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnRename.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnBulkRename.style.display = (isShare || isOffline || isHistory || isUpload) ? 'none' : 'inline-flex'; UI.btnDel.style.display = (isShare || isUpload) ? 'none' : 'inline-flex'; const delBtnSpan = UI.btnDel.querySelector('span'); if (delBtnSpan) { delBtnSpan.textContent = isHistory ? L.btn_clear_history : L.btn_del; UI.btnDel.setAttribute('data-pk-tip', isHistory ? L.tip_clear_history : L.tip_del); } if (isShare && UI.btnCancelShare) { UI.btnCancelShare.disabled = !hasSel; } const hasClipData = S.clipItems && S.clipItems.length > 0; const isPasteLocked = S.movingIds && S.movingIds.size > 0; if (UI.btnPaste) { UI.btnPaste.disabled = !hasClipData || isPasteLocked; const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isHistoryRoot = S.historyMode && S.path.length === 1; const shouldHidePaste = S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || isStarredRoot || isRecentRoot || isHistoryRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; UI.btnPaste.style.display = shouldHidePaste ? 'none' : 'inline-flex'; UI.btnPaste.style.cursor = isPasteLocked ? 'wait' : (UI.btnPaste.disabled ? 'not-allowed' : 'pointer'); } UI.btnCopy.disabled = !hasSel || isPasteLocked; UI.btnCut.disabled = !hasSel || isPasteLocked || hasProtectedItem; if (UI.btnNewFolder) { UI.btnNewFolder.disabled = isPasteLocked; if (isPasteLocked) UI.btnNewFolder.style.cursor = 'wait'; else UI.btnNewFolder.style.cursor = ''; } UI.btnDel.disabled = !hasSel || (n === 1 && hasProtectedItem); const isStandardRenameView = !(isShare || isOffline || isHistory || isUpload); if (isStandardRenameView) { if (n <= 1) { UI.btnRename.style.display = 'inline-flex'; UI.btnBulkRename.style.display = 'none'; UI.btnRename.disabled = (n === 0) || hasProtectedItem; } else { UI.btnRename.style.display = 'none'; UI.btnBulkRename.style.display = 'inline-flex'; UI.btnBulkRename.disabled = false; } } if (UI.btnRestore) UI.btnRestore.disabled = !hasSel; if (UI.btnDelForever) UI.btnDelForever.disabled = !hasSel; if (UI.btnPrune) { const isHiddenMode = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode; UI.btnPrune.style.display = isHiddenMode ? 'none' : 'inline-flex'; UI.btnPrune.disabled = isHiddenMode || !hasSelFolder; } if (UI.btnUnzip) { const cur = S.path[S.path.length - 1]; const isRoot = S.path.length === 1; const isUnzipHidden = S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || (S.analyzeMode && cur.id === 'analyze_root') || (cur.id === 'virtual_search_root') || (S.recentMode && isRoot) || (S.starredMode && isRoot); UI.btnUnzip.style.display = isUnzipHidden ? 'none' : 'inline-flex'; const isArchive = (it) => { if (!it || it.kind === 'drive#folder') return false; const n = (it.name || '').toLowerCase(); const m = (it.mime_type || '').toLowerCase(); return m.includes('zip') || m.includes('rar') || m.includes('7z') || n.endsWith('.zip') || n.endsWith('.rar') || n.endsWith('.7z'); }; const hasArchive = Array.from(S.sel).some(id => isArchive(S.itemMap.get(id))); UI.btnUnzip.disabled = S.trashMode || !hasArchive; } } async function getLinks() { const res = []; for (const id of S.sel) { let item = S.items.find(x => x.id === id); if (item && !item.web_content_link) { try { item = await apiGet(id); } catch { } } if (item?.web_content_link) res.push(item); } return res; } const generateQualityList = (data) => { const list =[]; const seenNames = new Set(); if (data.web_content_link) { list.push({ name: L.str_original, url: data.web_content_link, isOriginal: true }); seenNames.add(L.str_original); } const transcodeList =[]; if (data.medias && Array.isArray(data.medias)) { data.medias.forEach(m => { if (!m.link || !m.link.url) return; let name = m.resolution_name || m.video_stream_id || 'Unknown'; let streamUrl = m.link.url; if (m.video && m.video.video_type === 'mpegts' && !streamUrl.includes('.m3u8')) { streamUrl += '&ext=.m3u8'; } if (name.toLowerCase() === 'unknown' || !name) { name = L.str_original_fast; } if (name === 'Unknown') { } else if (name.includes('1080') || name === 'FHD') name = '1080P'; else if (name.includes('720') || name === 'HD') name = '720P'; else if (name.includes('480') || name === 'SD') name = '480P'; if (seenNames.has(name)) return; seenNames.add(name); transcodeList.push({ name: name, url: streamUrl, isOriginal: false, rawName: name }); }); } if (transcodeList.length > 0) { const getRank = (s) => { const n = s.toUpperCase(); if (n.includes('4K')) return 100; if (n.includes('2K')) return 90; if (n.includes('1080')) return 80; if (n.includes('720')) return 70; if (n.includes('480')) return 60; return 10; }; transcodeList.sort((a, b) => getRank(b.rawName) - getRank(a.rawName)); const highestStream = transcodeList[0]; if (!highestStream.name.includes('速') && !highestStream.name.includes('Fast') && highestStream.name !== L.str_original_fast) { const speedSuffix = (L.str_speed && L.str_speed.includes('速')) ? "(高速)" : "(Fast)"; highestStream.name = `${highestStream.name} ${speedSuffix}`; } } transcodeList.forEach(t => { list.push({ name: t.name, url: t.url, isOriginal: false }); }); list.sort((a, b) => { if (a.isOriginal) return -1; if (b.isOriginal) return 1; const getRank = (s) => { const n = s.toUpperCase(); if (n.includes('4K')) return 100; if (n.includes('2K')) return 90; if (n.includes('1080')) return 80; if (n.includes('720')) return 70; if (n.includes('480')) return 60; return 0; }; const rA = getRank(a.name); const rB = getRank(b.name); if (rA !== rB) return rB - rA; return b.name.localeCompare(a.name, undefined, { numeric: true }); }); return list; }; const getBestSource = (data) => { const fileName = (data.name || "").toLowerCase(); const mime = (data.mime_type || '').toLowerCase(); const generatedList = generateQualityList(data); const list = generatedList.map(item => ({ name: item.name, link: item.url, active: false, weight: 0, isOriginal: item.isOriginal })); const isLegacyFormat = fileName.endsWith('.avi') || fileName.endsWith('.wmv') || fileName.endsWith('.rmvb') || fileName.endsWith('.divx') || fileName.endsWith('.flv') || fileName.endsWith('.vob') || mime.includes('mpeg4') || mime.includes('avi') || mime.includes('x-ms-wmv'); const isMKV = fileName.endsWith('.mkv'); list.forEach(item => { const n = item.name; if (item.isOriginal) { item.weight = (isLegacyFormat || isMKV) ? -9999 : 50; } else { if (n.includes('高速') || n.includes('Fast')) item.weight = 500; else if (n === 'Unknown') item.weight = 400; else if (n.includes('1080P')) item.weight = 300; else if (n.includes('720P')) item.weight = 200; else if (n.includes('480P')) item.weight = 100; else item.weight = 10; } }); let bestMatch = null; if (list.length > 0) { const sortedByWeight = [...list].sort((a, b) => b.weight - a.weight); bestMatch = sortedByWeight[0]; } else { bestMatch = { name: L.str_original, link: data.web_content_link, active: true }; list.push(bestMatch); } bestMatch.active = true; return { src: bestMatch.link, name: bestMatch.name, list: list }; }; async function playVideo(item, startFullscreen = false) { if (S.trashMode) return; const getPhysicalId = (it) => { if ((S.offlineMode && it.kind === 'drive#task') || (S.uploadMode && it.file_id)) { return it.file_id || it.id; } return it.id; }; let pkHls = null; let isPlayerDestroyed = false; const L = getStrings(); const isVideoItem = (it) => { if (!it || it.isHeader || it.kind === 'drive#folder') return false; if (S.offlineMode && it.phase !== 'PHASE_TYPE_COMPLETE') return false; if (S.uploadMode && it.status !== 'DONE') return false; const m = (it.mime_type || '').toLowerCase(); const n = (it.name || '').toLowerCase(); const dur = (it.params && it.params.duration) || 0; const vExts = ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp']; return m.startsWith('video/') || dur > 0 || vExts.some(e => n.endsWith('.' + e)); }; const videoPlaylist = S.display.filter(i => isVideoItem(i)); let curListIdx = videoPlaylist.findIndex(v => v.id === item.id); const totalInList = videoPlaylist.length; let isSwitching = false; let switchReqId = 0; const showSadBox = (codecName) => { if (box.querySelector('.pk-err-dialog')) return; if (posterEl) { posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; posterEl.style.pointerEvents = 'auto'; } const savedPlayer = gmGet('pk_ext_player', 'potplayer'); const sadBoxSVG = `<svg width="85" height="85" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 45l15-15h30l15 15H20z" fill="#777"/><path d="M20 45L5 30h20l15 15H20z" fill="#999"/><path d="M80 45l15-15H75L60 45h20z" fill="#999"/><path fill-rule="evenodd" d="M20 45h60v35c0 5-5 5-5 5H25c-5 0-5-5-5-5V45zm16.5 14c0 3.3 1.5 6 3.5 6s3.5-2.7 3.5-6-1.5-6-3.5-6-3.5 2.7-3.5 6zm20 0c0 3.3 1.5 6 3.5 6s3.5-2.7 3.5-6-1.5-6-3.5-6-3.5 2.7-3.5 6z" fill="#aaa"/><path d="M38 16a12 8 0 1 0 24 0a12 8 0 1 0-24 0M48 24l2 3.5l2-3.5h-4z" fill="#aaa"/><path d="M47 13l6 6M53 13l-6 6" stroke="#181818" stroke-width="1.8" stroke-linecap="round"/></svg>`; const recommended = qualityList.find(q => !q.isOriginal) || qualityList[0]; const recommendedUrl = recommended.link || recommended.url; const resOptions = qualityList.map(q => { const qUrl = q.link || q.url; const isSelected = qUrl === recommendedUrl; return `<option value="${qUrl}" ${isSelected ? 'selected' : ''}>${q.name}</option>`; }).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 = ` <div class="pk-err-close" style="position:absolute;top:15px;right:15px;cursor:pointer;color:#fff;padding:5px;display:flex;align-items:center;justify-content:center;opacity:0.7;transition:opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></div> <div style="margin-bottom:20px;">${sadBoxSVG}</div> <div style="font-size:16px;font-weight:bold;color:#fff;margin-bottom:10px;max-width:400px;line-height:1.4;">${L.err_codec_t1.replace('{c}', codecName)}</div> <div style="font-size:12px;color:#999;margin-bottom:30px;">${L.err_codec_t2}</div> <div style="display:grid;grid-template-columns:80px 1fr;gap:15px;align-items:center;width:100%;margin-bottom:15px;"> <div style="color:#ccc;font-size:14px;text-align:right;">${lblRes}</div> <div style="position:relative;"> <select id="pk_err_res_sel" style="width:100%;padding:8px 12px;border-radius:6px;background:rgba(0,0,0,0.3);color:#fff;border:1px solid #444;outline:none;cursor:pointer;font-size:14px;"> ${resOptions} </select> </div> </div> <div style="display:grid;grid-template-columns:80px 1fr;gap:15px;align-items:center;width:100%;margin-bottom:25px;"> <div style="color:#ccc;font-size:14px;text-align:right;">${L.lbl_player}</div> <div style="position:relative;"> <select id="pk_err_player_sel" style="width:100%;padding:8px 12px;border-radius:6px;background:rgba(0,0,0,0.3);color:#fff;border:1px solid #444;outline:none;cursor:pointer;font-size:14px;"> <option value="potplayer" selected>PotPlayer</option> <option value="other">${L.opt_player_other}</option> </select> </div> </div> <button id="pk_err_launch_btn" style="background:#fff;color:#000;border:none;padding:10px 40px;border-radius:6px;font-size:14px;font-weight:bold;cursor:pointer;transition:background 0.2s;">${L.btn_start_play}</button> `; 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 = ` <div class="pk-modal-close" style="position:absolute;top:15px;right:15px;cursor:pointer;color:#aaa;">${mkSvg(icons.close)}</div> <h3 style="margin:0;font-size:16px;color:#fff;">${L.export_link_title}</h3> <div class="pk-field"> <textarea readonly style="width:100%;height:100px;background:#111;color:#4caf50;border:1px solid #333;border-radius:6px;padding:10px;font-family:monospace;font-size:12px;resize:none;word-break:break-all;outline:none;">${url}</textarea> </div> <div class="pk-modal-act" style="justify-content:flex-end;display:flex;gap:10px;"> <button class="pk-btn" id="pk_link_cancel" style="border:1px solid #444;background:transparent;color:#ddd;">${L.btn_close}</button> <button class="pk-btn pri" id="pk_copy_link_btn" style="background:var(--pk-pri);color:#fff;border:none;padding:8px 20px;border-radius:6px;font-weight:bold;cursor:pointer;">${L.btn_copy_link}</button> </div> `; ov.appendChild(m); d.appendChild(ov); const doClose = () => ov.remove(); m.querySelector('.pk-modal-close').onclick = doClose; m.querySelector('#pk_link_cancel').onclick = doClose; const cpBtn = m.querySelector('#pk_copy_link_btn'); const txt = m.querySelector('textarea'); setTimeout(() => txt.select(), 50); cpBtn.onclick = () => { GM_setClipboard(url); cpBtn.textContent = L.msg_copy_success; setTimeout(() => { cpBtn.textContent = L.btn_copy_link; }, 2000); }; ov.onclick = (evt) => { if(evt.target === ov) doClose(); }; m.onclick = (ev) => ev.stopPropagation(); }; dialog.querySelector('.pk-err-close').onclick = (e) => { e.stopPropagation(); dialog.remove(); }; dialog.onclick = (e) => e.stopPropagation(); const playerSel = dialog.querySelector('#pk_err_player_sel'); const launchBtn = dialog.querySelector('#pk_err_launch_btn'); playerSel.onchange = () => { launchBtn.textContent = (playerSel.value === 'other') ? L.btn_copy_link : L.btn_start_play; }; launchBtn.textContent = L.btn_start_play; launchBtn.onclick = (e) => { e.stopPropagation(); const selPlayer = playerSel.value; const selResLink = dialog.querySelector('#pk_err_res_sel').value; const selResName = dialog.querySelector('#pk_err_res_sel').options[dialog.querySelector('#pk_err_res_sel').selectedIndex].text; gmSet('pk_ext_player', selPlayer); let cleanLink = selResLink.replace('&ext=.m3u8', ''); if (cleanLink.includes('ts_downloader') && cleanLink.includes('url=')) { const urlParam = new URL(cleanLink).searchParams.get('url'); if (urlParam) cleanLink = decodeURIComponent(urlParam); } if (selPlayer === 'other') { GM_setClipboard(cleanLink); launchBtn.textContent = L.msg_copy_success; launchBtn.style.background = "#52c41a"; launchBtn.style.color = "#fff"; setTimeout(() => { if (typeof destroyPlayer === 'function') destroyPlayer(); }, 800); } else if (selPlayer === 'potplayer') { const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanLink} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; destroyPlayer(); } else { const curT = v.currentTime; currentLink = selResLink; currentResName = selResName; box.classList.add('buffering'); if (posterEl) { if (v.readyState >= 2 && v.currentTime > 0) { try { const cvs = document.createElement('canvas'); cvs.width = v.videoWidth; cvs.height = v.videoHeight; cvs.getContext('2d').drawImage(v, 0, 0); posterEl.querySelector('img').src = cvs.toDataURL('image/jpeg', 0.7); } catch (err) {} } posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; } if(resTxt) resTxt.textContent = currentResName; if(resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } dialog.remove(); shutterTargetTime = curT > 0.1 ? curT : 0; loadSource(currentLink, curT); v.play().catch(()=>{}); } }; }; const renderPlaylistItems = () => { const RANGE = 150; const start = Math.max(0, curListIdx - RANGE); const end = Math.min(videoPlaylist.length, curListIdx + RANGE + 1); return videoPlaylist.slice(start, end).map((v, i) => { const absIdx = start + i; const imgSrc = v.thumbnail_link || v.icon_link || ''; return ` <div class="pk-p-plist-item ${absIdx === curListIdx ? 'active' : ''}" data-idx="${absIdx}" data-name="${esc(v.name)}" data-size="${fmtSize(v.size)}"> <img src="${imgSrc}" style="filter:none !important;" onerror="this.style.display='none';" loading="lazy"> </div> `}).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 = `<span>${text}</span><span class="pk-p-resume-btn" id="pk_re_start">${L.btn_restart}</span><div class="pk-p-resume-close" id="pk_re_close">${mkSvg(icons.close)}</div>`; d.querySelector('#pk_p_box').appendChild(toast); toast.querySelector('#pk_re_start').onclick = (e) => { e.stopPropagation(); if (tCur) tCur.textContent = "00:00:00"; if (progFilled) progFilled.style.setProperty('width', '0%', 'important'); shutterTargetTime = 0.1; targetVideo._isRestarting = true; targetVideo.currentTime = 0; toast.remove(); }; toast.querySelector('#pk_re_close').onclick = (e) => { e.stopPropagation(); toast.remove(); }; setTimeout(() => { if(toast.parentNode) toast.remove(); }, 8000); }; if (savedT > 5) { showToast(L.msg_resume_hint.replace('{t}', fmtT(savedT))); } else if (skipOp > 0) { const dp = d.querySelector('.pk-player-box'); if(dp && !dp.querySelector('.pk-skip-toast')) { const tip = document.createElement('div'); tip.className = 'pk-skip-toast'; tip.style.cssText = "position:absolute;top:80px;left:20px;background:rgba(0,0,0,0.6);color:#ddd;padding:4px 8px;border-radius:4px;font-size:12px;pointer-events:none;animation:pkFadeIn 0.5s;"; tip.textContent = `${L.lbl_skip_op} ${skipOp}s`; dp.appendChild(tip); setTimeout(()=>tip.remove(), 3000); } } }; const softSwitch = async (newIdx, scrollMode = 'smooth') => { if (!videoPlaylist[newIdx]) return; switchReqId++; const myReqId = switchReqId; isSwitching = true; const v = d.querySelector('#pk_video'); const loader = d.querySelector('.pk-p-loading'); const poster = d.querySelector('#pk_p_poster'); if (v && v.currentTime > 5 && v.duration > 0) { gmSet('pk_progress_' + getPhysicalId(item), { t: v.currentTime, d: v.duration, ts: Date.now() }); } if (v) { v.pause(); v._isRestarting = false; v._hasShownResumeToast = false; Array.from(v.querySelectorAll('track')).forEach(t => t.remove()); if (subState.blobUrl) { URL.revokeObjectURL(subState.blobUrl); subState.blobUrl = null; } subState.hasSub = false; subState.track = null; const subNameLabel = d.querySelector('#pk_sub_name'); if (subNameLabel) subNameLabel.textContent = L.str_no_sub; v.src = ""; v.load(); } isSwitching = false; if (progFilled) progFilled.style.setProperty('width', '0%', 'important'); const newItem = videoPlaylist[newIdx]; if (tCur) tCur.textContent = '00:00:00'; if (tDur) { const newPId = getPhysicalId(newItem); const knownDur = (newItem.params && newItem.params.duration) || S.durationMap.get(newPId) || gmGet('pk_duration_' + newPId, 0); tDur.textContent = knownDur > 0 ? fmtT(knownDur) : '00:00:00'; } transformState = { rotate: 0, flipH: 1, flipV: 1, ratio: 'default' }; if (typeof applyTransform === 'function') applyTransform(); const ratioBtns = d.querySelectorAll('#pk_ratio_opts .pk-size-btn'); ratioBtns.forEach(btn => { if (btn.dataset.ratio === 'default') btn.classList.add('active'); else btn.classList.remove('active'); }); if (loader) loader.style.display = 'block'; hasCheckedProgress = false; const oldResumeToast = d.querySelector('.pk-p-resume-toast'); if (oldResumeToast) oldResumeToast.remove(); const errDialogs = d.querySelectorAll('.pk-err-dialog'); errDialogs.forEach(el => el.remove()); const linkOvs = d.querySelectorAll('.pk-link-export-ov'); linkOvs.forEach(el => el.remove()); const searchModal = d.querySelector('.pk-sub-search-modal'); if (searchModal) searchModal.remove(); const btnSearchS = d.querySelector('#pk_p_search'); if (btnSearchS) btnSearchS.style.setProperty('display', 'none', 'important'); const btnPipS = d.querySelector('#pk_p_pip'); if (btnPipS) btnPipS.style.setProperty('display', 'none', 'important'); curListIdx = newIdx; item = newItem; if (posterEl) { posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; const img = posterEl.querySelector('img'); if (img && newItem.thumbnail_link) { img.style.display = 'block'; img.src = newItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE'); img.style.opacity = '0'; img.onload = () => { img.style.opacity = '1'; if (btnSearchS) btnSearchS.style.setProperty('display', 'flex', 'important'); }; img.onerror = () => img.style.display = 'none'; } else if (img) { img.style.display = 'none'; } setTimeout(() => { posterEl.style.transition = 'opacity 0.4s ease'; }, 50); } if (loaderEl) loaderEl.style.display = 'block'; const pTitle = d.querySelector('.pk-player-title'); if(pTitle) pTitle.textContent = `[${newIdx + 1}/${totalInList}] ${newItem.name}`; const pTab = d.querySelector('#pk_p_plist_tab'); if(pTab) pTab.innerHTML = `${curListIdx + 1} / ${totalInList} <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>`; if (pScroll) { pScroll.innerHTML = renderPlaylistItems(); pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); if (pTip) pTip.style.display = 'none'; const idx = parseInt(e.currentTarget.dataset.idx); if (idx === curListIdx) return; softSwitch(idx, 'instant'); }; el.onmouseenter = (e) => { if (!plist.classList.contains('open')) return; pTip.innerHTML = `<strong>${e.currentTarget.dataset.name}</strong><br>${e.currentTarget.dataset.size}`; pTip.style.display = 'block'; }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => { if (pTip) pTip.style.display = 'none'; }; }); const activeItem = pScroll.querySelector('.pk-p-plist-item.active'); if (activeItem && box.classList.contains('plist-active')) { if (scrollMode === 'instant') { pScroll.style.scrollBehavior = 'auto'; } else { pScroll.style.scrollBehavior = 'smooth'; } activeItem.scrollIntoView({ behavior: scrollMode === 'instant' ? 'auto' : 'smooth', block: 'nearest', inline: 'center' }); if (scrollMode === 'instant') { setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); } } } const btnL = d.querySelector('#pk_p_side_L'); const btnR = d.querySelector('#pk_p_side_R'); if (btnL) btnL.style.display = newIdx === 0 ? 'none' : 'flex'; if (btnR) btnR.style.display = newIdx === totalInList - 1 ? 'none' : 'flex'; if (typeof updatePlistNav === 'function') updatePlistNav(); hasUserSeeked = false; try { const targetApiId = getPhysicalId(newItem); const newData = await apiGet(targetApiId); if (newData) { if (newData.thumbnail_link) newItem.thumbnail_link = newData.thumbnail_link; if (newData.icon_link) newItem.icon_link = newData.icon_link; } if (myReqId !== switchReqId) return; const freshData = getBestSource(newData); currentResName = freshData.name; currentLink = freshData.src; qualityList = freshData.list; const resTxt = d.querySelector('#pk_p_res_txt'); if (resTxt) resTxt.textContent = currentResName; const resList = d.querySelector('#pk_p_res_list'); if (resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } loadSource(freshData.src, null); if (typeof ThumbnailEngine !== 'undefined') { ThumbnailEngine.resetSource(freshData.src); } if(poster) poster.style.display = 'block'; if (d.querySelector('#pk_sub_name')) d.querySelector('#pk_sub_name').textContent = L.str_no_sub; autoMatchSubtitle(newItem); triggerResume(v, newItem); if (!isPlayerDestroyed) { await v.play().catch(()=>{}); } if (!v.paused && !isPlayerDestroyed) { box.classList.add('pk-v-started'); stopSpinner(); } updateState(); } catch (err) { if (myReqId === switchReqId) console.error("[SoftSwitch] Critical Error:", err); } finally { if (myReqId === switchReqId) { if(loader) loader.style.display = 'none'; } } }; const loadSource = (url, customStartTime = null) => { let startTime = 0; const skipOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; const pId = getPhysicalId(item); if (customStartTime !== null) { startTime = customStartTime; } else { const savedData = gmGet('pk_progress_' + pId, 0); let historyTime = 0; if (typeof savedData === 'number') { historyTime = savedData; } else if (savedData && typeof savedData === 'object') { historyTime = parseFloat(savedData.t || 0); } if (historyTime > 5) startTime = historyTime; else if (skipOp > 0) startTime = skipOp; } shutterTargetTime = startTime > 1 ? startTime : 0; if (tCur) tCur.textContent = fmtT(startTime); const totalDur = (item.params && item.params.duration) || S.durationMap.get(pId) || gmGet('pk_duration_' + pId, 0); if (progFilled && totalDur > 0) { const pct = Math.min(100, (startTime / totalDur) * 100); progFilled.style.setProperty('width', `${pct}%`, 'important'); } if (pkHls) { pkHls.destroy(); pkHls = null; } const isM3u8 = url.includes('.m3u8'); if (isM3u8 && window.Hls && window.Hls.isSupported()) { let finalPlayUrl = url; if (url.includes('&ext=.m3u8')) { const isHevc = (item.params?.video_codec === 'hevc') || (item.video_media_metadata?.video_codec === 'hevc'); if (isHevc) { console.log("[Hls] HEVC stream detected. Bypassing JS check to force hardware decoding."); } const realUrl = url.replace('&ext=.m3u8', ''); const duration = (item.params && item.params.duration) ? parseInt(item.params.duration) : 36000; const syntheticManifest = `#EXTM3U\n` + `#EXT-X-VERSION:3\n` + `#EXT-X-TARGETDURATION:${duration + 10}\n` + `#EXT-X-PLAYLIST-TYPE:VOD\n` + `#EXTINF:${duration},\n` + `${realUrl}\n` + `#EXT-X-ENDLIST`; const blob = new Blob([syntheticManifest], { type: 'application/x-mpegurl' }); finalPlayUrl = URL.createObjectURL(blob); console.log("[Hls] Generated synthetic manifest for TS stream"); } pkHls = new window.Hls({ debug: false, enableWorker: true, lowLatencyMode: true, startPosition: startTime, fragLoadingMaxRetry: 1, manifestLoadingMaxRetry: 1, levelLoadingMaxRetry: 1, fragLoadingTimeOut: 15000, manifestLoadingTimeOut: 10000, maxBufferHole: 0.5, maxFragLookUpTolerance: 0.1, nudgeOffset: 0.1, nudgeMaxRetry: 5, maxBufferLength: 120, maxMaxBufferLength: 600, backBufferLength: 90, maxBufferHole: 0.5, startLevel: -1, capLevelToPlayerSize: false }); pkHls.loadSource(finalPlayUrl); pkHls.attachMedia(v); const healthTimer = setInterval(() => { if (!pkHls || isPlayerDestroyed) { clearInterval(healthTimer); return; } if (box.classList.contains('buffering')) { if (!v._bufferingSince) v._bufferingSince = Date.now(); const isColdStart = v.currentTime < 1.0; const timeoutThreshold = isColdStart ? 60000 : 20000; if (Date.now() - v._bufferingSince > timeoutThreshold) { console.warn(`[Watchdog] Buffering timeout (${isColdStart ? 'Cold Start' : 'Mid-Stream'}). Forcing error...`); clearInterval(healthTimer); handleVideoError({ force: true, target: v }); } } else { v._bufferingSince = null; } if (!v.paused && v.currentTime > 0.5) { if (v.videoWidth === 0 || v.videoHeight === 0) { v._blackScreenCount = (v._blackScreenCount || 0) + 1; } else { const quality = v.getVideoPlaybackQuality ? v.getVideoPlaybackQuality() : null; const decodedFrames = quality ? quality.totalVideoFrames : (v.webkitDecodedFrameCount || 0); if (decodedFrames === 0) { v._blackScreenCount = (v._blackScreenCount || 0) + 1; } else { v._blackScreenCount = 0; } } if (v._blackScreenCount > 3) { console.warn(`[Watchdog] Black screen detected (Time: ${v.currentTime.toFixed(1)}, Frames: 0). Forcing external player.`); clearInterval(healthTimer); handleVideoError({ force: true, target: v }); } } }, 1000); pkHls.on(window.Hls.Events.LEVEL_LOADED, function (event, data) { if (!data.details || !data.details.videoCodec) return; const vCodec = data.details.videoCodec.toLowerCase(); const isBadVideo = vCodec.includes('mp4v') || vCodec.includes('hvc1') || vCodec.includes('hev1'); if (isBadVideo) { const testMime = `video/mp4; codecs="${vCodec}"`; if (window.MediaSource && !window.MediaSource.isTypeSupported(testMime)) { console.warn(`[VideoCheck] Unsupported video codec detected: ${vCodec}`); v.pause(); showSadBox(vCodec); if (pkHls) { pkHls.destroy(); pkHls = null; } } } }); pkHls.on(window.Hls.Events.AUDIO_TRACKS_UPDATED, function (event, data) { const tracks = data.audioTracks || []; let detectedBadCodec = null; for (const track of tracks) { const codec = (track.codec || '').toLowerCase(); const name = (track.name || '').toLowerCase(); const isSuspicious = codec.includes('ac-3') || codec.includes('ec-3') || codec.includes('dts') || name.includes('dts') || name.includes('truehd') || name.includes('atmos'); if (isSuspicious) { const testMime = `audio/mp4; codecs="${track.codec || 'ac-3'}"`; if (window.MediaSource && !window.MediaSource.isTypeSupported(testMime)) { detectedBadCodec = codec || name; break; } } } if (detectedBadCodec) { console.warn(`[AudioCheck] Unsupported codec detected: ${detectedBadCodec}`); v.pause(); showSadBox(detectedBadCodec.toUpperCase()); } }); pkHls.on(window.Hls.Events.ERROR, function (event, data) { if (data.fatal) { switch (data.type) { case window.Hls.ErrorTypes.NETWORK_ERROR: if (data.response && (data.response.code === 403 || data.response.code === 404 || data.response.code === 401)) { console.warn(`[Hls] Fatal Network Error (${data.response.code}). Triggering rollback.`); handleVideoError({ target: v }); } else { console.log("[Hls] Network error, trying to recover..."); pkHls.startLoad(); } break; case window.Hls.ErrorTypes.MEDIA_ERROR: pkHls.recoverMediaError(); setTimeout(() => { if (v.paused && !box.querySelector('.pk-err-dialog')) { handleVideoError({ target: v }); } }, 1500); break; default: handleVideoError({ target: v }); break; } } else if (data.details === 'bufferStalledError' && v.currentTime < 1) { handleVideoError({ target: v }); } }); } else { v.src = url; if (startTime > 0) { v.currentTime = startTime; } } }; let initialData = getBestSource(item); let currentLink = initialData.src; let qualityList = initialData.list; let currentResName = initialData.name; let lastWorkingLink = null; let lastWorkingResName = null; const failedUrls = new Set(); let posterUrl = item.thumbnail_link || ''; if (posterUrl && posterUrl.includes('SIZE_MEDIUM')) { posterUrl = posterUrl.replace('SIZE_MEDIUM', 'SIZE_LARGE'); } try { const linkUrl = new URL(currentLink); const linkDomain = linkUrl.origin; if (!document.head.querySelector(`link[href="${linkDomain}"]`)) { const pc = document.createElement('link'); pc.rel = 'preconnect'; pc.href = linkDomain; pc.crossOrigin = 'anonymous'; document.head.appendChild(pc); } } catch(e) {} const d = document.createElement('div'); d.id = 'pk-player-ov'; d.className = 'pk-ov pk-dark'; d.tabIndex = 0; d.style.cssText = "position:fixed;inset:0;z-index:2147483640;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;outline:none;will-change:transform;"; const icons = { play: '<path d="M9.5,4.3c-1.1-0.7-2.6,0.1-2.6,1.4v12.6c0,1.3,1.5,2.1,2.6,1.4l10.3-6.3c1.1-0.7,1.1-2.2,0-2.9L9.5,4.3z"/>', playCenter: '<path d="M7.7,4.3c-1.1-0.7-2.6,0.1-2.6,1.4v12.6c0,1.3,1.5,2.1,2.6,1.4l10.3-6.3c1.1-0.7,1.1-2.2,0-2.9L7.7,4.3z"/>', pause: '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>', vol: '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>', mute: '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73 4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>', full: '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>', settings: '<path d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z"/>', exitFull: '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>', close: '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>', search: '<g transform="scale(0.0234375)"><path d="M107.739429 580.388571a365.860571 365.860571 0 0 0 648.630857 85.504L635.611429 532.48a36.571429 36.571429 0 0 0-56.612572 2.852571l-39.131428 53.101715a109.714286 109.714286 0 0 1-167.716572 10.605714L235.008 455.387429a36.571429 36.571429 0 0 0-58.002286 6.729142l-69.266285 118.272z m-19.894858-110.738285l26.038858-44.470857a109.714286 109.714286 0 0 1 174.08-20.333715l137.216 143.798857a36.571429 36.571429 0 0 0 55.881142-3.584l39.131429-53.101714a109.714286 109.714286 0 0 1 169.691429-8.484571l102.985142 113.810285A365.714286 365.714286 0 1 0 87.844571 469.577143z m658.139429 318.317714a438.857143 438.857143 0 1 1 50.029714-52.736c1.316571 1.024 2.56 2.194286 3.803429 3.437714l206.921143 206.848a36.571429 36.571429 0 0 1-51.712 51.712l-206.921143-206.848a37.083429 37.083429 0 0 1-2.194286-2.413714zM526.628571 314.514286a73.142857 73.142857 0 1 1 0-146.285715 73.142857 73.142857 0 0 1 0 146.285715z" fill="currentColor"></path></g>', pip: '<g transform="scale(0.0234375)"><path d="M768 213.333333H256a85.333333 85.333333 0 0 0-85.333333 85.333334v426.666666a85.333333 85.333333 0 0 0 85.333333 85.333334h170.666667a42.666667 42.666667 0 1 1 0 85.333333H256a170.666667 170.666667 0 0 1-170.666667-170.666667V298.666667a170.666667 170.666667 0 0 1 170.666667-170.666667h512a170.666667 170.666667 0 0 1 170.666667 170.666667v128a42.666667 42.666667 0 1 1-85.333334 0V298.666667a85.333333 85.333333 0 0 0-85.333333-85.333334z m-128 341.333334a128 128 0 0 0-128 128v85.333333a128 128 0 0 0 128 128h170.666667a128 128 0 0 0 128-128v-85.333333a128 128 0 0 0-128-128h-170.666667z" fill="currentColor"></path></g>', rotL: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 3v5h5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>', rotR: '<path d="M21 12a9 9 0 1 1-9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 3v5h-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>', flipH: '<path d="M17 7l5 5-5 5M7 17l-5-5 5-5M2 12h20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>', flipV: '<path d="M7 7l5-5 5 5M17 17l-5 5-5-5M12 2v20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' }; const mkSvg = (path) => `<svg viewBox="0 0 24 24">${path}</svg>`; const renderQualityMenu = (list, activeName) => list.map(q => { const displayName = q.name.replace(/\s(\(|\uff08)/, '<br>$1'); return `<div class="pk-p-item ${q.name === activeName ? 'active' : ''}" data-link="${q.link}" style="line-height:1.3; padding:8px 12px;">${displayName}</div>`; }).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 = ` <div class="pk-player-box" id="pk_p_box" style="width: 90%; max-width: calc(1600px / var(--pk-zoom, 1)); min-width: 480px; height: 80%; border-radius: 0; box-shadow: 0 25px 50px rgba(0,0,0,0.5);"> <div id="pk_p_preview"> <div class="pk-prev-img-box" id="pk_p_img_box"></div> <div class="pk-prev-time">00:00</div> </div> <div id="pk_p_poster" style="position:absolute; inset:0; z-index:1; background:#000; display:flex; align-items:center; justify-content:center; pointer-events:none;"> <img src="${posterUrl}" style="width:100%; height:100%; object-fit:contain; opacity:${posterUrl ? 1 : 0};" onerror="this.style.display='none';"> </div> <div id="pk_sub_render_layer" style="position:absolute; inset:0; z-index:40; pointer-events:none; display:flex; flex-direction:column; justify-content:flex-end; align-items:center; padding-bottom:10%; text-align:center; overflow:hidden; transition: padding-bottom 0.1s linear;"> <span id="pk_sub_text" style="background:rgba(0,0,0,0.6); color:#fff; padding:4px 8px; border-radius:4px; font-size:24px; line-height:1.4; white-space:pre-wrap; display:none; text-shadow:0 1px 2px black; transition: font-size 0.1s linear, background-color 0.1s linear;"></span> </div> <video class="pk-player-video" id="pk_video" type="video/mp4" crossorigin="anonymous" referrerpolicy="no-referrer" preload="auto" fetchpriority="high" playsinline style="position:relative; z-index:0; transform: translateZ(0); backface-visibility: hidden; image-rendering: -webkit-optimize-contrast; -webkit-font-smoothing: antialiased;"> </video> <div class="pk-p-loading"><div class="pk-spin-lg" style="border-color:rgba(255,255,255,0.3); border-top-color:#fff;"></div></div> <div class="pk-p-center-play" id="pk_p_center_btn"> ${mkSvg(icons.playCenter)} </div> <div class="pk-p-vol-indicator" id="pk_p_vol_indicator"> <div style="width:24px;height:24px;display:flex;align-items:center;">${mkSvg(icons.vol)}</div> <span id="pk_p_vol_val">100%</span> </div> <div class="pk-p-seek-indicator" id="pk_p_seek_indicator"> 00:00 / 00:00 </div> <div class="pk-p-side-nav L" id="pk_p_side_L" data-pk-tip="${L.btn_prev_video}">${mkSvg('<path d="M15.5 19l-7-7 7-7"/>')}</div> <div class="pk-p-side-nav R" id="pk_p_side_R" data-pk-tip="${L.btn_next_video}">${mkSvg('<path d="M8.5 19l7-7-7-7"/>')}</div> <div class="pk-player-top"> <div class="pk-player-title">[${curListIdx + 1}/${totalInList}] ${esc(item.name)}</div> <div style="display:flex; gap:10px;"> <div class="pk-p-btn" id="pk_p_pip" data-pk-tip="${L.tip_pip}" style="display:none !important;">${mkSvg(icons.pip)}</div> <div class="pk-p-btn" id="pk_p_search" data-pk-tip="${L.tip_play_search}" style="display:none !important;">${mkSvg(icons.search)}</div> <div class="pk-p-btn" id="pk_p_close" data-pk-tip="${L.tip_close}">${mkSvg(icons.close)}</div> </div> </div> <div class="pk-p-plist-ov" id="pk_p_plist"> <div class="pk-p-plist-tab" id="pk_p_plist_tab" data-pk-tip="${L.tip_plist_open}"> ${curListIdx + 1} / ${totalInList} <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg> </div> <div class="pk-p-plist-strip"> <div class="pk-p-plist-nav L" id="pk_p_plist_L">${mkSvg('<path d="M15 6 L9 12 L15 18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>')}</div> <div class="pk-p-plist-scroll" id="pk_p_plist_scroll"> ${renderPlaylistItems()} </div> <div class="pk-p-plist-nav R" id="pk_p_plist_R">${mkSvg('<path d="M9 6 L15 12 L9 18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>')}</div> </div> </div> <div class="pk-p-prog-wrap" style="position: absolute; bottom: 64px; left: 20px; right: 20px; z-index: 61; display: flex; align-items: center; gap: 16px;"> <div class="pk-p-time-side" id="pk_t_cur" style="font-size: 14px; font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-weight: 600; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,0.9); font-variant-numeric: tabular-nums; min-width: 60px; text-align: right;">00:00:00</div> <div class="pk-player-progress-container" id="pk_p_prog_area" style="position: relative; flex: 1; height: 16px; cursor: pointer; display: flex; align-items: center;"> <div class="pk-player-progress-bg" style="width: 100%; height: 4px; background: rgba(255,255,255,0.2); position: relative; border-radius: 2px; backdrop-filter: blur(2px);"> <div class="pk-player-progress-filled" id="pk_p_filled" style="height: 100%; background: var(--pk-pri); width: 0; position: relative; border-radius: 2px;"> <div class="pk-player-progress-thumb" style="position: absolute; right: -11px; width: 22px; height: 22px; background: transparent; display: flex; align-items: center; justify-content: center;"> <svg viewBox="0 0 192 192" shape-rendering="geometricPrecision" style="width: 100%; height: 100%; overflow: visible; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.5));"> <path d="M23.3 57.5 82.5 37" fill="none" stroke="#444" stroke-width="12" stroke-linecap="round" stroke-miterlimit="10"/> <path d="m168.7 57.5-59.2-20.5" fill="none" stroke="#444" stroke-width="12" stroke-linecap="round" stroke-miterlimit="10"/> <rect x="22" y="57.5" width="148" height="92.5" rx="18" ry="18" fill="#fff" stroke="#444" stroke-width="12" /> <g class="pk-eye-open"> <path class="pk-p-eye" d="M69.7 118.5c3.8 0 6.9-3.1 6.9-6.9V97.1c0-3.8-3.1-6.9-6.9-6.9-3.8 0-6.9 3.1-6.9 6.9v14.6c0 3.8 3.1 6.8 6.9 6.8z" fill="#444" stroke="#444" stroke-width="5"/> <path class="pk-p-eye" d="M122.3 118.5c-3.8 0-6.9-3.1-6.9-6.9V97.1c0-3.8 3.1-6.9 6.9-6.9 3.8 0 6.9 3.1 6.9 6.9v14.6c0 3.8-3.1 6.8-6.9 6.8z" fill="#444" stroke="#444" stroke-width="5"/> </g> <g class="pk-eye-closed"> <rect class="pk-p-eye" x="57.7" y="100" width="24" height="8" rx="4" ry="4" fill="#444" /> <rect class="pk-p-eye" x="110.3" y="100" width="24" height="8" rx="4" ry="4" fill="#444" /> </g> </svg> </div> </div> </div> </div> <div class="pk-p-time-side" id="pk_t_dur" style="font-size: 14px; font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-weight: 600; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,0.9); font-variant-numeric: tabular-nums; min-width: 60px; text-align: left;">00:00:00</div> </div> <div class="pk-player-controls"> <div class="pk-p-btn" id="pk_p_play">${mkSvg(icons.pause)}</div> <div class="pk-p-vol-wrap" style="margin-left: 8px;"> <div class="pk-p-btn" id="pk_p_vol">${mkSvg(icons.vol)}</div> <input type="range" class="pk-p-vol-slider" id="pk_p_vol_slide" min="0" max="1" step="0.05" value="1"> </div> <div style="flex:1"></div> <div class="pk-p-menu-con"> <span id="pk_p_res_txt">${currentResName}</span> <div class="pk-p-pop" id="pk_p_res_list">${renderQualityMenu(qualityList, currentResName)}</div> </div> <div class="pk-p-menu-con"> <span id="pk_p_spd_txt">1.0x</span> <div class="pk-p-pop" id="pk_p_spd_list"> <div class="pk-p-item" data-spd="3.0">3.0x</div> <div class="pk-p-item" data-spd="2.0">2.0x</div> <div class="pk-p-item" data-spd="1.5">1.5x</div> <div class="pk-p-item" data-spd="1.25">1.25x</div> <div class="pk-p-item active" data-spd="1.0">1.0x</div> <div class="pk-p-item" data-spd="0.75">0.75x</div> <div class="pk-p-item" data-spd="0.5">0.5x</div> </div> </div> <div class="pk-p-menu-con" id="pk_sub_trigger"> <div class="pk-p-btn">${mkSvg(icons.settings)}</div> <div class="pk-p-sub-pop" id="pk_sub_panel" onclick="event.stopPropagation()"> <div class="pk-sub-tabs"> <div class="pk-sub-tab active" data-target="pane-sub">${L.tab_sub}</div> <div class="pk-sub-tab" data-target="pane-size">${L.tab_size}</div> <div class="pk-sub-tab" data-target="pane-more">${L.tab_more}</div> </div> <div class="pk-sub-pane active" id="pane-sub"> <div class="pk-sub-row"> <span class="pk-sub-label">${L.lbl_sub_sel}</span> <label style="display:flex;align-items:center;gap:4px;cursor:pointer;"> <input type="checkbox" id="pk_sub_toggle" class="pk-sub-check" checked> <span>${L.lbl_show_sub}</span> </label> </div> <div style="background:#333; padding:6px; border-radius:4px; color:#888; font-size:11px; margin-bottom:8px;" id="pk_sub_name"> ${L.str_no_sub} </div> <div class="pk-sub-btn-group"> <div class="pk-sub-btn" id="pk_sub_search_btn">${L.btn_sub_search}</div> <div class="pk-sub-btn" id="pk_sub_cloud_btn">${L.btn_sub_cloud}</div> <div class="pk-sub-btn" id="pk_sub_local_btn">${L.btn_sub_local}</div> <input type="file" id="pk_sub_file" accept=".srt,.vtt,.ass" style="display:none;"> </div> <div style="height:10px;"></div> <div class="pk-sub-row"> <span class="pk-sub-label">${L.lbl_sub_pos}</span> <div style="display:flex;align-items:center;gap:8px;"> <span style="font-size:11px;color:#888;white-space:nowrap;">${L.lbl_sub_bottom}</span> <input type="range" id="pk_sub_pos" class="pk-sub-slider" min="0" max="100" value="10"> <span style="font-size:11px;color:#888;white-space:nowrap;">${L.lbl_sub_top}</span> </div> </div> <div class="pk-sub-row"> <span class="pk-sub-label">${L.lbl_sub_bg_op}</span> <div style="display:flex;align-items:center;gap:8px;"> <span style="font-size:11px;color:#888;white-space:nowrap;">0%</span> <input type="range" id="pk_sub_bg_opacity" class="pk-sub-slider" min="0" max="100" value="60"> <span style="font-size:11px;color:#888;white-space:nowrap;">100%</span> </div> </div> <div class="pk-sub-row"> <span class="pk-sub-label">${L.lbl_sub_size}</span> <div class="pk-sub-ctrl"> <div class="pk-sub-ctrl-btn" id="pk_sub_size_dec">-</div> <div class="pk-sub-val" id="pk_sub_size_val">20</div> <div class="pk-sub-ctrl-btn" id="pk_sub_size_inc">+</div> </div> </div> <div class="pk-sub-row"> <span class="pk-sub-label">${L.lbl_sub_offset}</span> <div class="pk-sub-ctrl"> <div class="pk-sub-ctrl-btn" id="pk_sub_time_dec">-</div> <div class="pk-sub-val" id="pk_sub_time_val">0.0 ${L.unit_sec}</div> <div class="pk-sub-ctrl-btn" id="pk_sub_time_inc">+</div> </div> </div> </div> <div class="pk-sub-pane" id="pane-size"> <div class="pk-size-row"> <span class="pk-size-label">${L.lbl_ratio}</span> <div class="pk-size-opts" id="pk_ratio_opts"> <div class="pk-size-btn active" data-ratio="default">${L.opt_ratio_def}</div> <div class="pk-size-btn" data-ratio="16/9">16:9</div> <div class="pk-size-btn" data-ratio="4/3">4:3</div> </div> </div> <div class="pk-size-row" style="align-items: flex-start;"> <span class="pk-size-label" style="margin-top:8px;">${L.lbl_direction}</span> <div class="pk-size-opts" style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; width:100%;"> <div class="pk-size-btn" id="pk_btn_rot_l">${mkSvg(icons.rotL)} ${L.btn_rot_l}</div> <div class="pk-size-btn" id="pk_btn_rot_r">${mkSvg(icons.rotR)} ${L.btn_rot_r}</div> <div class="pk-size-btn" id="pk_btn_flip_h">${mkSvg(icons.flipH)} ${L.btn_flip_h}</div> <div class="pk-size-btn" id="pk_btn_flip_v">${mkSvg(icons.flipV)} ${L.btn_flip_v}</div> </div> </div> </div> <div class="pk-sub-pane" id="pane-more"> <div style="font-weight:bold; margin-bottom:5px; color:#fff;">${L.lbl_play_end}</div> <div class="pk-sub-radio-grp"> <label class="pk-sub-radio-item"><input type="radio" name="pk_pmode" value="list_loop"> ${L.opt_list_loop}</label> <label class="pk-sub-radio-item"><input type="radio" name="pk_pmode" value="single_loop"> ${L.opt_single_loop}</label> <label class="pk-sub-radio-item"><input type="radio" name="pk_pmode" value="stop"> ${L.opt_play_stop}</label> </div> <div class="pk-more-sep"></div> <div class="pk-sub-row"> <span class="pk-sub-label">${L.lbl_skip_op}</span> <div style="display:flex; align-items:center; gap:8px;"> <div class="pk-sub-btn" id="pk_op_mark" style="padding:2px 8px; font-size:11px;">${L.btn_mark}</div> <div class="pk-sub-ctrl"> <div class="pk-sub-ctrl-btn" id="pk_op_dec">-</div> <div class="pk-sub-val" id="pk_op_val">0 ${L.unit_sec}</div> <div class="pk-sub-ctrl-btn" id="pk_op_inc">+</div> </div> </div> </div> <div class="pk-sub-row"> <span class="pk-sub-label">${L.lbl_skip_ed}</span> <div style="display:flex; align-items:center; gap:8px;"> <div class="pk-sub-btn" id="pk_ed_mark" style="padding:2px 8px; font-size:11px;">${L.btn_mark}</div> <div class="pk-sub-ctrl"> <div class="pk-sub-ctrl-btn" id="pk_ed_dec">-</div> <div class="pk-sub-val" id="pk_ed_val">0 ${L.unit_sec}</div> <div class="pk-sub-ctrl-btn" id="pk_ed_inc">+</div> </div> </div> </div> </div> </div> <div class="pk-p-btn" id="pk_p_full" data-pk-tip="${L.tip_full_screen}">${mkSvg(icons.full)}</div> </div> </div> `; const v = d.querySelector('#pk_video'); const box = d.querySelector('#pk_p_box'); const btnPlay = d.querySelector('#pk_p_play'); const btnVol = d.querySelector('#pk_p_vol'); const slideVol = d.querySelector('#pk_p_vol_slide'); const btnFull = d.querySelector('#pk_p_full'); const btnClose = d.querySelector('#pk_p_close'); const btnSearch = d.querySelector('#pk_p_search'); const tCur = d.querySelector('#pk_t_cur'); const tDur = d.querySelector('#pk_t_dur'); const progArea = d.querySelector('#pk_p_prog_area'); const progFilled = d.querySelector('#pk_p_filled'); const resList = d.querySelector('#pk_p_res_list'); const resTxt = d.querySelector('#pk_p_res_txt'); const spdList = d.querySelector('#pk_p_spd_list'); const plist = d.querySelector('#pk_p_plist'); const pTab = d.querySelector('#pk_p_plist_tab'); const pScroll = d.querySelector('#pk_p_plist_scroll'); pScroll.onwheel = (e) => { e.preventDefault(); pScroll.scrollBy({ left: e.deltaY > 0 ? 300 : -300, behavior: 'smooth' }); }; let pTip = document.getElementById('pk_p_plist_tip_global'); if (!pTip) { pTip = document.createElement('div'); pTip.id = 'pk_p_plist_tip_global'; pTip.className = 'pk-p-plist-tip'; document.body.appendChild(pTip); } d.querySelector('#pk_p_side_L').onclick = (e) => { e.stopPropagation(); const prevIdx = (curListIdx - 1 + totalInList) % totalInList; softSwitch(prevIdx); }; d.querySelector('#pk_p_side_R').onclick = (e) => { e.stopPropagation(); const nextIdx = (curListIdx + 1) % totalInList; softSwitch(nextIdx); }; pTab.onmouseenter = () => box.classList.add('pk-tab-hover'); pTab.onmouseleave = () => box.classList.remove('pk-tab-hover'); const strip = d.querySelector('.pk-p-plist-strip'); if (strip) { strip.onmouseenter = () => box.classList.add('pk-tab-hover'); strip.onmouseleave = () => box.classList.remove('pk-tab-hover'); } pTab.onclick = (e) => { e.stopPropagation(); plist.classList.toggle('open'); box.classList.toggle('plist-active'); pTab.setAttribute('data-pk-tip', plist.classList.contains('open') ? L.tip_plist_close : L.tip_plist_open); if (plist.classList.contains('open')) { const activeItem = pScroll.querySelector('.active'); if (activeItem) activeItem.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'center' }); } }; pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onmouseenter = (e) => { if (!plist.classList.contains('open')) return; pTip.innerHTML = `<strong>${e.currentTarget.dataset.name}</strong><br>${e.currentTarget.dataset.size}`; pTip.style.display = 'block'; }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => pTip.style.display = 'none'; el.onclick = (e) => { e.stopPropagation(); if (pTip) pTip.style.display = 'none'; const idx = parseInt(e.currentTarget.dataset.idx); if (idx === curListIdx) return; softSwitch(idx); }; }); const updatePlistNav = () => { const sl = pScroll.scrollLeft; const sw = pScroll.scrollWidth; const cw = pScroll.clientWidth; d.querySelector('#pk_p_plist_L').style.display = sl <= 5 ? 'none' : 'flex'; d.querySelector('#pk_p_plist_R').style.display = (sl + cw >= sw - 5) ? 'none' : 'flex'; }; d.querySelector('#pk_p_plist_L').onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: -400, behavior: 'smooth' }); setTimeout(updatePlistNav, 300); }; d.querySelector('#pk_p_plist_R').onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: 400, behavior: 'smooth' }); setTimeout(updatePlistNav, 300); }; pScroll.addEventListener('scroll', updatePlistNav, { passive: true }); setTimeout(updatePlistNav, 50); document.body.appendChild(d); const initL = d.querySelector('#pk_p_side_L'); const initR = d.querySelector('#pk_p_side_R'); if (initL) initL.style.display = curListIdx === 0 ? 'none' : 'flex'; if (initR) initR.style.display = curListIdx === totalInList - 1 ? 'none' : 'flex'; let hideTimer = null; let isMouseOverUI = false; const resetHideTimer = () => { box.classList.remove('ui-hidden'); if (hideTimer) clearTimeout(hideTimer); if (isMouseOverUI) return; hideTimer = setTimeout(() => { if (!v.paused) box.classList.add('ui-hidden'); }, 3000); }; const protectedUIs = [ d.querySelector('.pk-player-top'), d.querySelector('.pk-player-controls'), d.querySelector('.pk-p-prog-wrap') ]; protectedUIs.forEach(ui => { if (!ui) return; ui.addEventListener('mouseenter', () => { isMouseOverUI = true; if (hideTimer) clearTimeout(hideTimer); }); ui.addEventListener('mouseleave', () => { isMouseOverUI = false; resetHideTimer(); }); }); box.addEventListener('mouseleave', () => { if (!isMouseOverUI) box.classList.add('ui-hidden'); }); box.addEventListener('mouseenter', resetHideTimer); box.addEventListener('mousemove', resetHideTimer); box.addEventListener('click', resetHideTimer); box.addEventListener('keydown', resetHideTimer); resetHideTimer(); const handleVideoError = (e) => { if (isSwitching) return; if (!v.getAttribute('src') && !pkHls) return; if (v.networkState === 2 && !v.error && !e.force) return; const errCode = v.error ? v.error.code : (e.force ? 4 : 0); const errMsg = v.error ? v.error.message : ""; console.warn(`[VideoError] Code: ${errCode}, Msg: ${errMsg}, Src: ${v.src || 'HLS'}`); if (currentLink) failedUrls.add(currentLink); if (lastWorkingLink && lastWorkingLink !== currentLink && !failedUrls.has(lastWorkingLink)) { console.log(`[Compatibility] Target stream failed, rolling back to: ${lastWorkingResName}`); if (resTxt) resTxt.textContent = `${L.str_compat_mode} (${lastWorkingResName})`; const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(217, 48, 37, 0.9);color:#fff;padding:6px 12px;border-radius:20px;font-size:12px;z-index:100;animation:pkFadeIn 0.5s;"; toast.textContent = L.str_switch_compat.replace('{n}', lastWorkingResName); box.appendChild(toast); setTimeout(() => toast.remove(), 4000); currentResName = lastWorkingResName; currentLink = lastWorkingLink; const savedTime = v.currentTime; loadSource(currentLink, savedTime); v.play().catch(()=>{}); const resList = d.querySelector('#pk_p_res_list'); if (resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } return; } const nextCandidate = qualityList.find(q => { const url = q.link || q.url; return url !== currentLink && !failedUrls.has(url); }); if (nextCandidate) { console.log(`[Compatibility] Auto-switching to next available source: ${nextCandidate.name}`); if (resTxt) resTxt.textContent = `${L.str_compat_mode} (${nextCandidate.name})`; const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(33, 150, 243, 0.9);color:#fff;padding:6px 12px;border-radius:20px;font-size:12px;z-index:100;animation:pkFadeIn 0.5s;"; toast.textContent = L.msg_fallback_report.replace('{n}', nextCandidate.name); box.appendChild(toast); setTimeout(()=>toast.remove(), 4000); currentResName = nextCandidate.name; currentLink = nextCandidate.link || nextCandidate.url; const savedTime = v.currentTime; loadSource(currentLink, savedTime); v.play().catch(()=>{}); const resList = d.querySelector('#pk_p_res_list'); if (resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } return; } if (errCode !== 4 && !e.force) { console.log("[Video] Recoverable error detected, staying in loader."); return; } const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'none'; showSadBox(currentResName); }; v.addEventListener('error', handleVideoError); v.addEventListener('loadstart', () => { const errOv = box.querySelector('.pk-err-ov'); if (errOv) errOv.remove(); const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'block'; }); (async () => { try { const targetApiId = getPhysicalId(item); const newData = await apiGet(targetApiId); const freshData = getBestSource(newData); qualityList = freshData.list; resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); if (v.error || !currentLink || (currentResName === L.str_original && freshData.name !== L.str_original)) { const savedTime = v.currentTime; currentLink = freshData.src; currentResName = freshData.name; resTxt.textContent = currentResName; loadSource(currentLink); v.currentTime = savedTime; v.play().catch(()=>{}); resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } } catch (e) { } })(); const fmtT = (s) => { s = Math.max(0, s || 0); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sc = Math.floor(s % 60); return String(h).padStart(2, '0') + ":" + String(m).padStart(2, '0') + ":" + String(sc).padStart(2, '0'); }; const fmtFullT = fmtT; const togglePlay = () => { if (v.paused) v.play(); else v.pause(); }; const ThumbnailEngine = (() => { let shadowV = null; let canvas = null; let ctx = null; let isInit = false; let cacheStore = null; let currentReqId = 0; const BASE_HEIGHT = 180; const DISPLAY_HEIGHT = 120; const previewBox = d.querySelector('#pk_p_preview'); const imgBox = d.querySelector('#pk_p_img_box'); const previewTime = previewBox.querySelector('.pk-prev-time'); const init = async () => { if (isInit) return; if (window.localforage) { cacheStore = window.localforage.createInstance({ name: 'pk_thumbs', storeName: 'snapshots' }); } shadowV = document.createElement('video'); shadowV.muted = true; shadowV.crossOrigin = 'anonymous'; shadowV.style.display = 'none'; shadowV.preload = 'auto'; shadowV.src = currentLink; canvas = document.createElement('canvas'); canvas.width = 160; canvas.height = 90; ctx = canvas.getContext('2d'); shadowV.onerror = () => console.warn("[Thumb] Shadow player error"); isInit = true; }; const getCacheKey = (time) => `${getPhysicalId(item)}_${Math.floor(time)}`; const generate = async (time) => { if (!isInit) await init(); if (cacheStore) { const cachedBlob = await cacheStore.getItem(getCacheKey(time)); if (cachedBlob) return URL.createObjectURL(cachedBlob); } return new Promise((resolve, reject) => { const seekHandler = () => { try { const vw = shadowV.videoWidth || 160; const vh = shadowV.videoHeight || 90; const ratio = vw / vh; const renderH = BASE_HEIGHT; const renderW = Math.floor(renderH * ratio); if (canvas.width !== renderW || canvas.height !== renderH) { canvas.width = renderW; canvas.height = renderH; } ctx.drawImage(shadowV, 0, 0, renderW, renderH); canvas.toBlob((blob) => { if (cacheStore) cacheStore.setItem(getCacheKey(time), blob).catch(()=>{}); resolve(URL.createObjectURL(blob)); }, 'image/jpeg', 0.8); } catch (e) { reject(e); } finally { shadowV.removeEventListener('seeked', seekHandler); } }; shadowV.addEventListener('seeked', seekHandler); shadowV.currentTime = time; }); }; const show = async (clientX, rect) => { const boxRect = box.getBoundingClientRect(); const pos = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); const targetTime = pos * v.duration; if (!isFinite(targetTime)) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let left = (clientX - boxRect.left) / scale; const halfWidth = (imgBox.offsetWidth / 2) || 80; const minX = halfWidth + 10; const maxX = (boxRect.width / scale) - halfWidth - 10; left = Math.max(minX, Math.min(maxX, left)); previewBox.style.left = `${left}px`; previewTime.textContent = fmtT(targetTime); previewBox.classList.add('show'); const myId = ++currentReqId; try { await sleep(50); if (myId !== currentReqId) return; const url = await generate(targetTime); if (myId !== currentReqId) return; const img = document.createElement('img'); img.src = url; const onImgReady = () => { if (myId !== currentReqId) return; if (img.naturalWidth && img.naturalHeight) { const ratio = img.naturalWidth / img.naturalHeight; imgBox.style.width = `${DISPLAY_HEIGHT * ratio}px`; imgBox.style.height = `${DISPLAY_HEIGHT}px`; } imgBox.style.display = 'flex'; img.classList.add('active'); imgBox.appendChild(img); const oldImages = imgBox.querySelectorAll('img'); if (oldImages.length > 1) { setTimeout(() => { for (let i = 0; i < oldImages.length - 1; i++) { oldImages[i].remove(); } }, 150); } }; if (img.complete) onImgReady(); else img.onload = onImgReady; } catch (e) { } }; const hide = () => { previewBox.classList.remove('show'); imgBox.style.display = 'none'; currentReqId++; setTimeout(() => { if (!previewBox.classList.contains('show')) { const imgs = imgBox.querySelectorAll('img'); imgs.forEach(i => i.remove()); } }, 500); }; const resetSource = (newUrl) => { if (shadowV) shadowV.src = newUrl; }; return { show, hide, resetSource }; })(); const updateState = () => { if (v.paused) { box.classList.add('paused'); btnPlay.innerHTML = mkSvg(icons.play); box.classList.remove('ui-hidden'); if (hideTimer) clearTimeout(hideTimer); } else { box.classList.remove('paused'); btnPlay.innerHTML = mkSvg(icons.pause); resetHideTimer(); } }; const updateVolUI = () => { slideVol.value = v.muted ? 0 : v.volume; btnVol.innerHTML = mkSvg((v.muted || v.volume === 0) ? icons.mute : icons.vol); }; let transcodeTimer = null; const destroyPlayer = () => { if (isPlayerDestroyed) return; isPlayerDestroyed = true; document.removeEventListener('keydown', playerKeyHandler); if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); } window.removeEventListener('resize', onResizeTransform); if (transcodeTimer) { clearInterval(transcodeTimer); transcodeTimer = null; } if (v.duration > 0 && v.currentTime > 5 && v.duration - v.currentTime > 5) { gmSet('pk_progress_' + getPhysicalId(item), { t: v.currentTime, d: v.duration, ts: Date.now() }); } v.pause(); v.muted = true; if (pkHls) { pkHls.stopLoad(); pkHls.detachMedia(); pkHls.destroy(); pkHls = null; } const targetId = item.id; const targetIdx = S.display.findIndex(x => x.id === targetId); if (targetIdx !== -1) { S.sel.clear(); S.sel.add(targetId); S.activeId = targetId; const rowTop = targetIdx * CONF.rowHeight; const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); } v.src = ""; v.load(); if (styleEl) styleEl.remove(); d.remove(); }; v.addEventListener('play', updateState); v.addEventListener('pause', updateState); const markStarted = () => { if (isPlayerDestroyed) return; if (box) { box.classList.add('pk-v-started'); stopSpinner(); updateState(); lastWorkingLink = currentLink; lastWorkingResName = currentResName; } }; v.addEventListener('playing', markStarted); v.addEventListener('seeked', () => { if(v.currentTime > 0.1) markStarted(); }); v.addEventListener('click', () => { markStarted(); togglePlay(); }); v.addEventListener('dblclick', (e) => { e.stopPropagation(); btnFull.click(); }); const posterEl = d.querySelector('#pk_p_poster'); const loaderEl = d.querySelector('.pk-p-loading'); let shutterTargetTime = 0; let isPiPDesired = false; const stopSpinner = (force = false) => { const isErrorVisible = !!box.querySelector('.pk-err-dialog'); if ((shutterTargetTime > 0 || isErrorVisible) && !force) return; box.classList.remove('buffering'); if (v.paused && v.readyState >= 2) { box.classList.add('pk-v-started'); updateState(); } if (loaderEl) loaderEl.style.display = 'none'; if (posterEl) { posterEl.style.pointerEvents = 'none'; posterEl.style.opacity = '0'; setTimeout(() => { if(posterEl.style.opacity === '0') { posterEl.style.display = 'none'; posterEl.style.pointerEvents = 'auto'; } }, 450); } }; const showSpinner = () => { box.classList.add('buffering'); if (loaderEl) loaderEl.style.display = 'block'; }; v.addEventListener('waiting', showSpinner); v.addEventListener('stalled', showSpinner); v.addEventListener('playing', stopSpinner); v.addEventListener('seeked', stopSpinner); v.addEventListener('canplaythrough', stopSpinner); let lastTextUpdate = 0; const updateTimeUI = () => { requestAnimationFrame(() => { if (isDragSeek) return; if (v.currentTime > 0.1) stopSpinner(); const dur = v.duration; const cur = v.currentTime; const now = performance.now(); if (dur > 0) { const pct = (cur / dur) * 100; progFilled.style.width = `${pct}%`; } if (now - lastTextUpdate > 250) { tCur.textContent = fmtT(cur); if (dur > 0 && isFinite(dur)) tDur.textContent = fmtT(dur); lastTextUpdate = now; } }); }; v.addEventListener('timeupdate', updateTimeUI); v.addEventListener('durationchange', updateTimeUI); v.addEventListener('timeupdate', () => { if (shutterTargetTime > 0) { if (v.currentTime >= shutterTargetTime - 0.5 && v.readyState >= 3) { v._isRestarting = false; console.log(`[Shutter] Target reached: ${v.currentTime}, Unlocking...`); shutterTargetTime = 0; stopSpinner(true); } } }); let hasCheckedProgress = false; let lastSaveTime = 0; const applyProgress = () => { if (hasCheckedProgress || v.duration <= 0) return; hasCheckedProgress = true; triggerResume(v, item); }; v.addEventListener('canplay', applyProgress, { once: true }); v.addEventListener('playing', () => { const now = Date.now(); const pId = getPhysicalId(item); const existing = gmGet('pk_progress_' + pId); let data = { t: v.currentTime, d: v.duration || 0, ts: now }; if (existing && typeof existing === 'object') { data.t = existing.t; if (!data.d) data.d = existing.d; } gmSet('pk_progress_' + pId, data); }); v.addEventListener('timeupdate', () => { const now = Date.now(); if (now - lastSaveTime > 3000) { const curT = v.currentTime; const totalT = v.duration || 0; const progressData = { t: curT, d: totalT, ts: now }; const pId = getPhysicalId(item); if (totalT > 0 && (totalT - curT < 5)) { progressData.t = 0; gmSet('pk_progress_' + pId, progressData); } else if (curT > 1) { gmSet('pk_progress_' + pId, progressData); } lastSaveTime = now; } }); v.addEventListener('loadedmetadata', () => { triggerResume(v, item); updateTimeUI(); const dur = v.duration; if (dur > 0 && isFinite(dur)) { const seconds = Math.round(dur); const pId = getPhysicalId(item); gmSet('pk_duration_' + pId, seconds); S.durationMap.set(pId, seconds); if (item.params) item.params.duration = seconds; if (typeof renderVisible === 'function') renderVisible(); } }); const enableMediaControls = () => { if (v.readyState >= 2) { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); const btnPip = d.querySelector('#pk_p_pip'); if (btnPip && document.pictureInPictureEnabled && !v.disablePictureInPicture) { btnPip.style.setProperty('display', 'flex', 'important'); if (isPiPDesired && !document.pictureInPictureElement) { v.requestPictureInPicture().catch(() => { isPiPDesired = false; }); } } } }; v.addEventListener('loadeddata', enableMediaControls); v.addEventListener('canplay', enableMediaControls); if (v.readyState >= 2) enableMediaControls(); let isDragSeek = false; let hasUserSeeked = false; let initialTimeBeforeDrag = 0; let progRectCache = null; let lastSeekRequestTime = 0; let lastMouseX = 0; const seekIndicator = d.querySelector('#pk_p_seek_indicator'); const updateVisualOnly = (clientX) => { if (!progRectCache || !v.duration) return 0; const pos = Math.max(0, Math.min(1, (clientX - progRectCache.left) / progRectCache.width)); const targetTime = pos * v.duration; progFilled.style.setProperty('width', `${pos * 100}%`, 'important'); seekIndicator.textContent = `${fmtFullT(targetTime)} / ${fmtFullT(v.duration)}`; if (tCur) tCur.textContent = fmtT(targetTime); return targetTime; }; const updateVideoOnDemand = (targetTime) => { const now = performance.now(); if (v.paused && (now - lastSeekRequestTime > 40)) { v.currentTime = targetTime; lastSeekRequestTime = now; } }; const stopDragging = (isCancel = false) => { if (!isDragSeek) return; if (isCancel) { v.currentTime = initialTimeBeforeDrag; const revertPos = (initialTimeBeforeDrag / v.duration) * 100; progFilled.style.setProperty('width', `${revertPos}%`, 'important'); } else { const finalT = updateVisualOnly(lastMouseX); v.currentTime = finalT; if (finalT > 5) { gmSet('pk_progress_' + getPhysicalId(item), { t: finalT, d: v.duration, ts: Date.now() }); } } isDragSeek = false; progRectCache = null; document.body.classList.remove('pk-dragging'); box.classList.remove('pk-is-seeking'); if (typeof ThumbnailEngine !== 'undefined') { if (progArea && !progArea.matches(':hover')) { ThumbnailEngine.hide(); } } const thumb = box.querySelector('.pk-player-progress-thumb'); if (thumb) { thumb.classList.remove('pk-look-r', 'pk-look-l'); thumb.classList.add('pk-blink-anim', 'pk-blink-hold'); setTimeout(() => thumb.classList.remove('pk-blink-anim'), 200); setTimeout(() => thumb.classList.remove('pk-blink-hold'), 300); } seekIndicator.style.display = 'none'; updateState(); }; const onMouseMove = (e) => { if (!isDragSeek) return; const topBar = d.querySelector('.pk-player-top'); if (topBar) { const barRect = topBar.getBoundingClientRect(); if (e.clientY >= barRect.top && e.clientY <= barRect.bottom) { stopDragging(true); return; } } const thumb = box.querySelector('.pk-player-progress-thumb'); if (thumb) { if (e.clientX > lastMouseX + 1) { thumb.classList.add('pk-look-r'); thumb.classList.remove('pk-look-l'); } else if (e.clientX < lastMouseX - 1) { thumb.classList.add('pk-look-l'); thumb.classList.remove('pk-look-r'); } } lastMouseX = e.clientX; const targetT = updateVisualOnly(e.clientX); updateVideoOnDemand(targetT); if (typeof ThumbnailEngine !== 'undefined' && progRectCache) { ThumbnailEngine.show(e.clientX, progRectCache); } }; progArea.addEventListener('mousedown', (e) => { if (!v.duration || isNaN(v.duration)) return; if (typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.hide(); isDragSeek = true; hasUserSeeked = true; lastMouseX = e.clientX; initialTimeBeforeDrag = v.currentTime; progRectCache = progArea.getBoundingClientRect(); document.body.classList.add('pk-dragging'); box.classList.add('pk-is-seeking'); seekIndicator.style.display = 'flex'; const targetT = updateVisualOnly(e.clientX); v.currentTime = targetT; e.preventDefault(); }); progArea.addEventListener('mousemove', (e) => { const rect = progArea.getBoundingClientRect(); ThumbnailEngine.show(e.clientX, rect); }); progArea.addEventListener('mouseleave', () => { ThumbnailEngine.hide(); }); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', () => stopDragging(false)); box.addEventListener('mouseleave', (e) => { if (isDragSeek) { const rect = box.getBoundingClientRect(); if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) { stopDragging(true); } } }); btnPlay.onclick = togglePlay; btnClose.onclick = destroyPlayer; btnFull.onclick = () => { if (!document.fullscreenElement) { box.requestFullscreen(); btnFull.innerHTML = mkSvg(icons.exitFull); } else { document.exitFullscreen(); btnFull.innerHTML = mkSvg(icons.full); } }; const subPanel = d.querySelector('#pk_sub_panel'); if (subPanel) { btnFull.addEventListener('mouseenter', () => subPanel.style.setProperty('display', 'none', 'important')); btnFull.addEventListener('mouseleave', () => subPanel.style.removeProperty('display')); } const btnPip = d.querySelector('#pk_p_pip'); if (btnPip) { if (!document.pictureInPictureEnabled || v.disablePictureInPicture) { btnPip.style.display = 'none'; } else { btnPip.onclick = async (e) => { e.stopPropagation(); try { if (document.pictureInPictureElement) { isPiPDesired = false; await document.exitPictureInPicture(); } else { isPiPDesired = true; await v.requestPictureInPicture(); window.focus(); } } catch (err) { console.error("PiP Error:", err); } }; } } btnSearch.onclick = (e) => { e.stopPropagation(); if (v) { v.pause(); setTimeout(() => { if (v) v.pause(); }, 100); } const posterImg = d.querySelector('#pk_p_poster img'); if (v.readyState >= 2 && v.videoWidth > 0) { startImageSearch(v, item.name, d, null); } else if (posterImg && posterImg.style.display !== 'none' && posterImg.src) { startImageSearch(posterImg, item.name, d, posterImg.src); } else { startImageSearch(v, item.name, d, null); } }; const initPosterImg = d.querySelector('#pk_p_poster img'); if (initPosterImg) { if (initPosterImg.complete && initPosterImg.src && initPosterImg.src.startsWith('http')) { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); } else { initPosterImg.addEventListener('load', () => { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); }); } } const savedMute = gmGet('pk_vol_muted', false); const savedVol = parseFloat(gmGet('pk_vol_level', 1.0)); v.muted = savedMute; v.volume = (Number.isFinite(savedVol) && savedVol >= 0 && savedVol <= 1) ? savedVol : 1.0; if (slideVol) slideVol.value = v.volume; updateVolUI(); btnVol.onclick = () => { v.muted = !v.muted; updateVolUI(); gmSet('pk_vol_muted', v.muted); }; slideVol.oninput = (e) => { v.muted = false; const val = parseFloat(e.target.value); v.volume = val; updateVolUI(); gmSet('pk_vol_muted', false); gmSet('pk_vol_level', val); }; spdList.querySelectorAll('.pk-p-item').forEach(item => { item.onclick = () => { const s = parseFloat(item.dataset.spd); v.playbackRate = s; spdList.querySelector('.active').classList.remove('active'); item.classList.add('active'); d.querySelector('#pk_p_spd_txt').textContent = item.textContent; }; }); function bindResEvents() { resList.querySelectorAll('.pk-p-item').forEach(item => { item.onclick = () => { const link = item.dataset.link; if(link === currentLink) return; const curT = v.currentTime; const isPaused = v.paused; const curRate = v.playbackRate; box.classList.add('buffering'); if (loaderEl) loaderEl.style.display = 'block'; if (posterEl) { if (v.readyState >= 2 && v.currentTime > 0) { try { const canvas = document.createElement('canvas'); canvas.width = v.videoWidth; canvas.height = v.videoHeight; canvas.getContext('2d').drawImage(v, 0, 0); const posterImg = posterEl.querySelector('img'); if (posterImg) { posterImg.src = canvas.toDataURL('image/jpeg', 0.7); posterImg.style.filter = 'brightness(0.8)'; } } catch (e) { console.warn("[ClaritySwitch] Frame capture failed."); } } posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; posterEl.style.pointerEvents = 'auto'; } currentLink = link; currentResName = item.textContent.trim(); shutterTargetTime = curT > 0.1 ? curT : 0; v.pause(); loadSource(link, curT); v.playbackRate = curRate; if(!isPaused) v.play().catch(()=>{}); resList.innerHTML = renderQualityMenu(qualityList, currentResName); if(resTxt) resTxt.textContent = currentResName; bindResEvents(); }; }); } bindResEvents(); const subState = { hasSub: false, size: 24, pos: 10, offset: 0, bgOpacity: 0.6, track: null, blobUrl: null }; const processSubtitleFile = (file) => { if (!file) return; const ext = file.name.split('.').pop().toLowerCase(); if (!['srt', 'vtt', 'ass', 'ssa'].includes(ext)) { showToast(L.err_sub_drop_type); return; } const reader = new FileReader(); reader.onload = (evt) => { const buffer = evt.target.result; const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis']; let text = ""; for (let enc of encodings) { try { const decoder = new TextDecoder(enc, { fatal: true }); text = decoder.decode(buffer); break; } catch (e) { if(enc === 'shift_jis') text = new TextDecoder('utf-8').decode(buffer); } } if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl); let vttText = ""; if (ext === 'ass' || ext === 'ssa') { vttText = convertAssToVtt(text); } else { vttText = convertSrtToVtt(text); } const blob = new Blob([vttText], { type: 'text/vtt' }); subState.blobUrl = URL.createObjectURL(blob); let targetTrack = null; if (v.textTracks) { for (let i = 0; i < v.textTracks.length; i++) { const t = v.textTracks[i]; if (t.label === 'pk-subs') targetTrack = t; t.mode = 'disabled'; } } if (!targetTrack) targetTrack = v.addTextTrack("subtitles", "pk-subs", "en"); targetTrack.oncuechange = () => { const cues = targetTrack.activeCues; const txtEl = d.querySelector('#pk_sub_text'); if (cues && cues.length > 0 && txtEl) { const text = cues[cues.length - 1].text; txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '<br>'); 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, '<br>'); 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 = `<div class="pk-spin-lg" style="width:14px;height:14px;border-width:2px;"></div> ${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, '<br>'); 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 = ` <div style="margin-bottom:10px; padding-bottom:10px; border-bottom:1px solid #333;"> <input id="pk_sub_search_input" value="${esc(keyword)}" placeholder="${L.ph_sub_search}" style="width:100%; box-sizing:border-box; background:#181818; border:1px solid #444; color:#fff; padding:8px 12px; border-radius:4px; outline:none; font-size:13px;"> </div> <div id="pk_sub_search_list" class="pk-scroll" style="flex:1; overflow-y:auto;"> </div> <button id="pk_sub_search_close" style="margin-top:10px; background:#333; color:#fff; border:none; padding:8px; border-radius:4px; cursor:pointer; font-size:13px;">${L.btn_close}</button> `; const resultList = searchOv.querySelector('#pk_sub_search_list'); const input = searchOv.querySelector('#pk_sub_search_input'); const doSearch = (query) => { const cleanQ = query.trim(); if (!cleanQ) return; const plusQ = encodeURIComponent(cleanQ).replace(/%20/g, '+'); const spaceQ = encodeURIComponent(cleanQ); const curLang = L.lang_code; let engines = []; if (curLang === 'zh') { engines = [ { name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` }, { name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` }, { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/zh/search2/sublanguageid-kor/moviename-${plusQ}` }, ]; } else if (curLang === 'tc') { engines = [ { name: 'R3Sub', url: `https://r3sub.com/search.php?s=${plusQ}` }, { name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` }, { name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` } ]; } else if (curLang === 'ko') { engines = [ { name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` }, { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ko/search2/sublanguageid-kor/moviename-${plusQ}` }, { name: 'SubtitleCat', url: `https://www.subtitlecat.com/index.php?search=${plusQ}` } ]; } else if (curLang === 'ja') { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ja/search2/sublanguageid-jpn/moviename-${plusQ}` }, { name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` }, { name: 'Anime Tosho', url: `https://animetosho.org/search?q=${plusQ}` } ]; } else { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/en/search2/sublanguageid-eng/moviename-${plusQ}` }, { name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` }, { name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` } ]; } let html = `<div style="padding: 10px 15px; display: flex; flex-direction: column; gap: 8px;">`; engines.forEach(e => { html += ` <a href="${e.url}" target="_blank" style=" display: block; padding: 10px 16px; background: #2a2a2a; color: #eee; text-decoration: none; border-radius: 6px; text-align: left; font-size: 13px; font-weight: 500; border: 1px solid #3d3d3d; transition: all 0.2s; " onmouseover="this.style.background='#3d3d3d';this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.background='#2a2a2a';this.style.borderColor='#3d3d3d'"> ${L.btn_go_search.replace('{n}', e.name)} </a>`; }); html += ` <div style="font-size: 11px; color: #777; margin-top: 10px; line-height: 1.6; text-align: left;"> ${L.tip_manual_sub} </div> </div>`; resultList.innerHTML = html; }; try { await loadJSZip(); doSearch(input.value); } catch (err) { resultList.innerHTML = `<div style="color:#d93025;">${L.msg_jszip_fail}</div>`; } input.oninput = () => doSearch(input.value); input.onkeydown = (ev) => { ev.stopPropagation(); }; searchOv.querySelector('#pk_sub_search_close').onclick = () => searchOv.remove(); }; d.querySelector('#pk_sub_toggle').onchange = (e) => { updateSubStyle(); if (v.textTracks) { Array.from(v.textTracks).forEach(t => { if (t !== subState.track) t.mode = 'disabled'; }); } }; if (v.textTracks) { v.textTracks.addEventListener('addtrack', (e) => { if (subState.hasSub && e.track !== subState.track) { e.track.mode = 'disabled'; } }); } const sizeVal = d.querySelector('#pk_sub_size_val'); d.querySelector('#pk_sub_size_dec').onclick = (e) => { e.stopPropagation(); subState.size = Math.max(12, subState.size - 2); sizeVal.textContent = subState.size; updateSubStyle(); }; d.querySelector('#pk_sub_size_inc').onclick = (e) => { e.stopPropagation(); subState.size = Math.min(80, subState.size + 2); sizeVal.textContent = subState.size; updateSubStyle(); }; d.querySelector('#pk_sub_pos').oninput = (e) => { e.stopPropagation(); subState.pos = parseInt(e.target.value); updateSubStyle(); }; d.querySelector('#pk_sub_bg_opacity').oninput = (e) => { e.stopPropagation(); subState.bgOpacity = parseInt(e.target.value) / 100; updateSubStyle(); }; const timeVal = d.querySelector('#pk_sub_time_val'); const adjustOffset = (delta) => { subState.offset += delta; timeVal.textContent = subState.offset.toFixed(1) + " " + L.unit_sec; if (subState.track && subState.track.cues) { const cues = Array.from(subState.track.cues); cues.forEach(cue => { cue.startTime += delta; cue.endTime += delta; }); } }; d.querySelector('#pk_sub_time_dec').onclick = (e) => { e.stopPropagation(); adjustOffset(-0.5); }; d.querySelector('#pk_sub_time_inc').onclick = (e) => { e.stopPropagation(); adjustOffset(0.5); }; d.querySelector('#pk_sub_panel').onclick = (e) => e.stopPropagation(); d.querySelectorAll('.pk-sub-tab').forEach(t => { t.onclick = () => { d.querySelectorAll('.pk-sub-tab, .pk-sub-pane').forEach(el => el.classList.remove('active')); t.classList.add('active'); d.querySelector('#' + t.dataset.target).classList.add('active'); }; }); const curPMode = gmGet('pk_play_mode', 'stop'); const pRadios = d.querySelectorAll('input[name="pk_pmode"]'); pRadios.forEach(r => { if (r.value === curPMode) r.checked = true; r.onchange = () => gmSet('pk_play_mode', r.value); }); let transformState = { rotate: 0, flipH: 1, flipV: 1, ratio: 'default' }; const applyTransform = () => { if (document.pictureInPictureElement === v) { v.style.transform = 'none'; return; } if (transformState.ratio === 'default') { v.style.objectFit = 'contain'; v.style.width = '100%'; v.style.height = box.classList.contains('plist-active') ? (document.fullscreenElement ? 'calc(100% - 84px)' : '100%') : '100%'; v.style.aspectRatio = 'auto'; v.style.margin = '0'; v.style.inset = 'auto'; } else { v.style.objectFit = 'fill'; v.style.aspectRatio = transformState.ratio; v.style.width = 'auto'; v.style.height = 'auto'; v.style.maxWidth = '100%'; v.style.maxHeight = '100%'; v.style.margin = 'auto'; v.style.inset = '0'; } let autoScale = 1; if (Math.abs(transformState.rotate) % 180 !== 0) { const boxW = box.clientWidth; const boxH = box.clientHeight; const vW = v.offsetWidth || boxW; const vH = v.offsetHeight || boxH; if (vW > 0 && vH > 0) { const scaleW = boxW / vH; const scaleH = boxH / vW; autoScale = Math.min(scaleW, scaleH); } } v.style.transform = `translateZ(0) rotate(${transformState.rotate}deg) scale(${autoScale}) scale(${transformState.flipH}, ${transformState.flipV})`; }; const onResizeTransform = () => requestAnimationFrame(applyTransform); window.addEventListener('resize', onResizeTransform); d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(btn => { btn.onclick = (e) => { d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); transformState.ratio = btn.dataset.ratio; applyTransform(); }; }); d.querySelector('#pk_btn_rot_l').onclick = () => { transformState.rotate -= 90; applyTransform(); }; d.querySelector('#pk_btn_rot_r').onclick = () => { transformState.rotate += 90; applyTransform(); }; d.querySelector('#pk_btn_flip_h').onclick = () => { transformState.flipH *= -1; applyTransform(); }; d.querySelector('#pk_btn_flip_v').onclick = () => { transformState.flipV *= -1; applyTransform(); }; const opVal = d.querySelector('#pk_op_val'); const edVal = d.querySelector('#pk_ed_val'); let valOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; let valEd = parseInt(gmGet('pk_skip_outro', 0)) || 0; opVal.textContent = valOp + " " + L.unit_sec; edVal.textContent = valEd + " " + L.unit_sec; const updateSkip = (type, delta) => { if (type === 'op') { valOp = Math.max(0, valOp + delta); opVal.textContent = valOp + " " + L.unit_sec; gmSet('pk_skip_intro', valOp); } else { valEd = Math.max(0, valEd + delta); edVal.textContent = valEd + " " + L.unit_sec; gmSet('pk_skip_outro', valEd); } }; d.querySelector('#pk_op_dec').onclick = () => updateSkip('op', -5); d.querySelector('#pk_op_inc').onclick = () => updateSkip('op', 5); d.querySelector('#pk_ed_dec').onclick = () => updateSkip('ed', -5); d.querySelector('#pk_ed_inc').onclick = () => updateSkip('ed', 5); d.querySelector('#pk_op_mark').onclick = (e) => { e.stopPropagation(); const markTime = Math.max(0, Math.floor(v.currentTime)); valOp = markTime; opVal.textContent = valOp + " " + L.unit_sec; gmSet('pk_skip_intro', valOp); }; d.querySelector('#pk_ed_mark').onclick = (e) => { e.stopPropagation(); if (!v.duration) return; const markTime = Math.max(0, Math.floor(v.duration - v.currentTime)); valEd = markTime; edVal.textContent = valEd + " " + L.unit_sec; gmSet('pk_skip_outro', valEd); }; let hasTriggeredEnd = false; v.addEventListener('timeupdate', () => { const skipEd = parseInt(gmGet('pk_skip_outro', 0)) || 0; if (skipEd > 0 && v.duration > 0 && !hasTriggeredEnd) { if (v.duration - v.currentTime <= skipEd) { hasTriggeredEnd = true; console.log(`[AutoSkip] Outro skipped at ${v.currentTime}`); v.onended(); } } }); v.addEventListener('play', () => hasTriggeredEnd = false); v.addEventListener('seeking', () => hasTriggeredEnd = false); v.addEventListener('enterpictureinpicture', applyTransform); v.addEventListener('leavepictureinpicture', () => { if (typeof isSwitching !== 'undefined' && !isSwitching) { isPiPDesired = false; } applyTransform(); }); v.onended = () => { const mode = gmGet('pk_play_mode', 'stop'); if (mode === 'single_loop') { v.currentTime = 0; v.play().catch(()=>{}); } else if (mode === 'list_loop') { const nextIdx = (curListIdx + 1) % totalInList; if (totalInList > 1) softSwitch(nextIdx); else { v.currentTime = 0; v.play().catch(()=>{}); } } }; const makeEditable = (el, type, callback) => { el.ondblclick = (e) => { e.stopPropagation(); const oldText = el.textContent; const oldVal = type === 'offset' ? parseFloat(oldText) : parseInt(oldText); el.innerHTML = `<input type="text" value="${oldVal}" style="width:100%;height:100%;border:none;background:transparent;color:#fff;text-align:center;font-size:12px;outline:none;padding:0;margin:0;">`; const input = el.querySelector('input'); input.focus(); input.select(); const finish = () => { const val = parseFloat(input.value); if (isNaN(val)) { el.textContent = oldText; } else { let finalVal = type === 'offset' ? val : Math.round(val); if (type !== 'offset') finalVal = Math.max(0, finalVal); if (type === 'offset') el.textContent = finalVal.toFixed(1) + " " + L.unit_sec; else el.textContent = finalVal; callback(finalVal); } el.ondblclick = (evt) => { evt.stopPropagation(); makeEditable(el, type, callback).ondblclick(evt); }; }; input.onblur = finish; input.onkeydown = (ev) => { ev.stopPropagation(); if (ev.key === 'Enter') { input.blur(); } }; input.onclick = (ev) => ev.stopPropagation(); }; }; makeEditable(d.querySelector('#pk_sub_size_val'), 'int', (val) => { subState.size = val; updateSubStyle(); }); makeEditable(d.querySelector('#pk_sub_time_val'), 'offset', (val) => { const delta = val - subState.offset; adjustOffset(delta); }); makeEditable(d.querySelector('#pk_op_val'), 'int', (val) => { valOp = val; gmSet('pk_skip_intro', val); d.querySelector('#pk_op_val').textContent = val + " " + L.unit_sec; }); makeEditable(d.querySelector('#pk_ed_val'), 'int', (val) => { valEd = val; gmSet('pk_skip_outro', val); d.querySelector('#pk_ed_val').textContent = val + " " + L.unit_sec; }); updateSubStyle(); const playerKeyHandler = (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (!document.getElementById('pk-player-ov')) return; e.stopPropagation(); e.preventDefault(); resetHideTimer(); switch(e.key) { case ' ': case 'k': togglePlay(); break; case 'ArrowRight': if (e.ctrlKey || e.metaKey) { const nextIdx = (curListIdx + 1) % totalInList; softSwitch(nextIdx); } else { const targetTime = Math.min(v.duration, v.currentTime + 10); v.currentTime = targetTime; if (tCur) tCur.textContent = fmtT(targetTime); if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important'); } break; case 'ArrowLeft': if (e.ctrlKey || e.metaKey) { const prevIdx = (curListIdx - 1 + totalInList) % totalInList; softSwitch(prevIdx); } else { const targetTime = Math.max(0, v.currentTime - 10); v.currentTime = targetTime; if (tCur) tCur.textContent = fmtT(targetTime); if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important'); } break; case 'p': case 'P': const btnPip = d.querySelector('#pk_p_pip'); if (btnPip && btnPip.style.display !== 'none') btnPip.click(); break; case 'e': case 'E': if (pTab) pTab.click(); break; case 'ArrowUp': case 'ArrowDown': v.muted = false; const delta = (e.key === 'ArrowUp' ? 0.05 : -0.05); v.volume = Math.max(0, Math.min(1, v.volume + delta)); updateVolUI(); const volInd = d.querySelector('#pk_p_vol_indicator'); const volVal = d.querySelector('#pk_p_vol_val'); if (volInd && volVal) { volVal.textContent = Math.round(v.volume * 100) + '%'; volInd.style.display = 'flex'; box.classList.add('pk-is-vol-active'); clearTimeout(v._volTimer); v._volTimer = setTimeout(() => { volInd.style.display = 'none'; box.classList.remove('pk-is-vol-active'); }, 1000); } break; case 'f': case 'F': if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click(); break; case 'Enter': btnFull.click(); break; case 'Escape': if (document.fullscreenElement) document.exitFullscreen(); else destroyPlayer(); break; } }; document.addEventListener('keydown', playerKeyHandler); const smartLoad = async () => { if (item.phase === 'PHASE_TYPE_RUNNING' || item.phase === 'PHASE_TYPE_PENDING') { console.log("[Transcode] Video is processing, entering polling mode."); const mask = document.createElement('div'); mask.className = 'pk-transcode-mask'; mask.innerHTML = ` <div class="pk-tc-icon"></div> <div style="font-weight:bold;margin-bottom:8px;">${L.msg_transcoding}</div> <div style="color:#888;font-size:12px;">${L.msg_transcoding_wait}</div> <div class="pk-tc-btn" id="pk_tc_force">${L.btn_force_play}</div> `; box.appendChild(mask); mask.querySelector('#pk_tc_force').onclick = (e) => { e.stopPropagation(); if (transcodeTimer) clearInterval(transcodeTimer); mask.remove(); loadSource(currentLink); v.play(); }; transcodeTimer = setInterval(async () => { try { const freshData = await apiGet(item.id); if (freshData.phase === 'PHASE_TYPE_COMPLETE') { clearInterval(transcodeTimer); mask.remove(); item = freshData; const best = getBestSource(freshData); currentLink = best.src; loadSource(currentLink); if (typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.resetSource(currentLink); v.play(); qualityList = best.list; currentResName = best.name; const resTxt = d.querySelector('#pk_p_res_txt'); const resList = d.querySelector('#pk_p_res_list'); if(resTxt) resTxt.textContent = currentResName; if(resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:20px;left:50%;transform:translateX(-50%);background:rgba(76, 175, 80, 0.9);color:#fff;padding:8px 16px;border-radius:20px;font-size:13px;z-index:100;animation:pkFadeIn 0.5s;"; toast.textContent = L.msg_transcode_done; box.appendChild(toast); setTimeout(()=>toast.remove(), 3000); } } catch(e) { console.warn("[TranscodePoll] Error:", e); } }, 3000); } else { const initDur = (item.params && item.params.duration) || S.durationMap.get(item.id) || gmGet('pk_duration_' + item.id, 0); if (tDur && initDur > 0) tDur.textContent = fmtT(initDur); loadSource(currentLink, null); setTimeout(() => { if (isPlayerDestroyed) return; const p = v.play(); if (p !== undefined) { p.catch(() => updateState()); } if (startFullscreen) { const box = d.querySelector('#pk_p_box'); const btnFull = d.querySelector('#pk_p_full'); if (box && box.requestFullscreen) { box.requestFullscreen().then(() => { if(btnFull) btnFull.innerHTML = mkSvg(icons.exitFull); }).catch(err => console.warn("Fullscreen auto-resume failed", err)); } } }, 100); } }; smartLoad(); autoMatchSubtitle(item); } let isImageOpening = false; async function showImage(startItem) { if (S.trashMode) return; if (document.querySelector('.pk-img-ov')) return; isImageOpening = true; let lastDirection = 1; const item = startItem; const imgList = S.display.filter(i => { if (i.isHeader) return false; if (S.offlineMode && i.phase !== 'PHASE_TYPE_COMPLETE') return false; if (S.uploadMode && i.status !== 'DONE') return false; return i.mime_type && i.mime_type.startsWith('image'); }); if (imgList.length === 0) return; let curIdx = imgList.findIndex(i => i.id === startItem.id); if (curIdx === -1) curIdx = 0; const d = document.createElement('div'); d.className = 'pk-img-ov'; d.tabIndex = 0; const icons = { close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>', full: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>', exitFull: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="M14 10l7-7"/><path d="M3 21l7-7"/></svg>', prev: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>', next: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>', rotate: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>', flipH: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 7l5 5-5 5M7 17l-5-5 5-5M2 12h20"/></svg>', flipV: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7l5-5 5 5M17 17l-5 5-5-5M12 2v20"/></svg>', searchlens: `<svg viewBox="0 0 24 24" fill="currentColor"><g transform="scale(0.0234375)"><path d="M107.739429 580.388571a365.860571 365.860571 0 0 0 648.630857 85.504L635.611429 532.48a36.571429 36.571429 0 0 0-56.612572 2.852571l-39.131428 53.101715a109.714286 109.714286 0 0 1-167.716572 10.605714L235.008 455.387429a36.571429 36.571429 0 0 0-58.002286 6.729142l-69.266285 118.272z m-19.894858-110.738285l26.038858-44.470857a109.714286 109.714286 0 0 1 174.08-20.333715l137.216 143.798857a36.571429 36.571429 0 0 0 55.881142-3.584l39.131429-53.101714a109.714286 109.714286 0 0 1 169.691429-8.484571l102.985142 113.810285A365.714286 365.714286 0 1 0 87.844571 469.577143z m658.139429 318.317714a438.857143 438.857143 0 1 1 50.029714-52.736c1.316571 1.024 2.56 2.194286 3.803429 3.437714l206.921143 206.848a36.571429 36.571429 0 0 1-51.712 51.712l-206.921143-206.848a37.083429 37.083429 0 0 1-2.194286-2.413714zM526.628571 314.514286a73.142857 73.142857 0 1 1 0-146.285715 73.142857 73.142857 0 0 1 0 146.285715z" fill="currentColor"></path></g></svg>`, leftArr: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6 L9 12 L15 18"/></svg>`, rightArr: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6 L15 12 L9 18"/></svg>`, upArr: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>` }; const renderImgListItems = () => { const RANGE = 150; const start = Math.max(0, curIdx - RANGE); const end = Math.min(imgList.length, curIdx + RANGE + 1); return imgList.slice(start, end).map((v, i) => { const absIdx = start + i; const imgSrc = v.thumbnail_link || v.icon_link || ''; return ` <div class="pk-p-plist-item ${absIdx === curIdx ? 'active' : ''}" data-idx="${absIdx}" data-name="${esc(v.name)}" data-size="${fmtSize(v.size)}"> <img src="${imgSrc}" style="filter:none !important;" onerror="this.style.display='none';" loading="lazy"> </div> `}).join(''); }; const listFixStyle = `<style> #pk_img_box { border-radius: 0 !important; } #pk_img_plist { pointer-events: none !important; left: 0 !important; right: 0 !important; width: 100% !important; margin: 0 !important; } #pk_img_plist .pk-p-plist-strip { display: none !important; opacity: 0 !important; position: relative !important; background: rgba(20, 20, 20, 0.98) !important; backdrop-filter: blur(15px) !important; -webkit-backdrop-filter: blur(15px) !important; border-top: 1px solid rgba(255,255,255,0.1); border-radius: 0 !important; width: 100% !important; } .pk-img-box.full #pk_img_plist .pk-p-plist-strip { border-radius: 0 !important; } .pk-img-box.full.plist-active { height: calc(100% - 84px) !important; } .pk-img-box.plist-active #pk_img_plist .pk-p-plist-strip { pointer-events: auto !important; } .pk-img-nav { position: absolute !important; top: 50% !important; transform: translateY(-50%) translateZ(0) !important; width: 60px !important; height: 60px !important; background: rgba(0, 0, 0, 0.3) !important; display: flex !important; align-items: center !important; justify-content: center !important; color: #fff !important; cursor: pointer !important; z-index: 40 !important; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important; opacity: 0 !important; border-radius: 50% !important; border: 1px solid rgba(255, 255, 255, 0.18) !important; pointer-events: auto !important; box-shadow: 0 4px 15px rgba(0,0,0,0.15) !important; } .pk-img-box:hover .pk-img-nav { opacity: 1 !important; } .pk-img-nav:hover { background: rgba(0, 0, 0, 0.45) !important; transform: translateY(-50%) scale(1.08) translateZ(0) !important; border-color: rgba(255, 255, 255, 0.3) !important; } .pk-img-prev { left: 30px !important; } .pk-img-next { right: 30px !important; } .pk-img-nav svg { width: 24px !important; height: 24px !important; fill: none; stroke: currentColor; stroke-width: 2.8; stroke-linecap: round; stroke-linejoin: round; } .pk-img-prev svg { margin-left: -2px; } .pk-img-next svg { margin-left: 2px; } #pk_img_plist .pk-p-plist-nav { position: absolute !important; top: 0 !important; bottom: 0 !important; width: 50px !important; background: transparent !important; z-index: 35 !important; pointer-events: auto !important; cursor: pointer !important; display: flex !important; align-items: center !important; justify-content: center !important; border: none !important; transition: background 0.2s; } #pk_img_plist .pk-p-plist-nav.L { left: 0 !important; } #pk_img_plist .pk-p-plist-nav.R { right: 0 !important; } #pk_img_plist .pk-p-plist-nav:hover { background: rgba(255,255,255,0.15) !important; color: #fff !important; } #pk_img_plist .pk-p-plist-scroll { padding: 0 45px !important; pointer-events: auto !important; scrollbar-width: none; } #pk_img_plist_tab { position: absolute !important; bottom: 100% !important; left: 50% !important; transform: translateX(-50%) !important; z-index: 100 !important; } #pk_img_plist_tab svg { width: 14px !important; height: 14px !important; stroke-width: 3.5 !important; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); } #pk_img_plist.open #pk_img_plist_tab svg { transform: rotate(180deg); } #pk_img_plist .pk-p-plist-scroll { display: flex !important; align-items: center !important; overflow-x: auto !important; overflow-y: hidden !important; scroll-behavior: smooth; } .pk-img-view-port { flex: 1; width: 100%; height: 100%; overflow: hidden; position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; } .pk-img-view-port.pk-long-image-mode { overflow-y: auto !important; overflow-x: hidden !important; align-items: flex-start; } .pk-img-view-port.pk-long-image-mode::-webkit-scrollbar { width: 8px; } .pk-img-view-port.pk-long-image-mode::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); } .pk-img-view-port.pk-long-image-mode::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); border-radius: 4px; } .pk-img-view-port.pk-long-image-mode::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.5); } .pk-long-image-mode img.pk-img-obj { width: 100% !important; max-width: 1200px !important; height: auto !important; object-fit: cover !important; margin: 0 auto !important; cursor: zoom-out !important; transform: none !important; } .pk-img-view-port.pk-fit-mode { overflow-y: hidden !important; align-items: center; } .pk-fit-mode img.pk-img-obj { height: 100% !important; object-fit: contain !important; cursor: zoom-in !important; } </style>`; d.innerHTML = listFixStyle + ` <div class="pk-img-box" id="pk_img_box"> <div class="pk-img-bar"> <div class="pk-img-title" id="pk_img_title"></div> <div class="pk-img-actions"> <div class="pk-img-btn" id="pk_img_search" data-pk-tip="${L.btn_img_search}" style="display:none;">${icons.searchlens}</div> <div class="pk-img-btn" id="pk_img_flip_v" data-pk-tip="${L.tip_flip_v}">${icons.flipV}</div> <div class="pk-img-btn" id="pk_img_mirror" data-pk-tip="${L.tip_mirror}">${icons.flipH}</div> <div class="pk-img-btn" id="pk_img_rot" data-pk-tip="${L.tip_rotate}">${icons.rotate}</div> <div class="pk-img-btn" id="pk_img_full" data-pk-tip="${L.tip_maximize}">${icons.full}</div> <div class="pk-img-btn" id="pk_img_close" data-pk-tip="${L.tip_close}">${icons.close}</div> </div> </div> <div class="pk-img-nav pk-img-prev" id="pk_img_prev" data-pk-tip="${L.btn_prev_video}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15.5 19l-7-7 7-7"/></svg></div> <div class="pk-img-nav pk-img-next" id="pk_img_next" data-pk-tip="${L.btn_next_video}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8.5 19l7-7-7-7"/></svg></div> <div class="pk-p-loading" id="pk_img_load"><div class="pk-spin-lg" style="border-color:rgba(255,255,255,0.3); border-top-color:#fff;"></div></div> <div class="pk-img-view-port" id="pk_img_viewport"><img class="pk-img-obj" id="pk_img_el" draggable="false"></div> <div class="pk-p-plist-ov" id="pk_img_plist"> <div class="pk-p-plist-tab" id="pk_img_plist_tab" data-pk-tip="${L.tip_plist_open}"> <span id="pk_img_idx_txt">${curIdx + 1} / ${imgList.length}</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="18 15 12 9 6 15"></polyline></svg> </div> <div class="pk-p-plist-strip"> <div class="pk-p-plist-nav L" id="pk_img_plist_L">${icons.leftArr}</div> <div class="pk-p-plist-scroll" id="pk_img_plist_scroll">${renderImgListItems()}</div> <div class="pk-p-plist-nav R" id="pk_img_plist_R">${icons.rightArr}</div> </div> </div> </div> `; document.body.appendChild(d); const box = d.querySelector('#pk_img_box'); const img = d.querySelector('#pk_img_el'); const viewport = d.querySelector('#pk_img_viewport'); const title = d.querySelector('#pk_img_title'); const loader = d.querySelector('#pk_img_load'); const btnFull = d.querySelector('#pk_img_full'); const btnRot = d.querySelector('#pk_img_rot'); const btnMirror = d.querySelector('#pk_img_mirror'); const btnFlipV = d.querySelector('#pk_img_flip_v'); const btnSearch = d.querySelector('#pk_img_search'); let scale = 1, transX = 0, transY = 0, rotation = 0, flipH = 1, flipV = 1, isDrag = false, startX, startY; let isLongImageMode = false; const updateTransform = () => { if (isLongImageMode) return; img.style.transform = `translate(${transX}px, ${transY}px) scale(${scale}) rotate(${rotation}deg) scaleX(${flipH}) scaleY(${flipV})`; }; const resetView = (keepOrientation = false) => { scale = 1; transX = 0; transY = 0; if (!keepOrientation) { rotation = 0; flipH = 1; flipV = 1; } isLongImageMode = false; viewport.classList.remove('pk-long-image-mode'); viewport.classList.remove('pk-fit-mode'); if(viewport.scrollTop) viewport.scrollTop = 0; img.style.transition = 'none'; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain'; img.style.maxWidth = 'none'; img.style.cursor = 'grab'; if (btnRot) btnRot.style.display = 'flex'; if (btnMirror) btnMirror.style.display = 'flex'; if (btnFlipV) btnFlipV.style.display = 'flex'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; let imgLoadId = 0; const loadCurrent = async (scrollMode = 'smooth') => { if (typeof updateImgPlistUI === 'function') { updateImgPlistUI(scrollMode); } imgLoadId++; const myId = imgLoadId; resetView(); img.style.opacity = '0'; const currentItem = imgList[curIdx]; title.textContent = `[${curIdx + 1}/${imgList.length}] ${currentItem.name}`; btnSearch.style.display = 'none'; btnSearch.onclick = (e) => { e.stopPropagation(); const thumbUrl = currentItem.thumbnail_link ? currentItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE') : (currentItem.icon_link || ''); const thumbImg = new Image(); thumbImg.crossOrigin = 'anonymous'; thumbImg.src = thumbUrl; startImageSearch(thumbImg, currentItem.name, d, thumbUrl); }; loader.style.display = 'block'; const checkLongImage = () => { if (myId !== imgLoadId) return; btnSearch.style.display = 'flex'; const nw = img.naturalWidth; const nh = img.naturalHeight; if (nw > 0 && nh > 0) { if (nh / nw > 2.5) { isLongImageMode = true; viewport.classList.add('pk-long-image-mode'); img.style.transform = 'none'; if (btnRot) btnRot.style.display = 'none'; if (btnMirror) btnMirror.style.display = 'none'; if (btnFlipV) btnFlipV.style.display = 'none'; } } img.style.opacity = '1'; }; img.removeAttribute('crossorigin'); if (currentItem.thumbnail_link) { img.src = currentItem.thumbnail_link; const handleThumb = () => { if (myId !== imgLoadId) return; checkLongImage(); loader.style.display = 'none'; }; img.onerror = () => { if (myId !== imgLoadId) return; img.style.opacity = '0'; }; if (img.complete) handleThumb(); else img.onload = handleThumb; } else { img.removeAttribute('src'); img.style.opacity = '0'; } let targetUrl = currentItem.web_content_link; if (!targetUrl && !currentItem._resolved) { try { const targetApiId = ((S.offlineMode && currentItem.kind === 'drive#task') || (S.uploadMode && currentItem.file_id)) ? currentItem.file_id : currentItem.id; const fullItem = await apiGet(targetApiId); if (fullItem) { if (fullItem.thumbnail_link) currentItem.thumbnail_link = fullItem.thumbnail_link; if (fullItem.icon_link) currentItem.icon_link = fullItem.icon_link; if (myId === imgLoadId) requestAnimationFrame(() => updateImgPlistUI(false)); } if (myId !== imgLoadId) return; targetUrl = fullItem.web_content_link; currentItem.web_content_link = targetUrl; currentItem._resolved = true; } catch (e) { console.warn("API Error", e); } } if (myId !== imgLoadId) return; const performFinalRender = () => { if (myId !== imgLoadId) return; if (targetUrl && targetUrl !== currentItem.thumbnail_link) { img.crossOrigin = 'anonymous'; img.src = targetUrl; } else { img.crossOrigin = 'anonymous'; } if (img.complete && img.naturalWidth > 0) { checkLongImage(); } else { img.addEventListener('load', checkLongImage, { once: true }); } btnSearch.onclick = (e) => { e.stopPropagation(); const thumbUrl = currentItem.thumbnail_link ? currentItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE') : (currentItem.icon_link || ''); const thumbImg = new Image(); thumbImg.crossOrigin = 'anonymous'; thumbImg.src = thumbUrl; startImageSearch(thumbImg, currentItem.name, d, thumbUrl); }; const mainDir = lastDirection, sideDir = -lastDirection, DEPTH = 5; const quickLoad = (url) => { if(!url) return; const p = new Image(); p.src = url; }; const getIdx = (offset) => (curIdx + offset + imgList.length) % imgList.length; quickLoad(imgList[getIdx(sideDir)].thumbnail_link); for (let i = 1; i <= DEPTH; i++) { if (myId !== imgLoadId) return; const next = imgList[getIdx(i * mainDir)]; quickLoad(next.thumbnail_link); if (next.web_content_link) { const p = new Image(); p.crossOrigin = 'anonymous'; p.src = next.web_content_link; } } }; if (targetUrl && targetUrl !== currentItem.thumbnail_link) { const tempImg = new Image(); tempImg.crossOrigin = 'anonymous'; tempImg.onload = performFinalRender; tempImg.onerror = performFinalRender; tempImg.src = targetUrl; } else { performFinalRender(); } }; d.addEventListener('wheel', (e) => { if (isLongImageMode) return; e.preventDefault(); const delta = e.deltaY > 0 ? -0.1 : 0.1; scale = Math.max(0.1, Math.min(10, scale + delta)); updateTransform(); }); img.onclick = () => { if (isLongImageMode) { viewport.classList.toggle('pk-fit-mode'); } }; img.addEventListener('mousedown', (e) => { if (e.button !== 0 || (isLongImageMode && !viewport.classList.contains('pk-fit-mode'))) return; e.preventDefault(); const z = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; isDrag = true; img.style.cursor = 'grabbing'; startX = e.clientX - transX * scale * z; startY = e.clientY - transY * scale * z; }); document.addEventListener('mousemove', (e) => { if (!isDrag || isLongImageMode) return; const z = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; let tx = (e.clientX - startX) / (scale * z); let ty = (e.clientY - startY) / (scale * z); if (img.naturalWidth && viewport) { const vw = viewport.clientWidth; const vh = viewport.clientHeight; const iw = img.naturalWidth; const ih = img.naturalHeight; const baseRatio = Math.min(vw / iw, vh / ih); let curW = iw * baseRatio * scale; let curH = ih * baseRatio * scale; if (Math.abs(rotation % 180) === 90) [curW, curH] = [curH, curW]; const limitX = curW > vw ? (curW - vw) / (2 * scale) : 0; const limitY = curH > vh ? (curH - vh) / (2 * scale) : 0; tx = Math.max(-limitX, Math.min(limitX, tx)); ty = Math.max(-limitY, Math.min(limitY, ty)); } transX = tx; transY = ty; updateTransform(); }); document.addEventListener('mouseup', () => { isDrag = false; if (!isLongImageMode && img) img.style.cursor = 'grab'; }); const resizeHandler = () => { if (isLongImageMode) return; resetView(true); }; window.addEventListener('resize', resizeHandler); d.querySelector('#pk_img_close').onclick = (e) => { e.stopPropagation(); const currentItem = imgList[curIdx]; if (currentItem) { const targetId = currentItem.id; const targetIdx = S.display.findIndex(x => x.id === targetId); if (targetIdx !== -1) { S.sel.clear(); S.sel.add(targetId); S.activeId = targetId; const rowTop = targetIdx * CONF.rowHeight; const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); } } window.removeEventListener('resize', resizeHandler); d.remove(); }; btnFull.onclick = (e) => { e.stopPropagation(); box.classList.toggle('full'); const isNowFull = box.classList.contains('full'); btnFull.innerHTML = isNowFull ? icons.exitFull : icons.full; btnFull.setAttribute('data-pk-tip', isNowFull ? L.tip_minimize : L.tip_maximize); if (isLongImageMode && viewport) { viewport.scrollTop = 0; } else { setTimeout(() => resetView(true), 210); } }; btnRot.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; rotation += 90; updateTransform(); }; if (btnMirror) { btnMirror.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; flipH *= -1; img.style.transition = 'none'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; } if (btnFlipV) { btnFlipV.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; flipV *= -1; img.style.transition = 'none'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; } const plist = d.querySelector('#pk_img_plist'); const pTab = d.querySelector('#pk_img_plist_tab'); const pScroll = d.querySelector('#pk_img_plist_scroll'); let pTip = document.getElementById('pk_p_plist_tip_global'); if (!pTip) { pTip = document.createElement('div'); pTip.id = 'pk_p_plist_tip_global'; pTip.className = 'pk-p-plist-tip'; document.body.appendChild(pTip); } const pTxt = d.querySelector('#pk_img_idx_txt'); const updateImgPlistUI = (scrollType = 'smooth') => { if (pTxt) pTxt.textContent = `${curIdx + 1} / ${imgList.length}`; pScroll.innerHTML = renderImgListItems(); pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const idx = parseInt(e.currentTarget.dataset.idx); if (idx === curIdx) return; curIdx = idx; loadCurrent('instant'); }; el.onmouseenter = (e) => { if (plist.classList.contains('open')) { const name = e.currentTarget.dataset.name; const size = e.currentTarget.dataset.size; pTip.innerHTML = `<strong>${name}</strong><br>${size}`; pTip.style.display = 'block'; } }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => { pTip.style.display = 'none'; }; }); const itemsInDom = pScroll.querySelectorAll('.pk-p-plist-item'); itemsInDom.forEach((el) => { const absIdx = parseInt(el.dataset.idx); const isActive = absIdx === curIdx; el.classList.toggle('active', isActive); if (scrollType !== false && isActive && plist.classList.contains('open')) { if (scrollType === 'instant') { pScroll.style.scrollBehavior = 'auto'; } else { pScroll.style.scrollBehavior = 'smooth'; } el.scrollIntoView({ behavior: scrollType === 'instant' ? 'auto' : 'smooth', block: 'nearest', inline: 'center' }); if (scrollType === 'instant') { setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); } } }); const sl = Math.ceil(pScroll.scrollLeft); const sw = pScroll.scrollWidth; const cw = pScroll.clientWidth; if (sw <= cw) { d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important'); d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important'); } else { if (sl <= 5) d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important'); else d.querySelector('#pk_img_plist_L').style.setProperty('display', 'flex', 'important'); if (sl + cw >= sw - 5) d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important'); else d.querySelector('#pk_img_plist_R').style.setProperty('display', 'flex', 'important'); } const btnPrev = d.querySelector('#pk_img_prev'); const btnNext = d.querySelector('#pk_img_next'); if (btnPrev) btnPrev.style.setProperty('display', curIdx === 0 ? 'none' : 'flex', 'important'); if (btnNext) btnNext.style.setProperty('display', curIdx === imgList.length - 1 ? 'none' : 'flex', 'important'); }; pTab.onclick = (e) => { e.stopPropagation(); const willOpen = !plist.classList.contains('open'); plist.classList.toggle('open'); if (typeof box !== 'undefined') box.classList.toggle('plist-active'); pTab.setAttribute('data-pk-tip', plist.classList.contains('open') ? L.tip_plist_close : L.tip_plist_open); if (!isLongImageMode) resetView(); if (willOpen) { updateImgPlistUI(false); pScroll.style.scrollBehavior = 'auto'; const activeItem = pScroll.querySelector('.active'); if (activeItem) { const targetLeft = activeItem.offsetLeft - (pScroll.clientWidth / 2) + (activeItem.clientWidth / 2); pScroll.scrollLeft = targetLeft; } setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); setTimeout(() => updateImgPlistUI(false), 100); } }; const handleListWheel = (e) => { e.stopPropagation(); e.preventDefault(); pScroll.scrollBy({ left: e.deltaY > 0 ? 300 : -300, behavior: 'smooth' }); }; pScroll.removeEventListener('wheel', handleListWheel); pScroll.addEventListener('wheel', handleListWheel, { passive: false }); pScroll.addEventListener('scroll', () => { requestAnimationFrame(() => updateImgPlistUI(false)); }, { passive: true }); const btnListL = d.querySelector('#pk_img_plist_L'); const btnListR = d.querySelector('#pk_img_plist_R'); if (btnListL) { btnListL.onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: -400, behavior: 'smooth' }); setTimeout(() => updateImgPlistUI(false), 300); }; } if (btnListR) { btnListR.onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: 400, behavior: 'smooth' }); setTimeout(() => updateImgPlistUI(false), 300); }; } setTimeout(() => updateImgPlistUI(false), 100); const goPrev = () => { if (curIdx > 0) { lastDirection = -1; curIdx--; loadCurrent(); } }; const goNext = () => { if (curIdx < imgList.length - 1) { lastDirection = 1; curIdx++; loadCurrent(); } }; d.querySelector('#pk_img_prev').onclick = (e) => { e.stopPropagation(); goPrev(); }; d.querySelector('#pk_img_next').onclick = (e) => { e.stopPropagation(); goNext(); }; d.focus(); d.addEventListener('keydown', (e) => { if (e.key === 'Escape') d.remove(); else if (e.key === 'ArrowLeft') goPrev(); else if (e.key === 'ArrowRight') goNext(); else if (e.key === 'f' || e.key === 'F') { if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click(); } else if (e.key === 'e' || e.key === 'E') { if (pTab) pTab.click(); } else if (e.key === 'm' || e.key === 'M') { e.preventDefault(); if (btnFull) btnFull.click(); } else if (e.key === 'r' || e.key === 'R') { e.preventDefault(); if (!isLongImageMode && btnRot) btnRot.click(); } else if (e.key === 'h' || e.key === 'H') { e.preventDefault(); if (!isLongImageMode && btnMirror) btnMirror.click(); } else if (e.key === 'v' || e.key === 'V') { e.preventDefault(); if (!isLongImageMode && btnFlipV) btnFlipV.click(); } }); try { await loadCurrent(); } catch (e) { console.error(e); } finally { isImageOpening = false; } } const SEARCH_CSS = ` .pk-search-ov { position: fixed; inset: 0; z-index: 2147483647; background: rgba(0,0,0,0.5); cursor: crosshair; } .pk-crop-box { position: absolute; border: 2px dashed #fff; background: rgba(255,255,255,0.1); pointer-events: none; box-shadow: 0 0 0 9999px rgba(0,0,0,0.5); } .pk-search-msg { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: #fff; padding: 10px 20px; border-radius: 5px; font-size: 14px; pointer-events: none; } `; async function startImageSearch(mediaElement, fileName, containerElement, originalLink) { if (document.querySelector('.pk-search-running-mask')) return; try { window.focus(); if (containerElement) containerElement.focus(); } catch (e) {} const L = getStrings(); const isVideo = mediaElement.tagName === 'VIDEO'; const MAX_SIDE = 1000; const BLOB_TYPE = 'image/jpeg'; const BLOB_QUALITY = 0.7; const ov = document.createElement('div'); ov.className = 'pk-search-running-mask'; ov.style.cssText = 'position:absolute; inset:0; z-index:2147483647; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; flex-direction:column; gap:20px; border-radius:inherit;'; ov.innerHTML = ` <div class="pk-spin-lg" style="border-color:rgba(255,255,255,0.3); border-top-color:#fff;"></div> <div style="color:#fff; font-size:14px; font-weight:bold; text-shadow:0 2px 4px rgba(0,0,0,0.5);">${L.str_processing}</div> `; const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (fsEl) { fsEl.appendChild(ov); } else if (containerElement) { containerElement.appendChild(ov); } else { document.body.appendChild(ov); } if (isVideo && !mediaElement.paused) mediaElement.pause(); try { let finalBlob = null; const cvs = document.createElement('canvas'); const ctx = cvs.getContext('2d'); const drawScaled = (source, srcW, srcH) => { let w = srcW, h = srcH; if (w === 0 || h === 0) throw new Error("Media dimensions not ready"); if (w > MAX_SIDE || h > MAX_SIDE) { const ratio = Math.min(MAX_SIDE / w, MAX_SIDE / h); w = Math.floor(w * ratio); h = Math.floor(h * ratio); } cvs.width = w; cvs.height = h; ctx.drawImage(source, 0, 0, w, h); }; let fetchUrl = originalLink; const isLocalData = fetchUrl && (fetchUrl.startsWith('blob:') || fetchUrl.startsWith('data:')); const BLOB_TYPE = 'image/jpeg'; const BLOB_QUALITY = 0.85; if (isVideo) { const sourceW = mediaElement.videoWidth; const sourceH = mediaElement.videoHeight; drawScaled(mediaElement, sourceW, sourceH); try { finalBlob = await new Promise((resolve, reject) => { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Empty")), BLOB_TYPE, BLOB_QUALITY); }); } catch (err) { throw new Error("Tainted: Video CORS Blocked"); } } else { let canvasSuccess = false; try { const sourceW = mediaElement.naturalWidth || mediaElement.width; const sourceH = mediaElement.naturalHeight || mediaElement.height; if (sourceW > 0 && sourceH > 0) { drawScaled(mediaElement, sourceW, sourceH); finalBlob = await new Promise((resolve, reject) => { try { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Tainted")), BLOB_TYPE, BLOB_QUALITY); } catch (e) { reject(e); } }); canvasSuccess = !!finalBlob; } } catch (canvasErr) { console.warn("[ImageSearch] DOM Canvas extraction failed (Tainted/Not Ready), fallback to network."); } if (!canvasSuccess && fetchUrl && !isLocalData) { console.log("[ImageSearch] Fetching image via network as fallback..."); try { const res = await fetch(fetchUrl, { mode: 'cors', credentials: 'omit' }); if (!res.ok && res.status !== 206) throw new Error(`Fetch HTTP ${res.status}`); finalBlob = await res.blob(); } catch (fetchErr) { console.warn(`[ImageSearch] Native fetch failed (${fetchErr.message}), fallback to GM_xhr...`); let fetchRetry = 0; let fetchSuccess = false; while (fetchRetry < 3 && !fetchSuccess) { try { finalBlob = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: fetchUrl, responseType: "blob", timeout: 30000, headers: { "Referer": "https://mypikpak.com/", "User-Agent": navigator.userAgent }, onload: (res) => { if (res.status === 200 || res.status === 206) resolve(res.response); else reject(new Error(`HTTP ${res.status}`)); }, onerror: () => reject(new Error("Network Error")), ontimeout: () => reject(new Error("Timeout")) }); }); fetchSuccess = true; } catch (err) { fetchRetry++; if (fetchRetry < 3) { console.warn(`[ImageSearch] GM_xhr retry ${fetchRetry}/3...`); await new Promise(r => setTimeout(r, 1500)); } } } } } if (!finalBlob) { throw new Error(L.err_network_break); } if (finalBlob.size > 8 * 1024 * 1024) { console.log(`[ImageSearch] Image too large (${(finalBlob.size/1024/1024).toFixed(1)}MB), applying compression...`); try { const bmp = await createImageBitmap(finalBlob); drawScaled(bmp, bmp.width, bmp.height); bmp.close(); finalBlob = await new Promise((resolve, reject) => { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Compression Failed")), BLOB_TYPE, BLOB_QUALITY); }); } catch (e) { console.warn("[ImageSearch] Compression failed, using original blob.", e); } } } if (!finalBlob) throw new Error(L.err_capture || "Capture failed"); const uploadAndGetUrl = async () => { if (typeof GM_xmlhttpRequest === 'undefined') throw new Error("Missing GM_xmlhttpRequest"); const txtDiv = ov.lastElementChild; const FAST_TIMEOUT = 4000; const uploadTask = (url, formData, parseType, stageText) => { return new Promise((resolve, reject) => { if (txtDiv) txtDiv.textContent = stageText; GM_xmlhttpRequest({ method: "POST", url: url, data: formData, timeout: FAST_TIMEOUT, responseType: parseType === 'json' ? 'json' : 'text', onload: (res) => { if (res.status === 200) resolve(res); else reject(new Error(`HTTP ${res.status}`)); }, onerror: () => reject(new Error("Network Error")), ontimeout: () => reject(new Error("Timeout")) }); }); }; const tryNode1 = async () => { const fd = new FormData(); fd.append('files[]', finalBlob, `pk_1.jpg`); const res = await uploadTask( "https://uguu.se/upload.php", fd, 'json', L.str_upload_1 ); if (res.response && res.response.success && res.response.files?.[0]?.url) { return res.response.files[0].url; } throw new Error("Uguu API Error"); }; const tryNode2 = async () => { console.warn("Node 1 failed, switching to Litterbox..."); const fd = new FormData(); fd.append('reqtype', 'fileupload'); fd.append('time', '1h'); fd.append('fileToUpload', finalBlob, `pk_2.jpg`); const res = await uploadTask( "https://litterbox.catbox.moe/resources/internals/api.php", fd, 'text', L.str_upload_2 ); return res.responseText.trim(); }; const tryNode3 = async () => { console.warn("Node 2 failed, switching to Catbox..."); const fd = new FormData(); fd.append('reqtype', 'fileupload'); fd.append('userhash', ''); fd.append('fileToUpload', finalBlob, `pk_3.jpg`); const res = await uploadTask( "https://catbox.moe/user/api.php", fd, 'text', L.str_upload_3 ); return res.responseText.trim(); }; try { return await tryNode1(); } catch (e1) { try { return await tryNode2(); } catch (e2) { try { return await tryNode3(); } catch (e3) { console.error("All upload nodes failed:", e1, e2, e3); throw new Error("All Upload Hosts Failed"); } } } }; try { const imgUrl = await uploadAndGetUrl(); if (!imgUrl || !imgUrl.startsWith('http')) { throw new Error("Invalid URL returned."); } const currentEngine = gmGet('pk_search_engine', 'google'); const encUrl = encodeURIComponent(imgUrl); let jumpUrl = ''; let engineName = ''; switch (currentEngine) { case 'yandex': jumpUrl = `https://yandex.com/images/search?rpt=imageview&url=${encUrl}`; engineName = 'Yandex'; break; case 'saucenao': jumpUrl = `https://saucenao.com/search.php?db=999&url=${encUrl}`; engineName = 'SauceNAO'; break; case 'tracemoe': { const cleanUrl = imgUrl.replace(/^https?:\/\//, ''); const proxyUrl = `https://wsrv.nl/?url=${cleanUrl}&output=jpg`; jumpUrl = `https://trace.moe/?url=${encodeURIComponent(proxyUrl)}`; engineName = 'trace.moe'; break; } case 'google': default: jumpUrl = `https://lens.google.com/uploadbyurl?url=${encUrl}`; engineName = 'Google Lens'; break; } const spinDiv = ov.querySelector('.pk-spin-lg'); const txtDiv = ov.lastElementChild; if (spinDiv) spinDiv.style.borderColor = '#4CAF50'; if (txtDiv) txtDiv.textContent = L.str_redirecting.replace('Google Lens', engineName); await sleep(600); window.open(jumpUrl, '_blank'); } catch (err) { console.warn("Auto upload failed, switching to manual fallback:", err); const txtDiv = ov.lastElementChild; if (txtDiv) txtDiv.textContent = L.str_upload_fail_copy; let fallbackBlob = null; try { const bmp = await createImageBitmap(finalBlob); const tmpCvs = document.createElement('canvas'); tmpCvs.width = bmp.width; tmpCvs.height = bmp.height; tmpCvs.getContext('2d').drawImage(bmp, 0, 0); bmp.close(); fallbackBlob = await new Promise(r => tmpCvs.toBlob(r, 'image/png')); } catch (e) { console.warn("Bitmap conversion failed, falling back to origin canvas:", e); fallbackBlob = await new Promise(r => cvs.toBlob(r, 'image/png')); } const MAX_CLIPBOARD_SIZE = 19.5 * 1024 * 1024; if (fallbackBlob.size > MAX_CLIPBOARD_SIZE) { console.log(`PNG too large, resizing...`); try { const ratio = Math.sqrt(MAX_CLIPBOARD_SIZE / fallbackBlob.size); const bmp = await createImageBitmap(fallbackBlob); const newW = Math.floor(bmp.width * ratio); const newH = Math.floor(bmp.height * ratio); const tmpCvs = document.createElement('canvas'); tmpCvs.width = newW; tmpCvs.height = newH; tmpCvs.getContext('2d').drawImage(bmp, 0, 0, newW, newH); bmp.close(); fallbackBlob = await new Promise(r => tmpCvs.toBlob(r, 'image/png')); } catch (e) { console.warn("Resize failed:", e); } } try { const item = new ClipboardItem({ 'image/png': fallbackBlob }); await navigator.clipboard.write([item]); } catch (clipErr) { console.error("Clipboard write failed:", clipErr); if (txtDiv) txtDiv.textContent = "Clipboard Failed"; await sleep(1000); } const ctrlVPhrase = (navigator.platform.toUpperCase().indexOf('MAC') >= 0) ? 'Cmd+V' : 'Ctrl+V'; const hintText = L.msg_manual_paste.replace('{cmd}', `<b style="color:#fff">${ctrlVPhrase}</b>`); ov.innerHTML = ` <div style="background:rgba(20,20,20,0.9); backdrop-filter:blur(10px); color:#FFC107; padding:20px 40px; border-radius:12px; text-align:center; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border:1px solid rgba(255,255,255,0.1);"> <div style="font-size:24px; margin-bottom:8px;">⚠️</div> <div style="font-size:15px; font-weight:bold;">${L.msg_copy_success}</div> <div style="font-size:12px; color:#ddd; margin-top:8px;">${hintText}</div> </div>`; await sleep(1500); const currentEngine = gmGet('pk_search_engine', 'google'); let manualUrl = ''; switch (currentEngine) { case 'yandex': manualUrl = 'https://yandex.com/images/'; break; case 'saucenao': manualUrl = 'https://saucenao.com/'; break; case 'tracemoe': manualUrl = 'https://trace.moe/'; break; case 'google': default: manualUrl = 'https://lens.google.com/upload'; break; } window.open(manualUrl, '_blank'); } } catch (e) { console.error("Search Error:", e); const errorMsg = e.message.includes('Tainted') ? "Security Error: CORS Blocked" : e.message; ov.innerHTML = `<div style="background:#d93025; color:#fff; padding:12px 24px; border-radius:8px; font-weight:bold;">❌ ${errorMsg}</div>`; await sleep(2000); } finally { ov.remove(); } } function dataURLtoBlob(dataurl) { let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--){ u8arr[n] = bstr.charCodeAt(n); } return new Blob([u8arr], {type:mime}); } const FILTER_EXTS = { video: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mpg','mpeg','rm','rmvb','asf','vob','dat','divx','f4v','m2ts','mts','tp','trp','ogv','mpe','m2v','m3u8'], audio: ['mp3','wav','flac','aac','ogg','wma','ape','m4a','amr','opus','m4b','alac','aiff','mid','midi','ra','dts','ac3','dsf','dff'], image: ['jpg','jpeg','png','gif','bmp','webp','svg','tif','tiff','ico','heic','heif','raw','cr2','nef','arw','dng','orf','avif','psd','ai','eps','jfif','jpe'], document: ['txt','html','pdf','pptx','chm','docx','xlsx','htm','doc','dwg','mdb','ppt','xls','rtf','odt','ods','odp','epub','mobi','azw3','djvu','cbz','cbr','md','log','csv','xml','json'], software: ['apk','exe','ipa','dmg','rpm','deb','msi','pkg','xapk','apks','aab','jar','bin','sh','bat','cmd'], archive: ['zip','rar','7z','tar','gz','iso','cab','bz2','xz','tgz','wim','esd','img','zst','lzh'], torrent: ['torrent'] }; const FILTER_NAMES = { video: L.cat_video, audio: L.cat_audio, image: L.cat_image, document: L.cat_document, software: L.cat_software, archive: L.cat_archive, torrent: L.cat_torrent, other: L.cat_other }; const fIcons = { all: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>`, video: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></svg>`, audio: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>`, image: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`, document: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`, software: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`, archive: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8v13H3V8"/><path d="M1 3h22v5H1z"/><path d="M10 12h4v4h-4z"/></svg>`, torrent: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/><path d="M9 14v4"/><path d="M12 12v6"/><path d="M15 15v3"/></svg>`, other: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>` }; 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 = `<span class="pk-f-ext ${S.filterState.ext === 'all' ? 'act' : ''}" data-ext="all">${L.cat_all}</span>`; let displayExts = [...mainExts]; if (S.filterState.ext !== 'all' && moreExts.includes(S.filterState.ext)) { displayExts[2] = S.filterState.ext; } displayExts.forEach(e => { html += `<span class="pk-f-ext ${S.filterState.ext === e ? 'act' : ''}" data-ext="${e}">${e}</span>`; }); 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 = ` <div style="font-size: 13px; color: #888; margin-bottom: 12px; padding-left: 5px;">${L.title_file_filter}</div> <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;"> <div class="pk-fc-btn ${S.filterState.cat === 'all' ? 'act' : ''}" data-cat="all">${fIcons.all} <span>${L.cat_all}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'video' ? 'act' : ''}" data-cat="video">${fIcons.video} <span>${L.cat_video}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'audio' ? 'act' : ''}" data-cat="audio">${fIcons.audio} <span>${L.cat_audio}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'image' ? 'act' : ''}" data-cat="image">${fIcons.image} <span>${L.cat_image}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'document' ? 'act' : ''}" data-cat="document">${fIcons.document} <span>${L.cat_document}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'software' ? 'act' : ''}" data-cat="software">${fIcons.software} <span>${L.cat_software}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'archive' ? 'act' : ''}" data-cat="archive">${fIcons.archive} <span>${L.cat_archive}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'torrent' ? 'act' : ''}" data-cat="torrent">${fIcons.torrent} <span>${L.cat_torrent}</span></div> <div class="pk-fc-btn ${S.filterState.cat === 'other' ? 'act' : ''}" data-cat="other">${fIcons.other} <span>${L.cat_other}</span></div> </div> `; document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = triggerEl.getBoundingClientRect(); let popLeft = rect.left / scale; if (popLeft + 420 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 430; pop.style.top = ((rect.bottom / scale) + 5) + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; pop.querySelectorAll('.pk-fc-btn').forEach(btn => { btn.onclick = (ev) => { ev.stopPropagation(); const cat = btn.dataset.cat; S.filterState.cat = cat; S.filterState.ext = 'all'; S.filterState.active = (cat !== 'all'); cleanup(); if (S.filterState.active) { renderActiveFilterUI(); } S.sel.clear(); refresh(); }; }); const closer = (ev) => { if (!pop.contains(ev.target) && !triggerEl.contains(ev.target)) { cleanup(); } }; setTimeout(() => document.addEventListener('mousedown', closer), 10); }; if (UI.filterBtn) UI.filterBtn.onclick = (e) => showFilterCatPopup(UI.filterBtn, e); if (UI.filterCatLabel) UI.filterCatLabel.onclick = (e) => showFilterCatPopup(UI.filterCatLabel, e); if (UI.filterExtsMoreBtn) { UI.filterExtsMoreBtn.onclick = (e) => { e.stopPropagation(); const existing = document.querySelector('#pk-filter-more-pop'); if (existing) { existing.remove(); return; } const pop = document.createElement('div'); pop.id = 'pk-filter-more-pop'; pop.style.cssText = ` position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; padding: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); z-index: 2147483647; max-width: 340px; display: flex; flex-wrap: wrap; gap: 8px; zoom: var(--pk-zoom, 1); `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); const exts = FILTER_EXTS[S.filterState.cat] ||[]; const mainExts = exts.slice(0, 3); const moreExts = exts.slice(3); let displayExts = [...mainExts]; if (S.filterState.ext !== 'all' && moreExts.includes(S.filterState.ext)) { displayExts[2] = S.filterState.ext; } const dropdownExts = exts.filter(ex => !displayExts.includes(ex)); pop.innerHTML = dropdownExts.map(ex => `<span class="pk-f-ext ${S.filterState.ext === ex ? 'act' : ''}" data-ext="${ex}" style="border:1px solid transparent; ${S.filterState.ext === ex ? 'background:rgba(0,103,192,0.1);' : 'background:var(--pk-hl);'}">${ex}</span>`).join(''); document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = UI.filterExtsWrap.getBoundingClientRect(); let popLeft = rect.left / scale; if (popLeft + 340 > window.innerWidth / scale) popLeft = (window.innerWidth / scale) - 350; pop.style.top = ((rect.bottom / scale) + 5) + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; pop.querySelectorAll('.pk-f-ext').forEach(span => { span.onclick = (ev) => { ev.stopPropagation(); S.filterState.ext = span.dataset.ext; cleanup(); renderActiveFilterUI(); S.sel.clear(); refresh(); }; }); const closer = (ev) => { if (!pop.contains(ev.target) && !UI.filterExtsMoreBtn.contains(ev.target)) { cleanup(); } }; setTimeout(() => document.addEventListener('mousedown', closer), 10); }; } if (UI.filterExitBtn) { UI.filterExitBtn.onclick = () => { S.filterState = { active: false, cat: 'all', ext: 'all' }; UI.filterBtn.style.display = 'flex'; UI.filterActiveUI.style.display = 'none'; S.sel.clear(); refresh(); }; } const getHistory = () => { try { return JSON.parse(gmGet('pk_search_history', '[]')); } catch { return []; } }; const saveHistory = (txt) => { if (!txt) return; let list = getHistory(); list = list.filter(x => x !== txt); list.unshift(txt); if (list.length > 3) list = list.slice(0, 3); gmSet('pk_search_history', JSON.stringify(list)); }; const renderHistory = () => { const list = getHistory(); if (list.length === 0) { UI.searchHist.style.display = 'none'; return; } let html = `<div class="pk-hist-hd"><span>${L.title_search_hist}</span><span class="pk-hist-clear-btn" id="pk-hist-del" style="cursor:pointer; opacity:0.8;">${L.btn_clear_hist}</span></div>`; list.forEach(txt => { html += `<div class="pk-select-item" style="display:flex; align-items:center; gap:10px; padding:8px 12px;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="flex-shrink:0; opacity:0.5;"> <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/> </svg> <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1;">${esc(txt)}</span> </div>`; }); UI.searchHist.innerHTML = html; UI.searchHist.style.display = 'flex'; UI.searchHist.querySelector('#pk-hist-del').onclick = (e) => { e.stopPropagation(); gmSet('pk_search_history', '[]'); UI.searchHist.style.display = 'none'; }; UI.searchHist.querySelectorAll('.pk-select-item').forEach(el => { el.onclick = (e) => { const val = el.querySelector('span').textContent; UI.searchInput.value = val; performSearch(val); UI.searchHist.style.display = 'none'; }; }); }; const performSearch = (val) => { const txt = val.trim(); if (!txt) { if (S.search && UI.searchClear) UI.searchClear.click(); return; } const isGlobal = UI.chkGlobal && UI.chkGlobal.checked && !S.uploadMode; if (txt) { saveHistory(txt); if (isGlobal && !S.preSearchPath) { S.preSearchPath = [...S.path]; } if (isGlobal) { S.sort = 'modified_time'; S.dir = 1; S.path = [ { id: '', name: L.btn_nav_home }, { id: 'virtual_search_root', name: L.str_search_results } ]; renderCrumb(); } } S.search = txt; if (S.dupMode) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.sel.clear(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); updateStat(); } else if (isGlobal) { if (globalNeedsSync) { setLoad(true); updateLoadTxt(L.str_analyzing); setTimeout(async () => { if (!S.scanning) { S.scanning = true; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); if (UI.chkGlobal) UI.chkGlobal.checked = false; }; try { await runFlattenScanOperation(true, [], true); globalNeedsSync = false; } catch (e) { console.error("[GlobalSearch] Sync Error:", e); } finally { S.scanning = false; } } load(false, true).finally(() => setLoad(false)); }, 50); } else { load(false, false); } } else { refresh(); } UI.searchClear.style.display = txt ? 'flex' : 'none'; UI.searchHist.style.display = 'none'; UI.searchInput.blur(); }; if (UI.searchInput) { UI.searchInput.oninput = (e) => { const val = e.target.value.trim(); UI.searchClear.style.display = val ? 'flex' : 'none'; if (!val && S.search && UI.searchClear) { UI.searchClear.click(); } }; UI.searchInput.onfocus = () => { renderHistory(); if (getHistory()?.length > 0) UI.searchHist.style.display = 'flex'; }; document.addEventListener('click', (e) => { if (!UI || !UI.searchInput || !UI.searchHist) return; if (!UI.searchInput.contains(e.target) && !UI.searchHist.contains(e.target)) { UI.searchHist.style.display = 'none'; } }); UI.searchInput.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') { performSearch(e.target.value); } }; if (UI.searchBtn) { UI.searchBtn.onclick = () => { performSearch(UI.searchInput.value); }; } if (UI.searchClear) { UI.searchClear.onclick = async () => { const wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; if (!S.search && UI.searchInput.value) { UI.searchInput.value = ''; UI.searchClear.style.display = 'none'; UI.searchInput.focus(); return; } const searchWasActive = !!S.search; const isInVirtualStack = S.path.some(node => node.id === 'virtual_search_root'); let requiresReload = false; UI.searchInput.value = ''; S.search = ''; S.lastGlobalResults = []; try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); if (prefStore['virtual_search_root']) { delete prefStore['virtual_search_root']; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } } catch(e) {} if ((wasGlobalChecked || isInVirtualStack) && searchWasActive) { const currentFolder = S.path[S.path.length - 1]; if (currentFolder.id !== 'virtual_search_root' && currentFolder.id !== '') { const traceStack = []; let ptrId = currentFolder.id; let ptrName = currentFolder.name; let safety = 100; traceStack.unshift({ id: ptrId, name: ptrName }); while (ptrId && ptrId !== 'root' && safety > 0) { if (globalParentIndex.has(ptrId)) { const parent = globalParentIndex.get(ptrId); ptrId = parent.id; ptrName = parent.name; if (ptrId === 'root' || ptrId === '') break; traceStack.unshift({ id: ptrId, name: ptrName }); } else { const node = S.itemMap.get(ptrId); if (node && node._lineage && node._lineage.length > 0) { const ancestors = node._lineage.filter(x => x.id !== '' && x.id !== 'root'); for (let k = ancestors.length - 1; k >= 0; k--) { traceStack.unshift(ancestors[k]); } } break; } safety--; } const realPath = [{ id: '', name: L.btn_nav_home }, ...traceStack]; S.path = realPath; } else if (isInVirtualStack || S.preSearchPath) { S.path = S.preSearchPath ? [...S.preSearchPath] : [{ id: '', name: L.btn_nav_home }]; } requiresReload = true; } S.preSearchPath = null; UI.searchClear.style.display = 'none'; UI.searchHist.style.display = 'none'; if (S.dupMode && !isInVirtualStack) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.sel.clear(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); updateStat(); } else if (S.isFlattened && !isInVirtualStack) { refresh(); updateStat(); } else { if (requiresReload) { const targetNode = S.path[S.path.length - 1]; const targetKey = S.getRealCacheKey(targetNode.id); const cachedData = (typeof globalCache !== 'undefined') ? (globalCache.get(targetKey) || globalCache.get('')) : null; if (cachedData) { if (cachedData.items) S.items = [...cachedData.items]; else if (Array.isArray(cachedData)) S.items = [...cachedData]; S.itemMap.clear(); for (const it of S.items) S.itemMap.set(it.id, it); } refresh(); setLoad(true); const p = load(false, true); if (UI.chkGlobal) UI.chkGlobal.checked = wasGlobalChecked; await p; } else { refresh(); updateStat(); } } }; } } UI.btnHelp.onclick = () => { const m = showModal(` <h3 style="border:none; margin-bottom:16px; font-size:18px; font-weight:700; color:var(--pk-fg); flex-shrink:0;">${L.modal_help_title}</h3> <div style="position:relative; flex:1; min-height:0; display:flex; flex-direction:column;"> ${L.help_desc} <div id="pk_help_fade" style="position:absolute; bottom:0; left:0; right:0; height:40px; background:linear-gradient(to bottom, transparent, var(--pk-bg)); pointer-events:none; transition:opacity 0.2s;"></div> </div> <div class="pk-modal-act" style="margin-top:20px; flex-shrink:0;"> <button class="pk-btn pri" id="help_close" style="width:100%; height:44px; justify-content:center; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold; border:none;">${L.btn_close}</button> </div> `); 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(` <h3 style="border:none; margin-bottom:16px; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.title_confirm}</h3> <div style="margin-bottom:25px; line-height:1.6; font-size:14px; color:var(--pk-fg); opacity:0.9;"> ${esc(L.msg_global_warn).replace(/\n/g, '<br>')} </div> <div style="margin-bottom:25px; display:flex; justify-content:flex-end;"> <label style="display:flex; align-items:center; cursor:pointer; font-size:13px; color:#888; user-select:none;"> <input type="checkbox" id="pk_warn_ignore" style="margin-right:8px; width:16px; height:16px; accent-color:var(--pk-pri);"> <span>${L.lbl_dont_show}</span> </label> </div> <div class="pk-modal-act" style="display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="cfm_cancel" style="height:40px; min-width:86px; border-radius:8px; justify-content:center; background:transparent; font-weight:500;">${L.btn_cancel}</button> <button class="pk-btn pri" id="cfm_ok" style="height:40px; min-width:86px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold; justify-content:center; border:none;">${L.btn_ok}</button> </div> `); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '420px', padding: '30px', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelector('#cfm_cancel').onclick = () => { m.remove(); resolve({ ok: false }); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve({ ok: false }); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#cfm_ok').click(); } }); m.querySelector('#cfm_ok').onclick = () => { const isChecked = m.querySelector('#pk_warn_ignore').checked; m.remove(); resolve({ ok: true, suppress: isChecked }); }; }); if (!userChoice.ok) { e.target.checked = false; return; } hasShownGlobalWarnSession = true; if (userChoice.suppress) { gmSet('pk_suppress_global_warn', true); } } if (!isGlobalIndexReady || globalNeedsSync) { S.scanning = true; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); UI.chkGlobal.checked = false; }; await runFlattenScanOperation(true); } refresh(); } else { if (S.scanning) { S.scanning = false; updateLoadTxt(L.str_stopping); } refresh(); } }; const runFlattenScanOperation = async (isSyncOnly = false, specificTargets =[], isSilent = false) => { S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; let fileMap = new Map(); let processedFolders = 0; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; setLoad(true); const isPartialScan = specificTargets && specificTargets.length > 0; let rootNodes =[]; if (isPartialScan) { updateLoadTxt(L.msg_init_scan_sel); specificTargets.forEach(item => { if (item.kind === 'drive#folder') { rootNodes.push({ id: item.id, name: item.name, lineage:[{ id: item.id, name: item.name }], retryCount: 0 }); } else if (!isSyncOnly) { item._lineage =[]; fileMap.set(item.id, item); } }); } else { updateLoadTxt(`${L.str_scanning} 0`); const startNode = isSyncOnly ? { id: '', name: 'Root' } : S.path[S.path.length - 1]; rootNodes =[{ id: startNode.id || '', name: startNode.name || 'Root', lineage: [], retryCount: 0 }]; } UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); if (S.isFlattened) { UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; } else { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.chkGlobal) UI.chkGlobal.checked = false; S.isFlattened = false; setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { console.log("♻️ Scan interrupted: Forcing background crawler to resume."); resumeBackgroundDiscovery(); } }, 1000); } }; S.scanning = true; try { await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { if (!isSyncOnly) { f._lineage = parent.lineage ||[]; fileMap.set(f.id, f); } }, onFolder: (folder, filesInFolder) => { processedFolders++; indexParents(folder.id, folder.name, filesInFolder); if (typeof globalLineageMap !== 'undefined') { globalLineageMap.set(folder.id, folder.lineage); } }, onProgress: (st) => { const folderText = isPartialScan ? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders) : `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : ""; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo + retryTag); } }); if (S.scanning && !signal.aborted && myScanId === S.scanId) { globalNeedsSync = false; isGlobalIndexReady = true; if (!isSyncOnly) { updateLoadTxt(L.str_merging); let tempItems = Array.from(fileMap.values()); if (S.scanFilter && !isSyncOnly) { const { minBytes, maxBytes, keyword } = S.scanFilter; const kwList = keyword ? keyword.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : []; tempItems = tempItems.filter(item => { if (kwList.length > 0) { const fullLowerName = (item.name || "").toLowerCase(); const lastDot = fullLowerName.lastIndexOf('.'); const nameWithoutExt = (item.kind !== 'drive#folder' && lastDot > 0) ? fullLowerName.substring(0, lastDot) : fullLowerName; if (kwList.some(k => nameWithoutExt.includes(k))) return false; } const sz = parseInt(item.size || 0); if (sz < minBytes) return false; if (maxBytes > 0 && sz > maxBytes) return false; return true; }); } const total = tempItems.length; S.items = new Array(total); S.itemMap.clear(); let lastYield = performance.now(); for (let i = 0; i < total; i++) { const item = tempItems[i]; S.items[i] = item; S.itemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(item.id); } if (i % 5000 === 0 && performance.now() - lastYield > 16) { updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`); await sleep(0); lastYield = performance.now(); } } S.isFlattened = true; S.sort = 'modified_time'; S.dir = 1; UI.chkAll.checked = false; S.sel.clear(); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important'); updateLoadTxt(L.str_rendering); await refresh(); const msg = L.msg_scan_done.replace('{n}', total).replace('{f}', processedFolders); if (!isSilent) showAlert(msg); } } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error_crit}: ${e.message}`); } if (!isSyncOnly && myScanId === S.scanId) { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; UI.lblGlobal.style.display = 'flex'; } if (myScanId === S.scanId) UI.chkGlobal.checked = false; } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } }; const openScanDupModal = async (initialTab) => { if (S.loading || S.scanning) return; const curFolderId = S.path[S.path.length - 1].id || ''; if (isPathBusy(curFolderId)) { showAlert(L.msg_flatten_blocked_moving); return; } S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; const selectedTargets =[]; if (S.sel.size > 0) { S.sel.forEach(id => { const item = S.itemMap.get(id); if (item) selectedTargets.push(item); }); } const lastMin = gmGet('pk_scan_last_min', 0); const lastMax = gmGet('pk_scan_last_max', ''); const lastUnit = gmGet('pk_scan_last_unit', 'MB'); const lastKeyword = gmGet('pk_scan_last_keyword', ''); let currentStrict = gmGet('pk_dup_strictness', 'strict'); const scanTxt = L.btn_scan; const dupTxt = L.tip_dup; const L_min = L.lbl_ana_min; const L_max = L.lbl_ana_max; let scanTargetDesc = selectedTargets.length > 0 ? L.lbl_scan_selected.replace('{n}', selectedTargets.length) : L.lbl_scan_current; let dupTargetDesc = selectedTargets.length > 0 ? L.lbl_dup_selected.replace('{n}', selectedTargets.length) : L.lbl_dup_current; const m = showModal(` <div class="pk-share-modal-root" style="width:480px; max-width:90vw; display:flex; flex-direction:column; overflow:visible;"> <div style="padding: 30px 30px 15px 30px; flex-shrink:0;"> <h3 style="margin: 0; font-size: 18px; font-weight: 700; border: none; line-height: 1.2; color: var(--pk-fg);">${L.title_file_analysis}</h3> </div> <div class="pk-s-tabs" style="margin: 0 30px 20px 30px; display:flex;"> <div class="pk-s-tab ${initialTab === 'scan' ? 'act' : ''}" data-val="scan">${scanTxt}</div> <div class="pk-s-tab ${initialTab === 'dup' ? 'act' : ''}" data-val="dup">${dupTxt}</div> </div> <div id="pane_scan" style="display:${initialTab === 'scan' ? 'block' : 'none'}; padding: 0 30px;"> <div style="font-size:13px; color:#888; margin-bottom:20px; line-height:1.5;">${scanTargetDesc}</div> <div style="margin-bottom:20px; position:relative;"> <input type="text" id="sc_keyword" value="${esc(lastKeyword)}" placeholder="${L.ph_keyword_filter}" oninput="this.style.borderColor = this.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'" style="width:100%; height:42px; padding:0 12px; border:2px solid ${lastKeyword ? 'var(--pk-pri)' : 'var(--pk-bd)'}; border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; outline:none; transition:border-color 0.2s; box-sizing:border-box;"> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; line-height:1; white-space:nowrap;">${L.lbl_keyword_filter}</div> </div> <div style="display:flex; align-items:center; gap:10px; margin-bottom:25px;"> <div style="flex:1; position:relative;"> <input type="number" id="sc_val_min" value="${lastMin === 0 ? '' : lastMin}" placeholder="0" min="0" step="1" style="width:100%; height:42px; padding:0 30px 0 12px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:16px; font-weight:700; outline:none; transition:border-color 0.2s; box-sizing:border-box; font-family:monospace;"> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; line-height:1; white-space:nowrap;">${L_min}</div> <div class="pk-num-ctrl"> <div class="pk-num-btn" id="sc_inc_min">${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}</div> <div class="pk-num-btn" id="sc_dec_min">${CONF.crumbIcons.down}</div> </div> </div> <div style="color:#888; font-weight:bold; flex-shrink:0;">-</div> <div style="flex:1; position:relative;"> <input type="number" id="sc_val_max" value="${lastMax}" min="0" step="1" placeholder="∞" style="width:100%; height:42px; padding:0 30px 0 12px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:16px; font-weight:700; outline:none; transition:border-color 0.2s; box-sizing:border-box; font-family:monospace;"> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; line-height:1; white-space:nowrap;">${L_max}</div> <div class="pk-num-ctrl"> <div class="pk-num-btn" id="sc_inc_max">${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}</div> <div class="pk-num-btn" id="sc_dec_max">${CONF.crumbIcons.down}</div> </div> </div> <div class="pk-ana-select"> <div class="pk-ana-trigger" id="sc_unit_btn"> <span id="sc_unit_txt">${lastUnit}</span> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg> </div> <div class="pk-ana-menu" id="sc_unit_menu"> <div class="pk-ana-item ${lastUnit === 'MB' ? 'act' : ''}" data-v="MB">MB</div> <div class="pk-ana-item ${lastUnit === 'GB' ? 'act' : ''}" data-v="GB">GB</div> <div class="pk-ana-item ${lastUnit === 'TB' ? 'act' : ''}" data-v="TB">TB</div> </div> </div> </div> </div> <div id="pane_dup" class="pk-scroll" style="display:${initialTab === 'dup' ? 'flex' : 'none'}; flex-direction:column; gap:16px; padding: 0 30px 25px 30px;"> <div style="font-size:13px; color:#888; margin-bottom:4px; line-height:1.5;">${dupTargetDesc}</div> <div style="position:relative;"> <label for="scan_video" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; height:54px; border:2px solid var(--pk-bd); border-radius:10px; padding:0 15px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box;"> <input type="checkbox" id="scan_video" checked style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer; margin-right:12px;"> <span style="font-size:14px; color:var(--pk-fg); font-weight:600; user-select:none;">${L.label_dup_video}</span> </label> </div> <div style="position:relative;"> <label for="scan_image" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; height:54px; border:2px solid var(--pk-bd); border-radius:10px; padding:0 15px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box;"> <input type="checkbox" id="scan_image" checked style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer; margin-right:12px;"> <span style="font-size:14px; color:var(--pk-fg); font-weight:600; user-select:none;">${L.label_dup_image}</span> </label> </div> <div style="position:relative;"> <label for="scan_other" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; height:54px; border:2px solid var(--pk-bd); border-radius:10px; padding:0 15px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box;"> <input type="checkbox" id="scan_other" checked style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer; margin-right:12px;"> <span style="font-size:14px; color:var(--pk-fg); font-weight:600; user-select:none;">${L.label_dup_other}</span> </label> </div> <div class="pk-custom-select" id="cs_sc_strict" style="margin-top:5px;"> <div class="pk-select-label">${L.label_dup_strictness}</div> <div class="pk-select-trigger"><span id="txt_sc_strict">${currentStrict === 'loose' ? L.opt_loose : L.opt_strict}</span>${CONF.crumbIcons.down}</div> <div class="pk-select-menu pk-scroll"> <div class="pk-select-item ${currentStrict === 'strict' ? 'act' : ''}" data-val="strict">${L.opt_strict}</div> <div class="pk-select-item ${currentStrict === 'loose' ? 'act' : ''}" data-val="loose">${L.opt_loose}</div> </div> </div> </div> <div style="padding: 20px 30px 30px 30px; flex-shrink:0;"> <div class="pk-modal-act" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 0;"> <button class="pk-btn" id="sc_cancel" style="height:46px; border-radius:12px; justify-content:center; background:transparent; font-weight:600; font-size:15px;">${L.btn_cancel}</button> <button class="pk-btn pri" id="sc_start" style="height:46px; border-radius:12px; background:var(--pk-pri); color:#fff; font-weight:bold; justify-content:center; border:none; font-size:15px; transition: filter 0.2s;">${L.btn_ok}</button> </div> </div> </div> `); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: 'auto', padding: '0', overflow: 'visible', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelectorAll('.pk-s-tab').forEach(tab => { tab.onclick = () => { m.querySelectorAll('.pk-s-tab').forEach(t => t.classList.remove('act')); tab.classList.add('act'); const curMode = tab.dataset.val; m.querySelector('#pane_scan').style.display = curMode === 'scan' ? 'block' : 'none'; m.querySelector('#pane_dup').style.display = curMode === 'dup' ? 'flex' : 'none'; }; }); const inpMin = m.querySelector('#sc_val_min'); const inpMax = m.querySelector('#sc_val_max'); const unitBtn = m.querySelector('#sc_unit_btn'); const unitMenu = m.querySelector('#sc_unit_menu'); const unitTxt = m.querySelector('#sc_unit_txt'); let currentUnit = lastUnit; m.querySelector('#sc_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; }; m.querySelector('#sc_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); }; m.querySelector('#sc_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; }; m.querySelector('#sc_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); }; unitBtn.onclick = (e) => { e.stopPropagation(); unitMenu.style.display = unitMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('.pk-ana-item').forEach(item => { item.onclick = () => { m.querySelectorAll('.pk-ana-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentUnit = item.dataset.v; unitTxt.textContent = currentUnit; unitMenu.style.display = 'none'; }; }); const closeMenu = () => { if (unitMenu) unitMenu.style.display = 'none'; }; setTimeout(() => document.addEventListener('click', closeMenu), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeMenu); _orgRemove(); }; if (S.dupConfig) { m.querySelector('#scan_video').checked = S.dupConfig.video; m.querySelector('#scan_image').checked = S.dupConfig.image; m.querySelector('#scan_other').checked = S.dupConfig.other; } const scStrictTrigger = m.querySelector('#cs_sc_strict .pk-select-trigger'); const scStrictMenu = m.querySelector('#cs_sc_strict .pk-select-menu'); const scStrictTxt = m.querySelector('#txt_sc_strict'); scStrictTrigger.onclick = (e) => { e.stopPropagation(); scStrictMenu.style.display = scStrictMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('#cs_sc_strict .pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('#cs_sc_strict .pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentStrict = item.dataset.val; scStrictTxt.textContent = item.textContent; scStrictMenu.style.display = 'none'; }; }); const saveScanInputs = () => { gmSet('pk_scan_last_min', parseInt(inpMin.value) || 0); gmSet('pk_scan_last_max', inpMax.value.trim()); gmSet('pk_scan_last_unit', currentUnit); gmSet('pk_scan_last_keyword', m.querySelector('#sc_keyword').value.trim()); gmSet('pk_dup_strictness', currentStrict); }; m.querySelector('#sc_cancel').onclick = () => { saveScanInputs(); m.remove(); }; m.querySelector('.pk-modal-close').onclick = () => { saveScanInputs(); m.remove(); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#sc_start').click(); } }); m.querySelector('#sc_start').onclick = async () => { const mode = m.querySelector('.pk-s-tab.act').dataset.val; saveScanInputs(); if (mode === 'scan') { const vMin = parseInt(inpMin.value) || 0; const vMax = parseInt(inpMax.value) || 0; const kw = m.querySelector('#sc_keyword').value.trim(); if (vMin < 0 || (vMax > 0 && vMin > vMax)) { inpMin.style.borderColor = '#d93025'; if (vMax > 0 && vMin > vMax) inpMax.style.borderColor = '#d93025'; return; } gmSet('pk_scan_last_min', vMin); gmSet('pk_scan_last_max', vMax > 0 ? vMax : ''); gmSet('pk_scan_last_unit', currentUnit); gmSet('pk_scan_last_keyword', kw); let mult = 1; if (currentUnit === 'MB') mult = 1024 * 1024; else if (currentUnit === 'GB') mult = 1024 * 1024 * 1024; else if (currentUnit === 'TB') mult = 1024 * 1024 * 1024 * 1024; S.scanFilter = { minBytes: Math.floor(vMin * mult), maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0, keyword: kw }; m.remove(); S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; S.scanning = true; UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); if (S.isFlattened) { UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; } else { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal) UI.chkGlobal.checked = false; S.isFlattened = false; setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { resumeBackgroundDiscovery(); } }, 1000); } }; S.lastScanTargets = selectedTargets; await runFlattenScanOperation(false, selectedTargets, false); } else { S.dupConfig = { video: m.querySelector('#scan_video').checked, image: m.querySelector('#scan_image').checked, other: m.querySelector('#scan_other').checked }; if (!S.dupConfig.video && !S.dupConfig.image && !S.dupConfig.other) return; m.remove(); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; let fileMap = new Map(); let processedFolders = 0; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; setLoad(true); const isPartialScan = selectedTargets.length > 0; let rootNodes =[]; if (isPartialScan) { updateLoadTxt(L.msg_init_scan_sel); selectedTargets.forEach(item => { if (item.kind === 'drive#folder') { rootNodes.push({ id: item.id, name: item.name, lineage:[{ id: item.id, name: item.name }], retryCount: 0 }); } else { item._lineage =[]; fileMap.set(item.id, item); } }); } else { updateLoadTxt(`${L.str_scanning} 0`); const startNode = S.path[S.path.length - 1]; rootNodes =[{ id: startNode.id || '', name: startNode.name || 'Root', lineage: [], retryCount: 0 }]; } UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); setLoad(false); }; try { await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { f._lineage = parent.lineage ||[]; fileMap.set(f.id, f); }, onFolder: (folder, filesInFolder) => { processedFolders++; if (typeof globalCache !== 'undefined' && !globalCache.has(folder.id)) { globalCache.set(folder.id, [...filesInFolder]); } indexParents(folder.id, folder.name, filesInFolder); if (typeof globalLineageMap !== 'undefined') { globalLineageMap.set(folder.id, folder.lineage); } }, onProgress: (st) => { const folderText = isPartialScan ? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders) : `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : ""; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo + retryTag); } }); if (S.scanning && !signal.aborted && myScanId === S.scanId) { updateLoadTxt(L.str_merging); const tempItems = Array.from(fileMap.values()); const cfg = S.dupConfig || { video: true, image: false, other: false }; let candidates = tempItems.filter(i => { if (!i.mime_type) return false; const isVideo = i.mime_type.startsWith('video'); const isImage = i.mime_type.startsWith('image'); const isOther = !isVideo && !isImage; if (isVideo && cfg.video) return true; if (isImage && cfg.image) return true; if (isOther && cfg.other) return true; return false; }); const preGroups = await computeDuplicateGroups(candidates, cfg, () => S.scanning && !signal.aborted && myScanId === S.scanId); if (preGroups.length === 0) { setLoad(false); showToast(L.msg_dup_none); S.dupRunning = false; S.scanning = false; return; } const total = tempItems.length; S.items = new Array(total); S.itemMap.clear(); let lastYield = performance.now(); for (let i = 0; i < total; i++) { const item = tempItems[i]; S.items[i] = item; S.itemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(item.id); } if (i % 5000 === 0 && performance.now() - lastYield > 16) { updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`); await sleep(0); lastYield = performance.now(); } } S.dupMode = true; S.isFlattened = false; S.sort = 'modified_time'; S.dir = 1; UI.chkAll.checked = false; S.sel.clear(); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; UI.lblGlobal.style.display = 'none'; UI.chkGlobal.checked = false; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important'); if (UI.lblSearchPath) UI.lblSearchPath.style.display = 'flex'; updateLoadTxt(L.str_rendering); S.display =[...S.items]; await refresh(); } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error_crit}: ${e.message}`); } if (myScanId === S.scanId) { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; UI.lblGlobal.style.display = 'flex'; } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } } }; }; UI.scan.onclick = () => openScanDupModal('scan'); const onOfflineFilterChange = () => { if (S.offlineMode) { S.offlineFilters = { running: UI.chkOffRun.checked, failed: UI.chkOffFail.checked, complete: UI.chkOffOk.checked }; refresh(); updateStat(); } }; if (UI.chkOffRun) UI.chkOffRun.onchange = onOfflineFilterChange; if (UI.chkOffFail) UI.chkOffFail.onchange = onOfflineFilterChange; if (UI.chkOffOk) UI.chkOffOk.onchange = onOfflineFilterChange; const onUploadFilterChange = () => { if (S.uploadMode) { S.uploadFilters = { running: UI.chkUpRun.checked, paused: UI.chkUpPause.checked, complete: UI.chkUpDone.checked }; refresh(); updateStat(); } }; if (UI.chkUpRun) UI.chkUpRun.onchange = onUploadFilterChange; if (UI.chkUpPause) UI.chkUpPause.onchange = onUploadFilterChange; if (UI.chkUpDone) UI.chkUpDone.onchange = onUploadFilterChange; const onDupFilterChange = () => { if(S.dupMode) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.sel.clear(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); } }; UI.chkName.onchange = onDupFilterChange; UI.chkSim.onchange = onDupFilterChange; UI.chkHash.onchange = onDupFilterChange; if (UI.chkSearchPath) { UI.chkSearchPath.onchange = () => { if (S.dupMode && S.search) { renderDupView(); } else if ((S.isFlattened || S.analyzeMode) && S.search) { refresh(); } }; } UI.btnExit.onclick = async () => { S.scanning = false; S.dupMode = false; S.suppressClearConfirm = false; S.isFlattened = false; S.scanFilter = null; if (S.filterState) S.filterState = { active: false, cat: 'all', ext: 'all' }; S._sortAppliedForId = null; S._comicApplied = false; if (S.analyzeMode) { S.analyzeMode = false; S.analyzeResultItems = null; S.analyzeSimGroups = null; S.analyzeMap = null; S.path = [{ id: '', name: L.btn_nav_home }]; } S.dupRawGroups = []; S.pinnedDupPath = null; if (UI.selDupFolder) UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } if (UI.chkSearchPath) UI.chkSearchPath.checked = false; isGUISensitive = false; S.sort = 'modified_time'; S.dir = 1; if (UI.crumb) UI.crumb.style.display = ''; S.items.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1; return a.name.localeCompare(b.name); }); S.clearSelection(); const targetNode = S.path[S.path.length - 1]; const targetKey = S.getRealCacheKey(targetNode.id); const cachedData = (typeof globalCache !== 'undefined') ? (globalCache.get(targetKey) || globalCache.get('')) : null; if (cachedData) { if (cachedData.items) S.items = [...cachedData.items]; else if (Array.isArray(cachedData)) S.items = [...cachedData]; S.itemMap.clear(); for (const it of S.items) S.itemMap.set(it.id, it); } else { S.items = []; S.itemMap.clear(); } setLoad(true, true); refresh(); updateQuotaUI(); await load(false, true); if (typeof S.wasGlobalChecked !== 'undefined' && UI.chkGlobal) { UI.chkGlobal.checked = S.wasGlobalChecked; } setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { console.log("♻️ Sandbox exited: Forcing global discovery to resume."); resumeBackgroundDiscovery(); } }, 1500); }; UI.cols.forEach(c => c.onclick = () => { if (S.dupMode) return; const k = c.dataset.k; if (S.sort === k) { S.dir *= -1; } else { S.sort = k; S.dir = 1; } refresh(); }); if (UI.btnFolderFirst) { S.renderFolderFirst = () => { UI.btnFolderFirst.style.color = S.folderFirst ? 'var(--pk-pri)' : '#666'; }; UI.btnFolderFirst.onmouseenter = () => { if (!S.folderFirst) UI.btnFolderFirst.style.color = 'var(--pk-fg)'; }; UI.btnFolderFirst.onmouseleave = () => { if (!S.folderFirst) UI.btnFolderFirst.style.color = '#666'; }; const nameWrap = el.querySelector('#pk-name-text-wrap'); if (nameWrap) { nameWrap.onmouseenter = () => { if (S.sort !== 'name') nameWrap.style.color = 'var(--pk-fg)'; }; nameWrap.onmouseleave = () => { if (S.sort !== 'name') nameWrap.style.color = '#666'; }; } S.renderFolderFirst(); UI.btnFolderFirst.onclick = (e) => { e.stopPropagation(); S.folderFirst = !S.folderFirst; const curNode = S.path[S.path.length - 1]; const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curNode.id.startsWith('virtual_') || curNode.id === 'virtual_search_root'); if (isStandard) { if (gmGet('pk_sort_independent', false)) { const folderId = curNode.id || 'root'; try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); const currentPref = prefStore[folderId] || { sort: S.sort, dir: S.dir }; currentPref.folderFirst = S.folderFirst; currentPref.sort = S.sort; currentPref.dir = S.dir; prefStore[folderId] = currentPref; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } catch(e) {} } else { const globalPref = JSON.parse(gmGet('pk_global_sort_pref', '{"sort":"modified_time","dir":1}')); globalPref.folderFirst = S.folderFirst; globalPref.sort = S.sort; globalPref.dir = S.dir; gmSet('pk_global_sort_pref', JSON.stringify(globalPref)); gmSet('pk_folder_first', S.folderFirst); } } else { gmSet('pk_folder_first', S.folderFirst); } S.renderFolderFirst(); refresh(); }; } const btnInvert = document.getElementById('pk-btn-invert'); if (btnInvert) { btnInvert.onclick = (e) => { e.stopPropagation(); if (S.loading || S.display.length === 0) return; const newSel = new Set(); for (let i = 0; i < S.display.length; i++) { const item = S.display[i]; if (item && !item.isHeader) { if (!S.sel.has(item.id)) { newSel.add(item.id); } } } S.sel = newSel; S.lastSelIdx = -1; S.activeId = null; renderVisible(); updateStat(); }; btnInvert.onmouseenter = () => btnInvert.style.color = 'var(--pk-pri)'; btnInvert.onmouseleave = () => btnInvert.style.color = 'var(--pk-fg)'; } S.handleSelectAll = (e) => { if (!e || !e.target) { if (UI.chkAll) UI.chkAll.checked = !UI.chkAll.checked; } S.activeId = null; S.lastSelIdx = -1; let totalVisible = 0; const allIds = []; const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; if (item.isHeader) continue; if (S.movingIds.has(item.id)) continue; totalVisible++; allIds.push(item.id); } const isSelectAllAction = (S.sel.size < totalVisible); UI.chkAll.checked = isSelectAllAction; if (isSelectAllAction) { S.sel = new Set(allIds); } else { S.sel.clear(); } requestAnimationFrame(() => { renderVisible(); updateStat(); UI.chkAll.checked = isSelectAllAction; }); }; if (UI.btnAnaSelect || UI.btnDupSmart) { const pop = document.createElement('div'); pop.className = 'pk-ana-pop'; pop.innerHTML = ` <div class="pk-ana-pop-row"><div class="pk-ana-opt" data-op="new">${L.opt_keep_new}</div><div class="pk-ana-opt" data-op="old">${L.opt_keep_old}</div></div> <div class="pk-ana-pop-row"><div class="pk-ana-opt" data-op="large">${L.opt_keep_large}</div><div class="pk-ana-opt" data-op="small">${L.opt_keep_small}</div></div> <div class="pk-ana-pop-row"><div class="pk-ana-opt" data-op="short">${L.opt_keep_short}</div><div class="pk-ana-opt" data-op="long">${L.opt_keep_long}</div></div>`; UI.win.appendChild(pop); let activeTargetBtn = null; const updatePopPos = () => { if (pop.style.display !== 'flex' || !activeTargetBtn) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const winRect = UI.win.getBoundingClientRect(); const btnRect = activeTargetBtn.getBoundingClientRect(); let left = (btnRect.left - winRect.left) / scale; const top = (btnRect.bottom - winRect.top) / scale; const winWidth = winRect.width / scale; const popWidth = 340; if (left + popWidth > winWidth - 10) { left = (btnRect.right - winRect.left) / scale - popWidth; } pop.style.left = Math.max(10, left) + 'px'; pop.style.top = (top + 5) + 'px'; }; const togglePop = (e, btn) => { e.stopPropagation(); const isVisible = (pop.style.display === 'flex' && activeTargetBtn === btn); if (isVisible) { pop.style.display = 'none'; activeTargetBtn = null; } else { if (S.analyzeMode && !S.hasShownAnaWarn) { showToast(L.msg_ana_warn, 'warning', 6000); S.hasShownAnaWarn = true; } pop.style.display = 'flex'; activeTargetBtn = btn; updatePopPos(); } }; if (UI.btnAnaSelect) UI.btnAnaSelect.onclick = (e) => togglePop(e, UI.btnAnaSelect); if (UI.btnDupSmart) UI.btnDupSmart.onclick = (e) => togglePop(e, UI.btnDupSmart); window.addEventListener('resize', updatePopPos); pop.querySelectorAll('.pk-ana-opt').forEach(opt => { opt.onclick = () => { const op = opt.dataset.op; S.sel.clear(); const isBetter = (type, curW, curM) => { if (type === 'new') return new Date(curM.modified_time) > new Date(curW.modified_time); if (type === 'old') return new Date(curM.modified_time) < new Date(curW.modified_time); if (type === 'large') return BigInt(curM.size || 0) > BigInt(curW.size || 0); if (type === 'small') return BigInt(curM.size || 0) < BigInt(curW.size || 0); if (type === 'short') return curM.name.length < curW.name.length; if (type === 'long') return curM.name.length > curW.name.length; return false; }; if (S.analyzeMode && S.analyzeSimGroups) { S.analyzeSimGroups.forEach(g => { const members = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); if (members.length < 2) return; let winner = members[0]; members.forEach(m => { if (isBetter(op, winner, m)) winner = m; }); members.forEach(m => { if (m.id !== winner.id) S.sel.add(m.id); }); }); } else if (S.dupMode && S.dupGroups) { const itemMap = new Map(); S.display.forEach(d => { if (d.isHeader) return; const gIdx = S.dupGroups.get(d.id); if (gIdx !== undefined) { if (!itemMap.has(gIdx)) itemMap.set(gIdx,[]); itemMap.get(gIdx).push(d); } }); itemMap.forEach(members => { if (members.length < 2) return; let winner = members[0]; members.forEach(m => { if (isBetter(op, winner, m)) winner = m; }); members.forEach(m => { if (m.id !== winner.id) S.sel.add(m.id); }); }); } pop.style.display = 'none'; activeTargetBtn = null; renderVisible(); updateStat(); }; }); document.addEventListener('mousedown', (e) => { const isClickInsideBtn = (UI.btnAnaSelect && UI.btnAnaSelect.contains(e.target)) || (UI.btnDupSmart && UI.btnDupSmart.contains(e.target)); if (!pop.contains(e.target) && !isClickInsideBtn) { pop.style.display = 'none'; activeTargetBtn = null; } }); } UI.btnRefresh.onclick = async () => { updateQuotaUI(); if (S.isFlattened) { if (!S.scanning) { S.scanning = true; UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; }; runFlattenScanOperation(false, S.lastScanTargets, true).catch(e => { console.error(e); S.scanning = false; }); } return; } const cur = S.path[S.path.length - 1]; const intent = UI.chkGlobal ? UI.chkGlobal.checked : false; if (cur.id === 'analyze_root') { setLoad(true); updateLoadTxt(L.str_refreshing); await sleep(200); await load(); return; } if (cur.id) S.cache.delete(cur.id); const p = load(false, true); if (UI.chkGlobal) UI.chkGlobal.checked = intent; await p; }; if (UI.btnAnalyze) { UI.btnAnalyze.onclick = async () => { if (S.trashMode) return; const curFolderId = S.path[S.path.length - 1].id || ''; if (isPathBusy(curFolderId)) { showAlert(L.msg_op_blocked_analyzing); return; } S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; const lastMin = gmGet('pk_analyze_last_min', 0); const lastMax = gmGet('pk_analyze_last_max', ''); const lastUnit = gmGet('pk_analyze_last_unit', 'GB'); const lastKeyword = gmGet('pk_analyze_last_keyword', ''); const lastSim = gmGet('pk_analyze_last_sim', 1.0); const lastAlgo = gmGet('pk_analyze_last_algo', 'sim'); const result = await new Promise((resolve) => { const L_min = L.lbl_ana_min; const L_max = L.lbl_ana_max; const analyzeTargetDesc = S.sel.size > 0 ? L.lbl_analyze_selected.replace('{n}', S.sel.size) : L.lbl_analyze_current; const anaSimTargetDesc = S.sel.size > 0 ? L.lbl_ana_sim_selected.replace('{n}', S.sel.size) : L.lbl_ana_sim_current; const m = showModal(` <h3 style="border:none; margin-bottom:15px; font-size:18px; font-weight:700;">${L.btn_analyze}</h3> <div class="pk-s-tabs" id="ana_tabs" style="margin-bottom:20px; display:flex;"> <div class="pk-s-tab act" data-val="large">${L.opt_ana_large}</div> <div class="pk-s-tab" data-val="similar">${L.opt_ana_sim}</div> </div> <div id="ana_pane_large"> <div style="font-size:13px; color:#888; margin-bottom:20px; line-height:1.5;">${analyzeTargetDesc}</div> <div style="margin-bottom:20px; position:relative;"> <input type="text" id="an_keyword" value="${esc(lastKeyword)}" placeholder="${L.ph_keyword_filter}" oninput="this.style.borderColor = this.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'" style="width:100%; height:42px; padding:0 12px; border:2px solid ${lastKeyword ? 'var(--pk-pri)' : 'var(--pk-bd)'}; border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; outline:none; transition:border-color 0.2s; box-sizing:border-box;"> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; line-height:1; white-space:nowrap;">${L.lbl_keyword_filter}</div> </div> <div style="display:flex; align-items:center; gap:10px; margin-bottom:25px;"> <div style="flex:1; position:relative;"> <input type="number" id="an_val_min" value="${lastMin === 0 ? '' : lastMin}" placeholder="0" min="0" step="1" style="width:100%; height:42px; padding:0 30px 0 12px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:16px; font-weight:700; outline:none; transition:border-color 0.2s; box-sizing:border-box; font-family:monospace;"> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; line-height:1; white-space:nowrap;">${L_min}</div> <div class="pk-num-ctrl"> <div class="pk-num-btn" id="an_inc_min">${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}</div> <div class="pk-num-btn" id="an_dec_min">${CONF.crumbIcons.down}</div> </div> </div> <div style="color:#888; font-weight:bold; flex-shrink:0;">-</div> <div style="flex:1; position:relative;"> <input type="number" id="an_val_max" value="${lastMax}" min="0" step="1" placeholder="∞" style="width:100%; height:42px; padding:0 30px 0 12px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:16px; font-weight:700; outline:none; transition:border-color 0.2s; box-sizing:border-box; font-family:monospace;"> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; line-height:1; white-space:nowrap;">${L_max}</div> <div class="pk-num-ctrl"> <div class="pk-num-btn" id="an_inc_max">${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}</div> <div class="pk-num-btn" id="an_dec_max">${CONF.crumbIcons.down}</div> </div> </div> <div class="pk-ana-select"> <div class="pk-ana-trigger" id="an_unit_btn"> <span id="an_unit_txt">${lastUnit}</span> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg> </div> <div class="pk-ana-menu" id="an_unit_menu"> <div class="pk-ana-item ${lastUnit === 'MB' ? 'act' : ''}" data-v="MB">MB</div> <div class="pk-ana-item ${lastUnit === 'GB' ? 'act' : ''}" data-v="GB">GB</div> <div class="pk-ana-item ${lastUnit === 'TB' ? 'act' : ''}" data-v="TB">TB</div> </div> </div> </div> </div> <div id="ana_pane_similar" style="display:none;"> <div style="font-size:13px; color:#888; margin-bottom:15px; line-height:1.5;">${anaSimTargetDesc}</div> <div style="display:flex; flex-wrap:wrap; gap:12px; margin-bottom:20px; align-items:center; width:100%;"> <label style="display:flex; align-items:center; cursor:pointer; flex-shrink:0;"> <input type="radio" name="ana_sim_algo" value="name" ${lastAlgo === 'name' ? 'checked' : ''} style="accent-color:var(--pk-pri); margin-right:6px;"> <span style="font-size:13px; color:var(--pk-fg); font-weight:500;">${L.lbl_name_match}</span> </label> <label style="display:flex; align-items:center; cursor:pointer; flex-shrink:0;"> <input type="radio" name="ana_sim_algo" value="sim" ${lastAlgo === 'sim' || !lastAlgo ? 'checked' : ''} style="accent-color:var(--pk-pri); margin-right:6px;"> <span style="font-size:13px; color:var(--pk-fg); font-weight:500;">${L.lbl_sim_match}</span> </label> <label style="display:flex; align-items:center; cursor:pointer; flex-shrink:0;"> <input type="radio" name="ana_sim_algo" value="contain" ${lastAlgo === 'contain' ? 'checked' : ''} style="accent-color:var(--pk-pri); margin-right:6px;"> <span style="font-size:13px; color:var(--pk-fg); font-weight:500;">${L.lbl_contain_match}</span> </label> <div id="pk_algo_help" style="display:flex; align-items:center; cursor:pointer; color:#888; transition:color 0.2s; flex-shrink:0; width:20px; height:20px; justify-content:center;" data-pk-tip="${L.title_algo_help}" onmouseover="this.style.color=document.querySelector('.pk-ov').classList.contains('pk-dark')?'#ddd':'#666'" onmouseout="this.style.color='#888'"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0; pointer-events:none;"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> </div> </div> <div class="pk-custom-select" id="cs_ana_sim" style="margin-bottom:25px;"> <div class="pk-select-label">${L.lbl_threshold}</div> <div class="pk-select-trigger"><span id="txt_ana_sim">${(lastSim === 0.01) ? L.opt_loose : L.opt_strict}</span>${CONF.crumbIcons.down}</div> <div class="pk-select-menu pk-scroll"> <div class="pk-select-item ${lastSim === 0.01 ? 'act' : ''}" data-val="0.01">${L.opt_loose}</div> <div class="pk-select-item ${lastSim !== 0.01 ? 'act' : ''}" data-val="1.0">${L.opt_strict}</div> </div> </div> </div> <div class="pk-modal-act"> <button class="pk-btn" id="an_cancel" style="height:40px; padding:0 20px;">${L.btn_cancel}</button> <button class="pk-btn pri" id="an_confirm" style="height:40px; padding:0 30px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold;">${L.btn_ok}</button> </div> `); let currentMode = 'large'; let currentSim = lastSim; const updateAlgoLabel = () => { const lblEl = m.querySelector('#cs_ana_sim .pk-select-label'); if (lblEl) lblEl.textContent = L.lbl_threshold; }; m.querySelectorAll('input[name="ana_sim_algo"]').forEach(r => r.addEventListener('change', updateAlgoLabel)); updateAlgoLabel(); m.querySelector('#pk_algo_help').onclick = (e) => { e.stopPropagation(); showAlert(L.algo_help_content, L.title_algo_help); }; m.querySelectorAll('.pk-s-tab').forEach(tab => { tab.onclick = () => { m.querySelectorAll('.pk-s-tab').forEach(t => t.classList.remove('act')); tab.classList.add('act'); currentMode = tab.dataset.val; m.querySelector('#ana_pane_large').style.display = currentMode === 'large' ? 'block' : 'none'; m.querySelector('#ana_pane_similar').style.display = currentMode === 'similar' ? 'block' : 'none'; }; }); const simTrigger = m.querySelector('#cs_ana_sim .pk-select-trigger'); const simMenu = m.querySelector('#cs_ana_sim .pk-select-menu'); const simTxt = m.querySelector('#txt_ana_sim'); simTrigger.onclick = (e) => { e.stopPropagation(); simMenu.style.display = simMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('#cs_ana_sim .pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('#cs_ana_sim .pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentSim = parseFloat(item.dataset.val); gmSet('pk_analyze_last_sim', currentSim); simTxt.textContent = (currentSim <= 0.5) ? L.opt_loose : L.opt_strict; simMenu.style.display = 'none'; }; }); const inpMin = m.querySelector('#an_val_min'); const inpMax = m.querySelector('#an_val_max'); const btn = m.querySelector('#an_unit_btn'); m.querySelector('#an_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#an_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#an_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; inpMax.dispatchEvent(new Event('input')); }; m.querySelector('#an_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); inpMax.dispatchEvent(new Event('input')); }; const menu = m.querySelector('#an_unit_menu'); const txt = m.querySelector('#an_unit_txt'); let currentUnit = lastUnit; btn.onclick = (e) => { e.stopPropagation(); menu.style.display = menu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('.pk-ana-item').forEach(item => { item.onclick = () => { m.querySelectorAll('.pk-ana-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentUnit = item.dataset.v; txt.textContent = currentUnit; menu.style.display = 'none'; }; }); const closeMenu = () => { if(menu) menu.style.display = 'none'; if(simMenu) simMenu.style.display = 'none'; }; setTimeout(() => document.addEventListener('click', closeMenu), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeMenu); _orgRemove(); }; setTimeout(() => { inpMin.focus(); inpMin.select(); }, 50); const kHandler = (e) => { if (e.key === 'Enter') m.querySelector('#an_confirm').click(); if (e.key === 'Escape') m.querySelector('#an_cancel').click(); }; inpMin.onkeydown = kHandler; inpMax.onkeydown = kHandler; const saveAnalyzeInputs = () => { gmSet('pk_analyze_last_min', parseInt(inpMin.value) || 0); gmSet('pk_analyze_last_max', inpMax.value.trim()); gmSet('pk_analyze_last_unit', currentUnit); gmSet('pk_analyze_last_keyword', m.querySelector('#an_keyword').value.trim()); const algo = m.querySelector('input[name="ana_sim_algo"]:checked')?.value; if (algo) gmSet('pk_analyze_last_algo', algo); }; m.querySelector('#an_cancel').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); }; m.querySelector('#an_confirm').onclick = () => { if (currentMode === 'large') { const vMin = parseInt(inpMin.value) || 0; const vMax = parseInt(inpMax.value) || 0; const kw = m.querySelector('#an_keyword').value.trim(); if (vMin < 0 || (vMax > 0 && vMin > vMax)) { inpMin.style.borderColor = '#d93025'; if (vMax > 0 && vMin > vMax) inpMax.style.borderColor = '#d93025'; return; } saveAnalyzeInputs(); let mult = 1; if (currentUnit === 'MB') mult = 1024 * 1024; else if (currentUnit === 'GB') mult = 1024 * 1024 * 1024; else if (currentUnit === 'TB') mult = 1024 * 1024 * 1024 * 1024; m.remove(); resolve({ mode: 'large', minBytes: Math.floor(vMin * mult), maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0, keyword: kw }); } else { const algo = m.querySelector('input[name="ana_sim_algo"]:checked').value; gmSet('pk_analyze_last_algo', algo); m.remove(); resolve({ mode: 'similar', threshold: currentSim, algo: algo }); } }; const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '540px', height: 'auto', minHeight: 'auto', overflow: 'visible', paddingBottom: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' }); } }); if (result === null) return; const isSimMode = result.mode === 'similar'; const minBytes = result.minBytes || 0; const maxBytes = result.maxBytes || 0; const simThreshold = result.threshold || 0.9; const simAlgo = result.algo || 'sim'; setLoad(true); isGUISensitive = true; let nodeMap = new Map(); const largeFolders = []; const startNodes = []; const getRealLineage = (item) => { if (item._lineage && item._lineage.length > 0) { return item._lineage; } const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root'); return [...cleanPath, { id: item.id, name: item.name }]; }; if (S.sel.size > 0) { S.sel.forEach(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') { const fullLineage = getRealLineage(item); startNodes.push({ id: item.id, name: item.name, icon_link: item.icon_link, starred: item.starred, tags: item.tags, lineage: fullLineage, retryCount: 0, _pathStr: fullLineage.map(x => x.name).join('/') }); } }); } else { const subFolders = S.items.filter(it => it.kind === 'drive#folder'); if (subFolders.length > 0) { subFolders.forEach(item => { const fullLineage = getRealLineage(item); startNodes.push({ id: item.id, name: item.name, icon_link: item.icon_link, starred: item.starred, tags: item.tags, lineage: fullLineage, retryCount: 0, _pathStr: fullLineage.map(x => x.name).join('/') }); }); } else { const cur = S.path[S.path.length - 1]; const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root'); if (cleanPath.length === 0) cleanPath.push({ id: '', name: L.btn_nav_home }); const rootName = cur.name || 'Root'; const actualCur = S.itemMap.get(cur.id); startNodes.push({ id: cur.id || '', name: rootName, icon_link: cur.icon_link, starred: actualCur ? actualCur.starred : false, tags: actualCur ? actualCur.tags :[], lineage: cleanPath, retryCount: 0, _pathStr: cleanPath.map(x => x.name).join('/') }); } } if (startNodes.length === 0) { setLoad(false); isGUISensitive = false; showToast(L.msg_analyze_only_normal_dir); return; } startNodes.forEach(n => { nodeMap.set(n.id, { id: n.id, name: n.name, icon_link: n.icon_link, starred: n.starred, tags: n.tags, size: 0, parentId: null, marked: false, _pathStr: n._pathStr, lineage: n.lineage, isRoot: true, files:[] }); }); const cacheKey = '__analyze_nodeMap_' + startNodes.map(n => n.id).join('_'); let useCache = false; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey) && globalDirtyFolders.size === 0) { const cachedArr = globalCache.get(cacheKey); nodeMap = new Map(cachedArr); useCache = true; console.log("[Analyze] Using cached global nodeMap."); } const propagateSize = (parentId, addSize) => { let curId = parentId; while (curId !== null && nodeMap.has(curId)) { const node = nodeMap.get(curId); node.size += addSize; if (!node.isRoot && node.size >= minBytes && !node.marked) { node.marked = true; largeFolders.push(node); } curId = node.parentId; } }; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); }; S.scanning = true; let totalFilesScanned = 0; let totalDirsScanned = 0; try { if (!useCache) { await coreRecursiveEngine(startNodes, { signal: signal, onFolder: (folder, filesInFolder, nextSubFolders) => { totalDirsScanned++; nextSubFolders.forEach(sub => { if (!nodeMap.has(sub.id)) { const fullPathStr = sub.lineage.map(x => x.name).join('/'); nodeMap.set(sub.id, { id: sub.id, name: sub.name, icon_link: sub.icon_link, starred: sub.starred, tags: sub.tags, size: 0, parentId: folder.id, marked: false, _pathStr: fullPathStr, lineage: sub.lineage, isRoot: false, files:[] }); } }); }, onFile: (file, parent) => { totalFilesScanned++; const sz = Number(file.size || 0); if (sz > 0) { propagateSize(parent.id, sz); } if (nodeMap.has(parent.id)) { const fingerprint = file.hash ? `${file.hash}_${sz}` : `${file.name}_${sz}`; let curId = parent.id; while (curId !== null && nodeMap.has(curId)) { nodeMap.get(curId).files.push(fingerprint); curId = nodeMap.get(curId).parentId; } } }, onProgress: (st) => { updateLoadTxt(`${L.str_scanning} ${st.folders} ${L.unit_folders} | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`); } }); if (!isRunning || signal.aborted || myScanId !== S.scanId) { throw new Error('StoppedByUser'); } if (typeof globalCache !== 'undefined') { globalCache.set(cacheKey, Array.from(nodeMap.entries())); } } else { Array.from(nodeMap.values()).forEach(node => { if (!node.isRoot && node.size >= minBytes) { largeFolders.push(node); } }); } } catch (e) { if (isRunning && e.message !== 'StoppedByUser' && e.name !== 'AbortError') { showAlert(`${L.str_error}: ${e.message}`); setLoad(false); } } finally { isGUISensitive = false; S.scanning = false; S.scanAbortController = null; if (!isRunning) { setLoad(false); return; } let viewItems = []; if (isSimMode) { updateLoadTxt(`${L.str_analyzing}...`); Array.from(nodeMap.values()).forEach(node => { node._ancestorSet = new Set(node.lineage ? node.lineage.map(p => p.id) : []); if (node.parentId && nodeMap.has(node.parentId)) { const parent = nodeMap.get(node.parentId); if (parent.files.length === node.files.length) { parent.isShell = true; } } }); const isDescendant = (childId, parentId) => { const childNode = nodeMap.get(childId); return childNode ? childNode._ancestorSet.has(parentId) : false; }; const folderArr = Array.from(nodeMap.values()) .filter(f => !f.isShell && (f.files.length >= 2 || f.size > 1024 * 1024)) .map(f => { const counts = new Map(); f.files.forEach(h => counts.set(h, (counts.get(h) || 0) + 1)); return { ...f, fileCounts: counts, _keys: Array.from(counts.keys()), totalFiles: f.files.length }; }) .sort((a, b) => b.totalFiles - a.totalFiles); const invertedIndex = new Map(); folderArr.forEach((f, i) => { f.fileCounts.forEach((_, hash) => { let arr = invertedIndex.get(hash); if (!arr) { arr = []; invertedIndex.set(hash, arr); } arr.push(i); }); }); const totalDocs = folderArr.length; const weightMap = new Map(); invertedIndex.forEach((arr, hash) => { const df = arr.length; let w = 1.0; if (totalDocs >= 20 && (df / totalDocs) > 0.05) { w = 0.05; } weightMap.set(hash, w); }); folderArr.forEach(f => { let wt = 0; f.fileCounts.forEach((count, hash) => { wt += count * weightMap.get(hash); }); f.weightedTotal = wt; }); let groups =[]; const assigned = new Set(); const total = folderArr.length; const candidateSeen = new Uint32Array(total); let lastYieldTime = performance.now(); updateLoadTxt(`${L.str_analyzing}... 0%`); try { if (simAlgo === 'name') { const nameGroups = new Map(); const cleanFolderName = (oldName) => { let cleanName = oldName.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim(); cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, ''); cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, ''); const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])"; const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\s)', 'i'); cleanName = cleanName.replace(atRegex, ''); const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i'); cleanName = cleanName.replace(hyphenRegex, ''); cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, ''); cleanName = cleanName.replace(/^[-_. ,,::;;\p{Extended_Pictographic}]+/u, ''); const pairs = [['【','】'], ['[',']'], ['《','》'],['<','>'], ['(',')'],['(',')'], ['{','}']]; pairs.forEach(([L_char, R_char]) => { const idxR_Fix = cleanName.indexOf(R_char); const idxL_Check = cleanName.indexOf(L_char); if (idxR_Fix > 0 && idxR_Fix <= 10 && (idxL_Check === -1 || idxL_Check > idxR_Fix)) { cleanName = L_char + cleanName; } const chars = cleanName.split(''); const stack = []; const toRemove = new Set(); for (let i = 0; i < chars.length; i++) { const c = chars[i]; if (c === L_char) stack.push(i); else if (c === R_char) { if (stack.length > 0) stack.pop(); else toRemove.add(i); } } stack.forEach(i => toRemove.add(i)); if (toRemove.size > 0) cleanName = chars.filter((_, i) => !toRemove.has(i)).join(''); }); const quoteCount = (cleanName.match(/'/g) || []).length; if (quoteCount % 2 !== 0) cleanName = cleanName.replace(/'/, ''); let finalResult = cleanName.toLowerCase().trim(); if (simThreshold <= 0.5) { finalResult = finalResult.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ''); } return finalResult || oldName.toLowerCase().trim(); }; folderArr.forEach(f => { const k = cleanFolderName(f.name); if (!nameGroups.has(k)) nameGroups.set(k,[]); nameGroups.get(k).push(f); }); const sizeRatioLimit = simThreshold >= 0.5 ? 0.05 : 0.10; for (const[k, items] of nameGroups) { if (items.length > 1) { const sorted = [...items].sort((a,b) => Number(a.size) - Number(b.size)); let currentGroup = [sorted[0]]; for (let i = 1; i < sorted.length; i++) { const target = sorted[i]; const root = currentGroup[0]; const rootSize = Number(root.size || 0); const targetSize = Number(target.size || 0); let isMatch = false; if (rootSize === 0 && targetSize === 0) isMatch = true; else { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); if (maxBase > 0 && (sizeDiff / maxBase) <= sizeRatioLimit) isMatch = true; } if (isMatch) currentGroup.push(target); else { if (currentGroup.length > 1) { const gNodes = currentGroup; let minS = Number.MAX_SAFE_INTEGER, maxS = 0; gNodes.forEach(n => { const sz = Number(n.size||0); if(sz<minS) minS=sz; if(sz>maxS) 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(sz<minS) minS=sz; if(sz>maxS) 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.length; x++){ for(let y=x+1; y<sg.length; y++){ const idx1 = sg[x] < sg[y] ? sg[x] : sg[y]; const idx2 = sg[x] > sg[y] ? sg[x] : sg[y]; const sVal = simMatrix[idx1][idx2]; if (sVal < subMin) subMin = sVal; if (sVal > subMax) subMax = sVal; } } const minPct = Math.round(subMin * 100); const maxPct = Math.round(subMax * 100); const range = (minPct === maxPct) ? `${minPct}%` : `${minPct}% ~ ${maxPct}%`; partitionedGroups.push({ ids: sg.map(idx => nodes[idx].id), type: `${sg.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${range}`, _sim: subMin }); } } }); } }); groups = partitionedGroups; } } catch (e) { console.error("[SimMode] Error:", e); } if (groups.length === 0) { setLoad(false); showToast(L.msg_dup_none); return; } groups.sort((a, b) => (b._sim - a._sim) || (b.ids.length - a.ids.length)); S.analyzeSimGroups = groups; viewItems = Array.from(assigned).map(id => { const node = nodeMap.get(id); return { id: node.id, kind: 'drive#folder', name: node.name, icon_link: node.icon_link, starred: node.starred, tags: node.tags, size: node.size.toString(), _pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home, _lineage: node.lineage, modified_time: new Date(getServerNow()).toISOString(), parent_id: node.parentId }; }); } else { updateLoadTxt(L.str_rendering); const uniqueResults = Array.from(new Set(largeFolders)); const kw = gmGet('pk_analyze_last_keyword', ''); const kwList = kw ? kw.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : []; let filteredResults = uniqueResults.filter(n => n.size >= minBytes && (maxBytes === 0 || n.size <= maxBytes)); if (kwList.length > 0) { filteredResults = filteredResults.filter(node => !kwList.some(k => (node.name || "").toLowerCase().includes(k))); } filteredResults.sort((a, b) => b.size - a.size); if (filteredResults.length === 0) { setLoad(false); let rangeStr = maxBytes > 0 ? `${fmtSize(minBytes)} - ${fmtSize(maxBytes)}` : `≥ ${fmtSize(minBytes)}`; showAlert(L.msg_analyze_no_large_folders.replace('{s}', rangeStr)); return; } viewItems = filteredResults.map(node => ({ id: node.id, kind: 'drive#folder', name: node.name, icon_link: node.icon_link, starred: node.starred, tags: node.tags, size: node.size.toString(), _pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home, _lineage: node.lineage, modified_time: new Date(getServerNow()).toISOString(), parent_id: node.parentId })); } S.analyzeMode = true; S.hasShownAnaWarn = false; S.sort = 'size'; S.dir = 1; S.isFlattened = false; S.dupMode = false; S.analyzeResultItems = [...viewItems]; S.analyzeMap = nodeMap; S.path = [{ id: 'analyze_root', name: L.str_analyze_results }]; renderCrumb(); S.items = viewItems; S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); S.sel.clear(); UI.scan.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; refresh(); updateStat(); setTimeout(() => { setLoad(false); isGUISensitive = false; }, 200); } }; } function showAnalyzeResultModal(list, threshold) { const L = getStrings(); const modalHtml = ` <div style="padding-bottom:15px; border-bottom:1px solid var(--pk-bd); margin-bottom:10px;"> <h3 style="margin:0; font-size:18px; font-weight:700;">${L.title_analyze_result}</h3> <div style="font-size:12px; color:#888; margin-top:5px;"> ${L.msg_analyze_summary_fmt.replace('{n}', list.length).replace('{s}', threshold)} </div> </div> <div class="pk-scroll" style="flex:1; overflow-y:auto; border:1px solid var(--pk-bd); border-radius:4px; background:var(--pk-bg);"> <div style="display:grid; grid-template-columns: 1fr 100px 80px; font-weight:bold; padding:8px 12px; border-bottom:1px solid var(--pk-bd); background:var(--pk-hl); font-size:12px; position:sticky; top:0;"> <div>${L.col_path_name}</div> <div style="text-align:right;">${L.col_size}</div> <div style="text-align:center;">${L.col_action}</div> </div> <div id="pk_analyze_list"></div> </div> <div class="pk-modal-act" style="margin-top:15px;"> <button class="pk-btn pri" id="pk_analyze_close" style="width:100px; justify-content:center;">${L.btn_close}</button> </div> `; 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 = ` <div style="overflow:hidden;"> <div style="font-weight:bold; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--pk-fg);" title="${esc(name)}">${esc(name)}</div> <div style="color:#999; font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="${esc(parentPath)}">${esc(parentPath || '/')}</div> </div> <div style="text-align:right; font-family:monospace; color:var(--pk-pri); font-weight:600;">${fmtSize(sizeNum)}</div> <div style="text-align:center;"> <button class="pk-btn" style="padding:2px 8px; font-size:11px; height:24px;" title="${L.tip_jump_to_folder}">${L.btn_jump}</button> </div> `; 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(` <h3 style="border:none; margin-bottom:0px; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.title_export_format}</h3> <div style="font-size:13px; color:#888; margin-bottom:20px; line-height:1.5;">${L.lbl_export_current}</div> <div style="display:grid; grid-template-columns: 1fr 1fr; gap:20px; margin-bottom:25px;"> <div class="pk-exp-card act" data-fmt="tree" style="border:2px solid var(--pk-pri); border-radius:8px; padding:15px; cursor:pointer; position:relative; background:var(--pk-bg); transition:all 0.2s;"> <div class="pk-exp-check" style="position:absolute; top:0; right:0; width:24px; height:24px; background:var(--pk-pri); border-bottom-left-radius:8px; display:flex; align-items:center; justify-content:center; color:#fff;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> </div> <div class="pk-exp-title" style="font-weight:bold; margin-bottom:10px; color:var(--pk-pri); font-size:15px;">${L.opt_tree_view}</div> <div style="font-size:12px; color:var(--pk-fg); line-height:1.5; font-family:Consolas, 'Courier New', monospace; opacity:0.8; white-space:pre; letter-spacing:0px;">Root\n├─ Folder 1\n│ ├─ Folder 1-1\n│ └─ Folder 1-2\n└─ Folder 2\n └─ Folder 2-1</div> </div> <div class="pk-exp-card" data-fmt="list" style="border:2px solid var(--pk-bd); border-radius:8px; padding:15px; cursor:pointer; position:relative; background:var(--pk-bg); transition:all 0.2s;"> <div class="pk-exp-check" style="position:absolute; top:0; right:0; width:24px; height:24px; background:var(--pk-pri); border-bottom-left-radius:8px; display:none; align-items:center; justify-content:center; color:#fff;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> </div> <div class="pk-exp-title" style="font-weight:bold; margin-bottom:10px; color:var(--pk-fg); font-size:15px;">${L.opt_list_view}</div> <div style="font-size:12px; color:var(--pk-fg); line-height:1.5; font-family:Consolas, 'Courier New', monospace; opacity:0.8; white-space:pre;">Root/Folder 1\nRoot/Folder 1/Folder 1-1\nRoot/Folder 1/Folder 1-2\nRoot/Folder 2\nRoot/Folder 2/Folder 2-1</div> </div> </div> <div class="pk-modal-act"> <button class="pk-btn" id="exp_cancel" style="height:40px; padding:0 20px;">${L.btn_cancel}</button> <button class="pk-btn pri" id="exp_confirm" style="height:40px; padding:0 30px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold;">${L.btn_ok}</button> </div> `); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = '520px'; modalBox.style.height = 'auto'; modalBox.style.minHeight = 'auto'; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' }); } let selectedFormat = 'tree'; m.querySelectorAll('.pk-exp-card').forEach(card => { card.onclick = () => { m.querySelectorAll('.pk-exp-card').forEach(c => { c.style.borderColor = 'var(--pk-bd)'; c.querySelector('.pk-exp-title').style.color = 'var(--pk-fg)'; c.querySelector('.pk-exp-check').style.display = 'none'; c.classList.remove('act'); }); card.style.borderColor = 'var(--pk-pri)'; card.querySelector('.pk-exp-title').style.color = 'var(--pk-pri)'; card.querySelector('.pk-exp-check').style.display = 'flex'; card.classList.add('act'); selectedFormat = card.dataset.fmt; }; }); m.querySelector('#exp_cancel').onclick = () => { m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); }; m.querySelector('#exp_confirm').onclick = () => { m.remove(); resolve(selectedFormat); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#exp_confirm').click(); } }); }); if (!format) return; setLoad(true); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); }; const rootNode = S.path[S.path.length - 1]; const startNodes =[{ id: rootNode.id || '', name: rootNode.name || L.btn_nav_home, lineage: [], retryCount: 0 }]; const itemTree = new Map(); try { await coreRecursiveEngine(startNodes, { signal: signal, onFolder: (folder, filesInFolder) => { itemTree.set(folder.id, [...filesInFolder]); if (typeof globalCache !== 'undefined' && !globalCache.has(folder.id)) { globalCache.set(folder.id, [...filesInFolder]); } indexParents(folder.id, folder.name, filesInFolder); }, onProgress: (st) => { updateLoadTxt(`${L.msg_exporting}\n${L.str_folders}: ${st.folders} | ${L.str_files}: ${st.files}`); } }); if (!isRunning || signal.aborted || myScanId !== S.scanId) return; updateLoadTxt(L.str_processing); await sleep(50); const rootName = startNodes[0].name; let outputLines =[]; const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); const sortItems = (items) => { return items.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1; return collator.compare(a.name, b.name); }); }; if (format === 'tree') { outputLines.push(rootName); const buildTree = (parentId, prefix) => { const children = itemTree.get(parentId); if (!children || children.length === 0) return; const sorted = sortItems(children); for (let i = 0; i < sorted.length; i++) { const child = sorted[i]; const isLast = (i === sorted.length - 1); const connector = isLast ? '└─ ' : '├─ '; outputLines.push(prefix + connector + child.name); if (child.kind === 'drive#folder') { const childPrefix = prefix + (isLast ? ' ' : '│ '); buildTree(child.id, childPrefix); } } }; buildTree(startNodes[0].id, ''); } else { const buildList = (parentId, currentPath) => { const children = itemTree.get(parentId); if (!children || children.length === 0) return; const sorted = sortItems(children); for (let i = 0; i < sorted.length; i++) { const child = sorted[i]; const fullPath = currentPath ? currentPath + '/' + child.name : child.name; outputLines.push(fullPath); if (child.kind === 'drive#folder') { buildList(child.id, fullPath); } } }; buildList(startNodes[0].id, rootName); } const outputText = outputLines.join('\r\n'); const blob = new Blob([outputText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const safeRootName = rootName.replace(/[\\/:*?"<>|]/g, '_'); const a = document.createElement('a'); a.href = url; a.download = `${safeRootName}_${format}.txt`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error}: ${e.message}`); } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; isGUISensitive = false; } } }; } UI.btnNewFolder.onclick = async () => { const name = await showPrompt(L.msg_newfolder_prompt, ''); if (!name) return; isGUISensitive = true; const cur = S.path[S.path.length - 1]; const cacheKey = cur.id || 'root'; try { await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: 'drive#folder', parent_id: cur.id || '', name: name }) }); if (cur.id) gmSet('pk_fmod_' + cur.id, new Date(getServerNow()).toISOString()); await sleep(300); S.cache.delete(cacheKey); if (typeof globalCache !== 'undefined') globalCache.delete(cacheKey); if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(cacheKey === 'root' ? '' : cacheKey); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (cacheKey !== 'root') { scannedFolderIds.delete(cacheKey); } if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); load(false, true); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { isGUISensitive = false; } }; UI.btnCopy.onclick = async () => { if (S.sel.size === 0) return; if (S.sel.size > 10000) { showToast(L.str_copying); await sleep(10); } const itemList = []; let count = 0; for (const id of S.sel) { const item = S.itemMap.get(id); if (item) itemList.push(item); count++; if (count % 10000 === 0) await sleep(0); } S.clipItems = itemList; S.clipType = 'copy'; const curId = S.path[S.path.length - 1].id || ''; S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId; setLoad(false); UI.btnPaste.disabled = false; showToast(L.msg_copy_done); }; UI.btnCut.onclick = async () => { if (S.sel.size === 0) return; const itemList = []; let count = 0; for (const id of S.sel) { const item = S.itemMap.get(id); if (!S.trashMode && isSystemItem(item)) { continue; } itemList.push(item); count++; if (count % 10000 === 0) await sleep(0); } if (itemList.length === 0) return; if (itemList.length > 10000) { showToast(L.str_moving); await sleep(10); } S.clipItems = itemList; S.clipType = 'move'; const curId = S.path[S.path.length - 1].id || ''; S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId; setLoad(false); UI.btnPaste.disabled = false; showToast(L.msg_cut_done); }; UI.btnPaste.onclick = async () => { if (!S.clipItems || S.clipItems.length === 0) { showToast(L.msg_paste_empty, 'error'); return; } if (S.movingIds && S.movingIds.size > 0) { showAlert(L.msg_op_blocked_moving); return; } const items = S.clipItems; const type = S.clipType; const srcId = S.clipSourceParentId; const destId = S.path[S.path.length - 1].id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; S.clipItems = []; S.clipType = ''; updateStat(); const targetFolderName = S.path[S.path.length - 1].name || "Target"; await executeFileTransfer(items, type, normalize(srcId), normalize(destId), targetFolderName); }; UI.btnRename.onclick = async () => { if (S.sel.size !== 1) return; const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (!item || (!S.trashMode && isSystemItem(item))) return; const newName = await showPrompt(L.msg_rename_prompt, item.name, L.modal_rename_title); if (!newName || newName === item.name) return; let progressTask = null; try { progressTask = FloatBarManager.create(L.str_renaming); await apiAction(`/${id}`, { name: newName }); item.name = newName; const nowIso = new Date(getServerNow()).toISOString(); item.modified_time = nowIso; if (item.kind === 'drive#folder') gmSet('pk_fmod_' + item.id, nowIso); const parentIdForFmod = item.parent_id || 'root'; gmSet('pk_fmod_' + parentIdForFmod, nowIso); const row = UI.in.querySelector(`.pk-row[data-id="${id}"]`); if (row && row.lastElementChild) { row.lastElementChild.textContent = fmtDate(nowIso); row.lastElementChild.style.transition = 'color 0.3s'; row.lastElementChild.style.color = 'var(--pk-pri)'; setTimeout(() => { if(row.lastElementChild) row.lastElementChild.style.color = ''; }, 2000); } const pid = item.parent_id || 'root'; if (typeof globalCache !== 'undefined') { if (globalCache.has(pid)) { const list = globalCache.get(pid); const target = list.find(f => f.id === id); if (target) target.name = newName; } globalDirtyFolders.add(pid === 'root' ? '' : pid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (S.lastGlobalResults && S.lastGlobalResults.length > 0) { const globalTarget = S.lastGlobalResults.find(f => f.id === id); if (globalTarget) globalTarget.name = newName; } if (S.analyzeResultItems) { const anaItem = S.analyzeResultItems.find(x => x.id === id); if (anaItem) anaItem.name = newName; } if (S.analyzeMap && S.analyzeMap.has(id)) { S.analyzeMap.get(id).name = newName; } if (S.dupMode) renderDupView(); else refresh(); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { if (progressTask) progressTask.destroy(); } }; UI.btnBulkRename.onclick = () => { if (S.sel.size < 2) return; if (S.movingIds && S.movingIds.size > 0) { const hasConflict = Array.from(S.sel).some(id => S.movingIds.has(id)); if (hasConflict) { showAlert(L.msg_op_blocked_analyzing); return; } } const getSmartDisplayHTML = (fullStr, hlStr) => { if (!hlStr || hlStr.indexOf('§§§MATCH_START§§§') === -1) { return esc(fullStr); } const parts = hlStr.split(/(§§§MATCH_START§§§.*?§§§MATCH_END§§§)/g); let result = ""; const PRE_CTX = 12; const SUF_CTX = 80; parts.forEach((part, index) => { if (part.startsWith('§§§MATCH_START§§§')) { const content = part.replace(/§§§MATCH_START§§§|§§§MATCH_END§§§/g, ''); result += `<span style="background:#fff2cc;color:#d93025;font-weight:bold;border-radius:2px;padding:0 2px;">${esc(content)}</span>`; } 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(` <div style="display:flex; flex-direction:column; gap:20px; padding:10px 0;"> <div> <h3 style="margin:0; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.modal_rename_multi_title}</h3> <div style="font-size:12px; color:#888; margin-top:4px;">${L.rn_stat.replace('{n}', S.sel.size).replace('{m}', '<span id="rn_stat_num">0</span>')}</div> </div> <div style="display:flex; gap:15px; align-items:center; flex-wrap:wrap;"> <label style="display:flex; align-items:center; cursor:pointer;"> <input type="radio" name="rn_mode" value="replace" checked style="accent-color:var(--pk-pri); transform:scale(1.2);"> <span style="margin-left:8px; font-weight:600; color:var(--pk-fg);">${L.label_replace}</span> </label> <label style="display:flex; align-items:center; cursor:pointer;"> <input type="radio" name="rn_mode" value="pattern" style="accent-color:var(--pk-pri); transform:scale(1.2);"> <span style="margin-left:8px; font-weight:600; color:var(--pk-fg);">${L.lbl_rn_mode_series}</span> </label> <label style="display:flex; align-items:center; cursor:pointer;"> <input type="radio" name="rn_mode" value="format" style="accent-color:var(--pk-pri); transform:scale(1.2);"> <span style="margin-left:8px; font-weight:600; color:var(--pk-fg);">${L.lbl_rn_mode_format}</span> </label> <label style="display:flex; align-items:center; cursor:pointer;"> <input type="radio" name="rn_mode" value="jav" style="accent-color:var(--pk-pri); transform:scale(1.2);"> <span style="margin-left:8px; font-weight:600; color:var(--pk-fg);">${L.label_jav}</span> </label> <label style="display:flex; align-items:center; cursor:pointer;"> <input type="radio" name="rn_mode" value="ad_remove" style="accent-color:var(--pk-pri); transform:scale(1.2);"> <span style="margin-left:8px; font-weight:600; color:var(--pk-fg);">${L.lbl_rn_mode_ad}</span> </label> <label style="display:flex; align-items:center; cursor:pointer;"> <input type="radio" name="rn_mode" id="rb_ext_fix" value="ext_fix" style="accent-color:var(--pk-pri); transform:scale(1.2);"> <span style="margin-left:8px; font-weight:600; color:var(--pk-fg);">${L.lbl_rn_mode_ext}</span> </label> </div> <div id="rn_inputs_area" style="min-height:85px; display:flex; flex-direction:column; justify-content:center;"> <div id="group_replace" style="position:relative; display:flex; flex-direction:column; gap:15px;"> <div style="position:absolute; top:-32px; right:0; display:flex; justify-content:flex-end; gap:15px; padding-right:5px;"> <label style="cursor:pointer; font-size:12px; color:var(--pk-fg); display:flex; align-items:center; gap:4px; user-select:none;"> <input type="checkbox" id="rn_include_ext" style="accent-color:var(--pk-pri);"> ${L.label_include_ext} </label> <label style="cursor:pointer; font-size:12px; color:var(--pk-fg); display:flex; align-items:center; gap:4px; user-select:none;"> <input type="checkbox" id="rn_case_sense" style="accent-color:var(--pk-pri);"> ${L.label_replace_note} </label> <label style="cursor:pointer; font-size:12px; color:var(--pk-fg); display:flex; align-items:center; gap:4px; user-select:none;"> <input type="checkbox" id="rn_regex" style="accent-color:var(--pk-pri);"> ${L.label_regex} </label> </div> <div style="display:flex; gap:15px;"> <div style="position:relative; flex:1;"> <input type="text" id="rn_find" placeholder="${L.placeholder_find}" autocomplete="off" style="${inputStyle}"> <div style="${labelStyle}">${L.label_replace_find}</div> </div> <div style="position:relative; flex:1;"> <input type="text" id="rn_rep" placeholder="${L.placeholder_replace}" autocomplete="off" style="${inputStyle}"> <div style="${labelStyle}">${L.label_replace_to}</div> </div> </div> </div> <div id="group_format" style="display:none; gap:15px; align-items: flex-start;"> <div class="pk-custom-select" id="cs_rn_case" style="flex:1;"> <div class="pk-select-label">${L.lbl_rn_case_convert}</div> <div class="pk-select-trigger"><span id="txt_rn_case">${L.btn_ok}</span>${CONF.crumbIcons.down}</div> <div class="pk-select-menu pk-scroll"> <div class="pk-select-item act" data-val="">${L.btn_ok}</div> <div class="pk-select-item" data-val="lower">${L.opt_rn_lower}</div> <div class="pk-select-item" data-val="upper">${L.opt_rn_upper}</div> <div class="pk-select-item" data-val="title">${L.opt_rn_title}</div> </div> </div> <div class="pk-custom-select" id="cs_rn_width" style="flex:1;"> <div class="pk-select-label">${L.lbl_rn_width_convert}</div> <div class="pk-select-trigger"><span id="txt_rn_width">${L.btn_ok}</span>${CONF.crumbIcons.down}</div> <div class="pk-select-menu pk-scroll"> <div class="pk-select-item act" data-val="">${L.btn_ok}</div> <div class="pk-select-item" data-val="half">${L.opt_rn_width_half}</div> <div class="pk-select-item" data-val="full">${L.opt_rn_width_full}</div> </div> </div> </div> <div id="group_pattern" style="position:relative; display:none;"> <input type="text" id="rn_pattern" value="Video {n}" placeholder="Video {n}" style="${inputStyle}"> <div style="${labelStyle}">${L.lbl_rn_pattern}</div> </div> <div id="group_jav" style="display:none; padding:15px; background:var(--pk-hl); border:2px dashed var(--pk-bd); border-radius:8px; color:var(--pk-fg); font-size:13px; text-align:center; user-select:none;"> ${L.tip_jav_mode_desc} </div> <div id="group_ad_remove" style="display:none; padding:15px; background:var(--pk-hl); border:2px dashed var(--pk-bd); border-radius:8px; color:var(--pk-fg); font-size:13px; text-align:center; user-select:none;"> ${L.tip_ad_remove_desc} </div> <div id="group_ext_fix" style="display:none; padding:15px; background:var(--pk-hl); border:2px dashed var(--pk-bd); border-radius:8px; color:var(--pk-fg); font-size:13px; text-align:center; user-select:none;"> ${L.tip_ext_fix_desc} </div> </div> <div style="position:relative; flex:1; min-height:300px;"> <div style="position:absolute; inset:0; border:2px solid var(--pk-bd); border-radius:8px; display:flex; flex-direction:column; overflow:hidden;"> <div style="display:flex; padding:12px 20px 12px 0; background:var(--pk-bg); border-bottom:1px dashed var(--pk-bd); color:#888; font-size:12px; font-weight:bold;"> <div id="rn_cb_wrapper" style="width:50px; display:flex; justify-content:center; visibility: hidden;"> <input type="checkbox" id="rn_cb_all" checked style="accent-color:var(--pk-pri); cursor:pointer;"> </div> <div style="width:36px; display:flex; align-items:center; justify-content:flex-start;">${L.col_type}</div> <div style="flex:1;">${L.col_old}</div> <div style="width:30px;"></div> <div style="flex:1;">${L.col_new}</div> </div> <div id="pk-rn-vp" style="flex:1; overflow-y:auto; padding:0;"> <div id="pk-rn-in" style="position:relative;"> <div style="padding:40px; text-align:center; color:#888;">${L.rn_tip_wait}</div> </div> </div> </div> <div style="${labelStyle}">${L.lbl_rn_preview_title}</div> </div> <div class="pk-modal-act" style="display:grid; grid-template-columns: 1fr 1fr; gap:15px; margin-top:5px;"> <button class="pk-btn" id="rn_cancel" style="height:44px; justify-content:center; background:transparent; font-weight:600; font-size:15px; border-radius:8px;">${L.btn_cancel}</button> <button class="pk-btn pri" id="rn_apply" disabled style="height:44px; justify-content:center; border-radius:8px; font-weight:bold; font-size:15px; border:none; background:var(--pk-pri); color:#fff;">${L.btn_ok}</button> </div> </div> `); 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 = `<div style="padding:40px; text-align:center; color:#888;">${initialTip}</div>`; 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 = `<div class="pk-hist-hd"><span>${L.title_search_hist}</span><span class="pk-hist-clear-btn" id="pk-hist-del" style="cursor:pointer; opacity:0.8;">${L.btn_clear_hist}</span></div>`; list.forEach(txt => { const displayTxt = txt === "" ? "(Empty)" : (txt.replace(/\s/g, '') === "" ? `"${txt}"` : txt); html += `<div class="pk-select-item" data-raw="${esc(txt)}" style="display:flex; align-items:center; gap:10px; padding:8px 12px;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="flex-shrink:0; opacity:0.5;"> <circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/> </svg> <span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1;">${esc(displayTxt)}</span> </div>`; }); 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 = `<span style="color:var(--pk-pri);font-weight:bold;">${esc(newName)}</span>`; counter++; } else { item.new = item.old; item.newNameHTML = `<span style="color:#aaa;text-decoration:line-through;">${esc(item.old)}</span>`; } }); }; 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 = `<span style="color:var(--pk-pri); font-weight:600;">${validCount}</span><span style="margin:0 2px;">/</span>${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 = `<img src="${it.thumbnail_link}" style="width:24px;height:24px;object-fit:cover;border-radius:4px;flex-shrink:0;" onerror="this.style.display='none';this.nextElementSibling.style.display='inline-flex';">`; const secondFallback = it.icon_link ? `<img src="${it.icon_link}" style="width:24px;height:24px;object-fit:contain;flex-shrink:0;" onerror="this.style.display='none';this.nextElementSibling.style.display='inline-flex';"><span style="display:none;align-items:center;">${fallbackSvg}</span>` : fallbackSvg; iconHtml += `<span style="display:none;align-items:center;justify-content:center;">${secondFallback}</span>`; } else { const iconSrc = it.icon_link; iconHtml = iconSrc ? `<img src="${iconSrc}" style="width:24px;height:24px;object-fit:contain;flex-shrink:0;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display='inline-flex';"><span style="display:none;align-items:center;flex-shrink:0;">${fallbackSvg}</span>` : fallbackSvg; } } row.innerHTML = ` <div style="width:50px; display:flex; justify-content:center; align-items:center; cursor:pointer;" class="rn-row-cb-area"> <input type="checkbox" class="rn-item-cb" ${isChecked ? 'checked' : ''} style="accent-color:var(--pk-pri); cursor:pointer;"> </div> <div style="width:36px; display:flex; align-items:center; justify-content:flex-start;" ${thumbAttr}>${iconHtml}</div> <div style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" data-pk-tip="${esc(c.old)}" ${thumbAttr}>${finalDisplayHTML}</div> <div style="width:30px; text-align:center; color:#ccc;">➜</div> <div style="flex:1; display:flex; align-items:center; overflow:hidden;"> <div style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" data-pk-tip="${esc(c.new)}"> ${c.newNameHTML} </div> ${c.conflict ? `<div style="color:#d93025; font-size:10px; margin-left:6px; flex-shrink:0; background:rgba(217,48,37,0.1); padding:0 4px; border-radius:4px; white-space:nowrap;">${L.str_name_conflict}</div>` : ''} </div> `; 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 = `<div style="padding:40px; text-align:center; color:#d93025;">❌ ${L.err_worker}: ${esc(error)}</div>`; 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 = `<span style="color:var(--pk-pri);font-weight:bold;">${newH}</span>`; } return { id: c.id, old: c.old, new: c.new, hl_old: c.hl_old, newNameHTML: newH, conflict: c.conflict }; }); recalcPatternNames(); const cbWrapper = m.querySelector('#rn_cb_wrapper'); const cbAll = m.querySelector('#rn_cb_all'); if (rnDisplay.length === 0) { rnIn.innerHTML = `<div style="padding:40px; text-align:center; color:#888;">${L.rn_tip_none}</div>`; 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 = `<div style="padding:40px; text-align:center; color:#888;">${L.rn_tip_wait}</div>`; 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 = `<div style="padding:40px; text-align:center; color:var(--pk-pri); display:flex; flex-direction:column; align-items:center; gap:10px;"> <div class="pk-spinner"></div> <div>${L.str_calc_changes}</div> </div>`; btnApply.disabled = true; txtStatNum.innerText = "..."; plannedChanges = []; rnDisplay = []; previewSelectedIds = new Set(); const cbAll = m.querySelector('#rn_cb_all'); if(cbAll) { cbAll.checked = false; cbAll.indeterminate = false; } const selectedIds = Array.from(S.sel); const items = S.display.filter(i => !i.isHeader); if (false) { const targetIds =[]; for (let i = 0; i < items.length; i++) { const item = items[i]; const id = item.id; if (!S.sel.has(id)) continue; if (isSystemItem(item)) continue; const isFolder = item.kind === 'drive#folder'; const mime = (item.mime_type || '').toLowerCase(); const duration = (item.params && item.params.duration) || 0; const isVideo = !isFolder && (mime.startsWith('video/') || duration > 0); if ((isFolder || isVideo) && extractKeyword(item.name)) { targetIds.push(id); } } if (targetIds.length === 0) { rnIn.innerHTML = `<div style="padding:40px; text-align:center; color:#888;">${L.rn_tip_none}</div>`; 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 = `<span style="color:#aaa;">${L.str_jav_no_match}</span>`; } else { d.newNameHTML = `<span style="color:var(--pk-pri);font-weight:bold;">${esc(cached.new)}</span>`; } if (cached.hlOld) { d.hl_old = cached.hlOld; } } else { d.newNameHTML = `<span style="color:#888; display:flex; align-items:center; gap:6px; font-style:italic;"><svg width="14" height="14" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="animation:pk-spin 1s linear infinite"><path d="M12 2A10 10 0 1 0 22 12A10 10 0 0 0 12 2Zm0 18a8 8 0 1 1 8-8A8 8 0 0 1 12 20Z" opacity=".25" fill="currentColor"/><path d="M12 4a8 8 0 0 1 7.89 6.7 1 1 0 0 0 1.95-.48A10 10 0 0 0 12 2Z" fill="currentColor"/></svg> ${L.str_jav_querying}</span>`; } }); 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 = ` <div style="width:80px; height:4px; background:#eee; border-radius:2px; overflow:hidden; flex-shrink:0; display:inline-block; align-self:center; transform:translateY(2px);"> <div style="width:${(javState.completed / javState.total) * 100}%; height:100%; background:var(--pk-pri); transition:width 0.2s;"></div> </div> <span style="display:inline-flex; align-items:baseline; gap:2px; color:inherit; vertical-align:baseline;"> <span style="color:var(--pk-pri); font-weight:600;">${javState.completed}</span> <span style="opacity:0.6;">/</span> <span>${javState.total}</span> </span>`; }; 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 = `<b style="color:var(--pk-pri)">${validCount}</b> / ${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 = `<span style="color:#aaa;">${L.str_jav_no_match}</span>`; 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 = `<span style="color:var(--pk-pri);font-weight:bold;">${esc(newName)}</span>`; } 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 = `<span style="color:var(--pk-pri); font-weight:600;">${validCount}</span><span style="margin:0 2px;">/</span>${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 = `<div style="padding:40px; text-align:center; color:#d93025;">❌ Worker Error</div>`; previewWorker.terminate(); URL.revokeObjectURL(workerUrl); }; previewWorker.postMessage({ selectedIds, items: itemsCopy, mode, pattern, findStr, repStr, useRegex, STR_INVALID_REGEX: L.err_invalid_regex, STR_EMPTY_FILENAME: L.str_empty_filename, validExtsList: VALID_EXTS_LIST, caseMode, widthMode, useCaseSense, useIncludeExt }); }; m.querySelector('#rn_cancel').onclick = () => m.remove(); [inpPattern, inpFind, inpRep, chkRegex, chkCase, chkIncludeExt].forEach(el => el.onchange = generatePreview); [inpPattern, inpFind, inpRep].forEach(el => el.onkeydown = (e) => { if(e.key==='Enter') generatePreview(); }); let selectedCase = ""; let selectedWidth = ""; const bindRNSelect = (id, onSelect) => { const container = m.querySelector(`#${id}`); if (!container) return; const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('span'); const items = container.querySelectorAll('.pk-select-item'); trigger.onclick = (e) => { e.stopPropagation(); const allMenus = m.querySelectorAll('.pk-select-menu'); const isOpen = menu.style.display === 'block'; allMenus.forEach(om => om.style.display = 'none'); menu.style.display = isOpen ? 'none' : 'block'; }; items.forEach(item => { item.onclick = (e) => { e.stopPropagation(); items.forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val); generatePreview(); }; }); }; bindRNSelect('cs_rn_case', (val) => { selectedCase = val; }); bindRNSelect('cs_rn_width', (val) => { selectedWidth = val; }); const closeDropdowns = () => m.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none'); setTimeout(() => document.addEventListener('click', closeDropdowns), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeDropdowns); _orgRemove(); }; const bindHeaderCheckbox = () => { const cbAll = m.querySelector('#rn_cb_all'); if(cbAll) { cbAll.onclick = (e) => { const isChecked = e.target.checked; if(isChecked) { plannedChanges.forEach(c => previewSelectedIds.add(c.id)); } else { previewSelectedIds.clear(); } recalcPatternNames(); renderRNVisible(); updateHeaderCheckbox(); }; } }; setTimeout(bindHeaderCheckbox, 0); btnApply.onclick = async () => { const validChanges = rnDisplay.filter(c => previewSelectedIds.has(c.id) && c.new !== c.old); const skippedCount = plannedChanges.length - validChanges.length; if (validChanges.length === 0) { showAlert(L.msg_rn_all_skipped); return; } let confirmMsg = L.rn_warn_confirm.replace('{n}', validChanges.length); if (!await showConfirm(confirmMsg)) return; const progressTask = FloatBarManager.create(L.str_renaming); m.remove(); let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; updateLoadTxt(L.str_stopping); }; const USER_LIMIT = parseInt(localStorage.getItem('pk_user_limit') || "200"); let currentLimit = 2; const MIN_LIMIT = 2; const queue = [...validChanges]; const activeTasks = new Set(); const stats = { success: 0, fail: 0, lastUiTime: 0 }; const total = validChanges.length; const runRenameTask = async (task) => { try { await apiAction(`/${task.id}`, { name: task.new }); stats.success++; const item = S.itemMap.get(task.id); if (item) { item.name = task.new; const nowIso = new Date(getServerNow()).toISOString(); item.modified_time = nowIso; if (item.kind === 'drive#folder') gmSet('pk_fmod_' + item.id, nowIso); const parentIdForFmod = item.parent_id || 'root'; gmSet('pk_fmod_' + parentIdForFmod, nowIso); const row = UI.in.querySelector(`.pk-row[data-id="${task.id}"]`); if (row && row.lastElementChild) row.lastElementChild.textContent = fmtDate(nowIso); if (S.analyzeResultItems) { const anaItem = S.analyzeResultItems.find(x => x.id === task.id); if (anaItem) anaItem.name = task.new; } if (S.analyzeMap && S.analyzeMap.has(task.id)) { S.analyzeMap.get(task.id).name = task.new; } if (S.lastGlobalResults && S.lastGlobalResults.length > 0) { const gItem = S.lastGlobalResults.find(x => x.id === task.id); if (gItem) gItem.name = task.new; } if (typeof globalDirtyFolders !== 'undefined') { const pid = item.parent_id || ''; globalDirtyFolders.add(pid); } } if (currentLimit < USER_LIMIT) currentLimit++; } catch (e) { if (!isRunning) return; if ((e.message && e.message.includes('429')) || (e.message && e.message.includes('Network'))) { currentLimit = Math.max(MIN_LIMIT, Math.floor(currentLimit / 2)); task.retryCount = (task.retryCount || 0) + 1; await sleep(Math.min(task.retryCount * 1000, 10000)); queue.push(task); } else { stats.fail++; } } }; try { updateLoadTxt(L.str_init_rename); while ((queue.length > 0 || activeTasks.size > 0) && isRunning) { while (queue.length > 0 && activeTasks.size < currentLimit && isRunning) { const task = queue.shift(); const p = runRenameTask(task).finally(() => activeTasks.delete(p)); activeTasks.add(p); } if (activeTasks.size > 0) await Promise.race(activeTasks); const now = Date.now(); if (now - stats.lastUiTime > 150) { progressTask.update(`${L.str_renaming} ${stats.success + stats.fail}/${total} | ${L.str_speed}: ${activeTasks.size} | ${L.str_success}: ${stats.success}`); stats.lastUiTime = now; } } if (!isRunning) throw new Error('StoppedByUser'); updateLoadTxt(L.str_refreshing_cache); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); if (S.dupMode) renderDupView(); else refresh(); let msgParts = []; if (stats.success > 0) msgParts.push(L.msg_bulkrename_done.replace('{n}', stats.success)); if (stats.fail > 0) msgParts.push(L.msg_rn_fail_count.replace('{n}', stats.fail)); const finalMsg = msgParts.join('\n'); await sleep(300); showAlert(finalMsg); } catch (e) { if (e.message !== 'StoppedByUser') showAlert(`${L.str_error_crit}: ${e.message}`); } finally { if (progressTask) progressTask.destroy(); setLoad(false); } }; }; UI.btnPrune.onclick = async () => { isGUISensitive = true; ensureItemMap(); const selectedFolders = Array.from(S.sel) .map(id => S.itemMap.get(id)) .filter(i => i && i.kind === 'drive#folder'); if (selectedFolders.length === 0) return; const hasConflict = selectedFolders.some(f => isPathBusy(f.id)); if (hasConflict) { showAlert(L.msg_prune_blocked_moving); return; } if (!await showConfirm(L.msg_prune_confirm)) return; setLoad(true); S.scanning = true; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); setLoad(false); isGUISensitive = false; }; const folderMap = new Map(); updateLoadTxt(L.str_scanning_dir); try { await coreRecursiveEngine(selectedFolders, { signal: signal, onFolder: (folder, filesInFolder, subFolders) => { const hasFiles = filesInFolder.some(f => f.kind !== 'drive#folder'); folderMap.set(folder.id, { id: folder.id, name: folder.name, parent_id: folder.parent_id, depth: folder.depth || 0, hasFiles: hasFiles, subFolderIds: subFolders.map(s => s.id) }); }, onProgress: (st) => { const folderText = `${L.str_scanning_dir} ${st.folders} ${L.unit_folders}`; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo); } }); if (!S.scanning || signal.aborted || myScanId !== S.scanId) return; updateLoadTxt(L.str_analyzing); await sleep(50); const allScanned = Array.from(folderMap.values()).sort((a, b) => b.depth - a.depth); const toDeleteList = []; const toDeleteIds = new Set(); for (let i = 0; i < allScanned.length; i++) { if (!S.scanning) return; const folder = allScanned[i]; const isSystemProtected = isSystemItem({ ...folder, kind: 'drive#folder' }); if (isSystemProtected) continue; if (!folder.hasFiles) { const allSubsWillBeDeleted = folder.subFolderIds.every(subId => toDeleteIds.has(subId)); if (allSubsWillBeDeleted) { toDeleteIds.add(folder.id); toDeleteList.push(folder); } } } if (toDeleteList.length === 0) { setLoad(false); showAlert(L.msg_prune_none); } else { setLoad(false); const cacheHitCount = Array.from(folderMap.keys()).filter(id => globalCache.has(id)).length; let confirmMsg = L.msg_prune_found.replace('{n}', toDeleteList.length); if (await showConfirm(confirmMsg)) { const allIds = toDeleteList.map(f => f.id); await executeBatchDelete(allIds, { silent: true, forceRefresh: false }); if (myScanId === S.scanId) { const deletedSet = new Set(allIds); if (S.lastGlobalResults && S.lastGlobalResults.length > 0) { S.lastGlobalResults = S.lastGlobalResults.filter(x => !deletedSet.has(x.id)); } if (S.analyzeMode && S.analyzeResultItems) { S.analyzeResultItems = S.analyzeResultItems.filter(x => !deletedSet.has(x.id)); } updateLoadTxt(L.str_refreshing); const affectedParentIds = new Set(); affectedParentIds.add(S.path[S.path.length - 1].id || 'root'); toDeleteList.forEach(folder => { if (folder.parent_id) affectedParentIds.add(folder.parent_id); else affectedParentIds.add('root'); }); affectedParentIds.forEach(pid => { if (typeof globalCache !== 'undefined') globalCache.delete(pid); S.cache.delete(pid); }); await load(false, true); showToast(L.str_cleanup_done); } } } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; isGUISensitive = false; if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); } } }; if (UI.btnUpPause) UI.btnUpPause.onclick = () => { const ids = Array.from(S.sel); ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task && S.upMng) S.upMng.pause(task, true); }); if (S.uploadMode) { refresh(); } }; if (UI.btnUpStart) UI.btnUpStart.onclick = () => { const ids = Array.from(S.sel); ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task && S.upMng) S.upMng.resume(task, true); }); if (S.uploadMode) { refresh(); } }; if (UI.btnUpDel) UI.btnUpDel.onclick = () => { if (S.sel.size === 0) return; const count = S.sel.size; const html = ` <div style="padding: 5px 0 0 0;"> <h3 style="margin:0 0 25px 0; font-size:18px; font-weight:700; color:var(--pk-fg); border:none; line-height:1.4;"> ${L.title_del_task_confirm_fmt.replace('{n}', count)} </h3> <label style="display:flex; align-items:center; cursor:pointer; user-select:none; margin-bottom:35px; font-size:14px; color:var(--pk-fg);"> <input type="checkbox" id="del_up_files" style="width:18px; height:18px; margin-right:10px; accent-color:var(--pk-pri); cursor:pointer;"> <span style="opacity:0.9;">${L.lbl_del_cloud_files_too}</span> </label> <div class="pk-modal-act" style="display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="del_up_cancel" style="height:36px; padding:0 24px; border-radius:6px; background:transparent; border:1px solid var(--pk-bd); font-weight:500; color:var(--pk-fg);">${L.btn_cancel}</button> <button class="pk-btn pri" id="del_up_confirm" style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); color:#fff; font-weight:600; border:none; box-shadow: 0 2px 6px rgba(0,0,0,0.2);">${L.btn_del}</button> </div> </div> `; const m = showModal(html); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => m.remove(); m.querySelector('#del_up_cancel').onclick = close; const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; m.querySelector('#del_up_confirm').onclick = async () => { const isDeleteFile = m.querySelector('#del_up_files').checked; m.remove(); const ids = Array.from(S.sel); const filesToDelete = []; ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task) { task._deleted = true; task._deleteFileIntent = isDeleteFile; if (isDeleteFile && task.file_id) { filesToDelete.push(task.file_id); } if (S.upMng) S.upMng.pause(task, true); } }); const idSet = S.sel; S.uploadTasks = S.uploadTasks.filter(t => !idSet.has(t.id)); S.clearSelection(); load(false, true); if (filesToDelete.length > 0) { try { await executeBatchDelete(filesToDelete, { silent: false, hardDelete: false, forceRefresh: false }); } catch(e) { console.error("Failed to delete uploaded files:", e); showToast(L.msg_file_del_failed + e.message, "error"); } } else { showToast(L.msg_task_del_success_fmt.replace('{n}', ids.length)); } }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#del_up_confirm').click(); } }); }; if (UI.btnUpClearAll) UI.btnUpClearAll.onclick = () => { if (S.uploadTasks.length === 0) return; const count = S.uploadTasks.length; const html = ` <div style="padding: 5px 0 0 0;"> <h3 style="margin:0 0 25px 0; font-size:18px; font-weight:700; color:var(--pk-fg); border:none; line-height:1.4;"> ${L.title_clear_task_confirm} </h3> <label style="display:flex; align-items:center; cursor:pointer; user-select:none; margin-bottom:35px; font-size:14px; color:var(--pk-fg);"> <input type="checkbox" id="clear_all_up_files" style="width:18px; height:18px; margin-right:10px; accent-color:var(--pk-pri); cursor:pointer;"> <span style="opacity:0.9;">${L.lbl_del_cloud_files_too}</span> </label> <div class="pk-modal-act" style="display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="clear_up_cancel" style="height:36px; padding:0 24px; border-radius:6px; background:transparent; border:1px solid var(--pk-bd); font-weight:500; color:var(--pk-fg);">${L.btn_cancel}</button> <button class="pk-btn pri" id="clear_up_confirm" style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); color:#fff; font-weight:600; border:none; box-shadow: 0 2px 6px rgba(0,0,0,0.2);">${L.btn_del}</button> </div> </div> `; const m = showModal(html); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => m.remove(); m.querySelector('#clear_up_cancel').onclick = close; const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; m.querySelector('#clear_up_confirm').onclick = async () => { const isDeleteFile = m.querySelector('#clear_all_up_files').checked; m.remove(); const filesToDelete =[]; S.uploadTasks.forEach(task => { task._deleted = true; task._deleteFileIntent = isDeleteFile; if (S.upMng) S.upMng.pause(task, true); if (isDeleteFile && task.file_id) { filesToDelete.push(task.file_id); } }); S.uploadTasks = []; S.clearSelection(); load(false, true); if (filesToDelete.length > 0) { try { await executeBatchDelete(filesToDelete, { silent: false, hardDelete: false, forceRefresh: false }); } catch(e) { console.error("Clear All: Cloud deletion failed", e); } } else { showToast(L.msg_task_clear_success_fmt.replace('{n}', count)); } }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#clear_up_confirm').click(); } }); }; UI.btnExt.onclick = async () => { const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.loading_detail); const targetApiId = ((S.offlineMode && item.kind === 'drive#task') || (S.uploadMode && item.file_id)) ? item.file_id : item.id; let detail = item; try { if (!targetApiId) throw new Error("File ID not ready"); detail = await apiGet(targetApiId); } catch (e) { console.warn("Fetch detail failed, using cached info"); if (!detail.web_content_link && !detail.medias) { setLoad(false); showToast(L.msg_video_fail, 'error'); return; } } setLoad(false); const qualities = generateQualityList(detail); const bestSource = getBestSource(detail); let selectedUrl = bestSource.src; let selectedResName = bestSource.name; const savedPlayer = gmGet('pk_ext_player', 'potplayer'); let selectedPlayer = (savedPlayer === 'potplayer') ? 'potplayer' : 'other'; const initialBtnTxt = (selectedPlayer === 'potplayer') ? L.btn_start_play : L.btn_copy_link; const m = showModal(` <h3 style="border:none; margin-bottom:24px; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.btn_ext}</h3> <div style="display:flex; flex-direction:column; gap:25px; margin-bottom:10px;"> <div class="pk-custom-select" id="cs_res"> <div class="pk-select-label">${L.lbl_resolution}</div> <div class="pk-select-trigger"> <span id="txt_res">${selectedResName}</span> <div style="display:flex; color:#999;">${CONF.crumbIcons.down}</div> </div> <div class="pk-select-menu pk-scroll"> ${qualities.map(q => `<div class="pk-select-item ${q.url === selectedUrl ? 'act' : ''}" data-val="${q.url}">${q.name}</div>`).join('')} </div> </div> <div class="pk-custom-select" id="cs_player"> <div class="pk-select-label">${L.lbl_player}</div> <div class="pk-select-trigger"> <span id="txt_player">${selectedPlayer === 'potplayer' ? 'PotPlayer' : L.opt_player_other}</span> <div style="display:flex; color:#999;">${CONF.crumbIcons.down}</div> </div> <div class="pk-select-menu"> <div class="pk-select-item ${selectedPlayer === 'potplayer' ? 'act' : ''}" data-val="potplayer">PotPlayer</div> <div class="pk-select-item ${selectedPlayer === 'other' ? 'act' : ''}" data-val="other">${L.opt_player_other}</div> </div> </div> </div> <div class="pk-modal-act" style="margin-top:20px; display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="ext_cancel" style="height:40px; min-width:86px; border-radius:8px; justify-content:center; background:transparent; font-weight:500;">${L.btn_cancel}</button> <button class="pk-btn pri" id="ext_run" style="height:40px; min-width:120px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold; justify-content:center; border:none; font-size:15px;">${initialBtnTxt}</button> </div> `); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '480px', padding: '30px', height: 'auto', minHeight: 'auto', overflow: 'visible' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const bindSelect = (id, onSelect) => { const container = m.querySelector(`#${id}`); const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('span'); trigger.onclick = (e) => { e.stopPropagation(); const allMenus = m.querySelectorAll('.pk-select-menu'); const isCurrentlyOpen = menu.style.display === 'block'; allMenus.forEach(om => om.style.display = 'none'); menu.style.display = isCurrentlyOpen ? 'none' : 'block'; }; container.querySelectorAll('.pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); container.querySelectorAll('.pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val); }; }); }; const runBtn = m.querySelector('#ext_run'); bindSelect('cs_res', (val) => { selectedUrl = val; }); bindSelect('cs_player', (val) => { selectedPlayer = val; runBtn.textContent = (val === 'potplayer') ? L.btn_start_play : L.btn_copy_link; }); const closeAllMenus = () => m.querySelectorAll('.pk-select-menu').forEach(om => om.style.display = 'none'); setTimeout(() => document.addEventListener('click', closeAllMenus), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeAllMenus); _orgRemove(); }; m.querySelector('#ext_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); runBtn.click(); } }); runBtn.onclick = () => { gmSet('pk_ext_player', selectedPlayer); let cleanUrl = selectedUrl.replace('&ext=.m3u8', ''); if (cleanUrl.includes('ts_downloader') && cleanUrl.includes('url=')) { const urlParam = new URL(cleanUrl).searchParams.get('url'); if (urlParam) cleanUrl = decodeURIComponent(urlParam); } if (selectedPlayer === 'potplayer') { m.remove(); const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanUrl} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; } else { GM_setClipboard(cleanUrl); runBtn.textContent = L.msg_copy_success; runBtn.style.background = "#52c41a"; runBtn.disabled = true; setTimeout(() => m.remove(), 1000); } }; }; UI.win.querySelector('#pk-down').onclick = async () => { const hasConflict = Array.from(S.sel).some(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') return isPathBusy(item.id); return S.movingIds.has(id); }); if (hasConflict) { showAlert(L.msg_resource_locked_download); return; } setLoad(true); isGUISensitive = true; const abortCtrl = new AbortController(); const { signal } = abortCtrl; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; abortCtrl.abort(); updateLoadTxt(L.str_stopping); }; const allFiles = []; const rootNodes = []; const HYDRATE_LIMIT = 20; S.sel.forEach(id => { const item = S.itemMap.get(id); if (item) { if (item.kind === 'drive#folder') rootNodes.push({...item, lineage:[], retryCount: 0}); else allFiles.push(item); } }); let progressTask = null; const fExtStr = gmGet('pk_dl_filter_ext', '').toLowerCase(); const fNameStr = gmGet('pk_dl_filter_name', '').toLowerCase(); const fExts = fExtStr.split(/[,,]/).map(s => s.trim().replace(/^\./, '')).filter(Boolean); const fNames = fNameStr.split(/[,,]/).map(s => s.trim()).filter(Boolean); try { await coreRecursiveEngine(rootNodes, { signal, onFile: (f) => { const lowName = f.name.toLowerCase(); const ext = lowName.split('.').pop(); const isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n)); if (!isBlocked) allFiles.push(f); }, onProgress: (st) => { updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`); } }); if (!isRunning) throw new Error('StoppedByUser'); if (allFiles.length === 0) { setLoad(false); showToast(L.msg_batch_no_files); return; } setLoad(false); if (allFiles.length > 10) { if (!await showConfirm(L.msg_down_confirm_total.replace('{n}', allFiles.length))) return; } progressTask = FloatBarManager.create(L.msg_batch_hydrating); const readyFiles = []; const hydrateQueue = [...allFiles]; const activeTasks = new Set(); while ((hydrateQueue.length > 0 || activeTasks.size > 0) && isRunning) { while (hydrateQueue.length > 0 && activeTasks.size < HYDRATE_LIMIT && isRunning) { const file = hydrateQueue.pop(); const p = (async () => { try { const detail = (file.web_content_link) ? file : await apiGet(file.id); if (detail?.web_content_link) readyFiles.push(detail); } catch (e) {} })().finally(() => activeTasks.delete(p)); activeTasks.add(p); } if (activeTasks.size > 0) await Promise.race(activeTasks); if (progressTask) progressTask.update(`${L.msg_batch_hydrating} ${readyFiles.length} / ${allFiles.length}`); } for (let i = 0; i < readyFiles.length; i++) { if (!isRunning) break; if (progressTask) progressTask.update(`${L.msg_down_progress} ${i + 1} / ${readyFiles.length}`); const link = document.createElement('a'); link.href = readyFiles[i].web_content_link; link.setAttribute('download', readyFiles[i].name); link.style.display = 'none'; document.body.appendChild(link); link.click(); setTimeout(() => { if (link.parentNode) link.remove(); }, 10000); if (i < readyFiles.length - 1) await sleep(2000); } if (isRunning && readyFiles.length > 0) { showToast(L.msg_down_success.replace('{n}', readyFiles.length)); } } catch (e) { if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); isGUISensitive = false; if (progressTask) progressTask.destroy(); } }; UI.win.querySelector('#pk-aria2').onclick = async () => { const hasConflict = Array.from(S.sel).some(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') return isPathBusy(item.id); return S.movingIds.has(id); }); if (hasConflict) { showAlert(L.msg_resource_locked_aria2); return; } let ariaUrl = gmGet('pk_aria2_url', ''); let ariaToken = gmGet('pk_aria2_token', ''); if (!ariaUrl) { const result = await new Promise((resolve) => { const m = showModal(` <h3 style="border:none; margin:0 0 12px 0; font-size:18px; font-weight:700; color:var(--pk-fg); display:flex; align-items:center; gap:10px; line-height:24px;"> <span style="width:22px; height:22px; display:flex; align-items:center; justify-content:center; flex-shrink:0;"> ${CONF.icons.aria2.replace('width="16"', 'width="22"').replace('height="16"', 'width="22"')} </span> <span>${L.btn_aria2} - ${L.btn_settings}</span> </h3> <div style="font-size:13px; color:#888; margin-bottom:28px; line-height:1.5;">${L.msg_aria2_not_set}</div> <div style="display:flex; flex-direction:column; gap:25px;"> <div style="position:relative; transform: translateZ(0);"> <div style="position:relative;"> <input type="text" id="pop_aria_url" value="${ariaUrl || 'http://localhost:6800/jsonrpc'}" placeholder="http://localhost:6800/jsonrpc" autocomplete="off" oninput="this.style.borderColor = this.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'" style="width:100%; height:44px; padding:0 70px 0 12px; border:2px solid ${ (ariaUrl || 'http://localhost:6800/jsonrpc') ? 'var(--pk-pri)' : '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; transform: translateZ(0);"> <div class="pk-select-label">${L.label_aria2_url}</div> <div id="btn_pop_aria_default" style="position:absolute; right:10px; top:50%; transform:translateY(-50%); font-size:11px; color:var(--pk-pri); cursor:pointer; font-weight:bold; padding:4px 8px; border-radius:4px; background:rgba(0,103,192,0.1); border:1px solid rgba(0,103,192,0.2);" onmouseover="this.style.background='rgba(0,103,192,0.2)'" onmouseout="this.style.background='rgba(0,103,192,0.1)'">Default</div> </div> <div class="pk-aria-status-box" id="pop_aria_test_res" style="cursor:help; margin-top: 8px;"> <div class="pk-aria-dot" id="pop_aria_test_dot"></div> <span id="pop_aria_test_txt" style="color:#888; font-size: 11px;">${L.lbl_aria2_status}</span> </div> </div> <div style="position:relative; transform: translateZ(0);"> <input type="text" id="pop_aria_token" value="${ariaToken || ''}" placeholder="${L.ph_aria2_secret}" autocomplete="one-time-code" spellcheck="false" data-lpignore="true" readonly onfocus="this.removeAttribute('readonly');" oninput="this.style.borderColor = this.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'" style="width:100%; height:44px; padding:0 12px; border:2px solid ${ariaToken ? 'var(--pk-pri)' : '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; -webkit-text-security: disc; transform: translateZ(0);"> <div class="pk-select-label">${L.label_aria2_token}</div> </div> </div> <div class="pk-modal-act" style="margin-top:30px; display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="pop_aria_cancel" style="height:40px; min-width:86px; padding:0 24px; border-radius:8px; justify-content:center; background:transparent; font-weight:500;">${L.btn_cancel}</button> <button class="pk-btn pri" id="pop_aria_save" style="height:40px; min-width:86px; padding:0 24px; border-radius:8px; background:var(--pk-pri); color:#fff; font-weight:bold; justify-content:center; border:none; font-size:14px;">${L.btn_save} & ${L.btn_aria2}</button> </div> `); const modalBox = m.querySelector('.pk-modal'); if (modalBox) Object.assign(modalBox.style, { width: '480px', padding: '30px', height: 'auto', minHeight: 'auto', overflow: 'visible' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); const inpU = m.querySelector('#pop_aria_url'); const inpT = m.querySelector('#pop_aria_token'); const dot = m.querySelector('#pop_aria_test_dot'); const txt = m.querySelector('#pop_aria_test_txt'); const boxRes = m.querySelector('#pop_aria_test_res'); let testTimer = null; const runLiveTest = async () => { const url = inpU.value.trim(); const token = inpT.value.trim(); const showTip = () => showAlert(L.tip_mixed_content, L.lbl_aria2_status); if (!url) { dot.className = 'pk-aria-dot'; txt.textContent = L.lbl_aria2_status; boxRes.onclick = showTip; boxRes.style.cursor = 'pointer'; return; } dot.className = 'pk-aria-dot wait'; txt.textContent = L.str_connecting; boxRes.onclick = showTip; boxRes.style.cursor = 'pointer'; const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: [`token:${token}`] }; let testUrl = url.replace(/^ws/i, 'http'); if (!testUrl.includes('/jsonrpc') && !testUrl.includes('?')) { testUrl = testUrl.endsWith('/') ? testUrl + 'jsonrpc' : testUrl + '/jsonrpc'; } try { await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: testUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 3000, onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error(r.status)); }, onerror: (e) => rejectReq(e) }); }); dot.className = 'pk-aria-dot ok'; txt.textContent = L.str_connected; boxRes.onclick = null; boxRes.style.cursor = 'default'; } catch (e) { dot.className = 'pk-aria-dot err'; txt.textContent = L.str_conn_fail; boxRes.onclick = showTip; boxRes.style.cursor = 'pointer'; } }; const triggerTest = () => { clearTimeout(testTimer); testTimer = setTimeout(runLiveTest, 600); }; inpU.oninput = (e) => { e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; triggerTest(); }; inpT.oninput = (e) => { e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; triggerTest(); }; m.querySelector('#btn_pop_aria_default').onclick = () => { inpU.value = 'http://localhost:6800/jsonrpc'; inpU.style.borderColor = 'var(--pk-pri)'; triggerTest(); }; setTimeout(runLiveTest, 200); m.querySelector('#pop_aria_cancel').onclick = () => { m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); }; m.querySelector('#pop_aria_save').onclick = async () => { let u = inpU.value.trim(); let t = inpT.value.trim(); if (!u) { inpU.style.borderColor = '#d93025'; return; } if (!/^https?:\/\/|^wss?:\/\//i.test(u)) u = 'http://' + u; const saveBtn = m.querySelector('#pop_aria_save'); const originalTxt = saveBtn.textContent; saveBtn.disabled = true; saveBtn.textContent = L.str_saving_dots; try { const testUrl = u.replace(/^ws/i, 'http'); const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_quick_test', params: [`token:${t}`] }; await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: testUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 5000, onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error('HTTP ' + r.status)); }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); gmSet('pk_aria2_url', u); gmSet('pk_aria2_token', t); m.remove(); resolve({ url: u, token: t }); } catch (err) { const confirmed = await showConfirm( L.msg_aria2_test_fail, L.title_aria2_fail ); if (confirmed) { gmSet('pk_aria2_url', u); gmSet('pk_aria2_token', t); m.remove(); resolve({ url: u, token: t }); } else { saveBtn.disabled = false; saveBtn.textContent = originalTxt; } } }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#pop_aria_save').click(); } }); }); if (!result) return; ariaUrl = result.url; ariaToken = result.token; } setLoad(true); isGUISensitive = true; const abortCtrl = new AbortController(); const { signal } = abortCtrl; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; abortCtrl.abort(); updateLoadTxt(L.str_stopping); }; if (ariaUrl.startsWith('ws')) ariaUrl = ariaUrl.replace(/^ws/, 'http'); if (!ariaUrl.includes('/jsonrpc') && !ariaUrl.includes('?')) { ariaUrl = ariaUrl.endsWith('/') ? ariaUrl + 'jsonrpc' : ariaUrl + '/jsonrpc'; } const allFiles = []; const rootNodes = []; const HYDRATE_LIMIT = 40; const fExtStr = gmGet('pk_dl_filter_ext', '').toLowerCase(); const fNameStr = gmGet('pk_dl_filter_name', '').toLowerCase(); const fExts = fExtStr.split(/[,,]/).map(s => s.trim().replace(/^\./, '')).filter(Boolean); const fNames = fNameStr.split(/[,,]/).map(s => s.trim()).filter(Boolean); const stats = { hydratedCount: 0, lastUiTime: 0 }; S.sel.forEach(id => { const item = S.itemMap.get(id); if (item) { if (item.kind === 'drive#folder') { rootNodes.push({...item, lineage: [{ id: item.id, name: item.name }], retryCount: 0}); } else { allFiles.push({ ...item, _lineage: [] }); } } }); let progressTask = null; try { await coreRecursiveEngine(rootNodes, { signal, onFile: (f, parent) => { const lowName = f.name.toLowerCase(); const ext = lowName.split('.').pop(); const isBlocked = fExts.some(e => ext === e) || fNames.some(n => lowName.includes(n)); if (!isBlocked) { f._lineage = parent.lineage || []; allFiles.push(f); } }, onProgress: (st) => { const now = Date.now(); if (now - stats.lastUiTime > 150) { updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`); stats.lastUiTime = now; } } }); if (!isRunning) throw new Error('StoppedByUser'); if (allFiles.length === 0) { setLoad(false); showAlert(L.msg_batch_no_files); return; } setLoad(false); progressTask = FloatBarManager.create(L.msg_batch_hydrating); const readyFiles =[]; const failedFiles = []; const hydrateQueue = [...allFiles]; const activeTasks = new Set(); const hydrateWithRetry = async (file, maxRetries = 3) => { if (file.web_content_link) return file; let lastErr = null; for (let i = 0; i < maxRetries; i++) { if (!isRunning) return null; try { const detail = await apiGet(file.id); if (detail && detail.web_content_link) return detail; throw new Error("Link Empty"); } catch (e) { lastErr = e; if (i < maxRetries - 1) await sleep(1000 * (i + 1)); } } throw lastErr; }; while ((hydrateQueue.length > 0 || activeTasks.size > 0) && isRunning) { while (hydrateQueue.length > 0 && activeTasks.size < HYDRATE_LIMIT && isRunning) { const file = hydrateQueue.pop(); const p = (async () => { try { const detail = await hydrateWithRetry(file); if (detail) { detail._lineage = file._lineage; readyFiles.push(detail); } } catch (e) { console.error(`[Hydrate Failed] ${file.name}:`, e); failedFiles.push(file.name + " " + L.str_aria2_fetch_err); } })().finally(() => { activeTasks.delete(p); stats.hydratedCount++; if (progressTask) progressTask.update(`${L.msg_batch_hydrating} ${stats.hydratedCount} / ${allFiles.length}`); }); activeTasks.add(p); } if (activeTasks.size > 0) await Promise.race(activeTasks); } if (!isRunning) throw new Error('StoppedByUser'); if (readyFiles.length > 0 && isRunning) { const BATCH_SIZE = 50; let successCount = 0; let rpcFatalError = false; for (let i = 0; i < readyFiles.length; i += BATCH_SIZE) { if (!isRunning || rpcFatalError) break; const chunk = readyFiles.slice(i, i + BATCH_SIZE); const sanitize = (s) => s.replace(/[\\/:*?"<>|]/g, '_').trim(); const payload = chunk.map(f => { let relativePrefix = ''; if (f._lineage && f._lineage.length > 0) { relativePrefix = f._lineage.map(n => sanitize(n.name)).join('/') + '/'; } const outPath = relativePrefix + sanitize(f.name); return { jsonrpc: '2.0', method: 'aria2.addUri', id: `pk_${Date.now()}_${Math.random().toString(16).slice(2)}`, params:[`token:${ariaToken}`, [f.web_content_link], { out: outPath, header:[`User-Agent: ${navigator.userAgent}`, `Referer: https://mypikpak.com/`] }] }; }); try { await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: ariaUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, onload: (r) => { if (r.status === 200) resolveReq(); else rejectReq(new Error(`RPC ${r.status}`)); }, onerror: () => rejectReq(new Error("Network Error")), ontimeout: () => rejectReq(new Error("Timeout")) }); }); successCount += chunk.length; } catch (rpcErr) { console.error("[RPC Batch Error]", rpcErr); chunk.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_rpc_err)); if (rpcErr.message === "Network Error" || rpcErr.message === "Timeout") { console.warn("[RPC Circuit Breaker] Aria2 disconnected. Aborting remaining batches."); rpcFatalError = true; const remainingFiles = readyFiles.slice(i + BATCH_SIZE); remainingFiles.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_aborted)); } } if (progressTask) progressTask.update(`${L.msg_aria2_sending_batch} ${successCount} / ${readyFiles.length}`); } if (failedFiles.length > 0) { let failListText = failedFiles.slice(0, 10).join('\n'); if (failedFiles.length > 10) { failListText += '\n...'; failListText += L.msg_aria2_batch_fail_log; } await showAlert(`${L.str_failed}: ${failedFiles.length}\n\n${failListText}`, L.title_alert); if (failedFiles.length > 10) { try { const blob = new Blob([failedFiles.join('\r\n')], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const now = new Date(); const dateStr = now.toISOString().replace(/[:.]/g, '-').slice(0, 19); a.href = url; a.download = `${L.str_aria2_fail_file_name}_${dateStr}.txt`; document.body.appendChild(a); a.click(); setTimeout(() => { if (a.parentNode) document.body.removeChild(a); URL.revokeObjectURL(url); }, 1000); } catch (exportErr) { console.error("[Export Error] Failed to generate failure log:", exportErr); } } } else { showToast(L.msg_aria2_sent.replace('{n}', successCount)); } } } catch (e) { if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') { showAlert(`${L.msg_aria2_check_fail}\n(${e.message})`); } } finally { setLoad(false); isGUISensitive = false; if (progressTask) progressTask.destroy(); } }; const ensureItemMap = () => { S.itemMap.clear(); const len = S.items.length; for (let i = 0; i < len; i++) { const item = S.items[i]; if (item && item.id) { S.itemMap.set(item.id, item); } } }; const executeBatchDelete = async (ids, options = {}) => { const { silent = false, deleteFiles = false, isTask = false, forceRefresh = false, hardDelete = false, explicitItems = [] } = options; if (!ids || ids.length === 0) return; const tempItemLookup = new Map(); if (S.itemMap) S.itemMap.forEach((v, k) => tempItemLookup.set(k, v)); if (explicitItems && explicitItems.length > 0) { explicitItems.forEach(item => { if (item && item.id) tempItemLookup.set(item.id, item); }); } const BATCH_SIZE = hardDelete ? 1000 : 200; const SKIP_VERIFY = hardDelete; const progressTask = FloatBarManager.create(L.str_deleting); const updateFloat = progressTask.update; S.movingSourceId = S.path[S.path.length - 1].id || 'root'; S.movingDestId = 'trash'; const allLockedIdsArray =[]; ids.forEach(id => { S.movingIds.add(id); allLockedIdsArray.push(id); if (isTask && deleteFiles) { const taskItem = tempItemLookup.get(id); if (taskItem && taskItem.file_id) { S.movingIds.add(taskItem.file_id); allLockedIdsArray.push(taskItem.file_id); } } }); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_ADD', ids: allLockedIdsArray, src: S.movingSourceId, dst: S.movingDestId }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); isGUISensitive = true; S.sel.clear(); S.lastSelIdx = -1; S.activeId = null; try { const BATCH_SIZE = 200; const totalToDelete = ids.length; let deletedCount = 0; const affectedParentIds = new Set(); const deletedSet = new Set(); affectedParentIds.add(S.path[S.path.length - 1].id || 'root'); for (let i = 0; i < totalToDelete; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); let retry = 0; const maxRetries = 3; let success = false; while (retry < maxRetries && !success) { try { if (isTask) { await apiCancelTask(chunk, deleteFiles); } else { const action = hardDelete ? 'files:batchDelete' : 'files:batchTrash'; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/${action}`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }) }); if (!res.ok) throw new Error(`API ${res.status}`); } success = true; } catch (err) { retry++; console.warn(`[Delete] Retry ${retry}/${maxRetries}`); updateFloat(`${L.str_deleting} (Retry ${retry})...`); await sleep(1000 * retry); if (retry >= maxRetries) throw err; } } if (!isTask && chunk.length > 0 && !SKIP_VERIFY) { const lastId = chunk[chunk.length - 1]; let verifyRetries = 0; while (verifyRetries < 20) { try { const meta = await apiGet(lastId); if (meta.trashed) break; } catch (e) { break; } updateFloat(`${L.str_deleting} ${deletedCount}/${totalToDelete}`); verifyRetries++; await sleep(500); } } const chunkSet = new Set(chunk); const chunkLockedIds =[]; chunk.forEach(id => { S.movingIds.delete(id); chunkLockedIds.push(id); const it = tempItemLookup.get(id); let physicalFolderId = null; if (it) { if (it.kind === 'drive#folder') { physicalFolderId = it.id; } else if (isTask && deleteFiles && it.file_id) { const isFolderTask = (it.mime_type && (it.mime_type.includes('folder') || it.mime_type.includes('directory'))) || (it.icon_link && it.icon_link.includes('folder')) || (typeof globalCache !== 'undefined' && globalCache.has(it.file_id)); if (isFolderTask) { physicalFolderId = it.file_id; } } } if (physicalFolderId && typeof globalCache !== 'undefined') { const purgeDescendants = (fid) => { const data = globalCache.get(fid) || (S.cache ? S.cache.get(fid) : null); if (data) { globalTombstoneCache.set(fid, Array.isArray(data) ?[...data] : {...data}); const list = Array.isArray(data) ? data : (data.items ||[]); list.forEach(child => { deletedSet.add(child.id); S.itemMap.delete(child.id); if (child.kind === 'drive#folder') { purgeDescendants(child.id); } }); globalCache.delete(fid); if (S.cache) S.cache.delete(fid); } }; purgeDescendants(physicalFolderId); } if (isTask && deleteFiles && it && it.file_id) { S.movingIds.delete(it.file_id); chunkLockedIds.push(it.file_id); deletedSet.add(it.file_id); if (typeof globalParentIndex !== 'undefined' && globalParentIndex.has(it.file_id)) { const parentInfo = globalParentIndex.get(it.file_id); if (parentInfo && parentInfo.id) { affectedParentIds.add(parentInfo.id === 'root' ? '' : parentInfo.id); } } affectedParentIds.add('root'); affectedParentIds.add(''); } if (it && S.analyzeMap) { const analyzeTargetId = physicalFolderId || id; const analyzeIdsToRemove = new Set([analyzeTargetId, id]); const queue = [analyzeTargetId, id]; while (queue.length > 0) { const currId = queue.shift(); S.analyzeMap.forEach((node, nId) => { if (node.parentId === currId && !analyzeIdsToRemove.has(nId)) { analyzeIdsToRemove.add(nId); queue.push(nId); } }); } analyzeIdsToRemove.forEach(remId => { if (S.analyzeMap.has(remId)) S.analyzeMap.delete(remId); }); const lostSize = parseInt(it.size || 0); if (lostSize > 0) { let currPid = it.parent_id; if (!currPid && isTask && typeof globalParentIndex !== 'undefined') { const pInfo = globalParentIndex.get(it.file_id); if (pInfo) currPid = pInfo.id; } let safety = 50; while (currPid && S.analyzeMap.has(currPid) && safety > 0) { const pNode = S.analyzeMap.get(currPid); pNode.size = Math.max(0, pNode.size - lostSize); currPid = pNode.parentId; safety--; } } if (S.analyzeSimGroups) { S.analyzeSimGroups.forEach(group => { group.ids = group.ids.filter(gid => !analyzeIdsToRemove.has(gid)); }); S.analyzeSimGroups = S.analyzeSimGroups.filter(group => group.ids.length >= 2); if (S.analyzeSimGroups.length === 0) { setTimeout(() => { if (UI.btnExit) UI.btnExit.click(); }, 500); } } if (S.analyzeResultItems) { S.analyzeResultItems = S.analyzeResultItems.filter(x => !analyzeIdsToRemove.has(x.id)); S.analyzeResultItems.forEach(resItem => { if (S.analyzeMap.has(resItem.id)) { resItem.size = S.analyzeMap.get(resItem.id).size.toString(); } }); } } if (it && it.parent_id) affectedParentIds.add(it.parent_id); S.itemMap.delete(id); deletedSet.add(id); }); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: chunkLockedIds }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); S.items = S.items.filter(x => !deletedSet.has(x.id)); if (typeof pkState !== 'undefined' && pkState && pkState.lastGlobalResults) { pkState.lastGlobalResults = pkState.lastGlobalResults.filter(x => !deletedSet.has(x.id)); } if (typeof globalCache !== 'undefined') { const cleanListChunk = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !deletedSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !deletedSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { globalCache.set(key, cleanListChunk(globalCache.get(key))); } for (const key of S.cache.keys()) { S.cache.set(key, cleanListChunk(S.cache.get(key))); } } if (!forceRefresh) { if (S.dupMode) renderDupView(); else refresh(); } await new Promise(r => requestAnimationFrame(r)); deletedCount += chunk.length; updateFloat(`${L.str_deleting} ${Math.min(deletedCount, totalToDelete)} / ${totalToDelete}`); if (deletedCount < totalToDelete) await sleep(50); } const cur = S.path[S.path.length - 1]; if (cur && cur.id) gmSet('pk_fmod_' + cur.id, new Date(getServerNow()).toISOString()); if (typeof globalCache !== 'undefined') { const cleanList = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !deletedSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !deletedSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { globalCache.set(key, cleanList(globalCache.get(key))); } for (const key of S.cache.keys()) { S.cache.set(key, cleanList(S.cache.get(key))); } affectedParentIds.forEach(pid => { const keysToCheck = (pid === 'root' || pid === '') ? ['root', ''] : [pid]; keysToCheck.forEach(key => { if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(key); }); }); globalCache.delete('root_trashed'); S.cache.delete('root_trashed'); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(); if (forceRefresh) { await load(false, true); } else { updateStat(); setTimeout(() => updateQuotaUI(), 2000); } if (!silent && !S.dupMode) { showToast(isTask ? L.msg_task_deleted : L.msg_del_items_done.replace('{n}', deletedCount)); } } catch (e) { console.error(e); showAlert(`${L.str_error}: ${e.message}`); } finally { if (typeof allLockedIdsArray !== 'undefined' && allLockedIdsArray.length > 0) { allLockedIdsArray.forEach(id => S.movingIds.delete(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: allLockedIdsArray }); } else if (ids && ids.length > 0) { ids.forEach(id => S.movingIds.delete(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: ids }); } if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); if (S.movingIds.size === 0) { isGUISensitive = false; } if (progressTask) progressTask.destroy(); } }; UI.btnDel.onclick = async () => { if (!S.sel.size) return; if (S.historyMode) { const count = S.sel.size; if (!await showConfirm(L.warn_clear_history.replace('{n}', count))) return; const selIds = new Set(S.sel); selIds.forEach(id => { gmSet('pk_progress_' + id, null); S.itemMap.delete(id); }); S.items = S.items.filter(it => !selIds.has(it.id)); S.clearSelection(); refresh(); showToast(L.msg_clear_history_done); return; } if (S.offlineMode) { const count = S.sel.size; const html = ` <div style="padding: 5px 0 0 0;"> <h3 style="margin:0 0 25px 0; font-size:18px; font-weight:700; color:var(--pk-fg); border:none; line-height:1.4;"> ${L.title_del_task_confirm_fmt.replace('{n}', count)} </h3> <label style="display:flex; align-items:center; cursor:pointer; user-select:none; margin-bottom:35px; font-size:14px; color:var(--pk-fg);"> <input type="checkbox" id="del_task_files" style="width:18px; height:18px; margin-right:10px; accent-color:var(--pk-pri); cursor:pointer;"> <span style="opacity:0.9;">${L.lbl_del_cloud_files_too}</span> </label> <div class="pk-modal-act" style="display:flex; justify-content:flex-end; gap:12px;"> <button class="pk-btn" id="del_task_cancel" style="height:36px; padding:0 24px; border-radius:6px; background:transparent; border:1px solid var(--pk-bd); font-weight:500; color:var(--pk-fg);">${L.btn_cancel}</button> <button class="pk-btn pri" id="del_task_confirm" style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); color:#fff; font-weight:600; border:none; box-shadow: 0 2px 6px rgba(0,0,0,0.2);">${L.btn_del}</button> </div> </div> `; if (typeof m !== 'undefined' && m.remove) m.remove(); const taskModal = showModal(html); const modalBox = taskModal.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => taskModal.remove(); taskModal.querySelector('#del_task_cancel').onclick = close; const closeBtn = taskModal.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; taskModal.querySelector('#del_task_confirm').onclick = async () => { const isDeleteFile = taskModal.querySelector('#del_task_files').checked; taskModal.remove(); await executeBatchDelete(Array.from(S.sel), { isTask: true, deleteFiles: isDeleteFile, forceRefresh: true }); }; taskModal.tabIndex = 0; setTimeout(() => taskModal.focus(), 10); taskModal.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); taskModal.querySelector('#del_task_confirm').click(); } }); return; } ensureItemMap(); setLoad(true); const totalCount = S.sel.size; updateLoadTxt(`${L.str_checking_bl}\n0 / ${totalCount}`); await sleep(16); const blSet = S.blSet; const blFolderSet = S.blFolderSet; const toDeleteIds = []; let blacklistedCount = 0; let processed = 0; let lastYieldTime = performance.now(); let protectedCount = 0; for (let id of S.sel) { const item = S.itemMap.get(id); if (!item) { processed++; continue; } if (!S.trashMode && isSystemItem(item)) { protectedCount++; processed++; continue; } const lowerName = item.name.toLowerCase().trim(); const isFolder = item.kind === 'drive#folder'; const isProtectedMode = gmGet('pk_skip_bl_on_del', true); if (isProtectedMode && (isFolder ? blFolderSet.has(lowerName) : blSet.has(lowerName))) { blacklistedCount++; } else { toDeleteIds.push(id); } processed++; if ((processed % 500) === 0) { const now = performance.now(); if (now - lastYieldTime > 12) { updateLoadTxt(`${L.str_checking_bl}\n${processed} / ${totalCount}`); await sleep(0); lastYieldTime = performance.now(); } } } setLoad(false); if (blacklistedCount > 0 && toDeleteIds.length === 0) { showAlert(L.msg_del_protected.replace('{n}', blacklistedCount)); S.sel.clear(); refresh(); updateStat(); return; } if (toDeleteIds.length === 0) { showAlert(L.msg_del_none); return; } if (!await showConfirm(L.warn_del.replace('{n}', toDeleteIds.length))) return; await executeBatchDelete(toDeleteIds); }; UI.btnDeselect.onclick = () => { S.clearSelection(); refresh(); }; const processBlacklistAction = async (action) => { const totalSelected = S.sel.size; if (totalSelected === 0) return; const isRemove = action === 'remove'; const progressTask = FloatBarManager.create(L.str_init_op); const updateFloat = progressTask.update; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; updateFloat(L.str_stopping); }; await sleep(16); const getCleanKey = (str) => str ? str.toLowerCase().trim() : ""; const parseList = (str) => str ? str.split(/[\r\n]+/).map(s => s.trim()).filter(s => s) : []; const fileListStr = gmGet('pk_blacklist', ''); const folderListStr = gmGet('pk_blacklist_folders', ''); let currentFiles = parseList(fileListStr); let currentFolders = parseList(folderListStr); const targetFileKeys = new Set(); const targetFolderKeys = new Set(); const toAddFiles = []; const toAddFolders = []; let processedCount = 0; let lastYieldTime = performance.now(); const len = S.items.length; for (let i = 0; i < len; i++) { if (!isRunning) break; const item = S.items[i]; if (S.sel.has(item.id)) { const name = item.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim(); const key = getCleanKey(name); if (item.kind === 'drive#folder') { targetFolderKeys.add(key); if (!isRemove) toAddFolders.push(name); } else { targetFileKeys.add(key); if (!isRemove) toAddFiles.push(name); } processedCount++; if ((processedCount & 63) === 0) { const now = performance.now(); if (now - lastYieldTime > 12) { updateFloat(`${L.str_analyzing} ${processedCount} / ${totalSelected}`); await sleep(0); lastYieldTime = performance.now(); } } } } if (!isRunning) { progressTask.destroy(); showAlert(L.msg_bl_stop); return; } updateFloat(L.str_processing); await sleep(10); let finalCount = 0; let dataChanged = false; if (isRemove) { const oldFileCount = currentFiles.length; const oldFolderCount = currentFolders.length; currentFiles = currentFiles.filter(name => !targetFileKeys.has(getCleanKey(name))); currentFolders = currentFolders.filter(name => !targetFolderKeys.has(getCleanKey(name))); if (oldFileCount !== currentFiles.length || oldFolderCount !== currentFolders.length) { dataChanged = true; finalCount = (oldFileCount - currentFiles.length) + (oldFolderCount - currentFolders.length); } } else { const existingFileKeys = new Set(currentFiles.map(s => getCleanKey(s))); const existingFolderKeys = new Set(currentFolders.map(s => getCleanKey(s))); let addedCount = 0; for (const name of toAddFiles) { const key = getCleanKey(name); if (!existingFileKeys.has(key)) { currentFiles.push(name); existingFileKeys.add(key); addedCount++; dataChanged = true; } } for (const name of toAddFolders) { const key = getCleanKey(name); if (!existingFolderKeys.has(key)) { currentFolders.push(name); existingFolderKeys.add(key); addedCount++; dataChanged = true; } } finalCount = addedCount; } if (dataChanged) { updateFloat(L.str_saving); await sleep(10); gmSet('pk_blacklist', currentFiles.join('\n')); gmSet('pk_blacklist_folders', currentFolders.join('\n')); S.updateBlCache(); renderVisible(); } progressTask.destroy(); const msgTemplate = isRemove ? L.msg_bl_remove_done : L.msg_bl_add_done; showToast(msgTemplate.replace('{n}', finalCount)); }; UI.btnBlacklistManager.onclick = showBlacklistModal; if (UI.btnTrashBlacklistManager) UI.btnTrashBlacklistManager.onclick = showBlacklistModal; if (UI.uploadWrap && UI.btnUpload) { UI.btnUpload.onclick = (e) => { e.stopPropagation(); const menu = UI.uploadWrap.querySelector('.pk-dropdown-menu'); const isActive = menu.style.display === 'flex'; document.querySelectorAll('.pk-dropdown-menu, .pk-select-menu').forEach(m => m.style.display = 'none'); document.querySelectorAll('.pk-dropdown-wrap').forEach(w => w.classList.remove('active')); if (!isActive) { menu.style.display = 'flex'; UI.uploadWrap.classList.add('active'); } }; UI.actUpFile.onclick = (e) => { e.stopPropagation(); UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none'; UI.uploadWrap.classList.remove('active'); UI.inpFile.click(); }; UI.actUpFolder.onclick = (e) => { e.stopPropagation(); UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none'; UI.uploadWrap.classList.remove('active'); UI.inpFolder.click(); }; const updateRowUI = (task) => { const row = document.querySelector(`.pk-row[data-id="${task.id}"]`); if (row) { const progBar = row.querySelector('.pk-up-prog-bar'); const progTxt = row.querySelector('.pk-up-prog-txt'); const spdTxt = row.querySelector('.pk-up-spd'); const statusCol = row.children[4]; const msgSpan = statusCol ? statusCol.querySelector('span:first-child') : null; if (progBar) progBar.style.width = task.progress + '%'; if (progTxt) progTxt.textContent = Math.floor(task.progress) + '%'; if (msgSpan) { msgSpan.textContent = task.message; const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; if (activeStatus.includes(task.status)) { msgSpan.style.color = 'var(--pk-pri)'; if (progBar) progBar.style.backgroundColor = 'var(--pk-pri)'; } } if (spdTxt) spdTxt.innerHTML = task.status === 'DONE' ? '<span style="color:#52c41a">${L.lbl_done_check}</span>' : S.upMng.fmtSpeed(task.speed); } }; const resolveTask = (task) => { if (S.uploadMode) updateRowUI(task); }; S.upMng = { limit: 3, running: 0, fmtSpeed: (bytesPerSec) => { if (bytesPerSec === 0) return '0 B/s'; const units = ['B/s', 'KB/s', 'MB/s', 'GB/s']; let i = 0; while (bytesPerSec >= 1024 && i < units.length - 1) { bytesPerSec /= 1024; i++; } return bytesPerSec.toFixed(2) + ' ' + units[i]; }, createTask: (file, parentId) => ({ id: 'up_' + Date.now() + '_' + Math.random().toString(36).substr(2), kind: 'pk#upload', file: file, name: file.name, size: file.size, parentId: parentId, status: 'WAITING', progress: 0, speed: 0, message: L.msg_task_waiting, _xhr: null, _lastCalcTime: 0, _lastCalcLoaded: 0, _lastUiTime: 0 }), scheduler: () => { if (S.upMng.running >= S.upMng.limit) return; const waiting = S.uploadTasks.find(t => t.status === 'WAITING'); if (waiting) S.upMng.start(waiting); }, start: async (task) => { if (S.quota && S.quota.limitRaw > 0) { const remaining = S.quota.limitRaw - S.quota.usedRaw; if (task.size > remaining) { task.status = 'ERROR'; task.message = L.err_quota_exceeded ; if (S.uploadMode) updateRowUI(task); S.upMng.scheduler(); return; } } S.upMng.running++; task.status = 'HASHING'; task.message = L.msg_task_hashing; task._totalUploadedBytes = 0; let _lastPollTime = Date.now(); let _lastPollBytes = 0; const speedTimer = setInterval(() => { if (task.status === 'UPLOADING') { const now = Date.now(); const timeDiff = now - _lastPollTime; const bytesDiff = task._totalUploadedBytes - _lastPollBytes; if (timeDiff > 0) { task.speed = Math.max(0, (bytesDiff / timeDiff) * 1000); } _lastPollTime = now; _lastPollBytes = task._totalUploadedBytes; if (S.uploadMode) updateRowUI(task); } }, 1500); const cryptoSign = async (secret, stringToSign) => { const enc = new TextEncoder(); const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]); const signature = await crypto.subtle.sign("HMAC", key, enc.encode(stringToSign)); return btoa(String.fromCharCode(...new Uint8Array(signature))); }; try { let finalParentId = task.parentId; if (finalParentId === 'root' || finalParentId === 'upload_root' || finalParentId === '') { const UPLOAD_FOLDER_NAME = 'My Upload'; const cacheKey = `lock_root_${UPLOAD_FOLDER_NAME}`; if (!S.upMng._syncLocks) S.upMng._syncLocks = new Map(); if (!S.upMng._syncLocks.has(cacheKey)) { const createAction = (async () => { const checkExisting = async (targetName) => { try { const list = await apiList('', 1000); const found = list.find(f => f.kind === 'drive#folder' && f.name === targetName); return found ? found.id : null; } catch (e) { return null; } }; let existingId = await checkExisting(UPLOAD_FOLDER_NAME); if (existingId) return existingId; let retry = 0; while (retry < 3) { try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: "drive#folder", parent_id: "", name: UPLOAD_FOLDER_NAME }) }); if (res.ok) { const data = await res.json(); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(''); if (typeof globalCache !== 'undefined') globalCache.delete(''); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); return data.file.id; } if (res.status === 400 || res.status === 429) { await new Promise(r => setTimeout(r, 1000)); existingId = await checkExisting(UPLOAD_FOLDER_NAME); if (existingId) return existingId; } } catch (e) {} retry++; } existingId = await checkExisting(UPLOAD_FOLDER_NAME); if (existingId) return existingId; throw new Error("My Upload folder creation failed"); })(); S.upMng._syncLocks.set(cacheKey, createAction); } try { finalParentId = await S.upMng._syncLocks.get(cacheKey); task.parentId = finalParentId; } catch (e) { S.upMng._syncLocks.delete(cacheKey); throw e; } } if (task.relativeFolder) { const folderNames = task.relativeFolder.split('/'); let currentPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (!S.upMng._syncLocks) S.upMng._syncLocks = new Map(); for (const name of folderNames) { const cacheKey = `lock_${currentPid}_${name}`; if (!S.upMng._syncLocks.has(cacheKey)) { const createAction = (async () => { const checkExisting = async (pid, targetName) => { try { const list = await apiList(pid, 1000); const found = list.find(f => f.kind === 'drive#folder' && f.name === targetName); return found ? found.id : null; } catch (e) { return null; } }; let existingId = await checkExisting(currentPid, name); if (existingId) return existingId; let retry = 0; while (retry < 3) { try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: "drive#folder", parent_id: currentPid, name: name }) }); if (res.ok) { const data = await res.json(); return data.file.id; } if (res.status === 400 || res.status === 429) { await new Promise(r => setTimeout(r, 1000)); existingId = await checkExisting(currentPid, name); if (existingId) return existingId; } } catch (e) {} retry++; } existingId = await checkExisting(currentPid, name); if (existingId) return existingId; throw new Error("Folder creation failed"); })(); S.upMng._syncLocks.set(cacheKey, createAction); } try { currentPid = await S.upMng._syncLocks.get(cacheKey); } catch (e) { S.upMng._syncLocks.delete(cacheKey); throw e; } } finalParentId = currentPid; } if (task._deleted) throw new Error("Aborted"); const hash = await calcSha1(task.file); if (task._deleted) throw new Error("Aborted"); task.status = 'UPLOADING'; task.message = L.msg_task_init_upload; _lastPollTime = Date.now(); _lastPollBytes = 0; const safePid = (finalParentId === 'root' || finalParentId === 'upload_root') ? '' : (finalParentId || ''); let res = null; let createRetry = 0; const maxCreateRetries = 5; while (createRetry < maxCreateRetries) { try { res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: "drive#file", parent_id: safePid, name: task.name, size: task.size, hash: hash, upload_type: "UPLOAD_TYPE_RESUMABLE" }) }); if (res.status === 429) { const waitMs = 2000 + Math.random() * 2000 * (createRetry + 1); console.warn(`[Upload] Rate limited (429). Retrying in ${Math.round(waitMs)}ms...`); await new Promise(r => setTimeout(r, waitMs)); createRetry++; continue; } if (!res.ok) { const errData = await res.json().catch(() => ({})); const errMsg = errData.error_description || `HTTP ${res.status}`; const isQuotaExceeded = res.status === 400 && (errData.error_code === 12 || errMsg.toLowerCase().includes('quota')); if (isQuotaExceeded) { const quotaErr = new Error(L.err_quota_exceeded); quotaErr.isFatal = true; throw quotaErr; } if (res.status === 404 || errMsg.toLowerCase().includes('not found') || errMsg.toLowerCase().includes('invalid parent')) { if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); throw new Error(L.err_parent_not_found); } throw new Error(errMsg); } break; } catch (e) { console.warn(`[Upload] Init request failed (${createRetry + 1}/${maxCreateRetries}):`, e.message); if (e.isFatal) throw e; createRetry++; if (createRetry >= maxCreateRetries) throw e; await new Promise(r => setTimeout(r, 1500)); } } const data = await res.json(); let newlyCreatedFileId = null; if (data.file) { if (data.file.id) { task.file_id = data.file.id; newlyCreatedFileId = data.file.id; } if (data.file.name) task.name = data.file.name; if (data.file.thumbnail_link) task.thumbnail_link = data.file.thumbnail_link; if (data.file.icon_link) task.icon_link = data.file.icon_link; } else if (data.task && data.task.file_id) { task.file_id = data.task.file_id; newlyCreatedFileId = data.task.file_id; if (data.task.name) task.name = data.task.name; } else if (data.id) { task.file_id = data.id; newlyCreatedFileId = data.id; if (data.name) task.name = data.name; } if (task._deleted) { console.warn(`[Upload] Task ${task.id} was deleted by user during initialization. Triggering self-destruct.`); if (newlyCreatedFileId && task._deleteFileIntent) { try { await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [newlyCreatedFileId] }) }); console.log(`[Upload] Ghost file ${newlyCreatedFileId} cleaned up.`); } catch (err) { console.error(`[Upload] Failed to cleanup ghost file ${newlyCreatedFileId}`, err); } } throw new Error("Aborted"); } if (data.upload_type === "UPLOAD_TYPE_URL" || data.phase === "PHASE_TYPE_COMPLETE" || (data.file && data.file.phase === "PHASE_TYPE_COMPLETE")) { task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_fast_success; if (S.uploadMode) { updateRowUI(task); requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); } setTimeout(() => updateQuotaUI(), 1000); if (typeof globalCache !== 'undefined') { const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (globalCache.has(targetPid)) { const cacheEntry = globalCache.get(targetPid); const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []); const newFileStub = { id: task.file_id, kind: 'drive#file', name: task.name, size: task.size, parent_id: targetPid, mime_type: task.mime_type || '', thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link, modified_time: new Date().toISOString(), hash: hash }; if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub); } } if (typeof globalDirtyFolders !== 'undefined') { const targetPid = task.parentId === 'root' ? '' : (task.parentId || ''); globalDirtyFolders.add(targetPid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } else if (data.resumable && data.resumable.params) { const p = data.resumable.params; const ossUA = 'aliyun-sdk-js/6.23.0 Microsoft Edge 144.0.0.0 on Windows 10 64-bit'; const objectName = p.key; const host = `https://${p.bucket}.${p.endpoint}`; const totalSize = task.file.size; let PART_SIZE = 5 * 1024 * 1024; if (totalSize > 4 * 1024 * 1024 * 1024) { PART_SIZE = 20 * 1024 * 1024; } else if (totalSize > 1 * 1024 * 1024 * 1024) { PART_SIZE = 10 * 1024 * 1024; } const partCount = Math.ceil(totalSize / PART_SIZE); const getAuth = async (method, subResource = '', contentType = '') => { const dateStr = new Date().toUTCString(); const headersToSign = { 'x-oss-date': dateStr, 'x-oss-security-token': p.security_token, 'x-oss-user-agent': ossUA }; let canonicalResource = `/${p.bucket}/${objectName}`; if (subResource) canonicalResource += `?${subResource}`; const canonicalHeaders = Object.keys(headersToSign).sort().map(k => `${k}:${headersToSign[k]}`).join('\n') + '\n'; const stringToSign = [method, "", contentType, dateStr, canonicalHeaders + canonicalResource].join("\n"); const signature = await cryptoSign(p.access_key_secret, stringToSign); return { date: dateStr, auth: `OSS ${p.access_key_id}:${signature}` }; }; const ossRequest = (method, query, body, contentType, onProgress) => { return new Promise(async (resolve, reject) => { try { const creds = await getAuth(method, query, contentType); const url = `${host}/${objectName.split('/').map(encodeURIComponent).join('/')}${query ? '?' + query : ''}`; const req = GM_xmlhttpRequest({ method: method, url: url, data: body, headers: { 'Authorization': creds.auth, 'x-oss-date': creds.date, 'x-oss-security-token': p.security_token, 'x-oss-user-agent': ossUA, 'Content-Type': contentType || '' }, upload: { onprogress: onProgress }, onload: (res) => { if (res.status >= 200 && res.status < 300) resolve(res); else reject(new Error(`OSS ${method} Error: ${res.status} ${res.statusText}`)); }, onerror: (err) => reject(new Error("Network Error")), onabort: () => reject(new Error("Aborted")) }); if (onProgress) task._xhr = { abort: () => req.abort() }; } catch (e) { reject(e); } }); }; task.message = L.msg_task_uploading; if (partCount <= 1) { await ossRequest('PUT', '', task.file, 'application/octet-stream', (pe) => { task._totalUploadedBytes = pe.loaded; task.progress = (pe.loaded / totalSize) * 100; const now = Date.now(); if (now - task._lastUiTime > 100) { if (S.uploadMode) updateRowUI(task); task._lastUiTime = now; } }); task.progress = 100; } else { task.message = L.msg_task_init_part; if (S.uploadMode) updateRowUI(task); const initRes = await ossRequest('POST', 'uploads', null, ''); const initXml = new DOMParser().parseFromString(initRes.responseText, "text/xml"); const uploadId = initXml.querySelector('UploadId')?.textContent || initXml.getElementsByTagName('UploadId')[0]?.textContent; if (!uploadId) throw new Error("Failed to get UploadId"); const parts = new Array(partCount); const CONCURRENCY = 3; let completedBytes = 0; const activeParts = new Map(); const updateProgress = () => { let activeTotal = 0; for (const bytes of activeParts.values()) activeTotal += bytes; const currentTotal = Math.min(totalSize, completedBytes + activeTotal); task._totalUploadedBytes = currentTotal; task.progress = (currentTotal / totalSize) * 100; const now = Date.now(); if (now - task._lastUiTime > 100) { if (S.uploadMode) updateRowUI(task); task._lastUiTime = now; } }; const pool = Array.from({length: partCount}, (_, k) => k + 1); const worker = async () => { while (pool.length > 0) { if (task.status === 'PAUSED' || !document.body.contains(el)) throw new Error("Aborted"); const i = pool.shift(); const startByte = (i - 1) * PART_SIZE; const endByte = Math.min(i * PART_SIZE, totalSize); const chunk = task.file.slice(startByte, endByte); activeParts.set(i, 0); const query = `partNumber=${i}&uploadId=${uploadId}`; const partRes = await ossRequest('PUT', query, chunk, 'application/octet-stream', (pe) => { activeParts.set(i, pe.loaded); updateProgress(); }); const finalizedSize = chunk.size; completedBytes += finalizedSize; activeParts.delete(i); updateProgress(); const etagHeader = partRes.responseHeaders.match(/etag:\s*"?([^"\r\n]+)"?/i); const etag = etagHeader ? etagHeader[1] : null; if (!etag) throw new Error(`Part ${i} missing ETag`); parts[i - 1] = { partNumber: i, etag: etag }; } }; task.message = L.msg_task_uploading_2; if (S.uploadMode) updateRowUI(task); const workers = Array(Math.min(partCount, CONCURRENCY)).fill(0).map(worker); await Promise.all(workers); if (task._deleted) throw new Error("Aborted"); const xmlBody = `<CompleteMultipartUpload>${parts.map(p => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>${p.etag}</ETag></Part>`).join('')}</CompleteMultipartUpload>`; await ossRequest('POST', `uploadId=${uploadId}`, xmlBody, 'application/xml'); if (task._deleted && task.file_id && task._deleteFileIntent) { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [task.file_id] }) }).catch(()=>{}); throw new Error("Aborted"); } task.progress = 100; task._totalUploadedBytes = totalSize; setTimeout(() => updateQuotaUI(), 1500); const tryFetchMeta = async (isRetry = false) => { if (task._deleted) return true; try { const meta = await apiGet(task.file_id); if (meta) { if (meta.icon_link) task.icon_link = meta.icon_link; if (meta.mime_type) task.mime_type = meta.mime_type; if (meta.medias) task.medias = meta.medias; const isValidThumb = meta.thumbnail_link && meta.thumbnail_link !== meta.icon_link; if (isValidThumb) { const testUrl = meta.thumbnail_link + (meta.thumbnail_link.includes('?') ? '&' : '?') + '_t=' + Date.now(); const isImageReady = await new Promise(resolve => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = testUrl; }); if (isImageReady) { task.thumbnail_link = testUrl; if (typeof globalCache !== 'undefined') { const pid = task.parentId === 'root' ? '' : (task.parentId || ''); if (globalCache.has(pid)) { const list = globalCache.get(pid); const target = Array.isArray(list) ? list.find(f => f.id === task.file_id) : (list.items ? list.items.find(f => f.id === task.file_id) : null); if (target) target.thumbnail_link = testUrl; } } if (isRetry && S.uploadMode) { requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); console.log(`[Upload] Cover synced globally: ${task.name}`); } return true; } else { console.log(`[Upload] Cover CDN not ready (404). Retrying later...`); return false; } } } } catch(e) { console.warn("[Upload] Meta fetch warning", e); } return false; }; (async () => { if (await tryFetchMeta(false)) return; for (let i = 0; i < 8; i++) { await sleep(3500); if (await tryFetchMeta(true)) return; } for (let i = 0; i < 20; i++) { await sleep(15000); if (await tryFetchMeta(true)) return; } console.log(`[Upload] Entering infinite polling mode for: ${task.name}`); while (true) { await sleep(60000); if (await tryFetchMeta(true)) return; } })(); } task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_upload_done; if (S.uploadMode) { updateRowUI(task); requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); } if (typeof globalCache !== 'undefined') { const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (globalCache.has(targetPid)) { const cacheEntry = globalCache.get(targetPid); const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []); const newFileStub = { id: task.file_id, kind: 'drive#file', name: task.name, size: task.size, parent_id: targetPid, mime_type: task.mime_type || '', thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link, modified_time: new Date().toISOString(), hash: hash }; if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub); } } if (typeof globalDirtyFolders !== 'undefined') { const targetPid = task.parentId === 'root' ? '' : (task.parentId || ''); globalDirtyFolders.add(targetPid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } } catch (e) { const isManualAbort = e.message === 'Aborted'; task.status = isManualAbort ? 'PAUSED' : 'ERROR'; task.message = isManualAbort ? L.msg_task_paused : (e.message || L.err_unknown); if (S.uploadMode) updateRowUI(task); } finally { clearInterval(speedTimer); task._xhr = null; S.upMng.running--; if (S.uploadMode) { refresh(); } S.upMng.scheduler(); } }, pause: (task, skipRender = false) => { if (task.status === 'UPLOADING' && task._xhr) { task._xhr.abort(); } else if (task.status === 'WAITING') { task.status = 'PAUSED'; task.message = L.msg_task_paused; if (S.uploadMode && !skipRender) { refresh(); } } }, resume: (task, skipRender = false) => { if (task.status === 'PAUSED' || task.status === 'ERROR') { task.status = 'WAITING'; task.message = L.msg_task_waiting; S.upMng.scheduler(); if (S.uploadMode && !skipRender) { refresh(); } } } }; const handleUploadInput = async (files) => { if (!files || files.length === 0) return; if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); const curPath = S.path[S.path.length - 1]; const isVirtual = curPath.id.startsWith('virtual_') || curPath.id.includes('_root') || curPath.id === 'upload_root'; const safeParentId = (curPath.id && !isVirtual) ? curPath.id : ''; let addedCount = 0; const processEntry = async (entry, parentId) => { if (entry.isFile) { return new Promise(resolve => { entry.file(file => { if (file.name.startsWith('.')) return resolve(); if (S.upMng) { const task = S.upMng.createTask(file, parentId); S.uploadTasks.push(task); addedCount++; } resolve(); }); }); } else if (entry.isDirectory) { } }; const fileList = Array.from(files); for (const file of fileList) { if (file.name.startsWith('.')) continue; let relativeFolder = ""; if (file.webkitRelativePath) { const parts = file.webkitRelativePath.split('/'); if (parts.length > 1) { parts.pop(); relativeFolder = parts.join('/'); } } if (S.upMng) { const task = S.upMng.createTask(file, safeParentId); task.relativeFolder = relativeFolder; S.uploadTasks.unshift(task); addedCount++; } } showToast(L.msg_task_added.replace('{n}', addedCount)); if (!S.uploadMode) { switchTab('upload'); } else { renderVisible(); updateStat(); } if (S.upMng) S.upMng.scheduler(); UI.inpFile.value = ''; UI.inpFolder.value = ''; }; UI.inpFile.onchange = (e) => handleUploadInput(e.target.files); UI.inpFolder.onchange = (e) => handleUploadInput(e.target.files); const dragMask = document.createElement('div'); dragMask.className = 'pk-drag-mask'; UI.win.appendChild(dragMask); const parseEntries = async (entries, relPath = "") => { for (const entry of entries) { if (entry.isFile) { const file = await new Promise(res => entry.file(res)); const curPath = S.path[S.path.length - 1]; const safeParentId = (curPath.id && !curPath.id.includes('_root')) ? curPath.id : ''; const task = S.upMng.createTask(file, safeParentId); task.relativeFolder = relPath; S.uploadTasks.unshift(task); } else if (entry.isDirectory) { const reader = entry.createReader(); const subEntries = await new Promise(res => reader.readEntries(res)); await parseEntries(subEntries, (relPath ? relPath + "/" : "") + entry.name); } } }; const canDragUpload = () => { return !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.uploadMode; }; let dragCounter = 0; const handleDragGuard = (e) => { e.preventDefault(); e.stopPropagation(); if (canDragUpload()) { e.dataTransfer.dropEffect = 'copy'; } else { e.dataTransfer.dropEffect = 'none'; } }; el.addEventListener('dragenter', (e) => { handleDragGuard(e); if (!canDragUpload()) return; dragCounter++; const curPath = S.path[S.path.length - 1]; const isRoot = S.path.length === 1 && (curPath.id === '' || curPath.id === 'root'); let destHtml = ""; if (isRoot) { const homeIcon = CONF.icons.home.replace('width="24"', 'width="16"').replace('height="24"', 'height="16"').replace('<svg', '<svg style="vertical-align:-3px;margin-right:4px;"'); destHtml = `${homeIcon}${L.btn_nav_home}`; } else { destHtml = esc(curPath.name); } dragMask.innerHTML = ` <div class="pk-drag-icon"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div> <div class="pk-drag-hint">${L.msg_drag_drop_hint}</div> <div class="pk-drag-path">${L.lbl_upload_to}<span style="color:var(--pk-pri);font-weight:600;display:inline-flex;align-items:center;">${destHtml}</span></div> `; dragMask.style.display = 'flex'; }); el.addEventListener('dragover', handleDragGuard); el.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); if (!canDragUpload()) return; dragCounter--; if (dragCounter <= 0) { dragMask.style.display = 'none'; dragCounter = 0; } }); el.addEventListener('drop', async (e) => { handleDragGuard(e); if (!canDragUpload()) return; dragMask.style.display = 'none'; dragCounter = 0; const items = e.dataTransfer.items; if (!items) return; if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); const entries =[]; for (let i = 0; i < items.length; i++) { const entry = items[i].webkitGetAsEntry(); if (entry) entries.push(entry); } if (entries.length > 0) { await parseEntries(entries); if (!S.uploadMode) switchTab('upload'); else { refresh(); updateStat(); } if (S.upMng) S.upMng.scheduler(); } }); document.addEventListener('click', (e) => { if (UI.uploadWrap && !UI.uploadWrap.contains(e.target)) { UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none'; UI.uploadWrap.classList.remove('active'); } }); } const switchTab = (mode) => { if (S.abortController) S.abortController.abort(); activeLoadId++; S.sortId++; if (S.loading) { S.loading = false; } S.items = []; S.display = []; S.recentResultItems = null; S.itemMap.clear(); S.sel.clear(); if (UI.in) UI.in.innerHTML = ''; if (UI.vp) UI.vp.scrollTop = 0; S.sort = (mode === 'history') ? 'play_time' : ((mode === 'offline') ? 'created_time' : 'modified_time'); if (mode === 'recent' || mode === 'history') { S.dir = 1; } S.dir = (mode === 'recent' || mode === 'offline') ? 1 : S.dir; S.folderFirst = false; if (S.renderFolderFirst) S.renderFolderFirst(); if (UI.chkGlobal && !S.trashMode && !S.shareMode && !S.starredMode && !S.offlineMode && !S.historyMode && !S.recentMode && !S.uploadMode) { S.wasGlobalChecked = UI.chkGlobal.checked; } S.trashMode = (mode === 'trash'); S.shareMode = (mode === 'share'); S.starredMode = (mode === 'starred'); S.recentMode = (mode === 'recent'); S.historyMode = (mode === 'history'); S.offlineMode = (mode === 'offline'); S.uploadMode = (mode === 'upload'); S.scanFilter = null; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; let rootName = L.btn_nav_home; if (S.trashMode) rootName = L.btn_nav_trash; if (S.shareMode) rootName = L.btn_nav_share; if (S.starredMode) rootName = L.btn_nav_starred; if (S.recentMode) rootName = L.btn_nav_recent; if (S.historyMode) rootName = L.btn_nav_history; if (S.offlineMode) rootName = L.title_offline; if (S.uploadMode) rootName = L.btn_nav_upload; if (S.offlineMode) { S.path = [{ id: 'offline_root', name: rootName }]; } else if (S.uploadMode) { S.path = [{ id: 'upload_root', name: rootName }]; } else if (S.recentMode) { S.path = [{ id: 'recent_root', name: rootName }]; } else if (S.historyMode) { S.path = [{ id: 'history_root', name: rootName }]; } else { S.path = [{ id: '', name: rootName }]; } UI.btnNavHome.classList.toggle('act', mode === 'home'); UI.btnNavTrash.classList.toggle('act', mode === 'trash'); if (UI.btnNavShare) UI.btnNavShare.classList.toggle('act', mode === 'share'); if (UI.btnNavStarred) UI.btnNavStarred.classList.toggle('act', mode === 'starred'); if (UI.btnNavRecent) UI.btnNavRecent.classList.toggle('act', mode === 'recent'); if (UI.btnNavHistory) UI.btnNavHistory.classList.toggle('act', mode === 'history'); if (UI.btnNavOffline) UI.btnNavOffline.classList.toggle('act', mode === 'offline'); if (UI.btnNavUpload) UI.btnNavUpload.classList.toggle('act', mode === 'upload'); if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.actionBar) UI.actionBar.style.display = 'flex'; if(UI.trashBar) UI.trashBar.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if(UI.crumb) { UI.crumb.style.opacity = '1'; UI.crumb.style.display = 'flex'; } if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; const stdBtns = [UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnBlacklistManager]; const shareBtns = [document.getElementById('pk-cancel-share')]; const upBtns = [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll]; const upSep = document.getElementById('pk-up-sep'); upBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(upSep) upSep.style.display = 'none'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; if (S.historyMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; [UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.scan) UI.scan.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.searchInput && UI.searchInput.parentNode) { UI.searchInput.parentNode.style.display = 'flex'; } if (UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; } else if (S.starredMode || S.recentMode) { UI.win.classList.remove('pk-mode-trash'); [UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; } else if (S.shareMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; } else if (S.offlineMode) { UI.win.classList.remove('pk-mode-trash'); [UI.btnNewFolder, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip].forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnDel, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; } else if (S.uploadMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(upSep) upSep.style.display = 'block'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex';[UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; } else if (S.trashMode) { UI.win.classList.add('pk-mode-trash'); if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.actionBar) UI.actionBar.style.display = 'none'; if(UI.trashBar) UI.trashBar.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; } else { UI.win.classList.remove('pk-mode-trash'); [UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if(UI.scan) UI.scan.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (UI.chkGlobal && typeof S.wasGlobalChecked !== 'undefined') { UI.chkGlobal.checked = S.wasGlobalChecked; } } if (S.starredMode || S.offlineMode || S.uploadMode) { if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; } S.clearSelection(); const isOffline = mode === 'offline'; const isRecent = mode === 'recent'; const realKey = S.getRealCacheKey(isOffline ? 'offline_root' : (isRecent ? 'recent_root' : '')); const hasCache = typeof globalCache !== 'undefined' && globalCache.has(realKey); const session = typeof globalCache !== 'undefined' ? globalCache.get('offline_session') : null; const isResumingOffline = isOffline && session && !session.completed; const recentCache = isRecent && hasCache ? globalCache.get(realKey) : null; const isResumingRecent = isRecent && recentCache && !Array.isArray(recentCache) && recentCache.nextToken; if (!((isOffline && (hasCache || isResumingOffline)) || (isRecent && (hasCache || isResumingRecent)))) { setLoad(true, true); } load(false, !(isOffline || isRecent)); if (window.pkSmartRefreshTrigger) { setTimeout(() => window.pkSmartRefreshTrigger(isOffline || isRecent), 100); } }; const btnCloud = el.querySelector('#pk-btn-cloud'); const showFolderSelector = (initialId, onConfirm, initialPath = null, fileFilter = null, customTitle = null) => { let currentPath = initialPath ? JSON.parse(JSON.stringify(initialPath)) : [{ id: '', name: L.picker_all }]; if (currentPath.length > 0) currentPath[0].name = L.picker_all; let currentList = []; let selectedFile = null; let sortMode = 'new'; const titleStr = customTitle || (fileFilter ? L.title_select_file : L.picker_title); const picker = showModal(` <div style="display:flex; flex-direction:column; height:480px; width:500px; max-width:90vw;"> <div style="padding:0 0 20px 0; display:flex; justify-content:space-between; align-items:center; flex-shrink:0;"> <h3 style="margin:0; font-size:18px; font-weight:700; color:var(--pk-fg); border:none;">${titleStr}</h3> <div id="pk_picker_close_btn" style="cursor:pointer; color:var(--pk-icon-c); display:flex; align-items:center; justify-content:center; padding:4px; border-radius:4px; transition:background 0.2s;"> ${CONF.icons.close} </div> </div> <div style="display:flex; align-items:center; gap:10px; margin-bottom:10px; height:32px;"> <div id="pk_picker_crumb" class="pk-no-scrollbar" style="flex:1; overflow-x:auto; overflow-y:hidden; white-space:nowrap; display:flex; align-items:center; font-size:14px; color:#666; height:100%; mask-image: linear-gradient(to right, transparent 0%, black 10px, black 95%, transparent 100%); -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10px, black 95%, transparent 100%); scroll-behavior: smooth;"></div> <div style="position:relative; flex-shrink:0;"> <div id="pk_sort_trigger" style="cursor:pointer; font-size:13px; color:var(--pk-fg); font-weight:500; display:flex; align-items:center; gap:4px; user-select:none; padding:4px 8px; border-radius:6px; background:transparent; transition:background 0.2s;"> <span id="pk_sort_txt">${L.picker_sort_new}</span> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="opacity:0.6;"><polyline points="6 9 12 15 18 9"/></svg> </div> <div id="pk_sort_menu" style="display:none; position:absolute; top:100%; right:0; background:var(--pk-bg); border:1px solid var(--pk-bd); border-radius:6px; box-shadow:0 4px 15px rgba(0,0,0,0.2); z-index:20; min-width:125px; overflow:hidden; margin-top:4px;"> <div class="pk-sort-opt" data-val="az" style="padding:8px 12px; font-size:14px; cursor:pointer; color:var(--pk-fg); display:flex; align-items:center; gap:8px;">${CONF.crumbIcons.sortAZ}<span>A-Z</span></div> <div class="pk-sort-opt" data-val="za" style="padding:8px 12px; font-size:14px; cursor:pointer; color:var(--pk-fg); display:flex; align-items:center; gap:8px;">${CONF.crumbIcons.sortZA}<span>Z-A</span></div> <div class="pk-sort-opt" data-val="new" style="padding:8px 12px; font-size:14px; cursor:pointer; color:var(--pk-fg); display:flex; align-items:center; gap:8px;">${CONF.crumbIcons.sortNew}<span>${L.picker_sort_new}</span></div> <div class="pk-sort-opt" data-val="old" style="padding:8px 12px; font-size:14px; cursor:pointer; color:var(--pk-fg); display:flex; align-items:center; gap:8px;">${CONF.crumbIcons.sortOld}<span>${L.picker_sort_old}</span></div> </div> </div> </div> <div id="pk_picker_list" class="pk-scroll" style="flex:1; overflow-y:auto; padding:0; border-top:1px solid var(--pk-bd);"> <div style="text-align:center; padding:20px; color:#999;">${L.loading}</div> </div> <div style="padding-top:15px; border-top:1px solid var(--pk-bd); display:flex; justify-content:space-between; align-items:center; flex-shrink:0;"> <div id="pk_picker_new" style="cursor:pointer; color:var(--pk-pri); display:${fileFilter ? 'none' : 'flex'}; align-items:center; gap:6px; font-size:14px; font-weight:500;"> ${CONF.icons.newfolder} <span>${L.picker_new}</span> </div> <div style="display:flex; gap:10px; margin-left:auto;"> <button class="pk-btn" id="pk_picker_cancel" style="border:none; background:transparent;">${L.btn_cancel}</button> <button class="pk-btn pri" id="pk_picker_ok" style="padding:0 24px; border-radius:6px;">${L.btn_ok}</button> </div> </div> </div> `); const mContent = picker.querySelector('.pk-modal'); mContent.style.padding = '20px'; mContent.style.width = 'fit-content'; picker.querySelector('.pk-modal-close').style.display = 'none'; const crumbEl = picker.querySelector('#pk_picker_crumb'); const listEl = picker.querySelector('#pk_picker_list'); listEl.onscroll = () => { const oldPop = document.querySelector('.pk-crumb-pop'); if (oldPop && typeof oldPop._cleanup === 'function') oldPop._cleanup(); }; const sortTrigger = picker.querySelector('#pk_sort_trigger'); const sortMenu = picker.querySelector('#pk_sort_menu'); const sortTxt = picker.querySelector('#pk_sort_txt'); const closeBtn = picker.querySelector('#pk_picker_close_btn'); closeBtn.onmouseover = () => closeBtn.style.background = 'var(--pk-hl)'; closeBtn.onmouseout = () => closeBtn.style.background = 'transparent'; closeBtn.onclick = () => picker.remove(); const applySort = () => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); currentList.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1; const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortMode === 'az') return collator.compare(a.name, b.name); if (sortMode === 'za') return collator.compare(b.name, a.name); if (sortMode === 'new') return new Date(b.modified_time) - new Date(a.modified_time); if (sortMode === 'old') return new Date(a.modified_time) - new Date(b.modified_time); return 0; }); }; const renderList = () => { listEl.innerHTML = ''; if (currentList.length === 0) { listEl.innerHTML = `<div class="pk-empty" style="padding-bottom:0;position:relative;height:100%;justify-content:center;">${CONF.emptySVG}<div class="pk-empty-txt" style="margin-top:10px;">${L.str_no_files}</div></div>`; return; } currentList.forEach(item => { const div = document.createElement('div'); div.style.cssText = "display:flex; align-items:center; padding:10px 8px; cursor:pointer; border-radius:6px; transition:background 0.1s; border-bottom:1px dashed var(--pk-bd);"; const isDir = item.kind === 'drive#folder'; const isSelected = selectedFile && selectedFile.id === item.id; let checkHtml = ""; if (!isDir && fileFilter) { checkHtml = `<input type="checkbox" ${isSelected ? 'checked' : ''} style="margin-right:12px; width:16px; height:16px; accent-color:var(--pk-pri); cursor:pointer;">`; } 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 ? `<img src="${iconSrc}" style="width:24px;height:24px;object-fit:contain;flex-shrink:0;margin-right:10px;">` : `<div style="margin-right:10px; display:flex; color:#FFC107;">${fallbackSvg}</div>`; } else { const fallbackSvg = getIcon(item).replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"'); iconHtml = iconSrc ? `<img src="${iconSrc}" style="width:24px;height:24px;object-fit:contain;flex-shrink:0;margin-right:10px;">` : `<div style="margin-right:10px; display:flex; color:#888;">${fallbackSvg}</div>`; } div.innerHTML = ` ${checkHtml} ${iconHtml} <div style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; color:var(--pk-fg); font-weight:${isSelected?'bold':'normal'};">${esc(item.name)}</div> ${!isDir ? `<div style="font-size:12px;color:#999;margin-left:8px;">${fmtSize(item.size)}</div>` : ''} `; div.onmouseover = () => div.style.background = 'var(--pk-hl)'; div.onmouseout = () => div.style.background = 'transparent'; if (isSelected) div.style.background = 'var(--pk-sel-bg)'; div.onclick = (e) => { if (isDir) { selectedFile = null; loadFolder(item.id, item.name); } else if (fileFilter) { selectedFile = (selectedFile && selectedFile.id === item.id) ? null : item; renderList(); } }; listEl.appendChild(div); }); }; let _pickerCrumbIdx = 0; let _lastPickerScroll = 0; const showPickerDropdown = async (e, parentId, triggerEl) => { const old = document.querySelector('.pk-crumb-pop'); if (old) { const wasSame = old._sourceEl === triggerEl; if (typeof old._cleanup === 'function') old._cleanup(); if (wasSame) return; } triggerEl.style.background = 'transparent'; triggerEl.innerHTML = CONF.crumbIcons.down; const svgD = triggerEl.querySelector('svg'); if (svgD) { svgD.style.width = '14px'; svgD.style.height = '14px'; svgD.style.display = 'block'; } const pop = document.createElement('div'); pop.className = 'pk-crumb-pop pk-scroll pk-show'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.style.zIndex = '2147483647'; pop._sourceEl = triggerEl; document.body.appendChild(pop); const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; pop.style.zoom = scale; const rect = triggerEl.getBoundingClientRect(); pop.style.top = ((rect.bottom / scale) + 5) + 'px'; pop.style.left = (rect.left / scale) + 'px'; const cleanup = () => { if (pop.parentNode) pop.remove(); triggerEl.innerHTML = CONF.crumbIcons.right; const svgR = triggerEl.querySelector('svg'); if (svgR) { svgR.style.width = '14px'; svgR.style.height = '14px'; svgR.style.display = 'block'; svgR.style.opacity = '0.6'; } document.removeEventListener('mousedown', closer); window.removeEventListener('resize', cleanup); }; pop._cleanup = cleanup; const closer = (ev) => { if (!pop.contains(ev.target) && ev.target !== triggerEl) cleanup(); }; document.addEventListener('mousedown', closer); window.addEventListener('resize', cleanup); const cacheKey = parentId || 'root'; let folders = null; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) { const raw = globalCache.get(cacheKey); if (Array.isArray(raw) || (raw && raw.items && !raw.nextToken)) { const items = Array.isArray(raw) ? raw : raw.items; folders = items.filter(f => f.kind === 'drive#folder'); } } const renderMenu = (list) => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); list.sort((a, b) => { const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortMode === 'new') return new Date(b.modified_time || 0) - new Date(a.modified_time || 0); return collator.compare(a.name, b.name); }); if (list.length === 0) { cleanup(); return; } pop.innerHTML = ''; list.forEach(f => { const itemDiv = document.createElement('div'); itemDiv.className = 'pk-crumb-item'; const iconSrc = f.icon_link || ''; const fallbackSvg = CONF.typeIcons.folder.replace(/width="\d+"/, 'width="18"').replace(/height="\d+"/, 'height="18"'); const iconHtml = iconSrc ? `<img src="${iconSrc}" style="width:18px;height:18px;object-fit:contain;flex-shrink:0;" onerror="this.style.display='none';this.nextElementSibling.style.display='inline-flex';"><span style="display:none;align-items:center;flex-shrink:0;">${fallbackSvg}</span>` : fallbackSvg; itemDiv.innerHTML = `${iconHtml}<span>${esc(f.name)}</span>`; 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 = `<div style="padding:10px; display:flex; justify-content:center;"><div class="pk-spin-lg" style="width:16px; height:16px; border-width:2px;"></div></div>`; 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('<svg', '<svg style="width:15px;height:15px;margin-right:4px;"'); sp.innerHTML = `${homeIcon}${L.btn_nav_home}`; sp.title = L.picker_all; } else { sp.textContent = p.name; } sp.style.cssText = "display:flex; align-items:center; height:100%; padding:0 6px; border-radius:4px; flex-shrink:0; transition:background 0.2s; white-space:nowrap;"; sp.style.cursor = isLast ? 'default' : 'pointer'; sp.style.color = isLast ? 'var(--pk-fg)' : '#888'; sp.style.fontWeight = isLast ? 'bold' : 'normal'; if (!isLast) { sp.onclick = () => { currentPath = currentPath.slice(0, i + 1); loadFolder(p.id, null, true); }; sp.onmouseover = () => sp.style.background = 'var(--pk-hl)'; sp.onmouseout = () => sp.style.background = 'transparent'; } crumbEl.appendChild(sp); if (!isLast) { const sep = document.createElement('span'); sep.className = 'pk-picker-arrow'; sep.innerHTML = CONF.crumbIcons.right; const svg = sep.querySelector('svg'); if(svg) { svg.style.width = '14px'; svg.style.height = '14px'; svg.style.display = 'block'; svg.style.opacity = '0.6'; } sep.style.cssText = "margin:0 2px; display:flex; align-items:center; cursor:pointer; padding:4px; border-radius:4px; transition:all 0.2s; flex-shrink:0; color:var(--pk-icon-c);"; sep.onmouseover = () => { sep.style.background = 'var(--pk-hl)'; sep.style.color = 'var(--pk-pri)'; if(svg) svg.style.opacity='1'; }; sep.onmouseout = () => { sep.style.background = 'transparent'; sep.style.color = 'var(--pk-icon-c)'; if(svg) svg.style.opacity='0.6'; }; sep.onclick = (e) => { e.stopPropagation(); if (typeof showPickerDropdown === 'function') showPickerDropdown(e, p.id, sep); }; crumbEl.appendChild(sep); } }); _pickerCrumbIdx = currentPath.length - 1; requestAnimationFrame(() => { crumbEl.scrollLeft = crumbEl.scrollWidth; }); }; const handlePickerWheel = (e) => { e.preventDefault(); document.querySelectorAll('.pk-crumb-pop').forEach(p => { if (typeof p._cleanup === 'function') p._cleanup(); }); const now = Date.now(); if (now - _lastPickerScroll < 120) return; _lastPickerScroll = now; const nodes = Array.from(crumbEl.children).filter(c => !c.classList.contains('pk-picker-arrow')); if (!nodes.length) return; if (e.deltaY < 0) _pickerCrumbIdx = Math.max(0, _pickerCrumbIdx - 1); else _pickerCrumbIdx = Math.min(nodes.length - 1, _pickerCrumbIdx + 1); const target = nodes[_pickerCrumbIdx]; if (target) { const centerOffset = target.offsetLeft + (target.offsetWidth / 2) - (crumbEl.clientWidth / 2); crumbEl.scrollTo({ left: centerOffset, behavior: 'smooth' }); } }; crumbEl.addEventListener('wheel', handlePickerWheel, { passive: false }); const loadFolder = async (id, name, isBack = false) => { listEl.innerHTML = `<div style="display:flex;justify-content:center;padding:20px;"><div class="pk-spin-lg" style="width:24px;height:24px;border-width:2px;"></div></div>`; if (name && !isBack) currentPath.push({ id, name }); renderCrumb(); const cacheKey = id || 'root'; let items = null; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) { const raw = globalCache.get(cacheKey); const isComplete = Array.isArray(raw) || (raw && raw.items && !raw.nextToken); if (isComplete) { items = Array.isArray(raw) ? raw : raw.items; } } try { if (items === null) { items = await apiList(id || '', 1000); if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, items); indexParents(id, name || L.picker_all, items); } currentList = items.filter(i => { if (i.kind === 'drive#folder') return true; if (fileFilter && fileFilter(i)) return true; return false; }); applySort(); renderList(); } catch (e) { listEl.innerHTML = `<div style="color:#d93025; text-align:center; padding:20px;">${L.str_error}: ${esc(e.message)}</div>`; } }; picker.querySelector('#pk_picker_cancel').onclick = () => picker.remove(); picker.querySelector('#pk_picker_ok').onclick = () => { const cur = currentPath[currentPath.length - 1]; const returnPathChain = JSON.parse(JSON.stringify(currentPath)); if (fileFilter && selectedFile) { onConfirm(selectedFile.id, selectedFile.name, selectedFile, returnPathChain); } else { onConfirm(cur.id, cur.name, null, returnPathChain); } picker.remove(); }; if (!fileFilter) { picker.querySelector('#pk_picker_new').onclick = async () => { const cur = currentPath[currentPath.length - 1]; const name = await showPrompt(L.msg_newfolder_prompt, '', L.picker_new); if (name) { try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: 'drive#folder', parent_id: cur.id || '', name: name }) }); if (!res.ok) throw new Error("Create Failed"); const data = await res.json(); const newFolder = data.file || data; const parentId = cur.id || 'root'; if (typeof globalCache !== 'undefined') globalCache.delete(parentId); if (S.cache) S.cache.delete(parentId); globalDirtyFolders.add(parentId); gmSet('pk_fmod_' + parentId, new Date(getServerNow()).toISOString()); if (newFolder && newFolder.id) await loadFolder(newFolder.id, newFolder.name); else await loadFolder(cur.id, null, true); } catch(e) { showAlert(e.message); } } }; } sortTrigger.onclick = (e) => { e.stopPropagation(); const isOpening = sortMenu.style.display !== 'block'; sortMenu.style.display = isOpening ? 'block' : 'none'; sortTrigger.style.background = isOpening ? 'var(--pk-hl)' : 'transparent'; }; picker.querySelectorAll('.pk-sort-opt').forEach(el => { el.onclick = () => { sortMode = el.dataset.val; sortTxt.textContent = el.textContent; sortMenu.style.display = 'none'; sortTrigger.style.background = 'transparent'; applySort(); renderList(); }; el.onmouseover = () => { el.style.background = 'var(--pk-hl)'; el.style.color = 'var(--pk-pri)'; }; el.onmouseout = () => { el.style.background = 'transparent'; el.style.color = 'var(--pk-fg)'; }; }); const closeSortMenu = () => { if (sortMenu) sortMenu.style.display = 'none'; if (sortTrigger) sortTrigger.style.background = 'transparent'; }; setTimeout(() => document.addEventListener('click', closeSortMenu), 0); const _orgRemove = picker.remove.bind(picker); picker.remove = () => { document.removeEventListener('click', closeSortMenu); _orgRemove(); }; loadFolder(initialId || '', null, true); }; if (btnCloud) { btnCloud.onclick = () => { const curFolder = S.path[S.path.length - 1]; const isVirtual = curFolder.id.startsWith('virtual_') || curFolder.id.includes('_root') || curFolder.id === 'analyze_root'; const isHomeSubDir = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.isFlattened && !S.dupMode && S.path.length > 1 && !isVirtual; let saveToId = isHomeSubDir ? (curFolder.id || '') : ''; let saveToName = isHomeSubDir ? (curFolder.name || L.lbl_default_folder) : L.lbl_default_folder; let currentSavePath = isHomeSubDir ? S.path.filter(p => !p.id.startsWith('virtual_')) : null; const m = showModal(` <div style="padding: 10px 5px 0 5px; display: flex; flex-direction: column; gap: 20px;"> <h3 style="border:none; margin:0; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.title_cloud_task}</h3> <textarea class="pk-cloud-area pk-scroll" id="pk_cloud_input" placeholder="${L.ph_cloud_links}"></textarea> <div style="display:flex; justify-content:space-between; align-items:center; margin-top:-8px; margin-bottom:-4px;"> <label style="display:flex; align-items:center; cursor:pointer; font-size:12px; color:var(--pk-fg); user-select:none; opacity:0.8; transition:opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.8"> <input type="checkbox" id="pk_cloud_smart_fix" checked style="accent-color:var(--pk-pri); margin-right:6px; width:14px; height:14px; cursor:pointer;"> ${L.lbl_smart_fix} </label> <div id="pk_cloud_error" style="display:none; color:#d93025; font-size:13px; font-weight:600;">${L.err_invalid_links}</div> </div> <div id="pk_cloud_dest_row" style="display:flex; align-items:flex-end; gap:8px; font-size:14px; color:var(--pk-fg); line-height:1;"> <span style="opacity:0.9; margin-bottom:1px;">${L.lbl_save_to}</span> <div style="display:flex; align-items:flex-end; gap:5px;"> <div style="line-height:0; transform:translateY(2px);"> ${CONF.typeIcons.folder.replace('width="30"', 'width="20"').replace('height="30"', 'height="20"')} </div> <span id="pk_cloud_dir_name" style="font-weight:600; max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-bottom:1px;">${saveToName}</span> <div style="display:flex; color:#aaa; transition:color 0.2s; margin-bottom:1px;" data-pk-tip="${L.tip_cloud_save_path}" onmouseover="this.style.color=document.querySelector('.pk-ov').classList.contains('pk-dark')?'#ddd':'#666'" onmouseout="this.style.color='#aaa'"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none;"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> </div> <span id="pk_cloud_change_dir" style="color:var(--pk-pri); cursor:pointer; font-size:14px; margin-left:2px; margin-bottom:1px; display:inline-block;">${L.btn_modify}</span> </div> </div> <div class="pk-modal-act" style="margin-top:10px; display:flex; align-items:center; justify-content:space-between; gap:20px;"> <div style="position:relative; flex-shrink:0;"> <span id="pk_cloud_torrent_trigger" style="color:var(--pk-pri); cursor:pointer; font-size:14px; font-weight:500; display:inline-block;">${L.btn_via_torrent}</span> <input type="file" id="pk_cloud_torrent_file" accept=".torrent" multiple style="display:none;"> </div> <div style="display:flex; gap:12px; align-items:center; flex-shrink:0;"> <button class="pk-btn" id="cloud_cancel" style="height:40px; width:110px; border-radius:8px; background:transparent; color:var(--pk-fg); font-weight:600; border:none; font-size:14px; flex-shrink:0; justify-content:center;">${L.btn_cancel}</button> <button class="pk-btn pri" id="cloud_submit" disabled style="height:40px; width:110px; border-radius:8px; background:var(--pk-pri); border:none; color:#fff; font-weight:bold; font-size:14px; display:inline-flex; align-items:center; justify-content:center; gap:6px; transition:all 0.2s; white-space:nowrap; flex-shrink:0;"> <span style="display:block !important; line-height:1;">${L.btn_create_now}</span> </button> </div> </div> </div> `); const modalBox = m.querySelector('.pk-modal'); modalBox.style.width = "560px"; modalBox.style.padding = "30px"; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) { closeBtn.style.top = "36px"; closeBtn.style.right = "30px"; } const input = m.querySelector('#pk_cloud_input'); const submit = m.querySelector('#cloud_submit'); const dirLabel = m.querySelector('#pk_cloud_dir_name'); input.focus(); m.querySelector('#pk_cloud_change_dir').onclick = () => { showFolderSelector(saveToId, (id, name, fullItem, selectedPathChain) => { saveToId = id; saveToName = name; dirLabel.textContent = name; if (selectedPathChain) { currentSavePath = selectedPathChain; } }, currentSavePath); }; const smartFixCheckbox = m.querySelector('#pk_cloud_smart_fix'); if (smartFixCheckbox) smartFixCheckbox.onchange = () => input.dispatchEvent(new Event('input')); const parseAndCleanLinks = (rawString, isSmartFix) => { const formattedVal = rawString.replace(/(https?:\/\/|ftp:\/\/|sftp:\/\/|magnet:\?|ed2k:\/\/|thunder:\/\/)/gi, '\n$1'); const lines = formattedVal.split('\n').map(l => l.trim()).filter(l => l); const uniqueTasks = new Map(); const linkRegex = /^(https?:\/\/|ftp:\/\/|sftp:\/\/|magnet:\?|ed2k:\/\/|thunder:\/\/)/i; const base32ToHex = (b32) => { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bits = ""; const input = b32.toUpperCase(); for (let i = 0; i < input.length; i++) { const val = alphabet.indexOf(input[i]); if (val === -1) return null; bits += val.toString(2).padStart(5, '0'); } let hex = ""; for (let i = 0; i + 4 <= bits.length; i += 4) { const chunk = bits.substring(i, i + 4); const num = parseInt(chunk, 2); if (!isNaN(num)) hex += num.toString(16); } return hex.toUpperCase(); }; const extractHexHash = (url) => { const match = url.match(/urn:btih:([^&]+)/i); if (!match) return null; const hash = match[1].toUpperCase(); if (hash.length === 40) return hash; if (hash.length === 32) return base32ToHex(hash); return null; }; const addResult = (url) => { const hash = extractHexHash(url); if (hash) { if (!uniqueTasks.has(hash)) { uniqueTasks.set(hash, url); } } else { uniqueTasks.set(url, url); } }; for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (linkRegex.test(line)) { addResult(line); continue; } if (isSmartFix) { let cleanedStr = line.replace(/[^a-zA-Z0-9]/g, ''); const hexMatches = cleanedStr.matchAll(/([a-fA-F0-9]{40})/g); for (const match of hexMatches) { addResult(`magnet:?xt=urn:btih:${match[1].toUpperCase()}`); cleanedStr = cleanedStr.replace(match[1], ' '.repeat(40)); } const b32Matches = cleanedStr.matchAll(/([a-zA-Z2-7]{32})/g); for (const match of b32Matches) { const hex = base32ToHex(match[1].toUpperCase()); if (hex) { addResult(`magnet:?xt=urn:btih:${hex}`); } } } } return Array.from(uniqueTasks.values()); }; input.oninput = () => { const rawVal = input.value.trim(); const isSmartFix = smartFixCheckbox ? smartFixCheckbox.checked : false; const linesRaw = rawVal.split('\n').map(l => l.trim()).filter(l => l); const hasInput = linesRaw.length > 0; const finalLinks = parseAndCleanLinks(rawVal, isSmartFix); const allValid = hasInput && (finalLinks.length === linesRaw.length || finalLinks.length > 0); const isError = hasInput && !allValid; m.querySelector('#pk_cloud_error').style.display = isError ? 'block' : 'none'; submit.disabled = !allValid; }; m.querySelector('#cloud_cancel').onclick = () => m.remove(); const torrentTrigger = m.querySelector('#pk_cloud_torrent_trigger'); const torrentFile = m.querySelector('#pk_cloud_torrent_file'); torrentTrigger.onclick = () => torrentFile.click(); const showSnapshotModal = (urlList, defaultId, defaultName) => { return new Promise((resolve) => { let currentSaveId = defaultId; let currentSaveName = defaultName; const displayUrl = urlList[0]; const countSuffix = urlList.length > 1 ? L.str_snap_link_count_suffix.replace('{n}', urlList.length) : ''; const snapIcon = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="3" width="20" height="18" rx="4" fill="#647EFF"/><circle cx="12" cy="12" r="5" stroke="white" stroke-width="2"/><path d="M16 8L18 6" stroke="white" stroke-width="2" stroke-linecap="round"/><path d="M6 18L8 16" stroke="white" stroke-width="2" stroke-linecap="round"/></svg>`; const sm = showModal(` <div style="padding: 10px 5px 0 5px; width: 480px; max-width: 90vw;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> <h3 style="margin: 0; font-size: 18px; font-weight: 700; border: none; color: var(--pk-fg);">${L.title_save_method}</h3> </div> <div style="font-size: 13px; color: #888; margin-bottom: 20px; display: flex; align-items: center; gap: 4px;"> ${L.msg_save_snapshot_desc} <div style="display:flex; cursor:help; color:#888;" data-pk-tip="${L.tip_snapshot_details}"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> </div> </div> <div style="border: 1px solid var(--pk-pri); background: rgba(0, 103, 192, 0.05); border-radius: 8px; padding: 15px; display: flex; align-items: center; gap: 12px; margin-bottom: 25px;"> <div style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> ${snapIcon} </div> <div style="flex: 1; overflow: hidden;"> <div style="font-size: 14px; color: var(--pk-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500;"> ${esc(displayUrl)}${countSuffix} </div> </div> </div> <div style="display:flex; align-items:baseline; gap:10px; font-size:14px; color:var(--pk-fg); margin-bottom: 30px;"> <span style="opacity:0.9;">${L.lbl_save_to}</span> <div style="display:flex; align-items:baseline; gap:6px;"> <span style="display:inline-flex; align-items:center; transform:translateY(5px);"> ${CONF.typeIcons.folder.replace('width="30"', 'width="20"').replace('height="30"', 'height="20"')} </span> <span id="snap_dir_name" style="font-weight:600; max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${esc(currentSaveName)}</span> <span id="snap_change_dir" style="color:var(--pk-pri); cursor:pointer; font-size:14px; margin-left:4px;">${L.btn_modify}</span> </div> </div> <div class="pk-modal-act" style="display: flex; justify-content: flex-end; gap: 12px;"> <button class="pk-btn" id="snap_cancel" style="height:36px; padding:0 20px; border-radius:6px; background:transparent;">${L.btn_cancel}</button> <button class="pk-btn pri" id="snap_save" style="height:36px; padding:0 20px; border-radius:6px; background:var(--pk-pri); color:#fff; font-weight:600; border:none;">${L.btn_save_snapshot}</button> </div> </div> `); const mBox = sm.querySelector('.pk-modal'); if (mBox) { mBox.style.padding = '24px'; mBox.style.width = 'auto'; } const closeBtn = sm.querySelector('.pk-modal-close'); if (closeBtn) { closeBtn.style.top = '30px'; closeBtn.style.right = '24px'; } let snapCurrentPath = currentSavePath; sm.querySelector('#snap_change_dir').onclick = () => { showFolderSelector(currentSaveId, (id, name, fullItem, selectedPathChain) => { currentSaveId = id; currentSaveName = name; sm.querySelector('#snap_dir_name').textContent = name; if (selectedPathChain) { snapCurrentPath = selectedPathChain; currentSavePath = selectedPathChain; saveToId = id; saveToName = name; dirLabel.textContent = name; } }, snapCurrentPath); }; const doClose = (result) => { sm.remove(); resolve(result ? { confirm: true, targetId: currentSaveId } : { confirm: false }); }; sm.querySelector('#snap_cancel').onclick = () => doClose(false); if (closeBtn) closeBtn.onclick = () => doClose(false); sm.querySelector('#snap_save').onclick = () => doClose(true); sm.tabIndex = 0; setTimeout(() => sm.focus(), 10); sm.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); sm.querySelector('#snap_save').click(); } }); }); }; torrentFile.onchange = async (e) => { const files = e.target.files; if (!files || files.length === 0) return; m.remove(); const fb = FloatBarManager.create(L.str_parsing_torrent); let successCount = 0; let failCount = 0; for (let i = 0; i < files.length; i++) { const file = files[i]; fb.update(`${L.str_parsing_torrent} (${i + 1}/${files.length})`); try { const magnetLink = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async (evt) => { try { const buf = new Uint8Array(evt.target.result); let pos = 0; const decodeSkip = () => { if (pos >= buf.length) return; const c = buf[pos]; if (c === 100 || c === 108) { pos++; while (pos < buf.length && buf[pos] !== 101) decodeSkip(); pos++; } else if (c === 105) { pos++; while (pos < buf.length && buf[pos] !== 101) pos++; pos++; } else if (c >= 48 && c <= 57) { let colon = pos; while (colon < buf.length && buf[colon] !== 58) colon++; const lenStr = new TextDecoder().decode(buf.slice(pos, colon)); const len = parseInt(lenStr, 10); pos = colon + 1 + len; } }; if (buf[0] !== 100) throw new Error(L.err_invalid_torrent); pos = 1; let infoHash = null; while (pos < buf.length && buf[pos] !== 101) { const keyStart = pos; decodeSkip(); let colon = keyStart; while (colon < buf.length && buf[colon] !== 58) colon++; const kLen = parseInt(new TextDecoder().decode(buf.slice(keyStart, colon))); const keyStr = new TextDecoder().decode(buf.slice(colon + 1, colon + 1 + kLen)); if (keyStr === "info") { const infoStart = pos; decodeSkip(); const infoEnd = pos; const infoBuf = buf.slice(infoStart, infoEnd); const hashBuf = await crypto.subtle.digest("SHA-1", infoBuf); infoHash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join(''); break; } else { decodeSkip(); } } if (infoHash) resolve(`magnet:?xt=urn:btih:${infoHash}&dn=${encodeURIComponent(file.name)}`); else reject(new Error(L.err_torrent_no_info)); } catch (err) { reject(err); } }; reader.onerror = () => reject(new Error(L.err_file_read)); reader.readAsArrayBuffer(file); }); fb.update(`${L.msg_creating_cloud_task} (${i + 1}/${files.length})`); let retry = 0; while (retry < 3) { try { await apiAddOfflineTask(magnetLink, saveToId); successCount++; break; } catch (reqErr) { if (reqErr.message && reqErr.message.includes('429')) { retry++; await sleep(2000 * retry); } else { throw reqErr; } } } } catch (e) { console.error(`Torrent parse/upload failed for ${file.name}:`, e); failCount++; } } fb.destroy(); if (failCount > 0) { showToast(L.msg_cloud_task_finish.replace('{s}', successCount).replace('{f}', failCount), 'warning'); } else if (successCount > 0) { showToast(L.msg_cloud_task_success.replace('{n}', successCount)); } if (successCount > 0) { if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; setTimeout(() => updateQuotaUI(), 1000); const curPathId = S.path[S.path.length - 1].id || ''; if (S.offlineMode || (saveToId && curPathId === saveToId)) { load(false, true); } else if (!saveToId && S.path.length === 1 && window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(true); } } }; submit.onclick = async () => { if (submit.disabled) return; const rawVal = input.value.trim(); if (!rawVal) return; const isSmartFix = smartFixCheckbox ? smartFixCheckbox.checked : false; m.style.display = 'none'; const finalLinks = parseAndCleanLinks(rawVal, isSmartFix); const videoPlatformRegex = /(?:youtube\.com|youtu\.be|twitter\.com|x\.com|tiktok\.com|douyin\.com|facebook\.com|fb\.watch|instagram\.com|t\.me|bilibili\.com)/i; const snapshotLinks = finalLinks.filter(l => /^https?:\/\//i.test(l) && !videoPlatformRegex.test(l)); const directLinks = finalLinks.filter(l => !/^https?:\/\//i.test(l) || videoPlatformRegex.test(l)); let snapshotParams = null; const isSingleSnapshotTask = (finalLinks.length === 1 && snapshotLinks.length === 1); if (isSingleSnapshotTask) { const result = await showSnapshotModal(snapshotLinks, saveToId, saveToName); if (!result.confirm) { m.style.display = 'flex'; return; } snapshotParams = { save_as: 'snapshot', targetId: result.targetId }; } m.remove(); const progressTask = FloatBarManager.create(L.msg_creating_cloud_task); let successCount = 0; let failCount = 0; const processQueue =[ ...directLinks.map(u => ({ url: u, isSnap: false })), ...snapshotLinks.map(u => ({ url: u, isSnap: true })) ]; for (let i = 0; i < processQueue.length; i++) { const item = processQueue[i]; progressTask.update(L.str_creating_task_n.replace('{n}', i + 1).replace('{t}', processQueue.length)); try { const pid = (item.isSnap && snapshotParams) ? snapshotParams.targetId : saveToId; const extras = item.isSnap ? { save_as: 'snapshot' } : {}; let retry = 0; while (retry < 3) { try { await apiAddOfflineTask(item.url, pid, extras); successCount++; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; break; } catch (reqErr) { if (reqErr.message && reqErr.message.includes('429')) { retry++; await sleep(2000 * retry); } else { throw reqErr; } } } } catch(e) { console.error(`Task Create Failed[${item.url}]:`, e); failCount++; } await sleep(300); } progressTask.destroy(); if (failCount > 0) { showToast(L.msg_cloud_task_finish.replace('{s}', successCount).replace('{f}', failCount), 'warning'); } else { showToast(L.msg_cloud_task_success.replace('{n}', successCount)); } setTimeout(() => updateQuotaUI(), 1000); const curPathId = S.path[S.path.length - 1].id || ''; if (S.offlineMode || (saveToId && curPathId === saveToId)) { load(false, true); } else if (!saveToId && S.path.length === 1) { if(window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); } }; }; } UI.btnNavHome.onclick = () => switchTab('home'); if(UI.btnNavStarred) UI.btnNavStarred.onclick = () => switchTab('starred'); if(UI.btnNavRecent) UI.btnNavRecent.onclick = () => switchTab('recent'); if(UI.btnNavHistory) UI.btnNavHistory.onclick = () => switchTab('history'); if(UI.btnNavUpload) UI.btnNavUpload.onclick = () => switchTab('upload'); if(UI.btnNavShare) UI.btnNavShare.onclick = () => switchTab('share'); if (UI.btnNavOffline) { UI.btnNavOffline.onclick = () => { switchTab('offline'); }; } UI.btnNavTrash.onclick = () => switchTab('trash'); if (UI.btnTrashRefresh) { UI.btnTrashRefresh.onclick = () => { updateQuotaUI(); load(false, true); }; } UI.btnRestore.onclick = async () => { if (S.sel.size === 0) return; ensureItemMap(); const progressTask = FloatBarManager.create(L.msg_prepare_restore); const updateFloat = progressTask.update; isGUISensitive = true; const ids = Array.from(S.sel); S.sel.clear(); S.lastSelIdx = -1; S.activeId = null; updateStat(); try { const BATCH_SIZE = 500; const total = ids.length; const taskIds =[]; const affectedParentIds = new Set(); const restoredFolders = []; ids.forEach(id => { const item = S.itemMap.get(id); if (item) { if (item.kind === 'drive#folder') restoredFolders.push(item); if (item.parent_id) affectedParentIds.add(item.parent_id); else affectedParentIds.add('root'); } }); updateFloat(L.msg_submit_request.replace('{c}', 0).replace('{t}', total)); for (let i = 0; i < total; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files:batchUntrash`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }) }); if (!res.ok) throw new Error(`Batch Untrash Error ${res.status}`); const data = await res.json(); if (data.task_id) { taskIds.push(data.task_id); } updateFloat(L.msg_submit_request.replace('{c}', Math.min(i + BATCH_SIZE, total)).replace('{t}', total)); await sleep(50); } if (taskIds.length > 0) { updateFloat(L.msg_wait_server.replace('{c}', 0).replace('{t}', taskIds.length)); const pendingTasks = new Set(taskIds); let pollRetries = 0; const maxPollRetries = 120; while (pendingTasks.size > 0 && pollRetries < maxPollRetries) { await sleep(1000); pollRetries++; const currentIdsToCheck = Array.from(pendingTasks).join(','); const filters = { phase: { eq: "PHASE_TYPE_COMPLETE" }, id: { in: currentIdsToCheck } }; const filterStr = encodeURIComponent(JSON.stringify(filters)); const pollUrl = `https://api-drive.mypikpak.com/drive/v1/tasks?with=reference_resource&type=&thumbnail_size=SIZE_SMALL&limit=100&filters=${filterStr}`; try { const tRes = await fetch(pollUrl, { headers: getHeaders() }); if (tRes.ok) { const tData = await tRes.json(); const completedTasks = tData.tasks || []; completedTasks.forEach(t => { if (pendingTasks.has(t.id)) { pendingTasks.delete(t.id); } }); const doneCount = taskIds.length - pendingTasks.size; updateFloat(L.msg_server_processing.replace('{c}', doneCount).replace('{t}', taskIds.length)); } } catch (err) { console.warn("[Restore] Poll failed, retrying...", err); } } if (pendingTasks.size > 0) { console.warn(`[Restore] Timeout waiting for tasks: ${Array.from(pendingTasks)}`); } } const allIdSet = new Set(ids); ids.forEach(id => S.itemMap.delete(id)); S.items = S.items.filter(x => !allIdSet.has(x.id)); ids.forEach(id => { const reviveFromTombstone = (fid) => { if (globalTombstoneCache.has(fid)) { const savedData = globalTombstoneCache.get(fid); globalCache.set(fid, savedData); globalTombstoneCache.delete(fid); const list = Array.isArray(savedData) ? savedData : (savedData.items ||[]); list.forEach(child => { if (child.kind === 'drive#folder') { reviveFromTombstone(child.id); } }); } }; const wipeCrawlerMemory = (fid) => { scannedFolderIds.delete(fid); const children = []; for (const [childId, parentInfo] of globalParentIndex.entries()) { if (parentInfo.id === fid) { children.push(childId); } } children.forEach(childId => wipeCrawlerMemory(childId)); }; reviveFromTombstone(id); wipeCrawlerMemory(id); }); if (typeof globalCache !== 'undefined') { const cleanListChunk = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !allIdSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !allIdSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { if (key && (key.endsWith('_root') || key === 'root_trashed')) { globalCache.set(key, cleanListChunk(globalCache.get(key))); } } for (const key of S.cache.keys()) { if (key && (key.endsWith('_root') || key === 'root_trashed')) { S.cache.set(key, cleanListChunk(S.cache.get(key))); } } } refresh(); affectedParentIds.forEach(pid => { if (pid && pid !== 'root') gmSet('pk_fmod_' + pid, new Date(getServerNow()).toISOString()); }); affectedParentIds.forEach(pid => { const keys = (pid === 'root' || pid === '') ? ['root', ''] : [pid]; keys.forEach(k => { if (typeof globalCache !== 'undefined') globalCache.delete(k); if (S.cache) S.cache.delete(k); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(k); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(k); }); const queueId = (pid === 'root') ? '' : pid; backgroundQueue.unshift({ id: queueId, name: "Refill_Restore", retryCount: 0 }); }); restoredFolders.forEach(folder => { scannedFolderIds.delete(folder.id); backgroundQueue.unshift({ id: folder.id, name: folder.name, retryCount: 0 }); }); runBackgroundCrawler(); if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; showToast(L.msg_restore_done.replace('{n}', total)); } catch(e) { showAlert(`${L.str_error}: ${e.message}`); load(false, true); } finally { if (progressTask) progressTask.destroy(); isGUISensitive = false; } }; UI.btnDelForever.onclick = async () => { if (S.sel.size === 0) return; ensureItemMap(); if (!await showConfirm(L.msg_del_forever_confirm.replace('{n}', S.sel.size))) return; const ids = Array.from(S.sel); await executeBatchDelete(ids, { hardDelete: true, silent: false }); }; UI.btnEmptyTrash.onclick = async () => { if (!await showConfirm(L.msg_empty_trash_confirm)) return; if (S.items.length === 0) return; S._isEmptyingTrash = true; setLoad(true); updateLoadTxt(L.str_deleting); try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files/trash:empty', { method: 'PATCH', headers: getHeaders() }); if (!res.ok) throw new Error(`API Error ${res.status}`); S.items = []; S.display = []; S.itemMap.clear(); S.clearSelection(); if (typeof globalCache !== 'undefined') globalCache.delete('root_trashed'); if (S.cache) S.cache.delete('root_trashed'); refresh(); updateStat(); showToast(L.msg_trash_emptied); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); S._isEmptyingTrash = false; } }; const openSettingsModal = () => { const inputStyle = `width:100%; height:44px; padding:0 15px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; font-weight:600; outline:none; transition:border-color 0.2s; box-sizing:border-box;`; const areaStyle = `width:100%; min-height:60px; max-height:120px; padding:12px 15px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:13px; font-weight:600; outline:none; transition:border-color 0.2s; box-sizing:border-box; resize:vertical; line-height:1.5; font-family:inherit; cursor:auto;`; const labelStyle = `position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; line-height:1; font-size:11px; color:var(--pk-pri); font-weight:bold; pointer-events:none; z-index:1;`; const curLang = gmGet('pk_lang', lang); const curEngine = gmGet('pk_search_engine', 'google'); const curAriaUrl = gmGet('pk_aria2_url', ''); const curAriaToken = gmGet('pk_aria2_token', ''); const curBlur = gmGet('pk_blur_thumb', false); let selectedLang = curLang; let selectedEngine = curEngine; let totalStorageBytes = 0; const keys = typeof GM_listValues !== 'undefined' ? GM_listValues() : Object.keys(localStorage); keys.forEach(k => { if (k.startsWith('pk_')) { const val = typeof GM_getValue !== 'undefined' ? GM_getValue(k) : localStorage.getItem(k); totalStorageBytes += (k.length + JSON.stringify(val || '').length); } }); if (typeof globalCache !== 'undefined') { for (const [k, v] of globalCache.entries()) { try { totalStorageBytes += k.toString().length + JSON.stringify(v).length; } catch(e){} } } const storageDisplay = fmtSize(totalStorageBytes); const m = showModal(` <div style="display:flex; flex-direction:column; height:580px; max-height:75vh; width:420px; max-width:95vw; overflow:hidden; overscroll-behavior:none; position:relative;"> <div style="padding: 30px 30px 15px 30px; flex-shrink:0; transform:translateZ(0);"> <h3 style="margin: 0; font-size: 18px; font-weight: 700; border: none; line-height: 1.2; color: var(--pk-fg);">${L.modal_settings_title}</h3> </div> <div class="pk-scroll pk-no-scrollbar" style="flex:1; overflow-y:auto; padding: 10px 30px 20px 30px; overscroll-behavior:contain; transform:translateZ(0);"> <div style="display:flex; flex-direction:column; gap:25px; padding-top:10px;"> <div class="pk-custom-select" id="cs_set_lang"> <div class="pk-select-label">${L.label_lang}</div> <div class="pk-select-trigger"><span id="txt_set_lang"></span>${CONF.crumbIcons.down}</div> <div class="pk-select-menu pk-scroll"> <div class="pk-select-item" data-val="zh">简体中文</div> <div class="pk-select-item" data-val="tc">繁體中文</div> <div class="pk-select-item" data-val="en">English</div> <div class="pk-select-item" data-val="ko">한국어</div> <div class="pk-select-item" data-val="ja">日本語</div> </div> </div> <div style="position:relative;"> <label for="set_turbo" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; justify-content:space-between; height:44px; border:2px solid var(--pk-bd); border-radius:8px; padding:0 12px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box; transform: translateZ(0);"> <span style="font-size:14px; color:var(--pk-fg);user-select:none;">${L.desc_turbo_mode}</span> <input type="checkbox" id="set_turbo" ${gmGet('pk_turbo_mode', false)?'checked':''} style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer;"> </label> <div class="pk-select-label">${L.label_turbo_mode}</div> </div> <div style="position:relative;"> <label for="set_thumb" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; justify-content:space-between; height:44px; border:2px solid var(--pk-bd); border-radius:8px; padding:0 12px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box; transform: translateZ(0);"> <span style="font-size:14px; color:var(--pk-fg);user-select:none;">${L.label_blur_cover}</span> <input type="checkbox" id="set_thumb" ${curBlur?'checked':''} style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer;"> </label> <div class="pk-select-label">${L.label_privacy_mode}</div> </div> <div style="position:relative;"> <label for="set_skip_bl" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; justify-content:space-between; height:44px; border:2px solid var(--pk-bd); border-radius:8px; padding:0 12px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box;"> <span style="font-size:14px; color:var(--pk-fg);user-select:none;">${L.lbl_skip_bl_on_del}</span> <input type="checkbox" id="set_skip_bl" ${gmGet('pk_skip_bl_on_del', true)?'checked':''} style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer;"> </label> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; pointer-events:none; line-height:1;">${L.title_blacklist}</div> </div> <div style="position:relative;"> <label for="set_keep_pos" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; justify-content:space-between; height:44px; border:2px solid var(--pk-bd); border-radius:8px; padding:0 12px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box;"> <span style="font-size:14px; color:var(--pk-fg);user-select:none;">${L.label_keep_pos}</span> <input type="checkbox" id="set_keep_pos" ${gmGet('pk_keep_pos', false)?'checked':''} style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer;"> </label> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; pointer-events:none; line-height:1;">${L.lbl_browse_exp}</div> </div> <div onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="position:relative; padding:15px; border:2px solid var(--pk-bd); border-radius:8px; transition:border-color 0.2s; cursor:default; transform: translateZ(0);"> <div class="pk-select-label">${L.label_sort_pref}</div> <div style="display:flex; flex-direction:column; gap:15px;"> <label style="display:flex; align-items:flex-start; gap:10px; cursor:pointer;"> <input type="radio" name="set_sort_pref" value="indep" ${gmGet('pk_sort_independent', false)?'checked':''} style="margin-top:4px; accent-color:var(--pk-pri);"> <div> <div style="font-size:14px; color:var(--pk-fg); font-weight:500; line-height:1.4;">${L.opt_sort_indep}</div> <div style="font-size:12px; color:#888; margin-top:2px;">${L.desc_sort_indep}</div> </div> </label> <label style="display:flex; align-items:flex-start; gap:10px; cursor:pointer;"> <input type="radio" name="set_sort_pref" value="global" ${!gmGet('pk_sort_independent', false)?'checked':''} style="margin-top:4px; accent-color:var(--pk-pri);"> <div> <div style="font-size:14px; color:var(--pk-fg); font-weight:500; line-height:1.4;">${L.opt_sort_global}</div> <div style="font-size:12px; color:#888; margin-top:2px;">${L.desc_sort_global}</div> </div> </label> </div> </div> <div style="position:relative;"> <label for="set_comic_mode" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'" style="display:flex; align-items:center; justify-content:space-between; height:44px; border:2px solid var(--pk-bd); border-radius:8px; padding:0 12px; cursor:pointer; background:var(--pk-bg); transition:border-color 0.2s; box-sizing:border-box;"> <span style="font-size:14px; color:var(--pk-fg); user-select:none;">${L.desc_comic_mode}</span> <input type="checkbox" id="set_comic_mode" ${gmGet('pk_comic_mode', false)?'checked':''} style="width:18px; height:18px; accent-color:var(--pk-pri); cursor:pointer;"> </label> <div style="position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; font-size:11px; color:var(--pk-pri); font-weight:bold; pointer-events:none; line-height:1;">${L.label_comic_mode}</div> </div> <div class="pk-custom-select" id="cs_set_engine"> <div class="pk-select-label">${L.label_search_engine}</div> <div class="pk-select-trigger"><span id="txt_set_engine"></span>${CONF.crumbIcons.down}</div> <div class="pk-select-menu pk-scroll"> <div class="pk-select-item" data-val="google">${L.opt_engine_google}</div> <div class="pk-select-item" data-val="yandex">${L.opt_engine_yandex}</div> <div class="pk-select-item" data-val="saucenao">${L.opt_engine_saucenao}</div> <div class="pk-select-item" data-val="tracemoe">${L.opt_engine_tracemoe}</div> </div> </div> <div id="pk_dl_group" style="position:relative; padding:25px 15px 15px 15px; border:2px solid var(--pk-bd); border-radius:8px; transition:border-color 0.2s;"> <div class="pk-select-label">${L.lbl_dl_filter}</div> <div style="display:flex; flex-direction:column; gap:15px;"> <div style="position:relative;"> <textarea id="set_dl_filter_ext" placeholder=".txt, .nfo" style="${areaStyle}">${esc(gmGet('pk_dl_filter_ext', ''))}</textarea> <div style="${labelStyle}">${L.label_dl_filter_ext}</div> </div> <div style="position:relative;"> <textarea id="set_dl_filter_name" placeholder="ReadMe1, ReadMe2" style="${areaStyle}">${esc(gmGet('pk_dl_filter_name', ''))}</textarea> <div style="${labelStyle}">${L.label_dl_filter_name}</div> </div> <div style="font-size:11px; color:#888;">${L.desc_dl_filter}</div> </div> </div> <div style="position:relative; transform: translateZ(0);"> <div style="position:relative;"> <input type="text" id="set_aria_url" value="${esc(curAriaUrl)}" placeholder="http://localhost:6800/jsonrpc" autocomplete="off" spellcheck="false" readonly onfocus="this.removeAttribute('readonly');" oninput="this.style.borderColor = this.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'" style="width:100%; height:44px; padding:0 70px 0 12px; border:2px solid ${curAriaUrl ? 'var(--pk-pri)' : '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; transform: translateZ(0);"> <div class="pk-select-label">${L.label_aria2_url}</div> <div id="btn_aria_default" style="position:absolute; right:10px; top:50%; transform:translateY(-50%); font-size:11px; color:var(--pk-pri); cursor:pointer; font-weight:bold; padding:4px 8px; border-radius:4px; background:rgba(0,103,192,0.1); border:1px solid rgba(0,103,192,0.2);" onmouseover="this.style.background='rgba(0,103,192,0.2)'" onmouseout="this.style.background='rgba(0,103,192,0.1)'">Default</div> </div> <div class="pk-aria-status-box" id="aria_test_res" style="margin-top: 8px;"> <div class="pk-aria-dot" id="aria_test_dot"></div> <span id="aria_test_txt" style="color:#888; font-size: 11px;">${L.lbl_aria2_status}</span> </div> </div> <div style="position:relative; transform: translateZ(0); -webkit-transform: translateZ(0);"> <input type="text" id="set_aria_token" value="${esc(curAriaToken)}" placeholder="${L.ph_aria2_secret}" autocomplete="off" spellcheck="false" data-lpignore="true" readonly onfocus="this.removeAttribute('readonly');" oninput="this.style.borderColor = this.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'" style="width:100%; height:44px; padding:0 12px; border:2px solid ${curAriaToken ? 'var(--pk-pri)' : '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; -webkit-text-security: disc; transform: translateZ(0);"> <div class="pk-select-label">${L.label_aria2_token}</div> </div> <div style="position:relative; padding:20px 15px 15px 15px; border:2px solid var(--pk-bd); border-radius:8px; transition:border-color 0.2s; transform:translateZ(0); backface-visibility:hidden;" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'"> <div class="pk-select-label" style="transform:translateY(-50.5%);">${L.lbl_pwd_manage}</div> <div id="btn_open_vault" style="display:flex; align-items:center; justify-content:center; gap:10px; height:44px; background:var(--pk-hl); border-radius:8px; cursor:pointer; transition:all 0.2s; color:var(--pk-fg); transform:translateZ(0);" onmouseover="this.style.background='var(--pk-sel-bg)'; this.style.color='var(--pk-pri)'" onmouseout="this.style.background='var(--pk-hl)'; this.style.color='var(--pk-fg)'"> <span style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;color:var(--pk-pri);">${CONF.icons.vault.replace('width="16"','width="20"').replace('height="16"','width="20"')}</span> <span style="font-size:14px; font-weight:700;">${L.title_pwd_vault}</span> </div> </div> <div style="position:relative; padding:20px 15px 15px 15px; border:2px solid var(--pk-bd); border-radius:8px; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s; transform:translateZ(0);" onmouseover="this.style.borderColor='var(--pk-pri)'" onmouseout="this.style.borderColor='var(--pk-bd)'"> <div class="pk-select-label" style="transform:translateY(-50.5%);">${L.lbl_config_manage}</div> <button class="pk-btn" id="btn_cfg_clean" style="background:transparent; border:1px solid #d93025; color:#d93025; height:36px; border-radius:6px; font-weight:600; width:100%; display:flex; align-items:center; justify-content:center;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px; flex-shrink:0;"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> <div style="display:flex; align-items:baseline;"> <span style="display:inline !important; font-size:13px; font-weight:600;">${L.btn_clean_data}</span> <span style="display:inline !important; font-size:13px; font-weight:600; margin-left:6px; opacity:0.8;">( ${storageDisplay} )</span> </div> </button> <div style="display:grid; grid-template-columns: 1fr 1fr; gap:12px;"> <button class="pk-btn" id="btn_cfg_export" style="background:var(--pk-hl); border:1px solid var(--pk-bd); height:36px; border-radius:6px; font-weight:600;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="margin-right:4px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> ${L.btn_export_data} </button> <button class="pk-btn" id="btn_cfg_import" style="background:var(--pk-hl); border:1px solid var(--pk-bd); height:36px; border-radius:6px; font-weight:600;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="margin-right:4px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> ${L.btn_import_data} </button> </div> <input type="file" id="cfg_import_input" accept=".json" style="display:none;"> </div> </div> </div> <div style="padding: 20px 30px 30px 30px; flex-shrink:0; background:var(--pk-bg); border-top:1px solid var(--pk-bd);"> <div class="pk-modal-act" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 0;"> <button class="pk-btn" id="set_cancel" style="height:44px; border-radius:10px; justify-content:center; background:transparent; font-weight:600; font-size:15px;">${L.btn_cancel}</button> <button class="pk-btn pri" id="set_save" style="height:44px; border-radius:10px; background:var(--pk-pri); color:#fff; font-weight:bold; justify-content:center; border:none; font-size:15px;">${L.btn_save}</button> </div> </div> </div> `); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: 'auto', padding: '0', overflow: 'hidden', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const bindSelect = (id, currentVal, onSelect) => { const container = m.querySelector(`#${id}`); const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('span'); const items = container.querySelectorAll('.pk-select-item'); items.forEach(item => { if (item.dataset.val === currentVal) { item.classList.add('act'); txt.textContent = item.textContent; } item.onclick = (e) => { e.stopPropagation(); items.forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val); }; }); trigger.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('.pk-select-menu').forEach(om => { if(om !== menu) om.style.display = 'none'; }); menu.style.display = menu.style.display === 'block' ? 'none' : 'block'; }; }; bindSelect('cs_set_lang', curLang, (val) => { selectedLang = val; }); bindSelect('cs_set_engine', curEngine, (val) => { selectedEngine = val; }); const ariaInp = m.querySelector('#set_aria_url'); const ariaTok = m.querySelector('#set_aria_token'); const ariaDot = m.querySelector('#aria_test_dot'); const ariaTxt = m.querySelector('#aria_test_txt'); const ariaBox = m.querySelector('#aria_test_res'); let ariaTimer = null; const runAriaTest = async () => { const url = ariaInp.value.trim(); const token = ariaTok.value.trim(); const showTip = () => showAlert(L.tip_mixed_content, L.lbl_aria2_status); if (!url) { ariaDot.className = 'pk-aria-dot'; ariaTxt.textContent = L.lbl_aria2_status; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; return; } ariaDot.className = 'pk-aria-dot wait'; ariaTxt.textContent = L.str_connecting; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_live_test', params: [`token:${token}`] }; let testUrl = url.replace(/^ws/i, 'http'); if (!testUrl.includes('/jsonrpc') && !testUrl.includes('?')) { testUrl = testUrl.endsWith('/') ? testUrl + 'jsonrpc' : testUrl + '/jsonrpc'; } try { await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: testUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 3000, onload: (r) => { if (r.status === 200) resolve(); else reject(new Error(r.status)); }, onerror: (e) => reject(e) }); }); ariaDot.className = 'pk-aria-dot ok'; ariaTxt.textContent = L.str_connected; ariaBox.onclick = null; ariaBox.style.cursor = 'default'; } catch (e) { ariaDot.className = 'pk-aria-dot err'; ariaTxt.textContent = L.str_conn_fail; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; } }; const debouncedTest = () => { clearTimeout(ariaTimer); ariaTimer = setTimeout(runAriaTest, 600); }; ariaInp.oninput = (e) => { if (e.target.oninput) e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; debouncedTest(); }; ariaTok.oninput = (e) => { if (e.target.oninput) e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; debouncedTest(); }; m.querySelector('#btn_aria_default').onclick = () => { ariaInp.value = 'http://localhost:6800/jsonrpc'; ariaInp.style.borderColor = 'var(--pk-pri)'; debouncedTest(); }; setTimeout(runAriaTest, 200); const clickAway = () => m.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none'); setTimeout(() => document.addEventListener('click', clickAway), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', clickAway); _orgRemove(); }; const extInp = m.querySelector('#set_dl_filter_ext'); const nameInp = m.querySelector('#set_dl_filter_name'); const groupEl = m.querySelector('#pk_dl_group'); const updateDlBorders = () => { const hasE = extInp.value.trim() !== ''; const hasN = nameInp.value.trim() !== ''; extInp.classList.toggle('pk-active-border', hasE); nameInp.classList.toggle('pk-active-border', hasN); groupEl.classList.toggle('pk-typing-active', hasE || hasN); }; extInp.oninput = nameInp.oninput = updateDlBorders; updateDlBorders(); m.querySelector('#btn_open_vault').onclick = (e) => { e.stopPropagation(); const subM = document.createElement('div'); subM.className = 'pk-modal-ov'; subM.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) subM.classList.add('pk-dark'); const savedCount = gmGet('pk_pwd_try_count', 10); const savedPwds = (() => { try { return JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x.p : x).join('\n'); } catch { return ''; } })(); subM.innerHTML = ` <style>#vault_cnt_val::-webkit-outer-spin-button, #vault_cnt_val::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .pk-dark .pk-sub-ctrl-btn:hover { background: #444 !important; }</style> <div class="pk-modal" style="width:380px; padding:24px; border-radius:12px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); background:var(--pk-bg); border:1px solid var(--pk-bd);"> <div class="pk-modal-close" style="top:26px; right:24px; color:var(--pk-icon-c); opacity:0.7;">${CONF.icons.close}</div> <h3 style="border:none; margin:0 0 10px 0; font-size:17px; font-weight:700; color:var(--pk-fg); display:flex; align-items:center; gap:10px;"> <span style="width:20px;height:20px;display:flex;align-items:center;justify-content:center;color:var(--pk-pri);">${CONF.icons.vault.replace('width="16"','width="22"').replace('height="16"','width="22"')}</span> ${L.title_pwd_vault} </h3> <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:4px;"> <span style="font-size:14px; color:var(--pk-fg); font-weight:600; opacity:0.9;">${L.lbl_pwd_try_count}</span> <div class="pk-sub-ctrl" style="width:110px; height:34px; display:flex; align-items:center; border:1.5px solid var(--pk-bd); border-radius:6px; overflow:hidden; background:var(--pk-hl);"> <div class="pk-sub-ctrl-btn" id="vault_cnt_dec" style="width:32px; height:100%; display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--pk-fg); font-weight:bold; border-right:1px solid var(--pk-bd); transition:background 0.2s;">-</div> <input type="number" id="vault_cnt_val" value="${savedCount}" min="10" max="50" style="flex:1; width:30px; height:100%; text-align:center; border:none; background:transparent; color:var(--pk-fg); outline:none; font-size:15px; font-weight:700; font-family:monospace; -moz-appearance:textfield;"> <div class="pk-sub-ctrl-btn" id="vault_cnt_inc" style="width:32px; height:100%; display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--pk-fg); font-weight:bold; border-left:1px solid var(--pk-bd); transition:background 0.2s;">+</div> </div> </div> <div style="font-size:12px; color:#888; margin-bottom:8px; padding-left:2px;">${L.tip_pwd_manual}</div> <div style="display:flex; flex-direction:column; gap:8px; position:relative;"> <textarea id="vault_pwd_area" placeholder="" spellcheck="false" wrap="off" style="width:100%; height:242px; padding:0 15px; border:2px solid var(--pk-bd); border-radius:8px; background-color:var(--pk-hl); background-image:linear-gradient(to right, var(--pk-hl) 4px, transparent 4px), linear-gradient(to bottom, transparent 31px, var(--pk-v-line) 31px); background-size:8px 32px, 100% 32px; background-attachment:local; color:var(--pk-fg); font-size:13px; font-family:'Fira Code', 'Consolas', monospace; line-height:32px; resize:none; outline:none; box-sizing:border-box; white-space:pre; transition:border-color 0.2s, box-shadow 0.2s; overflow-y:auto; cursor:auto;"></textarea> </div> <div class="pk-modal-act" style="display:flex; justify-content:flex-end; align-items:center; margin-top:28px; gap:20px; border-top:1px solid var(--pk-bd); padding-top:20px;"> <span id="vault_cancel" style="cursor:pointer; color:var(--pk-icon-c); font-size:14px; font-weight:600; transition:color 0.2s;" onmouseover="this.style.color='var(--pk-fg)'" onmouseout="this.style.color='var(--pk-icon-c)'">${L.btn_cancel}</span> <button class="pk-btn pri" id="vault_save" style="height:40px; padding:0 30px; border-radius:8px; background:var(--pk-pri); border:none; color:#fff; font-weight:bold; font-size:14px; box-shadow:0 4px 12px rgba(0,0,0,0.2); transition:transform 0.1s, filter 0.2s;">${L.btn_save}</button> </div> </div> `; document.body.appendChild(subM); const cntInp = subM.querySelector('#vault_cnt_val'); subM.querySelector('#vault_cnt_dec').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(cntInp.value)) || 10; cntInp.value = Math.max(10, v - 1); }; subM.querySelector('#vault_cnt_inc').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(cntInp.value)) || 0; cntInp.value = Math.min(50, v + 1); }; cntInp.oninput = () => { let raw = cntInp.value; if (raw === "") return; let v = parseInt(raw); if (isNaN(v)) { cntInp.value = 10; return; } if (v > 50) cntInp.value = 50; }; cntInp.onblur = () => { let v = parseInt(cntInp.value); if (isNaN(v) || v < 10) cntInp.value = 10; }; const area = subM.querySelector('#vault_pwd_area'); area.onfocus = () => area.style.borderColor = 'var(--pk-pri)'; area.onblur = () => area.style.borderColor = 'var(--pk-bd)'; area.value = savedPwds; area.oninput = () => { let lines = area.value.split('\n'); let changed = false; let msg = ""; if (lines.length > 50) { lines = lines.slice(0, 50); changed = true; msg = L.err_vault_max; } for (let i = 0; i < lines.length; i++) { if (lines[i].length > 127) { lines[i] = lines[i].substring(0, 127); changed = true; msg = L.err_pwd_len; } } if (changed) { const cursor = area.selectionStart; area.value = lines.join('\n'); area.setSelectionRange(cursor, cursor); showToast(msg, 'error'); area.style.borderColor = '#d93025'; setTimeout(() => { if(area) area.style.borderColor = 'var(--pk-pri)'; }, 1000); } }; const doSave = () => { let cnt = Math.floor(Number(cntInp.value)); if (isNaN(cnt) || cnt < 10) cnt = 10; if (cnt > 50) cnt = 50; const inputPwds = area.value.split('\n').map(s => s.trim()).filter(s => s); if (inputPwds.length > 50) { showToast(L.err_vault_max, 'error'); area.style.borderColor = '#d93025'; return; } try { const oldList = JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x : {p: x, h: 0}); const hitMap = new Map(oldList.map(x => [x.p, x.h])); const newList = [...new Set(inputPwds)].map(p => ({ p: p, h: hitMap.get(p) || 0 })); newList.sort((a, b) => b.h - a.h); gmSet('pk_pwd_try_count', cnt); gmSet('pk_pwd_vault', JSON.stringify(newList)); } catch(e) { gmSet('pk_pwd_vault', JSON.stringify([...new Set(inputPwds)])); } subM.remove(); showToast(L.msg_settings_saved); }; subM.querySelector('#vault_save').onclick = doSave; subM.querySelector('#vault_cancel').onclick = () => subM.remove(); subM.querySelector('.pk-modal-close').onclick = () => subM.remove(); }; m.querySelector('#btn_cfg_clean').onclick = async () => { const keys = typeof GM_listValues !== 'undefined' ? GM_listValues() : Object.keys(localStorage); const sizes = { index: 0, pref: 0, rules: 0, vault: 0, history: 0, cache: 0 }; if (typeof globalCache !== 'undefined') { for (const [k, v] of globalCache.entries()) { try { sizes.index += k.toString().length + JSON.stringify(v).length; } catch(e){} } } const getCat = (k) => { if (!k.startsWith('pk_')) return null; if (k.startsWith('pk_archive_pwd_') || k === 'pk_pwd_vault') return 'vault'; if (k.startsWith('pk_progress_') || k.startsWith('pk_duration_')) return 'history'; if (k.startsWith('pk_fmod_') || k === 'pk_captured_captcha') return 'cache'; const ruleKeys =['pk_blacklist', 'pk_blacklist_folders', 'pk_aria2_url', 'pk_aria2_token', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist']; if (ruleKeys.includes(k) || k.startsWith('pk_scan_last_') || k.startsWith('pk_analyze_last_') || k === 'pk_dup_strictness') return 'rules'; return 'pref'; }; keys.forEach(k => { const cat = getCat(k); if (cat) { const val = typeof GM_getValue !== 'undefined' ? GM_getValue(k) : localStorage.getItem(k); sizes[cat] += (k.length + (val ? JSON.stringify(val).length : 0)); } }); const renderLbl = (cat, txt, isChecked = false, isMandatory = false) => { const sz = sizes[cat]; if (sz === 0 && cat !== 'index') return ''; const szStr = fmtSize(sz); const checkAttr = (isChecked || isMandatory) ? 'checked' : ''; const disAttr = isMandatory ? 'disabled' : ''; const cursor = isMandatory ? 'not-allowed' : 'pointer'; const opacity = isMandatory ? '0.7' : '1'; return `<label style="display:flex; align-items:flex-start; gap:12px; cursor:${cursor}; color:var(--pk-fg); font-size:14px; opacity:${opacity};"> <input type="checkbox" class="clean-opt" value="${cat}" ${checkAttr} ${disAttr} style="width:18px; height:18px; accent-color:#d93025; cursor:inherit; margin-top:2px;"> <div style="display:flex; flex-direction:column;"> <span>${txt}</span> <span style="font-size:12px; color:#888; font-family:monospace; margin-top:2px;">${szStr}</span> </div> </label>`; }; 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(` <h3 style="border:none; margin-bottom:20px; font-size:18px; font-weight:700; color:var(--pk-fg);">${L.title_clean_data}</h3> <div style="display:flex; flex-direction:column; gap:16px; margin-bottom:25px;"> ${htmlOptions} </div> <div class="pk-modal-act"> <button class="pk-btn" id="clean_cancel">${L.btn_cancel}</button> <button class="pk-btn pri pk-btn-danger" id="clean_confirm">${L.btn_del}</button> </div> `); cleanM.querySelector('#clean_cancel').onclick = () => cleanM.remove(); cleanM.querySelector('#clean_confirm').onclick = async () => { const selected = Array.from(cleanM.querySelectorAll('.clean-opt:checked')).map(el => el.value); if (selected.length === 0) { cleanM.remove(); return; } if (!await showConfirm(L.msg_clean_confirm)) return; if (selected.includes('index')) { if (typeof globalCache !== 'undefined') globalCache.clear(); if (typeof S !== 'undefined' && S.cache) S.cache.clear(); if (typeof globalLineageMap !== 'undefined') globalLineageMap.clear(); if (typeof globalParentIndex !== 'undefined') globalParentIndex.clear(); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.clear(); } keys.forEach(k => { const cat = getCat(k); if (cat && selected.includes(cat)) { try { if (typeof GM_deleteValue !== 'undefined') { GM_deleteValue(k); } else if (typeof GM_setValue !== 'undefined') { GM_setValue(k, ''); } localStorage.removeItem(k); } catch (e) { console.warn("Delete config error:", e); } } }); showToast(L.msg_clean_success); setTimeout(() => location.reload(), 1500); }; }; m.querySelector('#btn_cfg_export').onclick = () => { const keys = typeof GM_listValues !== 'undefined' ? GM_listValues() : Object.keys(localStorage); const config = { "_pk_metadata": { "signature": "PIKPAK_ENHANCEMENT_MASTER", "version": version, "export_at": new Date().toISOString(), "author": "digbug82" } }; const pkKeys = keys.filter(k => k.startsWith('pk_') && k !== 'pk_captured_captcha'); const getCatWeight = (k) => { if (k.startsWith('pk_archive_pwd_') || k === 'pk_pwd_vault' || k === 'pk_share_limits') return 3; if (k.startsWith('pk_progress_') || k.startsWith('pk_duration_')) return 4; if (k.startsWith('pk_fmod_')) return 5; const ruleKeys =['pk_blacklist', 'pk_blacklist_folders', 'pk_aria2_url', 'pk_aria2_token', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_bn_find_hist', 'pk_bn_rep_hist']; if (ruleKeys.includes(k) || k.startsWith('pk_scan_last_') || k.startsWith('pk_analyze_last_') || k === 'pk_dup_strictness') return 2; return 1; }; pkKeys.sort((a, b) => { const wA = getCatWeight(a); const wB = getCatWeight(b); if (wA !== wB) return wA - wB; return a.localeCompare(b); }); pkKeys.forEach(k => { config[k] = typeof GM_getValue !== 'undefined' ? GM_getValue(k) : localStorage.getItem(k); }); const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const domEl = document.querySelector('.name.ellipsis'); let rawUserName = domEl ? (domEl.title || domEl.innerText) : 'Default'; const userName = rawUserName.trim().replace(/[\\/:*?"<>|]/g, '_'); const now = new Date(); const dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); const timeStr = now.getHours().toString().padStart(2,'0') + now.getMinutes().toString().padStart(2,'0'); const fileName = `PKM_Backup_${userName}_${dateStr}_${timeStr}.json`; const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); }; const fileInput = m.querySelector('#cfg_import_input'); m.querySelector('#btn_cfg_import').onclick = () => fileInput.click(); fileInput.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; if (!await showConfirm(L.msg_import_confirm)) { fileInput.value = ''; return; } const reader = new FileReader(); reader.onload = (ev) => { try { const config = JSON.parse(ev.target.result); if (!config._pk_metadata || config._pk_metadata.signature !== "PIKPAK_ENHANCEMENT_MASTER") { throw new Error("INVALID_SIGNATURE"); } Object.keys(config).forEach(k => { if (!k.startsWith('pk_')) return; let importedVal = config[k]; let localVal = typeof GM_getValue !== 'undefined' ? GM_getValue(k, null) : localStorage.getItem(k); if (localVal === null || localVal === undefined || localVal === '') { GM_setValue(k, importedVal); return; } try { if (k === 'pk_blacklist' || k === 'pk_blacklist_folders') { const localSet = new Set(localVal.split('\n').map(s => s.trim()).filter(s => s)); const importedSet = new Set(importedVal.split('\n').map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); GM_setValue(k, Array.from(localSet).join('\n')); } else if (k === 'pk_dl_filter_ext' || k === 'pk_dl_filter_name') { const localSet = new Set(localVal.split(/[,,\n]/).map(s => s.trim()).filter(s => s)); const importedSet = new Set(importedVal.split(/[,,\n]/).map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); GM_setValue(k, Array.from(localSet).join(', ')); } else if (typeof importedVal === 'string' && (importedVal.startsWith('[') || importedVal.startsWith('{'))) { const localObj = JSON.parse(localVal); const importedObj = JSON.parse(importedVal); if (Array.isArray(localObj) && Array.isArray(importedObj)) { if (k === 'pk_pwd_vault') { const map = new Map(); localObj.forEach(x => { if (typeof x === 'object') map.set(x.p, x); else map.set(x, {p: x, h: 0}); }); importedObj.forEach(x => { let p = typeof x === 'object' ? x.p : x; let h = typeof x === 'object' ? x.h : 0; if (map.has(p)) map.get(p).h += h; else map.set(p, {p, h}); }); let merged = Array.from(map.values()).sort((a,b) => b.h - a.h).slice(0, 50); GM_setValue(k, JSON.stringify(merged)); } else if (k === 'pk_expired_shares') { const map = new Map(); localObj.forEach(x => map.set(x.id, x)); importedObj.forEach(x => map.set(x.id, x)); GM_setValue(k, JSON.stringify(Array.from(map.values()))); } else { const mergedSet = new Set([...importedObj, ...localObj]); GM_setValue(k, JSON.stringify(Array.from(mergedSet).slice(0, 100))); } } else if (typeof localObj === 'object' && typeof importedObj === 'object') { const mergedObj = Object.assign({}, localObj, importedObj); GM_setValue(k, JSON.stringify(mergedObj)); } else { GM_setValue(k, importedVal); } } else { GM_setValue(k, importedVal); } } catch (e) { GM_setValue(k, importedVal); } }); showToast(L.msg_import_success); setTimeout(() => location.reload(), 1500); } catch (err) { let errorTip = ""; if (err.message === "INVALID_SIGNATURE") { errorTip = L.err_invalid_config; } else { errorTip = L.err_json_format; console.error("[Config Import]", err); } showAlert(errorTip, L.str_error); fileInput.value = ''; } }; reader.readAsText(file); }; m.querySelector('#set_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#set_save').click(); } }); m.querySelector('#set_save').onclick = async () => { const newTurbo = m.querySelector('#set_turbo').checked; const oldTurbo = gmGet('pk_turbo_mode', false); const newUrl = m.querySelector('#set_aria_url').value.trim(); const newToken = m.querySelector('#set_aria_token').value.trim(); const newBlur = m.querySelector('#set_thumb').checked; const newKeepPos = m.querySelector('#set_keep_pos').checked; const newSkipBl = m.querySelector('#set_skip_bl').checked; const newComicMode = m.querySelector('#set_comic_mode').checked; const sortPref = m.querySelector('input[name="set_sort_pref"]:checked').value; const saveBtn = m.querySelector('#set_save'); gmSet('pk_blur_thumb', newBlur); gmSet('pk_keep_pos', newKeepPos); gmSet('pk_comic_mode', newComicMode); gmSet('pk_skip_bl_on_del', newSkipBl); const wasIndep = gmGet('pk_sort_independent', false); const isIndep = (sortPref === 'indep'); gmSet('pk_sort_independent', isIndep); if (wasIndep && !isIndep) { gmSet('pk_folder_sort_prefs', '{}'); gmSet('pk_global_sort_pref', JSON.stringify({ sort: 'modified_time', dir: 1 })); S.sort = 'modified_time'; S.dir = 1; } gmSet('pk_lang', selectedLang); gmSet('pk_search_engine', selectedEngine); gmSet('pk_turbo_mode', newTurbo); gmSet('pk_dl_filter_ext', m.querySelector('#set_dl_filter_ext').value.trim()); gmSet('pk_dl_filter_name', m.querySelector('#set_dl_filter_name').value.trim()); const applyChangesAndClose = () => { m.remove(); const savedMsg = (T[selectedLang] || T['en']).msg_settings_saved; showToast(savedMsg); if (newTurbo !== oldTurbo) { setTimeout(() => location.reload(), 300); return; } if (curLang !== selectedLang) { let safePath = [...S.path]; if (safePath.some(n => n.id === 'virtual_search_root' || n.id === 'analyze_root')) { safePath = S.preSearchPath || [{ id: '', name: L.btn_nav_home }]; } globalSavedState = { path: safePath, trashMode: S.trashMode, shareMode: S.shareMode, starredMode: S.starredMode, recentMode: S.recentMode, historyMode: S.historyMode, offlineMode: S.offlineMode, uploadMode: S.uploadMode, isMaximized: UI.win.classList.contains('pk-maximized'), scrollTop: UI.vp ? UI.vp.scrollTop : 0, uploadTasks: S.uploadTasks }; document.removeEventListener('keydown', keyHandler); document.removeEventListener('mouseup', mouseHandler); if (typeof destroyTooltip === 'function') destroyTooltip(); if (visibilityListener && visibilityListener.abort) visibilityListener.abort(); el.remove(); openManager(S.cache, S.preLoadPromise); } else { renderVisible(); } }; if (!newUrl && !newToken) { gmSet('pk_aria2_url', ''); gmSet('pk_aria2_token', ''); applyChangesAndClose(); return; } saveBtn.disabled = true; saveBtn.textContent = L.str_saving_dots; try { const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_test', params: [`token:${newToken}`] }; let fetchUrl = (newUrl || "http://localhost:6800/jsonrpc").replace('ws', 'http'); await new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url: fetchUrl, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout: 5000, onload: (r) => { if(r.status === 200) resolveReq(); else rejectReq(new Error('HTTP ' + r.status)); }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); gmSet('pk_aria2_url', newUrl); gmSet('pk_aria2_token', newToken); applyChangesAndClose(); } catch (e) { if (await showConfirm(L.msg_aria2_test_fail, L.title_aria2_fail)) { gmSet('pk_aria2_url', newUrl); gmSet('pk_aria2_token', newToken); applyChangesAndClose(); } else { saveBtn.disabled = false; saveBtn.textContent = L.btn_save; } } }; }; UI.btnSettings.onclick = (e) => { if (e) e.stopPropagation(); const existing = document.getElementById('pk-settings-pop'); if (existing) { existing.remove(); return; } const isMax = UI.win.classList.contains('pk-maximized'); const pop = document.createElement('div'); pop.id = 'pk-settings-pop'; if (isMax) pop.className = 'pk-pop-max'; pop.style.cssText = ` position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; padding: 4px 0; box-shadow: 0 4px 15px rgba(0,0,0,0.15); z-index: 2147483647; min-width: 140px; display: flex; flex-direction: column; zoom: var(--pk-zoom, 1); `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.innerHTML = ` <div class="pk-dropdown-item" id="pk-set-menu-settings" style="padding:10px 16px;">${CONF.icons.settings} <span>${L.btn_settings}</span></div> <div style="height:1px; background:var(--pk-bd); margin:4px 0;"></div> <div class="pk-dropdown-item" id="pk-set-menu-logout" style="padding:10px 16px; color:#d93025;">${CONF.icons.logout} <span>${L.btn_logout}</span></div> `; document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = UI.btnSettings.getBoundingClientRect(); const winEl = document.querySelector('.pk-win'); const isMax = winEl && winEl.classList.contains('pk-maximized'); let popLeft, popBottom; if (isMax) { popLeft = (rect.left / scale); popBottom = (window.innerHeight - rect.top + 8) / scale; } else { popLeft = (rect.right / scale) + 10; popBottom = (window.innerHeight - rect.bottom) / scale; } pop.style.bottom = popBottom + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; const closer = (ev) => { if (!pop.contains(ev.target) && !UI.btnSettings.contains(ev.target)) cleanup(); }; setTimeout(() => document.addEventListener('mousedown', closer), 10); pop.querySelector('#pk-set-menu-settings').onclick = (ev) => { ev.stopPropagation(); cleanup(); openSettingsModal(); }; pop.querySelector('#pk-set-menu-logout').onclick = async (ev) => { ev.stopPropagation(); cleanup(); if (await showConfirm(L.msg_logout_confirm)) { const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && (k.startsWith('credentials') || k.startsWith('captcha') || k === 'pk_captured_captcha')) { keysToRemove.push(k); } } keysToRemove.forEach(k => localStorage.removeItem(k)); if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); window.location.href = 'https://mypikpak.com/drive/login'; } }; }; const ctx = el.querySelector('#pk-ctx'); if (!window.pkPropCache) window.pkPropCache = new Map(); ctx.querySelector('#ctx-property').onclick = async () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; if (!id) return; let item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.loading_detail); const isFolder = item.kind === 'drive#folder'; try { const freshData = await apiGet(id); item = { ...item, ...freshData }; } catch(e) { console.warn("Fetch detail failed, using cached info"); } if (isFolder) { const localFMod = gmGet('pk_fmod_' + item.id); if (localFMod) item.modified_time = localFMod; } setLoad(false); let cachedProp = window.pkPropCache.get(item.id); if (!cachedProp) { cachedProp = { size: BigInt(0), fileCount: 0, folderCount: 0, isDone: false, scanned: new Set(), isRunning: false }; window.pkPropCache.set(item.id, cachedProp); } let realSize = isFolder ? cachedProp.size : BigInt(item.size || 0); let fileCount = isFolder ? cachedProp.fileCount : 0; let folderCount = isFolder ? cachedProp.folderCount : 0; if (isFolder && item.usage && item.usage.size && item.usage.size !== "0") { realSize = BigInt(item.usage.size); fileCount = item.usage.file_count; folderCount = item.usage.folder_count; cachedProp.size = realSize; cachedProp.fileCount = fileCount; cachedProp.folderCount = folderCount; cachedProp.isDone = true; } const countStr = isFolder ? L.fmt_prop_count.replace('{f}', fileCount).replace('{d}', folderCount) : "-"; const formatTime = (iso) => fmtDate ? fmtDate(iso) : iso; let sourceStr = L.str_prop_unknown; let magnetLink = item.params?.url || item.audit?.source_url || ""; if (magnetLink) { sourceStr = L.str_prop_cloud; } else { sourceStr = L.str_prop_user; } const btnStyle = "margin-left:10px; padding:2px 8px; font-size:12px; background:var(--pk-pri); color:#fff; border:none; border-radius:4px; cursor:pointer; height:24px; white-space:nowrap;"; const rowStyle = "display:flex; align-items:center; margin-bottom:12px; font-size:13px; line-height:1.5;"; const labelStyle = "width:80px; color:#888; flex-shrink:0;"; const valStyle = "color:var(--pk-fg); flex:1; word-break:break-all;"; const mkRow = (lbl, val, copyVal = null, isHtml = false, valId = null) => { if (!val && val !== 0 && val !== "-") return ""; let btnHtml = copyVal ? `<button class="pk-btn-copy-prop" data-val="${esc(copyVal)}" style="${btnStyle}">${L.btn_copy_text}</button>` : ""; return `<div style="${rowStyle}"><div style="${labelStyle}">${lbl}:</div><div style="${valStyle}" ${valId ? `id="${valId}"` : ''}>${isHtml ? val : esc(val)}</div>${btnHtml}</div>`; }; 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 = `<span style="display:inline-flex;align-items:center;vertical-align:bottom;margin-right:2px;">${homeSvg}${esc(homeText)}</span>`; pathDisplayHtml = `<div style="line-height:1.6;word-break:break-all;">${homeGroup}${esc(restPath)}</div>`; } pathRowHtml = mkRow(L.lbl_prop_path, pathDisplayHtml, pathStr, true); } const html = ` <div style="padding:10px 0;"> ${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} </div> `; const m = showModal(` <h3 style="border-bottom:1px solid var(--pk-bd); padding-bottom:10px; margin-bottom:15px;">${L.title_property}</h3> ${html} `); m.querySelectorAll('.pk-btn-copy-prop').forEach(btn => { btn.onclick = (e) => { const txt = e.target.getAttribute('data-val'); GM_setClipboard(txt); const oldTxt = e.target.textContent; e.target.textContent = "OK"; e.target.style.background = "#4CAF50"; setTimeout(() => { e.target.textContent = oldTxt; e.target.style.background = "var(--pk-pri)"; }, 1500); }; }); if (isFolder && !cachedProp.isDone) { const sizeEl = m.querySelector('#pk_prop_size_val'); const countEl = m.querySelector('#pk_prop_count_val'); let lastUiUpdateTime = performance.now(); const updateUI = () => { const now = performance.now(); if (now - lastUiUpdateTime > 80) { if (sizeEl && sizeEl.isConnected) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl && countEl.isConnected) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); lastUiUpdateTime = now; } }; if (!cachedProp.isRunning) { cachedProp.isRunning = true; const localAbortCtrl = new AbortController(); const rootNodes =[{ id: item.id, name: item.name, lineage: [], retryCount: 0 }]; coreRecursiveEngine(rootNodes, { signal: localAbortCtrl.signal, onFolder: (f) => { if (f.id !== item.id && !cachedProp.scanned.has(f.id)) { cachedProp.folderCount++; cachedProp.scanned.add(f.id); } updateUI(); }, onFile: (f) => { if (!cachedProp.scanned.has(f.id)) { cachedProp.fileCount++; cachedProp.size += BigInt(f.size || 0); cachedProp.scanned.add(f.id); } updateUI(); }, onProgress: () => {} }).then(() => { cachedProp.isDone = true; cachedProp.isRunning = false; if (sizeEl && sizeEl.isConnected) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl && countEl.isConnected) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); }).catch(e => { cachedProp.isRunning = false; }); } else { const timer = setInterval(() => { if (!document.contains(m)) { clearInterval(timer); return; } if (sizeEl) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); if (cachedProp.isDone) clearInterval(timer); }, 200); } } }; const btnLocate = ctx.querySelector('#ctx-locate'); if (btnLocate) { btnLocate.onclick = async () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; if (!id) return; let item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.str_loc_tracing); try { if (item.kind === 'drive#task' || S.recentMode || S.uploadMode) { const lookupId = (item.kind === 'drive#task' || S.uploadMode) ? item.file_id : item.id; if (!lookupId) { showToast(L.err_folder_not_ready, 'error'); setLoad(false); return; } try { item = await apiGet(lookupId); } catch (e) { const errText = e.message || ""; if (errText.includes('404') || errText.includes('400')) { showToast(L.err_item_deleted, 'error'); } else { showToast(`${L.str_error}: ${e.message}`, 'error'); } setLoad(false); return; } } let pathChain = []; if (item._lineage && Array.isArray(item._lineage)) { pathChain = item._lineage.filter(p => p.id !== item.id && p.id !== 'virtual_search_root' && p.id !== 'analyze_root' && p.id !== 'recent_root' ); } else if (item.parent_id && typeof globalLineageMap !== 'undefined' && globalLineageMap.has(item.parent_id)) { pathChain = [...globalLineageMap.get(item.parent_id)]; } else { let currParentId = item.parent_id; for (let i = 0; i < 15; i++) { if (!currParentId || currParentId === 'root' || currParentId === '') break; if (typeof globalLineageMap !== 'undefined' && globalLineageMap.has(currParentId)) { const cachedLineage = globalLineageMap.get(currParentId); pathChain.unshift(...cachedLineage); break; } try { const res = await apiGet(currParentId); pathChain.unshift({ id: res.id, name: res.name }); currParentId = res.parent_id; } catch (e) { console.warn("Trace broken:", e); break; } } } if (pathChain.length > 0 && (pathChain[0].id === '' || pathChain[0].id === 'root')) { pathChain[0].id = ''; pathChain[0].name = L.btn_nav_home; } else { pathChain.unshift({ id: '', name: L.btn_nav_home }); } const targetContextId = (item.parent_id === 'root' || !item.parent_id) ? '' : item.parent_id; const needsRestoreGlobalCheck = S.starredMode || S.recentMode || S.historyMode || S.offlineMode || S.uploadMode || S.shareMode || S.isFlattened || S.dupMode || S.analyzeMode; S.starredMode = false; S.trashMode = false; S.shareMode = false; S.offlineMode = false; S.recentMode = false; S.historyMode = false; S.uploadMode = false; S.dupMode = false; S.isFlattened = false; S.analyzeMode = false; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; if (UI.btnNavStarred) UI.btnNavStarred.classList.remove('act'); if (UI.btnNavRecent) UI.btnNavRecent.classList.remove('act'); if (UI.btnNavHistory) UI.btnNavHistory.classList.remove('act'); if (UI.btnNavShare) UI.btnNavShare.classList.remove('act'); if (UI.btnNavOffline) UI.btnNavOffline.classList.remove('act'); if (UI.btnNavUpload) UI.btnNavUpload.classList.remove('act'); if (UI.btnNavHome) UI.btnNavHome.classList.add('act'); [UI.btnIdm, UI.btnBc, UI.btnAria2, UI.btnDown, UI.btnExt].forEach(b => { if(b) b.style.display = 'inline-flex'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; [UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; S.path = pathChain; if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal && needsRestoreGlobalCheck && typeof S.wasGlobalChecked !== 'undefined') { UI.chkGlobal.checked = S.wasGlobalChecked; } if (UI.scan) UI.scan.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'inline-flex'; if (UI.btnPaste) UI.btnPaste.style.display = S.clipItems && S.clipItems.length > 0 ? 'inline-flex' : 'inline-flex'; S.sort = 'modified_time'; S.dir = 1; await load(); let trackCount = 0; const maxTracks = 10; let trackerInterval = null; const stopTracking = () => { if (trackerInterval) { clearInterval(trackerInterval); trackerInterval = null; } }; let hasTriedRecovery = false; const performLocate = () => { const currentPathNode = S.path[S.path.length - 1]; const currentContextId = currentPathNode ? (currentPathNode.id || '') : ''; if (currentContextId !== targetContextId) { stopTracking(); return; } if (!S.loading && !S.itemMap.has(item.id) && !hasTriedRecovery) { hasTriedRecovery = true; console.warn(`[Locate] Target item ${item.id} not found in cache. Forcing network sync...`); const cacheKey = S.getRealCacheKey(currentContextId); S.cache.delete(cacheKey); if (typeof globalCache !== 'undefined') globalCache.delete(cacheKey); globalDirtyFolders.add(currentContextId || 'root'); updateLoadTxt(L.str_loc_stale); load(false, true); return; } if (S.loading || !S.itemMap.has(item.id)) return; S.sel.clear(); S.sel.add(item.id); S.activeId = item.id; const targetIdx = S.display.findIndex(x => x.id === item.id); if (targetIdx !== -1) { const vpHeight = UI.vp.clientHeight; const rowTop = targetIdx * CONF.rowHeight; const centerScroll = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); if (Math.abs(UI.vp.scrollTop - centerScroll) > (CONF.rowHeight / 2)) { UI.vp.scrollTop = centerScroll; renderVisible(); } const row = UI.in.querySelector(`.pk-row[data-id="${item.id}"]`); if (row) { if (!row.dataset.flashing) { row.dataset.flashing = "true"; row.style.transition = "none"; row.style.backgroundColor = "rgba(255, 193, 7, 0.5)"; void row.offsetWidth; requestAnimationFrame(() => { row.style.transition = "background-color 1.5s ease-out"; row.style.backgroundColor = ""; setTimeout(() => { if(row) delete row.dataset.flashing; }, 1500); }); } } } }; performLocate(); trackerInterval = setInterval(() => { trackCount++; const limit = hasTriedRecovery ? 25 : 10; if (S.loading || trackCount <= limit) performLocate(); else stopTracking(); }, 200); } catch (e) { console.error(e); showAlert(`${L.str_error}: ${e.message}`); if (UI.vp) UI.vp.style.opacity = '1'; } finally { setLoad(false); setTimeout(() => { if (UI.vp) UI.vp.style.opacity = '1'; }, 100); } }; } ctx.querySelector('#ctx-ext-play').onclick = () => { ctx.style.display = 'none'; UI.btnExt.click(); }; ctx.querySelector('#ctx-open').onclick = () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; if (!id) return; const item = S.items.find(x => x.id === id); if (!item) return; if (item.kind === 'drive#folder') { if (S.loading) return; S.path.push({ id: item.id, name: item.name }); load(); } else { const mime = (item.mime_type || "").toLowerCase(); const name = (item.name || "").toLowerCase(); if (name.endsWith('.torrent')) { handleTorrentFile(item); } else if (mime.includes('zip') || mime.includes('rar') || mime.includes('7z') || mime.includes('compressed') || mime.includes('archive') || name.endsWith('.zip') || name.endsWith('.rar') || name.endsWith('.7z') || name.endsWith('.tar') || name.endsWith('.gz')) { handleOpenArchive(item); } else if (mime.startsWith('video')) { playVideo(item); } else if (mime.startsWith('image')) { showImage(item); } else { UI.btnExt.click(); } } }; const starBtnCtx = ctx.querySelector('#ctx-star'); if (starBtnCtx) { starBtnCtx.onclick = async (e) => { ctx.style.display = 'none'; const action = e.target.getAttribute('data-action'); const isStar = (action === 'star'); const rawIds = Array.from(S.sel); const ids = rawIds.filter(id => { const it = S.itemMap.get(id); if (!it) return false; if (!S.trashMode && isSystemItem(it)) return false; const isCurrentlyStarred = !!(it.starred || (it.tags && it.tags.some(t => t.name === 'STAR'))); if (isStar && isCurrentlyStarred) return false; if (!isStar && !isCurrentlyStarred) return false; return true; }); if (ids.length === 0) { showToast(isStar ? L.msg_star_added : L.msg_unstar_done); return; } const starTask = FloatBarManager.create(isStar ? L.msg_starring : L.msg_unstarring); const total = ids.length; let successCount = 0; try { const url = `https://api-drive.mypikpak.com/drive/v1/files:${action}`; const headers = getHeaders(); const BATCH_SIZE = 100; for (let i = 0; i < total; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); const chunkSet = new Set(chunk); starTask.update(`${isStar ? L.msg_starring : L.msg_unstarring} ${Math.min(i + BATCH_SIZE, total)} / ${total}`); const res = await fetch(url, { method: 'POST', headers: headers, body: JSON.stringify({ "ids": chunk }) }); if (!res.ok) { const errText = await res.text(); if (res.status === 400 && errText.includes('captcha')) throw new Error(L.err_captcha_simple); throw new Error(`API ${res.status}`); } chunk.forEach(id => { if (isStar) S.starredSet.add(id); else S.starredSet.delete(id); const syncObject = (o) => { if (!o) return; o.starred = isStar; if (!o.tags) o.tags = []; if (isStar) { if (!o.tags.some(t => t.name === 'STAR')) o.tags.push({name: 'STAR', type: 0}); } else { o.tags = o.tags.filter(t => t.name !== 'STAR'); } }; syncObject(S.itemMap.get(id)); const deepSync = (cacheMap) => { if (!cacheMap) return; cacheMap.forEach((data) => { const list = Array.isArray(data) ? data : (data?.items || []); const target = list.find(f => f.id === id); if (target) syncObject(target); }); }; deepSync(globalCache); deepSync(S.cache); }); if (!isStar && S.starredMode && S.path.length === 1) { S.items = S.items.filter(it => !chunkSet.has(it.id)); S.display = S.display.filter(d => !d.isHeader && !chunkSet.has(d.id)); renderVisible(); updateStat(); } else { renderVisible(); } successCount += chunk.length; if (total > BATCH_SIZE) await sleep(50); } starTask.destroy(); showToast(isStar ? L.msg_star_added : L.msg_unstar_done); } catch (err) { console.error(err); if (starTask) starTask.destroy(); ids.forEach(id => { const revertStatus = !isStar; if (revertStatus) S.starredSet.add(id); else S.starredSet.delete(id); const item = S.itemMap.get(id); if (item) { item.starred = revertStatus; if (!item.tags) item.tags = []; if (revertStatus) { if (!item.tags.some(t => t.name === 'STAR')) item.tags.push({name: 'STAR', type: 0}); } else { item.tags = item.tags.filter(t => t.name !== 'STAR'); } } }); renderVisible(); showAlert(err.message); } }; } const observeUnzipTask = (taskId, folderId, fileId, skipUiRefresh = false) => { const checkStatus = async () => { try { const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/progress?task_id=${taskId}`, { headers: getHeaders() }); if (!res.ok) return; const data = await res.json(); if (data.phase === 'PHASE_TYPE_COMPLETE') { console.log(`[Unzip] Task finished: ${taskId}`); if (S && S.sel) S.clearSelection(); let physicalParentId = folderId || 'root'; if (fileId && S.itemMap.has(fileId)) { const it = S.itemMap.get(fileId); if (!it.params) it.params = {}; it.params.global_file_kind = '1'; if (it.parent_id) physicalParentId = it.parent_id; else if (it.parent_id === '') physicalParentId = 'root'; } S.items.forEach(it => { if ((it.kind === 'drive#task' || S.offlineMode || S.uploadMode) && it.file_id === fileId) { if (!it.params) it.params = {}; it.params.global_file_kind = '1'; } }); if (skipUiRefresh) return; if (S && S.sel && S.sel.size > 0) S.clearSelection(); refresh(); updateStat(); const dirtyTargets = new Set(); if (folderId) dirtyTargets.add(folderId); else dirtyTargets.add('root'); dirtyTargets.add(physicalParentId); dirtyTargets.forEach(target => { globalDirtyFolders.add(target); if (target === 'root') globalDirtyFolders.add(''); if (typeof globalCache !== 'undefined') globalCache.delete(target); if (typeof pkState !== 'undefined' && pkState && pkState.cache) pkState.cache.delete(target); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(target === 'root' ? '' : target); backgroundQueue.unshift({ id: target === 'root' ? '' : target, name: 'Unzipped_Update', retryCount: 0 }); }); runBackgroundCrawler(); const curPathNode = S.path[S.path.length - 1]; const curId = curPathNode.id || 'root'; if (dirtyTargets.has(curId) || curId === 'virtual_search_root' || S.isFlattened || S.dupMode) { if (window.pkSmartRefreshTrigger) { console.log(`[Unzip] Triggering force sync for ${curId}`); setTimeout(() => window.pkSmartRefreshTrigger(true), 1200); } } else { dirtyTargets.forEach(target => { apiList(target === 'root' ? '' : target, 500, null, null, false, true).then(newFiles => { if (typeof globalCache !== 'undefined') globalCache.set(target, newFiles); }).catch(()=>{}); }); } return; } if (data.phase === 'PHASE_TYPE_RUNNING' || data.phase === 'PHASE_TYPE_PENDING') { setTimeout(checkStatus, 4000); } } catch (e) { setTimeout(checkStatus, 8000); } }; checkStatus(); }; const askForPassword = (fileName, errorMsg, isBatch = false) => { return new Promise((resolve) => { const txtCancel = isBatch ? L.btn_skip : L.btn_cancel; const txtConfirm = isBatch ? L.btn_ok : L.btn_view_file; const m = showModal(` <div style="display:flex; flex-direction:column; height:100%; overflow:hidden;"> <div style="padding: 24px 50px 0 24px; flex-shrink:0;"> <div style="font-size:16px; font-weight:bold; color:var(--pk-fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; line-height:1.4;" title="${esc(fileName)}"> ${esc(fileName)} </div> </div> <div style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; padding: 0 40px;"> <div style="margin-bottom:16px; color:#FFC107;"> ${CONF.typeIcons.archive.replace(/width="\d+"/, 'width="64"').replace(/height="\d+"/, 'height="64"')} </div> <div style="font-size:14px; color:var(--pk-fg); margin-bottom:8px; font-weight:500;"> ${L.title_input_pwd} </div> <div style="font-size:12px; color:#ff4d4f; height:20px; margin-bottom:10px; opacity:${errorMsg?1:0}; font-weight:bold;"> ${esc(errorMsg) || L.err_pwd_simple} </div> <div style="position:relative; width:100%;"> <input type="text" id="retry_pwd" placeholder="${L.lbl_pwd_prompt}" style="width:100%; height:44px; background:var(--pk-hl); border:none; border-radius:6px; padding:0 36px 0 12px; font-size:14px; color:var(--pk-fg); outline:none; box-sizing:border-box; transition:background 0.2s;" autocomplete="off" name="pk_pwd_no_fill_${Date.now()}"> <div id="pwd_clear_btn" style="position:absolute; right:0; top:0; bottom:0; width:36px; display:none; align-items:center; justify-content:center; cursor:pointer; color:#999; transition:color 0.2s;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> </div> </div> </div> <div style="background:var(--pk-hl); padding:16px 24px; display:flex; justify-content:flex-end; gap:12px; flex-shrink:0;"> <button class="pk-btn" id="ask_skip" style="height:36px; padding:0 16px; font-size:14px; color:#666; background:transparent; border:none; font-weight:500;"> ${txtCancel} </button> <button class="pk-btn pri" id="ask_retry" style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); color:#fff; font-weight:600; font-size:14px; border:none; box-shadow:0 2px 5px rgba(0,0,0,0.1);"> ${txtConfirm} </button> </div> </div> `); 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(` <div style="display:flex; flex-direction:column; height:100%; overflow:hidden;"> <div style="padding: 20px 24px 0 24px; flex-shrink:0;"> <div style="font-size:16px; font-weight:700; color:var(--pk-fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-right:20px; line-height:1.4;" data-pk-tip="${esc(file.name)}"> ${esc(file.name)} </div> </div> <div id="arc_crumb" class="pk-no-scrollbar" style="display:flex; align-items:center; padding: 10px 24px; overflow-x:auto; white-space:nowrap; flex-shrink:0; font-size:14px; color:var(--pk-fg);"> </div> <div id="arc_list_container" class="pk-scroll" style="flex:1; overflow-y:auto; overflow-x:hidden; position:relative;"> <div id="arc_loading" style="position:absolute; inset:0; background:var(--pk-tip-bg); backdrop-filter:blur(4px); display:none; align-items:center; justify-content:center; z-index:3;"> <div class="pk-spin-lg" style="width:30px; height:30px; border-width:3px;"></div> </div> </div> <div style="background:var(--pk-hl); padding:16px 24px; display:flex; align-items:center; justify-content:space-between; flex-shrink:0;"> <span id="unzip_progress_text" style="font-size:13px; font-weight:bold; color:var(--pk-pri); opacity:0; transition:opacity 0.2s;"></span> <div style="display:flex; gap:12px; margin-left:auto;"> <button class="pk-btn" id="unzip_cancel" style="height:36px; padding:0 16px; font-size:14px; color:#666; background:transparent; border:none; font-weight:500;"> </button> <button class="pk-btn pri" id="unzip_confirm" style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); color:#fff; font-weight:600; font-size:14px; border:none; box-shadow:0 2px 5px rgba(0,0,0,0.1);"> ${L.btn_unzip_all} </button> </div> </div> </div> `); 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 = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block; opacity:0.5;"><polyline points="9 18 15 12 9 6"></polyline></svg>`; sep.style.margin = "0 6px"; crumbContainer.appendChild(sep); } }); arcCrumbIdx = pathNodes.length - 1; requestAnimationFrame(() => { crumbContainer.scrollTo({ left: crumbContainer.scrollWidth, behavior: 'smooth' }); }); listContainer.innerHTML = ''; listContainer.appendChild(loading); const items = data.files || data.list || []; if (items.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.style.cssText = "padding:50px; text-align:center; color:#999; font-size:13px;"; emptyMsg.textContent = L.str_empty_dir; listContainer.appendChild(emptyMsg); } else { items.forEach(f => { const itemName = f.filename || f.file_name || f.name || "Unknown"; const itemSize = f.filesize || f.size || 0; const isDir = f.kind === 'drive#folder' || (itemSize == 0 && !f.mime_type); const fullPath = (path || "") + itemName + (isDir ? "/" : ""); const row = document.createElement('div'); row.style.cssText = "display:flex; align-items:center; height:48px; padding:0 24px; cursor:default; transition:background 0.1s; border-bottom:1px solid rgba(0,0,0,0.03);"; if (isDir) { row.style.cursor = "pointer"; row.onmouseover = () => row.style.background = 'var(--pk-hl)'; row.onmouseout = () => row.style.background = 'transparent'; row.onclick = (e) => { e.stopPropagation(); pathNodes.push({ name: itemName, path: fullPath }); updateView(fullPath); }; } else { row.onmouseover = () => row.style.background = 'var(--pk-hl)'; row.onmouseout = () => row.style.background = 'transparent'; } const iconSrc = f.icon_link || ''; const fallbackSvg = (isDir ? CONF.typeIcons.folder : getIcon({ name: itemName, mime_type: f.mime_type })) .replace(/width="\d+"/, 'width="28"').replace(/height="\d+"/, 'height="28"'); const iconHtml = iconSrc ? `<img src="${iconSrc}" style="width:28px;height:28px;object-fit:contain;flex-shrink:0;" onerror="this.style.display='none';if(this.nextElementSibling)this.nextElementSibling.style.display='inline-flex';"><span style="display:none;align-items:center;flex-shrink:0;">${fallbackSvg}</span>` : fallbackSvg; row.innerHTML = ` <div style="margin-right:12px; display:flex; align-items:center; justify-content:center; width:28px;">${iconHtml}</div> <div style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; color:var(--pk-fg); line-height:1.5;">${esc(itemName)}</div> <div style="color:var(--pk-fg); font-size:14px; line-height:1.5; margin-left:15px; font-weight:normal;">${isDir ? '-' : fmtSize(itemSize)}</div> `; 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(` <h3>${L.title_confirm}</h3> <div style="margin:20px 0;line-height:1.5;">${L.msg_unzip_virtual_view_warn}</div> <div class="pk-modal-act"> <button class="pk-btn" id="vc_cancel">${L.btn_cancel}</button> <button class="pk-btn pri" id="vc_ok">${L.btn_understand_unzip}</button> </div> `); 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(` <h3>${L.title_confirm}</h3> <div style="margin:20px 0;line-height:1.5;">${L.msg_unzip_virtual_view_warn}</div> <div class="pk-modal-act"> <button class="pk-btn" id="vc_cancel">${L.btn_cancel}</button> <button class="pk-btn pri" id="vc_ok">${L.btn_understand_unzip}</button> </div> `); 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 = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`; const navRight = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`; let sideHtml = sideOpts.map(o => `<div class="pk-cal-side-item" data-val="${o.val}">${o.lbl}</div>` ).join(''); const monthStr = (L.cal_months || [])[viewMonth] || (viewMonth + 1 + L.unit_month); const headerHtml = ` <div class="pk-cal-nav-btn" id="cal_prev_y" title="-1Y">${navLeft}</div> <div class="pk-cal-nav-btn" id="cal_prev_m" title="-1M" style="margin-right:auto;">${navLeft}</div> <div style="font-weight:bold; font-size:14px;">${viewYear} ${monthStr}</div> <div class="pk-cal-nav-btn" id="cal_next_m" title="+1M" style="margin-left:auto;">${navRight}</div> <div class="pk-cal-nav-btn" id="cal_next_y" title="+1Y">${navRight}</div> `; const weeksHtml = (L.cal_week_days || ["S","M","T","W","T","F","S"]).map(w => `<div class="pk-cal-th">${w}</div>` ).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<firstDay; i++) daysHtml += `<div></div>`; 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 += `<div class="${cls}" data-ts="${currentTs}">${d}</div>`; } return ` <div class="pk-cal-side">${sideHtml}</div> <div class="pk-cal-main"> <div style="font-weight:bold; margin-bottom:10px; font-size:14px;">${L.cal_custom_title}</div> <div class="pk-cal-hd">${headerHtml}</div> <div class="pk-cal-grid"> ${weeksHtml} ${daysHtml} </div> </div> `; }; const refresh = () => { pop.innerHTML = buildHTML(); pop.querySelectorAll('.pk-cal-side-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const days = parseInt(el.dataset.val); onSelect({ days: days, ts: null }); pop.remove(); document.removeEventListener('mousedown', onClickOutside); }; }); pop.querySelectorAll('.pk-cal-td:not(.disabled)').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const ts = parseInt(el.dataset.ts); const diff = (ts - todayStart) / (1000 * 3600 * 24); const days = Math.max(1, Math.ceil(diff)); onSelect({ days: days, ts: ts }); pop.remove(); document.removeEventListener('mousedown', onClickOutside); }; }); pop.querySelector('#cal_prev_y').onclick = (e) => { e.stopPropagation(); viewYear--; refresh(); }; pop.querySelector('#cal_next_y').onclick = (e) => { e.stopPropagation(); viewYear++; refresh(); }; pop.querySelector('#cal_prev_m').onclick = (e) => { e.stopPropagation(); viewMonth--; if(viewMonth<0){viewMonth=11;viewYear--;} refresh(); }; pop.querySelector('#cal_next_m').onclick = (e) => { e.stopPropagation(); viewMonth++; if(viewMonth>11){viewMonth=0;viewYear++;} refresh(); }; }; refresh(); document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; if (anchorEl.offsetParent === null) { cleanup(); return; } const scale = parseFloat(document.documentElement.style.getPropertyValue('--pk-zoom')) || 1; const rect = anchorEl.getBoundingClientRect(); let top = (rect.bottom / scale) + 5; let left = rect.left / scale; if (top + 320 > window.innerHeight / scale) top = (rect.top / scale) - 320 - 5; if (left + 400 > window.innerWidth / scale) left = (window.innerWidth / scale) - 400 - 10; if (left < 10) left = 10; pop.style.top = top + 'px'; pop.style.left = left + 'px'; }; updatePosition(); requestAnimationFrame(() => { pop.style.visibility = 'visible'; void pop.offsetHeight; pop.style.opacity = '1'; }); window.addEventListener('resize', updatePosition); window.addEventListener('scroll', updatePosition, true); const onClickOutside = (e) => { if (!pop || !anchorEl || !pop.parentNode) return; if (!pop.contains(e.target) && !anchorEl.contains(e.target)) { cleanup(); } }; const cleanup = () => { window.removeEventListener('resize', updatePosition); window.removeEventListener('scroll', updatePosition, true); document.removeEventListener('mousedown', onClickOutside); pop.remove(); }; setTimeout(() => document.addEventListener('mousedown', onClickOutside), 10); }; const showShareDetail = (item) => { const checkIcon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#52c41a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="animation: pkPlayPop 0.3s ease-out;"><polyline points="20 6 9 17 4 12"></polyline></svg>`; const copyIcon = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="cursor:pointer;color:#888;"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`; 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(` <div class="pk-share-modal-root" style="width:420px; max-width:90vw; display:flex; flex-direction:column; overflow:hidden;"> <div style="display: flex; align-items: center; padding: 16px 50px 16px 24px; flex-shrink:0; border-bottom: 1px solid transparent;"> <h3 style="margin: 0; font-size: 18px; font-weight: 700; border: none; padding: 0; line-height: 1.2; color: var(--pk-fg);">${L.title_share_detail}</h3> </div> <div class="pk-scroll" style="flex:1; overflow-y:auto; overflow-x:hidden; padding: 0 24px;"> <div style="margin-bottom:16px; text-align:center; padding: 0 10px;"> <div style="font-size:15px; font-weight:700; color:var(--pk-fg); word-break:break-all; line-height:1.4; opacity:0.9;">${esc(item.name || item.title)}</div> </div> <div class="pk-share-stat-box"> <div class="pk-share-stat-item"> <div class="pk-share-stat-val">${item.view_count || 0}</div> <div class="pk-share-stat-lbl">${L.lbl_share_view}</div> </div> <div class="pk-share-stat-item"> <div class="pk-share-stat-val">${item.save_count || 0}</div> <div class="pk-share-stat-lbl">${L.lbl_share_save}</div> </div> </div> <div style="margin-bottom:20px;"> <div style="${labelStyle}">${L.lbl_share_link_title}</div> <div class="pk-share-input-wrap" style="${inputWrapStyle}"> <a href="${item.share_url}" target="_blank" style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-decoration:underline; color:var(--pk-fg); font-size:14px;">${item.share_url}</a> <div class="pk-copy-btn" data-val="${item.share_url}" style="${copyBtnStyle}" data-pk-tip="${L.btn_copy}">${copyIcon}</div> </div> </div> <div style="margin-bottom:20px;"> <div style="${labelStyle}">${L.lbl_share_pwd_title}</div> <div class="pk-share-input-wrap" style="${inputWrapStyle}"> <input type="text" id="pk_share_pwd_edit" value="${item.pass_code || ''}" style="${inputStyle}" placeholder="${L.str_no_pwd}"> <div class="pk-copy-btn" id="pk_copy_pwd" data-val="${item.pass_code || ''}" style="${copyBtnStyle}; display: ${item.pass_code ? 'flex' : 'none'};" data-pk-tip="${L.btn_copy}">${copyIcon}</div> </div> </div> <div style="margin-bottom:20px;"> <div style="${labelStyle}">${L.share_count_ed}</div> <div class="pk-share-input-wrap" id="pk_share_limit_edit" style="${inputWrapStyle}; cursor:pointer;"> <input type="text" id="pk_share_limit_val" readonly style="${inputStyle}; cursor:pointer; color:var(--pk-fg);" value="${item.limit_count > 0 ? (item.limit_count + ' ' + L.share_times) : L.share_unlimit}"> <div style="${copyBtnStyle}">${CONF.crumbIcons.right}</div> </div> </div> <div style="margin-bottom:20px;"> <div style="${labelStyle}">${L.lbl_share_expire_title}</div> <div class="pk-share-input-wrap" id="pk_share_exp_edit" style="${inputWrapStyle}; cursor:pointer;"> <input type="text" id="pk_share_exp_val" readonly style="${inputStyle}; cursor:pointer; color:var(--pk-fg);" value="${statusText}"> <div style="${copyBtnStyle}">${CONF.crumbIcons.right}</div> </div> </div> <div style="margin-bottom:30px;"> <div style="${labelStyle}">${L.lbl_share_code_title}</div> <div id="pk_phrase_list_container" style="border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-bg); overflow:hidden;"> <div id="pk_phrase_rows"></div> <div id="pk_btn_add_phrase" style="display:flex; align-items:center; gap:8px; color:var(--pk-pri); font-size:14px; height:44px; padding:0 12px; cursor:pointer; background:var(--pk-bg); transition:background 0.2s;" onmouseover="this.style.background='var(--pk-hl)'" onmouseout="this.style.background='var(--pk-bg)'"> <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg> <span>${L.btn_add_share_code}</span> </div> </div> </div> <div class="pk-share-footer" style="flex-shrink:0; margin-top:0; padding: 20px 24px 24px 24px; display: grid; grid-template-columns: 1fr 1fr; gap: 16px;"> <button id="pk_detail_cancel" class="pk-btn-quiet-red" style="width: 100%; height: 40px; display: flex; align-items: center; justify-content: center; margin: 0; border: none; background: rgba(217, 48, 37, 0.08); font-size: 14px; font-weight: 600; border-radius: 8px; cursor: pointer; transition: background 0.2s;"> ${L.btn_cancel_share} </button> <button id="pk_detail_copy_all" class="pk-btn-primary-action" style="width: 100%; height: 40px; display: flex; align-items: center; justify-content: center; margin: 0; border-radius: 8px;"> ${item.pass_code ? L.btn_copy_link_pwd : L.btn_copy_link} </button> </div> </div> `); 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 = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#52c41a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`; 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 = ` <div class="pk-modal" style="width:340px; padding:24px; border-radius:12px; box-shadow: 0 8px 30px rgba(0,0,0,0.3);"> <div class="pk-modal-close">${CONF.icons.close}</div> <h3 style="border:none; margin:0 0 16px 0; font-size:16px; font-weight:700; color:var(--pk-fg);">${L.title_edit_pwd}</h3> <div class="pk-field"> <input type="text" id="pk_new_pwd_input" placeholder="${L.ph_edit_pwd}" style="height:44px; font-size:14px; border:1px solid var(--pk-pri); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); padding:0 12px; outline:none;" value="${item.pass_code || ''}" autocomplete="off"> </div> <div id="pk_close_pwd_row" style="display:${item.pass_code ? 'flex' : 'none'}; align-items:center; gap:8px; margin-top:12px; cursor:pointer; color:var(--pk-pri); font-size:14px; width:fit-content; transition:opacity 0.2s;" onmouseover="this.style.opacity=0.8" onmouseout="this.style.opacity=1"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg> <span>${L.btn_close_pwd}</span> </div> <div style="display:flex; justify-content:flex-end; align-items:center; margin-top:24px; gap:20px;"> <span id="pk_edit_pwd_cancel" style="cursor:pointer; color:#888; font-size:14px; font-weight:500;">${L.btn_cancel}</span> <button class="pk-btn pri" id="pk_edit_pwd_save" disabled style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); border:none; color:#fff; font-weight:600; transition:all 0.2s; opacity: 0.4; cursor: not-allowed;">${L.btn_save}</button> </div> </div> `; 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 => ` <div class="pk-phrase-row" data-val="${esc(p)}" style="display:flex; align-items:center; width:100%; height:44px; padding:0 12px; border-bottom:1px solid var(--pk-bd); cursor:pointer; transition:background 0.1s; background:var(--pk-bg);"> <div style="flex:1; border:none; background:transparent; outline:none; font-size:14px; color:var(--pk-fg); font-weight:bold; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${esc(p)}</div> <div class="pk-copy-btn" data-val="${esc(p)}" style="margin-left:10px; display:flex; align-items:center; justify-content:center; cursor:pointer; color:#888;" data-pk-tip="${L.btn_copy}"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> </div> </div> `).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 = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#52c41a" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`; 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 = ` <div class="pk-modal" style="width:340px; padding:24px; border-radius:12px; box-shadow: 0 8px 30px rgba(0,0,0,0.3);"> <div class="pk-modal-close">${CONF.icons.close}</div> <h3 style="border:none; margin:0 0 16px 0; font-size:16px; font-weight:700; color:var(--pk-fg);">${oldVal ? L.title_edit_share_code : L.btn_add_share_code}</h3> <div class="pk-field"> <input type="text" id="pk_new_phrase_input" placeholder="${L.ph_edit_share_code}" style="height:44px; font-size:14px; border:1px solid var(--pk-pri); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); padding:0 12px; outline:none;" value="${oldVal || ''}" autocomplete="off"> </div> <div id="pk_del_phrase_row" style="display:${oldVal ? 'flex' : 'none'}; align-items:center; gap:8px; margin-top:12px; cursor:pointer; color:var(--pk-pri); font-size:14px; width:fit-content; transition:opacity 0.2s;" onmouseover="this.style.opacity=0.8" onmouseout="this.style.opacity=1"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg> <span>${L.btn_del_share_code}</span> </div> <div style="display:flex; justify-content:flex-end; align-items:center; margin-top:24px; gap:20px;"> <span id="pk_edit_phrase_cancel" style="cursor:pointer; color:#888; font-size:14px; font-weight:500;">${L.btn_cancel}</span> <button class="pk-btn pri" id="pk_edit_phrase_save" disabled style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); border:none; color:#fff; font-weight:600; transition:all 0.2s; opacity: 0.4; cursor: not-allowed;">${L.btn_save}</button> </div> </div> `; 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 = ` <div class="pk-modal" style="width:340px; padding:24px; border-radius:12px; box-shadow: 0 8px 30px rgba(0,0,0,0.3);"> <div class="pk-modal-close">${CONF.icons.close}</div> <h3 style="border:none; margin:0 0 24px 0; font-size:16px; font-weight:700; color:var(--pk-fg);">${L.share_count_ed}</h3> <div class="pk-s-opts" style="flex-direction:column; align-items:flex-start; gap:16px;"> <div style="display:flex; gap:15px; width:100%;"> <label class="pk-s-opt"><input type="radio" name="sh_mod_cnt" value="-1" ${currentLimit===0?'checked':''}> ${L.share_unlimit}</label> <label class="pk-s-opt"><input type="radio" name="sh_mod_cnt" value="100" ${currentLimit===100?'checked':''}> 100${L.share_times}</label> </div> <div style="display:flex; align-items:center; gap:10px; width:100%;"> <label class="pk-s-opt" style="flex-shrink:0;"><input type="radio" name="sh_mod_cnt" value="custom" ${![0,100].includes(currentLimit)?'checked':''}> ${L.share_custom}</label> <div style="position:relative; flex:1;"> <input type="number" id="sh_mod_cnt_val" class="pk-s-input" value="${currentLimit > 0 ? currentLimit : ''}" placeholder="> ${currentSave}" style="width:100%; height:36px; padding-right:32px;" ${![0,100].includes(currentLimit)?'':'disabled'}> <div class="pk-num-ctrl"> <div class="pk-num-btn" id="sh_cnt_inc">${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}</div> <div class="pk-num-btn" id="sh_cnt_dec">${CONF.crumbIcons.down}</div> </div> </div> </div> </div> <div style="display:flex; justify-content:flex-end; align-items:center; margin-top:24px; gap:20px;"> <span id="pk_mod_cnt_cancel" style="cursor:pointer; color:#888; font-size:14px; font-weight:500;">${L.btn_cancel}</span> <button class="pk-btn pri" id="pk_mod_cnt_save" style="height:36px; padding:0 24px; border-radius:6px; background:var(--pk-pri); border:none; color:#fff; font-weight:600; transition:all 0.2s;">${L.btn_save}</button> </div> </div> `; document.body.appendChild(subM); const radios = subM.querySelectorAll('input[name="sh_mod_cnt"]'); const input = subM.querySelector('#sh_mod_cnt_val'); const saveBtn = subM.querySelector('#pk_mod_cnt_save'); const ctrl = subM.querySelector('.pk-num-ctrl'); const updateCtrlState = () => { ctrl.style.opacity = input.disabled ? '0.3' : '1'; ctrl.style.pointerEvents = input.disabled ? 'none' : 'auto'; ctrl.style.cursor = input.disabled ? 'not-allowed' : 'default'; }; radios.forEach(r => r.onchange = () => { input.disabled = r.value !== 'custom'; if (!input.disabled) input.focus(); updateCtrlState(); }); updateCtrlState(); subM.querySelector('#sh_cnt_inc').onclick = (e) => { if (input.disabled) return; e.stopPropagation(); input.value = (parseInt(input.value) || currentSave) + 1; validate(); }; subM.querySelector('#sh_cnt_dec').onclick = (e) => { if (input.disabled) return; e.stopPropagation(); input.value = Math.max(currentSave + 1, (parseInt(input.value) || (currentSave + 2)) - 1); validate(); }; const doSave = async () => { const rVal = subM.querySelector('input[name="sh_mod_cnt"]:checked').value; let newLimit = 0; if (rVal === 'custom') { const val = parseInt(input.value); if (isNaN(val) || val <= 0) { input.style.borderColor = '#d93025'; return; } newLimit = val; } else { newLimit = parseInt(rVal); if (newLimit === -1) newLimit = 0; } if (newLimit > 0 && newLimit <= currentSave) { showToast(L.err_limit_too_low.replace('{n}', newLimit).replace('{s}', currentSave), 'error'); if(input.disabled) input.value = currentSave + 1; return; } saveBtn.textContent = "..."; saveBtn.disabled = true; item.limit_count = newLimit; const store = JSON.parse(gmGet('pk_share_limits', '{}')); if (newLimit > 0) { store[item.id] = newLimit; } else { delete store[item.id]; } gmSet('pk_share_limits', JSON.stringify(store)); if (newLimit === 0) { limitInput.value = L.share_unlimit; } else { limitInput.value = `${newLimit} ${L.share_times}`; } limitInput.style.color = 'var(--pk-fg)'; renderVisible(); if (window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(true); } setTimeout(() => { subM.remove(); showToast(L.msg_limit_updated); }, 200); }; subM.querySelector('#pk_mod_cnt_cancel').onclick = () => subM.remove(); subM.querySelector('.pk-modal-close').onclick = () => subM.remove(); saveBtn.onclick = doSave; input.onkeydown = (e) => { if(e.key === 'Enter') doSave(); }; }; const expField = m.querySelector('#pk_share_exp_edit'); const expInput = m.querySelector('#pk_share_exp_val'); expField.onclick = (e) => { e.stopPropagation(); const existing = document.querySelector('.pk-cal-pop'); if (existing) { existing.remove(); return; } renderCalendar(expField, async (res) => { const selectedDays = res.days; if (selectedDays === null) return; const originalText = expInput.value; expInput.value = "..."; try { const payload = {}; let newText = ""; let newStatusLeft = ""; if (selectedDays === -1) { payload.expiration_at = "-1"; newText = L.share_perm; newStatusLeft = "-1"; } else { const d = res.ts ? new Date(res.ts) : new Date(getServerNow() + selectedDays * 24 * 3600 * 1000); const pad = n => String(n).padStart(2, '0'); const ds = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; payload.expiration_at = `${ds}T23:59:59.000+08:00`; newText = res.ts ? ds : (selectedDays + L.share_days); if (res.ts) { const diff = Math.max(1, Math.ceil((res.ts - new Date(getServerNow()).setHours(0,0,0,0)) / (1000 * 3600 * 24))); newStatusLeft = diff + L.unit_days.trim(); } else { newStatusLeft = selectedDays + L.share_days; } } await apiUpdateShare(item.id, payload); item.expiration_days = selectedDays; item.expiration_at = payload.expiration_at; item.expiration_left = newStatusLeft; expInput.value = newText; expInput.style.color = 'var(--pk-fg)'; renderVisible(); const toast = document.createElement('div'); toast.style.cssText = "position:absolute; top:180px; left:50%; transform:translateX(-50%); width:max-content; background:var(--pk-toast-bg); color:var(--pk-toast-fg); padding:10px 24px; border-radius:8px; font-size:14px; font-weight:600; z-index:10007; pointer-events:none; box-shadow:0 8px 24px var(--pk-tip-sd); border:1px solid var(--pk-toast-bd);"; toast.textContent = L.msg_exp_updated; m.querySelector('.pk-modal').appendChild(toast); setTimeout(() => toast.remove(), 1200); } catch (err) { showAlert(`${L.str_error}: ${err.message}`); expInput.value = originalText; } }); }; m.querySelector('#pk_detail_cancel').onclick = async () => { if (await showConfirm(L.msg_cancel_share_confirm.replace('{n}', 1))) { setLoad(true); try { await apiCancelShare([item.id]); m.remove(); showAlert(L.msg_cancel_share_done.replace('{n}', 1)); load(false, true); } catch (e) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); } } }; m.querySelector('#pk_detail_copy_all').onclick = () => { const url = item.share_url; const pwd = item.pass_code || ''; const title = item.name || item.title; let text = url + '\n'; if (pwd) text += `${L.share_copy_pwd}: ${pwd}\n`; text += `${title}\n${L.share_copy_suffix}`; GM_setClipboard(text); const btn = m.querySelector('#pk_detail_copy_all'); const orgTxt = btn.textContent; btn.textContent = L.msg_copy_success; const originalColor = btn.style.backgroundColor; const originalBorder = btn.style.borderColor; btn.style.backgroundColor = "#52c41a"; btn.style.borderColor = "#52c41a"; setTimeout(() => { btn.textContent = orgTxt; btn.style.backgroundColor = originalColor; btn.style.borderColor = originalBorder; }, 1500); }; }; UI.btnCancelShare.onclick = async () => { const n = S.sel.size; if (n === 0) return; if (!await showConfirm(L.msg_cancel_share_confirm.replace('{n}', n))) return; setLoad(true); updateLoadTxt(L.str_processing); try { const selectedItems = Array.from(S.sel).map(id => S.itemMap.get(id)).filter(Boolean); const serverIds = selectedItems.filter(it => !it._is_local_phantom).map(it => it.id); const phantomIds = selectedItems.filter(it => it._is_local_phantom).map(it => it.id); if (serverIds.length > 0) { await apiCancelShare(serverIds); } if (phantomIds.length > 0) { const graveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); const newGraveyard = graveyard.filter(x => !phantomIds.includes(x.id)); gmSet('pk_expired_shares', JSON.stringify(newGraveyard)); } S.clearSelection(); await load(false, true); showToast(L.msg_cancel_share_done.replace('{n}', n)); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); } }; if (UI.btnCopyLinkOffline) { UI.btnCopyLinkOffline.onclick = () => { const ids = Array.from(S.sel); if (ids.length === 0) return; const tasks = ids.map(id => S.itemMap.get(id)).filter(t => t && (t.source_url || (t.params && t.params.url))); if (tasks.length === 0) return; const urls = tasks.map(t => t.source_url || t.params.url).join('\n'); GM_setClipboard(urls); showToast(L.msg_copy_success); }; } if (UI.btnRetryTask) { UI.btnRetryTask.onclick = async () => { const ids = Array.from(S.sel); if (ids.length === 0) return; const targets = ids.map(id => S.itemMap.get(id)) .filter(t => t && t.phase === 'PHASE_TYPE_ERROR' && (t.source_url || (t.params && t.params.url))); if (targets.length === 0) { showToast(L.err_no_failed_task, "error"); return; } setLoad(true); updateLoadTxt(L.str_processing); let successCount = 0; try { for (const task of targets) { const url = task.source_url || task.params.url; try { await apiAddOfflineTask(url); await apiCancelTask([task.id]); successCount++; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } catch (e) { console.error(`[Retry] Failed for ${task.name}:`, e); if (e.message && e.message.includes(L.err_task_exists)) { await apiCancelTask([task.id]).catch(()=>{}); successCount++; } } await sleep(150); } await load(false, true); showToast(L.msg_retry_submitted.replace('{n}', successCount)); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); S.clearSelection(); updateStat(); } }; } UI.btnUnzip.onclick = async () => { ensureItemMap(); const isArchive = (it) => { if (!it || it.kind === 'drive#folder') return false; const n = (it.name || '').toLowerCase(); const m = (it.mime_type || '').toLowerCase(); return m.includes('zip') || m.includes('rar') || m.includes('7z') || m.includes('compressed') || m.includes('archive') || n.endsWith('.zip') || n.endsWith('.rar') || n.endsWith('.7z') || n.endsWith('.tar') || n.endsWith('.gz'); }; const rawTargets = Array.from(S.sel).map(id => S.itemMap.get(id)).filter(i => isArchive(i)); const existingFolderNames = new Set( S.items.filter(i => i.kind === 'drive#folder').map(i => i.name) ); const targets = rawTargets.filter(item => { if (rawTargets.length === 1) return true; const isMarkedUnzipped = item.params && (item.params.global_file_kind === '1' || item.params.global_file_root); if (!isMarkedUnzipped) return true; let targetFolderName = item.name; const lastDot = targetFolderName.lastIndexOf('.'); if (lastDot > 0) targetFolderName = targetFolderName.substring(0, lastDot); return !existingFolderNames.has(targetFolderName); }); const skippedItems = rawTargets.filter(item => !targets.includes(item)); const skippedCount = skippedItems.length; if (skippedCount > 0) { if (await showConfirm(L.msg_unzip_skip_del_confirm.replace('{n}', skippedCount))) { const idsToDelete = skippedItems.map(i => i.id); await executeBatchDelete(idsToDelete, { silent: true, forceRefresh: false }); showToast(L.msg_del_items_done.replace('{n}', skippedCount)); } else { showToast(L.msg_skip_unzipped.replace('{n}', skippedCount)); } } if (targets.length === 1) { handleOpenArchive(targets[0]); } else if (targets.length > 1) { handleUnzip(targets); } }; ctx.querySelector('#ctx-share').onclick = async () => { ctx.style.display = 'none'; const ids = Array.from(S.sel); if (ids.length === 0) return; const item = S.itemMap.get(ids[0]); if (!item) return; const m = showModal(` <div class="pk-share-modal"> <div style="display: flex; align-items: center; padding: 16px 50px 0 24px; min-height: 30px;"> <h3 style="margin: 0; font-size: 18px; font-weight: 600; border: none; padding: 0; line-height: 1.2;">${L.share_title}</h3> </div> <div class="pk-s-sec" style="margin-top: 20px;"> <div class="pk-s-lbl">${L.share_mode}</div> <div class="pk-s-tabs" id="sh_mode_tabs"> <div class="pk-s-tab act" data-val="public">${L.share_public}</div> <div class="pk-s-tab" data-val="encrypted">${L.share_encrypted}</div> </div> </div> <div class="pk-s-sec"> <div class="pk-s-lbl">${L.share_expiry}</div> <div class="pk-s-opts"> <label class="pk-s-opt"><input type="radio" name="sh_exp" value="7"> 7${L.share_days}</label> <label class="pk-s-opt"><input type="radio" name="sh_exp" value="14"> 14${L.share_days}</label> <label class="pk-s-opt"><input type="radio" name="sh_exp" value="30"> 30${L.share_days}</label> <label class="pk-s-opt"><input type="radio" name="sh_exp" value="-1" checked> ${L.share_perm}</label> <div class="pk-s-opt" id="sh_exp_custom_btn" style="position:relative; margin-left:auto; border:1px solid var(--pk-bd); border-radius:4px; padding:2px 8px; transition:border-color 0.2s;"> <input type="radio" name="sh_exp" value="custom" style="display:none;"> <span id="sh_custom_txt">${L.share_custom}</span> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-left:4px; transform:translateY(1px);"><polyline points="6 9 12 15 18 9"></polyline></svg> </div> </div> </div> <div class="pk-s-sec" id="sh_pass_sec" style="opacity:0.3; pointer-events:none;"> <div class="pk-s-lbl">${L.share_pass}</div> <div class="pk-s-opts"> <label class="pk-s-opt"><input type="radio" name="sh_pass_type" value="rand" checked> ${L.share_rand}</label> <label class="pk-s-opt"><input type="radio" name="sh_pass_type" value="custom"> ${L.share_custom}</label> <input type="text" id="sh_pass_val" class="pk-s-input" placeholder="${L.ph_pass_range}" minlength="4" maxlength="10" disabled> </div> </div> <div class="pk-s-sec"> <div class="pk-s-lbl">${L.share_count}</div> <div class="pk-s-opts"> <label class="pk-s-opt"><input type="radio" name="sh_cnt" value="1"> 1${L.share_times}</label> <label class="pk-s-opt"><input type="radio" name="sh_cnt" value="5"> 5${L.share_times}</label> <label class="pk-s-opt"><input type="radio" name="sh_cnt" value="20"> 20${L.share_times}</label> <label class="pk-s-opt"><input type="radio" name="sh_cnt" value="-1" checked> ${L.share_unlimit}</label> <label class="pk-s-opt"><input type="radio" name="sh_cnt" value="custom"> ${L.share_custom}</label> <input type="text" id="sh_cnt_val" class="pk-s-input" placeholder="${L.share_unlimit}" style="width:60px; height:28px; text-align:center;" disabled autocomplete="off"> </div> </div> <div class="pk-modal-act" style="margin-top:10px; padding: 0 24px 24px 24px; display: grid; grid-template-columns: 1fr 1fr; gap: 15px;"> <button class="pk-btn" id="sh_cancel" style="justify-content: center; height: 40px; font-weight: 600; width: 100%; border-radius: 8px; font-size: 15px; background:transparent;">${L.btn_cancel}</button> <button class="pk-btn pri" id="sh_go" style="justify-content: center; height: 40px; font-weight: 600; width: 100%; border-radius: 8px; font-size: 15px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); background:var(--pk-pri); color:#fff; border:none;">${L.btn_share_start}</button> </div> </div> `); 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(` <div style="width: 400px; max-width: 90vw; display: flex; flex-direction: column; align-items: center; text-align: center; padding: 10px 5px 0 5px;"> <div style="width: 64px; height: 64px; border-radius: 50%; background: rgba(26, 94, 255, 0.1); color: var(--pk-pri); display: flex; align-items: center; justify-content: center; margin-bottom: 20px;"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg> </div> <div style="font-size: 18px; font-weight: 700; color: var(--pk-fg); margin-bottom: 24px;">${L.title_share_result}</div> <div style="width: 100%; display: flex; flex-direction: column; gap: 14px; margin-bottom: 30px;"> <input type="text" class="pk-share-res-val" value="${data.share_url}" readonly style="width: 100%; height: 44px; padding: 0 16px; border: 1.5px solid var(--pk-bd); border-radius: 10px; background: var(--pk-hl); color: var(--pk-pri); font-size: 14px; font-weight: 600; text-align: center; box-sizing: border-box; outline: none; transition: border-color 0.2s;"> ${finalPassCode ? ` <div style="display: flex; align-items: center; justify-content: center; gap: 10px;"> <span style="font-size: 13px; color: #888;">${L.lbl_share_code}:</span> <span style="font-size: 18px; font-weight: 800; font-family: 'JetBrains Mono', monospace; color: var(--pk-fg); letter-spacing: 1.5px;">${finalPassCode}</span> </div>` : ''} </div> <button class="pk-btn pri" id="res_copy" style="width: 100%; height: 46px; border-radius: 12px; background: var(--pk-pri); color: #fff; font-weight: 700; font-size: 15px; border: none; box-shadow: 0 4px 14px rgba(26, 94, 255, 0.3); justify-content: center;"> ${L.btn_copy_share} </button> </div> `); const modalBox = resM.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = 'auto'; modalBox.style.minWidth = '460px'; modalBox.style.overflow = 'visible'; modalBox.style.padding = '30px'; } resM.querySelector('#res_copy').onclick = () => { GM_setClipboard(fullText); const b = resM.querySelector('#res_copy'); b.textContent = L.msg_copy_success; setTimeout(() => resM.remove(), 1000); }; } catch (e) { showAlert(e.message); } finally { setLoad(false); } }; }; ctx.querySelector('#ctx-copy-name').onclick = () => { ctx.style.display = 'none'; const names = []; S.sel.forEach(id => { const item = S.itemMap.get(id); if (item && item.name) { const name = item.name; if (item.kind !== 'drive#folder' && name.includes('.') && name.lastIndexOf('.') > 0) { names.push(name.substring(0, name.lastIndexOf('.'))); } else { names.push(name); } } }); if (names.length > 0) { GM_setClipboard(names.join('\n')); showToast(L.msg_copy_success); } }; ctx.querySelector('#ctx-down').onclick = () => { ctx.style.display = 'none'; UI.win.querySelector('#pk-down').click(); }; ctx.querySelector('#ctx-add-bl').onclick = (e) => { ctx.style.display = 'none'; const action = e.target.getAttribute('data-action'); processBlacklistAction(action); }; ctx.querySelector('#ctx-copy').onclick = () => { ctx.style.display = 'none'; UI.btnCopy.click(); }; ctx.querySelector('#ctx-cut').onclick = () => { ctx.style.display = 'none'; UI.btnCut.click(); }; ctx.querySelector('#ctx-prune').onclick = () => { ctx.style.display = 'none'; UI.btnPrune.click(); }; ctx.querySelector('#ctx-rename').onclick = () => { ctx.style.display = 'none'; if (S.sel.size > 1) { UI.btnBulkRename.click(); } else { UI.btnRename.click(); } }; const ctxDel = ctx.querySelector('#ctx-del'); ctxDel.onclick = () => { ctx.style.display = 'none'; UI.btnDel.click(); }; if (S.historyMode) { const ctxDelTxt = ctxDel.childNodes[1]; if (ctxDelTxt) ctxDelTxt.textContent = " " + L.btn_clear_history; } const ctxShCancel = ctx.querySelector('#ctx-sh-cancel'); if (ctxShCancel) { ctxShCancel.onclick = () => { ctx.style.display = 'none'; if (UI.btnCancelShare) UI.btnCancelShare.click(); }; } const ctxShDetail = ctx.querySelector('#ctx-sh-detail'); if (ctxShDetail) { ctxShDetail.onclick = () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (item) showShareDetail(item); }; } const ctxShCopy = ctx.querySelector('#ctx-sh-copy'); if (ctxShCopy) { ctxShCopy.onclick = () => { ctx.style.display = 'none'; const id = Array.from(S.sel)[0]; const item = S.itemMap.get(id); if (!item) return; const url = item.share_url || ""; const pwd = item.pass_code || ""; const title = item.name || item.title || ""; let text = url + '\n'; if (pwd) text += `${L.share_copy_pwd}: ${pwd}\n`; text += `${title}\n${L.share_copy_suffix}`; GM_setClipboard(text); showToast(L.msg_copy_success); }; } const ctxRestore = ctx.querySelector('#ctx-restore'); if (ctxRestore) { ctxRestore.onclick = () => { ctx.style.display = 'none'; UI.btnRestore.click(); }; } const ctxDelForever = ctx.querySelector('#ctx-del-forever'); if (ctxDelForever) { ctxDelForever.onclick = () => { ctx.style.display = 'none'; UI.btnDelForever.click(); }; } updateStat(); const restoreUIState = () => { const navs =[UI.btnNavHome, UI.btnNavTrash, UI.btnNavShare, UI.btnNavStarred, UI.btnNavRecent, UI.btnNavHistory, UI.btnNavOffline, UI.btnNavUpload]; navs.forEach(n => { if(n) n.classList.remove('act'); }); const stdBtns =[UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnBlacklistManager]; const shareBtns =[UI.btnCancelShare]; const upBtns =[UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll]; const upSep = el.querySelector('#pk-up-sep'); upBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(upSep) upSep.style.display = 'none'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; if (S.trashMode) { UI.win.classList.add('pk-mode-trash'); if(UI.btnNavTrash) UI.btnNavTrash.classList.add('act'); if(UI.actionBar) UI.actionBar.style.display = 'none'; if(UI.trashBar) UI.trashBar.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.trash_title; } else if (S.shareMode) { if(UI.btnNavShare) UI.btnNavShare.classList.add('act'); if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.btn_nav_share; stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); } else if (S.offlineMode) { if(UI.btnNavOffline) UI.btnNavOffline.classList.add('act'); if (S.path[0]) S.path[0].name = L.title_offline; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex';[UI.btnNewFolder, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip].forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnDel, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); } else if (S.uploadMode) { if(UI.btnNavUpload) UI.btnNavUpload.classList.add('act'); if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.btn_nav_upload; stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(upSep) upSep.style.display = 'block'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; } else if (S.historyMode) { if(UI.btnNavHistory) UI.btnNavHistory.classList.add('act'); if (S.path[0]) S.path[0].name = L.btn_nav_history; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager) b.style.display = 'none'; }); if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; } else if (S.recentMode || S.starredMode) { if (S.recentMode && UI.btnNavRecent) UI.btnNavRecent.classList.add('act'); if (S.starredMode && UI.btnNavStarred) UI.btnNavStarred.classList.add('act'); if (S.recentMode && S.path[0]) S.path[0].name = L.btn_nav_recent; if (S.starredMode && S.path[0]) S.path[0].name = L.btn_nav_starred; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; } else { if(UI.btnNavHome) UI.btnNavHome.classList.add('act'); if (S.path.length > 0 && S.path[0].id === '') S.path[0].name = L.btn_nav_home; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; } if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.crumb) { UI.crumb.style.opacity = '1'; UI.crumb.style.display = 'flex'; } refresh(); }; restoreUIState(); load(); const AUTO_REFRESH_INTERVAL = 30000; const startSmartRefresh = () => { if (UI.win.dataset.autoRefresh) return null; UI.win.dataset.autoRefresh = "true"; let silentAbortController = null; let retryTimer = null; let lastUserInteractionTime = 0; const updateInteractionTime = () => { lastUserInteractionTime = Date.now(); }; el.addEventListener('mousedown', updateInteractionTime, true); el.addEventListener('keydown', updateInteractionTime, true); const diffWorkerBlob = new Blob([` self.onmessage = function(e) { const { oldList, newList } = e.data; if (oldList.length !== newList.length) { self.postMessage(true); return; } for (let i = 0; i < newList.length; i++) { const a = oldList[i]; const b = newList[i]; if (a.id !== b.id || a.modified_time !== b.modified_time || a.size !== b.size || a.hash !== b.hash || a.starred !== b.starred) { self.postMessage(true); return; } } self.postMessage(false); }; `], { type: 'application/javascript' }); const diffWorker = new Worker(URL.createObjectURL(diffWorkerBlob)); const runWatchdogAudit = async () => { if (document.hidden) return; try { const list = await apiShareList(); const toAutoCancel = list.filter(it => it.kind === 'pikpak#share' && it.limit_count > 0 && it.save_count >= it.limit_count && it.share_status === 'OK'); if (toAutoCancel.length > 0) { const cancelIds = toAutoCancel.map(x => x.id); const expiredGraveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); toAutoCancel.forEach(it => { it.share_status = 'EXPIRED'; it._is_local_phantom = true; expiredGraveyard.push(it); }); if (expiredGraveyard.length > 50) expiredGraveyard.splice(0, expiredGraveyard.length - 50); gmSet('pk_expired_shares', JSON.stringify(expiredGraveyard)); await apiCancelShare(cancelIds).catch(() => {}); const store = JSON.parse(gmGet('pk_share_limits', '{}')); cancelIds.forEach(id => delete store[id]); gmSet('pk_share_limits', JSON.stringify(store)); console.log(`[Watchdog] Autonomous Audit: Auto-canceled ${cancelIds.length} shares.`); if (S.shareMode && window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); } } catch (e) { console.warn("[Watchdog] Audit failed", e); } }; const checkAndRefresh = async (isRetry = false, bypassLock = false) => { if (document.hidden) return; const isTyping = ['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName); const hasUIState = S.sel.size > 0 || (UI.ctx && UI.ctx.style.display === 'block'); const isInteracting = !bypassLock && (Date.now() - lastUserInteractionTime < 1500); const isBusy = S._isEmptyingTrash || S.loading || S.scanning || S.dupMode || S.isFlattened || hasUIState || isTyping || isInteracting; if (isBusy) { if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => checkAndRefresh(true, bypassLock), 2500); return; } if (S.historyMode || S.uploadMode) return; const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const cacheKey = S.shareMode ? 'share_root' : (isStarredRoot ? 'starred_root' : (isRecentRoot ? 'recent_root' : (cur.id || 'root'))); if (cur.id === 'virtual_search_root' || cur.id === 'analyze_root' || cur.id === 'upload_root') return; if (silentAbortController) silentAbortController.abort(); silentAbortController = new AbortController(); const signal = silentAbortController.signal; try { const runWatchdogAudit = async (targetList) => { const toAutoCancel = targetList.filter(it => it.kind === 'pikpak#share' && it.limit_count > 0 && it.save_count >= it.limit_count && it.share_status === 'OK'); if (toAutoCancel.length > 0) { const cancelIds = toAutoCancel.map(x => x.id); const expiredGraveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); toAutoCancel.forEach(it => { it.share_status = 'EXPIRED'; it._is_local_phantom = true; expiredGraveyard.push(it); }); if (expiredGraveyard.length > 50) expiredGraveyard.splice(0, expiredGraveyard.length - 50); gmSet('pk_expired_shares', JSON.stringify(expiredGraveyard)); await apiCancelShare(cancelIds).catch(() => {}); const store = JSON.parse(gmGet('pk_share_limits', '{}')); cancelIds.forEach(id => delete store[id]); gmSet('pk_share_limits', JSON.stringify(store)); console.log(`[Watchdog] Background Audit: Auto-canceled ${cancelIds.length} shares.`); return true; } return false; }; let allFetchedItems = []; if (!S.shareMode) { apiShareList().then(list => runWatchdogAudit(list)); } if (S.shareMode) { allFetchedItems = await apiShareList(); await runWatchdogAudit(allFetchedItems); } else if (S.offlineMode) { const rawTasks =[]; await apiTaskList(1000, (batch) => { if (batch && batch.length) rawTasks.push(...batch); }); allFetchedItems = rawTasks.map(t => { const ref = t.reference_resource || {}; return { id: t.id, kind: 'drive#task', name: ref.name || t.name || t.file_name || 'Untitled Task', size: t.file_size, phase: t.phase, progress: parseInt(t.progress || 0), message: t.message, icon_link: t.icon_link, thumbnail_link: ref.thumbnail_link ? ref.thumbnail_link : t.icon_link, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || '', file_id: t.file_id || '', source_url: (t.params && t.params.url) ? t.params.url : '', params: Object.assign({}, t.params || {}, ref.params || {}), mime_type: ref.mime_type || '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))) }; }); } else if (S.recentMode) { let nextToken = null; const limit = 500; do { if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.sel.size > 0) return; const filters = encodeURIComponent('{"phase":{"in":"PHASE_TYPE_COMPLETE"}}'); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?limit=${limit}&filters=${filters}&thumbnail_size=SIZE_MEDIUM&with_reference_resource=true&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const netPriority = bypassLock ? 'high' : 'low'; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: netPriority }); if (!res.ok) throw new Error("Recent SWR fetch error"); const json = await res.json(); const validTasks = (json.tasks ||[]).filter(t => t.phase === 'PHASE_TYPE_COMPLETE' && (t.type === 'offline' || t.type === 'upload') && t.file_id !== "" ); const mapped = validTasks.map(t => { const ref = t.reference_resource || {}; const mime = ref.mime_type || ''; const isFolder = (ref.kind === 'drive#folder') || (mime === 'application/x-directory') || (t.icon_link && t.icon_link.includes('folder')); return { id: t.file_id || t.id, kind: isFolder ? 'drive#folder' : 'drive#file', name: ref.name || t.file_name || t.name, size: t.file_size, thumbnail_link: ref.thumbnail_link || t.icon_link || '', icon_link: t.icon_link || '', web_content_link: t.file_id ? null : null, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || t.created_time, mime_type: mime, parent_id: '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))), trashed: false, params: Object.assign({}, t.params || {}, ref.params || {}), _sourceTaskId: t.id }; }); allFetchedItems.push(...mapped); nextToken = json.next_page_token; if (allFetchedItems.length >= 2000) break; } while (nextToken); const seen = new Set(); allFetchedItems = allFetchedItems.filter(f => { if (seen.has(f.id)) return false; seen.add(f.id); return true; }); } else { let nextToken = null; const limit = 500; const now = Date.now(); do { if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.sel.size > 0) return; const currentIsStarredRoot = S.starredMode && S.path.length === 1; const targetParentId = (S.trashMode || currentIsStarredRoot) ? '*' : (cur.id || ''); const filterObj = { "trashed": { "eq": S.trashMode } }; if (currentIsStarredRoot) { filterObj.trashed = { "eq": false }; filterObj.system_tag = { "in": "STAR" }; } else if (!S.trashMode) { filterObj.phase = { "eq": "PHASE_TYPE_COMPLETE" }; } const filters = `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`; const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&parent_id=${targetParentId}&_t=${now}${nextToken ? `&page_token=${nextToken}` : ''}`; const netPriority = bypassLock ? 'high' : 'low'; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: netPriority }); if (!res.ok) throw new Error("Silent fetch error"); const data = await res.json(); if (data.files) { allFetchedItems.push(...data.files.map(f => minifyFile(f, true))); } nextToken = data.next_page_token; } while (nextToken); } if (S.analyzeMode && S.analyzeMap) { allFetchedItems.forEach(item => { if (item.kind === 'drive#folder' && S.analyzeMap.has(item.id)) { item.size = S.analyzeMap.get(item.id).size.toString(); } }); } if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.sel.size > 0) return; const nowCur = S.path[S.path.length - 1]; const nowStarredRoot = S.starredMode && S.path.length === 1; let currentExpectedKey = 'root'; if (S.shareMode) currentExpectedKey = 'share_root'; else if (S.offlineMode) currentExpectedKey = 'offline_root'; else if (nowStarredRoot) currentExpectedKey = 'starred_root'; else currentExpectedKey = nowCur.id || 'root'; if (currentExpectedKey === cacheKey) { diffWorker.onmessage = (e) => { const hasChanges = e.data; if (!hasChanges) { console.log("[SmartRefresh] Cache verified. No changes."); return; } if ((!bypassLock && Date.now() - lastUserInteractionTime < 1500) || S.sel.size > 0) { if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => checkAndRefresh(true, bypassLock), 2000); return; } if (allFetchedItems.length > 0) { const sample = allFetchedItems[0]; if (S.shareMode) { } else if (S.trashMode && !sample.trashed) { console.warn("[SmartRefresh] Dirty data blocked: Home data trying to enter Trash view."); return; } else if (!S.trashMode && sample.trashed) { console.warn("[SmartRefresh] Dirty data blocked: Trash data trying to enter Home view."); return; } } S.cache.set(cacheKey, allFetchedItems); if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, allFetchedItems); const newItemMap = new Map(); const newStarredSet = new Set(); for (let i = 0; i < allFetchedItems.length; i++) { const item = allFetchedItems[i]; newItemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { newStarredSet.add(item.id); } } requestAnimationFrame(() => { if (S.sel.size > 0 || S.loading || S.scanning) return; S.items = allFetchedItems; S.itemMap = newItemMap; S.starredSet = newStarredSet; const scrollTop = UI.vp.scrollTop; refresh(); if (UI.vp) UI.vp.scrollTop = scrollTop; console.log(`[SmartRefresh] SWR Sync Complete. Updated ${allFetchedItems.length} items.`); }); }; const simplify = (list) => list.map(x => { let diffHash = x.hash; if (x.kind === 'drive#task') { diffHash = `${x.phase}_${x.progress}_${x.params?.global_file_kind || ''}_${x.hash || ''}`; } else if (x.params?.global_file_kind) { diffHash = `${x.params.global_file_kind}_${x.hash || ''}`; } return { id: x.id, modified_time: x.modified_time, size: x.size, hash: diffHash, starred: !!(x.starred || (x.tags && x.tags.some(t => t.name === 'STAR'))) }; }); if (S.shareMode || cacheKey === 'share_root') { const toAutoCancel = allFetchedItems.filter(it => it.kind === 'pikpak#share' && it.limit_count > 0 && it.save_count >= it.limit_count && it.share_status === 'OK'); if (toAutoCancel.length > 0) { const cancelIds = toAutoCancel.map(x => x.id); const expiredGraveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); toAutoCancel.forEach(it => { it.share_status = 'EXPIRED'; it._is_local_phantom = true; expiredGraveyard.push(it); }); if (expiredGraveyard.length > 50) expiredGraveyard.splice(0, expiredGraveyard.length - 50); gmSet('pk_expired_shares', JSON.stringify(expiredGraveyard)); await apiCancelShare(cancelIds).catch(() => {}); console.log(`[Watchdog] Auto-canceled and archived ${cancelIds.length} shares.`); const store = JSON.parse(gmGet('pk_share_limits', '{}')); cancelIds.forEach(id => delete store[id]); gmSet('pk_share_limits', JSON.stringify(store)); allFetchedItems = allFetchedItems.filter(it => !cancelIds.includes(it.id)); } } diffWorker.postMessage({ oldList: simplify(S.items), newList: simplify(allFetchedItems) }); } } catch (e) { } }; window.pkSmartRefreshTrigger = (isForce = false) => { const session = globalCache.get('offline_session'); if (S.offlineMode && (!session || !session.completed) && !isForce) { console.log(`[SmartRefresh] Pagination in progress (${S.items.length} items loaded). SWR standby...`); return; } if (S.recentMode) { const cached = globalCache.get('recent_root'); if (cached && !Array.isArray(cached) && cached.nextToken && !isForce) { console.log(`[SmartRefresh] Pagination in progress (${S.items.length} items loaded). SWR standby...`); return; } } checkAndRefresh(false, isForce); }; const onVisibilityChange = () => { if (retryTimer) clearTimeout(retryTimer); checkAndRefresh(false, false); runWatchdogAudit(); }; const uiSyncTimer = setInterval(() => checkAndRefresh(false, true), 60000); const watchdogTimer = setInterval(runWatchdogAudit, 60000); document.addEventListener('visibilitychange', onVisibilityChange); return { handler: onVisibilityChange, abort: () => { if(silentAbortController) silentAbortController.abort(); if(retryTimer) clearTimeout(retryTimer); if(uiSyncTimer) clearInterval(uiSyncTimer); if(watchdogTimer) clearInterval(watchdogTimer); document.removeEventListener('visibilitychange', onVisibilityChange); el.removeEventListener('mousedown', updateInteractionTime, true); el.removeEventListener('keydown', updateInteractionTime, true); if (diffWorker) diffWorker.terminate(); delete window.pkSmartRefreshTrigger; } }; }; const visibilityListener = startSmartRefresh(); const handleClose = () => { let safePath = [...S.path]; if (safePath.some(n => n.id === 'virtual_search_root' || n.id === 'analyze_root')) { safePath = S.preSearchPath || [{ id: '', name: L.btn_nav_home }]; } globalSavedState = { path: safePath, trashMode: S.trashMode, isMaximized: UI.win.classList.contains('pk-maximized') }; if (S.offlineMode && S.items.length > 0) { const cacheKey = 'offline_root'; const cacheData = { items: [...S.items], nextToken: null }; if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, cacheData); } pkState = null; delete window.pkUpdateCrawlerUI; if (S && S.broadcast) S.broadcast.close(); if (visibilityListener && visibilityListener.abort) { visibilityListener.abort(); } if (typeof destroyTooltip === 'function') destroyTooltip(); el.remove(); document.removeEventListener('keydown', keyHandler); document.removeEventListener('mouseup', mouseHandler); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }; UI.btnClose.addEventListener('click', () => { document.body.classList.remove('pk-body-max'); el.style.display = 'none'; }); updateCrawlerUI(); S.updateBlCache(); if (S.items && S.items.length > 0) { S.itemMap.clear(); S.items.forEach(i => S.itemMap.set(i.id, i)); } async function syncGlobalStarredStatus() { let nextToken = null; try { const latestStarredIds = new Set(); do { const filter = encodeURIComponent('{"starred":{"eq":true},"trashed":{"eq":false}}'); const url = `https://api-drive.mypikpak.com/drive/v1/files?filters=${filter}&limit=1000${nextToken ? `&page_token=${nextToken}` : ''}`; const res = await fetch(url, { headers: getHeaders() }); if (!res.ok) break; const data = await res.json(); if (data.files) { data.files.forEach(f => latestStarredIds.add(f.id)); } nextToken = data.next_page_token; } while (nextToken); S.pendingMap.forEach((targetStatus, id) => { if (targetStatus) { latestStarredIds.add(id); } else { latestStarredIds.delete(id); } }); if (latestStarredIds.size > 0 || S.items.length > 0) { S.starredSet = latestStarredIds; let updatedCount = 0; if (typeof renderVisible === 'function') renderVisible(); } } catch (e) { console.warn(L.err_star_sync_fail + ":", e); } } const refreshQuotaText = () => { const txt = el.querySelector('#pk-quota-txt'); if (!txt || !S.quota) return; const isMaxNow = UI.win.classList.contains('pk-maximized'); txt.textContent = isMaxNow ? `${S.quota.usedStr} / ${S.quota.limitStr}` : `${S.quota.pct}%`; }; const updateQuotaUI = async () => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/about?_t=${Date.now()}`, { headers: getHeaders() }); if (!res.ok) { if (res.status === 401 || res.status === 403) { setTimeout(updateQuotaUI, 2000); } return; } const data = await res.json(); const q = data.quota; if (!q) return; const used = parseInt(q.usage); const limit = parseInt(q.limit); const pct = Math.min(100, (used / limit) * 100).toFixed(1); const fQ = (v, d) => { let n = v, i = 0, units = ['B','KB','MB','GB','TB']; while(n >= 1024 && i < 4) { n /= 1024; i++; } return n.toFixed(d) + ' ' + units[i]; }; S.quota = { usedStr: fQ(used, 2), limitStr: fQ(limit, 0), pct: pct, usedRaw: used, limitRaw: limit }; const bar = el.querySelector('#pk-quota-bar'); const panel = el.querySelector('#pk-quota-panel'); if (bar) bar.style.width = pct + '%'; refreshQuotaText(); if (panel) panel.setAttribute('data-pk-tip', `${L.lbl_storage}: ${pct}% (${S.quota.usedStr} / ${S.quota.limitStr})`); if (bar) { if (parseFloat(pct) > 90) bar.style.background = '#d93025'; else if (parseFloat(pct) > 70) bar.style.background = '#faad14'; else bar.style.background = 'var(--pk-pri)'; } } catch (e) { console.warn("[Quota] Sync failed"); } }; updateQuotaUI(); const quotaTimer = setInterval(updateQuotaUI, 300000); const originalClose = UI.btnClose.onclick; UI.btnClose.onclick = (e) => { clearInterval(quotaTimer); if(originalClose) originalClose.call(UI.btnClose, e); }; await load(false, true); if (globalSavedState && globalSavedState.scrollTop !== undefined) { setTimeout(() => { if (typeof UI !== 'undefined' && UI.vp) { UI.vp.scrollTop = globalSavedState.scrollTop; delete globalSavedState.scrollTop; } }, 50); } syncGlobalStarredStatus(); if (gmGet('pk_turbo_mode', false)) { setTimeout(() => { if (typeof showToast === 'function') { showToast(getStrings().msg_turbo_activated, 'success', 5000); } }, 800); } } let backgroundQueue = []; let isBackgroundRunning = false; let scannedFolderIds = new Set(); let globalPreloadPromise = null; let globalCache = new Map(); let globalLineageMap = new Map(); let globalParentIndex = new Map(); let globalTombstoneCache = new Map(); let globalDirtyFolders = new Set(); let globalNeedsSync = false; let isGlobalIndexReady = false; let hasShownGlobalWarnSession = false; let serverClockOffset = 0; let hasSyncedTime = false; let globalSavedState = null; const syncTime = (headers) => { if (!headers) return; const serverDate = headers.get('Date') || headers.get('date'); if (serverDate) { const remoteTime = new Date(serverDate).getTime(); const localTime = Date.now(); serverClockOffset = remoteTime - localTime; hasSyncedTime = true; } }; const getServerNow = () => Date.now() + serverClockOffset; let isGUISensitive = false; let pkState = null; const indexParents = (parentId, parentName, files) => { if (!files || !Array.isArray(files)) return; const pId = parentId || 'root'; const pName = parentName || 'Root'; for (const f of files) { if (f.kind === 'drive#folder') { globalParentIndex.set(f.id, { id: pId, name: pName }); } } }; const DurationProber = (() => { let queue = []; let isRunning = false; let probeVideo = null; let loopTimer = null; let runToken = 0; const startNext = async () => { if (!isRunning || queue.length === 0) { isRunning = false; return; } const currentToken = runToken; if (document.hidden) { console.log(`[Prober] Running in background... Queue: ${queue.length}`); } const isUserWatching = !!document.getElementById('pk-player-ov'); const isSystemScanning = typeof pkState !== 'undefined' && pkState && pkState.scanning; if (isUserWatching || isSystemScanning) { loopTimer = setTimeout(startNext, 2000); return; } const item = queue.shift(); if (!probeVideo) { probeVideo = document.createElement('video'); probeVideo.muted = true; probeVideo.style.display = 'none'; } console.log(`[Prober] Probing duration for: ${item.name}`); let watchdog = null; const currentVideo = probeVideo; const cleanup = () => { if (watchdog) clearTimeout(watchdog); if (currentVideo) { currentVideo.removeEventListener('loadedmetadata', onLoaded); currentVideo.removeEventListener('error', onError); currentVideo.src = ""; currentVideo.load(); } if (isRunning && currentToken === runToken) { loopTimer = setTimeout(startNext, 1500); } }; const saveAndNotify = (targetItem, dur) => { gmSet('pk_duration_' + targetItem.id, dur); if (typeof pkState !== 'undefined' && pkState) { [pkState.items, pkState.display].forEach(list => { const found = list.find(i => i.id === targetItem.id); if (found && found.params) found.params.duration = dur; }); const row = document.querySelector(`.pk-row[data-id="${targetItem.id}"]`); if (row) { const cols = row.children; const durCol = cols.length > 1 ? cols[cols.length - 2] : null; if (durCol) { durCol.style.color = 'var(--pk-pri)'; durCol.textContent = fmtDur(dur); setTimeout(() => { if(durCol) durCol.style.color = ''; }, 2000); } } } }; const onLoaded = () => { if (currentToken === runToken) { const dur = Math.round(currentVideo.duration); if (dur > 0) { console.log(`[Prober] Success: ${item.name} -> ${dur}s`); saveAndNotify(item, dur); } } cleanup(); }; const onError = () => { cleanup(); }; currentVideo.addEventListener('loadedmetadata', onLoaded); currentVideo.addEventListener('error', onError); try { const res = await apiGet(item.id); if (currentToken !== runToken) { cleanup(); return; } if (!res || !res.web_content_link) { cleanup(); return; } const url = res.web_content_link; const nameLower = (item.name || '').toLowerCase(); const urlLower = url.toLowerCase(); const isM3u8 = nameLower.endsWith('.m3u8') || urlLower.includes('.m3u8'); const isWmv = nameLower.endsWith('.wmv') || urlLower.includes('.wmv') || nameLower.endsWith('.asf') || urlLower.includes('.asf'); const isAvi = nameLower.endsWith('.avi') || urlLower.includes('.avi') || nameLower.endsWith('.divx') || urlLower.includes('.divx'); const isFlv = nameLower.endsWith('.flv') || urlLower.includes('.flv'); const isMkv = nameLower.endsWith('.mkv') || urlLower.includes('.mkv'); const isRmvb = nameLower.endsWith('.rmvb') || urlLower.includes('.rmvb') || nameLower.endsWith('.rm') || urlLower.includes('.rm'); if (isM3u8) { const fetchOptions = window.AbortSignal ? { signal: AbortSignal.timeout(15000) } : {}; const text = await fetch(url, fetchOptions).then(r => r.text()); if (currentToken === runToken) { const matches = text.matchAll(/#EXTINF:([\d.]+)/g); let total = 0; for (const m of matches) total += parseFloat(m[1]); if (total > 0) saveAndNotify(item, Math.round(total)); } cleanup(); } else if (isWmv || isAvi || isFlv || isMkv || isRmvb) { const rangeEnd = isMkv ? 65535 : 8191; const fetchOptions = { headers: { 'Range': `bytes=0-${rangeEnd}` }, ...(window.AbortSignal ? { signal: AbortSignal.timeout(15000) } : {}) }; try { const response = await fetch(url, fetchOptions); if (currentToken === runToken && (response.ok || response.status === 206)) { const buffer = await response.arrayBuffer(); const view = new DataView(buffer); const bytes = new Uint8Array(buffer); let seconds = 0; let formatName = ''; if (isWmv) { formatName = nameLower.endsWith('.asf') ? 'ASF' : 'WMV'; const guid = [0xA1, 0xDC, 0xAB, 0x8C, 0x47, 0xA9, 0xCF, 0x11, 0x8E, 0xE4, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65]; let foundIdx = -1; for (let i = 0; i < bytes.length - 88; i++) { let match = true; for (let j = 0; j < 16; j++) { if (bytes[i + j] !== guid[j]) { match = false; break; } } if (match) { foundIdx = i; break; } } if (foundIdx !== -1) { const playDur = view.getBigUint64(foundIdx + 64, true); const preroll = view.getBigUint64(foundIdx + 80, true); seconds = Number(playDur - preroll) / 10000000; } } else if (isAvi) { formatName = nameLower.endsWith('.divx') ? 'DIVX' : 'AVI'; const avih = [0x61, 0x76, 0x69, 0x68]; let foundIdx = -1; for (let i = 0; i < bytes.length - 28; i++) { if (bytes[i] === avih[0] && bytes[i+1] === avih[1] && bytes[i+2] === avih[2] && bytes[i+3] === avih[3]) { foundIdx = i; break; } } if (foundIdx !== -1) { const microSecPerFrame = view.getUint32(foundIdx + 8, true); const totalFrames = view.getUint32(foundIdx + 24, true); seconds = (microSecPerFrame * totalFrames) / 1000000; } } else if (isFlv) { formatName = 'FLV'; const durKey = [0x00, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00]; let foundIdx = -1; for (let i = 0; i < bytes.length - 19; i++) { let match = true; for (let j = 0; j < 11; j++) { if (bytes[i + j] !== durKey[j]) { match = false; break; } } if (match) { foundIdx = i; break; } } if (foundIdx !== -1) seconds = view.getFloat64(foundIdx + 11, false); } else if (isRmvb) { formatName = 'RM/RMVB'; const prop = [0x50, 0x52, 0x4F, 0x50]; let foundIdx = -1; for (let i = 0; i < bytes.length - 36; i++) { if (bytes[i] === prop[0] && bytes[i+1] === prop[1] && bytes[i+2] === prop[2] && bytes[i+3] === prop[3]) { foundIdx = i; break; } } if (foundIdx !== -1) { const ms = view.getUint32(foundIdx + 32, false); seconds = ms / 1000; } } else if (isMkv) { formatName = 'MKV'; let timecodeScale = 1000000; let durationVal = 0; for (let i = 0; i < bytes.length - 10; i++) { if (bytes[i] === 0x2A && bytes[i+1] === 0xD7 && bytes[i+2] === 0xB1) { let len = bytes[i+3] & 0x7F; if (len === 3) timecodeScale = (bytes[i+4]<<16) | (bytes[i+5]<<8) | bytes[i+6]; if (len === 4) timecodeScale = (bytes[i+4]<<24) | (bytes[i+5]<<16) | (bytes[i+6]<<8) | bytes[i+7]; break; } } for (let i = 0; i < bytes.length - 10; i++) { if (bytes[i] === 0x44 && bytes[i+1] === 0x89) { let lenByte = bytes[i+2]; if (lenByte === 0x84) { durationVal = view.getFloat32(i+3, false); break; } else if (lenByte === 0x88) { durationVal = view.getFloat64(i+3, false); break; } } } if (durationVal > 0) { seconds = (durationVal * timecodeScale) / 1000000000; } } if (seconds > 0) { console.log(`[Prober] Success (${formatName} Binary Parsing): ${item.name} -> ${Math.round(seconds)}s`); saveAndNotify(item, Math.round(seconds)); } else { console.warn(`[Prober] Binary parsed but got 0 duration for ${item.name}`); } } } catch (e) { console.warn(`[Prober] Failed to parse binary header for ${item.name}`); } cleanup(); } else { if (document.hidden) { console.log(`[Prober] Video tag throttled by browser, pausing prober: ${item.name}`); queue.unshift(item); isRunning = false; cleanup(); return; } watchdog = setTimeout(() => { console.warn(`[Prober] Watchdog timeout fetching metadata for: ${item.name}`); cleanup(); }, 15000); currentVideo.src = url; currentVideo.load(); } } catch(e) { cleanup(); } }; return { add: (item, isBackground = false) => { if (queue.some(i => i.id === item.id)) return; if (isBackground) { queue.push(item); } else { queue.unshift(item); } if (!isRunning) { isRunning = true; startNext(); } }, checkAndRun: () => { if (!isRunning && queue.length > 0) { isRunning = true; startNext(); } }, reset: () => { console.log(`[Prober] Resetting queue (Dropped ${queue.length} tasks).`); runToken++; queue = []; isRunning = false; if (loopTimer) clearTimeout(loopTimer); if (probeVideo) { probeVideo.removeAttribute('src'); probeVideo.load(); probeVideo = null; } } }; })(); const minifyFile = (f, isBackground = false) => { if (f._minified) return f; const { id, kind, name, parent_id, size, mime_type, thumbnail_link, icon_link, web_content_link, hash, gcid, md5_checksum } = f; const trashed = !!f.trashed; const tags = f.tags ? [...f.tags] : []; const lineage = f._lineage ? [...f._lineage] : undefined; const isStarred = !!(f.starred || f.star || f.is_star || (tags.some(t => t.name === 'STAR'))); let duration = 0; const parse = (v) => { if (!v) return 0; const n = parseInt(v, 10); return isNaN(n) ? 0 : n; }; if (f.video_media_metadata?.duration) duration = parse(f.video_media_metadata.duration); if (!duration && f.audio_media_metadata?.duration) duration = parse(f.audio_media_metadata.duration); if (!duration && f.medias && Array.isArray(f.medias)) { for (const m of f.medias) { const d = m.duration ? parse(m.duration) : (m.video?.duration ? parse(m.video.duration) : 0); if (d > 0) { duration = d; break; } } } if (!duration && f.params?.duration) duration = parse(f.params.duration); let final_modified_time = f.modified_time; if (!duration && id) duration = gmGet('pk_duration_' + id, 0); if (kind === 'drive#folder' && id) { const localFMod = gmGet('pk_fmod_' + id); if (localFMod) final_modified_time = localFMod; } if (!duration && kind === 'drive#file') { const ext = (name || '').split('.').pop().toLowerCase(); const mime = (mime_type || '').toLowerCase(); if (['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'ts'].includes(ext) || mime.startsWith('video/')) { const probeItem = { id, name, kind, mime_type, parent_id, size }; setTimeout(() => DurationProber.add(probeItem, isBackground), 3000); } } return { id, kind, name, parent_id, size, file_count: (function() { if (f.file_count !== undefined) return f.file_count; if (f.usage && f.usage.file_count !== undefined) return f.usage.file_count; if (f.params && f.params.file_count !== undefined) return f.params.file_count; if (f.audit && f.audit.file_count !== undefined) return f.audit.file_count; return undefined; })(), modified_time: final_modified_time, thumbnail_link, icon_link, mime_type, trashed, web_content_link, tags, starred: isStarred, params: { duration, width: f.video_media_metadata?.width || f.params?.width, height: f.video_media_metadata?.height || f.params?.height, global_file_kind: f.params?.global_file_kind, global_file_root: f.params?.global_file_root }, hash: hash || md5_checksum || gcid, _lineage: lineage, _minified: true }; }; async function runBackgroundCrawler() { if (isBackgroundRunning) return; isBackgroundRunning = true; if (window.pkUpdateCrawlerUI) window.pkUpdateCrawlerUI(); const homeBtn = document.querySelector('#pk-nav-home'); if (homeBtn) homeBtn.classList.add('pk-status-dot'); const userSetLimit = parseInt(localStorage.getItem('pk_user_limit') || "50"); const BACKGROUND_MAX_CONCURRENCY = Math.min(userSetLimit, 32); let currentConcurrencyLimit = 5; const MIN_CONCURRENCY = 2; let activeRequests = 0; let pendingRetries = 0; const fetchFolderContents = async (folder) => { activeRequests++; try { let files; if (globalCache.has(folder.id)) { files = globalCache.get(folder.id); } else { files = await apiList(folder.id, 1000, null, null, false, true); if (!isGUISensitive) { globalCache.set(folder.id, files); } } if (files && Array.isArray(files)) { for (let i = 0; i < files.length; i++) { const f = files[i]; if (f.kind === 'drive#folder') { if (!scannedFolderIds.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name, retryCount: 0 }); scannedFolderIds.add(f.id); } } } } if (currentConcurrencyLimit < BACKGROUND_MAX_CONCURRENCY) { currentConcurrencyLimit += 0.2; } } catch (err) { currentConcurrencyLimit = MIN_CONCURRENCY; folder.retryCount = (folder.retryCount || 0) + 1; const backoffTime = Math.min(folder.retryCount * 5000, 30000); pendingRetries++; try { await sleep(backoffTime); if (!isGUISensitive) backgroundQueue.unshift(folder); } finally { pendingRetries--; } } finally { activeRequests--; } }; while (backgroundQueue.length > 0 || activeRequests > 0 || pendingRetries > 0 || (typeof globalDirtyFolders !== 'undefined' && globalDirtyFolders.size > 0)) { const isUserBusy = pkState && (pkState.scanning || pkState.loading || document.getElementById('pk-player-ov')); if (isUserBusy) { if (homeBtn) homeBtn.classList.remove('pk-status-dot'); await sleep(2000); continue; } if (homeBtn) homeBtn.classList.add('pk-status-dot'); if (backgroundQueue.length > 0 && activeRequests < Math.floor(currentConcurrencyLimit)) { const folder = backgroundQueue.pop(); fetchFolderContents(folder); await sleep(50); } else if (activeRequests > 0 || pendingRetries > 0) { await sleep(500); } else if (typeof globalDirtyFolders !== 'undefined' && globalDirtyFolders.size > 0) { const dirtyId = Array.from(globalDirtyFolders)[0]; globalDirtyFolders.delete(dirtyId); if (typeof globalCache !== 'undefined') { for (const k of globalCache.keys()) { if (k && k.startsWith('__analyze_nodeMap_')) { globalCache.delete(k); } } } const normalizedId = dirtyId === 'root' ? '' : dirtyId; backgroundQueue.unshift({ id: normalizedId, name: "Dirty_Reval", retryCount: 0 }); continue; } else { let discovered = 0; if (typeof globalCache !== 'undefined') { for (const [parentFid, files] of globalCache) { if (!files) continue; for (let i = 0; i < files.length; i++) { const f = files[i]; if (f.kind === 'drive#folder' && !scannedFolderIds.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name, retryCount: 0 }); scannedFolderIds.add(f.id); discovered++; } } if (discovered > 0) break; } } if (discovered === 0) break; } } isBackgroundRunning = false; if (window.pkUpdateCrawlerUI) window.pkUpdateCrawlerUI(); if (homeBtn) homeBtn.classList.remove('pk-status-dot'); } async function preLoadRootFiles(onProgress) { if (globalPreloadPromise) return globalPreloadPromise; console.log("Initiating background pre-load..."); globalPreloadPromise = new Promise(async (resolve) => { try { const isAuthReady = await waitForAuth(15000); if (!isAuthReady) { console.warn("Background Crawler: Auth token wait timeout."); } const rootFiles = await apiList('', 1000, onProgress); globalCache.set('root', rootFiles); console.log("Background pre-load (Root) successful."); const rootFolders = rootFiles.filter(f => f.kind === 'drive#folder'); rootFolders.forEach(f => { if (!scannedFolderIds.has(f.id)) { backgroundQueue.push({ id: f.id, name: f.name }); scannedFolderIds.add(f.id); } }); runBackgroundCrawler(); } catch (e) { console.error("Background pre-load failed:", e); } finally { resolve(globalCache.has('root')); } }); return globalPreloadPromise; } async function tryInject() { if (location.href.includes('/login') || location.pathname.includes('login')) return; console.log("🚀 PikPak Script: Attempting inject..."); if (document.getElementById('pk-launch')) { console.log("🚀 PikPak Script: Already injected."); return; } if (!document.body) { console.log("🚀 PikPak Script: Body not ready, retrying..."); setTimeout(tryInject, 500); return; } inject(); const isTurbo = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; if (isTurbo) { console.log("🚀[Turbo Mode] Fast-track rendering..."); const startTurbo = async () => { const preload = preLoadRootFiles(); if (!document.querySelector('.pk-ov')) { await openManager(globalCache, preload); } }; setTimeout(startTurbo, 100); } else { setTimeout(() => { preLoadRootFiles(); }, 1500); } document.addEventListener('visibilitychange', () => { if (!document.hidden) { if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); if (typeof isBackgroundRunning !== 'undefined' && !isBackgroundRunning) runBackgroundCrawler(); } }); console.log("🚀 PikPak Script: INJECT SUCCESS! Background pre-load started."); } function inject() { if (document.getElementById('pk-launch')) return; const b = document.createElement('button'); b.id = 'pk-launch'; const isTurbo = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; const displayStyle = isTurbo ? 'none!important' : 'flex!important'; b.style.cssText = `position:fixed;bottom:20px;right:20px;width:50px;height:50px;border-radius:50%;background:#1a5eff;border:none;cursor:pointer;z-index:2147483647;box-shadow:0 4px 12px rgba(0,0,0,0.3);padding:0;overflow:hidden;transition:transform 0.1s;display:${displayStyle};align-items:center!important;justify-content:center!important;`; b.innerHTML = `<svg width="60%" height="60%" viewBox="0 0 238 200" version="1.1" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0 C1.82724609 0.01353516 1.82724609 0.01353516 3.69140625 0.02734375 C4.59761719 0.03894531 5.50382812 0.05054688 6.4375 0.0625 C5.95097979 7.11704304 4.33696858 12.90149479 1.6875 19.4375 C1.35234375 20.32566406 1.0171875 21.21382812 0.671875 22.12890625 C0.3315625 22.98097656 -0.00875 23.83304688 -0.359375 24.7109375 C-0.66198242 25.47583496 -0.96458984 26.24073242 -1.27636719 27.02880859 C-3.01571023 29.77913653 -4.60880008 30.70366989 -7.5625 32.0625 C-10.93383789 32.72265625 -10.93383789 32.72265625 -14.78515625 33.125 C-15.47874237 33.20142731 -16.17232849 33.27785461 -16.88693237 33.3565979 C-18.36660067 33.51855298 -19.84685768 33.67520381 -21.3276062 33.82696533 C-25.19232303 34.22318595 -29.05286739 34.65697538 -32.9140625 35.0859375 C-33.67180466 35.16903168 -34.42954681 35.25212585 -35.21025085 35.33773804 C-40.99791882 35.97875931 -46.74864414 36.77615252 -52.5 37.6875 C-61.81496788 39.10080547 -71.19269316 40.07620454 -80.5625 41.0625 C-19.8425 41.0625 40.8775 41.0625 103.4375 41.0625 C91.8875 39.7425 80.3375 38.4225 68.4375 37.0625 C63.8175 36.4025 59.1975 35.7425 54.4375 35.0625 C49.17221542 34.42736314 43.90722683 33.79696512 38.63671875 33.20703125 C37.62996094 33.08714844 36.62320313 32.96726563 35.5859375 32.84375 C34.69052246 32.74126953 33.79510742 32.63878906 32.87255859 32.53320312 C30.35601376 32.0467485 28.59527547 31.44037784 26.4375 30.0625 C23.38532266 24.97553776 21.3341425 19.45473677 19.1875 13.9375 C18.91695801 13.25671387 18.64641602 12.57592773 18.36767578 11.87451172 C16.82394482 7.78804812 16.13851057 4.42502757 16.4375 0.0625 C33.20320897 -0.76054389 50.04132 2.04640823 66.578125 4.53515625 C70.96365446 5.13439358 75.35589707 5.627565 79.75488281 6.11669922 C97.85972043 8.13836316 97.85972043 8.13836316 106.6875 9.4375 C107.39487305 9.52700928 108.10224609 9.61651855 108.83105469 9.70874023 C113.96714941 10.51808328 116.87598017 12.31623275 120.4375 16.0625 C121.69830294 18.53927732 122.67025259 20.7202309 123.5625 23.3125 C124.02136126 24.56846882 124.48232815 25.8236702 124.9453125 27.078125 C125.27250149 28.00288179 125.27250149 28.00288179 125.60630035 28.94632053 C126.38750394 31.05750635 126.38750394 31.05750635 127.44002533 32.93062496 C131.07482517 39.83448151 131.00351579 46.31795394 130.95507812 53.99243164 C130.96050802 55.37978344 130.96763552 56.76712947 130.97631836 58.15446472 C130.99445028 61.89829685 130.98752708 65.6416848 130.97480202 69.38552403 C130.96462344 73.31622656 130.97408092 77.24689291 130.98034668 81.17759705 C130.98760817 87.77544941 130.97807403 94.37312221 130.95898438 100.97094727 C130.93720936 108.58452515 130.94427739 116.19767461 130.96629 123.81124216 C130.98447611 130.36524706 130.98698696 136.91912344 130.97653532 143.47314543 C130.97031913 147.38014362 130.96941296 151.2869408 130.98268127 155.19392586 C130.99428653 158.8672447 130.9861299 162.54001414 130.96310425 166.213274 C130.95534421 168.19404482 130.96713242 170.17486244 130.97961426 172.15560913 C130.90049754 180.52230774 129.95755225 186.09535704 124.25390625 192.5234375 C123.51011719 193.15507812 122.76632813 193.78671875 122 194.4375 C121.25878906 195.08460938 120.51757812 195.73171875 119.75390625 196.3984375 C114.7661098 199.98157627 110.22842399 200.35421576 104.22135925 200.32992554 C103.39785408 200.33445665 102.5743489 200.33898776 101.72588903 200.34365618 C98.968488 200.35630894 96.21128426 200.35467924 93.45385742 200.35302734 C91.475975 200.35901206 89.49809491 200.36581748 87.5202179 200.37338257 C82.14823484 200.39105594 76.77631549 200.39573853 71.40430617 200.39701414 C66.91878502 200.39891354 62.4332787 200.40627158 57.94776326 200.41335833 C47.36384951 200.42964512 36.77996977 200.43452703 26.19604492 200.43310547 C15.28118177 200.43190408 4.36651636 200.45300486 -6.54829675 200.4845928 C-15.92170288 200.51075235 -25.29504442 200.52147289 -34.66848677 200.52019465 C-40.26569836 200.51968491 -45.86273424 200.52537507 -51.45990944 200.54655075 C-56.725388 200.56592749 -61.99052314 200.5660613 -67.25601387 200.55151749 C-69.1861191 200.54942757 -71.11624579 200.55414114 -73.04631424 200.5662384 C-75.68641426 200.58171127 -78.32533312 200.57236959 -80.96540833 200.55697632 C-81.72466655 200.56726344 -82.48392478 200.57755057 -83.26619083 200.58814943 C-90.327556 200.49750269 -96.39704041 197.82485418 -101.375 192.75 C-102.18904297 191.95142578 -102.18904297 191.95142578 -103.01953125 191.13671875 C-108.29053612 184.05088689 -108.01804154 177.09915158 -108.0300293 168.55004883 C-108.04229625 167.18245883 -108.05575106 165.81487905 -108.07029724 164.4473114 C-108.10523797 160.74401042 -108.12059214 157.04088761 -108.13013434 153.33744264 C-108.13673436 151.01403475 -108.14708893 148.69067299 -108.15863991 146.36728477 C-108.19836069 138.23287671 -108.22038571 130.09860956 -108.22827148 121.96411133 C-108.23610728 114.43116961 -108.28516577 106.89925647 -108.35333699 99.36664182 C-108.41007964 92.86514961 -108.43519788 86.36399446 -108.43721896 79.86225718 C-108.43904166 75.9947118 -108.45309089 72.1282487 -108.50003624 68.26096535 C-108.72797687 48.29049317 -107.52961567 30.83210742 -95.5625 14.0625 C-92.23797604 10.732487 -88.44904231 10.20048941 -83.953125 9.5 C-83.20613342 9.37633057 -82.45914185 9.25266113 -81.68951416 9.12524414 C-74.04584045 7.901492 -66.3645662 7.06662299 -58.66394043 6.29776001 C-54.62860447 5.8940274 -50.59547976 5.46951727 -46.5625 5.04296875 C-45.77776306 4.96008102 -44.99302612 4.8771933 -44.18450928 4.79179382 C-36.33754684 3.9513441 -28.53467892 2.87051571 -20.734375 1.67578125 C-13.79617508 0.63078847 -7.03103815 -0.06826251 0 0 Z M-47 131 L-15 106 L-47 81 L-47 91 L-27 106 L-47 121 Z M45.4375 89.0625 C43.16309531 93.61130937 44.11732026 99.81887268 44.0625 104.8125 C44.02511719 106.08867188 43.98773438 107.36484375 43.94921875 108.6796875 C43.6563417 116.25277258 43.6563417 116.25277258 46.7109375 122.91015625 C50.0632924 125.55649945 51.41007501 125.90713502 55.50390625 125.58984375 C58.83921214 124.68021487 60.4149221 122.75927054 62.4375 120.0625 C64.03299443 115.26404894 63.62174204 110.1852134 63.625 105.1875 C63.64336914 103.71603516 63.64336914 103.71603516 63.66210938 102.21484375 C63.77173933 93.57358621 63.77173933 93.57358621 59.75 86.1875 C54.01325068 83.39664894 49.78182352 84.71817648 45.4375 89.0625 Z M-18.5625 155.0625 C-20.89546251 157.88967213 -20.89546251 157.88967213 -20.3125 161.125 C-19.8031756 164.161959 -19.8031756 164.161959 -17.5625 166.0625 C-15.5023267 166.81656896 -13.41368556 167.49416461 -11.3125 168.125 C-10.19359375 168.46660156 -9.0746875 168.80820313 -7.921875 169.16015625 C-1.62436639 170.85169635 4.26860909 171.24487637 10.75 171.25 C11.9555957 171.26836914 11.9555957 171.26836914 13.18554688 171.28710938 C21.14907742 171.30632948 28.31945463 169.57146397 35.875 167.125 C36.88433594 166.80660156 37.89367187 166.48820313 38.93359375 166.16015625 C41.73511224 165.200361 41.73511224 165.200361 43.4375 162.0625 C43.1133631 158.74009676 42.82973697 157.45473697 40.4375 155.0625 C35.63637087 154.61062902 31.50016124 155.74460874 26.9375 157.0625 C14.69655136 160.31686985 0.092469 160.8899845 -11.5625 155.0625 C-15.0625 154.72916667 -15.0625 154.72916667 -18.5625 155.0625 Z " fill="#FDFDFD" transform="translate(107.5625,-0.0625)"/> </svg>`; const savedLeft = gmGet('pk_pos_left', null); const savedTop = gmGet('pk_pos_top', null); if (savedLeft !== null && savedTop !== null) { b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = savedLeft; b.style.top = savedTop; } else { b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = '10px'; b.style.top = '430px'; } let isDragging = false; let dragStartX, dragStartY; const constrainBall = () => { const rect = b.getBoundingClientRect(); let newLeft = rect.left; let newTop = rect.top; const maxL = window.innerWidth - 50; const maxT = window.innerHeight - 50; if (newLeft > maxL) newLeft = maxL; if (newTop > maxT) newTop = maxT; if (newLeft < 0) newLeft = 0; if (newTop < 0) newTop = 0; b.style.left = newLeft + 'px'; b.style.top = newTop + 'px'; }; window.addEventListener('resize', constrainBall); const blockNativeDrag = (e) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; }; b.addEventListener('dragenter', blockNativeDrag); b.addEventListener('dragover', blockNativeDrag); b.addEventListener('drop', (e) => { blockNativeDrag(e); if (!document.querySelector('.pk-ov') || document.querySelector('.pk-ov').style.display === 'none') { const clickEvt = new MouseEvent('mousedown', { clientX: e.clientX, clientY: e.clientY }); b.dispatchEvent(clickEvt); const upEvt = new MouseEvent('mouseup', { clientX: e.clientX, clientY: e.clientY }); document.dispatchEvent(upEvt); } }); b.onmousedown = (e) => { isDragging = false; dragStartX = e.clientX; dragStartY = e.clientY; const rect = b.getBoundingClientRect(); b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = rect.left + 'px'; b.style.top = rect.top + 'px'; b.style.transition = 'none'; const offsetX = e.clientX - rect.left; const offsetY = e.clientY - rect.top; const onMove = (em) => { if (!isDragging && (Math.abs(em.clientX - dragStartX) > 3 || Math.abs(em.clientY - dragStartY) > 3)) { isDragging = true; } if (isDragging) { b.style.left = (em.clientX - offsetX) + 'px'; b.style.top = (em.clientY - offsetY) + 'px'; } }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); b.style.transition = 'transform 0.1s'; if (!isDragging) { if (window.innerWidth < 720 || window.innerHeight < 340) { return; } const existingWin = document.querySelector('.pk-ov'); if (existingWin) { if (existingWin.style.display === 'none') { const currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) return; if (existingWin.querySelector('.pk-win.pk-maximized')) { document.body.classList.add('pk-body-max'); } existingWin.style.display = 'flex'; existingWin.focus(); } else { existingWin.style.display = 'none'; } } else { const currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) return; openManager(globalCache, globalPreloadPromise); } } else { constrainBall(); gmSet('pk_pos_left', b.style.left); gmSet('pk_pos_top', b.style.top); } }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }; document.body.appendChild(b); setTimeout(constrainBall, 0); console.log("🚀 Button Created!"); } const startObserver = () => { if (!document.body) return; const obs = new MutationObserver(() => { if (!document.getElementById('pk-launch')) { tryInject(); } }); obs.observe(document.body, { childList: true, subtree: true }); }; if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', () => { tryInject(); startObserver(); }); } else { tryInject(); startObserver(); } })() ;