// ==UserScript== // @name PikPak 增强大师 // @name:zh-CN PikPak 增强大师 // @name:zh-TW PikPak 增強大師 // @name:en PikPak Enhancement Master // @name:ko PikPak Enhancement Master // @name:ja PikPak Enhancement Master // @name:id PikPak Enhancement Master // @name:ms PikPak Enhancement Master // @namespace https://github.com/digbug82/ // @version 4.0.0 // @author digbug82 // @license AGPL-3.0-or-later // @description PikPak 网盘增强:集成 Aria2/Gopeed/ABDM/IDM 下载、下载加速、下载过滤、分享链接解析、文件/文件夹查重、批量重命名、资源清理、批量解压、PotPlayer 直达、M3U 导出、排序与搜索增强、TXT 磁链提取、云归档、数据迁移、目录树导出、以图搜图、视音频播放增强等。 // @description:zh-CN PikPak 网盘增强:集成 Aria2/Gopeed/ABDM/IDM 下载、下载加速、下载过滤、分享链接解析、文件/文件夹查重、批量重命名、资源清理、批量解压、PotPlayer 直达、M3U 导出、排序与搜索增强、TXT 磁链提取、云归档、数据迁移、目录树导出、以图搜图、视音频播放增强等。 // @description:zh-TW PikPak 網盤增強:整合 Aria2/Gopeed/ABDM/IDM 下載、下載加速、下載過濾、分享連結解析、檔案/資料夾查重、批次重命名、資源清理、批次解壓、PotPlayer 直達、M3U 匯出、排序與搜尋增強、TXT 磁連提取、雲封存、資料遷移、目錄樹匯出、以圖搜圖、視音訊播放增強等。 // @description:en PikPak cloud drive enhancement: integrates Aria2/Gopeed/ABDM/IDM downloads, download acceleration, download filtering, share-link parsing, file/folder deduplication, batch renaming, resource cleanup, batch extraction, PotPlayer direct open, M3U export, sorting and search enhancements, TXT magnet extraction, cloud archive, data migration, directory tree export, reverse image search, and enhanced audio/video playback. // @description:ko PikPak 클라우드 드라이브 강화: Aria2/Gopeed/ABDM/IDM 다운로드, 다운로드 가속, 다운로드 필터링, 공유 링크 파싱, 파일/폴더 중복 검사, 일괄 이름 변경, 리소스 정리, 일괄 압축 해제, PotPlayer 바로 열기, M3U 내보내기, 정렬 및 검색 강화, TXT 마그넷 추출, 클라우드 아카이브, 데이터 마이그레이션, 디렉터리 트리 내보내기, 이미지 역검색, 오디오/비디오 재생 강화 등을 통합합니다. // @description:ja PikPak クラウドドライブ強化:Aria2/Gopeed/ABDM/IDM ダウンロード、ダウンロード高速化、ダウンロードフィルター、共有リンク解析、ファイル/フォルダ重複チェック、一括リネーム、リソース整理、一括解凍、PotPlayer 直接起動、M3U エクスポート、並び替えと検索の強化、TXT マグネット抽出、クラウドアーカイブ、データ移行、ディレクトリツリー出力、画像逆検索、音声/動画再生強化などを統合します。 // @description:id Peningkatan cloud drive PikPak: mengintegrasikan unduhan Aria2/Gopeed/ABDM/IDM, akselerasi unduhan, filter unduhan, parsing tautan berbagi, deduplikasi file/folder, ganti nama massal, pembersihan sumber daya, ekstraksi massal, buka langsung dengan PotPlayer, ekspor M3U, peningkatan penyortiran dan pencarian, ekstraksi magnet dari TXT, arsip cloud, migrasi data, ekspor pohon direktori, pencarian gambar terbalik, serta peningkatan pemutaran audio/video. // @description:ms Penambahbaikan pemacu awan PikPak: mengintegrasikan muat turun Aria2/Gopeed/ABDM/IDM, pecutan muat turun, penapisan muat turun, penghuraian pautan perkongsian, deduplikasi fail/folder, penamaan semula pukal, pembersihan sumber, nyahmampat pukal, buka terus dengan PotPlayer, eksport M3U, peningkatan susunan dan carian, pengekstrakan magnet TXT, arkib awan, migrasi data, eksport pepohon direktori, carian imej songsang serta peningkatan main balik audio/video. // @match https://mypikpak.com/drive/* // @match https://app.mypikpak.com/* // @match https://drive.mypikpak.com/* // @icon https://cdn.jsdelivr.net/gh/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 // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_info // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_download // @connect catbox.moe // @connect litterbox.catbox.moe // @connect uguu.se // @connect mypikpak.com // @connect upload.pikpak.site // @connect *.upload.pikpak.site // @connect web-vod-xdrive.mypikpak.com // @connect whatslink.info // @connect localhost // @connect 127.0.0.1 // @run-at document-start // @require https://cdn.jsdelivr.net/npm/hls.js@1.5.8/dist/hls.min.js // @require https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js // ==/UserScript== /* * ============================================================================ * COPYRIGHT & LICENSE NOTICE * ============================================================================ * PikPak Enhancement Master * * Copyright (C) 2025-2026 digbug82. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License * for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * Source code: https://github.com/digbug82/PikPak_Enhancement_Master * * Acknowledgements: * This project is inspired in part by the UI design language, file-management * structure, and some web-side API interaction ideas of PikPak File Manager * v1.2.0 by 브랜뉴: * https://github.com/poihoii/PikPak_FileManager * * PikPak File Manager is licensed under the MIT License. Its original * copyright and license notice are preserved in THIRD_PARTY_NOTICES.md: * https://github.com/digbug82/PikPak_Enhancement_Master/blob/main/THIRD_PARTY_NOTICES.md * * Special thanks and respect to the original project and its author. * ============================================================================ */ (() => { "use strict"; const CONFIG_SIZE = { KB: 1024, MB: 1024 * 1024 }; const CONFIG_LIMIT_SCHEMA = { entries: [ { key: 'pk_lang', type: 'enum', defaultValue: '', localLimit: { allowed: ['', 'zh', 'tc', 'en', 'ko', 'ja', 'id', 'ms'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_theme', type: 'enum', defaultValue: 'auto', localLimit: { allowed: ['auto', 'light', 'dark'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_turbo_mode', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_file_view_mode', type: 'enum', defaultValue: 'grid', localLimit: { allowed: ['grid', 'list'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_blur_thumb', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_blur_scope', type: 'enum', defaultValue: 'off', localLimit: { allowed: ['off', 'list', 'grid', 'both'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_hide_button_text', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_comic_mode', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_suppress_global_warn', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_folder_view_prefs', type: 'jsonMap', defaultValue: '{}', localLimit: { maxItems: 10000, maxKeyLen: 128, maxSize: CONFIG_SIZE.MB, valueType: 'viewMode' }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'mapLocalFirst' }, { key: 'pk_view_independent', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_folder_sort_prefs', type: 'jsonMap', defaultValue: '{}', localLimit: { maxItems: 10000, maxKeyLen: 128, maxSize: CONFIG_SIZE.MB, valueType: 'sortPref' }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'mapLocalFirst' }, { key: 'pk_global_sort_pref', type: 'sortPref', defaultValue: '{"sort":"modified_time","dir":1}', localLimit: { maxSize: 4096 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_sort_independent', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_folder_first', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_play_mode', type: 'enum', defaultValue: 'stop', localLimit: { allowed: ['stop', 'loop', 'next'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_skip_intro', type: 'number', defaultValue: 0, localLimit: { min: 0, max: 3600 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_skip_outro', type: 'number', defaultValue: 0, localLimit: { min: 0, max: 3600 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_vol_level', type: 'number', defaultValue: 1, localLimit: { min: 0, max: 1 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_vol_muted', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_default_video_quality', type: 'enum', defaultValue: 'original', localLimit: { allowed: ['original', '1080p', '720p', '480p'], maxLen: 32 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_video_load_progress_cache', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_audio_play_mode', type: 'enum', defaultValue: 'order', localLimit: { allowed: ['order', 'loop', 'single', 'shuffle'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_audio_vol_level', type: 'number', defaultValue: 1, localLimit: { min: 0, max: 1 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_audio_vol_muted', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_ext_player', type: 'enum', defaultValue: 'potplayer', localLimit: { allowed: ['potplayer', 'vlc', 'mpv', 'iina', 'custom'], maxLen: 64 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_default_open_player', type: 'enum', defaultValue: 'script', localLimit: { allowed: ['script', 'potplayer'], maxLen: 64 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_blacklist', type: 'lineList', defaultValue: '', localLimit: { maxItems: 3000, maxItemLen: 512, maxSize: 512 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'dedupeUnion' }, { key: 'pk_blacklist_folders', type: 'lineList', defaultValue: '', localLimit: { maxItems: 3000, maxItemLen: 512, maxSize: 512 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'dedupeUnion' }, { key: 'pk_skip_bl_on_del', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_downloader_type', type: 'enum', defaultValue: 'aria2', localLimit: { allowed: ['aria2', 'gopeed', 'abdm', 'idm'], aliases: { motrix: 'aria2' }, maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_downloader_prefer_original_video_link', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_download_accel_enable', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_download_accel_domain', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'groupLocalDefaultOnly' }, { key: 'pk_download_accel_mode', type: 'enum', defaultValue: 'prefix', localLimit: { allowed: ['prefix', 'query'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'groupLocalDefaultOnly' }, { key: 'pk_download_accel_query_param', type: 'queryParam', defaultValue: 'url', localLimit: { maxLen: 64 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'groupLocalDefaultOnly' }, { key: 'pk_dl_filter_ext', type: 'arrayList', defaultValue: '', localLimit: { maxItems: 512, maxItemLen: 32, maxSize: 16 * CONFIG_SIZE.KB, split: 'commaLine', stripLeadingDot: true, lowercase: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'dedupeUnion' }, { key: 'pk_dl_filter_name', type: 'arrayList', defaultValue: '', localLimit: { maxItems: 1000, maxItemLen: 256, maxSize: 256 * CONFIG_SIZE.KB, split: 'commaLine' }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'dedupeUnion' }, { key: 'pk_dl_filter_size_min', type: 'numberOrEmpty', defaultValue: '', localLimit: { min: 0, max: Number.MAX_SAFE_INTEGER }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'groupLocalDefaultOnly' }, { key: 'pk_dl_filter_size_max', type: 'numberOrEmpty', defaultValue: '', localLimit: { min: 0, max: Number.MAX_SAFE_INTEGER }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'groupLocalDefaultOnly' }, { key: 'pk_dl_filter_size_unit', type: 'enum', defaultValue: 'MB', localLimit: { allowed: ['B', 'KB', 'MB', 'GB', 'TB'], maxLen: 8 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'groupLocalDefaultOnly' }, { key: 'pk_search_engine', type: 'enum', defaultValue: 'google', localLimit: { allowed: ['google', 'yandex', 'saucenao', 'tracemoe'], maxLen: 32 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_dup_strictness', type: 'enum', defaultValue: 'strict', localLimit: { allowed: ['strict', 'normal', 'loose'], maxLen: 32 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_expired_shares', type: 'jsonArray', defaultValue: '[]', localLimit: { maxItems: 500, preferredItems: 50, maxFieldLen: 512, maxSize: 64 * CONFIG_SIZE.KB, keepRecent: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'dedupeByShare' }, { key: 'pk_share_limits', type: 'shareLimits', defaultValue: '{}', localLimit: { maxItems: 500, maxKeyLen: 128, maxSize: 128 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'mapLocalFirst' }, { key: 'pk_share_parse_history', type: 'shareParseHistory', defaultValue: '[]', localLimit: { maxItems: 3000, maxTextLen: 512, maxIdLen: 256, maxPassLen: 10, maxSize: 2 * CONFIG_SIZE.MB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'shareHistoryLocalFirst' }, { key: 'pk_pwd_vault', type: 'pwdVault', defaultValue: '[]', localLimit: { maxItems: 50, maxItemLen: 512, maxSize: 32 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'dedupeByPassword' }, { key: 'pk_pwd_try_count', type: 'number', defaultValue: 10, localLimit: { min: 10, max: 50, integer: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_clipboard_magnet_focus', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_clipboard_magnet_paste', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'default', mergeStrategy: 'localDefaultOnly' }, { key: 'pk_magnet_archive_journal', type: 'jsonArray', defaultValue: '[]', localLimit: { maxItems: 2000, maxFieldLen: 2048, maxSize: 1024 * CONFIG_SIZE.KB, keepRecent: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_keep_pos', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_pos_left', type: 'cssPx', defaultValue: '', localLimit: { min: 0, max: 20000 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_pos_top', type: 'cssPx', defaultValue: '', localLimit: { min: 0, max: 20000 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_audio_mini_pos', type: 'jsonObject', defaultValue: '{}', localLimit: { maxItems: 20, maxFieldLen: 256, maxSize: 8 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_potplayer_custom_path', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_potplayer_launch_state', type: 'jsonObject', defaultValue: '{}', localLimit: { maxItems: 32, maxFieldLen: 2048, maxSize: 16 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_potplayer_protocol_state', type: 'jsonObject', defaultValue: '{}', localLimit: { maxItems: 32, maxFieldLen: 2048, maxSize: 16 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_user_limit', type: 'number', defaultValue: 50, localLimit: { min: 2, max: 128, integer: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_aria2_url', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_aria2_token', type: 'string', defaultValue: '', localLimit: { maxLen: 4096 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_aria2_dir', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_aria2_keep_structure', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_gopeed_url', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_gopeed_token', type: 'string', defaultValue: '', localLimit: { maxLen: 4096 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_gopeed_dir', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_gopeed_keep_structure', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_abdm_url', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_abdm_dir', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_idm_export_mode', type: 'enum', defaultValue: 'url', localLimit: { allowed: ['url', 'ef2', 'bat'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_idm_url_export_type', type: 'enum', defaultValue: 'txt', localLimit: { allowed: ['txt', 'json'], maxLen: 16 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_idm_exe_path', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_idm_bat_root', type: 'string', defaultValue: '', localLimit: { maxLen: 2048 }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_idm_bat_keep_structure', type: 'boolean', defaultValue: true, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_idm_bat_add_queue', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_idm_bat_start_queue', type: 'boolean', defaultValue: false, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_search_history', type: 'jsonArray', defaultValue: '[]', localLimit: { maxItems: 3, maxFieldLen: 1024, maxSize: 8 * CONFIG_SIZE.KB, keepRecent: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_bn_find_hist', type: 'jsonArray', defaultValue: '[]', localLimit: { maxItems: 3, maxFieldLen: 1024, maxSize: 8 * CONFIG_SIZE.KB, keepRecent: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_bn_rep_hist', type: 'jsonArray', defaultValue: '[]', localLimit: { maxItems: 3, maxFieldLen: 1024, maxSize: 8 * CONFIG_SIZE.KB, keepRecent: true }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_captured_captcha', type: 'string', defaultValue: '', localLimit: { maxLen: 2048, ttlMs: 24 * 60 * 60 * 1000 }, isPrefix: false, lru: false, ttl: true, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_ghost_files', type: 'ghostFiles', defaultValue: '[]', localLimit: { maxItems: 1000, maxItemLen: 128, maxSize: 128 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_script_update_cache', type: 'scriptUpdateCache', defaultValue: '', localLimit: { maxSize: 64 * CONFIG_SIZE.KB, ttlMs: 7 * 24 * 60 * 60 * 1000 }, isPrefix: false, lru: false, ttl: true, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_i18n_manifest', type: 'i18nCache', defaultValue: '', localLimit: { maxSize: 512 * CONFIG_SIZE.KB, ttlMs: 7 * 24 * 60 * 60 * 1000 }, isPrefix: false, lru: false, ttl: true, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_migration_stub', type: 'string', defaultValue: '', localLimit: { maxLen: CONFIG_SIZE.MB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_cfg_sync_last_remote_hash', type: 'string', defaultValue: '', localLimit: { maxLen: 128, maxSize: 64 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_cfg_sync_last_local_hash', type: 'string', defaultValue: '', localLimit: { maxLen: 128, maxSize: 64 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_cfg_sync_last_sync_at', type: 'string', defaultValue: '', localLimit: { maxLen: 64, maxSize: 64 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { key: 'pk_cfg_sync_last_report', type: 'jsonObject', defaultValue: '{}', localLimit: { maxItems: 80, maxFieldLen: 4096, maxSize: 64 * CONFIG_SIZE.KB }, isPrefix: false, lru: false, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { prefix: 'pk_archive_pwd_', type: 'archivePwd', defaultValue: '', localLimit: { maxItems: 1000, maxItemLen: 512, maxSize: 256 * CONFIG_SIZE.KB }, isPrefix: true, lru: true, ttl: false, cloudSync: 'default', mergeStrategy: 'fillOnly' }, { prefix: 'pk_duration_', type: 'duration', defaultValue: 0, localLimit: { maxItems: 10000, maxSize: CONFIG_SIZE.MB }, isPrefix: true, lru: true, ttl: false, cloudSync: 'default', mergeStrategy: 'fillOnly' }, { prefix: 'pk_scan_last_', type: 'prefixValue', defaultValue: '', localLimit: { maxItems: 500, maxItemLen: 2048, maxSize: 128 * CONFIG_SIZE.KB }, isPrefix: true, lru: true, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { prefix: 'pk_analyze_last_', type: 'prefixValue', defaultValue: '', localLimit: { maxItems: 500, maxItemLen: 2048, maxSize: 128 * CONFIG_SIZE.KB }, isPrefix: true, lru: true, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { prefix: 'pk_fmod_', type: 'prefixValue', defaultValue: '', localLimit: { maxItems: 5000, maxKeyLen: 128, maxItemLen: 2048, maxSize: 512 * CONFIG_SIZE.KB }, isPrefix: true, lru: true, ttl: false, cloudSync: 'strict-local', mergeStrategy: 'none' }, { prefix: 'pk_i18n_', type: 'i18nCache', defaultValue: '', localLimit: { maxSize: 512 * CONFIG_SIZE.KB, ttlMs: 7 * 24 * 60 * 60 * 1000 }, isPrefix: true, lru: false, ttl: true, cloudSync: 'strict-local', mergeStrategy: 'none' }, { prefix: 'pk_script_update_dismiss_', type: 'scriptUpdateDismiss', defaultValue: '', localLimit: { maxSize: 64 * CONFIG_SIZE.KB, ttlMs: 30 * 24 * 60 * 60 * 1000 }, isPrefix: true, lru: false, ttl: true, cloudSync: 'strict-local', mergeStrategy: 'none' }, { prefix: 'pk_clipboard_magnet_ignore_', type: 'ttlValue', defaultValue: '', localLimit: { maxSize: 64 * CONFIG_SIZE.KB, ttlMs: 24 * 60 * 60 * 1000 }, isPrefix: true, lru: false, ttl: true, cloudSync: 'strict-local', mergeStrategy: 'none' }, { prefix: 'pk_clipboard_magnet_last_', type: 'ttlValue', defaultValue: '', localLimit: { maxSize: 64 * CONFIG_SIZE.KB, ttlMs: 7 * 24 * 60 * 60 * 1000 }, isPrefix: true, lru: false, ttl: true, cloudSync: 'strict-local', mergeStrategy: 'none' } ] }; const CONFIG_LIMIT_EXACT = new Map(CONFIG_LIMIT_SCHEMA.entries.filter(x => x.key).map(x => [x.key, x])); const CONFIG_LIMIT_PREFIXES = CONFIG_LIMIT_SCHEMA.entries.filter(x => x.prefix).sort((a, b) => b.prefix.length - a.prefix.length); const CONFIG_NORMALIZE_MODES = new Set(['localWrite', 'import', 'cloudPack', 'cloudMerge']); function isCapacityProbeStorageKey(key) { const k = String(key || ''); return /^pk_lbm_.*_(?:payload_limit|chunk_size)$/i.test(k) || /^pk_lbm_.*probe/i.test(k) || /^pk_.*(?:capacity|quota)_probe/i.test(k) || /^pk_.*probe.*(?:capacity|quota|payload|chunk|lbm)/i.test(k); } function getConfigLimitSchema(key) { const k = String(key || ''); if (isCapacityProbeStorageKey(k)) return { key: k, type: 'prohibitedProbe', defaultValue: undefined, localLimit: {}, isPrefix: false, lru: false, ttl: false, cloudSync: 'none', mergeStrategy: 'drop' }; return CONFIG_LIMIT_EXACT.get(k) || CONFIG_LIMIT_PREFIXES.find(item => k.startsWith(item.prefix)) || null; } function estimateTextSize(value) { const text = typeof value === 'string' ? value : JSON.stringify(value ?? ''); try { if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(text).length; } catch (e) {} return String(text || '').length * 2; } function trimTextByBytesOrChars(value, maxSize) { let text = String(value ?? ''); const limit = Math.max(0, Number(maxSize) || 0); if (!limit || estimateTextSize(text) <= limit) return text; let lo = 0, hi = text.length; while (lo < hi) { const mid = Math.ceil((lo + hi) / 2); if (estimateTextSize(text.slice(0, mid)) <= limit) lo = mid; else hi = mid - 1; } return text.slice(0, lo); } function safeStringLimit(value, maxLen, fallback = '', shouldTrim = true) { if (value === null || value === undefined) return fallback; let text = String(value); if (shouldTrim) text = text.trim(); if (!Number.isFinite(Number(maxLen)) || maxLen <= 0) return text; return text.slice(0, Number(maxLen)); } function safeNumberClamp(value, min, max, fallback = 0, integer = false) { let n = Number(value); if (!Number.isFinite(n)) n = Number(fallback); if (!Number.isFinite(n)) n = 0; const lo = Number.isFinite(Number(min)) ? Number(min) : -Number.MAX_SAFE_INTEGER; const hi = Number.isFinite(Number(max)) ? Number(max) : Number.MAX_SAFE_INTEGER; n = Math.min(Math.max(n, lo), hi); return integer ? Math.round(n) : n; } function safeBoolean(value, fallback = false) { if (value === true || value === false) return value; if (typeof value === 'string') { const s = value.trim().toLowerCase(); if (['true', '1', 'yes', 'on'].includes(s)) return true; if (['false', '0', 'no', 'off'].includes(s)) return false; } if (value === null || value === undefined || value === '') return !!fallback; return !!value; } function safeEnum(value, allowed, fallback = '', aliases = null) { const raw = String(value ?? '').trim(); const canonical = aliases && Object.prototype.hasOwnProperty.call(aliases, raw) ? aliases[raw] : raw; return Array.isArray(allowed) && allowed.includes(canonical) ? canonical : fallback; } function parseConfigJson(value, fallback) { if (value && typeof value === 'object') return value; if (typeof value === 'string') { const s = value.trim(); if (!s) return fallback; try { return JSON.parse(s); } catch (e) { return fallback; } } return fallback; } function trimJsonListBySize(list, maxSize) { const out = Array.isArray(list) ? list.slice() : []; if (!maxSize) return out; while (out.length && estimateTextSize(JSON.stringify(out)) > maxSize) out.pop(); return out; } function normalizeLineList(text, options = {}) { const maxItems = options.maxItems || 3000; const maxItemLen = options.maxItemLen || 512; const maxSize = options.maxSize || 0; const source = Array.isArray(text) ? text.join('\n') : String(text ?? ''); const seen = new Set(); const out = []; let size = 0; source.split(/\r?\n/).forEach(line => { let item = safeStringLimit(line, maxItemLen); if (!item || seen.has(item)) return; const addSize = estimateTextSize(item) + (out.length ? 1 : 0); if (out.length >= maxItems || (maxSize && size + addSize > maxSize)) return; seen.add(item); out.push(item); size += addSize; }); return out.join('\n'); } function normalizeArrayList(list, options = {}) { let raw; if (Array.isArray(list)) raw = list; else { const text = String(list ?? ''); raw = options.split === 'commaLine' ? text.split(/[,,\n\r]+/) : text.split(/\r?\n/); } const maxItems = options.maxItems || 100; const maxItemLen = options.maxItemLen || 256; const maxSize = options.maxSize || 0; const seen = new Set(); const out = []; let size = 0; raw.forEach(value => { let item = safeStringLimit(value, maxItemLen); if (options.stripLeadingDot) item = item.replace(/^\.+/, ''); if (options.lowercase) item = item.toLowerCase(); if (!item) return; const dedupeKey = options.lowercase ? item.toLowerCase() : item; if (seen.has(dedupeKey)) return; const addSize = estimateTextSize(item) + (out.length ? 2 : 0); if (out.length >= maxItems || (maxSize && size + addSize > maxSize)) return; seen.add(dedupeKey); out.push(item); size += addSize; }); return out; } function normalizeSortPref(value) { const src = parseConfigJson(value, {}); const allowed = ['starred', 'name', 'size', 'modified_time', 'duration', 'progress', 'play_time', 'path']; const sort = safeEnum(src && src.sort, allowed, 'modified_time'); const dir = Number(src && src.dir) === -1 ? -1 : 1; return { sort, dir }; } function normalizeJsonMap(obj, options = {}) { const src = parseConfigJson(obj, {}); if (!src || typeof src !== 'object' || Array.isArray(src)) return {}; const out = {}; const maxItems = options.maxItems || 1000; const maxKeyLen = options.maxKeyLen || 128; const maxSize = options.maxSize || 0; const keys = Object.keys(src); for (const rawKey of keys) { if (Object.keys(out).length >= maxItems) break; const key = safeStringLimit(rawKey, maxKeyLen); if (!key || Object.prototype.hasOwnProperty.call(out, key)) continue; let val = src[rawKey]; if (options.valueType === 'viewMode') val = safeEnum(val, ['grid', 'list'], 'grid'); else if (options.valueType === 'sortPref') val = normalizeSortPref(val); else val = limitConfigRecordFields(val, { maxFieldLen: options.maxFieldLen || 512 }); out[key] = val; if (maxSize && estimateTextSize(JSON.stringify(out)) > maxSize) { delete out[key]; break; } } return out; } function limitConfigRecordFields(raw, options = {}) { if (Array.isArray(raw)) { return raw.slice(0, options.maxItems || 64).map(item => limitConfigRecordFields(item, options)); } if (!raw || typeof raw !== 'object') { return safeStringLimit(raw, options.maxFieldLen || 512); } const out = {}; const maxItems = options.maxItems || 64; const maxFieldLen = options.maxFieldLen || 512; Object.keys(raw).slice(0, maxItems).forEach(k => { const key = safeStringLimit(k, 128); const v = raw[k]; if (v === null || v === undefined) return; if (typeof v === 'number') out[key] = Number.isFinite(v) ? v : 0; else if (typeof v === 'boolean') out[key] = v; else if (typeof v === 'object') out[key] = safeStringLimit(JSON.stringify(v), maxFieldLen); else out[key] = safeStringLimit(v, maxFieldLen); }); return out; } function normalizeJsonArrayValue(value, options = {}) { const parsed = parseConfigJson(value, []); const src = Array.isArray(parsed) ? parsed : (parsed && Array.isArray(parsed.items) ? parsed.items : []); const maxItems = options.maxItems || 100; const work = options.keepRecent ? src.slice(-maxItems) : src.slice(0, maxItems); const out = work.map(item => limitConfigRecordFields(item, options)).filter(item => { if (item === null || item === undefined) return false; if (typeof item === 'string') return item.trim() !== ''; return true; }); return trimJsonListBySize(out, options.maxSize || 0); } function parseConfigTime(value) { if (!value) return 0; if (typeof value === 'number' && Number.isFinite(value)) return Math.max(0, Math.floor(value)); const t = Date.parse(value); return Number.isFinite(t) ? t : 0; } function normalizeShareParseStatus(status) { const v = String(status || 'ok').trim(); return ['ok', 'expired', 'cancelled', 'not_found', 'bad_pass', 'auth', 'network', 'unknown'].includes(v) ? v : 'unknown'; } function normalizeShareParseHistoryList(value, options = {}) { const parsed = parseConfigJson(value, []); const src = Array.isArray(parsed) ? parsed : (parsed && Array.isArray(parsed.items) ? parsed.items : []); const maxTextLen = options.maxTextLen || 512; const maxIdLen = options.maxIdLen || 256; const maxPassLen = options.maxPassLen || 10; const makeKey = rec => `${rec.share_id}\n${rec.pass_code || ''}`; const map = new Map(); src.forEach(raw => { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return; const shareId = safeStringLimit(raw.share_id || raw.shareId || '', maxIdLen); if (!shareId) return; const passCode = safeStringLimit(raw.pass_code || raw.passCode || '', maxPassLen); const rec = { id: safeStringLimit(raw.id || `sph_${shareId}_${Date.now().toString(36)}`, maxIdLen), share_id: shareId, key: safeStringLimit(raw.key || `${shareId}\n${passCode}`, maxTextLen), raw: safeStringLimit(raw.raw || raw.link || raw.url || shareId, maxTextLen), pass_code: passCode, title: safeStringLimit(raw.title || raw.share_title || shareId, maxTextLen), share_user: safeStringLimit(raw.share_user || raw.shareUser || '', maxTextLen), first_success_at: parseConfigTime(raw.first_success_at || raw.firstSuccessAt || raw.last_success_at || raw.lastSuccessAt || Date.now()), last_success_at: parseConfigTime(raw.last_success_at || raw.lastSuccessAt || raw.first_success_at || raw.firstSuccessAt || Date.now()), last_open_at: parseConfigTime(raw.last_open_at || raw.lastOpenAt || 0), last_check_at: parseConfigTime(raw.last_check_at || raw.lastCheckAt || 0), last_status: normalizeShareParseStatus(raw.last_status || raw.lastStatus || 'ok'), last_error_key: safeStringLimit(raw.last_error_key || raw.lastErrorKey || '', maxTextLen), last_error_msg: safeStringLimit(raw.last_error_msg || raw.lastErrorMsg || '', maxTextLen), last_error_at: parseConfigTime(raw.last_error_at || raw.lastErrorAt || 0), success_count: safeNumberClamp(raw.success_count || raw.successCount || 1, 1, Number.MAX_SAFE_INTEGER, 1, true), root_file_count: safeNumberClamp(raw.root_file_count || raw.rootFileCount || 0, 0, Number.MAX_SAFE_INTEGER, 0, true), root_folder_count: safeNumberClamp(raw.root_folder_count || raw.rootFolderCount || 0, 0, Number.MAX_SAFE_INTEGER, 0, true), root_total_count: safeNumberClamp(raw.root_total_count || raw.rootTotalCount || 0, 0, Number.MAX_SAFE_INTEGER, 0, true), pinned: safeBoolean(raw.pinned, false), note: safeStringLimit(raw.note || '', maxTextLen), schema: 1 }; const key = safeStringLimit(rec.key || makeKey(rec), maxTextLen); const old = map.get(key); if (!old || rec.pinned || rec.last_open_at > old.last_open_at || rec.last_success_at > old.last_success_at) map.set(key, rec); }); const list = Array.from(map.values()).sort((a, b) => { if (!!a.pinned !== !!b.pinned) return a.pinned ? -1 : 1; if (a.last_open_at !== b.last_open_at) return b.last_open_at - a.last_open_at; if (a.last_success_at !== b.last_success_at) return b.last_success_at - a.last_success_at; return Math.max(b.last_check_at, b.last_error_at, b.first_success_at) - Math.max(a.last_check_at, a.last_error_at, a.first_success_at); }).slice(0, options.maxItems || 3000); return trimJsonListBySize(list, options.maxSize || 0); } function normalizeShareLimitsValue(value, options = {}) { const parsed = parseConfigJson(value, {}); const out = {}; const maxItems = options.maxItems || 500; const maxKeyLen = options.maxKeyLen || 128; const add = (id, limit) => { if (Object.keys(out).length >= maxItems) return; const key = safeStringLimit(id, maxKeyLen); const n = safeNumberClamp(limit, 0, Number.MAX_SAFE_INTEGER, 0, true); if (key && n > 0 && !Object.prototype.hasOwnProperty.call(out, key)) out[key] = n; }; if (Array.isArray(parsed)) parsed.forEach(item => item && add(item.id || item.share_id || item.key, item.limit || item.limit_count || item.value)); else if (parsed && typeof parsed === 'object') Object.keys(parsed).forEach(k => add(k, parsed[k])); while (Object.keys(out).length && estimateTextSize(JSON.stringify(out)) > (options.maxSize || Infinity)) { delete out[Object.keys(out).pop()]; } return out; } function normalizePwdVaultValue(value, options = {}) { const parsed = parseConfigJson(value, []); const src = Array.isArray(parsed) ? parsed : []; const map = new Map(); src.forEach(item => { const pass = safeStringLimit(item && typeof item === 'object' ? (item.p || item.password || item.value) : item, options.maxItemLen || 512); if (!pass) return; const hit = safeNumberClamp(item && typeof item === 'object' ? item.h : 0, 0, Number.MAX_SAFE_INTEGER, 0, true); const old = map.get(pass); map.set(pass, { p: pass, h: (old ? old.h : 0) + hit }); }); const list = Array.from(map.values()).sort((a, b) => b.h - a.h).slice(0, options.maxItems || 50); return trimJsonListBySize(list, options.maxSize || 0); } function normalizeArchivePwdValue(value, options = {}) { let src = parseConfigJson(value, value); if (Array.isArray(src)) { const success = src.find(x => x && typeof x === 'object' && (x.ok || x.success || x.status === 'OK')); src = success || src[0]; } if (src && typeof src === 'object') src = src.p || src.password || src.value || src.pass || ''; return safeStringLimit(src, options.maxItemLen || 512); } function normalizeDurationValue(value) { const parsed = parseConfigJson(value, value); const raw = parsed && typeof parsed === 'object' ? (parsed.duration ?? parsed.seconds ?? parsed.t ?? parsed.d) : parsed; return safeNumberClamp(raw, 0, 365 * 24 * 3600, 0, true); } function normalizeGhostFilesValue(value, options = {}) { const parsed = parseConfigJson(value, []); const src = Array.isArray(parsed) ? parsed : []; const seen = new Set(); const out = []; src.forEach(id => { const key = safeStringLimit(id, options.maxItemLen || 128); if (!key || seen.has(key) || out.length >= (options.maxItems || 1000)) return; seen.add(key); out.push(key); }); return trimJsonListBySize(out, options.maxSize || 0); } function normalizeTtlCacheValue(value, options = {}) { if (value === null || value === undefined || value === '') return ''; const parsed = parseConfigJson(value, null); const now = Date.now(); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { const expiresAt = parseConfigTime(parsed.expiresAt || parsed.expireAt || parsed.expire_at); const checkedAt = parseConfigTime(parsed.checkedAt || parsed.updatedAt || parsed.savedAt); if (expiresAt && expiresAt < now) return ''; if (!expiresAt && checkedAt && options.ttlMs && now - checkedAt > options.ttlMs) return ''; const text = JSON.stringify(parsed); return estimateTextSize(text) > (options.maxSize || Infinity) ? '' : text; } const text = safeStringLimit(value, options.maxItemLen || 2048); return estimateTextSize(text) > (options.maxSize || Infinity) ? '' : text; } function shouldRemoveCapturedCaptchaValue(value, options = {}) { if (value === null || value === undefined || value === '') return false; const parsed = parseConfigJson(value, null); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return true; const now = Date.now(); const expiresAt = parseConfigTime(parsed.expiresAt || parsed.expireAt || parsed.expire_at); if (expiresAt && expiresAt < now) return true; const savedAt = parseConfigTime(parsed.savedAt || parsed.saved_at || parsed.checkedAt || parsed.checked_at || parsed.updatedAt || parsed.updated_at); if (!expiresAt && !savedAt) return true; return !!(savedAt && options.ttlMs && now - savedAt > options.ttlMs); } function normalizeScriptUpdateDismissValue(value, options = {}) { const now = Date.now(); const parsed = parseConfigJson(value, null); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { const expiresAt = parseConfigTime(parsed.expiresAt); if (expiresAt && expiresAt < now) return ''; return JSON.stringify({ v: 1, expiresAt: expiresAt || (now + (options.ttlMs || 30 * 24 * 60 * 60 * 1000)) }); } if (value === '1' || value === 1 || value === true) return JSON.stringify({ v: 1, expiresAt: now + (options.ttlMs || 30 * 24 * 60 * 60 * 1000) }); return ''; } function normalizeCssPxValue(value, options = {}) { if (value === null || value === undefined || value === '') return ''; const n = parseFloat(String(value).replace(/[^\d.-]/g, '')); if (!Number.isFinite(n)) return ''; return `${safeNumberClamp(n, options.min || 0, options.max || 10000, 0, true)}px`; } function normalizeConfigValue(key, value, mode = 'localWrite') { mode = CONFIG_NORMALIZE_MODES.has(mode) ? mode : 'localWrite'; const schema = getConfigLimitSchema(key); if (!schema) return value; const limits = schema.localLimit || {}; if (schema.type === 'prohibitedProbe') return undefined; switch (schema.type) { case 'string': return safeStringLimit(value, limits.maxLen || 256); case 'queryParam': { const param = safeStringLimit(value, limits.maxLen || 64); return /^[A-Za-z0-9_.~-]+$/.test(param) ? param : schema.defaultValue; } case 'boolean': return safeBoolean(value, schema.defaultValue); case 'number': return safeNumberClamp(value, limits.min, limits.max, schema.defaultValue, !!limits.integer); case 'numberOrEmpty': return value === '' || value === null || value === undefined ? schema.defaultValue : safeNumberClamp(value, limits.min, limits.max, schema.defaultValue, !!limits.integer); case 'enum': return safeEnum(value, limits.allowed, schema.defaultValue, limits.aliases); case 'cssPx': return normalizeCssPxValue(value, limits); case 'lineList': return normalizeLineList(value, limits); case 'arrayList': return normalizeArrayList(value, limits).join(', '); case 'jsonMap': return JSON.stringify(normalizeJsonMap(value, limits)); case 'sortPref': return JSON.stringify(normalizeSortPref(value)); case 'jsonObject': return JSON.stringify(normalizeJsonMap(value, limits)); case 'jsonArray': return JSON.stringify(normalizeJsonArrayValue(value, limits)); case 'shareLimits': return JSON.stringify(normalizeShareLimitsValue(value, limits)); case 'shareParseHistory': return JSON.stringify(normalizeShareParseHistoryList(value, limits)); case 'pwdVault': return JSON.stringify(normalizePwdVaultValue(value, limits)); case 'archivePwd': return normalizeArchivePwdValue(value, limits); case 'duration': return normalizeDurationValue(value); case 'ghostFiles': return JSON.stringify(normalizeGhostFilesValue(value, limits)); case 'scriptUpdateCache': case 'i18nCache': case 'ttlValue': return normalizeTtlCacheValue(value, limits); case 'scriptUpdateDismiss': return normalizeScriptUpdateDismissValue(value, limits); case 'prefixValue': return safeStringLimit(value, limits.maxItemLen || 2048); default: return value; } } function getConfigLiveInputLimitedValue(key, value, cursor = 0) { const schema = getConfigLimitSchema(key); if (!schema || !['string', 'queryParam'].includes(schema.type)) return null; const limits = schema.localLimit || {}; const maxLen = Number(limits.maxLen) || (schema.type === 'queryParam' ? 64 : 256); const raw = String(value ?? ''); const pos = Math.max(0, Math.min(raw.length, Number(cursor) || 0)); if (schema.type === 'queryParam') { const filter = s => String(s || '').replace(/[^A-Za-z0-9_.~-]/g, ''); const filtered = filter(raw); return { value: filtered.slice(0, maxLen), cursor: Math.min(filter(raw.slice(0, pos)).length, maxLen), clipped: filtered.length > maxLen }; } return { value: raw.slice(0, maxLen), cursor: Math.min(pos, maxLen), clipped: raw.length > maxLen }; } function showConfigInputLimitTip(el) { const now = Date.now(); const last = Number(el && el.dataset ? (el.dataset.pkLiveLimitTipAt || 0) : 0); if (now - last < 1500) return; if (el && el.dataset) el.dataset.pkLiveLimitTipAt = String(now); const L = getStrings(); let tip = document.getElementById('pk-config-input-limit-tip'); if (!tip) { tip = document.createElement('div'); tip.id = 'pk-config-input-limit-tip'; tip.className = 'pk-config-input-limit-tip'; document.body.appendChild(tip); } tip.textContent = L.msg_config_input_over_limit; const rect = el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null; const left = rect ? Math.min(Math.max(rect.left + rect.width / 2, 80), Math.max(80, window.innerWidth - 80)) : window.innerWidth / 2; const top = rect ? Math.max(24, rect.top - 10) : window.innerHeight / 2; tip.style.left = `${left}px`; tip.style.top = `${top}px`; clearTimeout(tip._pkHideTimer); tip.classList.remove('show'); void tip.offsetWidth; tip.classList.add('show'); tip._pkHideTimer = setTimeout(() => tip.classList.remove('show'), 1800); } function bindConfigLiveInputLimits(root, pairs = []) { if (!root || !Array.isArray(pairs)) return; pairs.forEach(([selector, key]) => { const el = root.querySelector(selector); if (!el || el.dataset.pkLiveLimitBound === '1') return; el.dataset.pkLiveLimitBound = '1'; const oldInput = typeof el.oninput === 'function' ? el.oninput : null; const applyLimit = () => { if (el.dataset.pkComposing === '1') return false; const start = typeof el.selectionStart === 'number' ? el.selectionStart : String(el.value || '').length; const limited = getConfigLiveInputLimitedValue(key, el.value, start); if (!limited || limited.value === el.value) return false; el.value = limited.value; try { el.setSelectionRange(limited.cursor, limited.cursor); } catch (e) {} if (limited.clipped) showConfigInputLimitTip(el); return true; }; el.addEventListener('compositionstart', () => { el.dataset.pkComposing = '1'; }); el.addEventListener('compositionend', e => { delete el.dataset.pkComposing; setTimeout(() => { applyLimit(); if (oldInput) oldInput.call(el, e); }, 0); }); el.oninput = e => { applyLimit(); if (oldInput) oldInput.call(el, e); }; el.addEventListener('blur', applyLimit); }); } function bindPlainTextMaxLengthLimits(root, pairs = []) { if (!root || !Array.isArray(pairs)) return; pairs.forEach(pair => { const [selector, maxLen, notifyAtLimit] = pair || []; const el = root.querySelector(selector); const limit = Math.max(0, Number(maxLen) || 0); if (!el || !limit || el.dataset.pkPlainTextLimitBound === '1') return; el.dataset.pkPlainTextLimitBound = '1'; const oldInput = typeof el.oninput === 'function' ? el.oninput : null; const applyLimit = () => { if (el.dataset.pkComposing === '1') return false; const raw = String(el.value ?? ''); if (raw.length < limit) { if (notifyAtLimit) delete el.dataset.pkPlainTextLimitReached; return false; } if (raw.length === limit) { if (notifyAtLimit && el.dataset.pkPlainTextLimitReached !== '1') { el.dataset.pkPlainTextLimitReached = '1'; showConfigInputLimitTip(el); } return false; } const start = typeof el.selectionStart === 'number' ? el.selectionStart : raw.length; el.value = raw.slice(0, limit); try { el.setSelectionRange(Math.min(start, limit), Math.min(start, limit)); } catch (e) {} el.dataset.pkPlainTextLimitReached = '1'; showConfigInputLimitTip(el); return true; }; el.addEventListener('compositionstart', () => { el.dataset.pkComposing = '1'; }); el.addEventListener('compositionend', e => { delete el.dataset.pkComposing; setTimeout(() => { applyLimit(); if (oldInput) oldInput.call(el, e); }, 0); }); el.oninput = e => { applyLimit(); if (oldInput) oldInput.call(el, e); }; el.addEventListener('blur', applyLimit); }); } function getConfigListTextareaLimitedValue(key, value, full = false) { const schema = getConfigLimitSchema(key); if (!schema || !['arrayList', 'lineList'].includes(schema.type)) return null; const limits = schema.localLimit || {}; const raw = String(value ?? ''); if (full) return { value: normalizeConfigValue(key, raw, 'localWrite'), clipped: false }; const maxItems = Math.max(1, Number(limits.maxItems) || 1000); const maxItemLen = Math.max(1, Number(limits.maxItemLen) || 256); const maxSize = Math.max(0, Number(limits.maxSize) || 0); const commaLine = schema.type === 'arrayList' && limits.split === 'commaLine'; const sep = commaLine && (raw.match(/[,,]/g) || []).length > (raw.match(/[\r\n]/g) || []).length ? ', ' : '\n'; const parts = commaLine ? raw.split(/[,,\n\r]+/) : raw.split(/\r?\n/); const out = []; let clipped = false; for (let i = 0; i < parts.length; i++) { let item = String(parts[i] ?? '').trim(); if (!item) continue; if (item.length > maxItemLen) { item = item.slice(0, maxItemLen); clipped = true; } if (out.length >= maxItems) { clipped = true; break; } const candidate = out.concat(item).join(sep); if (maxSize && estimateTextSize(candidate) > maxSize) { return { value: trimTextByBytesOrChars(out.join(sep), maxSize), clipped: true }; } out.push(item); } if (out.length < parts.map(x => String(x ?? '').trim()).filter(Boolean).length) clipped = true; return clipped ? { value: out.join(sep), clipped: true } : { value: raw, clipped: false }; } function bindConfigListTextareaLimits(root, pairs = []) { if (!root || !Array.isArray(pairs)) return; pairs.forEach(([selector, key]) => { const el = root.querySelector(selector); if (!el || el.dataset.pkListLimitBound === '1') return; el.dataset.pkListLimitBound = '1'; const oldInput = typeof el.oninput === 'function' ? el.oninput : null; let timer = 0; const runOldInput = e => { if (oldInput) oldInput.call(el, e || { target: el }); }; const applyLimit = (full = false, notify = true, e = null) => { if (el.dataset.pkComposing === '1') return false; const raw = String(el.value ?? ''); const limited = getConfigListTextareaLimitedValue(key, raw, full); if (!limited || limited.value === raw) return false; const start = typeof el.selectionStart === 'number' ? el.selectionStart : raw.length; el.value = limited.value; const cursor = Math.min(start, String(limited.value).length); try { el.setSelectionRange(cursor, cursor); } catch (err) {} if (notify && limited.clipped) showConfigInputLimitTip(el); runOldInput(e); return true; }; el.addEventListener('paste', e => setTimeout(() => applyLimit(false, true, e), 0)); el.addEventListener('compositionstart', () => { el.dataset.pkComposing = '1'; }); el.addEventListener('compositionend', e => { delete el.dataset.pkComposing; setTimeout(() => { if (!applyLimit(false, true, e)) runOldInput(e); }, 0); }); el.oninput = e => { runOldInput(e); clearTimeout(timer); timer = setTimeout(() => applyLimit(false, true, e), 300); }; el.addEventListener('blur', e => { clearTimeout(timer); applyLimit(true, false, e); }); }); } function isPikPakFileNameTooLong(name) { return String(name ?? '').trim().length > (Number(CONF.fileNameMaxLen) || 1024); } function showPikPakFileNameLimitTip() { showToast(getStrings().msg_config_input_over_limit, 'warning'); } function getStoredConfigKeys() { const keys = new Set(); try { if (typeof GM_listValues !== 'undefined') { const gmKeys = GM_listValues(); if (Array.isArray(gmKeys)) gmKeys.forEach(k => keys.add(k)); } } catch (e) {} try { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k) keys.add(k); } } catch (e) {} return Array.from(keys); } function getStoredConfigRaw(key) { try { if (typeof GM_getValue !== 'undefined') { const marker = `__pk_missing_${Date.now()}_${Math.random()}__`; const v = GM_getValue(key, marker); if (v !== marker) return v; } } catch (e) {} try { const v = localStorage.getItem(key); if (v !== null) return v; } catch (e) {} return undefined; } function setStoredConfigRaw(key, value) { if (value === undefined) return removeStoredConfigKey(key); try { if (typeof GM_setValue !== 'undefined') GM_setValue(key, value); } catch (e) {} try { localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); } catch (e) {} } function removeStoredConfigKey(key) { try { if (typeof GM_deleteValue !== 'undefined') GM_deleteValue(key); } catch (e) {} try { localStorage.removeItem(key); } catch (e) {} } function configValueEquals(a, b) { if (a === b) return true; try { return JSON.stringify(a) === JSON.stringify(b); } catch (e) { return String(a) === String(b); } } function getConfigEntryRank(key, value, fallbackRank) { const parsed = parseConfigJson(value, null); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { const t = parseConfigTime(parsed.lastUsedAt || parsed.last_used_at || parsed.updatedAt || parsed.updated_at || parsed.savedAt || parsed.saved_at || parsed.checkedAt || parsed.expiresAt || parsed.ts || parsed.time); if (t) return t; } return fallbackRank; } function cleanupPrefixKeys(prefix, options = {}) { const keys = getStoredConfigKeys().filter(k => k.startsWith(prefix)); const entries = []; keys.forEach((key, index) => { if (options.maxKeyLen && key.length > prefix.length + options.maxKeyLen) { removeStoredConfigKey(key); return; } const raw = getStoredConfigRaw(key); const normalized = normalizeConfigValue(key, raw, 'localWrite'); if (normalized === undefined || (options.removeEmpty && (normalized === '' || normalized === '[]' || normalized === '{}'))) { removeStoredConfigKey(key); return; } if (!configValueEquals(raw, normalized)) setStoredConfigRaw(key, normalized); entries.push({ key, value: normalized, rank: getConfigEntryRank(key, normalized, index), size: estimateTextSize(key) + estimateTextSize(typeof normalized === 'string' ? normalized : JSON.stringify(normalized)) }); }); entries.sort((a, b) => b.rank - a.rank); const keep = new Set(); let total = 0; const maxItems = options.maxItems || Infinity; const maxSize = options.maxSize || Infinity; entries.forEach(entry => { if (keep.size >= maxItems || total + entry.size > maxSize) return; keep.add(entry.key); total += entry.size; }); entries.forEach(entry => { if (!keep.has(entry.key)) removeStoredConfigKey(entry.key); }); } function cleanupLruPrefixKeys(prefix, options = {}) { cleanupPrefixKeys(prefix, options); } function cleanupTtlPrefixKeys(prefix, options = {}) { const now = Date.now(); getStoredConfigKeys().filter(k => k.startsWith(prefix)).forEach(key => { const raw = getStoredConfigRaw(key); const parsed = parseConfigJson(raw, null); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { const expiresAt = parseConfigTime(parsed.expiresAt || parsed.expireAt || parsed.expire_at); const checkedAt = parseConfigTime(parsed.checkedAt || parsed.updatedAt || parsed.savedAt); if ((expiresAt && expiresAt < now) || (!expiresAt && checkedAt && options.ttlMs && now - checkedAt > options.ttlMs)) { removeStoredConfigKey(key); } } }); cleanupPrefixKeys(prefix, options); } function cleanupConfigPrefixKeys() { const captchaSchema = getConfigLimitSchema('pk_captured_captcha'); const captchaRaw = getStoredConfigRaw('pk_captured_captcha'); if (captchaRaw !== undefined && shouldRemoveCapturedCaptchaValue(captchaRaw, (captchaSchema && captchaSchema.localLimit) || {})) { removeStoredConfigKey('pk_captured_captcha'); } getStoredConfigKeys().forEach(key => { if (isCapacityProbeStorageKey(key)) removeStoredConfigKey(key); }); CONFIG_LIMIT_SCHEMA.entries.filter(entry => entry.key).forEach(entry => { const raw = getStoredConfigRaw(entry.key); if (raw === undefined) return; const normalized = normalizeConfigValue(entry.key, raw, 'localWrite'); if (normalized === undefined || normalized === '') { if (entry.ttl || entry.type === 'prohibitedProbe') removeStoredConfigKey(entry.key); return; } if (!configValueEquals(raw, normalized)) setStoredConfigRaw(entry.key, normalized); }); cleanupLruPrefixKeys('pk_archive_pwd_', { maxItems: 1000, maxItemLen: 512, maxSize: 256 * CONFIG_SIZE.KB, removeEmpty: true }); cleanupLruPrefixKeys('pk_duration_', { maxItems: 10000, maxSize: CONFIG_SIZE.MB }); cleanupLruPrefixKeys('pk_scan_last_', { maxItems: 500, maxItemLen: 2048, maxSize: 128 * CONFIG_SIZE.KB }); cleanupLruPrefixKeys('pk_analyze_last_', { maxItems: 500, maxItemLen: 2048, maxSize: 128 * CONFIG_SIZE.KB }); cleanupLruPrefixKeys('pk_fmod_', { maxItems: 5000, maxKeyLen: 128, maxItemLen: 2048, maxSize: 512 * CONFIG_SIZE.KB }); cleanupTtlPrefixKeys('pk_i18n_', { maxSize: 512 * CONFIG_SIZE.KB, ttlMs: 7 * 24 * 60 * 60 * 1000 }); cleanupTtlPrefixKeys('pk_script_update_dismiss_', { maxSize: 64 * CONFIG_SIZE.KB, ttlMs: 30 * 24 * 60 * 60 * 1000 }); cleanupTtlPrefixKeys('pk_clipboard_magnet_ignore_', { maxSize: 64 * CONFIG_SIZE.KB, ttlMs: 24 * 60 * 60 * 1000 }); cleanupTtlPrefixKeys('pk_clipboard_magnet_last_', { maxSize: 64 * CONFIG_SIZE.KB, ttlMs: 7 * 24 * 60 * 60 * 1000 }); } let configPrefixCleanupTimer = 0; function shouldCleanupAfterConfigWrite(key) { const schema = getConfigLimitSchema(key); return !!(schema && (schema.isPrefix || schema.ttl || ['pk_ghost_files', 'pk_pwd_vault', 'pk_share_parse_history', 'pk_share_limits', 'pk_expired_shares'].includes(schema.key))); } function scheduleConfigPrefixCleanup(reason = 'write', delay = 1200) { try { if (configPrefixCleanupTimer) clearTimeout(configPrefixCleanupTimer); configPrefixCleanupTimer = setTimeout(() => { configPrefixCleanupTimer = 0; const run = () => { try { cleanupConfigPrefixKeys(); } catch (e) { console.warn('[ConfigLimit] cleanup failed:', e); } }; if (typeof requestIdleCallback === 'function') requestIdleCallback(run, { timeout: 3000 }); else setTimeout(run, 0); }, delay); } catch (e) {} } if (window.self !== window.top) return; const parseCloudLinks = (rawString, isSmartFix) => { const formattedVal = String(rawString || '').replace(/(https?:\/\/|ftp:\/\/|sftp:\/\/|magnet:\?|ed2k:\/\/|thunder:\/\/)/gi, '\n$1'); const lines = formattedVal.split('\n').map(l => l.trim()).filter(l => l); const uniqueTasks = new Map(); const linkRegex = /^(https?:\/\/|ftp:\/\/|sftp:\/\/|magnet:\?|ed2k:\/\/|thunder:\/\/)/i; const base32ToHex = (b32) => { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bits = ""; const input = String(b32 || '').toUpperCase(); for (let i = 0; i < input.length; i++) { const val = alphabet.indexOf(input[i]); if (val === -1) return null; bits += val.toString(2).padStart(5, '0'); } let hex = ""; for (let i = 0; i + 4 <= bits.length; i += 4) { const chunk = bits.substring(i, i + 4); const num = parseInt(chunk, 2); if (!isNaN(num)) hex += num.toString(16); } return hex.toUpperCase(); }; const extractHexHash = (url) => { const match = String(url || '').match(/urn:btih:([^&]+)/i); if (!match) return null; const hash = match[1].toUpperCase(); if (hash.length === 40) return hash; if (hash.length === 32) return base32ToHex(hash); return null; }; const addResult = (url) => { const hash = extractHexHash(url); if (hash) { if (!uniqueTasks.has(hash)) uniqueTasks.set(hash, url); } else { uniqueTasks.set(url, url); } }; for (let i = 0; i < lines.length; i++) { let line = lines[i]; if (linkRegex.test(line)) { addResult(line); continue; } if (isSmartFix) { let cleanedStr = line.replace(/[^a-zA-Z0-9]/g, ''); const hexMatches = cleanedStr.matchAll(/([a-fA-F0-9]{40})/g); for (const match of hexMatches) { addResult(`magnet:?xt=urn:btih:${match[1].toUpperCase()}`); cleanedStr = cleanedStr.replace(match[1], ' '.repeat(40)); } const b32Matches = cleanedStr.matchAll(/([a-zA-Z2-7]{32})/g); for (const match of b32Matches) { const hex = base32ToHex(match[1].toUpperCase()); if (hex) addResult(`magnet:?xt=urn:btih:${hex}`); } } } return Array.from(uniqueTasks.values()); }; (() => { const inject = () => { const s = document.createElement('script'); s.textContent = `(function(){ const _f = window.fetch; window.fetch = async function(...args) { const raw = args[0]; const url = raw && raw.url ? raw.url : (raw ? raw.toString() : ''); if (url.includes('area_accessible')) { return new Promise((_, reject) => { reject(new Error("Bypassed area_accessible")); }); } const isTurbo = localStorage.getItem('pk_turbo_mode') === 'true'; if (isTurbo) { if (location.href.includes('/login') || location.pathname.includes('login')) { return _f.apply(this, args); } if (url.includes(':incremental_sync') || url.includes(':sync')) { return new Response(JSON.stringify({ error_code: 0, data: [], files: [], tasks: [], next_page_token: '' }), {status: 200, headers: {'Content-Type': 'application/json'}}); } try { const opts = args[1] || {}; let cap = null; let auth = null; if (opts.headers) { if (opts.headers instanceof Headers) { cap = opts.headers.get('x-captcha-token') || opts.headers.get('X-Captcha-Token'); auth = opts.headers.get('authorization') || opts.headers.get('Authorization'); } else if (typeof opts.headers === 'object') { const capKey = Object.keys(opts.headers).find(k => k.toLowerCase() === 'x-captcha-token'); if (capKey) cap = opts.headers[capKey]; const authKey = Object.keys(opts.headers).find(k => k.toLowerCase() === 'authorization'); if (authKey) auth = opts.headers[authKey]; } } if (cap && cap.length > 20) localStorage.setItem('pk_captured_captcha', String(cap).slice(0, 2048)); if (auth && auth.length > 20) document.dispatchEvent(new CustomEvent('pk-token-captured', { detail: auth })); } catch (e) {} } const res = await _f.apply(this, args); try { const u = url.split('?')[0]; const authReadyUrl = u.includes('/drive/v1/about') || u.includes('/v1/user/me'); const authErrorUrl = u.includes('/drive/v1/about') || u.includes('/drive/v1/files') || u.includes('/v1/user/me') || u.includes('/v1/auth/token'); if (res && res.ok && authReadyUrl) { document.dispatchEvent(new CustomEvent('pk-official-auth-ready', { detail: { url: u, status: res.status } })); } else if (res && authErrorUrl && (res.status === 401 || res.status === 403 || res.status === 400)) { document.dispatchEvent(new CustomEvent('pk-official-auth-error', { detail: { url: u, status: res.status } })); } } catch (e) {} return res; }; const _W = window.Worker; window.Worker = function(url, opts) { const isTurbo = localStorage.getItem('pk_turbo_mode') === 'true'; if (!isTurbo) return new _W(url, opts); if (location.href.includes('/login') || location.pathname.includes('login')) { return new _W(url, opts); } const u = url ? url.toString() : ''; const blockList = ['query_db', 'sync', 'database', 'index', 'calc_sha1', 'query_docs']; if (blockList.some(k => u.includes(k))) { return new _W('data:application/javascript,self.onmessage=()=>{}'); } return new _W(url, opts); }; })()`; (document.head || document.documentElement).appendChild(s).remove(); }; inject(); })(); window.addEventListener('beforeunload', (e) => { const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; const tasks = pkState?.uploadTasks || []; if (tasks.some(t => activeStatus.includes(t.status))) { e.preventDefault(); return e.returnValue; } }); window.pkAddGhostFile = function(id) { try { const ghosts = JSON.parse(localStorage.getItem('pk_ghost_files') || '[]'); if (!ghosts.includes(id)) { ghosts.push(id); localStorage.setItem('pk_ghost_files', normalizeConfigValue('pk_ghost_files', ghosts, 'localWrite')); } } catch(e){} }; window.pkRemoveGhostFile = function(id) { try { const ghosts = JSON.parse(localStorage.getItem('pk_ghost_files') || '[]'); const updated = ghosts.filter(x => x !== id); localStorage.setItem('pk_ghost_files', normalizeConfigValue('pk_ghost_files', updated, 'localWrite')); } catch(e){} }; window.pkCleanupGhostFiles = function() { try { const ghosts = JSON.parse(localStorage.getItem('pk_ghost_files') || '[]'); if (ghosts.length > 0) { const BATCH_SIZE = 100; for (let i = 0; i < ghosts.length; i += BATCH_SIZE) { const chunk = ghosts.slice(i, i + BATCH_SIZE); fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }) }).catch(()=>{}); } localStorage.setItem('pk_ghost_files', normalizeConfigValue('pk_ghost_files', [], 'localWrite')); } } catch(e){} try { const phases = "PHASE_TYPE_UNKNOW,PHASE_TYPE_PENDING,PHASE_TYPE_RUNNING,PHASE_TYPE_PAUSED,PHASE_TYPE_ERROR"; const filters = encodeURIComponent(JSON.stringify({ "phase": { "in": phases } })); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?type=upload&limit=100&filters=${filters}&_t=${Date.now()}`; fetch(url, { headers: getHeaders() }) .then(res => res.json()) .then(cloudData => { const cloudTasks = cloudData.tasks || []; const ghostIds = []; cloudTasks.forEach(ct => { if (ct.file_id) { const ref = ct.reference_resource || {}; const taskName = ref.name || ct.name || ct.file_name || 'Ghost Task'; if (taskName === 'upload' || taskName === 'Ghost Task') { ghostIds.push(ct.file_id); } } }); if (ghostIds.length > 0) { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ghostIds }) }).catch(()=>{}); } }).catch(()=>{}); } catch (e) {} }; window.addEventListener('beforeunload', () => { const tasks = pkState?.uploadTasks || []; const filesToDelete = tasks.filter(t => t.status !== 'DONE' && t.file_id && !t._deleted).map(t => t.file_id); if (filesToDelete.length > 0) { const BATCH_SIZE = 100; for (let i = 0; i < filesToDelete.length; i += BATCH_SIZE) { const chunk = filesToDelete.slice(i, i + BATCH_SIZE); try { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }), keepalive: true }).catch(()=>{}); } catch(e) {} } if (typeof window.pkRemoveGhostFile === 'function') { filesToDelete.forEach(id => window.pkRemoveGhostFile(id)); } } }); ; const CONF = { rowHeight: 40, buffer: 20, dupGridHeaderHeight: { normal: 40, max: 60 }, dupGridSectionGap: { normal: 14, max: 18 }, dupGridBodyGapY: { normal: 16, max: 20 }, SYSTEM_FOLDER_NAME: 'My Pack', fileNameMaxLen: 1024, officialDirFileFallbackIcon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wIDQuOEMwIDIuMTQ5MDMgMi4xNDkwMyAwIDQuOCAwSDE5LjJDMjEuODUxIDAgMjQgMi4xNDkwMyAyNCA0LjhWMTkuMkMyNCAyMS44NTEgMjEuODUxIDI0IDE5LjIgMjRINC44QzIuMTQ5MDMgMjQgMCAyMS44NTEgMCAxOS4yVjQuOFoiIGZpbGw9IiNBOUJBRDIiLz4KPHBhdGggb3BhY2l0eT0iMC40IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUuMzk5OSA2LjA0MDAzQzUuMzk5OSA0LjgyNSA2LjM4ODQ2IDMuODQwMDMgNy42MDc5IDMuODQwMDNIMTIuMDIzOUwxOS4xOTk5IDEwLjk5VjE4LjE0QzE5LjE5OTkgMTkuMzU1MSAxOC4yMTEzIDIwLjM0IDE2Ljk5MTkgMjAuMzRINy42MDc5QzYuMzg4NDYgMjAuMzQgNS4zOTk5IDE5LjM1NTEgNS4zOTk5IDE4LjE0VjYuMDQwMDNaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyLjAyMzkgMy44NDAwM0wxOS4xOTk5IDEwLjk5SDE0LjIzMTlDMTMuMDEyNSAxMC45OSAxMi4wMjM5IDEwLjAwNTEgMTIuMDIzOSA4Ljc5MDAzVjMuODQwMDNaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K', officialDirFolderFallbackIcon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wLjI1IDQuMTg3MjNDMC4yNSAyLjAxMjc2IDIuMDEyNzYgMC4yNSA0LjE4NzIzIDAuMjVIOC40NDM2MkM5LjQ2NzEyIDAuMjUgMTAuNDc2NiAwLjQ4ODQ0NSAxMS4zOTE5IDAuOTQ2NDI5VjAuOTQ2NDI5QzEyLjMwNzIgMS40MDQ0MSAxMy4zMTY2IDEuNjQyODYgMTQuMzQwMSAxLjY0Mjg2SDE2LjkzNjlDMTguNDkwNSAxLjY0Mjg2IDE5Ljc1IDIuOTAyMzMgMTkuNzUgNC40NTU5NlYxNC41NUMxOS43NSAxNy40MjE5IDE3LjQyMTkgMTkuNzUgMTQuNTUgMTkuNzVINS40NUMyLjU3ODEyIDE5Ljc1IDAuMjUgMTcuNDIxOSAwLjI1IDE0LjU1VjQuMTg3MjNaIiBmaWxsPSIjRURBODMzIi8+CjxyZWN0IHg9IjIuMzM4ODciIHk9IjMuMDM1MTYiIHdpZHRoPSIxNS42IiBoZWlnaHQ9IjExLjciIHJ4PSIwLjk3NSIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTAuMjUgNi41ODc4OUgxOS43NVYxNS44NTA0QzE5Ljc1IDE4LjAwNDMgMTguMDAzOSAxOS43NTA0IDE1Ljg1IDE5Ljc1MDRINC4xNUMxLjk5NjA5IDE5Ljc1MDQgMC4yNSAxOC4wMDQzIDAuMjUgMTUuODUwNFY2LjU4Nzg5WiIgZmlsbD0iI0ZGQkI0NyIvPgo8L3N2Zz4K', linkBookmarkFolderNameMaxLen: 1024, linkBookmarkTitleMaxLen: 1024, linkBookmarkHrefMaxLen: 65534, linkBookmarkPlainHrefMaxLen: 1024, linkBookmarkSafeTotalBytes: 18 * 1024 * 1024, linkBookmarkConfigCloudReserveBytes: 11 * 1024 * 1024, browserDownloadConfirmFileCount: 10, browserDownloadConfirmTotalBytes: 10 * 1024 * 1024 * 1024, downloadAccelEnable: false, offlineReferenceMissingCodes:['404','not_found','not found','file_not_found','item_not_found','resource_not_found','file deleted','resource deleted','already_deleted','deleted'], downloadAccelDomain: '', downloadAccelMode: 'prefix', downloadAccelQueryParam: 'url', downloaderType: 'aria2', downloaderPreferOriginalVideoLink: true, aria2KeepFolderStructure: true, aria2DownloadDir: '', aria2TolerantOptions: {'continue':'true','max-tries':'0','retry-wait':'15','timeout':'90','connect-timeout':'45','lowest-speed-limit':'1K'}, gopeedApiUrl: '', gopeedDownloadDir: '', gopeedKeepFolderStructure: true, gopeedBatchSize: 10, gopeedRequestTimeout: 8000, abdmApiUrl: '', abdmDownloadDir: '', abdmRequestTimeout: 8000, idmExportMode: 'url', idmUrlExportType: 'txt', idmExePath: '', idmBatDownloadRoot: '', idmBatKeepFolderStructure: true, uploadPartSizeOfficial: 1 * 1024 * 1024, uploadPartMaxCount: 10000, uploadPartConcurrencyOfficial: 5, mouseSideNavHistoryMax: 50, shareParseInsightConcurrency: 128, clipboardMagnetPaste: true, clipboardMagnetFocusCooldown: 2500, clipboardMagnetDenyCooldown: 60000, clipboardMagnetPromptGap: 10 * 60 * 1000, clipboardMagnetIgnoreTTL: 10 * 60 * 1000, clipboardMagnetMaxChars: 200000, clipboardMagnetPreviewQueueMax: 3, magnetPreviewApi: 'https://whatslink.info/api/v1/link', magnetPreviewTimeout: 9000, magnetPreviewCacheTTL: 60 * 60 * 1000, magnetPreviewErrorCacheTTL: 10 * 60 * 1000, magnetPreviewCircuitTTL: 5 * 60 * 1000, magnetPreviewMaxShots: 5, magnetArchiveFolderKey: 'folder_magnet_archive', magnetArchiveFolderStorageName: 'PEM-CLOUD-ARCHIVE', magnetArchiveMaxSelected: 50, magnetArchivePreviewMaxRows: 50, magnetArchiveJournalMax: 2000, magnetArchiveMaxSourceChars: 200000, magnetArchiveSourceKeys: ['source_url','sourceUrl','original_url','originalUrl','magnet','link','href','url'], magnetArchiveFolderHeaderHrefPrefix: 'pem-cloud-archive-folder:', magnetArchiveStateHrefPrefix: 'pem-cloud-archive-state:', magnetArchiveFolderHeaderTitle: 'PEM-CLOUD-ARCHIVE-FOLDER', magnetArchiveStateTitle: 'PEM-CLOUD-ARCHIVE-STATE', magnetArchiveStateHrefMaxLen: 48000, magnetArchiveMagnetHrefMaxLen: 48000, textPreviewMaxBytes: 30 * 1024 * 1024, offlineTaskCopyExtBlacklist: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mp3','flac','wav','m4a','aac','jpg','jpeg','png','gif','webp','srt','ass','vtt','zip','rar','7z','tar','gz','pdf','txt'], txtPreviewHotkeys: { parse: 'Ctrl+Enter', fullscreen: 'F', close: 'Esc' }, audioHotkeys: { mini: 'P' }, playerVolumeStep: 0.05, audioCoverRangeBytes: 192 * 1024, audioCoverCacheMax: 80, audioMiniPosKey: 'pk_audio_mini_pos', potplayerLaunchStateKey: 'pk_potplayer_launch_state', potplayerLaunchStateSchemaVersion: 1, potplayerProtocolStateKey: 'pk_potplayer_protocol_state', potplayerCustomPathKey: 'pk_potplayer_custom_path', potplayerProtocolStateSchemaVersion: 1, potplayerProtocolRepairVersion: '20260501', potplayerRegDeleteFileName: 'pikpak-potplayer-delete.reg', potplayerRegCustomInstallFileName: 'pikpak-potplayer-install-custom.reg', potplayerBrowserPolicyRegFileName: 'pikpak-potplayer-browser-policy.reg', potplayerAutoRepairFailThreshold: 2, potplayerPromptCooldown: 10 * 60 * 1000, potplayerLikelyOpenTrustTTL: 24 * 60 * 60 * 1000, potplayerSuppressTodayTTL: 24 * 60 * 60 * 1000, potplayerPostRepairConfirmDelay: 6000, scriptUpdateManifestUrl: 'https://raw.githubusercontent.com/digbug82/PikPak_Enhancement_Master/main/version.json', scriptUpdateCheckTTL: 24 * 60 * 60 * 1000, scriptUpdateCacheKey: 'pk_script_update_cache', scriptUpdateDismissPrefix: 'pk_script_update_dismiss_', scriptUpdateProjectUrl: 'https://github.com/digbug82/PikPak_Enhancement_Master', linkBookmarkSettingsUrl: 'https://api-drive.mypikpak.com/user/v1/settings?items=bookmark', linkBookmarkSettingsSaveUrl: 'https://api-drive.mypikpak.com/user/v1/settings', linkBookmarkSyncFolderName: 'PikPak Enhancement Master Sync', configCloudManifestTitlePrefix: 'PEM-CONFIG-MANIFEST-', configCloudChunkTitlePrefix: 'PEM-CONFIG-CHUNK-', configCloudManifestHrefPrefix: 'pem-config-manifest:', configCloudChunkSize: 40000, configCloudMaxChunks: 256, configCloudSchemaVersion: 1, logoSVG: (size = '24px', id = 'pk-logo-' + Math.random().toString(36).slice(2)) => ``, emptySVG: ``, dupHashSVG: ``, dupSimSVG: ``, dupNameSVG: ``, dupContainSVG: ``, crumbIcons: { right: ``, down: ``, sortAZ: ``, sortZA: ``, sortNew: ``, sortOld: ``, sortProgressAsc:``, sortProgressDesc:``, sortLarge: ``, sortSmall: ``, sortStarNew: ``, sortStarOld: ``, sortTypeDurAsc: ``, sortTypeDurDesc: ``, sortPathAsc: ``, sortPathDesc: `` }, icons: { offline: ``, navShare: ``, shareParse: ``, linkBookmark: ``, cloudArchive: ``, linkBookmarkFallback: ``, linkBookmarkFolderDel: ``, linkBookmarkFolderRename: ``, configCloud: ``, search: ``, openLink: ``, calendar: ``, unshare: ``, refresh: ``, retry: ``, settings: ``, home: ``, recent: ``, history: ``, trash: ``, emptyTrash: ``, restore: ``, delForever: ``, newfolder: ``, linkBookmarkNewLink: ``, del: ``, deselect: ``, copy: ``, cut: ``, paste: ``, rename: ``, bulkrename: ``, unzip: ``, migrate: ``, exportM3U: ``, prune: ``, blacklist: ``, invert: ``, folderFirst: ``, viewList: ``, viewGrid: ``, analyze: ``, scanDup: ``, export: ``, stop: ``, ext: ``, download: ``, aria2:``, idm: ``, gopeed: ``, abdm: ``, moon: ``, sun: ``, cloudDownload: ``, configCloudUpload: ``, configCloudPull: ``,share: ``, maximize: ``, minimize: ``, close: ``, help: ``, configCloudDetail: ``, warning: ``, eye: ``, eyeoff: ``, lock: ``, uploadBtn: ``, upFile: ``, upFolder: ``, navUpload: ``, taskStart: ``, taskPause: ``, logout: ``, blMarker: ``, vault: `` }, audioIcons: {play:``,pause:``,prev:``,next:``,vol:``,mute:``,modeOrder:``,modeList:``,modeSingle:``,modeShuffle:``}, typeIcons: { folder: ``, systemFolder: ``, video: ``, image: ``, audio: ``, archive: ``, archiveUnzipped: ``, text: ``, pdf: ``, subtitle: ``, executable: ``, web: ``, apk: ``, file: `` } }; ; CONF.audioIcons.playlist = ``; CONF.audioIcons.miniFold = ``; CONF.audioIcons.mini = ``; CONF.audioIcons.restore = ``; CONF.audioIcons.close = CONF.icons.close; CONF.icons.txtPreviewFullscreenSVG = CONF.icons.maximize; CONF.icons.txtPreviewExitFullscreenSVG = CONF.icons.minimize; CONF.icons.txtPreviewParseLinksSVG = CONF.icons.cloudDownload; CONF.icons.shareParseStop = ``; CONF.icons.shareParseBack = ``; CONF.icons.imgSearch = ``; CONF.icons.dragUploadPlus = ``; CONF.icons.crumbTrash = ``; CONF.icons.crumbStar = ``; CONF.icons.snapLink = ``; CONF.icons.infoCircle = ``; function pkIconRaw(name, group = 'icons') { const source = group === 'audioIcons' ? CONF.audioIcons : (group === 'icons' ? CONF.icons : null); const icon = source && source[name]; return typeof icon === 'string' ? icon : ''; } function pkSvgAttrRegex(name) { return new RegExp("(\\s" + name + "\\s*=\\s*)([\"'])(.*?)\\2", "i"); } function pkSvgAttrValue(value) { return String(value ?? '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); } function pkSvgGetAttr(attrs, name) { const match = String(attrs || '').match(pkSvgAttrRegex(name)); return match ? match[3] : ''; } function pkSvgSetAttr(attrs, name, value) { const safeValue = pkSvgAttrValue(value); const re = pkSvgAttrRegex(name); if (re.test(attrs)) return attrs.replace(re, (full, prefix, quote) => `${prefix}${quote}${safeValue}${quote}`); return `${attrs} ${name}="${safeValue}"`; } function pkSvgMergeClassAttr(attrs, className) { const extra = String(className || '').trim(); if (!extra) return attrs; const merged = []; const seen = new Set(); `${pkSvgGetAttr(attrs, 'class')} ${extra}`.trim().split(/\s+/).forEach(cls => { if (!cls || seen.has(cls)) return; seen.add(cls); merged.push(cls); }); return pkSvgSetAttr(attrs, 'class', merged.join(' ')); } function pkSvgMergeStyleAttr(attrs, style) { const extra = String(style || '').trim(); if (!extra) return attrs; const current = pkSvgGetAttr(attrs, 'style').trim(); const merged = current ? `${current.replace(/;?\s*$/, ';')}${extra}` : extra; return pkSvgSetAttr(attrs, 'style', merged); } function pkIconHtml(name, options = {}) { const opts = options || {}; const raw = pkIconRaw(name, opts.group || 'icons'); if (!raw) return ''; return String(raw).replace(/]*)>/i, (match, attrs = '') => { let nextAttrs = attrs || ''; const size = opts.size; const width = opts.width != null ? opts.width : size; const height = opts.height != null ? opts.height : size; if (width != null) nextAttrs = pkSvgSetAttr(nextAttrs, 'width', width); if (height != null) nextAttrs = pkSvgSetAttr(nextAttrs, 'height', height); if (opts.className) nextAttrs = pkSvgMergeClassAttr(nextAttrs, opts.className); if (opts.style) nextAttrs = pkSvgMergeStyleAttr(nextAttrs, opts.style); if (Object.prototype.hasOwnProperty.call(opts, 'ariaHidden') || !pkSvgGetAttr(nextAttrs, 'aria-hidden')) { nextAttrs = pkSvgSetAttr(nextAttrs, 'aria-hidden', Object.prototype.hasOwnProperty.call(opts, 'ariaHidden') ? opts.ariaHidden : true); } if (Object.prototype.hasOwnProperty.call(opts, 'focusable') || !pkSvgGetAttr(nextAttrs, 'focusable')) { nextAttrs = pkSvgSetAttr(nextAttrs, 'focusable', Object.prototype.hasOwnProperty.call(opts, 'focusable') ? opts.focusable : false); } return ``; }); } function pkIconSized(name, size = 16, options = {}) { return pkIconHtml(name, Object.assign({ size }, options || {})); } function pkIconInline(name, options = {}) { const opts = Object.assign({}, options || {}); const inlineStyle = 'vertical-align:-3px;margin-right:4px;'; opts.style = opts.style ? `${inlineStyle}${opts.style}` : inlineStyle; return pkIconHtml(name, opts); } function pkIconWithClass(name, className, options = {}) { return pkIconHtml(name, Object.assign({}, options || {}, { className })); } const CSS = ` :root { --pk-bg: #ffffff; --pk-bg-rgb: 255, 255, 255; --pk-fg: #1a1a1a; --pk-bd: #e5e5e5; --pk-hl: #f0f0f0; --pk-sel-bg: #e6f3ff; --pk-sel-bd: #cce8ff; --pk-pri: #0067c0; --pk-btn-hov: #e0e0e0; --pk-gh: #f5f5f5; --pk-gh-fg: #333; --pk-sb-bg: transparent; --pk-sb-th: #ccc; --pk-sb-hov: #aaa; --pk-icon-c: #888; --pk-tip-bg: rgba(255, 255, 255, 0.95); --pk-tip-fg: #1a1a1a; --pk-tip-bd: rgba(0, 0, 0, 0.06); --pk-tip-sd: rgba(0, 0, 0, 0.12); --pk-toast-bg: rgba(255, 255, 255, 0.95); --pk-toast-fg: #1a1a1a; --pk-toast-bd: rgba(0, 0, 0, 0.08); --pk-match-bg: #fff2cc; --pk-match-fg: #d93025; --pk-v-line: #d1d1d1; } .pk-no-transition, .pk-no-transition * { transition: none !important; } .pk-magnet-preview-wrap { width:420px; max-width:86vw; display:flex; flex-direction:column; color:var(--pk-fg); overflow:hidden; } .pk-magnet-hero { width:100%; height:210px; background:var(--pk-hl); display:flex; align-items:center; justify-content:center; overflow:hidden; border-radius:14px 14px 0 0; } .pk-magnet-hero img { width:100%; height:100%; object-fit:cover; display:block; } .pk-magnet-empty { width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:14px; opacity:0.62; } .pk-magnet-body { padding:18px 20px 20px 20px; display:flex; flex-direction:column; gap:12px; } .pk-magnet-title { font-size:16px; font-weight:800; line-height:1.45; overflow:hidden; word-break:break-all; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; text-overflow:ellipsis; } .pk-magnet-desc { font-size:12px; line-height:1.5; opacity:0.68; } .pk-magnet-meta { display:grid; grid-template-columns:1fr 1fr 1fr; gap:8px; } .pk-magnet-meta-item { border:1px solid var(--pk-bd); border-radius:10px; padding:8px 9px; background:var(--pk-bg); min-width:0; } .pk-magnet-meta-label { font-size:11px; opacity:0.58; margin-bottom:4px; } .pk-magnet-meta-value { font-size:13px; font-weight:700; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; } .pk-magnet-hash { font-size:11px; line-height:1.45; opacity:0.62; word-break:break-all; background:var(--pk-hl); border-radius:8px; padding:8px 10px; } .pk-magnet-save-row { display:flex; align-items:center; gap:5px; min-width:0; height:26px; margin-top:-2px; font-size:12px; color:var(--pk-fg); } .pk-magnet-save-label { opacity:.72; flex-shrink:0; } .pk-magnet-save-icon { width:16px; height:16px; display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; } .pk-magnet-save-icon svg { width:16px !important; height:16px !important; display:block; } .pk-magnet-save-name { font-weight:700; max-width:150px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pk-magnet-save-help { color:#aaa; display:flex; align-items:center; flex-shrink:0; } .pk-magnet-save-change { color:var(--pk-pri); cursor:pointer; font-size:12px; font-weight:700; flex-shrink:0; } .pk-magnet-save-change:hover { text-decoration:underline; } .pk-magnet-source { font-size:11px; line-height:1.35; opacity:.58; display:flex; align-items:center; gap:4px; } .pk-magnet-source a { color:var(--pk-pri); text-decoration:none; font-weight:700; } .pk-magnet-source a:hover { text-decoration:underline; } .pk-magnet-warn { font-size:12px; line-height:1.45; color:#d93025; background:rgba(217,48,37,0.08); border:1px solid rgba(217,48,37,0.16); border-radius:8px; padding:8px 10px; } .pk-magnet-shots { display:flex; gap:7px; overflow-x:auto; padding-bottom:2px; } .pk-magnet-thumb-wrap { width:68px; height:42px; border-radius:7px; overflow:hidden; flex:0 0 auto; position:relative; border:1px solid var(--pk-bd); box-sizing:border-box; cursor:pointer; user-select:none; transition:border-color .16s ease; } .pk-magnet-thumb-wrap:hover { } .pk-magnet-thumb-wrap.active { border-color:transparent; } .pk-magnet-thumb-wrap.active::after { content:""; position:absolute; inset:0; border:2px solid var(--pk-pri); border-radius:7px; box-sizing:border-box; pointer-events:none; z-index:2; } .pk-magnet-thumb-wrap .pk-magnet-thumb { width:100%; height:100%; border:none; border-radius:0; display:block; object-fit:cover; } .pk-magnet-shot-time { position:absolute; right:3px; bottom:3px; padding:1px 4px; border-radius:4px; background:rgba(0,0,0,.62); color:#fff; font-size:10px; line-height:1.25; pointer-events:none; z-index:3; } .pk-magnet-thumb { width:68px; height:42px; border-radius:7px; object-fit:cover; flex:0 0 auto; border:1px solid var(--pk-bd); box-sizing:border-box; cursor:pointer; user-select:none; -webkit-user-drag:none; transition:border-color .16s ease, border-width .16s ease; } .pk-magnet-thumb:hover { } .pk-magnet-thumb.active { border:2px solid var(--pk-pri); } .pk-magnet-hero img { user-select:none; -webkit-user-drag:none; } .pk-magnet-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:2px; } .pk-magnet-archive-preview { width:760px; max-width:88vw; color:var(--pk-fg); display:flex; flex-direction:column; gap:14px; } .pk-magnet-archive-summary { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; } .pk-magnet-archive-card { border:1px solid var(--pk-bd); border-radius:12px; background:var(--pk-bg); padding:10px 12px; min-width:0; } .pk-magnet-archive-card b { display:block; font-size:20px; line-height:1.2; color:var(--pk-pri); } .pk-magnet-archive-card span { display:block; font-size:12px; opacity:.68; margin-top:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .pk-magnet-archive-table { border:1px solid var(--pk-bd); border-radius:12px; overflow:hidden; max-height:420px; overflow-y:auto; overflow-x:hidden; scrollbar-width:none; -ms-overflow-style:none; } .pk-magnet-archive-table::-webkit-scrollbar { width:0!important; height:0!important; display:none!important; } .pk-magnet-archive-row { display:grid; grid-template-columns:128px minmax(0,1.1fr) minmax(0,1.4fr); gap:10px; align-items:center; padding:9px 11px; border-bottom:1px solid var(--pk-bd); font-size:12px; } .pk-magnet-archive-fail-row { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.5fr); gap:10px; align-items:center; padding:9px 11px; border-bottom:1px solid var(--pk-bd); font-size:12px; } .pk-magnet-archive-row:last-child { border-bottom:none; } .pk-magnet-archive-head { background:var(--pk-hl); font-weight:800; position:sticky; top:0; z-index:1; } .pk-magnet-archive-status { display:inline-flex; align-items:center; justify-content:center; min-width:0; border-radius:999px; padding:3px 8px; font-size:11px; font-weight:800; white-space:nowrap; } .pk-magnet-archive-status.ok { background:rgba(82,196,26,.14); color:#237804; } .pk-magnet-archive-status.dup { background:rgba(250,173,20,.16); color:#b45309; } .pk-magnet-archive-status.skip { background:rgba(148,163,184,.18); color:#64748b; } .pk-magnet-archive-status.bad { background:rgba(217,48,37,.12); color:#b91c1c; } .pk-magnet-archive-title { display:flex; align-items:center; gap:8px; min-width:0; overflow:hidden; } .pk-magnet-archive-title-icon { width:24px; height:24px; flex:0 0 24px; display:inline-flex; align-items:center; justify-content:center; overflow:hidden; } .pk-magnet-archive-title-icon img { width:24px; height:24px; display:block; } .pk-magnet-archive-title-icon svg { width:24px!important; height:24px!important; display:block; flex-shrink:0; } .pk-magnet-archive-title-name { flex:1 1 auto; min-width:0; } .pk-magnet-archive-text { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; } html.pk-txt-preview-fullscreen-lock, body.pk-txt-preview-fullscreen-lock { overflow:hidden !important; } .pk-modal-ov.pk-txt-preview-ov:not(.pk-modal-over-player-webfullscreen) { position:fixed !important; inset:0 !important; display:block !important; z-index:2147483641 !important; } .pk-modal-ov.pk-txt-preview-fullscreen-ov { padding:0 !important; align-items:stretch !important; justify-content:stretch !important; } .pk-modal-ov .pk-modal.pk-txt-preview-modal { position:absolute !important; top:10% !important; left:50% !important; transform:translateX(-50%) !important; width:auto !important; padding:18px; height:80% !important; min-height:0 !important; max-height:none !important; overflow:hidden; box-sizing:border-box; min-width:0; display:flex !important; flex-direction:column; } .pk-modal-ov .pk-modal.pk-txt-preview-modal.pk-txt-preview-fullscreen { position:fixed !important; inset:0 !important; top:0 !important; left:0 !important; transform:none !important; width:100vw !important; height:100vh !important; min-height:0 !important; max-height:none !important; margin:0 !important; padding:14px !important; border-radius:0 !important; } .pk-modal-ov .pk-txt-preview-title { border:none; margin:0; padding:0 112px 0 0; font-size:18px; font-weight:700; color:var(--pk-fg); line-height:1.45; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; width:min(760px,86vw); max-width:86vw; box-sizing:border-box; flex:0 0 auto; min-height:28px; position:relative; z-index:2; } .pk-modal-ov .pk-modal.pk-txt-preview-modal.pk-txt-preview-fullscreen .pk-txt-preview-title { width:100%; max-width:100%; } .pk-modal-ov .pk-txt-preview-head-actions { position:absolute; top:15px; right:50px; display:flex; align-items:center; gap:6px; z-index:11; } .pk-modal-ov .pk-modal.pk-txt-preview-modal.pk-txt-preview-fullscreen .pk-txt-preview-head-actions { top:14px; right:50px; } .pk-modal-ov .pk-txt-preview-icon-btn { width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; border:0; border-radius:6px; background:transparent; color:#000; cursor:pointer; padding:0; transition:background .1s,color .1s,opacity .1s; box-sizing:border-box; } .pk-modal-ov.pk-dark .pk-txt-preview-icon-btn { color:#fff; } .pk-modal-ov .pk-txt-preview-icon-btn:hover:not(:disabled) { background:var(--pk-hl); color:#000; } .pk-modal-ov.pk-dark .pk-txt-preview-icon-btn:hover:not(:disabled) { color:#fff; } .pk-modal-ov .pk-txt-preview-icon-btn:disabled { opacity:1; cursor:not-allowed; pointer-events:none; } .pk-modal-ov.pk-txt-preview-ov .pk-modal-close { color:#000 !important; } .pk-modal-ov.pk-txt-preview-ov.pk-dark .pk-modal-close { color:#fff !important; } .pk-modal-ov.pk-txt-preview-ov .pk-modal-close:hover { color:#000 !important; } .pk-modal-ov.pk-txt-preview-ov.pk-dark .pk-modal-close:hover { color:#fff !important; } .pk-modal-ov .pk-txt-preview-icon-btn svg { width:16px !important; height:16px !important; display:block; pointer-events:none; } .pk-modal-ov .pk-txt-preview-link-btn svg { width:18px !important; height:18px !important; transform:scale(1.08); } .pk-modal-ov .pk-txt-preview { width:min(760px,86vw); max-width:86vw; height:auto; flex:1 1 0; min-height:0; display:flex; flex-direction:column; gap:12px; color:var(--pk-fg); overflow:hidden; min-width:0; } .pk-modal-ov .pk-modal.pk-txt-preview-modal.pk-txt-preview-fullscreen .pk-txt-preview { width:100%; max-width:none; height:auto; flex:1 1 0; min-height:0; } .pk-modal-ov .pk-txt-preview-body { flex:1 1 0; min-height:0; max-height:none; border:1px solid var(--pk-bd); border-radius:8px; background:rgba(var(--pk-bg-rgb),0.92); overflow-y:auto; overflow-x:hidden; display:block; position:relative; scrollbar-color:var(--pk-sb-th) var(--pk-sb-bg); overscroll-behavior:contain; } .pk-modal-ov .pk-txt-preview-body::-webkit-scrollbar { width:6px; height:6px; } .pk-modal-ov .pk-txt-preview-body::-webkit-scrollbar-track { background:var(--pk-sb-bg); } .pk-modal-ov .pk-txt-preview-body::-webkit-scrollbar-thumb { background:var(--pk-sb-th); border-radius:3px; } .pk-modal-ov .pk-txt-preview-body::-webkit-scrollbar-thumb:hover { background:var(--pk-sb-hov); } .pk-modal-ov .pk-txt-preview-pre { display:block; width:100%; min-width:0; min-height:100%; margin:0; padding:16px; overflow:visible; white-space:pre-wrap; word-break:break-word; overflow-wrap:anywhere; color:var(--pk-fg); background:transparent; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace; font-size:13px; line-height:1.7; box-sizing:border-box; } .pk-modal-ov .pk-txt-preview-status { min-height:100%; display:flex; align-items:center; justify-content:center; padding:24px; text-align:center; color:var(--pk-fg); opacity:.72; font-size:14px; line-height:1.7; white-space:pre-line; box-sizing:border-box; } .pk-modal-ov .pk-txt-preview-error { color:#d93025; opacity:1; } .pk-script-update-toast { position:relative;top:auto;left:auto;transform:translateY(-15px) scale(0.95);opacity:0;transition:all .3s cubic-bezier(.23,1,.32,1);max-width:80vw;pointer-events:auto;cursor:pointer;background:rgba(250,173,20,.96);color:#fff;border:1px solid rgba(250,173,20,.22);box-shadow:0 10px 26px rgba(0,0,0,.18);border-radius:999px;padding:9px 10px 9px 14px;display:flex;align-items:center;gap:10px;font-size:13px;font-weight:700;line-height:1.4; } .pk-script-update-toast.pk-dark { box-shadow:0 10px 28px rgba(0,0,0,.42); } .pk-script-update-msg { min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .pk-script-update-open { border:1px solid rgba(255,255,255,.5);background:rgba(255,255,255,.16);color:#fff;border-radius:999px;height:24px;padding:0 10px;font-size:12px;font-weight:800;cursor:pointer;white-space:nowrap; } .pk-script-update-close { width:22px;height:22px;border:0;border-radius:50%;background:rgba(255,255,255,.18);color:#fff;cursor:pointer;font-size:16px;line-height:22px;padding:0;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0; } .pk-script-version-box { position:relative;padding:18px 14px 14px 14px;border:2px solid var(--pk-bd);border-radius:8px;background:var(--pk-bg);display:flex;flex-direction:column;gap:9px;transition:border-color .2s; } .pk-script-version-grid { display:grid;grid-template-columns:auto 1fr;gap:8px 14px;align-items:center;font-size:13px; } .pk-script-version-k { color:var(--pk-fg);opacity:.72;font-weight:600;white-space:nowrap; } .pk-script-version-v { color:var(--pk-fg);font-weight:800;text-align:right;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .pk-script-version-v.new { color:var(--pk-pri);cursor:pointer; } .pk-cfg-cloud { position:relative;padding:25px 15px 15px 15px;border:2px solid var(--pk-bd);border-radius:8px;display:flex;flex-direction:column;gap:12px;transition:border-color .2s;transform:translateZ(0);overflow:visible;isolation:isolate; } .pk-cfg-cloud:hover { border-color:var(--pk-pri); } .pk-cfg-cloud > * { position:relative;z-index:1; } .pk-cfg-cloud > .pk-select-label { z-index:3;background:var(--pk-bg); } .pk-cfg-cloud-main { display:flex;align-items:flex-start;gap:12px;min-width:0; } .pk-cfg-cloud-icon { width:44px;height:44px;max-width:44px;max-height:44px;border-radius:10px;background:var(--pk-hl);color:var(--pk-pri);display:flex;align-items:center;justify-content:center;flex:0 0 44px;overflow:hidden;pointer-events:none; } .pk-cfg-cloud-icon svg { width:24px !important;height:24px !important;max-width:24px !important;max-height:24px !important;display:block;flex:0 0 24px;transform:none !important;margin:0 !important; } .pk-cfg-cloud-body { flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:8px; } .pk-cfg-cloud-statusline { display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0; } .pk-cfg-cloud-status { font-size:14px;font-weight:800;color:var(--pk-fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .pk-cfg-cloud-status[data-state="failed"], .pk-cfg-cloud-v.warn { color:#d93025; } .pk-cfg-cloud-status[data-state="synced"] { color:#13a10e; } .pk-cfg-cloud-status[data-state="local_changed"], .pk-cfg-cloud-status[data-state="remote_changed"], .pk-cfg-cloud-status[data-state="conflict"] { color:#b45309; } .pk-cfg-cloud-grid { display:grid;grid-template-columns:auto minmax(0,1fr);gap:6px 12px;font-size:12px;line-height:1.5; } .pk-cfg-cloud-k { color:var(--pk-fg);opacity:.62;font-weight:700;white-space:nowrap; } .pk-cfg-cloud-v { color:var(--pk-fg);font-weight:700;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .pk-cfg-cloud-scope { font-size:12px;line-height:1.55;color:var(--pk-fg);opacity:.68;background:var(--pk-hl);border-radius:8px;padding:8px 10px; } .pk-cfg-cloud-actions { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;align-items:stretch; } .pk-cfg-cloud-actions .pk-btn { height:38px;border-radius:6px;border:1px solid var(--pk-bd);background:var(--pk-hl);font-weight:700;justify-content:center;align-items:center;min-width:0;gap:6px;line-height:1.35;padding:0 10px; } .pk-cfg-cloud-actions .pk-btn svg { width:16px !important;height:16px !important;max-width:16px !important;max-height:16px !important;display:block;flex:0 0 16px;transform:none !important;margin:0 !important; } #btn_cfg_cloud_upload svg { width:18px !important;height:18px !important;max-width:18px !important;max-height:18px !important;flex:0 0 18px; } .pk-cfg-cloud-actions .pk-btn span { overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .pk-cfg-cloud-actions .pk-btn.danger { color:#d93025;background:transparent;border-color:#d93025; } .pk-cfg-cloud-actions .pk-btn:disabled { opacity:.62;cursor:not-allowed; } .pk-cfg-cloud-detail { display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px 14px;max-height:58vh;overflow:auto;font-size:13px;line-height:1.55;color:var(--pk-fg); } .pk-cfg-cloud-detail-k { font-weight:800;opacity:.66;white-space:nowrap; } .pk-cfg-cloud-detail-v { min-width:0;word-break:break-all;white-space:pre-wrap; } .pk-cfg-cloud-detail-v.warn { color:#d93025;font-weight:800; } .pk-cfg-cloud-detail-empty { font-size:14px;line-height:1.7;color:var(--pk-fg);opacity:.72; } @media (max-width:520px) { .pk-cfg-cloud-main { gap:10px; } .pk-cfg-cloud-icon { width:40px;height:40px;max-width:40px;max-height:40px;flex-basis:40px; } .pk-cfg-cloud-actions { grid-template-columns:1fr; } } .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: 100vw; height: 100vh; 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-ov,.pk-modal-ov,.pk-img-ov,#pk-player-ov,#pk-audio-ov,.pk-cal-pop,.pk-crumb-pop,.pk-hist-pop,.pk-tooltip,.pk-msg-toast,.pk-float-bar-item,.pk-selection-box,.pk-drag-ghost{line-height:1.5;} .pk-ov :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark),.pk-modal-ov :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark),.pk-img-ov :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark),#pk-player-ov :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark),#pk-audio-ov :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark),.pk-cal-pop :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark),.pk-crumb-pop :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark),.pk-hist-pop :where(div,span,button,input,textarea,select,label,a,p,h1,h2,h3,h4,h5,h6,li,td,th,mark){line-height:1.5;} .pk-ov :where(.pk-select-item,.pk-hist-item,.pk-ctx-item,.pk-crumb-item,.pk-menu-item,.pk-dropdown-item),.pk-modal-ov :where(.pk-select-item,.pk-hist-item,.pk-ctx-item,.pk-crumb-item,.pk-menu-item,.pk-dropdown-item),.pk-hist-pop :where(.pk-select-item,.pk-hist-item),.pk-crumb-pop :where(.pk-crumb-item,.pk-select-item){min-height:32px;line-height:1.5;display:flex;align-items:center;box-sizing:border-box;} .pk-ov :where(.pk-select-item span,.pk-hist-item span,.pk-ctx-item span,.pk-crumb-item span,.pk-menu span,#pk-rn-vp span,#pk-rn-vp mark),.pk-modal-ov :where(.pk-select-item span,.pk-hist-item span,.pk-ctx-item span,.pk-crumb-item span,.pk-menu span,#pk-rn-vp span,#pk-rn-vp mark),.pk-hist-pop :where(.pk-select-item span,.pk-hist-item span),.pk-crumb-pop :where(.pk-crumb-item span,.pk-select-item span){line-height:1.5;min-height:1.5em;box-sizing:border-box;} .pk-ov :where(mark,#pk-rn-vp span[style*="background"],.pk-row mark,.pk-modal mark),.pk-modal-ov :where(mark,#pk-rn-vp span[style*="background"]){display:inline-block;line-height:1.35;padding-top:1px;padding-bottom:1px;vertical-align:baseline;} .pk-win { width: 90%; max-width: 1600px; min-width: 720px; min-height: 340px; height: 80%; background: var(--pk-bg); color: var(--pk-fg); line-height:1.5; 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; overflow-x: hidden; box-sizing: border-box; } .pk-nav-btn { width: 44px !important; max-width: 100% !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; box-sizing: border-box !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; max-width: calc(100% - 20px) !important; height: 48px !important; padding: 0 15px !important; margin-bottom: 20px !important; box-sizing: border-box !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; min-width:0; flex:1 1 auto; overflow:visible; white-space:nowrap; } .pk-tt svg { color: #333; margin-right: 0; 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-btn.pk-btn-ghost { border:none; background:transparent; } .pk-btn.pk-btn-wide { padding:0 24px; border-radius:6px; } #pk-theme svg { transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } #pk-theme:active svg { transform: rotate(90deg) scale(0.8); } .pk-btn:hover:not(:disabled) { background: var(--pk-btn-hov) !important; } .pk-btn.pri:hover:not(:disabled) { background: var(--pk-pri) !important; filter: brightness(1.15); color: #fff !important; } .pk-copy-success-freeze, .pk-copy-success-freeze:hover, .pk-copy-success-freeze:disabled, .pk-copy-success-freeze:disabled:hover, .pk-btn.pk-copy-success-freeze, .pk-btn.pk-copy-success-freeze:hover, .pk-btn.pk-copy-success-freeze:disabled, .pk-btn.pk-copy-success-freeze:disabled:hover, #pk_err_launch_btn.pk-copy-success-freeze, #pk_err_launch_btn.pk-copy-success-freeze:hover, #pk_err_launch_btn.pk-copy-success-freeze:disabled, #pk_err_launch_btn.pk-copy-success-freeze:disabled:hover { background:#52c41a !important; color:#fff !important; filter:none !important; opacity:1 !important; cursor:default !important; pointer-events:none !important; } .pk-potfix-entry { align-self:flex-start; height:30px; padding:0 2px; border:none; background:transparent; color:var(--pk-pri); font-size:12px; font-weight:700; cursor:pointer; display:inline-flex; align-items:center; justify-content:flex-start; } .pk-potfix-entry:hover { text-decoration:underline; background:transparent !important; } .pk-potfix-desc { white-space: pre-line;line-height:1.62; font-size:12px; color:var(--pk-fg); opacity:0.72; } .pk-potfix-linkbox { width:100%; min-height:72px; max-height:110px; resize:none; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-hl); color:var(--pk-fg); padding:10px 12px; box-sizing:border-box; font-size:12px; line-height:1.45; font-family:"SF Mono","Consolas",monospace; word-break:break-all; outline:none; } .pk-potfix-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:0; } .pk-potfix-actions .pk-btn, .pk-potfix-repair-actions .pk-btn { height:40px; justify-content:center; border-radius:6px; border:1px solid var(--pk-bd); background:var(--pk-bg); color:var(--pk-fg); box-shadow:0 1px 4px rgba(0,0,0,.06); } .pk-potfix-repair-panel { border:1px solid var(--pk-bd); border-radius:6px; background:rgba(var(--pk-bg-rgb),0.76); padding:12px; display:flex; flex-direction:column; gap:10px; flex:0 0 auto; } .pk-potfix-repair-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; } .pk-potfix-secondary { grid-column:1 / -1; position:static; display:flex; justify-content:flex-start; gap:8px; flex-wrap:wrap; margin:0; min-width:0; background:transparent; padding-right:0; } .pk-potfix-secondary .pk-btn { height:30px; padding:0 12px; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); opacity:.72; font-size:12px; box-shadow:0 2px 8px rgba(0,0,0,.08); } .pk-potfix-secondary .pk-btn:hover:not(:disabled) { opacity:1; color:var(--pk-pri); border-color:var(--pk-pri); background:var(--pk-hl) !important; } .pk-potfix-footer { position:relative; display:grid; grid-template-columns:1fr minmax(220px,236px) auto; align-items:center; gap:10px 12px; margin-top:8px; padding:12px 0 2px; border-top:1px solid var(--pk-bd); } .pk-potfix-retry { grid-column:2; width:100%; height:42px !important; border:none !important; border-radius:6px !important; background:var(--pk-pri) !important; color:#fff !important; font-weight:700 !important; justify-content:center !important; box-shadow:0 2px 10px rgba(0,103,192,.28); } .pk-potfix-foot { grid-column:3; display:flex; justify-content:flex-end; min-width:56px; } .pk-potfix-version-note { font-size:12px; line-height:1.45; color:#b26a00; background:rgba(250,173,20,0.10); border:1px solid rgba(250,173,20,0.22); border-radius:8px; padding:8px 10px; } .pk-modal.pk-potfix-modal { overflow-x:hidden !important; overflow-y:auto !important; max-height:92vh !important; align-items:stretch; gap:12px !important; clip-path:inset(0 round 12px); } .pk-modal.pk-potfix-modal > * { flex-shrink:0; } .pk-potfix-adv-desc { font-size:12px; line-height:1.55; color:var(--pk-fg); opacity:.66; } .pk-potfix-path-label { font-size:12px; font-weight:700; color:var(--pk-fg); opacity:.82; } .pk-potfix-path-row { display:grid; grid-template-columns:1fr auto; gap:8px; align-items:center; } .pk-potfix-path-input { height:36px; min-width:0; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); padding:0 10px; font-size:12px; outline:none; box-sizing:border-box; } .pk-potfix-path-input:focus { border-color:var(--pk-pri); box-shadow:0 0 0 2px rgba(0,103,192,.12); } .pk-potfix-path-row .pk-btn { height:34px; border:1px solid var(--pk-bd); border-radius:6px; background:transparent; color:var(--pk-fg); font-size:12px; justify-content:center; opacity:.86; } .pk-potfix-repair-actions .pk-btn:hover:not(:disabled), .pk-potfix-path-row .pk-btn:hover:not(:disabled) { opacity:1; color:var(--pk-pri); border-color:var(--pk-pri); background:var(--pk-hl) !important; } .pk-potfix-status { font-size:12px; line-height:1.45; border-radius:6px; padding:8px 10px; background:var(--pk-hl); color:var(--pk-fg); opacity:.86; } .pk-potfix-status.warn { color:#b26a00; background:rgba(250,173,20,0.10); border:1px solid rgba(250,173,20,0.18); opacity:1; } @media (max-width:520px) { .pk-modal.pk-potfix-modal { width:calc(100vw - 32px) !important; padding:22px !important; } .pk-potfix-actions, .pk-potfix-repair-actions, .pk-potfix-path-row, .pk-potfix-footer { grid-template-columns:1fr; } .pk-potfix-secondary { grid-column:auto; padding-right:0; margin-bottom:2px; } .pk-potfix-retry, .pk-potfix-foot { grid-column:auto; } .pk-potfix-foot { justify-content:center; } } .pk-btn:disabled { opacity: 0.4; cursor: not-allowed; } .pk-btn.pri { color: var(--pk-pri); font-weight: 600; } .pk-btn svg { width: 18px; height: 18px; flex-shrink: 0; display: inline-block; vertical-align: -4px; } .pk-btn span { white-space: nowrap; pointer-events: none; } .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-global-search-locked input[type="checkbox"]{display:none!important}.pk-global-search-locked{pointer-events:none;cursor:default;opacity:1} .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-btn:hover { color: var(--pk-pri); } .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-upload-tools { min-width: 0; flex-shrink: 0 !important; } .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-search-path-con.pk-share-path-compact .pk-txt-long { display: none !important; } #pk-search-path-con.pk-share-path-compact .pk-txt-short { display: inline !important; } #pk-dup-folder-sel-wrap { position: relative; display: inline-flex; align-items: center; width: 220px; min-width: 0; flex-shrink: 0; } #pk-dup-folder-sel { position: absolute !important; inset: 0 !important; width: 0 !important; height: 0 !important; opacity: 0 !important; pointer-events: none !important; } #pk-dup-folder-btn { width: 100%; height: 30px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); padding: 0 28px 0 10px; font-size: 12px; text-align: left; cursor: pointer; position: relative; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #pk-dup-folder-btn:hover { background: var(--pk-btn-hov); border-color: var(--pk-pri); } #pk-dup-folder-btn::after { content: ""; position: absolute; right: 10px; top: 50%; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-65%) rotate(45deg); opacity: 0.7; pointer-events: none; } .pk-dup-folder-pop { position:fixed; min-width:220px; max-width:520px; max-height:320px; background:#ffffff; color:#1f1f1f; border:1px solid rgba(0,0,0,0.12); border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,0.18); z-index:2147483647; overflow:hidden; display:flex; flex-direction:column; } .pk-dup-folder-pop.pk-dark { background:rgba(28,28,28,0.98); color:#f5f5f5; border-color:rgba(255,255,255,0.12); box-shadow:0 10px 28px rgba(0,0,0,0.45); } .pk-dup-folder-head { flex:0 0 auto; padding:4px; border-bottom:1px solid rgba(0,0,0,0.08); background:#ffffff; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-head { background:rgba(28,28,28,0.98); border-bottom:1px solid rgba(255,255,255,0.10); } .pk-dup-folder-list { flex:1 1 auto; min-height:0; max-height:272px; overflow-y:auto; overflow-x:hidden; padding:4px; } .pk-dup-folder-item { width:100%; min-height:30px; border:0; background:transparent; color:inherit; border-radius:6px; padding:0 10px; font-size:12px; text-align:left; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pk-dup-folder-item:hover { background:rgba(0,0,0,0.06); } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item:hover { background:rgba(255,255,255,0.08); } .pk-dup-folder-item.act { background:rgba(26,115,232,0.12); color:#1a73e8; font-weight:600; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item.act { background:rgba(138,180,248,0.18); color:#8ab4f8; } .pk-dup-folder-item.pk-reset { position:relative; top:auto; z-index:auto; color:#1a73e8; font-weight:700; background:transparent; border-bottom:0; border-radius:6px; } .pk-dup-folder-item.pk-reset:hover { background:#f7f9fc; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item.pk-reset { color:#8ab4f8; background:transparent; border-bottom:0; } .pk-dup-folder-pop.pk-dark .pk-dup-folder-item.pk-reset:hover { background:rgba(255,255,255,0.06); } .pk-ov.pk-hide-btn-text .pk-txt-long, .pk-ov.pk-auto-hide-btn-text .pk-txt-long { display: none !important; } .pk-ov.pk-hide-btn-text .pk-txt-short, .pk-ov.pk-auto-hide-btn-text .pk-txt-short { display: inline !important; } .pk-ov.pk-hide-btn-text #pk-dup-folder-sel-wrap, .pk-ov.pk-auto-hide-btn-text #pk-dup-folder-sel-wrap { max-width: 100px !important; } .pk-ov.pk-hide-btn-text .pk-btn:not(#pk-filter-btn):not(#pk-filter-exit-btn):not(#pk-share-parse-back-list) > span, .pk-ov.pk-auto-hide-btn-text .pk-btn:not(#pk-filter-btn):not(#pk-filter-exit-btn):not(#pk-share-parse-back-list) > span, .pk-ov.pk-hide-btn-text .pk-nav-btn > span, .pk-ov.pk-auto-hide-btn-text .pk-nav-btn > span { display: none !important; } .pk-ov.pk-hide-btn-text .pk-tb .pk-btn:not(#pk-filter-btn):not(#pk-filter-exit-btn):not(#pk-share-parse-back-list), .pk-ov.pk-auto-hide-btn-text .pk-tb .pk-btn:not(#pk-filter-btn):not(#pk-filter-exit-btn):not(#pk-share-parse-back-list), .pk-ov.pk-hide-btn-text .pk-ft .pk-grp .pk-btn:not(#pk-filter-btn):not(#pk-filter-exit-btn):not(#pk-share-parse-back-list), .pk-ov.pk-auto-hide-btn-text .pk-ft .pk-grp .pk-btn:not(#pk-filter-btn):not(#pk-filter-exit-btn):not(#pk-share-parse-back-list) { padding:0 8px !important; min-width:32px; width:32px; justify-content:center; gap:0 !important; } .pk-ov.pk-hide-btn-text #pk-share-parse-back-list > span, .pk-ov.pk-auto-hide-btn-text #pk-share-parse-back-list > span { display: none !important; } .pk-ov.pk-hide-btn-text #pk-share-parse-back-list, .pk-ov.pk-auto-hide-btn-text #pk-share-parse-back-list { padding: 0 8px !important; min-width: 32px; justify-content: center; gap: 0 !important; } .pk-ov.pk-hide-btn-text #pk-empty-trash, .pk-ov.pk-auto-hide-btn-text #pk-empty-trash { width:32px !important; min-width:32px !important; height:32px !important; padding:0 !important; justify-content:center !important; gap:0 !important; } .pk-ov.pk-hide-btn-text .pk-link-bookmark-mode .pk-lbm-tools .pk-lbm-btn > span, .pk-ov.pk-auto-hide-btn-text .pk-link-bookmark-mode .pk-lbm-tools .pk-lbm-btn > span, .pk-ov.pk-hide-btn-text .pk-link-bookmark-mode .pk-lbm-card-actions .pk-lbm-card-act > span, .pk-ov.pk-auto-hide-btn-text .pk-link-bookmark-mode .pk-lbm-card-actions .pk-lbm-card-act > span { display:none !important; } .pk-ov.pk-hide-btn-text .pk-link-bookmark-mode .pk-lbm-tools .pk-lbm-btn, .pk-ov.pk-auto-hide-btn-text .pk-link-bookmark-mode .pk-lbm-tools .pk-lbm-btn { min-width:34px; padding:0 8px !important; gap:0 !important; } .pk-ov.pk-hide-btn-text .pk-link-bookmark-mode .pk-lbm-card-actions .pk-lbm-card-act, .pk-ov.pk-auto-hide-btn-text .pk-link-bookmark-mode .pk-lbm-card-actions .pk-lbm-card-act { min-width:24px; padding:0 4px !important; gap:0 !important; justify-content:center !important; } .pk-ov.pk-hide-btn-text #pk-btn-upload, .pk-ov.pk-auto-hide-btn-text #pk-btn-upload { width:32px !important; min-width:32px !important; height:32px !important; padding:0 !important; gap:0 !important; } .pk-ov.pk-hide-btn-text #pk-btn-upload .pk-btn-arrow, .pk-ov.pk-auto-hide-btn-text #pk-btn-upload .pk-btn-arrow { display:none !important; } .pk-ov.pk-hide-btn-text .pk-btn-arrow, .pk-ov.pk-auto-hide-btn-text .pk-btn-arrow { margin-left: 0 !important; } .pk-ov.pk-hide-btn-text .pk-maximized .pk-sidebar, .pk-ov.pk-auto-hide-btn-text .pk-maximized .pk-sidebar { width: 68px !important; align-items: center !important; padding: 16px 0 !important; } .pk-ov.pk-hide-btn-text .pk-maximized .pk-nav-btn, .pk-ov.pk-auto-hide-btn-text .pk-maximized .pk-nav-btn { width: 44px !important; justify-content: center !important; padding: 0 !important; gap: 0 !important; height: 44px !important; border-radius: 10px !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-btn-cloud, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-btn-cloud { border-radius: 50% !important; width: 44px !important; height: 44px !important; padding: 0 !important; margin-bottom: 12px !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-btn-cloud svg, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-btn-cloud svg { margin-right: 0 !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-quota-wrap, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-quota-wrap { align-items: center !important; padding: 0 !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-quota-panel, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-quota-panel { align-items: center !important; margin-bottom: 2px !important; opacity: 0.6 !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-quota-head, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-quota-head { flex-direction: column !important; align-items: center !important; justify-content: flex-end !important; gap: 2px !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-transfer-detail-btn, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-transfer-detail-btn { order: 1 !important; align-items: center !important; margin: 0 0 2px 0 !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-quota-txt, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-quota-txt { order: 2 !important; font-size: 10px !important; font-weight: 700 !important; transform: scale(0.8) !important; line-height: 1 !important; } .pk-ov.pk-hide-btn-text .pk-maximized #pk-quota-bar-box, .pk-ov.pk-auto-hide-btn-text .pk-maximized #pk-quota-bar-box { width: 44px !important; height: 4px !important; border-radius: 2px !important; } .pk-btn-toggle { border: 1px solid var(--pk-bd); background: var(--pk-bg); color: var(--pk-fg); height: 30px; border-radius: 4px; padding: 0 10px; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; flex-shrink: 0; } .pk-btn-toggle:hover { background: var(--pk-btn-hov); border-color: var(--pk-pri); } .pk-btn-toggle span { font-weight: 700; color: var(--pk-pri); } #pk-dup-exit { flex-shrink: 0; } .pk-nav { display: flex !important; align-items: center !important; gap: 4px; overflow: hidden; white-space: nowrap; font-size: 13px; color: #666; margin: 0 8px; height: 100%; max-width: 60%; } .pk-nav span { cursor: pointer; padding: 2px 6px; border-radius: 4px; } .pk-nav span:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-nav span.act { font-weight: 600; color: var(--pk-fg); cursor: pointer; } .pk-grid-hd, .pk-row { display: grid; column-gap: 10px; align-items: center; font-size: 14px; color: var(--pk-fg); box-sizing: border-box; width: 100%; } .pk-grid-hd > div, .pk-row > div { display: flex; align-items: center; justify-content: flex-start !important; overflow: hidden; white-space: nowrap; text-align: left; } .pk-grid-hd > div:first-child, .pk-row > div:first-child { justify-content: center !important; overflow: visible !important; } .pk-grid-hd { height: 36px; border-bottom: 1px solid var(--pk-bd); font-size: 13px; color: #666; user-select: none; padding: 0 22px 0 16px; } .pk-row { padding: 0 16px; } .pk-col { cursor: pointer; font-weight: 600; display: flex; align-items: center; justify-content: flex-start; } .pk-col:hover { color: var(--pk-fg); } .pk-view-switch { display: inline-flex; align-items: center; gap: 4px; margin-left: 12px; padding: 3px; border: 1px solid var(--pk-bd); border-radius: 999px; background: var(--pk-bg); flex-shrink: 0; } .pk-view-switch[style*="display: none"] { margin-left: 0; } .pk-view-btn { width: 34px; height: 28px; border: none; border-radius: 999px; background: transparent; color: #777; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s,color 0.2s,transform 0.2s; padding: 0; } .pk-view-btn:hover { color: var(--pk-fg); background: var(--pk-hl); } .pk-view-btn.active { background: var(--pk-pri); color: #fff; box-shadow: 0 3px 10px rgba(0,103,192,0.22); } .pk-mode-view-syncing .pk-view-btn { transition: none !important; } .pk-share-grid-exit-sync .pk-grid-hd,.pk-share-grid-exit-sync .pk-vp{visibility:hidden!important;pointer-events:none!important;} .pk-link-bookmark-grid-exit-sync .pk-grid-hd,.pk-link-bookmark-grid-exit-sync .pk-vp{visibility:hidden!important;pointer-events:none!important;} .pk-grid-hd.pk-grid-view-hd { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 18px 0 16px; overflow: visible; position: relative; z-index: 30; } .pk-grid-hd.pk-grid-view-hd > div { overflow: visible; min-width: 0; } .pk-grid-hd.pk-grid-view-hd > div:first-child { width: auto; flex: 0 1 auto; } .pk-grid-check-tools { display:flex; align-items:center; gap:10px; min-width:0; flex:0 1 auto; overflow:visible !important; padding-left:9px; box-sizing:border-box; } .pk-grid-check-tools > input[type="checkbox"] { flex:0 0 auto; margin:0; } .pk-grid-check-tools > #pk-grid-folder-first { flex:0 0 auto; } .pk-grid-check-tools { display:flex; align-items:center; gap:8px; min-width:0; overflow:visible !important; padding-left:9px; box-sizing:border-box; } .pk-grid-sort-wrap { position: relative; display: inline-flex; align-items: center; flex: 0 0 auto; flex-shrink: 0; overflow: visible !important; z-index: 2; } .pk-grid-sort-trigger { height: 30px; border: 1px solid var(--pk-bd); border-radius: 999px; background: var(--pk-bg); color: var(--pk-fg); padding: 0 12px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer; font-size: 12px; font-weight: 600; transition: border-color 0.2s,background 0.2s,color 0.2s; max-width: 100%; } .pk-grid-sort-trigger:hover, .pk-grid-sort-wrap.open .pk-grid-sort-trigger { border-color: var(--pk-pri); color: var(--pk-pri); background: var(--pk-hl); } #pk-btn-invert:hover,#pk-grid-invert:hover{color:var(--pk-pri)!important;} .pk-grid-sort-trigger svg { flex-shrink: 0; } .pk-grid-sort-trigger span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .pk-grid-sort-menu { position: absolute; top: calc(100% + 8px); right: 0; min-width: 156px; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 12px; box-shadow: 0 12px 36px rgba(0,0,0,0.16); padding: 6px; display: none; z-index: 10020; } .pk-grid-sort-wrap.open .pk-grid-sort-menu { display: block; } .pk-grid-sort-opt { border-radius: 10px; padding: 9px 12px; display: flex; align-items: center; gap: 10px; color: var(--pk-fg); cursor: pointer; transition: background 0.15s,color 0.15s; font-size: 13px; font-weight: 500; white-space: nowrap; } .pk-grid-sort-opt:hover, .pk-grid-sort-opt.active { background: var(--pk-hl); color: var(--pk-pri); } .pk-grid-view .pk-vp { padding: 14px 0 18px; } .pk-grid-view .pk-in { left: 0; right: 0; } .pk-view-switching .pk-row, .pk-view-switching .pk-row::before, .pk-view-switching .pk-row::after, .pk-view-switching .pk-row *, .pk-grid-resizing .pk-row, .pk-grid-resizing .pk-row::before, .pk-grid-resizing .pk-row::after, .pk-grid-resizing .pk-row *, .pk-grid-scrolling .pk-row, .pk-grid-scrolling .pk-row::before, .pk-grid-scrolling .pk-row::after, .pk-grid-scrolling .pk-row * { transition: none !important; animation: none !important; } .pk-view-switching.pk-grid-view .pk-row, .pk-view-switching.pk-grid-view .pk-row:hover, .pk-view-switching.pk-grid-view .pk-row.sel, .pk-view-switching.pk-grid-view .pk-row.sel.pk-focused, .pk-dark .pk-view-switching.pk-grid-view .pk-row, .pk-dark .pk-view-switching.pk-grid-view .pk-row:hover, .pk-dark .pk-view-switching.pk-grid-view .pk-row.sel, .pk-dark .pk-view-switching.pk-grid-view .pk-row.sel.pk-focused, .pk-grid-resizing.pk-grid-view .pk-row, .pk-grid-resizing.pk-grid-view .pk-row:hover, .pk-grid-resizing.pk-grid-view .pk-row.sel, .pk-grid-resizing.pk-grid-view .pk-row.sel.pk-focused, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row:hover, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row.sel, .pk-dark .pk-grid-resizing.pk-grid-view .pk-row.sel.pk-focused { border-color: transparent !important; box-shadow: none !important; outline: none !important; } .pk-grid-view .pk-row { box-sizing: border-box; border-radius: 16px; border: 1px solid transparent; background: #e3e9f2; box-shadow: none; overflow: hidden; transition: background 0.2s; } .pk-grid-view .pk-row:hover { background: #d9e1ec; transform: none; box-shadow: none; } .pk-grid-view .pk-row.sel { background: #d4deeb; border-color: rgba(0,103,192,0.15); box-shadow: 0 0 0 1px rgba(0,103,192,0.15); } .pk-grid-view .pk-row.sel.pk-focused { border-color: var(--pk-pri); box-shadow: 0 0 0 1px var(--pk-pri); } .pk-grid-scrolling.pk-grid-view .pk-row.pk-focused:not(.sel), .pk-dark .pk-grid-scrolling.pk-grid-view .pk-row.pk-focused:not(.sel) { background: var(--pk-sel-bg) !important; border-color: var(--pk-pri) !important; box-shadow: none !important; outline: none !important; } .pk-grid-card-body { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; box-sizing: border-box; padding: 10px; gap: 10px; } .pk-gv-check { position: absolute; top: 12px; left: 12px; width: 24px; height: 24px; background: #fff; border-radius: 6px; z-index: 4; display: flex; align-items: center; justify-content: center; cursor: pointer; box-sizing: border-box; opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(-2px); transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease; } .pk-gv-check::before { content: ''; position: absolute; left: 50%; top: 50%; width: 18px; height: 18px; margin-left: -9px; margin-top: -9px; border-radius: 50%; border: 1.5px solid #c7cdd6; background: transparent; box-sizing: border-box; transition: all 0.15s; } .pk-gv-check::after { content: ''; position: absolute; left: 50%; top: 50%; width: 4px; height: 8px; margin-left: -2px; margin-top: -5px; border: solid #fff; border-width: 0 1.5px 1.5px 0; opacity: 0; transform: rotate(45deg) scale(0.8); transform-origin: center; transition: opacity 0.15s, transform 0.15s; box-sizing: border-box; } .pk-grid-view .pk-row:hover .pk-gv-check, .pk-grid-view .pk-row.sel .pk-gv-check, .pk-grid-view .pk-row:focus-within .pk-gv-check { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); } .pk-grid-view .pk-row:hover .pk-gv-check::before { border-color: #818c9b; } .pk-grid-view .pk-row.sel .pk-gv-check::before { background: var(--pk-pri); border-color: var(--pk-pri); } .pk-grid-view .pk-row.sel .pk-gv-check::after { opacity: 1; transform: rotate(45deg) scale(1); } .pk-grid-view .pk-row input[type="checkbox"] { position: absolute; inset: 0; width: 100% !important; height: 100% !important; margin: 0 !important; opacity: 0; cursor: pointer; z-index: 2; } .pk-gv-more { position: absolute; top: 12px; right: 12px; width: 24px; height: 24px; border: none; border-radius: 6px; background: #fff; color: #8a94a4; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; z-index: 5; padding: 0; opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(-2px); transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease, color 0.2s; } .pk-grid-view .pk-row:hover .pk-gv-more, .pk-grid-view .pk-row:focus-within .pk-gv-more, .pk-grid-view .pk-row.sel.pk-focused .pk-gv-more, .pk-grid-view .pk-row.pk-sel-single .pk-gv-more { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); } .pk-gv-more:hover { color: var(--pk-pri); } .pk-gv-more svg { width: 16px; height: 16px; flex-shrink: 0; } .pk-gv-cover { position: relative; height: calc(100% - 56px); min-height: 140px; background: transparent; display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 12px; } .pk-gv-cover:not(.pk-gv-file) { align-items: flex-end; padding-bottom: 8px; box-sizing: border-box; } .pk-gv-cover.pk-gv-file { width: auto; max-width: 100%; aspect-ratio: 1 / 1; align-self: center; border-radius: 18px; flex: 0 0 auto; isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount { width: 100%; height: 100%; display: block; position: relative; isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount > div { width: 100%; height: 100%; position: relative; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount .pk-max-thumb { width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important; object-fit: cover !important; border-radius: 0 !important; transform: translateZ(0); transform-origin: center center; backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform, opacity; } .pk-gv-cover.pk-gv-has-thumb { background: transparent; } .pk-gv-cover img { max-width: 100%; max-height: 100%; object-fit: cover; display: block; border-radius: 12px; } .pk-gv-cover.pk-gv-has-thumb > img { width: 100%; height: 100%; } .pk-gv-cover.pk-gv-blur > img, .pk-gv-cover.pk-gv-blur .pk-gv-media-mount [data-pk-thumb-ready="1"] > .pk-max-thumb, .pk-gv-cover.pk-gv-blur .pk-gv-media-mount .pk-max-thumb[data-pk-frozen-cover="1"], .pk-gv-cover.pk-gv-blur .pk-gv-folder-preview img[data-pk-id][data-pk-src], .pk-gv-cover.pk-gv-blur .pk-gv-folder-preview img[data-pk-frozen-cover="1"] { filter: blur(8px); transform: scale(1.04); } .pk-gv-cover.pk-gv-file .pk-gv-media-mount { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } .pk-gv-cover.pk-gv-file .pk-gv-media-mount > div { width: 100%; height: 100%; } .pk-gv-cover .pk-gv-icon { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } .pk-gv-cover .pk-gv-icon svg, .pk-gv-cover .pk-gv-icon img { width: 108px !important; height: 108px !important; max-width: 108px !important; max-height: 108px !important; object-fit: contain !important; } .pk-gv-cover .pk-gv-icon img { border-radius: 10px; } .pk-gv-folder-preview.pk-gv-folder-has-thumb { background: transparent; isolation: isolate; transform: translateZ(0); } .pk-gv-folder-preview.pk-gv-folder-has-thumb img { width: 100%; height: 100%; display: block; object-fit: cover; border-radius: 0; transform: translateZ(0) scale(1.02); transform-origin: center center; backface-visibility: hidden; will-change: transform; } .pk-gv-folder-shell { position: relative; width: 176px; height: 138px; margin: 12px auto 0; display: flex; align-items: flex-end; justify-content: center; transform: translateY(12px); } .pk-gv-folder-back { position: absolute; top: 19px; left: 0; right: 0; bottom: 0; background: #fbc645; border-radius: 12px; } .pk-gv-folder-tab { position: absolute; top: 6px; left: 15px; width: 62px; height: 23px; background: #fbc645; border-radius: 9px 9px 0 0; } .pk-gv-folder-preview { position: absolute; top: 0; left: 15px; right: 15px; bottom: 23px; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #fff7e6; z-index: 1; display: flex; align-items: center; justify-content: center; isolation: isolate; transform: translateZ(0); } .pk-gv-folder-preview { position: absolute; top: 0; left: 15px; right: 15px; bottom: 23px; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #fff7e6; z-index: 1; display: flex; align-items: center; justify-content: center; isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-folder-fallback { width: 102px !important; height: 102px !important; max-width: 102px !important; max-height: 102px !important; object-fit: contain !important; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; } .pk-gv-folder-front { position: absolute; left: -1px; right: -1px; bottom: -1px; height: 86px; background: #f9b126; border-radius: 12px; z-index: 2; box-shadow: 0 -2px 12px rgba(249, 177, 38, 0.2); transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; } .pk-gv-folder-shell { isolation: isolate; transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; } .pk-gv-folder-back, .pk-gv-folder-tab { transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; } .pk-gv-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 48px; height: 48px; border-radius: 50%; background: rgba(0,0,0,0.4); display: inline-flex; align-items: center; justify-content: center; pointer-events: none; z-index: 3; } .pk-gv-play svg { width: 28px !important; height: 28px !important; color: #fff; margin-left: 3px; } .pk-gv-fav { position: absolute; right: 14px; bottom: 14px; width: 24px; height: 24px; border-radius: 50%; background: rgba(255,255,255,0.95); color: #f4b400; display: grid; place-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.1); pointer-events: none; z-index: 3; line-height: 0; } .pk-dark .pk-gv-fav { background: rgba(39,45,54,0.96); box-shadow: 0 6px 14px rgba(0,0,0,0.24); } .pk-gv-fav .pk-star-icon { width: 14px !important; height: 14px !important; display: block !important; margin: 0 !important; padding: 0 !important; transform: none !important; transform-origin: center center !important; } .pk-gv-fav .pk-star-icon svg { width: 14px !important; height: 14px !important; display: block !important; margin: 0 !important; padding: 0 !important; transform: none !important; } .pk-gv-bl { position: absolute; left: 14px; bottom: 14px; width: 24px; height: 24px; border-radius: 50%; background: rgba(255,255,255,0.95); display: grid; place-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.1); pointer-events: none; z-index: 3; line-height: 0; } .pk-gv-bl svg { width: 14px !important; height: 14px !important; display: block !important; margin: 0 !important; padding: 0 !important; transform: none !important; transform-origin: center center !important; } .pk-dark .pk-gv-bl { background: rgba(39,45,54,0.96); box-shadow: 0 6px 14px rgba(0,0,0,0.24); } .pk-gv-info { display:grid; grid-template-rows:20px 16px; row-gap:2px; align-content:end; align-self:stretch; width:100%; min-width:0; padding:0 2px; height:38px; min-height:38px; box-sizing:border-box; overflow:hidden; text-align:left; } .pk-grid-view .pk-name { display:flex; align-items:center; justify-content:flex-start; align-self:stretch; width:100%; min-width:0; height:20px; overflow:hidden; text-align:left; } .pk-grid-view .pk-name .pk-name-main { display:inline-flex; align-items:center; min-width:0; max-width:100%; overflow:hidden; } .pk-grid-view .pk-name .pk-name-txt { display:block; flex:0 1 auto; width:auto; min-width:0; max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; line-height:20px; font-size:14px; font-weight:500; color:#1a1a1a; word-break:normal; text-align:left; } .pk-grid-view .pk-name .pk-tag-default { flex:0 0 auto; align-self:center; margin-left:6px; margin-top:0 !important; margin-bottom:0 !important; padding:0 6px; min-width:auto; height:16px; border-radius:10px; font-size:11px; line-height:1 !important; white-space:nowrap; display:inline-flex !important; align-items:center !important; justify-content:center !important; text-align:center; box-sizing:border-box; vertical-align:middle; transform:none !important; position:relative; top:-2px; } .pk-gv-meta { display:flex; align-items:center; justify-content:flex-start; align-self:stretch; width:100%; min-width:0; height:16px; gap:0; color:#1a1a1a; font-size:12px; line-height:16px; overflow:hidden; text-align:left; } .pk-gv-meta-item { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:0 1 auto; text-align:left; } .pk-gv-date-wrap { display:flex; align-items:center; min-width:0; flex:0 1 auto; overflow:hidden; gap:0; } .pk-gv-date { flex:0 1 auto; min-width:0; text-align:left; } .pk-gv-size { flex:0 0 auto; max-width:40%; text-align:left; } .pk-gv-dur { flex:0 0 auto; max-width:34%; text-align:left; } .pk-gv-fcount { flex:0 0 auto; max-width:34%; text-align:left; } .pk-gv-meta-sep { display:block; width:3px; height:3px; margin:0 6px; border-radius:50%; background:currentColor; opacity:0.5; flex:0 0 auto; align-self:center; } .pk-gv-date, .pk-gv-size, .pk-gv-dur, .pk-gv-fcount { color: inherit; opacity: 1; } .pk-history-grid-meta .pk-history-grid-pct { flex:0 0 auto; max-width:28%; } .pk-history-grid-progress { position:absolute; left:0; right:0; bottom:0; height:4px; background:rgba(0,0,0,0.16); overflow:hidden; pointer-events:none; z-index:4; } .pk-history-grid-progress-bar { height:100%; width:0; background:var(--pk-pri); } .pk-dark .pk-history-grid-progress { background:rgba(255,255,255,0.22); } .pk-dark .pk-grid-view .pk-row { background: rgba(39,45,54,0.96); border-color: transparent; box-shadow: 0 12px 26px rgba(0,0,0,0.22); outline: none; background-clip: padding-box; } .pk-dark .pk-grid-view .pk-row:hover { background: rgba(45,52,62,0.98); border-color: transparent; outline: none; } .pk-dark .pk-grid-view .pk-row.sel { background: rgba(34,60,96,0.9); border-color: rgba(92,169,255,0.42); box-shadow: 0 12px 28px rgba(18,44,82,0.32); outline: none; background-clip: padding-box; } .pk-dark .pk-grid-view .pk-row.sel:not(.pk-sel-single), .pk-dark .pk-grid-view .pk-row.sel:not(.pk-sel-single):hover, .pk-dark .pk-grid-view .pk-row.sel:not(.pk-sel-single).pk-focused { transition: none !important; animation: none !important; } .pk-dark .pk-grid-view .pk-row.pk-sel-single, .pk-dark .pk-grid-view .pk-row.pk-sel-single:hover, .pk-dark .pk-grid-view .pk-row.pk-sel-single.pk-focused { transition: none !important; animation: none !important; } .pk-dark .pk-gv-check::before { background: rgba(37,42,50,0.92); border-color: rgba(124,137,156,0.9); } .pk-dark .pk-gv-more { background: rgba(34,40,48,0.94); color: #c0c7d2; box-shadow: 0 6px 14px rgba(0,0,0,0.24); } .pk-dark .pk-gv-more:hover { background: rgba(46,54,64,0.98); color: #fff; } .pk-dark .pk-gv-cover.pk-gv-file { background: transparent; } .pk-dark .pk-gv-cover.pk-gv-file.pk-gv-has-thumb:has(.pk-gv-media-mount > [data-pk-thumb-ready="1"]) { background: linear-gradient(180deg,#313844 0%,#2a313b 100%); } .pk-dark .pk-gv-cover.pk-gv-folder { background: transparent; } .pk-dark .pk-gv-folder-preview { background: #fff7e6; } .pk-dark .pk-grid-view .pk-name .pk-name-txt, .pk-dark .pk-gv-meta, .pk-dark .pk-gv-meta-item, .pk-dark .pk-gv-date, .pk-dark .pk-gv-size { color:#fff; } .pk-modal::-webkit-scrollbar, .pk-vp::-webkit-scrollbar, .pk-prev-list::-webkit-scrollbar, .pk-scroll::-webkit-scrollbar, #pk-rn-vp::-webkit-scrollbar, .pk-bl-area::-webkit-scrollbar, textarea::-webkit-scrollbar, .pk-sub-pane::-webkit-scrollbar, #pk_sub_search_list::-webkit-scrollbar, .pk-p-pop::-webkit-scrollbar, .pk-share-modal::-webkit-scrollbar { width: 6px; height: 6px; } .pk-no-scrollbar::-webkit-scrollbar { display: none !important; } .pk-no-scrollbar { -ms-overflow-style: none !important; scrollbar-width: none !important; } .pk-vp::-webkit-scrollbar-track, .pk-modal::-webkit-scrollbar-track, .pk-prev-list::-webkit-scrollbar-track, .pk-scroll::-webkit-scrollbar-track, #pk-rn-vp::-webkit-scrollbar-track, .pk-bl-area::-webkit-scrollbar-track, textarea::-webkit-scrollbar-track, .pk-sub-pane::-webkit-scrollbar-track, #pk_sub_search_list::-webkit-scrollbar-track, .pk-p-pop::-webkit-scrollbar-track { background: var(--pk-sb-bg); } .pk-vp::-webkit-scrollbar-thumb, .pk-modal::-webkit-scrollbar-thumb, .pk-prev-list::-webkit-scrollbar-thumb, .pk-scroll::-webkit-scrollbar-thumb, #pk-rn-vp::-webkit-scrollbar-thumb, .pk-bl-area::-webkit-scrollbar-thumb, textarea::-webkit-scrollbar-thumb, .pk-sub-pane::-webkit-scrollbar-thumb, #pk_sub_search_list::-webkit-scrollbar-thumb, .pk-p-pop::-webkit-scrollbar-thumb { background: var(--pk-sb-th); border-radius: 3px; } .pk-vp::-webkit-scrollbar-thumb:hover, .pk-modal::-webkit-scrollbar-thumb:hover, .pk-prev-list::-webkit-scrollbar-thumb:hover, .pk-scroll::-webkit-scrollbar-thumb:hover { background: var(--pk-sb-hov); } ::-webkit-scrollbar { cursor: default; } .pk-vp { flex: 1; overflow-y: auto; position: relative; background: var(--pk-bg); scrollbar-gutter: stable; } .pk-in { position: absolute; width: 100%; top: 0; } .pk-link-bookmark-mode .pk-grid-hd { display:none !important; } .pk-link-bookmark-mode .pk-ft { display:none !important; } .pk-link-bookmark-mode .pk-vp { padding:0 !important; overflow:hidden; } .pk-link-bookmark-mode .pk-selection-box { display:none !important; } .pk-link-bookmark-mode .pk-drag-mask { display:none !important; } .pk-link-bookmark-mode .pk-lbm-link-icon,.pk-link-bookmark-mode .pk-lbm-link-icon * { -webkit-user-drag:none; user-drag:none; } .pk-link-bookmark-mode .pk-in { position:absolute !important; inset:0; height:100% !important; transform:none !important; } .pk-lbm { height:100%; display:grid; grid-template-columns:260px minmax(0,1fr); gap:12px; padding:12px; box-sizing:border-box; background:linear-gradient(135deg,rgba(var(--pk-bg-rgb),1),rgba(0,103,192,.035)); overflow:hidden; } .pk-lbm-panel { min-width:0; min-height:0; border:1px solid var(--pk-bd); border-radius:8px; background:rgba(var(--pk-bg-rgb),.78); display:flex; flex-direction:column; overflow:hidden; box-shadow:0 8px 24px rgba(0,0,0,.06); } .pk-lbm-side { padding:20px 14px 16px; gap:12px; } .pk-lbm-side-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; } .pk-lbm-side-title { font-size:16px; font-weight:800; color:var(--pk-fg); } .pk-lbm-side-add { border:0; background:transparent; color:var(--pk-pri); height:30px; padding:0 4px; display:inline-flex; align-items:center; gap:6px; font-size:13px; font-weight:700; cursor:default; white-space:nowrap; } .pk-lbm-side-add svg { width:16px !important; height:16px !important; flex-shrink:0; } .pk-lbm-folder-list { display:flex; flex-direction:column; gap:10px; min-height:0; flex:1 1 auto; overflow:hidden; padding-right:2px; } .pk-link-bookmark-mode .pk-lbm-pager { flex:0 0 auto; min-height:32px; display:flex; align-items:center; justify-content:center; gap:8px; padding:8px 2px 0; border-top:1px solid var(--pk-bd); color:var(--pk-fg); box-sizing:border-box; } .pk-link-bookmark-mode .pk-lbm-pager[hidden] { display:none !important; } .pk-link-bookmark-mode .pk-lbm-folder-pager,.pk-link-bookmark-mode .pk-lbm-link-pager { justify-content:space-between; padding:8px 2px 0; } .pk-link-bookmark-mode .pk-lbm-pager-nav { display:inline-flex; align-items:center; gap:6px; } .pk-link-bookmark-mode .pk-lbm-pager-btn { width:28px; height:28px; border:1px solid var(--pk-bd); border-radius:6px; background:rgba(var(--pk-bg-rgb),.68); color:var(--pk-fg); display:inline-flex; align-items:center; justify-content:center; padding:0; cursor:pointer; } .pk-link-bookmark-mode .pk-lbm-pager-btn:hover:not(:disabled) { background:var(--pk-hl); color:var(--pk-pri); } .pk-link-bookmark-mode .pk-lbm-pager-btn:disabled { opacity:.42; cursor:not-allowed; } .pk-link-bookmark-mode .pk-lbm-pager-btn svg { width:14px !important; height:14px !important; display:block; } .pk-link-bookmark-mode .pk-lbm-pager-prev svg { transform:rotate(180deg); } .pk-link-bookmark-mode .pk-lbm-pager-info { min-width:48px; height:28px; border:0; background:transparent; color:var(--pk-fg); text-align:center; font-size:12px; font-weight:800; font-variant-numeric:tabular-nums; opacity:.86; white-space:nowrap; cursor:pointer; padding:0 4px; } .pk-link-bookmark-mode .pk-lbm-pager-info:hover { color:var(--pk-pri); opacity:1; } .pk-link-bookmark-mode .pk-lbm-pager-jump { min-width:48px; height:28px; display:inline-flex; align-items:center; justify-content:center; gap:4px; color:var(--pk-fg); font-size:12px; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap; } .pk-link-bookmark-mode .pk-lbm-pager-jump input { width:32px; height:22px; border:1px solid var(--pk-pri); border-radius:5px; background:rgba(var(--pk-bg-rgb),.86); color:var(--pk-fg); text-align:center; outline:0; padding:0 3px; font:inherit; box-sizing:border-box; } .pk-link-bookmark-mode .pk-lbm-pager-total { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; font-weight:750; opacity:.68; font-variant-numeric:tabular-nums; } .pk-lbm-folder { min-height:56px; border:1px solid var(--pk-bd); border-radius:8px; padding:0 10px 0 12px; display:grid; grid-template-columns:28px minmax(0,1fr) auto auto; align-items:center; gap:10px; background:rgba(var(--pk-bg-rgb),.68); color:var(--pk-fg); box-sizing:border-box; text-align:left; cursor:pointer; font:inherit; } .pk-lbm-folder.active { border-color:var(--pk-pri); background:var(--pk-sel-bg); } .pk-lbm-folder-ico { width:24px; height:24px; display:flex; align-items:center; justify-content:center; color:var(--pk-pri); overflow:hidden; flex-shrink:0; } .pk-lbm-folder-ico svg { width:24px !important; height:24px !important; display:block; } .pk-lbm-folder-name { min-width:0; max-width:100%; font-size:14px; font-weight:650; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block; line-height:20px; padding-right:4px; box-sizing:border-box; } .pk-lbm-folder-main { min-width:0; max-width:100%; display:flex; align-items:center; gap:6px; overflow:hidden; } .pk-lbm-folder-count { font-size:13px; opacity:.86; font-variant-numeric:tabular-nums; flex-shrink:0; white-space:nowrap; } .pk-lbm-folder-actions { display:flex; align-items:center; gap:4px; opacity:.72; flex-shrink:0; } .pk-lbm-folder:hover .pk-lbm-folder-actions,.pk-lbm-folder.active .pk-lbm-folder-actions { opacity:1; } .pk-lbm-folder-act { width:24px; height:24px; border:0; border-radius:5px; background:transparent; color:var(--pk-fg); display:inline-flex; align-items:center; justify-content:center; padding:0; cursor:pointer; opacity:.72; } .pk-lbm-folder-act:hover { background:var(--pk-hl); opacity:1; } .pk-lbm-folder-act.danger { color:#d93025; } .pk-lbm-folder-act svg { width:13px !important; height:13px !important; flex-shrink:0; } .pk-lbm-badge { height:22px; padding:0 9px; border-radius:999px; background:rgba(0,103,192,.14); color:var(--pk-pri); display:inline-flex; align-items:center; justify-content:center; font-size:12px; font-weight:800; flex-shrink:0; } .pk-lbm-badge.dup { background:rgba(245,158,11,.16); color:#b45309; } .pk-lbm-sync-tip { margin-top:10px; color:var(--pk-fg); opacity:.68; display:flex; align-items:flex-start; gap:7px; font-size:12px; line-height:1.5; } .pk-lbm-main { padding:16px; gap:14px; } .pk-lbm-toolbar { display:flex; align-items:center; gap:10px; flex:0 0 auto; min-width:0; } .pk-lbm-search { width:min(330px,32%); min-width:220px; height:36px; border:1px solid var(--pk-bd); border-radius:6px; display:flex; align-items:center; gap:6px; padding:0 8px 0 12px; color:var(--pk-fg); background:rgba(var(--pk-bg-rgb),.72); box-sizing:border-box; } .pk-lbm-search:focus-within { border-color:var(--pk-pri); box-shadow:none; } .pk-lbm-search input { border:0; outline:0; background:transparent; color:var(--pk-fg); min-width:0; flex:1; font-size:13px; pointer-events:auto; cursor:text; } .pk-lbm-search-act { width:22px; height:22px; border:0; border-radius:999px; background:transparent; color:#999; opacity:1; display:inline-flex; align-items:center; justify-content:center; padding:0; flex:0 0 auto; cursor:pointer; transition:background-color .16s ease,opacity .16s ease,transform .16s ease,color .16s ease; } .pk-lbm-search-act:hover { opacity:1; color:#999; background:transparent; } .pk-lbm-search-act:active { transform:scale(.92); background:transparent; } .pk-lbm-search-act svg { width:16px !important; height:16px !important; opacity:1; flex-shrink:0; color:inherit !important; } .pk-lbm-search-clear { width:20px; height:20px; display:none; color:#999; border-radius:50%; } .pk-lbm-search.has-value .pk-lbm-search-clear { display:inline-flex; } .pk-lbm-search-clear:hover { opacity:1; background:var(--pk-hl); color:#666; } .pk-lbm-search-clear:active { transform:scale(.92); background:var(--pk-hl); } .pk-lbm-search-submit { color:#999; } .pk-lbm-search-submit:hover { opacity:1; background:transparent; color:var(--pk-pri); } .pk-lbm-search-submit:active { background:transparent; color:var(--pk-pri); } .pk-lbm-tools { margin-left:auto; display:flex; align-items:center; gap:8px; flex-wrap:nowrap; } .pk-lbm-archive-tools, .pk-lbm-folder-tools { display:flex; align-items:center; gap:8px; flex-wrap:nowrap; } .pk-lbm-archive-tools:empty, .pk-lbm-folder-tools:empty { display:none; } .pk-lbm-btn { height:34px; padding:0 12px; border:1px solid var(--pk-bd); border-radius:6px; background:transparent; color:var(--pk-fg); display:inline-flex; align-items:center; justify-content:center; gap:6px; font-size:13px; font-weight:650; cursor:pointer; white-space:nowrap; } .pk-lbm-btn.pri { background:var(--pk-pri); color:#fff; border-color:var(--pk-pri); } .pk-lbm-btn.danger { color:#d93025; border-color:rgba(217,48,37,.45); } .pk-lbm-btn svg { width:16px !important; height:16px !important; flex-shrink:0; } .pk-lbm-status { margin-left:10px; display:inline-flex; align-items:center; gap:8px; color:var(--pk-fg); font-size:13px; font-weight:700; white-space:nowrap; } .pk-lbm-status-dot { width:8px; height:8px; border-radius:50%; background:#16c784; box-shadow:0 0 0 4px rgba(22,199,132,.12); } .pk-link-bookmark-mode .pk-lbm-status.syncing .pk-lbm-status-dot { background:#3b82f6; box-shadow:0 0 0 4px rgba(59,130,246,.14); } .pk-link-bookmark-mode .pk-lbm-status.saving .pk-lbm-status-dot { background:#f59e0b; box-shadow:0 0 0 4px rgba(245,158,11,.16); } .pk-link-bookmark-mode .pk-lbm-status.conflict .pk-lbm-status-dot { background:#faad14; box-shadow:0 0 0 4px rgba(250,173,20,.16); } .pk-link-bookmark-mode .pk-lbm-status.failed .pk-lbm-status-dot { background:#d93025; box-shadow:0 0 0 4px rgba(217,48,37,.14); } .pk-link-bookmark-mode .pk-lbm-alert { border:1px solid rgba(217,48,37,.2); background:rgba(217,48,37,.08); color:#d93025; border-radius:6px; padding:9px 11px; font-size:13px; font-weight:650; line-height:1.45; display:none; } .pk-link-bookmark-mode .pk-lbm-alert.conflict { border-color:rgba(250,173,20,.28); background:rgba(250,173,20,.1); color:#b45309; } .pk-link-bookmark-mode .pk-lbm-alert.verify { border-color:rgba(217,48,37,.24); background:rgba(217,48,37,.1); color:#b91c1c; } .pk-link-bookmark-mode .pk-lbm-duplicate-warning { border:1px solid rgba(245,158,11,.35); background:rgba(245,158,11,.12); color:#92400e; border-radius:6px; padding:10px 12px; font-size:13px; font-weight:700; line-height:1.45; display:none; align-items:center; justify-content:space-between; gap:12px; } .pk-link-bookmark-mode .pk-lbm-merge-btn { height:28px; border:1px solid rgba(180,83,9,.28); border-radius:6px; background:#fff7ed; color:#b45309; display:inline-flex; align-items:center; justify-content:center; padding:0 10px; font-size:12px; font-weight:800; cursor:pointer; white-space:nowrap; } .pk-lbm-grid { flex:1 1 auto; min-height:0; overflow:auto; display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:14px; padding:2px 2px 4px; align-content:start; scrollbar-gutter:stable; } .pk-link-bookmark-mode .pk-lbm-grid.is-empty { display:flex; align-items:center; justify-content:center; } .pk-link-bookmark-mode .pk-lbm-empty { width:100%; min-height:220px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0; color:var(--pk-fg); text-align:center; padding:24px; padding-bottom:10vh; box-sizing:border-box; } .pk-link-bookmark-mode .pk-lbm-empty-icon { width:auto; height:auto; display:flex; align-items:center; justify-content:center; opacity:1; } .pk-link-bookmark-mode .pk-lbm-empty-icon svg { width:35vmin; max-width:220px; height:auto; margin-bottom:3vmin; filter:drop-shadow(0 15px 25px rgba(0,0,0,.05)); } .pk-link-bookmark-mode .pk-lbm-empty-title { font-size:clamp(13px,3vmin,16px); color:#94a3b8; font-weight:500; letter-spacing:1px; } .pk-ov.pk-dark .pk-link-bookmark-mode .pk-lbm-empty-title { color:#666e75; } .pk-link-bookmark-mode .pk-lbm-empty-desc { font-size:13px; opacity:.64; max-width:380px; line-height:1.5; } .pk-lbm-card { min-width:0; height:156px; border:1px solid var(--pk-bd); border-radius:8px; background:rgba(var(--pk-bg-rgb),.72); padding:18px 16px 12px; display:flex; flex-direction:column; gap:10px; box-sizing:border-box; box-shadow:0 8px 22px rgba(0,0,0,.05); } .pk-lbm-card-head { display:flex; align-items:center; gap:12px; min-width:0; } .pk-lbm-link-icon { width:42px; height:42px; border-radius:8px; background:#E9EBEE; color:#9AA0A6; display:flex; align-items:center; justify-content:center; font-size:16px; font-weight:900; flex-shrink:0; overflow:hidden; box-sizing:border-box; } .pk-lbm-link-icon.pk-lbm-link-icon-has-img { background:transparent; } .pk-lbm-link-icon img { width:100%; height:100%; object-fit:cover; border-radius:8px; display:block; background:transparent; } .pk-lbm-link-body { scrollbar-width:none; -ms-overflow-style:none; } .pk-lbm-link-body::-webkit-scrollbar { width:0; height:0; display:none; } .pk-lbm-folder-menu { height:auto !important; max-height:none !important; overflow:visible !important; box-shadow:none !important; } .pk-lbm-input-changed { border-color:var(--pk-pri) !important; } .pk-lbm-link-icon span { width:100%; height:100%; display:flex; align-items:center; justify-content:center; } .pk-lbm-link-icon svg { width:24px !important; height:24px !important; } .pk-lbm-link-title-wrap { min-width:0; display:flex; align-items:center; gap:6px; flex:1 1 auto; } .pk-lbm-link-title { min-width:0; flex:1 1 auto; font-size:15px; font-weight:800; color:var(--pk-fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pk-lbm-restore-dot { width:18px; height:18px; display:inline-flex; align-items:center; justify-content:center; flex:0 0 auto; border-radius:999px; } .pk-lbm-restore-dot::before { content:""; width:8px; height:8px; border-radius:999px; display:block; box-shadow:0 0 0 3px rgba(128,128,128,.12); } .pk-lbm-restore-dot.is-restored::before { background:#2e7d32; } .pk-lbm-restore-dot.is-unrestored::before { background:#9aa0a6; } .pk-lbm-meta { min-width:0; min-height:20px; display:grid; grid-template-columns:minmax(0,1fr); align-items:center; column-gap:8px; } .pk-link-bookmark-mode .pk-lbm-card.has-source .pk-lbm-meta { grid-template-columns:minmax(0,1fr) 82px; } .pk-lbm-url { color:var(--pk-fg); opacity:.66; font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; line-height:20px; min-width:0; } .pk-link-bookmark-mode .pk-lbm-card.has-source { height:156px; } .pk-link-bookmark-mode .pk-lbm-source-folder { width:82px; min-width:0; display:flex; align-items:center; justify-self:start; gap:5px; color:var(--pk-fg); opacity:.58; font-size:12px; line-height:20px; overflow:hidden; } .pk-link-bookmark-mode .pk-lbm-source-folder svg { width:13px !important; height:13px !important; flex-shrink:0; color:var(--pk-pri); } .pk-link-bookmark-mode .pk-lbm-source-folder span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pk-lbm-card-actions { margin-top:auto; padding-top:10px; border-top:1px solid var(--pk-bd); display:flex; align-items:center; justify-content:space-between; gap:4px; min-width:0; } .pk-lbm-card-act { min-width:0; height:26px; border:0; background:transparent; color:var(--pk-fg); opacity:.72; display:inline-flex; align-items:center; justify-content:center; gap:4px; font-size:12px; cursor:pointer; padding:0 1px; border-radius:4px; white-space:nowrap; flex:0 0 auto; } .pk-lbm-card-act span { white-space:nowrap; writing-mode:horizontal-tb; text-orientation:mixed; line-height:1; } .pk-lbm-card-act svg { width:14px !important; height:14px !important; flex-shrink:0; } .pk-lbm-card-act.danger { color:#d93025; } .pk-link-bookmark-mode .pk-lbm-btn:disabled,.pk-link-bookmark-mode .pk-lbm-side-add:disabled,.pk-link-bookmark-mode .pk-lbm-card-act:disabled,.pk-link-bookmark-mode .pk-lbm-folder-act:disabled { opacity:.46; cursor:not-allowed; } .pk-maximized .pk-lbm { grid-template-columns:330px minmax(0,1fr); gap:14px; padding:14px; } .pk-maximized .pk-lbm-grid { grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:16px; } @media (max-width:900px) { .pk-lbm { grid-template-columns:210px minmax(0,1fr); padding:10px; } .pk-lbm-toolbar { align-items:flex-start; flex-direction:column; } .pk-lbm-search { width:100%; } .pk-lbm-tools { margin-left:0; flex-wrap:wrap; } .pk-lbm-status { margin-left:0; } .pk-lbm-grid { grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); } } .pk-row { height: 40px; border: 1px solid transparent; cursor: default; padding: 0 16px; border-radius: 4px; } .pk-row:hover { background: var(--pk-hl); } .pk-row.sel { background: var(--pk-sel-bg); border: 1px solid transparent; } .pk-row.sel.pk-focused { border: 1px solid var(--pk-pri); border-radius: 4px; } .pk-grid-view .pk-row.sel.pk-focused { border-color: var(--pk-pri); box-shadow: 0 0 0 1px var(--pk-pri); border-radius: 16px; } .pk-name { display: flex; align-items: center; overflow: visible; min-width: 0; cursor: default; } .pk-name .pk-name-txt { transition: color 0.1s; border-bottom: 1px solid transparent; } .pk-name svg { flex-shrink: 0; margin-right: 8px; cursor: default; } .pk-win:not(.pk-mode-trash) .pk-name .pk-name-txt { cursor: pointer; } .pk-win:not(.pk-mode-trash) .pk-name .pk-name-txt:hover { color: var(--pk-pri); } .pk-tag-default { cursor: default !important; color: #999 !important; border: 1px solid #ccc !important; font-weight: normal !important; } .pk-tag-default:hover { color: #999 !important; } .pk-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; line-height: 1.5; padding: 2px 0; margin-top: -2px; } .pk-group-hd { display: flex; background: var(--pk-gh); color: var(--pk-gh-fg); font-weight: bold; align-items: center; padding: 0 16px; border-top: 4px solid var(--pk-bg) !important; border-bottom: 4px solid var(--pk-bg) !important; background-clip: padding-box; height: 40px !important; box-sizing: border-box; margin-top: 0 !important; } .pk-group-hd .pk-tag { margin-left: auto; background: #666; color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; border: 1px solid #555; } .pk-group-hd .pk-cnt { margin-left: 10px; color: var(--pk-fg); font-size: 12px; opacity: 0.9; } .pk-group-hd .pk-name { height:100%; align-items:center !important; } .pk-group-hd .pk-name .pk-name-txt, .pk-group-hd .pk-name span { display:flex !important; align-items:center !important; height:100% !important; line-height:1.2 !important; padding:0 !important; margin-top:0 !important; } .pk-loading-ov { position: absolute; inset: 0; background: rgba(var(--pk-bg-rgb), 0.75); z-index: 999; display: none; flex-direction: column; align-items: center; justify-content: center; color: var(--pk-fg); gap: 28px; backdrop-filter: blur(10px) saturate(180%); -webkit-backdrop-filter: blur(10px) saturate(180%); transition: all 0.3s ease; } .pk-spin-lg { width: 56px; height: 56px; border: 4px solid rgba(136, 136, 136, 0.25); border-top-color: var(--pk-pri); border-radius: 50%; position: relative; animation: pk-ultra-spin 1s linear infinite; transform: translateZ(0); } .pk-spin-sm { width:24px; height:24px; border-width:2px; } .pk-spin-xs { width:16px; height:16px; border-width:2px; } .pk-spin-float { width: 16px; height: 16px; border-width: 2px; flex: 0 0 16px; } .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: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.5); z-index: 10001; display: flex; align-items: center; justify-content: center; overscroll-behavior: none; overflow: hidden; padding: 20px; box-sizing: border-box; } .pk-modal { position: relative; background: var(--pk-bg); color: var(--pk-fg); padding: 25px; border-radius: 12px; width: 500px; max-height: 100%; overflow: hidden !important; display: flex; flex-direction: column; gap: 15px; border: 1px solid var(--pk-bd); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); overscroll-behavior: none; margin: auto; flex-shrink: 1; } .pk-modal h3 { margin: 0 0 5px 0; font-size: 16px; padding-bottom: 10px; padding-right: 40px; color: var(--pk-fg); } .pk-modal-close { position: absolute; top: 15px; right: 15px; cursor: pointer; color: var(--pk-icon-c); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background 0.1s, color: 0.1s; } .pk-modal-close:hover { background: var(--pk-hl); color: var(--pk-fg); } .pk-field { display: flex; flex-direction: column; gap: 5px; font-size: 13px; } .pk-field input, .pk-field select { padding: 6px; border: 1px solid var(--pk-bd); border-radius: 4px; background: var(--pk-bg); color: var(--pk-fg); } .pk-field select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; background-size: 16px; padding-right: 32px !important; cursor: pointer; } .pk-modal-act { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } .pk-credit { font-size: 11px; color: #888; text-align: center; margin-top: 20px; border-top: 1px solid var(--pk-bd); padding-top: 10px; } .pk-credit a { color: #888; text-decoration: none; } .pk-credit a:hover { text-decoration: underline; } .pk-prev-list { flex: 1; overflow-y: auto; border: 1px solid var(--pk-bd); max-height: 300px; } .pk-prev-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 5px 10px; border-bottom: 1px solid var(--pk-bd); font-size: 12px; } .pk-prev-row:nth-child(odd) { background: var(--pk-hl); } .pk-sep { width: 1px; height: 16px; background: var(--pk-bd); margin: 0 4px; display: none; flex-shrink: 0; } .pk-sep-sm { width: 1px; height: 16px; background: var(--pk-bd); margin: 0 8px; flex-shrink: 0; } #pk-refresh, #pk-trash-refresh { width: auto !important; justify-content: center !important; flex-shrink: 0; } #pk-refresh svg, #pk-trash-refresh svg { width: 16px !important; height: 16px !important; } .pk-grid-hd input[type="checkbox"], .pk-row input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; margin: 0 auto !important; flex-shrink: 0; accent-color: var(--pk-pri); box-sizing: content-box; transform: translateZ(0); display: block; position: relative; } .pk-player-box { position: relative; width: 100%; height: 100%; background: #000; display: flex; flex-direction: column; user-select: none; overflow: hidden; } .pk-player-video { width: 100%; height: 100%; object-fit: contain; outline: none; transform: translateZ(0); backface-visibility: hidden; image-rendering: -webkit-optimize-contrast; -webkit-font-smoothing: antialiased; } .pk-player-top { position: absolute; top: 0; left: 0; right: 0; height: 64px; background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.3) 60%, transparent 100%); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; z-index: 100 !important; opacity: 1; transition: opacity 0.3s; pointer-events: auto; } .pk-player-box.ui-hidden .pk-player-top { opacity: 0 !important; pointer-events: none !important; } .pk-player-title { color: #fff; font-size: 16px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); letter-spacing: 0.5px; } .pk-player-controls { position: absolute; bottom: 0; left: 0; right: 0; height: 64px; background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.35) 60%, transparent 100%); display: flex; align-items: center; padding: 0 16px; z-index: 80 !important; gap: 4px; opacity: 0; transition: opacity 0.3s; } .pk-player-box:hover .pk-player-controls, .pk-player-box.paused .pk-player-controls { opacity: 1; } .pk-player-progress-container { position: absolute; bottom: 64px; left: 12px; right: 12px; height: 14px; cursor: pointer; z-index: 11; display: flex; align-items: center; } .pk-player-progress-bg { width: 100%; height: 4px; background: rgba(255, 255, 255, 0.25); position: relative; border-radius: 2px; transition: height 0.1s, transform 0.1s; backdrop-filter: blur(2px); } .pk-player-progress-container:hover .pk-player-progress-bg { height: 6px; transform: scaleY(1.1); } .pk-player-progress-filled { height: 100%; background: var(--pk-pri); width: 0; position: relative; border-radius: 2px; transition: none !important; will-change: width; } .pk-player-progress-thumb { position: absolute; right: -7px; top: 50%; transform: translateY(-50%) scale(0); width: 14px; height: 14px; border-radius: 50%; background: #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .pk-player-progress-container:hover .pk-player-progress-thumb { transform: translateY(-50%) scale(1); } .pk-share-preview-limit-marker { position: absolute; top: 50%; width: 2px; height: 14px; transform: translate(-50%, -50%); background: rgba(255,255,255,0.95); border-radius: 2px; box-shadow: 0 0 6px rgba(0,0,0,0.65); z-index: 4; pointer-events: auto; } .pk-share-preview-limit-label { position: absolute; left: 50%; bottom: 15px; transform: translateX(-50%); white-space: nowrap; background: rgba(0,0,0,0.65); color: #fff; font-size: 12px; line-height: 1; padding: 5px 8px; border-radius: 12px; opacity: 0; pointer-events: none; transition: opacity .15s ease; text-shadow: none; } #pk_p_prog_area:hover .pk-share-preview-limit-label, .pk-share-preview-limit-marker:hover .pk-share-preview-limit-label { opacity: 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; transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s 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:fullscreen.plist-active .pk-p-loading, #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, #pk_p_box:-webkit-full-screen.plist-active .pk-p-loading { 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_box:fullscreen.plist-active .pk-p-resume-toast, #pk_p_box:-webkit-full-screen.plist-active .pk-p-resume-toast { bottom: 169px !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 { transition: height 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease !important; } #pk_p_box:fullscreen .pk-player-controls, #pk_p_box:-webkit-full-screen .pk-player-controls, #pk_p_box:fullscreen .pk-p-prog-wrap, #pk_p_box:-webkit-full-screen .pk-p-prog-wrap, #pk_p_box:fullscreen .pk-p-resume-toast, #pk_p_box:-webkit-full-screen .pk-p-resume-toast { transition: bottom 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease !important; } #pk_p_box:fullscreen .pk-p-side-nav, #pk_p_box:fullscreen .pk-p-center-play, #pk_p_box:fullscreen .pk-p-seek-indicator, #pk_p_box:fullscreen .pk-p-loading, #pk_p_box:-webkit-full-screen .pk-p-side-nav, #pk_p_box:-webkit-full-screen .pk-p-center-play, #pk_p_box:-webkit-full-screen .pk-p-seek-indicator, #pk_p_box:-webkit-full-screen .pk-p-loading { transition: top 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important; } .pk-p-plist-tab:hover { color: rgba(255, 255, 255, 0.8); } .pk-p-plist-tab:hover::before { opacity: 1; filter: none; } .pk-p-plist-tab svg { transition: transform 0.3s; } .pk-p-plist-ov.open .pk-p-plist-tab svg { transform: rotate(180deg); } .pk-p-plist-strip { height: 110px; background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); display: flex; align-items: center; position: relative; border-top: 1px solid rgba(255, 255, 255, 0.1); } .pk-p-plist-scroll { flex: 1; display: flex; overflow-x: auto; gap: 2px; padding: 0 50px; height: 100%; align-items: center; } .pk-p-plist-scroll::-webkit-scrollbar { display: none; } .pk-p-plist-item { flex-shrink: 0; width: 140px; height: 80px; background: #333; cursor: pointer; position: relative; overflow: hidden; border: 2px solid transparent; border-radius: 4px; will-change: opacity, transform; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .pk-p-plist-item.active { border-color: var(--pk-pri); box-shadow: 0 0 10px var(--pk-pri); } .pk-p-plist-ph { position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; overflow:hidden; background:#3a3a3a; } .pk-p-plist-ph img, .pk-p-plist-ph svg { width:100% !important; height:100% !important; max-width:none !important; max-height:none !important; display:block; border-radius:0 !important; } .pk-p-plist-ph img { object-fit:cover !important; } .pk-p-plist-ph svg { object-fit:fill !important; } .pk-p-plist-item > img { position:relative; z-index:2; width:100%; height:100%; object-fit:cover; opacity:1; transition:opacity 0.3s ease-in-out; display:block; } .pk-p-plist-item img:error { opacity:0 !important; } .pk-p-plist-nav { position: absolute; top: 0; bottom: 0; width: 50px; background: transparent !important; color: rgba(255, 255, 255, 0.7); cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 35; transition: all 0.2s; border: none; } .pk-p-plist-nav:hover { background: rgba(255, 255, 255, 0.15) !important; color: #fff; } .pk-p-plist-nav.L { left: 0; } .pk-p-plist-nav.R { right: 0; } .pk-p-plist-nav svg { width: 28px; height: 28px; stroke-width: 3; } .pk-p-plist-tip { position: fixed; background: rgba(0, 0, 0, 0.9); color: #fff; padding: 8px 12px; border-radius: 6px; font-size: 12px; pointer-events: none; z-index: 2147483647; max-width: 340px; line-height: 1.4; display: none; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255, 255, 255, 0.1); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-p-resume-btn { color: #4aa1ff; cursor: pointer; font-weight: bold; text-decoration: none; } .pk-p-resume-btn:hover { text-decoration: underline; } .pk-p-resume-close { cursor: pointer; color: #888; margin-left: 4px; display: flex; align-items: center; } #pk-audio-ov { position:fixed; inset:0; z-index:2147483641; background:rgba(0,0,0,.78); backdrop-filter:blur(8px); display:flex; align-items:center; justify-content:center; outline:none; padding:18px; box-sizing:border-box; font-family:inherit; } #pk-audio-ov .pk-audio-box { width:min(680px,100%); height:80vh; max-height:calc(100vh - 36px); background:rgba(var(--pk-bg-rgb),.94); color:var(--pk-fg); border:1px solid var(--pk-bd); border-radius:12px; box-shadow:0 25px 60px rgba(0,0,0,.45); overflow:hidden; display:flex; flex-direction:row; min-width:0; position:relative; } #pk-audio-ov .pk-audio-box.pk-audio-playlist-open { width:min(1000px,100%); } #pk-audio-ov .pk-audio-player-main { flex:1 1 680px; min-width:0; min-height:0; height:100%; display:flex; flex-direction:column; } #pk-audio-ov .pk-audio-top { height:56px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 16px 0 20px; border-bottom:1px solid var(--pk-bd); background:rgba(var(--pk-bg-rgb),.88); } #pk-audio-ov .pk-audio-title { font-size:15px; font-weight:800; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } #pk-audio-ov .pk-audio-actions { display:flex; align-items:center; justify-content:flex-end; gap:6px; flex-shrink:0; } #pk-audio-ov .pk-audio-main { flex:1 1 auto; justify-content:center; padding:28px 28px 24px; display:flex; flex-direction:column; align-items:center; text-align:center; gap:12px; min-width:0; min-height:0; } #pk-audio-ov .pk-audio-icon { width:188px; height:188px; border-radius:30px; display:flex; align-items:center; justify-content:center; background:var(--pk-hl); color:var(--pk-pri); box-shadow:inset 0 0 0 1px var(--pk-bd); overflow:hidden; flex-shrink:0; } #pk-audio-ov .pk-audio-icon svg { width:138px !important; height:138px !important; } #pk-audio-ov .pk-audio-icon img { width:100%; height:100%; object-fit:cover; display:block; } #pk-audio-ov .pk-audio-name { width:100%; margin-top:14px; font-size:18px; font-weight:800; line-height:1.45; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } #pk-audio-ov .pk-audio-meta { display:flex; flex-wrap:wrap; justify-content:center; gap:8px; font-size:12px; opacity:.76; } #pk-audio-ov .pk-audio-chip { border:1px solid var(--pk-bd); background:var(--pk-hl); border-radius:999px; padding:4px 10px; max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } #pk-audio-ov .pk-audio-status { min-height:20px; font-size:13px; color:var(--pk-pri); font-weight:700; } #pk-audio-ov .pk-audio-status.err { color:#ff6b6b; } #pk-audio-ov .pk-audio-progress-row { display:grid; grid-template-columns:56px 1fr 56px; gap:12px; align-items:center; padding:0 24px 18px; } #pk-audio-ov .pk-audio-time { font-size:12px; font-variant-numeric:tabular-nums; opacity:.78; text-align:center; } #pk-audio-ov .pk-audio-progress-wrap { position:relative; display:flex; align-items:center; width:100%; min-width:0; } #pk-audio-ov .pk-audio-progress-tip { position:absolute; left:0; bottom:calc(100% + 10px); transform:translate(-50%,4px); padding:4px 8px; border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); border:1px solid var(--pk-bd); box-shadow:0 6px 16px rgba(0,0,0,.28); font-size:12px; line-height:1.35; white-space:nowrap; opacity:0; pointer-events:none; transition:opacity .14s ease,transform .14s ease; z-index:20; font-variant-numeric:tabular-nums; } #pk-audio-ov .pk-audio-progress-tip::after { content:""; position:absolute; left:50%; top:100%; transform:translateX(-50%); border:5px solid transparent; border-top-color:var(--pk-bd); } #pk-audio-ov .pk-audio-progress-tip.show { opacity:1; transform:translate(-50%,0); } #pk-audio-ov .pk-audio-range { -webkit-appearance:none; appearance:none; width:100%; height:5px; border-radius:999px; background:linear-gradient(to right,var(--pk-pri) 0%,var(--pk-pri) var(--pk-audio-pct,0%),rgba(127,127,127,.32) var(--pk-audio-pct,0%),rgba(127,127,127,.32) 100%); outline:none; cursor:pointer; } #pk-audio-ov .pk-audio-range::-webkit-slider-thumb { -webkit-appearance:none; width:15px; height:15px; border-radius:50%; background:#fff; border:2px solid var(--pk-pri); box-shadow:0 2px 8px rgba(0,0,0,.25); } #pk-audio-ov .pk-audio-range::-moz-range-thumb { width:15px; height:15px; border-radius:50%; background:#fff; border:2px solid var(--pk-pri); box-shadow:0 2px 8px rgba(0,0,0,.25); } #pk-audio-ov .pk-audio-controls { min-height:66px; display:flex; align-items:center; justify-content:center; gap:12px; padding:12px 18px 18px; border-top:1px solid var(--pk-bd); background:rgba(var(--pk-bg-rgb),.88); } #pk-audio-ov .pk-audio-btn { width:40px; height:40px; border:0; border-radius:50%; background:transparent; color:var(--pk-fg); display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0; transition:background .15s,color .15s,transform .15s; flex-shrink:0; } #pk-audio-ov .pk-audio-btn:hover { background:var(--pk-hl); color:var(--pk-pri); } #pk-audio-ov .pk-audio-btn:active { transform:scale(.96); } #pk-audio-ov .pk-audio-btn.pk-audio-btn-active { background:var(--pk-hl); color:var(--pk-pri); box-shadow:inset 0 0 0 1px var(--pk-bd); } #pk-audio-ov .pk-audio-btn[data-tip] { position:relative; } #pk-audio-ov .pk-audio-btn[data-tip]::after { content:attr(data-tip); position:absolute; left:50%; bottom:calc(100% + 8px); transform:translate(-50%,4px); padding:4px 8px; border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); border:1px solid var(--pk-bd); box-shadow:0 6px 16px rgba(0,0,0,.28); font-size:12px; line-height:1.35; white-space:nowrap; opacity:0; pointer-events:none; transition:opacity .14s ease,transform .14s ease; z-index:20; } #pk-audio-ov .pk-audio-btn[data-tip]::before { content:""; position:absolute; left:50%; bottom:calc(100% + 3px); transform:translateX(-50%); border:5px solid transparent; border-top-color:var(--pk-bd); opacity:0; pointer-events:none; transition:opacity .14s ease; z-index:20; } #pk-audio-ov .pk-audio-btn[data-tip]:hover::after,#pk-audio-ov .pk-audio-btn[data-tip]:focus-visible::after { opacity:1; transform:translate(-50%,0); } #pk-audio-ov .pk-audio-btn[data-tip]:hover::before,#pk-audio-ov .pk-audio-btn[data-tip]:focus-visible::before { opacity:1; } #pk-audio-ov .pk-audio-actions .pk-audio-btn[data-tip]::after { top:calc(100% + 8px); bottom:auto; transform:translate(-50%,-4px); } #pk-audio-ov .pk-audio-actions .pk-audio-btn[data-tip]::before { top:calc(100% + 3px); bottom:auto; border-top-color:transparent; border-bottom-color:var(--pk-bd); } #pk-audio-ov .pk-audio-actions .pk-audio-btn[data-tip]:hover::after,#pk-audio-ov .pk-audio-actions .pk-audio-btn[data-tip]:focus-visible::after { transform:translate(-50%,0); } #pk-audio-ov .pk-audio-btn svg { width:22px; height:22px; fill:currentColor; } #pk-audio-ov #pk_audio_play { width:48px; height:48px; background:var(--pk-pri); color:#fff; } #pk-audio-ov #pk_audio_play:hover { color:#fff; filter:brightness(1.08); } #pk-audio-ov .pk-audio-volume-wrap { display:flex; align-items:center; gap:8px; min-width:128px; } #pk-audio-ov .pk-audio-volume { width:86px; } #pk-audio-ov .pk-audio-playlist-panel { display:flex; flex:0 0 0; width:0; min-width:0; max-width:0; border-left:0; background:rgba(var(--pk-bg-rgb),.9); flex-direction:column; overflow:hidden; opacity:0; visibility:hidden; pointer-events:none; } #pk-audio-ov .pk-audio-playlist-open .pk-audio-playlist-panel { flex:0 0 300px; width:auto; max-width:320px; border-left:1px solid var(--pk-bd); opacity:1; visibility:visible; pointer-events:auto; } #pk-audio-ov .pk-audio-playlist-head { height:56px; flex:0 0 56px; display:flex; align-items:center; justify-content:space-between; gap:10px; padding:0 14px; border-bottom:1px solid var(--pk-bd); font-size:14px; font-weight:800; background:rgba(var(--pk-bg-rgb),.88); } #pk-audio-ov .pk-audio-playlist-head span:last-child { font-size:12px; font-weight:700; opacity:.62; } #pk-audio-ov .pk-audio-playlist-body { flex:1 1 auto; min-height:0; overflow:auto; padding:8px; scrollbar-gutter:stable; } #pk-audio-ov .pk-audio-playlist-body::-webkit-scrollbar { width:6px; height:6px; } #pk-audio-ov .pk-audio-playlist-body::-webkit-scrollbar-track { background:var(--pk-sb-bg); } #pk-audio-ov .pk-audio-playlist-body::-webkit-scrollbar-thumb { background:var(--pk-sb-th); border-radius:3px; } #pk-audio-ov .pk-audio-playlist-body::-webkit-scrollbar-thumb:hover { background:var(--pk-sb-hov); } #pk-audio-ov .pk-audio-playlist-item { width:100%; min-width:0; border:1px solid transparent; border-radius:8px; background:transparent; color:var(--pk-fg); display:grid; grid-template-columns:24px minmax(0,1fr); gap:6px; align-items:center; text-align:left; padding:9px 8px 9px 4px; box-sizing:border-box; cursor:pointer; position:relative; font-family:inherit; } #pk-audio-ov .pk-audio-playlist-item::before { content:""; position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:3px; background:transparent; } #pk-audio-ov .pk-audio-playlist-item:hover { background:var(--pk-hl); } #pk-audio-ov .pk-audio-playlist-item-active { background:var(--pk-hl); border-color:var(--pk-bd); color:var(--pk-pri); } #pk-audio-ov .pk-audio-playlist-item-active::before { background:var(--pk-pri); } #pk-audio-ov .pk-audio-playlist-index { font-size:12px; font-weight:800; opacity:.58; font-variant-numeric:tabular-nums; text-align:right; } #pk-audio-ov .pk-audio-playlist-text { min-width:0; display:block; } #pk-audio-ov .pk-audio-playlist-name { min-width:0; display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px; font-weight:700; line-height:1.35; } #pk-audio-ov .pk-audio-playlist-meta { min-width:0; display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; opacity:.62; line-height:1.35; margin-top:2px; color:var(--pk-fg); } #pk-audio-ov .pk-audio-playlist-empty { height:120px; display:flex; align-items:center; justify-content:center; color:var(--pk-fg); opacity:.62; font-size:13px; } #pk-audio-ov #pk_audio { display:none; } #pk-audio-mini { position:fixed; right:24px; bottom:24px; z-index:2147483642; width:300px; max-width:calc(100vw - 16px); box-sizing:border-box; background:rgba(var(--pk-bg-rgb,255,255,255),.94); color:var(--pk-fg,#222); border:1px solid var(--pk-bd,rgba(0,0,0,.12)); border-radius:12px; box-shadow:0 18px 46px rgba(0,0,0,.36); backdrop-filter:blur(10px); overflow:hidden; font-family:inherit; line-height:1.5; user-select:none; } #pk-audio-mini.pk-dark { background:rgba(var(--pk-bg-rgb,32,32,32),.94); color:var(--pk-fg,#f5f5f5); border-color:var(--pk-bd,#333); box-shadow:0 18px 46px rgba(0,0,0,.52); } #pk-audio-mini .pk-audio-mini-drag { height:42px; display:flex; align-items:center; justify-content:space-between; gap:10px; padding:0 10px 0 14px; border-bottom:1px solid var(--pk-bd,rgba(0,0,0,.12)); background:rgba(var(--pk-bg-rgb,255,255,255),.86); cursor:move; touch-action:none; } #pk-audio-mini.pk-dark .pk-audio-mini-drag { background:rgba(var(--pk-bg-rgb,32,32,32),.86); } #pk-audio-mini .pk-audio-mini-fold { width:34px; height:34px; border:0; border-radius:50%; background:transparent; color:var(--pk-fg); display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0; position:relative; transition:background .15s,color .15s,transform .15s; flex-shrink:0; } #pk-audio-mini .pk-audio-mini-fold:hover { background:var(--pk-hl); color:var(--pk-pri); } #pk-audio-mini .pk-audio-mini-fold:active { transform:scale(.96); } #pk-audio-mini .pk-audio-mini-fold svg { width:18px; height:18px; fill:currentColor; } #pk-audio-mini .pk-audio-mini-title-wrap { flex:1 1 auto; min-width:0; height:24px; overflow:hidden; display:none; align-items:center; color:var(--pk-fg); font-size:13px; font-weight:800; line-height:24px; opacity:.92; } #pk-audio-mini.pk-audio-mini-collapsed .pk-audio-mini-title-wrap { display:flex; } #pk-audio-mini .pk-audio-mini-title-track { display:flex; align-items:center; min-width:0; max-width:100%; overflow:hidden; white-space:nowrap; transform:translateX(0); will-change:transform; } #pk-audio-mini .pk-audio-mini-title-text { flex:0 0 auto; display:inline-block; min-width:0; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } #pk-audio-mini .pk-audio-mini-title-text + .pk-audio-mini-title-text { display:none; margin-left:32px; } #pk-audio-mini .pk-audio-mini-title-wrap.pk-audio-mini-title-scroll .pk-audio-mini-title-track { max-width:none; overflow:visible; animation:pkAudioMiniTitleScroll var(--pk-mini-title-duration,12s) linear infinite; } #pk-audio-mini .pk-audio-mini-title-wrap.pk-audio-mini-title-scroll .pk-audio-mini-title-text { max-width:none; overflow:visible; text-overflow:clip; } #pk-audio-mini .pk-audio-mini-title-wrap.pk-audio-mini-title-scroll .pk-audio-mini-title-text + .pk-audio-mini-title-text { display:inline-block; } #pk-audio-mini .pk-audio-mini-title-wrap.pk-audio-mini-title-scroll:hover .pk-audio-mini-title-track { animation-play-state:paused; } @keyframes pkAudioMiniTitleScroll { from { transform:translateX(0); } to { transform:translateX(var(--pk-mini-title-shift,-120px)); } } #pk-audio-mini .pk-audio-mini-body { padding:14px; display:flex; flex-direction:column; gap:10px; min-width:0; } #pk-audio-mini.pk-audio-mini-collapsed .pk-audio-mini-drag { border-bottom:0; } #pk-audio-mini.pk-audio-mini-collapsed .pk-audio-mini-body { display:none; } #pk-audio-mini .pk-audio-mini-name { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; font-weight:800; color:var(--pk-fg); } #pk-audio-mini .pk-audio-mini-meta { min-height:18px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; color:var(--pk-fg); opacity:.66; } #pk-audio-mini .pk-audio-mini-controls { display:flex; align-items:center; justify-content:center; gap:8px; padding-top:2px; } #pk-audio-mini .pk-audio-mini-volume-wrap { width:34px; height:34px; position:relative; display:flex; align-items:center; justify-content:center; flex-shrink:0; } #pk-audio-mini .pk-audio-mini-volume-wrap::before { content:""; position:absolute; left:50%; bottom:100%; transform:translateX(-50%); width:44px; height:18px; background:transparent; } #pk-audio-mini .pk-audio-mini-volume-panel { position:absolute; left:50%; bottom:calc(100% + 2px); transform:translate(-50%,4px); width:34px; height:118px; display:flex; align-items:center; justify-content:center; opacity:0; visibility:hidden; pointer-events:none; transition:opacity .14s ease,transform .14s ease,visibility .14s; z-index:4; background:var(--pk-bg); border:1px solid var(--pk-bd); border-radius:14px; box-shadow:0 8px 24px rgba(0,0,0,.22); } #pk-audio-mini .pk-audio-mini-volume-panel::before { content:""; position:absolute; left:50%; bottom:-14px; transform:translateX(-50%); width:44px; height:18px; background:transparent; } #pk-audio-mini .pk-audio-mini-volume-wrap:hover .pk-audio-mini-volume-panel,#pk-audio-mini .pk-audio-mini-volume-wrap:focus-within .pk-audio-mini-volume-panel,#pk-audio-mini .pk-audio-mini-volume-wrap.pk-audio-mini-volume-active .pk-audio-mini-volume-panel { opacity:1; visibility:visible; pointer-events:auto; transform:translate(-50%,0); } #pk-audio-mini .pk-audio-mini-volume-panel::after { display:none; } #pk-audio-mini .pk-audio-mini-volume { -webkit-appearance:none; appearance:none; writing-mode:vertical-lr; direction:rtl; width:6px; height:84px; border-radius:999px; background:linear-gradient(to top,var(--pk-pri) 0%,var(--pk-pri) var(--pk-mini-vol-pct,0%),rgba(127,127,127,.28) var(--pk-mini-vol-pct,0%),rgba(127,127,127,.28) 100%); outline:none; cursor:pointer; padding:0; margin:0; } #pk-audio-mini .pk-audio-mini-volume::-webkit-slider-runnable-track { width:6px; height:84px; border-radius:999px; background:transparent; } #pk-audio-mini .pk-audio-mini-volume::-webkit-slider-thumb { -webkit-appearance:none; appearance:none; width:14px; height:14px; border-radius:50%; background:var(--pk-pri); border:2px solid var(--pk-bg); box-shadow:0 1px 5px rgba(0,0,0,.24); margin-left:-4px; } #pk-audio-mini .pk-audio-mini-volume::-moz-range-track { width:6px; height:84px; border-radius:999px; background:rgba(127,127,127,.28); } #pk-audio-mini .pk-audio-mini-volume::-moz-range-progress { width:6px; border-radius:999px; background:var(--pk-pri); } #pk-audio-mini .pk-audio-mini-volume::-moz-range-thumb { width:14px; height:14px; border-radius:50%; background:var(--pk-pri); border:2px solid var(--pk-bg); box-shadow:0 1px 5px rgba(0,0,0,.24); } #pk-audio-mini .pk-audio-mini-btn { width:34px; height:34px; border:0; border-radius:50%; background:transparent; color:var(--pk-fg); display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0; position:relative; transition:background .15s,color .15s,transform .15s; flex-shrink:0; } #pk-audio-mini .pk-audio-mini-btn:hover { background:var(--pk-hl); color:var(--pk-pri); } #pk-audio-mini .pk-audio-mini-btn:active { transform:scale(.96); } #pk-audio-mini .pk-audio-mini-btn svg { width:20px; height:20px; fill:currentColor; } #pk-audio-mini #pk_audio_mini_play { width:42px; height:42px; background:var(--pk-pri); color:#fff; } #pk-audio-mini #pk_audio_mini_play:hover { color:#fff; filter:brightness(1.08); } #pk-audio-mini .pk-audio-mini-btn[data-tip]::after { content:attr(data-tip); position:absolute; left:50%; bottom:calc(100% + 8px); transform:translate(-50%,4px); padding:4px 8px; border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); border:1px solid var(--pk-bd); box-shadow:0 6px 16px rgba(0,0,0,.28); font-size:12px; line-height:1.35; white-space:nowrap; opacity:0; pointer-events:none; transition:opacity .14s ease,transform .14s ease; z-index:2; } #pk-audio-mini .pk-audio-mini-btn[data-tip]::before { content:""; position:absolute; left:50%; bottom:calc(100% + 3px); transform:translateX(-50%); border:5px solid transparent; border-top-color:var(--pk-bd); opacity:0; pointer-events:none; transition:opacity .14s ease; z-index:2; } #pk-audio-mini .pk-audio-mini-btn[data-tip]:hover::after,#pk-audio-mini .pk-audio-mini-btn[data-tip]:focus-visible::after { opacity:1; transform:translate(-50%,0); } #pk-audio-mini .pk-audio-mini-btn[data-tip]:hover::before,#pk-audio-mini .pk-audio-mini-btn[data-tip]:focus-visible::before { opacity:1; } #pk-audio-mini .pk-audio-mini-audio-host { display:none; } @media (max-width:760px){#pk-audio-ov .pk-audio-box.pk-audio-playlist-open{width:min(680px,100%);flex-direction:column;}#pk-audio-ov .pk-audio-box.pk-audio-playlist-open .pk-audio-playlist-panel{flex:0 0 auto;width:100%;max-width:none;max-height:240px;border-left:0;border-top:1px solid var(--pk-bd);}#pk-audio-ov .pk-audio-playlist-head{height:44px;flex-basis:44px;}} @media (max-width:640px){#pk-audio-ov{padding:10px;}#pk-audio-ov .pk-audio-main{padding:26px 18px 22px;}#pk-audio-ov .pk-audio-progress-row{grid-template-columns:46px 1fr 46px;gap:8px;padding:0 14px 16px;}#pk-audio-ov .pk-audio-controls{gap:8px;flex-wrap:wrap;}#pk-audio-ov .pk-audio-volume-wrap{min-width:112px;}#pk-audio-ov .pk-audio-name{font-size:16px;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}} @keyframes pkFadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .pk-img-ov { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; 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: 1600px; 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: calc(100% - 1px); 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); } } @keyframes pk-spin { 100% { 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 { 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; } .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-toast-container { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 12px; pointer-events: none; align-items: center; } .pk-msg-toast { position: relative; background: var(--pk-toast-bg); color: var(--pk-toast-fg); padding: 12px 28px; border-radius: 12px; font-size: 14px; font-weight: 600; pointer-events: none; opacity: 0; transform: translateY(-15px) scale(0.95); 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; max-width: 80vw; 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.pk-show, .pk-msg-toast.show { opacity: 1; transform: translateY(0) scale(1); } .pk-msg-toast.pk-hide { opacity: 0; transform: translateY(-15px) scale(0.95); } .pk-float-bar-item { position: fixed; left: 50%; transform: translateX(-50%) scale(0.9); background: var(--pk-toast-bg); color: var(--pk-toast-fg); padding: 10px 24px; border-radius: 30px; font-size: 13px; font-weight: 600; box-shadow: 0 10px 30px var(--pk-tip-sd); z-index: 2147483647 !important; border: 1px solid var(--pk-toast-bd); display: flex; align-items: center; gap: 12px; transition: bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease, transform 0.3s ease; opacity: 0; pointer-events: none; white-space: nowrap; backdrop-filter: blur(8px); } .pk-float-bar-item.pk-show { opacity: 1; transform: translateX(-50%) scale(1); } .pk-float-bar-item.pk-hide { opacity: 0; transform: translateX(-50%) scale(0.9); } .pk-float-txt { line-height: 1.5; } .pk-config-input-limit-tip{position:fixed;left:50%;top:50%;transform:translate(-50%,-100%) scale(.96);opacity:0;z-index:2147483647;pointer-events:none;background:rgba(250,173,20,.96);color:#fff;border:1px solid rgba(250,173,20,.25);border-radius:10px;padding:8px 14px;font-size:13px;font-weight:700;line-height:1.35;box-shadow:0 8px 24px rgba(0,0,0,.22);transition:opacity .18s ease,transform .18s ease;white-space:nowrap;} .pk-config-input-limit-tip.show{opacity:1;transform:translate(-50%,-115%) scale(1);} .pk-crumb-sep { width: 22px; height: 22px; margin: 0 2px; cursor: pointer; border-radius: 4px; color: #aaa; transition: all 0.2s; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } .pk-crumb-sep:hover, .pk-crumb-sep.pk-active { background: var(--pk-hl); color: currentColor; } .pk-crumb-sep svg { width: 14px; height: 14px; stroke-width: 3.5; transition: transform 0.2s ease; } .pk-crumb-pop { position: fixed; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); z-index: 2147483647 !important; min-width: 180px; max-width: 320px; max-height: 50vh; overflow-y: auto; padding: 6px 0; opacity: 0; pointer-events: none; transition: opacity 0.1s ease; border-top: 1px solid rgba(255, 255, 255, 0.05); } .pk-crumb-pop.pk-show { opacity: 1; pointer-events: auto; } .pk-crumb-item { padding: 8px 12px; font-size: 13px; line-height: 1.5; cursor: pointer; display: flex; align-items: center; justify-content: flex-start; gap: 8px; color: var(--pk-fg); } body.pk-body-max .pk-crumb-pop .pk-crumb-item { font-size:16px; } body.pk-body-max .pk-crumb-pop .pk-crumb-name-wrap .pk-tag-default { font-size:14px !important; height:18px; } .pk-crumb-item:hover { background: var(--pk-hl); } .pk-crumb-empty { justify-content:center; cursor:default; opacity:.62; color:var(--pk-fg); } .pk-crumb-item.pk-moving { opacity: 0.4; filter: grayscale(1); cursor: wait; pointer-events: none; } .pk-crumb-item svg { flex-shrink: 0; } .pk-crumb-name-wrap { display: inline-flex; align-items: center; justify-content: flex-start; min-width: 0; max-width: calc(100% - 26px); flex: 0 1 auto; overflow: hidden; } .pk-crumb-name { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 0 1 auto; padding-bottom: 2px; margin-bottom: -2px; } .pk-crumb-name-wrap .pk-tag-default { flex: 0 0 auto !important; margin-left: 6px; min-width: auto; height: 16px; padding: 0 6px; line-height: 1 !important; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap !important; overflow: visible !important; text-overflow: clip !important; padding-bottom: 0 !important; margin-bottom: 0 !important; } .pk-picker-root { display:flex; flex-direction:column; height:480px; width:500px; max-width:90vw; } .pk-picker-header { padding:0 0 20px 0; display:flex; justify-content:space-between; align-items:center; flex-shrink:0; } .pk-picker-title { margin:0; font-size:18px; font-weight:700; color:var(--pk-fg); border:none; } .pk-picker-close { cursor:pointer; color:var(--pk-icon-c); display:flex; align-items:center; justify-content:center; padding:4px; border-radius:4px; transition:background 0.2s; } .pk-picker-bar { display:flex; align-items:center; gap:10px; margin-bottom:10px; height:32px; } .pk-picker-crumb { flex:1; overflow-x:auto; overflow-y:hidden; white-space:nowrap; display:flex; align-items:center; font-size:14px; color:#666; height:100%; scroll-behavior:smooth; } .pk-picker-sort-wrap { position:relative; flex-shrink:0; } .pk-picker-sort-trigger { 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; } .pk-picker-sort-menu { 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; } .pk-picker-sort-menu .pk-sort-opt { padding:8px 12px; font-size:14px; cursor:pointer; color:var(--pk-fg); display:flex; align-items:center; gap:8px; } .pk-picker-list { flex:1; overflow-y:auto; padding:0; border-top:1px solid var(--pk-bd); } .pk-loading-line { text-align:center; padding:20px; color:#999; } .pk-loading-line.pk-loading-flex { display:flex; justify-content:center; color:inherit; } .pk-picker-footer { padding-top:15px; border-top:1px solid var(--pk-bd); display:flex; justify-content:space-between; align-items:center; flex-shrink:0; } .pk-picker-new { cursor:pointer; color:var(--pk-pri); align-items:center; gap:6px; font-size:14px; font-weight:500; } .pk-picker-actions { display:flex; gap:10px; margin-left:auto; } .pk-picker-row { display:flex; align-items:center; padding:10px 8px; cursor:pointer; border-radius:6px; transition:background 0.1s; border-bottom:1px dashed var(--pk-bd); } .pk-picker-empty { padding-bottom:0; position:relative; height:100%; justify-content:center; } .pk-picker-empty-txt { margin-top:10px; } .pk-picker-check { margin-right:12px; width:16px; height:16px; accent-color:var(--pk-pri); cursor:pointer; } .pk-picker-icon-img { width:24px; height:24px; object-fit:contain; flex-shrink:0; margin-right:10px; } .pk-picker-icon-fallback { margin-right:10px; display:flex; } .pk-picker-icon-folder { color:#FFC107; } .pk-picker-icon-file { color:#888; } .pk-picker-name { flex:1; min-width:0; display:flex; align-items:center; font-size:14px; color:var(--pk-fg); } .pk-picker-name-text { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:0 1 auto; } .pk-picker-name .pk-tag-default { flex:0 0 auto !important; margin-left:6px; min-width:auto; height:16px; padding:0 6px; line-height:1 !important; display:inline-flex; align-items:center; justify-content:center; white-space:nowrap !important; overflow:visible; text-overflow:clip; padding-bottom:0 !important; margin-bottom:0 !important; } .pk-picker-size { font-size:12px; color:#999; margin-left:8px; } .pk-picker-error { color:#d93025; text-align:center; padding:20px; } .pk-picker-crumb-node { display:flex; align-items:center; height:100%; padding:0 6px; border-radius:4px; flex-shrink:0; transition:background 0.2s; white-space:nowrap; cursor:pointer; } .pk-picker-arrow { 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); } .pk-crumb-icon-img { width:18px; height:18px; object-fit:contain; flex-shrink:0; } .pk-crumb-icon-fallback { display:none; align-items:center; flex-shrink:0; } @keyframes pkFadeInOnly { from { opacity: 0; } to { opacity: 1; } } .pk-share-modal { width: 540px !important; box-sizing: border-box; display: flex; flex-direction: column; gap: 20px; padding: 0 !important; overflow: visible !important; } .pk-s-sec { display: flex; flex-direction: column; gap: 12px; padding: 0 28px; box-sizing: border-box; } .pk-detail-cancel-btn { cursor: pointer; color: var(--pk-pri); font-size: 15px; padding: 8px 12px; border-radius: 6px; transition: background 0.2s; margin-left: -12px; } .pk-detail-cancel-btn:hover { background: var(--pk-hl); } #pk_edit_pwd_cancel, #pk_edit_phrase_cancel, #pk_mod_cnt_cancel { display:inline-flex; align-items:center; justify-content:center; min-width:64px; height:32px; padding:0 14px; border-radius:8px; color:#666; transition:background 0.18s ease, box-shadow 0.18s ease, color 0.18s ease; } .pk-modal-ov:not(.pk-dark) #pk_edit_pwd_cancel:hover, .pk-modal-ov:not(.pk-dark) #pk_edit_phrase_cancel:hover, .pk-modal-ov:not(.pk-dark) #pk_mod_cnt_cancel:hover { background:rgba(0,0,0,0.06); box-shadow:0 2px 8px rgba(0,0,0,0.08); color:#444; } .pk-modal-ov.pk-dark #pk_edit_pwd_cancel:hover, .pk-modal-ov.pk-dark #pk_edit_phrase_cancel:hover, .pk-modal-ov.pk-dark #pk_mod_cnt_cancel:hover { background:rgba(255,255,255,0.08); box-shadow:0 2px 8px rgba(0,0,0,0.22); color:#ddd; } .pk-share-stat-box { display: flex; background: var(--pk-hl); border-radius: 10px; padding: 10px 0; margin-bottom: 18px; border: 1px solid var(--pk-bd); } .pk-share-stat-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; } .pk-share-stat-item:first-child::after { content: ''; position: absolute; right: 0; top: 25%; bottom: 25%; width: 1px; background: var(--pk-bd); } .pk-share-stat-val { font-size: 20px; font-weight: 700; color: var(--pk-fg); line-height: 1.1; font-family: "Inter", system-ui, sans-serif; } .pk-share-stat-lbl { font-size: 11px; color: #888; margin-top: 2px; font-weight: 500; } .pk-s-lbl { font-size: 13px; font-weight: 700; color: var(--pk-fg); opacity: 0.6; text-transform: uppercase; letter-spacing: 0.5px; } .pk-s-tabs { display: flex; background: var(--pk-hl); border-radius: 8px; padding: 4px; gap: 4px; border: 1px solid var(--pk-bd); } .pk-s-tab { flex: 1; height: 36px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-fg); font-size: 14px; border-radius: 6px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0.7; } .pk-s-tab.act { background: var(--pk-bg); color: var(--pk-pri); opacity: 1; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); font-weight: 700; } .pk-s-opts { display: flex; flex-flow: row nowrap; gap: 20px; align-items: center; justify-content: flex-start; } .pk-s-opt { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; color: var(--pk-fg); white-space: nowrap; flex-shrink: 0; } .pk-s-lbl { font-size: 13px; font-weight: 600; color: var(--pk-fg); opacity: 0.8; } .pk-s-tabs { display: flex; background: var(--pk-hl); border-radius: 6px; padding: 3px; gap: 2px; border: 1px solid var(--pk-bd); } .pk-s-tab { flex: 1; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--pk-fg); font-size: 13px; border-radius: 4px; transition: all 0.2s; opacity: 0.7; } .pk-s-tab.act { background: var(--pk-bg); color: var(--pk-pri); opacity: 1; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); font-weight: bold; } .pk-s-opts { display: flex; flex-wrap: nowrap; gap: 12px; align-items: center; justify-content: space-between; } .pk-s-opt { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; color: var(--pk-fg); white-space: nowrap; flex-shrink: 0; } .pk-s-opt input[type="radio"] { appearance: none; width: 16px; height: 16px; border: 1px solid var(--pk-icon-c); border-radius: 50%; position: relative; cursor: pointer; margin: 0; background: var(--pk-bg); transition: all 0.2s; } .pk-s-opt input:checked { border-color: var(--pk-pri); border-width: 5px; } .pk-s-input { flex: 1; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 4px; color: var(--pk-fg); padding: 5px 10px; font-size: 13px; outline: none; transition: border-color 0.2s; min-width: 0; } .pk-s-input:focus { border-color: var(--pk-pri); } .pk-s-input:disabled { opacity: 0.3; cursor: not-allowed; background: var(--pk-hl); } .pk-share-res-val { font-family: "SF Mono", "Consolas", monospace; font-weight: 600; color: var(--pk-pri) !important; } .pk-cal-pop { position: absolute; background: var(--pk-bg); color: var(--pk-fg); border: 1px solid var(--pk-bd); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); border-radius: 8px; z-index: 10005; display: flex; font-size: 13px; overflow: hidden; animation: pkFadeIn 0.2s; user-select: none; } .pk-cal-side { width: 110px; background: var(--pk-hl); border-right: 1px solid var(--pk-bd); display: flex; flex-direction: column; padding: 8px 0; } .pk-cal-side-item { padding: 8px 16px; cursor: pointer; color: var(--pk-fg); transition: background 0.2s; } .pk-cal-side-item:hover { background: rgba(0, 0, 0, 0.05); } .pk-cal-side-item.active { color: var(--pk-pri); font-weight: bold; background: var(--pk-bg); } .pk-cal-main { width: 280px; padding: 10px; display: flex; flex-direction: column; } .pk-cal-hd { display: flex; justify-content: space-between; align-items: center; padding: 0 8px 10px 8px; border-bottom: 1px solid var(--pk-bd); } .pk-cal-nav-btn { cursor:pointer; width:24px; height:24px; padding:0; border-radius:4px; color:#888; display:inline-flex; align-items:center; justify-content:center; line-height:0; } .pk-cal-nav-btn svg { display:block; flex:0 0 auto; } .pk-cal-nav-btn:hover { background:var(--pk-hl); color:var(--pk-pri); } .pk-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-top: 10px; } .pk-cal-th { text-align: center; color: #888; font-size: 12px; padding-bottom: 6px; } .pk-cal-td { height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; transition: all 0.2s; position: relative; color: var(--pk-fg); } .pk-cal-td:hover:not(.disabled):not(.selected) { background: var(--pk-hl); color: var(--pk-pri); } .pk-cal-td.selected { background: var(--pk-pri); color: #fff; font-weight: bold; } .pk-cal-td.disabled { color: #888; cursor: not-allowed; opacity: 0.7; } .pk-share-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; padding: 16px 24px 24px 24px; border-top: 1px solid var(--pk-bd); background: transparent; } .pk-btn-quiet-red { color: #d93025; font-size: 14px; font-weight: 500; cursor: pointer; padding: 8px 12px; border-radius: 6px; transition: background 0.2s; margin-left: -8px; } .pk-btn-quiet-red:hover { background: rgba(217, 48, 37, 0.08); } .pk-btn-primary-action { background: var(--pk-pri); color: #fff; border: none; height: 38px; padding: 0 20px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .pk-btn-primary-action:hover { filter: brightness(1.08); box-shadow: 0 2px 8px rgba(0, 103, 192, 0.25); } .pk-cal-td.today::after { content: ''; position: absolute; bottom: 4px; width: 4px; height: 4px; background: currentColor; border-radius: 50%; opacity: 0.5; } .pk-share-icon-wrap { position: relative; width: 30px; height: 30px; margin-right: 12px; flex-shrink: 0; transition: transform 0.2s ease; transform-origin: center center; display:flex; align-items:center; justify-content:center; line-height:0; overflow:visible; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; margin:0 !important; } .pk-share-icon-wrap > div:not(.pk-share-lock) { width:100%; height:100%; display:flex; align-items:center; justify-content:center; margin:0 !important; flex:0 0 100%; } .pk-share-icon-wrap > div:not(.pk-share-lock) > svg, .pk-share-icon-wrap > svg { width:100%; height:100%; display:block !important; margin:0 !important; transform:scale(0.94) !important; transform-origin:center center !important; } .pk-share-icon-disabled { transform:none !important; } .pk-share-lock { position: absolute; bottom: -2px; right: -4px; width: 14px; height: 14px; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 10; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); border: none; box-shadow: none; pointer-events: none; } .pk-ov.pk-dark .pk-share-lock { filter: drop-shadow(0 0 2px rgba(0, 0, 0, 1)) drop-shadow(0 2px 5px rgba(0, 0, 0, 1)); } .pk-name { display: flex !important; align-items: center; min-width: 0; width: 100%; overflow: hidden; } .pk-name-txt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .pk-select-item { padding: 10px 12px; border-radius: 5px; cursor: pointer; color: var(--pk-fg); font-size: 14px; transition: background 0.1s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-select-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-select-item.act { background: rgba(0, 103, 192, 0.1); color: var(--pk-pri); font-weight: 700; } .pk-select-label { position: absolute; top: 0; transform: translate3d(0, -50%, 0); -webkit-transform: translate3d(0, -50%, 0); backface-visibility: hidden; -webkit-backface-visibility: hidden; will-change: transform; left: 10px; background: var(--pk-bg); padding: 0 5px; font-size: 11px; color: var(--pk-pri); font-weight: bold; pointer-events: none; z-index: 10; line-height: 1; pointer-events: none; } .pk-field input, .pk-field select, .pk-share-modal input, .pk-ov input { transform: translateZ(0); will-change: transform; } .pk-maximized { position: fixed !important; width: 100% !important; height: 100% !important; max-width: none !important; top: 0 !important; left: 0 !important; border-radius: 0 !important; border: none !important; z-index: 2147483647 !important; } .pk-maximized .pk-sidebar { width: 190px !important; align-items: flex-start !important; padding: 20px 10px !important; overflow-x: hidden !important; box-sizing: border-box !important; } .pk-maximized .pk-nav-btn { width: 100% !important; max-width: 100% !important; justify-content: flex-start !important; padding: 0 15px !important; gap: 10px; height: 54px !important; border-radius: 8px !important; box-sizing: border-box !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-quota-wrap { margin-top: auto; width: 100%; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; } #pk-quota-head { width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; gap: 2px; line-height: 1; } #pk-transfer-detail-btn { order: 1; display: flex; align-items: center; justify-content: center; width: 24px; min-width: 24px; height: 18px; margin: 0 0 2px 0; color: var(--pk-fg); opacity: 0.72; cursor: pointer; user-select: none; transition: none; line-height: 1; } #pk-quota-txt { order: 2; } #pk-transfer-detail-btn svg { width: 18px !important; height: 18px !important; display: block; } #pk-transfer-detail-btn:hover { color: var(--pk-pri); }1.04); } .pk-maximized #pk-quota-wrap { margin-top: auto !important; width: 100% !important; display: flex !important; flex-direction: column !important; justify-content: flex-end !important; align-items: stretch !important; padding: 0 10px 6px 10px !important; box-sizing: border-box !important; } .pk-maximized #pk-quota-head { flex-direction: row !important; justify-content: space-between !important; align-items: flex-end !important; gap: 6px !important; } .pk-maximized #pk-transfer-detail-btn { order: 2; display: flex !important; align-items: flex-end !important; margin: 0 0 -1px 6px !important; } .pk-maximized #pk-quota-txt { order: 1; } .pk-maximized #pk-quota-panel { width: 100% !important; align-items: flex-start !important; padding: 0 !important; margin-bottom: 0 !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-share-parse-root-mode .pk-ft { display:none !important; } .pk-share-parse-root-mode .pk-vp { padding:0 !important; } .pk-share-parse-panel { width:min(560px,calc(100% - 48px)); height:100%; margin:0 auto; display:flex; flex-direction:column; justify-content:center; gap:14px; color:var(--pk-fg); box-sizing:border-box; padding-bottom:0; } .pk-share-parse-panel input { width:100%; height:42px; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); padding:0 12px; box-sizing:border-box; font-size:14px; outline:none; } .pk-share-parse-panel input:focus { border-color:var(--pk-pri); box-shadow:none; } .pk-share-parse-actions { display:grid; grid-template-columns:repeat(4,minmax(max-content,1fr)); gap:8px; align-items:center; } .pk-share-parse-panel .pk-share-parse-btn { width:100%; min-width:max-content; height:36px; padding:0 14px; border:none; border-radius:6px; background:var(--pk-pri); color:#fff; cursor:pointer; font-weight:700; white-space:nowrap; box-sizing:border-box; } .pk-share-parse-panel .pk-share-parse-btn:disabled { opacity:.62; cursor:not-allowed; } .pk-share-parse-desc { margin:0; font-size:13px; color:#888; line-height:1.5; } .pk-share-parse-result { border:1px solid var(--pk-bd); border-radius:6px; padding:12px 14px; font-size:13px; line-height:1.65; background:var(--pk-hl); color:var(--pk-fg); word-break:break-all; } .pk-share-parse-result.ok { border-color:rgba(82,196,26,.35); background:rgba(82,196,26,.08); } .pk-share-parse-result.err { border-color:rgba(217,48,37,.35); background:rgba(217,48,37,.08); color:#d93025; } .pk-share-parse-result.loading { opacity:.78; } .pk-share-parse-list-state { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; flex-direction:column; gap:12px; color:var(--pk-fg); opacity:.72; font-size:13px; pointer-events:none; } .pk-share-parse-list-state.err { color:#d93025; opacity:.9; } .pk-share-parse-panel .pk-share-parse-btn.sub { background:var(--pk-hl); color:var(--pk-fg); border:1px solid var(--pk-bd); } .pk-share-history-wrap { width:min(760px,92vw); height:min(640px,82vh); display:flex; flex-direction:column; overflow:hidden; color:var(--pk-fg); box-sizing:border-box; } .pk-share-history-head { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:4px 4px 14px 4px; flex-shrink:0; } .pk-share-history-title { margin:0; border:none; font-size:18px; font-weight:700; color:var(--pk-fg); line-height:1.3; } .pk-share-history-search-wrap { width:100%; height:38px; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-bg); color:var(--pk-fg); padding:0 8px 0 12px; box-sizing:border-box; outline:none; font-size:14px; flex-shrink:0; display:flex; align-items:center; gap:6px; } .pk-share-history-search-wrap:focus-within { border-color:var(--pk-pri); box-shadow:none; } .pk-share-history-search { border:0; outline:0; background:transparent; color:var(--pk-fg); min-width:0; flex:1; height:100%; padding:0; font-size:14px; box-sizing:border-box; } .pk-share-history-search:focus { box-shadow:none; } .pk-share-history-search-action { width:22px; height:22px; border:0; border-radius:999px; background:transparent; color:#999; opacity:1; display:inline-flex; align-items:center; justify-content:center; padding:0; flex:0 0 auto; cursor:pointer; transition:background-color .16s ease,opacity .16s ease,transform .16s ease,color .16s ease; } .pk-share-history-search-action:hover { opacity:1; color:#999; background:transparent; } .pk-share-history-search-action:active { transform:scale(.92); background:transparent; } .pk-share-history-search-action svg { width:16px !important; height:16px !important; opacity:1; flex-shrink:0; color:inherit !important; } .pk-share-history-search-clear { width:20px; height:20px; display:none; color:#999; border-radius:50%; } .pk-share-history-search-wrap.has-value .pk-share-history-search-clear { display:inline-flex; } .pk-share-history-search-clear:hover { opacity:1; background:var(--pk-hl); color:#666; } .pk-share-history-search-clear:active { transform:scale(.92); background:var(--pk-hl); } .pk-share-history-search-submit { color:#999; } .pk-share-history-search-submit:hover { opacity:1; background:transparent; color:var(--pk-pri); } .pk-share-history-search-submit:active { background:transparent; color:var(--pk-pri); } .pk-share-history-list { flex:1 1 0; min-height:0; overflow-y:auto; overflow-x:hidden; display:flex; flex-direction:column; gap:8px; padding:12px 4px; margin-top:10px; border-top:1px solid var(--pk-bd); border-bottom:1px solid var(--pk-bd); box-sizing:border-box; overscroll-behavior:contain; } .pk-share-history-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:12px; padding:12px; border:1px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); box-sizing:border-box; } .pk-share-history-main { min-width:0; display:flex; flex-direction:column; gap:5px; } .pk-share-history-name { font-size:14px; font-weight:700; color:var(--pk-fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .pk-share-history-hit { display:inline-block; padding:0 2px; border-radius:3px; background:#fff1bf; color:#ff5a1f; font-weight:800; line-height:1.2; box-shadow:inset 0 0 0 1px rgba(255,138,0,.16); } .pk-share-history-meta { font-size:12px; color:#888; display:flex; flex-wrap:wrap; gap:6px 10px; line-height:1.45; } .pk-share-history-note { font-size:12px; color:var(--pk-fg); opacity:.86; line-height:1.55; word-break:break-word; white-space:pre-wrap; overflow:visible; } .pk-share-history-note-title { border:none; margin-bottom:18px; font-size:18px; font-weight:700; color:var(--pk-fg); font-family:inherit; } .pk-share-history-note-wrap { position:relative; width:420px; max-width:82vw; } .pk-share-history-note-wrap::after { content:""; position:absolute; right:2px; bottom:2px; width:16px; height:16px; pointer-events:none; border-radius:0 0 6px 0; background:linear-gradient(135deg,transparent 0 56%,rgba(150,158,170,.72) 56% 60%,transparent 60% 68%,rgba(150,158,170,.72) 68% 72%,transparent 72%); } .pk-keyword-resize-wrap { position:relative; } .pk-keyword-resize-wrap::after { content:""; position:absolute; right:2px; bottom:2px; width:16px; height:16px; pointer-events:none; border-radius:0 0 6px 0; background:linear-gradient(135deg,transparent 0 56%,rgba(150,158,170,.72) 56% 60%,transparent 60% 68%,rgba(150,158,170,.72) 68% 72%,transparent 72%); } .pk-keyword-resize-input { display:block; width:100%; height:42px; min-height:42px; max-height:160px; padding:12px 22px 8px 12px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; line-height:18px; outline:none; transition:border-color .2s; box-sizing:border-box; font-family:inherit; resize:vertical; } .pk-keyword-resize-input::-webkit-scrollbar,.pk-keyword-resize-input::-webkit-scrollbar-thumb,.pk-keyword-resize-input::-webkit-scrollbar-track { cursor:default; } .pk-keyword-resize-input::-webkit-scrollbar-corner { background:transparent; } .pk-keyword-resize-input::-webkit-resizer { background:transparent; border:0; } .pk-share-history-note-input { display:block; width:100%; max-width:100%; height:120px; padding:12px 18px 12px 12px; border:1px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; line-height:1.5; resize:vertical; outline:none; box-sizing:border-box; font-family:inherit; } .pk-share-history-note-input::-webkit-scrollbar,.pk-share-history-note-input::-webkit-scrollbar-thumb,.pk-share-history-note-input::-webkit-scrollbar-track { cursor:default; } .pk-share-history-note-input::-webkit-scrollbar-corner { background:transparent; } .pk-share-history-note-input::-webkit-resizer { background:transparent; border:0; } .pk-dl-filter-area-wrap { position:relative; width:100%; } .pk-dl-filter-area-wrap::after { content:""; position:absolute; right:2px; bottom:2px; width:16px; height:16px; pointer-events:none; border-radius:0 0 6px 0; background:linear-gradient(135deg,transparent 0 56%,rgba(150,158,170,.72) 56% 60%,transparent 60% 68%,rgba(150,158,170,.72) 68% 72%,transparent 72%); } #pk_dl_group #set_dl_filter_ext,#pk_dl_group #set_dl_filter_name { display:block; padding-right:22px!important; } #pk_dl_group #set_dl_filter_ext::-webkit-scrollbar-corner,#pk_dl_group #set_dl_filter_name::-webkit-scrollbar-corner { background:transparent; } #pk_dl_group #set_dl_filter_ext::-webkit-resizer,#pk_dl_group #set_dl_filter_name::-webkit-resizer { background:transparent; border:0; } .pk-share-history-note-input:focus { border-color:var(--pk-pri); box-shadow:none; } .pk-share-history-note-act { display:flex; justify-content:flex-end; gap:12px; margin-top:18px; } .pk-share-history-note-act .pk-btn { height:38px; padding:0 22px; border-radius:8px; font-family:inherit; } .pk-share-history-note-act .pri { background:var(--pk-pri)!important; color:#fff!important; border-color:var(--pk-pri)!important; font-weight:700; } .pk-share-history-note-act .pri:hover,#pk_share_history_close.pk-share-history-close:hover { filter:brightness(1.08); box-shadow:none!important; } #pk_share_history_close.pk-share-history-close { background:var(--pk-pri)!important; color:#fff!important; border-color:var(--pk-pri)!important; font-weight:700; } .pk-share-history-actions { display:flex; flex-direction:column; gap:6px; align-items:stretch; justify-content:center; } .pk-share-history-actions button { height:28px; min-width:78px; padding:0 10px; border:1px solid var(--pk-bd); border-radius:6px; background:var(--pk-hl); color:var(--pk-fg); cursor:pointer; font-size:12px; white-space:nowrap; } .pk-share-history-actions button:hover { border-color:var(--pk-pri); color:var(--pk-pri); } .pk-share-history-actions button.pri { background:var(--pk-pri); border-color:var(--pk-pri); color:#fff; font-weight:700; } .pk-share-history-actions button.danger { color:#d93025; } .pk-share-history-actions button.danger:hover { border-color:#d93025; color:#d93025; } .pk-share-history-empty { flex:1; min-height:220px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:0; color:var(--pk-fg); text-align:center; padding:24px; box-sizing:border-box; } .pk-share-history-empty-icon { width:auto; height:auto; display:flex; align-items:center; justify-content:center; opacity:1; } .pk-share-history-empty-icon svg { width:35vmin; max-width:220px; height:auto; margin-bottom:3vmin; filter:drop-shadow(0 15px 25px rgba(0,0,0,.05)); } .pk-share-history-empty-title { font-size:clamp(13px,3vmin,16px); color:#94a3b8; font-weight:500; letter-spacing:1px; } .pk-ov.pk-dark .pk-share-history-empty-title { color:#666e75; } .pk-share-history-foot { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 4px 2px 4px; flex-shrink:0; } .pk-share-history-foot .pk-btn { height:36px; padding:0 18px; border-radius:6px; } .pk-share-history-count { color:#888; font-size:12px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } @media (max-width:680px) { .pk-share-history-item { grid-template-columns:1fr; } .pk-share-history-actions { flex-direction:row; flex-wrap:wrap; justify-content:flex-start; } .pk-share-history-actions button { min-width:0; } } .pk-share-insight-path { color:var(--pk-fg); font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } #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: 50px !important; height: 50px !important; } .pk-maximized .pk-tb { height: 60px !important; padding: 0 16px !important; gap: 10px !important; } .pk-maximized .pk-btn { height: 40px !important; font-size: 15px !important; padding: 0 16px !important; } .pk-btn svg { width: auto; height: auto; } #pk-down svg { width: 16px !important; height: 16px !important; display: block !important; transform: none !important; transform-origin: center center !important; shape-rendering: geometricPrecision; } #pk-theme svg, #pk-maximize svg { width: 16px !important; height: 16px !important; } #pk-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-close svg { width: 28px !important; height: 28px !important; } .pk-maximized .pk-grid-hd { height: 50px !important; font-size: 15px !important; padding: 0 26px 0 20px !important; } .pk-maximized:not(.pk-grid-view) .pk-row, .pk-maximized:not(.pk-grid-view) .pk-group-hd { height: 60px !important; font-size: 16px !important; padding: 0 20px !important; } .pk-maximized:not(.pk-grid-view) .pk-group-hd { border-top-width: 10px !important; border-bottom-width: 10px !important; } .pk-maximized.pk-grid-view .pk-group-hd { height: 60px !important; font-size: 16px !important; padding: 0 20px !important; border-top-width: 10px !important; border-bottom-width: 10px !important; box-sizing: border-box !important; align-items: center !important; } .pk-maximized.pk-grid-view .pk-group-hd > div { height: 100% !important; display: flex !important; align-items: center !important; } .pk-maximized.pk-grid-view .pk-group-hd .pk-name { height: 100% !important; display: flex !important; align-items: center !important; min-height: 0 !important; } .pk-maximized.pk-grid-view .pk-group-hd .pk-name .pk-name-txt, .pk-maximized.pk-grid-view .pk-group-hd .pk-name span { display: inline-flex !important; align-items: center !important; height: 100% !important; line-height: 1 !important; padding-top: 0 !important; padding-bottom: 0 !important; margin-top: 0 !important; } .pk-maximized:not(.pk-grid-view) .pk-name svg { width: 60px !important; height: 60px !important; margin-right: 20px !important; } .pk-maximized:not(.pk-grid-view) .pk-row .pk-name .pk-name-txt { white-space: normal !important; display: -webkit-box !important; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden !important; text-overflow: ellipsis !important; line-height: 1.35 !important; word-break: break-all !important; margin-top: 0 !important; } .pk-maximized.pk-grid-view .pk-row { padding: 0 !important; } .pk-max-icon-box { position: relative !important; width: 50px !important; height: 50px !important; margin-right: 20px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; flex-shrink: 0 !important; vertical-align: middle !important; overflow: visible !important; } .pk-placeholder-icon { position: absolute !important; inset: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; z-index: 1 !important; transition: opacity 0.2s, visibility 0.2s; } .pk-placeholder-icon svg { width: 44px !important; height: 44px !important; object-fit: contain !important; } .pk-max-icon-box .pk-max-thumb { position: absolute !important; top: 0 !important; left: 0 !important; width: 50px !important; height: 50px !important; object-fit: cover !important; border-radius: 4px !important; opacity: 0; z-index: 2 !important; transition: opacity 0.25s ease-in-out; background: transparent; } .pk-maximized .pk-share-icon-wrap { width: 50px !important; height: 50px !important; margin-right: 20px !important; background: transparent !important; overflow: visible !important; } .pk-maximized .pk-row:has(.pk-max-thumb) .pk-name-txt { padding-left: 0 !important; } .pk-maximized .pk-nav-btn svg { width: 28px !important; height: 28px !important; } #pk-nav-share-parse svg { width:22px !important; height:22px !important; } #pk-nav-link-bookmark svg { width:26px !important; height:26px !important; } .pk-maximized #pk-nav-share-parse svg { width:26px !important; height:26px !important; } .pk-maximized #pk-nav-link-bookmark svg { width:31px !important; height:31px !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), body:has(#pk-audio-ov) { #pk-launch { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } } .pk-maximized .pk-search input { height: 40px !important; font-size: 15px !important; padding: 0 70px 0 15px !important; } .pk-maximized #pk-search-btn { width: 18px !important; height: 18px !important; right: 14px !important; } .pk-maximized .pk-search-clear { width: 24px !important; height: 24px !important; right: 40px !important; } .pk-maximized input[type="checkbox"] { width: 18px !important; height: 18px !important; transform: none !important; margin: 0 8px 0 0 !important; } .pk-maximized .pk-star-icon, .pk-maximized .pk-star-toggle, .pk-maximized .pk-col[data-k="starred"] svg { width: 16px !important; height: 16px !important; } .pk-maximized .pk-view-switch { padding: 4px !important; } .pk-maximized .pk-view-btn { width: 38px !important; height: 32px !important; } .pk-maximized .pk-grid-hd { height: 58px !important; } .pk-maximized .pk-grid-hd.pk-grid-view-hd { padding: 0 24px 0 20px !important; gap: 14px !important; overflow: visible !important; } .pk-maximized .pk-grid-check-tools { padding-left: 5px !important; box-sizing: border-box !important; } .pk-maximized .pk-grid-sort-trigger { height: 36px !important; font-size: 13px !important; padding: 0 14px !important; min-width: 122px !important; } .pk-maximized .pk-grid-view .pk-row { border-radius: 22px !important; } .pk-maximized .pk-grid-card-body { padding: 12px 12px 14px !important; gap: 12px !important; } .pk-maximized .pk-gv-check { top: 18px !important; left: 18px !important; width: 22px !important; height: 22px !important; } .pk-maximized .pk-gv-more { top: 16px !important; right: 16px !important; width: 28px !important; height: 28px !important; } .pk-maximized .pk-gv-cover { height: calc(100% - 86px) !important; min-height: 180px !important; border-radius: 18px !important; } .pk-maximized .pk-gv-cover .pk-gv-icon svg, .pk-maximized .pk-gv-cover .pk-gv-icon img { width: 122px !important; height: 122px !important; max-width: 122px !important; max-height: 122px !important; } .pk-maximized .pk-gv-folder-shell { width: 216px !important; max-width: 216px !important; height: 152px !important; min-height: 152px !important; margin-top: 12px !important; transform: translateY(12px) !important; } .pk-maximized .pk-gv-folder-back { top: 19px !important; border-radius: 12px !important; } .pk-maximized .pk-gv-folder-tab { top: 6px !important; left: 17px !important; width: 68px !important; height: 25px !important; border-radius: 10px 10px 0 0 !important; } .pk-maximized .pk-gv-folder-preview { top: 0 !important; left: 17px !important; right: 17px !important; bottom: 25px !important; border-radius: 10px !important; } .pk-maximized .pk-gv-folder-fallback, .pk-maximized .pk-gv-folder-fallback svg { width: 116px !important; height: 116px !important; max-width: 116px !important; max-height: 116px !important; } .pk-maximized .pk-gv-folder-front { height: 90px !important; border-radius: 12px !important; } .pk-maximized .pk-gv-info { grid-template-rows:22px 18px !important; row-gap:4px !important; align-self:stretch !important; width:100% !important; min-width:0 !important; height:44px !important; min-height:44px !important; padding:0 2px !important; text-align:left !important; } .pk-maximized.pk-grid-view .pk-name .pk-name-txt { font-size:16px !important; line-height:22px !important; text-align:left !important; } .pk-maximized.pk-grid-view .pk-name { display:flex !important; align-items:center !important; justify-content:flex-start !important; align-self:stretch !important; width:100% !important; min-width:0 !important; height:22px !important; overflow:hidden !important; text-align:left !important; } .pk-maximized.pk-grid-view .pk-share-icon-wrap { width: 40px !important; height: 40px !important; margin-right: 16px !important; position: relative !important; display: block !important; overflow: visible !important; } .pk-maximized.pk-grid-view .pk-share-icon-wrap { width: 50px !important; height: 50px !important; margin-right: 20px !important; display: flex !important; align-items: center !important; justify-content: center !important; transform: none !important; overflow: visible !important; } .pk-maximized.pk-grid-view .pk-share-icon-wrap > svg { width: 100% !important; height: 100% !important; min-width: 100% !important; min-height: 100% !important; transform: scale(1.25) !important; transform-origin: center center !important; margin: 0 !important; } .pk-maximized:not(.pk-grid-view) .pk-name { display:flex !important; align-items:center !important; min-width:0 !important; width:100% !important; height:auto !important; overflow:visible !important; text-align:left !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap { position:relative !important; width:50px !important; height:50px !important; min-width:50px !important; min-height:50px !important; margin-right:20px !important; display:flex !important; align-items:center !important; justify-content:center !important; flex:0 0 50px !important; transform:none !important; overflow:visible !important; background:transparent !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap > svg { width:100% !important; height:100% !important; min-width:100% !important; min-height:100% !important; margin:0 !important; transform:scale(1.16) !important; transform-origin:center center !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap > div:not(.pk-share-lock) { width:100% !important; height:100% !important; display:flex !important; align-items:center !important; justify-content:center !important; transform:none !important; margin:0 !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap > div:not(.pk-share-lock) > svg { width:100% !important; height:100% !important; min-width:100% !important; min-height:100% !important; transform:scale(1.16) !important; transform-origin:center center !important; margin:0 !important; } .pk-maximized:not(.pk-grid-view) .pk-share-icon-wrap img { width:100% !important; height:100% !important; min-width:100% !important; min-height:100% !important; object-fit:contain !important; margin:0 !important; transform:none !important; } .pk-maximized:not(.pk-grid-view) .pk-share-lock { width:18px !important; height:18px !important; bottom:-2px !important; right:-6px !important; z-index:15 !important; } .pk-maximized:not(.pk-grid-view) .pk-share-lock svg { width:100% !important; height:100% !important; min-width:100% !important; min-height:100% !important; margin:0 !important; transform:none !important; display:block !important; color:#fff !important; } .pk-maximized .pk-row > div { font-size: 16px !important; font-weight: normal !important; } .pk-maximized .pk-row div[style*="font-size:12px"], .pk-maximized .pk-row div[style*="font-size: 12px"] { font-size: 15px !important; font-weight: 400 !important; opacity: 0.9; } .pk-maximized .pk-row div[style*="tabular-nums"] { font-weight: 400 !important; } .pk-maximized .pk-ft { height: 50px !important; font-size: 15px !important; padding: 0 20px !important; } .pk-maximized .pk-stat { font-size: 15px !important; } .pk-maximized .pk-ft .pk-btn { height: 36px !important; font-size: 15px !important; } .pk-maximized #pk-filter-cat-label { height: 40px !important; font-size: 15px !important; padding: 0 16px !important; } .pk-maximized #pk-filter-exts-wrap { height: 40px !important; } .pk-maximized .pk-f-ext { font-size: 15px !important; padding: 6px 12px !important; } .pk-maximized #pk-filter-exit-btn { height: 40px !important; font-size: 15px !important; padding: 0 20px !important; } .pk-maximized .pk-bl-area { font-size: 15px !important; line-height: 1.6 !important; } .pk-maximized #pk-crumb span { font-size: 16px !important; display: inline-flex !important; align-items: center !important; height: 32px !important; padding: 0 8px !important; border-radius: 4px !important; margin: auto 2px !important; } .pk-maximized #pk-crumb div span { font-size: 18px !important; } .pk-maximized #pk-crumb div span[style*="font-size:11px"], .pk-maximized #pk-crumb div span[style*="font-size: 11px"] { font-size: 14px !important; margin-left: 12px !important; } .pk-maximized #pk-crumb svg { width: 18px !important; height: 18px !important; vertical-align: middle !important; margin-top: -1px !important; } .pk-maximized .pk-crumb-sep { width: 26px !important; height: 26px !important; margin: auto 4px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; } .pk-maximized .pk-crumb-sep svg { width: 20px !important; height: 20px !important; } .pk-custom-select { position: relative; width: 100%; } .pk-select-trigger { display: flex; align-items: center; justify-content: space-between; height: 44px; padding: 0 15px; border: 2px solid var(--pk-bd); border-radius: 8px; background: var(--pk-bg); color: var(--pk-fg); font-size: 14px; font-weight: 600; cursor: pointer; box-sizing: border-box; transition: all 0.2s; } .pk-select-trigger:hover { border-color: var(--pk-pri); } .pk-select-trigger svg { width: 16px !important; height: 16px !important; min-width: 16px; min-height: 16px; color: #999; flex-shrink: 0; } .pk-dropdown-wrap { position: relative; display: inline-flex; height: 32px; align-items: center; } .pk-dropdown-menu { position: absolute; top: 100%; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); display: none; z-index: 10010; min-width: 140px; padding: 4px 0; margin-top: 6px; flex-direction: column; overflow: hidden; } .pk-dropdown-item { padding: 10px 16px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--pk-fg); font-size: 13px; transition: background 0.1s; white-space: nowrap; } .pk-dropdown-item:hover { background: var(--pk-hl); color: var(--pk-pri); } .pk-pop-max { width: 170px !important; min-width: 170px !important; padding: 6px 0 !important; border-radius: 10px !important; box-shadow: 0 8px 30px rgba(0,0,0,0.3) !important; } .pk-pop-max .pk-dropdown-item { padding: 12px 15px !important; font-size: 15px !important; gap: 10px !important; font-weight: 600 !important; } .pk-pop-max .pk-dropdown-item svg { width: 22px !important; height: 22px !important; } .pk-btn-arrow { margin-left: 2px; opacity: 0.6; transition: transform 0.2s; } .pk-aria-status-box { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: bold; margin-top: 6px; transform: translateZ(0); -webkit-transform: translateZ(0); backface-visibility: hidden; will-change: transform; cursor: default; } .pk-setting-fieldset{position:relative;padding:25px 15px 15px 15px;border:2px solid var(--pk-bd);border-radius:8px;transition:border-color .2s;transform:translateZ(0);backface-visibility:hidden;}.pk-setting-fieldset.pk-select-open{z-index:10040;}.pk-custom-select.pk-select-open{z-index:10050!important;}.pk-setting-fieldset:hover:not(.pk-inner-active){border-color:var(--pk-pri);}.pk-setting-fieldset.pk-inner-active{border-color:var(--pk-bd)!important;}.pk-setting-stack{display:flex;flex-direction:column;gap:15px;}.pk-setting-checkrow{display:flex;align-items:center;justify-content:space-between;height:44px;min-height:44px;border:2px solid var(--pk-bd);border-radius:8px;padding:0 12px;cursor:pointer;background:var(--pk-bg);transition:border-color .2s;box-sizing:border-box;}.pk-setting-checkrow:hover{border-color:var(--pk-pri);}.pk-setting-checkrow span{font-size:14px;font-weight:400;color:var(--pk-fg);user-select:none;line-height:1.4;}.pk-setting-checkrow input{width:16px;height:16px;accent-color:var(--pk-pri);cursor:pointer;flex-shrink:0;margin-left:12px;}.pk-modal-ov [data-setting-key].pk-setting-focus-flash{outline:2px solid rgba(0,103,192,.68);outline-offset:3px;background:rgba(0,103,192,.08);border-radius:8px;transition:background-color .2s,outline-color .2s;}.pk-gopeed-help{font-size:12px;color:#888;line-height:1.55;background:var(--pk-hl);border:1px solid var(--pk-bd);border-radius:8px;padding:10px 12px;} .pk-token-eye { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 28px; height: 28px; border: none; border-radius: 6px; background: transparent; color: #8a94a4; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; transition: background 0.15s, color 0.15s; z-index: 20; } .pk-token-eye:hover { background: rgba(0,103,192,0.1); color: var(--pk-pri); } .pk-token-eye svg { width: 18px; height: 18px; display: block; } .pk-aria-dot { width: 8px; height: 8px; border-radius: 50%; background: #ccc; transform: translateZ(0); } .pk-aria-dot.ok { background: #52c41a; box-shadow: 0 0 8px rgba(82, 196, 26, 0.5); } .pk-aria-dot.err { background: #ff4d4f; } .pk-aria-dot.wait { background: var(--pk-pri); animation: pk-pulse 1.5s infinite; } .pk-dropdown-wrap.active .pk-btn-arrow { transform: rotate(180deg); } .pk-select-menu { position: absolute; top: 100%; left: 0; right: 0; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; margin-top: 6px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); z-index: 10010; display: none; overflow: hidden; max-height: 240px; } .pk-share-icon-wrap { position: relative; width: 30px; height: 30px; margin-right: 12px; flex-shrink: 0; display:flex; align-items:center; justify-content:center; transform:none !important; } .pk-share-icon-wrap > svg, .pk-share-icon-wrap > img { width: 100%; height: 100%; display: block; flex-shrink: 0; } .pk-share-icon-wrap > div:not(.pk-share-lock) { width:100%; height:100%; display:flex; align-items:center; justify-content:center; transform:none !important; margin:0 !important; } .pk-share-icon-wrap > div:not(.pk-share-lock) > svg, .pk-share-icon-wrap > svg { transform:scale(0.94) !important; transform-origin:center center !important; } .pk-share-lock { position: absolute; bottom: -3px; right: -6px; width: 17px; height: 17px; background: transparent; display: flex; align-items: center; justify-content: center; z-index: 10; filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); border: none; box-shadow: none; pointer-events: none; } .pk-ov.pk-dark .pk-share-lock { filter: drop-shadow(0 0 2px rgba(0, 0, 0, 1)) drop-shadow(0 2px 5px rgba(0, 0, 0, 1)); } .pk-bl-marker { position: absolute !important; bottom: 0px !important; left: 4px !important; width: 10px !important; height: 10px !important; display: flex !important; align-items: center; justify-content: center; z-index: 99 !important; pointer-events: none; filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 2px rgba(255,255,255,0.5)); } .pk-maximized .pk-bl-marker { width: 18px !important; height: 18px !important; bottom: -1px !important; left: 9px !important; } .pk-min-icon, .pk-max-icon-box { position: relative !important; overflow: visible !important; display: inline-flex !important; } .pk-min-icon { width:24px; height:24px; align-items:center; justify-content:center; vertical-align:middle; margin-right:12px; flex-shrink:0; } .pk-min-icon-inner { display:flex; transform:translateX(4px) scale(0.96); } .pk-min-icon-img { width:24px; height:24px; object-fit:contain; border-radius:4px; } .pk-min-icon-fallback { display:none; align-items:center; justify-content:center; } .pk-min-media-box { width:24px; height:24px; margin-right:12px; position:relative; flex-shrink:0; display:inline-flex; vertical-align:middle; overflow:visible !important; border-radius:4px; } .pk-min-media-inner { position:absolute; inset:0; overflow:hidden; border-radius:4px; z-index:1; } .pk-min-ph { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; z-index:1; transition:opacity 0.2s; } .pk-min-ph-img { width:100%; height:100%; object-fit:contain; border-radius:4px; pointer-events:none; } .pk-min-thumb { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:2; transition:opacity 0.2s; } .pk-active-border { border-color: var(--pk-pri) !important; } #pk_dl_group { transform: translateZ(0); backface-visibility: hidden; will-change: border-color; } #pk_video_playback_group:hover, #pk_dl_group:hover, #pk_download_accel_group:hover { border-color: var(--pk-pri) !important; } #pk_dl_group.pk-typing-active:hover, #pk_download_accel_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; } 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-audio-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-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: -60px; 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; } html.pk-player-web-fullscreen-lock, body.pk-player-web-fullscreen-lock { overflow: hidden !important; } .pk-web-fullscreen-ov { position: fixed !important; inset: 0 !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; max-width: none !important; max-height: none !important; margin: 0 !important; padding: 0 !important; transform-origin: 0 0 !important; display: block !important; justify-content: initial !important; align-items: initial !important; background: #000 !important; overflow: hidden !important; z-index: 2147483646 !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen { position: absolute !important; inset: 0 !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; min-width: 0 !important; max-width: none !important; max-height: none !important; transform: none !important; margin: 0 !important; border-radius: 0 !important; overflow: hidden !important; z-index: 1 !important; box-shadow: none !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active { height: 100vh !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-player-top { position: fixed !important; top: 0 !important; left: 0 !important; right: 0 !important; transform: none !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen #pk_video, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen #pk_p_poster { height: 100% !important; bottom: auto !important; top: 0 !important; transform: translate3d(0, 0, 0) !important; transform-origin: center center !important; will-change: transform; backface-visibility: hidden; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active #pk_video, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active #pk_p_poster { height: calc(100% - 84px) !important; transform: translate3d(0, 0, 0) !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active .pk-p-side-nav { top: 50% !important; transform: translateY(calc(-50% - 42px)) translateZ(0) !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active .pk-p-center-play, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active .pk-p-seek-indicator, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active .pk-p-loading { top: 50% !important; transform: translate(-50%, calc(-50% - 42px)) translateZ(0) !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen #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-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) #pk_p_plist { transform: translateY(100%) translateZ(0) !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-p-plist-strip { opacity: 0 !important; pointer-events: none !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-player-controls, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-p-prog-wrap, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-p-resume-toast { transform: translateY(0) translateZ(0) !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-p-side-nav { top: 50% !important; transform: translateY(-50%) translateZ(0) !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-p-center-play, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-p-seek-indicator, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen:not(.plist-active) .pk-p-loading { top: 50% !important; transform: translate(-50%, -50%) translateZ(0) !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen #pk_video, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen #pk_p_poster { transition: height 0.2s cubic-bezier(0.4, 0, 0.2, 1), transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-player-controls, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-p-prog-wrap, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-p-resume-toast { transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-p-side-nav, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-p-center-play, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-p-seek-indicator, .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-p-loading { transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease !important; will-change: transform, opacity; backface-visibility: hidden; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.paused.pk-v-started:not(.buffering):not(.pk-is-seeking) .pk-p-center-play { animation: none !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .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-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active #pk_p_plist { transform: translateY(0); } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active .pk-player-controls { bottom: 0 !important; transform: translateY(-84px) translateZ(0); } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active .pk-p-prog-wrap { bottom: 64px !important; transform: translateY(-84px) translateZ(0); } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen.plist-active .pk-p-resume-toast { bottom: 85px !important; transform: translateY(-84px) translateZ(0); } #pk_p_box:fullscreen #pk_p_web_full, #pk_p_box:-webkit-full-screen #pk_p_web_full, #pk_p_box:-moz-full-screen #pk_p_web_full { display: none !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: height 0.52s cubic-bezier(0.22, 1, 0.36, 1), top 0.52s cubic-bezier(0.22, 1, 0.36, 1); } .pk-player-controls { position: absolute; bottom: 0; left: 0; right: 0; z-index: 80 !important; transition: bottom 0.52s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease; } .pk-p-prog-wrap { position: absolute; bottom: 64px; left: 20px; right: 20px; z-index: 60 !important; display: flex; align-items: center; gap: 16px; transition: bottom 0.52s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease; 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.52s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease, transform 0.52s cubic-bezier(0.22, 1, 0.36, 1) !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.6s cubic-bezier(0.22, 1, 0.36, 1); } #pk_p_box.plist-active .pk-p-plist-strip { opacity: 1; pointer-events: auto !important; } #pk_p_plist .pk-p-plist-strip { transition: opacity 0.12s ease-out !important; } .pk-player-box .pk-p-plist-strip { border-top: none !important; } #pk_p_box:not(.pk-web-fullscreen):not(:fullscreen):not(:-webkit-full-screen) #pk_p_plist .pk-p-plist-tab::before { bottom: -2px !important; } .pk-web-fullscreen-ov #pk_p_box.pk-web-fullscreen .pk-p-plist-tab { margin-top: 2px !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); } .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-win:not(.pk-analyze-group-tools-on) #pk-ana-select-btn, #pk-win:not(.pk-analyze-group-tools-on) #pk-ana-sort-btn { display: none !important; pointer-events: none !important; } #pk-share-parse-back-list.pk-share-exit-compact > span { display: none !important; } #pk-share-parse-back-list.pk-share-exit-compact { padding: 0 8px !important; min-width: 32px; justify-content: center; gap: 0 !important; } #pk-share-parse-back-list.pk-share-exit-cold:hover:not(:disabled),#pk-share-parse-back-list.pk-share-exit-cold:focus,#pk-share-parse-back-list.pk-share-exit-cold:active{background:transparent!important;filter:none!important;} .pk-ana-select-btn:hover { border-color: var(--pk-pri); color: var(--pk-pri); background: rgba(var(--pk-bg-rgb), 0.05); } .pk-ana-pop { position: absolute; background: var(--pk-bg); border: 1px solid var(--pk-bd); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 1000; display: none; flex-direction: column; padding: 8px; width: 340px; gap: 4px; pointer-events: auto; } .pk-ana-pop-row { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } .pk-ana-opt { padding: 8px 4px; border-radius: 4px; cursor: pointer; font-size: 12px; color: var(--pk-fg); transition: background 0.1s; text-align: center; border: 1px solid transparent; background: var(--pk-hl); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-ana-opt:hover { background: var(--pk-sel-bg); color: var(--pk-pri); border-color: var(--pk-sel-bd); } `; ; const sleep = ms => new Promise(r => setTimeout(r, ms)); function getLogicalRect(el) { if (!el || typeof el.getBoundingClientRect !== 'function') return { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }; return el.getBoundingClientRect(); } 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); if (v === null || v === undefined) return def; return normalizeConfigValue(key, v, 'localWrite'); } return def; } function gmSet(key, val) { const normalized = normalizeConfigValue(key, val, 'localWrite'); if (normalized === undefined) { removeStoredConfigKey(key); return; } let result; if (typeof GM_setValue !== 'undefined') result = GM_setValue(key, normalized); else { try { localStorage.setItem(key, typeof normalized === 'string' ? normalized : JSON.stringify(normalized)); } catch (e) {} } if (key === 'pk_turbo_mode') { try { localStorage.setItem(key, String(normalized)); } catch (e) {} } if (shouldCleanupAfterConfigWrite(key)) scheduleConfigPrefixCleanup('gmSet'); return result; } function gmHas(key) { try { if (typeof GM_listValues !== 'undefined') { const keys = GM_listValues(); if (Array.isArray(keys)) return keys.includes(key); } if (typeof GM_getValue !== 'undefined') { const marker = `__pk_missing_${key}_${Date.now()}_${Math.random()}__`; return GM_getValue(key, marker) !== marker; } } catch (e) {} return false; } async function gmSetAsync(key, val) { const r = gmSet(key, val); if (r && typeof r.then === 'function') await r; } scheduleConfigPrefixCleanup('startup', 1800); let pkScriptUpdateCheckPromise=null; function getScriptVersion(){try{return String((typeof GM_info!=='undefined'&&GM_info.script&&GM_info.script.version)||'0').replace(/^v/i,'').trim();}catch(e){return '0';}} function compareScriptVersion(a,b){const norm=v=>String(v||'0').replace(/^v/i,'').split(/[+-]/)[0].split('.').map(x=>parseInt(x,10)||0);const pa=norm(a),pb=norm(b);for(let i=0;idb?1:-1;}return 0;} function readScriptUpdateCache(){try{const raw=localStorage.getItem(CONF.scriptUpdateCacheKey);if(!raw)return null;const data=JSON.parse(raw);return data&&typeof data==='object'?data:null;}catch(e){return null;}} function writeScriptUpdateCache(data){try{localStorage.setItem(CONF.scriptUpdateCacheKey,normalizeConfigValue(CONF.scriptUpdateCacheKey,JSON.stringify(data),'localWrite'));scheduleConfigPrefixCleanup('scriptUpdateCache');}catch(e){}} async function fetchScriptUpdateInfo(force=false){const now=Date.now();const cached=readScriptUpdateCache();if(!force&&cached&&cached.checkedAt&&(now-cached.checkedAt){let result={checkedAt:now,ok:false,latestVersion:'',homepage:CONF.scriptUpdateProjectUrl,changelog:'',error:''};try{const url=`${CONF.scriptUpdateManifestUrl}${CONF.scriptUpdateManifestUrl.includes('?')?'&':'?'}_t=${now}`;const res=await fetch(url,{cache:'no-store'});if(!res.ok)throw new Error(`HTTP ${res.status}`);const data=await res.json();const latestVersion=String(data.version||data.latestVersion||data.tag||'').replace(/^v/i,'').trim();if(!latestVersion)throw new Error('Empty version');result={checkedAt:now,ok:true,latestVersion,homepage:String(data.homepage||data.url||CONF.scriptUpdateProjectUrl),changelog:String(data.changelog||data.changelogUrl||''),error:''};}catch(e){result.error=e&&e.message?e.message:String(e||'');}writeScriptUpdateCache(result);return result;})();if(!force)pkScriptUpdateCheckPromise=job;try{return await job;}finally{if(pkScriptUpdateCheckPromise===job)pkScriptUpdateCheckPromise=null;}} function isScriptUpdateNew(info){return !!(info&&info.ok&&info.latestVersion&&compareScriptVersion(info.latestVersion,getScriptVersion())>0);} function getScriptUpdateDismissKey(version){return `${CONF.scriptUpdateDismissPrefix}${String(version||'').replace(/[^0-9A-Za-z_.-]/g,'_')}`;} function isScriptUpdateDismissed(version){try{const raw=localStorage.getItem(getScriptUpdateDismissKey(version));if(raw==='1')return true;const data=raw?JSON.parse(raw):null;return !!(data&&data.v===1&&(!data.expiresAt||Number(data.expiresAt)>Date.now()));}catch(e){return false;}} function markScriptUpdateDismissed(version){try{const key=getScriptUpdateDismissKey(version);localStorage.setItem(key,normalizeConfigValue(key,'1','localWrite'));scheduleConfigPrefixCleanup('scriptUpdateDismiss');}catch(e){}} function formatScriptUpdateCheckedAt(ts){const L=getStrings();const n=Number(ts)||0;if(!n)return L.str_script_update_not_checked;const d=new Date(n);if(!Number.isFinite(d.getTime()))return L.str_script_update_not_checked;const p=x=>String(x).padStart(2,'0');return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;} function getScriptUpdateLatestText(info){const L=getStrings();if(info&&info.ok&&info.latestVersion)return info.latestVersion;if(info&&info.checkedAt)return L.str_script_update_failed;return L.str_script_update_not_checked;} function getDefaultPotPlayerProtocolState() { return { schemaVersion: CONF.potplayerProtocolStateSchemaVersion, repairVersion: "", confirmedRepairVersion: "", installRegDownloadedAt: 0, deleteRegDownloadedAt: 0, browserPolicyRegDownloadedAt: 0, customPlayerPath: "", customPlayerPathUpdatedAt: 0, pathBoundRepairVersion: "", userConfirmedFixedAt: 0, lastPromptAt: 0, neverAutoPrompt: false, suppressUntil: 0 }; } function normalizePotPlayerProtocolState(rawState) { const def = getDefaultPotPlayerProtocolState(); let data = rawState; if (typeof data === 'string') { try { data = data ? JSON.parse(data) : null; } catch (e) { data = null; } } if (!data || typeof data !== 'object' || Array.isArray(data)) return def; const toTime = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 0; }; return { schemaVersion: CONF.potplayerProtocolStateSchemaVersion, repairVersion: typeof data.repairVersion === 'string' ? data.repairVersion : "", confirmedRepairVersion: typeof data.confirmedRepairVersion === 'string' ? data.confirmedRepairVersion : "", installRegDownloadedAt: toTime(data.installRegDownloadedAt), deleteRegDownloadedAt: toTime(data.deleteRegDownloadedAt), browserPolicyRegDownloadedAt: toTime(data.browserPolicyRegDownloadedAt), customPlayerPath: typeof data.customPlayerPath === 'string' ? data.customPlayerPath : "", customPlayerPathUpdatedAt: toTime(data.customPlayerPathUpdatedAt), pathBoundRepairVersion: typeof data.pathBoundRepairVersion === 'string' ? data.pathBoundRepairVersion : "", userConfirmedFixedAt: toTime(data.userConfirmedFixedAt), lastPromptAt: toTime(data.lastPromptAt), neverAutoPrompt: data.neverAutoPrompt === true, suppressUntil: toTime(data.suppressUntil) }; } function readPotPlayerProtocolState() { try { const state = normalizePotPlayerProtocolState(gmGet(CONF.potplayerProtocolStateKey, '')); const storedPath = gmGet(CONF.potplayerCustomPathKey, ''); const checkedStoredPath = validatePotPlayerCustomPath(storedPath); if (checkedStoredPath.ok) { state.customPlayerPath = checkedStoredPath.path; return state; } const checkedLegacyPath = validatePotPlayerCustomPath(state.customPlayerPath); if (checkedLegacyPath.ok && !storedPath) { gmSet(CONF.potplayerCustomPathKey, checkedLegacyPath.path); state.customPlayerPath = checkedLegacyPath.path; } return state; } catch (e) { return getDefaultPotPlayerProtocolState(); } } function writePotPlayerProtocolState(state) { try { const normalized = normalizePotPlayerProtocolState(state); const rawPath = String(normalized.customPlayerPath || '').trim(); if (!rawPath) { normalized.customPlayerPath = ''; gmSet(CONF.potplayerProtocolStateKey, normalized); if (typeof GM_deleteValue !== 'undefined') GM_deleteValue(CONF.potplayerCustomPathKey); try { localStorage.removeItem(CONF.potplayerCustomPathKey); } catch (e) {} return; } const checkedPath = validatePotPlayerCustomPath(rawPath); if (checkedPath.ok) { normalized.customPlayerPath = checkedPath.path; gmSet(CONF.potplayerCustomPathKey, checkedPath.path); } else { const storedPath = gmGet(CONF.potplayerCustomPathKey, ''); const checkedStoredPath = validatePotPlayerCustomPath(storedPath); normalized.customPlayerPath = checkedStoredPath.ok ? checkedStoredPath.path : ''; } gmSet(CONF.potplayerProtocolStateKey, normalized); } catch (e) {} } function updatePotPlayerProtocolState(patch = {}) { const state = readPotPlayerProtocolState(); Object.assign(state, patch, { repairVersion: CONF.potplayerProtocolRepairVersion }); writePotPlayerProtocolState(state); return state; } function recordPotPlayerProtocolRegDownload(kind) { const now = Date.now(); let patch; if (kind === 'delete') patch = { deleteRegDownloadedAt: now }; else if (kind === 'browser_policy') patch = { browserPolicyRegDownloadedAt: now }; else patch = { installRegDownloadedAt: now }; return updatePotPlayerProtocolState(patch); } function recordPotPlayerProtocolUserFixed() { const now = Date.now(); const state = readPotPlayerProtocolState(); const patch = { confirmedRepairVersion: CONF.potplayerProtocolRepairVersion, userConfirmedFixedAt: now }; if (state.customPlayerPath) { patch.pathBoundRepairVersion = CONF.potplayerProtocolRepairVersion; } return updatePotPlayerProtocolState(patch); } function recordPotPlayerProtocolAutoPrompt() { return updatePotPlayerProtocolState({ lastPromptAt: Date.now() }); } function suppressPotPlayerAutoPromptToday() { return updatePotPlayerProtocolState({ suppressUntil: Date.now() + CONF.potplayerSuppressTodayTTL }); } function disablePotPlayerAutoPrompt() { return updatePotPlayerProtocolState({ neverAutoPrompt: true }); } function shouldAutoPromptPotPlayerRepair(playUrl) { const cleanUrl = String(playUrl || '').trim(); if (!cleanUrl) return false; const now = Date.now(); const launchState = readPotPlayerLaunchState(); const protocolState = readPotPlayerProtocolState(); if (launchState.consecutiveFailCount < CONF.potplayerAutoRepairFailThreshold) return false; if (launchState.lastLikelyOpenAt > 0 && now - launchState.lastLikelyOpenAt < CONF.potplayerLikelyOpenTrustTTL) return false; if (protocolState.suppressUntil > now) return false; if (protocolState.neverAutoPrompt === true) return false; if (protocolState.lastPromptAt > 0 && now - protocolState.lastPromptAt < CONF.potplayerPromptCooldown) return false; return true; } function schedulePotPlayerAutoRepairPrompt(playUrl, opts = {}) { const cleanUrl = String(playUrl || '').trim(); if (!cleanUrl) return; if (opts.autoRepairPrompt === false) return; const source = normalizePotPlayerLaunchSource(opts.source); const autoPromptDelay = Number.isFinite(Number(opts.autoRepairPromptDelay)) ? Math.max(0, Number(opts.autoRepairPromptDelay)) : 180; setTimeout(() => { if (!shouldAutoPromptPotPlayerRepair(cleanUrl)) return; recordPotPlayerProtocolAutoPrompt(); openPotPlayerProtocolRepairHelper(cleanUrl, { source, autoPrompt: true, button: null }); }, autoPromptDelay); } function clearPotPlayerLaunchFailCount() { const state = readPotPlayerLaunchState(); state.consecutiveFailCount = 0; writePotPlayerLaunchState(state); return state; } function escapeRegString(value) { return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } function normalizePotPlayerCustomPath(input) { return String(input || '').trim().replace(/^["']+|["']+$/g, '').trim(); } function validatePotPlayerCustomPath(input) { const path = normalizePotPlayerCustomPath(input); if (!path) return { ok: false, path }; if (!/\.exe$/i.test(path)) return { ok: false, path }; if (!(/[A-Za-z]:\\/.test(path) || path.includes('\\'))) return { ok: false, path }; if (/[\r\n]/.test(path)) return { ok: false, path }; return { ok: true, path }; } function savePotPlayerCustomPath(input) { const checked = validatePotPlayerCustomPath(input); if (!checked.ok) return { ok: false, changed: false, path: checked.path }; const state = readPotPlayerProtocolState(); const oldPath = state.customPlayerPath || ""; const changed = oldPath !== checked.path; const patch = { customPlayerPath: checked.path }; if (changed) { patch.customPlayerPathUpdatedAt = Date.now(); patch.confirmedRepairVersion = ""; patch.pathBoundRepairVersion = ""; } updatePotPlayerProtocolState(patch); return { ok: true, changed, wasConfigured: !!oldPath, path: checked.path }; } function getValidSavedPotPlayerPath() { const state = readPotPlayerProtocolState(); const checked = validatePotPlayerCustomPath(state.customPlayerPath); return checked.ok ? checked.path : ""; } function buildPotPlayerInstallReg(customPlayerPath = "") { const checked = validatePotPlayerCustomPath(customPlayerPath); const hasCustomPath = checked.ok; const command = hasCustomPath ? `"${checked.path}" "%1"` : 'cmd.exe /d /s /c "start "" "PotPlayerMini64.exe" "%1" || start "" "PotPlayerMini.exe" "%1" || start "" "PotPlayer.exe" "%1""'; const defaultIcon = hasCustomPath ? `"${checked.path}",1` : 'PotPlayerMini64.exe,1'; return [ 'Windows Registry Editor Version 5.00', '', '; PikPak Enhancement Master - PotPlayer protocol repair', '; User-level protocol registration only. No browser policy is written.', hasCustomPath ? '; This file binds potplayer:// to the custom PotPlayer executable path below.' : '; This file uses the PotPlayer executable discoverable from the Windows path.', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer]', '@="URL:PotPlayer Protocol"', '"URL Protocol"=""', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\DefaultIcon]', `@="${escapeRegString(defaultIcon)}"`, '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\shell]', '@="open"', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\shell\\open]', '@="open"', '', '[HKEY_CURRENT_USER\\Software\\Classes\\potplayer\\shell\\open\\command]', `@="${escapeRegString(command)}"`, '' ].join('\r\n'); } function buildPotPlayerDeleteReg() { return [ 'Windows Registry Editor Version 5.00', '', '; PikPak Enhancement Master - PotPlayer protocol cleanup', '; User-level cleanup only. System-level registrations are not modified.', '', '[-HKEY_CURRENT_USER\\Software\\Classes\\potplayer]', '' ].join('\r\n'); } function buildPotPlayerBrowserPolicyReg() { return [ 'Windows Registry Editor Version 5.00', '', '; PikPak Enhancement Master - optional browser external protocol helper', '; Advanced option only. These HKLM policy keys may require administrator rights.', '; Enterprise, school, or company devices may be controlled by existing policies.', '', '[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Google\\Chrome]', '"ExternalProtocolDialogShowAlwaysOpenCheckbox"=dword:00000001', '', '[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Edge]', '"ExternalProtocolDialogShowAlwaysOpenCheckbox"=dword:00000001', '' ].join('\r\n'); } function makeUtf16LeBlob(text) { const input = String(text || ''); const buffer = new ArrayBuffer(2 + input.length * 2); const view = new DataView(buffer); view.setUint16(0, 0xFEFF, true); for (let i = 0; i < input.length; i++) { view.setUint16(2 + i * 2, input.charCodeAt(i), true); } return new Blob([buffer], { type: 'application/octet-stream' }); } function downloadPotPlayerRegFile(kind) { const isDelete = kind === 'delete'; const isBrowserPolicy = kind === 'browser_policy'; const savedPath = getValidSavedPotPlayerPath(); if (!isDelete && !isBrowserPolicy && !savedPath) return false; const content = isBrowserPolicy ? buildPotPlayerBrowserPolicyReg() : (isDelete ? buildPotPlayerDeleteReg() : buildPotPlayerInstallReg(savedPath)); const blob = makeUtf16LeBlob(content); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = isBrowserPolicy ? CONF.potplayerBrowserPolicyRegFileName : (isDelete ? CONF.potplayerRegDeleteFileName : CONF.potplayerRegCustomInstallFileName); a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); recordPotPlayerProtocolRegDownload(isBrowserPolicy ? 'browser_policy' : (isDelete ? 'delete' : 'install')); return true; } function getDefaultPotPlayerLaunchState() { return { schemaVersion: CONF.potplayerLaunchStateSchemaVersion, lastLikelyOpenAt: 0, lastLikelyFailAt: 0, consecutiveFailCount: 0, lastFailSource: "", lastCopiedAt: 0, lastLaunchAt: 0 }; } function normalizePotPlayerLaunchState(rawState) { const def = getDefaultPotPlayerLaunchState(); let data = rawState; if (typeof data === 'string') { try { data = data ? JSON.parse(data) : null; } catch (e) { data = null; } } if (!data || typeof data !== 'object' || Array.isArray(data)) return def; const toTime = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? n : 0; }; const toCount = (v) => { const n = parseInt(v, 10); return Number.isFinite(n) && n > 0 ? n : 0; }; return { schemaVersion: CONF.potplayerLaunchStateSchemaVersion, lastLikelyOpenAt: toTime(data.lastLikelyOpenAt), lastLikelyFailAt: toTime(data.lastLikelyFailAt), consecutiveFailCount: toCount(data.consecutiveFailCount), lastFailSource: typeof data.lastFailSource === 'string' ? data.lastFailSource : "", lastCopiedAt: toTime(data.lastCopiedAt), lastLaunchAt: toTime(data.lastLaunchAt) }; } function readPotPlayerLaunchState() { try { return normalizePotPlayerLaunchState(gmGet(CONF.potplayerLaunchStateKey, '')); } catch (e) { return getDefaultPotPlayerLaunchState(); } } function writePotPlayerLaunchState(state) { try { gmSet(CONF.potplayerLaunchStateKey, normalizePotPlayerLaunchState(state)); } catch (e) {} } function normalizePotPlayerLaunchSource(source) { return ['normal', 'error_fallback', 'unknown'].includes(source) ? source : 'unknown'; } function recordPotPlayerLaunchAttempt(source, launchAt = Date.now()) { const state = readPotPlayerLaunchState(); const time = Number.isFinite(Number(launchAt)) && Number(launchAt) > 0 ? Number(launchAt) : Date.now(); state.lastLaunchAt = time; writePotPlayerLaunchState(state); return state; } function recordPotPlayerLaunchResult(result, opts = {}) { const now = Date.now(); const launchAt = Number.isFinite(Number(opts.launchAt)) && Number(opts.launchAt) > 0 ? Number(opts.launchAt) : now; const copiedAt = Number.isFinite(Number(opts.copiedAt)) && Number(opts.copiedAt) > 0 ? Number(opts.copiedAt) : now; const source = normalizePotPlayerLaunchSource(opts.source); const state = readPotPlayerLaunchState(); if (result === 'likely_open') { state.lastLikelyOpenAt = now; state.lastLaunchAt = launchAt; state.consecutiveFailCount = 0; state.lastFailSource = ""; } else if (result === 'likely_fail') { state.lastLikelyFailAt = now; state.lastLaunchAt = launchAt; state.lastCopiedAt = copiedAt; state.consecutiveFailCount += 1; state.lastFailSource = source; } else { return state; } writePotPlayerLaunchState(state); return state; } function getBlurScope() { const legacyBlur = gmGet('pk_blur_thumb', false); const scope = gmGet('pk_blur_scope', legacyBlur ? 'list' : 'off'); return ['off', 'list', 'grid', 'both'].includes(scope) ? scope : (legacyBlur ? 'list' : 'off'); } function isBlurEnabledForView(view) { const scope = getBlurScope(); return scope === 'both' || scope === view; } const calcSha1 = async (file) => { if (!window.crypto || !window.crypto.subtle) return ""; const hexFromBytes = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); const sha1Bytes = async (buffer) => new Uint8Array(await crypto.subtle.digest('SHA-1', buffer)); const calcXunleiCid = async () => { const CID_SAMPLE_SIZE = 0x5000; if (file.size < 0xF000) return hexFromBytes(await sha1Bytes(await file.arrayBuffer())); const head = file.slice(0, CID_SAMPLE_SIZE); const midStart = Math.floor(file.size / 3); const mid = file.slice(midStart, midStart + CID_SAMPLE_SIZE); const tail = file.slice(Math.max(0, file.size - CID_SAMPLE_SIZE), file.size); return hexFromBytes(await sha1Bytes(await new Blob([head, mid, tail]).arrayBuffer())); }; const calcXunleiGcid = async () => { let partSize = 0x40000; while ((file.size / partSize) > 0x200 && partSize < 0x200000) partSize <<= 1; const digests = []; if (file.size === 0) digests.push(await sha1Bytes(new ArrayBuffer(0))); for (let offset = 0; offset < file.size; offset += partSize) { const buffer = await file.slice(offset, Math.min(file.size, offset + partSize)).arrayBuffer(); digests.push(await sha1Bytes(buffer)); } const merged = new Uint8Array(digests.length * 20); digests.forEach((bytes, index) => merged.set(bytes, index * 20)); return hexFromBytes(await sha1Bytes(merged.buffer)); }; const queryGcidByCid = async (cid) => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/resource/cid?cid=${encodeURIComponent(String(cid || '').toLowerCase())}&file_size=${encodeURIComponent(String(file.size || 0))}`, { method: 'GET', headers: getHeaders() }); if (!res.ok) return ""; const data = await res.json().catch(() => null); return data && data.gcid ? String(data.gcid).toUpperCase() : ""; } catch (e) { console.warn('[Upload] CID precheck failed:', e); return ""; } }; const cid = await calcXunleiCid(); const gcid = await queryGcidByCid(cid); return gcid || await calcXunleiGcid(); }; function getLang(){const u=gmGet('pk_lang','');if(u)return u;const n=navigator.language.toLowerCase();return (n==='zh'||n.startsWith('zh-cn')||n.startsWith('zh-sg'))?'zh':(n.startsWith('zh-tw')||n.startsWith('zh-hk')||n.startsWith('zh-mo'))?'tc':(n.startsWith('id')||n.startsWith('in'))?'id':n.startsWith('ms')?'ms':n.startsWith('ko')?'ko':n.startsWith('ja')?'ja':'en';} const I18N_CONF={defaultLang:'zh',remoteLangs:['tc','en','ko','ja','id','ms'],manifestUrl:'https://cdn.jsdelivr.net/gh/digbug82/PikPak_Enhancement_Master@main/i18n/manifest.json',baseUrl:'https://cdn.jsdelivr.net/gh/digbug82/PikPak_Enhancement_Master@main/i18n/',cachePrefix:'pk_i18n_',manifestKey:'pk_i18n_manifest',cacheTTL:7*24*60*60*1000,manifestTTL:24*60*60*1000,requiredKeys:['title','modal_settings_title','label_lang','msg_settings_saved','btn_nav_home']}; let pkRemoteI18n=null; let pkI18nReadyPromise=null; let pkI18nReadyLang=''; function getI18nCacheKey(lang){return `${I18N_CONF.cachePrefix}${lang}`;} function readI18nCache(lang,expectedVersion=''){try{const raw=gmGet(getI18nCacheKey(lang),'');if(!raw)return null;const parsed=typeof raw==='string'?JSON.parse(raw):raw;if(!parsed||parsed.lang!==lang||!parsed.data||typeof parsed.data!=='object'||Array.isArray(parsed.data))return null;if(parsed.expiresAt&&parsed.expiresAtObject.prototype.hasOwnProperty.call(data,k)));} async function loadI18nManifest(force=false){if(!force){const cached=readI18nManifestCache();if(isValidI18nManifest(cached))return cached;}const manifestUrl=`${I18N_CONF.manifestUrl}${I18N_CONF.manifestUrl.includes('?')?'&':'?'}_t=${Date.now()}`;const manifest=await fetchI18nJson(manifestUrl);if(!isValidI18nManifest(manifest))throw new Error('Invalid i18n manifest');writeI18nManifestCache(manifest);return manifest;} async function loadRemoteLangPack(force=false,langOverride=''){const lang=langOverride||getLang();if(!I18N_CONF.remoteLangs.includes(lang)||!I18N_CONF.manifestUrl){pkRemoteI18n=null;return null;}const manifest=await loadI18nManifest(force);const version=String(manifest.version||'');if(!force){const cached=readI18nCache(lang,version);if(cached){pkRemoteI18n={lang,version:cached.version,data:cached.data};return pkRemoteI18n;}}const file=manifest.files&&manifest.files[lang];if(!file){pkRemoteI18n=null;return null;}const baseUrl=manifest.baseUrl||I18N_CONF.baseUrl;const fileUrl=/^https?:\/\//.test(file)?file:`${baseUrl}${file}`;const data=await fetchI18nJson(`${fileUrl}${fileUrl.includes('?')?'&':'?'}_v=${encodeURIComponent(version)}`);if(!isValidI18nPayload(data))throw new Error('Invalid i18n payload');writeI18nCache(lang,version,data);pkRemoteI18n={lang,version,data};return pkRemoteI18n;} function ensureI18nReady(force=false,langOverride=''){const lang=langOverride||getLang();if(force||!pkI18nReadyPromise||pkI18nReadyLang!==lang){pkI18nReadyLang=lang;pkI18nReadyPromise=loadRemoteLangPack(force,lang).catch(()=>null);}return pkI18nReadyPromise;} async function ensureI18nReadyBeforeOpen(langOverride=''){const lang=langOverride||getLang();if(lang==='zh')return null;try{return await Promise.race([ensureI18nReady(false,lang),sleep(4000).then(()=>null)]);}catch(e){return null;}} const T_LOCAL = { zh: { "title": "PikPak 增强大师", "str_original": "原画", "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": "类型", "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": "回收站", "menu_share_parse": "分享解析", "title_share_parse": "分享解析", "menu_link_bookmark": "链接收藏夹", "title_link_bookmark": "链接收藏夹", "desc_link_bookmark_space_notice": "保存链接、云归档与云配置,不占用存储容量", "label_link_bookmark_folders": "文件夹", "btn_link_bookmark_new_folder": "新建文件夹", "btn_link_bookmark_rename_folder": "重命名文件夹", "btn_link_bookmark_delete_folder": "删除文件夹", "tip_link_bookmark_rename_folder": "重命名文件夹 [F2]", "tip_link_bookmark_delete_folder": "删除文件夹 [Delete]", "ph_link_bookmark_search": "搜索标题和链接 Ctrl+F", "btn_link_bookmark_new_link": "新建链接", "tip_link_bookmark_new_link": "新建链接 [Alt+N]", "fmt_link_bookmark_total_links": "{n} 个链接", "fmt_link_bookmark_total_folders": "{n} 个文件夹", "tip_link_bookmark_page_jump": "点击输入页码", "label_link_bookmark_synced": "已同步", "label_link_bookmark_syncing": "同步中", "label_link_bookmark_sync_failed": "同步失败", "badge_link_bookmark_sync": "同步", "tip_link_bookmark_sync_folder": "该文件夹为脚本同步数据使用,请勿随意编辑或删除", "btn_link_bookmark_open": "打开", "btn_link_bookmark_parse": "预览", "folder_magnet_archive": "云归档", "btn_magnet_archive_check": "云归档", "tip_magnet_archive_check": "归档选中项磁链到链接收藏夹 [Alt] + [Y]", "title_magnet_archive_check": "云归档检测", "msg_magnet_archive_check_no_selected": "请先选择要云归档的项目", "msg_magnet_archive_check_over_limit": "一次最多选择 {max} 个项目进行云归档。", "msg_magnet_archive_checking": "正在检测选中项磁链...", "msg_magnet_archive_index_failed": "云归档索引读取失败,无法确认是否已归档,请稍后重试。", "btn_magnet_archive_write": "确认归档", "btn_magnet_archive_delete_dup_source": "删除重复源文件", "msg_magnet_archive_write_no_item": "没有可写入的云归档项。", "msg_magnet_archive_writing": "正在写入云归档收藏...", "msg_magnet_archive_write_success": "已写入云归档 {count} 项。", "msg_magnet_archive_write_duplicate_only": "可归档项已全部存在于链接收藏夹,无需重复写入。", "msg_magnet_archive_write_failed": "云归档写入失败,源文件未被删除。", "msg_magnet_archive_write_verify_failed": "云归档写入后回读校验失败,源文件未被删除,请稍后重试。", "msg_magnet_archive_write_save_failed": "链接收藏夹写入失败,源文件未被删除。", "msg_magnet_archive_state_too_large": "云归档状态记录过大,源文件未删除。", "msg_magnet_archive_write_partial": "已写入 {ok} 项,另有 {dup} 项重复未写入。", "title_magnet_archive_failure_list": "云归档异常清单", "title_magnet_archive_delete_confirm": "云归档确认", "msg_magnet_archive_delete_confirm": "云归档已写入完成,详情可在「链接收藏夹 > 云归档」中查看。是否将 {n} 个原资源移入回收站?", "msg_magnet_archive_delete_mixed_confirm": "云归档已写入完成,详情可在「链接收藏夹 > 云归档」中查看。并检测到重复资源。是否将 {n} 个已归档和重复源资源移入回收站?", "msg_magnet_archive_delete_dup_confirm": "检测到 {n} 个资源的磁链已存在于云归档中。是否将这些重复源资源移入回收站?", "msg_magnet_archive_delete_no_source": "没有可删除的来源资源,已跳过删除。", "msg_magnet_archive_delete_dup_no_source": "没有可删除的重复源资源,已跳过删除。", "msg_magnet_archive_delete_running": "正在将原资源移入回收站...", "msg_magnet_archive_delete_success": "已将 {n} 个原资源移入回收站。", "msg_magnet_archive_delete_failed": "云归档写入成功,但原资源移入回收站失败,请手动处理。", "msg_magnet_archive_delete_partial_unknown": "部分源资源删除结果无法逐项确认,请到回收站或云盘列表复核。", "msg_magnet_archive_delete_verify_failed": "源资源移入回收站后校验失败:{id}", "msg_magnet_archive_delete_status_failed": "原资源可能已移入回收站,但云归档状态回写失败,请复核。", "msg_magnet_archive_restore_success": "已提交云归档解析任务。", "msg_magnet_archive_restore_failed": "云归档解析失败,请稍后重试。", "msg_magnet_archive_restore_no_magnet": "该云归档项缺少可解析磁链。", "msg_magnet_archive_restore_status_failed": "解析任务已提交,但云归档状态回写失败。", "label_magnet_archive_can_archive": "可归档", "label_magnet_archive_duplicate": "重复", "label_magnet_archive_skipped": "跳过", "label_magnet_archive_invalid": "异常", "label_magnet_archive_source": "标题", "label_magnet_archive_key": "详情", "label_magnet_archive_reason": "状态", "reason_magnet_archive_ready": "检测到可追溯磁链", "reason_magnet_archive_duplicate_existing": "链接收藏夹已存在同主键磁链", "reason_magnet_archive_duplicate_batch": "本次选中项内重复", "reason_magnet_archive_no_magnet": "未发现可追溯磁链", "reason_magnet_archive_detail_failed": "详情获取失败,请稍后重试", "reason_magnet_archive_invalid": "磁链格式异常或缺少 xt", "reason_magnet_archive_href_over_limit": "磁链超过单条链接长度上限", "status_magnet_restore_none": "未提交", "status_magnet_restore_done": "已提交", "btn_magnet_archive_clear_restored": "删除已提交", "btn_magnet_archive_clear_all_links": "清空", "tip_magnet_archive_clear_restored": "清除已提交的链接 [Delete]", "tip_magnet_archive_clear_all_links": "清理全部链接 [Shift] + [Delete]", "msg_magnet_archive_clear_restored_empty": "没有已提交状态的云归档链接。", "msg_magnet_archive_clear_all_empty": "当前云归档没有可清理的链接。", "msg_magnet_archive_clear_restored_confirm": "将清除 {n} 个已提交状态的云归档链接,并同步删除其状态记录。是否继续?", "msg_magnet_archive_clear_all_confirm": "将清理当前云归档中的全部 {n} 个链接,并同步删除状态记录;此操作不可逆,是否继续?", "msg_magnet_archive_clear_success": "已清理 {n} 个云归档链接。", "msg_magnet_archive_clear_failed": "云归档链接清理失败,请刷新后重试。", "btn_link_bookmark_copy": "复制", "btn_link_bookmark_edit": "编辑", "btn_link_bookmark_delete": "删除", "msg_link_bookmark_data_invalid": "官方 bookmark 数据异常", "msg_link_bookmark_empty": "暂无链接", "msg_link_bookmark_no_match": "暂无匹配链接", "msg_link_bookmark_read_failed_retry": "读取失败,请点击刷新官方重试", "folder_link_bookmark_unnamed": "未命名文件夹", "folder_link_bookmark_my_links": "我的链接", "card_link_bookmark_unnamed_title": "未命名链接", "card_link_bookmark_empty_href": "空链接", "label_link_bookmark_date_empty": "--", "title_link_bookmark_new_link": "新建链接", "title_link_bookmark_edit_link": "编辑链接", "label_link_bookmark_link_title": "链接标题", "label_link_bookmark_href": "链接地址", "label_link_bookmark_folder": "所属文件夹", "label_link_bookmark_saving": "保存中", "label_link_bookmark_conflict": "官方已更新", "msg_link_bookmark_conflict": "官方收藏夹已在其他位置更新,请刷新后再操作", "msg_link_bookmark_delete_link_confirm": "确认删除该链接?", "msg_magnet_archive_delete_link_confirm": "确认删除该云归档磁链?删除后再次归档将作为全新条目。", "msg_magnet_archive_delete_link_irreversible_confirm": "该云归档磁链关联的原资源已移入回收站或被删除。如未另行保存磁链,请谨慎操作。确认删除?", "msg_link_bookmark_delete_sync_link_confirm": "该链接可能是脚本同步数据,删除后可能影响配置恢复,确认删除?", "msg_link_bookmark_duplicate_folders_detected": "检测到重名文件夹,网页端不支持编辑重名文件夹,建议合并修复。", "btn_link_bookmark_merge_duplicates": "合并修复", "msg_link_bookmark_merge_duplicates_confirm": "将把同名文件夹内的链接合并到第一个同名文件夹,并删除多余重名文件夹。该操作会同步到官方 bookmark,是否继续?", "msg_link_bookmark_duplicate_folder_write_blocked": "该文件夹存在重名,网页端不支持稳定保存,请先合并修复。", "msg_magnet_archive_reserved_folder_name_conflict": "检测到普通链接文件夹占用了云归档内部保留名 {name}。请先将该普通文件夹重命名为其他名称后,再重新执行云归档。", "msg_link_bookmark_save_verify_failed": "保存后校验失败,请点击刷新官方后重试。", "msg_link_bookmark_total_over_limit": "链接收藏夹总容量已超过安全上限(当前 {size} / 上限 {limit}),已阻止保存。", "msg_link_bookmark_shared_budget_over_limit": "普通链接与云归档容量已超过预留预算(当前 {size} / 上限 {limit}),已阻止保存。", "badge_link_bookmark_duplicate": "重名", "msg_link_bookmark_delete_folder_confirm": "确认删除该文件夹?", "msg_link_bookmark_delete_non_empty_folder_confirm": "该文件夹内仍有链接,删除后将一并删除,确认继续?", "msg_link_bookmark_delete_sync_folder_confirm": "该文件夹为脚本同步数据使用,删除后可能影响配置恢复,确认删除?", "msg_link_bookmark_open_blocked": "该链接暂不支持直接打开,可复制后手动处理", "msg_link_bookmark_copy_success": "复制成功", "msg_link_bookmark_href_duplicate": "该链接已存在", "msg_link_bookmark_save_failed_retry": "保存失败,请点击刷新官方后重试", "tip_share_preview_limit_end": "预览范围的末尾", "desc_share_parse_space_notice": "解析分享内容,未保存前不占用存储空间", "msg_share_preview_limited_save": "分享预览可能存在播放限制,保存到我的 PikPak 后可完整播放", "msg_share_preview_soft_limit": "后续内容无法保证播放,保存到我的 PikPak 后可完整观看", "msg_share_ext_preview_limited_save": "外部播放视频可能不完整,保存到我的 PikPak 后可完整播放", "msg_share_download_preview_limited": "该分享视频存在下载限制,请先保存到我的 PikPak 后再下载", "msg_share_download_no_direct_link": "未返回下载直链", "ph_share_link": "请输入 PikPak 分享链接", "ph_share_pass": "密码(无则留空)", "btn_share_parse": "解析", "btn_share_parse_loading": "解析中...", "btn_share_parse_clipboard": "从剪切板导入", "msg_share_clipboard_empty": "剪切板为空", "msg_share_clipboard_no_link": "剪切板中未找到有效分享链接", "msg_share_clipboard_denied": "无法读取剪切板,请手动粘贴", "msg_share_parse_invalid_link": "分享链接无效", "msg_share_parse_empty_link": "请输入分享链接", "msg_share_parse_success": "分享解析成功", "msg_share_parse_failed": "分享解析失败", "msg_share_parse_bad_pass": "密码错误", "msg_share_parse_region_unavailable": "当前地区不可用,请开启代理后重试", "msg_share_parse_expired": "分享已失效或已过期", "msg_share_parse_network_error": "网络异常,分享解析失败", "msg_share_parse_auth_error": "登录态或权限异常,请重新登录后再试", "msg_share_parse_not_found": "分享不存在", "msg_share_parse_cancelled": "分享已被取消", "msg_share_parse_unknown_error": "接口返回未知错误,分享解析失败", "msg_share_parse_need_parse_first": "请先解析分享链接", "msg_share_parse_loading_files": "正在加载分享文件列表...", "msg_share_parse_load_failed": "分享文件列表加载失败", "msg_share_parse_token_missing": "未获取到分享访问凭证", "msg_share_parse_token_expired": "分享访问凭证已失效,请重新解析", "msg_share_parse_detail_failed": "分享目录加载失败", "title_share_parse_history": "分享解析历史", "btn_share_parse_clear": "清空", "btn_share_parse_history": "历史记录", "msg_share_parse_history_empty": "暂无历史记录", "btn_share_parse_history_reparse": "解析", "btn_share_parse_history_delete": "删除记录", "btn_share_parse_history_clear": "清空历史", "msg_share_parse_history_clear_confirm": "确认清空分享解析历史?", "btn_share_parse_history_pin": "置顶", "btn_share_parse_history_unpin": "取消置顶", "btn_share_parse_history_edit_note": "编辑备注", "btn_share_parse_history_save_note": "保存备注", "ph_share_parse_history_search": "搜索标题和备注 Ctrl+F", "label_share_parse_history_last_success": "最近进入", "label_share_parse_history_success_count": "进入次数", "label_share_parse_history_share_user": "分享者", "label_share_parse_history_root_items": "根目录项目", "label_share_parse_history_status": "状态", "label_share_parse_history_last_check": "最近检测", "label_share_parse_history_status_unknown": "异常", "msg_share_parse_history_cleared": "分享解析历史已清空", "msg_share_parse_history_delete_pinned_confirm": "此记录已置顶,确认删除?", "label_share_parse_root": "分享根目录", "label_share_parse_path_root": "分享解析", "btn_share_parse_save_selected": "保存选中项到我的 PikPak", "tip_share_parse_save_selected": "保存选中项到我的 PikPak [Alt+P]", "btn_share_parse_saving": "保存中...", "msg_share_parse_save_progress": "正在保存 {done}/{total} 项...", "msg_share_parse_save_task_progress": "等待保存任务完成 {done}/{total} 个...", "msg_share_parse_select_first": "请先选择要保存的分享文件或文件夹", "msg_share_parse_save_need_token": "未获取到分享访问凭证,请重新解析", "msg_share_parse_save_need_list": "请先加载分享目录", "msg_share_parse_save_start": "正在保存到我的 PikPak...", "msg_share_parse_save_success": "已保存到我的 PikPak", "msg_share_parse_save_failed": "保存到我的 PikPak 失败", "msg_share_parse_save_partial_failed": "部分文件保存失败", "msg_share_parse_save_own": "不能保存自己的分享内容", "msg_share_parse_save_space_not_enough": "空间不足,无法保存", "msg_share_parse_save_token_expired": "分享访问凭证已失效,请重新解析", "msg_share_parse_save_share_expired": "分享已失效或已过期", "msg_share_parse_save_cancelled": "分享已取消或删除", "msg_share_parse_save_too_many": "文件数量过多,无法保存", "msg_share_parse_save_task_failed": "保存任务失败", "msg_share_parse_save_auth_error": "登录态已失效,请重新登录后再试", "msg_share_parse_save_network_error": "网络异常,保存到我的 PikPak 失败", "msg_share_unzip_no_access": "未获取到分享压缩包访问凭证,请重新解析分享。", "label_share_parse_selected_count": "已选择 {n} 项", "btn_share_parse_insight": "分享文件透视", "btn_share_parse_stop_scan": "停止扫描", "btn_share_parse_back_to_list": "返回分享目录", "msg_share_parse_scan_done": "当前分享全部文件扫描完成", "msg_share_parse_scan_failed": "分享文件扫描失败", "msg_share_parse_scan_empty": "未扫描到分享文件", "msg_share_parse_scan_need_list": "请先加载分享文件列表", "msg_share_parse_scan_progress": "已扫描 {folder} 个文件夹,发现 {file} 个文件", "btn_nav_upload": "我的上传", "title_offline": "我的离线", "trash_title": "回收站", "trash_notice": "回收站的文件最多保存15天", "trash_remaining_today": "今日过期", "trash_remaining_unknown": "无法计算", "ctx_open": "打开", "ctx_add_bl": "添加到资源管理器", "ctx_remove_bl": "从资源管理器移除", "ctx_rename": "重命名", "ctx_copy": "复制", "ctx_copy_name": "复制文件名", "ctx_copy_link": "复制链接", "ctx_del": "删除", "ctx_down": "下载", "ctx_star": "添加星标", "ctx_unstar": "取消星标", "ctx_locate": "在文件夹中查看", "ctx_share": "分享", "btn_down": "下载", "tip_down": "下载 [Alt] + [D]", "btn_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_clear_all_history": "清空全部", "tip_clear_all_history": "清空全部播放历史 [Shift] + [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": "关闭", "txt_preview_title": "TXT 预览", "txt_preview_loading": "正在加载文本...", "txt_preview_too_large": "文件过大,请下载后在本地查看。", "txt_preview_load_failed": "TXT 内容加载失败,请稍后重试。", "msg_open_download_after": "请下载后打开", "txt_preview_decode_failed": "文本编码解析失败,无法预览该文件。", "txt_preview_empty": "文本内容为空。", "txt_preview_close": "关闭", "txt_preview_fullscreen": "网页全屏", "txt_preview_exit_fullscreen": "退出网页全屏", "txt_preview_parse_links": "解析磁链", "txt_preview_parse_links_working": "正在提交云下载...", "txt_preview_no_links": "没有识别到可下载链接", "txt_preview_links_submitted": "已识别 {n} 个链接,已提交云下载", "txt_preview_links_failed": "TXT 链接提交云下载失败", "tip_close": "关闭 [Esc]", "tip_theme": "切换主题 [Alt] + [T]", "tip_rotate": "旋转 [R]", "tip_mirror": "镜像翻转 [H]", "tip_flip_v": "垂直翻转 [V]", "tip_maximize": "最大化 [M]", "tip_minimize": "最小化 [M]", "tip_web_fullscreen": "网页全屏", "tip_exit_web_fullscreen": "退出网页全屏", "tip_full_screen": "全屏 [Enter]", "tip_star_project": "喜欢这个项目?Star 支持一下。", "btn_view_file": "查看文件", "btn_copy_text": "复制", "btn_stop": "停止", "btn_exit_script": "返回登录界面", "btn_settings": "设置", "btn_logout": "退出 PikPak", "msg_logout_confirm": "确定要退出登录吗?", "tip_settings": "设置和更多 [Alt] + [S]", "tip_transfer_quota_detail": "传输配额详情", "modal_transfer_quota_title": "传输配额详情", "transfer_quota_desc": "传输配额包括每个账号每月享有的传输流量,以及非会员每日的下行限额,通常足以满足常规使用。这里显示了您的传输配额使用详情。", "transfer_quota_monthly": "每月传输配额", "transfer_quota_cloud_download": "云下载流量", "transfer_quota_download": "下行流量", "transfer_quota_upload": "上传流量", "transfer_quota_daily_non_vip": "每日限额 · 非会员", "transfer_quota_download_note": "包括在线播放、查看和下载的传输", "transfer_quota_used_total": "已用 {u} / 共 {t}", "err_transfer_quota_fetch": "获取传输配额详情失败。", "lbl_upload_to": "上传文件至: ", "msg_move_done": "移动完成。", "msg_upload_dup_confirm": "发现 {n} 个名称与大小相同的文件,是否跳过?", "btn_upload": "本地上传", "btn_up_file": "上传文件", "btn_up_folder": "上传文件夹", "btn_cloud_download": "云下载", "btn_up_pause": "暂停任务", "tip_up_pause": "暂停任务 [Alt] + [P]", "btn_up_start": "开始任务", "tip_up_start": "开始任务 [Alt] + [G]", "btn_up_del": "删除任务", "tip_up_del": "删除任务 [Delete]", "btn_up_clear_all": "清空任务", "tip_up_clear_all": "清空任务 [Shift] + [Delete]", "tip_upload_official_fallback": "上传异常时建议使用官方上传", "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_pwd": "密码", "title_share_detail": "分享详情", "ctx_share_detail": "查看分享详情", "ctx_share_copy": "复制链接和密码", "col_view": "浏览", "col_save": "保存", "col_share_time": "分享时间", "col_share_status": "分享状态", "lbl_limit_reached": "次数已满", "lbl_limit_tip": "限制次数", "lbl_share_view": "浏览", "lbl_share_save": "保存", "lbl_share_link_title": "分享链接", "lbl_share_pwd_title": "密码", "lbl_share_expire_title": "有效期", "btn_copy_link_pwd": "复制链接和密码", "str_expire_suffix": "后过期", "ph_edit_pwd": "输入分享密码,支持 4-10 位", "btn_close_pwd": "关闭密码", "str_no_pwd": "无密码", "title_edit_share_code": "分享代码修改", "ph_edit_share_code": "支持 5-18 位文字、数字、及符号等", "btn_add_share_code": "添加分享代码", "btn_del_share_code": "删除分享代码", "share_title": "分享文件", "share_mode": "分享方式", "share_public": "公开链接", "share_encrypted": "加密链接", "share_expiry": "设置有效期", "share_pass": "设置密码", "share_count": "设置提取次数", "share_count_ed": "提取次数", "share_perm": "永久有效", "share_unlimit": "不限", "share_rand": "系统随机", "share_custom": "自定义", "share_days": "天", "share_times": "次", "btn_share_start": "立即分享", "cal_custom_title": "自定义有效期", "lbl_share_code": "密码", "btn_copy_share": "复制全部", "str_share_expired": "已过期", "str_share_deleted": "文件已删", "title_edit_pwd": "密码修改", "lbl_share_code_title": "分享代码", "btn_migrate": "数据迁移", "tip_migrate": "打包选中项并准备迁移至新账号 [Alt] + [M]", "btn_export_m3u": "导出 M3U", "tip_export_m3u": "导出选中视频的 M3U 播放列表 [Alt] + [L]", "msg_m3u_no_video": "未选中视频", "msg_m3u_export_limit": "一次最多导出 {n} 个视频", "msg_m3u_export_progress": "正在导出 M3U {done}/{total}...", "msg_m3u_share_limited_save": "视频可能不完整,保存到我的 PikPak 后可完整播放", "msg_m3u_no_valid_link": "没有可导出的视频直链", "msg_m3u_export_done": "M3U 已导出:{n} 个视频", "msg_m3u_export_partial": "M3U 已导出:{n} 个视频,跳过 {s} 个", "msg_migrate_confirm": "确定要将选中的 {n} 个项目迁移至其他账号吗?\n\n打包成功后,会自动退出。\n登录【目标账号】以完成接收。", "msg_migrate_packing": "正在构建加密迁移包...", "msg_migrate_ready": "✅ 迁移包已就绪!\n\n即将退出当前账号,请登录您的【目标账号】。\n重新登录后系统将自动接管转存工作。\n(注:迁移数据包将在 1 天后自动过期失效)", "err_migrate_ban": "打包失败:选中的内容中包含违规或被官方限制分享的资源。\n请剔除违规文件后重试。", "msg_migrate_detect": "📦 检测到来自另一个账号的迁移数据包!\n\n共包含 {n} 个项目。\n接收后将保存到以下路径:\n主页 / Pack From Shared\n若该文件夹不存在,将自动创建。\n\n是否立即开始迁移?\n选择“是”将执行迁移;选择“否”将取消本次迁移。", "msg_migrate_same_account": "迁移已取消:您登录的仍然是原账号,存根已清除。", "msg_migrate_saving": "正在从加密通道高速转存数据...", "msg_migrate_success": "🎉 跨账号迁移完成!\n所有文件已成功转存至当前账号。", "ph_pass_range": "4-10位字符", "cal_week_days": ["日", "一", "二", "三", "四", "五", "六"], "cal_months": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], "title_file_analysis": "文件分析", "btn_scan": "文件透视", "lbl_scan_selected": "对选中的 {n} 个项目执行文件透视", "lbl_keyword_filter": "排除关键词", "ph_keyword_filter": "排除包含的关键词,多个用逗号分隔", "lbl_scan_current": "对当前路径下所有项目执行文件透视", "tip_dup": "文件查重", "lbl_dup_selected": "对选中的 {n} 个项目执行文件查重", "lbl_dup_current": "对当前路径下所有项目执行文件查重", "tip_scan_dup": "筛选或查重文件", "lbl_dup_reset": "↺ 复原 (取消置顶 & 清空选择)", "lbl_dup_select_folder": "📂 按文件夹选择", "lbl_dup_select_folder_short": "📂 文件夹", "lbl_dup_invert": "反选模式", "lbl_dup_invert_short": "反选", "tip_dup_invert_limit": "仅按文件夹选择可用", "fmt_dup_count": "({n}个重复)", "tag_hash": "精准匹配", "tag_hash_short": "精准", "tag_name": "名称相似", "tag_name_short": "名称", "tag_sim": "时长相似", "tag_sim_short": "时长", "label_dup_video": "视频文件 (精准匹配 + 时长相似 + 名称相似)", "label_dup_image": "图片文件 (精准匹配 + 名称相似)", "label_dup_other": "其他文件 (精准匹配 + 名称相似)", "btn_analyze": "文件夹分析", "tip_analyze": "筛选或查重文件夹", "btn_export": "导出目录", "tip_export": "生成并下载当前路径的文件树列表", "title_export_format": "导出目录样式", "lbl_export_current": "对当前路径下所有项目执行目录导出", "opt_tree_view": "目录树", "opt_list_view": "目录列表", "opt_grid_view": "网格视图", "msg_exporting": "正在生成目录树...", "str_analyze_results": "匹配结果", "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_file": "请通过“粘贴”或文件右键菜单“添加到资源管理器”导入。", "modal_bl_preview": "检索结果", "btn_bl_delete": "删除选中项", "modal_rename_title": "重命名", "modal_rename_multi_title": "批量重命名", "label_replace": "替换/删除", "label_replace_note": "区分大小写", "label_include_ext": "包含后缀", "label_regex": "正则 (Regex)", "placeholder_find": "查找内容", "placeholder_replace": "替换为 (留空删除)", "label_jav": "FC2 纯净命名", "lbl_rn_pattern": "命名模板", "lbl_rn_case_convert": "大小写转换", "opt_rn_lower": "全部小写 (abc)", "lbl_rn_mode_series": "剧集模式", "lbl_rn_mode_format": "格式化", "lbl_rn_mode_ad": "前缀去广告", "lbl_rn_mode_ext": "后缀修复", "opt_rn_upper": "全部大写 (ABC)", "opt_rn_title": "首字母大写 (Abc)", "lbl_rn_width_convert": "全半角转换", "opt_rn_width_half": "全角转半角 (A->A)", "opt_rn_width_full": "半角转全角 (A->A)", "lbl_rn_preview_title": "变更预览", "tip_jav_mode_desc": "✨ 智能提取FC2并去除无关字符", "tip_ad_remove_desc": "🧹 智能滤除头部广告、网址及垃圾符号,自动清洗Emoji并修复括号格式", "tip_ext_fix_desc": "🧩 根据文件真实类型 (MIME) 智能修正后缀", "label_replace_find": "查找内容", "label_replace_to": "替换为", "label_more": "更多", "btn_unzip": "批量解压", "tip_unzip": "批量解压 [Alt] + [U]", "btn_unzip_all": "全部解压", "btn_unzip_selected": "解压选中 {n} 项", "btn_understand_unzip": "理解并解压", "title_input_pwd": "需要解压密码", "lbl_pwd_prompt": "请输入密码:", "btn_ext": "外部播放", "tip_ext": "使用PotPlayer播放或获取播放链接 [Alt] + [E]", "btn_img_search": "以图搜图 [F]", "tip_pip": "画中画 [P]", "str_no_sub": "无字幕", "lbl_sub_sel": "字幕选择", "lbl_show_sub": "显示字幕", "btn_sub_search": "搜索在线字幕", "btn_sub_cloud": "打开云盘字幕", "btn_sub_local": "打开本地字幕", "lbl_sub_pos": "字幕位置", "lbl_sub_bottom": "底部", "lbl_sub_top": "顶部", "lbl_sub_bg_op": "背景透明", "lbl_sub_size": "字幕大小", "lbl_sub_offset": "字幕进度", "title_sel_sub": "选择字幕", "ph_sub_search": "输入关键词,下方链接自动更新...", "str_compat_mode": "兼容模式", "lang_code": "zh", "btn_go_search": "🔍 去 {n} 手动搜", "btn_restart": "从头播放", "btn_prev_video": "上一个 [Ctrl + ←]", "btn_next_video": "下一个 [Ctrl + →]", "tip_plist_open": "展开 [E]", "tip_plist_close": "收起 [E]", "tab_sub": "字幕", "tab_size": "尺寸", "tab_more": "更多", "lbl_ratio": "比例", "lbl_direction": "方向", "opt_ratio_def": "默认", "btn_rot_l": "向左旋转", "btn_rot_r": "向右旋转", "btn_flip_h": "水平翻转", "btn_flip_v": "垂直翻转", "lbl_play_end": "当播放结束", "opt_list_loop": "列表循环", "opt_single_loop": "单集循环", "opt_play_stop": "播完暂停", "lbl_skip_op": "跳过片头", "lbl_skip_ed": "跳过片尾", "btn_start_play": "开始播放", "btn_copy_link": "复制链接", "opt_player_script": "脚本播放器", "opt_player_potplayer": "PotPlayer", "msg_potplayer_launching": "正在唤起 PotPlayer...", "msg_potplayer_maybe_failed": "可能未唤起 PotPlayer,播放链接已复制,可手动粘贴到播放器。", "msg_potplayer_link_copied": "链接已复制", "potplayer_fix_entry": "PotPlayer 协议修复", "potplayer_fix_title": "PotPlayer 协议修复助手", "potplayer_fix_desc": "如果点击 PotPlayer 后没有响应,通常是系统没有正确注册 potplayer:// 协议,或协议指向旧路径。请按下面步骤填写 PotPlayer 程序路径并生成修复文件。", "potplayer_fix_manual_tip": "如果曾经修复过错误路径,请先下载并执行清理旧协议文件。\n推荐流程:填写 PotPlayer 程序路径 → 保存路径 → 下载协议修复文件 → 手动执行 .reg → 点击“已执行修复文件,重新尝试”。", "potplayer_fix_copy_link": "复制播放链接", "potplayer_fix_download_delete": "下载清理旧协议文件", "potplayer_fix_i_have_fixed": "已执行修复文件,重新尝试", "potplayer_fix_close": "关闭", "potplayer_fix_copy_success": "播放链接已复制", "potplayer_fix_install_downloaded": "协议修复文件已下载,请双击执行后再点击重新尝试", "potplayer_fix_delete_downloaded": "旧协议清理文件已下载,请双击执行后再重新生成修复文件", "potplayer_fix_confirmed": "已确认执行修复文件,正在重新尝试", "potplayer_fix_skip_today": "今日不再提示", "potplayer_fix_never_auto": "不再自动提示", "potplayer_fix_reason_auto": "已连续多次未检测到 PotPlayer 打开信号", "potplayer_fix_skip_today_done": "今日不再自动提示", "potplayer_fix_never_auto_done": "已关闭自动提示", "potplayer_fix_auto_title": "PotPlayer 可能需要协议修复", "potplayer_fix_advanced_desc": "请填写 PotPlayer.exe、PotPlayerMini64.exe 或 PotPlayerPortable.exe 的完整路径,不要填写其他程序。", "potplayer_fix_path_label": "PotPlayer 程序路径", "potplayer_fix_path_placeholder": "例如 C:\\PotPlayer\\PotPlayerMini64.exe", "potplayer_fix_save_path": "保存路径", "potplayer_fix_path_saved": "路径已保存,请重新下载并执行协议修复文件", "potplayer_fix_path_invalid": "请输入有效的 PotPlayer exe 路径", "potplayer_fix_retry_need_path": "请先填写 PotPlayer 程序路径,并下载执行协议修复文件", "potplayer_fix_path_changed": "路径已变更,修复状态已失效,请重新下载并执行协议修复文件", "potplayer_fix_download_custom_install": "下载协议修复文件", "potplayer_fix_version_outdated": "已保存的修复记录较旧,建议重新生成协议修复文件", "potplayer_fix_browser_policy": "下载兜底修复文件", "potplayer_fix_browser_policy_desc": "兜底修复文件仅当已执行协议修复文件、PotPlayer 本体能正常打开,但浏览器中点击 PotPlayer 仍无响应时使用。可能需要管理员权限。", "potplayer_fix_browser_policy_downloaded": "浏览器协议确认修复文件已下载", "potplayer_fix_path_status_empty": "请先填写 PotPlayer 程序路径", "potplayer_fix_path_status_confirmed": "已记录当前路径,请确认已执行修复文件后重新尝试", "potplayer_fix_path_status_unconfirmed": "路径已保存,请下载并执行协议修复文件", "tip_copy_link": "复制链接 [Alt] + [C]", "opt_player_other": "其他 (导出链接)", "lbl_player": "播放器", "btn_mark": "标记", "lbl_resolution": "清晰度", "str_switch_compat": "当前清晰度不可用,已为您恢复至 {n}", "type_img": "图像", "type_doc": "文档", "type_archive": "压缩文件", "type_sub": "字幕文件", "type_app": "应用程序", "type_suffix": "文件", "label_turbo_mode": "极速模式", "desc_turbo_mode": "自动开启并替代网页界面 (推荐)", "ph_aria2_secret": "密钥 (选填)", "str_connected": "连接成功", "str_conn_fail": "连接失败", "str_connecting": "正在测试...", "tip_mixed_content": "常用 RPC 端口:\n• 6800 (AriaNg 默认)\n• 16800 (Motrix 默认)", "picker_title": "选择文件夹", "picker_all": "全部文件", "picker_new": "新建文件夹", "picker_sort_new": "更新", "trash_sort_more_to_less": "剩余时长更多", "trash_sort_less_to_more": "剩余时长更少", "picker_sort_star_new": "星标·更新", "picker_sort_star_old": "星标·更旧", "picker_sort_type_dur_asc": "类型/时长·顺序", "picker_sort_type_dur_desc": "类型/时长·逆序", "picker_sort_duration_asc": "时长·顺序", "picker_sort_duration_desc": "时长·逆序", "picker_sort_progress_more": "播放更多", "picker_sort_progress_less": "播放更少", "picker_sort_old": "更旧", "picker_sort_large": "更大", "picker_sort_small": "更小", "picker_sort_path_asc": "路径顺序", "grid_folder_count": "{n}项", "picker_sort_path_desc": "路径逆序", "title_select_file": "选择文件", "placeholder_search": "搜索文件 Ctrl+F", "placeholder_search_short": "搜索 Ctrl+F", "title_search_hist": "搜索历史", "btn_clear_hist": "清空", "lbl_global_search": "全盘搜索", "lbl_search_path": "匹配路径", "lbl_search_path_short": "路径", "str_search_results": "搜索结果", "modal_settings_title": "设置", "label_lang": "语言 (Language)", "label_keep_pos": "保持浏览位置 (返回时定位)", "label_sort_pref": "排序偏好", "opt_sort_indep": "每个文件夹独立", "opt_sort_global": "全部相同", "desc_sort_indep": "调整排序方式仅针对当前文件夹生效", "desc_sort_global": "全部文件夹使用相同的排序", "label_view_pref": "视图模式偏好", "opt_view_indep": "每个文件夹独立", "opt_view_global": "全部相同", "desc_view_indep": "调整视图仅对当前文件夹生效", "desc_view_global": "全部文件夹使用相同的列表/网格视图", "label_search_engine": "搜图引擎", "opt_engine_google": "Google Lens (综合)", "opt_engine_yandex": "Yandex (综合)", "opt_engine_saucenao": "SauceNAO (Pixiv/插画)", "opt_engine_tracemoe": "trace.moe (动漫截图)", "lbl_video_playback_settings": "视频播放设置", "label_default_open_player": "默认打开方式", "label_default_video_quality": "默认视频清晰度/外部播放清晰度", "label_video_load_progress_cache": "打开视频时读取播放进度", "opt_quality_1080p": "1080P", "opt_quality_720p": "720P", "opt_quality_480p": "480P", "label_dup_strictness": "相似匹配阈值", "opt_strict": "严格", "opt_loose": "宽松", "desc_comic_mode": "纯图片或纯视频文件夹默认 A-Z 排序", "btn_send_downloader": "发送 {n}", "tip_send_downloader": "发送 {n} [Alt] + [A]", "label_downloader_type": "下载器类型", "label_downloader_aria2": "Aria2", "label_downloader_gopeed": "Gopeed", "opt_downloader_abdm": "ABDM", "label_downloader_idm": "IDM", "label_downloader_prefer_original_video_link": "视频优先使用播放链路加速下载", "btn_push_abdm": "发送 ABDM", "tip_push_abdm": "发送 ABDM [Alt] + [A]", "btn_export_idm": "导出 IDM", "tip_export_idm": "导出 IDM [Alt] + [A]", "label_idm_config": "IDM 导出配置", "label_idm_export_mode": "IDM 导出方式", "opt_idm_export_url": "导出 IDM URL 清单", "opt_idm_export_bat": "导出 IDM BAT 清单", "label_idm_url_export_type": "导出类型", "opt_idm_url_export_txt": "TXT (.txt)", "opt_idm_url_export_ef2": "EF2 (.ef2)", "label_idm_exe_path": "IDMan.exe 所在路径", "ph_idm_exe_path": "例:D:\\Internet Download Manager\\IDMan.exe", "ph_idm_download_dir": "留空使用 IDM 默认路径", "msg_idm_exe_missing": "未找到 IDMan.exe,请在设置中填写 IDMan.exe 所在路径后重新导出 BAT。", "msg_idm_exporting": "正在导出 IDM 清单...", "msg_idm_url_exported": "已导出 IDM URL 清单:{n} 个链接。", "msg_idm_bat_exported": "已导出 IDM BAT 清单:{n} 个任务。", "msg_idm_exported_with_fail": "IDM 导出完成:成功 {s} 个,失败 {f} 个。", "msg_idm_not_http": "IDM 当前仅支持 HTTP/HTTPS 直链导出", "str_idm_url_file_name": "idm_urls", "str_idm_bat_file_name": "idm_bat", "label_downloader_config": "{n} 配置", "label_downloader_url": "{n} 地址", "label_downloader_token": "{n} 密钥", "label_downloader_dir": "{n} 下载路径", "lbl_downloader_status": "下载器连接测试", "label_aria2_url": "Aria2 地址", "label_aria2_keep_structure": "下载保存文件夹内部结构", "ph_aria2_dir": "留空使用默认路径", "label_gopeed_config": "Gopeed 配置", "label_gopeed_url": "Gopeed API 地址", "label_gopeed_token": "Gopeed API Token", "label_gopeed_dir": "Gopeed 下载路径", "ph_gopeed_url": "http://127.0.0.1:9999", "ph_gopeed_token": "Token (选填)", "ph_gopeed_dir": "留空使用 Gopeed 默认路径", "desc_gopeed_setup": "1. 打开 Gopeed 设置 → 高级。\n2. 在 API 区域将通讯协议设为 TCP;本机使用通常填 IP:127.0.0.1、端口:9999。\n3. 脚本中的 Gopeed API 地址应填写为 http://IP:端口,例如 http://127.0.0.1:9999。如设置了接口令牌,请填写相同 Token;修改通讯协议、IP、端口或 Token 后,建议重启 Gopeed 再测试连接。", "label_abdm_config": "ABDM 设置", "label_abdm_url": "ABDM API 地址", "ph_abdm_url": "http://localhost:15151", "label_abdm_dir": "ABDM 下载路径", "ph_abdm_dir": "留空则使用 ABDM 默认下载目录", "desc_abdm_setup": "1. 启动 AB Download Manager。\n2. 确认 ABDM 已开启本地监听服务;默认地址通常为 http://localhost:15151。\n3. 如果修改过端口,请在脚本中填写对应地址;修改后建议重启 ABDM 再测试连接。", "label_download_accel_enable": "下载加速", "label_download_accel_domain": "加速地址", "desc_download_accel_domain": "下载时使用加速地址", "label_download_accel_mode": "链接传递方式", "label_download_accel_mode_prefix": "拼接原始链接", "label_download_accel_mode_query": "作为 URL 参数传递", "label_download_accel_query_param": "URL 参数名", "msg_download_accel_invalid_domain": "加速地址无效", "str_aria2_zero_byte_skipped": "0KB 文件已跳过", "label_privacy_mode": "隐私图", "label_blur_cover": "模糊媒体封面缩略图", "label_hide_button_text": "隐藏按钮文字", "desc_hide_button_text": "只保留图标与悬浮提示", "opt_privacy_off": "关闭", "opt_privacy_list": "仅列表视图", "opt_privacy_grid": "仅网格视图", "opt_privacy_both": "列表和网格视图", "label_dl_filter_ext": "后缀过滤", "label_dl_filter_name": "关键词过滤", "ph_dl_filter_ext": ".txt, .mp4,多个用逗号分隔", "ph_dl_filter_name": "ReadMe1, ReadMe2,多个用逗号分隔", "lbl_dl_filter": "下载过滤", "lbl_config_manage": "配置管理", "lbl_config_cloud_sync": "云同步配置", "label_config_cloud_last_sync": "上次同步", "label_config_cloud_remote_size": "云端配置", "label_config_cloud_chunk_count": "分片数量", "label_config_cloud_scope": "同步范围", "btn_config_cloud_upload": "上传到云端", "btn_config_cloud_pull": "从云端拉取", "btn_config_cloud_clear": "清空云端配置", "status_config_cloud_unsynced": "未同步", "status_config_cloud_synced": "已同步", "status_config_cloud_local_changed": "本地有更新", "status_config_cloud_remote_changed": "云端有更新", "status_config_cloud_conflict": "冲突", "status_config_cloud_failed": "同步失败", "status_config_cloud_empty": "云端配置为空", "msg_config_cloud_uploading": "正在上传云同步配置...", "msg_config_cloud_upload_success": "云同步配置已上传", "msg_config_cloud_upload_failed": "云同步配置上传失败", "msg_config_cloud_pulling": "正在拉取云端配置...", "msg_config_cloud_pull_success": "云端配置已合并到本地", "msg_config_cloud_pull_failed": "云端配置拉取失败", "msg_config_cloud_pull_cancelled": "已取消拉取,未写入本地配置", "msg_config_cloud_pull_no_write": "没有可写入本地的配置", "msg_config_cloud_pull_apply_summary": "本次将更新 {n} 项配置。", "msg_config_cloud_pull_conflict_summary": "{n} 项冲突将保留本地版本。", "msg_config_cloud_clear_confirm": "确认清空云端配置?", "msg_config_cloud_clearing": "正在清空云端配置...", "msg_config_cloud_clear_success": "云端配置已清空", "msg_config_cloud_clear_empty": "云端配置为空", "msg_config_cloud_clear_failed": "清空云端配置失败", "msg_config_cloud_busy_clean_block": "云配置同步中,请稍后再清理本地数据", "msg_config_cloud_local_clean_busy": "正在清理本地数据,请稍后再操作云配置", "msg_config_input_over_limit": "内容超过上限", "title_config_cloud_detail": "云同步详情", "msg_config_cloud_no_report": "暂无同步报告", "label_config_cloud_report_operation": "操作类型", "label_config_cloud_report_status": "状态", "label_config_cloud_report_time": "同步时间", "label_config_cloud_report_stage": "失败阶段", "label_config_cloud_report_reason": "失败原因", "label_config_cloud_report_written_remote": "是否写入云端", "label_config_cloud_report_keep_old_remote": "是否保留旧云端配置", "label_config_cloud_report_keep_local": "是否保留本地配置", "label_config_cloud_report_written_local": "是否写入本地", "label_config_cloud_report_local_total": "本地配置项数量", "label_config_cloud_report_uploaded_count": "上传配置项数量", "label_config_cloud_report_skipped_count": "严禁同步跳过项数量", "label_config_cloud_report_cloud_total": "云端配置项数量", "label_config_cloud_report_allowed_count": "允许合并项数量", "label_config_cloud_report_pull_apply_count": "应用项数量", "label_config_cloud_report_pull_merge_count": "合并项数量", "label_config_cloud_report_pull_skip_count": "跳过项数量", "label_config_cloud_report_pull_discard_count": "严禁同步丢弃项数量", "label_config_cloud_report_pull_invalid_count": "非法云端项数量", "label_config_cloud_report_pull_conflict_count": "冲突保留本地数量", "label_config_cloud_report_pull_clip_count": "裁剪项数量", "label_config_cloud_report_pull_unchanged_count": "未变化项数量", "label_config_cloud_report_raw_size": "原始 JSON 大小", "label_config_cloud_report_payload_size": "payloadBytes", "label_config_cloud_report_compressed_size": "压缩后大小", "label_config_cloud_report_encoded_size": "编码后大小", "label_config_cloud_report_config_count": "configCount", "label_config_cloud_report_updated_at": "updatedAt", "label_config_cloud_report_chunk_size": "单片长度", "label_config_cloud_report_chunk_count": "分片数量", "label_config_cloud_report_hash": "payloadHash", "label_config_cloud_report_hash_algorithm": "hash 算法", "label_config_cloud_report_local_hash": "本地配置 hash", "label_config_cloud_report_sync_id": "syncId", "label_config_cloud_report_skipped_manifest": "跳过 manifest 数量", "label_config_cloud_report_rollback": "本地快照回滚", "label_config_cloud_report_oversize_categories": "容量占用排行", "label_config_cloud_report_cleanup": "旧同步数据清理", "label_config_cloud_report_deleted_manifest": "删除 manifest 数量", "label_config_cloud_report_deleted_chunk": "删除 chunk 数量", "label_config_cloud_report_keep_bookmarks": "是否保留普通链接收藏夹数据", "label_config_cloud_cleanup_failed": "旧同步数据清理失败 / 需下次清理", "label_config_cloud_cleanup_ok": "已清理", "label_config_cloud_operation_upload": "上传到云端", "label_config_cloud_operation_clear": "清空云端配置", "label_config_cloud_operation_pull": "从云端拉取", "label_config_cloud_status_success": "成功", "label_config_cloud_status_failed": "失败", "label_config_cloud_status_aborted": "中止", "label_config_cloud_status_preview": "预览", "label_config_cloud_status_cancel": "取消", "label_config_cloud_abort_too_many_chunks": "超过 256 分片", "label_config_cloud_user_cancelled": "用户取消", "label_config_cloud_bool_yes": "是", "label_config_cloud_bool_no": "否", "label_config_cloud_none": "--", "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_share_parse_history": "分享解析历史记录", "opt_cfg_cache": "运行缓存 (缩略图/语言包/更新检查/临时状态)", "msg_import_confirm": "导入的配置将与当前设置合并(合并名单/记录,覆盖冲突的基础设置),是否继续?", "msg_import_success": "配置导入成功,页面即将刷新...", "err_invalid_config": "无效的配置文件:未检测到指纹标识或格式错误", "err_json_format": "文件解析失败:JSON 语法错误或文件已损坏", "lbl_storage": "存储空间", "lbl_browse_exp": "浏览体验", "lbl_skip_bl_on_del": "删除时跳过管理器中记录资源", "lbl_pwd_manage": "解压密码管理", "title_pwd_vault": "解压密码库", "lbl_pwd_try_count": "单个压缩包密码匹配上限", "tip_pwd_manual": "每行记录一个密码,回车换行", "str_root_dir_cn": "根目录", "btn_ana_select": "一键勾选", "btn_in_group_sort": "组内排序", "opt_sort_time": "时间", "opt_sort_size": "大小", "opt_sort_path": "路径", "opt_sort_name": "名称", "opt_keep_new": "保留最新的", "opt_keep_old": "保留最旧的", "opt_keep_large": "保留最大的", "opt_keep_small": "保留最小的", "opt_keep_short": "保留名称最短的", "opt_keep_long": "保留名称最长的", "label_clipboard_magnet_focus": "剪贴板磁链识别", "desc_clipboard_magnet_focus": "回到前台时检测剪贴板磁链", "msg_magnet_preview_desc": "已识别到剪贴板磁链,确认后将直接创建云下载任务。", "msg_magnet_preview_fail": "未获取到公开预览信息,仍可继续添加。", "str_magnet_unknown_name": "未知资源", "str_no_preview": "暂无预览图", "lbl_magnet_count": "文件数", "lbl_magnet_size": "总大小", "lbl_magnet_type": "类型", "lbl_magnet_hash": "磁链", "btn_magnet_continue": "高速云下载", "lbl_magnet_preview_source": "预览信息来自", "msg_magnet_preview_rate_limited": "预览服务请求过于频繁,已临时降级。仍可继续添加。", "msg_magnet_preview_timeout": "预览服务响应超时,仍可继续添加。", "msg_magnet_preview_network": "预览服务暂不可用,仍可继续添加。", "loading": "加载中...", "loading_detail": "正在全速索引目录结构...", "loading_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_stopping": "正在停止...", "str_merging": "合并数据中...", "str_rendering": "渲染列表中...", "str_scanning": "扫描中...", "str_analyzing": "分析中...", "str_deleting": "删除中...", "str_saving": "保存中...", "str_checking_bl": "匹配名单记录中...", "str_processing": "系统正在全速处理中...", "str_cleanup_done": "清理完成。", "str_copying": "复制到剪贴板...", "str_moving": "准备移动...", "str_sorting": "正在排序...", "str_refreshing": "刷新中...", "str_refreshing_cache": "刷新缓存中...", "str_group": "组", "str_init_rename": "初始化重命名...", "str_renaming": "重命名中...", "str_calc_changes": "计算变更中...", "str_scanning_dir": "扫描目录结构...", "str_init_op": "初始化操作...", "str_init_scan": "正在初始化全盘扫描...", "str_upload_1": "正在上传图片...", "str_upload_2": "正在重试上传...", "str_upload_fail_copy": "上传失败,准备写入剪贴板...", "str_preparing": "准备解压...", "str_unzipping": "正在解压: {n}", "str_unzipping_state": "解压中...", "str_unzipping_prog_0": "解压中: 0%", "str_unzipping_prog_100": "解压中: 100%", "str_unzipping_prog_fmt": "解压中: {n}%", "msg_task_waiting": "等待中...", "msg_task_hashing": "校验文件中...", "msg_task_init_upload": "初始化上传...", "msg_task_uploading": "正在上传...", "msg_task_init_part": "初始化分片...", "str_loc_tracing": "正在回溯路径...", "str_loc_stale": "缓存已过期,正在同步云端...", "str_verifying": "正在验证...", "str_server_indexing": "服务器处理中", "str_creating_task_n": "创建任务中", "msg_submit_request": "正在提交请求", "msg_wait_server": "等待服务器处理", "msg_server_processing": "服务器处理中", "str_jav_querying": "正在查询...", "msg_limit_updated": "提取次数已更新", "str_selected_items": "已选项目", "title_alert": "提示", "title_confirm": "确认", "title_prompt": "输入", "btn_ok": "确定", "btn_yes": "是", "btn_no": "否", "btn_save": "保存配置", "btn_cancel": "取消", "btn_create": "创建", "btn_skip": "跳过", "msg_down_success": "已成功调用浏览器下载 {n} 个文件。", "msg_clear_history_done": "已从历史记录中移除", "msg_skip_unzipped": "已跳过 {n} 个已解压的项目。", "msg_unzip_skip_del_confirm": "检测到 {n} 个压缩包在当前路径存在同名文件夹且状态标记为已解压,大概率已完成解压。是否将其移入回收站?\n\n(建议:请先检查同名文件夹内容是否完整)", "msg_cancel_share_confirm": "确定要取消选中的 {n} 个分享吗?\n链接将立即失效。", "msg_pwd_updated": "密码已更新", "msg_exp_updated": "有效期已更新", "msg_cancel_share_done": "已取消 {n} 个分享。", "msg_cancel_share_ing": "正在取消分享...", "msg_drag_drop_hint": "将文件拖拽到此处并释放", "str_drag_files": " 等 {n} 个文件", "msg_creating_share": "正在创建分享...", "title_share_result": "分享成功", "msg_no_files": "选中的项目为空。", "msg_scan_filter_empty": "筛选条件已排除全部文件。", "warn_del": "确定要删除选中的 {n} 项吗?", "msg_clear_sel_confirm": "已选中 {n} 个重复文件,确认要取消当前的勾选吗?", "str_bl_stat": "匹配: {n} 项 | 已选中: {m} 项", "str_hits": "命中", "msg_settings_saved": "设置已保存。页面将刷新。", "msg_settings_saved_plain": "设置已保存。", "msg_script_update_new": "检测到新版 V{v}。受发布体积限制,当前版本可能无法自动同步完整更新,可前往 GitHub 项目主页查看更新日志与项目说明。", "msg_script_update_open": "查看说明", "label_script_update_info": "版本信息", "label_script_current_version": "当前版本", "label_script_latest_version": "最新版本", "label_script_last_check": "上次检查", "str_script_update_not_checked": "未检查", "str_script_update_failed": "检查失败", "str_name_conflict": "(可能重名)", "msg_newfolder_prompt": "新文件夹名称:", "msg_newfolder_created": "已新建文件夹:{n}", "msg_rename_prompt": "输入新名称:", "msg_copy_done": "已复制。请选择粘贴位置。", "msg_cut_done": "准备移动。请选择粘贴位置。", "msg_paste_empty": "没有可粘贴的项目。", "msg_copy_empty": "剪贴板为空或无文本数据。", "msg_add_success": "已追加 {n} 行数据。", "msg_del_done": "已删除选中行。", "msg_del_select": "请先点击选中要删除的行!", "msg_del_items_done": "已删除 {n} 个项目。", "msg_copy_success": "复制成功", "str_redirecting": "正在跳转 Google Lens...", "msg_manual_paste": "图床上传超时。已复制截图,请在新窗口按 {cmd}", "msg_starring": "正在添加星标...", "msg_unstarring": "正在取消星标...", "msg_star_added": "已添加星标", "msg_unstar_done": "已取消星标", "msg_empty_trash_confirm": "确定要清空回收站吗?此操作无法撤销!", "msg_trash_emptied": "回收站已清空。", "msg_del_forever_confirm": "确定要彻底删除这 {n} 个项目吗?此操作无法撤销!", "msg_restore_done": "已成功还原 {n} 个项目。", "msg_auto_sub_load": "已自动加载字幕:{n}", "msg_dl_sub": "正在下载字幕...", "msg_fallback_report": "当前源播放异常,已切换备用源:{n}", "tip_manual_sub": "提示:下载 .srt 或 .vtt 后,直接拖入播放器即可加载。", "msg_resume_hint": "已为您从 {t} 继续播放,点击这里 ", "msg_unzip_confirm_n": "确定要在当前目录解压这 {n} 个文件吗?", "msg_task_paused": "已暂停", "msg_task_added": "已添加 {n} 个上传任务", "msg_task_fast_success": "秒传成功", "msg_task_upload_done": "上传完成", "log_dirty_data": "拦截到脏数据入侵!请求模式与当前视图不符,已物理熔断。", "msg_network_unstable": "网络连接波动,正在自动恢复...", "msg_skip_locked": "{n} 个被锁定", "msg_skip_self": "{n} 个原地移动", "msg_skip_conflict": "{n} 个子路径忙碌", "msg_skip_invalid": "已自动跳过无效项: ", "msg_creating_cloud_task": "正在创建云下载任务...", "str_parsing_torrent": "正在解析种子文件...", "err_torrent_no_info": "解析失败:未发现有效信息", "err_file_read": "文件读取失败", "msg_cloud_task_finish": "创建完成:{s} 成功,{f} 失败", "msg_cloud_task_success": "🎉 已成功创建 {n} 个任务", "msg_prepare_restore": "准备还原...", "msg_smart_matching_n": "正在智能匹配密码 ({n}个)...", "msg_system_busy_retry": "系统繁忙,等待重试 ({n}/5)...", "msg_unzip_running_bg": "[{n}] 已在云端解压中,请稍后刷新查看", "msg_share_code_updated": "分享代码已更新", "msg_ghost_task_resume": "云端未完成 (点击恢复重选文件)", "msg_parsing_files": "正在解析文件,请稍候...", "msg_token_expired_retry": "登录过期,正在重试...", "msg_oss_upload_forbidden": "直传签名失败,请重试或临时使用官方上传。", "str_empty_str": "(空)", "err_clipboard_failed": "写入剪贴板失败", "err_cors_blocked": "安全拦截: 跨域访问被拒绝", "err_worker_failed": "工作线程遇到错误", "str_target_folder": "目标文件夹", "str_unknown_name": "未知名称", "btn_default": "默认", "msg_retry_submitted": "已重试提交 {n} 个任务", "msg_aria2_batch_fail_log": "\n\n检测到失败项较多,已为您自动导出完整错误清单 (.txt)", "str_aria2_fetch_err": "(获取链接失败)", "str_aria2_rpc_err": "(投递失败)", "str_aria2_aborted": "(已取消)", "str_downloader_fail_file_name": "{d}_失败清单", "msg_op_blocked_moving": "⚠️ 操作拦截\n\n后台正在执行文件搬运,请等待当前任务完成后再操作。", "msg_analyze_only_normal_dir": "请选择文件夹。", "msg_analyze_no_large_folders": "没有发现符合阈值范围的文件夹 ({s})", "title_del_task_confirm_fmt": "确认删除 {n} 条传输任务?", "lbl_del_cloud_files_too": "同时删除云盘内的文件", "msg_task_del_success_fmt": "已删除 {n} 个任务", "title_clear_task_confirm": "确认清空所有上传任务?", "msg_task_clear_success_fmt": "已清空 {n} 个上传任务", "msg_unzip_virtual_view_warn": "您正在虚拟视图中操作。解压后的文件将存放在各压缩包所在的原始文件夹中,而不会直接出现在当前列表中。

是否继续?", "msg_smart_matching_file": "智能匹配密码中... ({n})", "msg_unzip_batch_submitted": "✅ 已完成 {n} 个解压任务", "msg_unzip_batch_skipped": " ({n} 个跳过)", "msg_unzip_check_source": "。请前往源目录查看结果。", "msg_task_deleted": "任务已删除", "msg_scan_done": "扫描完成!\n共发现 {n} 个文件,遍历了 {f} 个文件夹。", "msg_down_progress": "正在调用浏览器下载...", "msg_down_confirm_total": "✅ 扫描完毕,共找到 {n} 个文件,总大小约 {s}。\n\n⚠️ 警告:当前任务规模较大,浏览器批量下载可能导致页面卡顿或被浏览器拦截。\n建议当文件数超过 {fc} 个或总大小超过 {fs} 时,优先使用 Aria2。\n\n是否仍然使用浏览器下载?", "msg_downloader_sending_batch": "🚀 正在分批发送任务至 {n}...", "msg_downloader_prepare_original_link": "正在提取原画播放链路.", "msg_downloader_check_fail": "{n} 连接失败!\n请检查地址和 Token。", "msg_downloader_sent": "已将 {n} 个文件发送到 {d}。", "msg_downloader_sent_with_fail": "已发送到 {d}:成功 {s} 个,失败 {f} 个。", "msg_downloader_keep_structure_dir_confirm": "当前下载器未填写下载路径,勾选的下载保存文件夹内部结构将无效。\n\n是否继续下载?", "tip_downloader_switch_locked": "批量任务进行中,暂不能切换下载器", "msg_gopeed_not_http": "Gopeed 当前仅支持 HTTP/HTTPS 直链任务", "msg_abdm_not_http": "ABDM 当前仅支持 HTTP/HTTPS 直链任务", "msg_batch_scanning": "🚀 正在高速扫描目录结构...", "msg_batch_hydrating": "⚡ 正在并行提取下载链路...", "msg_batch_no_files": "未发现可下载的文件。", "msg_batch_filtered": "下载过滤规则已跳过 {n} 个文件。", "msg_batch_all_filtered": "已全部过滤:{n} 个文件均命中下载过滤规则。", "msg_dup_none": "未发现重复项。", "msg_bl_stop": "操作已停止。", "msg_bl_add_done": "已将 {n} 个项目添加到记录。", "msg_bl_remove_done": "已从记录中移除 {n} 个项目。", "msg_bl_empty": "名单列表为空,无法运行。", "msg_bl_clear_confirm": "确定要清空所有记录条目吗?此操作不可恢复。", "msg_blacklist_run_none": "网盘中未发现符合名单条件的项目。", "msg_bl_run_limit": "⚠️ 模式限制\n\n清理操作涉及物理文件递归操作。目前处于非标准目录,无法准确定位物理扫描范围。\n\n请返回主页常规文件夹后再执行清理。", "msg_del_protected": "资源管理器中已记录的 {n} 个文件,已保护并跳过删除。", "msg_del_none": "没有可删除的文件。", "msg_offline_reference_missing": "云盘文件已删除", "rn_tip_wait": "请设置规则", "rn_tip_jav": "点击上方按钮开始智能匹配", "rn_tip_none": "没有匹配的项目或名称", "rn_tip_series_folder_only": "选中项均为文件夹,没有匹配的文件", "rn_stat": "匹配: {n} 项 | 有效变更: {m} 项", "rn_warn_confirm": "确定要重命名 {n} 个文件吗?", "msg_bulkrename_done": "已重命名 {n} 个项目。", "msg_rn_all_skipped": "❌ 所有项目均因重名被跳过,未执行任何操作。", "msg_rn_fail_count": "跳过已重名 {n} 个项目", "msg_prune_confirm": "是否开始搜索当前列表中的空文件夹?", "msg_prune_none": "未发现空文件夹。", "msg_prune_found": "发现了 {n} 个空文件夹。\n是否立即删除?", "msg_global_warn": "即将开始全盘文件同步。\n\n文件同步后缓存到本地内存中,网页刷新前持续存在。\n\n是否继续?", "msg_init_scan_sel": "正在初始化选中项扫描...", "warn_clear_history": "确定要从历史记录中移除选中的 {n} 项吗?\n(这不会删除您的云端文件)", "warn_clear_all_history": "确定要清空全部播放历史吗?\n这会清除播放历史,但不会删除您的云端文件。", "msg_clear_all_history_done": "已清空播放历史", "err_official_history_delete": "播放历史删除失败", "err_official_history_clear": "播放历史清空失败", "str_jav_no_match": "(未匹配到番号)", "msg_unzip_fail": "解压请求失败", "msg_turbo_activated": "极速模式已激活:脚本已深度接管网页逻辑,确保稳定流畅。", "msg_ana_warn": "文件夹查重提示:判定基于算法推测,删除前请务必人工核对,以防误删。", "err_migrate_too_many": "⚠️ 选中项过多\n\n当前请求数据量 ({s}MB) 已超过服务器 4MB 的硬性限制。\n\n【解决方案】:\n请在常规目录中直接选中【文件夹】进行迁移,而不是在全盘透视模式下选中海量独立文件。", "msg_migrate_quota_err": "⚠️ 目标账号空间不足!\n\n系统提示:{d}\n\n是否保留迁移记录?\n(选\"是\"保留记录,清理空间后刷新页面可重试;选\"否\"则终止本次迁移)", "msg_migrate_err_keep": "迁移异常:\n{e}\n\n是否保留迁移记录以便稍后重试?\n(选\"否\"将清除记录,不再弹窗)", "title_migrate_fail": "迁移失败", "err_invalid_links": "请输入正确的链接", "err_invalid_torrent": "无效的种子文件格式", "err_torrent_complex": "解析复杂度过高,可能是非法文件", "err_torrent_format": "种子文件结构损坏", "err_torrent_len": "字段长度解析异常", "err_torrent_char": "解析遇到非法字符", "err_share_code_exists": "该分享代码已被占用", "err_folder_not_ready": "云端文件夹正在创建中,请稍后再试", "err_item_deleted": "该项目不存在", "err_clipboard_denied": "剪贴板访问被拒绝", "err_worker": "工作线程错误", "err_capture": "截图失败。", "err_captcha_simple": "验证失败。请在网页列表手动收藏一次文件以完成验证。", "err_sub_dl_fail": "字幕下载失败: ", "err_sub_drop_type": "只解析字幕类型文件", "err_codec_t1": "无法播放视频编码 ({c})", "err_codec_t2": "您的浏览器不支持该视频格式。
请点击下方按钮调用外部播放器。", "err_web_unsupported_t1": "无法在网页中播放此视频", "err_web_unsupported_t2": "当前设备或浏览器可能不支持该视频的网页解码。
请点击下方按钮调用外部播放器。", "err_pwd_simple": "密码错误", "err_task_exists": "任务已存在", "err_network_break": "图片节点网络断流,请再次点击重试", "err_no_failed_task": "未选中失败的任务", "err_unknown": "未知错误", "err_invalid_regex": "无效的正则表达式", "err_parent_not_found": "文件夹不存在", "msg_sys_error": "不允许操作系统文件夹", "msg_video_fail": "无法获取视频链接。", "audio_player_title": "音乐播放器", "audio_loading": "正在加载音频...", "audio_ready": "音频已就绪", "audio_playing": "正在播放", "audio_paused": "已暂停", "audio_play_failed": "音频播放失败", "audio_link_missing": "无法获取音频链接", "audio_format_unsupported": "当前音频格式可能不受浏览器支持", "audio_link_expired": "音频链接已失效,请重试", "audio_share_unsupported": "暂不支持播放此分享音频", "audio_prev": "上一首 [Ctrl + ←]", "audio_next": "下一首 [Ctrl + →]", "audio_mini_prev": "上一首", "audio_mini_next": "下一首", "audio_play": "播放", "audio_pause": "暂停", "audio_mute": "静音", "audio_unmute": "取消静音", "audio_volume": "音量", "audio_playlist": "播放列表", "audio_playlist_close": "收起列表", "audio_playlist_empty": "暂无音频", "audio_progress": "播放进度", "audio_format": "音频格式", "audio_mode_order": "顺序播放", "audio_mode_list": "列表循环", "audio_mode_single": "单曲循环", "audio_mode_shuffle": "随机播放", "audio_mini_mode": "弹窗模式", "audio_mini_restore": "恢复播放器", "audio_mini_close": "关闭音乐", "err_paste_descendant": "不能移动或复制到当前或当前子目录下", "err_quota_exceeded": "存储空间不足", "err_name_exists": "文件名称不能重复", "err_share_pass": "自定义密码需为 4-10 位字符", "err_share_limit": "分享失败:单次最多只能分享 100 个项目", "err_migrate_limit": "迁移失败:单次最多只能迁移 100 个项目", "lbl_hard_delete": "彻底删除 (不进入回收站)", "str_error": "错误", "str_error_crit": "严重错误", "str_action_failed": "操作失败", "str_scan_error": "扫描错误", "err_limit_too_low": "修改失败:新次数 ({n}) 必须大于当前已保存次数 ({s})", "err_vault_max": "密码金库最多仅支持存储 50 个常用密码", "err_pwd_len": "单个密码长度不能超过 127 个字符", "msg_share_parse_history_recorded": "已记录分享解析历史", "share_copy_suffix": "复制这段内容后打开 PikPak-App,畅享极速秒播", "str_success": "成功:", "str_upload_3": "节点2超时,尝试最后节点...", "lbl_done_check": "✔ 完成" } }; function getStrings() { const lang = getLang(); const base = T_LOCAL.zh || {}; const local = T_LOCAL[lang] || {}; const remote = pkRemoteI18n && pkRemoteI18n.lang === lang ? pkRemoteI18n.data : null; return mergeI18nPack(mergeI18nPack(base, remote), local); }; const pkI18nBootReady = ensureI18nReady(); let cachedCredKey = null; let cachedCaptchaKey = null; let memoryCapturedToken = ''; document.addEventListener('pk-token-captured', (e) => { memoryCapturedToken = e.detail; }); function resetHeaderCache() { cachedCredKey = null; cachedCaptchaKey = null; memoryCapturedToken = ''; } function purgeAllCachesOnLogout() { resetHeaderCache(); if (typeof globalCache !== 'undefined') globalCache.clear(); if (typeof globalLineageMap !== 'undefined') globalLineageMap.clear(); if (typeof globalParentIndex !== 'undefined') globalParentIndex.clear(); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.clear(); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.clear(); if (typeof backgroundQueue !== 'undefined') backgroundQueue.length = 0; if (typeof isBackgroundRunning !== 'undefined') isBackgroundRunning = false; if (typeof DurationProber !== 'undefined') DurationProber.reset(); const ui = document.querySelector('.pk-ov'); if (ui) { ui.remove(); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; } const btn = document.getElementById('pk-launch'); if (btn) btn.remove(); if (typeof pkState !== 'undefined' && pkState) pkState = null; if (typeof globalSavedState !== 'undefined') globalSavedState = null; if (typeof globalPreloadPromise !== 'undefined') globalPreloadPromise = null; } async function confirmedLogout(reason = 'unknown', graceMs = 5000, waitMs = 5200) { console.warn(`[Auth] Confirmed logout requested: ${reason}`); if (typeof window.pkEnterAuthRecoveryWindow === 'function') { window.pkEnterAuthRecoveryWindow(`confirmed-logout:${reason}`, graceMs); } await sleep(800); const isAuthReady = await waitForAuth(waitMs); if (isAuthReady) { console.warn(`[Auth] Logout aborted, auth recovered: ${reason}`); if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); return false; } console.warn(`[Auth] Logout confirmed after recheck: ${reason}`); if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); if (!location.href.includes('/login')) window.location.href = 'https://mypikpak.com/drive/login'; return true; } (() => { let authCheckTimer = null; let authRouteGraceTimer = null; let authRecoveryUntil = 0; let authRecoveryReason = ''; let authRecoveryNeedReload = false; let officialAuthReadyUntil = 0; let pageHiddenAt = document.hidden ? Date.now() : 0; const AUTH_PURGE_DELAY_MS = 1200; const AUTH_ROUTE_GRACE_MS = 4000; const AUTH_RESUME_GRACE_MS = 5000; const AUTH_RESUME_MIN_HIDDEN_MS = 45000; const OFFICIAL_AUTH_READY_TTL_MS = 2 * 60 * 1000; const isLoginRoute = () => location.href.includes('/login') || location.pathname.includes('login'); const hasCredentialSnapshot = () => { if (memoryCapturedToken && memoryCapturedToken.length > 10) return true; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('credentials')) { try { const v = JSON.parse(localStorage.getItem(k)); if (v && v.access_token) return true; } catch {} } } return false; }; const markAuthRecovered = () => { authRecoveryUntil = 0; authRecoveryReason = ''; authRecoveryNeedReload = false; if (authRouteGraceTimer) { clearTimeout(authRouteGraceTimer); authRouteGraceTimer = null; } }; const isAuthRecoveryReloadReason = (reason = '') => !String(reason || '').startsWith('resume:'); const enterAuthRecoveryWindow = (reason = 'unknown', graceMs = AUTH_ROUTE_GRACE_MS) => { const now = Date.now(); const wasRecovering = authRecoveryUntil > now; const needReload = isAuthRecoveryReloadReason(reason); authRecoveryUntil = Math.max(authRecoveryUntil, now + graceMs); authRecoveryReason = String(reason || 'unknown'); authRecoveryNeedReload = !!(needReload || (wasRecovering && authRecoveryNeedReload)); if (authRouteGraceTimer) clearTimeout(authRouteGraceTimer); authRouteGraceTimer = setTimeout(() => { authRouteGraceTimer = null; if (isLoginRoute() && !hasCredentialSnapshot()) { checkAndPurgeAuth(`grace-expired:${reason}`); } else if (isOfficialAuthReady()) { markAuthRecovered(); } }, graceMs + 50); }; const isOfficialAuthReady = () => officialAuthReadyUntil > Date.now(); const markOfficialAuthReady = (reason = 'official-auth-ready') => { officialAuthReadyUntil = Date.now() + OFFICIAL_AUTH_READY_TTL_MS; markAuthRecovered(); }; const markOfficialAuthError = (reason = 'official-auth-error') => { officialAuthReadyUntil = 0; enterAuthRecoveryWindow(reason, AUTH_ROUTE_GRACE_MS); }; document.addEventListener('pk-official-auth-ready', (e) => { const shouldReloadAfterAuth = authRecoveryUntil > Date.now() && authRecoveryNeedReload; markOfficialAuthReady(e.detail?.url || 'official-auth-ready'); if (shouldReloadAfterAuth && typeof window.pkForceManagerReloadAfterAuth === 'function') { window.pkForceManagerReloadAfterAuth(`official-auth-ready:${authRecoveryReason || 'unknown'}`); } }); document.addEventListener('pk-official-auth-error', (e) => { markOfficialAuthError(`official-auth-error:${e.detail?.status || 'unknown'}`); }); const checkAndPurgeAuth = (reason = 'unknown') => { if (authCheckTimer) clearTimeout(authCheckTimer); const extraWait = authRecoveryUntil > Date.now() ? (authRecoveryUntil - Date.now() + 50) : 0; authCheckTimer = setTimeout(() => { if (hasCredentialSnapshot()) { markAuthRecovered(); return; } if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); if (!isLoginRoute()) window.location.href = 'https://mypikpak.com/drive/login'; }, Math.max(AUTH_PURGE_DELAY_MS, extraWait)); }; const handlePageResume = (reason = 'resume') => { const hiddenFor = pageHiddenAt ? (Date.now() - pageHiddenAt) : 0; pageHiddenAt = 0; if (hiddenFor < AUTH_RESUME_MIN_HIDDEN_MS && reason !== 'pageshow-bfcache') return; enterAuthRecoveryWindow(`resume:${reason}`, AUTH_RESUME_GRACE_MS); checkAndPurgeAuth(`resume:${reason}`); }; document.addEventListener('pk-token-captured', () => { const shouldReloadAfterAuth = authRecoveryUntil > Date.now() && authRecoveryNeedReload; const reloadReason = authRecoveryReason || 'unknown'; markAuthRecovered(); if (shouldReloadAfterAuth && typeof window.pkForceManagerReloadAfterAuth === 'function') { window.pkForceManagerReloadAfterAuth(`token-captured:${reloadReason}`); } }); document.addEventListener('visibilitychange', () => { if (document.hidden) { pageHiddenAt = Date.now(); return; } handlePageResume('visibility'); }); window.addEventListener('pageshow', (e) => { if (e.persisted) { handlePageResume('pageshow-bfcache'); return; } if (pageHiddenAt) handlePageResume('pageshow'); }); window.addEventListener('focus', () => { if (pageHiddenAt) handlePageResume('focus'); }); window.addEventListener('storage', (e) => { if (e.key && (e.key.startsWith('credentials') || e.key.startsWith('captcha') || e.key === 'pk_captured_captcha')) { if (!e.newValue) { enterAuthRecoveryWindow(`storage-remove:${e.key}`); checkAndPurgeAuth(`storage-remove:${e.key}`); } else { resetHeaderCache(); markAuthRecovered(); } } }); const _origSetItem = Storage.prototype.setItem; Storage.prototype.setItem = function(key, value) { _origSetItem.apply(this, arguments); if (key && (key.startsWith('credentials') || key.startsWith('captcha') || key === 'pk_captured_captcha')) { resetHeaderCache(); markAuthRecovered(); } }; const _origRemoveItem = Storage.prototype.removeItem; Storage.prototype.removeItem = function(key) { _origRemoveItem.apply(this, arguments); if (key && (key.startsWith('credentials') || key.startsWith('captcha'))) { enterAuthRecoveryWindow(`remove:${key}`); checkAndPurgeAuth(`remove:${key}`); } }; const checkAuthRoute = () => { if (isLoginRoute()) { enterAuthRecoveryWindow('route-login'); checkAndPurgeAuth('route-login'); } else { markAuthRecovered(); } }; const _origPushState = history.pushState; history.pushState = function() { _origPushState.apply(this, arguments); checkAuthRoute(); }; const _origReplaceState = history.replaceState; history.replaceState = function() { _origReplaceState.apply(this, arguments); checkAuthRoute(); }; window.pkEnterAuthRecoveryWindow = enterAuthRecoveryWindow; window.pkMarkAuthRecovered = markAuthRecovered; window.pkIsAuthRecoveryActive = () => authRecoveryUntil > Date.now(); window.addEventListener('popstate', checkAuthRoute); })(); function getHeaders() { const recoveryActive = typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive(); let token = memoryCapturedToken || '', captcha = ''; if (recoveryActive) { cachedCredKey = null; cachedCaptchaKey = null; } if (!token && cachedCredKey) { try { const v = JSON.parse(localStorage.getItem(cachedCredKey)); if (v && v.access_token) token = v.token_type + ' ' + v.access_token; } catch {} } if (!token) { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('credentials')) { try { const v = JSON.parse(localStorage.getItem(k)); if (v && v.access_token) { token = v.token_type + ' ' + v.access_token; cachedCredKey = k; break; } } catch {} } } } if (cachedCaptchaKey) { try { const v = JSON.parse(localStorage.getItem(cachedCaptchaKey)); if (v) captcha = v.captcha_token; } catch {} } if (!captcha) { captcha = localStorage.getItem('pk_captured_captcha') || ''; if (!captcha) { for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('captcha')) { try { const v = JSON.parse(localStorage.getItem(k)); if (v) { captcha = v.captcha_token; cachedCaptchaKey = k; break; } } catch {} } } } } const headers = { 'Content-Type': 'application/json', 'Authorization': token, 'x-device-id': localStorage.getItem('deviceid') || '', 'x-captcha-token': captcha }; if (headers.Authorization && headers.Authorization.length > 10 && typeof window.pkMarkAuthRecovered === 'function' && typeof window.pkIsOfficialAuthReady === 'function' && window.pkIsOfficialAuthReady()) { window.pkMarkAuthRecovered(); } return headers; } let authReadyProbePromise = null; let authReadyProbeAt = 0; async function probeAuthReadyByAbout(headers) { if (!headers || !headers.Authorization || headers.Authorization.length <= 10) return false; if (typeof window.pkIsOfficialAuthReady === 'function' && window.pkIsOfficialAuthReady()) return true; if (authReadyProbePromise) return authReadyProbePromise; if (Date.now() - authReadyProbeAt < 900) return false; authReadyProbeAt = Date.now(); authReadyProbePromise = (async () => { try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/about?_t=${Date.now()}`, { method: 'GET', headers, cache: 'no-store' }); if (!res.ok) { if ((res.status === 400 || res.status === 401 || res.status === 403) && typeof window.pkEnterAuthRecoveryWindow === 'function') { window.pkEnterAuthRecoveryWindow(`about-probe-${res.status}`, 5000); } return false; } const data = await res.clone().json().catch(() => null); if (data && (data.kind === 'drive#about' || data.quota || data.user || data.sub)) { if (typeof window.pkMarkOfficialAuthReady === 'function') window.pkMarkOfficialAuthReady('about-probe'); return true; } } catch (e) {} return false; })().finally(() => { authReadyProbePromise = null; }); return authReadyProbePromise; } async function waitForAuth(timeout = 10000) { const start = Date.now(); let probeCount = 0; let hardTimeout = timeout + ((typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 3000 : 0); while (Date.now() - start < hardTimeout) { const h = getHeaders(); if (h.Authorization && h.Authorization.length > 10) { if (typeof window.pkIsOfficialAuthReady === 'function' && window.pkIsOfficialAuthReady()) { if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); return true; } if ((probeCount % 3) === 0 && await probeAuthReadyByAbout(h)) return true; } if ((probeCount++ % 5) === 0 && typeof resetHeaderCache === 'function') { resetHeaderCache(); } if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { hardTimeout = Math.max(hardTimeout, timeout + 3000); } await sleep((typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 250 : 200); } return false; } async function apiList(parentId, limit = 1000, onProgress, signal, trashed = false, isBackground = false) { let all = [], next = null, safe = 5000; const TIMEOUT_MS = 25000; const filters = trashed ? `&filters=${encodeURIComponent('{"trashed":{"eq":true}}')}&parent_id=*` : `&parent_id=${parentId || ''}`; do { let pageRetries = 0; const MAX_PAGE_RETRIES = 5; let pageSuccess = false; while (pageRetries < MAX_PAGE_RETRIES && !pageSuccess) { if (signal?.aborted) throw new DOMException('Aborted by user', 'AbortError'); const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&with_audit=true&_t=${Date.now()}${next ? `&page_token=${next}` : ''}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true }); try { const res = await fetch(url, { headers: getHeaders(), signal: controller.signal }); clearTimeout(timeoutId); if (!res.ok) { if (res.status === 404) { if (!isBackground) console.warn(`[API] 404 Not Found (Skipped): ${parentId || 'Root'}`); return []; } if (res.status === 401 || res.status === 403) { console.warn(`[API] ${res.status} Unauthorized. Throwing hard auth error...`); localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); throw new Error(`API Error ${res.status}`); } if (res.status === 400) { console.warn(`[API] 400 Error. Possible captcha intercept.`); localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); try { if (typeof showToast !== 'undefined') showToast(getStrings().err_captcha_simple, 'error'); } catch(e){} throw new Error('CAPTCHA_INTERCEPT'); } if (res.status === 429) { await sleep(3000 * (pageRetries + 1)); throw new Error('RATE_LIMIT'); } throw new Error(`HTTP_${res.status}`); } syncTime(res.headers); const data = await res.json(); if (!data.files && data.next_page_token) { throw new Error('PAGINATION_INCOMPLETE'); } if (data.files) { const validFiles = data.files.filter(f => trashed ? f.trashed : !f.trashed).map(f => minifyFile(f, isBackground)); all.push(...validFiles); if (onProgress) onProgress(all.length); if (isBackground && parentId !== undefined && typeof globalCache !== 'undefined') { globalCache.set(parentId, { items: [...all], nextToken: data.next_page_token }); } } next = data.next_page_token; pageSuccess = true; if (next) await sleep(50); } catch (e) { clearTimeout(timeoutId); pageRetries++; safe--; let isTimeout = false; let errMsg = e.message; if (e.name === 'AbortError' && !(signal && signal.aborted)) { isTimeout = true; e = new Error('FETCH_TIMEOUT'); errMsg = 'Local Timeout'; } const isNetworkError = e.name === 'TypeError' || errMsg.includes('fetch') || errMsg.includes('PAGINATION') || isTimeout; if ((isNetworkError || errMsg === 'AUTH_RETRY') && safe > 0) { const backoff = pageRetries === 1 ? 500 : Math.min(pageRetries * 2000, 10000); console.warn(`[API] Retry ${pageRetries}/${MAX_PAGE_RETRIES} for ${parentId || 'Root'} due to ${errMsg}. Wait ${backoff}ms`); await sleep(backoff); continue; } throw e; } } } while (next && safe > 0); return all; } async function apiGet(id) { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${id}?thumbnail_size=SIZE_MEDIUM&_t=${Date.now()}`, { headers: getHeaders() }); try { syncTime(res.headers); } catch (e) {} if (!res.ok) { const data = await res.json().catch(() => ({})); const msg = typeof pickOfficialApiErrorText === 'function' ? pickOfficialApiErrorText(data, `API Error ${res.status}`) : (data.error_description || data.message || data.msg || data.error || `API Error ${res.status}`); const err = new Error(msg); err.status = res.status; err.statusCode = res.status; err.code = String(data.code || data.error || data.error_code || (res.status === 404 ? 'not_found' : '') || ''); err.errorCode = Number(data.error_code || 0) || 0; err.data = data; err.response = { status: res.status, statusCode: res.status, data }; throw err; } 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 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_left_seconds: item.expiration_left_seconds, 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; let lastErr = null; for (let i = 0; i < 3; i++) { try { res = await fetch(url, { headers: getHeaders(), signal }); if (res.ok) break; if (res.status === 401 || res.status === 403) throw new Error(`API Error ${res.status}`); lastErr = new Error(`Offline Task API ${res.status}`); if (res.status === 429) await sleep(2000); else await sleep(1000); } catch (e) { lastErr = e; const msg = String(e && e.message || e || ''); if (msg.includes('401') || msg.includes('403') || (e && e.name === 'AbortError')) throw e; await sleep(1000); } } if (!res || !res.ok) { if (signal && signal.aborted) throw new DOMException('Aborted', 'AbortError'); throw lastErr || new TypeError('Failed to fetch offline tasks'); } 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; } function getOfflineTaskPrimaryId(taskOrItem) { if (!taskOrItem) return ''; if (typeof taskOrItem === 'string' || typeof taskOrItem === 'number') return String(taskOrItem || '').trim(); if (typeof taskOrItem !== 'object') return ''; return String(taskOrItem.id || taskOrItem.task_id || '').trim(); } async function apiCancelTask(ids, deleteFiles = false, options = {}) { const strict = !!(options && options.strict); let filesToDelete = []; const explicitTaskMap = new Map(); if (options && Array.isArray(options.taskItems)) { options.taskItems.forEach(task => { const id = getOfflineTaskPrimaryId(task); if (id) explicitTaskMap.set(id, task); }); } if (deleteFiles && (explicitTaskMap.size || (typeof pkState !== 'undefined' && pkState.itemMap))) { ids.forEach(taskId => { const task = explicitTaskMap.get(String(taskId || '')) || (typeof pkState !== 'undefined' && pkState.itemMap ? pkState.itemMap.get(taskId) : null); if (task && String(task.id || task.task_id || '') === String(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}`); if (strict && res.ok && await isTaskDeleteFailedPayload(res, chunk)) throw new Error('Task Del Err failed'); } catch(e) { if (strict) throw e; console.warn("Task delete warning:", e); } } if (filesToDelete.length > 0) { 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); try { await fetch(trashUrl, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: fileChunk }) }); } catch (e) { if (!strict) throw e; console.warn("Task file delete warning:", e); } } } return true; } async function isTaskDeleteFailedPayload(res, ids = []) { let json = null; try { const text = await res.clone().text(); if (!text || !text.trim()) return false; json = JSON.parse(text); } catch (e) { return false; } const idSet = new Set((ids || []).map(id => String(id || '')).filter(Boolean)); const isFailText = value => /^(failed|fail|error|rejected|denied)$/i.test(String(value || '').trim()); const hasOkText = value => /^(success|ok|deleted|cancelled|canceled|not_found|task_not_found|already_deleted)$/i.test(String(value || '').trim()); const scan = value => { if (!value || typeof value !== 'object') return false; if (Array.isArray(value)) return value.some(scan); const localId = String(value.task_id || value.taskId || value.id || '').trim(); const applies = !localId || idSet.size === 0 || idSet.has(localId); const fields = [value.status, value.result, value.code, value.error, value.reason, value.message].filter(v => v !== undefined && v !== null); if (applies && fields.some(isFailText) && !fields.some(hasOkText)) return true; return Object.keys(value).some(key => scan(value[key])); }; return scan(json); } function isOfficialApiReadableErrorText(value) { const text = String(value || '').trim().replace(/\s+/g, ' '); if (!text) return ''; const isCodeLike = /^[a-z0-9_.:-]+$/i.test(text) && /[_:.-]/.test(text) && !/[^\x00-\x7F]/.test(text); return isCodeLike ? '' : text; } function pickOfficialApiErrorText(data, fallback = '') { const seen = new Set(); const out = []; const push = v => { const text = isOfficialApiReadableErrorText(v); if (!text || seen.has(text)) return; seen.add(text); out.push(text); }; const walk = obj => { if (!obj || typeof obj !== 'object') return; ['error_description', 'message', 'msg', 'status_text', 'description', 'detail', 'reason'].forEach(k => push(obj[k])); if (Array.isArray(obj.details)) obj.details.forEach(walk); if (obj.error && typeof obj.error === 'object') walk(obj.error); }; walk(data); return out.join(' / ') || fallback; } 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(() => ({})); const msg = pickOfficialApiErrorText(err, `Add Task Error ${res.status}`); const e = new Error(msg); e.status = res.status; e.code = err && err.error ? String(err.error) : ''; e.errorCode = err && err.error_code !== undefined ? Number(err.error_code) : 0; e.data = err; throw e; } 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; } function patchLocalShareItem(state, baseItem, patch = {}) { const id = String((baseItem && baseItem.id) || patch.id || patch.share_id || ''); const S0 = state && typeof state === 'object' ? state : null; if (!id || !S0) return null; const apply = obj => { if (!obj || String(obj.id || obj.share_id || '') !== id) return; Object.assign(obj, patch); }; if (baseItem) Object.assign(baseItem, patch); if (S0.itemMap && S0.itemMap.has(id)) apply(S0.itemMap.get(id)); else if (S0.itemMap) S0.itemMap.set(id, Object.assign({}, baseItem || {}, patch)); [S0.items, S0.display].forEach(list => Array.isArray(list) && list.forEach(apply)); const syncCacheMap = cacheMap => { if (!cacheMap || typeof cacheMap.forEach !== 'function') return; cacheMap.forEach(data => { const list = Array.isArray(data) ? data : (data && Array.isArray(data.items) ? data.items : []); if (Array.isArray(list)) list.forEach(apply); }); }; syncCacheMap(S0.cache); if (typeof globalCache !== 'undefined') syncCacheMap(globalCache); return (S0.itemMap && S0.itemMap.get(id)) || baseItem || null; } // Original author: digbug82. Modified versions must retain this attribution and the AGPL-3.0-or-later license notice. async function coreRecursiveEngine(roots, options) { const { signal, onFile, onFolder, onFolderError, onProgress, preferFresh = false, listFolder = null, maxRetries = Infinity } = 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, failedFolders: 0, currentConcurrency: 10 }; const USER_LIMIT = parseInt(localStorage.getItem('pk_user_limit') || "200"); const ABSOLUTE_MAX = 128; const MIN_CONCURRENCY = 5; const processFolder = async (current) => { inFlight.add(current.id); try { let files = []; const folderId = current.id === 'root' ? '' : (current.id || ''); if (!listFolder && !preferFresh && typeof globalCache !== 'undefined' && globalCache.has(folderId)) { const cachedData = globalCache.get(folderId); if (Array.isArray(cachedData)) { files = cachedData; stats.cacheHits++; } } let isFromNetwork = false; let start = 0; if (listFolder) { start = performance.now(); files = await listFolder(current, signal); isFromNetwork = true; } else if (files.length === 0) { start = performance.now(); files = await apiList(folderId, 1000, null, signal, false, true); isFromNetwork = true; if (typeof globalCache !== 'undefined') globalCache.set(folderId, files); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.add(folderId); } if (signal && signal.aborted) return; const nextSubFolders = []; for (const f of files) { if (f.kind === 'drive#folder') { const myLineage = [...(current.lineage || []), { id: f.id, name: f.name }]; nextSubFolders.push({ ...f, lineage: myLineage, depth: (current.depth || 0) + 1, retryCount: 0 }); } else { stats.files++; if (onFile) onFile(f, current); } } if (onFolder) onFolder(current, files, nextSubFolders); queue.push(...nextSubFolders); stats.folders++; if (isFromNetwork) { const rtt = performance.now() - start; const DYNAMIC_MAX = Math.min(USER_LIMIT, ABSOLUTE_MAX); if (rtt < 800) { if (stats.currentConcurrency < DYNAMIC_MAX) stats.currentConcurrency += 1; } else if (rtt > 3000) { stats.currentConcurrency = Math.max(MIN_CONCURRENCY, Math.floor(stats.currentConcurrency * 0.8)); } } } catch (err) { if (err.name === 'AbortError' || (signal && signal.aborted)) return; stats.currentConcurrency = Math.max(MIN_CONCURRENCY, Math.floor(stats.currentConcurrency * 0.5)); stats.retries++; current.retryCount = (current.retryCount || 0) + 1; if (current.retryCount >= maxRetries) { stats.failedFolders++; if (onFolderError) onFolderError(current, err, stats); return; } 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 AGPL-3.0-or-later ", "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;"); 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; } function isOfficialDirFallbackFolder(item) { return !!(item && item.kind === 'drive#folder'); } function getOfficialDirFallbackSize(mode = 'list') { if (mode === 'grid') return 72; if (mode === 'listMax') return 50; return 24; } function getOfficialDirFallbackSrc(item = null) { return isOfficialDirFallbackFolder(item) ? CONF.officialDirFolderFallbackIcon : CONF.officialDirFileFallbackIcon; } function getOfficialDirFallbackIconHtml(item = null, mode = 'list') { const size = getOfficialDirFallbackSize(mode); const src = getOfficialDirFallbackSrc(item); return ``; } function getOfficialFolderFallbackIconHtml(size = 24) { const n = Math.max(1, Number(size) || 24); return ``; } function getDirListIconSlotHtml(item = null, options = {}) { const mode = options.mode || 'list'; const size = Number(options.size || getOfficialDirFallbackSize(mode)) || 24; const fallbackHtml = options.fallbackHtml || getOfficialDirFallbackIconHtml(item, mode); const remoteSrc = String(options.remoteSrc || ''); const remoteClass = options.remoteClass || ''; const remoteObjectFit = options.remoteObjectFit || 'contain'; const remoteRadius = options.remoteRadius || '0'; const hasRemote = !!remoteSrc; const showFallbackFirst = !hasRemote || (typeof navigator !== 'undefined' && navigator.onLine === false); const slotStyle = `position:relative;width:${size}px;height:${size}px;min-width:${size}px;display:flex;align-items:center;justify-content:center;line-height:0;flex-shrink:0;overflow:visible;`; const fallbackStyle = `position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:${showFallbackFirst ? '1' : '0'};transition:opacity .12s ease;`; const remoteStyle = `position:absolute;inset:0;width:${size}px;height:${size}px;object-fit:${remoteObjectFit};border-radius:${remoteRadius};margin:0!important;background:transparent;opacity:0;transition:opacity .12s ease;`; const remoteHtml = hasRemote ? `` : ''; return `${fallbackHtml}${remoteHtml}`; } function getPreviewIconFailCache() { if (!window.pkPreviewIconFailCache) window.pkPreviewIconFailCache = new Set(); return window.pkPreviewIconFailCache; } function isPreviewIconFailed(src) { return !!src && getPreviewIconFailCache().has(src); } function markPreviewIconFailed(src) { if (src) getPreviewIconFailCache().add(src); } function bindPreviewIconFallback(root) { if (!root) return; root.querySelectorAll('img[data-pk-preview-src]').forEach(img => { if (img.dataset.pkPreviewBound === '1') return; img.dataset.pkPreviewBound = '1'; img.addEventListener('error', () => { markPreviewIconFailed(img.dataset.pkPreviewSrc || img.getAttribute('src') || ''); img.style.display = 'none'; const fallback = img.nextElementSibling; if (fallback) fallback.style.display = 'inline-flex'; }, { once: true }); }); } async function openManager(initialCache, preloadPromise) { const L = getStrings(); const lang = getLang(); function normalizeAriaRpcUrl(url) { let u = (url || '').trim(); if (!u) u = 'http://localhost:6800/jsonrpc'; if (!/^https?:\/\//i.test(u) && !/^wss?:\/\//i.test(u)) u = 'http://' + u; if (!u.includes('/jsonrpc') && !u.includes('?')) { u = u.endsWith('/') ? u + 'jsonrpc' : u + '/jsonrpc'; } return u; } function normalizeAriaDownloadDir(dir) { return String(dir || '').trim().replace(/^["']|["']$/g, ''); } function normalizeDownloaderType(type) { const t = String(type || '').trim().toLowerCase(); if (t === 'gopeed') return 'gopeed'; if (t === 'abdm') return 'abdm'; if (t === 'idm') return 'idm'; if (t === 'motrix') return 'aria2'; return 'aria2'; } function getDownloaderName(type = getCurrentDownloaderType()) { const t = normalizeDownloaderType(type); if (t === 'gopeed') return L.label_downloader_gopeed; if (t === 'abdm') return L.opt_downloader_abdm || 'ABDM'; if (t === 'idm') return L.label_downloader_idm; return L.label_downloader_aria2; } function getCurrentDownloaderType() { return normalizeDownloaderType(gmGet('pk_downloader_type', CONF.downloaderType)); } function isStoredDownloaderAddressCleared(type) { const t = normalizeDownloaderType(type); const key = t === 'gopeed' ? 'pk_gopeed_url' : (t === 'abdm' ? 'pk_abdm_url' : (t === 'aria2' ? 'pk_aria2_url' : '')); if (!key || !gmHas(key)) return false; return String(gmGet(key, '') ?? '').trim() === ''; } function getStoredAriaRpcUrlForDownload() { return gmHas('pk_aria2_url') ? gmGet('pk_aria2_url', '') : getDefaultRpcUrlForDownloader('aria2'); } function getStoredGopeedUrlForSettings() { if (!gmHas('pk_gopeed_url')) return normalizeGopeedApiUrl(CONF.gopeedApiUrl); const raw = gmGet('pk_gopeed_url', ''); return String(raw ?? '').trim() ? normalizeGopeedApiUrl(raw) : ''; } function getStoredAbdmUrlForSettings() { if (!gmHas('pk_abdm_url')) return normalizeAbdmApiUrl(CONF.abdmApiUrl); const raw = gmGet('pk_abdm_url', ''); return String(raw ?? '').trim() ? normalizeAbdmApiUrl(raw) : ''; } function getSettingsModalOverlay() { const modals = Array.from(document.querySelectorAll('.pk-modal-ov')).reverse(); return modals.find(modal => modal.querySelector('#cs_downloader_type')) || null; } function getSettingsRowElement(modal, settingKey) { if (!modal) return null; const marked = modal.querySelector(`[data-setting-key="${settingKey}"]`); if (marked) return marked; if (settingKey === 'label_downloader_type') { const select = modal.querySelector('#cs_downloader_type'); return select ? (select.closest('[data-setting-key],.pk-custom-select,.pk-setting-fieldset') || select) : null; } if (settingKey === 'label_aria2_url') { const input = modal.querySelector('#set_aria_url'); return input ? (input.closest('[data-setting-key],.pk-setting-stack>div,.pk-setting-fieldset') || input) : null; } if (settingKey === 'label_gopeed_url') { const input = modal.querySelector('#set_gopeed_url'); return input ? (input.closest('[data-setting-key],.pk-setting-stack>div,.pk-setting-fieldset') || input) : null; } if (settingKey === 'label_gopeed_dir') { const input = modal.querySelector('#set_gopeed_dir'); return input ? (input.closest('[data-setting-key],.pk-setting-stack>div,.pk-setting-fieldset') || input) : null; } if (settingKey === 'label_abdm_url') { const input = modal.querySelector('#set_abdm_url'); return input ? (input.closest('[data-setting-key],.pk-setting-stack>div,.pk-setting-fieldset') || input) : null; } if (settingKey === 'label_abdm_dir') { const input = modal.querySelector('#set_abdm_dir'); return input ? (input.closest('[data-setting-key],.pk-setting-stack>div,.pk-setting-fieldset') || input) : null; } if (settingKey === 'label_idm_exe_path') { const input = modal.querySelector('#set_idm_exe_path'); return input ? (input.closest('[data-setting-key],.pk-setting-stack>div,.pk-setting-fieldset') || input) : null; } if (settingKey === 'label_idm_bat_root') { const input = modal.querySelector('#set_idm_bat_root'); return input ? (input.closest('[data-setting-key],.pk-setting-stack>div,.pk-setting-fieldset') || input) : null; } return null; } function focusSettingsRow(settingKey) { const modal = getSettingsModalOverlay(); const row = getSettingsRowElement(modal, settingKey); if (!row) return; const applyFocus = () => { row.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' }); row.classList.remove('pk-setting-focus-flash'); void row.offsetWidth; row.classList.add('pk-setting-focus-flash'); setTimeout(() => row.classList.remove('pk-setting-focus-flash'), 1500); }; if (typeof requestAnimationFrame === 'function') requestAnimationFrame(applyFocus); else setTimeout(applyFocus, 0); } async function openSettingsAndFocusDownloaderType() { await openSettingsModal(); focusSettingsRow('label_downloader_type'); } async function openSettingsAndFocusDownloaderAddress(type) { await openSettingsModal(); const t = normalizeDownloaderType(type); focusSettingsRow(t === 'gopeed' ? 'label_gopeed_url' : (t === 'abdm' ? 'label_abdm_url' : 'label_aria2_url')); } async function openSettingsAndFocusIdmExePath() { await openSettingsModal(); focusSettingsRow('label_idm_exe_path'); } async function openSettingsAndFocusDownloaderDownloadDir(type) { await openSettingsModal(); const t = normalizeDownloaderType(type); const target = t === 'gopeed' ? { key: 'label_gopeed_dir', selector: '#set_gopeed_dir' } : (t === 'abdm' ? { key: 'label_abdm_dir', selector: '#set_abdm_dir' } : { key: 'label_idm_bat_root', selector: '#set_idm_bat_root' }); focusSettingsRow(target.key); setTimeout(() => { const modal = getSettingsModalOverlay(); const input = modal && modal.querySelector(target.selector); if (!input) return; input.removeAttribute('readonly'); try { input.focus({ preventScroll: true }); } catch (e) { input.focus(); } if (typeof input.select === 'function') input.select(); }, 80); } function getDownloaderKeepStructureMissingDirInfo(type) { if (!getDownloaderKeepStructurePref()) return null; const t = normalizeDownloaderType(type); if (t === 'gopeed') { const dir = normalizeGopeedDownloadDir(gmGet('pk_gopeed_dir', CONF.gopeedDownloadDir)); return dir ? null : { type: t, settingKey: 'label_gopeed_dir' }; } if (t === 'abdm') { const dir = normalizeAbdmDownloadDir(gmGet('pk_abdm_dir', CONF.abdmDownloadDir)); return dir ? null : { type: t, settingKey: 'label_abdm_dir' }; } if (t === 'idm') { const mode = normalizeIdmExportMode(gmGet('pk_idm_export_mode', CONF.idmExportMode)); const dir = normalizeIdmBatDownloadRoot(gmGet('pk_idm_bat_root', CONF.idmBatDownloadRoot)); return mode === 'bat' && !dir ? { type: t, settingKey: 'label_idm_bat_root' } : null; } return null; } async function confirmDownloaderKeepStructureWithoutDir(type) { const info = getDownloaderKeepStructureMissingDirInfo(type); if (!info) return true; const ok = await showConfirm(L.msg_downloader_keep_structure_dir_confirm, L.title_confirm); if (!ok) await openSettingsAndFocusDownloaderDownloadDir(info.type); return ok; } function getDefaultRpcUrlForDownloader(type) { const t = normalizeDownloaderType(type); if (t === 'gopeed') return 'http://127.0.0.1:9999'; if (t === 'abdm') return 'http://localhost:15151'; return 'http://localhost:6800/jsonrpc'; } function formatDownloaderText(template, typeOrName) { const name = ['aria2', 'gopeed', 'abdm', 'idm'].includes(String(typeOrName || '').toLowerCase()) ? getDownloaderName(typeOrName) : String(typeOrName || getDownloaderName()); return String(template || '').replace(/\{n\}/g, name).replace(/\{d\}/g, name); } function formatDownloaderCountText(template, typeOrName, counts = {}) { const name = ['aria2', 'gopeed', 'abdm', 'idm'].includes(String(typeOrName || '').toLowerCase()) ? getDownloaderName(typeOrName) : String(typeOrName || getDownloaderName()); return String(template || '') .replace(/\{d\}/g, name) .replace(/\{n\}/g, String(counts.n ?? '')) .replace(/\{s\}/g, String(counts.s ?? '')) .replace(/\{f\}/g, String(counts.f ?? '')); } function buildAriaRpcParams(token, params = []) { const t = (token || '').trim(); return t ? [`token:${t}`, ...params] : params; } function getBoolPref(key, def) { const v = gmGet(key, def); if (v === undefined || v === null) return !!def; if (v === true || v === false) return v; if (typeof v === 'string') { const s = v.trim().toLowerCase(); if (s === 'true') return true; if (s === 'false') return false; } return !!v; } function getDownloaderKeepStructurePref() { if (gmHas('pk_aria2_keep_structure')) return getBoolPref('pk_aria2_keep_structure', CONF.aria2KeepFolderStructure); if (gmHas('pk_gopeed_keep_structure')) return getBoolPref('pk_gopeed_keep_structure', CONF.gopeedKeepFolderStructure); if (gmHas('pk_idm_bat_keep_structure')) return getBoolPref('pk_idm_bat_keep_structure', CONF.idmBatKeepFolderStructure); return !!CONF.aria2KeepFolderStructure; } function isValidDownloadAccelHost(hostname) { const host = String(hostname || '').trim().toLowerCase(); if (!host) return false; if (host === 'localhost') return true; if (host.startsWith('xn--') || host.includes('.xn--')) return false; if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return host.split('.').every(n => Number(n) >= 0 && Number(n) <= 255); return /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(host); } function normalizeDownloadAccelBase(input) { const raw = String(input || '').trim(); if (!raw) return ''; try { const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`; const u = new URL(candidate); if (!/^https?:$/i.test(u.protocol) || !u.hostname || u.origin === 'null') return ''; if (!isValidDownloadAccelHost(u.hostname)) return ''; const path = (u.pathname || '').replace(/\/+$/, ''); return `${u.origin}${path === '/' ? '' : path}${u.search}`; } catch (e) { return ''; } } function normalizeDownloadAccelMode(input) { return String(input || '').trim().toLowerCase() === 'query' ? 'query' : 'prefix'; } function normalizeDownloadAccelQueryParam(input) { const param = String(input || '').trim(); return /^[A-Za-z0-9_.~-]+$/.test(param) ? param : 'url'; } function getDownloadAccelModePref() { return normalizeDownloadAccelMode(gmGet('pk_download_accel_mode', CONF.downloadAccelMode)); } function getDownloadAccelQueryParamPref() { return normalizeDownloadAccelQueryParam(gmGet('pk_download_accel_query_param', CONF.downloadAccelQueryParam)); } function rewriteDownloadLinkForAccel(rawUrl) { try { if (!rawUrl) return rawUrl; if (!getBoolPref('pk_download_accel_enable', CONF.downloadAccelEnable)) return rawUrl; const accelBase = normalizeDownloadAccelBase(gmGet('pk_download_accel_domain', CONF.downloadAccelDomain)); if (!accelBase) return rawUrl; const raw = String(rawUrl).trim(); const src = new URL(raw); if (!/^https?:$/i.test(src.protocol) || !src.hostname) return rawUrl; const mode = getDownloadAccelModePref(); if (mode === 'query') { const sep = accelBase.includes('?') ? (/[?&]$/.test(accelBase) ? '' : '&') : '?'; return `${accelBase}${sep}${encodeURIComponent(getDownloadAccelQueryParamPref())}=${encodeURIComponent(raw)}`; } return `${accelBase}/${raw}`; } catch (e) { return rawUrl; } } function isZeroByteFile(file) { if (!file) return false; const rawSize = file.size ?? file.file_size ?? file.bytes; if (rawSize === 0 || rawSize === '0') return true; const n = Number(rawSize); return Number.isFinite(n) && n === 0; } function getDownloadFilterRules() { 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 fSizeMinStr = gmGet('pk_dl_filter_size_min', ''); const fSizeMaxStr = gmGet('pk_dl_filter_size_max', ''); const fSizeUnitStr = gmGet('pk_dl_filter_size_unit', 'MB'); let fSizeMin = fSizeMinStr ? parseFloat(fSizeMinStr) : -1; let fSizeMax = fSizeMaxStr ? parseFloat(fSizeMaxStr) : -1; if (fSizeUnitStr === 'GB') { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024 * 1024; } else if (fSizeUnitStr === 'TB') { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024 * 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024 * 1024 * 1024; } else { if (fSizeMin >= 0) fSizeMin *= 1024 * 1024; if (fSizeMax >= 0) fSizeMax *= 1024 * 1024; } return { fExts, fNames, fSizeMin, fSizeMax, hasRules: fExts.length > 0 || fNames.length > 0 || (Number.isFinite(fSizeMin) && fSizeMin >= 0) || (Number.isFinite(fSizeMax) && fSizeMax >= 0) }; } function isDownloadFilterBlocked(item, rules = getDownloadFilterRules()) { if (!item) return false; if (item.kind === 'drive#folder') return false; const lowName = String(item.name || '').toLowerCase(); const ext = lowName.split('.').pop(); if (rules.fExts.some(e => ext === e)) return true; if (rules.fNames.some(n => lowName.includes(n))) return true; if (rules.fSizeMin >= 0 || rules.fSizeMax >= 0) { const sz = parseInt(item.size || 0, 10); if (Number.isFinite(sz)) { if (rules.fSizeMin >= 0 && sz < rules.fSizeMin) return true; if (rules.fSizeMax >= 0 && sz > rules.fSizeMax) return true; } } return false; } function downloadEmptyFile(file) { try { const blob = new Blob([''], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = (file && file.name) || 'empty'; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => { if (a.parentNode) a.remove(); URL.revokeObjectURL(url); }, 1000); return true; } catch (e) { return false; } } function isAriaWsRpcUrl(url) { return /^wss?:\/\//i.test(url || ''); } function getAriaRpcError(data) { const list = Array.isArray(data) ? data : [data]; const hit = list.find(x => x && x.error); if (!hit) return null; const err = hit.error || {}; return new Error(err.message || `RPC ${err.code || 'Error'}`); } function aria2RpcHttpRequest(url, payload, timeout = 5000) { return new Promise((resolveReq, rejectReq) => { GM_xmlhttpRequest({ method: 'POST', url, data: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, timeout, onload: (r) => { if (r.status !== 200) { rejectReq(new Error('HTTP ' + r.status)); return; } try { const data = r.responseText ? JSON.parse(r.responseText) : null; const rpcErr = getAriaRpcError(data); if (rpcErr) rejectReq(rpcErr); else resolveReq(data); } catch (e) { rejectReq(new Error('Invalid JSON')); } }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); } function aria2RpcWsRequest(url, payload, timeout = 5000) { return new Promise((resolveReq, rejectReq) => { let ws = null; let settled = false; const requests = Array.isArray(payload) ? payload : [payload]; const pending = new Set(requests.map(req => String(req.id))); const results = []; const finish = (ok, value) => { if (settled) return; settled = true; clearTimeout(timer); try { if (ws && ws.readyState <= 1) ws.close(); } catch (e) {} ok ? resolveReq(value) : rejectReq(value); }; const timer = setTimeout(() => finish(false, new Error('Timeout')), timeout); try { ws = new WebSocket(url); } catch (e) { finish(false, e); return; } ws.onopen = () => { try { requests.forEach(req => ws.send(JSON.stringify(req))); } catch (e) { finish(false, e); } }; ws.onmessage = (ev) => { try { const data = JSON.parse(ev.data); const list = Array.isArray(data) ? data : [data]; const rpcErr = getAriaRpcError(list); if (rpcErr) { finish(false, rpcErr); return; } list.forEach(item => { if (!item || item.id === undefined || item.id === null) return; const id = String(item.id); if (!pending.has(id)) return; pending.delete(id); results.push(item); }); if (pending.size === 0) { finish(true, Array.isArray(payload) ? results : results[0]); } } catch (e) { finish(false, new Error('Invalid JSON')); } }; ws.onerror = () => finish(false, new Error('Network Error')); ws.onclose = () => { if (!settled) finish(false, new Error('Network Error')); }; }); } function aria2RpcRequest(url, payload, timeout = 5000) { const rpcUrl = normalizeAriaRpcUrl(url); return isAriaWsRpcUrl(rpcUrl) ? aria2RpcWsRequest(rpcUrl, payload, timeout) : aria2RpcHttpRequest(rpcUrl, payload, timeout); } function normalizeGopeedApiUrl(url) { let u = String(url || '').trim(); if (!u) u = CONF.gopeedApiUrl; if (!/^https?:\/\//i.test(u)) u = 'http://' + u; try { const parsed = new URL(u); if (!/^https?:$/i.test(parsed.protocol) || !parsed.hostname) return CONF.gopeedApiUrl; parsed.pathname = parsed.pathname.replace(/\/+$/, ''); parsed.search = ''; parsed.hash = ''; return parsed.toString().replace(/\/$/, ''); } catch (e) { return CONF.gopeedApiUrl; } } function normalizeGopeedDownloadDir(dir) { return String(dir || '').trim().replace(/^["']|["']$/g, ''); } function normalizeAbdmApiUrl(url) { let u = String(url || '').trim(); if (!u) u = CONF.abdmApiUrl; try { const parsed = new URL(u); if (!/^https?:$/i.test(parsed.protocol) || !parsed.hostname) return CONF.abdmApiUrl; parsed.pathname = parsed.pathname.replace(/\/+$/, ''); parsed.search = ''; parsed.hash = ''; return parsed.toString().replace(/\/$/, ''); } catch (e) { return CONF.abdmApiUrl; } } function normalizeAbdmDownloadDir(dir) { let s = String(dir || '').trim().replace(/^["']|["']$/g, ''); if (!s) return ''; s = s.replace(/\\/g, '/').replace(/\/+$/g, ''); return s; } function normalizeIdmExportMode(mode) { return String(mode || '').trim().toLowerCase() === 'bat' ? 'bat' : 'url'; } function normalizeIdmUrlExportType(type) { return String(type || '').trim().toLowerCase() === 'ef2' ? 'ef2' : 'txt'; } function normalizeIdmBatDownloadRoot(dir) { return String(dir || '').trim().replace(/^["']|["']$/g, '').replace(/[\r\n"]+/g, '_'); } function normalizeIdmExePath(path) { let s = String(path || '').trim().replace(/[\x00-\x1f\x7f]+/g, ''); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) s = s.slice(1, -1).trim(); return s.replace(/[\x00-\x1f\x7f]+/g, ''); } function gopeedApiRequest(apiUrl, token, method, path, body = null, timeout = CONF.gopeedRequestTimeout) { return new Promise((resolveReq, rejectReq) => { const url = `${normalizeGopeedApiUrl(apiUrl)}${String(path || '').startsWith('/') ? '' : '/'}${path || ''}`; const headers = { 'Content-Type': 'application/json' }; const t = String(token || '').trim(); if (t) headers['X-Api-Token'] = t; GM_xmlhttpRequest({ method, url, data: body ? JSON.stringify(body) : undefined, headers, timeout, onload: (r) => { if (r.status === 401) { const err = new Error('Unauthorized'); err.status = r.status; rejectReq(err); return; } if (r.status < 200 || r.status >= 300) { const err = new Error('HTTP ' + r.status); err.status = r.status; rejectReq(err); return; } try { const data = r.responseText ? JSON.parse(r.responseText) : null; if (!data || typeof data !== 'object' || typeof data.code === 'undefined') { rejectReq(new Error('Invalid Gopeed API')); return; } if (Number(data.code) !== 0) { const err = new Error(data.msg || `Gopeed API ${data.code}`); err.code = data.code; rejectReq(err); return; } resolveReq(data.data); } catch (e) { rejectReq(new Error('Invalid JSON')); } }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); } async function testGopeedConnection(apiUrl, token, timeout = 3000) { const info = await gopeedApiRequest(apiUrl, token, 'GET', '/api/v1/info', null, timeout); if (!info || typeof info !== 'object' || !Object.prototype.hasOwnProperty.call(info, 'version')) throw new Error('Invalid Gopeed API'); return info; } function abdmApiRequest(path, body, options = {}) { return new Promise((resolveReq, rejectReq) => { const apiUrl = normalizeAbdmApiUrl(options.apiUrl || gmGet('pk_abdm_url', CONF.abdmApiUrl)); const method = String(options.method || (body ? 'POST' : 'GET')).toUpperCase() === 'GET' ? 'GET' : 'POST'; const url = `${apiUrl}${String(path || '').startsWith('/') ? '' : '/'}${path || ''}`; GM_xmlhttpRequest({ method, url, data: method === 'GET' || body === null || body === undefined ? undefined : JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, timeout: Number(options.timeout || CONF.abdmRequestTimeout) || CONF.abdmRequestTimeout, onload: (r) => { if (r.status < 200 || r.status >= 300) { const err = new Error('HTTP ' + r.status); err.status = r.status; rejectReq(err); return; } resolveReq(r.responseText || ''); }, onerror: () => rejectReq(new Error('Network Error')), ontimeout: () => rejectReq(new Error('Timeout')) }); }); } async function testAbdmConnection(apiUrl, timeout = 3000) { try { return await abdmApiRequest('/queues', null, { apiUrl, method: 'GET', timeout }); } catch (e) { return await abdmApiRequest('/', null, { apiUrl, method: 'GET', timeout }); } } function classifyGopeedError(err, createPhase = false) { const msg = err && err.message ? err.message : String(err || ''); const status = err && err.status; const setupMsg = L.desc_gopeed_setup; if (status === 401 || /unauthori[sz]ed|token/i.test(msg)) return { fatal: true, message: setupMsg }; if (msg === 'Timeout') return { fatal: true, message: setupMsg }; if (msg === 'Network Error') return { fatal: true, message: setupMsg }; if (/Invalid JSON|Invalid Gopeed API/i.test(msg)) return { fatal: true, message: setupMsg }; if (/^HTTP\s/i.test(msg)) return { fatal: true, message: setupMsg }; return { fatal: false, message: msg }; } function classifyAbdmError(err, createPhase = false) { const msg = err && err.message ? err.message : String(err || ''); if (msg === 'Timeout') return { fatal: true, message: L.desc_abdm_setup }; if (msg === 'Network Error') return { fatal: true, message: L.desc_abdm_setup }; if (/Invalid API/i.test(msg)) return { fatal: true, message: L.desc_abdm_setup }; if (/^HTTP\s/i.test(msg)) return { fatal: true, message: L.desc_abdm_setup }; return { fatal: false, message: createPhase ? L.str_aria2_rpc_err : msg }; } function sanitizeDownloadFileName(name) { const s = String(name || '').replace(/[\\/:*?"<>|]/g, '_').trim(); return s || 'download'; } function buildGopeedDownloadPath(downloadDir, relativePath = '') { const base = normalizeGopeedDownloadDir(downloadDir); if (!base) return ''; const rawRelativePath = String(relativePath || '').trim(); if (!rawRelativePath) return base; const sep = base.includes('\\') ? '\\' : '/'; const segments = rawRelativePath .split(/[\\/]+/) .map(seg => String(seg || '').trim()) .filter(seg => seg && seg !== '.' && seg !== '..') .map(seg => sanitizeDownloadFileName(seg)) .filter(Boolean); if (!segments.length) return base; const trimmedBase = base.replace(/[\\/]+$/g, ''); const safeBase = trimmedBase || (sep === '\\' ? base : '/'); return `${safeBase}${safeBase.endsWith(sep) ? '' : sep}${segments.join(sep)}`; } function buildAbdmDownloadFolder(downloadDir, relativePath = '') { const base = normalizeAbdmDownloadDir(downloadDir); if (!base) return ''; const rawRelativePath = String(relativePath || '').trim(); if (!rawRelativePath) return base; const segments = rawRelativePath .split(/[\\/]+/) .map(seg => String(seg || '').trim()) .filter(seg => seg && seg !== '.' && seg !== '..') .map(seg => sanitizeDownloadFileName(seg)) .filter(Boolean); if (!segments.length) return base; return `${base}/${segments.join('/')}`; } function buildStandardDownloadTasks(files, options = {}) { const keepStructure = !!options.keepStructure; const downloadDir = String(options.downloadDir || ''); const headers = { 'User-Agent': navigator.userAgent, Referer: 'https://mypikpak.com/' }; return (files || []).map(file => { const originalUrl = file && file.web_content_link ? String(file.web_content_link) : ''; const url = rewriteDownloadLinkForAccel(originalUrl); const lineage = Array.isArray(file && file._lineage) ? file._lineage : []; const relativePath = keepStructure && lineage.length ? lineage.map(n => sanitizeDownloadFileName(n && n.name)).join('/') : ''; const fileName = sanitizeDownloadFileName(file && file.name); return { url, fileName, relativePath, downloadDir, headers, file, accelerated: !!(url && originalUrl && url !== originalUrl) }; }).filter(task => /^https?:\/\//i.test(task.url)); } function sanitizeIdmPathSegment(name) { const s = String(name || '').replace(/[\x00-\x1f\\/:*?"<>|]/g, '_').trim(); return s || 'download'; } function buildIdmBatDownloadPath(downloadRoot, relativePath = '') { const base = normalizeIdmBatDownloadRoot(downloadRoot); if (!base) return ''; const rawRelativePath = String(relativePath || '').trim(); if (!rawRelativePath) return base.replace(/[\\/]+$/g, '') || base; const sep = base.includes('\\') ? '\\' : '/'; const segments = rawRelativePath .split(/[\\/]+/) .map(seg => String(seg || '').trim()) .filter(seg => seg && seg !== '.' && seg !== '..') .map(seg => sanitizeIdmPathSegment(seg)) .filter(Boolean); const trimmedBase = base.replace(/[\\/]+$/g, ''); if (!segments.length) return trimmedBase || base; return `${trimmedBase || base}${(trimmedBase || base).endsWith(sep) ? '' : sep}${segments.join(sep)}`; } function escapeBatArg(value) { return String(value ?? '') .replace(/[\r\n]+/g, ' ') .replace(/\0/g, '') .replace(/\^/g, '^^') .replace(/%/g, '%%') .replace(/"/g, '""') .replace(/([&|<>])/g, '^$1'); } function escapeIdmBatUrl(value) { return String(value ?? '') .trim() .replace(/[\r\n]+/g, '') .replace(/\0/g, '') .replace(/"/g, '%22') .replace(/%/g, '%%'); } function normalizeIdmExportUrl(url) { return String(url || '').trim().replace(/[\r\n]+/g, '').replace(/"/g, '%22'); } function normalizeIdmEf2Line(value) { return String(value || '').trim().replace(/[\r\n\0]+/g, ' ').replace(//g, '%3E'); } function buildIdmEf2Export(tasks) { const lines = []; (tasks || []).forEach(task => { const url = normalizeIdmEf2Line(normalizeIdmExportUrl(task && task.url)); if (!url) return; const headers = task && task.headers ? task.headers : {}; const referer = normalizeIdmEf2Line(headers.Referer || headers.referer || ''); const userAgent = normalizeIdmEf2Line(headers['User-Agent'] || headers['user-agent'] || ''); lines.push('<', url); if (referer) lines.push(`referer: ${referer}`); if (userAgent) lines.push(`User-Agent: ${userAgent}`); lines.push('>'); }); return lines.join('\r\n') + (lines.length ? '\r\n' : ''); } function formatExportTimestamp() { const pad = n => String(n).padStart(2, '0'); const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`; } function downloadTextExport(text, fileName, mime = 'text/plain;charset=utf-8', withBom = false) { const blob = new Blob([withBom ? '\uFEFF' : '', text], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); if (a.parentNode) a.parentNode.removeChild(a); }, 1000); } function buildIdmBatScript(tasks, options = {}) { const root = normalizeIdmBatDownloadRoot(options.downloadRoot); const idmExePath = normalizeIdmExePath(options.idmExePath); const keepFolderStructure = !!options.keepFolderStructure; const missingMsg = escapeBatArg(L.msg_idm_exe_missing); const lines = [ '@echo off', 'setlocal DisableDelayedExpansion', 'chcp 65001 >nul', '', `set "IDM=${escapeBatArg(idmExePath)}"`, 'if not defined IDM set "IDM=%ProgramFiles(x86)%\\Internet Download Manager\\IDMan.exe"', 'if not exist "%IDM%" set "IDM=%ProgramFiles%\\Internet Download Manager\\IDMan.exe"', 'if not exist "%IDM%" set "IDM=IDMan.exe"', 'if not exist "%IDM%" where IDMan.exe >nul 2>nul', `if not exist "%IDM%" if errorlevel 1 echo ${missingMsg}`, 'if not exist "%IDM%" if errorlevel 1 pause', 'if not exist "%IDM%" if errorlevel 1 exit /b 1', '' ]; (tasks || []).forEach(task => { const cmd = ['"%IDM%"', '/d', `"${escapeIdmBatUrl(normalizeIdmExportUrl(task.url))}"`]; const savePath = buildIdmBatDownloadPath(root, keepFolderStructure ? task.relativePath : ''); if (savePath) { lines.push(`if not exist "${escapeBatArg(savePath)}" mkdir "${escapeBatArg(savePath)}"`); cmd.push('/p', `"${escapeBatArg(savePath)}"`); } cmd.push('/f', `"${escapeBatArg(sanitizeIdmPathSegment(task.fileName))}"`); cmd.push('/n'); lines.push(cmd.join(' ')); }); return lines.join('\r\n') + '\r\n'; } async function submitGopeedTasks(tasks, options = {}) { const apiUrl = normalizeGopeedApiUrl(options.apiUrl); const token = options.token || ''; const batchSize = Math.max(1, Number(options.batchSize || CONF.gopeedBatchSize) || CONF.gopeedBatchSize); const failedFiles = []; let successCount = 0; let fatalError = null; for (let i = 0; i < tasks.length; i += batchSize) { if (fatalError || (options.isRunning && !options.isRunning())) break; const chunk = tasks.slice(i, i + batchSize); for (let offset = 0; offset < chunk.length; offset++) { const task = chunk[offset]; const taskIndex = i + offset; if (fatalError || (options.isRunning && !options.isRunning())) break; const payload = { req: { url: task.url, extra: { method: 'GET', header: task.headers, body: '' } }, opts: { name: task.fileName, path: buildGopeedDownloadPath(task.downloadDir, task.relativePath) } }; try { await gopeedApiRequest(apiUrl, token, 'POST', '/api/v1/tasks', payload, CONF.gopeedRequestTimeout); successCount++; } catch (err) { const info = classifyGopeedError(err, true); failedFiles.push(`${task.fileName} ${L.str_aria2_rpc_err}`); if (info.fatal) { fatalError = info.message; const isConnFatal = err && (err.message === 'Network Error' || err.message === 'Timeout'); const remaining = tasks.slice(taskIndex + 1); remaining.forEach(t => failedFiles.push(`${t.fileName} ${isConnFatal ? L.str_aria2_aborted : L.str_aria2_rpc_err}`)); } } if (typeof options.onProgress === 'function') options.onProgress(successCount, tasks.length); } if (!fatalError && i + batchSize < tasks.length) await sleep(250); } return { successCount, failedFiles, fatalError }; } async function submitAbdmTasks(tasks, options = {}) { const apiUrl = normalizeAbdmApiUrl(options.apiUrl); const failedFiles = []; let successCount = 0; let fatalError = null; for (let i = 0; i < tasks.length; i++) { if (fatalError) break; if (options.isRunning && !options.isRunning()) { tasks.slice(i).forEach(t => failedFiles.push(`${t.fileName} ${L.str_aria2_aborted}`)); break; } const task = tasks[i]; const folder = buildAbdmDownloadFolder(task.downloadDir, task.relativePath); const payload = { downloadSource: { link: task.url, headers: {} }, name: task.fileName }; if (folder) payload.folder = folder; try { await abdmApiRequest('/start-headless-download', payload, { apiUrl, method: 'POST', timeout: CONF.abdmRequestTimeout }); successCount++; } catch (err) { const info = classifyAbdmError(err, true); failedFiles.push(`${task.fileName} ${L.str_aria2_rpc_err}`); if (info.fatal) { fatalError = info.message; tasks.slice(i + 1).forEach(t => failedFiles.push(`${t.fileName} ${L.str_aria2_aborted}`)); } } if (typeof options.onProgress === 'function') options.onProgress(successCount, tasks.length); } return { successCount, failedFiles, fatalError }; } if (document.querySelector('.pk-ov')) return; document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; const S = { path: (globalSavedState && globalSavedState.path) ? [...globalSavedState.path] : [{ id: '', name: L.btn_nav_home }], items: [], itemMap: new Map(), display: [], sel: new Set(), selMode: 'explicit', selEx: new Set(), cache: initialCache || new Map(), sort: (globalSavedState && globalSavedState.sort) ? globalSavedState.sort : 'modified_time', dir: (globalSavedState && globalSavedState.dir !== undefined) ? globalSavedState.dir : 1, scanning: false, dupMode: false, dupRunning: false, dupResultSig: '', virtualSortMaskSig: '', resumeQuietUntil: 0, folderFirst: false, dupReasons: new Map(), dupGroups: new Map(), dupRawGroups: [], dupGridMeta: null, dupGridMetaKey: '', dupGridMetaLayoutKey: '', dupGridMetaDisplayRef: null, dupGridMetaDirty: true, dupGridScrollRaf: 0, dupGridScrollEndTimer: 0, dupGridScrollDirty: false, dupGridLastScrollTop: 0, dupGridLastRenderedTop: -1, offlineFilters: { running: true, failed: true, complete: true }, uploadFilters: { running: true, paused: true, complete: true }, activeId: null, clipItems: [], clipType: '', clipSourceParentId: null, loading: false, firstPageReady: false, pagingLoading: false, liveRefreshCtx: null, offlinePagingPending: false, offlineLightProbeTimer: 0, offlineLightProbeTimerReason: '', offlineLightProbeTimerDueAt: 0, offlineLightProbeRunning: false, offlineLightProbeAbortController: null, offlineLightProbeReqId: 0, offlineLightProbePendingReason: '', offlineLightProbeLastAt: 0, offlineLightProbeFailCount: 0, offlineLightProbePendingWrite: false, offlineDeleteTombstones: new Map(), configCloudBusy: false, localCleanBusy: false, magnetArchiveBusy: false, lastSelIdx: -1, dupConfig: { video: true, image: true, other: true }, search: '', viewMode: (globalSavedState && globalSavedState.viewMode) ? globalSavedState.viewMode : (() => { const readSavedFolderView = (key, fallback = 'grid') => { try { const prefs = JSON.parse(gmGet('pk_folder_view_prefs', '{}')); const saved = prefs[key]; const mode = typeof saved === 'string' ? saved : (saved && saved.viewMode); return mode === 'list' ? 'list' : (mode === 'grid' ? 'grid' : fallback); } catch(e) { return fallback; } }; if (globalSavedState && globalSavedState.trashMode) { return readSavedFolderView('trash_root', 'list'); } if (gmGet('pk_view_independent', false)) { return readSavedFolderView('root', 'grid'); } return gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'; })(), sortId: 0, isFlattened: false, filterState: { active: false, cat: 'all', ext: 'all' }, suppressClearConfirm: false, preSearchPath: null, lastGlobalResults: [], lastSearchGlobal: false, folderLineageMap: globalLineageMap, strictFolderFirstSnapshot: null, trashMode: (globalSavedState && globalSavedState.trashMode) || false, shareMode: (globalSavedState && globalSavedState.shareMode) || false, shareParseMode: (globalSavedState && globalSavedState.shareParseMode) || false, linkBookmarkMode: (globalSavedState && globalSavedState.linkBookmarkMode) || false, linkBookmarkFolders: null, linkBookmarkSelectedFolderIndex: 0, linkBookmarkSearch: '', linkBookmarkSearchDraft: '', linkBookmarkFolderPage: 1, linkBookmarkLinkPage: 1, linkBookmarkFolderPageSize: 3, linkBookmarkLinkPageSize: 1, linkBookmarkLinkPageFitKey: '', linkBookmarkLinkPageFitCap: 0, linkBookmarkIconRuntimeCache: new Map(), linkBookmarkSyncStatus: 'idle', linkBookmarkError: '', linkBookmarkRemoteHash: '', linkBookmarkAuthSig: '', linkBookmarkHasLoaded: false, linkBookmarkFetchReqId: 0, linkBookmarkSaveReqId: 0, shareParseInfo: null, shareParseDraft: { raw: '', pass_code: '' }, shareParseLoading: false, shareParseError: '', shareParseReqId: 0, shareParseListActive: false, shareParseItems: [], shareParseDisplay: [], shareParseItemMap: new Map(), shareParseFolderCache: new Map(), shareParsePath: [], shareParseCurParentId: '', shareParseNextPageToken: '', shareParseAutoLoadToken: 0, shareParseAutoLoadRunning: false, shareParseAutoLoadTarget: '', shareParseNavSessionKey: '', shareParseNavBackStack: [], shareParseNavForwardStack: [], shareParseNavBusy: false, shareParseNavSuspendRecord: false, shareParseLatestChildId: null, shareParseListLoading: false, shareParseListError: '', shareParseListReqId: 0, shareParseSaving: false, shareParseSaveReqId: 0, shareParseSaveError: '', shareParseSaveResult: null, shareParseInsightMode: false, shareParseInsightItems: [], shareParseInsightDisplay: [], shareParseInsightItemMap: new Map(), shareParseInsightFilterState: { active: false, cat: 'all', ext: 'all' }, shareParseInsightScanning: false, shareParseInsightStop: false, shareParseInsightProgress: { folder: 0, file: 0 }, shareParseInsightError: '', shareParseInsightReqId: 0, shareParseInsightReturnPath: [], shareParseInsightReturnParentId: '', shareParseInsightToastStopped: false, starredMode: (globalSavedState && globalSavedState.starredMode) || false, recentMode: (globalSavedState && globalSavedState.recentMode) || false, recentRefreshFirstPageOnly: false, recentLoadNextPageOnly: false, recentLoadingNextPage: false, recentLightProbeRunning: false, recentLightProbeReqId: 0, recentLightProbeTimer: 0, recentLightProbeLastAt: 0, recentRootPagingActive: false, recentRootPagingLoadId: 0, recentRootPagingStartedAt: 0, historyMode: (globalSavedState && globalSavedState.historyMode) || false, historyItems: [], historyNextPageToken: '', historyTailNextPageToken: '', historyKnownIds: new Set(), historyReqId: 0, historyPageReqId: 0, historyLoading: false, historyLoadingMore: false, historyFirstPageLoading: false, historySilentRevalidating: false, historyDirty: false, historyScrollTop: 0, historyDeletedEventIds: new Set(), historyDeletedFileIds: new Set(), historyPagingCursorStale: false, historyLoadedPageCount: 0, historyLoadedAt: 0, historyLastPageErrorAt: 0, historyAbortController: null, historyPageAbortController: null, historyRefreshFirstPageOnly: false, offlineMode: (globalSavedState && globalSavedState.offlineMode) || false, uploadMode: (globalSavedState && globalSavedState.uploadMode) || false, navBuckets: { home: { backStack: [], current: null, forwardStack: [] }, starred: { backStack: [], current: null, forwardStack: [] }, recent: { backStack: [], current: null, forwardStack: [] } }, navScrollStore: { home: Object.create(null), starred: Object.create(null), recent: Object.create(null) }, navActiveBucket: null, navFrozenBucket: null, navContext: 'none', navSyncSig: '', navMaxHistory: CONF.mouseSideNavHistoryMax, navSuspendRecord: false, navTransitionBusy: false, browserNavInitDone: false, browserNavRestoring: false, browserNavExitGuarding: false, browserNavSeq: 0, browserNavSig: '', browserOverlayObserver: null, browserOverlaySyncTimer: 0, navLastModalType: '', navVideoBackArmed: false, scanId: 0, preloaded: (globalSavedState || (initialCache && initialCache.has('root'))) ? true : false, preLoadPromise: preloadPromise || null, blSet: new Set(), blFolderSet: new Set(), starredSet: new Set(), pendingMap: new Map(), durationMap: new Map(), loadedThumbs: new Set(), movingIds: new Set(), movingSourceId: null, movingDestId: null, uploadTasks: (globalSavedState && globalSavedState.uploadTasks) ? globalSavedState.uploadTasks : [], broadcast: new BroadcastChannel('pk_act_sync'), getNavBucketKey: () => { if (S.starredMode) return 'starred'; if (S.recentMode) return 'recent'; if (S.trashMode || S.shareMode || S.shareParseMode || S.linkBookmarkMode || S.historyMode || S.offlineMode || S.uploadMode) return null; if (S.isFlattened || S.dupMode || S.analyzeMode) return null; const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; if (cur && typeof cur.id === 'string' && (cur.id.startsWith('virtual_') || cur.id === 'analyze_root')) return null; return 'home'; }, getActiveNavBucket: () => { const key = S.getNavBucketKey(); return key ? S.navBuckets[key] : null; }, getShareParseNavSessionKey: () => { const info = S.shareParseInfo || {}; return info.share_id && info.pass_code_token ? `${info.share_id}|${info.pass_code_token}` : ''; }, resetShareParseSideNav: () => { S.shareParseNavSessionKey = ''; S.shareParseNavBackStack = []; S.shareParseNavForwardStack = []; S.shareParseNavBusy = false; S.shareParseNavSuspendRecord = false; }, ensureShareParseNavSession: () => { const key = S.getShareParseNavSessionKey(); if (!key) { S.resetShareParseSideNav(); return ''; } if (S.shareParseNavSessionKey !== key) { S.shareParseNavSessionKey = key; S.shareParseNavBackStack = []; S.shareParseNavForwardStack = []; S.shareParseNavBusy = false; S.shareParseNavSuspendRecord = false; } return key; }, cloneShareParsePath: (path = S.shareParsePath) => (Array.isArray(path) ? path : []).map(n => ({ id: n && n.id || '', name: n && n.name || '', parent_id: n && n.parent_id || '', _sharePanel: !!(n && n._sharePanel), _shareRoot: !!(n && n._shareRoot) })), isSameShareParsePath: (a, b) => { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if ((a[i] && a[i].id || '') !== (b[i] && b[i].id || '')) return false; } return true; }, getShareParsePathSig: (path = S.shareParsePath) => { const arr = Array.isArray(path) ? path : []; return arr.map(n => n && n.id || '').join('>'); }, trimShareParseNavStack: (stack) => { if (!Array.isArray(stack) || stack.length <= S.navMaxHistory) return; stack.splice(0, stack.length - S.navMaxHistory); }, makeShareParseNavSnapshot: () => { const sessionKey = S.ensureShareParseNavSession(); if (!sessionKey || !S.shareParseMode || !S.shareParseListActive || S.shareParseInsightMode) return null; const path = S.cloneShareParsePath(S.shareParsePath); if (path.length < 2) return null; return { sessionKey, path, parentId: S.shareParseCurParentId || getShareParseRootParentId(), scrollTop: UI.vp ? Math.max(0, Math.round(UI.vp.scrollTop || 0)) : 0 }; }, pushShareParseNavSnapshot: (snap = null, clearForward = true) => { const curSnap = snap || S.makeShareParseNavSnapshot(); if (!curSnap || !curSnap.sessionKey) return false; const stack = S.shareParseNavBackStack || (S.shareParseNavBackStack = []); const last = stack[stack.length - 1]; if (!last || !S.isSameShareParsePath(last.path, curSnap.path)) { stack.push(curSnap); S.trimShareParseNavStack(stack); } if (clearForward) S.shareParseNavForwardStack = []; return true; }, loadShareParseNavSnapshot: async (snap) => { if (!snap || !Array.isArray(snap.path) || snap.path.length < 2) return false; if (snap.sessionKey !== S.getShareParseNavSessionKey()) return false; const target = snap.path[snap.path.length - 1]; S.shareParseNavBusy = true; S.shareParseNavSuspendRecord = true; try { await loadShareParseFolder({ id: snap.parentId || (target && target.id) || getShareParseRootParentId(), name: target && target.name || '', parent_id: target && target.parent_id || '', _sharePath: S.cloneShareParsePath(snap.path) }, { append: false, suppressShareParseNav: true, shareParseRestoreScrollTop: snap.scrollTop }); return true; } finally { S.shareParseNavSuspendRecord = false; S.shareParseNavBusy = false; } }, shareParseSideBack: async () => { if (S.shareParseMode && S.shareParseInsightMode) { handleShareParseInsightBack(); return true; } if (!S.shareParseMode || !S.shareParseListActive || S.shareParseNavBusy) return false; if (!S.ensureShareParseNavSession()) return false; const nav = S.getBrowserNavPayload(); const back = S.shareParseNavBackStack || []; if (!S.shareParseInsightMode && nav && nav.type === 'share_parse' && back.length) { history.back(); return true; } const curPath = S.cloneShareParsePath(S.shareParsePath); if (curPath.length <= 2) return false; if (!back.length) return false; let target = back.pop(); while (target && (!Array.isArray(target.path) || target.path.length < 2 || target.sessionKey !== S.getShareParseNavSessionKey())) { target = back.pop(); } if (!target) return false; const curSnap = S.makeShareParseNavSnapshot(); const ok = await S.loadShareParseNavSnapshot(target); if (!ok) { back.push(target); return false; } if (curSnap) { S.shareParseNavForwardStack.push(curSnap); S.trimShareParseNavStack(S.shareParseNavForwardStack); } return true; }, shareParseSideForward: async (direct = false) => { if (!S.shareParseMode || !S.shareParseListActive || S.shareParseInsightMode || S.shareParseNavBusy) return false; if (!S.ensureShareParseNavSession()) return false; const nav = S.getBrowserNavPayload(); const forward = S.shareParseNavForwardStack || []; if (!direct && nav && nav.type === 'share_parse' && forward.length) { history.forward(); return true; } if (!forward.length) return false; let target = forward.pop(); while (target && (!Array.isArray(target.path) || target.path.length < 2 || target.sessionKey !== S.getShareParseNavSessionKey())) { target = forward.pop(); } if (!target) return false; const curSnap = S.makeShareParseNavSnapshot(); const ok = await S.loadShareParseNavSnapshot(target); if (!ok) { forward.push(target); return false; } if (curSnap) { S.shareParseNavBackStack.push(curSnap); S.trimShareParseNavStack(S.shareParseNavBackStack); } return true; }, cloneNavPath: (path = S.path) => (Array.isArray(path) ? path : []).map(n => ({ id: n.id, name: n.name })), getNavPathSig: (path = S.path) => { const arr = Array.isArray(path) ? path : []; return arr.map(n => n && n.id || '').join('>'); }, saveNavScrollTop: (bucketKey = null, path = S.path, scrollTop = null) => { const key = bucketKey || S.getNavBucketKey() || S.navActiveBucket || null; if (!key || !S.navScrollStore[key] || !UI.vp) return; const sig = S.getNavPathSig(path); if (!sig) return; S.navScrollStore[key][sig] = Math.max(0, Math.round(scrollTop == null ? UI.vp.scrollTop : scrollTop)); }, getNavScrollTop: (bucketKey = null, path = S.path) => { const key = bucketKey || S.getNavBucketKey() || S.navActiveBucket || null; if (!key || !S.navScrollStore[key]) return null; const sig = S.getNavPathSig(path); if (!sig) return null; const v = S.navScrollStore[key][sig]; return Number.isFinite(v) ? v : null; }, restoreNavScrollTop: (bucketKey = null, path = S.path) => { if (!gmGet('pk_keep_pos', true) || !UI.vp) return; const savedTop = S.getNavScrollTop(bucketKey, path); if (!Number.isFinite(savedTop)) return; requestAnimationFrame(() => { if (!UI.vp) return; const maxTop = Math.max(0, UI.vp.scrollHeight - UI.vp.clientHeight); UI.vp.scrollTop = Math.max(0, Math.min(savedTop, maxTop)); if (typeof renderVisible === 'function') renderVisible(); }); }, isSameNavPath: (a, b) => { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if ((a[i] && a[i].id) !== (b[i] && b[i].id)) return false; } return true; }, trimNavStack: (stack) => { if (!Array.isArray(stack) || stack.length <= S.navMaxHistory) return; stack.splice(0, stack.length - S.navMaxHistory); }, getNavContext: () => { if (S.isStrictVirtualNavMode()) return 'virtual'; const key = S.getNavBucketKey(); return key || 'none'; }, recordNavSnapshot: (bucketKey, path = S.path) => { const bucket = bucketKey ? S.navBuckets[bucketKey] : null; if (!bucket) return false; const snap = S.cloneNavPath(path); if (!snap.length) return false; if (bucket.current && S.isSameNavPath(bucket.current, snap)) return false; if (bucket.current && bucket.current.length) { bucket.backStack.push(S.cloneNavPath(bucket.current)); S.trimNavStack(bucket.backStack); } bucket.current = snap; bucket.forwardStack = []; return true; }, getBrowserNavPayload: (state = history.state) => { const root = state && typeof state === 'object' ? state : null; const nav = root && root.__pkPemBrowserNav === 1 && root.pkNav && typeof root.pkNav === 'object' ? root.pkNav : null; return nav && nav.schema === 1 && (nav.type === 'drive' || nav.type === 'share_parse' || nav.type === 'overlay') ? nav : null; }, makeBrowserNavPayload: (bucketKey, path = S.path, seq = S.browserNavSeq) => { if (!bucketKey || !S.navBuckets[bucketKey]) return null; const snap = S.cloneNavPath(path); if (!snap.length) return null; const sig = `drive|${bucketKey}|${S.getNavPathSig(snap)}`; return { schema: 1, type: 'drive', seq, bucket: bucketKey, path: snap, sig, scrollTop: UI.vp ? Math.max(0, Math.round(UI.vp.scrollTop || 0)) : 0 }; }, makeShareParseBrowserNavPayload: (snap = null, seq = S.browserNavSeq) => { const curSnap = snap || S.makeShareParseNavSnapshot(); if (!curSnap || !curSnap.sessionKey || !Array.isArray(curSnap.path) || curSnap.path.length < 2) return null; const path = S.cloneShareParsePath(curSnap.path); const sig = `share_parse|${curSnap.sessionKey}|${S.getShareParsePathSig(path)}`; return { schema: 1, type: 'share_parse', seq, sessionKey: curSnap.sessionKey, path, parentId: curSnap.parentId || '', sig, scrollTop: Number.isFinite(curSnap.scrollTop) ? Math.max(0, Math.round(curSnap.scrollTop)) : 0 }; }, writeBrowserNavPayload: (nav, mode = 'push') => { if (!nav || S.browserNavRestoring || S.browserNavRebuildLock || S.navTransitionBusy) return false; const curState = history.state && typeof history.state === 'object' ? history.state : {}; const curNav = S.getBrowserNavPayload(curState); const same = curNav && curNav.sig === nav.sig; const finalMode = mode === 'replace' || same || !S.browserNavInitDone ? 'replace' : 'push'; const nextSeq = finalMode === 'push' && (!curNav || curNav.sig !== nav.sig) ? S.browserNavSeq + 1 : S.browserNavSeq; const finalSeq = finalMode === 'push' ? nextSeq : (curNav && Number.isFinite(curNav.seq) ? curNav.seq : nextSeq); const finalNav = { ...nav, seq: finalSeq }; const nextState = { ...curState, __pkPemBrowserNav: 1, pkNav: finalNav }; try { history[finalMode === 'push' ? 'pushState' : 'replaceState'](nextState, '', location.href); S.browserNavInitDone = true; S.browserNavSeq = finalSeq; S.browserNavSig = finalNav.sig; return true; } catch (e) { return false; } }, writeBrowserNavState: (bucketKey, path = S.path, mode = 'push') => { const nav = S.makeBrowserNavPayload(bucketKey, path, S.browserNavSeq); return S.writeBrowserNavPayload(nav, mode); }, syncBrowserNavState: (bucketKey, mode = 'push') => { if (!bucketKey || !S.navBuckets[bucketKey]) return false; if (S.browserNavRestoring || S.navSuspendRecord || S.navTransitionBusy) return false; return S.writeBrowserNavState(bucketKey, S.path, mode); }, writeShareParseBrowserNavState: (snap = null, mode = 'push') => { const nav = S.makeShareParseBrowserNavPayload(snap, S.browserNavSeq); return S.writeBrowserNavPayload(nav, mode); }, syncShareParseBrowserNavState: (mode = 'push') => { if (S.browserNavRestoring || S.shareParseNavSuspendRecord || S.shareParseNavBusy || S.navTransitionBusy) return false; if (!S.shareParseMode || !S.shareParseListActive || S.shareParseInsightMode) return false; return S.writeShareParseBrowserNavState(null, mode); }, getBrowserOverlaySelector: () => '#pk-player-ov,#pk-audio-ov,.pk-img-ov,.pk-modal-ov', isBrowserOverlayVisible: (el) => { if (!el || !el.isConnected) return false; const st = window.getComputedStyle(el); return st.display !== 'none' && st.visibility !== 'hidden'; }, getBrowserOverlayKind: (el) => { if (!el) return ''; if (el.id === 'pk-player-ov') return 'player'; if (el.id === 'pk-audio-ov') return 'audio'; if (el.classList && el.classList.contains('pk-img-ov')) return 'image'; if (el.classList && el.classList.contains('pk-txt-preview-ov')) return 'text_preview'; if (el.classList && el.classList.contains('pk-modal-ov')) return 'modal'; return 'overlay'; }, getBrowserOverlayZIndex: (el, index = 0) => { const z = Number.parseInt(window.getComputedStyle(el).zIndex, 10); return Number.isFinite(z) ? z : index; }, getBrowserOverlays: () => Array.from(document.querySelectorAll(S.getBrowserOverlaySelector())).filter(el => S.isBrowserOverlayVisible(el)), getTopBrowserOverlay: () => { const list = S.getBrowserOverlays(); if (!list.length) return null; return list.map((el, i) => ({ el, z: S.getBrowserOverlayZIndex(el, i), i })).sort((a, b) => (a.z - b.z) || (a.i - b.i)).pop().el; }, ensureBrowserOverlayId: (el) => { if (!el || !el.dataset) return ''; if (!el.dataset.pkBrowserOverlayId) el.dataset.pkBrowserOverlayId = `bo_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`; return el.dataset.pkBrowserOverlayId; }, getBrowserOverlayById: (id) => { const key = String(id || ''); if (!key) return null; return S.getBrowserOverlays().find(el => el.dataset && el.dataset.pkBrowserOverlayId === key) || null; }, locatePreviewSourceItem: (item = null) => { const source = item && typeof item === 'object' ? item : null; const id = source && String(source.id || source.file_id || ''); const fileId = source && String(source.file_id || ''); if (!id || !Array.isArray(S.display) || !UI.vp) return false; const targetIdx = S.display.findIndex(x => x && (String(x.id || '') === id || String(x.file_id || '') === id || (fileId && String(x.id || '') === fileId) || (fileId && String(x.file_id || '') === fileId))); if (targetIdx < 0) return false; const targetItem = S.display[targetIdx]; const targetId = targetItem && targetItem.id; if (!targetId) return false; S.sel.clear(); S.sel.add(targetId); S.activeId = targetId; S.lastSelIdx = targetIdx; const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight || 0; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); return true; }, closeBrowserOverlay: (el = null) => { const target = el || S.getTopBrowserOverlay(); if (!target) return false; const kind = S.getBrowserOverlayKind(target); if (kind === 'player') return S.closePlayerOverlay(); if (kind === 'audio') return S.closeAudioOverlay(); if (kind === 'image') return S.closeImageOverlay(); if (kind === 'modal' || kind === 'text_preview') { const closeBtn = target.querySelector('.pk-modal-close'); if (closeBtn) closeBtn.click(); else target.remove(); return true; } target.remove(); return true; }, makeBrowserOverlayPayload: (overlay = null, seq = S.browserNavSeq) => { const target = overlay || S.getTopBrowserOverlay(); if (!target) return null; const curNav = S.getBrowserNavPayload(); if (!curNav) return null; const overlayId = S.ensureBrowserOverlayId(target); if (!overlayId) return null; const kind = S.getBrowserOverlayKind(target); const sig = `overlay|${overlayId}`; return { schema: 1, type: 'overlay', seq, kind, overlayId, sig, base: curNav, scrollTop: UI.vp ? Math.max(0, Math.round(UI.vp.scrollTop || 0)) : 0 }; }, syncBrowserOverlayNavState: () => false, normalizeBrowserOverlayNavState: () => { const curNav = S.getBrowserNavPayload(); if (!curNav || curNav.type !== 'overlay') return false; if (S.getBrowserOverlayById(curNav.overlayId)) return false; const base = curNav.base && typeof curNav.base === 'object' ? curNav.base : null; if (!base || !base.schema || !base.type) return false; const curState = history.state && typeof history.state === 'object' ? history.state : {}; const nextState = { ...curState, __pkPemBrowserNav: 1, pkNav: base }; try { history.replaceState(nextState, '', location.href); S.browserNavSeq = Number.isFinite(base.seq) ? base.seq : S.browserNavSeq; S.browserNavSig = base.sig || ''; return true; } catch (e) { return false; } }, scheduleBrowserOverlaySync: () => { if (S.browserOverlaySyncTimer) clearTimeout(S.browserOverlaySyncTimer); S.browserOverlaySyncTimer = setTimeout(() => { S.browserOverlaySyncTimer = 0; if (S.browserNavRestoring) return; S.normalizeBrowserOverlayNavState(); }, 30); }, bindBrowserOverlayHistory: () => { if (S.browserOverlayObserver) { try { S.browserOverlayObserver.disconnect(); } catch (e) {} S.browserOverlayObserver = null; } S.normalizeBrowserOverlayNavState(); return true; }, handleBrowserOverlayPopState: async (nav, targetSeq, direction = 0) => { const restoreBaseState = (base) => { if (!base || !base.schema || !base.type) return false; const curState = history.state && typeof history.state === 'object' ? history.state : {}; try { history.replaceState({ ...curState, __pkPemBrowserNav: 1, pkNav: base }, '', location.href); S.browserNavSeq = Number.isFinite(base.seq) ? base.seq : S.browserNavSeq; S.browserNavSig = base.sig || S.browserNavSig || ''; return true; } catch (e) { return false; } }; if (nav && nav.type === 'overlay') { const base = nav.base && typeof nav.base === 'object' ? nav.base : null; if (base) restoreBaseState(base); const topOverlay = S.getTopBrowserOverlay(); if (topOverlay) S.closeBrowserOverlay(topOverlay); return true; } const topOverlay = S.getTopBrowserOverlay(); if (topOverlay) S.closeBrowserOverlay(topOverlay); return false; }, syncInternalNavStackFromBrowser: (bucketKey, targetPath, direction) => { const bucket = bucketKey ? S.navBuckets[bucketKey] : null; if (!bucket) return; const target = S.cloneNavPath(targetPath); const current = bucket.current && bucket.current.length ? S.cloneNavPath(bucket.current) : S.cloneNavPath(S.path); if (direction < 0) { if (current.length && !S.isSameNavPath(current, target)) { bucket.forwardStack.push(current); S.trimNavStack(bucket.forwardStack); } if (bucket.backStack.length && S.isSameNavPath(bucket.backStack[bucket.backStack.length - 1], target)) bucket.backStack.pop(); } else if (direction > 0) { if (current.length && !S.isSameNavPath(current, target)) { bucket.backStack.push(current); S.trimNavStack(bucket.backStack); } if (bucket.forwardStack.length && S.isSameNavPath(bucket.forwardStack[bucket.forwardStack.length - 1], target)) bucket.forwardStack.pop(); } bucket.current = target; }, syncShareParseInternalNavStackFromBrowser: (nav, direction) => { if (!nav || nav.type !== 'share_parse' || nav.sessionKey !== S.getShareParseNavSessionKey()) return; const target = { sessionKey: nav.sessionKey, path: S.cloneShareParsePath(nav.path), parentId: nav.parentId || '', scrollTop: Number.isFinite(nav.scrollTop) ? Math.max(0, Math.round(nav.scrollTop)) : 0 }; const current = S.makeShareParseNavSnapshot(); if (direction < 0) { if (current && !S.isSameShareParsePath(current.path, target.path)) { S.shareParseNavForwardStack.push(current); S.trimShareParseNavStack(S.shareParseNavForwardStack); } if (S.shareParseNavBackStack.length && S.isSameShareParsePath(S.shareParseNavBackStack[S.shareParseNavBackStack.length - 1].path, target.path)) S.shareParseNavBackStack.pop(); } else if (direction > 0) { if (current && !S.isSameShareParsePath(current.path, target.path)) { S.shareParseNavBackStack.push(current); S.trimShareParseNavStack(S.shareParseNavBackStack); } if (S.shareParseNavForwardStack.length && S.isSameShareParsePath(S.shareParseNavForwardStack[S.shareParseNavForwardStack.length - 1].path, target.path)) S.shareParseNavForwardStack.pop(); } }, isBrowserNavRootPayload: (nav) => { if (!nav || !nav.type) return false; if (nav.type === 'drive') return Array.isArray(nav.path) && nav.path.length <= 1; if (nav.type === 'share_parse') return Array.isArray(nav.path) && nav.path.length <= 2; return false; }, getCurrentBrowserNavPayloadForGuard: () => { if (S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode) return S.makeShareParseBrowserNavPayload(null, S.browserNavSeq); const key = S.getNavBucketKey(); return key ? S.makeBrowserNavPayload(key, S.path, S.browserNavSeq) : null; }, pushBrowserNavExitGuard: (nav = null) => { if (S.browserNavExitGuarding) return false; const baseNav = nav || S.getCurrentBrowserNavPayloadForGuard(); if (!baseNav || !baseNav.sig) return false; S.browserNavExitGuarding = true; try { const curState = history.state && typeof history.state === 'object' ? history.state : {}; const curSeq = Number.isFinite(baseNav.seq) ? baseNav.seq : S.browserNavSeq; const curNav = { ...baseNav, seq: curSeq }; const nextNav = { ...baseNav, seq: curSeq + 1 }; history.replaceState({ ...curState, __pkPemBrowserNav: 1, pkNav: curNav }, '', location.href); history.pushState({ ...curState, __pkPemBrowserNav: 1, pkNav: nextNav }, '', location.href); S.browserNavInitDone = true; S.browserNavSeq = nextNav.seq; S.browserNavSig = nextNav.sig; return true; } catch (e) { return false; } finally { S.browserNavExitGuarding = false; } }, handleBrowserNavMissingState: async () => { const overlay = S.getTopBrowserOverlay(); if (overlay) { S.closeBrowserOverlay(overlay); const guardNav = S.getCurrentBrowserNavPayloadForGuard(); if (guardNav) S.pushBrowserNavExitGuard(guardNav); return true; } if (S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode) { const ok = await S.shareParseSideBack(); if (ok) { S.syncShareParseBrowserNavState('replace'); return true; } const nav = S.makeShareParseBrowserNavPayload(null, S.browserNavSeq); return S.pushBrowserNavExitGuard(nav); } if (S.isStrictVirtualNavMode()) { await S.exitVirtualNavMode(); const keyAfterExit = S.getNavBucketKey(); if (keyAfterExit) S.writeBrowserNavState(keyAfterExit, S.path, 'replace'); return true; } const key = S.getNavBucketKey(); const bucket = key ? S.navBuckets[key] : null; if (bucket && bucket.backStack && bucket.backStack.length) { const ok = await S.navGoBack(); if (ok) S.writeBrowserNavState(key, S.path, 'replace'); return true; } const nav = key ? S.makeBrowserNavPayload(key, S.path, S.browserNavSeq) : S.getCurrentBrowserNavPayloadForGuard(); return S.pushBrowserNavExitGuard(nav); }, handleBrowserNavPopState: async (e) => { const nav = S.getBrowserNavPayload(e && e.state); const targetSeq = nav && Number.isFinite(nav.seq) ? nav.seq : S.browserNavSeq; if (!nav) return await S.handleBrowserNavMissingState(); if (S.browserNavRestoring || S.navTransitionBusy || S.shareParseNavBusy) return true; const direction = targetSeq === S.browserNavSeq ? 0 : (targetSeq < S.browserNavSeq ? -1 : 1); if (await S.handleBrowserOverlayPopState(nav, targetSeq, direction)) return true; if (nav.type === 'drive') { if (!S.navBuckets[nav.bucket] || !Array.isArray(nav.path) || !nav.path.length) return false; const targetPath = S.cloneNavPath(nav.path); const prevSuspend = S.navSuspendRecord; S.browserNavRestoring = true; S.navSuspendRecord = true; try { S.syncInternalNavStackFromBrowser(nav.bucket, targetPath, direction); S.browserNavSeq = targetSeq; S.browserNavSig = nav.sig || `drive|${nav.bucket}|${S.getNavPathSig(targetPath)}`; const ok = await S.loadNavPath(targetPath, nav.bucket, { skipNavSync: true }); if (ok) { S.navActiveBucket = nav.bucket; S.navContext = nav.bucket; S.navFrozenBucket = null; S.navSyncSig = `${nav.bucket}|${S.getNavPathSig(targetPath)}`; if (UI.vp && Number.isFinite(nav.scrollTop)) requestAnimationFrame(() => { UI.vp.scrollTop = Math.max(0, Math.round(nav.scrollTop)); }); if (direction < 0 && S.isBrowserNavRootPayload(nav)) setTimeout(() => S.pushBrowserNavExitGuard(nav), 0); } return ok; } finally { S.navSuspendRecord = prevSuspend; S.browserNavRestoring = false; } } if (nav.type === 'share_parse') { if (!S.shareParseMode || !S.shareParseListActive || S.shareParseInsightMode) return false; if (nav.sessionKey !== S.getShareParseNavSessionKey() || !Array.isArray(nav.path) || nav.path.length < 2) return false; S.browserNavRestoring = true; try { S.syncShareParseInternalNavStackFromBrowser(nav, direction); S.browserNavSeq = targetSeq; S.browserNavSig = nav.sig || `share_parse|${nav.sessionKey}|${S.getShareParsePathSig(nav.path)}`; const ok = await S.loadShareParseNavSnapshot({ sessionKey: nav.sessionKey, path: S.cloneShareParsePath(nav.path), parentId: nav.parentId || '', scrollTop: Number.isFinite(nav.scrollTop) ? Math.max(0, Math.round(nav.scrollTop)) : 0 }); if (ok && direction < 0 && S.isBrowserNavRootPayload(nav)) setTimeout(() => S.pushBrowserNavExitGuard(nav), 0); return ok; } finally { S.browserNavRestoring = false; } } return false; }, restoreFrozenNavBucketPath: () => { const key = S.navFrozenBucket; const bucket = key ? S.navBuckets[key] : null; if (!bucket || !bucket.current || !bucket.current.length) return false; S.navSuspendRecord = true; stopLiveManualRefreshBeforePathChange('path_change'); S.path = S.cloneNavPath(bucket.current); S.navActiveBucket = key; S.navContext = key; S.navSyncSig = ''; queueMicrotask(() => { S.navSuspendRecord = false; }); return true; }, loadNavPath: async (path, bucketKey = null, opts = null) => { const targetPath = S.cloneNavPath(path); if (!targetPath.length) return false; if (S.navTransitionBusy) return false; const key = bucketKey || S.getNavBucketKey() || S.navActiveBucket || null; const skipNavSync = !!(opts && opts.skipNavSync); const prevPath = S.cloneNavPath(S.path); const prevActiveBucket = S.navActiveBucket; const prevContext = S.navContext; const prevSyncSig = S.navSyncSig; const prevSuspendRecord = S.navSuspendRecord; const prevScrollTop = UI.vp ? UI.vp.scrollTop : 0; const prevSelMode = S.selMode; const prevSel = new Set(S.sel); const prevSelEx = new Set(S.selEx); const prevLastSelIdx = S.lastSelIdx; const prevActiveIdForRollback = S.activeId; const targetSig = S.getNavPathSig(targetPath); S.navTransitionBusy = true; S.navSuspendRecord = true; S.saveNavScrollTop(prevActiveBucket || S.getNavBucketKey(), prevPath, prevScrollTop); stopLiveManualRefreshBeforePathChange('path_change'); S.path = targetPath; S.navActiveBucket = key; S.navContext = key || 'none'; S.navSyncSig = ''; try { updateQuotaUI(); const pendingLoad = Promise.resolve(load()); await new Promise(r => requestAnimationFrame(r)); S.clearSelection(); pendingLoad.then(() => { if (S.getNavPathSig() === targetSig) { S.restoreNavScrollTop(key, targetPath); } }).catch(()=>{}); return true; } catch (err) { stopLiveManualRefreshBeforePathChange('path_change'); S.path = prevPath; S.navActiveBucket = prevActiveBucket; S.navContext = prevContext; S.navSyncSig = prevSyncSig || ''; S.selMode = prevSelMode; S.sel = new Set(prevSel); S.selEx = new Set(prevSelEx); S.lastSelIdx = prevLastSelIdx; S.activeId = prevActiveIdForRollback; if (typeof updateStat === 'function') updateStat(); try { updateQuotaUI(); await load(); S.restoreNavScrollTop(prevActiveBucket || S.getNavBucketKey(), prevPath); } catch (restoreErr) {} return false; } finally { S.navSuspendRecord = prevSuspendRecord; S.navTransitionBusy = false; if (!skipNavSync) { S.navSyncSig = ''; S.syncNavContextState(); } } }, navGoBack: async () => { const key = S.getNavBucketKey(); const bucket = key ? S.navBuckets[key] : null; if (!bucket || !bucket.backStack.length || S.navTransitionBusy) return false; const prevPath = S.cloneNavPath(bucket.backStack[bucket.backStack.length - 1]); if (!prevPath.length) return false; const currentPath = bucket.current && bucket.current.length ? S.cloneNavPath(bucket.current) : S.cloneNavPath(S.path); const ok = await S.loadNavPath(prevPath, key, { skipNavSync: true }); if (!ok) return false; bucket.backStack.pop(); if (currentPath.length) { bucket.forwardStack.push(currentPath); S.trimNavStack(bucket.forwardStack); } bucket.current = S.cloneNavPath(prevPath); S.navActiveBucket = key; S.navContext = key; S.navFrozenBucket = null; S.navSyncSig = ''; S.syncNavContextState(); return true; }, navGoForward: async () => { const key = S.getNavBucketKey(); const bucket = key ? S.navBuckets[key] : null; if (!bucket || !bucket.forwardStack.length || S.navTransitionBusy) return false; const nextPath = S.cloneNavPath(bucket.forwardStack[bucket.forwardStack.length - 1]); if (!nextPath.length) return false; const currentPath = bucket.current && bucket.current.length ? S.cloneNavPath(bucket.current) : S.cloneNavPath(S.path); const ok = await S.loadNavPath(nextPath, key, { skipNavSync: true }); if (!ok) return false; bucket.forwardStack.pop(); if (currentPath.length) { bucket.backStack.push(currentPath); S.trimNavStack(bucket.backStack); } bucket.current = S.cloneNavPath(nextPath); S.navActiveBucket = key; S.navContext = key; S.navFrozenBucket = null; S.navSyncSig = ''; S.syncNavContextState(); return true; }, exitVirtualNavMode: async () => { const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; const isAnalyzeGroupedGrid = S.analyzeMode && !!S.analyzeSimGroups && !!cur && cur.id === 'analyze_root' && S.viewMode === 'grid'; const shouldSyncGroupedGridExit = (S.dupMode && S.viewMode === 'grid') || isAnalyzeGroupedGrid; S.scanning = false; S.dupMode = false; S.dupResultSig = ''; S.virtualSortMaskSig = ''; S.suppressClearConfirm = false; S.isFlattened = false; S.scanFilter = null; if (S.filterState) S.filterState = { active: false, cat: 'all', ext: 'all' }; if (shouldSyncGroupedGridExit) S._groupedGridExitSyncPending = true; S._sortAppliedForId = null; S._comicApplied = false; if (S.analyzeMode) { S.analyzeMode = false; S.analyzeResultItems = null; S.analyzeSimGroups = null; S.analyzeMap = null; } S.dupRawGroups = []; S.dupBuckets = null; S.dupItemMap = null; S.pinnedDupPath = null; if (UI.selDupFolder) UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } if (UI.chkSearchPath) UI.chkSearchPath.checked = false; isGUISensitive = false; S.sort = 'modified_time'; S.dir = 1; if (UI.crumb) UI.crumb.style.display = ''; const frozenKey = S.navFrozenBucket; const frozenBucket = frozenKey ? S.navBuckets[frozenKey] : null; const fallbackPath = [{ id: '', name: L.btn_nav_home }]; const targetPath = (frozenBucket && frozenBucket.current && frozenBucket.current.length) ? frozenBucket.current : fallbackPath; S.navFrozenBucket = null; const targetLeaf = targetPath[targetPath.length - 1] || { id: '' }; if (typeof applyResolvedViewMode === 'function') { applyResolvedViewMode((targetLeaf && targetLeaf.id) || ''); } return await S.loadNavPath(targetPath, frozenBucket ? frozenKey : null); }, getTopModalOverlay: () => { const list = Array.from(document.querySelectorAll('.pk-modal-ov')).filter(m => { if (!m || !m.isConnected) return false; const st = window.getComputedStyle(m); return st.display !== 'none' && st.visibility !== 'hidden'; }); return list.length ? list[list.length - 1] : null; }, hasFrontOverlayForSideNav: () => { return !!(document.getElementById('pk-player-ov') || document.getElementById('pk-audio-ov') || document.querySelector('.pk-img-ov') || S.getTopModalOverlay()); }, isManagerSideNavScope: () => { const win = document.querySelector('.pk-ov'); return !!(win && win.style.display !== 'none'); }, closePlayerOverlay: () => { const player = document.getElementById('pk-player-ov'); if (!player) return false; S.navVideoBackArmed = false; const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; if (typeof player._pkDestroyPlayer === 'function') { player._pkDestroyPlayer(); return true; } const v = player.querySelector('#pk_video'); if (v) { try { v.pause(); v.removeAttribute('src'); v.load(); } catch (e) {} } player.remove(); return true; }, closeAudioOverlay: () => { const player = document.getElementById('pk-audio-ov'); if (!player) return false; if (typeof player._pkDestroyAudioPlayer === 'function') { player._pkDestroyAudioPlayer(); return true; } const audio = player.querySelector('#pk_audio'); if (audio) { try { audio.pause(); audio.removeAttribute('src'); audio.load(); } catch (e) {} } player.remove(); return true; }, closeImageOverlay: () => { const d = document.querySelector('.pk-img-ov'); if (!d) return false; const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; const cleanup = d._pkResizeHandler; if (cleanup) { window.removeEventListener('resize', cleanup); d._pkResizeHandler = null; } const imgList = d._pkImgList || []; const curIdx = Number.isInteger(d._pkCurIdx) ? d._pkCurIdx : -1; const currentItem = curIdx >= 0 ? imgList[curIdx] : null; if (currentItem) { const targetId = currentItem.id; const targetIdx = S.display.findIndex(x => x.id === targetId); if (targetIdx !== -1) { S.sel.clear(); S.sel.add(targetId); S.activeId = targetId; const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); } } d.remove(); return true; }, closeTopModalOverlay: () => { const openModal = S.getTopModalOverlay(); if (!openModal) return false; const closeBtn = openModal.querySelector('.pk-modal-close'); if (closeBtn) closeBtn.click(); else openModal.remove(); return true; }, handleOverlayMouseSideBack: () => { const nav = S.getBrowserNavPayload(); if (nav && nav.type === 'overlay') { history.back(); return true; } const player = document.getElementById('pk-player-ov'); if (player) { return S.closePlayerOverlay(); } const audioPlayer = document.getElementById('pk-audio-ov'); if (audioPlayer) { return S.closeAudioOverlay(); } const imgPlayer = document.querySelector('.pk-img-ov'); if (imgPlayer) { return S.closeImageOverlay(); } if (S.closeTopModalOverlay()) { return true; } return false; }, canHandleMouseSideBack: () => S.isManagerSideNavScope(), canHandleMouseSideForward: () => S.isManagerSideNavScope(), handleMouseSideBack: async () => { if (!S.isManagerSideNavScope() || S.navTransitionBusy) return false; if (S.handleOverlayMouseSideBack()) return true; if (S.shareParseMode) { const ok = await S.shareParseSideBack(); if (ok) S.syncShareParseBrowserNavState('replace'); return true; } if (S.isStrictVirtualNavMode()) { await S.exitVirtualNavMode(); return true; } if (S.getNavBucketKey() !== null) { const key = S.getNavBucketKey(); const ok = await S.navGoBack(); if (ok) S.writeBrowserNavState(key, S.path, 'replace'); return true; } return true; }, handleMouseSideForward: async () => { if (!S.isManagerSideNavScope() || S.navTransitionBusy) return false; const player = document.getElementById('pk-player-ov'); if (player) { const v = player.querySelector('#pk_video'); if (v) { if (v.ended) { try { v.currentTime = 0; } catch (e) {} v.play().catch(()=>{}); return true; } if (v.paused) { v.play().catch(()=>{}); return true; } v.pause(); return true; } return true; } const audioPlayer = document.getElementById('pk-audio-ov'); if (audioPlayer) { const playBtn = audioPlayer.querySelector('#pk_audio_play'); if (playBtn) { playBtn.click(); return true; } const audio = audioPlayer.querySelector('#pk_audio'); if (audio) { if (audio.ended) { try { audio.currentTime = 0; } catch (e) {} audio.play().catch(()=>{}); return true; } if (audio.paused) { audio.play().catch(()=>{}); return true; } audio.pause(); return true; } return true; } if (S.hasFrontOverlayForSideNav()) return true; if (S.shareParseMode) { const ok = await S.shareParseSideForward(true); if (ok) S.syncShareParseBrowserNavState('replace'); return true; } if (S.isStrictVirtualNavMode()) return true; if (S.getNavBucketKey() !== null) { const key = S.getNavBucketKey(); const ok = await S.navGoForward(); if (ok) S.writeBrowserNavState(key, S.path, 'replace'); return true; } return true; }, syncNavContextState: () => { const ctx = S.getNavContext(); const sig = `${ctx}|${(Array.isArray(S.path) ? S.path.map(n => n.id).join('>') : '')}`; if (S.navSyncSig === sig) return; const prevCtx = S.navContext; const prevBucketKey = (prevCtx === 'home' || prevCtx === 'starred' || prevCtx === 'recent') ? prevCtx : null; const curBucketKey = (ctx === 'home' || ctx === 'starred' || ctx === 'recent') ? ctx : null; if (S.navSuspendRecord) { S.navContext = ctx; S.navSyncSig = sig; if (curBucketKey) S.navActiveBucket = curBucketKey; return; } if (ctx === 'virtual') { if (prevBucketKey) S.navFrozenBucket = prevBucketKey; S.navContext = 'virtual'; S.navSyncSig = sig; return; } if (ctx === 'none') { if (prevBucketKey) S.navFrozenBucket = prevBucketKey; S.navContext = 'none'; S.navSyncSig = sig; return; } S.navActiveBucket = curBucketKey; const recorded = S.recordNavSnapshot(curBucketKey); if (recorded || !S.browserNavInitDone) S.syncBrowserNavState(curBucketKey, recorded ? 'push' : 'replace'); if (prevCtx === 'virtual' && S.navFrozenBucket === curBucketKey) { S.navFrozenBucket = null; } else if (prevCtx !== 'virtual') { S.navFrozenBucket = null; } S.navContext = curBucketKey; S.navSyncSig = sig; }, canUseMouseSideNav: () => S.canHandleMouseSideBack() || S.canHandleMouseSideForward(), isStrictVirtualNavMode: () => { if (S.isFlattened || S.dupMode || S.analyzeMode) return true; const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; return !!(cur && typeof cur.id === 'string' && (cur.id.startsWith('virtual_') || cur.id === 'analyze_root')); }, getSelectableItems: () => { const out = []; const list = Array.isArray(S.display) ? S.display : []; for (let i = 0; i < list.length; i++) { const item = list[i]; if (!item || item.isHeader) continue; if (S.shareParseMode && (!S.shareParseListActive || !item._isShareItem)) continue; if (S.movingIds && S.movingIds.has(item.id)) continue; out.push(item); } return out; }, getSelectableIds: () => { const out = []; const list = Array.isArray(S.display) ? S.display : []; for (let i = 0; i < list.length; i++) { const item = list[i]; if (!item || item.isHeader) continue; if (S.shareParseMode && (!S.shareParseListActive || !item._isShareItem)) continue; if (S.movingIds && S.movingIds.has(item.id)) continue; out.push(item.id); } return out; }, getSelectableCount: () => { let count = 0; const list = Array.isArray(S.display) ? S.display : []; for (let i = 0; i < list.length; i++) { const item = list[i]; if (!item || item.isHeader) continue; if (S.shareParseMode && (!S.shareParseListActive || !item._isShareItem)) continue; if (S.movingIds && S.movingIds.has(item.id)) continue; count++; } return count; }, isAllSelectionMode: () => S.selMode === 'all', isSelected: (id) => { if (!id) return false; return S.selMode === 'all' ? !S.selEx.has(id) : S.sel.has(id); }, getSelectedCount: () => { return S.getSelectedIds().length; }, getSelectedIds: () => { const isValidShareId = (id) => { if (!S.shareParseMode) return true; const item = (S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id)) || (S.itemMap && S.itemMap.get(id)); return !!(S.shareParseListActive && item && item._isShareItem); }; if (S.selMode !== 'all') return Array.from(S.sel).filter(isValidShareId); const ids = []; const list = S.getSelectableIds(); for (let i = 0; i < list.length; i++) { const id = list[i]; if (!S.selEx.has(id) && isValidShareId(id)) ids.push(id); } return ids; }, setSelected: (id, selected) => { if (!id) return; if (S.shareParseMode) { const item = (S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id)) || (S.itemMap && S.itemMap.get(id)); if (!S.shareParseListActive || !item || !item._isShareItem) return; } if (S.selMode === 'all') { if (selected) S.selEx.delete(id); else S.selEx.add(id); return; } if (selected) S.sel.add(id); else S.sel.delete(id); }, toggleSelected: (id) => { if (!id) return false; const next = !S.isSelected(id); S.setSelected(id, next); return next; }, setExplicitSelection: (ids) => { S.selMode = 'explicit'; S.selEx.clear(); let nextIds = Array.isArray(ids) ? ids : []; if (S.shareParseMode) { nextIds = S.shareParseListActive ? nextIds.filter(id => { const item = (S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id)) || (S.itemMap && S.itemMap.get(id)); return item && item._isShareItem; }) : []; } S.sel = new Set(nextIds); }, setAllSelection: (enabled = true) => { if (enabled) { if (S.shareParseMode && !S.shareParseListActive) { S.selMode = 'explicit'; S.sel.clear(); S.selEx.clear(); return; } S.selMode = 'all'; S.sel.clear(); S.selEx.clear(); } else { S.selMode = 'explicit'; S.sel.clear(); S.selEx.clear(); } }, invertSelection: () => { const total = S.getSelectableCount(); if (total <= 0) { S.clearSelection(); return; } if (S.selMode === 'all') { S.setExplicitSelection(Array.from(S.selEx)); } else { const nextExcluded = new Set(S.sel); S.selMode = 'all'; S.sel.clear(); S.selEx = nextExcluded; } S.lastSelIdx = -1; S.activeId = null; }, clearSelection: () => { S.selMode = 'explicit'; S.sel.clear(); S.selEx.clear(); S.lastSelIdx = -1; S.activeId = null; if (typeof updateStat === 'function') updateStat(); }, updateBlCache: () => { const parse = (key) => new Set(gmGet(key, '').toLowerCase().split('\n').map(s => s.trim()).filter(s => s)); S.blSet = parse('pk_blacklist'); S.blFolderSet = parse('pk_blacklist_folders'); }, getRealCacheKey: (id) => { if (id === 'trash_root') return 'root_trashed'; if (id === 'offline_root') return 'offline_root'; if (id === 'recent_root') return 'recent_root'; if (id === 'history_root') return 'history_root'; if (id === 'root' || id === '' || !id) { if (S.shareMode) return 'share_root'; if (S.starredMode) return 'starred_root'; if (S.recentMode) return 'recent_root'; if (S.historyMode) return 'history_root'; if (S.trashMode) return 'root_trashed'; if (S.offlineMode) return 'offline_root'; return 'root'; } return id; } }; pkState = S; if (S.uploadTasks.length > 0) { S.uploadTasks.forEach(task => { if (task.status === 'WAITING') task.message = L.msg_task_waiting; else if (task.status === 'DONE') task.message = L.msg_task_upload_done; else if (task.status === 'PAUSED') task.message = L.msg_task_paused; else if (task.status === 'HASHING') task.message = L.msg_task_hashing; else if (task.status === 'UPLOADING') task.message = L.msg_task_uploading; }); } const FloatBarManager = (() => { const activeBars = []; const BASE_BOTTOM = 30; const GAP = 10; const reposition = () => { let currentBottom = BASE_BOTTOM; activeBars.forEach(item => { item.el.style.bottom = `${currentBottom}px`; currentBottom += (item.el.offsetHeight || 40) + GAP; }); }; const create = (initialText) => { const id = 'pk_fb_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); const el = document.createElement('div'); el.className = 'pk-float-bar-item pk-hide'; el.innerHTML = `
${esc(initialText)}`; const isDark = document.querySelector('.pk-ov')?.classList.contains('pk-dark'); if (isDark) el.classList.add('pk-dark'); const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (fsEl) fsEl.appendChild(el); else document.body.appendChild(el); const entry = { id, el }; activeBars.push(entry); requestAnimationFrame(() => { reposition(); el.classList.remove('pk-hide'); el.classList.add('pk-show'); }); 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.classList.remove('pk-show'); el.classList.add('pk-hide'); 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) { 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'; if (gmGet('pk_hide_button_text', false)) el.classList.add('pk-hide-btn-text'); 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'); if (!scrollTarget) { e.preventDefault(); return; } const st = scrollTarget.scrollTop; const sh = scrollTarget.scrollHeight; const ch = scrollTarget.clientHeight; const isUp = e.deltaY < 0; const isDown = e.deltaY > 0; const isAtTop = st <= 0.5; const isAtBottom = (st + ch) >= (sh - 1.5); if ((isUp && isAtTop) || (isDown && isAtBottom)) { if (e.cancelable) e.preventDefault(); } }, { passive: false }); const mkLbl = (id, txt, shortTxt, tip) => { const tipAttr = tip ? ` data-pk-tip="${tip}"` : ''; return ``; }; const savedTheme = gmGet('pk_theme', 'auto'); const sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; let isDark = savedTheme === 'dark' || (savedTheme === 'auto' && sysDark); if (isDark) el.classList.add('pk-dark'); const themeIcon = isDark ? CONF.icons.sun : CONF.icons.moon; const initialGridShell = S.viewMode === 'grid' && !S.shareMode && !S.shareParseMode && !S.linkBookmarkMode && !S.offlineMode && !S.uploadMode && !S.historyMode; const winClass = 'pk-win pk-lang-' + lang + (initialGridShell ? ' pk-grid-view' : ''); const initialHeaderStyle = initialGridShell ? 'visibility:hidden;' : ''; const initialGridHeaderHtml = `
`; const ctxIcons = { locate: ``, info: ``, open: ``, star: ``, unstar: ``, download: ``, copy: ``, move: ``, rename: ``, renameBulk: ``, blAdd: ``, blRem: ``, share: CONF.icons.share, copyLink: ``, copyName: ``, trash: ``, restore: ``, delForever: `` }; el.innerHTML = `
${L.loading_detail}
${CONF.icons.cloudDownload}${L.btn_cloud_download}
${CONF.icons.home}${L.btn_nav_home}
${CONF.icons.navUpload}${L.btn_nav_upload}
${CONF.icons.offline}${L.btn_nav_offline}
${CONF.icons.recent}${L.btn_nav_recent}
${CONF.icons.history}${L.btn_nav_history}
${CONF.icons.navShare}${L.btn_nav_share}
${L.btn_nav_starred}
${CONF.icons.trash}${L.btn_nav_trash}
${CONF.icons.shareParse}${L.menu_share_parse}
--%
${CONF.icons.help}
${CONF.icons.settings}${L.btn_settings}
${CONF.logoSVG('45px')}
${L.title} by digbug82
${themeIcon}
${CONF.icons.maximize}
${CONF.icons.close}
${mkLbl('pk-chk-hash', L.tag_hash, L.tag_hash_short)} ${mkLbl('pk-chk-sim', L.tag_sim, L.tag_sim_short)} ${mkLbl('pk-chk-name', L.tag_name, L.tag_name_short)}
${CONF.icons.upFile} ${L.btn_up_file}
${CONF.icons.upFolder} ${L.btn_up_folder}
${initialGridShell ? initialGridHeaderHtml : `
${L.col_name}
${CONF.icons.folderFirst} ${L.lbl_folder_first}
${L.col_size}
${L.col_dur}
${L.col_date}
`}
${L.status_ready.replace('{n}', 0)}
${CONF.icons.share} ${L.ctx_share}
${ctxIcons.open} ${L.ctx_open}
${ctxIcons.star} ${L.ctx_star}
${ctxIcons.info} ${L.ctx_property}
${ctxIcons.download} ${L.ctx_down}
${ctxIcons.copyName} ${L.ctx_copy_name}
${ctxIcons.move} ${L.btn_cut}
${ctxIcons.copy} ${L.ctx_copy}
${ctxIcons.rename} ${L.ctx_rename}
${CONF.icons.prune} ${L.btn_prune}
${ctxIcons.trash} ${L.ctx_del}
${ctxIcons.blAdd} ${L.ctx_add_bl}
`; document.body.appendChild(el); hardenPkAutofill(el); installPkAutofillGuard(el); const destroyTooltip = (() => { const tipEl = document.createElement('div'); tipEl.className = 'pk-tooltip'; document.body.appendChild(tipEl); let activeTarget = null; let isMenuOpen = false; let lastMouseX = 0, lastMouseY = 0; let lastTipSig = ''; const hideTip = () => { tipEl.style.display = 'none'; tipEl.classList.remove('show'); tipEl.style.left = '-9999px'; activeTarget = null; lastTipSig = ''; }; 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 htmlTip = target.getAttribute('data-pk-tip-html'); const thumb = target.getAttribute('data-pk-thumb'); if (!text && !htmlTip && !thumb) { hideTip(); return; } const isGridTooltip = !!target.closest('.pk-grid-view, .pk-grid-card, .pk-grid-card-row, .pk-gv-cover'); const tooltipView = isGridTooltip ? 'grid' : 'list'; const isBlur = (typeof isBlurEnabledForView === 'function') ? isBlurEnabledForView(tooltipView) : gmGet('pk_blur_thumb', false); const rowId = target.closest('[data-id]')?.dataset.id || ''; const tipSig = [rowId, text || '', htmlTip || '', thumb || '', tooltipView, isBlur ? 'blur' : 'clear', isDark ? 'dark' : 'light'].join('|'); activeTarget = target; if (tipEl.parentNode && tipEl.nextSibling) { document.body.appendChild(tipEl); } if (lastTipSig === tipSig && tipEl.style.display === 'block') { tipEl.classList.add('show'); return; } let html = ''; const usableThumb = thumb && thumb !== 'undefined' && thumb !== 'null' && (thumb.startsWith('http') || thumb.startsWith('blob:')) && !isPreviewIconFailed(thumb); if (usableThumb) { const safeThumb = String(thumb).replace(/"/g, '"'); html += ``; } if (htmlTip) { html += `
${htmlTip}
`; } else if (text) { html += `
${target.classList.contains('pk-share-history-title-tip') ? esc(text) : text}
`; } lastTipSig = tipSig; tipEl.innerHTML = html; const tipImg = tipEl.querySelector('img[data-pk-tooltip-thumb]'); if (tipImg) { tipImg.addEventListener('error', () => { markPreviewIconFailed(tipImg.dataset.pkTooltipThumb || tipImg.getAttribute('src') || ''); tipImg.remove(); lastTipSig = ''; }, { once: true }); } tipEl.style.display = 'block'; requestAnimationFrame(() => tipEl.classList.add('show')); }; document.addEventListener('mouseover', (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; if (isMenuOpen) return; if (document.body.classList.contains('pk-dragging')) { hideTip(); return; } if (document.querySelector('#pk-ctx')?.style.display === 'block') return; const target = e.target.closest('[data-pk-tip], [data-pk-thumb]'); if (!target) return; if (target === activeTarget) return; const isName = target.classList.contains('pk-name') || target.closest('.pk-name'); const isPath = target.classList.contains('pk-path') || target.closest('.pk-path'); const isThumb = target.hasAttribute('data-pk-thumb'); const isUI = target.closest('.pk-tb, .pk-sidebar, .pk-hd, .pk-ft, .pk-player-box, .pk-modal, .pk-img-box'); const isForceTip = target.classList.contains('pk-force-tip'); const isShareHistoryTitleTip = target.classList.contains('pk-share-history-title-tip'); const isOverflowTip = target.dataset.pkTipOverflow === '1'; const isTooltipTextBlocked = el => !!el && (el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1); if (isOverflowTip && !isTooltipTextBlocked(target)) return; if (isShareHistoryTitleTip && target.dataset.pkSmartCut !== '1' && !isTooltipTextBlocked(target)) return; const isHeaderActionTip = /^(pk-btn-folder-first|pk-btn-invert|pk-grid-folder-first|pk-grid-invert)$/.test(target.id); if (isHeaderActionTip) { const ov = target.closest('.pk-ov'); if (!ov || (!ov.classList.contains('pk-hide-btn-text') && !ov.classList.contains('pk-auto-hide-btn-text'))) return; } if (!isName && !isPath && !isThumb && !isUI && !isForceTip) { if (target.scrollWidth <= target.clientWidth + 1) return; } renderTip(target); updatePos({ clientX: lastMouseX, clientY: lastMouseY }); }); const updatePos = (e) => { if (!tipEl || isMenuOpen || tipEl.style.display === 'none') return; let left = (e.clientX) + 15; let top = (e.clientY) + 15; const rect = getLogicalRect(tipEl); const w = rect.width; const h = rect.height; const winW = window.innerWidth; const winH = window.innerHeight; if (left + w > winW - 10) left = (e.clientX) - w - 15; if (top + h > winH - 10) top = (e.clientY) - h - 15; tipEl.style.left = `${left}px`; tipEl.style.top = `${top}px`; }; const onGlobalMouseMove = (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; if (document.body.classList.contains('pk-dragging')) { hideTip(); return; } updatePos(e); if (activeTarget && tipEl.style.display !== 'none') { const actualTarget = e.target.closest('[data-pk-tip],[data-pk-thumb]'); if (actualTarget !== activeTarget) { hideTip(); } } }; document.addEventListener('mousemove', onGlobalMouseMove); const onGlobalMouseOut = (e) => { const target = e.target.closest('[data-pk-tip],[data-pk-thumb]'); if (target && target === activeTarget) { if (target.contains(e.relatedTarget)) return; hideTip(); } }; document.addEventListener('mouseout', onGlobalMouseOut); const onGlobalCtxMenu = () => { isMenuOpen = true; hideTip(); }; const onGlobalClick = () => { isMenuOpen = false; if(activeTarget) hideTip(); }; const onGlobalWheel = () => { isMenuOpen = false; hideTip(); }; window.addEventListener('contextmenu', onGlobalCtxMenu, true); window.addEventListener('click', onGlobalClick, true); window.addEventListener('wheel', onGlobalWheel, { passive: true, capture: true }); window.pkRefreshTooltip = () => { if (isMenuOpen || document.body.classList.contains('pk-dragging')) { hideTip(); return; } const elUnder = document.elementFromPoint(lastMouseX, lastMouseY); if (!elUnder) { hideTip(); return; } const target = elUnder.closest('[data-pk-tip], [data-pk-thumb]'); if (!target) { hideTip(); return; } if (target.closest('.pk-grid-card-row')) { hideTip(); return; } const isName = target.classList.contains('pk-name') || target.closest('.pk-name'); const isPath = target.classList.contains('pk-path') || target.closest('.pk-path'); const isThumb = target.hasAttribute('data-pk-thumb'); const isUI = target.closest('.pk-tb, .pk-sidebar, .pk-hd, .pk-ft, .pk-player-box, .pk-modal, .pk-img-box'); const isForceTip = target.classList.contains('pk-force-tip'); const isShareHistoryTitleTip = target.classList.contains('pk-share-history-title-tip'); const isOverflowTip = target.dataset.pkTipOverflow === '1'; const isTooltipTextBlocked = el => !!el && (el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1); if (isOverflowTip && !isTooltipTextBlocked(target)) { hideTip(); return; } if (isShareHistoryTitleTip && target.dataset.pkSmartCut !== '1' && !isTooltipTextBlocked(target)) { hideTip(); return; } const isHeaderActionTip = /^(pk-btn-folder-first|pk-btn-invert|pk-grid-folder-first|pk-grid-invert)$/.test(target.id); if (isHeaderActionTip) { const ov = target.closest('.pk-ov'); if (!ov || (!ov.classList.contains('pk-hide-btn-text') && !ov.classList.contains('pk-auto-hide-btn-text'))) { hideTip(); return; } } if (!isName && !isPath && !isThumb && !isUI && !isForceTip) { if (target.scrollWidth <= target.clientWidth + 1) { hideTip(); return; } } renderTip(target); updatePos({ clientX: lastMouseX, clientY: lastMouseY }); }; return () => { document.removeEventListener('mousemove', onGlobalMouseMove); document.removeEventListener('mouseout', onGlobalMouseOut); window.removeEventListener('contextmenu', onGlobalCtxMenu, true); window.removeEventListener('click', onGlobalClick, true); window.removeEventListener('wheel', onGlobalWheel, { passive: true, capture: true }); delete window.pkRefreshTooltip; if (tipEl && tipEl.parentNode) tipEl.remove(); if (style && style.parentNode) style.remove(); }; })(); const UI = { win: el.querySelector('.pk-win'), vp: el.querySelector('#pk-vp'), in: el.querySelector('#pk-in'), loader: el.querySelector('#pk-loader'), loadTxt: el.querySelector('#pk-load-txt'), stopBtn: el.querySelector('#pk-stop-load'), crumb: el.querySelector('#pk-crumb'), stat: el.querySelector('#pk-stat'), chkAll: el.querySelector('#pk-all'), scan: el.querySelector('#pk-scan-dup'), dupTools: el.querySelector('#pk-dup-tools'), dupFilters: el.querySelector('#pk-dup-filters'), chkName: el.querySelector('#pk-chk-name'), chkSim: el.querySelector('#pk-chk-sim'), chkHash: el.querySelector('#pk-chk-hash'), selDupFolder: el.querySelector('#pk-dup-folder-sel'), offTools: el.querySelector('#pk-offline-tools'), chkOffRun: el.querySelector('#pk-off-run'), chkOffFail: el.querySelector('#pk-off-fail'), chkOffOk: el.querySelector('#pk-off-ok'), upTools: el.querySelector('#pk-upload-tools'), chkUpRun: el.querySelector('#pk-chk-up-run'), chkUpPause: el.querySelector('#pk-chk-up-pause'), chkUpDone: el.querySelector('#pk-chk-up-done'), btnAnalyze: el.querySelector('#pk-analyze'), btnExport: el.querySelector('#pk-export'), btnFolderFirst: el.querySelector('#pk-btn-folder-first'), btnNavHome: el.querySelector('#pk-nav-home'), btnNavOffline: el.querySelector('#pk-nav-offline'), btnNavUpload: el.querySelector('#pk-nav-upload'), btnNavRecent: el.querySelector('#pk-nav-recent'), btnNavHistory: el.querySelector('#pk-nav-history'), btnNavShare: el.querySelector('#pk-nav-share'), btnNavStarred: el.querySelector('#pk-nav-starred'), btnNavTrash: el.querySelector('#pk-nav-trash'), btnNavShareParse: el.querySelector('#pk-nav-share-parse'), btnNavLinkBookmark: el.querySelector('#pk-nav-link-bookmark'), trashBar: el.querySelector('#pk-trash-bar'), btnTrashRefresh: el.querySelector('#pk-trash-refresh'), bottomGrp: el.querySelector('.pk-ft .pk-grp'), actionBar: el.querySelector('#pk-actionbar'), btnRestore: el.querySelector('#pk-restore'), btnDelForever: el.querySelector('#pk-del-forever'), btnEmptyTrash: el.querySelector('#pk-empty-trash'), btnTrashBlacklistManager: el.querySelector('#pk-trash-blacklist-manager'), btnDupSmart: el.querySelector('#pk-dup-smart-btn'), btnDupSort: el.querySelector('#pk-dup-sort-btn'), btnExit: el.querySelector('#pk-btn-exit'), btnCopy: el.querySelector('#pk-copy'), btnCut: el.querySelector('#pk-cut'), btnDel: el.querySelector('#pk-del'), btnClearHistoryAll: el.querySelector('#pk-clear-history-all'), btnDeselect: el.querySelector('#pk-deselect'), btnRename: el.querySelector('#pk-rename'), btnBulkRename: el.querySelector('#pk-bulkrename'), btnPrune: el.querySelector('#pk-prune'), btnUnzip: el.querySelector('#pk-unzip'), btnMigrate: el.querySelector('#pk-migrate'), btnMagnetArchiveCheck: el.querySelector('#pk-magnet-archive-check'), btnExportM3U: el.querySelector('#pk-export-m3u'), 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'), btnShareParseSave: el.querySelector('#pk-share-parse-save'), btnShareParseInsight: el.querySelector('#pk-share-parse-insight'), btnShareParseStopScan: el.querySelector('#pk-share-parse-stop-scan'), btnShareParseBackList: el.querySelector('#pk-share-parse-back-list'), 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'), btnRefresh: el.querySelector('#pk-refresh'), btnNewFolder: el.querySelector('#pk-newfolder'), btnSettings: el.querySelector('#pk-settings'), btnClose: el.querySelector('#pk-close'), btnTransferQuotaDetail: el.querySelector('#pk-transfer-detail-btn'), btnTheme: el.querySelector('#pk-theme'), btnExt: el.querySelector('#pk-ext'), btnImgSearch: el.querySelector('#pk-img-search'), btnAria2: el.querySelector('#pk-aria2'), btnDown: el.querySelector('#pk-down'), pop: el.querySelector('#pk-pop'), ctx: el.querySelector('#pk-ctx'), cols: el.querySelectorAll('.pk-col'), searchInput: el.querySelector('#pk-search-input'), chkGlobal: el.querySelector('#pk-chk-global'), lblGlobal: el.querySelector('#pk-lbl-global'), chkSearchPath: el.querySelector('#pk-chk-search-path'), lblSearchPath: el.querySelector('#pk-search-path-con'), btnAnaSelect: (function() { const parent = el.querySelector('#pk-search-path-con'); const b = document.createElement('button'); b.className = 'pk-ana-select-btn'; b.id = 'pk-ana-select-btn'; b.style.marginRight = '4px'; b.innerHTML = ` ${L.btn_ana_select}`; if (parent) parent.parentNode.insertBefore(b, parent); return b; })(), btnAnaSort: (function() { const parent = el.querySelector('#pk-search-path-con'); const b = document.createElement('button'); b.className = 'pk-ana-select-btn'; b.id = 'pk-ana-sort-btn'; b.innerHTML = ` ${L.btn_in_group_sort}`; if (parent) parent.parentNode.insertBefore(b, parent); return b; })(), topBar: el.querySelector('#pk-top-bar'), searchClear: el.querySelector('#pk-search-clear'), searchBtn: el.querySelector('#pk-search-btn'), searchHist: el.querySelector('#pk-search-hist'), filterBar: el.querySelector('#pk-filter-bar'), filterBtn: el.querySelector('#pk-filter-btn'), filterActiveUI: el.querySelector('#pk-filter-active-ui'), filterCatLabel: el.querySelector('#pk-filter-cat-label'), filterExtsWrap: el.querySelector('#pk-filter-exts-wrap'), filterExtsMain: el.querySelector('#pk-filter-exts-main'), filterExtsMoreBtn: el.querySelector('#pk-filter-exts-more-btn'), filterExitBtn: el.querySelector('#pk-filter-exit-btn') }; (() => { const searchWrap = el.querySelector('.pk-search'); const afterSearch = searchWrap ? searchWrap.nextSibling : null; const topBar = searchWrap && searchWrap.parentNode; if (topBar && UI.btnShareParseInsight) topBar.insertBefore(UI.btnShareParseInsight, afterSearch); if (topBar && UI.btnShareParseBackList) topBar.insertBefore(UI.btnShareParseBackList, afterSearch); })(); const updateDownloaderButtonLabel = () => { if (!UI.btnAria2) return; const type = getCurrentDownloaderType(); const span = UI.btnAria2.querySelector('span'); const iconHtml = type === 'idm' ? CONF.icons.idm : (type === 'abdm' ? CONF.icons.abdm : (type === 'gopeed' ? CONF.icons.gopeed : CONF.icons.aria2)); const oldIcon = UI.btnAria2.querySelector('svg'); if (oldIcon && iconHtml) { oldIcon.outerHTML = iconHtml; } else if (!oldIcon && iconHtml) { UI.btnAria2.insertAdjacentHTML('afterbegin', iconHtml + ' '); } if (type === 'idm') { if (span) span.textContent = L.btn_export_idm; UI.btnAria2.dataset.pkTip = L.tip_export_idm; return; } if (type === 'abdm') { if (span) span.textContent = L.btn_push_abdm; UI.btnAria2.dataset.pkTip = L.tip_push_abdm; return; } if (span) span.textContent = formatDownloaderText(L.btn_send_downloader, type); UI.btnAria2.dataset.pkTip = formatDownloaderText(L.tip_send_downloader, type); }; updateDownloaderButtonLabel(); const isRealHomeUploadView = () => { const path = Array.isArray(S.path) ? S.path : []; const cur = path[path.length - 1] || { id: '' }; const pathStartsAtHome = path.length > 0 && path[0] && path[0].id === ''; const hasVirtualNode = path.some(p => { const id = String((p && p.id) || ''); return id === 'virtual_search_root' || id === 'analyze_root' || id.startsWith('virtual_'); }); return pathStartsAtHome && !S.trashMode && !S.shareMode && !S.shareParseMode && !S.offlineMode && !S.uploadMode && !S.historyMode && !S.recentMode && !S.starredMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.scanning && !S.search && !hasVirtualNode && cur.id !== 'virtual_search_root' && cur.id !== 'analyze_root'; }; const closeLocalUploadMenu = () => { if (typeof window.__pkCloseUploadMenu === 'function') { try { window.__pkCloseUploadMenu(); } catch(e) {} } if (UI.uploadWrap) UI.uploadWrap.classList.remove('active'); const menus = []; if (UI.uploadWrap) { const localMenu = UI.uploadWrap.querySelector('.pk-dropdown-menu'); if (localMenu) menus.push(localMenu); } document.querySelectorAll('.pk-dropdown-menu[data-pk-portal="1"]').forEach(m => menus.push(m)); menus.forEach(menu => { if (!menu) return; if (menu._pkPlaceRaf1) cancelAnimationFrame(menu._pkPlaceRaf1); if (menu._pkPlaceRaf2) cancelAnimationFrame(menu._pkPlaceRaf2); menu._pkPlaceRaf1 = 0; menu._pkPlaceRaf2 = 0; menu.style.display = 'none'; if (menu._pkOriginParent && menu.parentNode !== menu._pkOriginParent) { menu._pkOriginParent.appendChild(menu); } menu.style.position = ''; menu.style.top = ''; menu.style.left = ''; menu.style.right = ''; menu.style.bottom = ''; menu.style.marginTop = ''; menu.style.zIndex = ''; menu.style.minWidth = ''; menu.style.visibility = ''; menu.style.pointerEvents = ''; menu.style.zoom = ''; menu.style.transformOrigin = ''; delete menu.dataset.pkPortal; }); }; const syncLocalUploadVisibility = () => { if (!UI.uploadWrap) return; const shouldShow = isRealHomeUploadView(); UI.uploadWrap.style.display = shouldShow ? 'inline-flex' : 'none'; if (!shouldShow) closeLocalUploadMenu(); }; const getBlacklistCleanKey = (str) => String(str || '').replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const getBlacklistItemName = (item) => { if (!item) return ''; return String(item.name || item.title || '').replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim(); }; const getSelectedBlacklistItems = () => { const selectedIds = S.getSelectedIds(); const items = []; for (const id of selectedIds) { const item = S.itemMap.get(id) || (Array.isArray(S.items) ? S.items.find(x => x && x.id === id) : null) || (Array.isArray(S.display) ? S.display.find(x => x && x.id === id) : null); if (item && !item.isHeader && getBlacklistItemName(item)) items.push(item); } return items; }; const isBlacklistFolderItem = (item) => item && item.kind === 'drive#folder'; const isItemInBlacklist = (item) => { const key = getBlacklistCleanKey(getBlacklistItemName(item)); if (!key) return false; return isBlacklistFolderItem(item) ? S.blFolderSet.has(key) : S.blSet.has(key); }; const getBlacklistContextAction = () => { if (S.updateBlCache) S.updateBlCache(); const items = getSelectedBlacklistItems(); if (items.length === 0) return 'add'; return items.every(isItemInBlacklist) ? 'remove' : 'add'; }; const setBlacklistContextItemState = (el) => { if (!el) return; const action = getBlacklistContextAction(); el.innerHTML = action === 'remove' ? `${ctxIcons.blRem} ${L.ctx_remove_bl}` : `${ctxIcons.blAdd} ${L.ctx_add_bl}`; el.setAttribute('data-action', action); }; const isAnalyzeGroupedRoot = () => { const cur = S.path && S.path.length ? S.path[S.path.length - 1] : null; return !!(S.analyzeMode && cur && cur.id === 'analyze_root' && S.analyzeSimGroups); }; const isGroupedResultMode = () => !!(S.dupMode || isAnalyzeGroupedRoot()); const canUseGridView = () => !S.shareMode && !S.linkBookmarkMode && (!S.shareParseMode || S.shareParseListActive) && !S.offlineMode && !S.uploadMode; const isGridView = () => S.viewMode === 'grid' && canUseGridView(); const getListRowHeight = () => UI.win && UI.win.classList.contains('pk-maximized') ? 60 : 40; const getGridCardHeight = () => UI.win && UI.win.classList.contains('pk-maximized') ? 324 : 286; const getGridCardRadius = () => UI.win && UI.win.classList.contains('pk-maximized') ? 22 : 18; const getGridLayout = () => { const vpRect = getLogicalRect(UI.vp || UI.in); const vpWidth = Math.max(0, Math.floor((vpRect && vpRect.width) || UI.vp?.clientWidth || 0)); const isMax = UI.win && UI.win.classList.contains('pk-maximized'); const padX = isMax ? 20 : 16; const gapX = isMax ? 18 : 16; const gapY = isMax ? 20 : 16; const minCardWidth = isMax ? 236 : 208; const innerWidth = Math.max(minCardWidth, vpWidth - padX * 2); const cols = Math.max(1, Math.floor((innerWidth + gapX) / (minCardWidth + gapX))); const cardWidth = Math.max(188, Math.floor((innerWidth - gapX * (cols - 1)) / cols)); const cardHeight = Math.max(220, CONF.rowHeight - gapY); return { padX, gapX, gapY, cols, cardWidth, cardHeight }; }; const isGroupedGridView = () => !!(isGroupedResultMode() && isGridView()); const getGroupedGridSectionMetrics = () => { const isMax = !!(UI.win && UI.win.classList.contains('pk-maximized')); return { headerHeight: isMax ? CONF.dupGridHeaderHeight.max : CONF.dupGridHeaderHeight.normal, sectionGap: isMax ? CONF.dupGridSectionGap.max : CONF.dupGridSectionGap.normal, bodyGapY: isMax ? CONF.dupGridBodyGapY.max : CONF.dupGridBodyGapY.normal }; }; const getGroupedGridRowStride = (gridLayout) => { const cardHeight = Number(gridLayout && gridLayout.cardHeight) || 0; const gapY = Math.max(0, Number(gridLayout && gridLayout.gapY) || 0); return Math.max(1, cardHeight + gapY); }; const getSectionRowStride = (dupGridMeta, section) => Math.max(1, Number(section && (section.rowStride || section.rowHeight)) || Number(dupGridMeta && dupGridMeta.rowStride) || getGroupedGridRowStride((dupGridMeta && dupGridMeta.gridLayout) || getGridLayout()) ); const getDupGridSectionWindow = (sections, rangeTop, rangeBottom) => { const len = Array.isArray(sections) ? sections.length : 0; if (!len) return { start: 0, end: 0 }; const num = (section, key, fallback) => { const value = Number(section && section[key]); return Number.isFinite(value) ? value : fallback; }; let lo = 0; let hi = len; while (lo < hi) { const mid = (lo + hi) >> 1; if (num(sections[mid], 'bottom', Infinity) < rangeTop) lo = mid + 1; else hi = mid; } const start = lo; lo = 0; hi = len; while (lo < hi) { const mid = (lo + hi) >> 1; if (num(sections[mid], 'top', Infinity) > rangeBottom) hi = mid; else lo = mid + 1; } const end = lo; if (start < end) return { start: Math.max(0, start - 1), end: Math.min(len, end + 1) }; lo = 0; hi = len; while (lo < hi) { const mid = (lo + hi) >> 1; if (num(sections[mid], 'top', Infinity) < rangeTop) lo = mid + 1; else hi = mid; } const center = Math.max(0, Math.min(len - 1, lo)); return { start: Math.max(0, center - 2), end: Math.min(len, center + 3) }; }; const releaseGroupedGridMeta = (meta) => { if (!meta) return; if (meta.indexLayout && typeof meta.indexLayout.clear === 'function') meta.indexLayout.clear(); if (meta.indexToSection && typeof meta.indexToSection.clear === 'function') meta.indexToSection.clear(); if (meta.repeatedIds && typeof meta.repeatedIds.clear === 'function') meta.repeatedIds.clear(); if (Array.isArray(meta.sections)) { for (let i = 0; i < meta.sections.length; i++) { const section = meta.sections[i]; if (!section) continue; if (Array.isArray(section.itemIndices)) section.itemIndices.length = 0; if (Array.isArray(section.itemIds)) section.itemIds.length = 0; } meta.sections.length = 0; } }; const clearGroupedGridMetaCache = () => { releaseGroupedGridMeta(S.dupGridMeta); S.dupGridMeta = null; S.dupGridMetaKey = ''; S.dupGridMetaLayoutKey = ''; S.dupGridMetaDisplayRef = null; S.dupGridMetaDirty = true; }; const buildGroupedGridMetaKey = (baseKey = getGridLayoutKey()) => { const display = Array.isArray(S.display) ? S.display : []; const displayLen = display.length; if (!displayLen) return `${baseKey}::grouped-grid::empty`; let firstId = ''; let lastId = ''; let runSig = ''; let prevRunKey = null; let runCount = 0; for (let i = 0; i < displayLen; i++) { const item = display[i]; if (!item) continue; const itemId = item.id || ''; if (!firstId) firstId = itemId; lastId = itemId || lastId; const runKey = item.isHeader ? `h:${itemId || i}` : `i:${S.dupGroups.has(itemId) ? S.dupGroups.get(itemId) : -1}`; if (runKey !== prevRunKey) { runSig += `${prevRunKey === null ? '' : '|'}${runKey}`; prevRunKey = runKey; runCount++; } } return `${baseKey}::grouped-grid::${displayLen}::${runCount}::${firstId}::${lastId}::${runSig}`; }; const getDupGridShortStableIdPart = (id) => { const raw = String(id || ''); if (raw.length <= 14) return raw; return `${raw.slice(0, 6)}~${raw.slice(-4)}`; }; const getDupGridShortMemberDigest = (ids) => { const arr = Array.isArray(ids) ? ids.filter(Boolean).map(String) : []; if (!arr.length) return ''; let hash = 2166136261; for (let i = 0; i < arr.length; i++) { const s = arr[i]; for (let j = 0; j < s.length; j++) { hash ^= s.charCodeAt(j); hash = Math.imul(hash, 16777619) >>> 0; } hash ^= 124; hash = Math.imul(hash, 16777619) >>> 0; } const first = getDupGridShortStableIdPart(arr[0]); const second = arr.length > 2 ? getDupGridShortStableIdPart(arr[1]) : ''; const last = arr.length > 1 ? getDupGridShortStableIdPart(arr[arr.length - 1]) : ''; return [arr.length, hash.toString(36), first, second, last].join(':'); }; const getDupGridShortTextDigest = (text) => { const raw = String(text || ''); if (!raw) return ''; let hash = 2166136261; for (let i = 0; i < raw.length; i++) { hash ^= raw.charCodeAt(i); hash = Math.imul(hash, 16777619) >>> 0; } return [raw.length, hash.toString(36), raw.slice(0, 24)].join(':'); }; const getGroupedGridMeta = (force = false) => { if (!isGroupedGridView()) { clearGroupedGridMetaCache(); return null; } const display = Array.isArray(S.display) ? S.display : []; const rawLayoutKey = getGridLayoutKey(); const dupCfg = S.dupConfig || {}; const invertChk = typeof document !== 'undefined' ? document.getElementById('pk-dup-invert') : null; const blurScope = typeof getBlurScope === 'function' ? getBlurScope() : ''; const layoutKey = [ rawLayoutKey, getLang(), S.viewMode || '', S.dupMode ? 'dup' : '', isAnalyzeGroupedRoot() ? 'analyzeGrouped' : '', S.sortId || 0, S.scanId || 0, S.dupMode && Array.isArray(S.dupRawGroups) ? S.dupRawGroups.length : 0, S.analyzeMode && Array.isArray(S.analyzeSimGroups) ? S.analyzeSimGroups.length : 0, S.groupSortOp || '', S.groupSortDir || '', S.search || '', UI.chkSearchPath && UI.chkSearchPath.checked ? 'path1' : 'path0', S.pinnedDupPath || '', UI.selDupFolder ? UI.selDupFolder.value || '' : '', UI.chkHash && UI.chkHash.checked ? 'hash1' : 'hash0', UI.chkSim && UI.chkSim.checked ? 'sim1' : 'sim0', UI.chkName && UI.chkName.checked ? 'name1' : 'name0', dupCfg.video ? 'video1' : 'video0', dupCfg.image ? 'image1' : 'image0', dupCfg.other ? 'other1' : 'other0', gmGet('pk_dup_strictness', 'strict'), gmGet('pk_analyze_last_algo', ''), gmGet('pk_analyze_last_sim', ''), invertChk && invertChk.checked ? 'inv1' : 'inv0', blurScope === 'both' || blurScope === 'grid' ? 'blur' : 'clear', blurScope ].join('::'); if (!force && !S.dupGridMetaDirty && S.dupGridMeta && S.dupGridMetaLayoutKey === layoutKey && S.dupGridMetaDisplayRef === display) return S.dupGridMeta; const metaKey = buildGroupedGridMetaKey(layoutKey); const gridLayout = getGridLayout(); const sectionMetrics = getGroupedGridSectionMetrics(); const cols = Math.max(1, gridLayout.cols); const sectionGapY = Math.max(0, gridLayout.gapY || 0); const rowStride = getGroupedGridRowStride(gridLayout); const vpRect = getLogicalRect(UI.vp || UI.in); const fullWidth = Math.max(0, Math.floor((vpRect && vpRect.width) || UI.vp?.clientWidth || 0)); const sections = []; const indexLayout = new Map(); const indexToSection = new Map(); let current = null; let totalHeight = 0; const pushSection = () => { if (!current) return; const itemCount = current.itemIndices.length; const rows = itemCount > 0 ? Math.ceil(itemCount / cols) : 0; const headerTop = totalHeight; const bodyTop = headerTop + sectionMetrics.headerHeight + (itemCount > 0 ? sectionGapY : 0); const bodyHeight = rows * rowStride; const sectionHeight = sectionMetrics.headerHeight + (itemCount > 0 ? (sectionGapY + bodyHeight) : 0); const section = { key: current.key, groupIndex: current.groupIndex, reason: current.reason, title: current.title, headerIndex: current.headerIndex, itemIndices: [...current.itemIndices], itemIds: [...current.itemIds], itemCount, rows, cols, top: headerTop, headerHeight: sectionMetrics.headerHeight, bodyTop, bodyHeight, rowHeight: rowStride, rowStride, totalHeight: sectionHeight, bottom: headerTop + sectionHeight }; if (current.headerIndex !== null) { indexLayout.set(current.headerIndex, { kind: 'header', sectionKey: current.key, top: headerTop, left: 0, width: fullWidth, height: sectionMetrics.headerHeight }); indexToSection.set(current.headerIndex, section); } for (let localIdx = 0; localIdx < current.itemIndices.length; localIdx++) { const displayIndex = current.itemIndices[localIdx]; const rowIdx = Math.floor(localIdx / cols); const colIdx = localIdx % cols; indexLayout.set(displayIndex, { kind: 'item', sectionKey: current.key, localIndex: localIdx, rowIdx, colIdx, top: bodyTop + rowIdx * rowStride, left: gridLayout.padX + colIdx * (gridLayout.cardWidth + gridLayout.gapX), width: gridLayout.cardWidth, height: gridLayout.cardHeight }); indexToSection.set(displayIndex, section); } sections.push(section); totalHeight += sectionHeight; current = null; }; for (let i = 0; i < display.length; i++) { const d = display[i]; if (!d) continue; if (d.isHeader) { pushSection(); const parsedGroupIndex = /^grp_\d+$/.test(d.id || '') ? parseInt(String(d.id).replace('grp_', ''), 10) : -1; current = { key: d.id || `grp_auto_${i}`, headerIndex: i, groupIndex: parsedGroupIndex, reason: d.type || '', title: d.name || '', itemIndices: [], itemIds: [] }; continue; } if (!current) { current = { key: `loose_${i}`, headerIndex: null, groupIndex: S.dupGroups.has(d.id) ? S.dupGroups.get(d.id) : -1, reason: S.dupReasons.get(d.id) || '', title: '', itemIndices: [], itemIds: [] }; } current.itemIndices.push(i); if (d.id) current.itemIds.push(d.id); } pushSection(); if (sections.length > 0) totalHeight = Math.max(0, totalHeight); const repeatedIds = new Set(); const seenIds = new Set(); const firstSectionById = new Map(); for (const section of sections) { section.memberDigest = getDupGridShortMemberDigest(section.itemIds); section.groupReasonDigest = getDupGridShortTextDigest(section.reason || ''); section.groupTitleDigest = getDupGridShortTextDigest(section.title || ''); for (let localIdx = 0; localIdx < section.itemIds.length; localIdx++) { const itemId = String(section.itemIds[localIdx] || ''); if (!itemId) continue; if (seenIds.has(itemId)) repeatedIds.add(itemId); seenIds.add(itemId); if (firstSectionById.has(itemId) && firstSectionById.get(itemId) !== section.key) repeatedIds.add(itemId); else firstSectionById.set(itemId, section.key); } } const stableScope = S.dupMode ? 'dup-file' : (isAnalyzeGroupedRoot() ? 'dup-folder' : ''); if (stableScope) { for (const section of sections) { if (!section || section.headerIndex === null || !Array.isArray(section.itemIds) || !section.itemIds.length || !section.memberDigest) continue; const header = display[section.headerIndex] || null; const groupSource = stableScope === 'dup-folder' && Array.isArray(S.analyzeSimGroups) ? S.analyzeSimGroups[section.groupIndex] : null; const rawGroupReason = String(section.reason || (header && header.type) || ''); const rawGroupTitle = String(section.title || (header && header.name) || ''); const groupReasonDigest = section.groupReasonDigest || getDupGridShortTextDigest(rawGroupReason); const groupTitleDigest = section.groupTitleDigest || getDupGridShortTextDigest(rawGroupTitle); const groupSig = [ `dupSection:${section.key || ''}`, `dupGroup:${section.groupIndex}`, `dupType:${groupReasonDigest}`, `dupTitle:${groupTitleDigest}`, `dupGroupSim:${groupSource && groupSource._sim !== undefined ? String(groupSource._sim) : ''}`, `dupMembers:${section.memberDigest}` ].join('|'); section.dupGroupSig = groupSig; } } const nextMeta = { key: metaKey, cols, gridLayout, sectionMetrics, fullWidth, rowHeight: rowStride, rowStride, totalHeight: Math.max(0, totalHeight), sections, indexLayout, indexToSection, repeatedIds }; releaseGroupedGridMeta(S.dupGridMeta); S.dupGridMetaKey = metaKey; S.dupGridMetaLayoutKey = layoutKey; S.dupGridMetaDisplayRef = display; S.dupGridMetaDirty = false; S.dupGridMeta = nextMeta; return S.dupGridMeta; }; const findDupGridHitIndex = (logicalOffsetX, logicalOffsetY) => { if (!isGroupedGridView()) return -1; const dupGridMeta = getGroupedGridMeta(); if (!dupGridMeta || !dupGridMeta.sections || !dupGridMeta.sections.length) return -1; const gridLayout = dupGridMeta.gridLayout || getGridLayout(); const cols = Math.max(1, dupGridMeta.cols || gridLayout.cols || 1); if (logicalOffsetX < gridLayout.padX || logicalOffsetY < 0) return -1; const colStride = gridLayout.cardWidth + gridLayout.gapX; const relX = logicalOffsetX - gridLayout.padX; const colIdx = Math.floor(relX / Math.max(1, colStride)); const withinColX = relX - colIdx * colStride; if (colIdx < 0 || colIdx >= cols || withinColX < 0 || withinColX > gridLayout.cardWidth) return -1; for (const section of dupGridMeta.sections) { if (logicalOffsetY < section.top) break; if (logicalOffsetY >= section.bottom) continue; if (logicalOffsetY < section.bodyTop || logicalOffsetY >= section.bodyTop + section.bodyHeight) return -1; const rowStride = getSectionRowStride(dupGridMeta, section); const relY = logicalOffsetY - section.bodyTop; const rowIdx = Math.floor(relY / rowStride); const withinRowY = relY - rowIdx * rowStride; if (rowIdx < 0 || rowIdx >= section.rows || withinRowY < 0 || withinRowY > gridLayout.cardHeight) return -1; const localIdx = rowIdx * Math.max(1, section.cols || cols) + colIdx; if (localIdx < 0 || localIdx >= section.itemIndices.length) return -1; return section.itemIndices[localIdx]; } return -1; }; const getDupGridAnchorIndex = (logicalCenterX, logicalCenterY) => { if (!isGroupedGridView()) return -1; const dupGridMeta = getGroupedGridMeta(); if (!dupGridMeta) return -1; const hitIdx = findDupGridHitIndex(logicalCenterX, logicalCenterY); if (hitIdx >= 0) return hitIdx; const sections = dupGridMeta.sections || []; if (!sections.length) return -1; const findSectionIndex = () => { let lo = 0; let hi = sections.length - 1; while (lo <= hi) { const mid = (lo + hi) >> 1; const section = sections[mid]; if (logicalCenterY < section.top) { hi = mid - 1; } else if (logicalCenterY >= section.bottom) { lo = mid + 1; } else { return mid; } } const rightIdx = Math.max(0, Math.min(sections.length - 1, lo)); const leftIdx = Math.max(0, rightIdx - 1); const left = sections[leftIdx]; const right = sections[rightIdx]; const leftDist = left ? Math.min(Math.abs(logicalCenterY - left.top), Math.abs(logicalCenterY - left.bottom)) : Infinity; const rightDist = right ? Math.min(Math.abs(logicalCenterY - right.top), Math.abs(logicalCenterY - right.bottom)) : Infinity; return rightDist < leftDist ? rightIdx : leftIdx; }; const section = sections[findSectionIndex()]; const itemIndices = section && Array.isArray(section.itemIndices) ? section.itemIndices : []; if (!section || !itemIndices.length) return -1; const gridLayout = dupGridMeta.gridLayout || getGridLayout(); const cols = Math.max(1, section.cols || dupGridMeta.cols || gridLayout.cols || 1); const rowStride = getSectionRowStride(dupGridMeta, section); const rows = Math.max(1, section.rows || Math.ceil(itemIndices.length / cols)); const colStride = Math.max(1, gridLayout.cardWidth + gridLayout.gapX); const approxCol = Math.max(0, Math.min(cols - 1, Math.round((logicalCenterX - gridLayout.padX - gridLayout.cardWidth / 2) / colStride))); const bodyTop = Number(section.bodyTop) || section.top || 0; const bodyBottom = bodyTop + Math.max(0, section.bodyHeight || rows * rowStride); let approxRow = 0; if (logicalCenterY >= bodyBottom) { approxRow = rows - 1; } else if (logicalCenterY > bodyTop) { approxRow = Math.floor((logicalCenterY - bodyTop) / rowStride); } approxRow = Math.max(0, Math.min(rows - 1, approxRow)); let bestIdx = -1; let bestScore = Infinity; const rowStart = Math.max(0, approxRow - 2); const rowEnd = Math.min(rows - 1, approxRow + 2); const colStart = Math.max(0, approxCol - 2); const colEnd = Math.min(cols - 1, approxCol + 2); for (let row = rowStart; row <= rowEnd; row++) { for (let col = colStart; col <= colEnd; col++) { const localIdx = row * cols + col; if (localIdx < 0 || localIdx >= itemIndices.length) continue; const idx = itemIndices[localIdx]; const layout = dupGridMeta.indexLayout ? dupGridMeta.indexLayout.get(idx) : null; const left = layout ? layout.left : gridLayout.padX + col * colStride; const top = layout ? layout.top : bodyTop + row * rowStride; const width = layout ? layout.width : gridLayout.cardWidth; const height = layout ? layout.height : gridLayout.cardHeight; const cx = left + (width / 2); const cy = top + (height / 2); const score = Math.abs(cy - logicalCenterY) * 4 + Math.abs(cx - logicalCenterX); if (score < bestScore) { bestScore = score; bestIdx = idx; } } } if (bestIdx >= 0) return bestIdx; if (logicalCenterY <= bodyTop) return itemIndices[0] || -1; if (logicalCenterY >= bodyBottom) return itemIndices[itemIndices.length - 1] || -1; const boundaryLocalIdx = Math.max(0, Math.min(itemIndices.length - 1, approxRow * cols + approxCol)); return itemIndices[boundaryLocalIdx] || -1; }; const getItemScrollTopByIndex = (targetIdx) => { if (targetIdx < 0) return 0; if (isGridView()) { if (isGroupedGridView()) { const dupGridMeta = getGroupedGridMeta(); const layout = dupGridMeta && dupGridMeta.indexLayout ? dupGridMeta.indexLayout.get(targetIdx) : null; if (layout) return Math.max(0, layout.top); const section = dupGridMeta && dupGridMeta.indexToSection ? dupGridMeta.indexToSection.get(targetIdx) : null; if (section) { if (section.headerIndex === targetIdx) return Math.max(0, section.top || 0); const localIdx = Array.isArray(section.itemIndices) ? section.itemIndices.indexOf(targetIdx) : -1; if (localIdx >= 0) { const cols = Math.max(1, section.cols || dupGridMeta.cols || 1); const rowIdx = Math.floor(localIdx / cols); return Math.max(0, (section.bodyTop || 0) + rowIdx * getSectionRowStride(dupGridMeta, section)); } } } const gridLayout = getGridLayout(); const visualRow = Math.floor(targetIdx / Math.max(1, gridLayout.cols)); return gridLayout.gapY + visualRow * getGroupedGridRowStride(gridLayout); } return targetIdx * CONF.rowHeight; }; const getViewportAnchorId = (preferSelection = false) => { const selectedIds = S.getSelectedIds(); const fallbackId = S.activeId || (selectedIds.length ? selectedIds[selectedIds.length - 1] : null); if (preferSelection && fallbackId && S.display.some(x => x.id === fallbackId)) return fallbackId; if (!UI.vp || !S.display.length) return fallbackId; const centerY = UI.vp.scrollTop + (UI.vp.clientHeight / 2); if (isGridView()) { const gridLayout = getGridLayout(); if (isGroupedGridView()) { const usableWidth = Math.max(1, UI.vp.clientWidth - gridLayout.padX * 2); const logicalCenterX = gridLayout.padX + (usableWidth / 2); const targetIdx = getDupGridAnchorIndex(logicalCenterX, centerY); if (targetIdx >= 0 && targetIdx < S.display.length) { return S.display[targetIdx]?.id || fallbackId; } return fallbackId; } const cols = Math.max(1, gridLayout.cols); const totalRows = Math.max(1, Math.ceil(S.display.length / cols)); const adjustedCenterY = Math.max(0, centerY - gridLayout.gapY); const visualRow = Math.max(0, Math.min(totalRows - 1, Math.floor(adjustedCenterY / Math.max(1, CONF.rowHeight)))); const usableWidth = Math.max(1, UI.vp.clientWidth - gridLayout.padX * 2); const centerX = usableWidth / 2; const visualCol = Math.max(0, Math.min(cols - 1, Math.floor(centerX / Math.max(1, gridLayout.cardWidth + gridLayout.gapX)))); const targetIdx = Math.min(S.display.length - 1, visualRow * cols + visualCol); return S.display[targetIdx]?.id || fallbackId; } const targetIdx = Math.max(0, Math.min(S.display.length - 1, Math.floor(centerY / Math.max(1, CONF.rowHeight)))); return S.display[targetIdx]?.id || fallbackId; }; const syncLayoutMetrics = () => { CONF.rowHeight = isGridView() ? getGridCardHeight() : getListRowHeight(); }; const getGridLayoutKey = () => { const vpRect = getLogicalRect(UI.vp || UI.in); const vpWidth = Math.max(0, Math.floor((vpRect && vpRect.width) || UI.vp?.clientWidth || 0)); const vpHeight = Math.max(0, Math.floor(UI.vp?.clientHeight || 0)); const gridLayout = getGridLayout(); const isMax = UI.win && UI.win.classList.contains('pk-maximized') ? 1 : 0; return [vpWidth, vpHeight, gridLayout.cols, gridLayout.cardWidth, gridLayout.cardHeight, isMax].join(':'); }; const clearGridStableRows = (container) => { if (!container) return; if (container._pkGridRowMap) container._pkGridRowMap.clear(); container._pkGridRowMapKey = ''; container._pkGridStableRowMode = false; S._dupGridRepeatedIdKey = ''; S._dupGridRepeatedIds = null; clearGroupedGridMetaCache(); }; const resetViewportRenderState = () => { if (S._gridRelayoutRaf1) { cancelAnimationFrame(S._gridRelayoutRaf1); S._gridRelayoutRaf1 = 0; } if (S._gridRelayoutRaf2) { cancelAnimationFrame(S._gridRelayoutRaf2); S._gridRelayoutRaf2 = 0; } if (S.dupGridScrollRaf) { cancelAnimationFrame(S.dupGridScrollRaf); S.dupGridScrollRaf = 0; } if (S.dupGridScrollEndTimer) { clearTimeout(S.dupGridScrollEndTimer); S.dupGridScrollEndTimer = 0; } S.dupGridScrollDirty = false; S.dupGridLastScrollTop = 0; S.dupGridLastRenderedTop = -1; if (!UI.in) return; clearGridStableRows(UI.in); const pool = UI.in._pkVisibleRowPool || []; for (let i = 0; i < pool.length; i++) { const row = pool[i]; if (!row) continue; if (typeof resetPooledRow === 'function') resetPooledRow(row); if (row.parentNode === UI.in) UI.in.removeChild(row); } if (typeof cleanupNonPooledChildren === 'function') { cleanupNonPooledChildren(UI.in); } }; const beginFolderViewSync = () => { if (S._folderViewSyncing) return; S._folderViewSyncing = true; resetViewportRenderState(); if (UI.vp) { UI.vp.style.visibility = 'hidden'; UI.vp.style.pointerEvents = 'none'; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = 'hidden'; }; const endFolderViewSync = (force = false) => { if (!S._folderViewSyncing) return; if (S._folderViewSyncHold && !force) return; requestAnimationFrame(() => { if (S._folderViewSyncHold && !force) return; if (UI.vp) { UI.vp.style.visibility = ''; UI.vp.style.pointerEvents = ''; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = ''; S._folderViewSyncing = false; }); }; const beginShareParseGridExitSync = () => { S._shareParseGridExitSync = true; if (UI.win) UI.win.classList.add('pk-share-grid-exit-sync', 'pk-view-switching', 'pk-mode-view-syncing'); if (UI.vp) { UI.vp.style.visibility = 'hidden'; UI.vp.style.pointerEvents = 'none'; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = 'hidden'; }; const endShareParseGridExitSync = () => { if (!S._shareParseGridExitSync) return; requestAnimationFrame(() => { requestAnimationFrame(() => { S._shareParseGridExitSync = false; if (UI.win) UI.win.classList.remove('pk-share-grid-exit-sync', 'pk-view-switching', 'pk-mode-view-syncing'); if (S._folderViewSyncing && !S._folderViewSyncHold) { endFolderViewSync(true); return; } if (UI.vp) { UI.vp.style.visibility = ''; UI.vp.style.pointerEvents = ''; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = ''; }); }); }; const beginLinkBookmarkGridExitSync = () => { S._linkBookmarkGridExitSync = true; if (UI.win) UI.win.classList.add('pk-link-bookmark-grid-exit-sync', 'pk-view-switching', 'pk-mode-view-syncing'); if (UI.vp) { UI.vp.style.visibility = 'hidden'; UI.vp.style.pointerEvents = 'none'; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = 'hidden'; }; const endLinkBookmarkGridExitSync = () => { if (!S._linkBookmarkGridExitSync) return; requestAnimationFrame(() => { requestAnimationFrame(() => { S._linkBookmarkGridExitSync = false; if (UI.win) UI.win.classList.remove('pk-link-bookmark-grid-exit-sync', 'pk-view-switching', 'pk-mode-view-syncing'); if (S._folderViewSyncing && !S._folderViewSyncHold) { endFolderViewSync(true); return; } if (UI.vp) { UI.vp.style.visibility = ''; UI.vp.style.pointerEvents = ''; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = ''; }); }); }; function ensureStaticPanelViewportVisible() { if (S._shareParseGridExitSync) S._shareParseGridExitSync = false; if (S._linkBookmarkGridExitSync) S._linkBookmarkGridExitSync = false; if (S._folderViewSyncing) S._folderViewSyncing = false; if (UI.win) UI.win.classList.remove('pk-share-grid-exit-sync', 'pk-link-bookmark-grid-exit-sync', 'pk-view-switching', 'pk-mode-view-syncing'); if (UI.vp) { UI.vp.style.visibility = ''; UI.vp.style.pointerEvents = ''; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = ''; } const scheduleGridRelayout = (forceFull = false) => { if (!isGridView()) { endFolderViewSync(); return; } if (S._gridRelayoutRaf1) cancelAnimationFrame(S._gridRelayoutRaf1); if (S._gridRelayoutRaf2) cancelAnimationFrame(S._gridRelayoutRaf2); const prevKey = S._gridLayoutKey || ''; S._gridRelayoutRaf1 = requestAnimationFrame(() => { S._gridRelayoutRaf2 = requestAnimationFrame(() => { if (!UI.vp || !UI.in || !isGridView()) { endFolderViewSync(); return; } const nextKey = getGridLayoutKey(); if (forceFull || nextKey !== prevKey) { S._gridLayoutKey = nextKey; renderList(); } else if (typeof renderVisible === 'function') { renderVisible(); endFolderViewSync(); } else { endFolderViewSync(); } }); }); }; const ensureGridMediaStore = () => { if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); if (!window.pkThumbTriState) window.pkThumbTriState = { folder: Object.create(null), file: Object.create(null) }; if (!window.pkThumbTriState.folder) window.pkThumbTriState.folder = Object.create(null); if (!window.pkThumbTriState.file) window.pkThumbTriState.file = Object.create(null); if (!window.pkGridMediaStore) window.pkGridMediaStore = { folder: Object.create(null), file: Object.create(null) }; if (!window.pkGridMediaStore.folder) window.pkGridMediaStore.folder = Object.create(null); if (!window.pkGridMediaStore.file) window.pkGridMediaStore.file = Object.create(null); return window.pkGridMediaStore; }; const getStableFolderMediaState = (item) => { ensureGridMediaStore(); const tri = window.pkThumbTriState.folder; const store = window.pkGridMediaStore.folder; const id = item && item.id; if (!id) return { state: 'none', src: '' }; const state = tri[id] || 'unknown'; const cached = store[id] || ''; if (state === 'ok' && cached) return { state: 'ok', src: cached }; if (state === 'ok' && item.thumbnail_link) { store[id] = item.thumbnail_link; return { state: 'ok', src: item.thumbnail_link }; } if ((state === 'unknown' || state === 'probing') && cached) return { state: 'ok', src: cached }; return { state, src: '' }; }; const markStableFolderMediaState = (id, state, src = '') => { ensureGridMediaStore(); if (!id) return; window.pkThumbTriState.folder[id] = state; if (state === 'ok' && src) { window.pkGridMediaStore.folder[id] = src; window.pkGlobalThumbCache.add(id); } else if (state === 'fail' && window.pkGridMediaStore.folder[id]) { delete window.pkGridMediaStore.folder[id]; } }; const getStableFileMediaState = (item) => { ensureGridMediaStore(); const tri = window.pkThumbTriState.file; const store = window.pkGridMediaStore.file; const id = item && item.id; if (!id) return { state: 'none', src: '' }; const state = tri[id] || 'unknown'; const cached = store[id] || ''; if (state === 'ok' && cached) return { state: 'ok', src: cached }; if (state === 'ok' && item.thumbnail_link) { store[id] = item.thumbnail_link; return { state: 'ok', src: item.thumbnail_link }; } if ((state === 'unknown' || state === 'probing') && cached) return { state: 'ok', src: cached }; return { state, src: '' }; }; const markStableFileMediaState = (id, state, src = '') => { ensureGridMediaStore(); if (!id) return; window.pkThumbTriState.file[id] = state; if (state === 'ok' && src) { window.pkGridMediaStore.file[id] = src; window.pkGlobalThumbCache.add(id); } else if (state === 'fail' && window.pkGridMediaStore.file[id]) { delete window.pkGridMediaStore.file[id]; } }; window.ensureGridMediaStore = ensureGridMediaStore; window.getStableFolderMediaState = getStableFolderMediaState; window.markStableFolderMediaState = markStableFolderMediaState; window.getStableFileMediaState = getStableFileMediaState; window.markStableFileMediaState = markStableFileMediaState; const ensureGridMediaDomCache = () => { if (!window.pkGridMediaDomCache) window.pkGridMediaDomCache = { folder: Object.create(null), file: Object.create(null) }; return window.pkGridMediaDomCache; }; const isTrustedChildRealThumb = (child) => { if (!child || !child.thumbnail_link || child.thumbnail_link === child.icon_link) return false; const mime = (child.mime_type || '').toLowerCase(); const duration = Number((child.params && child.params.duration) || (child.medias && child.medias[0] && child.medias[0].duration) || 0); const hasMediaMeta = Array.isArray(child.medias) && child.medias.length > 0; const stableState = child.id && typeof getStableFileMediaState === 'function' ? getStableFileMediaState(child).state : 'unknown'; return stableState === 'ok' || mime.startsWith('video/') || mime.startsWith('image/') || duration > 0 || hasMediaMeta; }; const getFolderEmbedCacheStamp = (item) => { if (!item || item.kind !== 'drive#folder') return ''; if (typeof globalCache === 'undefined') return 'nogc'; const normalize = (data) => (data && !Array.isArray(data) && data.items) ? data.items : data; const raw = globalCache.get(item.id); if (!raw) return 'miss'; if (!Array.isArray(raw) && raw.nextToken) { const pending = normalize(raw); const len = Array.isArray(pending) ? pending.filter(Boolean).length : 0; return `loading:${len}`; } const children = normalize(raw); if (!Array.isArray(children)) return 'invalid'; const visibleChildren = children.filter(Boolean); if (!visibleChildren.length) return 'empty'; const nonFolders = visibleChildren.filter(child => child.kind !== 'drive#folder'); if (!nonFolders.length) { return `folders:${visibleChildren.length}:${visibleChildren.slice(0, 3).map(child => child.id || '').join(',')}`; } const hasRealChildThumb = (child) => isTrustedChildRealThumb(child); const isFallbackPlaceholderChild = (child) => { const mime = (child.mime_type || '').toLowerCase(); const duration = (child.params && child.params.duration) || 0; return !hasRealChildThumb(child) && (mime.startsWith('video/') || mime.startsWith('image/') || duration > 0); }; const realThumbChild = nonFolders.find(hasRealChildThumb); if (realThumbChild) { const stableChildMedia = typeof getStableFileMediaState === 'function' ? getStableFileMediaState(realThumbChild) : { state: 'unknown', src: '' }; const realThumbSrc = stableChildMedia.src || realThumbChild.thumbnail_link || ''; return ['real', realThumbChild.id || '', realThumbSrc || '', realThumbChild.icon_link || '', realThumbChild.mime_type || '', realThumbChild.name || ''].join(':'); } if (nonFolders.every(isFallbackPlaceholderChild)) { const target = nonFolders[0] || {}; return ['fallback', target.id || '', target.icon_link || '', target.thumbnail_link || '', target.mime_type || '', target.name || ''].join(':'); } const ordinaryPlaceholderChild = nonFolders.find(child => !hasRealChildThumb(child) && !isFallbackPlaceholderChild(child)); if (ordinaryPlaceholderChild) { return ['ordinary', ordinaryPlaceholderChild.id || '', ordinaryPlaceholderChild.icon_link || '', ordinaryPlaceholderChild.thumbnail_link || '', ordinaryPlaceholderChild.mime_type || '', ordinaryPlaceholderChild.name || ''].join(':'); } return `blank:${visibleChildren.length}:${nonFolders.length}`; }; const makeGridMediaCacheKey = (item) => [item?.id || '', item?.kind || '', item?.thumbnail_link || '', item?.icon_link || '', getFolderEmbedCacheStamp(item), window.pkGridMediaRetryStamp || '0'].join('::'); const stashFrozenGridMediaNode = (itemId, mediaKey, node, kind = '') => { if (!itemId || !mediaKey || !node) return; const cache = ensureGridMediaDomCache(); const bucket = kind === 'drive#folder' ? cache.folder : cache.file; bucket[itemId] = { key: mediaKey, node }; }; const takeFrozenGridMediaNode = (item) => { if (!item || !item.id) return null; const cache = ensureGridMediaDomCache(); const bucket = item.kind === 'drive#folder' ? cache.folder : cache.file; const hit = bucket[item.id]; const mediaKey = makeGridMediaCacheKey(item); if (!hit || hit.key !== mediaKey || !hit.node) return null; delete bucket[item.id]; return hit.node; }; window.ensureGridMediaDomCache = ensureGridMediaDomCache; window.stashFrozenGridMediaNode = stashFrozenGridMediaNode; window.takeFrozenGridMediaNode = takeFrozenGridMediaNode; function resetDirectoryGridMediaFallbackState(reason = '') { window.pkGridMediaRetryStamp = Date.now(); window.pkDirIconRetryStamp = window.pkGridMediaRetryStamp; if (window.pkThumbTriState) { ['folder', 'file'].forEach(type => { const bucket = window.pkThumbTriState[type]; if (!bucket) return; Object.keys(bucket).forEach(id => { if (bucket[id] === 'fail' || bucket[id] === 'probing') delete bucket[id]; }); }); } window.pkGridMediaDomCache = { folder: Object.create(null), file: Object.create(null) }; try { if (typeof getPreviewIconFailCache === 'function') getPreviewIconFailCache().clear(); } catch (e) {} try { if (UI && UI.win) { UI.win.querySelectorAll('.pk-gv-media-mount').forEach(mount => { if (mount && mount.dataset) delete mount.dataset.pkMediaKey; }); } } catch (e) {} requestAnimationFrame(() => { try { if (typeof refresh === 'function') Promise.resolve(refresh()).catch(() => {}); else if (typeof renderVisible === 'function') renderVisible(); } catch (e) {} }); } window.resetDirectoryGridMediaFallbackState = resetDirectoryGridMediaFallbackState; const buildGridFolderPreview = (folderItem, folderIconHtml) => { const hasThumb = !!(folderItem.thumbnail_link && folderItem.thumbnail_link !== folderItem.icon_link); const stableFolderMedia = hasThumb ? getStableFolderMediaState(folderItem) : { state: 'none', src: '' }; const folderThumbState = stableFolderMedia.state; const folderResolvedSrc = stableFolderMedia.src || folderItem.thumbnail_link || ''; const folderHasCoverThumb = hasThumb && folderThumbState === 'ok' && !!folderResolvedSrc; if (hasThumb && folderThumbState === 'unknown' && !folderResolvedSrc && folderItem.id && window.pkThumbTriState && window.pkThumbTriState.folder) { window.pkThumbTriState.folder[folderItem.id] = 'probing'; } const makePreviewBaseHtml = (display = 'flex', opacity = '1', iconSrc = folderItem.icon_link, svgHtml = folderIconHtml) => { const hasRemote = !!iconSrc; const showFallbackFirst = !hasRemote || (typeof navigator !== 'undefined' && navigator.onLine === false); const remoteHtml = hasRemote ? `` : ''; return `${svgHtml}${remoteHtml}`; }; const makeItemPlaceholderHtml = (child) => { const childIconHtml = getOfficialDirFallbackIconHtml(child, 'grid'); return makePreviewBaseHtml('flex', '1', child.icon_link, childIconHtml); }; const resolveCachedPreview = () => { const normalize = (data) => (data && !Array.isArray(data) && data.items) ? data.items : data; if (typeof globalCache === 'undefined') return { decided: false, hasThumb: false, hidePreview: false, html: '' }; const raw = globalCache.get(folderItem.id); if (!raw) return { decided: false, hasThumb: false, hidePreview: false, html: '' }; if (!Array.isArray(raw) && raw.nextToken) { return { decided: false, hasThumb: false, hidePreview: false, html: '' }; } const children = normalize(raw); if (!Array.isArray(children)) return { decided: false, hasThumb: false, hidePreview: false, html: '' }; if (children.length === 0) return { decided: true, hasThumb: false, hidePreview: true, html: '' }; const visibleChildren = children.filter(Boolean); if (!visibleChildren.length) return { decided: true, hasThumb: false, hidePreview: true, html: '' }; const nonFolders = visibleChildren.filter(child => child.kind !== 'drive#folder'); if (!nonFolders.length) { return { decided: true, hasThumb: false, hidePreview: false, html: makePreviewBaseHtml('flex', '1') }; } const hasRealChildThumb = (child) => isTrustedChildRealThumb(child); const isFallbackPlaceholderChild = (child) => { const mime = (child.mime_type || '').toLowerCase(); const duration = (child.params && child.params.duration) || 0; return !hasRealChildThumb(child) && (mime.startsWith('video/') || mime.startsWith('image/') || duration > 0); }; const makeChildRealThumbHtml = (child) => { const stableChildMedia = typeof getStableFileMediaState === 'function' ? getStableFileMediaState(child) : { state: 'unknown', src: '' }; const childThumbState = stableChildMedia.state; const childResolvedSrc = stableChildMedia.src || child.thumbnail_link || ''; if (!childResolvedSrc || childThumbState === 'fail') return ''; if (childThumbState === 'unknown' && child.id && window.pkThumbTriState && window.pkThumbTriState.file) { window.pkThumbTriState.file[child.id] = 'probing'; } const imgOpacity = childThumbState === 'ok' ? '1' : '0'; const baseOpacity = childThumbState === 'ok' ? '0' : '1'; return `
${makePreviewBaseHtml('flex', baseOpacity, folderItem.icon_link, folderIconHtml)}
`; }; const realThumbChild = nonFolders.find(hasRealChildThumb); if (realThumbChild) { const realThumbHtml = makeChildRealThumbHtml(realThumbChild); if (realThumbHtml) { return { decided: true, hasThumb: true, hidePreview: false, html: realThumbHtml }; } } if (nonFolders.every(isFallbackPlaceholderChild)) { return { decided: true, hasThumb: false, hidePreview: false, html: makeItemPlaceholderHtml(nonFolders[0]) }; } const ordinaryPlaceholderChild = nonFolders.find(child => !hasRealChildThumb(child) && !isFallbackPlaceholderChild(child)); if (ordinaryPlaceholderChild) { return { decided: true, hasThumb: false, hidePreview: false, html: makeItemPlaceholderHtml(ordinaryPlaceholderChild) }; } return { decided: true, hasThumb: false, hidePreview: false, html: '' }; }; if (folderHasCoverThumb) { return { hasThumb: true, hidePreview: false, html: `${makePreviewBaseHtml('none', '1')}` }; } const cachedPreview = resolveCachedPreview(); const probeBasePreview = cachedPreview.decided ? cachedPreview : { hasThumb: false, hidePreview: false, html: '' }; if (hasThumb && folderThumbState !== 'fail') { return { hasThumb: false, hidePreview: false, html: `${probeBasePreview.html}` }; } if (cachedPreview.decided) { return { hasThumb: cachedPreview.hasThumb, hidePreview: cachedPreview.hidePreview, html: cachedPreview.html }; } return { hasThumb: false, hidePreview: false, html: '' }; }; const renderFrozenGridMedia = (item) => { const iconFallback = getOfficialDirFallbackIconHtml(item, 'grid'); const isFolder = item.kind === 'drive#folder'; const hasThumb = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); if (isFolder) { const folderEmbed = buildGridFolderPreview(item, iconFallback); return `
`; } const makeFileFallbackHtml = (opacity = '1') => { const hasRemoteIcon = !!item.icon_link; const showFallbackFirst = !hasRemoteIcon || (typeof navigator !== 'undefined' && navigator.onLine === false); const remoteHtml = hasRemoteIcon ? `` : ''; return `
${iconFallback}${remoteHtml}
`; }; if (hasThumb) { const stableFileMedia = getStableFileMediaState(item); const fileThumbState = stableFileMedia.state; const fileResolvedSrc = stableFileMedia.src || item.thumbnail_link || ''; if (fileThumbState === 'fail' || !fileResolvedSrc) { return makeFileFallbackHtml(); } if (fileThumbState === 'unknown') { window.pkThumbTriState.file[item.id] = 'probing'; } const imgOpacity = fileThumbState === 'ok' ? '1' : '0'; const baseOpacity = fileThumbState === 'ok' ? '0' : '1'; return `
${makeFileFallbackHtml(baseOpacity)}
`; } return makeFileFallbackHtml(); }; const isGridVideoItem = (item) => { const mime = ((item && item.mime_type) || '').toLowerCase(); const duration = Number((item && item.params && item.params.duration) || (item && item.medias && item.medias[0] && item.medias[0].duration) || 0); return mime.startsWith('video/') || duration > 0; }; const syncGridVideoPlayState = (scope, item = null) => { const root = scope && scope.nodeType === 1 ? (scope.matches('.pk-grid-card-row, .pk-row') ? scope : (scope.closest('.pk-grid-card-row, .pk-row') || scope)) : null; if (!root) return; const mount = root.querySelector('.pk-gv-media-mount'); const play = root.querySelector('.pk-gv-play'); if (!play) return; const isVideo = item ? isGridVideoItem(item) : !!(mount && mount.dataset && mount.dataset.pkIsVideo === '1'); const hasThumb = item ? !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link) : !!(mount && mount.dataset && mount.dataset.pkHasThumb === '1'); if (!mount || !isVideo || !hasThumb) { play.style.display = 'none'; return; } const wrap = mount.firstElementChild; const thumb = mount.querySelector('img.pk-max-thumb'); const wrapperReady = !!(wrap && wrap.dataset && wrap.dataset.pkThumbReady === '1'); const imgReady = !!(thumb && thumb.complete && (thumb.naturalWidth > 1 || thumb.naturalHeight > 1)); play.style.display = (wrapperReady || imgReady) ? 'flex' : 'none'; }; window.syncGridVideoPlayState = syncGridVideoPlayState; const patchFrozenGridMedia = (row, item, prevMediaNode = null, prevMediaKey = '') => { const mount = row.querySelector('.pk-gv-media-mount'); if (!mount || !item) return; const mediaKey = makeGridMediaCacheKey(item); mount.dataset.pkIsVideo = isGridVideoItem(item) ? '1' : '0'; mount.dataset.pkHasThumb = (item.thumbnail_link && item.thumbnail_link !== item.icon_link) ? '1' : '0'; const syncGridVideoPlay = () => { if (typeof syncGridVideoPlayState === 'function') { syncGridVideoPlayState(row, item); } }; const queueSyncGridVideoPlay = () => requestAnimationFrame(syncGridVideoPlay); const cachedMediaNode = typeof takeFrozenGridMediaNode === 'function' ? takeFrozenGridMediaNode(item) : null; if (cachedMediaNode) { mount.innerHTML = ''; mount.appendChild(cachedMediaNode); mount.dataset.pkMediaKey = mediaKey; queueSyncGridVideoPlay(); return; } if (prevMediaNode && prevMediaKey === mediaKey) { mount.innerHTML = ''; mount.appendChild(prevMediaNode); mount.dataset.pkMediaKey = mediaKey; queueSyncGridVideoPlay(); return; } if (mount.dataset.pkMediaKey === mediaKey && mount.firstElementChild) { queueSyncGridVideoPlay(); return; } mount.dataset.pkMediaKey = mediaKey; mount.innerHTML = renderFrozenGridMedia(item); queueSyncGridVideoPlay(); }; window.renderFrozenGridMedia = renderFrozenGridMedia; window.patchFrozenGridMedia = patchFrozenGridMedia; const getRowClassName = (isSelected, isFocused, isMoving, selectedCount = -1) => { let cls = 'pk-row'; if (isGridView()) cls += ' pk-grid-card-row'; if (isSelected) cls += ' sel'; if (isSelected && isGridView() && (selectedCount === 1 || (selectedCount < 0 && S.getSelectedCount() === 1))) cls += ' pk-sel-single'; if (isFocused) cls += ' pk-focused'; if (isMoving) cls += ' pk-moving'; return cls; }; const ensureVisibleRowPool = (container, need) => { if (!container._pkVisibleRowPool) container._pkVisibleRowPool = []; const pool = container._pkVisibleRowPool; while (pool.length < need) { const row = document.createElement('div'); row._pkPooledRow = true; row.style.position = 'absolute'; pool.push(row); } return pool; }; const resetPooledRow = (row) => { row.className = ''; row.innerHTML = ''; row.onclick = null; row.ondblclick = null; row.oncontextmenu = null; row.onmouseover = null; row.onmouseout = null; row.onmouseenter = null; row.onmouseleave = null; row.onmousedown = null; row.onmouseup = null; row.style.cssText = 'position:absolute;'; row.removeAttribute('data-id'); row.removeAttribute('data-pk-media-key'); row.removeAttribute('data-pk-thumb'); row.removeAttribute('data-pk-bound-id'); row.removeAttribute('data-pk-bound-kind'); row.removeAttribute('data-pk-history-stable'); row.removeAttribute('data-pk-history-sig'); row.removeAttribute('data-pk-grid-stable'); row.removeAttribute('data-pk-grid-sig'); delete row.dataset.pkBoundId; delete row.dataset.pkBoundKind; delete row.dataset.pkHistoryStable; delete row.dataset.pkHistorySig; delete row.dataset.pkGridStable; delete row.dataset.pkGridSig; return row; }; const softResetPooledRow = (row) => { row.onclick = null; row.ondblclick = null; row.oncontextmenu = null; row.onmouseover = null; row.onmouseout = null; row.onmouseenter = null; row.onmouseleave = null; row.onmousedown = null; row.onmouseup = null; return row; }; const cleanupNonPooledChildren = (container) => { Array.from(container.children).forEach(node => { if (!node._pkPooledRow) container.removeChild(node); }); }; let pkStatRaf = 0; const scheduleStatUpdate = () => { if (pkStatRaf) return; pkStatRaf = requestAnimationFrame(() => { pkStatRaf = 0; if (typeof updateStat === 'function') updateStat(); }); }; const flushVisibleRowPool = (container, usedCount) => { const pool = container._pkVisibleRowPool || []; if (container._pkHistoryStableRowMode) { for (const row of pool) { if (!row) continue; if (row._pkUsedThisPass) { if (row.parentNode !== container) container.appendChild(row); } else if (row.parentNode === container) { container.removeChild(row); } row._pkUsedThisPass = false; } return; } if (container._pkGridStableRowMode) { for (const row of pool) { if (!row) continue; if (row._pkGridUsedThisPass) { if (row.parentNode !== container) container.appendChild(row); } else if (row.parentNode === container) { container.removeChild(row); } row._pkGridUsedThisPass = false; } const map = container._pkGridRowMap; if (map) { for (const [id, row] of map) { if (!row || !row._pkPooledRow || !pool.includes(row) || (row.dataset.pkBoundId || '') !== id) { map.delete(id); } } const maxStableRows = Math.max(usedCount + 24, usedCount * 2); if (map.size > maxStableRows) { for (const [id, row] of map) { if (map.size <= maxStableRows) break; if (row && row.parentNode === container) continue; map.delete(id); } } } return; } for (let i = 0; i < pool.length; i++) { const row = pool[i]; if (i < usedCount) { if (row.parentNode !== container) container.appendChild(row); } else if (row.parentNode === container) { container.removeChild(row); } } }; const refreshVisibleSelectionState = () => { const pool = (UI.in && UI.in._pkVisibleRowPool) || []; const selectedCountForSelectionState = isGridView() ? S.getSelectedCount() : -1; const singleGridSel = selectedCountForSelectionState === 1; for (const row of pool) { if (!row || row.parentNode !== UI.in) continue; if (row.classList.contains('pk-group-hd')) { const grpChk = row.querySelector('.pk-grp-chk'); if (!grpChk) continue; const gIds = Array.isArray(row._pkGroupIds) ? row._pkGroupIds : []; let selCount = 0; gIds.forEach(id => { if (S.isSelected(id)) selCount++; }); const isAll = gIds.length > 0 && selCount === gIds.length; const isInd = selCount > 0 && selCount < gIds.length; if (grpChk.checked !== isAll) grpChk.checked = isAll; if (grpChk.indeterminate !== isInd) grpChk.indeterminate = isInd; continue; } const boundId = row.dataset.pkBoundId || ''; if (!boundId) continue; const isSelected = S.isSelected(boundId); const isFocused = S.activeId === boundId; const isMoving = S.movingIds.has(boundId); row.className = getRowClassName(isSelected, isFocused, isMoving, selectedCountForSelectionState); const chk = row.querySelector('input[type="checkbox"]'); if (chk && chk.checked !== isSelected) chk.checked = isSelected; if (singleGridSel && isSelected) row.classList.add('pk-sel-single'); else row.classList.remove('pk-sel-single'); if (isFocused && !isSelected) { row.style.backgroundColor = 'var(--pk-sel-bg)'; row.style.border = '1px solid var(--pk-pri)'; row.style.borderRadius = isGridView() ? `${getGridCardRadius()}px` : '4px'; } else { row.style.backgroundColor = ''; row.style.border = ''; row.style.borderRadius = ''; } } }; const getVirtualModeViewKey = (folderId = null) => { const hasExplicitId = folderId !== null; const safeId = hasExplicitId ? String(folderId || '') : ''; if (safeId === 'file_insight' || safeId === 'file_dup' || safeId === 'folder_insight' || safeId === 'folder_dup' || safeId === 'global_search') return safeId; if (safeId === 'virtual_search_root') return 'global_search'; if (hasExplicitId) return ''; const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; const curId = String((cur && cur.id) || ''); if (curId === 'virtual_search_root' || path.some(p => p && p.id === 'virtual_search_root')) return 'global_search'; if (S.analyzeMode && S.analyzeSimGroups) return 'folder_dup'; if (S.analyzeMode) return 'folder_insight'; if (S.dupMode) return 'file_dup'; if (S.isFlattened) return 'file_insight'; return ''; }; const resolvePreferredViewMode = (folderId = null) => { const curNode = folderId !== null ? { id: folderId } : (S.path[S.path.length - 1] || { id: 'root' }); const curId = String(curNode.id || 'root'); const safeId = folderId !== null ? (folderId || 'root') : curId; const globalMode = gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'; const independentView = gmGet('pk_view_independent', false); const readModePref = (key, fallback = globalMode) => { try { const prefStore = JSON.parse(gmGet('pk_folder_view_prefs', '{}')); const saved = prefStore[key]; const mode = typeof saved === 'string' ? saved : (saved && saved.viewMode); return mode === 'list' ? 'list' : (mode === 'grid' ? 'grid' : fallback); } catch(e) { return fallback; } }; const virtualKey = getVirtualModeViewKey(safeId); const specialKey = safeId === 'share_parse_insight' || (S.shareParseMode && S.shareParseInsightMode) ? 'share_parse_insight' : (safeId === 'share_parse_list' || (S.shareParseMode && S.shareParseListActive) ? 'share_parse_list' : (virtualKey || (safeId === 'trash_root' || S.trashMode ? 'trash_root' : (safeId === 'starred_root' || S.starredMode ? 'starred_root' : (safeId === 'recent_root' || S.recentMode ? 'recent_root' : (safeId === 'history_root' || S.historyMode ? 'history_root' : '')))))); if (specialKey) { if (!independentView) return globalMode; return readModePref(specialKey, 'grid'); } const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curId.startsWith('virtual_') || curId === 'virtual_search_root'); if (!isStandard) return globalMode; if (gmGet('pk_view_independent', false)) { return readModePref(safeId, 'grid'); } return globalMode; }; const persistViewPreference = () => { const nextMode = S.viewMode === 'grid' ? 'grid' : 'list'; const curNode = S.path[S.path.length - 1]; const curId = String((curNode && curNode.id) || 'root'); const independentView = gmGet('pk_view_independent', false); const virtualKey = getVirtualModeViewKey(); const specialKey = S.shareParseMode && S.shareParseInsightMode ? 'share_parse_insight' : (S.shareParseMode && S.shareParseListActive ? 'share_parse_list' : (virtualKey || (S.trashMode ? 'trash_root' : (S.starredMode ? 'starred_root' : (S.recentMode ? 'recent_root' : (S.historyMode ? 'history_root' : '')))))); if (specialKey) { if (!independentView) { gmSet('pk_file_view_mode', nextMode); return; } try { const prefStore = JSON.parse(gmGet('pk_folder_view_prefs', '{}')); prefStore[specialKey] = nextMode; gmSet('pk_folder_view_prefs', JSON.stringify(prefStore)); } catch(e) {} return; } gmSet('pk_file_view_mode', nextMode); const isStandard = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!curId.startsWith('virtual_') || curId === 'virtual_search_root'); if (!isStandard) return; if (gmGet('pk_view_independent', false)) { const folderId = curId; try { const prefStore = JSON.parse(gmGet('pk_folder_view_prefs', '{}')); prefStore[folderId] = nextMode; gmSet('pk_folder_view_prefs', JSON.stringify(prefStore)); } catch(e) {} } }; const readJsonPref = (key, fallback) => { try { const raw = gmGet(key, JSON.stringify(fallback)); const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; return parsed && typeof parsed === 'object' ? parsed : fallback; } catch(e) { return fallback; } }; const normalizeSortDir = (dir) => Number(dir) === -1 ? -1 : 1; const getCurrentPathNode = () => (Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : { id: '' }); const getSortPrefKey = (folderId = null) => { const hasExplicitId = folderId !== null; const safeId = hasExplicitId ? String(folderId || '') : ''; if (hasExplicitId) { if (safeId === 'file_dup') return ''; if (safeId === 'virtual_search_root') return 'global_search'; if (['starred_root', 'recent_root', 'history_root', 'trash_root', 'file_insight', 'folder_insight', 'folder_dup', 'global_search', 'share_parse_list', 'share_parse_insight'].includes(safeId)) return safeId; } const path = Array.isArray(S.path) ? S.path : []; const cur = hasExplicitId ? { id: safeId } : getCurrentPathNode(); const curId = String((cur && cur.id) || ''); const pathHasGlobalSearch = path.some(p => p && p.id === 'virtual_search_root'); if (S.shareParseMode) { if (S.shareParseInsightMode) return 'share_parse_insight'; if (S.shareParseListActive) return 'share_parse_list'; return ''; } if (S.shareMode || S.offlineMode || S.uploadMode) return ''; if (S.dupMode) return ''; if (S.analyzeMode) { const isAnalyzeRoot = curId === 'analyze_root'; if (S.analyzeSimGroups) return isAnalyzeRoot ? '' : 'folder_dup'; return 'folder_insight'; } if (S.isFlattened) return 'file_insight'; if (pathHasGlobalSearch || curId === 'virtual_search_root') return 'global_search'; if (S.historyMode) return 'history_root'; if (S.trashMode) return 'trash_root'; if (S.starredMode) return 'starred_root'; if (S.recentMode) return 'recent_root'; if (curId.startsWith('virtual_') && curId !== 'virtual_search_root') return ''; return curId || 'root'; }; const isPathSortSupportedForContext = (key) => { const cur = getCurrentPathNode(); const curId = String((cur && cur.id) || ''); if (key === 'file_insight' || key === 'share_parse_insight') return true; if (key === 'folder_insight') return !!(S.analyzeMode && curId === 'analyze_root' && !S.analyzeSimGroups); if (key === 'global_search') return curId === 'virtual_search_root'; return false; }; const getDefaultSortForContext = (key) => { if (key === 'history_root') return { sort: 'play_time', dir: 1 }; return { sort: 'modified_time', dir: 1 }; }; const getLegalSortsForContext = (key) => { if (!key) return []; if (key === 'history_root') return ['starred', 'name', 'size', 'duration', 'progress', 'play_time']; const legal = ['name', 'size', 'modified_time', 'duration']; if (key !== 'share_parse_list' && key !== 'share_parse_insight') legal.unshift('starred'); if (isPathSortSupportedForContext(key)) legal.splice(legal.indexOf('size'), 0, 'path'); if (S.analyzeMode && getCurrentPathNode().id === 'analyze_root') { const idx = legal.indexOf('duration'); if (idx !== -1) legal.splice(idx, 1); } return legal; }; const sanitizeSortForContext = (sortState, key) => { const fallback = getDefaultSortForContext(key); const savedSort = sortState && typeof sortState.sort === 'string' ? sortState.sort : ''; const savedDir = normalizeSortDir(sortState && sortState.dir); const legal = getLegalSortsForContext(key); if (savedSort && legal.includes(savedSort)) { return { sort: savedSort, dir: savedDir, fallbackOnly: false }; } return { sort: fallback.sort, dir: fallback.dir, fallbackOnly: true }; }; const getSortContextSignature = (folderId = null, key = null) => { const prefKey = key !== null ? key : getSortPrefKey(folderId); const pathIds = (Array.isArray(S.path) ? S.path : []).map(p => String((p && p.id) || '')).join('/'); const sharePathIds = (Array.isArray(S.shareParsePath) ? S.shareParsePath : []).map(p => String((p && p.id) || '')).join('/'); return [ prefKey || '', pathIds, S.shareParseMode ? 'share' : '', S.shareParseInsightMode ? 'share_insight' : '', S.shareParseCurParentId || '', sharePathIds, S.analyzeMode ? 'analyze' : '', S.analyzeSimGroups ? 'folder_dup' : '', S.dupMode ? 'file_dup' : '', S.isFlattened ? 'flat' : '' ].join('|'); }; const getCurrentSortSourceItems = () => { if (S.shareParseMode && S.shareParseInsightMode) return Array.isArray(S.shareParseInsightItems) ? S.shareParseInsightItems : []; if (S.shareParseMode && S.shareParseListActive) return Array.isArray(S.shareParseItems) ? S.shareParseItems : []; return Array.isArray(S.items) ? S.items : []; }; const isPureMediaSortContext = () => { const items = getCurrentSortSourceItems(); const files = items.filter(item => item && !item.isHeader && item.kind !== 'drive#folder'); if (!files.length) return false; return files.every(isImageLikeItem) || files.every(isVideoLikeItem); }; const canMediaSortIntervene = (key) => { if (!key || !gmGet('pk_comic_mode', true)) return false; const path = Array.isArray(S.path) ? S.path : []; const cur = getCurrentPathNode(); const curId = String((cur && cur.id) || ''); if (key === 'starred_root') return !!(S.starredMode && path.length > 1); if (key === 'recent_root') return !!(S.recentMode && path.length > 1); if (key === 'folder_insight') return !!(S.analyzeMode && curId !== 'analyze_root' && !S.analyzeSimGroups); if (key === 'folder_dup') return !!(S.analyzeMode && curId !== 'analyze_root' && S.analyzeSimGroups); if (key === 'global_search') return path.some(p => p && p.id === 'virtual_search_root') && curId !== 'virtual_search_root'; if (key === 'share_parse_list') return !!(S.shareParseMode && S.shareParseListActive); if (['history_root', 'trash_root', 'file_insight', 'share_parse_insight'].includes(key)) return false; return !S.shareMode && !S.offlineMode && !S.uploadMode && !S.dupMode && !S.isFlattened && !S.historyMode && !S.trashMode; }; const resolvePreferredSortState = (folderId = null, opts = {}) => { const key = getSortPrefKey(folderId); if (!key) return null; const sig = getSortContextSignature(folderId, key); if (S._sortManualSig === sig) { const manual = sanitizeSortForContext({ sort: S.sort, dir: S.dir }, key); return { ...manual, key, signature: sig, manual: true, fallbackOnly: false }; } const independentSort = gmGet('pk_sort_independent', false); let saved = null; if (independentSort) { const prefStore = readJsonPref('pk_folder_sort_prefs', {}); saved = prefStore[key] || null; } else { saved = readJsonPref('pk_global_sort_pref', { sort: 'modified_time', dir: 1 }); } const sanitized = sanitizeSortForContext(saved, key); if (opts.allowMedia !== false && canMediaSortIntervene(key) && isPureMediaSortContext()) { return { sort: 'name', dir: 1, fallbackOnly: true, key, signature: sig, mediaOverride: true }; } return { ...sanitized, key, signature: sig }; }; const applyResolvedSortState = (folderId = null) => { const key = getSortPrefKey(folderId); const sig = getSortContextSignature(folderId, key); if (S._sortContextSig !== sig) { S._sortContextSig = sig; S._sortManualSig = null; S._mediaSortAutoSig = null; } const resolved = resolvePreferredSortState(folderId); if (!resolved) return null; S.sort = resolved.sort; S.dir = normalizeSortDir(resolved.dir); S._mediaSortAutoSig = resolved.mediaOverride ? resolved.signature : null; try { if (typeof updateGridSortUIInstant === 'function') updateGridSortUIInstant(); } catch(e) {} return resolved; }; const persistSortPreference = () => { const key = getSortPrefKey(); if (!key) return false; const state = sanitizeSortForContext({ sort: S.sort, dir: S.dir }, key); if (state.fallbackOnly) return false; if (gmGet('pk_sort_independent', false)) { try { const prefStore = readJsonPref('pk_folder_sort_prefs', {}); const prev = prefStore[key] || {}; prefStore[key] = { ...prev, sort: state.sort, dir: state.dir }; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); return true; } catch(e) { return false; } } try { const globalPref = readJsonPref('pk_global_sort_pref', {}); globalPref.sort = state.sort; globalPref.dir = state.dir; delete globalPref.folderFirst; gmSet('pk_global_sort_pref', JSON.stringify(globalPref)); } catch(e) { gmSet('pk_global_sort_pref', JSON.stringify({ sort: state.sort, dir: state.dir })); } return true; }; const applyManualSortSelection = (sort) => { const key = getSortPrefKey(); const cur = getCurrentPathNode(); if (!key && (S.dupMode || (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups))) return; if (key && !getLegalSortsForContext(key).includes(sort)) return; if (S.sort === sort) S.dir *= -1; else { S.sort = sort; S.dir = 1; } if (key) { const sig = getSortContextSignature(null, key); S._sortContextSig = sig; S._sortManualSig = sig; S._mediaSortAutoSig = null; } S.gridSortMenuOpen = true; updateGridSortUIInstant(); persistSortPreference(); refresh(); }; const getGridSortOptions = () => { const cur = S.path[S.path.length - 1] || { id: 'root' }; const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const key = getSortPrefKey(); const legalSorts = getLegalSortsForContext(key); if (key && !legalSorts.includes(S.sort)) { const safe = sanitizeSortForContext({ sort: S.sort, dir: S.dir }, key); S.sort = safe.sort; S.dir = safe.dir; } const isHistoryGridSort = !!S.historyMode; const isTrashGridSort = !!S.trashMode; const supportsPathSort = !isHistoryGridSort && isPathSortSupportedForContext(key); const supportsDurationTypeSort = !isAnalyzeRoot && !isHistoryGridSort; const opts = [ { sort: 'starred', ascLabel: L.picker_sort_star_new, descLabel: L.picker_sort_star_old, ascIcon: CONF.crumbIcons.sortStarNew, descIcon: CONF.crumbIcons.sortStarOld }, { sort: 'name', ascLabel: 'A-Z', descLabel: 'Z-A', ascIcon: CONF.crumbIcons.sortAZ, descIcon: CONF.crumbIcons.sortZA }, { sort: 'size', ascLabel: L.picker_sort_large, descLabel: L.picker_sort_small, ascIcon: CONF.crumbIcons.sortLarge, descIcon: CONF.crumbIcons.sortSmall }, { sort: 'modified_time', ascLabel: isTrashGridSort ? L.trash_sort_more_to_less : L.picker_sort_new, descLabel: isTrashGridSort ? L.trash_sort_less_to_more : L.picker_sort_old, ascIcon: CONF.crumbIcons.sortNew, descIcon: CONF.crumbIcons.sortOld } ]; if (isHistoryGridSort) { opts.splice(opts.length - 1, 1, { sort: 'duration', ascLabel: L.picker_sort_duration_asc, descLabel: L.picker_sort_duration_desc, ascIcon: CONF.crumbIcons.sortTypeDurAsc, descIcon: CONF.crumbIcons.sortTypeDurDesc }, { sort: 'progress', ascLabel: L.picker_sort_progress_more, descLabel: L.picker_sort_progress_less, ascIcon: CONF.crumbIcons.sortProgressAsc, descIcon: CONF.crumbIcons.sortProgressDesc }, { sort: 'play_time', ascLabel: L.picker_sort_new, descLabel: L.picker_sort_old, ascIcon: CONF.crumbIcons.sortNew, descIcon: CONF.crumbIcons.sortOld } ); return opts; } if (supportsPathSort) { opts.splice(2, 0, { sort: 'path', ascLabel: L.picker_sort_path_asc, descLabel: L.picker_sort_path_desc, ascIcon: CONF.crumbIcons.sortPathAsc, descIcon: CONF.crumbIcons.sortPathDesc } ); } if (supportsDurationTypeSort) { opts.splice(opts.length - 1, 0, { sort: 'duration', ascLabel: L.picker_sort_type_dur_asc, descLabel: L.picker_sort_type_dur_desc, ascIcon: CONF.crumbIcons.sortTypeDurAsc, descIcon: CONF.crumbIcons.sortTypeDurDesc } ); } if (S.shareParseMode) opts.shift(); return key ? opts.filter(opt => legalSorts.includes(opt.sort)) : opts; }; const resolveGridSortOptionState = (opt) => { const isActive = opt.sort === S.sort; const dir = isActive ? S.dir : 1; return { ...opt, dir, active: isActive, label: dir === -1 ? opt.descLabel : opt.ascLabel, icon: dir === -1 ? opt.descIcon : opt.ascIcon }; }; const getGridSortMeta = () => { const opts = getGridSortOptions(); const fallbackOpt = { sort: 'modified_time', ascLabel: L.picker_sort_new, descLabel: L.picker_sort_old, ascIcon: CONF.crumbIcons.sortNew, descIcon: CONF.crumbIcons.sortOld }; const target = opts.find(opt => opt.sort === S.sort) || opts[0] || fallbackOpt; return resolveGridSortOptionState(target); }; const bindGridSortOptions = (root) => { if (!root) return; root.querySelectorAll('.pk-grid-sort-opt').forEach(opt => { opt.onclick = (e) => { e.stopPropagation(); applySortSelection(opt.dataset.sort); }; }); }; const updateGridSortUIInstant = () => { const wrap = UI.gridSortWrap || document.querySelector('.pk-grid-sort-wrap'); if (!wrap) return; const meta = getGridSortMeta(); const trigger = wrap.querySelector('#pk-grid-sort-trigger'); if (trigger) trigger.innerHTML = `${meta.icon}${meta.label}`; const menu = wrap.querySelector('.pk-grid-sort-menu'); if (menu) { menu.innerHTML = getGridSortOptions().map(opt => { const state = resolveGridSortOptionState(opt); return `
${state.icon}${state.label}
`; }).join(''); bindGridSortOptions(menu); } UI.gridSortWrap = wrap; wrap.classList.toggle('open', !!S.gridSortMenuOpen); }; const closeGridSortMenu = (evt) => { if (!UI.gridSortWrap) return; if (!evt) { UI.gridSortWrap.classList.remove('open'); S.gridSortMenuOpen = false; return; } const target = evt.target; const isNodeTarget = !!target && typeof target === 'object' && typeof target.nodeType === 'number'; if (typeof evt.composedPath === 'function') { const path = evt.composedPath(); if (Array.isArray(path) && path.includes(UI.gridSortWrap)) return; } else if (isNodeTarget && UI.gridSortWrap.contains(target)) { return; } UI.gridSortWrap.classList.remove('open'); S.gridSortMenuOpen = false; }; const applySortSelection = (sort) => { applyManualSortSelection(sort); }; const syncFolderFirstFromGlobal = () => { const saved = gmGet('pk_folder_first', false); S.folderFirst = saved === true || saved === 'true'; if (S.renderFolderFirst) S.renderFolderFirst(); }; const setFolderFirstGlobal = (enabled) => { S.folderFirst = enabled === true; gmSet('pk_folder_first', S.folderFirst); if (S.renderFolderFirst) S.renderFolderFirst(); }; const rememberFolderFirstBeforeStrictMode = () => { if (S.strictFolderFirstSnapshot === null) { S.strictFolderFirstSnapshot = S.folderFirst === true; } }; const restoreFolderFirstAfterStrictMode = () => { if (S.strictFolderFirstSnapshot === null) return; S.strictFolderFirstSnapshot = null; syncFolderFirstFromGlobal(); }; const toggleFolderFirst = () => { setFolderFirstGlobal(!S.folderFirst); closeGridSortMenu(); refresh(); }; const ensureViewSwitch = () => { const host = (S.trashMode && UI.trashBar) ? UI.trashBar : UI.actionBar; if (!host) return; let wrap = UI.viewSwitch || el.querySelector('#pk-view-switch'); if (!wrap) { wrap = document.createElement('div'); wrap.className = 'pk-view-switch'; wrap.id = 'pk-view-switch'; wrap.innerHTML = ``; UI.viewSwitch = wrap; UI.btnViewList = wrap.querySelector('#pk-view-list'); UI.btnViewGrid = wrap.querySelector('#pk-view-grid'); UI.btnViewList.onclick = (e) => { e.stopPropagation(); if (S.viewMode === 'list') return; const anchorId = getViewportAnchorId(true); const finishViewSwitch = () => requestAnimationFrame(() => UI.win.classList.remove('pk-view-switching')); UI.win.classList.add('pk-view-switching'); S.viewMode = 'list'; persistViewPreference(); UI.win.classList.remove('pk-grid-view'); syncLayoutMetrics(); renderViewSwitch(); renderList(); if (anchorId) { requestAnimationFrame(() => { const targetIdx = S.display.findIndex(x => x.id === anchorId); if (targetIdx !== -1) { const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); } finishViewSwitch(); }); } else { finishViewSwitch(); } }; UI.btnViewGrid.onclick = (e) => { e.stopPropagation(); if (!canUseGridView() || S.viewMode === 'grid') return; const anchorId = getViewportAnchorId(true); const finishViewSwitch = () => requestAnimationFrame(() => UI.win.classList.remove('pk-view-switching')); UI.win.classList.add('pk-view-switching'); S.viewMode = 'grid'; persistViewPreference(); UI.win.classList.add('pk-grid-view'); syncLayoutMetrics(); renderViewSwitch(); renderList(); if (anchorId) { requestAnimationFrame(() => { const targetIdx = S.display.findIndex(x => x.id === anchorId); if (targetIdx !== -1) { const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); } finishViewSwitch(); }); } else { finishViewSwitch(); } }; } if (!UI.viewSwitch) UI.viewSwitch = wrap; if (!UI.btnViewList) UI.btnViewList = wrap.querySelector('#pk-view-list'); if (!UI.btnViewGrid) UI.btnViewGrid = wrap.querySelector('#pk-view-grid'); if (wrap.parentNode !== host) { if (S.trashMode && UI.btnEmptyTrash && UI.btnEmptyTrash.parentNode === host) UI.btnEmptyTrash.insertAdjacentElement('afterend', wrap); else if (S.trashMode && UI.btnTrashRefresh && UI.btnTrashRefresh.parentNode === host) UI.btnTrashRefresh.insertAdjacentElement('afterend', wrap); else if (UI.btnCancelShare && UI.btnCancelShare.parentNode === host) UI.btnCancelShare.insertAdjacentElement('afterend', wrap); else host.appendChild(wrap); } }; const renderViewSwitch = () => { const usable = canUseGridView(); if (!usable) { if (UI.viewSwitch) { UI.viewSwitch.style.display = 'none'; UI.viewSwitch.style.visibility = 'hidden'; UI.viewSwitch.style.pointerEvents = 'none'; } return; } ensureViewSwitch(); if (!UI.viewSwitch) return; UI.viewSwitch.style.display = 'inline-flex'; UI.viewSwitch.style.visibility = ''; UI.viewSwitch.style.pointerEvents = ''; if (UI.btnViewList) UI.btnViewList.classList.toggle('active', !isGridView()); if (UI.btnViewGrid) UI.btnViewGrid.classList.toggle('active', isGridView()); }; const applyResolvedViewMode = (prefKey = null) => { const nextViewMode = (typeof resolvePreferredViewMode === 'function') ? resolvePreferredViewMode(prefKey) : (gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'); const modeChanged = S.viewMode !== nextViewMode; if (modeChanged) { beginFolderViewSync(); if (UI.win) UI.win.classList.add('pk-view-switching', 'pk-mode-view-syncing'); } S.viewMode = nextViewMode; S._gridLayoutKey = ''; clearGroupedGridMetaCache(); if (UI.win) UI.win.classList.toggle('pk-grid-view', isGridView()); syncLayoutMetrics(); renderViewSwitch(); if (modeChanged && UI.win) requestAnimationFrame(() => UI.win.classList.remove('pk-mode-view-syncing')); return nextViewMode; }; 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; let autoHideBtnTextRaf = 0; let autoHideBtnTextBound = false; let autoHideBtnTextLockWidth = 0; const isAutoHideVisibleEl = (node) => { if (!node || !node.isConnected) return false; const st = getComputedStyle(node); if (st.display === 'none' || st.visibility === 'hidden') return false; const r = node.getBoundingClientRect(); return r.width > 0 && r.height > 0; }; const detectAutoHideButtonTextOverflow = () => { if (!el || !UI || !UI.win || el.style.display === 'none' || !isAutoHideVisibleEl(UI.win)) return false; const tol = 4; const isShareParseRootActionsOverflow = () => { if (!S.shareParseMode || S.shareParseListActive) return false; const actions = el.querySelector('.pk-share-parse-root-mode .pk-share-parse-actions'); if (!isAutoHideVisibleEl(actions)) return false; const host = isAutoHideVisibleEl(UI.vp) ? UI.vp : (isAutoHideVisibleEl(UI.win) ? UI.win : null); if (!host) return false; const hr = host.getBoundingClientRect(); const leftLimit = Math.max(0, hr.left); const rightLimit = Math.min(window.innerWidth || hr.right, hr.right); const ar = actions.getBoundingClientRect(); if (ar.left < leftLimit - tol || ar.right > rightLimit + tol) return true; const btns = Array.from(actions.querySelectorAll('.pk-share-parse-btn')).filter(isAutoHideVisibleEl); for (const btn of btns) { const br = btn.getBoundingClientRect(); if (br.left < leftLimit - tol || br.right > rightLimit + tol) return true; if (btn.scrollWidth > btn.clientWidth + tol || btn.scrollHeight > btn.clientHeight + tol) return true; } return false; }; if (isShareParseRootActionsOverflow()) return true; const shareParseBottomBar = S.shareParseMode && S.shareParseListActive ? UI.bottomGrp : null; const shareParseRootActions = S.shareParseMode && !S.shareParseListActive ? el.querySelector('.pk-share-parse-root-mode .pk-share-parse-actions') : null; const bars = [el.querySelector('#pk-top-bar'), UI.actionBar, UI.trashBar, shareParseBottomBar, shareParseRootActions, el.querySelector('.pk-link-bookmark-mode .pk-lbm-toolbar')].filter(isAutoHideVisibleEl); for (const bar of bars) { const br = bar.getBoundingClientRect(); const children = Array.from(bar.children).filter(isAutoHideVisibleEl); for (const child of children) { const cr = child.getBoundingClientRect(); if (cr.right > br.right + tol || cr.left < br.left - tol) return true; if (child.scrollWidth > child.clientWidth + tol || child.scrollHeight > child.clientHeight + tol) return true; const spans = Array.from(child.querySelectorAll('span')).filter(isAutoHideVisibleEl); for (const span of spans) { if (span.scrollWidth > span.clientWidth + tol || span.scrollHeight > span.clientHeight + tol) return true; } } } return false; }; const updateAutoHideButtonText = () => { autoHideBtnTextRaf = 0; if (!el || !UI || !UI.win) return; const syncQuotaText = () => { try { if (typeof refreshQuotaText === 'function') refreshQuotaText(); } catch(e) {} }; if (el.style.display === 'none') { el.classList.remove('pk-auto-hide-btn-text'); autoHideBtnTextLockWidth = 0; syncQuotaText(); return; } const width = window.innerWidth || 0; const unlockGap = S.shareParseMode && S.shareParseListActive ? 0 : 96; const wasAuto = el.classList.contains('pk-auto-hide-btn-text'); if (wasAuto) { if (!autoHideBtnTextLockWidth) autoHideBtnTextLockWidth = width; if (width < autoHideBtnTextLockWidth + unlockGap) return; el.classList.remove('pk-auto-hide-btn-text'); if (detectAutoHideButtonTextOverflow()) { el.classList.add('pk-auto-hide-btn-text'); autoHideBtnTextLockWidth = width; } else { autoHideBtnTextLockWidth = 0; } syncQuotaText(); } else if (detectAutoHideButtonTextOverflow()) { el.classList.add('pk-auto-hide-btn-text'); autoHideBtnTextLockWidth = width; syncQuotaText(); } else { autoHideBtnTextLockWidth = 0; } }; const requestAutoHideButtonTextCheck = () => { if (autoHideBtnTextRaf) return; autoHideBtnTextRaf = requestAnimationFrame(updateAutoHideButtonText); }; const bindAutoHideButtonTextObservers = () => { if (autoHideBtnTextBound) return; autoHideBtnTextBound = true; const targets = [UI.win, el.querySelector('#pk-top-bar'), UI.actionBar, UI.trashBar, UI.bottomGrp, UI.crumb, UI.vp, UI.in].filter(Boolean); if (typeof ResizeObserver !== 'undefined') { const ro = new ResizeObserver(requestAutoHideButtonTextCheck); targets.forEach(t => ro.observe(t)); } if (typeof MutationObserver !== 'undefined') { const mo = new MutationObserver(requestAutoHideButtonTextCheck); targets.slice(1).forEach(t => mo.observe(t, { attributes: true, childList: true, subtree: true, attributeFilter: ['style', 'class'] })); } requestAutoHideButtonTextCheck(); }; const checkGuiResponsiveness = () => { const width = window.innerWidth; 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; } if (typeof syncShareParseInsightSearchPathLabel === 'function') syncShareParseInsightSearchPathLabel(); 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; } requestAutoHideButtonTextCheck(); }; const syncDupFolderButtonText = () => { const sel = UI.selDupFolder; const btn = document.getElementById('pk-dup-folder-btn'); const txt = document.getElementById('pk-dup-folder-btn-txt'); if (!sel || !btn || !txt) return; const placeholder = window.innerWidth <= 1200 ? L.lbl_dup_select_folder_short : L.lbl_dup_select_folder; let label = placeholder; const opt = sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex] : null; if (opt && opt.value && opt.value !== "__RESET__") label = (opt.textContent || placeholder).trim(); txt.textContent = label; btn.removeAttribute('title'); btn.removeAttribute('data-pk-tip'); }; (() => { const sel = UI.selDupFolder; const btn = document.getElementById('pk-dup-folder-btn'); const wrap = document.getElementById('pk-dup-folder-sel-wrap'); if (!sel || !btn || !wrap) return; let pop = null; const closePop = () => { if (pop && pop.parentNode) pop.remove(); pop = null; btn.classList.remove('act'); }; const openPop = () => { closePop(); const opts = Array.from(sel.options).filter(o => !(o.disabled && !o.value)); opts.sort((a, b) => (b.value === "__RESET__") - (a.value === "__RESET__")); if (!opts.length) return; pop = document.createElement('div'); pop.className = 'pk-dup-folder-pop'; pop.classList.toggle('pk-dark', !!document.querySelector('.pk-ov')?.classList.contains('pk-dark')); pop.innerHTML = '
'; const head = pop.firstElementChild; const list = pop.lastElementChild; opts.forEach(o => { const item = document.createElement('button'); item.type = 'button'; item.className = `pk-dup-folder-item${o.value === sel.value ? ' act' : ''}${o.value === "__RESET__" ? ' pk-reset' : ''}`; const itemText = (o.textContent || '').trim(); item.dataset.value = o.value; item.textContent = itemText; item.removeAttribute('title'); if (itemText && o.value !== "__RESET__") item.setAttribute('data-pk-tip', itemText); if (o.value === "__RESET__") head.appendChild(item); else list.appendChild(item); }); document.body.appendChild(pop); pop.addEventListener('mousedown', (e) => e.stopPropagation(), true); pop.addEventListener('click', (e) => e.stopPropagation(), true); pop.addEventListener('wheel', (e) => e.stopPropagation(), { passive: true }); const rect = typeof getLogicalRect === 'function' ? getLogicalRect(btn) : btn.getBoundingClientRect(); const popRect = pop.getBoundingClientRect(); const winW = window.innerWidth; const winH = window.innerHeight; let left = rect.left; let top = rect.bottom + 4; if (left + popRect.width > winW - 8) left = Math.max(8, winW - popRect.width - 8); if (top + popRect.height > winH - 8) top = Math.max(8, rect.top - popRect.height - 4); pop.style.left = `${left}px`; pop.style.top = `${top}px`; btn.classList.add('act'); const activeItem = list.querySelector('.pk-dup-folder-item.act'); if (activeItem) activeItem.scrollIntoView({ block: 'nearest' }); }; btn.addEventListener('click', (e) => { e.stopPropagation(); syncDupFolderButtonText(); if (pop) closePop(); else openPop(); }); document.addEventListener('click', (e) => { if (!pop) return; const t = e.target; if (pop.contains(t)) { const item = t.closest('.pk-dup-folder-item'); if (!item) return; const pickedValue = item.dataset.value || ''; S._dupFolderPickedScrollTop = !!(pickedValue && pickedValue !== "__RESET__"); sel.value = pickedValue; syncDupFolderButtonText(); closePop(); sel.dispatchEvent(new Event('change', { bubbles: true })); return; } if (!wrap.contains(t)) closePop(); }, true); const handleDupFolderViewportChange = (e) => { if (!pop) return; const t = e && e.target; if (t && pop.contains(t)) return; closePop(); }; window.addEventListener('resize', handleDupFolderViewportChange, true); window.addEventListener('scroll', handleDupFolderViewportChange, true); syncDupFolderButtonText(); })(); window.addEventListener('resize', () => { checkGuiResponsiveness(); syncDupFolderButtonText(); if (el.style.display === 'none') return; const isAuthManagerReloading = !!( typeof window.pkIsAuthManagerReloading === 'function' && window.pkIsAuthManagerReloading() ); if (isAuthManagerReloading) { if (typeof window.pkDeferAuthManagerRelayout === 'function') { window.pkDeferAuthManagerRelayout(); } return; } const isDupAnalyzeMaskBusy = !!( S.loading && S.dupMode && S.dupRunning && UI.loader && UI.loader.style.display !== 'none' ); if (isDupAnalyzeMaskBusy) return; if (isGridView() && typeof scheduleGridRelayout === 'function') { scheduleGridRelayout(true); } else if (typeof renderList === 'function') { requestAnimationFrame(() => { if (typeof syncLayoutMetrics === 'function') syncLayoutMetrics(); renderList(); }); } else if (typeof renderVisible === 'function') { requestAnimationFrame(() => { if (typeof syncLayoutMetrics === 'function') syncLayoutMetrics(); if (UI.in) UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; renderVisible(); }); } }); checkGuiResponsiveness(); bindAutoHideButtonTextObservers(); if (UI.btnClose) { UI.btnClose.addEventListener('click', () => { isForcedHidden = false; requestAnimationFrame(checkGuiResponsiveness); }); } if (UI.btnTheme) { UI.btnTheme.onclick = (e) => { if (e) e.stopPropagation(); el.classList.add('pk-no-transition'); void el.offsetHeight; const wasDark = el.classList.contains('pk-dark'); const newTheme = wasDark ? 'light' : 'dark'; const newIcon = wasDark ? CONF.icons.moon : CONF.icons.sun; UI.btnTheme.innerHTML = newIcon; if (wasDark) { el.classList.remove('pk-dark'); } else { el.classList.add('pk-dark'); } const uploadMenu = (UI.uploadWrap && UI.uploadWrap.querySelector('.pk-dropdown-menu')) || document.querySelector('.pk-dropdown-menu[data-pk-portal="1"]'); if (uploadMenu) uploadMenu.classList.toggle('pk-dark', !wasDark); const crumbPop = document.getElementById('pk-main-crumb-pop'); if (crumbPop) crumbPop.classList.toggle('pk-dark', !wasDark); document.querySelectorAll('#pk-toast-container,.pk-msg-toast,.pk-float-bar-item,#pk-script-update-toast,.pk-script-update-toast,#pk-audio-mini,#pk-audio-ov').forEach(n => n.classList.toggle('pk-dark', !wasDark)); const audioMiniNow = document.getElementById('pk-audio-mini'); const audioFullNow = document.getElementById('pk-audio-ov'); if (audioMiniNow && typeof syncAudioMiniTheme === 'function') requestAnimationFrame(() => syncAudioMiniTheme(audioMiniNow)); if (audioFullNow && typeof syncAudioFullPlayerTheme === 'function') requestAnimationFrame(() => syncAudioFullPlayerTheme(audioFullNow)); gmSet('pk_theme', newTheme); void el.offsetHeight; requestAnimationFrame(() => { el.classList.remove('pk-no-transition'); }); }; } const btnMax = el.querySelector('#pk-maximize'); const isTurbo = gmGet('pk_turbo_mode', false); let isWinMaximized = (globalSavedState && typeof globalSavedState.isMaximized !== 'undefined') ? globalSavedState.isMaximized : isTurbo; let pkSyncHideMenuLock = false; const closeTransientDropdowns = (includeEscape = false, blurSearch = false, dispatchNative = false) => { if (pkSyncHideMenuLock) return; pkSyncHideMenuLock = true; try { closeLocalUploadMenu(); if (blurSearch && UI.searchInput && document.activeElement === UI.searchInput) UI.searchInput.blur(); document.querySelectorAll('#pk-main-crumb-pop,.pk-crumb-pop').forEach(n => n.remove()); if (UI.crumb) { UI.crumb.querySelectorAll('.pk-crumb-sep.pk-active').forEach(btn => { btn.classList.remove('pk-active'); btn.innerHTML = CONF.crumbIcons.right; }); } if (UI.searchHist) { UI.searchHist.style.display = 'none'; UI.searchHist.innerHTML = ''; } if (UI.pop) UI.pop.style.display = 'none'; if (UI.ctx) UI.ctx.style.display = 'none'; document.querySelectorAll('.pk-select-menu,.pk-hist-pop').forEach(menu => { menu.style.display = 'none'; }); document.querySelectorAll('.pk-dropdown-menu[data-pk-portal="1"]').forEach(n => n.remove()); if (dispatchNative) { if (includeEscape) { const keyInit = { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true }; document.dispatchEvent(new KeyboardEvent('keydown', keyInit)); document.dispatchEvent(new KeyboardEvent('keyup', keyInit)); } document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, button: 0 })); document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, button: 0 })); document.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: 0, clientY: 0, button: 0 })); } } catch (e) {} setTimeout(() => { pkSyncHideMenuLock = false; }, 0); }; const syncHideNativeExitDropdown = () => closeTransientDropdowns(false); const pkWinHideObserver = new MutationObserver(() => { if (!el.isConnected) return; const cs = getComputedStyle(el); if (el.hidden || cs.display === 'none' || cs.visibility === 'hidden') { requestAnimationFrame(syncHideNativeExitDropdown); } }); pkWinHideObserver.observe(el, { attributes: true, attributeFilter: ['style', 'class', 'hidden'] }); if (isTurbo) { if (btnMax) btnMax.style.display = 'none'; if (UI.btnClose) UI.btnClose.style.display = 'none'; } const win = el.querySelector('.pk-win'); if (isWinMaximized) { if (win) win.classList.add('pk-maximized'); document.body.classList.add('pk-body-max'); CONF.rowHeight = 60; if (btnMax) { btnMax.innerHTML = CONF.icons.minimize; btnMax.setAttribute('data-pk-tip', L.tip_minimize); } } else { if (win) win.classList.remove('pk-maximized'); document.body.classList.remove('pk-body-max'); CONF.rowHeight = 40; if (btnMax) { btnMax.innerHTML = CONF.icons.maximize; btnMax.setAttribute('data-pk-tip', L.tip_maximize); } } if (btnMax) { btnMax.onclick = (e) => { if (e) e.stopPropagation(); el.classList.add('pk-no-transition'); const vp = UI.vp; const oldRowHeight = CONF.rowHeight; const centerIndex = (vp.scrollTop + vp.clientHeight / 2) / oldRowHeight; isWinMaximized = !isWinMaximized; btnMax.innerHTML = isWinMaximized ? CONF.icons.minimize : CONF.icons.maximize; btnMax.setAttribute('data-pk-tip', isWinMaximized ? L.tip_minimize : L.tip_maximize); const win = el.querySelector('.pk-win'); if (isWinMaximized) { win.classList.add('pk-maximized'); document.body.classList.add('pk-body-max'); CONF.rowHeight = 60; } else { win.classList.remove('pk-maximized'); document.body.classList.remove('pk-body-max'); CONF.rowHeight = 40; } if (typeof renderList === 'function') { renderList(); } else if (typeof renderVisible === 'function') { if(UI.in) UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; renderVisible(); } if (typeof refreshQuotaText === 'function') refreshQuotaText(); requestAnimationFrame(() => { void el.offsetHeight; const newRowHeight = CONF.rowHeight; const newVpHeight = vp.clientHeight; const targetScrollTop = (centerIndex * newRowHeight) - (newVpHeight / 2); vp.scrollTop = Math.max(0, targetScrollTop); renderVisible(); el.classList.remove('pk-no-transition'); }); }; } function getPkAutofillSafeSelector() { return 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]),textarea,select'; } function isPkAutofillScopeNode(node) { if (!node || node.nodeType !== 1) return false; return !!( node.matches?.('.pk-ov,.pk-modal-ov,#pk-toast-container,#pk-player-ov,#pk-audio-ov,.pk-img-ov,.pk-search-running-mask') || node.closest?.('.pk-ov,.pk-modal-ov,#pk-toast-container,#pk-player-ov,#pk-audio-ov,.pk-img-ov,.pk-search-running-mask') || (node.id && /^pk[-_]/i.test(String(node.id))) || (node.className && typeof node.className === 'string' && /\bpk[-_]/i.test(node.className)) ); } function hardenPkAutofillField(el) { if (!el || el.nodeType !== 1 || !el.matches?.(getPkAutofillSafeSelector())) return; if (!isPkAutofillScopeNode(el)) return; if (el.dataset.pkAutofillSafe === '1') return; el.dataset.pkAutofillSafe = '1'; el.setAttribute('autocomplete', 'new-password'); el.setAttribute('autocorrect', 'off'); el.setAttribute('autocapitalize', 'off'); el.setAttribute('spellcheck', 'false'); el.setAttribute('data-lpignore', 'true'); el.setAttribute('data-1p-ignore', 'true'); el.setAttribute('data-bwignore', 'true'); el.setAttribute('data-form-type', 'other'); const tag = String(el.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea') { const oldName = el.getAttribute('name') || ''; if (oldName) el.dataset.pkOriginName = oldName; window.__pkAutofillSafeSeq = (window.__pkAutofillSafeSeq || 0) + 1; el.setAttribute('name', `pk_noauto_${Date.now().toString(36)}_${window.__pkAutofillSafeSeq}_${Math.random().toString(36).slice(2, 8)}`); } el.addEventListener('focus', () => { el.setAttribute('autocomplete', 'new-password'); el.setAttribute('autocorrect', 'off'); el.setAttribute('autocapitalize', 'off'); el.setAttribute('spellcheck', 'false'); }, true); } function hardenPkAutofill(root = document) { if (!root) return; const selector = getPkAutofillSafeSelector(); if (root.nodeType === 1 && root.matches?.(selector)) hardenPkAutofillField(root); const base = root.nodeType === 1 || root.nodeType === 9 || root.nodeType === 11 ? root : document; base.querySelectorAll?.(selector).forEach(hardenPkAutofillField); } function installPkAutofillGuard(root = document) { hardenPkAutofill(root); if (window.__pkAutofillGuardBound) return; window.__pkAutofillGuardBound = true; document.addEventListener('focusin', e => { const t = e.target; if (t && t.matches?.(getPkAutofillSafeSelector()) && isPkAutofillScopeNode(t)) hardenPkAutofillField(t); }, true); const observer = new MutationObserver(mutations => { mutations.forEach(mu => { mu.addedNodes.forEach(node => { if (!node || node.nodeType !== 1) return; if (isPkAutofillScopeNode(node) || node.querySelector?.('.pk-ov,.pk-modal-ov,#pk-toast-container,#pk-player-ov,#pk-audio-ov,.pk-img-ov,.pk-search-running-mask,input,textarea,select')) { hardenPkAutofill(node); } }); }); }); observer.observe(document.documentElement || document.body, { childList: true, subtree: true }); window.__pkAutofillGuardObserver = observer; } let modalZIndexCounter = 2147483640; function showModal(html) { const modalHost = getPkToastHost(); let container = document.getElementById('pk-toast-container'); if (container && modalHost) modalHost.appendChild(container); const m = document.createElement('div'); m.className = 'pk-modal-ov'; const isPlayerWebFullscreen = !!document.querySelector('.pk-web-fullscreen-ov'); if (isPlayerWebFullscreen) { m.classList.add('pk-modal-over-player-webfullscreen'); m.style.setProperty('z-index', '2147483647', 'important'); } else { m.style.zIndex = (++modalZIndexCounter).toString(); } if (document.querySelector('.pk-ov').classList.contains('pk-dark')) { m.classList.add('pk-dark'); } m.innerHTML = `
${CONF.icons.close}
${html}
`; hardenPkAutofill(m); installPkAutofillGuard(m); 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'); } }); } }); (modalHost || document.body).appendChild(m); if (document.getElementById('pk-toast-container')) ensurePkToastContainer(); m.querySelector('.pk-modal-close').addEventListener('click', () => m.remove()); return m; } function showAlert(msg, title = L.title_alert) { return new Promise((resolve) => { const m = showModal(`

${title}

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

${title}

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

${title}

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

${L.title_confirm}

${esc(L.msg_clear_sel_confirm.replace('{n}', S.getSelectedCount())).replace(/\n/g, '
')}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) Object.assign(modalBox.style, { width: '420px', padding: '30px' }); m.querySelector('#cfm_no').onclick = () => { m.remove(); resolve(false); }; m.querySelector('#cfm_yes').onclick = () => { const isChecked = m.querySelector('#pk_session_suppress').checked; if (isChecked) S.suppressClearConfirm = true; m.remove(); resolve(true); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(false); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#cfm_yes').click(); } }); }); }; function showPrompt(msg, val = '', title = L.title_prompt, opt = {}) { 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 duplicateText = opt.duplicateText || L.err_name_exists; const duplicateCheck = typeof opt.duplicateCheck === 'function' ? opt.duplicateCheck : (v) => S.items.some(item => item.name === v && item.name !== val); const m = showModal(`

${cleanTitle}

${title}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '480px', height: 'auto', minHeight: 'auto', padding: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const inp = m.querySelector('#prm_input'); const err = m.querySelector('#prm_err'); const okBtn = m.querySelector('#prm_ok'); const maxLen = Math.max(0, Number(opt.maxLen || 0)); let lastLimitTipAt = 0; const showLimitTip = () => { const now = Date.now(); if (now - lastLimitTipAt < 1500) return; lastLimitTipAt = now; showPikPakFileNameLimitTip(); }; const validate = (notify = false) => { const v = inp.value.trim(); const isEmpty = v === ''; const isTooLong = !!(maxLen && v.length > maxLen); const isDup = !isTooLong && !!v && duplicateCheck(v); err.textContent = isTooLong ? getStrings().msg_config_input_over_limit : duplicateText; err.style.visibility = (isTooLong || isDup) ? 'visible' : 'hidden'; if (isTooLong && notify) showLimitTip(); if (isDup || isEmpty || isTooLong) { okBtn.disabled = true; okBtn.style.opacity = '0.4'; okBtn.style.cursor = 'not-allowed'; inp.style.borderColor = (isDup || isTooLong) ? '#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(true)); validate(false); 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(); if (maxLen && v.length > maxLen) { validate(true); return; } m.remove(); resolve(v); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); }; }); } function getPkToastHost() { return document.fullscreenElement || document.webkitFullscreenElement || document.body; } function getPkTopModalZIndex() { let top = 0; document.querySelectorAll('.pk-modal-ov').forEach(el => { const z = Number(getComputedStyle(el).zIndex); if (Number.isFinite(z)) top = Math.max(top, z); }); return top; } function ensurePkToastContainer() { let container = document.getElementById('pk-toast-container'); const host = getPkToastHost(); if (!container) { container = document.createElement('div'); container.id = 'pk-toast-container'; } const topModalZ = getPkTopModalZIndex(); container.style.zIndex = String(topModalZ >= 2147483647 ? 2147483647 : Math.max(2147483647, topModalZ + 1)); if (host) host.appendChild(container); return container; } function normalizePkToastKeyText(value) { const div = document.createElement('div'); div.innerHTML = String(value || ''); return (div.textContent || div.innerText || String(value || '')).replace(/\s+/g, ' ').trim(); } function hidePkToastElement(t) { if (!t) return; if (t._pkToastHideTimer) clearTimeout(t._pkToastHideTimer); if (t._pkToastRemoveTimer) clearTimeout(t._pkToastRemoveTimer); t.classList.remove('pk-show'); t.classList.add('pk-hide'); t._pkToastRemoveTimer = setTimeout(() => { if (t.parentNode) t.remove(); }, 300); } function schedulePkToastHide(t, displayTime) { if (!t) return; if (t._pkToastHideTimer) clearTimeout(t._pkToastHideTimer); if (t._pkToastRemoveTimer) clearTimeout(t._pkToastRemoveTimer); t._pkToastHideTimer = setTimeout(() => hidePkToastElement(t), displayTime); } function showToast(msg, type = 'success', duration = 0) { let container = ensurePkToastContainer(); const toastType = String(type || 'success'); const toastText = normalizePkToastKeyText(msg); const toastKey = `${toastType}|${toastText}`; const displayTime = duration > 0 ? duration : (toastType === 'success' ? 2200 : 3500); const ov = document.querySelector('.pk-ov'); const savedThemeNow = gmGet('pk_theme', 'auto'); const sysDarkNow = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const isDark = ov ? ov.classList.contains('pk-dark') : (savedThemeNow === 'dark' || (savedThemeNow === 'auto' && sysDarkNow)); document.querySelectorAll('.pk-msg-toast').forEach(n => n.classList.toggle('pk-dark', isDark)); const existed = Array.from(document.querySelectorAll('.pk-msg-toast')).find(n => n.dataset.pkToastKey === toastKey && n.parentNode && !n.classList.contains('pk-hide')); if (existed) { if (isDark) existed.classList.add('pk-dark'); else existed.classList.remove('pk-dark'); existed.classList.remove('pk-hide'); existed.classList.add('pk-show'); schedulePkToastHide(existed, displayTime); return; } const t = document.createElement('div'); t.className = `pk-msg-toast ${toastType} pk-hide`; t.dataset.pkToastKey = toastKey; if (isDark) t.classList.add('pk-dark'); if (toastType === 'error' || toastType === 'warning') { const icon = pkIconHtml('warning', { style: 'flex-shrink: 0;' }); t.innerHTML = `${icon}${msg}`; t.style.backgroundColor = toastType === 'warning' ? 'rgba(250, 173, 20, 0.95)' : 'rgba(217, 48, 37, 0.95)'; t.style.color = '#ffffff'; if (toastType === 'warning') t.style.border = '1px solid rgba(250, 173, 20, 0.2)'; } else { t.textContent = msg; } container.prepend(t); requestAnimationFrame(() => { t.classList.remove('pk-hide'); t.classList.add('pk-show'); }); schedulePkToastHide(t, displayTime); } function showScriptUpdateToast(info) { const latest = info && info.latestVersion ? String(info.latestVersion) : ''; if (!latest || document.getElementById('pk-script-update-toast') || isScriptUpdateDismissed(latest)) return; let container = ensurePkToastContainer(); const t = document.createElement('div'); t.id = 'pk-script-update-toast'; t.className = 'pk-script-update-toast'; const ov = document.querySelector('.pk-ov'); const savedThemeNow = gmGet('pk_theme', 'auto'); const sysDarkNow = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const isDark = ov ? ov.classList.contains('pk-dark') : (savedThemeNow === 'dark' || (savedThemeNow === 'auto' && sysDarkNow)); if (isDark) t.classList.add('pk-dark'); const msg = esc(L.msg_script_update_new).replace('{v}', `${esc(latest)}`); t.innerHTML = `${msg}`; const openUrl = () => { const m = {tc:'README(tc).md',en:'README(en).md',ko:'README(ko).md',ja:'README(ja).md',id:'README(id).md',ms:'README(ms).md'}; const f = m[getLang()]; window.open(`${CONF.scriptUpdateProjectUrl}/blob/main/${f ? `i18n/${f}` : 'README.md'}`, '_blank', 'noopener,noreferrer'); }; t.onclick = (e) => { if (e.target && e.target.closest('[data-pk-script-update-close]')) return; openUrl(); }; const openBtn = t.querySelector('[data-pk-script-update-open]'); if (openBtn) openBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openUrl(); }; const closeBtn = t.querySelector('[data-pk-script-update-close]'); if (closeBtn) closeBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); markScriptUpdateDismissed(latest); t.style.opacity = '0'; t.style.transform = 'translateY(-15px) scale(0.95)'; setTimeout(() => { if (t.parentNode) t.remove(); }, 300); }; container.prepend(t); requestAnimationFrame(() => { t.style.transform = 'translateY(0) scale(1)'; t.style.opacity = '1'; }); } async function runScriptUpdateCheck() { try { const info = await fetchScriptUpdateInfo(false); if (isScriptUpdateNew(info)) showScriptUpdateToast(info); } catch (e) {} } const getBlacklistLineItems = value => String(value || '').split(/\r?\n/).map(x => x.trim()).filter(Boolean); function getBlacklistAppendStats(storageKey, oldValue, appendValue) { const schema = getConfigLimitSchema(storageKey); const limits = (schema && schema.localLimit) || {}; const maxItemLen = limits.maxItemLen || 512; const oldNorm = normalizeConfigValue(storageKey, oldValue || '', 'localWrite'); const mergedRaw = oldNorm + (oldNorm && !oldNorm.endsWith('\n') ? '\n' : '') + String(appendValue || ''); const normalized = normalizeConfigValue(storageKey, mergedRaw, 'localWrite'); const oldSet = new Set(getBlacklistLineItems(oldNorm)); const newSet = new Set(getBlacklistLineItems(normalized)); let actualAdded = 0; newSet.forEach(item => { if (!oldSet.has(item)) actualAdded++; }); const seen = new Set(oldSet); let expectedAdded = 0; getBlacklistLineItems(appendValue).forEach(line => { const item = safeStringLimit(line, maxItemLen); if (!item || seen.has(item)) return; seen.add(item); expectedAdded++; }); return { normalized, actualAdded, expectedAdded, clipped: actualAdded < expectedAdded }; } function showBlacklistModal() { const icons = { paste: ``, delRow: ``, trash: ``, rocket: ``, save: `` }; const toastStyle = ` position: absolute; top: 120px; left: 50%; transform: translateX(-50%); background: var(--pk-toast-bg); backdrop-filter: blur(10px); color: var(--pk-toast-fg); border: 1px solid var(--pk-toast-bd); padding: 8px 24px; border-radius: 99px; font-size: 13px; z-index: 2000; pointer-events: none; opacity: 0; transition: opacity 0.3s, transform 0.3s; box-shadow: 0 8px 20px var(--pk-tip-sd); text-align: center; font-weight: 500; display: flex; align-items: center; gap: 8px; `; const textareaStyle = ` flex: 1; resize: none; border: 2px solid transparent; border-radius: 8px; padding: 15px; background: var(--pk-hl); color: var(--pk-fg); font-size: 13px; font-family: inherit; cursor: auto; outline: none; line-height: 1.6; letter-spacing: 0.3px; transition: background 0.2s, border-color 0.2s, box-shadow 0.2s; width: 100%; box-sizing: border-box; `; const modalInnerStyle = ` `; const m = showModal(` ${modalInnerStyle}

${L.title_blacklist}

${pkIconHtml('infoCircle', { width: 14, height: 14, style: 'flex-shrink:0;' })} ${L.tip_bl_desc}
`); const modalEl = m.querySelector('.pk-modal'); Object.assign(modalEl.style, { width: '600px', maxWidth: '90vw', padding: '30px', height: '600px', maxHeight: '85vh' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); const areaFolder = m.querySelector('#bl_folder_input'); const areaFile = m.querySelector('#bl_file_input'); const radios = m.querySelectorAll('input[name="bl_mode"]'); const groupFolder = m.querySelector('#group_bl_folder'); const groupFile = m.querySelector('#group_bl_file'); radios.forEach(r => { r.onchange = () => { const mode = r.value; groupFolder.style.display = mode === 'folder' ? 'flex' : 'none'; groupFile.style.display = mode === 'file' ? 'flex' : 'none'; }; }); const toast = m.querySelector('#pk_bl_toast'); const showToast = (msg) => { toast.textContent = msg; toast.style.opacity = '1'; setTimeout(() => { toast.style.opacity = '0'; }, 2000); }; const loadLargeText = async (el, storageKey, originalPlaceholder) => { el.placeholder = L.str_loading_placeholder; await sleep(350); const data = await new Promise(r => setTimeout(() => r(gmGet(storageKey, '')), 0)); if (data) { const prevDisplay = el.style.display; el.style.display = 'none'; el.value = data; el.style.display = prevDisplay; } el.placeholder = originalPlaceholder; el.setSelectionRange(0, 0); el.blur(); }; loadLargeText(areaFolder, 'pk_blacklist_folders', L.ph_bl_file); 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, storageKey) => { 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 stats = getBlacklistAppendStats(storageKey, el.value, cleanText); el.value = stats.normalized; el.scrollTop = el.scrollHeight; showToast(stats.clipped ? L.msg_config_input_over_limit : L.msg_add_success.replace('{n}', stats.actualAdded)); } 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'), 'pk_blacklist_folders'); setupSafeControls(areaFile, m.querySelector('#btn_paste_file'), m.querySelector('#btn_del_file'), 'pk_blacklist'); 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.msg_op_blocked_moving); return; } const fDir = areaFolder.value.trim(); const fFile = areaFile.value.trim(); gmSet('pk_blacklist_folders', fDir); gmSet('pk_blacklist', fFile); S.updateBlCache(); if (!fDir && !fFile) { showToast(L.msg_bl_empty); return; } 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); isGUISensitive = 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); showToast(L.msg_bl_empty, 'warning'); return; } updateLoadTxt(L.str_init_scan); await sleep(50); const foundMatches = []; const rootNodes = [{ id: '', name: 'Root', lineage: [], retryCount: 0 }]; await coreRecursiveEngine(rootNodes, { signal: signal, preferFresh: true, onFile: (f, parent) => { const cleanName = f.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (S.blSet.has(cleanName)) { f._lineage = parent.lineage || []; foundMatches.push({ item: f, type: 'FILE' }); } }, onFolder: (folder, filesInFolder, nextSubFolders) => { const cleanName = folder.name.replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const isSystemRootFolder = (folder.parent_id === '' || folder.parent_id === 'root') && folder.name === CONF.SYSTEM_FOLDER_NAME; if (S.blFolderSet.has(cleanName) && !isSystemRootFolder) { folder._lineage = (folder.lineage || []).slice(0, -1); foundMatches.push({ item: folder, type: 'FOLDER' }); nextSubFolders.length = 0; } }, onProgress: (st) => { const folderText = `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const statusInfo = ` | ${L.str_hits}: ${foundMatches.length} | ${L.str_speed}: ${st.currentConcurrency}`; 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; isGUISensitive = false; scheduleBackgroundResume(); } } }; } function showPreviewModal(matches) { const modalHtml = `

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

${L.col_type}
${L.col_name}
${L.col_path}
`; const m = showModal(modalHtml); const modalEl = m.querySelector('.pk-modal'); Object.assign(modalEl.style, { width: "800px", maxWidth: "90vw", height: "80vh", padding: "0", display: "flex", flexDirection: "column" }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: "24px", right: "24px" }); const listDiv = m.querySelector('#bl_prev_list'); const listIn = m.querySelector('#bl_prev_in'); const chkAll = m.querySelector('#bl_prev_all'); const btnDel = m.querySelector('#bl_prev_del'); const topStat = m.querySelector('#bl_top_stat'); const ROW_HEIGHT = 44; listIn.style.height = `${matches.length * ROW_HEIGHT}px`; const selectedIds = new Set(matches.map(m => m.item.id)); let lastIdx = -1; const updateCount = () => { const c = selectedIds.size; if (topStat) { topStat.innerHTML = L.str_bl_stat .replace('{n}', matches.length) .replace('{m}', `${c}`); } btnDel.disabled = c === 0; btnDel.style.opacity = c === 0 ? 0.5 : 1; btnDel.style.cursor = c === 0 ? 'not-allowed' : 'pointer'; chkAll.checked = c > 0 && c === matches.length; chkAll.indeterminate = c > 0 && c < matches.length; }; updateCount(); let isRenderScheduled = false; const renderPreviewList = () => { const top = listDiv.scrollTop; const h = listDiv.clientHeight || 500; const buffer = 15; const start = Math.max(0, Math.floor(top / ROW_HEIGHT) - buffer); const end = Math.min(matches.length, Math.ceil((top + h) / ROW_HEIGHT) + buffer); listIn.innerHTML = ''; const fragment = document.createDocumentFragment(); for (let i = start; i < end; i++) { const mMatch = matches[i]; const row = document.createElement('div'); row.dataset.id = mMatch.item.id; row.style.cssText = `position:absolute; top:${i * ROW_HEIGHT}px; width:100%; display:grid; grid-template-columns: 40px 50px 1fr 1fr; padding:0; border-bottom:1px dashed var(--pk-bd); font-size:13px; align-items:center; height:${ROW_HEIGHT}px; transition:background 0.1s; cursor:pointer;`; row.onmouseover = () => row.style.backgroundColor = 'var(--pk-hl)'; row.onmouseout = () => row.style.backgroundColor = 'transparent'; const isFolder = mMatch.type === 'FOLDER'; const it = mMatch.item; const mime = (it.mime_type || '').toLowerCase(); const isMedia = mime.startsWith('video/') || mime.startsWith('image/'); if (isFolder && (!it.thumbnail_link || it.thumbnail_link === it.icon_link) && typeof globalCache !== 'undefined') { const scanDeepCover = (targetId, depth) => { if (depth > 5) return null; const raw = globalCache.get(targetId); if (!raw) return null; const files = (raw && !Array.isArray(raw) && raw.items) ? raw.items : raw; if (!files || files.length === 0) return null; const vid = files.find(f => f.mime_type?.startsWith('video/') && f.thumbnail_link); if (vid) return vid.thumbnail_link; const img = files.find(f => f.mime_type?.startsWith('image/') && f.thumbnail_link); if (img) return img.thumbnail_link; const subFolders = files.filter(f => f.kind === 'drive#folder'); for (const sub of subFolders) { if (globalCache.has(sub.id)) { const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; continue; } if (sub.thumbnail_link && sub.thumbnail_link !== sub.icon_link && !sub._coverResolved) { return sub.thumbnail_link; } const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; } return null; }; const foundThumb = scanDeepCover(it.id, 0); if (foundThumb) { it.thumbnail_link = foundThumb; } } const hasCover = it.thumbnail_link && it.thumbnail_link !== it.icon_link; const usableCover = hasCover && !isPreviewIconFailed(it.thumbnail_link); const fallbackSvg = getIcon(it).replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"'); const makePreviewImg = (src, fit, radius = '0') => { const safeSrc = String(src || '').replace(/"/g, '"'); return ``; }; let iconHtml = ''; if (!isFolder && isMedia && usableCover) { iconHtml = makePreviewImg(it.thumbnail_link, 'cover', '4px'); const secondFallback = it.icon_link && !isPreviewIconFailed(it.icon_link) ? `${makePreviewImg(it.icon_link, 'contain')}${fallbackSvg}` : fallbackSvg; iconHtml += `${secondFallback}`; } else { const iconSrc = it.icon_link; iconHtml = iconSrc && !isPreviewIconFailed(iconSrc) ? `${makePreviewImg(iconSrc, 'contain')}${fallbackSvg}` : fallbackSvg; } const lineage = mMatch.item._lineage ||[]; const relativePath = lineage.map(x => x.name).join('/'); const homeIcon = pkIconHtml('home', { width: 14, height: 14, style: 'margin-right:4px;flex-shrink:0;vertical-align:-1px;' }); row.ondragstart = (e) => { e.preventDefault(); return false; }; let pathHtml = `
${homeIcon}${esc(L.btn_nav_home)}`; if (relativePath) { pathHtml += `/${esc(relativePath)}`; } pathHtml += `
`; const homeGroupTip = `${homeIcon}${esc(L.btn_nav_home)}`; const fullPathTip = `
${homeGroupTip}${relativePath ? '/' + esc(relativePath) : ''}
`; const thumbAttr = (typeof usableCover !== 'undefined' && usableCover) ? `data-pk-thumb="${it.thumbnail_link}"` : ''; const isChecked = selectedIds.has(mMatch.item.id); row.innerHTML = `
${iconHtml}
${esc(mMatch.item.name)}
${pathHtml}
`; bindPreviewIconFallback(row); const chk = row.querySelector('input'); row.onclick = (e) => { const curIdx = i; const id = mMatch.item.id; if (e.target === chk && !e.shiftKey && !e.ctrlKey && !e.metaKey) return; if (e.shiftKey && lastIdx !== -1) { const s = Math.min(lastIdx, curIdx); const eIdx = Math.max(lastIdx, curIdx); if (!e.ctrlKey && !e.metaKey) selectedIds.clear(); for (let k = s; k <= eIdx; k++) selectedIds.add(matches[k].item.id); } else if (e.ctrlKey || e.metaKey) { if (selectedIds.has(id)) selectedIds.delete(id); else selectedIds.add(id); } else { selectedIds.clear(); selectedIds.add(id); } lastIdx = curIdx; updateCount(); renderPreviewList(); }; chk.onchange = (e) => { if(e.target.checked) { selectedIds.add(mMatch.item.id); lastIdx = i; } else selectedIds.delete(mMatch.item.id); updateCount(); renderPreviewList(); }; fragment.appendChild(row); } listIn.appendChild(fragment); }; listDiv.onscroll = () => { if (!isRenderScheduled) { requestAnimationFrame(() => { renderPreviewList(); isRenderScheduled = false; }); isRenderScheduled = true; } }; renderPreviewList(); m.addEventListener('click', (e) => { if (m._blockClick) return; if (e.target.closest('[data-id]') || e.target.closest('button') || e.target.closest('input') || e.target.closest('label')) return; if (selectedIds.size > 0) { selectedIds.clear(); lastIdx = -1; updateCount(); renderPreviewList(); } }); chkAll.onchange = (e) => { const checked = e.target.checked; if(checked) { matches.forEach(m => selectedIds.add(m.item.id)); } else { selectedIds.clear(); } updateCount(); renderPreviewList(); }; const mqBox = document.createElement('div'); mqBox.className = 'pk-bl-mq'; listDiv.appendChild(mqBox); let isDragging = false, isMarquee = false, startX = 0, startY = 0; let blScrollSpeed = 0, blScrollRaf = null, blLastX = 0, blLastY = 0, blLastCtrl = false; listDiv.onmousedown = (e) => { if (e.button !== 0 || e.target.closest('input')) return; const rect = getLogicalRect(listDiv); isDragging = true; isMarquee = false; startX = e.clientX - rect.left; startY = e.clientY - rect.top + listDiv.scrollTop; const rawStartX = e.clientX, rawStartY = e.clientY; const updateMarqueeBox = () => { const curRect = getLogicalRect(listDiv); const curX = blLastX - curRect.left; const maxScrollHeight = listIn.offsetHeight; const curY = Math.max(0, Math.min(maxScrollHeight, blLastY - curRect.top + listDiv.scrollTop)); const top = Math.min(startY, curY); const left = Math.min(startX, curX); const width = Math.abs(curX - startX); const height = Math.abs(curY - startY); mqBox.style.display = 'block'; mqBox.style.top = top + 'px'; mqBox.style.left = left + 'px'; mqBox.style.width = width + 'px'; mqBox.style.height = height + 'px'; const sIdx = Math.floor(top / ROW_HEIGHT); const eIdx = Math.floor((top + height) / ROW_HEIGHT); for (let i = 0; i < matches.length; i++) { const isInside = i >= sIdx && i <= eIdx; if (isInside) { selectedIds.add(matches[i].item.id); } else if (!blLastCtrl) { selectedIds.delete(matches[i].item.id); } } updateCount(); if (!isRenderScheduled) { requestAnimationFrame(() => { renderPreviewList(); isRenderScheduled = false; }); isRenderScheduled = true; } }; const runBlScroll = () => { if (!isMarquee || blScrollSpeed === 0) { blScrollRaf = null; return; } listDiv.scrollTop += blScrollSpeed; updateMarqueeBox(); blScrollRaf = requestAnimationFrame(runBlScroll); }; const onMouseMove = (me) => { if (!isDragging) return; if (!isMarquee) { if (Math.abs(me.clientX - rawStartX) > 5 || Math.abs(me.clientY - rawStartY) > 5) { isMarquee = true; if (!me.ctrlKey && !me.metaKey && !me.shiftKey) { selectedIds.clear(); lastIdx = -1; updateCount(); renderPreviewList(); } } else return; } blLastX = me.clientX; blLastY = me.clientY; blLastCtrl = me.ctrlKey || me.metaKey; const curRect = listDiv.getBoundingClientRect(); if (blLastY > curRect.bottom - 5) blScrollSpeed = Math.min(45, 2 + Math.pow((blLastY - curRect.bottom + 5) / 5, 1.3)); else if (blLastY < curRect.top + 5) blScrollSpeed = -Math.min(45, 2 + Math.pow((curRect.top + 5 - blLastY) / 5, 1.3)); else blScrollSpeed = 0; if (blScrollSpeed !== 0 && !blScrollRaf) blScrollRaf = requestAnimationFrame(runBlScroll); updateMarqueeBox(); }; const onMouseUp = () => { if (isDragging && mqBox.style.display === 'block') { m._blockClick = true; setTimeout(() => m._blockClick = false, 100); } isDragging = false; blScrollSpeed = 0; if (blScrollRaf) { cancelAnimationFrame(blScrollRaf); blScrollRaf = null; } mqBox.style.display = 'none'; window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }; m.querySelector('#bl_prev_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 50); m.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) { e.preventDefault(); e.stopPropagation(); matches.forEach(m => selectedIds.add(m.item.id)); updateCount(); renderPreviewList(); } }, true); btnDel.onclick = async () => { if (selectedIds.size === 0) return; const confirmed = await showConfirm(L.warn_del.replace('{n}', selectedIds.size)); if (!confirmed) return; const isHardDelete = m.querySelector('#bl_prev_hard_delete').checked; m.remove(); const allIds = Array.from(selectedIds).filter(id => { const match = matches.find(m => m.item.id === id); if (match && isSystemItem(match.item)) { console.warn(`[Security] Prevented deletion of system folder: ${match.item.name}`); return false; } return true; }); const itemsToDelete = matches.filter(m => allIds.includes(m.item.id)).map(m => m.item); await executeBatchDelete(allIds, { silent: false, explicitItems: itemsToDelete, hardDelete: isHardDelete }); }; } function updateLoadAction(isExitMode = false) { if (!UI.stopBtn) return; UI.stopBtn.removeAttribute('data-pk-tip'); const txtEl = UI.stopBtn.querySelector('span'); if (txtEl) txtEl.textContent = isExitMode ? L.btn_exit_script : L.btn_stop; } function isCurrentStarredRoot() { return !!(S.starredMode && Array.isArray(S.path) && S.path.length === 1); } function isCurrentRecentRoot() { return !!(S.recentMode && Array.isArray(S.path) && S.path.length === 1); } function shouldHideNewFolderButtonForCurrentMode() { const cur = S.path && S.path.length ? S.path[S.path.length - 1] : {}; return !!( S.trashMode || S.shareMode || S.shareParseMode || S.linkBookmarkMode || S.offlineMode || S.uploadMode || S.historyMode || isCurrentStarredRoot() || isCurrentRecentRoot() || S.isFlattened || S.dupMode || (cur && (cur.id === 'analyze_root' || cur.id === 'virtual_search_root')) ); } function shouldHideAnalyzeButtonsForCurrentMode() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : {}; const isInVirtualSearch = path.some(p => p && p.id === 'virtual_search_root'); return !!( S.trashMode || S.shareMode || S.shareParseMode || S.linkBookmarkMode || S.starredMode || S.recentMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || S.analyzeMode || isInVirtualSearch || (cur && (cur.id === 'analyze_root' || cur.id === 'virtual_search_root')) ); } function shouldSuppressRefreshForVirtualResultPage() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; const curId = String((cur && cur.id) || ''); const isSearchResultRoot = curId === 'virtual_search_root'; const isFileInsightResult = !!S.isFlattened || curId === 'file_insight'; const isFileDupResult = !!S.dupMode || curId === 'file_dup'; const isAnalyzeRoot = !!(S.analyzeMode && curId === 'analyze_root'); const isFolderInsightRoot = isAnalyzeRoot && !S.analyzeSimGroups; const isFolderDupRoot = isAnalyzeRoot && !!S.analyzeSimGroups; return !!( isSearchResultRoot || isFileInsightResult || isFileDupResult || isFolderInsightRoot || isFolderDupRoot ); } function syncRefreshButtonVisibility() { if (!UI.btnRefresh) return; const hideRefresh = shouldSuppressRefreshForVirtualResultPage(); if (hideRefresh) { UI.btnRefresh.dataset.pkRefreshSuppressed = '1'; UI.btnRefresh.style.display = 'none'; UI.btnRefresh.disabled = true; return; } const wasSuppressed = UI.btnRefresh.dataset.pkRefreshSuppressed === '1'; delete UI.btnRefresh.dataset.pkRefreshSuppressed; if (wasSuppressed && !S.linkBookmarkMode && !S.shareParseMode && !S.uploadMode) { UI.btnRefresh.style.display = 'inline-flex'; UI.btnRefresh.disabled = false; } } function isGlobalSearchResultContext() { const path = Array.isArray(S.path) ? S.path : []; if (!path.length) return false; return !!S.lastSearchGlobal && path.some(p => String((p && p.id) || '') === 'virtual_search_root'); } function syncGlobalSearchCheckboxState() { if (!UI || !UI.chkGlobal) return; const locked = isGlobalSearchResultContext(); const box = UI.chkGlobal; if (locked) box.checked = true; box.disabled = !!locked; const label = UI.lblGlobal || box.closest('label') || box.parentElement; if (label) { label.classList.toggle('pk-global-search-locked', !!locked); label.setAttribute('aria-disabled', locked ? 'true' : 'false'); } } function shouldSkipSmartRefreshAfterLoad(realCacheKey) { const cacheKey = String(realCacheKey || ''); return !!( (S.recentMode && cacheKey === 'recent_root') || (S.starredMode && cacheKey === 'starred_root') || isCurrentStarredRoot() ); } function syncFirstPageHeavyActionButtons() { const locked = !!S.loading || !S.firstPageReady; const blRun = document.querySelector('#bl_run'); if (S.starredMode) { if (UI.btnNewFolder) { UI.btnNewFolder.style.display = shouldHideNewFolderButtonForCurrentMode() ? 'none' : 'flex'; } if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.scan) UI.scan.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; } [UI.btnExport, UI.btnAnalyze, UI.scan, blRun].forEach(btn => { if (!btn) return; btn.disabled = locked; }); } function setLoad(b, isInline = false) { S.loading = b; S.firstPageReady = !b; syncFirstPageHeavyActionButtons(); if (b) { if (isInline) { UI.loader.style.display = 'none'; } else { UI.loader.style.display = 'flex'; if (UI.loadTxt) UI.loadTxt.textContent = L.loading_detail; updateLoadAction(false); } } else { UI.loader.style.display = 'none'; updateLoadAction(false); if (typeof consumeOfflineLightProbePendingWrite === 'function') consumeOfflineLightProbePendingWrite('load_done'); } } function updateLoadTxt(txt) { if (UI.loadTxt) UI.loadTxt.innerText = txt; updateLoadAction(txt === L.str_waiting_token); } let activeLoadId = 0; const RECENT_LIGHT_PROBE_INTERVAL = 30000; const RECENT_LIGHT_PROBE_ONLINE_COOLDOWN = 10000; function isRecentLightProbeRoot() { const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; return !!(S.recentMode && Array.isArray(S.path) && S.path.length === 1 && cur && cur.id === 'recent_root'); } function getRecentItemTime(item) { return Date.parse((item && (item._recent_event_time || item.event_time || item.added_time || item.created_time || item.modified_time || item.updated_time)) || '') || 0; } function uniqueSortRecentItems(items) { const map = new Map(); (Array.isArray(items) ? items : []).forEach(item => { const id = String((item && item.id) || ''); if (!id || map.has(id)) return; map.set(id, item); }); return Array.from(map.values()).sort((a, b) => getRecentItemTime(b) - getRecentItemTime(a)); } function mapRecentEventsToItems(events) { const latestByFileId = new Map(); (Array.isArray(events) ? events : []).forEach(ev => { const ref = ev && ev.reference_resource; if (!ref || typeof ref !== 'object' || ref.trashed) return; const id = String(ev.file_id || ref.id || ''); if (!id) return; const mime = ref.mime_type || ''; const isFolder = (ref.kind === 'drive#folder') || (mime === 'application/x-directory'); const isFile = (ref.kind === 'drive#file') || (!isFolder && !!mime); if (!isFolder && !isFile) return; const eventTime = ev.event_time || ev.created_time || ev.create_time || ev.updated_time || ref.modified_time || ref.updated_time || ref.created_time || ''; const eventTs = Date.parse(eventTime || '') || 0; const prev = latestByFileId.get(id); if (prev && prev.eventTs >= eventTs) return; latestByFileId.set(id, { eventTs, item: { id, kind: isFolder ? 'drive#folder' : 'drive#file', name: ref.name || ev.file_name || ev.name || id, size: ref.size || ref.file_size || ev.file_size || 0, thumbnail_link: ref.thumbnail_link || ref.icon_link || '', icon_link: ref.icon_link || ev.icon_link || '', web_content_link: ref.web_content_link || '', created_time: eventTime, modified_time: eventTime || ref.modified_time || ref.updated_time || ref.created_time || '', mime_type: mime, parent_id: ref.parent_id || '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))), trashed: false, params: Object.assign({}, ev.params || {}, ref.params || {}), _recent_event_id: String(ev.id || ev.event_id || ''), _recent_event_type: ev.type || '', _recent_event_time: eventTime } }); }); return Array.from(latestByFileId.values()).sort((a, b) => b.eventTs - a.eventTs).map(entry => entry.item); } function getRecentRootSnapshot() { if (typeof globalCache !== 'undefined' && globalCache.has('recent_root')) return globalCache.get('recent_root'); if (S.cache && S.cache.has('recent_root')) return S.cache.get('recent_root'); return null; } function getRecentRootSnapshotItems(snap = getRecentRootSnapshot()) { return snap && !Array.isArray(snap) && Array.isArray(snap.items) ? snap.items : (Array.isArray(snap) ? snap : []); } function getRecentRootSnapshotNextToken(snap = getRecentRootSnapshot()) { return snap && !Array.isArray(snap) ? (snap.nextToken || null) : null; } function markRecentRootPagingActive(loadId, reason = '') { S.recentRootPagingActive = true; S.recentRootPagingLoadId = Number(loadId) || activeLoadId || 0; S.recentRootPagingStartedAt = Date.now(); } function clearRecentRootPagingActive(loadId = 0, reason = '') { const id = Number(loadId) || 0; if (id && S.recentRootPagingLoadId && S.recentRootPagingLoadId !== id) return; S.recentRootPagingActive = false; S.recentRootPagingLoadId = 0; S.recentRootPagingStartedAt = 0; } function isRecentRootPagingActive() { if (!S.recentRootPagingActive) return false; const loadId = Number(S.recentRootPagingLoadId || 0); if (loadId && loadId !== activeLoadId) { clearRecentRootPagingActive(0, 'stale_load'); return false; } const startedAt = Number(S.recentRootPagingStartedAt || 0); if (startedAt && Date.now() - startedAt > 180000) { clearRecentRootPagingActive(loadId, 'timeout_guard'); return false; } return true; } function canRunRecentLightProbe() { if (!isRecentLightProbeRoot()) return false; if (document.hidden) return false; if (S.loading || S.scanning || S.recentLoadingNextPage || S.recentRefreshFirstPageOnly || S.recentLoadNextPageOnly || S.recentLightProbeRunning || isRecentRootPagingActive()) return false; if (S.networkResumeCtx && (S.networkResumeCtx.mode === 'recent' || S.networkResumeCtx.realCacheKey === 'recent_root')) return false; if (S._networkResumeLoad) return false; if (S.search || (UI.searchInput && String(UI.searchInput.value || '').trim())) return false; if (S.getSelectedCount && S.getSelectedCount() > 0) return false; if (UI.ctx && UI.ctx.style.display === 'block') return false; if (typeof navigator !== 'undefined' && navigator.onLine === false) return false; return true; } function stopRecentLightProbe(reason = '') { if (S.recentLightProbeTimer) { clearTimeout(S.recentLightProbeTimer); S.recentLightProbeTimer = 0; } S.recentLightProbeReqId++; S.recentLightProbeRunning = false; if (reason === 'mode_change' || reason === 'manual_refresh') clearRecentRootPagingActive(0, reason); } function scheduleRecentLightProbe(reason = 'timer', delay = RECENT_LIGHT_PROBE_INTERVAL) { if (!isRecentLightProbeRoot()) return; if (S.recentLightProbeTimer) clearTimeout(S.recentLightProbeTimer); S.recentLightProbeTimer = setTimeout(() => { S.recentLightProbeTimer = 0; runRecentLightProbe(reason); }, Math.max(0, Number(delay) || 0)); } async function fetchRecentLightProbeFirstPage(signal) { const url = `https://api-drive.mypikpak.com/drive/v1/events?thumbnail_size=SIZE_MEDIUM&limit=100&_t=${Date.now()}`; const res = await fetch(url, { headers: getHeaders(), signal }); if (!res.ok) throw new Error(`Recent Probe API ${res.status}`); const json = await res.json(); const events = Array.isArray(json.events) ? json.events : (Array.isArray(json.data) ? json.data : []); return mapRecentEventsToItems(events); } async function runRecentLightProbe(reason = 'timer') { if (!isRecentLightProbeRoot()) return; const now = Date.now(); const lastAt = Number(S.recentLightProbeLastAt || 0); if (reason === 'timer' && lastAt && now - lastAt < RECENT_LIGHT_PROBE_INTERVAL) { scheduleRecentLightProbe('timer', RECENT_LIGHT_PROBE_INTERVAL - (now - lastAt)); return; } if (reason === 'online' && lastAt && now - lastAt < RECENT_LIGHT_PROBE_ONLINE_COOLDOWN) { scheduleRecentLightProbe('timer', RECENT_LIGHT_PROBE_INTERVAL); return; } if (!canRunRecentLightProbe()) { if (reason === 'enter' || reason === 'online') scheduleRecentLightProbe(reason, reason === 'enter' ? 1500 : 5000); else scheduleRecentLightProbe('timer', RECENT_LIGHT_PROBE_INTERVAL); return; } const reqId = ++S.recentLightProbeReqId; const loadId = activeLoadId; const pathKey = getLiveManualRefreshPathKey(); const modeKey = getLiveManualRefreshModeKey(); const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; S.recentLightProbeRunning = true; try { const probeItems = await fetchRecentLightProbeFirstPage(controller ? controller.signal : undefined); if ( reqId !== S.recentLightProbeReqId || loadId !== activeLoadId || pathKey !== getLiveManualRefreshPathKey() || modeKey !== getLiveManualRefreshModeKey() || !isRecentLightProbeRoot() || isRecentRootPagingActive() ) return; const oldSnap = getRecentRootSnapshot(); const oldToken = getRecentRootSnapshotNextToken(oldSnap); const cachedItems = getRecentRootSnapshotItems(oldSnap); const baseItems = Array.isArray(S.items) && S.items.length ? S.items : cachedItems; const existingIds = new Set([...(Array.isArray(baseItems) ? baseItems : []), ...cachedItems].map(item => String((item && item.id) || '')).filter(Boolean)); const newItems = probeItems.filter(item => item && item.id && !existingIds.has(String(item.id))); S.recentLightProbeLastAt = Date.now(); if (!newItems.length) return; const mergedItems = uniqueSortRecentItems([...newItems, ...(Array.isArray(baseItems) ? baseItems : [])]); S.items = mergedItems; S.recentResultItems = [...mergedItems]; S.itemMap.clear(); S.items.forEach(item => { if (item && item.id) S.itemMap.set(item.id, item); }); const nextSnap = { items: [...mergedItems], nextToken: oldToken }; S.cache.set('recent_root', nextSnap); if (typeof globalCache !== 'undefined') globalCache.set('recent_root', nextSnap); const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; await refresh(); if (UI && UI.vp) UI.vp.scrollTop = oldScrollTop; updateStat(); } catch (e) { if (!e || e.name !== 'AbortError') console.warn('[Recent] Lightweight probe failed:', e); } finally { if (S.recentLightProbeReqId === reqId) S.recentLightProbeRunning = false; if (isRecentLightProbeRoot()) scheduleRecentLightProbe('timer', RECENT_LIGHT_PROBE_INTERVAL); } } function isHistoryRootView() { const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; return !!(S.historyMode && Array.isArray(S.path) && S.path.length === 1 && cur && cur.id === 'history_root'); } function ensureHistorySessionState() { if (!Array.isArray(S.historyItems)) S.historyItems = []; if (!(S.historyKnownIds instanceof Set)) S.historyKnownIds = new Set(); if (!(S.historyDeletedEventIds instanceof Set)) S.historyDeletedEventIds = new Set(); if (!(S.historyDeletedFileIds instanceof Set)) S.historyDeletedFileIds = new Set(); if (!S.historyTailNextPageToken && S.historyNextPageToken) S.historyTailNextPageToken = S.historyNextPageToken; } function getHistoryItemTime(item) { return Number((item && (item._history_ts || item.historyPlayTime || 0)) || 0); } function getHistoryItemEventId(item) { return String((item && (item._history_event_id || item.officialEventId || item.eventId || '')) || ''); } function isHistoryItemDeletedBySession(item) { const id = String((item && item.id) || ''); const eventId = getHistoryItemEventId(item); return !!( (eventId && S.historyDeletedEventIds && S.historyDeletedEventIds.has(eventId)) || (id && S.historyDeletedFileIds && S.historyDeletedFileIds.has(id)) ); } function normalizeHistorySessionItems(items) { ensureHistorySessionState(); const latestByFile = new Map(); (Array.isArray(items) ? items : []).forEach(item => { if (!item || !item.id || isHistoryItemDeletedBySession(item)) return; const id = String(item.id); const prev = latestByFile.get(id); if (!prev || getHistoryItemTime(item) >= getHistoryItemTime(prev)) latestByFile.set(id, item); }); const out = Array.from(latestByFile.values()).sort((a, b) => getHistoryItemTime(b) - getHistoryItemTime(a)); S.historyKnownIds = new Set(out.map(item => String((item && item.id) || '')).filter(Boolean)); return out; } function getHistorySnapshot() { if (Array.isArray(S.historyItems) && S.historyItems.length) return { items: S.historyItems, nextToken: S.historyTailNextPageToken || '' }; if (S.cache && S.cache.has('history_root')) return S.cache.get('history_root'); if (typeof globalCache !== 'undefined' && globalCache.has('history_root')) return globalCache.get('history_root'); return null; } function getHistorySnapshotItems(snap = getHistorySnapshot()) { return snap && !Array.isArray(snap) && Array.isArray(snap.items) ? snap.items : (Array.isArray(snap) ? snap : []); } function hydrateHistorySessionFromSnapshot(snap = getHistorySnapshot()) { ensureHistorySessionState(); const items = normalizeHistorySessionItems(getHistorySnapshotItems(snap)); if (items.length || !Array.isArray(S.historyItems)) S.historyItems = items; const snapToken = snap && !Array.isArray(snap) ? (snap.nextToken || snap.tailNextToken || '') : ''; if (!S.historyTailNextPageToken && snapToken) S.historyTailNextPageToken = snapToken; if (!S.historyNextPageToken && S.historyTailNextPageToken) S.historyNextPageToken = S.historyTailNextPageToken; return S.historyItems; } function syncHistorySessionCache() { ensureHistorySessionState(); S.historyItems = normalizeHistorySessionItems(S.historyItems); S.historyLoadedAt = Date.now(); const snapshot = { items: [...S.historyItems], nextToken: S.historyTailNextPageToken || '', tailNextToken: S.historyTailNextPageToken || '', loadedPageCount: S.historyLoadedPageCount || 0, updatedAt: S.historyLoadedAt, sourceSig: 'official_events_session' }; if (S.cache) S.cache.set('history_root', snapshot); if (typeof globalCache !== 'undefined') { globalCache.set('history_root', snapshot); globalCache.set('history_session', { nextToken: S.historyTailNextPageToken || '', completed: !S.historyTailNextPageToken, loadedPageCount: S.historyLoadedPageCount || 0, updatedAt: S.historyLoadedAt }); } return snapshot; } function resetHistorySessionState() { if (S.historyAbortController && S.historyAbortController.abort) S.historyAbortController.abort(); if (S.historyPageAbortController && S.historyPageAbortController.abort) S.historyPageAbortController.abort(); S.historyReqId++; S.historyPageReqId++; S.historyItems = []; S.historyNextPageToken = ''; S.historyTailNextPageToken = ''; S.historyKnownIds = new Set(); S.historyDeletedEventIds = new Set(); S.historyDeletedFileIds = new Set(); S.historyPagingCursorStale = false; S.historyLoadedPageCount = 0; S.historyLoadedAt = 0; S.historyDirty = false; S.historyScrollTop = 0; S.historyLoading = false; S.historyFirstPageLoading = false; S.historySilentRevalidating = false; S.historyLoadingMore = false; S.pagingLoading = false; if (S.cache) S.cache.delete('history_root'); if (typeof globalCache !== 'undefined') { globalCache.delete('history_root'); globalCache.delete('history_session'); } } async function fetchOfficialHistoryEventsPage(pageToken = '', signal, limit = 50) { const filters = encodeURIComponent(JSON.stringify({ type: { in: 'TYPE_PLAY' } })); const url = `https://api-drive.mypikpak.com/drive/v1/events?thumbnail_size=SIZE_SMALL&limit=${limit}&filters=${filters}${pageToken ? `&page_token=${encodeURIComponent(pageToken)}` : ''}&_t=${Date.now()}`; const res = await fetch(url, { headers: getHeaders(), signal }); if (!res.ok) throw new Error(`History API ${res.status}`); const data = await res.json(); const events = Array.isArray(data.events) ? data.events : (Array.isArray(data.data) ? data.data : []); return { events, nextToken: data.next_page_token || data.nextPageToken || '' }; } function getOfficialHistoryEventTime(ev, ref) { return Date.parse( (ev && (ev.event_time || ev.play_time || ev.updated_time || ev.created_time || ev.create_time)) || (ref && (ref.modified_time || ref.updated_time || ref.created_time)) || '' ) || Date.now(); } function parseOfficialHistoryEvents(events) { ensureHistorySessionState(); const latestByFileId = new Map(); (Array.isArray(events) ? events : []).forEach(ev => { if (!ev || ev.type && ev.type !== 'TYPE_PLAY') return; const ref = ev.reference_resource || {}; const params = ev.params || {}; const refParams = ref.params || {}; const id = String(ev.file_id || ref.id || ''); if (!id || (S.historyDeletedFileIds && S.historyDeletedFileIds.has(id))) return; const officialEventId = String(ev.id || ev.event_id || ''); if (officialEventId && S.historyDeletedEventIds && S.historyDeletedEventIds.has(officialEventId)) return; const mime = ref.mime_type || ''; const isFolder = (ref.kind === 'drive#folder') || mime === 'application/x-directory'; const playSeconds = parseOfficialPlaySeconds(params.play_seconds); const playDuration = parseOfficialPlaySeconds(params.play_duration || refParams.duration); const playTime = getOfficialHistoryEventTime(ev, ref); const rawF = { id, kind: isFolder ? 'drive#folder' : (ref.kind || 'drive#file'), name: ref.name || ev.file_name || ev.name || id, size: Number(ref.size || ref.file_size || ev.file_size || 0), thumbnail_link: ref.thumbnail_link || ref.icon_link || '', icon_link: ref.icon_link || '', web_content_link: ref.web_content_link || '', created_time: ref.created_time || ev.create_time || ev.created_time || '', modified_time: ref.modified_time || ref.updated_time || ev.updated_time || ev.create_time || ev.created_time || '', mime_type: mime, parent_id: ref.parent_id || '', params: Object.assign({}, refParams, params), starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))), trashed: !!ref.trashed }; const meta = { id, t: playSeconds, d: playDuration || 0, ts: playTime, officialEventId, rawEvent: ev, rawF }; const prev = latestByFileId.get(id); if (!prev || meta.ts >= prev.ts) latestByFileId.set(id, meta); }); return Array.from(latestByFileId.values()).sort((a, b) => b.ts - a.ts); } async function completeHistoryMetas(metas, signal) { const list = Array.isArray(metas) ? metas : []; const CONCURRENCY = 6; for (let i = 0; i < list.length; i += CONCURRENCY) { if (signal && signal.aborted) throw new DOMException('Aborted', 'AbortError'); const chunk = list.slice(i, i + CONCURRENCY); await Promise.all(chunk.map(async meta => { if (!meta || (meta.rawF && meta.rawF.id && meta.rawF.name && (meta.rawF.mime_type || meta.rawF.kind))) return; try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${encodeURIComponent(meta.id)}?thumbnail_size=SIZE_MEDIUM`, { headers: getHeaders(), signal }); if (!res.ok) return; const f = await res.json(); if (f && !f.trashed) meta.rawF = f; } catch (e) { if (e && e.name === 'AbortError') throw e; } })); } return list; } function buildHistoryItem(meta) { if (!meta || !meta.id) return null; const rawF = meta.rawF || { id: meta.id, kind: 'drive#file', name: meta.id, params: {} }; rawF._pkSkipDurationProbe = true; const f = minifyFile(rawF); f._pkSkipDurationProbe = true; f._history_progress = meta.t || 0; const cloudDur = f.params && f.params.duration ? f.params.duration : 0; f._history_duration = meta.d || cloudDur || 0; f._history_ts = meta.ts || Date.now(); f._history_event_id = meta.officialEventId || ''; f.officialEventId = meta.officialEventId || ''; f._history_reference_resource = meta.rawEvent ? meta.rawEvent.reference_resource || null : null; f._history_raw_event = meta.rawEvent || null; return f; } function mergeHistoryMetas(metas) { ensureHistorySessionState(); const built = (Array.isArray(metas) ? metas : []).map(buildHistoryItem).filter(Boolean); S.historyItems = normalizeHistorySessionItems([...built, ...S.historyItems]); syncHistorySessionCache(); return built.length; } async function renderHistorySession(options = {}) { ensureHistorySessionState(); const preserveScroll = options.preserveScroll !== false; const shouldWriteDom = isHistoryRootView(); const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; S.historyItems = normalizeHistorySessionItems(S.historyItems); S.items = [...S.historyItems]; S.itemMap.clear(); S.items.forEach(item => { if (item && item.id) S.itemMap.set(item.id, item); }); S.pagingLoading = !!S.historyLoadingMore; if (!shouldWriteDom) return; if (UI.loader && UI.loader.style.display !== 'none') setLoad(false); await refresh(); if (UI && UI.vp && preserveScroll) { const top = Number.isFinite(S.historyScrollTop) && S.historyScrollTop > 0 ? S.historyScrollTop : oldScrollTop; UI.vp.scrollTop = Math.max(0, top); } updateStat(); } async function loadOfficialHistoryFirstPage(options = {}) { ensureHistorySessionState(); const manualRebuild = !!options.manualRebuild; const hasSessionItems = Array.isArray(S.historyItems) && S.historyItems.length > 0; const reqId = ++S.historyReqId; const loadId = activeLoadId; const modeKey = getLiveManualRefreshModeKey(); const pathKey = getLiveManualRefreshPathKey(); if (S.historyAbortController && S.historyAbortController.abort) S.historyAbortController.abort(); S.historyAbortController = typeof AbortController !== 'undefined' ? new AbortController() : null; const signal = S.historyAbortController ? S.historyAbortController.signal : undefined; S.historyLoading = true; S.historyFirstPageLoading = !hasSessionItems; S.historySilentRevalidating = hasSessionItems && !manualRebuild; if (!hasSessionItems) { setLoad(true, true); updateLoadTxt(L.loading_detail); } else { await renderHistorySession({ preserveScroll: true }); } try { const pageData = await fetchOfficialHistoryEventsPage('', signal, 50); if (signal && signal.aborted) return; const metas = await completeHistoryMetas(parseOfficialHistoryEvents(pageData.events), signal); if (signal && signal.aborted) return; if (reqId !== S.historyReqId || loadId !== activeLoadId || modeKey !== getLiveManualRefreshModeKey() || pathKey !== getLiveManualRefreshPathKey()) { mergeHistoryMetas(metas); return; } if (manualRebuild || !hasSessionItems) { S.historyItems = []; S.historyKnownIds = new Set(); S.historyLoadedPageCount = 0; } const previousTailToken = S.historyTailNextPageToken || ''; mergeHistoryMetas(metas); if (manualRebuild || !previousTailToken || (S.historyLoadedPageCount || 0) <= 1) { S.historyTailNextPageToken = pageData.nextToken || ''; S.historyNextPageToken = S.historyTailNextPageToken; } else if (pageData.nextToken && previousTailToken && pageData.nextToken !== previousTailToken) { S.historyPagingCursorStale = true; S.historyTailNextPageToken = previousTailToken; S.historyNextPageToken = previousTailToken; } S.historyLoadedPageCount = Math.max(S.historyLoadedPageCount || 0, 1); S.historyDirty = false; syncHistorySessionCache(); await renderHistorySession({ preserveScroll: hasSessionItems && !manualRebuild }); } catch (e) { if (!e || e.name !== 'AbortError') console.warn('[OfficialHistory] First page failed:', e); if (!hasSessionItems && isHistoryRootView()) { setLoad(false); updateStat(); } } finally { if (S.historyReqId === reqId) { S.historyLoading = false; S.historyFirstPageLoading = false; S.historySilentRevalidating = false; } if (isHistoryRootView()) { S.pagingLoading = !!S.historyLoadingMore; updateStat(); } } } async function loadOfficialHistoryNextPage() { ensureHistorySessionState(); if (!isHistoryRootView()) return; if (S.historyLoadingMore || S.historyFirstPageLoading) return; const pageToken = S.historyTailNextPageToken || S.historyNextPageToken || ''; if (!pageToken) return; if (!UI.vp || UI.vp.scrollHeight <= UI.vp.clientHeight + 20) return; if (UI.vp.scrollTop + UI.vp.clientHeight < UI.vp.scrollHeight - 300) return; const reqId = ++S.historyPageReqId; const modeKey = getLiveManualRefreshModeKey(); const pathKey = getLiveManualRefreshPathKey(); if (S.historyPageAbortController && S.historyPageAbortController.abort) S.historyPageAbortController.abort(); S.historyPageAbortController = typeof AbortController !== 'undefined' ? new AbortController() : null; const signal = S.historyPageAbortController ? S.historyPageAbortController.signal : undefined; S.historyLoadingMore = true; S.pagingLoading = true; updateStat(); try { const pageData = await fetchOfficialHistoryEventsPage(pageToken, signal, 50); if (signal && signal.aborted) return; const metas = await completeHistoryMetas(parseOfficialHistoryEvents(pageData.events), signal); if (signal && signal.aborted) return; mergeHistoryMetas(metas); S.historyTailNextPageToken = pageData.nextToken || ''; S.historyNextPageToken = S.historyTailNextPageToken; S.historyLoadedPageCount = Math.max(1, Number(S.historyLoadedPageCount || 0)) + 1; S.historyLastPageErrorAt = 0; syncHistorySessionCache(); if (reqId === S.historyPageReqId && modeKey === getLiveManualRefreshModeKey() && pathKey === getLiveManualRefreshPathKey() && isHistoryRootView()) { await renderHistorySession({ preserveScroll: true }); } } catch (e) { if (!e || e.name !== 'AbortError') { const now = Date.now(); if (!S.historyLastPageErrorAt || now - S.historyLastPageErrorAt > 5000) console.warn('[OfficialHistory] Page failed:', e); S.historyLastPageErrorAt = now; } } finally { if (S.historyPageReqId === reqId) { S.historyLoadingMore = false; S.pagingLoading = false; } if (isHistoryRootView()) updateStat(); } } if (typeof window !== 'undefined' && !window.__pkRecentLightProbeOnlineBound) { window.__pkRecentLightProbeOnlineBound = true; window.addEventListener('online', () => { if (typeof window.resetDirectoryGridMediaFallbackState === 'function') window.resetDirectoryGridMediaFallbackState('online'); scheduleRecentLightProbe('online', 0); }); document.addEventListener('visibilitychange', () => { if (!document.hidden && (typeof navigator === 'undefined' || navigator.onLine !== false) && typeof window.resetDirectoryGridMediaFallbackState === 'function') { window.resetDirectoryGridMediaFallbackState('visible-online'); } }); } const OFFLINE_LIGHT_PROBE_INTERVAL = 30000; const OFFLINE_LIGHT_PROBE_LIMIT = 500; const OFFLINE_DELETE_TOMBSTONE_TTL = 8 * 60 * 1000; const OFFLINE_RETRY_TOMBSTONE_TTL = 8 * 60 * 1000; const OFFLINE_LIGHT_PROBE_MIN_DELAY = 3000; const OFFLINE_LIGHT_PROBE_MAX_DELAY = 60000; const OFFLINE_LIGHT_PROBE_REASON_PRIORITY = { add: 1, retry: 2, delete: 3, reference_missing: 4, enter: 5, visible: 6, online: 7, paging_done: 8, pending_write: 9, timer: 10, backoff: 11 }; const OFFLINE_LIGHT_PROBE_REASON_ALIASES = { add_exists: 'add', batch_add: 'add', batch_delete: 'delete', batch_retry: 'retry', retry_exists: 'retry', retry_probe: 'retry', load_done: 'pending_write', search_clear: 'pending_write', context_closed: 'pending_write', scanning_done: 'pending_write' }; function hasOfflineLightProbeSearchState() { return !!(S.search || (UI.searchInput && String(UI.searchInput.value || '').trim())); } function isContextMenuOpenForOfflineProbe() { return !!(UI.ctx && UI.ctx.style.display && UI.ctx.style.display !== 'none'); } function isOfflineLightProbeModeRoot() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; return !!( S.offlineMode === true && !S.trashMode && !S.shareMode && !S.shareParseMode && !S.linkBookmarkMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.uploadMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && path.length === 1 && cur && cur.id === 'offline_root' ); } function isOfflineLightProbeRoot() { return !!(isOfflineLightProbeModeRoot() && !hasOfflineLightProbeSearchState()); } function isOfflinePagingActive() { let session = null; try { session = (typeof globalCache !== 'undefined') ? globalCache.get('offline_session') : null; } catch (e) { session = null; } const sessionPending = !!(session && session.nextToken && !session.completed); return !!(S.offlineMode && (S.offlinePagingPending || sessionPending || (S.pagingLoading && sessionPending))); } function canRunOfflineLightProbe(reason = '') { if (!isOfflineLightProbeRoot()) return false; if (document.hidden) return false; if (typeof navigator !== 'undefined' && navigator.onLine === false) return false; if (S.loading || S.scanning) return false; if (isOfflinePagingActive()) return false; if (hasOfflineLightProbeSearchState()) return false; if (S.getSelectedCount && S.getSelectedCount() > 0) return false; if (isContextMenuOpenForOfflineProbe()) return false; if (S.offlineLightProbeRunning) return false; if (S.networkResumeCtx && (S.networkResumeCtx.mode === 'offline' || S.networkResumeCtx.realCacheKey === 'offline_root')) return false; if (S._networkResumeLoad) return false; return true; } function clearOfflineLightProbeTimer() { if (S.offlineLightProbeTimer) { clearTimeout(S.offlineLightProbeTimer); S.offlineLightProbeTimer = 0; } S.offlineLightProbeTimerReason = ''; S.offlineLightProbeTimerDueAt = 0; } function abortOfflineLightProbe(reason = '') { S.offlineLightProbeReqId++; const controller = S.offlineLightProbeAbortController; S.offlineLightProbeAbortController = null; S.offlineLightProbeRunning = false; S.offlineLightProbePendingReason = ''; if (controller) { try { controller.abort(); } catch (e) {} } } function stopOfflineLightProbe(reason = '') { clearOfflineLightProbeTimer(); abortOfflineLightProbe(reason); if (reason === 'mode_change' || reason === 'manual_refresh' || reason === 'not_root') { S.offlineLightProbePendingReason = ''; S.offlineLightProbePendingWrite = false; } } function normalizeOfflineLightProbeReason(reason = '') { const key = String(reason || 'timer').trim() || 'timer'; return OFFLINE_LIGHT_PROBE_REASON_ALIASES[key] || (Object.prototype.hasOwnProperty.call(OFFLINE_LIGHT_PROBE_REASON_PRIORITY, key) ? key : 'timer'); } function getOfflineLightProbeReasonRank(reason = '') { const key = normalizeOfflineLightProbeReason(reason); return OFFLINE_LIGHT_PROBE_REASON_PRIORITY[key] || OFFLINE_LIGHT_PROBE_REASON_PRIORITY.timer; } function mergeOfflineLightProbePendingReason(currentReason = '', incomingReason = '') { const current = currentReason ? normalizeOfflineLightProbeReason(currentReason) : ''; const incoming = incomingReason ? normalizeOfflineLightProbeReason(incomingReason) : ''; if (!current) return incoming; if (!incoming) return current; const currentRank = getOfflineLightProbeReasonRank(current); const incomingRank = getOfflineLightProbeReasonRank(incoming); if (incomingRank < currentRank) return incoming; if (incomingRank > currentRank) return current; return incoming; } function isOfflineLightProbeFastReason(reason = '') { const key = normalizeOfflineLightProbeReason(reason); return key === 'add' || key === 'retry' || key === 'delete' || key === 'reference_missing'; } function clampOfflineLightProbeDelay(value, min = 0, max = 120000) { const delay = Number(value); if (!Number.isFinite(delay)) return min; return Math.max(min, Math.min(max, delay)); } function normalizeOfflineLightProbeExpiresIn(value) { if (value === undefined || value === null || value === '') return null; const seconds = Number(value); if (!Number.isFinite(seconds) || seconds <= 0) return null; return seconds; } function classifyOfflineLightProbeActivity(activeItems = []) { const items = Array.isArray(activeItems) ? activeItems : []; let hasRunning = false; let hasPending = false; let hasPaused = false; let hasError = false; for (const item of items) { const phase = String((item && (item.phase || item._offlinePhase || (item._offlineTask && item._offlineTask.phase))) || '').toUpperCase(); if (phase === 'PHASE_TYPE_RUNNING') hasRunning = true; else if (phase === 'PHASE_TYPE_PENDING' || phase === 'PHASE_TYPE_UNKNOWN' || phase === 'PHASE_TYPE_UNKNOW') hasPending = true; else if (phase === 'PHASE_TYPE_PAUSED') hasPaused = true; else if (phase === 'PHASE_TYPE_ERROR') hasError = true; } const hasActive = items.length > 0; return { hasActive, hasRunning, hasPending, hasPaused, hasError, onlyError: hasActive && hasError && !hasRunning && !hasPending && !hasPaused, noActive: !hasActive }; } function getOfflineLightProbeActivityDelay(activity = {}) { if (activity.hasRunning) return 4000; if (activity.hasPending) return 6000; if (activity.hasPaused) return 25000; if (activity.onlyError) return 30000; if (activity.noActive) return 45000; if (activity.hasError) return 30000; return OFFLINE_LIGHT_PROBE_INTERVAL; } function getOfflineLightProbeExpiresDelay(activeResult = null, completeResult = null, activity = {}) { const activeExpiresIn = normalizeOfflineLightProbeExpiresIn(activeResult && activeResult.expiresIn); if (activity.hasActive && activeExpiresIn !== null) { return clampOfflineLightProbeDelay(activeExpiresIn * 1000, OFFLINE_LIGHT_PROBE_MIN_DELAY, OFFLINE_LIGHT_PROBE_MAX_DELAY); } const completeExpiresIn = normalizeOfflineLightProbeExpiresIn(completeResult && completeResult.expiresIn); if (completeExpiresIn !== null) { const minDelay = activity.noActive ? OFFLINE_LIGHT_PROBE_INTERVAL : 15000; return clampOfflineLightProbeDelay(completeExpiresIn * 1000, minDelay, OFFLINE_LIGHT_PROBE_MAX_DELAY); } return null; } function getOfflineLightProbeReasonDelay(reason = 'timer') { const key = normalizeOfflineLightProbeReason(reason); if (key === 'backoff') return getOfflineLightProbeBackoffDelay(); let delay = OFFLINE_LIGHT_PROBE_INTERVAL; if (key === 'add' || key === 'retry' || key === 'delete' || key === 'reference_missing') delay = 1000; else if (key === 'enter') delay = 1300; else if (key === 'visible') delay = 1500; else if (key === 'online') delay = 4000; else if (key === 'paging_done') delay = 1500; else if (key === 'pending_write') delay = 1500; if (Number(S.offlineLightProbeFailCount || 0) >= 2 && isOfflineLightProbeFastReason(key)) delay = Math.max(delay, 5000); return delay; } function getOfflineLightProbeDynamicDelay(context = {}) { const activity = context.activity || classifyOfflineLightProbeActivity(context.activeItems || []); const activityDelay = getOfflineLightProbeActivityDelay(activity); const expiresDelay = getOfflineLightProbeExpiresDelay(context.activeResult, context.completeResult, activity); const reasonDelay = getOfflineLightProbeReasonDelay(context.reason || 'timer'); const delays = [reasonDelay, activityDelay]; if (expiresDelay !== null) delays.push(expiresDelay); return clampOfflineLightProbeDelay(Math.min(...delays), 800, 120000); } function getOfflineLightProbeNextDelay(context = {}) { if (context.failed) return getOfflineLightProbeBackoffDelay(); return getOfflineLightProbeDynamicDelay(context); } function getOfflineLightProbeSkipDelay(reason = '') { return getOfflineLightProbeReasonDelay(reason); } function getOfflineLightProbeBackoffDelay() { const count = Math.max(1, Number(S.offlineLightProbeFailCount || 0)); if (count >= 4) return 120000; if (count >= 3) return 60000; if (count >= 2) return 45000; return OFFLINE_LIGHT_PROBE_INTERVAL; } function scheduleOfflineLightProbe(reason = 'timer', delay = null) { const normalizedReason = normalizeOfflineLightProbeReason(reason); if (!isOfflineLightProbeModeRoot()) { stopOfflineLightProbe('not_root'); return; } if (document.hidden) { clearOfflineLightProbeTimer(); return; } if (S.offlineLightProbeRunning) { S.offlineLightProbePendingReason = mergeOfflineLightProbePendingReason(S.offlineLightProbePendingReason, normalizedReason); return; } if (S.offlineLightProbeTimer) { const existingReason = normalizeOfflineLightProbeReason(S.offlineLightProbeTimerReason || 'timer'); const mergedReason = mergeOfflineLightProbePendingReason(existingReason, normalizedReason); if (mergedReason === existingReason && getOfflineLightProbeReasonRank(normalizedReason) > getOfflineLightProbeReasonRank(existingReason)) return; clearOfflineLightProbeTimer(); reason = mergedReason; } else { reason = normalizedReason; } let safeDelay = delay === null || delay === undefined ? getOfflineLightProbeReasonDelay(reason) : Math.max(0, Number(delay) || 0); if (Number(S.offlineLightProbeFailCount || 0) >= 2 && isOfflineLightProbeFastReason(reason)) safeDelay = Math.max(safeDelay, 5000); S.offlineLightProbeTimerReason = reason; S.offlineLightProbeTimerDueAt = Date.now() + safeDelay; S.offlineLightProbeTimer = setTimeout(() => { const runReason = S.offlineLightProbeTimerReason || reason; S.offlineLightProbeTimer = 0; S.offlineLightProbeTimerReason = ''; S.offlineLightProbeTimerDueAt = 0; runOfflineLightProbe(runReason); }, safeDelay); } function consumeOfflineLightProbePendingWrite(reason = 'pending_write') { if (!S.offlineLightProbePendingWrite) return false; if (!isOfflineLightProbeModeRoot()) { S.offlineLightProbePendingWrite = false; S.offlineLightProbePendingReason = ''; return false; } if (document.hidden) return false; const normalizedReason = normalizeOfflineLightProbeReason(reason || 'pending_write'); if (S.offlineLightProbeRunning || canRunOfflineLightProbe(normalizedReason)) { scheduleOfflineLightProbe('pending_write', getOfflineLightProbeReasonDelay('pending_write')); return true; } return false; } function extractOfflineLightProbeTasks(json) { if (!json || typeof json !== 'object') return []; if (Array.isArray(json.tasks)) return json.tasks; if (json.data && Array.isArray(json.data.tasks)) return json.data.tasks; if (Array.isArray(json.data)) return json.data; if (Array.isArray(json.files)) return json.files; return []; } async function fetchOfflineLightProbeFirstPage(group, signal) { const activePhases = 'PHASE_TYPE_UNKNOW,PHASE_TYPE_PENDING,PHASE_TYPE_RUNNING,PHASE_TYPE_PAUSED,PHASE_TYPE_ERROR'; const completePhases = 'PHASE_TYPE_COMPLETE'; const phases = group === 'complete' ? completePhases : activePhases; const filters = encodeURIComponent(JSON.stringify({ phase: { in: phases } })); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?type=offline&limit=${OFFLINE_LIGHT_PROBE_LIMIT}&filters=${filters}&thumbnail_size=SIZE_SMALL&with_reference_resource=true&_t=${Date.now()}`; const res = await fetch(url, { headers: getHeaders(), signal }); if (!res.ok) throw new Error(`Offline Light Probe API ${res.status}`); const json = await res.json(); return { tasks: extractOfflineLightProbeTasks(json), nextPageToken: json.next_page_token || json.nextPageToken || (json.data && (json.data.next_page_token || json.data.nextPageToken)) || null, expiresIn: json.expires_in || json.expiresIn || (json.data && (json.data.expires_in || json.data.expiresIn)) || null }; } function normalizeOfflineTaskToItem(task, group = 'active') { if (!task || typeof task !== 'object') return null; const id = String(task.id || ''); if (!id) return null; const ref = task.reference_resource || {}; const params = Object.assign({}, task.params || {}, ref.params || {}); const phase = task.phase || ''; const progressValue = Number(task.progress || 0); const nameMeta = getOfflineTaskDisplayNameMeta(task, ref, params, group, progressValue); const updatedTime = task.updated_time || task.modified_time || ref.modified_time || ref.updated_time || ''; const completedTime = task.completed_time || (task.params && task.params.completed_time) || ''; const iconLink = task.icon_link || ref.icon_link || ''; const thumbnailLink = task.thumbnail_link || ref.thumbnail_link || iconLink || ''; const sizeValue = Number(task.file_size || (ref && ref.size) || 0); const hasReferenceResource = !!(ref && (ref.id || ref.name || ref.kind || ref.mime_type || ref.thumbnail_link || ref.icon_link || ref.size)); const referenceMissing = isOfflineCompleteTaskLike(Object.assign({}, task, { progress: progressValue }), group) && !hasReferenceResource; const item = { id, task_id: id, kind: 'drive#task', name: nameMeta.name || 'Untitled Task', phase, _offlinePhase: phase, progress: Number.isFinite(progressValue) ? progressValue : 0, message: task.message || task.status_text || '', size: Number.isFinite(sizeValue) ? sizeValue : 0, file_id: task.file_id || (task.params && task.params.file_id) || (ref && ref.id) || '', icon_link: iconLink, thumbnail_link: thumbnailLink, created_time: task.created_time || '', updated_time: updatedTime, modified_time: updatedTime || task.created_time || '', completed_time: completedTime, source_url: (task.params && task.params.url) || task.source_url || '', params, mime_type: (ref && ref.mime_type) || '', _ref_kind: (ref && ref.kind) || '', starred: !!(ref && (ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR')))), _offlineTask: task, _offlineGroup: group, _offlineResourceName: nameMeta.resourceName, _offlineDisplayNameSource: nameMeta.source, _offlineDisplayNameAuthoritative: nameMeta.authoritative }; return referenceMissing ? applyOfflineReferenceMissingPatch(item) : item; } function isOfflineCompletePhase(item) { return String((item && item.phase) || '').toUpperCase() === 'PHASE_TYPE_COMPLETE'; } function isOfflineCompleteTaskLike(taskOrItem, group = '') { if (!taskOrItem || typeof taskOrItem !== 'object') return false; const phase = String(taskOrItem.phase || taskOrItem._offlinePhase || (taskOrItem._offlineTask && taskOrItem._offlineTask.phase) || '').toUpperCase(); if (phase === 'PHASE_TYPE_COMPLETE') return true; if (group === 'complete' || taskOrItem._offlineGroup === 'complete') return true; const progress = Number(taskOrItem.progress || (taskOrItem._offlineTask && taskOrItem._offlineTask.progress) || 0); return Number.isFinite(progress) && progress >= 100; } function getOfflineTaskDisplayNameMeta(task, ref = {}, params = {}, group = 'active', progressValue = 0) { const complete = isOfflineCompleteTaskLike(Object.assign({}, task || {}, { progress: progressValue }), group); const refName = String((ref && ref.name) || '').trim(); const taskName = String((task && task.name) || '').trim(); const paramName = String((params && params.name) || (task && task.params && task.params.name) || '').trim(); const fileName = String((task && task.file_name) || '').trim(); if (complete && refName) return { name: refName, source: 'reference_resource', authoritative: true, resourceName: refName }; if (taskName) return { name: taskName, source: 'task', authoritative: false, resourceName: refName }; if (refName) return { name: refName, source: 'reference_resource', authoritative: complete, resourceName: refName }; if (paramName) return { name: paramName, source: 'params', authoritative: false, resourceName: '' }; if (fileName) return { name: fileName, source: 'file_name', authoritative: false, resourceName: '' }; return { name: '', source: 'empty', authoritative: false, resourceName: '' }; } function getOfflineLightItemGroup(item) { if (isOfflineCompletePhase(item)) return 'complete'; return (item && item._offlineGroup === 'complete') ? 'complete' : 'active'; } function hasOfflineLightValue(value) { return value !== undefined && value !== null && value !== ''; } function getOfflineReferenceMissingText() { const L = getStrings(); return L.msg_offline_reference_missing || L.err_item_deleted || ''; } function isOfflineReferenceMissingMarked(item) { return !!(item && (item._offlineReferenceMissing === true || item._offlineFileMissing === true)); } function isOfflineReferenceMissingLocalMarked(item) { return !!(item && (item._offlineReferenceMissingLocal === true || item._offlineReferenceMissingSource === 'local')); } function getOfflineReferenceLookupId(item) { if (!item || typeof item !== 'object') return ''; if (String(item.kind || '').toLowerCase().includes('task') || item._offlineTask || item._offlinePhase) { return String(item.file_id || (item.params && item.params.file_id) || (item.reference_resource && item.reference_resource.id) || '').trim(); } return String(item.id || item.file_id || '').trim(); } function isOfflineReferenceMissingError(error) { if (!error) return false; const status = Number(error.status || error.statusCode || (error.response && (error.response.status || error.response.statusCode)) || 0); if (status === 404) return true; const parts = []; const push = v => { if (v === undefined || v === null) return; parts.push(String(v)); }; push(error.status); push(error.statusCode); push(error.code); push(error.error); push(error.message); push(error.name); const data = error.data && typeof error.data === 'object' ? error.data : null; if (data) { ['status', 'statusCode', 'code', 'error', 'error_code', 'error_description', 'message', 'msg', 'reason', 'detail'].forEach(k => push(data[k])); if (data.error && typeof data.error === 'object') { ['status', 'statusCode', 'code', 'error', 'error_code', 'error_description', 'message', 'msg', 'reason', 'detail'].forEach(k => push(data.error[k])); } } const lower = parts.join(' ').toLowerCase(); return (CONF.offlineReferenceMissingCodes || []).some(code => { const key = String(code || '').toLowerCase().trim(); return key && lower.includes(key); }); } function isCompletedOfflineTaskItem(item) { if (!item || typeof item !== 'object') return false; const kind = String(item.kind || '').toLowerCase(); const looksLikeTask = kind.includes('task') || !!item._offlineTask || !!item.task_id || !!item._offlinePhase || item._offlineGroup === 'complete'; return !!(looksLikeTask && isOfflineCompleteTaskLike(item)); } function applyOfflineReferenceMissingPatch(item, patch = {}) { const missingText = patch._offlineMissingMessage || getOfflineReferenceMissingText(); return Object.assign({}, item || {}, patch, { _offlineReferenceMissing: true, _offlineFileMissing: true, _offlineMissingMessage: missingText, _offlineProgressDisplay: '-', message: missingText, status_text: missingText }); } function canWriteOfflineReferenceMissingLocalState(context = {}) { return !!( isOfflineLightProbeModeRoot() && !document.hidden && !isOfflineUserActionLocalWriteBlockedByLoading() && !S.scanning ); } async function patchOfflineReferenceMissingLocally(item, context = {}) { const id = getOfflineTaskPrimaryId(item); if (!id || !isCompletedOfflineTaskItem(item)) return false; if (isOfflineTaskDeletedByTombstone(id) || isOfflineTaskBlockedByRetryTombstone(id)) return false; if (!canWriteOfflineReferenceMissingLocalState(context)) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return false; } const patchItem = oldItem => applyOfflineReferenceMissingPatch(oldItem || item, { _offlineReferenceMissingLocal: true, _offlineReferenceMissingSource: 'local' }); let changed = false; let cacheChanged = false; let nextItems = S.items; let nextDisplay = S.display; const patchList = (list) => { if (!Array.isArray(list)) return list; const index = list.findIndex(row => getOfflineTaskPrimaryId(row) === id); if (index < 0) return list; const oldItem = list[index]; const newItem = patchItem(oldItem); if (getOfflineLightItemSignature(oldItem) === getOfflineLightItemSignature(newItem)) return list; const out = list.slice(); out[index] = newItem; return out; }; const patchedItems = patchList(S.items); if (patchedItems !== S.items) { changed = true; nextItems = patchedItems; } const patchedDisplay = patchList(S.display); if (patchedDisplay !== S.display) { changed = true; nextDisplay = patchedDisplay; } const cacheItems = getOfflineRootCacheItems(); const patchedCache = patchList(cacheItems); if (patchedCache !== cacheItems) cacheChanged = true; if (!changed && !cacheChanged) return false; const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; if (changed) { S.items = nextItems; S.display = nextDisplay; syncOfflineLightProbeStores(S.items); } if (cacheChanged) writeOfflineRootCacheItems(patchedCache); else if (changed && hasOfflineRootCacheItems() && isOfflineLightProbeModeRoot() && !hasOfflineLightProbeSearchState()) writeOfflineRootCacheItems(S.items); if (typeof renderVisible === 'function') renderVisible(); else if (typeof refresh === 'function') await refresh(); if (UI && UI.vp && canWriteOfflineReferenceMissingLocalState(context)) UI.vp.scrollTop = oldScrollTop; updateStat(); scheduleOfflineReferenceProbe(context.reason || context.source || 'reference_missing', context.delay || 1000); return true; } async function markOfflineReferenceMissingFromError(item, error, context = {}) { if (!isCompletedOfflineTaskItem(item) || !isOfflineReferenceMissingError(error)) return false; return patchOfflineReferenceMissingLocally(item, context); } function scheduleOfflineReferenceProbe(reason = 'reference_missing', delay = 1000) { if (!isOfflineLightProbeModeRoot()) return false; scheduleOfflineLightProbe(reason || 'reference_missing', delay); return true; } function mergeOfflineLightItem(existing, incoming) { if (!existing) return Object.assign({}, incoming); const out = Object.assign({}, existing); const id = String(incoming.id || existing.id || ''); out.id = id; out.task_id = id; out.kind = incoming.kind || existing.kind || 'drive#task'; ['phase', '_offlinePhase', '_offlineGroup', '_offlineTask', 'progress', 'starred'].forEach(key => { if (incoming[key] !== undefined) out[key] = incoming[key]; }); const incomingNameIsAuthoritative = !!incoming._offlineDisplayNameAuthoritative && hasOfflineLightValue(incoming.name); const existingNameIsAuthoritative = !!out._offlineDisplayNameAuthoritative && hasOfflineLightValue(out.name); if (incomingNameIsAuthoritative || (!existingNameIsAuthoritative && (hasOfflineLightValue(incoming.name) || !hasOfflineLightValue(out.name)))) { out.name = incoming.name; out._offlineDisplayNameSource = incoming._offlineDisplayNameSource; out._offlineDisplayNameAuthoritative = !!incoming._offlineDisplayNameAuthoritative; } else if (!hasOfflineLightValue(out.name) && hasOfflineLightValue(incoming.name)) { out.name = incoming.name; } if (hasOfflineLightValue(incoming._offlineResourceName) || !hasOfflineLightValue(out._offlineResourceName)) out._offlineResourceName = incoming._offlineResourceName; ['_offlineDisplayNameSource', '_offlineDisplayNameAuthoritative'].forEach(key => { if (!existingNameIsAuthoritative && incoming[key] !== undefined) out[key] = incoming[key]; }); ['message', 'file_id', 'icon_link', 'thumbnail_link', 'created_time', 'updated_time', 'modified_time', 'completed_time', 'source_url', 'mime_type', '_ref_kind'].forEach(key => { if (hasOfflineLightValue(incoming[key]) || !hasOfflineLightValue(out[key])) out[key] = incoming[key]; }); if (Number(incoming.size || 0) > 0 || !Number(out.size || 0)) out.size = incoming.size; out.params = Object.assign({}, existing.params || {}, incoming.params || {}); if (incoming._offlineReferenceMissing === true || incoming._offlineFileMissing === true) { out._offlineReferenceMissing = true; out._offlineFileMissing = true; out._offlineMissingMessage = incoming._offlineMissingMessage || getOfflineReferenceMissingText(); out._offlineProgressDisplay = '-'; } else if (incoming._offlineReferenceMissing === false || incoming._offlineFileMissing === false) { out._offlineReferenceMissing = false; out._offlineFileMissing = false; out._offlineMissingMessage = ''; out._offlineProgressDisplay = ''; } else if (existing._offlineReferenceMissing === true || existing._offlineFileMissing === true) { out._offlineReferenceMissing = true; out._offlineFileMissing = true; out._offlineMissingMessage = existing._offlineMissingMessage || getOfflineReferenceMissingText(); out._offlineProgressDisplay = '-'; } if (out._offlineReferenceMissing || out._offlineFileMissing) { out.message = out._offlineMissingMessage || getOfflineReferenceMissingText(); out.status_text = out.message; out._offlineProgressDisplay = '-'; if (incoming._offlineReferenceMissingLocal === true || existing._offlineReferenceMissingLocal === true) { out._offlineReferenceMissingLocal = true; out._offlineReferenceMissingSource = 'local'; } } else { out._offlineReferenceMissingLocal = false; out._offlineReferenceMissingSource = ''; } return out; } function getOfflineDeleteTombstoneStore() { if (!S.offlineDeleteTombstones || !(S.offlineDeleteTombstones instanceof Map)) S.offlineDeleteTombstones = new Map(); return S.offlineDeleteTombstones; } function cloneOfflineDeleteItem(item) { if (!item || typeof item !== 'object') return item; try { if (typeof structuredClone === 'function') return structuredClone(item); } catch (e) {} try { return JSON.parse(JSON.stringify(item)); } catch (e) { return Object.assign({}, item); } } function cleanupOfflineDeleteTombstones(now = Date.now()) { const store = getOfflineDeleteTombstoneStore(); store.forEach((stone, taskId) => { if (!stone || Number(stone.expireAt || 0) <= now) store.delete(taskId); }); return store; } function addOfflineDeleteTombstone(taskId, snapshot = {}) { const id = getOfflineTaskPrimaryId(taskId); if (!id) return null; const now = Date.now(); const store = cleanupOfflineDeleteTombstones(now); const stone = { taskId: id, reason: 'delete', oldSnapshot: snapshot.oldSnapshot ? cloneOfflineDeleteItem(snapshot.oldSnapshot) : null, oldIndex: Number.isFinite(snapshot.oldIndex) ? snapshot.oldIndex : -1, oldGroup: snapshot.oldGroup || 'active', oldSelected: !!snapshot.oldSelected, createdAt: now, expireAt: now + OFFLINE_DELETE_TOMBSTONE_TTL, status: snapshot.status || 'pending', snapshot }; store.set(id, stone); return stone; } function removeOfflineDeleteTombstone(taskId) { const id = getOfflineTaskPrimaryId(taskId); if (!id) return false; return getOfflineDeleteTombstoneStore().delete(id); } function markOfflineDeleteTombstone(taskId, status) { const id = getOfflineTaskPrimaryId(taskId); if (!id) return false; const store = cleanupOfflineDeleteTombstones(); const stone = store.get(id); if (!stone) return false; stone.status = status; if (stone.snapshot) stone.snapshot.status = status; return true; } function isOfflineTaskDeletedByTombstone(taskOrItem) { const id = getOfflineTaskPrimaryId(taskOrItem); if (!id) return false; return cleanupOfflineDeleteTombstones().has(id); } function filterOfflineDeletedTombstones(items) { cleanupOfflineDeleteTombstones(); return (Array.isArray(items) ? items : []).filter(item => { const id = getOfflineTaskPrimaryId(item); return !id || !isOfflineTaskDeletedByTombstone(id); }); } function getOfflineRetryTombstoneStore() { if (!S.offlineRetryTombstones || !(S.offlineRetryTombstones instanceof Map)) S.offlineRetryTombstones = new Map(); return S.offlineRetryTombstones; } function cleanupOfflineRetryTombstones(now = Date.now()) { const store = getOfflineRetryTombstoneStore(); store.forEach((stone, oldTaskId) => { if (!stone || Number(stone.expireAt || 0) <= now) { store.delete(oldTaskId); return; } if (isOfflineTaskDeletedByTombstone(stone.oldTaskId) || (stone.newTaskId && isOfflineTaskDeletedByTombstone(stone.newTaskId))) { store.delete(oldTaskId); } }); return store; } function addOfflineRetryTombstone(oldTaskId, retryInfo = {}) { const oldId = getOfflineTaskPrimaryId(oldTaskId); if (!oldId || isOfflineTaskDeletedByTombstone(oldId)) return null; const newId = getOfflineTaskPrimaryId(retryInfo.newTaskId || retryInfo.newSnapshot); if (newId && isOfflineTaskDeletedByTombstone(newId)) return null; const now = Date.now(); const store = cleanupOfflineRetryTombstones(now); const oldSnapshot = retryInfo.oldSnapshot ? cloneOfflineDeleteItem(retryInfo.oldSnapshot) : null; const newSnapshot = retryInfo.newSnapshot ? cloneOfflineDeleteItem(retryInfo.newSnapshot) : null; const stone = { oldTaskId: oldId, newTaskId: newId || '', reason: 'retry', oldSnapshot, newSnapshot, oldIndex: Number.isFinite(retryInfo.oldIndex) ? retryInfo.oldIndex : -1, oldGroup: retryInfo.oldGroup || 'active', oldSelected: !!retryInfo.oldSelected, sourceUrl: retryInfo.sourceUrl || '', createdAt: now, expireAt: now + OFFLINE_RETRY_TOMBSTONE_TTL, status: retryInfo.status || 'created' }; store.set(oldId, stone); return stone; } function removeOfflineRetryTombstone(oldTaskId) { const id = getOfflineTaskPrimaryId(oldTaskId); if (!id) return false; return getOfflineRetryTombstoneStore().delete(id); } function markOfflineRetryTombstone(oldTaskId, status, patch = {}) { const id = getOfflineTaskPrimaryId(oldTaskId); if (!id) return false; const store = cleanupOfflineRetryTombstones(); const stone = store.get(id); if (!stone) return false; stone.status = status || stone.status; Object.assign(stone, patch || {}); return true; } function isOfflineTaskBlockedByRetryTombstone(taskOrItem) { const id = getOfflineTaskPrimaryId(taskOrItem); if (!id || isOfflineTaskDeletedByTombstone(id)) return false; return cleanupOfflineRetryTombstones().has(id); } function filterOfflineRetryTombstones(items) { cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); return filterOfflineDeletedTombstones(items).filter(item => { const id = getOfflineTaskPrimaryId(item); return !id || !isOfflineTaskBlockedByRetryTombstone(id); }); } function getOfflineLightSortTime(item, group) { const value = group === 'complete' ? (item.completed_time || item.updated_time || item.modified_time || item.created_time) : (item.updated_time || item.modified_time || item.created_time); return Date.parse(value || '') || 0; } function getOfflineLightItemSignature(item) { if (!item) return ''; return [ item.id || '', getOfflineLightItemGroup(item), item.phase || '', Number(item.progress || 0), item.message || '', item.name || '', item._offlineResourceName || '', item._offlineDisplayNameSource || '', item._offlineDisplayNameAuthoritative ? '1' : '0', item._offlineReferenceMissing || item._offlineFileMissing ? '1' : '0', item._offlineReferenceMissingLocal ? '1' : '0', item._offlineReferenceMissingSource || '', item._offlineMissingMessage || '', item._offlineProgressDisplay || '', Number(item.size || 0), item.file_id || '', item.thumbnail_link || '', item.icon_link || '', item.created_time || '', item.updated_time || item.modified_time || '', item.completed_time || '' ].join('\u001f'); } function getOfflineLightListSignature(items) { return (Array.isArray(items) ? items : []).map(getOfflineLightItemSignature).join('\u001e'); } function sortOfflineLightGroup(items, group, oldOrder) { return items.slice().sort((a, b) => { const ta = getOfflineLightSortTime(a, group); const tb = getOfflineLightSortTime(b, group); if (ta !== tb) return tb - ta; const ai = oldOrder.has(String(a.id || '')) ? oldOrder.get(String(a.id || '')) : -1; const bi = oldOrder.has(String(b.id || '')) ? oldOrder.get(String(b.id || '')) : -1; if (ai !== bi) return ai - bi; return String(a.id || '').localeCompare(String(b.id || '')); }); } function mergeOfflineLightProbeItems(currentItems, activeProbeItems, completeProbeItems) { cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); const current = []; const currentSeen = new Set(); (Array.isArray(currentItems) ? filterOfflineRetryTombstones(currentItems) : []).forEach(item => { const id = String((item && item.id) || ''); if (!id || currentSeen.has(id)) return; currentSeen.add(id); current.push(item); }); const beforeSig = getOfflineLightListSignature(current); const byId = new Map(); const groupById = new Map(); const oldOrder = new Map(); current.forEach((item, index) => { const id = String(item.id || ''); byId.set(id, item); groupById.set(id, getOfflineLightItemGroup(item)); oldOrder.set(id, index); }); const probeById = new Map(); const probeGroupById = new Map(); (Array.isArray(activeProbeItems) ? filterOfflineRetryTombstones(activeProbeItems) : []).forEach(item => { const id = String((item && item.id) || ''); if (!id) return; probeById.set(id, item); probeGroupById.set(id, 'active'); }); (Array.isArray(completeProbeItems) ? filterOfflineRetryTombstones(completeProbeItems) : []).forEach(item => { const id = String((item && item.id) || ''); if (!id) return; probeById.set(id, item); probeGroupById.set(id, 'complete'); }); probeById.forEach((item, id) => { const merged = mergeOfflineLightItem(byId.get(id), item); const group = isOfflineCompletePhase(merged) ? 'complete' : (probeGroupById.get(id) || getOfflineLightItemGroup(merged)); merged._offlineGroup = group; byId.set(id, merged); groupById.set(id, group); }); const active = []; const complete = []; byId.forEach((item, id) => { const group = groupById.get(id) === 'complete' || isOfflineCompletePhase(item) ? 'complete' : 'active'; item._offlineGroup = group; if (group === 'complete') complete.push(item); else active.push(item); }); const mergedItems = sortOfflineLightGroup(active, 'active', oldOrder).concat(sortOfflineLightGroup(complete, 'complete', oldOrder)); return { items: mergedItems, changed: beforeSig !== getOfflineLightListSignature(mergedItems) }; } function getOfflineRootSnapshot() { if (typeof globalCache !== 'undefined' && globalCache.has('offline_root')) return globalCache.get('offline_root'); if (S.cache && S.cache.has('offline_root')) return S.cache.get('offline_root'); return null; } function makeOfflineRootSnapshotWithItems(items) { const oldSnap = getOfflineRootSnapshot(); const nextItems = Array.isArray(items) ? [...items] : []; if (oldSnap && !Array.isArray(oldSnap) && typeof oldSnap === 'object') { const nextSnap = Object.assign({}, oldSnap, { items: nextItems }); if (oldSnap.fetchedIds instanceof Set) { const fetchedIds = new Set(oldSnap.fetchedIds); fetchedIds.forEach(id => { if (isOfflineTaskDeletedByTombstone(id) || isOfflineTaskBlockedByRetryTombstone(id)) fetchedIds.delete(id); }); nextItems.forEach(item => { if (item && item.id) fetchedIds.add(item.id); }); nextSnap.fetchedIds = fetchedIds; } return nextSnap; } return { items: nextItems, nextToken: null }; } function syncOfflineLightProbeStores(items) { S.itemMap.clear(); (Array.isArray(items) ? items : []).forEach(item => { if (!item || !item.id) return; S.itemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) S.starredSet.add(item.id); else S.starredSet.delete(item.id); }); } function isOfflineLightProbeContextValid(ctx) { return !!( ctx && ctx.reqId === S.offlineLightProbeReqId && ctx.loadId === activeLoadId && ctx.pathKey === getLiveManualRefreshPathKey() && ctx.modeKey === getLiveManualRefreshModeKey() && isOfflineLightProbeRoot() && !document.hidden && !S.loading && !S.scanning && !isOfflinePagingActive() && !hasOfflineLightProbeSearchState() && (!S.getSelectedCount || S.getSelectedCount() === 0) && !isContextMenuOpenForOfflineProbe() ); } async function mergeOfflineLightProbeResult(ctx, activeProbeItems, completeProbeItems) { if (!isOfflineLightProbeContextValid(ctx)) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return false; } const result = mergeOfflineLightProbeItems(S.items, activeProbeItems, completeProbeItems); S.offlineLightProbePendingWrite = false; if (!result.changed) return false; if (!isOfflineLightProbeContextValid(ctx)) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return false; } const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; S.items = result.items; S.display = result.items; syncOfflineLightProbeStores(S.items); const nextSnap = makeOfflineRootSnapshotWithItems(S.items); S.cache.set('offline_root', nextSnap); if (typeof globalCache !== 'undefined') globalCache.set('offline_root', nextSnap); await refresh(); if (UI && UI.vp && isOfflineLightProbeContextValid(ctx)) UI.vp.scrollTop = oldScrollTop; updateStat(); return true; } function isOfflineTaskAlreadyExistsError(error) { const L = getStrings(); const parts = []; const push = v => { if (v === undefined || v === null) return; parts.push(String(v)); }; push(error && error.message); push(error && error.code); push(error && error.error); push(error && error.errorCode); push(error && error.status_text); const data = error && error.data && typeof error.data === 'object' ? error.data : error; if (data && typeof data === 'object') { ['error', 'error_description', 'message', 'msg', 'status_text', 'description', 'detail', 'reason', 'code'].forEach(k => push(data[k])); if (data.error && typeof data.error === 'object') { ['error', 'error_description', 'message', 'msg', 'status_text', 'description', 'detail', 'reason', 'code'].forEach(k => push(data.error[k])); } } const raw = parts.join(' '); const lower = raw.toLowerCase(); return !!( raw.includes(L.err_task_exists) || /任务已存在|任務已存在|已存在任务|已存在任務/.test(raw) || /already[\s_-]*(exists|exist)|task[\s\S]{0,40}(exists|exist)|duplicate[\s_-]*(task|url|resource)|file[_\s-]*exists?|resource[_\s-]*exists?/.test(lower) ); } function isOfflineTaskAlreadyExistsResult(result) { if (!result || typeof result !== 'object') return false; return isOfflineTaskAlreadyExistsError({ data: result }); } function isOfflineTaskCandidateFromAddResult(task) { if (!task || typeof task !== 'object') return false; const id = String(task.id || '').trim(); if (!id) return false; const phase = String(task.phase || '').toUpperCase(); const type = String(task.type || task.kind || task.upload_type || '').toLowerCase(); return !!(phase || type || task.reference_resource || task.params || task.file_id || task.url || id); } function extractOfflineTaskFromAddResult(result) { if (!result || typeof result !== 'object') return null; const data = result.data && typeof result.data === 'object' ? result.data : null; const file = result.file && typeof result.file === 'object' ? result.file : null; const dataFile = data && data.file && typeof data.file === 'object' ? data.file : null; const candidates = [ result.task, file && file.task, data && data.task, Array.isArray(result.tasks) ? result.tasks[0] : null, data && Array.isArray(data.tasks) ? data.tasks[0] : null, file, dataFile, result ]; for (const candidate of candidates) { if (isOfflineTaskCandidateFromAddResult(candidate)) return candidate; } return null; } function isOfflineUserActionLocalWriteBlockedByLoading() { if (!S.loading) return false; return !isOfflinePagingActive(); } function canWriteOfflineAddedTasks() { return !!( isOfflineLightProbeRoot() && !document.hidden && !isOfflineUserActionLocalWriteBlockedByLoading() && !S.scanning && !hasOfflineLightProbeSearchState() ); } function scheduleOfflineTaskAddProbe(reason = 'add', delay = 1000) { if (!isOfflineLightProbeModeRoot()) return false; scheduleOfflineLightProbe(reason || 'add', delay); return true; } async function insertOfflineAddedTasks(tasks, context = {}) { cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); const sourceTasks = Array.isArray(tasks) ? tasks : [tasks]; const seen = new Set(); const activeItems = []; const completeItems = []; let nowIso = ''; for (const task of sourceTasks) { const id = String((task && task.id) || '').trim(); if (!id || seen.has(id)) continue; if (isOfflineTaskDeletedByTombstone(id) || isOfflineTaskBlockedByRetryTombstone(id)) continue; seen.add(id); const group = String(task.phase || '').toUpperCase() === 'PHASE_TYPE_COMPLETE' ? 'complete' : 'active'; const item = normalizeOfflineTaskToItem(task, group); if (!item || !item.id) continue; if (!nowIso) { try { nowIso = new Date(typeof getServerNow === 'function' ? getServerNow() : Date.now()).toISOString(); } catch (e) { nowIso = new Date().toISOString(); } } if (group === 'complete') { if (!item.completed_time) item.completed_time = nowIso; if (!item.updated_time && !item.modified_time) item.updated_time = nowIso; completeItems.push(item); } else { if (!item.updated_time && !item.modified_time && !item.created_time) { item.created_time = nowIso; item.updated_time = nowIso; item.modified_time = nowIso; } activeItems.push(item); } } if (!activeItems.length && !completeItems.length) return false; if (!canWriteOfflineAddedTasks()) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return false; } const result = mergeOfflineLightProbeItems(S.items, activeItems, completeItems); if (!result.changed) return false; if (!canWriteOfflineAddedTasks()) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return false; } const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; S.items = result.items; S.display = result.items; syncOfflineLightProbeStores(S.items); const nextSnap = makeOfflineRootSnapshotWithItems(S.items); S.cache.set('offline_root', nextSnap); if (typeof globalCache !== 'undefined') globalCache.set('offline_root', nextSnap); await refresh(); if (UI && UI.vp && canWriteOfflineAddedTasks()) UI.vp.scrollTop = oldScrollTop; updateStat(); return true; } async function handleOfflineTaskAdded(result, context = {}) { const ctx = Object.assign({ source: 'unknown', batch: false, schedule: true }, context || {}); const results = Array.isArray(result) ? result : [result]; const tasks = []; const seen = new Set(); let exists = false; for (const item of results) { if (!item) continue; if (isOfflineTaskAlreadyExistsResult(item)) { exists = true; continue; } const task = extractOfflineTaskFromAddResult(item); const id = String((task && task.id) || '').trim(); if (!id || seen.has(id)) continue; seen.add(id); tasks.push(task); } const inserted = tasks.length ? await insertOfflineAddedTasks(tasks, ctx) : false; let scheduled = false; if (ctx.schedule !== false && (tasks.length || exists || results.some(Boolean))) { const reason = ctx.reason || (exists ? 'add_exists' : (ctx.batch ? 'batch_add' : 'add')); scheduled = scheduleOfflineTaskAddProbe(reason, ctx.delay || 1000); } return { inserted, scheduled, taskCount: tasks.length, exists }; } function handleOfflineTaskAddError(error, context = {}) { if (!isOfflineTaskAlreadyExistsError(error)) return false; scheduleOfflineTaskAddProbe((context && context.reason) || 'add_exists', (context && context.delay) || 1000); return true; } function isOfflineUserDeleteTaskItem(item) { if (!item || typeof item !== 'object') return false; const id = getOfflineTaskPrimaryId(item); if (!id) return false; const kind = String(item.kind || '').toLowerCase(); return !!( kind.includes('task') || item._offlineTask || item.task_id || item._offlinePhase || String(item.phase || '').startsWith('PHASE_TYPE_') ); } function getOfflineRootCacheItems() { const snap = getOfflineRootSnapshot(); if (Array.isArray(snap)) return snap; if (snap && Array.isArray(snap.items)) return snap.items; return []; } function hasOfflineRootCacheItems() { const snap = getOfflineRootSnapshot(); return !!(Array.isArray(snap) || (snap && Array.isArray(snap.items))); } function writeOfflineRootCacheItems(items, removedIds = null) { const oldSnap = getOfflineRootSnapshot(); const nextItems = Array.isArray(items) ? [...items] : []; let nextSnap; if (oldSnap && !Array.isArray(oldSnap) && typeof oldSnap === 'object') { nextSnap = Object.assign({}, oldSnap, { items: nextItems }); if (oldSnap.fetchedIds instanceof Set) { const fetchedIds = new Set(oldSnap.fetchedIds); if (removedIds) removedIds.forEach(id => fetchedIds.delete(id)); fetchedIds.forEach(id => { if (isOfflineTaskDeletedByTombstone(id) || isOfflineTaskBlockedByRetryTombstone(id)) fetchedIds.delete(id); }); nextItems.forEach(item => { if (item && item.id) fetchedIds.add(item.id); }); nextSnap.fetchedIds = fetchedIds; } } else { nextSnap = oldSnap && Array.isArray(oldSnap) ? nextItems : { items: nextItems, nextToken: null }; } S.cache.set('offline_root', nextSnap); if (typeof globalCache !== 'undefined') globalCache.set('offline_root', nextSnap); return nextSnap; } function captureOfflineDeleteSnapshot(item, taskId = '') { const id = getOfflineTaskPrimaryId(taskId || item); const localItem = (item && typeof item === 'object') ? item : (S.itemMap && S.itemMap.get(id)); const oldSnapshot = cloneOfflineDeleteItem(localItem); const cacheItems = getOfflineRootCacheItems(); return { taskId: id, oldSnapshot, oldIndex: Array.isArray(S.items) ? S.items.findIndex(x => getOfflineTaskPrimaryId(x) === id) : -1, oldDisplayIndex: Array.isArray(S.display) ? S.display.findIndex(x => getOfflineTaskPrimaryId(x) === id) : -1, oldCacheIndex: cacheItems.findIndex(x => getOfflineTaskPrimaryId(x) === id), oldGroup: getOfflineLightItemGroup(localItem || oldSnapshot), oldSelected: S.isSelected ? S.isSelected(id) : (S.sel && S.sel.has(id)), oldScrollTop: UI && UI.vp ? UI.vp.scrollTop : 0, oldItemsRef: S.items, oldDisplayRef: S.display }; } function syncOfflineDeleteSelectionAfterRemove(idSet) { if (!idSet || idSet.size === 0) return; if (S.selMode === 'all') { idSet.forEach(id => S.selEx.add(id)); return; } idSet.forEach(id => S.sel.delete(id)); } async function removeOfflineTaskItemsLocally(taskIds, context = {}) { const ids = (Array.isArray(taskIds) ? taskIds : [taskIds]).map(getOfflineTaskPrimaryId).filter(Boolean); const idSet = new Set(ids); if (idSet.size === 0) return { changed: false }; cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; let changed = false; let cacheChanged = false; const filterDeleted = (list, markVisible = true) => { if (!Array.isArray(list)) return []; let removed = false; const next = list.filter(item => { const id = getOfflineTaskPrimaryId(item); if (id && idSet.has(id)) { removed = true; return false; } return true; }); if (removed) { if (markVisible) changed = true; else cacheChanged = true; } return next; }; S.items = filterDeleted(S.items); S.display = filterDeleted(S.display); idSet.forEach(id => { if (S.itemMap && S.itemMap.has(id)) changed = true; if (S.itemMap) S.itemMap.delete(id); if (S.starredSet) S.starredSet.delete(id); }); syncOfflineDeleteSelectionAfterRemove(idSet); const hadCacheItems = hasOfflineRootCacheItems(); const cacheItems = hadCacheItems ? getOfflineRootCacheItems() : S.items; const nextCacheItems = hadCacheItems ? filterDeleted(cacheItems, false) : [...S.items]; writeOfflineRootCacheItems(nextCacheItems, idSet); if (changed) { syncOfflineLightProbeStores(S.items); await refresh(); if (UI && UI.vp) UI.vp.scrollTop = Number.isFinite(context.scrollTop) ? context.scrollTop : oldScrollTop; updateStat(); } return { changed: changed || cacheChanged, uiChanged: changed }; } function insertOfflineRestoredItem(list, item, index) { const id = getOfflineTaskPrimaryId(item); if (!id) return { list: Array.isArray(list) ? list : [], changed: false }; const next = Array.isArray(list) ? [...list] : []; const existingIndex = next.findIndex(x => getOfflineTaskPrimaryId(x) === id); if (existingIndex !== -1) { next[existingIndex] = Object.assign({}, item, next[existingIndex]); return { list: next, changed: true }; } const safeIndex = Number.isFinite(index) && index >= 0 && index <= next.length ? index : next.length; next.splice(safeIndex, 0, item); return { list: next, changed: true }; } async function restoreOfflineDeletedSnapshots(snapshots, context = {}) { const list = Array.isArray(snapshots) ? snapshots : [snapshots]; let changed = false; let scrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; const hadCacheItems = hasOfflineRootCacheItems(); let cacheItems = getOfflineRootCacheItems(); for (const snapshot of list) { if (!snapshot || !snapshot.oldSnapshot) continue; const item = cloneOfflineDeleteItem(snapshot.oldSnapshot); const id = getOfflineTaskPrimaryId(item || snapshot.taskId); if (!id) continue; if (Number.isFinite(snapshot.oldScrollTop)) scrollTop = snapshot.oldScrollTop; let result = { changed: false }; if (snapshot.oldIndex >= 0 || (Array.isArray(S.items) && S.items.some(x => getOfflineTaskPrimaryId(x) === id))) { result = insertOfflineRestoredItem(S.items, item, snapshot.oldIndex); S.items = result.list; changed = changed || result.changed; } if (snapshot.oldDisplayIndex >= 0 || (Array.isArray(S.display) && S.display.some(x => getOfflineTaskPrimaryId(x) === id))) { result = insertOfflineRestoredItem(S.display, item, snapshot.oldDisplayIndex); S.display = result.list; changed = changed || result.changed; } if (snapshot.oldCacheIndex >= 0 || cacheItems.some(x => getOfflineTaskPrimaryId(x) === id)) { result = insertOfflineRestoredItem(cacheItems, item, snapshot.oldCacheIndex); cacheItems = result.list; changed = changed || result.changed; } if (snapshot.oldSelected && S.setSelected) S.setSelected(id, true); else if (S.setSelected) S.setSelected(id, false); } if (changed) { writeOfflineRootCacheItems(hadCacheItems ? cacheItems : S.items); syncOfflineLightProbeStores(S.items); await refresh(); if (UI && UI.vp) UI.vp.scrollTop = scrollTop; updateStat(); } return changed; } function scheduleOfflineTaskDeleteProbe(reason = 'delete', delay = 1000) { if (!isOfflineLightProbeModeRoot()) return false; scheduleOfflineLightProbe(reason || 'delete', delay); return true; } async function handleOfflineTaskDelete(taskItems, context = {}) { const ctx = Object.assign({ userInitiated: true, source: 'delete', batch: false, deleteFiles: false, silent: false }, context || {}); if (!ctx.userInitiated || ctx.source === 'retry' || ctx.retryInternal === true || ctx.uploadTask === true) return { handled: false }; if (!isOfflineLightProbeModeRoot()) return { handled: false }; cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); const rawItems = Array.isArray(taskItems) ? taskItems : [taskItems]; const targets = []; const seen = new Set(); for (const raw of rawItems) { const id = getOfflineTaskPrimaryId(raw); const item = (raw && typeof raw === 'object') ? raw : (S.itemMap && S.itemMap.get(id)); const taskId = getOfflineTaskPrimaryId(item || id); if (!taskId || seen.has(taskId)) continue; if (!isOfflineUserDeleteTaskItem(item || { id: taskId, kind: 'drive#task' })) continue; seen.add(taskId); targets.push(item || { id: taskId, task_id: taskId, kind: 'drive#task' }); } if (!targets.length) return { handled: false }; const ids = targets.map(getOfflineTaskPrimaryId).filter(Boolean); const snapshots = targets.map(item => captureOfflineDeleteSnapshot(item)); const progressTask = ctx.progress === false ? null : FloatBarManager.create(L.str_deleting); try { snapshots.forEach(snapshot => addOfflineDeleteTombstone(snapshot.taskId, snapshot)); await removeOfflineTaskItemsLocally(ids, { scrollTop: snapshots[0] && snapshots[0].oldScrollTop }); let failed = []; if (typeof ctx.deleteApi === 'function') { await ctx.deleteApi(ids, ctx); } else { for (const id of ids) { try { await apiCancelTask([id], !!ctx.deleteFiles, { strict: true, taskItems: targets }); } catch (e) { failed.push({ id, error: e }); } } } if (failed.length) { const failedSet = new Set(failed.map(x => x.id)); const failedSnapshots = snapshots.filter(snapshot => failedSet.has(snapshot.taskId)); failedSet.forEach(id => { markOfflineDeleteTombstone(id, 'failed'); removeOfflineDeleteTombstone(id); }); ids.filter(id => !failedSet.has(id)).forEach(id => markOfflineDeleteTombstone(id, 'confirmed')); await restoreOfflineDeletedSnapshots(failedSnapshots, { partial: failed.length < ids.length }); if (failed.length < ids.length) scheduleOfflineTaskDeleteProbe('batch_delete', ctx.delay || 1000); const firstError = failed[0] && failed[0].error ? failed[0].error : new Error(L.str_failed); firstError._offlineDeletePartialHandled = true; firstError._offlineDeleteFailedIds = failed.map(x => x.id); throw firstError; } ids.forEach(id => markOfflineDeleteTombstone(id, 'confirmed')); scheduleOfflineTaskDeleteProbe(ctx.batch || ids.length > 1 ? 'batch_delete' : 'delete', ctx.delay || 1000); if (!ctx.silent) showToast(ids.length > 1 && L.msg_task_del_success_fmt ? L.msg_task_del_success_fmt.replace('{n}', ids.length) : L.msg_task_deleted); return { handled: true, success: true, ids }; } catch (e) { if (e && e._offlineDeletePartialHandled) throw e; ids.forEach(id => { markOfflineDeleteTombstone(id, 'failed'); removeOfflineDeleteTombstone(id); }); await restoreOfflineDeletedSnapshots(snapshots, { error: e }); throw e; } finally { if (progressTask) progressTask.destroy(); } } function extractOfflineRetrySourceUrl(taskOrItem) { if (!taskOrItem || typeof taskOrItem !== 'object') return ''; const task = taskOrItem._offlineTask && typeof taskOrItem._offlineTask === 'object' ? taskOrItem._offlineTask : taskOrItem; const ref = task.reference_resource || taskOrItem.reference_resource || {}; const taskParams = task.params && typeof task.params === 'object' ? task.params : {}; const itemParams = taskOrItem.params && typeof taskOrItem.params === 'object' ? taskOrItem.params : {}; const refParams = ref.params && typeof ref.params === 'object' ? ref.params : {}; const candidates = [ taskParams.url, itemParams.url, task.source_url, taskOrItem.source_url, task.url, taskOrItem.url, refParams.url ]; for (const raw of candidates) { let value = raw; if (value && typeof value === 'object') value = value.url || value.href || value.link || ''; value = String(value || '').trim(); if (value) return value; } return ''; } function isOfflineRetryableTask(taskOrItem) { if (!taskOrItem || typeof taskOrItem !== 'object') return false; const id = getOfflineTaskPrimaryId(taskOrItem); if (!id || isOfflineTaskDeletedByTombstone(id)) return false; const task = taskOrItem._offlineTask && typeof taskOrItem._offlineTask === 'object' ? taskOrItem._offlineTask : taskOrItem; const phase = String(taskOrItem.phase || task.phase || taskOrItem._offlinePhase || '').toUpperCase(); const kind = String(taskOrItem.kind || task.kind || '').toLowerCase(); const looksLikeTask = kind.includes('task') || !!taskOrItem._offlineTask || !!taskOrItem.task_id || !!taskOrItem._offlinePhase || phase.startsWith('PHASE_TYPE_') || isOfflineReferenceMissingLocalMarked(taskOrItem); const hasSourceUrl = !!extractOfflineRetrySourceUrl(taskOrItem); if (!looksLikeTask || !hasSourceUrl) return false; if (isOfflineReferenceMissingLocalMarked(taskOrItem)) return true; if (phase === 'PHASE_TYPE_RUNNING' || phase === 'PHASE_TYPE_PENDING' || phase === 'PHASE_TYPE_PAUSED' || phase === 'PHASE_TYPE_COMPLETE') return false; const status = String(taskOrItem.status || task.status || taskOrItem.message || task.message || '').toLowerCase(); const failed = phase === 'PHASE_TYPE_ERROR' || phase === 'ERROR' || phase === 'FAILED' || phase === 'FAILURE' || /(^|[^a-z])(error|failed|failure)([^a-z]|$)/i.test(status); return !!failed; } function captureOfflineRetrySnapshot(item, taskId = '') { return captureOfflineDeleteSnapshot(item, taskId); } async function restoreOfflineRetrySnapshots(snapshots, context = {}) { return restoreOfflineDeletedSnapshots(snapshots, context); } function canWriteOfflineRetryLocalState(context = {}) { return !!( isOfflineLightProbeModeRoot() && !document.hidden && !isOfflineUserActionLocalWriteBlockedByLoading() && !S.scanning ); } async function insertOfflineRetryNewTasksLocally(tasks, context = {}) { cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); const sourceTasks = Array.isArray(tasks) ? tasks : [tasks]; const seen = new Set(); const activeItems = []; const completeItems = []; let nowIso = ''; for (const task of sourceTasks) { const id = getOfflineTaskPrimaryId(task); if (!id || seen.has(id)) continue; if (isOfflineTaskDeletedByTombstone(id) || isOfflineTaskBlockedByRetryTombstone(id)) continue; seen.add(id); const group = String(task.phase || '').toUpperCase() === 'PHASE_TYPE_COMPLETE' ? 'complete' : 'active'; const item = normalizeOfflineTaskToItem(task, group); if (!item || !item.id) continue; if (!nowIso) { try { nowIso = new Date(typeof getServerNow === 'function' ? getServerNow() : Date.now()).toISOString(); } catch (e) { nowIso = new Date().toISOString(); } } if (group === 'complete') { if (!item.completed_time) item.completed_time = nowIso; if (!item.updated_time && !item.modified_time) item.updated_time = nowIso; completeItems.push(item); } else { if (!item.updated_time && !item.modified_time && !item.created_time) { item.created_time = nowIso; item.updated_time = nowIso; item.modified_time = nowIso; } activeItems.push(item); } } if (!activeItems.length && !completeItems.length) return false; if (!canWriteOfflineRetryLocalState(context) || hasOfflineLightProbeSearchState()) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return false; } const result = mergeOfflineLightProbeItems(S.items, activeItems, completeItems); if (!result.changed) return false; if (!canWriteOfflineRetryLocalState(context) || hasOfflineLightProbeSearchState()) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return false; } const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; S.items = result.items; S.display = result.items; syncOfflineLightProbeStores(S.items); const nextSnap = makeOfflineRootSnapshotWithItems(S.items); S.cache.set('offline_root', nextSnap); if (typeof globalCache !== 'undefined') globalCache.set('offline_root', nextSnap); await refresh(); if (UI && UI.vp && canWriteOfflineRetryLocalState(context)) UI.vp.scrollTop = oldScrollTop; updateStat(); return true; } async function removeOfflineRetryOldTasksLocally(oldTaskIds, context = {}) { if (!canWriteOfflineRetryLocalState(context)) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; return { changed: false }; } return removeOfflineTaskItemsLocally(oldTaskIds, context); } function scheduleOfflineTaskRetryProbe(reason = 'retry', delay = 1000) { if (!isOfflineLightProbeModeRoot()) return false; scheduleOfflineLightProbe(reason || 'retry', delay); return true; } async function cancelOfflineRetryOldTasks(successes) { let failedCount = 0; for (const success of successes) { try { await apiCancelTask([success.oldTaskId], false, { strict: true, taskItems: [success.oldItem || { id: success.oldTaskId, task_id: success.oldTaskId, kind: 'drive#task' }], retryInternal: true, source: 'retry' }); markOfflineRetryTombstone(success.oldTaskId, 'cancelled_old'); } catch (e) { failedCount++; markOfflineRetryTombstone(success.oldTaskId, 'cancel_old_failed'); } await sleep(50); } return failedCount; } async function handleOfflineTaskRetry(taskItems, context = {}) { const ctx = Object.assign({ userInitiated: true, source: 'retry', batch: false, delay: 1000 }, context || {}); if (!ctx.userInitiated || ctx.source === 'delete' || ctx.uploadTask === true) return { handled: false }; if (!isOfflineLightProbeModeRoot()) return { handled: false }; cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); const rawItems = Array.isArray(taskItems) ? taskItems : [taskItems]; const targets = []; const seen = new Set(); for (const raw of rawItems) { const id = getOfflineTaskPrimaryId(raw); const item = (raw && typeof raw === 'object') ? raw : (S.itemMap && S.itemMap.get(id)); const taskId = getOfflineTaskPrimaryId(item || id); if (!taskId || seen.has(taskId)) continue; if (!isOfflineRetryableTask(item || { id: taskId, task_id: taskId, kind: 'drive#task' })) continue; const sourceUrl = extractOfflineRetrySourceUrl(item); if (!sourceUrl) continue; seen.add(taskId); targets.push({ oldTaskId: taskId, oldItem: item || { id: taskId, task_id: taskId, kind: 'drive#task' }, sourceUrl, snapshot: captureOfflineRetrySnapshot(item || { id: taskId, task_id: taskId, kind: 'drive#task' }, taskId) }); } if (!targets.length) return { handled: false }; const progressTask = ctx.progress === false || typeof FloatBarManager === 'undefined' ? null : FloatBarManager.create(L.str_processing); const successes = []; let noTaskIdCount = 0; let existsCount = 0; let failedCount = 0; try { for (const target of targets) { try { const result = await apiAddOfflineTask(target.sourceUrl); const resultExists = isOfflineTaskAlreadyExistsResult(result); const newTask = extractOfflineTaskFromAddResult(result); const newTaskId = getOfflineTaskPrimaryId(newTask); if (resultExists) existsCount++; if (newTaskId && newTaskId !== target.oldTaskId && !isOfflineTaskDeletedByTombstone(newTaskId)) { successes.push(Object.assign({}, target, { newTask, newTaskId, exists: resultExists })); } else { noTaskIdCount++; } } catch (e) { if (isOfflineTaskAlreadyExistsError(e)) { existsCount++; const newTask = extractOfflineTaskFromAddResult((e && e.data) || e); const newTaskId = getOfflineTaskPrimaryId(newTask); if (newTaskId && newTaskId !== target.oldTaskId && !isOfflineTaskDeletedByTombstone(newTaskId)) { successes.push(Object.assign({}, target, { newTask, newTaskId, exists: true })); } else { noTaskIdCount++; } } else { failedCount++; } } await sleep(150); } if (successes.length) { successes.forEach(success => { addOfflineRetryTombstone(success.oldTaskId, { newTaskId: success.newTaskId, oldSnapshot: success.snapshot && success.snapshot.oldSnapshot, newSnapshot: success.newTask, oldIndex: success.snapshot && success.snapshot.oldIndex, oldGroup: success.snapshot && success.snapshot.oldGroup, oldSelected: success.snapshot && success.snapshot.oldSelected, sourceUrl: success.sourceUrl, status: 'created' }); }); await insertOfflineRetryNewTasksLocally(successes.map(success => success.newTask), ctx); await removeOfflineRetryOldTasksLocally(successes.map(success => success.oldTaskId), { scrollTop: successes[0].snapshot && successes[0].snapshot.oldScrollTop }); const cancelFailedCount = await cancelOfflineRetryOldTasks(successes); if (cancelFailedCount) failedCount += cancelFailedCount; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } if (successes.length || noTaskIdCount || existsCount) { const reason = successes.length ? (ctx.batch || successes.length > 1 ? 'batch_retry' : 'retry') : (existsCount ? 'retry_exists' : 'retry_probe'); scheduleOfflineTaskRetryProbe(reason, ctx.delay || 1000); } return { handled: true, success: successes.length > 0, successCount: successes.length, failedCount, skippedCount: Math.max(0, rawItems.length - targets.length), noTaskIdCount, existsCount, ids: successes.map(success => success.oldTaskId), newIds: successes.map(success => success.newTaskId) }; } finally { if (progressTask) progressTask.destroy(); } } async function runOfflineLightProbe(reason = 'timer') { reason = normalizeOfflineLightProbeReason(reason); if (!isOfflineLightProbeModeRoot()) { stopOfflineLightProbe('not_root'); return; } if (document.hidden) { clearOfflineLightProbeTimer(); abortOfflineLightProbe('hidden'); return; } if (S.offlineLightProbeRunning) { S.offlineLightProbePendingReason = mergeOfflineLightProbePendingReason(S.offlineLightProbePendingReason, reason || 'timer'); return; } if (!canRunOfflineLightProbe(reason)) { if (isOfflineLightProbeModeRoot() && !document.hidden) scheduleOfflineLightProbe(reason === 'timer' ? 'timer' : reason, getOfflineLightProbeSkipDelay(reason)); return; } const reqId = ++S.offlineLightProbeReqId; const ctx = { reqId, loadId: activeLoadId, pathKey: getLiveManualRefreshPathKey(), modeKey: getLiveManualRefreshModeKey() }; const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; S.offlineLightProbeRunning = true; S.offlineLightProbeAbortController = controller; S.offlineLightProbePendingReason = ''; S.offlineLightProbeLastAt = Date.now(); let successContext = null; let failed = false; let aborted = false; try { const [activeResult, completeResult] = await Promise.all([ fetchOfflineLightProbeFirstPage('active', controller ? controller.signal : undefined), fetchOfflineLightProbeFirstPage('complete', controller ? controller.signal : undefined) ]); if (!isOfflineLightProbeContextValid(ctx)) { if (isOfflineLightProbeModeRoot()) S.offlineLightProbePendingWrite = true; S.offlineLightProbeFailCount = 0; S.offlineLightProbeLastAt = Date.now(); successContext = { reason, activeResult, completeResult, activeItems: [], completeItems: [], activity: classifyOfflineLightProbeActivity([]), pendingWrite: true }; return; } const activeItems = (activeResult.tasks || []).map(task => normalizeOfflineTaskToItem(task, 'active')).filter(Boolean); const completeItems = (completeResult.tasks || []).map(task => normalizeOfflineTaskToItem(task, 'complete')).filter(Boolean); await mergeOfflineLightProbeResult(ctx, activeItems, completeItems); S.offlineLightProbeFailCount = 0; S.offlineLightProbeLastAt = Date.now(); successContext = { reason, activeResult, completeResult, activeItems, completeItems, activity: classifyOfflineLightProbeActivity(activeItems), pendingWrite: !!S.offlineLightProbePendingWrite }; } catch (e) { if (controller) { try { controller.abort(); } catch (abortErr) {} } aborted = !!(e && e.name === 'AbortError'); if (!aborted) { failed = true; S.offlineLightProbeFailCount = Number(S.offlineLightProbeFailCount || 0) + 1; } } finally { if (S.offlineLightProbeReqId === reqId) { const pendingReason = S.offlineLightProbePendingReason; S.offlineLightProbeRunning = false; S.offlineLightProbeAbortController = null; S.offlineLightProbePendingReason = ''; if (isOfflineLightProbeModeRoot() && !document.hidden) { const normalizedPendingReason = normalizeOfflineLightProbeReason(pendingReason || ''); if (pendingReason && !(failed && (normalizedPendingReason === 'timer' || normalizedPendingReason === 'backoff'))) scheduleOfflineLightProbe(normalizedPendingReason, getOfflineLightProbeSkipDelay(normalizedPendingReason)); else if (failed) scheduleOfflineLightProbe('backoff', getOfflineLightProbeBackoffDelay()); else if (!aborted) scheduleOfflineLightProbe('timer', getOfflineLightProbeNextDelay(successContext || { reason: 'timer' })); } } } } if (typeof window !== 'undefined' && !window.__pkOfflineLightProbeEventsBound) { window.__pkOfflineLightProbeEventsBound = true; window.addEventListener('online', () => { if (isOfflineLightProbeModeRoot() && !document.hidden) scheduleOfflineLightProbe('online', getOfflineLightProbeReasonDelay('online')); }); document.addEventListener('visibilitychange', () => { if (document.hidden) { clearOfflineLightProbeTimer(); abortOfflineLightProbe('hidden'); } else if (isOfflineLightProbeModeRoot() && (typeof navigator === 'undefined' || navigator.onLine !== false)) { if (!consumeOfflineLightProbePendingWrite('pending_write')) scheduleOfflineLightProbe('visible', getOfflineLightProbeReasonDelay('visible')); } }); } function getLiveManualRefreshPathKey() { return Array.isArray(S.path) ? S.path.map(p => String((p && p.id) || 'root')).join('>') : ''; } function getAnalyzeLiveManualRefreshModePart() { if (!S.analyzeMode) return ''; return S.analyzeSimGroups ? 'analyze_folder_dup' : 'analyze_folder_insight'; } function getLiveManualRefreshModeKey() { const isSearchSubPath = isSearchResultRealSubPath(); return [ S.trashMode ? 'trash' : 'drive', S.shareMode ? 'share' : '', S.shareParseMode ? 'share_parse' : '', S.linkBookmarkMode ? 'link_bookmark' : '', S.starredMode ? 'starred' : '', S.recentMode ? 'recent' : '', S.historyMode ? 'history' : '', S.offlineMode ? 'offline' : '', S.uploadMode ? 'upload' : '', S.isFlattened ? 'flattened' : '', S.dupMode ? 'dup' : '', getAnalyzeLiveManualRefreshModePart(), isSearchSubPath ? 'search_result' : '', (S.search && !isSearchSubPath) ? 'search' : '' ].join('|'); } function isStarredRealSubPath() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; const id = String((cur && cur.id) || ''); if (!S.starredMode) return false; if (path.length <= 1) return false; if (!id || id === 'starred_root') return false; if (id.startsWith('virtual_')) return false; return !path.some(p => { const pid = String((p && p.id) || ''); return pid === 'recent_root' || pid === 'history_root' || pid === 'offline_root' || pid === 'upload_root' || pid === 'share_parse_root' || pid === 'analyze_root' || pid.startsWith('virtual_'); }); } function isRecentRealSubPath() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; const id = String((cur && cur.id) || ''); if (!S.recentMode) return false; if (path.length <= 1) return false; if (!id || id === 'recent_root') return false; if (id.startsWith('virtual_')) return false; return !path.some(p => { const pid = String((p && p.id) || ''); return pid === 'history_root' || pid === 'offline_root' || pid === 'upload_root' || pid === 'share_parse_root' || pid === 'analyze_root' || pid.startsWith('virtual_'); }); } function isSearchResultRealSubPath() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; const id = String((cur && cur.id) || ''); const hasSearchRoot = path.some(p => String((p && p.id) || '') === 'virtual_search_root'); if (!hasSearchRoot) return false; if (path.length <= 1) return false; if (!id || id === 'virtual_search_root') return false; if (id.startsWith('virtual_')) return false; return !path.some(p => { const pid = String((p && p.id) || ''); if (pid === 'virtual_search_root') return false; return pid === 'starred_root' || pid === 'trash_root' || pid === 'recent_root' || pid === 'history_root' || pid === 'offline_root' || pid === 'upload_root' || pid === 'share_parse_root' || pid === 'analyze_root' || pid === 'global_search' || pid === 'search_result' || pid === 'file_dup' || pid === 'file_insight' || pid === 'folder_dup' || pid === 'folder_insight' || pid.startsWith('virtual_'); }); } function isFolderInsightRealSubPath() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; const id = String((cur && cur.id) || ''); const hasAnalyzeRoot = path.some(p => String((p && p.id) || '') === 'analyze_root'); if (!S.analyzeMode) return false; if (S.analyzeSimGroups) return false; if (S.dupMode) return false; if (!hasAnalyzeRoot) return false; if (path.length <= 1) return false; if (!id || id === 'analyze_root') return false; if (id === 'folder_dup' || id === 'file_dup' || id === 'folder_insight') return false; if (id.startsWith('virtual_')) return false; return !path.some(p => { const pid = String((p && p.id) || ''); return pid === 'recent_root' || pid === 'history_root' || pid === 'offline_root' || pid === 'upload_root' || pid === 'share_parse_root' || pid.startsWith('virtual_'); }); } function isFolderDupRealSubPath() { const path = Array.isArray(S.path) ? S.path : []; const cur = path.length ? path[path.length - 1] : null; const id = String((cur && cur.id) || ''); const hasAnalyzeRoot = path.some(p => String((p && p.id) || '') === 'analyze_root'); if (!S.analyzeMode) return false; if (!S.analyzeSimGroups) return false; if (S.dupMode) return false; if (!hasAnalyzeRoot) return false; if (path.length <= 1) return false; if (!id || id === 'analyze_root') return false; if (id === 'folder_dup' || id === 'file_dup' || id === 'folder_insight') return false; if (id.startsWith('virtual_')) return false; return !path.some(p => { const pid = String((p && p.id) || ''); return pid === 'recent_root' || pid === 'history_root' || pid === 'offline_root' || pid === 'upload_root' || pid === 'share_parse_root' || pid.startsWith('virtual_'); }); } function canUseLiveManualRefresh() { const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; const curId = String((cur && cur.id) || ''); const isRecentSubPath = isRecentRealSubPath(); const isFolderInsightSubPath = isFolderInsightRealSubPath(); const isFolderDupSubPath = isFolderDupRealSubPath(); const isSearchSubPath = isSearchResultRealSubPath(); const hasVirtualPath = Array.isArray(S.path) && S.path.some(p => { const id = String((p && p.id) || ''); if ((isFolderInsightSubPath || isFolderDupSubPath) && id === 'analyze_root') return false; if (isRecentSubPath && id === 'recent_root') return false; if (isSearchSubPath && id === 'virtual_search_root') return false; return id.startsWith('virtual_') || id === 'analyze_root' || id === 'recent_root' || id === 'history_root' || id === 'offline_root' || id === 'upload_root' || id === 'share_parse_root' || id === 'global_search' || id === 'search_result'; }); return !!cur && !S.trashMode && !S.shareMode && !S.shareParseMode && !S.linkBookmarkMode && (!S.starredMode || isStarredRealSubPath()) && (!S.recentMode || isRecentSubPath) && !S.historyMode && !S.offlineMode && !S.uploadMode && !S.isFlattened && !S.dupMode && (!S.analyzeMode || isFolderInsightSubPath || isFolderDupSubPath) && !S.scanning && (!S.search || isSearchSubPath) && !hasVirtualPath && !curId.startsWith('virtual_'); } function isLiveManualRefreshSameTarget(ctx = S.liveRefreshCtx) { return !!(ctx && S.liveRefreshCtx === ctx && ctx.id === activeLoadId && ctx.pathKey === getLiveManualRefreshPathKey() && ctx.modeKey === getLiveManualRefreshModeKey()); } function isLiveManualRefreshCurrent(ctx = S.liveRefreshCtx) { return !!(isLiveManualRefreshSameTarget(ctx) && ctx.status === 'refreshing' && ctx.contextStillCurrent !== false && canUseLiveManualRefresh()); } function canWriteLiveManualRefreshProjection(ctx) { return !!(ctx && S.liveRefreshCtx === ctx && ctx.status === 'refreshing' && ctx.contextStillCurrent !== false && isLiveManualRefreshCurrent(ctx)); } function canUseLiveManualRefreshStableTie(ctx = S.liveRefreshCtx) { return !!(ctx && S.liveRefreshCtx === ctx && ctx.status === 'refreshing' && ctx.contextStillCurrent !== false && ctx.oldOrderIndex instanceof Map && isLiveManualRefreshSameTarget(ctx) && canUseLiveManualRefresh()); } function getLiveManualRefreshStableOrder(item) { const ctx = S.liveRefreshCtx; if (!canUseLiveManualRefreshStableTie(ctx)) return Number.MAX_SAFE_INTEGER; const id = String((item && item.id) || ''); if (!id || !ctx.oldOrderIndex.has(id)) return Number.MAX_SAFE_INTEGER; const index = ctx.oldOrderIndex.get(id); return Number.isFinite(index) ? index : Number.MAX_SAFE_INTEGER; } function getLiveManualRefreshStatSuffix() { const ctx = S.liveRefreshCtx; if (!ctx || !isLiveManualRefreshSameTarget(ctx)) return ''; if (ctx.status === 'refreshing') return ' (Loading...)'; return ''; } function updateLiveManualRefreshStatus(ctx, status, showToastLevel = '') { if (!ctx || S.liveRefreshCtx !== ctx || !isLiveManualRefreshSameTarget(ctx)) return; ctx.status = status; ctx.statusText = ''; if (typeof updateStat === 'function') updateStat(); } function clearLiveManualRefreshLater(ctx, delay = 1800) { if (!ctx) return; const token = Symbol('liveRefreshClear'); ctx.clearToken = token; setTimeout(() => { if (S.liveRefreshCtx === ctx && isLiveManualRefreshSameTarget(ctx) && ctx.clearToken === token && ctx.status !== 'refreshing') { S.liveRefreshCtx = null; if (typeof updateStat === 'function') updateStat(); } }, delay); } function captureLiveManualRefreshAnchor() { if (!UI.vp || !UI.in) return null; const vpRect = UI.vp.getBoundingClientRect(); const rows = UI.in.querySelectorAll('.pk-row[data-id]'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; const id = row.dataset ? row.dataset.id : ''; if (!id) continue; const rect = row.getBoundingClientRect(); if (rect.bottom >= vpRect.top && rect.top <= vpRect.bottom) { return { id, offset: rect.top - vpRect.top, scrollTop: UI.vp.scrollTop }; } } return null; } function hasLiveManualRefreshUserScrolled(anchor) { if (!anchor || !UI.vp) return false; const oldTop = Number(anchor.scrollTop); const nowTop = Number(UI.vp.scrollTop); if (!Number.isFinite(oldTop) || !Number.isFinite(nowTop)) return false; return Math.abs(nowTop - oldTop) > 12; } function findLiveManualRefreshRow(id) { if (!UI.in || !id) return null; const rows = UI.in.querySelectorAll('.pk-row[data-id]'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (row.dataset && row.dataset.id === id) return row; } return null; } function restoreLiveManualRefreshAnchor(anchor) { if (!anchor || !UI.vp) return; requestAnimationFrame(() => { if (!anchor || !UI.vp || hasLiveManualRefreshUserScrolled(anchor)) return; const row = findLiveManualRefreshRow(anchor.id); if (!row) return; const vpRect = UI.vp.getBoundingClientRect(); const rect = row.getBoundingClientRect(); const delta = rect.top - vpRect.top - anchor.offset; if (Number.isFinite(delta) && Math.abs(delta) > 1) UI.vp.scrollTop += delta; }); } function syncLiveManualRefreshStoresFromItems(items) { const list = Array.isArray(items) ? items : []; S.itemMap.clear(); for (let i = 0; i < list.length; i++) { const item = list[i]; if (!item || !item.id) continue; const id = String(item.id); S.itemMap.set(id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) S.starredSet.add(id); else S.starredSet.delete(id); } } async function flushLiveManualRefreshProjection(ctx, force = false) { if (!canWriteLiveManualRefreshProjection(ctx)) return; if (!force && ctx.rafPromise) { ctx.pendingFlush = true; return ctx.rafPromise; } const flushGeneration = ctx.flushGeneration || 0; const run = async () => { if (!canWriteLiveManualRefreshProjection(ctx) || (ctx.flushGeneration || 0) !== flushGeneration) return; const anchor = captureLiveManualRefreshAnchor(); if (!canWriteLiveManualRefreshProjection(ctx) || (ctx.flushGeneration || 0) !== flushGeneration) return; const values = Array.from(ctx.liveMap.values()); if (!canWriteLiveManualRefreshProjection(ctx) || (ctx.flushGeneration || 0) !== flushGeneration) return; S.items = values; syncLiveManualRefreshStoresFromItems(S.items); try { await refresh(); if (!canWriteLiveManualRefreshProjection(ctx) || (ctx.flushGeneration || 0) !== flushGeneration) return; const projected = Array.isArray(S.display) ? S.display.filter(item => item && !item.isHeader) : values; if (!canWriteLiveManualRefreshProjection(ctx) || (ctx.flushGeneration || 0) !== flushGeneration) return; S.items = projected; ctx.liveMap = new Map(projected.map(item => [String(item.id || ''), item]).filter(([id]) => !!id)); syncLiveManualRefreshStoresFromItems(S.items); if (!canWriteLiveManualRefreshProjection(ctx) || (ctx.flushGeneration || 0) !== flushGeneration) return; restoreLiveManualRefreshAnchor(anchor); updateStat(); } finally { if (ctx && S.liveRefreshCtx === ctx && (ctx.flushGeneration || 0) === flushGeneration) { ctx.rafPromise = null; ctx.pendingFlush = false; } } }; if (force) return run(); ctx.rafPromise = new Promise(resolve => { requestAnimationFrame(() => { if (!canWriteLiveManualRefreshProjection(ctx) || (ctx.flushGeneration || 0) !== flushGeneration) { resolve(); return; } run().finally(resolve); }); }); return ctx.rafPromise; } function abandonLiveManualRefresh(reason = 'abandoned', options = {}) { const ctx = S.liveRefreshCtx; if (!ctx || (ctx.status !== 'refreshing' && ctx.status !== 'interrupted')) return; const detach = options.detach !== false || reason === 'context_switch' || reason === 'path_change' || reason === 'mode_change'; ctx.abandonReason = reason; ctx.noLocalMutation = reason !== 'local_mutation'; ctx.contextStillCurrent = false; ctx.status = 'abandoned'; if (options.localRemovedIds && ctx.localRemovedIds) { options.localRemovedIds.forEach(id => { if (id) ctx.localRemovedIds.add(String(id)); }); } if (ctx.abortController && typeof ctx.abortController.abort === 'function') { try { ctx.abortController.abort(); } catch(e) {} } ctx.pendingFlush = false; ctx.rafPending = false; ctx.rafPromise = null; ctx.flushGeneration = (ctx.flushGeneration || 0) + 1; if (detach) { if (S.liveRefreshCtx === ctx) S.liveRefreshCtx = null; if (S.mergeRefreshCtx === ctx) S.mergeRefreshCtx = null; return; } if (options.showStatus !== false && isLiveManualRefreshSameTarget(ctx)) { updateLiveManualRefreshStatus(ctx, 'abandoned', options.toast || ''); clearLiveManualRefreshLater(ctx); } } function stopLiveManualRefreshBeforePathChange(reason = 'path_change') { const ctx = S.liveRefreshCtx; if (!ctx) return; if (ctx.status === 'refreshing' || ctx.status === 'interrupted') { abandonLiveManualRefresh(reason, { showStatus: false, detach: true }); return; } ctx.contextStillCurrent = false; ctx.abandonReason = reason; ctx.pendingFlush = false; ctx.rafPending = false; ctx.rafPromise = null; ctx.flushGeneration = (ctx.flushGeneration || 0) + 1; if (S.liveRefreshCtx === ctx) S.liveRefreshCtx = null; if (S.mergeRefreshCtx === ctx) S.mergeRefreshCtx = null; } const computeDuplicateGroups = async (candidates, cfg, isRunningFn) => { const groups =[]; const assigned = new Set(); const strictness = gmGet('pk_dup_strictness', 'strict'); const sizeRatioLimit = (strictness === 'loose') ? 0.10 : 0.05; const activeStages = ['hash', ...(cfg.video ? ['video'] : []), 'name']; const stageSpan = 100 / activeStages.length; const setDupProgress = (pct) => { const safePct = Math.max(0, Math.min(100, Math.round(pct))); updateLoadTxt(L.loading_dup.replace('{p}', safePct)); }; let lastDupPct = 0; const setStageProgress = (stageKey, processed, total, stageStartRatio = 0, stageEndRatio = 1) => { const stageIndex = activeStages.indexOf(stageKey); if (stageIndex < 0) return; const start = stageIndex * stageSpan; const end = (stageIndex + 1) * stageSpan; const safeStartRatio = Math.max(0, Math.min(1, stageStartRatio)); const safeEndRatio = Math.max(safeStartRatio, Math.min(1, stageEndRatio)); let innerRatio = safeEndRatio; if (total > 0) { const raw = Math.max(0, Math.min(1, processed / total)); innerRatio = safeStartRatio + ((safeEndRatio - safeStartRatio) * raw); } const nextPct = start + ((end - start) * innerRatio); if (nextPct >= lastDupPct) { lastDupPct = nextPct; setDupProgress(nextPct); } }; setDupProgress(0); if (isRunningFn()) { const hashMap = new Map(); let hashProcessed = 0; const hashBuildTotal = candidates.length; for (const item of candidates) { const hash = item.gcid || item.md5_checksum || item.hash; const key = hash ? `${hash}|${item.size}` : null; if (key) { if (!hashMap.has(key)) hashMap.set(key,[]); hashMap.get(key).push(item); } hashProcessed++; if (hashProcessed % 500 === 0) { if (!isRunningFn()) break; setStageProgress('hash', hashProcessed, hashBuildTotal, 0, 0.7); await sleep(0); } } if (!isRunningFn()) return groups; const hashEntries = Array.from(hashMap.entries()); for (let i = 0; i < hashEntries.length; i++) { const [key, items] = hashEntries[i]; if (items.length > 1) { const ids = items.map(i => i.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_hash); }); groups.push({ ids: ids, type: L.tag_hash }); } if ((i + 1) % 200 === 0) { if (!isRunningFn()) break; setStageProgress('hash', i + 1, hashEntries.length, 0.7, 1); await sleep(0); } } if (!isRunningFn()) return groups; setStageProgress('hash', 1, 1, 1, 1); } if (isRunningFn() && cfg.video) { const simCandidates = candidates.filter(i => i.mime_type.startsWith('video') && !assigned.has(i.id)); const validVideos = simCandidates.filter(item => (parseFloat(item.params?.duration || 0) > 0)); validVideos.sort((a, b) => parseFloat(a.params?.duration || 0) - parseFloat(b.params?.duration || 0)); let processedCount = 0; const totalCount = validVideos.length; if (totalCount <= 0) { setStageProgress('video', 0, 0); } for (let i = 0; i < totalCount; i++) { processedCount++; if (processedCount % 500 === 0) { if (!isRunningFn()) break; setStageProgress('video', processedCount, totalCount); await sleep(0); } if (assigned.has(validVideos[i].id)) continue; const root = validVideos[i]; const rootDur = parseFloat(root.params?.duration || 0); const rootSize = parseInt(root.size || 0); const groupItems = [root]; for (let j = i + 1; j < totalCount; j++) { const target = validVideos[j]; if (assigned.has(target.id)) continue; const durThreshold = (strictness === 'loose') ? 3.0 : 2.0; const targetDur = parseFloat(target.params?.duration || 0); const durDiff = Math.abs(targetDur - rootDur); if (durDiff > durThreshold) break; const targetSize = parseInt(target.size || 0); if (rootSize > 0 && targetSize > 0) { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); const ratio = sizeDiff / maxBase; if (strictness === 'loose' || ratio <= 0.10) { groupItems.push(target); } } } if (groupItems.length > 1) { const ids = groupItems.map(x => x.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_sim); }); groups.push({ ids: ids, type: L.tag_sim }); } } if (!isRunningFn()) return groups; setStageProgress('video', totalCount, totalCount); } if (isRunningFn()) { const cleanNameAd = (oldName) => { let cleanName = oldName; cleanName = cleanName.replace(/^【[^】]+】 *[-_.]? */, ''); cleanName = cleanName.replace(/^[a-z0-9-]+[.](?:com|net|org|cc|xyz|vip|top|la) +/i, ''); const adKw = "(?:[.]com|[.]net|[.]org|[.]cc|[.]xyz|[.]vip|[.]top|[.]la|2048|www[.])"; const atRegex = new RegExp('^.*?' + adKw + '.*?(?:@|--+|_\\\\s)', 'i'); cleanName = cleanName.replace(atRegex, ''); const hyphenRegex = new RegExp('^[a-z0-9.-]+' + adKw + '-', 'i'); cleanName = cleanName.replace(hyphenRegex, ''); cleanName = cleanName.replace(/^(?:精品加群|福利合集)[0-9]+[-_]+ */, ''); cleanName = cleanName.replace(/^[-_. ,,::;;\p{Extended_Pictographic}]+/u, ''); const pairs = [['【','】'], ['[',']'], ['《','》'],['<','>'], ['(',')'],['(',')'], ['{','}']]; pairs.forEach(([L_char, R_char]) => { const idxR_Fix = cleanName.indexOf(R_char); const idxL_Check = cleanName.indexOf(L_char); if (idxR_Fix > 0 && idxR_Fix <= 10 && (idxL_Check === -1 || idxL_Check > idxR_Fix)) { cleanName = L_char + cleanName; } const chars = cleanName.split(''); const stack =[]; const toRemove = new Set(); for (let i = 0; i < chars.length; i++) { const c = chars[i]; if (c === L_char) { stack.push(i); } else if (c === R_char) { if (stack.length > 0) stack.pop(); else toRemove.add(i); } } stack.forEach(i => toRemove.add(i)); if (toRemove.size > 0) { cleanName = chars.filter((_, i) => !toRemove.has(i)).join(''); } }); const quoteCount = (cleanName.match(/'/g) || []).length; if (quoteCount % 2 !== 0) cleanName = cleanName.replace(/'/, ''); let result = cleanName.trim(); const lastDot = result.lastIndexOf('.'); if (lastDot > 0) result = result.substring(0, lastDot); result = result .replace(/\s*[-_ ..。]*\s*(?:\(\s*\d+\s*\)|(\s*\d+\s*)|\[\s*\d+\s*\]|【\s*\d+\s*】)\s*$/u, '') .replace(/\s*(?:[-_ ..。]*\s*)?(?:副本|复制|拷贝|拷貝|コピー|複製|복사본|사본|복사)\s*(?:\d+|\(\s*\d+\s*\)|(\s*\d+\s*)|\[\s*\d+\s*\]|【\s*\d+\s*】)?\s*$/u, '') .replace(/\s*[-_ ..。]+\s*(?:copy|duplicate|dup|salinan|salin|duplikat)\s*(?:\d+|\(\s*\d+\s*\)|(\s*\d+\s*)|\[\s*\d+\s*\]|【\s*\d+\s*】)?\s*$/i, '') .trim(); let finalResult = result ? result.toLowerCase() : oldName.replace(/\.[^/.]+$/, "").toLowerCase().trim(); if (strictness === 'loose') { finalResult = finalResult.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ''); } return finalResult; }; const remainingItems = candidates.filter(i => !assigned.has(i.id)); const getTypeGroup = (mime) => { if (!mime) return 'other'; if (mime.startsWith('video')) return 'video'; if (mime.startsWith('image')) return 'image'; return 'other'; }; const typeNameMap = new Map(); let nameBuildProcessed = 0; const nameBuildTotal = remainingItems.length; for (const item of remainingItems) { const tGroup = getTypeGroup(item.mime_type); const cleaned = cleanNameAd(item.name); if (cleaned) { const key = tGroup + '|' + cleaned; if (!typeNameMap.has(key)) typeNameMap.set(key,[]); typeNameMap.get(key).push(item); } nameBuildProcessed++; if (nameBuildProcessed % 500 === 0) { if (!isRunningFn()) break; setStageProgress('name', nameBuildProcessed, nameBuildTotal, 0, 0.7); await sleep(0); } } if (!isRunningFn()) return groups; const typeEntries = Array.from(typeNameMap.entries()); for (let i = 0; i < typeEntries.length; i++) { const [key, items] = typeEntries[i]; if (items.length > 1) { const sortedForAlgo = [...items].sort((a, b) => parseInt(a.size || 0) - parseInt(b.size || 0)); let currentGroup = [sortedForAlgo[0]]; const tempGroups =[]; for (let j = 1; j < sortedForAlgo.length; j++) { const target = sortedForAlgo[j]; const root = currentGroup[0]; const rootSize = parseInt(root.size || 0); const targetSize = parseInt(target.size || 0); let isMatch = false; if (rootSize === 0 && targetSize === 0) { isMatch = true; } else if (rootSize > 0 && targetSize > 0) { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); if ((sizeDiff / maxBase) <= sizeRatioLimit) { isMatch = true; } } if (isMatch) currentGroup.push(target); else { if (currentGroup.length > 1) tempGroups.push(currentGroup); currentGroup = [target]; } } if (currentGroup.length > 1) tempGroups.push(currentGroup); tempGroups.forEach(grp => { const ids = grp.map(i => i.id); ids.forEach(id => { assigned.add(id); S.dupReasons.set(id, L.tag_name); }); groups.push({ ids: ids, type: L.tag_name }); }); } if ((i + 1) % 200 === 0) { if (!isRunningFn()) break; setStageProgress('name', i + 1, typeEntries.length, 0.7, 1); await sleep(0); } } if (!isRunningFn()) return groups; setStageProgress('name', 1, 1, 1, 1); } setDupProgress(100); return groups; }; function buildDuplicateBucketsFromGroups(groups, sourceItems) { const dupItemMap = new Map(); const items = Array.isArray(sourceItems) ? sourceItems : []; for (let i = 0, len = items.length; i < len; i++) { dupItemMap.set(items[i].id, items[i]); } const dupBuckets = { all: [], hash: [], sim: [], name: [] }; for (let i = 0, len = groups.length; i < len; i++) { const g = groups[i]; const validIds = []; for (let k = 0, idsLen = g.ids.length; k < idsLen; k++) { const id = g.ids[k]; if (dupItemMap.has(id)) validIds.push(id); } if (validIds.length <= 1) continue; const normalizedGroup = { ...g, ids: validIds }; dupBuckets.all.push(normalizedGroup); if (g.type === L.tag_hash) dupBuckets.hash.push(normalizedGroup); else if (g.type === L.tag_sim) dupBuckets.sim.push(normalizedGroup); else if (g.type === L.tag_name) dupBuckets.name.push(normalizedGroup); } return { dupItemMap, dupBuckets }; } function getNetworkResumeMode(realCacheKey = '') { if (S.offlineMode && realCacheKey === 'offline_root') return 'offline'; if (S.recentMode && realCacheKey === 'recent_root') return 'recent'; return 'standard'; } function isProtectedNetworkResumeTarget(realCacheKey, mode = '') { const resumeMode = mode || getNetworkResumeMode(realCacheKey); if (resumeMode === 'offline') return !!(S.offlineMode && realCacheKey === 'offline_root'); if (resumeMode === 'recent') return !!(S.recentMode && realCacheKey === 'recent_root'); return !!(resumeMode === 'standard' && typeof canUseLiveManualRefresh === 'function' && canUseLiveManualRefresh()); } function getNetworkResumeCachedState(realCacheKey, mode = '') { const readCache = key => { if (S.cache && S.cache.has(key)) return S.cache.get(key); if (typeof globalCache !== 'undefined' && globalCache.has(key)) return globalCache.get(key); return null; }; const resumeMode = mode || getNetworkResumeMode(realCacheKey); if (resumeMode === 'offline') { const rootSnap = readCache('offline_root'); const session = readCache('offline_session'); const items = rootSnap && !Array.isArray(rootSnap) && Array.isArray(rootSnap.items) ? rootSnap.items : (Array.isArray(rootSnap) ? rootSnap : []); const token = session && session.nextToken && !session.completed ? session.nextToken : ''; return items.length && token ? { items, nextToken: token, session, mode: resumeMode } : null; } if (resumeMode === 'recent') { const snap = readCache('recent_root'); const session = readCache('recent_session'); const items = snap && !Array.isArray(snap) && Array.isArray(snap.items) ? snap.items : (Array.isArray(snap) ? snap : []); const snapToken = snap && !Array.isArray(snap) && snap.nextToken ? snap.nextToken : ''; const sessionToken = session && session.nextToken && !session.completed ? session.nextToken : ''; const token = sessionToken || snapToken; return items.length && token ? { items, nextToken: token, session, mode: resumeMode } : null; } const snap = readCache(realCacheKey); return snap && !Array.isArray(snap) && Array.isArray(snap.items) && snap.nextToken ? { items: snap.items, nextToken: snap.nextToken, mode: resumeMode } : null; } function clearNetworkResumeCtx(reason = '') { const ctx = S.networkResumeCtx; if (!ctx) return; if (ctx.timer) clearTimeout(ctx.timer); if (ctx.onlineHandler) window.removeEventListener('online', ctx.onlineHandler); if (ctx.visibilityHandler) document.removeEventListener('visibilitychange', ctx.visibilityHandler); if (S.networkResumeCtx === ctx) S.networkResumeCtx = null; } function isNetworkResumeCtxCurrent(ctx) { if (!ctx) return false; const cur = Array.isArray(S.path) && S.path.length ? S.path[S.path.length - 1] : null; const curId = String((cur && cur.id) || 'root'); const curCacheKey = S.getRealCacheKey(curId); if (ctx.mode === 'offline') return !!(S.offlineMode && ctx.realCacheKey === 'offline_root' && curCacheKey === 'offline_root'); if (ctx.mode === 'recent') return !!(S.recentMode && ctx.realCacheKey === 'recent_root' && curCacheKey === 'recent_root'); return ctx.pathKey === getLiveManualRefreshPathKey() && ctx.modeKey === getLiveManualRefreshModeKey() && ctx.realCacheKey === curCacheKey && canUseLiveManualRefresh(); } async function flushNetworkResumeVisibleState() { if (!Array.isArray(S.items) || S.items.length === 0) return; const oldScrollTop = UI && UI.vp ? UI.vp.scrollTop : 0; try { await refresh(); } catch (e) { try { renderVisible(); } catch (err) {} } if (UI && UI.vp) UI.vp.scrollTop = oldScrollTop; updateStat(); } function scheduleNetworkResumeCtx(ctx) { clearNetworkResumeCtx('replace'); ctx.mode = ctx.mode || getNetworkResumeMode(ctx.realCacheKey); let running = false; const tryResume = async source => { if (running || S.networkResumeCtx !== ctx) return; if (!isNetworkResumeCtxCurrent(ctx)) { clearNetworkResumeCtx('context_changed'); return; } const cached = getNetworkResumeCachedState(ctx.realCacheKey, ctx.mode); if (!cached || !cached.nextToken) { clearNetworkResumeCtx('no_token'); return; } if (S.loading || S.scanning) { ctx.schedule(1500); return; } if (typeof navigator !== 'undefined' && navigator.onLine === false) { ctx.schedule(2500); return; } running = true; try { const h = getHeaders(); if (!h.Authorization || h.Authorization.length < 10) throw new Error('NO_AUTH'); const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/about?_t=${Date.now()}`, { headers: h, cache: 'no-store' }); if (!res.ok) throw new Error(`PROBE_${res.status}`); } catch (e) { running = false; ctx.retry = (ctx.retry || 0) + 1; ctx.schedule(Math.min(15000, 1500 + ctx.retry * 1000)); return; } if (S.networkResumeCtx !== ctx || !isNetworkResumeCtxCurrent(ctx)) { running = false; clearNetworkResumeCtx('context_changed_after_probe'); return; } clearNetworkResumeCtx('resume'); S._networkResumeLoad = true; load(false, false).finally(() => { S._networkResumeLoad = false; if (ctx.mode === 'recent') scheduleRecentLightProbe('online', 1200); }); }; ctx.schedule = delay => { if (ctx.timer) clearTimeout(ctx.timer); ctx.timer = setTimeout(() => tryResume('timer'), delay); }; ctx.onlineHandler = () => tryResume('online'); ctx.visibilityHandler = () => { if (!document.hidden) tryResume('visible'); }; window.addEventListener('online', ctx.onlineHandler); document.addEventListener('visibilitychange', ctx.visibilityHandler); S.networkResumeCtx = ctx; ctx.schedule(typeof navigator !== 'undefined' && navigator.onLine === false ? 3000 : 1200); } async function load(isHistoryNav = false, forceUpdate = false) { if (S.scanning) return; if (S.liveRefreshCtx) { const sameLiveRefreshTarget = S.liveRefreshCtx.pathKey === getLiveManualRefreshPathKey() && S.liveRefreshCtx.modeKey === getLiveManualRefreshModeKey(); if (!sameLiveRefreshTarget || !canUseLiveManualRefresh()) { if (S.liveRefreshCtx.status === 'refreshing' || S.liveRefreshCtx.status === 'interrupted') { abandonLiveManualRefresh('context_switch', { showStatus: false, detach: true }); } else { stopLiveManualRefreshBeforePathChange('context_switch'); } } } if (S.shareParseMode) { setLoad(false); if (UI.btnExport) UI.btnExport.style.display = 'none'; renderCrumb(); if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); if (S.shareParseListActive) { syncShareParseMirror(); renderList(); } else { renderShareParsePanel(); } syncShareParseInsightFilterBar(); updateShareParseInsightButtons(); updateStat(); return; } if (S.linkBookmarkMode) { setLoad(false); renderCrumb(); if (!S.linkBookmarkHasLoaded && S.linkBookmarkSyncStatus !== 'syncing') loadLinkBookmarkOfficial(true); else renderLinkBookmarkPanel(); updateStat(); 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) { forceUpdate = true; } let realCacheKey = S.getRealCacheKey(folderId); if (S.offlineMode && realCacheKey === 'offline_root') { cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); } const isAnalyzeSub = S.path.some(p => p.id === 'analyze_root'); if (isAnalyzeSub) { S.analyzeMode = true; if (S.strictFolderFirstSnapshot !== null) { syncFolderFirstFromGlobal(); } if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode(S.analyzeSimGroups ? 'folder_dup' : 'folder_insight'); } 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; } const recentRootNextPageIntent = !!(S.recentLoadNextPageOnly && S.recentMode && S.path.length === 1 && cur && cur.id === 'recent_root' && realCacheKey === 'recent_root'); if (!recentRootNextPageIntent && (globalDirtyFolders.has(folderId) || (folderId === 'root' && globalDirtyFolders.has('')))) { forceUpdate = true; globalDirtyFolders.delete(folderId); globalDirtyFolders.delete(''); globalDirtyFolders.delete('root'); } const keepVisualOnThisForceLoad = !!(S.keepVisualOnNextForceLoad && forceUpdate && !recentRootNextPageIntent && canUseLiveManualRefresh()); S.keepVisualOnNextForceLoad = false; const useLiveManualRefresh = !!(keepVisualOnThisForceLoad && canUseLiveManualRefresh()); const isNetworkResumeLoad = !!S._networkResumeLoad; S._networkResumeLoad = false; if (!isNetworkResumeLoad && S.networkResumeCtx) { const sameNetworkResumeTarget = isNetworkResumeCtxCurrent(S.networkResumeCtx); if (!sameNetworkResumeTarget || forceUpdate || isHistoryNav) clearNetworkResumeCtx('new_load'); } if (S.liveRefreshCtx) { const sameLiveRefreshTarget = S.liveRefreshCtx.pathKey === getLiveManualRefreshPathKey() && S.liveRefreshCtx.modeKey === getLiveManualRefreshModeKey(); if (!sameLiveRefreshTarget || !canUseLiveManualRefresh()) { if (S.liveRefreshCtx.status === 'refreshing' || S.liveRefreshCtx.status === 'interrupted') { abandonLiveManualRefresh('context_switch', { showStatus: false, detach: true }); } else { stopLiveManualRefreshBeforePathChange('context_switch'); } } } if (!recentRootNextPageIntent && !keepVisualOnThisForceLoad && !isNetworkResumeLoad) S.clearSelection(); if (typeof UI !== 'undefined' && UI.vp && !forceUpdate && !recentRootNextPageIntent && !isNetworkResumeLoad) { UI.vp.scrollTop = 0; } if (UI.win) UI.win.setAttribute('data-cur-pid', folderId); activeLoadId++; const currentId = activeLoadId; if (S.offlineMode && realCacheKey === 'offline_root') { clearOfflineLightProbeTimer(); abortOfflineLightProbe('load_start'); } else { stopOfflineLightProbe('mode_change'); } if (S.recentMode && realCacheKey === 'recent_root') { markRecentRootPagingActive(currentId, 'load_start'); } else { clearRecentRootPagingActive(0, 'non_recent_load'); } let nextToken = null; const fetchedIds = new Set(); let isResuming = false; S.pagingLoading = 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) { if (S.search) { S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; } else if (UI.searchInput && UI.searchClear) { UI.searchClear.style.display = String(UI.searchInput.value || '').trim() ? 'flex' : 'none'; } } if (cur.id === 'virtual_search_root') { if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('global_search'); renderCrumb(); syncGlobalSearchCheckboxState(); 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 (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('global_search'); if (UI.chkGlobal && isGlobalSearchResultContext()) UI.chkGlobal.checked = true; syncGlobalSearchCheckboxState(); } if (UI.viewSwitch) { const usableViewSwitch = canUseGridView(); UI.viewSwitch.style.display = usableViewSwitch ? 'inline-flex' : 'none'; UI.viewSwitch.style.visibility = usableViewSwitch ? '' : 'hidden'; UI.viewSwitch.style.pointerEvents = usableViewSwitch ? '' : 'none'; } if (UI.scan) UI.scan.style.display = shouldHideAnalyzeButtonsForCurrentMode() ? 'none' : 'flex'; if (UI.btnExit) UI.btnExit.style.display = (isAnalyzeSub && folderId !== 'analyze_root') ? 'flex' : 'none'; if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (UI.offTools) UI.offTools.style.display = S.offlineMode ? 'flex' : 'none'; if (UI.upTools) UI.upTools.style.display = S.uploadMode ? 'flex' : 'none'; if (UI.crumb) UI.crumb.style.display = ''; if (UI.searchInput && UI.searchInput.parentNode) { UI.searchInput.parentNode.style.display = S.trashMode ? 'none' : 'flex'; } if (UI.cntFolderFirst) { UI.cntFolderFirst.style.display = (S.trashMode || S.dupMode) ? 'none' : 'flex'; } if (UI.btnAnalyze) { UI.btnAnalyze.style.display = shouldHideAnalyzeButtonsForCurrentMode() ? 'none' : 'flex'; } if (UI.btnExport) { UI.btnExport.style.display = shouldHideAnalyzeButtonsForCurrentMode() ? '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 && isGlobalSearchResultContext()) { 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 = shouldHideNewFolderButtonForCurrentMode() ? '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) { 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); } } }; const getMergeRefreshPathKey = getLiveManualRefreshPathKey; const getMergeRefreshModeKey = getLiveManualRefreshModeKey; let mergeRefreshCtx = null; const clearMergeRefreshCtx = () => { if (S.mergeRefreshCtx && S.mergeRefreshCtx.id === currentId) S.mergeRefreshCtx = null; }; const isMergeRefreshStillCurrent = () => { const ctx = mergeRefreshCtx; if (!ctx) return false; const currentPathId = S.path[S.path.length - 1]?.id || ''; return ctx.id === currentId && S.liveRefreshCtx === ctx && ctx.status === 'refreshing' && ctx.contextStillCurrent !== false && currentId === activeLoadId && !signal.aborted && String(currentPathId) === String(snapshot.pathId || '') && ctx.pathKey === getMergeRefreshPathKey() && ctx.modeKey === getMergeRefreshModeKey(); }; if (useLiveManualRefresh) { const oldItems = Array.isArray(S.items) ? S.items.filter(item => item && item.id) : []; S.itemMap.clear(); oldItems.forEach(item => S.itemMap.set(item.id, item)); mergeRefreshCtx = { id: currentId, status: 'refreshing', complete: false, folderId, realCacheKey, pathKey: getMergeRefreshPathKey(), modeKey: getMergeRefreshModeKey(), oldIds: new Set(oldItems.map(item => String(item.id || '')).filter(Boolean)), seenIds: new Set(), liveMap: new Map(oldItems.map(item => [String(item.id || ''), item]).filter(([id]) => !!id)), oldOrderIndex: new Map(oldItems.map((item, index) => [String(item.id || ''), index]).filter(([id]) => !!id)), pageTokens: new Set(), localRemovedIds: new Set(), noLocalMutation: true, noPageError: true, contextStillCurrent: true, tokenLoopSafe: true, abortController: S.abortController, rafPromise: null, rafPending: false, pendingFlush: false, flushGeneration: 0, scrollTop: UI.vp ? UI.vp.scrollTop : 0 }; S.mergeRefreshCtx = mergeRefreshCtx; S.liveRefreshCtx = mergeRefreshCtx; updateLiveManualRefreshStatus(mergeRefreshCtx, 'refreshing'); } 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.offlineMode && realCacheKey === 'offline_root') { S.items = filterOfflineRetryTombstones(S.items); fetchedIds.clear(); S.items.forEach(it => { if (it && it.id) fetchedIds.add(it.id); }); } 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(); S.pagingLoading = !!nextToken; if (S.offlineMode) { syncOfflinePagingLoadingFromSession(); } else { S.offlinePagingPending = false; } refresh(); updateStat(); if (S.offlineMode) { const session = getOfflineSessionForLoadingState(); if (isOfflineSessionLoading(session)) { isResuming = true; nextToken = session && session.nextToken ? session.nextToken : nextToken; S.offlinePagingPending = true; S.pagingLoading = true; S.items.forEach(it => fetchedIds.add(it.id)); updateStat(); } else { scheduleOfflineLightProbe('enter', 1200); setLoad(false); return; } } else if (S.historyMode) { hydrateHistorySessionFromSnapshot(cachedData); S.items = [...S.historyItems]; S.itemMap.clear(); S.items.forEach(it => { if (it && it.id) { fetchedIds.add(it.id); S.itemMap.set(it.id, it); } }); syncHistorySessionCache(); nextToken = null; isResuming = S.items.length > 0; S.pagingLoading = false; setLoad(false); } else if (S.recentMode && realCacheKey === 'recent_root') { const session = globalCache.get('recent_session'); const sessionToken = session && session.nextToken && !session.completed ? session.nextToken : null; if (!nextToken && sessionToken) nextToken = sessionToken; if (nextToken) { isResuming = true; S.items.forEach(it => fetchedIds.add(it.id)); globalCache.set('recent_session', { phase: 'active', nextToken: nextToken || null, completed: false, count: S.items.length }); } else { setLoad(false); 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 if (!nextToken) { setLoad(false); if (window.pkSmartRefreshTrigger && !shouldSkipSmartRefreshAfterLoad(realCacheKey)) { 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 { if (S.recentMode && realCacheKey === 'recent_root') { globalCache.set('recent_session', { phase: 'active', nextToken: nextToken || null, completed: false, count: S.items.length }); } } } } if (!nextToken && !isResuming) { if (S.recentMode && realCacheKey === 'recent_root') { globalCache.set('recent_session', { phase: 'active', nextToken: null, completed: false, count: 0 }); } const keepNetworkSafeVisual = !!(forceUpdate && !keepVisualOnThisForceLoad && !isResuming && Array.isArray(S.items) && S.items.length > 0 && isProtectedNetworkResumeTarget(realCacheKey)); if (!keepVisualOnThisForceLoad && !keepNetworkSafeVisual) { S.items = []; S.display = []; S.itemMap.clear(); refresh(); } if (keepNetworkSafeVisual) { setLoad(false); } else { setLoad(true, true); updateLoadTxt(L.loading_detail); } } isGUISensitive = false; const currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) { if (!isResuming) { setLoad(true); updateLoadTxt(L.str_waiting_token); } if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('load-missing-token', 5000); const initialWait = (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 6500 : 5000; let isAuthReady = await waitForAuth(initialWait); if (!isAuthReady) { console.warn("PikPak Master: Auth token wait timeout. Entering recovery recheck before logout."); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('load-missing-token-recheck', 5000); await sleep(1200); isAuthReady = await waitForAuth(5000); } if (!isAuthReady) { console.warn("PikPak Master: Auth token still missing after recovery recheck."); const didLogout = await confirmedLogout('load-missing-token-final', 5000, 5200); if (didLogout) return; setLoad(false); if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); return; } if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); } if (!isResuming && el) { el.focus(); } UI.stopBtn.onclick = () => { const isAuthSyncMode = (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) || (UI.loadTxt && UI.loadTxt.innerText === L.str_waiting_token); if (isAuthSyncMode) { handleClose(); window.location.href = `${window.location.origin}/login`; return; } const stoppedLiveManualRefresh = !!(mergeRefreshCtx && S.liveRefreshCtx === mergeRefreshCtx); if (stoppedLiveManualRefresh) { abandonLiveManualRefresh('manual_stop', { toast: 'info', detach: false }); } else { S.abortController.abort(); } if (!stoppedLiveManualRefresh && S.loading && !S.isFlattened && !S.dupMode && !S.analyzeMode && !S.shareMode && !S.offlineMode && S.path.length > 1) { stopLiveManualRefreshBeforePathChange('path_change'); S.path.pop(); 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(); } } S.pagingLoading = false; updateStat(); setLoad(false); isGUISensitive = false; }; try { let pageCount = 0; const limit = 500; let totalItemsLoaded = S.items.length; do { if (currentId !== activeLoadId) { clearMergeRefreshCtx(); return; } if (signal.aborted) throw new DOMException('Aborted', 'AbortError'); const currentPageToken = nextToken || ''; if (mergeRefreshCtx) { if (!isMergeRefreshStillCurrent()) { mergeRefreshCtx.contextStillCurrent = false; abandonLiveManualRefresh('context_switch', { showStatus: false, detach: true }); return; } if (mergeRefreshCtx.pageTokens.has(currentPageToken)) { mergeRefreshCtx.tokenLoopSafe = false; throw new Error('DUPLICATE_PAGE_TOKEN'); } mergeRefreshCtx.pageTokens.add(currentPageToken); } 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; cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); const uniqueTasks = batchTasks.filter(t => { const id = getOfflineTaskPrimaryId(t); return id && !isOfflineTaskDeletedByTombstone(id) && !isOfflineTaskBlockedByRetryTombstone(id) && !fetchedIds.has(id) && !S.itemMap.has(id); }); const offlineStillLoading = currentPhase !== 'done' || !!nextToken; const newItems = uniqueTasks.map(t => normalizeOfflineTaskToItem(t, currentPhase === 'done' ? 'complete' : 'active')).filter(Boolean); 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)); S.offlinePagingPending = offlineStillLoading; S.pagingLoading = offlineStillLoading; 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); S.offlinePagingPending = false; S.pagingLoading = false; data = { files: [], next_page_token: null }; break; } else if (S.historyMode && S.path.length === 1) { if (pageCount > 0) break; await loadOfficialHistoryFirstPage({ manualRebuild: !!(forceUpdate && S.historyRefreshFirstPageOnly) }); data = { files: [], next_page_token: null }; break; } else if (S.recentMode && S.path.length === 1 && cur && cur.id === 'recent_root') { const recentPageToken = nextToken || ''; const recentLimit = recentPageToken ? 50 : 100; const url = `https://api-drive.mypikpak.com/drive/v1/events?thumbnail_size=SIZE_MEDIUM&limit=${recentLimit}${recentPageToken ? `&page_token=${encodeURIComponent(recentPageToken)}` : ''}&_t=${Date.now()}`; 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 events = Array.isArray(json.events) ? json.events : (Array.isArray(json.data) ? json.data : []); const recentNextToken = json.next_page_token || json.nextPageToken || null; data = { files: mapRecentEventsToItems(events).filter(f => f.id && !fetchedIds.has(f.id)), next_page_token: recentNextToken }; } else if (S.shareMode) { if (pageCount > 0) break; const shares = await apiShareList(); data = { files: shares, next_page_token: null }; } else if (S.uploadMode) { if (pageCount > 0) break; try { const phases = "PHASE_TYPE_UNKNOW,PHASE_TYPE_PENDING,PHASE_TYPE_RUNNING,PHASE_TYPE_PAUSED,PHASE_TYPE_ERROR"; const filters = encodeURIComponent(JSON.stringify({ "phase": { "in": phases } })); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?type=upload&limit=100&filters=${filters}&_t=${Date.now()}`; const res = await fetch(url, { headers: getHeaders(), signal: signal }); if (res.ok) { const cloudData = await res.json(); const cloudTasks = cloudData.tasks || []; const cloudIds = new Set(cloudTasks.map(ct => ct.id)); const cloudFileIds = new Set(cloudTasks.map(ct => ct.file_id).filter(Boolean)); S.uploadTasks = S.uploadTasks.filter(lt => { if (lt.status === 'DONE' || (!lt.file_id && !lt._uploadId)) return true; if ((lt.name === 'upload' || lt.name === 'Ghost Task') && !lt.file && lt.file_id) { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [lt.file_id] }) }).catch(()=>{}); if (S.upMng) S.upMng.removeTask(lt.id); return false; } const existsInCloud = cloudIds.has(lt.id) || (lt.file_id && cloudFileIds.has(lt.file_id)) || (lt._sourceTaskId && cloudIds.has(lt._sourceTaskId)); if (!existsInCloud) { if (S.upMng) S.upMng.removeTask(lt.id); return false; } return true; }); cloudTasks.forEach(ct => { const existsLocally = S.uploadTasks.some(lt => lt.id === ct.id || lt.file_id === ct.file_id || lt._sourceTaskId === ct.id); if (!existsLocally && ct.file_id) { const ref = ct.reference_resource || {}; const taskName = ref.name || ct.name || ct.file_name || 'Ghost Task'; if (taskName === 'upload' || taskName === 'Ghost Task') { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [ct.file_id] }) }).catch(()=>{}); return; } const ghostTask = { id: 'up_' + Date.now() + '_' + Math.random().toString(36).substr(2), _sourceTaskId: ct.id, kind: 'pk#upload', file: null, name: taskName, size: ct.file_size, parentId: '', status: 'PAUSED', progress: parseInt(ct.progress || 0), speed: 0, message: L.msg_ghost_task_resume, file_id: ct.file_id, hash: (ct.params && ct.params.hash) || '', _xhr: null, _lastCalcTime: 0, _lastCalcLoaded: 0, _lastUiTime: 0, icon_link: ct.icon_link || '', thumbnail_link: ct.thumbnail_link || '' }; S.uploadTasks.push(ghostTask); if (S.upMng) S.upMng.saveTask(ghostTask); } }); } } catch (e) { if ((e && e.name === 'AbortError') || signal.aborted || currentId !== activeLoadId) return; console.warn("[Upload] Failed to sync cloud upload tasks:", e); } data = { files: [...S.uploadTasks], next_page_token: null }; } else { const isStarredRoot = S.starredMode && S.path.length === 1; const filterObj = { "trashed": { "eq": S.trashMode } }; if (isStarredRoot) { filterObj.trashed = { "eq": false }; filterObj.system_tag = { "in": "STAR" }; } else if (!S.trashMode) { filterObj.phase = { "eq": "PHASE_TYPE_COMPLETE" }; } const filters = `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`; const targetParentId = (S.trashMode || isStarredRoot) ? '*' : (cur.id || ''); const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&parent_id=${targetParentId}&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: 'high' }); if (currentId !== activeLoadId) return; if (!res.ok) { if (res.status === 429) { await sleep(1000); continue; } throw new Error("API " + res.status); } data = await res.json(); } if (data.files && data.files.length >= 0) { S.pagingLoading = !!data.next_page_token; 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 || (mergeRefreshCtx && !isMergeRefreshStillCurrent())) { console.warn("[Load Guard] Context Drift Detected. Aborting render."); if (mergeRefreshCtx) { mergeRefreshCtx.contextStillCurrent = false; abandonLiveManualRefresh('context_switch', { showStatus: false, detach: true }); } clearMergeRefreshCtx(); 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 pageUniqueFiles = []; const pageSeenIds = new Set(); validFiles.forEach(f => { const id = String((f && f.id) || ''); if (!id || pageSeenIds.has(id)) return; pageSeenIds.add(id); pageUniqueFiles.push(f); }); const newUniqueFiles = mergeRefreshCtx ? pageUniqueFiles : pageUniqueFiles.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 (mergeRefreshCtx) { if (!isMergeRefreshStillCurrent()) { mergeRefreshCtx.contextStillCurrent = false; abandonLiveManualRefresh('context_switch', { showStatus: false, detach: true }); clearMergeRefreshCtx(); return; } let mergeChanged = false; newUniqueFiles.forEach(f => { const id = String((f && f.id) || ''); if (!id) return; if (mergeRefreshCtx.localRemovedIds && mergeRefreshCtx.localRemovedIds.has(id)) return; fetchedIds.add(id); mergeRefreshCtx.seenIds.add(id); const old = mergeRefreshCtx.liveMap.get(id) || S.itemMap.get(id); const merged = old ? Object.assign({}, old, f) : f; mergeRefreshCtx.liveMap.set(id, merged); S.itemMap.set(id, merged); if (merged.starred || (merged.tags && merged.tags.some(t => t.name === 'STAR'))) S.starredSet.add(id); else S.starredSet.delete(id); mergeChanged = true; }); totalItemsLoaded = mergeRefreshCtx.liveMap.size; if (mergeChanged || (pageCount === 0 && !isResuming)) { const suppressSortMask = pageCount > 0; if (suppressSortMask) S._suppressSortMaskForPaging = true; try { await flushLiveManualRefreshProjection(mergeRefreshCtx); } finally { if (suppressSortMask) S._suppressSortMaskForPaging = false; } if (!canWriteLiveManualRefreshProjection(mergeRefreshCtx)) { clearMergeRefreshCtx(); return; } if (pageCount === 0) { const authLoadKey = (!snapshot.pathId || snapshot.pathId === 'root') ? 'root' : String(snapshot.pathId); window.__pkLastManagerLoadOk = { at: Date.now(), key: authLoadKey, count: mergeRefreshCtx.liveMap.size }; } if (mergeRefreshCtx.liveMap.size === 0) { UI.in.innerHTML = ''; renderVisible(); } } } else 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.recentMode && realCacheKey === 'recent_root') { globalCache.set('recent_session', { phase: 'active', nextToken: data.next_page_token || null, completed: false, count: S.items.length }); } 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); } const refreshPagingAppendView = async () => { const suppressSortMask = pageCount > 0; if (suppressSortMask) S._suppressSortMaskForPaging = true; try { await refresh(); } finally { if (suppressSortMask) S._suppressSortMaskForPaging = false; } }; if (pageCount === 0 || UI.loader.style.display !== 'none') { await refreshPagingAppendView(); if (UI.loader.style.display !== 'none') { setLoad(false); } if (pageCount === 0) { const authLoadKey = (!snapshot.pathId || snapshot.pathId === 'root') ? 'root' : String(snapshot.pathId); window.__pkLastManagerLoadOk = { at: Date.now(), key: authLoadKey, count: S.items.length }; } if (S.items.length === 0) { UI.in.innerHTML = ''; renderVisible(); } } else { if (pageCount % 4 === 0) await refreshPagingAppendView(); } } else if (S.recentMode && realCacheKey === 'recent_root') { globalCache.set('recent_session', { phase: 'active', nextToken: data.next_page_token || null, completed: false, count: S.items.length }); const tempCache = { items: [...S.items], nextToken: data.next_page_token }; S.cache.set(realCacheKey, tempCache); if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, tempCache); } } if (mergeRefreshCtx) { const incomingNextToken = data.next_page_token || ''; if (incomingNextToken && mergeRefreshCtx.pageTokens.has(incomingNextToken)) { mergeRefreshCtx.tokenLoopSafe = false; throw new Error('DUPLICATE_PAGE_TOKEN'); } } nextToken = data.next_page_token; pageCount++; if (currentId === activeLoadId) { const isProgressiveSearch = !!((S.offlineMode || S.recentMode) && S.search); const statCount = isProgressiveSearch ? S.display.reduce((cnt, item) => cnt + (item && !item.isHeader && !(S.movingIds && S.movingIds.has(item.id)) ? 1 : 0), 0) : totalItemsLoaded; const selectedCount = S.selMode === 'all' ? Math.max(0, statCount - (S.selEx ? S.selEx.size : 0)) : (S.getSelectedCount ? S.getSelectedCount() : 0); let selectedSizeText = ''; if (selectedCount > 0 && S.selMode !== 'all') { let hasSelectedFolder = false; let selectedSize = 0; for (const id of S.getSelectedIds()) { const item = S.itemMap.get(id); if (!item) continue; const sz = parseInt(item.size || 0, 10) || 0; if (item.kind === 'drive#folder') { hasSelectedFolder = true; } else { selectedSize += sz; } } if (!S.shareMode && !hasSelectedFolder) selectedSizeText = `\u00A0\u00A0${fmtSize(selectedSize)}`; } UI.stat.textContent = L.status_ready.replace('{n}', statCount) + getPagingLoadingSuffix() + (selectedCount > 0 ? `\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${L.sel_count.replace('{n}', selectedCount)}${selectedSizeText}` : ''); } if (nextToken) await sleep(0); } while (nextToken && currentId === activeLoadId); if (currentId === activeLoadId) { if (mergeRefreshCtx) { if (!isMergeRefreshStillCurrent()) { mergeRefreshCtx.contextStillCurrent = false; abandonLiveManualRefresh('context_switch', { showStatus: false, detach: true }); clearMergeRefreshCtx(); return; } mergeRefreshCtx.complete = true; const missingIds = []; const canConfirmMissing = mergeRefreshCtx.status === 'refreshing' && mergeRefreshCtx.complete === true && mergeRefreshCtx.noLocalMutation === true && mergeRefreshCtx.noPageError === true && mergeRefreshCtx.contextStillCurrent === true && mergeRefreshCtx.tokenLoopSafe === true; if (canConfirmMissing) { mergeRefreshCtx.oldIds.forEach(id => { if (!mergeRefreshCtx.seenIds.has(id)) missingIds.push(id); }); } if (missingIds.length > 0) { if (!canWriteLiveManualRefreshProjection(mergeRefreshCtx)) { clearMergeRefreshCtx(); return; } const missingSet = new Set(missingIds); missingSet.forEach(id => { mergeRefreshCtx.liveMap.delete(id); S.itemMap.delete(id); S.starredSet.delete(id); }); if (S.selMode !== 'all' && S.getSelectedIds && S.setExplicitSelection) { S.setExplicitSelection(S.getSelectedIds().filter(id => !missingSet.has(String(id || '')))); } else if (S.selMode === 'all' && S.selEx) { missingSet.forEach(id => S.selEx.delete(id)); } } await flushLiveManualRefreshProjection(mergeRefreshCtx, true); if (!canWriteLiveManualRefreshProjection(mergeRefreshCtx)) { clearMergeRefreshCtx(); return; } } if (mergeRefreshCtx && !canWriteLiveManualRefreshProjection(mergeRefreshCtx)) { clearMergeRefreshCtx(); return; } if (!(S.trashMode && S.items.length === 0 && pageCount > 1)) { S.cache.set(realCacheKey, [...S.items]); } if (typeof globalCache !== 'undefined') globalCache.set(realCacheKey, [...S.items]); if (S.recentMode && realCacheKey === 'recent_root') { globalCache.set('recent_session', { phase: 'done', nextToken: null, completed: true, count: S.items.length }); } if (!mergeRefreshCtx && window.pkSmartRefreshTrigger && !shouldSkipSmartRefreshAfterLoad(realCacheKey)) { 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(); S.pagingLoading = false; if (S.offlineMode) S.offlinePagingPending = false; if (!mergeRefreshCtx) refresh(); else { updateLiveManualRefreshStatus(mergeRefreshCtx, 'completed'); clearMergeRefreshCtx(); S.mergeRefreshCtx = null; clearLiveManualRefreshLater(mergeRefreshCtx); } updateStat(); setLoad(false); S._networkRetryCount = 0; if (S.recentMode && realCacheKey === 'recent_root') clearRecentRootPagingActive(currentId, 'load_completed'); if (S.networkResumeCtx && S.networkResumeCtx.realCacheKey === realCacheKey) clearNetworkResumeCtx('load_completed'); if (S.offlineMode && realCacheKey === 'offline_root' && currentId === activeLoadId) scheduleOfflineLightProbe('enter', 1200); if (S.recentMode && realCacheKey === 'recent_root' && currentId === activeLoadId) scheduleRecentLightProbe('timer', RECENT_LIGHT_PROBE_INTERVAL); } } catch (e) { if (mergeRefreshCtx && mergeRefreshCtx.status === 'abandoned' && S.liveRefreshCtx !== mergeRefreshCtx) { clearMergeRefreshCtx(); return; } if (mergeRefreshCtx && S.liveRefreshCtx === mergeRefreshCtx) { isGUISensitive = false; S.pagingLoading = false; if (S.offlineMode) S.offlinePagingPending = false; if (e && e.name === 'AbortError') { if (mergeRefreshCtx.status === 'refreshing') updateLiveManualRefreshStatus(mergeRefreshCtx, 'abandoned'); await flushLiveManualRefreshProjection(mergeRefreshCtx, true); clearLiveManualRefreshLater(mergeRefreshCtx); clearMergeRefreshCtx(); setLoad(false); return; } mergeRefreshCtx.noPageError = false; mergeRefreshCtx.status = 'interrupted'; await flushLiveManualRefreshProjection(mergeRefreshCtx, true); updateLiveManualRefreshStatus(mergeRefreshCtx, 'interrupted', 'warning'); clearLiveManualRefreshLater(mergeRefreshCtx, 5000); clearMergeRefreshCtx(); setLoad(false); return; } clearMergeRefreshCtx(); isGUISensitive = false; if (S.recentMode && realCacheKey === 'recent_root') clearRecentRootPagingActive(currentId, 'load_error'); if (currentId === activeLoadId) { S.pagingLoading = false; if (S.offlineMode) S.offlinePagingPending = false; updateStat(); if (!S._isRetrying) setLoad(false); if (e.name !== 'AbortError') { console.error("API Error encountered:", e); if (typeof resetHeaderCache === 'function') resetHeaderCache(); const isAuthError = e.message.includes('401') || e.message.includes('403') || e.message.includes('400') || e.message.includes('CAPTCHA') || e.message.includes('AUTH_RETRY'); const isNotFoundError = e.message.includes('404'); const isNetworkError = e.name === 'TypeError' || e.message.includes('Failed to fetch') || e.message.includes('NetworkError'); if (!S._isRetrying || isNetworkError) { S._isRetrying = true; setLoad(true); if (isAuthError) { resetHeaderCache(); updateLoadTxt(L.str_waiting_token); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('auth-error', 5000); await sleep(1000); try { let h = getHeaders(); if (!h.Authorization || h.Authorization.length < 10) { const isAuthReady = await waitForAuth(5000); if (!isAuthReady) { if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('auth-error-missing-token-recheck', 5000); await sleep(1200); const isAuthReadyAgain = await waitForAuth(5000); if (!isAuthReadyAgain) { const didLogout = await confirmedLogout('auth-error-missing-token-recheck-final', 5000, 5200); if (didLogout) return; S._isRetrying = false; setLoad(false); return; } } h = getHeaders(); } let testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token rejected by server, entering recovery recheck before logout."); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('auth-error-about-recheck', 5000); await sleep(1200); const isAuthReady = await waitForAuth(5000); if (!isAuthReady) { const didLogout = await confirmedLogout('auth-error-about-first-reject', 5000, 5200); if (didLogout) return; h = getHeaders(); } else { h = getHeaders(); } testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token still rejected by server after recovery recheck."); const didLogout = await confirmedLogout('auth-error-about-second-reject', 5000, 5200); if (didLogout) return; h = getHeaders(); testRes = await fetch('https://api-drive.mypikpak.com/drive/v1/about', { headers: h }); if (!testRes.ok) { console.warn("[Auth] Token still unavailable after confirmed logout recheck, keeping current view."); S._isRetrying = false; setLoad(false); return; } } } if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); const testData = await testRes.json(); if (testData.error) { console.warn("[Auth] API returned error inside response."); const didLogout = await confirmedLogout('auth-error-about-payload-error', 5000, 5200); if (didLogout) return; S._isRetrying = false; setLoad(false); return; } if (!testData.sub || testData.sub === '') { console.warn("[Auth] Token valid but no user info."); const didLogout = await confirmedLogout('auth-error-about-empty-sub', 5000, 5200); if (didLogout) return; console.warn("[Auth] Auth recovered after empty user info, retrying file load."); if (typeof window.pkMarkOfficialAuthReady === 'function') window.pkMarkOfficialAuthReady('about-empty-sub-recovered'); updateLoadTxt(L.loading_detail); load(false, true).finally(() => { S._isRetrying = false; }); return; } } catch(testErr) { console.warn("[Auth] Token verification failed (CORS/Network/Parse):", testErr); const didLogout = await confirmedLogout('auth-error-about-verify-exception', 5000, 5200); if (didLogout) return; S._isRetrying = false; setLoad(false); return; } load(false, true).finally(() => { S._isRetrying = false; }); return; } if (isNotFoundError) { if (S.path.length > 1 || S.path[0].id !== '') { stopLiveManualRefreshBeforePathChange('path_change'); S.path = [{ id: '', name: L.btn_nav_home }]; load(false, true).finally(() => { S._isRetrying = false; }); return; } } if (isNetworkError) { S._isRetrying = false; S._networkRetryCount = 0; S.pagingLoading = false; if (S.offlineMode) S.offlinePagingPending = false; setLoad(false); const resumeMode = getNetworkResumeMode(realCacheKey); const canProtectNetworkList = isProtectedNetworkResumeTarget(realCacheKey, resumeMode); if (canProtectNetworkList) await flushNetworkResumeVisibleState(); else updateStat(); const resumeSnap = canProtectNetworkList ? getNetworkResumeCachedState(realCacheKey, resumeMode) : null; const canResumeAfterNetwork = !!(resumeSnap && resumeSnap.nextToken && Array.isArray(resumeSnap.items) && resumeSnap.items.length > 0 && currentId === activeLoadId && !signal.aborted); if (canResumeAfterNetwork) { scheduleNetworkResumeCtx({ id: `net_${Date.now()}_${Math.random().toString(36).slice(2)}`, mode: resumeMode, realCacheKey, folderId, pathKey: getLiveManualRefreshPathKey(), modeKey: getLiveManualRefreshModeKey(), createdAt: Date.now(), retry: 0 }); console.warn(`[Network] Connection drop detected. Current list is kept; ${resumeMode} paging will resume from cached nextToken after network recovery.`); } else if (canProtectNetworkList && Array.isArray(S.items) && S.items.length > 0) { console.warn("[Network] Connection drop detected. Protected list is kept; automatic full reload is skipped."); } else { console.warn("[Network] Connection drop detected. Current load is stopped without modal reload."); } showToast(L.msg_network_unstable, 'warning'); return; } S._isRetrying = false; } if (S.items.length === 0) { setLoad(false); showAlert(L.str_failed + " " + e.message); } } } } } async function refresh() { S.sortId++; const currentReqId = S.sortId; syncLocalUploadVisibility(); if (S.linkBookmarkMode) { setLoad(false); renderCrumb(); loadLinkBookmarkOfficial(true); updateStat(); return; } if (S.shareParseMode) { setLoad(false); renderCrumb(); if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); if (S.shareParseListActive) { syncShareParseMirror(); renderList(); } else { renderShareParsePanel(); } syncShareParseInsightFilterBar(); updateStat(); return; } const prevViewMode = S.viewMode === 'grid' ? 'grid' : 'list'; const nextCur = S.path[S.path.length - 1] || { id: '' }; const nextIsStandardView = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!nextCur.id.startsWith('virtual_') || nextCur.id === 'virtual_search_root'); const groupedGridExitSyncPending = !!S._groupedGridExitSyncPending; if (nextIsStandardView) { const nextFolderId = nextCur.id || 'root'; const nextViewMode = (typeof resolvePreferredViewMode === 'function') ? resolvePreferredViewMode(nextFolderId) : (gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'); if (prevViewMode !== nextViewMode || (groupedGridExitSyncPending && nextViewMode === 'grid')) { beginFolderViewSync(); } S._groupedGridExitSyncPending = false; S.viewMode = nextViewMode; } else { S._groupedGridExitSyncPending = false; endFolderViewSync(); } renderCrumb(); if (!S.dupMode) S.dupItemMap = null; const isStrictVirtual = S.isFlattened || S.dupMode || S.analyzeMode; const cur = S.path[S.path.length - 1]; S.syncNavContextState(); if (!isStrictVirtual) { restoreFolderFirstAfterStrictMode(); } const hideAnalyzeButtons = shouldHideAnalyzeButtonsForCurrentMode(); 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 = hideAnalyzeButtons ? 'none' : 'flex'; } if (UI.btnExport) { UI.btnExport.style.display = hideAnalyzeButtons ? 'none' : 'flex'; } if (UI.scan) { UI.scan.style.display = hideAnalyzeButtons ? 'none' : 'flex'; } if (UI.btnExit) { UI.btnExit.style.display = isStrictVirtual ? 'flex' : 'none'; } if (UI.lblSearchPath) { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const showAnalyzeGroupTools = isAnalyzeRoot && !S.search && S.analyzeSimGroups && !S.shareParseMode; const isAnalyzeDupView = S.analyzeMode && S.analyzeSimGroups; if (UI.win) { UI.win.classList.toggle('pk-analyze-group-tools-on', !!showAnalyzeGroupTools); UI.win.classList.toggle('pk-share-parse-mode', !!S.shareParseMode); UI.win.classList.toggle('pk-link-bookmark-mode', !!S.linkBookmarkMode); } UI.lblSearchPath.style.display = (S.dupMode || S.isFlattened || isAnalyzeRoot) ? 'flex' : 'none'; if (UI.btnAnaSelect) UI.btnAnaSelect.style.display = showAnalyzeGroupTools ? 'flex' : 'none'; if (UI.btnAnaSort) UI.btnAnaSort.style.display = showAnalyzeGroupTools ? 'flex' : 'none'; if (UI.vp) UI.vp.style.overflowX = (S.dupMode || isAnalyzeDupView) ? 'hidden' : ''; } if (UI.filterBar) { if (S.isFlattened && !S.dupMode) { UI.filterBar.style.display = 'flex'; if (S.filterState.active) { UI.filterBtn.style.display = 'none'; UI.filterActiveUI.style.display = 'flex'; if (typeof renderActiveFilterUI === 'function') renderActiveFilterUI(); } else { UI.filterBtn.style.display = 'flex'; UI.filterActiveUI.style.display = 'none'; } } else { UI.filterBar.style.display = 'none'; } } const isStarredRoot = isCurrentStarredRoot(); const isRecentRoot = S.recentMode && S.path.length === 1; if (UI.btnNewFolder) { UI.btnNewFolder.style.display = shouldHideNewFolderButtonForCurrentMode() ? 'none' : 'flex'; } if (UI.btnPaste) { const isPasteBlocked = S.isFlattened || S.dupMode || S.shareMode || S.linkBookmarkMode || 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'; if (S.search && !S.dupMode && !isInsideSearchResult) { const q = S.search.toLowerCase(); const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; if (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups) { const newDisplay = []; S.analyzeSimGroups.forEach((g, gIdx) => { const groupItems = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); const isGroupHit = g.type.toLowerCase().includes(q) || groupItems.some(it => { const nameMatch = it.name.toLowerCase().includes(q); let pathMatch = false; if (includePath && it._pathStr) { const homeText = L.btn_nav_home; const parentPath = (it._pathStr === homeText || it._pathStr.startsWith(homeText + '/')) ? it._pathStr : (homeText + '/' + it._pathStr); const fullItemPath = parentPath.endsWith('/') ? (parentPath + it.name) : (parentPath + '/' + it.name); pathMatch = fullItemPath.toLowerCase().includes(q); } return nameMatch || pathMatch; }); if (isGroupHit) { newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: g.type, count: g.ids.length, type: g.type || L.str_group }); groupItems.sort((a, b) => parseInt(b.size || 0) - parseInt(a.size || 0)); groupItems.forEach(it => { if (it.name.toLowerCase().includes(q)) { const name = it.name; const idx = name.toLowerCase().indexOf(q); const len = q.length; const start = Math.max(0, idx - 15); const end = Math.min(name.length, idx + len + 30); let pre = start > 0 ? "..." : ""; let suf = end < name.length ? "..." : ""; it._hlNameHTML = `${pre}${esc(name.substring(start, idx))}${esc(name.substring(idx, idx + len))}${esc(name.substring(idx + len, end))}${suf}`; } else { delete it._hlNameHTML; } newDisplay.push(it); }); } }); S.display = newDisplay; } else if (UI.chkGlobal && UI.chkGlobal.checked && cur.id === 'virtual_search_root') { let results = []; const seenIds = new Set(); let realMyPackId = ''; if (typeof globalCache !== 'undefined') { const rootFiles = globalCache.get('') || globalCache.get('root'); if (rootFiles) { const realObj = rootFiles.find(f => f.kind === 'drive#folder' && f.name === CONF.SYSTEM_FOLDER_NAME); if (realObj) realMyPackId = realObj.id; } } if (CONF.SYSTEM_FOLDER_NAME.toLowerCase().includes(q)) { const realObj = (globalCache.get('') || globalCache.get('root'))?.find(f => f.name === CONF.SYSTEM_FOLDER_NAME); const sysRoot = { id: realMyPackId, kind: 'drive#folder', name: CONF.SYSTEM_FOLDER_NAME, parent_id: '', size: 0, modified_time: '', starred: false, tags: [], _lineage: [], _isSystemRoot: true, icon_link: realObj ? realObj.icon_link : '', thumbnail_link: realObj ? realObj.icon_link : '' }; results.push(sysRoot); if (realMyPackId) seenIds.add(realMyPackId); } if (typeof globalCache !== 'undefined') { for (const [key, files] of globalCache) { if (currentReqId !== S.sortId) return; if (!Array.isArray(files)) continue; if (key && (key.endsWith('_root') || key === 'root_trashed')) continue; let parentLineage = (typeof globalLineageMap !== 'undefined') ? globalLineageMap.get(key) : undefined; if (!parentLineage) { if (key === '' || key === 'root') parentLineage = []; else parentLineage = null; } for (let i = 0, len = files.length; i < len; i++) { const f = files[i]; if (seenIds.has(f.id)) continue; if (f && f.name && f.name.toLowerCase().includes(q)) { let itemLineage = parentLineage; const fObj = { ...f, _lineage: itemLineage }; const fName = fObj.name; const fIdx = fName.toLowerCase().indexOf(q); if (fIdx !== -1) { const start = Math.max(0, fIdx - 15); const end = Math.min(fName.length, fIdx + q.length + 30); let pre = start > 0 ? "..." : ""; let suf = end < fName.length ? "..." : ""; const b = fName.substring(start, fIdx); const m = fName.substring(fIdx, fIdx + q.length); const a = fName.substring(fIdx + q.length, end); fObj._hlNameHTML = `${pre}${esc(b)}${esc(m)}${esc(a)}${suf}`; } results.push(fObj); seenIds.add(f.id); } } } } S.display = results; S.items = results; S.lastGlobalResults = [...results]; ensureItemMap(); renderCrumb(); } else if (isInVirtualSearch && cur.id !== 'virtual_search_root') { let ancestors = (typeof globalLineageMap !== 'undefined' && globalLineageMap.get(cur.id)) || cur._lineage || []; const currentFolderAsAncestor = [...ancestors, { id: cur.id, name: cur.name }]; S.display = S.items.map(item => { const itemLineage = (item.kind === 'drive#folder' && typeof globalLineageMap !== 'undefined' && globalLineageMap.has(item.id)) ? globalLineageMap.get(item.id) : currentFolderAsAncestor; return { ...item, _lineage: itemLineage }; }); } else { const query = S.search.toLowerCase(); const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const isPathMode = S.isFlattened || (S.analyzeMode && cur.id === 'analyze_root'); const rootPathStr = S.path.map(p => p.name).join('/'); let filtered = S.items.filter(i => { if (!i) return false; if (i.name && i.name.toLowerCase().includes(query)) return true; if (includePath && isPathMode) { let pStr = ""; if (S.analyzeMode) { pStr = i._pathStr || L.btn_nav_home; } else if (i._lineage) { let cleanLineage = i._lineage.map(x => x.name).filter(n => n && n !== 'Root' && n !== L.str_root_dir_cn); if (cleanLineage[0] !== L.btn_nav_home) cleanLineage.unshift(L.btn_nav_home); pStr = cleanLineage.join('/'); } const fullItemPath = pStr.endsWith('/') ? (pStr + i.name) : (pStr + '/' + i.name); if (fullItemPath.toLowerCase().includes(query)) return true; } return false; }); if (S.offlineMode) { const cfg = S.offlineFilters; filtered = filtered.filter(i => { const p = i.phase; if (p === 'PHASE_TYPE_COMPLETE') return cfg.complete; if (['PHASE_TYPE_RUNNING', 'PHASE_TYPE_PENDING', 'PHASE_TYPE_PAUSED'].includes(p)) return cfg.running; return cfg.failed; }); } filtered.forEach(item => { const name = item.name; const idx = name.toLowerCase().indexOf(query); if (idx !== -1) { const len = query.length; const start = Math.max(0, idx - 15); const end = Math.min(name.length, idx + len + 30); let prefix = start > 0 ? "..." : ""; let suffix = end < name.length ? "..." : ""; const partBefore = name.substring(start, idx); const partMatch = name.substring(idx, idx + len); const partAfter = name.substring(idx + len, end); item._hlNameHTML = `${prefix}${esc(partBefore)}${esc(partMatch)}${esc(partAfter)}${suffix}`; } }); S.display = filtered; } } else { if (S.path.length > 0 && S.path[0].id === 'starred') { S.display = S.items.filter(i => (i.starred || (i.tags && i.tags.some(t => t.name === 'STAR'))) && !i.trashed); } else if (S.isFlattened && S.filterState && S.filterState.active && S.filterState.cat !== 'all') { const cat = S.filterState.cat; const ext = S.filterState.ext; const fExts = { video: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mpg','mpeg','rm','rmvb','asf','vob','dat','divx','f4v','m2ts','mts','tp','trp','ogv','mpe','m2v','m3u8'], audio: ['mp3','wav','flac','aac','ogg','wma','ape','m4a','amr','opus','m4b','alac','aiff','mid','midi','ra','dts','ac3','dsf','dff'], image: ['jpg','jpeg','png','gif','bmp','webp','svg','tif','tiff','ico','heic','heif','raw','cr2','nef','arw','dng','orf','avif','psd','ai','eps','jfif','jpe'], document: ['txt','html','pdf','pptx','chm','docx','xlsx','htm','doc','dwg','mdb','ppt','xls','rtf','odt','ods','odp','epub','mobi','azw3','djvu','cbz','cbr','md','log','csv','xml','json'], software: ['apk','exe','ipa','dmg','rpm','deb','msi','pkg','xapk','apks','aab','jar','bin','sh','bat','cmd'], archive: ['zip','rar','7z','tar','gz','iso','cab','bz2','xz','tgz','wim','esd','img','zst','lzh'], torrent: ['torrent'] }; let matchExts =[]; if (cat === 'other') { const definedExts = new Set([...fExts.video, ...fExts.audio, ...fExts.image, ...fExts.document, ...fExts.software, ...fExts.archive, ...fExts.torrent]); S.display = S.items.filter(i => { if (i.kind === 'drive#folder') return false; const n = (i.name || '').toLowerCase(); const e = n.split('.').pop(); return !definedExts.has(e); }); } else { if (ext === 'all') { matchExts = fExts[cat] ||[]; } else { matchExts = [ext]; } S.display = S.items.filter(i => { if (i.kind === 'drive#folder') return false; const n = (i.name || '').toLowerCase(); const e = n.split('.').pop(); return matchExts.includes(e); }); } } else if (S.offlineMode) { const cfg = S.offlineFilters; S.display = S.items.filter(i => { const p = i.phase; if (p === 'PHASE_TYPE_COMPLETE') return cfg.complete; if (['PHASE_TYPE_RUNNING', 'PHASE_TYPE_PENDING', 'PHASE_TYPE_PAUSED'].includes(p)) return cfg.running; return cfg.failed; }); } else if (S.uploadMode) { const cfg = S.uploadFilters; S.display = S.items.filter(i => { const s = i.status; if (s === 'DONE') return cfg.complete; if (s === 'PAUSED' || s === 'ERROR') return cfg.paused; return cfg.running; }); } else if (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups) { const newDisplay =[]; S.analyzeSimGroups.forEach((g, gIdx) => { newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: g.type, count: g.ids.length, type: g.type || L.str_group }); const groupItems = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); groupItems.sort((a, b) => { const pA = a._pathStr || ""; const pB = b._pathStr || ""; if (pA.length !== pB.length) return pA.length - pB.length; return pA.localeCompare(pB); }); let lastPathInGroup = null; groupItems.forEach(it => { const currentPath = it._pathStr || ""; if (currentPath === lastPathInGroup && currentPath !== "") { it._isSameFolder = true; } else { it._isSameFolder = false; lastPathInGroup = currentPath; } newDisplay.push(it); }); }); S.display = newDisplay; } else { S.display = [...S.items]; } } if (currentReqId !== S.sortId) return; const isFlattenBaseEmpty = S.isFlattened && !S.dupMode && !S.analyzeMode && !S.scanning && Array.isArray(S.items) && S.items.length === 0; const isAnalyzeBaseEmpty = S.analyzeMode && cur && cur.id === 'analyze_root' && !S.analyzeSimGroups && !S.scanning && Array.isArray(S.items) && S.items.length === 0; if ((isFlattenBaseEmpty || isAnalyzeBaseEmpty) && !S._autoExitingVirtualEmpty) { S._autoExitingVirtualEmpty = true; try { if (typeof S.exitVirtualNavMode === 'function') { await S.exitVirtualNavMode(); } else if (UI.btnExit) { UI.btnExit.click(); } else { S.isFlattened = false; S.analyzeMode = false; S.dupMode = false; refresh(); } } finally { S._autoExitingVirtualEmpty = false; } return; } const visibleSet = new Set(S.display.map(i => i.id)); if (S.selMode === 'all') { const nextExcluded = new Set(); for (const id of S.selEx) { if (visibleSet.has(id)) nextExcluded.add(id); } S.selEx = nextExcluded; } else { const nextSelected = S.getSelectedIds().filter(id => visibleSet.has(id)); S.setExplicitSelection(nextSelected); } S.dupReasons.clear(); S.dupGroups.clear(); if (!S.dupMode) S.pinnedDupPath = null; const isStandardView = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && !S.analyzeMode && (!cur.id.startsWith('virtual_') || cur.id === 'virtual_search_root'); const folderFirstViewKey = S.starredMode ? '__starred__' : (S.recentMode ? '__recent__' : ''); if (isStandardView) { const folderId = cur.id || 'root'; if (S._viewAppliedForId !== folderId) { S.viewMode = resolvePreferredViewMode(folderId); S._viewAppliedForId = folderId; } if (S._sortAppliedForId !== folderId) { syncFolderFirstFromGlobal(); S._sortAppliedForId = folderId; if (S.renderFolderFirst) S.renderFolderFirst(); } } else if (folderFirstViewKey) { if (S._sortAppliedForId !== folderFirstViewKey) { syncFolderFirstFromGlobal(); S._sortAppliedForId = folderFirstViewKey; if (S.renderFolderFirst) S.renderFolderFirst(); } } if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); const makeDupResultSig = () => { const buckets = S.dupBuckets && Array.isArray(S.dupBuckets.all) ? S.dupBuckets.all : []; const first = buckets[0] && (buckets[0].id || buckets[0].name || buckets[0].path || ''); const last = buckets[buckets.length - 1] && (buckets[buckets.length - 1].id || buckets[buckets.length - 1].name || buckets[buckets.length - 1].path || ''); return ['dup', S.scanId || 0, S.items.length || 0, buckets.length || 0, S.dupRawGroups && S.dupRawGroups.length || 0, first, last, S.dupConfig && S.dupConfig.video ? 1 : 0, S.dupConfig && S.dupConfig.image ? 1 : 0, S.dupConfig && S.dupConfig.other ? 1 : 0].join('|'); }; if (S.dupMode) { const dupResumeSig = makeDupResultSig(); const canReuseDupResult = !S._precomputedDupBuckets && !S.scanning && !S.dupRunning && S.dupBuckets && Array.isArray(S.dupBuckets.all) && S.dupBuckets.all.length > 0 && S.dupItemMap && Array.isArray(S.items) && S.items.length > 0 && S.dupResultSig === dupResumeSig; if (canReuseDupResult) { 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(); updateStat(); return; } 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 dupItemMap; let dupBuckets; const precomputedDup = S._precomputedDupBuckets || null; if (precomputedDup) { S._precomputedDupBuckets = null; dupItemMap = precomputedDup.dupItemMap; dupBuckets = precomputedDup.dupBuckets; if (precomputedDup.dupReasons instanceof Map) { S.dupReasons.clear(); precomputedDup.dupReasons.forEach((v, k) => S.dupReasons.set(k, v)); } } else { 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; }); ({ dupItemMap, dupBuckets } = buildDuplicateBucketsFromGroups(groups, S.items)); } S.dupItemMap = dupItemMap; if (dupBuckets.all.length === 0) { const savedSearch = String(S.search || ''); const savedSearchInput = UI.searchInput ? String(UI.searchInput.value || '') : savedSearch; const savedSearchPathChecked = UI.chkSearchPath ? !!UI.chkSearchPath.checked : false; const savedLastSearchGlobal = !!S.lastSearchGlobal; const savedGlobalChecked = UI.chkGlobal ? !!UI.chkGlobal.checked : false; S.dupRunning = false; setLoad(false); showToast(L.msg_dup_none); await S.exitVirtualNavMode(); S.search = savedSearch; S.lastSearchGlobal = savedLastSearchGlobal; if (UI.searchInput) UI.searchInput.value = savedSearchInput || savedSearch; if (UI.searchClear) UI.searchClear.style.display = String((UI.searchInput && UI.searchInput.value) || savedSearch || '').trim() ? 'flex' : 'none'; if (UI.searchHist) UI.searchHist.style.display = 'none'; if (UI.chkSearchPath) UI.chkSearchPath.checked = savedSearchPathChecked; if (UI.chkGlobal) UI.chkGlobal.checked = savedGlobalChecked; syncLocalUploadVisibility(); if (S.search) { await refresh(); updateStat(); } return; } S.search = ''; S.lastGlobalResults = []; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.searchHist) UI.searchHist.style.display = 'none'; S.dupBuckets = dupBuckets; S.dupRawGroups = dupBuckets.all; if (UI.chkName) UI.chkName.checked = true; if (UI.chkSim) UI.chkSim.checked = true; if (UI.chkHash) UI.chkHash.checked = true; const applyDupSelection = () => { const targetPath = UI.selDupFolder.value; const invertChk = document.getElementById('pk-dup-invert'); if (!targetPath || targetPath === "__RESET__") { S.pinnedDupPath = null; S.sel.clear(); if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } renderDupView(); updateStat(); if(targetPath) UI.selDupFolder.value = ""; return; } if (invertChk) { invertChk.disabled = false; invertChk.parentNode.style.opacity = '1'; } const isInvert = invertChk ? invertChk.checked : false; S.pinnedDupPath = targetPath; const itemMap = new Map(); for(const item of S.items) itemMap.set(item.id, item); S.sel.clear(); S.dupRawGroups.forEach(g => { const filesInTarget = []; const filesInOther = []; g.ids.forEach(id => { const item = itemMap.get(id); if (item) { let path = ""; if (item._cachedPath !== undefined) { path = item._cachedPath; } else { if (item._lineage && item._lineage.length > 0) { path = item._lineage.map(p => p.name).join('/'); } else { const curFolder = S.path[S.path.length - 1]; path = curFolder ? curFolder.name : L.current_dir; } item._cachedPath = path; } if (path === targetPath) filesInTarget.push(item); else filesInOther.push(item); } }); if (filesInOther.length > 0) { if (isInvert) { filesInOther.forEach(f => S.sel.add(f.id)); } else { filesInTarget.forEach(f => S.sel.add(f.id)); } } else if (filesInTarget.length > 0) { if (!isInvert) { filesInTarget.forEach(f => S.sel.add(f.id)); } } }); renderDupView(); updateStat(); if (S._dupFolderPickedScrollTop) { S._dupFolderPickedScrollTop = false; if (S._dupFolderPickedTopRaf) cancelAnimationFrame(S._dupFolderPickedTopRaf); S._dupFolderPickedTopRaf = requestAnimationFrame(() => { S._dupFolderPickedTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } }; UI.selDupFolder.onchange = applyDupSelection; setTimeout(() => { const invertChk = document.getElementById('pk-dup-invert'); if (invertChk) { invertChk.onchange = () => { if (UI.selDupFolder.value && UI.selDupFolder.value !== "__RESET__") { applyDupSelection(); } }; } const smartBtn = document.getElementById('pk-dup-smart-btn'); if (smartBtn) { smartBtn.addEventListener('click', () => { if (S.pinnedDupPath) { S.pinnedDupPath = null; UI.selDupFolder.value = ""; if (invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } renderDupView(); } }, { capture: true }); } }, 0); S.dupRunning = false; setLoad(false); if (UI.dupTools) UI.dupTools.style.display = 'flex'; if (UI.dupFilters) { UI.dupFilters.style.display = 'flex'; const simChkWrapper = UI.dupFilters.querySelector('#pk-chk-sim')?.closest('.pk-dup-chk'); if (simChkWrapper) { simChkWrapper.style.display = S.dupConfig.video ? 'flex' : 'none'; } } renderDupView(); S.dupResultSig = makeDupResultSig(); } else { if (UI.dupTools) UI.dupTools.style.display = 'none'; if (UI.dupFilters) UI.dupFilters.style.display = 'none'; if (S.uploadMode || (S.analyzeMode && S.analyzeSimGroups && cur.id === 'analyze_root')) { renderList(); if (gmGet('pk_keep_pos', true) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { const targetTop = getItemScrollTopByIndex(targetIdx); UI.vp.scrollTop = Math.max(0, targetTop - (UI.vp.clientHeight / 2) + (CONF.rowHeight / 2)); S.clearSelection(); S.activeId = S.latestChildId; renderVisible(); } S.latestChildId = null; } updateStat(); return; } const suppressSortMaskForPaging = S._suppressSortMaskForPaging === true; const makeVirtualSortMaskSig = (list = S.display) => { if (!(S.isFlattened && !S.dupMode && !S.analyzeMode && !S.scanning) || !Array.isArray(list) || !list.length) return ''; const first = list[0] && list[0].id || ''; const last = list[list.length - 1] && list[list.length - 1].id || ''; return ['flat', S.scanId || 0, S.sort || '', S.dir || 1, S.folderFirst ? 1 : 0, S.search || '', UI.chkSearchPath && UI.chkSearchPath.checked ? 1 : 0, JSON.stringify(S.filterState || {}), list.length, first, last].join('|'); }; const currentVirtualSortMaskSig = makeVirtualSortMaskSig(S.display); const suppressSortMaskForVirtualResume = !!(currentVirtualSortMaskSig && S.virtualSortMaskSig === currentVirtualSortMaskSig); const showLargeSortMask = S.display.length > 5000 && !S.offlineMode && !S.historyMode && !suppressSortMaskForPaging && !suppressSortMaskForVirtualResume; if (showLargeSortMask) { 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 liveStableOrder = canUseLiveManualRefreshStableTie(S.liveRefreshCtx); 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; const historySortDur = S.historyMode ? (parseFloat(item._history_duration || 0) || finalDur) : 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, id: String(item.id || ''), lo: liveStableOrder ? getLiveManualRefreshStableOrder(item) : Number.MAX_SAFE_INTEGER, n: item.name, k: item.kind === 'drive#folder' ? 1 : 0, s: item.size || 0, t: item.modified_time, d: S.historyMode ? historySortDur : finalDur, st: isStarred ? 1 : 0, mp: isMyPack ? 1 : 0, m: mimeGroup, p: pathStr, ct: item.created_time, pt: item._history_ts || 0, pg: historySortDur > 0 ? Math.min(100, Math.max(0, ((item._history_progress || 0) / historySortDur) * 100)) : -1, v: parseInt(item.view_count || 0), sc: parseInt(item.save_count || 0), ss: item.share_status || 'OK', ed: parseInt(item.expiration_days || -1), lc: parseInt(item.limit_count || 0) }; } const workerCode = ` self.onmessage = function(e) { const { proxy, sort, dir, reqId, folderFirst, locale, liveStableOrder } = 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); }; const compareLiveManualRefreshStableTie = (a, b) => { if (!liveStableOrder) return 0; const max = Number.MAX_SAFE_INTEGER; const ai = Number.isFinite(a.lo) ? a.lo : max; const bi = Number.isFinite(b.lo) ? b.lo : max; if (ai !== bi) return ai - bi; const aid = String(a.id || ''); const bid = String(b.id || ''); if (!aid || !bid || aid === bid) return 0; return aid < bid ? -1 : 1; }; const compareNameTie = (a, b, multiplier = dir) => { const nameCmp = compareNames(a.n, b.n) * multiplier; if (nameCmp !== 0) return nameCmp; return compareLiveManualRefreshStableTie(a, b); }; const compareStableThenName = (a, b, multiplier = dir) => { const stableTie = compareLiveManualRefreshStableTie(a, b); if (stableTie !== 0) return stableTie; return compareNames(a.n, b.n) * multiplier; }; 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 compareStableThenName(a, b); } 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 compareStableThenName(a, b); } if (sort === 'modified_time') { const tA = parseTime(a.t), tB = parseTime(b.t); if (tA !== tB) return (tB - tA) * dir; return compareStableThenName(a, b); } if (sort === 'created_time') { const tA = parseTime(a.ct), tB = parseTime(b.ct); if (tA !== tB) return (tB - tA) * dir; return compareStableThenName(a, b); } if (sort === 'progress') { const pctA = typeof a.pg === 'number' ? a.pg : -1; const pctB = typeof b.pg === 'number' ? b.pg : -1; if (pctA !== pctB) return (pctB - pctA) * dir; return compareStableThenName(a, b); } if (sort === 'play_time') { if (a.pt !== b.pt) return (b.pt - a.pt) * dir; return compareStableThenName(a, b); } if (sort === 'path') { const pathCmp = compareNames(a.p, b.p); if (pathCmp !== 0) return pathCmp * dir; return compareStableThenName(a, b); } if (sort === 'size') { if (a.k !== b.k) return folderFirst ? (b.k - a.k) : (a.k - b.k); const sizeA = parseSize(a.s), sizeB = parseSize(b.s); if (sizeA !== sizeB) return (sizeB - sizeA) * dir; return compareStableThenName(a, b); } if (sort === 'duration') { if (a.k !== b.k) return folderFirst ? (b.k - a.k) : (a.k - b.k); if (a.k) return compareStableThenName(a, b); 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 compareStableThenName(a, b); } const isVidA = (a.m === 1); const isVidB = (b.m === 1); if (isVidA !== isVidB) return isVidA ? -1 : 1; if (isVidA) { return compareStableThenName(a, b, 1); } 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 compareStableThenName(a, b); } } if (sort === 'starred') { if (a.st !== b.st) return b.st - a.st; const timeCmp = (parseTime(b.t) - parseTime(a.t)) * dir; if (timeCmp !== 0) return timeCmp; return compareLiveManualRefreshStableTie(a, b); } return compareNameTie(a, b); }); const sortedIndices = new Uint32Array(proxy.length); for (let i = 0; i < proxy.length; i++) sortedIndices[i] = proxy[i].i; self.postMessage({ indices: sortedIndices, reqId: reqId }, [sortedIndices.buffer]); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); const sortWorker = new Worker(workerUrl); sortWorker.onmessage = (e) => { const { indices, reqId } = e.data; URL.revokeObjectURL(workerUrl); sortWorker.terminate(); if (reqId === S.sortId) { resolve(Array.from(indices).map(idx => S.display[idx])); } else resolve(null); }; const sortLocale = ({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en','id':'id','ms':'ms'})[getLang()] || 'en'; sortWorker.postMessage({ proxy: proxyList, sort: S.sort, dir: S.dir, reqId: currentReqId, folderFirst: S.folderFirst, locale: sortLocale, liveStableOrder }); }); if (showLargeSortMask) setLoad(false); if (sortedList === null) return; S.display = sortedList; const finalVirtualSortMaskSig = makeVirtualSortMaskSig(S.display); if (finalVirtualSortMaskSig) S.virtualSortMaskSig = finalVirtualSortMaskSig; } if (currentReqId !== S.sortId) return; const currentIds = new Set(S.display.map(i => i.id)); if (S.selMode === 'all') { const nextExcluded = new Set(); for (const id of S.selEx) { if (currentIds.has(id)) nextExcluded.add(id); } S.selEx = nextExcluded; } else { const nextSelected = S.getSelectedIds().filter(id => currentIds.has(id)); S.setExplicitSelection(nextSelected); } const refreshLiveCtx = S.liveRefreshCtx; if (canWriteLiveManualRefreshProjection(refreshLiveCtx) && (!S.search || isSearchResultRealSubPath()) && !S.dupMode && !S.isFlattened && !S.analyzeMode) { const projectedLiveItems = S.display.filter(item => item && !item.isHeader); if (canWriteLiveManualRefreshProjection(refreshLiveCtx)) { S.items = projectedLiveItems; refreshLiveCtx.liveMap = new Map(projectedLiveItems.map(item => [String(item.id || ''), item]).filter(([id]) => !!id)); syncLiveManualRefreshStoresFromItems(S.items); } } renderList(); if (gmGet('pk_keep_pos', true) && S.latestChildId) { const targetIdx = S.display.findIndex(x => x.id === S.latestChildId); if (targetIdx !== -1) { const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; const centerScroll = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); UI.vp.scrollTop = centerScroll; S.activeId = S.latestChildId; S.clearSelection(); S.activeId = S.latestChildId; renderVisible(); } S.latestChildId = null; } updateStat(); } function renderDupView() { if (!S.dupMode) return; clearGridStableRows(UI.in); const L = getStrings(); const showName = UI.chkName.checked; const showSim = UI.chkSim.checked; const showHash = UI.chkHash.checked; const newDisplay = []; if (!S.dupItemMap || S.dupItemMap.size !== S.items.length) { S.dupItemMap = new Map(); const len = S.items.length; for (let i = 0; i < len; i++) { S.dupItemMap.set(S.items[i].id, S.items[i]); } } const itemMap = S.dupItemMap; const getCachedPath = (item) => { if (item._cachedPath !== undefined) return item._cachedPath; let pStr = ""; if (item._lineage && item._lineage.length > 0) { pStr = item._lineage.map(node => node.name).join('/'); } else { const curFolder = S.path[S.path.length - 1]; pStr = curFolder ? curFolder.name : L.current_dir; } item._cachedPath = pStr; return pStr; }; S.dupGroups.clear(); let dupBuckets = S.dupBuckets; if (!dupBuckets || !Array.isArray(dupBuckets.all)) { dupBuckets = { all: Array.isArray(S.dupRawGroups) ? S.dupRawGroups : [], hash: [], sim: [], name: [] }; const allLen = dupBuckets.all.length; for (let i = 0; i < allLen; i++) { const g = dupBuckets.all[i]; if (g.type === L.tag_hash) dupBuckets.hash.push(g); else if (g.type === L.tag_sim) dupBuckets.sim.push(g); else if (g.type === L.tag_name) dupBuckets.name.push(g); } S.dupBuckets = dupBuckets; } if (dupBuckets.all.length === 0 && !S.dupRunning) { if (UI.btnExit) { UI.btnExit.click(); } else { S.dupMode = false; S.isFlattened = false; refresh(); } return; } if (!S.dupRunning) { let hasLiveDupGroup = false; const allGroups = dupBuckets.all || []; for (let i = 0; i < allGroups.length; i++) { const ids = allGroups[i].ids || []; let liveCount = 0; for (let k = 0; k < ids.length; k++) { if (itemMap.has(ids[k])) { liveCount++; if (liveCount >= 2) { hasLiveDupGroup = true; break; } } } if (hasLiveDupGroup) break; } if (!hasLiveDupGroup) { if (UI.btnExit) { UI.btnExit.click(); } else { S.dupMode = false; S.isFlattened = false; refresh(); } return; } } let groupsToRender = []; if (showHash) groupsToRender.push(...dupBuckets.hash); if (showSim) groupsToRender.push(...dupBuckets.sim); if (showName) groupsToRender.push(...dupBuckets.name); if (S.pinnedDupPath) { const pinnedGroups = []; const normalGroups = []; for (const g of groupsToRender) { let isPinned = false; for (const id of g.ids) { const item = itemMap.get(id); if (item) { const path = getCachedPath(item); if (path === S.pinnedDupPath) { isPinned = true; break; } } } if (isPinned) pinnedGroups.push(g); else normalGroups.push(g); } groupsToRender = [...pinnedGroups, ...normalGroups]; } const dynamicFolderStats = new Map(); const groupsLen = groupsToRender.length; const searchKey = S.search ? S.search.toLowerCase().trim() : null; const includePath = S.search && UI.chkSearchPath ? UI.chkSearchPath.checked : false; for (let gIdx = 0; gIdx < groupsLen; gIdx++) { const g = groupsToRender[gIdx]; if (g.type === L.tag_name && !showName) continue; if (g.type === L.tag_sim && !showSim) continue; if (g.type === L.tag_hash && !showHash) continue; const ids = g.ids; const idsLen = ids.length; 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 (groupItems.length < 2) continue; const activeGroupIds = groupItems.map(item => item.id); for (let k = 0; k < activeGroupIds.length; k++) { S.dupGroups.set(activeGroupIds[k], gIdx); } 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 = groupItems[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 = `${activeGroupIds.length} ${L.str_items}`; const smartTitle = `${countStr} | ${featureStr}${baseName}`; newDisplay.push({ id: `grp_${gIdx}`, isHeader: true, name: smartTitle, count: activeGroupIds.length, type: g.type || L.str_group, groupIds: activeGroupIds }); const selectedDupPath = S.pinnedDupPath || ""; const compareDupGroupData = (a, b) => { const op = S.groupSortOp || 'path'; const dir = S.groupSortDir || 1; let res = 0; if (op === 'time') { res = new Date(b.it.modified_time) - new Date(a.it.modified_time); } else if (op === 'size') { const sa = BigInt(a.it.size || 0), sb = BigInt(b.it.size || 0); res = sa < sb ? 1 : (sa > sb ? -1 : 0); } else if (op === 'path') { if (a.path.length !== b.path.length) res = a.path.length - b.path.length; else res = a.path.localeCompare(b.path); } else if (op === 'name') { res = a.it.name.localeCompare(b.it.name); } return res * dir; }; const sortDupGroupChunk = arr => arr.slice().sort(compareDupGroupData); const groupData = groupItems.map((it, idx) => ({ it, path: groupPaths[idx] })); let sortedGroupData; if (selectedDupPath) { const groupA = []; const groupB = []; for (let i = 0; i < groupData.length; i++) { const row = groupData[i]; if (row.path === selectedDupPath) groupA.push(row); else groupB.push(row); } if (groupA.length > 0) { sortedGroupData = sortDupGroupChunk(groupA).concat(sortDupGroupChunk(groupB)); } else { sortedGroupData = sortDupGroupChunk(groupData); } } else { sortedGroupData = sortDupGroupChunk(groupData); } let lastFullPath = null; for (let idx = 0; idx < sortedGroupData.length; idx++) { const { it: item, path: fullPath } = sortedGroupData[idx]; item._dupFullPath = fullPath; if (idx > 0 && fullPath === lastFullPath) { item._isSameFolder = true; } else { item._isSameFolder = false; } lastFullPath = fullPath; newDisplay.push(item); } } const dupFolderWrap = document.getElementById('pk-dup-folder-sel-wrap'); if (dupFolderWrap) { dupFolderWrap.style.width = "220px"; dupFolderWrap.style.flexShrink = "0"; } UI.selDupFolder.style.width = "220px"; UI.selDupFolder.style.flexShrink = "0"; const currentSelection = UI.selDupFolder.value; const invertChk = document.getElementById('pk-dup-invert'); const invertLabel = invertChk ? invertChk.parentNode : null; if (dynamicFolderStats.size <= 1) { if (dupFolderWrap) dupFolderWrap.style.display = 'none'; UI.selDupFolder.style.display = 'none'; if (invertLabel) invertLabel.style.display = 'none'; } else { if (dupFolderWrap) dupFolderWrap.style.display = 'inline-flex'; UI.selDupFolder.style.display = 'inline-block'; if (invertLabel) invertLabel.style.display = 'inline-flex'; } let dropdownHtml = ``; if (dynamicFolderStats.size > 0) { dropdownHtml += ``; } const truncateMiddle = (str, len = 30) => { if (!str || str.length <= len * 2 + 3) return str; return str.substring(0, len) + ' ... ' + str.substring(str.length - len); }; const sortLocale = ({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en','id':'id','ms':'ms'})[getLang()] || 'en'; const collator = new Intl.Collator(sortLocale, { numeric: true }); const sortedFolders = Array.from(dynamicFolderStats.entries()) .sort((a, b) => { const arrA = a[0].split('/'); const arrB = b[0].split('/'); const len = Math.min(arrA.length, arrB.length); for (let i = 0; i < len; i++) { const cmp = collator.compare(arrA[i], arrB[i]); if (cmp !== 0) return cmp; } return arrA.length - arrB.length; }); const optionsArr = sortedFolders.map(([path, count]) => { return ``; }); dropdownHtml += optionsArr.join(''); UI.selDupFolder.innerHTML = dropdownHtml; if (currentSelection && dynamicFolderStats.has(currentSelection)) { UI.selDupFolder.value = currentSelection; } else if (currentSelection === "__RESET__") { UI.selDupFolder.value = "__RESET__"; } else { if (S.pinnedDupPath && !dynamicFolderStats.has(S.pinnedDupPath)) { S.pinnedDupPath = null; } UI.selDupFolder.value = ""; } syncDupFolderButtonText(); S.display = newDisplay; const visibleIds = new Set(newDisplay.map(d => d.id)); for(let id of S.sel) { if (!visibleIds.has(id)) S.sel.delete(id); } if (UI.chkAll) UI.chkAll.checked = false; renderList(); updateStat(); } function parseShareInput(raw) { const text = String(raw || '').trim(); if (!text) return ''; const linkMatch = text.match(/(?:https?:\/\/)?(?:www\.)?(?:mypikpak\.com|app\.mypikpak\.com|drive\.mypikpak\.com)\/s\/([A-Za-z0-9_-]{8,128})(?=[^A-Za-z0-9_-]|$)/i); if (linkMatch && linkMatch[1]) return linkMatch[1]; return /^[A-Za-z0-9_-]{8,128}$/.test(text) ? text : ''; } function formatShareParseDisplayLink(raw) { const text = String(raw || '').trim(); if (!text) return ''; const shareId = parseShareInput(text); return shareId ? `https://mypikpak.com/s/${shareId}` : text; } function parseSharePassCode(raw) { const text = String(raw || '').trim(); if (!text) return ''; const validPass = /^[A-Za-z0-9]{4,10}$/; const scan = (src, regex) => { let m; while ((m = regex.exec(src))) { const v = String(m[1] || '').trim(); if (validPass.test(v)) return v; } return ''; }; const decoded = (() => { try { return decodeURIComponent(text); } catch(e) { return text; } })(); return scan(text, /[?&#](?:s_code|pass_code|passcode|pass|pwd)=([A-Za-z0-9]{4,10})(?=&|#|\s|$)/ig) || scan(decoded, /[?&#](?:s_code|pass_code|passcode|pass|pwd)=([A-Za-z0-9]{4,10})(?=&|#|\s|$)/ig) || scan(decoded, /(?:密码|密碼|提取码|提取碼|访问码|訪問碼|口令|パスワード|パスコード|パス|コード|비밀번호|비번|암호|코드)\s*[::]?\s*([A-Za-z0-9]{4,10})(?=\s|$|[,,;;。、])/g) || scan(decoded, /\b(?:password|passcode|pass|code|kata\s+sandi|kata\s+laluan|sandi|kode|kod)\b\s*[::]?\s*([A-Za-z0-9]{4,10})(?=\s|$|[,,;;。、])/ig); } function parseShareClipboardText(text) { const raw = String(text || '').trim(); if (!raw) return null; const linkMatch = raw.match(/https?:\/\/(?:www\.)?(?:mypikpak\.com|app\.mypikpak\.com|drive\.mypikpak\.com)\/s\/([A-Za-z0-9_-]{8,128})(?=[^A-Za-z0-9_-]|$)/i); const shareId = linkMatch && linkMatch[1] ? linkMatch[1] : parseShareInput(raw); if (!shareId) return null; return { link: formatShareParseDisplayLink(shareId), pass_code: parseSharePassCode(raw) }; } async function handleShareParseClipboardImport() { const L = getStrings(); if (S.shareParseLoading) return; if (!navigator.clipboard || typeof navigator.clipboard.readText !== 'function') { showToast(L.msg_share_clipboard_denied, 'warning'); return; } let text = ''; try { text = await navigator.clipboard.readText(); } catch(e) { showToast(L.msg_share_clipboard_denied, 'warning'); return; } if (!String(text || '').trim()) { showToast(L.msg_share_clipboard_empty, 'warning'); return; } const parsed = parseShareClipboardText(text); if (!parsed || !parsed.link) { showToast(L.msg_share_clipboard_no_link, 'warning'); return; } const linkInput = UI.in ? UI.in.querySelector('#pk-share-parse-link') : null; const passInput = UI.in ? UI.in.querySelector('#pk-share-parse-pass') : null; S.shareParseDraft = { raw: parsed.link, pass_code: parsed.pass_code || '' }; if (linkInput) linkInput.value = parsed.link; if (passInput) passInput.value = parsed.pass_code || ''; await handleShareParseSubmit(); } function stringifyShareParsePayload(data) { try { return JSON.stringify(data || {}); } catch(e) { return String(data || ''); } } function getShareParsePayload(data) { return data && data.data && typeof data.data === 'object' ? data.data : (data || {}); } function readOfficialFieldValue(value) { if (value === undefined || value === null) return ''; if (typeof value === 'object') return value.code || value.error_code || value.status || value.result || value.phase || value.type || value.name || ''; return value; } function getOfficialStatusFields(data) { const payload = getShareParsePayload(data); const raw = data && typeof data === 'object' ? data : {}; const task = payload.task || raw.task || {}; return { shareStatus: readOfficialFieldValue(payload.share_status ?? raw.share_status), shareStatusText: readOfficialFieldValue(payload.share_status_text ?? raw.share_status_text), status: readOfficialFieldValue(payload.status ?? raw.status), statusCode: readOfficialFieldValue(payload.status_code ?? raw.status_code), error: readOfficialFieldValue(payload.error ?? raw.error), errorCode: readOfficialFieldValue(payload.error_code ?? raw.error_code ?? payload.code ?? raw.code), code: readOfficialFieldValue(payload.code ?? raw.code), result: readOfficialFieldValue(payload.result ?? raw.result), phase: readOfficialFieldValue(payload.phase ?? raw.phase ?? task.phase), taskPhase: readOfficialFieldValue(task.phase), statusText: readOfficialFieldValue(payload.status_text ?? raw.status_text), errorDescription: readOfficialFieldValue(payload.error_description ?? raw.error_description), message: readOfficialFieldValue(payload.message ?? payload.msg ?? raw.message ?? raw.msg) }; } function normalizeOfficialStatusValue(value) { return String(value ?? '').trim().toUpperCase().replace(/[\s-]+/g, '_'); } function readOfficialErrorText(data) { const f = getOfficialStatusFields(data); return [f.shareStatus, f.shareStatusText, f.status, f.statusCode, f.error, f.errorCode, f.code, f.result, f.phase, f.taskPhase, f.statusText, f.errorDescription, f.message].filter(v => v !== undefined && v !== null && String(v).trim() !== '').join(' '); } function getOfficialStatusTokens(data) { const f = getOfficialStatusFields(data); return [f.shareStatus, f.status, f.statusCode, f.error, f.errorCode, f.code, f.result, f.phase, f.taskPhase].map(normalizeOfficialStatusValue).filter(Boolean); } function hasOfficialStatusToken(data, values = []) { const allowed = new Set((values || []).map(normalizeOfficialStatusValue).filter(Boolean)); return getOfficialStatusTokens(data).some(v => allowed.has(v)); } function getOfficialErrorCode(data) { const f = getOfficialStatusFields(data); const n = Number(f.errorCode || f.code || 0); return Number.isFinite(n) ? n : 0; } function hasOfficialExplicitError(data) { const f = getOfficialStatusFields(data); const errCode = getOfficialErrorCode(data); const shareStatus = normalizeOfficialStatusValue(f.shareStatus); const tokens = getOfficialStatusTokens(data).filter(v => !['OK','SUCCESS','STATUS_OK','PHASE_TYPE_COMPLETE','PHASE_TYPE_COMPLETED','COMPLETE'].includes(v)); return !!(errCode || (shareStatus && shareStatus !== 'OK') || tokens.some(v => /^ERROR(?:_|$)/.test(v) || /^ERR(?:_|$)/.test(v))); } function hasShareParsePassSignal(data) { return hasOfficialStatusToken(data, ['PASS_CODE_ERROR','PASS_CODE_EMPTY','PASS_CODE_INVALID','PASS_CODE_WRONG','WRONG_PASS_CODE','INVALID_PASS_CODE','BAD_PASS_CODE','PASSWORD_ERROR','PASSWORD_INVALID','PASSWORD_WRONG','ACCESS_CODE_ERROR','ACCESS_CODE_INVALID']); } function hasShareParseRegionUnavailableSignal(data) { return getOfficialErrorCode(data) === 9 || hasOfficialStatusToken(data, ['PROHIBITED','SHARE_STATUS_PROHIBITED','STATUS_PROHIBITED','REGION_PROHIBITED','REGION_UNAVAILABLE','AREA_UNAVAILABLE']); } function isOfficialShareStatusOk(data) { return normalizeOfficialStatusValue(getOfficialStatusFields(data).shareStatus) === 'OK'; } function resolveOfficialErrorByStableFields(status, data, scope = '') { const errCode = getOfficialErrorCode(data); const hasPassSignal = hasShareParsePassSignal(data); const hasRegionSignal = hasShareParseRegionUnavailableSignal(data); const hasExpiredSignal = status === 410 || hasOfficialStatusToken(data, ['EXPIRED','SHARE_EXPIRED','LINK_EXPIRED','SHARE_STATUS_EXPIRED','RESOURCE_EXPIRED']); const hasCancelSignal = hasOfficialStatusToken(data, ['CANCELLED','CANCELED','DELETED','REMOVED','SHARE_CANCELLED','SHARE_CANCELED','SHARE_DELETED','SHARE_REMOVED']); const hasNotFoundSignal = status === 404 || hasOfficialStatusToken(data, ['NOT_FOUND','SHARE_NOT_FOUND','FILE_NOT_FOUND','RESOURCE_NOT_FOUND']); const hasTokenExpiredSignal = hasOfficialStatusToken(data, ['TOKEN_EXPIRED','ACCESS_TOKEN_EXPIRED','PASS_CODE_TOKEN_EXPIRED','INVALID_TOKEN','TOKEN_INVALID','UNAUTHORIZED','FORBIDDEN']); const phase = normalizeOfficialStatusValue(getOfficialStatusFields(data).phase || getOfficialStatusFields(data).taskPhase); if (scope === 'share_parse') { if (isOfficialShareStatusOk(data)) return ''; if (hasRegionSignal) return 'msg_share_parse_region_unavailable'; if (hasPassSignal) return 'msg_share_parse_bad_pass'; if (status === 401 || status === 403) return 'msg_share_parse_auth_error'; if (hasNotFoundSignal) return 'msg_share_parse_not_found'; if (hasExpiredSignal) return 'msg_share_parse_expired'; if (hasCancelSignal) return 'msg_share_parse_cancelled'; if (status && status >= 400) return 'msg_share_parse_unknown_error'; if (hasOfficialExplicitError(data)) return 'msg_share_parse_unknown_error'; } if (scope === 'share_save') { if (hasOfficialStatusToken(data, ['FILE_RESTORE_OWN','RESTORE_OWN','OWN_SHARE','SAVE_OWN']) || errCode === 9) return 'msg_share_parse_save_own'; if (hasOfficialStatusToken(data, ['FILE_SPACE_NOT_ENOUGH','SPACE_NOT_ENOUGH','QUOTA_NOT_ENOUGH','INSUFFICIENT_STORAGE','INSUFFICIENT_SPACE']) || errCode === 8) return 'msg_share_parse_save_space_not_enough'; if (hasOfficialStatusToken(data, ['TOO_MANY','TOO_MANY_FILES','FILE_COUNT_EXCEED','LIMIT_EXCEEDED','COUNT_LIMIT_EXCEEDED'])) return 'msg_share_parse_save_too_many'; if (hasOfficialStatusToken(data, ['PARTIAL','PARTIAL_FAILED','FAILED_FILES','FAIL_LIST','RESTORE_PARTIAL_FAILED'])) return 'msg_share_parse_save_partial_failed'; if (hasTokenExpiredSignal) return 'msg_share_parse_save_token_expired'; if (status === 401 || status === 403) return 'msg_share_parse_save_auth_error'; if (hasNotFoundSignal || hasExpiredSignal || hasRegionSignal) return 'msg_share_parse_save_share_expired'; if (hasCancelSignal) return 'msg_share_parse_save_cancelled'; if (phase === 'PHASE_TYPE_ERROR') return 'msg_share_parse_save_task_failed'; if (errCode && errCode !== 0) return 'msg_share_parse_save_failed'; if (status && status >= 400) return 'msg_share_parse_save_failed'; if (hasOfficialExplicitError(data)) return 'msg_share_parse_save_failed'; } return ''; } function resolveShareParseErrorKey(status, data) { return resolveOfficialErrorByStableFields(status, data, 'share_parse'); } function makeShareParseError(key, status = 0, data = null) { const err = new Error(key || 'msg_share_parse_failed'); err.shareParseKey = key || 'msg_share_parse_failed'; err.status = status; err.data = data; return err; } async function fetchShareParseInfo(shareId, passCode) { const headers = getHeaders(); if (!headers.Authorization || headers.Authorization.length < 10) throw makeShareParseError('msg_share_parse_auth_error'); const url = `https://api-drive.mypikpak.com/drive/v1/share?share_id=${encodeURIComponent(shareId)}&pass_code=${encodeURIComponent(passCode || '')}`; const res = await fetch(url, { method: 'GET', headers }); syncTime(res.headers); if (!res.ok) { const bodyText = await res.text().catch(() => ''); let errData = {}; try { errData = bodyText ? JSON.parse(bodyText) : {}; } catch(e) { errData = { raw_text: bodyText }; } throw makeShareParseError(resolveShareParseErrorKey(res.status, errData), res.status, errData); } const data = await res.json(); const key = resolveShareParseErrorKey(0, data); if (key) throw makeShareParseError(key, 200, data); return data; } function readShareParseUserText(raw) { const data = getShareParsePayload(raw); const user = data.share_user || data.share_user_info || data.user || data.creator || data.owner || data.user_info || {}; if (typeof user === 'string') return user; return data.share_user_name || data.user_name || data.nickname || user.nickname || user.name || user.user_name || user.username || user.email || user.id || ''; } function readShareParseTitle(raw) { const data = getShareParsePayload(raw); const share = data.share || data.share_info || {}; const file = data.file || data.file_info || {}; return data.title || data.share_title || data.name || data.file_name || share.title || share.name || file.name || ''; } function buildShareParseInfo(shareId, passCode, rawInfo) { const root = rawInfo || {}; const data = getShareParsePayload(root); return { share_id: shareId, pass_code: passCode || '', pass_code_token: data.pass_code_token || root.pass_code_token || '', share_title: readShareParseTitle(root), share_user: readShareParseUserText(root), raw_share_info: root, parsed_at: Date.now() }; } const SHARE_PARSE_HISTORY_KEY = 'pk_share_parse_history'; const SHARE_PARSE_HISTORY_SCHEMA = 1; const SHARE_PARSE_HISTORY_BATCH_SIZE = 40; let activeShareParseHistoryModal = null; let activeShareParseHistoryNoteModal = null; function getShareParsePassMaxLen() { return (((getConfigLimitSchema(SHARE_PARSE_HISTORY_KEY) || {}).localLimit || {}).maxPassLen || 10); } function getShareParseHistoryKey(shareId, passCode) { return `${String(shareId || '').trim()}\n${safeStringLimit(passCode || '', getShareParsePassMaxLen())}`; } function makeShareParseHistoryId(shareId, passCode) { return `sph_${String(shareId || '').replace(/[^A-Za-z0-9_-]/g, '').slice(0, 36)}_${String(passCode || '').replace(/[^A-Za-z0-9]/g, '').slice(0, getShareParsePassMaxLen())}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } function parseShareParseHistoryTime(v) { if (!v) return 0; if (typeof v === 'number' && Number.isFinite(v)) return Math.max(0, Math.floor(v)); const t = Date.parse(v); return Number.isFinite(t) ? t : 0; } function normalizeShareParseHistoryStatus(status) { const v = String(status || 'ok').trim(); return ['ok','expired','cancelled','not_found','bad_pass','auth','network','unknown'].includes(v) ? v : 'unknown'; } function normalizeShareParseHistoryRecord(raw) { if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null; const shareId = String(raw.share_id || raw.shareId || '').trim(); if (!shareId) return null; const passCode = safeStringLimit(raw.pass_code || raw.passCode || '', getShareParsePassMaxLen()); const key = getShareParseHistoryKey(shareId, passCode); const firstSuccessAt = parseShareParseHistoryTime(raw.first_success_at || raw.firstSuccessAt || raw.last_success_at || raw.lastSuccessAt || Date.now()); const lastSuccessAt = parseShareParseHistoryTime(raw.last_success_at || raw.lastSuccessAt || firstSuccessAt); const lastOpenAt = parseShareParseHistoryTime(raw.last_open_at || raw.lastOpenAt || 0); const asCount = (v) => Math.max(0, Math.floor(Number(v) || 0)); return { id: String(raw.id || makeShareParseHistoryId(shareId, passCode)), share_id: shareId, key, raw: formatShareParseDisplayLink(raw.raw || raw.link || raw.url || shareId), pass_code: passCode, title: String(raw.title || raw.share_title || shareId), share_user: String(raw.share_user || raw.shareUser || ''), first_success_at: firstSuccessAt || lastSuccessAt || Date.now(), last_success_at: lastSuccessAt || firstSuccessAt || Date.now(), last_open_at: lastOpenAt, last_check_at: parseShareParseHistoryTime(raw.last_check_at || raw.lastCheckAt || 0), last_status: normalizeShareParseHistoryStatus(raw.last_status || raw.lastStatus || 'ok'), last_error_key: String(raw.last_error_key || raw.lastErrorKey || ''), last_error_msg: String(raw.last_error_msg || raw.lastErrorMsg || ''), last_error_at: parseShareParseHistoryTime(raw.last_error_at || raw.lastErrorAt || 0), success_count: Math.max(1, asCount(raw.success_count || raw.successCount || 1)), root_file_count: asCount(raw.root_file_count || raw.rootFileCount || 0), root_folder_count: asCount(raw.root_folder_count || raw.rootFolderCount || 0), root_total_count: asCount(raw.root_total_count || raw.rootTotalCount || 0), pinned: !!raw.pinned, note: String(raw.note || ''), schema: SHARE_PARSE_HISTORY_SCHEMA }; } function readShareParseHistory() { try { const stored = gmGet(SHARE_PARSE_HISTORY_KEY, '[]'); const parsed = typeof stored === 'string' ? JSON.parse(stored || '[]') : stored; const list = Array.isArray(parsed) ? parsed : (parsed && Array.isArray(parsed.items) ? parsed.items : []); const out = []; const seenIds = new Set(); list.forEach(item => { const rec = normalizeShareParseHistoryRecord(item); if (!rec || seenIds.has(rec.id)) return; seenIds.add(rec.id); out.push(rec); }); return out; } catch(e) { return []; } } function writeShareParseHistory(list) { try { gmSet(SHARE_PARSE_HISTORY_KEY, JSON.stringify(Array.isArray(list) ? list : [])); return true; } catch(e) { return false; } } function sortShareParseHistory(list) { const collator = new Intl.Collator(({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en','id':'id','ms':'ms'})[getLang()] || undefined, { numeric: true, sensitivity: 'base' }); return (Array.isArray(list) ? list.slice() : []).sort((a, b) => { if (!!a.pinned !== !!b.pinned) return a.pinned ? -1 : 1; const ta = parseShareParseHistoryTime(a.last_success_at); const tb = parseShareParseHistoryTime(b.last_success_at); if (ta !== tb) return tb - ta; return collator.compare(String(a.title || a.share_id || ''), String(b.title || b.share_id || '')); }); } function getShareParseRootSummary(items) { const src = Array.isArray(items) ? items : []; let folder = 0; let file = 0; src.forEach(item => { if (!item || item.isHeader) return; if (item.kind === 'drive#folder') folder++; else file++; }); return { root_file_count: file, root_folder_count: folder, root_total_count: file + folder }; } function recordShareParseHistoryFromCurrent() { try { const info = S.shareParseInfo || {}; if (!info.share_id || !info.pass_code_token) return false; const shareId = String(info.share_id || '').trim(); const passCode = String(info.pass_code || ''); const key = getShareParseHistoryKey(shareId, passCode); const now = Date.now(); const summary = getShareParseRootSummary(S.shareParseItems); const list = readShareParseHistory(); let idx = list.findIndex(rec => rec && (rec.key === key || (rec.share_id === shareId && String(rec.pass_code || '') === passCode))); if (idx < 0) idx = findShareParseHistoryPassUpdateIndex(list, shareId, passCode); const base = idx >= 0 ? list[idx] : null; const next = { id: base ? base.id : makeShareParseHistoryId(shareId, passCode), share_id: shareId, key, raw: formatShareParseDisplayLink((S.shareParseDraft && S.shareParseDraft.raw) || info.raw || shareId), pass_code: passCode, title: String(info.share_title || (base && base.title) || shareId), share_user: String(info.share_user || ''), first_success_at: base ? base.first_success_at : now, last_success_at: now, last_open_at: base ? (base.last_open_at || 0) : 0, success_count: (base ? (Number(base.success_count) || 0) : 0) + 1, root_file_count: summary.root_file_count, root_folder_count: summary.root_folder_count, root_total_count: summary.root_total_count, last_check_at: now, last_status: 'ok', last_error_key: '', last_error_msg: '', last_error_at: 0, pinned: base ? !!base.pinned : false, note: base ? String(base.note || '') : '', schema: SHARE_PARSE_HISTORY_SCHEMA }; if (idx >= 0) list[idx] = next; else list.push(next); const ok = writeShareParseHistory(list); if (ok) { S.shareParseHistoryActiveId = next.id; S.shareParseHistoryPendingId = ''; } return ok; } catch(e) { return false; } } function updateShareParseHistoryRecord(id, patch) { const list = readShareParseHistory(); const idx = list.findIndex(rec => rec && rec.id === id); if (idx < 0) return null; list[idx] = normalizeShareParseHistoryRecord(Object.assign({}, list[idx], patch || {})); writeShareParseHistory(list.filter(Boolean)); return list[idx]; } function clearShareParseHistoryStorage() { writeShareParseHistory([]); refreshShareParseHistoryModal(); } function formatShareParseHistorySummary(rec) { const L = getStrings(); const total = Number(rec.root_total_count || 0); const folders = Number(rec.root_folder_count || 0); const files = Number(rec.root_file_count || 0); return `${L.label_share_parse_history_root_items}: ${total} (${L.str_folders}: ${folders}, ${L.str_files}: ${files})`; } function resolveShareParseHistoryErrorStatus(err) { const key = String((err && err.shareParseKey) || ''); if (/bad_pass/i.test(key)) return 'bad_pass'; if (/cancel/i.test(key)) return 'cancelled'; if (/not_found/i.test(key)) return 'not_found'; if (/expired|token_expired|share_expired/i.test(key)) return 'expired'; if (/auth/i.test(key)) return 'auth'; if (/network/i.test(key) || (err && (err.name === 'TypeError' || err.name === 'AbortError'))) return 'network'; return 'unknown'; } function formatShareParseHistoryStatusMeta(rec) { const L = getStrings(); const status = normalizeShareParseHistoryStatus(rec && rec.last_status); if (!rec || status === 'ok') return ''; const label = L[`label_share_parse_history_status_${status}`] || (rec.last_error_key && L[rec.last_error_key]) || rec.last_error_msg || L.label_share_parse_history_status_unknown || L.str_unknown_name; const checked = rec.last_check_at ? ` ${L.label_share_parse_history_last_check}: ${fmtDate(rec.last_check_at)}` : ''; return `${L.label_share_parse_history_status}: ${label}${checked}`; } function markShareParseHistoryRecordError(id, err, msg) { if (!id) return null; const status = resolveShareParseHistoryErrorStatus(err); const now = Date.now(); const rec = updateShareParseHistoryRecord(id, { last_check_at: now, last_status: status, last_error_key: String((err && err.shareParseKey) || ''), last_error_msg: String(msg || ''), last_error_at: now }); if (rec) refreshShareParseHistoryModal(); return rec; } function findShareParseHistoryRecordByShare(shareId, passCode) { const sid = String(shareId || '').trim(); if (!sid) return null; const pass = String(passCode || ''); const key = getShareParseHistoryKey(sid, pass); return readShareParseHistory().find(rec => rec && (rec.key === key || (rec.share_id === sid && String(rec.pass_code || '') === pass))) || null; } function findShareParseHistoryPassUpdateIndex(list, shareId, passCode) { const sid = String(shareId || '').trim(); if (!sid || !Array.isArray(list)) return -1; const pass = String(passCode || ''); let fallback = -1; const same = []; for (let i = 0; i < list.length; i++) { const rec = list[i]; if (!rec || rec.share_id !== sid) continue; same.push({ index: i, rec }); if (normalizeShareParseHistoryStatus(rec.last_status) === 'bad_pass') return i; if (fallback < 0 && !rec.pass_code && normalizeShareParseHistoryStatus(rec.last_status) !== 'ok') fallback = i; } if (fallback >= 0) return fallback; if (pass && same.length === 1 && String(same[0].rec.pass_code || '') !== pass) return same[0].index; return -1; } function findShareParseHistoryPassUpdateRecord(shareId, passCode) { const list = readShareParseHistory(); const idx = findShareParseHistoryPassUpdateIndex(list, shareId, passCode); return idx >= 0 ? list[idx] : null; } function syncShareParseHistoryPassAfterInfoSuccess(id, info, raw) { if (!id || !info || !info.share_id || !info.pass_code_token) return null; const list = readShareParseHistory(); const idx = list.findIndex(rec => rec && rec.id === id); if (idx < 0) return null; const base = list[idx]; const shareId = String(info.share_id || '').trim(); const passCode = String(info.pass_code || ''); const key = getShareParseHistoryKey(shareId, passCode); const now = Date.now(); const conflictIdx = list.findIndex((rec, i) => i !== idx && rec && (rec.key === key || (rec.share_id === shareId && String(rec.pass_code || '') === passCode))); const conflict = conflictIdx >= 0 ? list[conflictIdx] : null; const firstSuccess = Math.min(...[base.first_success_at, conflict && conflict.first_success_at].map(parseShareParseHistoryTime).filter(Boolean)); const next = normalizeShareParseHistoryRecord(Object.assign({}, conflict || {}, base, { share_id: shareId, key, raw: formatShareParseDisplayLink(raw || info.raw || shareId), pass_code: passCode, title: String(info.share_title || base.title || (conflict && conflict.title) || shareId), share_user: String(info.share_user || base.share_user || (conflict && conflict.share_user) || ''), first_success_at: firstSuccess || base.first_success_at || now, last_check_at: now, last_status: 'ok', last_error_key: '', last_error_msg: '', last_error_at: 0, pinned: !!(base.pinned || (conflict && conflict.pinned)), note: String(base.note || (conflict && conflict.note) || ''), schema: SHARE_PARSE_HISTORY_SCHEMA })); list[idx] = next; const out = list.filter((rec, i) => rec && i !== conflictIdx); if (writeShareParseHistory(out)) { S.shareParseHistoryActiveId = next.id; refreshShareParseHistoryModal(); return next; } return null; } function getCurrentShareParseHistoryRecordId() { const info = S.shareParseInfo || {}; const rec = findShareParseHistoryRecordByShare(info.share_id, info.pass_code); return rec ? rec.id : ''; } function markShareParseHistoryCurrentError(err, msg) { const id = S.shareParseHistoryReparseId || S.shareParseHistoryPendingId || getCurrentShareParseHistoryRecordId(); return markShareParseHistoryRecordError(id, err, msg); } function matchesShareParseHistoryQuery(rec, query) { if (!query) return true; const q = String(query || '').toLowerCase(); return [rec.title, rec.note].some(v => String(v || '').toLowerCase().includes(q)); } function renderShareParseHistoryHighlightedText(text, query) { const raw = String(text || ''); const q = String(query || '').trim(); if (!q) return esc(raw); const lower = raw.toLowerCase(); const needle = q.toLowerCase(); if (!needle) return esc(raw); let html = ''; let pos = 0; let idx = lower.indexOf(needle, pos); while (idx >= 0) { html += esc(raw.slice(pos, idx)); html += `${esc(raw.slice(idx, idx + q.length))}`; pos = idx + q.length; idx = lower.indexOf(needle, pos); } html += esc(raw.slice(pos)); return html; } function renderShareParseHistorySmartText(text, query, baseLimit = 48) { const raw = String(text || ''); const q = String(query || '').trim(); if (!raw) return ''; if (!q) return esc(raw); const lower = raw.toLowerCase(); const needle = q.toLowerCase(); const hit = lower.indexOf(needle); if (hit < 0) return esc(raw); const limit = Math.max(baseLimit, q.length + 18); let start = 0; let end = raw.length; if (raw.length > limit) { const side = Math.max(8, Math.floor((limit - q.length) / 2)); start = Math.max(0, hit - side); end = Math.min(raw.length, hit + q.length + side); if (end - start < limit) { if (start === 0) end = Math.min(raw.length, limit); else if (end === raw.length) start = Math.max(0, raw.length - limit); } } const snippet = `${start > 0 ? '…' : ''}${raw.slice(start, end)}${end < raw.length ? '…' : ''}`; return renderShareParseHistoryHighlightedText(snippet, q); } function isShareParseHistorySmartCut(text, query, baseLimit = 48) { const raw = String(text || ''); const q = String(query || '').trim(); if (!raw || !q) return false; const needle = q.toLowerCase(); const hit = raw.toLowerCase().indexOf(needle); if (hit < 0) return false; return raw.length > Math.max(baseLimit, q.length + 18); } function createShareParseHistoryItem(rec, query = '') { const L = getStrings(); const item = document.createElement('div'); item.className = 'pk-share-history-item'; item.dataset.id = rec.id; const title = rec.title || rec.share_id || L.str_unknown_name || ''; const shareUser = rec.share_user || '-'; const meta = [ `${L.label_share_parse_history_share_user}: ${shareUser}`, `${L.label_share_parse_history_last_success}: ${fmtDate(rec.last_success_at)}`, `${L.label_share_parse_history_success_count}: ${rec.success_count || 1}`, formatShareParseHistorySummary(rec) ]; const statusMeta = formatShareParseHistoryStatusMeta(rec); if (statusMeta) meta.push(statusMeta); const titleHtml = renderShareParseHistorySmartText(title, query, 52); const titleTipHtml = String(query || '').trim() ? renderShareParseHistoryHighlightedText(title, query) : ''; const titleSmartCut = isShareParseHistorySmartCut(title, query, 52); const noteHtml = rec.note ? renderShareParseHistoryHighlightedText(rec.note, query) : ''; item.innerHTML = `
${rec.pinned ? `${esc(L.btn_share_parse_history_pin)} ` : ''}${titleHtml}
${meta.map(v => `${esc(v)}`).join('')}
${rec.note ? `
${noteHtml}
` : ''}
`; return item; } function promptShareParseHistoryNote(value = '') { if (activeShareParseHistoryNoteModal && activeShareParseHistoryNoteModal.isConnected) { const input = activeShareParseHistoryNoteModal.querySelector('#pk_share_history_note'); if (input) try { input.focus(); } catch (e) {} return Promise.resolve(null); } activeShareParseHistoryNoteModal = null; const L = getStrings(); return new Promise(resolve => { const m = showModal(`

${esc(L.btn_share_parse_history_edit_note)}

`); activeShareParseHistoryNoteModal = m; const box = m.querySelector('.pk-modal'); if (box) Object.assign(box.style, { width: 'auto', padding: '28px' }); const input = m.querySelector('#pk_share_history_note'); const noteLimit = ((getConfigLimitSchema('pk_share_parse_history') || {}).localLimit || {}).maxTextLen || 512; bindPlainTextMaxLengthLimits(m, [['#pk_share_history_note', noteLimit, true]]); const close = (v) => { if (activeShareParseHistoryNoteModal === m) activeShareParseHistoryNoteModal = null; m.remove(); resolve(v); }; m.querySelector('#pk_share_history_note_cancel').onclick = () => close(null); m.querySelector('#pk_share_history_note_save').onclick = () => close(input ? input.value.trim() : ''); m.querySelector('.pk-modal-close').onclick = () => close(null); }); } async function reparseShareParseHistoryRecord(rec) { if (!rec) return; const raw = formatShareParseDisplayLink(rec.raw || rec.share_id || ''); const passCode = safeStringLimit(rec.pass_code || '', getShareParsePassMaxLen()); S.shareParseHistoryActiveId = rec.id; S.shareParseHistoryPendingId = rec.id; updateShareParseHistoryRecord(rec.id, { last_open_at: Date.now(), last_check_at: Date.now() }); if (!S.shareParseMode) { try { if (typeof switchTab === 'function') switchTab('shareParse'); } catch(e) {} } S.shareParseDraft = { raw, pass_code: passCode }; if (S.shareParseMode && S.shareParseListActive) { resetShareParseListState(false); renderShareParsePanel(); } else if (S.shareParseMode) { renderShareParsePanel(); } const linkInput = UI.in ? UI.in.querySelector('#pk-share-parse-link') : null; const passInput = UI.in ? UI.in.querySelector('#pk-share-parse-pass') : null; if (linkInput) linkInput.value = raw; if (passInput) passInput.value = passCode; S.shareParseDraft = { raw, pass_code: passCode }; S.shareParseHistoryReparseId = rec.id; try { await handleShareParseSubmit(); } finally { if (S.shareParseHistoryReparseId === rec.id) S.shareParseHistoryReparseId = ''; } } function refreshShareParseHistoryModal() { if (activeShareParseHistoryModal && activeShareParseHistoryModal._pkRefreshShareHistory) { activeShareParseHistoryModal._pkRefreshShareHistory(); } } function openShareParseHistoryModal() { if (activeShareParseHistoryModal && activeShareParseHistoryModal.isConnected) { const searchEl = activeShareParseHistoryModal.querySelector('#pk_share_history_search'); const box = activeShareParseHistoryModal.querySelector('.pk-modal'); if (box) box.style.zIndex = '100000'; if (searchEl) setTimeout(() => { try { searchEl.focus({ preventScroll: true }); } catch (e) { searchEl.focus(); } }, 0); return activeShareParseHistoryModal; } if (activeShareParseHistoryModal && !activeShareParseHistoryModal.isConnected) activeShareParseHistoryModal = null; const L = getStrings(); let allRecords = sortShareParseHistory(readShareParseHistory()); let filtered = allRecords.slice(); let rendered = 0; let committedQuery = ''; const m = showModal(`

${esc(L.title_share_parse_history)}

`); activeShareParseHistoryModal = m; const box = m.querySelector('.pk-modal'); if (box) Object.assign(box.style, { width: 'auto', padding: '24px', height: 'auto', minHeight: 'auto' }); const listEl = m.querySelector('#pk_share_history_list'); const searchEl = m.querySelector('#pk_share_history_search'); const searchWrap = m.querySelector('#pk_share_history_search_wrap'); const searchSubmitBtn = m.querySelector('#pk_share_history_search_submit'); const searchClearBtn = m.querySelector('#pk_share_history_search_clear'); const countEl = m.querySelector('#pk_share_history_count'); const clearBtn = m.querySelector('#pk_share_history_clear'); const updateCount = () => { if (countEl) countEl.textContent = `${filtered.length}/${allRecords.length}`; if (clearBtn) clearBtn.disabled = allRecords.length === 0; }; const renderMore = () => { if (!listEl) return; if (!filtered.length) { listEl.innerHTML = `
${CONF.emptySVG}
${esc(L.msg_share_parse_history_empty)}
`; rendered = 0; updateCount(); return; } if (rendered === 0) listEl.innerHTML = ''; const frag = document.createDocumentFragment(); const next = Math.min(filtered.length, rendered + SHARE_PARSE_HISTORY_BATCH_SIZE); const activeQuery = committedQuery; for (let i = rendered; i < next; i++) frag.appendChild(createShareParseHistoryItem(filtered[i], activeQuery)); listEl.appendChild(frag); rendered = next; updateCount(); }; const applyFilter = () => { const q = committedQuery; allRecords = sortShareParseHistory(readShareParseHistory()); filtered = allRecords.filter(rec => matchesShareParseHistoryQuery(rec, q)); rendered = 0; if (listEl) listEl.scrollTop = 0; renderMore(); }; m._pkRefreshShareHistory = applyFilter; if (listEl) { listEl.onscroll = () => { if (filtered.length && rendered < filtered.length && listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight - 80) renderMore(); }; listEl.onclick = async (e) => { const btn = e.target && e.target.closest ? e.target.closest('button[data-act]') : null; if (!btn) return; const id = btn.dataset.id || ''; const act = btn.dataset.act || ''; const rec = allRecords.find(x => x.id === id); if (!rec) return; if (act === 'reparse') { m.remove(); await reparseShareParseHistoryRecord(rec); return; } if (act === 'pin') { updateShareParseHistoryRecord(id, { pinned: !rec.pinned }); applyFilter(); return; } if (act === 'note') { const note = await promptShareParseHistoryNote(rec.note || ''); if (note === null) return; updateShareParseHistoryRecord(id, { note }); applyFilter(); return; } if (act === 'delete') { if (rec.pinned && !await showConfirm(L.msg_share_parse_history_delete_pinned_confirm)) return; const next = readShareParseHistory().filter(x => x.id !== id); writeShareParseHistory(next); applyFilter(); } }; } const syncShareHistorySearchDraftUI = () => { const hasValue = !!(searchEl && searchEl.value.trim()); if (searchWrap) searchWrap.classList.toggle('has-value', hasValue); }; const submitShareHistorySearch = () => { committedQuery = searchEl ? searchEl.value.trim() : ''; syncShareHistorySearchDraftUI(); applyFilter(); }; if (searchEl) { syncShareHistorySearchDraftUI(); searchEl.oninput = () => { syncShareHistorySearchDraftUI(); if (!searchEl.value.trim() && committedQuery) { committedQuery = ''; applyFilter(); searchEl.focus(); } }; searchEl.onkeydown = (e) => { if (e.key !== 'Enter' || e.isComposing || e.keyCode === 229) return; e.preventDefault(); submitShareHistorySearch(); }; } bindPlainTextMaxLengthLimits(m, [['#pk_share_history_search', ((getConfigLimitSchema('pk_search_history') || {}).localLimit || {}).maxFieldLen || 1024]]); if (searchSubmitBtn) searchSubmitBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); submitShareHistorySearch(); }; if (searchClearBtn) searchClearBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (searchEl) searchEl.value = ''; const hadQuery = !!committedQuery; committedQuery = ''; syncShareHistorySearchDraftUI(); if (hadQuery) applyFilter(); else if (searchEl) searchEl.focus(); }; m.querySelector('#pk_share_history_close').onclick = () => m.remove(); if (clearBtn) clearBtn.onclick = async () => { if (!allRecords.length) return; if (!await showConfirm(L.msg_share_parse_history_clear_confirm)) return; clearShareParseHistoryStorage(); showToast(L.msg_share_parse_history_cleared); }; const orgRemove = m.remove.bind(m); m.remove = () => { if (activeShareParseHistoryModal === m) activeShareParseHistoryModal = null; orgRemove(); }; renderMore(); return m; } function getShareParseErrorMessage(err) { const L = getStrings(); if (err && err.shareParseKey && L[err.shareParseKey]) return L[err.shareParseKey]; if (err && (err.name === 'TypeError' || err.name === 'AbortError')) return L.msg_share_parse_network_error; return L.msg_share_parse_failed || L.msg_share_parse_unknown_error; } function clearShareParseListMirror() { const hasShareMirror = Array.isArray(S.items) && S.items.some(item => item && item._isShareItem); if (S.shareParseMode || hasShareMirror) { S.items = []; S.display = []; if (S.itemMap && typeof S.itemMap.clear === 'function') S.itemMap.clear(); } if (S.sel && typeof S.sel.clear === 'function') S.sel.clear(); if (S.selEx && typeof S.selEx.clear === 'function') S.selEx.clear(); S.selMode = 'explicit'; S.activeId = null; S.lastSelIdx = -1; } function syncShareParseMirror() { if (syncShareParseInsightMirror()) return; if (!(S.shareParseMode && S.shareParseListActive)) { clearShareParseListMirror(); return; } const items = Array.isArray(S.shareParseItems) ? S.shareParseItems : []; const baseDisplay = Array.isArray(S.shareParseDisplay) ? S.shareParseDisplay : items; const q = String(S.search || '').trim().toLowerCase(); const includePath = !!(UI.chkSearchPath && UI.chkSearchPath.checked); const pathQ = normalizeSharePathSearchText(q); const display = sortShareParseDisplayList(q ? baseDisplay.filter(item => { if (!item || item.isHeader) return false; const name = String(item.name || '').toLowerCase(); if (name.includes(q)) return true; if (!includePath) return false; const path = normalizeSharePathSearchText(item._shareFilePathText || item._pathStr || getSharePathText(item._sharePath) || ''); return path.includes(pathQ); }) : baseDisplay); if (!S.shareParseItemMap || typeof S.shareParseItemMap.clear !== 'function') S.shareParseItemMap = new Map(); S.shareParseItemMap.clear(); items.forEach(item => { if (item && item.id) S.shareParseItemMap.set(item.id, item); }); S.items = items; S.display = display; if (S.itemMap && typeof S.itemMap.clear === 'function') { S.itemMap.clear(); display.forEach(item => { if (item && item.id && !item.isHeader) S.itemMap.set(item.id, item); }); } } function resetShareParseListState(clearInfo = false) { S.shareParseListReqId = (S.shareParseListReqId || 0) + 1; if (!S.shareParseSaving) { S.shareParseSaveReqId = (S.shareParseSaveReqId || 0) + 1; S.shareParseSaveError = ''; S.shareParseSaveResult = null; } S.shareParseListActive = false; S.shareParseItems = []; S.shareParseDisplay = []; S.shareParseItemMap = new Map(); S.shareParsePath = []; S.shareParseCurParentId = ''; S.shareParseNextPageToken = ''; cancelShareParseAutoPaging(); S.shareParseListLoading = false; S.shareParseListError = ''; S.shareParseLatestChildId = null; S.resetShareParseSideNav(); resetShareParseInsightState(true); if (clearInfo) { S.shareParseInfo = null; S.shareParseHistoryActiveId = ''; S.shareParseHistoryPendingId = ''; } clearShareParseListMirror(); updateShareParseInsightButtons(); } function ensureShareParseFolderCache() { if (!(S.shareParseFolderCache instanceof Map)) S.shareParseFolderCache = new Map(); return S.shareParseFolderCache; } function getShareParseFolderCacheScopeKey() { const info = S.shareParseInfo || {}; const shareId = String(info.share_id || ''); if (!shareId) return ''; return JSON.stringify([shareId, String(info.pass_code || '')]); } function getShareParseFolderCacheKey(parentId = '') { const scope = getShareParseFolderCacheScopeKey(); if (!scope) return ''; return `${scope}|${String(parentId || '')}`; } function cloneShareParseCacheItem(item) { const copy = Object.assign({}, item || {}); if (copy.params && typeof copy.params === 'object') copy.params = Object.assign({}, copy.params); if (Array.isArray(copy.medias)) copy.medias = copy.medias.slice(); if (Array.isArray(copy._shareAncestorIds)) copy._shareAncestorIds = copy._shareAncestorIds.slice(); if (Array.isArray(copy._sharePath)) copy._sharePath = S.cloneShareParsePath(copy._sharePath); if (copy._rawShareFile && typeof copy._rawShareFile === 'object') copy._rawShareFile = Object.assign({}, copy._rawShareFile); const info = S.shareParseInfo || {}; if (info.share_id) copy._shareId = info.share_id; if (info.pass_code_token) copy._passCodeToken = info.pass_code_token; return copy; } function readShareParseFolderCache(parentId = '') { const key = getShareParseFolderCacheKey(parentId); if (!key) return null; const cache = ensureShareParseFolderCache(); const hit = cache.get(key); if (!hit) return null; cache.delete(key); cache.set(key, hit); return { parentId: hit.parentId || '', path: S.cloneShareParsePath(hit.path || []), items: (Array.isArray(hit.items) ? hit.items : []).map(cloneShareParseCacheItem), nextPageToken: hit.nextPageToken || '', complete: !!hit.complete, savedAt: hit.savedAt || 0 }; } function writeShareParseFolderCacheEntry(parentId = '', pathNodes = [], items = [], nextPageToken = '') { const key = getShareParseFolderCacheKey(parentId); if (!key) return; const cache = ensureShareParseFolderCache(); if (cache.has(key)) cache.delete(key); cache.set(key, { parentId: String(parentId || ''), path: S.cloneShareParsePath(pathNodes || []), items: (Array.isArray(items) ? items : []).map(cloneShareParseCacheItem), nextPageToken: nextPageToken || '', complete: !nextPageToken, savedAt: Date.now() }); } function writeShareParseFolderCache(parentId = '') { writeShareParseFolderCacheEntry( parentId, S.shareParsePath || [], S.shareParseItems || [], S.shareParseNextPageToken || '' ); } function restoreShareParseFolderScroll(opts = {}, append = false) { if (append) return; if (Number.isFinite(opts.shareParseRestoreScrollTop) && UI.vp) { S.shareParseLatestChildId = null; const top = Math.max(0, opts.shareParseRestoreScrollTop); requestAnimationFrame(() => { if (!UI.vp) return; const maxTop = Math.max(0, UI.vp.scrollHeight - UI.vp.clientHeight); UI.vp.scrollTop = Math.min(top, maxTop); if (typeof renderVisible === 'function') renderVisible(); }); } else if (gmGet('pk_keep_pos', true) && S.shareParseLatestChildId && UI.vp) { const childId = S.shareParseLatestChildId; S.shareParseLatestChildId = null; requestAnimationFrame(() => { if (!S.shareParseMode || !UI.vp) return; const targetIdx = (S.display || []).findIndex(x => x && x.id === childId); if (targetIdx === -1) return; const rowTop = getItemScrollTopByIndex(targetIdx); const centerScroll = Math.max(0, rowTop - (UI.vp.clientHeight / 2) + (CONF.rowHeight / 2)); UI.vp.scrollTop = centerScroll; S.clearSelection(); S.activeId = childId; if (typeof renderVisible === 'function') renderVisible(); }); } else { S.shareParseLatestChildId = null; } } function updateShareParseSaveButton() { const L = getStrings(); const btn = UI.btnShareParseSave; if (!btn) return; const visible = !!(S.shareParseMode && S.shareParseListActive); btn.style.display = visible ? 'inline-flex' : 'none'; btn.disabled = !visible || !!S.shareParseSaving; btn.setAttribute('data-pk-tip', S.shareParseSaving ? L.btn_share_parse_saving : L.tip_share_parse_save_selected); const span = btn.querySelector('span'); if (span) span.textContent = S.shareParseSaving ? L.btn_share_parse_saving : L.btn_share_parse_save_selected; updateShareParseInsightButtons(); } function resetShareParseInsightState(clearData = true) { S.shareParseInsightReqId = (S.shareParseInsightReqId || 0) + 1; S.shareParseInsightMode = false; S.shareParseInsightScanning = false; S.shareParseInsightStop = true; S.shareParseInsightError = ''; S.shareParseInsightProgress = { folder: 0, file: 0 }; S.shareParseInsightToastStopped = false; if (clearData) { S.shareParseInsightItems = []; S.shareParseInsightDisplay = []; S.shareParseInsightItemMap = new Map(); S.shareParseInsightFilterState = { active: false, cat: 'all', ext: 'all' }; S.shareParseInsightReturnPath = []; S.shareParseInsightReturnParentId = ''; } } function setSearchWrapVisible(visible) { const box = UI.searchInput ? (UI.searchInput.closest('.pk-search') || UI.searchInput.parentNode) : null; if (box) box.style.setProperty('display', visible ? 'flex' : 'none', 'important'); } function clearShareParseSearch() { S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; } function sortShareParseDisplayList(list) { const src = Array.isArray(list) ? list.slice() : []; const sort = S.sort || 'modified_time'; const dir = S.dir || 1; const collator = new Intl.Collator(({'zh':'zh-CN','tc':'zh-TW','ja':'ja','ko':'ko','en':'en','id':'id','ms':'ms'})[getLang()] || undefined, { numeric: true, sensitivity: 'base', caseFirst: 'lower' }); const charWeight = (str) => { const c = String(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 cmpName = (a, b) => { const aa = String(a || ''), bb = String(b || ''); const wa = charWeight(aa), wb = charWeight(bb); return wa !== wb ? wa - wb : collator.compare(aa, bb); }; const asSize = v => parseInt(v || 0, 10) || 0; const asTime = v => v ? new Date(v).getTime() || 0 : 0; const asDur = item => parseFloat((item && item.params && item.params.duration) || item.duration || item.media_duration || 0) || 0; const asPath = item => String((item && (item._shareFilePathText || item._pathStr)) || (item && item._sharePath ? getSharePathText(item._sharePath) : '') || ''); const extGroup = item => { const ext = String(item && item.name || '').split('.').pop().toLowerCase(); if (['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp'].includes(ext)) return 1; if (['jpg','jpeg','png','gif','bmp','svg','webp','tiff','ico'].includes(ext)) return 2; return 3; }; src.sort((a, b) => { const fa = a && a.kind === 'drive#folder' ? 1 : 0; const fb = b && b.kind === 'drive#folder' ? 1 : 0; if (S.folderFirst && fa !== fb) return fb - fa; if (sort === 'path') { const pc = cmpName(asPath(a), asPath(b)); if (pc !== 0) return pc * dir; return cmpName(a && a.name, b && b.name) * dir; } if (sort === 'size') { if (fa !== fb) return S.folderFirst ? (fb - fa) : (fa - fb); const sa = asSize(a && a.size), sb = asSize(b && b.size); if (sa !== sb) return (sb - sa) * dir; return cmpName(a && a.name, b && b.name) * dir; } if (sort === 'duration') { if (fa !== fb) return S.folderFirst ? (fb - fa) : (fa - fb); if (fa) return cmpName(a && a.name, b && b.name) * dir; const da = asDur(a), db = asDur(b); const ha = da > 0, hb = db > 0; if (ha !== hb) return ha ? -1 : 1; if (ha && da !== db) return (db - da) * dir; const ga = extGroup(a), gb = extGroup(b); if (ga !== gb) return ga - gb; return cmpName(a && a.name, b && b.name) * dir; } if (sort === 'modified_time') { const ta = asTime(a && a.modified_time), tb = asTime(b && b.modified_time); if (ta !== tb) return (tb - ta) * dir; return cmpName(a && a.name, b && b.name) * dir; } return cmpName(a && a.name, b && b.name) * dir; }); return src; } function filterShareInsightItemsByCategory(items) { const state = S.shareParseInsightFilterState || { active: false, cat: 'all', ext: 'all' }; if (!state.active || state.cat === 'all') return Array.isArray(items) ? items.slice() : []; 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'] }; const definedExts = new Set([...fExts.video, ...fExts.audio, ...fExts.image, ...fExts.document, ...fExts.software, ...fExts.archive, ...fExts.torrent]); const matchExts = state.ext === 'all' ? (fExts[state.cat] || []) : [state.ext]; return (Array.isArray(items) ? items : []).filter(item => { if (!item || item.kind === 'drive#folder') return false; const ext = String(item.name || '').toLowerCase().split('.').pop(); return state.cat === 'other' ? !definedExts.has(ext) : matchExts.includes(ext); }); } function syncShareParseInsightMirror() { if (!(S.shareParseMode && S.shareParseListActive && S.shareParseInsightMode)) return false; const items = Array.isArray(S.shareParseInsightItems) ? S.shareParseInsightItems : []; const baseDisplay = filterShareInsightItemsByCategory(items); const q = String(S.search || '').trim().toLowerCase(); const includePath = !!(UI.chkSearchPath && UI.chkSearchPath.checked); const pathQ = normalizeSharePathSearchText(q); const display = sortShareParseDisplayList(q ? baseDisplay.filter(item => { if (!item || item.isHeader) return false; const name = String(item.name || '').toLowerCase(); if (name.includes(q)) return true; if (!includePath) return false; const path = normalizeSharePathSearchText(item._shareFilePathText || item._pathStr || getSharePathText(item._sharePath) || ''); return path.includes(pathQ); }) : baseDisplay); if (!S.shareParseInsightItemMap || typeof S.shareParseInsightItemMap.clear !== 'function') S.shareParseInsightItemMap = new Map(); S.shareParseInsightItemMap.clear(); items.forEach(item => { if (item && item.id) S.shareParseInsightItemMap.set(item.id, item); }); S.items = items; S.display = display; if (S.itemMap && typeof S.itemMap.clear === 'function') { S.itemMap.clear(); display.forEach(item => { if (item && item.id && !item.isHeader) S.itemMap.set(item.id, item); }); } return true; } function formatShareParseInsightProgress() { const L = getStrings(); const p = S.shareParseInsightProgress || { folder: 0, file: 0 }; return (L.msg_share_parse_scan_progress || '').replace('{folder}', p.folder || 0).replace('{file}', p.file || 0); } function syncSearchPathTooltipByTextState() { const con = UI && UI.lblSearchPath; if (!con) return; const L = getStrings(); const shortTxt = con.querySelector('.pk-txt-short'); const longTxt = con.querySelector('.pk-txt-long'); const shortVisible = !!(shortTxt && getComputedStyle(shortTxt).display !== 'none' && (!longTxt || getComputedStyle(longTxt).display === 'none')); if (shortVisible) { con.setAttribute('data-pk-tip', L.lbl_search_path); } else { con.removeAttribute('data-pk-tip'); con.removeAttribute('title'); } } function syncShareParseInsightExitCompact() { const btn = UI && UI.btnShareParseBackList; if (!btn) return; btn.classList.remove('pk-share-exit-compact'); } function syncShareParseInsightSearchPathLabel() { const con = UI && UI.lblSearchPath; if (con) { con.classList.remove('pk-share-path-compact'); syncSearchPathTooltipByTextState(); } syncShareParseInsightExitCompact(); } function updateShareParseInsightButtons() { const L = getStrings(); const inList = !!(S.shareParseMode && S.shareParseListActive && S.shareParseInfo && S.shareParseInfo.pass_code_token); const insightMode = !!S.shareParseInsightMode; const scanning = !!S.shareParseInsightScanning; if (UI.btnShareParseInsight) { const visible = inList && !insightMode && !scanning; UI.btnShareParseInsight.style.display = visible ? 'inline-flex' : 'none'; UI.btnShareParseInsight.disabled = !visible; UI.btnShareParseInsight.setAttribute('data-pk-tip', L.btn_share_parse_insight); UI.btnShareParseInsight.style.marginLeft = '8px'; UI.btnShareParseInsight.style.marginRight = '0'; const span = UI.btnShareParseInsight.querySelector('span'); if (span) span.textContent = L.btn_share_parse_insight; } if (UI.btnShareParseStopScan) { const visible = inList && insightMode && scanning; UI.btnShareParseStopScan.style.display = visible ? 'inline-flex' : 'none'; UI.btnShareParseStopScan.disabled = !visible; UI.btnShareParseStopScan.setAttribute('data-pk-tip', L.btn_share_parse_stop_scan); } if (UI.btnShareParseBackList) { const visible = inList && insightMode && !scanning; UI.btnShareParseBackList.style.display = visible ? 'inline-flex' : 'none'; UI.btnShareParseBackList.disabled = !visible; UI.btnShareParseBackList.style.marginLeft = '8px'; UI.btnShareParseBackList.style.color = '#d93025'; UI.btnShareParseBackList.setAttribute('data-pk-tip', L.btn_exit); UI.btnShareParseBackList.removeAttribute('title'); UI.btnShareParseBackList.innerHTML = `${CONF.icons.close} ${L.btn_exit}`; UI.btnShareParseBackList.classList.toggle('pk-share-exit-cold', visible); UI.btnShareParseBackList.onpointerenter = () => UI.btnShareParseBackList.classList.remove('pk-share-exit-cold'); if (!visible) UI.btnShareParseBackList.classList.remove('pk-share-exit-compact', 'pk-share-exit-cold'); } syncShareParseInsightSearchPathLabel(); } function isShareDetailFolder(raw) { raw = raw || {}; const mime = raw.mime_type || raw.mimeType || raw.content_type || ''; const kind = String(raw.kind || '').toLowerCase(); return raw.kind === 'drive#folder' || kind === 'folder' || kind.includes('folder') || mime === 'application/x-directory'; } function getShareDetailFileId(raw, parentId = '', index = 0) { raw = raw || {}; const name = raw.name || raw.file_name || raw.title || ''; return String(raw.id || raw.file_id || raw.fileId || raw.resource_id || raw.resourceId || `share_${parentId || 'root'}_${index}_${name}`); } function getSharePathText(pathNodes) { const L = getStrings(); const nodes = (Array.isArray(pathNodes) ? pathNodes : []).filter(n => n && !n._sharePanel); const text = nodes.map(n => n.name || '').filter(Boolean).join(' / '); return text || L.label_share_parse_path_root; } function normalizeSharePathSearchText(text) { return String(text || '').toLowerCase().replace(/[\\/]+/g, '/').replace(/\s*\/\s*/g, '/'); } function mapShareInsightFile(file, index, pathNodes, parentId) { const raw = file || {}; const id = getShareDetailFileId(raw, parentId, index); const name = raw.name || raw.file_name || raw.title || ''; const mime = raw.mime_type || raw.mimeType || raw.content_type || ''; const source = { ...raw, id, kind: 'drive#file', name, parent_id: parentId || raw.parent_id || raw.parentId || '', size: raw.size || raw.file_size || 0, mime_type: mime, thumbnail_link: raw.thumbnail_link || raw.thumbnailLink || raw.thumbnail || '', icon_link: raw.icon_link || raw.iconLink || raw.icon || '', modified_time: raw.modified_time || raw.modifiedTime || raw.updated_time || raw.created_time || '', hash: raw.hash || raw.gcid || raw.md5_checksum || '', tags: [], starred: false, trashed: false, _pkSkipDurationProbe: true }; const mapped = minifyFile(source, false); const curPath = Array.isArray(pathNodes) && pathNodes.length ? pathNodes.slice() : buildShareParseRootPath(); const ancestorIds = curPath.map(n => n && n.id).filter(idv => idv && idv !== 'share_parse_panel'); const pathText = getSharePathText(curPath); return Object.assign(mapped, { _isShareItem: true, _isShareInsightItem: true, _shareId: (S.shareParseInfo && S.shareParseInfo.share_id) || '', _passCodeToken: (S.shareParseInfo && S.shareParseInfo.pass_code_token) || '', _shareParentId: parentId || raw.parent_id || raw.parentId || '', _shareAncestorIds: ancestorIds, _sharePath: curPath, _shareFilePathText: pathText, _rawShareFile: raw, _pkSkipDurationProbe: true }); } async function scanShareParseInsight(reqId, targets = null, silent = false) { const currentPath = Array.isArray(S.shareParsePath) && S.shareParsePath.length ? S.shareParsePath.slice() : buildShareParseRootPath(); const currentParentId = S.shareParseCurParentId || getShareParseRootParentId(); const selected = Array.isArray(targets) ? targets.filter(item => item && item._isShareItem && item.id) : []; const queue = []; const result = []; const resultKeys = new Set(); const pushInsightFile = (file, pathNodes, parentId) => { const item = mapShareInsightFile(file, result.length, pathNodes, parentId || ''); const key = `${item._shareParentId || parentId || ''}/${item.id || item.name || result.length}`; if (resultKeys.has(key)) return; resultKeys.add(key); result.push(item); }; if (selected.length) { selected.forEach(item => { const parentId = item._shareParentId || item.parent_id || ''; const itemPath = Array.isArray(item._sharePath) && item._sharePath.length ? item._sharePath.slice() : (item.kind === 'drive#folder' ? currentPath.concat([{ id: item.id, name: item.name || '', parent_id: parentId }]) : currentPath); if (item.kind === 'drive#folder') queue.push({ id: item.id, path: itemPath }); else pushInsightFile(item._rawShareFile || item, itemPath, parentId); }); } else { queue.push({ id: currentParentId || '', path: currentPath }); } let scannedFolders = 0; const startedAt = Date.now(); const syncInsightProgress = () => { S.shareParseInsightProgress = { folder: scannedFolders, file: result.length }; if (silent) { const sec = Math.max(1, Math.round((Date.now() - startedAt) / 1000)); const speed = Math.max(1, Math.round(scannedFolders / sec)); updateLoadTxt(`${L.str_scanning} ${scannedFolders} ${L.unit_folders} | ${L.str_files}: ${result.length} | ${L.str_speed}: ${speed}`); } else { S.shareParseInsightItems = result.slice(); S.shareParseInsightDisplay = S.shareParseInsightItems.slice(); syncShareParseInsightMirror(); renderShareParseCurrentView(); } }; const canContinueShareParseInsight = () => S.shareParseMode && (silent || S.shareParseInsightMode) && S.shareParseInsightReqId === reqId && !S.shareParseInsightStop; const scanConcurrency = Math.max(1, Math.min(128, parseInt(CONF.shareParseInsightConcurrency || 128, 10) || 128)); const scanShareParseInsightFolder = async (node) => { const parentId = node.id || ''; const directItems = []; const consumeCachedItems = (items) => { (Array.isArray(items) ? items : []).forEach((item, idx) => { const raw = item && item._rawShareFile ? item._rawShareFile : item; const itemId = item && item.id ? item.id : getShareDetailFileId(raw, parentId, idx); const name = (item && item.name) || (raw && (raw.name || raw.file_name || raw.title)) || ''; if ((item && item.kind === 'drive#folder') || isShareDetailFolder(raw)) { const itemPath = Array.isArray(item && item._sharePath) && item._sharePath.length ? S.cloneShareParsePath(item._sharePath) : node.path.concat([{ id: itemId, name, parent_id: parentId }]); queue.push({ id: itemId, path: itemPath }); } else { pushInsightFile(raw || item, node.path, parentId); } }); }; const consumeRawFiles = (files) => { const baseIndex = directItems.length; (Array.isArray(files) ? files : []).forEach((file, idx) => { const cacheItem = mapShareDetailFile(file, baseIndex + idx, { parentId, path: node.path }); directItems.push(cacheItem); const itemId = getShareDetailFileId(file, parentId, baseIndex + idx); const name = file.name || file.file_name || file.title || ''; if (isShareDetailFolder(file)) { queue.push({ id: itemId, path: node.path.concat([{ id: itemId, name, parent_id: parentId }]) }); } else { pushInsightFile(file, node.path, parentId); } }); }; let pageToken = ''; const cached = readShareParseFolderCache(parentId); if (cached) { directItems.push(...cached.items); consumeCachedItems(cached.items); pageToken = cached.nextPageToken || ''; syncInsightProgress(); if (!pageToken) { scannedFolders++; syncInsightProgress(); return; } } do { if (!canContinueShareParseInsight()) break; const raw = await fetchShareDetail(parentId, pageToken || ''); if (!canContinueShareParseInsight()) break; consumeRawFiles(getShareDetailFiles(raw)); syncInsightProgress(); pageToken = getShareDetailNextToken(raw); writeShareParseFolderCacheEntry(parentId, node.path, directItems, pageToken || ''); if (pageToken) await sleep(30); } while (pageToken && canContinueShareParseInsight()); scannedFolders++; syncInsightProgress(); }; await new Promise((resolve, reject) => { let running = 0; let settled = false; const pump = () => { if (settled) return; if (!canContinueShareParseInsight()) { settled = true; resolve(); return; } while (running < scanConcurrency && queue.length && canContinueShareParseInsight()) { const node = queue.shift(); running++; scanShareParseInsightFolder(node).then(() => { running--; pump(); }).catch(err => { if (settled) return; settled = true; reject(err); }); } if (!queue.length && running === 0) { settled = true; resolve(); } }; pump(); }); return result; } function filterShareParseInsightItemsByScanRule(items, scanFilter) { const src = Array.isArray(items) ? items.slice() : []; const rawTotal = src.length; const hasActiveFilter = !!(scanFilter && ((scanFilter.keyword || '').trim() || scanFilter.minBytes > 0 || scanFilter.maxBytes > 0)); if (!hasActiveFilter) return { items: src, rawTotal, hasActiveFilter }; const kwList = String(scanFilter.keyword || '').toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k); const minBytes = Math.max(0, parseInt(scanFilter.minBytes || 0, 10) || 0); const maxBytes = Math.max(0, parseInt(scanFilter.maxBytes || 0, 10) || 0); return { items: src.filter(item => { if (kwList.length > 0) { const fullLowerName = String(item && item.name || '').toLowerCase(); const lastDot = fullLowerName.lastIndexOf('.'); const nameWithoutExt = lastDot > 0 ? fullLowerName.substring(0, lastDot) : fullLowerName; if (kwList.some(k => nameWithoutExt.includes(k))) return false; } const sz = parseInt(item && item.size || 0, 10) || 0; if (sz < minBytes) return false; if (maxBytes > 0 && sz > maxBytes) return false; return true; }), rawTotal, hasActiveFilter }; } function openShareParseInsightScanModal(selectedTargets = null) { const L = getStrings(); const shareTargets = Array.isArray(selectedTargets) ? selectedTargets.filter(item => item && item._isShareItem && item.id).slice() : []; 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', ''); const targetDesc = shareTargets.length > 0 ? L.lbl_scan_selected.replace('{n}', shareTargets.length) : L.lbl_scan_current; const inputNonce = `${Date.now()}_${Math.random().toString(36).slice(2)}`; const m = showModal(`

${L.btn_share_parse_insight}

${targetDesc}
${L.lbl_keyword_filter}
${L.lbl_ana_min}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
-
${L.lbl_ana_max}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${lastUnit}
MB
GB
TB
`); 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' }); } bindPlainTextMaxLengthLimits(m, [['#spis_keyword', ((getConfigLimitSchema('pk_scan_last_keyword') || {}).localLimit || {}).maxItemLen || 2048]]); const inpMin = m.querySelector('#spis_val_min'); const inpMax = m.querySelector('#spis_val_max'); const unitBtn = m.querySelector('#spis_unit_btn'); const unitMenu = m.querySelector('#spis_unit_menu'); const unitTxt = m.querySelector('#spis_unit_txt'); let currentUnit = lastUnit; const syncLimitBorder = (inp) => { if (!inp) return; const raw = String(inp.value || '').trim(); const active = raw && (inp !== inpMin || Number(raw) !== 0); inp.style.borderColor = active ? 'var(--pk-pri)' : 'var(--pk-bd)'; }; [inpMin, inpMax].forEach(inp => { syncLimitBorder(inp); inp.addEventListener('input', () => syncLimitBorder(inp)); }); m.querySelector('#spis_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#spis_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#spis_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; inpMax.dispatchEvent(new Event('input')); }; m.querySelector('#spis_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); inpMax.dispatchEvent(new Event('input')); }; unitBtn.onclick = (e) => { e.stopPropagation(); unitMenu.style.display = unitMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('#spis_unit_menu .pk-ana-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('#spis_unit_menu .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(); }; const saveShareInsightScanInputs = () => { 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('#spis_keyword').value.trim()); }; m.querySelector('#spis_cancel').onclick = () => { saveShareInsightScanInputs(); m.remove(); }; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) closeBtn.onclick = () => { saveShareInsightScanInputs(); m.remove(); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#spis_start').click(); } }); m.querySelector('#spis_start').onclick = () => { saveShareInsightScanInputs(); const vMin = parseInt(inpMin.value) || 0; const vMax = parseInt(inpMax.value) || 0; const kw = m.querySelector('#spis_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; const scanFilter = { minBytes: Math.floor(vMin * mult), maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0, keyword: kw }; m.remove(); if (typeof runShareParseInsightScan === 'function') runShareParseInsightScan(scanFilter, shareTargets); }; return m; } function handleShareParseInsightStart() { const L = getStrings(); if (!(S.shareParseMode && S.shareParseListActive)) { showToast(L.msg_share_parse_scan_need_list, 'warning'); return; } const info = S.shareParseInfo || {}; if (!info.share_id || !info.pass_code_token) { showToast(L.msg_share_parse_save_need_token || L.msg_share_parse_token_missing, 'warning'); return; } if (S.shareParseInsightScanning) return; openShareParseInsightScanModal(getSelectedShareParseItems()); } async function runShareParseInsightScan(scanFilter = null, presetTargets = null) { const L = getStrings(); if (!(S.shareParseMode && S.shareParseListActive)) { showToast(L.msg_share_parse_scan_need_list, 'warning'); return; } const info = S.shareParseInfo || {}; if (!info.share_id || !info.pass_code_token) { showToast(L.msg_share_parse_save_need_token || L.msg_share_parse_token_missing, 'warning'); return; } if (S.shareParseInsightScanning) return; const insightTargets = Array.isArray(presetTargets) ? presetTargets.filter(item => item && item._isShareItem && item.id) : getSelectedShareParseItems(); const oldStopHandler = UI.stopBtn ? UI.stopBtn.onclick : null; clearShareParseSearch(); const reqId = (S.shareParseInsightReqId || 0) + 1; S.shareParseInsightReqId = reqId; S.shareParseInsightMode = false; S.shareParseInsightScanning = true; S.shareParseInsightStop = false; S.shareParseInsightToastStopped = false; S.shareParseInsightError = ''; S.shareParseInsightItems = []; S.shareParseInsightDisplay = []; S.shareParseInsightItemMap = new Map(); S.shareParseInsightFilterState = { active: false, cat: 'all', ext: 'all' }; S.shareParseInsightProgress = { folder: 0, file: 0 }; S.shareParseInsightReturnPath = Array.isArray(S.shareParsePath) ? S.shareParsePath.slice() : buildShareParseRootPath(); S.shareParseInsightReturnParentId = S.shareParseCurParentId || getShareParseRootParentId(); setLoad(true); updateLoadTxt(`${L.str_scanning} 0 ${L.unit_folders} | ${L.str_files}: 0`); if (UI.stopBtn) UI.stopBtn.onclick = handleShareParseInsightStop; try { const rawItems = await scanShareParseInsight(reqId, insightTargets, true); if (!S.shareParseMode || S.shareParseInsightReqId !== reqId) return; const filteredResult = filterShareParseInsightItemsByScanRule(rawItems, scanFilter); const items = filteredResult.items; if (S.shareParseInsightStop) { return; } if (!items.length) { S.shareParseInsightItems = []; S.shareParseInsightDisplay = []; S.shareParseInsightItemMap = new Map(); S.shareParseInsightError = ''; S.shareParseInsightProgress = { folder: 0, file: 0 }; if (filteredResult.rawTotal > 0 && filteredResult.hasActiveFilter) showToast(L.msg_scan_filter_empty || L.msg_share_parse_scan_empty); else showToast(L.msg_share_parse_scan_empty, 'warning'); updateShareParseInsightButtons(); renderShareParseCurrentView(); return; } S.shareParseInsightMode = true; if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('share_parse_insight'); S.shareParseInsightItems = items.slice(); S.shareParseInsightDisplay = S.shareParseInsightItems.slice(); S.shareParseInsightError = ''; S.sel.clear(); S.selEx.clear(); S.selMode = 'explicit'; S.activeId = null; S.lastSelIdx = -1; syncShareParseInsightMirror(); showToast(L.msg_share_parse_scan_done, 'success'); renderShareParseCurrentView(); } catch(e) { if (!S.shareParseMode || S.shareParseInsightReqId !== reqId) return; const msg = getShareParseErrorMessage(e) || L.msg_share_parse_scan_failed; S.shareParseInsightError = ''; showToast(msg, 'error'); } finally { if (S.shareParseInsightReqId === reqId) { S.shareParseInsightScanning = false; setLoad(false); if (UI.stopBtn) UI.stopBtn.onclick = oldStopHandler; updateShareParseInsightButtons(); renderShareParseCurrentView(); } } } function handleShareParseInsightStop() { if (!S.shareParseInsightScanning) return; S.shareParseInsightStop = true; S.shareParseInsightScanning = false; S.shareParseInsightToastStopped = true; setLoad(false); updateShareParseInsightButtons(); renderShareParseCurrentView(); } function handleShareParseInsightBack() { if (!S.shareParseMode) return; const returnPath = Array.isArray(S.shareParseInsightReturnPath) && S.shareParseInsightReturnPath.length ? S.shareParseInsightReturnPath.slice() : buildShareParseRootPath(); const returnParentId = S.shareParseInsightReturnParentId || getShareParseRootParentId(); resetShareParseInsightState(false); S.shareParseInsightMode = false; if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('share_parse_list'); S.shareParseInsightStop = true; S.shareParseInsightScanning = false; S.shareParseInsightError = ''; S.shareParsePath = returnPath; S.shareParseCurParentId = returnParentId; S.sel.clear(); S.selEx.clear(); S.selMode = 'explicit'; S.activeId = null; S.lastSelIdx = -1; syncShareParseMirror(); renderShareParseCurrentView(); } function getSelectedShareParseItems() { if (!(S.shareParseMode && S.shareParseListActive)) return []; const ids = S.getSelectedIds(); const out = []; ids.forEach(id => { const item = (S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id)) || (S.itemMap && S.itemMap.get(id)); if (item && item._isShareItem && item.id) out.push(item); }); return out; } function normalizeShareParseIdList(value) { if (Array.isArray(value)) return value.map(v => String(v || '').trim()).filter(Boolean); if (value === undefined || value === null) return []; return String(value).split(',').map(v => v.trim()).filter(Boolean); } function getShareParseTraceFileId(item) { const raw = (item && item._rawShareFile) || {}; const params = raw.params || {}; const ids = normalizeShareParseIdList(raw.trace_file_ids || raw.traceFileIds || params.trace_file_ids || params.traceFileIds || raw.trace_file_id || raw.traceFileId); return ids[0] || (item && item.id) || ''; } function getShareParseAncestorIds(item) { const raw = (item && item._rawShareFile) || {}; const params = raw.params || {}; const ids = normalizeShareParseIdList(item && item._shareAncestorIds).concat( normalizeShareParseIdList(raw.ancestor_ids || raw.ancestorIds || params.ancestor_ids || params.ancestorIds) ); return Array.from(new Set(ids)); } function groupShareParseRestoreItems(items) { const groups = new Map(); const pushGroup = (key, item) => { if (!groups.has(key)) groups.set(key, []); groups.get(key).push(item); }; (items || []).forEach(item => { if (!item || !item._isShareItem || !item.id) return; const raw = item._rawShareFile || {}; const ancestorIds = getShareParseAncestorIds(item); const parentId = String(item._shareParentId || item.parent_id || raw.parent_id || raw.parentId || '').trim(); const sharePath = Array.isArray(item._sharePath) ? item._sharePath.map(n => n && (n.id || n.name)).filter(Boolean).join('/') : ''; const pathText = String(item._shareFilePathText || sharePath || '').trim(); let key = ''; if (ancestorIds.length || parentId) key = `a:${ancestorIds.join('/')}|p:${parentId}`; else if (pathText) key = `path:${pathText}`; else key = `item:${item.id}`; pushGroup(key, item); }); return Array.from(groups.values()).filter(group => group.length); } function buildShareParseRestorePayload(items, saveInfo = null) { const info = saveInfo || S.shareParseInfo || {}; const fileIds = items.map(item => item && item.id).filter(Boolean); const traceIds = items.map(getShareParseTraceFileId).filter(Boolean); const ancestorIds = Array.from(new Set(items.flatMap(getShareParseAncestorIds))); const payload = { share_id: info.share_id || '', pass_code_token: info.pass_code_token || '', file_ids: fileIds, params: { trace_file_ids: (traceIds.length ? traceIds : fileIds).join(',') } }; if (ancestorIds.length) payload.ancestor_ids = ancestorIds; return payload; } function resolveShareParseSaveErrorKey(status, data) { return resolveOfficialErrorByStableFields(status, data, 'share_save'); } function getShareParseSaveErrorMessage(err) { const L = getStrings(); const raw = err && err.data ? err.data : null; const data = raw ? getShareParsePayload(raw) : {}; const officialDesc = String((data && data.error_description) || (raw && raw.error_description) || '').trim(); if (officialDesc && !/^msg_/.test(officialDesc)) return officialDesc; if (err && err.shareParseKey && L[err.shareParseKey]) return L[err.shareParseKey]; if (err && (err.name === 'TypeError' || err.name === 'AbortError')) return L.msg_share_parse_save_network_error; return (err && err.message && !/^msg_/.test(err.message)) ? `${L.msg_share_parse_save_failed}: ${err.message}` : L.msg_share_parse_save_failed; } function getShareParseOfficialErrorMessage(err) { const raw = err && err.data ? err.data : null; const data = raw ? getShareParsePayload(raw) : {}; const isOfficialText = (v, allowCode = false) => { const t = String(v || '').trim(); return t && !/^msg_/.test(t) && (allowCode || !/^[a-z0-9_.:-]+$/i.test(t)); }; const pick = (obj, allowCode = false) => { if (!obj || typeof obj !== 'object') return ''; const fields = ['status_text','error_description','error_message','errorMessage','message','msg','description','detail','reason']; for (const k of fields) if (isOfficialText(obj[k], allowCode)) return String(obj[k]).trim(); return ''; }; return pick(data) || pick(raw) || pick(data.error) || pick(raw && raw.error) || pick(data, true) || pick(raw, true) || pick(data.error, true) || pick(raw && raw.error, true) || ''; } function extractShareParseSaveTaskIds(raw) { const data = getShareParsePayload(raw); const ids = []; const push = (v) => { if (v !== undefined && v !== null && String(v).trim()) ids.push(String(v).trim()); }; push(data.task_id || data.taskId || data.restore_task_id || data.restoreTaskId || raw && (raw.task_id || raw.taskId || raw.restore_task_id || raw.restoreTaskId)); normalizeShareParseIdList(data.task_ids || data.taskIds || data.restore_task_ids || data.restoreTaskIds || raw && (raw.task_ids || raw.taskIds || raw.restore_task_ids || raw.restoreTaskIds)).forEach(push); const collectTask = (task) => { if (!task || typeof task !== 'object') return; push(task.id || task.task_id || task.taskId || task.restore_task_id || task.restoreTaskId); }; collectTask(data.task || raw && raw.task); const list = data.tasks || data.task_list || raw && (raw.tasks || raw.task_list); if (Array.isArray(list)) list.forEach(collectTask); return Array.from(new Set(ids)); } async function pollShareParseSaveTasks(taskIds, reqId, onProgress) { while (true) { await sleep(2000); if (S.shareParseSaveReqId !== reqId) return false; let complete = 0; for (const taskId of taskIds) { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${encodeURIComponent(taskId)}?_t=${Date.now()}`, { headers: getHeaders() }); if (!res.ok) continue; const data = await res.json().catch(() => ({})); const phase = String(data.phase || (data.task && data.task.phase) || '').toUpperCase(); const key = resolveShareParseSaveErrorKey(0, data); if (key || phase === 'PHASE_TYPE_ERROR') { throw makeShareParseError(key || 'msg_share_parse_save_task_failed', 200, data); } if (phase === 'PHASE_TYPE_COMPLETE' || phase === 'PHASE_TYPE_COMPLETED' || phase === 'COMPLETE' || phase === 'SUCCESS') complete++; } if (onProgress) onProgress(complete, taskIds.length); if (complete >= taskIds.length) return true; } } async function handleShareParseSaveSelected() { const L = getStrings(); if (S.shareParseSaving) return; if (!(S.shareParseMode && S.shareParseListActive)) { showToast(L.msg_share_parse_save_need_list, 'warning'); return; } const info = S.shareParseInfo || {}; if (!info.share_id || !info.pass_code_token) { showToast(L.msg_share_parse_save_need_token, 'warning'); return; } const selectedItems = getSelectedShareParseItems(); if (!selectedItems.length) { showToast(L.msg_share_parse_select_first, 'warning'); return; } const reqId = (S.shareParseSaveReqId || 0) + 1; S.shareParseSaveReqId = reqId; S.shareParseSaving = true; S.shareParseSaveError = ''; S.shareParseSaveResult = null; updateShareParseSaveButton(); showToast(L.msg_share_parse_save_start); const saveInfo = { share_id: info.share_id || '', pass_code_token: info.pass_code_token || '' }; const totalSaveCount = selectedItems.length; let savedSubmitCount = 0; const formatSaveProgress = (done, total) => (L.msg_share_parse_save_progress || L.btn_share_parse_saving).replace('{done}', done).replace('{total}', total); const formatTaskProgress = (done, total) => (L.msg_share_parse_save_task_progress || L.btn_share_parse_saving).replace('{done}', done).replace('{total}', total); const progressTask = FloatBarManager.create(formatSaveProgress(0, totalSaveCount)); try { const headers = getHeaders(); if (!headers.Authorization || headers.Authorization.length < 10) throw makeShareParseError('msg_share_parse_save_auth_error'); const groups = groupShareParseRestoreItems(selectedItems); if (!groups.length) throw makeShareParseError('msg_share_parse_select_first'); const allTaskIds = []; const results = []; for (const groupItems of groups) { if (S.shareParseSaveReqId !== reqId) return; progressTask.update(formatSaveProgress(savedSubmitCount, totalSaveCount)); const payload = buildShareParseRestorePayload(groupItems, saveInfo); const res = await fetch('https://api-drive.mypikpak.com/drive/v1/share/restore', { method: 'POST', headers, body: JSON.stringify(payload) }); syncTime(res.headers); const bodyText = await res.text().catch(() => ''); let data = {}; try { data = bodyText ? JSON.parse(bodyText) : {}; } catch(e) { data = { raw_text: bodyText }; } if (S.shareParseSaveReqId !== reqId) return; if (!res.ok) throw makeShareParseError(resolveShareParseSaveErrorKey(res.status, data), res.status, data); const key = resolveShareParseSaveErrorKey(0, data); if (key) throw makeShareParseError(key, 200, data); extractShareParseSaveTaskIds(data).forEach(id => allTaskIds.push(id)); results.push(data || {}); savedSubmitCount += groupItems.length; progressTask.update(formatSaveProgress(Math.min(savedSubmitCount, totalSaveCount), totalSaveCount)); } const taskIds = Array.from(new Set(allTaskIds)); if (taskIds.length) { progressTask.update(formatTaskProgress(0, taskIds.length)); const done = await pollShareParseSaveTasks(taskIds, reqId, (doneCount, totalCount) => { progressTask.update(formatTaskProgress(doneCount, totalCount)); }); if (S.shareParseSaveReqId !== reqId) return; if (done === false) return; } S.shareParseSaveResult = { groups: groups.length, taskIds, results }; if (S.shareParseMode && S.shareParseListActive) { S.clearSelection(); renderVisible(); } showToast(L.msg_share_parse_save_success); } catch(e) { if (S.shareParseSaveReqId !== reqId) return; const msg = getShareParseSaveErrorMessage(e); S.shareParseSaveError = msg; showToast(msg, 'error'); } finally { if (S.shareParseSaveReqId === reqId) { S.shareParseSaving = false; updateShareParseSaveButton(); updateStat(); } progressTask.destroy(); } } function getShareParseRootParentId() { const info = S.shareParseInfo || {}; const raw = info.raw_share_info || {}; const data = getShareParsePayload(raw); const share = data.share || data.share_info || {}; return data.root_id || data.root_folder_id || data.share_root_id || share.root_id || share.root_folder_id || share.share_root_id || ''; } function buildShareParseRootPath() { const L = getStrings(); const info = S.shareParseInfo || {}; const rootId = getShareParseRootParentId(); const title = L.picker_all || L.label_share_parse_root; return [ { id: 'share_parse_panel', name: L.label_share_parse_path_root, _sharePanel: true }, { id: rootId || '', name: title, parent_id: rootId || '', _shareRoot: true } ]; } function getShareDetailFiles(raw) { const data = getShareParsePayload(raw); if (Array.isArray(data.files)) return data.files; if (Array.isArray(data.file_list)) return data.file_list; if (Array.isArray(data.list)) return data.list; if (data.file && typeof data.file === 'object') return [data.file]; if (data.file_info && typeof data.file_info === 'object') return [data.file_info]; if (Array.isArray(raw && raw.files)) return raw.files; return []; } function getShareDetailNextToken(raw) { const data = getShareParsePayload(raw); return data.next_page_token || data.nextPageToken || (raw && (raw.next_page_token || raw.nextPageToken)) || ''; } function resolveShareDetailErrorKey(status, data) { if (hasOfficialStatusToken(data, ['PASS_CODE_TOKEN_EXPIRED','TOKEN_EXPIRED','ACCESS_TOKEN_EXPIRED','INVALID_TOKEN','TOKEN_INVALID'])) return 'msg_share_parse_token_expired'; const base = resolveShareParseErrorKey(status, data); if (base) return base; if (status && status >= 400) return 'msg_share_parse_detail_failed'; return ''; } async function fetchShareDetail(parentId, pageToken, signal = null) { const info = S.shareParseInfo || {}; if (!info.share_id) throw makeShareParseError('msg_share_parse_need_parse_first'); if (!info.pass_code_token) throw makeShareParseError('msg_share_parse_token_missing'); const headers = getHeaders(); if (!headers.Authorization || headers.Authorization.length < 10) throw makeShareParseError('msg_share_parse_auth_error'); const params = [ ['share_id', info.share_id], ['pass_code_token', info.pass_code_token], ['limit', '50'], ['thumbnail_size', 'SIZE_LARGE'], ['order', '3'], ['folders_first', 'true'] ]; if (parentId !== undefined && parentId !== null && String(parentId) !== '') params.push(['parent_id', parentId]); if (pageToken !== undefined && pageToken !== null && String(pageToken) !== '') params.push(['page_token', pageToken]); const query = params.map(([k, v]) => `${k}=${encodeURIComponent(v == null ? '' : String(v))}`).join('&'); const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/share/detail?${query}`, { method: 'GET', headers, signal: signal || undefined }); syncTime(res.headers); if (!res.ok) { const bodyText = await res.text().catch(() => ''); let errData = {}; try { errData = bodyText ? JSON.parse(bodyText) : {}; } catch(e) { errData = { raw_text: bodyText }; } throw makeShareParseError(resolveShareDetailErrorKey(res.status, errData), res.status, errData); } const data = await res.json(); const key = resolveShareDetailErrorKey(res.status, data); if (key) throw makeShareParseError(key, 200, data); return data; } function mapShareDetailFile(file, index = 0, ctx = null) { const raw = file || {}; const parentId = ctx && ctx.parentId !== undefined ? (ctx.parentId || '') : (raw.parent_id || raw.parentId || S.shareParseCurParentId || ''); const rawId = raw.id || raw.file_id || raw.fileId || raw.resource_id || raw.resourceId || ''; const name = raw.name || raw.file_name || raw.title || ''; const mime = raw.mime_type || raw.mimeType || raw.content_type || ''; const rawKind = String(raw.kind || '').toLowerCase(); const isFolder = raw.kind === 'drive#folder' || rawKind === 'folder' || rawKind.includes('folder') || mime === 'application/x-directory'; const id = String(rawId || `share_${parentId || 'root'}_${index}_${name}`); const source = { ...raw, id, kind: isFolder ? 'drive#folder' : 'drive#file', name, parent_id: parentId, size: raw.size || raw.file_size || 0, mime_type: mime, thumbnail_link: raw.thumbnail_link || raw.thumbnailLink || raw.thumbnail || '', icon_link: raw.icon_link || raw.iconLink || raw.icon || '', modified_time: raw.modified_time || raw.modifiedTime || raw.updated_time || raw.created_time || '', hash: raw.hash || raw.gcid || raw.md5_checksum || '', tags: [], starred: false, trashed: false, _pkSkipDurationProbe: true }; const mapped = minifyFile(source, false); const curPath = ctx && Array.isArray(ctx.path) && ctx.path.length ? S.cloneShareParsePath(ctx.path) : (Array.isArray(S.shareParsePath) && S.shareParsePath.length ? S.shareParsePath.slice() : buildShareParseRootPath()); const ancestorIds = curPath.map(n => n && n.id).filter(idv => idv && idv !== 'share_parse_panel'); const sharePath = isFolder ? curPath.concat([{ id, name, parent_id: parentId }]) : curPath; return Object.assign(mapped, { _isShareItem: true, _shareId: (S.shareParseInfo && S.shareParseInfo.share_id) || '', _passCodeToken: (S.shareParseInfo && S.shareParseInfo.pass_code_token) || '', _shareParentId: parentId, _shareAncestorIds: ancestorIds, _sharePath: sharePath, _rawShareFile: raw, _pkSkipDurationProbe: true }); } function renderShareParseListStatus(message, type = '') { if (!UI.in) return; if (typeof clearGridStableRows === 'function') clearGridStableRows(UI.in); UI.in.style.height = '100%'; UI.in.style.transform = 'none'; if (type === 'loading') { const spin = '
'; UI.in.innerHTML = `
${spin}
${esc(message || '')}
`; } else { UI.in.innerHTML = `
${CONF.emptySVG}
${esc(message || '')}
`; } if (UI.pop) { UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; } if (UI.ctx) UI.ctx.style.display = 'none'; } function syncShareParseInsightFilterBar() { if (!UI.filterBar) return; const visible = !!(S.shareParseMode && S.shareParseListActive && S.shareParseInsightMode && !S.shareParseInsightScanning); UI.filterBar.style.display = visible ? 'flex' : 'none'; if (!visible) return; const state = S.shareParseInsightFilterState || { active: false, cat: 'all', ext: 'all' }; if (state.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'; } } function renderShareParseCurrentView() { if (!S.shareParseMode) return; renderCrumb(); if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); if (S.shareParseListActive) { syncShareParseMirror(); renderList(); } else { renderShareParsePanel(); } updateShareParseInsightButtons(); syncShareParseInsightFilterBar(); updateStat(); } function clearShareParseAutoPagingResumeTimer() { if (S.shareParseAutoLoadRetryTimer) { clearTimeout(S.shareParseAutoLoadRetryTimer); S.shareParseAutoLoadRetryTimer = 0; } } function cancelShareParseAutoPaging() { S.shareParseAutoLoadToken = (S.shareParseAutoLoadToken || 0) + 1; S.shareParseAutoLoadRunning = false; S.shareParseAutoLoadTarget = ''; S.shareParseAutoLoadInterrupted = false; S.shareParseAutoLoadResumeTarget = ''; S.shareParseAutoLoadRegionBlocked = false; S.shareParseRegionResumeCtx = null; clearShareParseAutoPagingResumeTimer(); } function isShareParseRecoverableNetworkError(e) { if (typeof navigator !== 'undefined' && navigator.onLine === false) return true; const key = String((e && e.shareParseKey) || ''); if (key === 'msg_share_parse_network_error') return true; const name = String((e && e.name) || ''); if (name === 'AbortError') return true; if (name === 'TypeError' && !(e && e.shareParseKey)) return true; return false; } function isShareParseRegionUnavailableError(e) { return String((e && e.shareParseKey) || '') === 'msg_share_parse_region_unavailable'; } function scheduleShareParseRegionResumeProbe(delay = 5000) { clearShareParseAutoPagingResumeTimer(); if (!(S.shareParseMode && S.shareParseAutoLoadRegionBlocked)) return; S.shareParseAutoLoadRetryTimer = setTimeout(() => { S.shareParseAutoLoadRetryTimer = 0; resumeShareParseAutoPagingAfterNetworkBack(); if (S.shareParseAutoLoadRegionBlocked) scheduleShareParseRegionResumeProbe(8000); }, Math.max(1200, Number(delay) || 5000)); } function markShareParseAutoPagingInterrupted(parentId = '', reason = 'network') { S.shareParseAutoLoadInterrupted = true; S.shareParseAutoLoadResumeTarget = String(parentId || S.shareParseCurParentId || ''); S.shareParseAutoLoadRunning = false; S.shareParseAutoLoadRegionBlocked = reason === 'region'; S.pagingLoading = false; updateStat(); if (S.shareParseAutoLoadRegionBlocked) scheduleShareParseRegionResumeProbe(); } function resumeShareParseAutoPagingAfterNetworkBack() { if (!(S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode)) return; if (S.shareParseAutoLoadRunning || S.shareParseListLoading) return; if (typeof navigator !== 'undefined' && navigator.onLine === false) return; if (S.shareParseRegionResumeCtx) { const ctx = S.shareParseRegionResumeCtx; S.shareParseRegionResumeCtx = null; loadShareParseFolder(ctx.folderNode || null, Object.assign({}, ctx.opts || {}, { forceReload: true })).catch(() => {}); return; } if (!S.shareParseNextPageToken) return; scheduleShareParseAutoPaging(S.shareParseAutoLoadResumeTarget || S.shareParseCurParentId || '', { force: true }); } function scheduleShareParseAutoPaging(parentId = '', opts = {}) { if (!S.shareParseMode || S.shareParseInsightMode || !S.shareParseListActive || !S.shareParseNextPageToken) return; if (S.shareParseAutoLoadInterrupted && !(opts && opts.force)) return; S.shareParseAutoLoadInterrupted = false; S.shareParseAutoLoadResumeTarget = ''; S.shareParseAutoLoadRegionBlocked = false; clearShareParseAutoPagingResumeTimer(); S.pagingLoading = true; updateStat(); const target = String(parentId || S.shareParseCurParentId || ''); if (S.shareParseAutoLoadRunning && S.shareParseAutoLoadTarget === target) return; const token = (S.shareParseAutoLoadToken || 0) + 1; S.shareParseAutoLoadToken = token; S.shareParseAutoLoadTarget = target; setTimeout(() => drainShareParseAutoPaging(token, target).catch(() => {}), 0); } async function drainShareParseAutoPaging(token, target) { S.shareParseAutoLoadRunning = true; try { while ( S.shareParseAutoLoadToken === token && S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode && !S.shareParseAutoLoadInterrupted && String(S.shareParseCurParentId || '') === String(target || '') && S.shareParseNextPageToken ) { if (S.shareParseListLoading) { await sleep(80); continue; } await loadShareParseFolder(null, { append: true, autoPaging: true }); await sleep(30); } } finally { if (S.shareParseAutoLoadToken === token) { S.shareParseAutoLoadRunning = false; S.shareParseAutoLoadTarget = ''; S.pagingLoading = !!(S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode && S.shareParseNextPageToken && !S.shareParseAutoLoadInterrupted); updateStat(); } } } if (!window.__pkShareParseAutoPagingResumeBound) { window.__pkShareParseAutoPagingResumeBound = true; window.addEventListener('online', resumeShareParseAutoPagingAfterNetworkBack); window.addEventListener('focus', resumeShareParseAutoPagingAfterNetworkBack); window.addEventListener('pageshow', resumeShareParseAutoPagingAfterNetworkBack); document.addEventListener('visibilitychange', () => { if (!document.hidden) resumeShareParseAutoPagingAfterNetworkBack(); }); } async function loadShareParseFolder(folderNode = null, opts = {}) { const L = getStrings(); if (!S.shareParseMode) return false; const append = !!opts.append; const forceReload = !!opts.forceReload; const deferActivate = !!opts.deferActivate && !append; if (!append) cancelShareParseAutoPaging(); if (!append && S.shareParseInsightMode) resetShareParseInsightState(true); if (append && (!S.shareParseListActive || !S.shareParseNextPageToken || S.shareParseListLoading)) return false; const parentId = append ? (S.shareParseCurParentId || '') : (folderNode && folderNode.id ? folderNode.id : getShareParseRootParentId()); const pageToken = append ? S.shareParseNextPageToken : ''; const targetPathForNav = (!append && folderNode && Array.isArray(folderNode._sharePath) && folderNode._sharePath.length) ? S.cloneShareParsePath(folderNode._sharePath) : (!append ? buildShareParseRootPath() : null); const shouldRecordShareParseNav = !append && !opts.suppressShareParseNav && !S.shareParseNavSuspendRecord && !S.browserNavRestoring && S.shareParseListActive && !S.shareParseInsightMode; const prevShareParseNavSnap = shouldRecordShareParseNav ? S.makeShareParseNavSnapshot() : null; const recordedShareParseNav = !!(prevShareParseNavSnap && targetPathForNav && !S.isSameShareParsePath(prevShareParseNavSnap.path, targetPathForNav) && S.pushShareParseNavSnapshot(prevShareParseNavSnap, true)); const reqId = (S.shareParseListReqId || 0) + 1; S.shareParseListReqId = reqId; if (!deferActivate) S.shareParseListActive = true; if (!append && !deferActivate && typeof applyResolvedViewMode === 'function') applyResolvedViewMode('share_parse_list'); S.shareParseListLoading = true; S.shareParseListError = ''; S.shareParseCurParentId = parentId || ''; S.pagingLoading = !!append; if (append) updateStat(); if (!append && !forceReload) { const cached = readShareParseFolderCache(parentId || ''); if (cached) { S.shareParseListActive = true; if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('share_parse_list'); S.shareParseItems = cached.items; S.shareParseDisplay = S.shareParseItems.slice(); S.shareParseItemMap = new Map(); S.shareParseItems.forEach(item => { if (item && item.id) S.shareParseItemMap.set(item.id, item); }); S.shareParsePath = cached.path.length ? cached.path : (targetPathForNav ? S.cloneShareParsePath(targetPathForNav) : buildShareParseRootPath()); S.shareParseCurParentId = cached.parentId || parentId || ''; S.shareParseNextPageToken = cached.nextPageToken || ''; S.shareParseListLoading = false; S.pagingLoading = !!S.shareParseNextPageToken; S.sel.clear(); S.selEx.clear(); S.selMode = 'explicit'; S.activeId = null; S.lastSelIdx = -1; syncShareParseMirror(); renderShareParseCurrentView(); restoreShareParseFolderScroll(opts, false); if (!append && !opts.suppressShareParseNav && !S.browserNavRestoring && S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode) S.syncShareParseBrowserNavState(recordedShareParseNav ? 'push' : 'replace'); scheduleShareParseAutoPaging(S.shareParseCurParentId || parentId || ''); return true; } } if (!append) { S.shareParseItems = []; S.shareParseDisplay = []; S.shareParseItemMap = new Map(); S.shareParseNextPageToken = ''; S.sel.clear(); S.selEx.clear(); S.selMode = 'explicit'; S.activeId = null; S.lastSelIdx = -1; if (folderNode && Array.isArray(folderNode._sharePath) && folderNode._sharePath.length) { S.shareParsePath = folderNode._sharePath.slice(); } else { S.shareParsePath = buildShareParseRootPath(); } } if (!deferActivate) syncShareParseMirror(); renderShareParseCurrentView(); let ok = false; try { const raw = await fetchShareDetail(parentId || '', pageToken || ''); if (!S.shareParseMode || S.shareParseListReqId !== reqId) return false; const mapped = getShareDetailFiles(raw).map((file, idx) => mapShareDetailFile(file, idx)); if (deferActivate) { S.shareParseListActive = true; if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('share_parse_list'); } S.shareParseItems = append ? S.shareParseItems.concat(mapped) : mapped; S.shareParseDisplay = S.shareParseItems.slice(); S.shareParseItemMap = new Map(); S.shareParseItems.forEach(item => { if (item && item.id) S.shareParseItemMap.set(item.id, item); }); S.shareParseNextPageToken = getShareDetailNextToken(raw); S.shareParseListError = ''; S.shareParseAutoLoadInterrupted = false; S.shareParseAutoLoadResumeTarget = ''; S.shareParseAutoLoadRegionBlocked = false; S.shareParseRegionResumeCtx = null; writeShareParseFolderCache(parentId || ''); ok = true; } catch(e) { if (!S.shareParseMode || S.shareParseListReqId !== reqId) return false; const msg = getShareParseErrorMessage(e) || L.msg_share_parse_load_failed; const isRegionBlocked = isShareParseRegionUnavailableError(e); S.shareParseListError = msg; if (!append) { S.shareParseItems = []; S.shareParseDisplay = []; S.shareParseItemMap = new Map(); if (isRegionBlocked) { const retryFolderNode = folderNode ? Object.assign({}, folderNode, { _sharePath: Array.isArray(folderNode._sharePath) ? S.cloneShareParsePath(folderNode._sharePath) : folderNode._sharePath }) : null; S.shareParseRegionResumeCtx = { folderNode: retryFolderNode, opts: { append: false, forceReload: true, deferActivate } }; markShareParseAutoPagingInterrupted(parentId || '', 'region'); } else { S.shareParseAutoLoadInterrupted = false; S.shareParseAutoLoadResumeTarget = ''; S.shareParseAutoLoadRegionBlocked = false; S.shareParseRegionResumeCtx = null; } } else if (isRegionBlocked) { S.shareParseNextPageToken = pageToken || S.shareParseNextPageToken || ''; markShareParseAutoPagingInterrupted(parentId || '', 'region'); } else if (isShareParseRecoverableNetworkError(e)) { S.shareParseNextPageToken = pageToken || S.shareParseNextPageToken || ''; markShareParseAutoPagingInterrupted(parentId || ''); } else { S.shareParseNextPageToken = ''; S.shareParseAutoLoadInterrupted = false; S.shareParseAutoLoadResumeTarget = ''; S.shareParseAutoLoadRegionBlocked = false; S.shareParseRegionResumeCtx = null; } if (!isRegionBlocked || Date.now() - Number(S.shareParseRegionUnavailableToastAt || 0) > 12000) { showToast(msg, 'error'); if (isRegionBlocked) S.shareParseRegionUnavailableToastAt = Date.now(); } } finally { if (S.shareParseListReqId === reqId) { S.shareParseListLoading = false; S.pagingLoading = !!(S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode && S.shareParseNextPageToken && !S.shareParseAutoLoadInterrupted); if (S.shareParseMode) renderShareParseCurrentView(); restoreShareParseFolderScroll(opts, append); if (!append && S.shareParseMode && S.shareParseListReqId === reqId && !S.shareParseAutoLoadRegionBlocked) scheduleShareParseAutoPaging(parentId || ''); } } if (ok && !append && !opts.suppressShareParseNav && !S.browserNavRestoring && S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode) S.syncShareParseBrowserNavState(recordedShareParseNav ? 'push' : 'replace'); return ok; } function loadShareParseNextPage() { return loadShareParseFolder(null, { append: true }); } function maybeLoadShareParseNextPage() { if (S.shareParseInsightMode) return; scheduleShareParseAutoPaging(S.shareParseCurParentId || '', { force: true }); } const AUDIO_FILE_EXTS = new Set(['mp3','wav','flac','aac','m4a','ogg','opus','ape','wma','amr','m4b','alac','aiff','aif','mid','midi','ra','dts','ac3','dsf','dff']); function getItemExt(it) { const name = String((it && it.name) || '').toLowerCase(); const m = name.match(/\.([^.\/\\]+)$/); return m ? m[1] : ''; } function isAudioLikeItem(it) { if (!it || it.isHeader || it.kind === 'drive#folder') return false; const m = String((it && it.mime_type) || '').toLowerCase(); return m.startsWith('audio/') || AUDIO_FILE_EXTS.has(getItemExt(it)); } function getAudioDirectUrl(detail) { if (!detail) return ''; if (detail.web_content_link) return detail.web_content_link; const oct = detail.links && detail.links['application/octet-stream']; return (oct && oct.url) || ''; } function getAudioPhysicalId(it) { if (!it) return ''; if ((S.offlineMode && it.kind === 'drive#task') || (S.uploadMode && it.file_id)) return it.file_id || it.id || ''; return it.id || ''; } function isVideoLikeItem(it) { if (isAudioLikeItem(it)) return false; const m = String((it && it.mime_type) || '').toLowerCase(); const n = String((it && it.name) || '').toLowerCase(); const dur = Number((it && it.params && it.params.duration) || 0); return m.startsWith('video/') || dur > 0 || ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp'].some(e => n.endsWith('.' + e)); } function isImageLikeItem(it) { const m = String((it && it.mime_type) || '').toLowerCase(); const n = String((it && it.name) || '').toLowerCase(); return m.startsWith('image/') || ['jpg','jpeg','png','gif','webp','bmp','avif'].some(e => n.endsWith('.' + e)); } function isArchiveLike(it) { const m = String((it && it.mime_type) || '').toLowerCase(); const n = String((it && it.name) || '').toLowerCase(); return m.includes('zip') || m.includes('rar') || m.includes('archive') || ['zip','rar','7z','tar','gz','bz2','xz'].some(e => n.endsWith('.' + e)); } let activeTextPreviewReqId = 0; let activeShareTextPreviewFileId = ''; const textPreviewStateMap = new WeakMap(); function isTxtFileLike(it) { if (!it || it.isHeader || it.kind === 'drive#folder') return false; const ext = String((it && it.file_extension) || '').toLowerCase(); const m = String((it && it.mime_type) || '').toLowerCase(); return ext === '.txt' || getItemExt(it) === 'txt' || m.includes('text/plain'); } function showUnsupportedOpenToast() { const L = getStrings(); showToast(L.msg_open_download_after, 'warning'); } function getTextDirectUrlFromFileInfo(fileInfo) { if (!fileInfo) return ''; if (fileInfo.web_content_link) return fileInfo.web_content_link; const oct = fileInfo.links && fileInfo.links['application/octet-stream']; return (oct && oct.url) || ''; } function makeTextPreviewError(code, cause = null) { const err = new Error(code || 'txt_preview_load_failed'); err.code = code || 'load_failed'; if (cause) err.cause = cause; return err; } function stripTextBom(text) { return text && text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text; } function decodeUtf16Bytes(bytes, littleEndian, offset = 0) { let out = ''; let chunk = []; const flush = () => { if (chunk.length) { out += String.fromCharCode.apply(null, chunk); chunk = []; } }; for (let i = offset; i + 1 < bytes.length; i += 2) { const code = littleEndian ? (bytes[i] | (bytes[i + 1] << 8)) : ((bytes[i] << 8) | bytes[i + 1]); chunk.push(code); if (chunk.length >= 8192) flush(); } flush(); return stripTextBom(out); } function tryDecodeText(bytes, label) { try { return stripTextBom(new TextDecoder(label).decode(bytes)); } catch(e) { return null; } } function getTextDecodeScore(text) { if (!text) return 0; let bad = 0; for (let i = 0; i < text.length; i++) { const code = text.charCodeAt(i); if (code === 0xFFFD) bad += 1; else if ((code < 32 && code !== 9 && code !== 10 && code !== 13) || code === 127) bad += 0.4; } return bad / Math.max(1, text.length); } function decodeTextArrayBuffer(buffer) { const bytes = new Uint8Array(buffer || new ArrayBuffer(0)); if (!bytes.byteLength) return { text: '', encoding: '' }; if (bytes.length >= 3 && bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) { const text = tryDecodeText(bytes.subarray(3), 'utf-8'); if (text !== null) return { text, encoding: 'utf-8-bom' }; } if (bytes.length >= 2 && bytes[0] === 0xFF && bytes[1] === 0xFE) return { text: decodeUtf16Bytes(bytes, true, 2), encoding: 'utf-16le' }; if (bytes.length >= 2 && bytes[0] === 0xFE && bytes[1] === 0xFF) return { text: decodeUtf16Bytes(bytes, false, 2), encoding: 'utf-16be' }; const attempts = []; const utf8 = tryDecodeText(bytes, 'utf-8'); if (utf8 !== null) { const score = getTextDecodeScore(utf8); if (score <= 0.001) return { text: utf8, encoding: 'utf-8' }; attempts.push({ text: utf8, encoding: 'utf-8', score }); } ['gb18030','gbk','big5','shift_jis','euc-kr','windows-1252'].forEach(label => { const text = tryDecodeText(bytes, label); if (text !== null) attempts.push({ text, encoding: label, score: getTextDecodeScore(text) }); }); attempts.sort((a, b) => a.score - b.score); const best = attempts[0]; if (!best || best.score > 0.25) throw makeTextPreviewError('decode_failed'); return { text: best.text, encoding: best.encoding }; } async function readTextFromDirectUrl(url, fileInfo) { if (!url) throw makeTextPreviewError('load_failed'); const size = Number(fileInfo && fileInfo.size); if (Number.isFinite(size) && size > CONF.textPreviewMaxBytes) throw makeTextPreviewError('too_large'); let res; try { res = await fetch(url); } catch(e) { throw makeTextPreviewError('load_failed', e); } if (!res || !res.ok) throw makeTextPreviewError('load_failed'); const contentLength = Number(res.headers && res.headers.get ? (res.headers.get('content-length') || 0) : 0); if (Number.isFinite(contentLength) && contentLength > CONF.textPreviewMaxBytes) throw makeTextPreviewError('too_large'); let buffer; try { buffer = await res.arrayBuffer(); } catch(e) { throw makeTextPreviewError('load_failed', e); } if (!buffer || buffer.byteLength === 0) return ''; if (buffer.byteLength > CONF.textPreviewMaxBytes) throw makeTextPreviewError('too_large'); try { return decodeTextArrayBuffer(buffer).text; } catch(e) { if (e && e.code === 'decode_failed') throw e; throw makeTextPreviewError('decode_failed', e); } } async function readTextFromFileInfo(fileInfo) { const url = getTextDirectUrlFromFileInfo(fileInfo); if (!url) throw makeTextPreviewError('load_failed'); return readTextFromDirectUrl(url, fileInfo); } function getTextPreviewState(modal) { return modal ? textPreviewStateMap.get(modal) : null; } function formatTextPreviewShortcutLabel(label, shortcut) { return shortcut ? `${label} (${shortcut})` : label; } function setTextPreviewButtonTip(btn, label) { if (!btn || !label) return; btn.setAttribute('aria-label', label); btn.setAttribute('data-pk-tip', label); btn.removeAttribute('title'); } function setupTextPreviewShortcutKeys(modal) { if (!modal) return; modal.tabIndex = 0; setTimeout(() => { if (document.contains(modal)) modal.focus(); }, 10); modal.addEventListener('keydown', (e) => { const target = e.target; const tag = String(target && target.tagName || '').toLowerCase(); if (target && (target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select')) return; const key = String(e.key || '').toLowerCase(); if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); modal.querySelector('.pk-modal-close')?.click(); return; } if (key === 'f' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); e.stopPropagation(); modal.querySelector('#pk_txt_preview_fullscreen')?.click(); return; } if (e.key === 'Enter' && e.ctrlKey && !e.altKey && !e.metaKey) { const parseBtn = modal.querySelector('#pk_txt_preview_parse_links'); if (parseBtn && parseBtn.style.display !== 'none' && !parseBtn.disabled) { e.preventDefault(); e.stopPropagation(); parseBtn.click(); } } }, true); } function cleanupTextPreviewModal(modal) { const state = getTextPreviewState(modal); if (state) { state.text = ''; state.loading = false; state.parsingLinks = false; } document.documentElement.classList.remove('pk-txt-preview-fullscreen-lock'); document.body.classList.remove('pk-txt-preview-fullscreen-lock'); if (modal) textPreviewStateMap.delete(modal); } function getTextPreviewDefaultCloudTarget() { const path = Array.isArray(S.path) ? S.path : []; const curFolder = path[path.length - 1] || {}; const curId = String(curFolder.id || ''); const isVirtual = curId.startsWith('virtual_') || curId.includes('_root') || curId === 'analyze_root'; const isHomeSubDir = !S.trashMode && !S.shareMode && !S.shareParseMode && !S.offlineMode && !S.uploadMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.isFlattened && !S.dupMode && path.length > 1 && !isVirtual; const L = getStrings(); return { id: isHomeSubDir ? curId : '', name: isHomeSubDir ? (curFolder.name || L.lbl_default_folder) : L.lbl_default_folder, path: isHomeSubDir ? path.filter(p => !String(p.id || '').startsWith('virtual_')) : null }; } function isCloudTaskRateLimited(reqErr) { const errText = String((reqErr && (reqErr.message || reqErr.status || reqErr.code)) || ''); return errText.includes('429'); } async function apiAddOfflineTaskWithRetry(url, targetId = '', extraParams = {}) { let retry = 0; while (retry < 3) { try { return await apiAddOfflineTask(url, targetId || '', extraParams || {}); } catch (reqErr) { if (isCloudTaskRateLimited(reqErr)) { retry++; if (retry >= 3) throw reqErr; await sleep(2000 * retry); } else { throw reqErr; } } } return null; } function refreshCloudTaskView(targetId = '', options = {}) { setTimeout(() => updateQuotaUI(), 1000); const path = Array.isArray(S.path) ? S.path : []; const curPathId = (path[path.length - 1] && path[path.length - 1].id) || ''; if (S.offlineMode) { if (options.skipOfflineProbe === true) return; scheduleOfflineTaskAddProbe(options.reason || 'add', options.delay || 1000); return; } else if (targetId && curPathId === targetId) { load(false, true); } else if (!targetId && path.length === 1) { if (window.pkSmartRefreshTrigger) window.pkSmartRefreshTrigger(true); } } function showSnapshotModal(urlList, options = {}) { const L = getStrings(); return new Promise((resolve) => { let currentSaveId = options.targetId || ''; let currentSaveName = options.targetName || L.lbl_default_folder; let currentSavePath = options.currentPath || null; const displayUrl = urlList[0]; const countSuffix = urlList.length > 1 ? (L.str_snap_link_count_suffix || '').replace('{n}', urlList.length) : ''; const snapIcon = pkIconSized('snapLink', 24); const sm = showModal(`

${L.title_save_method}

${L.msg_save_snapshot_desc}
${pkIconSized('infoCircle', 14)}
${snapIcon}
${esc(displayUrl)}${countSuffix}
${L.lbl_save_to}
${getOfficialFolderFallbackIconHtml(20)} ${esc(currentSaveName)} ${L.btn_modify}
`); const mBox = sm.querySelector('.pk-modal'); if (mBox) { mBox.style.padding = '24px'; mBox.style.width = 'auto'; } const closeBtn = sm.querySelector('.pk-modal-close'); if (closeBtn) { closeBtn.style.top = '30px'; closeBtn.style.right = '24px'; } 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) currentSavePath = selectedPathChain; if (typeof options.onTargetChange === 'function') options.onTargetChange(id, name, fullItem, selectedPathChain); }, currentSavePath); }; const doClose = (result) => { sm.remove(); resolve(result ? { confirm: true, targetId: currentSaveId, targetName: currentSaveName, path: currentSavePath } : { 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(); } }); }); } async function submitCloudLinks(links, options = {}) { const L = getStrings(); const finalLinks = Array.from(new Set((Array.isArray(links) ? links : []).map(l => String(l || '').trim()).filter(Boolean))); if (!finalLinks.length) return { successCount: 0, failCount: 0, cancelled: false }; let saveToId = options.targetId || ''; let saveToName = options.targetName || L.lbl_default_folder; let currentSavePath = options.currentPath || null; const sourceModal = options.sourceModal || null; const hideSourceModal = !!sourceModal && options.hideSourceModal !== false; const sourceDisplay = options.sourceDisplay || 'flex'; if (sourceModal && hideSourceModal) sourceModal.style.display = 'none'; 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; let snapshotLinks = []; let directLinks = finalLinks; if (!options.skipSnapshot) { snapshotLinks = finalLinks.filter(l => /^https?:\/\//i.test(l) && !videoPlatformRegex.test(l)); directLinks = finalLinks.filter(l => !/^https?:\/\//i.test(l) || videoPlatformRegex.test(l)); } let snapshotParams = null; const isSingleSnapshotTask = !options.skipSnapshot && finalLinks.length === 1 && snapshotLinks.length === 1; if (isSingleSnapshotTask) { const result = await showSnapshotModal(snapshotLinks, { targetId: saveToId, targetName: saveToName, currentPath: currentSavePath, onTargetChange: (id, name, fullItem, selectedPathChain) => { saveToId = id; saveToName = name; if (selectedPathChain) currentSavePath = selectedPathChain; if (typeof options.onTargetChange === 'function') options.onTargetChange(id, name, fullItem, selectedPathChain); } }); if (!result.confirm) { if (sourceModal && hideSourceModal && document.contains(sourceModal)) sourceModal.style.display = sourceDisplay; return { successCount: 0, failCount: 0, cancelled: true }; } saveToId = result.targetId || ''; saveToName = result.targetName || saveToName; currentSavePath = result.path || currentSavePath; snapshotParams = { save_as: 'snapshot', targetId: saveToId }; } if (sourceModal && options.removeSourceModal && document.contains(sourceModal)) sourceModal.remove(); const progressTask = FloatBarManager.create(L.msg_creating_cloud_task); let successCount = 0; let failCount = 0; let addExistsCount = 0; const addResults = []; let lastFailMessage = ''; const processQueue = [ ...directLinks.map(u => ({ url: u, isSnap: false })), ...snapshotLinks.map(u => ({ url: u, isSnap: true })) ]; try { for (let i = 0; i < processQueue.length; i++) { const item = processQueue[i]; progressTask.update(L.str_creating_task_n); try { const pid = (item.isSnap && snapshotParams) ? snapshotParams.targetId : saveToId; const extras = item.isSnap ? { save_as: 'snapshot' } : {}; const created = await apiAddOfflineTaskWithRetry(item.url, pid, extras); if (!created) throw new Error('Cloud task create failed after retry'); addResults.push(created); successCount++; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } catch(e) { if (isOfflineTaskAlreadyExistsError(e)) { addExistsCount++; successCount++; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; continue; } console.error(`${options.logPrefix || 'Task Create Failed'}[${item.url}]:`, e); failCount++; if (e && e.message) lastFailMessage = e.message; } await sleep(300); } } finally { progressTask.destroy(); } if (failCount > 0) { const failSummary = L.msg_cloud_task_finish.replace('{s}', successCount).replace('{f}', failCount); showToast(lastFailMessage ? `${failSummary} / ${lastFailMessage}` : failSummary, 'warning'); } else { showToast((options.successMessage || L.msg_cloud_task_success).replace('{n}', successCount)); } if (successCount > 0) { const batch = processQueue.length > 1; const source = options.source || (batch ? 'batch' : 'single'); await handleOfflineTaskAdded(addResults, { source, batch, schedule: false }); scheduleOfflineTaskAddProbe(addExistsCount && !addResults.length ? 'add_exists' : (batch ? 'batch_add' : 'add'), 1000); } if (successCount > 0 || !S.offlineMode) { refreshCloudTaskView(saveToId, { reason: processQueue.length > 1 ? 'batch_add' : 'add', skipOfflineProbe: successCount > 0 }); } return { successCount, failCount, cancelled: false }; } function updateTextPreviewModalControls(modal) { if (!modal || !document.contains(modal)) return; const L = getStrings(); const state = getTextPreviewState(modal); if (!state) return; const parseBtn = modal.querySelector('#pk_txt_preview_parse_links'); const maxBtn = modal.querySelector('#pk_txt_preview_fullscreen'); if (parseBtn) { const canShowParse = !state.loading && !state.error && !state.empty && !!state.text; const canParse = canShowParse && !state.parsingLinks; parseBtn.style.display = canShowParse ? '' : 'none'; const baseLabel = state.parsingLinks ? (L.txt_preview_parse_links_working || L.txt_preview_parse_links) : L.txt_preview_parse_links; const label = formatTextPreviewShortcutLabel(baseLabel, CONF.txtPreviewHotkeys.parse); parseBtn.disabled = !canParse; setTextPreviewButtonTip(parseBtn, label); } if (maxBtn) { const baseLabel = state.fullscreen ? L.txt_preview_exit_fullscreen : L.txt_preview_fullscreen; const label = formatTextPreviewShortcutLabel(baseLabel, CONF.txtPreviewHotkeys.fullscreen); maxBtn.innerHTML = state.fullscreen ? CONF.icons.txtPreviewExitFullscreenSVG : CONF.icons.txtPreviewFullscreenSVG; maxBtn.setAttribute('aria-pressed', state.fullscreen ? 'true' : 'false'); setTextPreviewButtonTip(maxBtn, label); } } function toggleTextPreviewFullscreen(modal) { const state = getTextPreviewState(modal); if (!modal || !state) return; const modalBox = modal.querySelector('.pk-modal.pk-txt-preview-modal'); if (!modalBox) return; state.fullscreen = !state.fullscreen; modalBox.classList.toggle('pk-txt-preview-fullscreen', state.fullscreen); modal.classList.toggle('pk-txt-preview-fullscreen-ov', state.fullscreen); document.documentElement.classList.toggle('pk-txt-preview-fullscreen-lock', state.fullscreen); document.body.classList.toggle('pk-txt-preview-fullscreen-lock', state.fullscreen); updateTextPreviewModalControls(modal); requestAnimationFrame(() => { const bodyEl = modal.querySelector('.pk-txt-preview-body'); if (bodyEl) bodyEl.scrollTop = bodyEl.scrollTop; }); } async function handleTextPreviewParseLinks(modal) { const state = getTextPreviewState(modal); if (!modal || !document.contains(modal) || !state || state.loading || state.error || state.parsingLinks) return; const L = getStrings(); const links = parseCloudLinks(state.text, true); if (!links.length) { showToast(L.txt_preview_no_links, 'warning'); return; } state.parsingLinks = true; updateTextPreviewModalControls(modal); try { const target = getTextPreviewDefaultCloudTarget(); await submitCloudLinks(links, { targetId: target.id || '', targetName: target.name || L.lbl_default_folder, currentPath: target.path || null, successMessage: L.txt_preview_links_submitted || L.msg_cloud_task_success, logPrefix: 'TXT Cloud Task Create Failed' }); } catch(e) { console.error('TXT preview parse links failed:', e); showToast(L.txt_preview_links_failed || L.str_action_failed, 'error'); } finally { state.parsingLinks = false; updateTextPreviewModalControls(modal); } } function showTextPreviewModal(fileInfo) { const L = getStrings(); const title = (fileInfo && fileInfo.name) || L.txt_preview_title; const parseLabel = formatTextPreviewShortcutLabel(L.txt_preview_parse_links, CONF.txtPreviewHotkeys.parse); const fullLabel = formatTextPreviewShortcutLabel(L.txt_preview_fullscreen, CONF.txtPreviewHotkeys.fullscreen); const closeLabel = formatTextPreviewShortcutLabel(L.txt_preview_close || L.btn_close || L.btn_cancel, CONF.txtPreviewHotkeys.close); document.querySelectorAll('.pk-modal-ov').forEach(ov => { if (ov.querySelector('.pk-txt-preview')) ov.remove(); }); const state = { text: '', loading: true, error: false, empty: false, fileName: title, parsingLinks: false, fullscreen: false }; const m = showModal(`

${esc(title)}


`); m.classList.add('pk-txt-preview-ov'); const modalBox = m.querySelector('.pk-modal'); if (modalBox) modalBox.classList.add('pk-txt-preview-modal'); textPreviewStateMap.set(m, state); const titleEl = m.querySelector('.pk-txt-preview-title'); if (titleEl) { titleEl.removeAttribute('title'); titleEl.removeAttribute('data-pk-tip'); } m._pkLocateItem = fileInfo || null; const originalRemove = m.remove.bind(m); m.remove = () => { const locateItem = m._pkLocateItem; cleanupTextPreviewModal(m); originalRemove(); setTimeout(() => S.locatePreviewSourceItem(locateItem), 0); }; const parseBtn = m.querySelector('#pk_txt_preview_parse_links'); const maxBtn = m.querySelector('#pk_txt_preview_fullscreen'); const closeBtn = m.querySelector('.pk-modal-close'); if (parseBtn) parseBtn.onclick = () => { handleTextPreviewParseLinks(m); }; if (maxBtn) maxBtn.onclick = () => toggleTextPreviewFullscreen(m); setTextPreviewButtonTip(closeBtn, closeLabel); setupTextPreviewShortcutKeys(m); updateTextPreviewModalState(m, 'loading'); return m; } function updateTextPreviewModalState(modal, mode, payload = {}) { if (!modal || !document.contains(modal)) return; const L = getStrings(); const statusEl = modal.querySelector('.pk-txt-preview-status'); const preEl = modal.querySelector('.pk-txt-preview-pre'); const bodyEl = modal.querySelector('.pk-txt-preview-body'); if (!statusEl || !preEl) return; const previewState = getTextPreviewState(modal); if (previewState) { previewState.loading = mode === 'loading'; previewState.error = mode === 'error' || mode === 'too_large'; previewState.empty = mode === 'empty'; if (mode === 'success') previewState.text = String(payload.text || ''); else previewState.text = ''; } const showStatus = (key, isError = false) => { preEl.style.display = 'none'; preEl.textContent = ''; statusEl.style.display = 'flex'; statusEl.classList.toggle('pk-txt-preview-error', !!isError); statusEl.textContent = L[key] || L.txt_preview_load_failed || L.str_action_failed; }; if (mode === 'success') { statusEl.style.display = 'none'; statusEl.classList.remove('pk-txt-preview-error'); preEl.style.display = 'block'; preEl.textContent = String(payload.text || ''); } else if (mode === 'empty') showStatus('txt_preview_empty', false); else if (mode === 'too_large') showStatus('txt_preview_too_large', true); else if (mode === 'error') showStatus(payload.key || 'txt_preview_load_failed', true); else showStatus('txt_preview_loading', false); if (bodyEl) bodyEl.scrollTop = 0; updateTextPreviewModalControls(modal); } function updateTextPreviewModalError(modal, err) { const code = err && err.code; if (code === 'too_large') updateTextPreviewModalState(modal, 'too_large'); else if (code === 'decode_failed') updateTextPreviewModalState(modal, 'error', { key: 'txt_preview_decode_failed' }); else updateTextPreviewModalState(modal, 'error', { key: 'txt_preview_load_failed' }); } function isTextPreviewRequestCurrent(reqId, modal, isShare, fileId = '') { if (!modal || !document.contains(modal)) return false; if (activeTextPreviewReqId !== reqId) return false; if (!isShare) return true; return !!S.shareParseMode && String(activeShareTextPreviewFileId || '') === String(fileId || ''); } async function handleOpenTextFile(item) { if (!item || item._isShareItem) return; const modal = showTextPreviewModal(item); const reqId = ++activeTextPreviewReqId; activeShareTextPreviewFileId = ''; const isCurrent = () => isTextPreviewRequestCurrent(reqId, modal, false); try { const itemUrl = getTextDirectUrlFromFileInfo(item); let detail = item; const readFromDetail = async (fileInfo) => { const text = await readTextFromFileInfo(fileInfo); if (!isCurrent()) return; updateTextPreviewModalState(modal, text.length ? 'success' : 'empty', { text }); }; try { if (!itemUrl) { if (!item.id) throw makeTextPreviewError('load_failed'); detail = await apiGet(item.id); } await readFromDetail(detail); } catch(e) { if (itemUrl && e && e.code === 'load_failed') { if (!item.id) throw makeTextPreviewError('load_failed', e); detail = await apiGet(item.id); await readFromDetail(detail); } else { throw e; } } } catch(e) { if (isCurrent()) updateTextPreviewModalError(modal, e); } } async function handleOpenShareTextFile(item) { if (!item || !item._isShareItem) return; const modal = showTextPreviewModal(item); const reqId = ++activeTextPreviewReqId; const fileId = String(item.id || item.file_id || ''); activeShareTextPreviewFileId = fileId; const isCurrent = () => isTextPreviewRequestCurrent(reqId, modal, true, fileId); try { const detail = await fetchShareFileInfo(item); if (!isCurrent()) return; const text = await readTextFromFileInfo(detail); if (!isCurrent()) return; updateTextPreviewModalState(modal, text.length ? 'success' : 'empty', { text }); } catch(e) { if (isCurrent()) updateTextPreviewModalError(modal, e); } } function getShareFileInfoAccessToken(detail) { const apps = Array.isArray(detail && detail.apps) ? detail.apps : []; for (const app of apps) { const link = String((app && app.link) || ''); if (!link || link.indexOf('access_token=') < 0) continue; try { const u = new URL(link); const token = u.searchParams.get('access_token'); if (token) return token; } catch(e) { const m = link.match(/[?&]access_token=([^&#]+)/); if (m) return decodeURIComponent(m[1]); } } return ''; } function getShareDirectUrl(detail) { if (!detail) return ''; if (detail.web_content_link) return detail.web_content_link; const links = detail.links || {}; const keys = Object.keys(links || {}); for (const k of keys) { const v = links[k]; if (v && v.url) return v.url; } const medias = Array.isArray(detail.medias) ? detail.medias : []; for (const m of medias) { if (m && m.link && m.link.url) return m.link.url; } return ''; } async function fetchShareFileInfo(item, options = {}) { const info = S.shareParseInfo || {}; const shareId = item._shareId || info.share_id || ''; const passToken = item._passCodeToken || info.pass_code_token || ''; if (!shareId || !passToken) throw makeShareParseError('msg_share_unzip_no_access'); const fileId = item.id || item.file_id || ''; const url = `https://api-drive.mypikpak.com/drive/v1/share/file_info?share_id=${encodeURIComponent(shareId)}&file_id=${encodeURIComponent(fileId)}&pass_code_token=${encodeURIComponent(passToken)}`; const res = await fetch(url, { method: 'GET', headers: getHeaders(), signal: options.signal || undefined }); syncTime(res.headers); const data = await res.json().catch(() => ({})); if (!res.ok || (data.share_status && data.share_status !== 'OK')) throw makeShareParseError(resolveShareDetailErrorKey(res.status, data), res.status, data); return data.file_info || data.file || data; } function isShareDownloadDirectUrl(url) { const value = String(url || '').trim(); if (!/^https?:\/\//i.test(value)) return false; let normalized = value.toLowerCase(); try { normalized = decodeURIComponent(normalized); } catch(e) {} if (normalized.includes('ts_downloader') || normalized.includes('.m3u8')) return false; try { const parsed = new URL(value); const path = String(parsed.pathname || '').toLowerCase(); if (/(^|\/)hls(?:\/|$)/.test(path)) return false; const format = String(parsed.searchParams.get('format') || parsed.searchParams.get('type') || '').toLowerCase(); if (format === 'hls' || format === 'm3u8') return false; } catch(e) { return false; } return true; } function getShareDownloadDirectUrl(detail) { if (!detail) return ''; const webContentLink = String(detail.web_content_link || ''); if (isShareDownloadDirectUrl(webContentLink)) return webContentLink; const links = detail.links && typeof detail.links === 'object' ? detail.links : {}; const linkEntries = Object.entries(links).sort(([a], [b]) => { const score = key => /original|origin|download|content/i.test(String(key || '')) ? 0 : 1; return score(a) - score(b); }); for (const [, link] of linkEntries) { const url = typeof link === 'string' ? link : (link && link.url); if (isShareDownloadDirectUrl(url)) return String(url); } const medias = Array.isArray(detail.medias) ? detail.medias.slice() : []; medias.sort((a, b) => { const token = media => String((media && (media.media_name || media.name || media.type)) || '').toUpperCase(); const score = media => ['ORIGINAL','ORIGIN','RESOLUTION_ORIGINAL'].includes(token(media)) ? 0 : 1; return score(a) - score(b); }); for (const media of medias) { const url = media && media.link && media.link.url; if (isShareDownloadDirectUrl(url)) return String(url); } return ''; } function getShareDownloadLineage(item) { const sharePath = item && item._sharePath; const itemPath = item && item.path; const rawPath = (Array.isArray(sharePath) ? sharePath.length > 0 : !!sharePath) ? sharePath : ((Array.isArray(itemPath) ? itemPath.length > 0 : !!itemPath) ? itemPath : S.shareParsePath); let nodes = []; if (Array.isArray(rawPath)) { nodes = rawPath.filter(node => node && !node._sharePanel && !node._shareRoot && node.id !== 'share_parse_panel').map(node => typeof node === 'string' ? { id: '', name: node } : { id: node.id || '', name: node.name || '' }); } else if (typeof rawPath === 'string') { nodes = rawPath.split(/[\\/]+/).filter(Boolean).map(name => ({ id: '', name })); } nodes = nodes.filter(node => node.name && node.id !== 'share_parse_panel'); if (nodes.length && item && item.kind !== 'drive#folder') { const tail = nodes[nodes.length - 1]; if ((tail.id && tail.id === item.id) || (!tail.id && tail.name === item.name)) nodes.pop(); } if (item && item.kind === 'drive#folder') { const tail = nodes[nodes.length - 1]; if (!tail || ((tail.id || '') !== (item.id || '') && tail.name !== item.name)) nodes.push({ id: item.id || '', name: item.name || '' }); } return nodes.filter(node => node.name); } function getShareSelectedDownloadLineage(item) { if (!item || item.kind !== 'drive#folder') return []; return [{ id: item.id || '', name: item.name || '' }].filter(node => node.name); } function getShareResolvedDownloadLineage(item) { if (item && Array.isArray(item._lineage)) return item._lineage.filter(node => node && node.name).map(node => ({ id: node.id || '', name: node.name || '' })); return getShareDownloadLineage(item); } function isShareDownloadPreviewLimited(detail) { const params = detail && detail.params && typeof detail.params === 'object' ? detail.params : {}; const range = Number(params.anonymous_play_range); if (!Number.isFinite(range) || range <= 0 || range >= 1) return false; const duration = Number(params.duration || detail?.duration || 0); const limitSeconds = Number(params.anonymous_play_seconds || 0); if (Number.isFinite(duration) && duration > 0 && Number.isFinite(limitSeconds) && limitSeconds > 0) { return duration > limitSeconds + 1; } return true; } async function resolveShareDownloadTask(item, options = {}) { if (!item || !item._isShareItem || item.kind === 'drive#folder') return null; let detail = null; let directUrl = ''; for (let attempt = 0; attempt < 2; attempt++) { detail = await fetchShareFileInfo(item, { signal: options.signal }); if (isShareDownloadPreviewLimited(detail)) throw makeShareParseError('msg_share_download_preview_limited'); directUrl = getShareDownloadDirectUrl(detail); if (directUrl) break; } if (!directUrl) throw makeShareParseError('msg_share_download_no_direct_link'); const task = mergeSharePlayableDetail(Object.assign({}, item), detail); task.web_content_link = directUrl; task.url = directUrl; task._lineage = getShareResolvedDownloadLineage(item); task._sharePath = Array.isArray(item._sharePath) ? S.cloneShareParsePath(item._sharePath) : item._sharePath; delete task._shareResolvedDetail; return task; } async function listShareDownloadFolder(folder, signal = null) { const parentId = String((folder && folder.id) || ''); const path = Array.isArray(folder && folder._sharePath) && folder._sharePath.length ? S.cloneShareParsePath(folder._sharePath) : buildShareParseRootPath().concat(getShareDownloadLineage(folder)); const out = []; const seen = new Set(); const appendItems = items => { (Array.isArray(items) ? items : []).forEach((item, index) => { const mapped = item && item._isShareItem ? cloneShareParseCacheItem(item) : mapShareDetailFile(item, index, { parentId, path }); const key = `${mapped.kind || ''}:${mapped.id || mapped.name || index}`; if (seen.has(key)) return; seen.add(key); out.push(mapped); }); }; let pageToken = ''; const cached = readShareParseFolderCache(parentId); if (cached) { appendItems(cached.items); pageToken = cached.nextPageToken || ''; if (cached.complete) return out; } do { if (signal && signal.aborted) throw new DOMException('Aborted', 'AbortError'); const raw = await fetchShareDetail(parentId, pageToken, signal); appendItems(getShareDetailFiles(raw)); pageToken = getShareDetailNextToken(raw); writeShareParseFolderCacheEntry(parentId, path, out, pageToken || ''); if (pageToken) await sleep(30); } while (pageToken); return out; } function getDownloadSourceItemById(id) { if (!id) return null; if (S.shareParseMode && S.shareParseListActive) { return (S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id)) || (S.itemMap && S.itemMap.get(id)) || null; } return S.itemMap && S.itemMap.get(id); } function formatShareDownloadFailure(item, error) { const L = getStrings(); const name = String((item && item.name) || ''); const reason = error && error.shareParseKey ? getShareParseErrorMessage(error) : L.str_aria2_fetch_err; return `${name} ${reason}`.trim(); } function triggerAnchorBrowserDownload(file, url) { const href = String(url || ''); const name = sanitizeDownloadFileName(file && file.name); if (!href) return false; const link = document.createElement('a'); link.href = href; link.download = name; link.setAttribute('download', name); link.style.display = 'none'; document.body.appendChild(link); link.click(); setTimeout(() => { if (link.parentNode) link.remove(); }, 10000); return true; } function triggerNamedBrowserDownload(file, url) { const href = String(url || ''); const name = sanitizeDownloadFileName(file && file.name); if (!href) return false; if (typeof GM_download === 'function') { try { GM_download({ url: href, name, saveAs: false, onerror: e => { console.warn('[Browser Download Failed]', name, e); triggerAnchorBrowserDownload(file, href); } }); return true; } catch (e) { console.warn('[Browser Download Fallback]', name, e); return triggerAnchorBrowserDownload(file, href); } } return triggerAnchorBrowserDownload(file, href); } async function showBrowserDownloadFailures(list, successCount = 0) { const L = getStrings(); const failures = Array.isArray(list) ? list.filter(Boolean) : []; if (!failures.length) return; const success = successCount > 0 ? `${L.msg_down_success.replace('{n}', successCount)}\n` : ''; const failedLabel = String(L.str_failed || '').replace(/[::]+$/g, ''); const summary = `${success}${failedLabel}: ${failures.length} ${L.str_items}`; const preview = failures.slice(0, 10).join('\n') + (failures.length > 10 ? '\n...' : ''); await showAlert(`${summary}\n\n${preview}`, L.title_alert); } function mergeSharePlayableDetail(item, detail) { if (!item || !detail) return item; const directUrl = getShareDirectUrl(detail); Object.assign(item, { name: detail.name || item.name, size: detail.size || item.size, mime_type: detail.mime_type || item.mime_type, icon_link: detail.icon_link || item.icon_link, thumbnail_link: detail.thumbnail_link || item.thumbnail_link, web_content_link: directUrl || item.web_content_link || '', medias: Array.isArray(detail.medias) ? detail.medias : (item.medias || []), hash: detail.hash || detail.gcid || item.hash || '', gcid: detail.hash || detail.gcid || item.gcid || '', md5_checksum: detail.md5_checksum || item.md5_checksum || '', params: Object.assign({}, item.params || {}, detail.params || {}), _shareResolvedDetail: detail, _pkSkipDurationProbe: true }); return item; } async function resolveSharePlayableFile(item) { if (!item || !item._isShareItem) return item; if (item._shareResolvedDetail && (item.web_content_link || (Array.isArray(item.medias) && item.medias.length) || isArchiveLike(item))) return item; const detail = await fetchShareFileInfo(item); return mergeSharePlayableDetail(item, detail); } function parseShareRestoreTraceMap(raw) { const data = getShareParsePayload(raw); const params = data.params || (data.task && data.task.params) || raw.params || {}; let trace = params.trace_file_ids || params.traceFileIds || data.trace_file_ids || data.traceFileIds || ''; if (!trace) return {}; if (typeof trace === 'object') return trace; try { return JSON.parse(trace); } catch(e) {} return {}; } async function restoreShareFileForOpen(item, detail = null) { const info = S.shareParseInfo || {}; const sourceId = getShareParseTraceFileId(item) || (detail && detail.id) || item.id || ''; const ancestorIds = Array.from(new Set(getShareParseAncestorIds(item))); const payload = { share_id: item._shareId || info.share_id || '', pass_code_token: item._passCodeToken || info.pass_code_token || '', file_ids: [], params: { trace_file_ids: sourceId } }; if (ancestorIds.length) payload.ancestor_ids = ancestorIds; const res = await fetch('https://api-drive.mypikpak.com/drive/v1/share/restore', { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); syncTime(res.headers); const data = await res.json().catch(() => ({})); if (!res.ok) throw makeShareParseError(resolveShareParseSaveErrorKey(res.status, data), res.status, data); const key = resolveShareParseSaveErrorKey(0, data); if (key) throw makeShareParseError(key, 200, data); const taskIds = extractShareParseSaveTaskIds(data); if (!taskIds.length) return data.file_id || data.fileId || ''; for (let loop = 0; loop < 60; loop++) { await sleep(1000); for (const taskId of taskIds) { const tRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${encodeURIComponent(taskId)}?_t=${Date.now()}`, { headers: getHeaders() }); if (!tRes.ok) continue; const tData = await tRes.json().catch(() => ({})); const phase = String(tData.phase || (tData.task && tData.task.phase) || '').toUpperCase(); const tKey = resolveShareParseSaveErrorKey(0, tData); if (tKey || phase === 'PHASE_TYPE_ERROR') throw makeShareParseError(tKey || 'msg_share_parse_save_task_failed', 200, tData); if (phase === 'PHASE_TYPE_COMPLETE' || phase === 'PHASE_TYPE_COMPLETED' || phase === 'COMPLETE' || phase === 'SUCCESS') { const map = parseShareRestoreTraceMap(tData); return map[sourceId] || Object.values(map)[0] || tData.file_id || (tData.task && tData.task.file_id) || ''; } } } throw makeShareParseError('msg_share_parse_save_task_failed'); } async function submitShareArchiveUnzip(item, detail, pwdValue, archiveFiles) { const restoredId = await restoreShareFileForOpen(item, detail); if (!restoredId) throw makeShareParseError('msg_share_parse_save_task_failed'); const savedFile = Object.assign({}, detail || item, { id: restoredId, _isShareItem: false }); return sendUnzipRequest(savedFile, pwdValue || "", archiveFiles || []); } async function handleOpenShareArchive(item) { const L = getStrings(); if (!item || !item._isShareItem) return; setLoad(true); updateLoadTxt(L.loading); try { const detail = await fetchShareFileInfo(item); mergeSharePlayableDetail(item, detail); const token = getShareFileInfoAccessToken(detail); if (!token) throw makeShareParseError('msg_share_unzip_no_access'); const gcid = detail.hash || detail.gcid || item.hash || item.gcid || ''; setLoad(false); await showArchivePreview(item, "", { title: detail.name || item.name, gcid, fileId: detail.id || item.id, listUrl: `https://api-drive.mypikpak.com/decompress/v1/list?access_token=${encodeURIComponent(token)}`, getHeaders: getHeaders, submitUnzip: (pwd, files) => submitShareArchiveUnzip(item, detail, pwd, files) }); } catch (e) { setLoad(false); showToast(getShareParseOfficialErrorMessage(e) || ((e && e.message && !/^msg_/.test(e.message) && !e.shareParseKey) ? e.message : '') || L.str_action_failed, 'error'); } } async function handleOpenShareFile(item) { const L = getStrings(); if (!item || !item._isShareItem) return; setLoad(true); updateLoadTxt(L.loading); try { const resolved = await resolveSharePlayableFile(item); setLoad(false); if (isAudioLikeItem(resolved)) playAudio(resolved); else if (isVideoLikeItem(resolved)) playVideo(resolved); else if (isImageLikeItem(resolved)) showImage(resolved); } catch (e) { setLoad(false); const key = e && e.shareParseKey; showToast((key && L[key]) || getShareParseErrorMessage(e) || e.message || L.str_action_failed, 'error'); } } function handleShareParseFolderOpen(item) { if (!item || !item._isShareItem || item.kind !== 'drive#folder') return; loadShareParseFolder(item, { append: false }); } function handleShareParseCrumbClick(index) { if (!S.shareParseMode || S.shareParseInsightMode) return; const path = Array.isArray(S.shareParsePath) ? S.shareParsePath : []; if (index <= 0) { S.shareParseLatestChildId = null; resetShareParseListState(false); renderShareParseCurrentView(); return; } if (gmGet('pk_keep_pos', true) && index < path.length - 1) { S.shareParseLatestChildId = (path[index + 1] && path[index + 1].id) || null; } else { S.shareParseLatestChildId = null; } const targetPath = path.slice(0, index + 1); const target = targetPath[targetPath.length - 1]; if (!target) return; loadShareParseFolder({ id: target.id || '', name: target.name || '', parent_id: target.parent_id || '', _sharePath: targetPath }, { append: false }); } async function handleShareParseSubmit() { const L = getStrings(); if (S.shareParseLoading) return; const linkInput = UI.in ? UI.in.querySelector('#pk-share-parse-link') : null; const passInput = UI.in ? UI.in.querySelector('#pk-share-parse-pass') : null; const raw = linkInput ? linkInput.value : ''; const autoPassCode = parseSharePassCode(raw); const shareParseLimits = ((getConfigLimitSchema('pk_share_parse_history') || {}).localLimit || {}); const passMaxLen = shareParseLimits.maxPassLen || 10; const passCode = safeStringLimit((passInput ? passInput.value.trim() : '') || autoPassCode, passMaxLen); if (passInput && passInput.value !== passCode) passInput.value = passCode; S.shareParseDraft = { raw, pass_code: passCode }; if (!raw.trim()) { S.shareParseInfo = null; S.shareParseError = L.msg_share_parse_empty_link; resetShareParseListState(false); showToast(L.msg_share_parse_empty_link, 'warning'); renderShareParsePanel(); return; } const shareId = parseShareInput(raw); if (!shareId) { S.shareParseInfo = null; S.shareParseError = L.msg_share_parse_invalid_link; resetShareParseListState(false); showToast(L.msg_share_parse_invalid_link, 'error'); renderShareParsePanel(); return; } const displayRaw = formatShareParseDisplayLink(shareId); S.shareParseDraft = { raw: displayRaw, pass_code: passCode }; if (linkInput) linkInput.value = displayRaw; const pendingHistory = findShareParseHistoryPassUpdateRecord(shareId, passCode) || findShareParseHistoryRecordByShare(shareId, passCode); S.shareParseHistoryPendingId = pendingHistory ? pendingHistory.id : ''; const reqId = (S.shareParseReqId || 0) + 1; S.shareParseReqId = reqId; S.shareParseLoading = true; S.shareParseError = ''; S.shareParseInfo = null; resetShareParseListState(false); renderShareParsePanel(); try { const rawInfo = await fetchShareParseInfo(shareId, passCode); if (S.shareParseReqId !== reqId) return; const info = buildShareParseInfo(shareId, passCode, rawInfo); if (!info.pass_code_token) throw makeShareParseError('msg_share_parse_bad_pass'); if (S.shareParseHistoryPendingId) syncShareParseHistoryPassAfterInfoSuccess(S.shareParseHistoryPendingId, info, displayRaw); if (!S.shareParseMode) return; S.shareParseInfo = info; S.shareParseError = ''; const rootLoaded = await loadShareParseFolder(null, { append: false, forceReload: true, deferActivate: true }); if (rootLoaded && S.shareParseMode && S.shareParseReqId === reqId && S.shareParseInfo && S.shareParseInfo.pass_code_token) { recordShareParseHistoryFromCurrent(); showToast(L.msg_share_parse_success); } } catch(e) { if (!S.shareParseMode || S.shareParseReqId !== reqId) return; const msg = getShareParseErrorMessage(e); markShareParseHistoryCurrentError(e, msg); S.shareParseInfo = null; S.shareParseError = msg; resetShareParseListState(false); showToast(msg, 'error'); } finally { if (S.shareParseReqId === reqId) { S.shareParseLoading = false; S.shareParseHistoryPendingId = ''; if (S.shareParseMode) renderShareParseCurrentView(); } } } function renderShareParsePanel() { const L = getStrings(); ensureStaticPanelViewportVisible(); const draft = S.shareParseDraft || { raw: '', pass_code: '' }; const isLoading = !!S.shareParseLoading; if (!S.shareParseListActive) clearShareParseListMirror(); const disabledAttr = isLoading ? ' disabled' : ''; setSearchWrapVisible(false); if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.lblSearchPath) UI.lblSearchPath.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.stat) UI.stat.style.display = 'none'; const resultHtml = (() => { if (S.shareParseError) return `
${esc(S.shareParseError)}
`; return ''; })(); if (UI.win) { UI.win.classList.remove('pk-grid-view', 'pk-view-switching', 'pk-grid-resizing', 'pk-grid-scrolling', 'pk-mode-trash'); UI.win.classList.add('pk-share-parse-mode', 'pk-share-parse-root-mode'); } const hd = UI.win ? UI.win.querySelector('.pk-grid-hd') : null; if (hd) { hd.style.display = 'none'; hd.style.visibility = ''; hd.classList.remove('pk-grid-view-hd'); } if (UI.vp) { UI.vp.scrollTop = 0; UI.vp.scrollLeft = 0; } if (UI.in) { if (typeof clearGridStableRows === 'function') clearGridStableRows(UI.in); UI.in.style.height = '100%'; UI.in.style.transform = 'none'; UI.in.innerHTML = `
${resultHtml}
`; const linkInput = UI.in.querySelector('#pk-share-parse-link'); const passInput = UI.in.querySelector('#pk-share-parse-pass'); const btn = UI.in.querySelector('#pk-share-parse-submit'); const clipboardBtn = UI.in.querySelector('#pk-share-parse-clipboard'); const clearBtn = UI.in.querySelector('#pk-share-parse-clear'); const historyBtn = UI.in.querySelector('#pk-share-parse-history'); const syncDraft = () => { S.shareParseDraft = { raw: linkInput ? linkInput.value : '', pass_code: passInput ? passInput.value : '' }; }; if (linkInput) { linkInput.oninput = syncDraft; linkInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); handleShareParseSubmit(); } }; } if (passInput) { passInput.oninput = syncDraft; passInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); handleShareParseSubmit(); } }; } const shareParseLimits = ((getConfigLimitSchema('pk_share_parse_history') || {}).localLimit || {}); bindPlainTextMaxLengthLimits(UI.in, [ ['#pk-share-parse-link', shareParseLimits.maxTextLen || 512], ['#pk-share-parse-pass', shareParseLimits.maxPassLen || 10] ]); if (btn) btn.onclick = handleShareParseSubmit; if (clipboardBtn) clipboardBtn.onclick = handleShareParseClipboardImport; if (clearBtn) clearBtn.onclick = () => { if (linkInput) linkInput.value = ''; if (passInput) passInput.value = ''; syncDraft(); }; if (historyBtn) historyBtn.onclick = openShareParseHistoryModal; if (typeof requestAutoHideButtonTextCheck === 'function') requestAutoHideButtonTextCheck(); } if (UI.pop) { UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; } if (UI.ctx) UI.ctx.style.display = 'none'; } function getDefaultLinkBookmarkFolders() { return [{ folder: 'My Links', list: [] }]; } function makeLinkBookmarkHash(value) { const str = String(value || ''); let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0).toString(16); } function getLinkBookmarkAuthSig(headers) { const h = headers || getHeaders(); return h && h.Authorization ? makeLinkBookmarkHash(h.Authorization) : ''; } function resetLinkBookmarkMemoryForAccount(authSig) { S.linkBookmarkFolders = getDefaultLinkBookmarkFolders(); S.linkBookmarkSelectedFolderIndex = 0; S.linkBookmarkSearch = ''; S.linkBookmarkSearchDraft = ''; S.linkBookmarkFolderPage = 1; S.linkBookmarkLinkPage = 1; S.linkBookmarkFolderPageSize = 3; S.linkBookmarkLinkPageSize = 1; S.linkBookmarkIconRuntimeCache = new Map(); S.linkBookmarkError = ''; S.linkBookmarkRemoteHash = ''; S.linkBookmarkHasLoaded = false; S.linkBookmarkAuthSig = authSig || ''; } function ensureLinkBookmarkFolders() { if (!Array.isArray(S.linkBookmarkFolders) || S.linkBookmarkFolders.length === 0) { S.linkBookmarkFolders = getDefaultLinkBookmarkFolders(); S.linkBookmarkSelectedFolderIndex = 0; } if (S.linkBookmarkSelectedFolderIndex < 0 || S.linkBookmarkSelectedFolderIndex >= S.linkBookmarkFolders.length) { S.linkBookmarkSelectedFolderIndex = 0; } return S.linkBookmarkFolders; } function unwrapOfficialBookmarkSetting(value) { let cur = value; for (let i = 0; i < 3; i++) { if (!cur || typeof cur !== 'object' || Array.isArray(cur)) break; if (Object.prototype.hasOwnProperty.call(cur, 'value')) { cur = cur.value; continue; } if (Object.prototype.hasOwnProperty.call(cur, 'data') && (typeof cur.data === 'string' || Array.isArray(cur.data))) { cur = cur.data; continue; } if (Object.prototype.hasOwnProperty.call(cur, 'content')) { cur = cur.content; continue; } break; } return cur; } function extractOfficialBookmarkSetting(payload) { if (Array.isArray(payload) || typeof payload === 'string' || payload === null) return payload; if (!payload || typeof payload !== 'object') return undefined; const candidates = []; const pushOwn = (obj, key) => { if (obj && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, key)) candidates.push(obj[key]); }; pushOwn(payload, 'bookmark'); pushOwn(payload.settings, 'bookmark'); pushOwn(payload.data, 'bookmark'); pushOwn(payload.data && payload.data.settings, 'bookmark'); pushOwn(payload.items, 'bookmark'); pushOwn(payload.data && payload.data.items, 'bookmark'); const scanArray = (arr) => { if (!Array.isArray(arr)) return; arr.forEach(item => { if (!item || typeof item !== 'object' || Array.isArray(item)) return; const key = String(item.item || item.key || item.name || item.item_name || item.type || '').trim(); if (key !== 'bookmark') return; if (Object.prototype.hasOwnProperty.call(item, 'value')) candidates.push(item.value); else if (Object.prototype.hasOwnProperty.call(item, 'data')) candidates.push(item.data); else if (Object.prototype.hasOwnProperty.call(item, 'content')) candidates.push(item.content); }); }; scanArray(payload.settings); scanArray(payload.data); scanArray(payload.items); scanArray(payload.data && payload.data.settings); scanArray(payload.data && payload.data.items); return candidates.length ? unwrapOfficialBookmarkSetting(candidates[0]) : undefined; } function makeLinkBookmarkInvalidError() { const e = new Error('LINK_BOOKMARK_INVALID'); e.linkBookmarkInvalid = true; return e; } function normalizeLinkBookmarkText(value) { return value === null || typeof value === 'undefined' ? '' : String(value); } function getLinkBookmarkFieldMaxLen(type) { if (type === 'href') return Number(CONF.linkBookmarkHrefMaxLen) || 65534; if (type === 'title') return Number(CONF.linkBookmarkTitleMaxLen) || 1024; return Number(CONF.linkBookmarkFolderNameMaxLen) || 1024; } function getLinkBookmarkPlainHrefMaxLen() { return Number(CONF.linkBookmarkPlainHrefMaxLen) || 1024; } function normalizeLinkBookmarkLimitedText(value, type, shouldTrim = false) { return safeStringLimit(normalizeLinkBookmarkText(value), getLinkBookmarkFieldMaxLen(type), '', shouldTrim); } function isLinkBookmarkFieldTooLong(value, type) { return normalizeLinkBookmarkText(value).trim().length > getLinkBookmarkFieldMaxLen(type); } function isLinkBookmarkOfficialFieldOverLimit(value, type) { return normalizeLinkBookmarkText(value).length > getLinkBookmarkFieldMaxLen(type); } function isLinkBookmarkDomainLikeHost(host) { const raw = normalizeLinkBookmarkText(host).trim().replace(/^\[|\]$/g, ''); if (!raw) return false; const lower = raw.toLowerCase(); if (lower === 'localhost') return true; if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(lower)) return true; try { const u = new URL(`https://${raw}`); const h = normalizeLinkBookmarkText(u.hostname).toLowerCase(); return h === 'localhost' || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(h) || h.includes('.'); } catch(e) { return false; } } function isLinkBookmarkHrefUrlLike(value) { const raw = normalizeLinkBookmarkText(value).trim(); if (!raw || /\s/.test(raw)) return false; if (/^[A-Za-z][A-Za-z0-9+.-]{1,31}:[^\s]*$/.test(raw)) return true; if (/^\/\//.test(raw)) { try { const u = new URL(`https:${raw}`); return isLinkBookmarkDomainLikeHost(u.hostname); } catch(e) { return false; } } try { const u = new URL(raw); if (u.protocol && u.hostname) return true; } catch(e) {} const first = raw.split(/[\/?#]/)[0] || ''; return isLinkBookmarkDomainLikeHost(first); } function isLinkBookmarkUnsafeLongPlainHref(value) { const raw = normalizeLinkBookmarkText(value).trim(); return raw.length > getLinkBookmarkPlainHrefMaxLen() && !isLinkBookmarkHrefUrlLike(raw); } function isLinkBookmarkConfigCloudPseudoLink(folder, link) { const folderName = normalizeLinkBookmarkText(folder && folder.folder).trim(); if (folderName !== CONF.linkBookmarkSyncFolderName) return false; const title = normalizeLinkBookmarkText(link && link.title).trim(); const href = normalizeLinkBookmarkText(link && link.href).trim(); if (title.startsWith(CONF.configCloudManifestTitlePrefix) && href.startsWith(CONF.configCloudManifestHrefPrefix)) return true; if (title.startsWith(CONF.configCloudChunkTitlePrefix) && /^[A-Za-z0-9_-]+$/.test(href)) return true; return false; } function findLinkBookmarkOfficialFieldOverLimit(folders) { const list = Array.isArray(folders) ? folders : []; for (let folderIndex = 0; folderIndex < list.length; folderIndex++) { const folder = list[folderIndex]; if (isLinkBookmarkOfficialFieldOverLimit(folder && folder.folder, 'folder')) return { type: 'folder', folderIndex, linkIndex: -1 }; const links = Array.isArray(folder && folder.list) ? folder.list : []; for (let linkIndex = 0; linkIndex < links.length; linkIndex++) { const link = links[linkIndex]; if (isLinkBookmarkOfficialFieldOverLimit(link && link.title, 'title')) return { type: 'title', folderIndex, linkIndex }; if (!isLinkBookmarkConfigCloudPseudoLink(folder, link) && !isMagnetArchivePseudoLink(link) && isLinkBookmarkOfficialFieldOverLimit(link && link.href, 'href')) return { type: 'href', folderIndex, linkIndex }; } } return null; } function findLinkBookmarkUnsafeLongPlainHref(folders) { const list = Array.isArray(folders) ? folders : []; for (let folderIndex = 0; folderIndex < list.length; folderIndex++) { const folder = list[folderIndex]; const links = Array.isArray(folder && folder.list) ? folder.list : []; for (let linkIndex = 0; linkIndex < links.length; linkIndex++) { const link = links[linkIndex]; if (!isLinkBookmarkConfigCloudPseudoLink(folder, link) && !isMagnetArchivePseudoLink(link) && isLinkBookmarkUnsafeLongPlainHref(link && link.href)) return { type: 'href', folderIndex, linkIndex }; } } return null; } function assertLinkBookmarkOfficialFieldsSafe(folders) { if (findLinkBookmarkOfficialFieldOverLimit(folders) || findLinkBookmarkUnsafeLongPlainHref(folders)) throw makeLinkBookmarkInvalidError(); } const LINK_BOOKMARK_UI_TEMP_KEYS = new Set(['selected', 'uiId', 'isEditing', 'searchMatched', 'syncStatus', 'selectedFolderIndex', 'searchKeyword', '_selected', '_uiId', '_isEditing', '_searchMatched', '_syncStatus']); function cloneLinkBookmarkOfficialFields(source) { const out = {}; if (!source || typeof source !== 'object' || Array.isArray(source)) return out; Object.keys(source).forEach(key => { if (LINK_BOOKMARK_UI_TEMP_KEYS.has(key)) return; const value = source[key]; if (typeof value === 'function' || typeof value === 'undefined') return; out[key] = value; }); return out; } function isDangerousGeneratedLinkBookmarkIcon(icon) { const raw = normalizeLinkBookmarkText(icon).trim(); if (!raw || raw === '/favicon.ico' || raw.startsWith('/') || /^https?:\/\//i.test(raw) || /^\/\//.test(raw) || /^data:image\//i.test(raw)) return false; if (!/\/favicon\.ico(?:[?#].*)?$/i.test(raw)) return false; const first = raw.split(/[\/?#]/).filter(Boolean)[0] || ''; return !!first && /[=\s"'`<>{}\[\]();\\]/.test(first); } function normalizeLinkBookmarkIconForOfficial(icon) { const raw = normalizeLinkBookmarkText(icon).trim(); if (!raw) return ''; return isDangerousGeneratedLinkBookmarkIcon(raw) ? '/favicon.ico' : raw; } function sanitizeLinkBookmarkLinkForOfficial(link) { const extra = cloneLinkBookmarkOfficialFields(link); const out = { title: normalizeLinkBookmarkLimitedText(link && link.title, 'title'), href: normalizeLinkBookmarkLimitedText(link && link.href, 'href'), icon: normalizeLinkBookmarkIconForOfficial(link && link.icon), date: normalizeLinkBookmarkText(link && link.date) }; Object.keys(extra).forEach(key => { if (key === 'title' || key === 'href' || key === 'icon' || key === 'date') return; out[key] = extra[key]; }); return out; } function sanitizeLinkBookmarkFolderForOfficial(folder) { const extra = cloneLinkBookmarkOfficialFields(folder); const out = { folder: normalizeLinkBookmarkLimitedText(folder && folder.folder, 'folder'), list: [] }; const listSource = Array.isArray(folder && folder.list) ? folder.list : []; out.list = listSource .filter(link => link && typeof link === 'object' && !Array.isArray(link)) .map(link => sanitizeLinkBookmarkLinkForOfficial(link)); Object.keys(extra).forEach(key => { if (key === 'folder' || key === 'list') return; out[key] = extra[key]; }); return out; } function sanitizeLinkBookmarkFoldersForOfficial(source, useFallback = true) { if (!Array.isArray(source) || source.length === 0) return useFallback ? getDefaultLinkBookmarkFolders() : []; const folders = []; source.forEach(folder => { if (!folder || typeof folder !== 'object' || Array.isArray(folder)) return; folders.push(sanitizeLinkBookmarkFolderForOfficial(folder)); }); return folders.length ? folders : (useFallback ? getDefaultLinkBookmarkFolders() : []); } function normalizeOfficialLinkBookmarkFolders(source) { return sanitizeLinkBookmarkFoldersForOfficial(source, true); } function makeLinkBookmarkDataHash(folders) { return makeLinkBookmarkHash(JSON.stringify(sanitizeLinkBookmarkFoldersForOfficial(folders, true))); } function makeLinkBookmarkSaveVerifySnapshot(folders) { return sanitizeLinkBookmarkFoldersForOfficial(folders, true).map(folder => { const linkCounts = new Map(); const list = Array.isArray(folder && folder.list) ? folder.list : []; list.forEach(link => { const key = [ normalizeLinkBookmarkText(link && link.href), normalizeLinkBookmarkText(link && link.title) ].join('\n'); linkCounts.set(key, (linkCounts.get(key) || 0) + 1); }); return { folder: normalizeLinkBookmarkText(folder && folder.folder), count: list.length, links: Array.from(linkCounts.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([key, count]) => [key, count]) }; }).sort((a, b) => a.folder.localeCompare(b.folder)); } function makeLinkBookmarkSaveVerifyHash(folders) { return makeLinkBookmarkHash(JSON.stringify(makeLinkBookmarkSaveVerifySnapshot(folders))); } function isLinkBookmarkSaveSemanticallyVerified(expectedFolders, actualFolders) { return makeLinkBookmarkSaveVerifyHash(expectedFolders) === makeLinkBookmarkSaveVerifyHash(actualFolders); } function cloneLinkBookmarkFoldersForEditing(source) { return sanitizeLinkBookmarkFoldersForOfficial(source, true).map(folder => { const next = cloneLinkBookmarkOfficialFields(folder); next.folder = normalizeLinkBookmarkText(folder.folder); next.list = (Array.isArray(folder.list) ? folder.list : []).map(link => sanitizeLinkBookmarkLinkForOfficial(link)); return next; }); } function parseOfficialLinkBookmarkPayload(payload) { const setting = extractOfficialBookmarkSetting(payload); if (typeof setting === 'undefined' || setting === null) { return { folders: getDefaultLinkBookmarkFolders(), raw: '' }; } let parsed = setting; let raw = ''; if (typeof setting === 'string') { raw = setting.trim(); if (!raw) return { folders: getDefaultLinkBookmarkFolders(), raw: '' }; try { parsed = JSON.parse(raw); } catch(e) { throw makeLinkBookmarkInvalidError(); } } else { raw = JSON.stringify(setting); } if (parsed === null) return { folders: getDefaultLinkBookmarkFolders(), raw }; if (!Array.isArray(parsed)) throw makeLinkBookmarkInvalidError(); return { folders: normalizeOfficialLinkBookmarkFolders(parsed), raw }; } function formatLinkBookmarkDate(value) { const L = getStrings(); const raw = normalizeLinkBookmarkText(value).trim(); if (!raw) return L.label_link_bookmark_date_empty; let ts = NaN; if (/^\d{10,13}$/.test(raw)) { ts = Number(raw); if (raw.length === 10) ts *= 1000; } else { ts = Date.parse(raw); } if (!Number.isFinite(ts)) return L.label_link_bookmark_date_empty; const d = new Date(ts); const y = d.getFullYear(); if (!Number.isFinite(y) || y < 1970 || y > 9999) return L.label_link_bookmark_date_empty; const pad = n => String(n).padStart(2, '0'); return `${y}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } function getLinkBookmarkFolderDisplayName(folder, L) { const name = normalizeLinkBookmarkText(folder && folder.folder).trim(); if (name === 'My Links') return L.folder_link_bookmark_my_links; if (isMagnetArchiveBookmarkFolder(folder)) return getMagnetArchiveFolderName(L); return name || L.folder_link_bookmark_unnamed; } function getLinkBookmarkSyncLabel(status, L) { if (status === 'syncing') return L.label_link_bookmark_syncing; if (status === 'saving') return L.label_link_bookmark_saving; if (status === 'conflict') return L.label_link_bookmark_conflict; if (status === 'failed') return L.label_link_bookmark_sync_failed; return L.label_link_bookmark_synced; } function getLinkBookmarkFolderUniqueKey(folder) { return normalizeLinkBookmarkText(folder && folder.folder).trim(); } function getLinkBookmarkDuplicateFolderKeys(folders) { const seen = new Set(); const dup = new Set(); sanitizeLinkBookmarkFoldersForOfficial(folders, true).forEach(folder => { const key = getLinkBookmarkFolderUniqueKey(folder); if (seen.has(key)) dup.add(key); else seen.add(key); }); return dup; } function hasDuplicateLinkBookmarkFolders(folders) { return getLinkBookmarkDuplicateFolderKeys(folders).size > 0; } function showLinkBookmarkDuplicateFolderWriteBlocked(message) { const L = getStrings(); if (S.linkBookmarkMode) renderLinkBookmarkPanel(); showToast(message || L.msg_link_bookmark_duplicate_folder_write_blocked, 'warning'); } function isLinkBookmarkWriteBusy() { return S.linkBookmarkSyncStatus === 'saving'; } function getLinkBookmarkWriteBusyTip(L = getStrings()) { return L.label_link_bookmark_saving || L.loading_detail || ''; } function canPerformLinkBookmarkWriteNow() { const L = getStrings(); if (!isLinkBookmarkWriteBusy()) return true; showToast(getLinkBookmarkWriteBusyTip(L), 'info'); return false; } function canPerformLinkBookmarkOrdinaryWrite(message) { if (!canPerformLinkBookmarkWriteNow()) return false; if (!hasDuplicateLinkBookmarkFolders(S.linkBookmarkFolders)) return true; showLinkBookmarkDuplicateFolderWriteBlocked(message); return false; } function mergeDuplicateLinkBookmarkFolders(source) { const result = []; const firstIndexByKey = new Map(); const indexMap = []; sanitizeLinkBookmarkFoldersForOfficial(source, true).forEach((folder, sourceIndex) => { const next = sanitizeLinkBookmarkFolderForOfficial(folder); const key = getLinkBookmarkFolderUniqueKey(next); if (firstIndexByKey.has(key)) { const targetIndex = firstIndexByKey.get(key); const target = result[targetIndex]; if (!Array.isArray(target.list)) target.list = []; target.list.push(...(Array.isArray(next.list) ? next.list : [])); indexMap[sourceIndex] = targetIndex; return; } const resultIndex = result.length; firstIndexByKey.set(key, resultIndex); indexMap[sourceIndex] = resultIndex; result.push(next); }); return { folders: result.length ? result : getDefaultLinkBookmarkFolders(), indexMap }; } function makeLinkBookmarkVerifyFailedError(cause) { const e = new Error('LINK_BOOKMARK_VERIFY_FAILED'); e.linkBookmarkVerifyFailed = true; e.cause = cause; return e; } function parseLinkBookmarkOfficialResponseBody(text) { if (!text) return null; try { return JSON.parse(text); } catch(e) { return text; } } function extractLinkBookmarkOfficialMessage(error) { const data = error && error.response; const list = []; const add = value => { const text = normalizeLinkBookmarkText(value).trim(); if (text && !list.includes(text)) list.push(text); }; if (data && typeof data === 'object' && !Array.isArray(data)) { add(data.error_description); add(data.message); if (Array.isArray(data.details)) data.details.forEach(item => { if (item && typeof item === 'object') { add(item.error_description); add(item.message); add(item.detail); } }); add(data.detail); add(data.error); } else { add(data); } const msg = normalizeLinkBookmarkText(error && error.message).trim(); if (msg && !/^LINK_BOOKMARK_/i.test(msg) && msg !== 'AUTH_MISSING') add(msg); return list[0] || ''; } function getLinkBookmarkFailureMessage(error, fallback) { let cur = error; let guard = 0; while (cur && guard < 5) { const msg = extractLinkBookmarkOfficialMessage(cur); if (msg) return msg; cur = cur.cause; guard++; } return fallback; } async function fetchOfficialLinkBookmarkSnapshot(headers) { const res = await fetch(CONF.linkBookmarkSettingsUrl, { method: 'GET', headers, cache: 'no-store' }); if (typeof syncTime === 'function') syncTime(res.headers); if (!res.ok) { const bodyText = await res.text().catch(() => ''); const data = parseLinkBookmarkOfficialResponseBody(bodyText); if (res.status === 400 || res.status === 401 || res.status === 403) { localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); } const err = new Error(`LINK_BOOKMARK_HTTP_${res.status}`); err.response = data; throw err; } const payload = await res.json(); const parsed = parseOfficialLinkBookmarkPayload(payload); return { folders: parsed.folders, hash: makeLinkBookmarkDataHash(parsed.folders) }; } function buildOfficialLinkBookmarkSavePayload(folders) { assertLinkBookmarkOfficialFieldsSafe(folders); return { item: 'bookmark', value: JSON.stringify(sanitizeLinkBookmarkFoldersForOfficial(folders, true)) }; } function getLinkBookmarkSafeTotalBytes() { return Math.max(0, Number(CONF.linkBookmarkSafeTotalBytes) || 18 * 1024 * 1024); } function getLinkBookmarkConfigCloudReserveBytes() { return Math.max(0, Number(CONF.linkBookmarkConfigCloudReserveBytes) || 11 * 1024 * 1024); } function estimateLinkBookmarkFoldersBytes(folders) { return estimateTextSize(JSON.stringify(sanitizeLinkBookmarkFoldersForOfficial(folders, true))); } function isLinkBookmarkConfigCloudFolder(folder) { const name = normalizeLinkBookmarkText(folder && folder.folder).trim(); const list = Array.isArray(folder && folder.list) ? folder.list : []; return name === CONF.linkBookmarkSyncFolderName || list.some(link => isConfigCloudSyncPseudoLink(link)); } function formatLinkBookmarkCapacityMessage(template, size, limit) { return normalizeLinkBookmarkText(template).replace('{size}', fmtSize(size)).replace('{limit}', fmtSize(limit)); } function validateLinkBookmarkCapacityBudget(folders) { const L = getStrings(); const safeTotal = getLinkBookmarkSafeTotalBytes(); const configReserve = getLinkBookmarkConfigCloudReserveBytes(); const sanitized = sanitizeLinkBookmarkFoldersForOfficial(folders, true); const totalBytes = estimateLinkBookmarkFoldersBytes(sanitized); if (safeTotal > 0 && totalBytes > safeTotal) { return { ok: false, type: 'total', size: totalBytes, limit: safeTotal, message: formatLinkBookmarkCapacityMessage(L.msg_link_bookmark_total_over_limit, totalBytes, safeTotal) }; } const sharedLimit = Math.max(0, safeTotal - configReserve); if (sharedLimit > 0) { const sharedFolders = sanitized.filter(folder => !isLinkBookmarkConfigCloudFolder(folder)); const sharedBytes = estimateLinkBookmarkFoldersBytes(sharedFolders); if (sharedBytes > sharedLimit) { return { ok: false, type: 'shared', size: sharedBytes, limit: sharedLimit, message: formatLinkBookmarkCapacityMessage(L.msg_link_bookmark_shared_budget_over_limit, sharedBytes, sharedLimit) }; } } return { ok: true, type: 'ok', size: totalBytes, limit: safeTotal }; } async function postOfficialLinkBookmarkFolders(folders, headers) { const capacity = validateLinkBookmarkCapacityBudget(folders); if (!capacity.ok) { const err = new Error(capacity.message); err.linkBookmarkCapacityExceeded = true; err.capacity = capacity; throw err; } const res = await fetch(CONF.linkBookmarkSettingsSaveUrl, { method: 'POST', headers, body: JSON.stringify(buildOfficialLinkBookmarkSavePayload(folders)) }); if (typeof syncTime === 'function') syncTime(res.headers); const bodyText = await res.text().catch(() => ''); const data = parseLinkBookmarkOfficialResponseBody(bodyText); if (!res.ok) { if (res.status === 400 || res.status === 401 || res.status === 403) { localStorage.removeItem('pk_captured_captcha'); resetHeaderCache(); } const err = new Error(`LINK_BOOKMARK_SAVE_HTTP_${res.status}`); err.response = data; throw err; } return data; } async function fetchOfficialLinkBookmarkSaveVerification(headers, expectedFolders) { const delays = [0, 500, 1200]; let lastVerified = null; for (let i = 0; i < delays.length; i++) { if (delays[i] > 0) await sleep(delays[i]); const verified = await fetchOfficialLinkBookmarkSnapshot(headers); lastVerified = verified; if (verified && isLinkBookmarkSaveSemanticallyVerified(expectedFolders, verified.folders)) return verified; } return lastVerified; } function getClampedLinkBookmarkFolderIndex(index, folders) { const len = Array.isArray(folders) ? folders.length : 0; if (!len) return 0; const n = Number(index); return Number.isInteger(n) && n >= 0 && n < len ? n : 0; } async function saveLinkBookmarkFoldersWithConflictCheck(nextFolders, nextSelectedFolderIndex, options = {}) { const L = getStrings(); const isMergeRepair = !!(options && options.mergeRepair); const allowCurrentDuplicateFolders = !!(options && options.allowCurrentDuplicateFolders); const silentDuplicateFolderToast = !!(options && options.silentDuplicateFolderToast); if (isLinkBookmarkWriteBusy()) { showToast(getLinkBookmarkWriteBusyTip(L), 'info'); return false; } if (findLinkBookmarkOfficialFieldOverLimit(nextFolders) || findLinkBookmarkUnsafeLongPlainHref(nextFolders)) { showToast(L.msg_config_input_over_limit, 'warning'); return false; } const folders = sanitizeLinkBookmarkFoldersForOfficial(nextFolders, true); const capacity = validateLinkBookmarkCapacityBudget(folders); if (!capacity.ok) { showToast(capacity.message, 'warning'); return false; } const currentHasDuplicateFolders = !allowCurrentDuplicateFolders && hasDuplicateLinkBookmarkFolders(S.linkBookmarkFolders); if (!isMergeRepair && (currentHasDuplicateFolders || hasDuplicateLinkBookmarkFolders(folders))) { if (!silentDuplicateFolderToast) showLinkBookmarkDuplicateFolderWriteBlocked(L.msg_link_bookmark_duplicate_folder_write_blocked); return false; } const selectedIndex = getClampedLinkBookmarkFolderIndex(nextSelectedFolderIndex, folders); const expectedRemoteHash = S.linkBookmarkRemoteHash; const reqId = ++S.linkBookmarkSaveReqId; S.linkBookmarkFolders = folders; S.linkBookmarkSelectedFolderIndex = selectedIndex; S.linkBookmarkSyncStatus = 'saving'; S.linkBookmarkError = ''; if (S.linkBookmarkMode) renderLinkBookmarkPanel(); try { let headers = getHeaders(); if (!headers.Authorization || headers.Authorization.length < 10) { if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('link-bookmark-save-missing-token', 5000); const ready = await waitForAuth(8000); if (!ready) throw new Error('AUTH_MISSING'); headers = getHeaders(); } const authSig = getLinkBookmarkAuthSig(headers); if (authSig && S.linkBookmarkAuthSig && authSig !== S.linkBookmarkAuthSig) { resetLinkBookmarkMemoryForAccount(authSig); S.linkBookmarkSyncStatus = 'failed'; S.linkBookmarkError = L.msg_link_bookmark_save_failed_retry; if (S.linkBookmarkMode) renderLinkBookmarkPanel(); return false; } const latest = await fetchOfficialLinkBookmarkSnapshot(headers); if (reqId !== S.linkBookmarkSaveReqId) return false; if (!expectedRemoteHash || latest.hash !== expectedRemoteHash) { S.linkBookmarkSyncStatus = 'conflict'; S.linkBookmarkError = L.msg_link_bookmark_conflict; if (S.linkBookmarkMode) { renderLinkBookmarkPanel(); updateStat(); } showToast(L.msg_link_bookmark_conflict, 'warning'); return false; } await postOfficialLinkBookmarkFolders(folders, headers); if (reqId !== S.linkBookmarkSaveReqId) return false; let verified; try { verified = await fetchOfficialLinkBookmarkSaveVerification(headers, folders); } catch(verifyError) { throw makeLinkBookmarkVerifyFailedError(verifyError); } if (reqId !== S.linkBookmarkSaveReqId) return false; if (!verified || !isLinkBookmarkSaveSemanticallyVerified(folders, verified.folders)) { S.linkBookmarkSyncStatus = 'failed'; S.linkBookmarkError = L.msg_link_bookmark_save_verify_failed; if (S.linkBookmarkMode) { renderLinkBookmarkPanel(); updateStat(); } showToast(L.msg_link_bookmark_save_verify_failed, 'error'); return false; } S.linkBookmarkFolders = verified.folders; S.linkBookmarkSelectedFolderIndex = selectedIndex; S.linkBookmarkRemoteHash = verified.hash; S.linkBookmarkSyncStatus = 'synced'; S.linkBookmarkError = ''; S.linkBookmarkAuthSig = authSig || S.linkBookmarkAuthSig || ''; S.linkBookmarkHasLoaded = true; if (S.linkBookmarkMode) { renderLinkBookmarkPanel(); updateStat(); } return true; } catch(e) { if (reqId !== S.linkBookmarkSaveReqId) return false; S.linkBookmarkSyncStatus = 'failed'; S.linkBookmarkError = e && e.linkBookmarkVerifyFailed ? getLinkBookmarkFailureMessage(e, L.msg_link_bookmark_save_verify_failed) : (e && e.linkBookmarkInvalid ? L.msg_link_bookmark_data_invalid : getLinkBookmarkFailureMessage(e, L.msg_link_bookmark_save_failed_retry)); console.warn('[Link Bookmark] Official bookmark save failed:', e); if (S.linkBookmarkMode) { renderLinkBookmarkPanel(); updateStat(); } showToast(S.linkBookmarkError, 'error'); return false; } } async function loadLinkBookmarkOfficial(force = false) { const L = getStrings(); if (isLinkBookmarkWriteBusy()) { if (force) showToast(getLinkBookmarkWriteBusyTip(L), 'info'); return false; } let headers = getHeaders(); const initialAuthSig = getLinkBookmarkAuthSig(headers); if (initialAuthSig && S.linkBookmarkAuthSig && initialAuthSig !== S.linkBookmarkAuthSig) { resetLinkBookmarkMemoryForAccount(initialAuthSig); } if (force || !S.linkBookmarkHasLoaded) { ensureLinkBookmarkFolders(); } const reqId = ++S.linkBookmarkFetchReqId; S.linkBookmarkSyncStatus = 'syncing'; S.linkBookmarkError = ''; if (S.linkBookmarkMode) renderLinkBookmarkPanel(); try { if (!headers.Authorization || headers.Authorization.length < 10) { if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('link-bookmark-missing-token', 5000); const ready = await waitForAuth(8000); if (!ready) throw new Error('AUTH_MISSING'); headers = getHeaders(); } const authSig = getLinkBookmarkAuthSig(headers); if (authSig && S.linkBookmarkAuthSig && authSig !== S.linkBookmarkAuthSig) { resetLinkBookmarkMemoryForAccount(authSig); S.linkBookmarkSyncStatus = 'syncing'; if (S.linkBookmarkMode) renderLinkBookmarkPanel(); } const parsed = await fetchOfficialLinkBookmarkSnapshot(headers); if (reqId !== S.linkBookmarkFetchReqId) return; S.linkBookmarkFolders = parsed.folders; S.linkBookmarkSelectedFolderIndex = Math.min(Math.max(0, S.linkBookmarkSelectedFolderIndex || 0), S.linkBookmarkFolders.length - 1); S.linkBookmarkSyncStatus = 'synced'; S.linkBookmarkError = ''; S.linkBookmarkRemoteHash = parsed.hash; S.linkBookmarkAuthSig = authSig || initialAuthSig || S.linkBookmarkAuthSig || ''; S.linkBookmarkHasLoaded = true; } catch(e) { if (reqId !== S.linkBookmarkFetchReqId) return; S.linkBookmarkSyncStatus = 'failed'; S.linkBookmarkError = e && e.linkBookmarkInvalid ? L.msg_link_bookmark_data_invalid : L.msg_link_bookmark_read_failed_retry; console.warn('[Link Bookmark] Official bookmark read failed:', e); } finally { if (reqId !== S.linkBookmarkFetchReqId) return; if (S.linkBookmarkMode) { renderLinkBookmarkPanel(); updateStat(); } } } function isDefaultLinkBookmarkFolder(folder) { return normalizeLinkBookmarkText(folder && folder.folder).trim() === 'My Links'; } function isSyncLinkBookmarkFolder(folder) { return normalizeLinkBookmarkText(folder && folder.folder).trim() === CONF.linkBookmarkSyncFolderName; } function getConfigCloudSyncPseudoInfo(link) { const title = normalizeLinkBookmarkText(link && link.title).trim(); if (title.startsWith(CONF.configCloudManifestTitlePrefix)) { const syncId = title.slice(CONF.configCloudManifestTitlePrefix.length).trim(); return syncId ? { type: 'manifest', syncId, index: -1 } : null; } if (title.startsWith(CONF.configCloudChunkTitlePrefix)) { const rest = title.slice(CONF.configCloudChunkTitlePrefix.length).trim(); const pos = rest.lastIndexOf('-'); if (pos <= 0) return null; const syncId = rest.slice(0, pos); const index = parseInt(rest.slice(pos + 1), 10); if (!syncId || !Number.isInteger(index) || index < 0) return null; return { type: 'chunk', syncId, index }; } return null; } function isConfigCloudSyncPseudoLink(link) { return !!getConfigCloudSyncPseudoInfo(link); } function getLinkBookmarkVisibleLinks(folder) { return (Array.isArray(folder && folder.list) ? folder.list : []).filter(link => !isConfigCloudSyncPseudoLink(link) && !isMagnetArchivePseudoLink(link)); } function getLinkBookmarkVisibleLinkCount(folder) { return getLinkBookmarkVisibleLinks(folder).length; } function shouldHideLinkBookmarkFolderInOrdinaryView(folder) { return isSyncLinkBookmarkFolder(folder) && getLinkBookmarkVisibleLinkCount(folder) === 0; } function getConfigCloudManifestTitle(syncId) { return `${CONF.configCloudManifestTitlePrefix}${syncId}`; } function getConfigCloudChunkTitle(syncId, index) { return `${CONF.configCloudChunkTitlePrefix}${syncId}-${String(index).padStart(4, '0')}`; } function configCloudStringToBytes(text) { return new TextEncoder().encode(String(text || '')); } function configCloudBytesToBase64Url(bytes) { const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []); let binary = ''; for (let i = 0; i < arr.length; i += 0x8000) { const chunk = arr.subarray(i, i + 0x8000); binary += String.fromCharCode.apply(null, chunk); } return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function stripConfigCloudHrefPrefix(value, prefixes = []) { let s = String(value || '').trim(); prefixes.filter(Boolean).forEach(prefix => { if (s.startsWith(prefix)) s = s.slice(prefix.length); }); return s; } function configCloudBase64UrlToBytes(value, prefixes = []) { let s = stripConfigCloudHrefPrefix(value, prefixes); s = s.replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; const binary = atob(s); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes; } function configCloudBase64UrlToText(value) { const bytes = configCloudBase64UrlToBytes(value, [CONF.configCloudManifestHrefPrefix]); return new TextDecoder().decode(bytes); } async function hashConfigCloudEncodedPayload(encodedPayload, preferredAlgorithm = '') { const text = String(encodedPayload || ''); if ((!preferredAlgorithm || preferredAlgorithm === 'sha256') && typeof crypto !== 'undefined' && crypto && crypto.subtle && typeof crypto.subtle.digest === 'function') { const digest = await crypto.subtle.digest('SHA-256', configCloudStringToBytes(text)); const hex = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join(''); return { algorithm: 'sha256', hash: `sha256:${hex}` }; } if (preferredAlgorithm === 'sha256') throw new Error('CONFIG_CLOUD_HASH_UNAVAILABLE'); return { algorithm: 'fnv1a', hash: `fnv1a:${makeLinkBookmarkHash(text)}` }; } async function compressConfigCloudPayloadBytes(bytes) { if (typeof CompressionStream === 'function' && typeof Blob !== 'undefined' && typeof Response !== 'undefined') { try { const compressed = await new Response(new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip'))).arrayBuffer(); return { bytes: new Uint8Array(compressed), compression: 'gzip' }; } catch (e) {} } return { bytes, compression: 'none' }; } async function decompressConfigCloudPayloadBytes(bytes, compression) { const mode = String(compression || '').trim().toLowerCase(); if (mode === 'none' || mode === 'identity' || mode === 'raw') return { bytes, compression: 'none' }; if (mode === 'gzip') { if (typeof DecompressionStream !== 'function' || typeof Blob === 'undefined' || typeof Response === 'undefined') throw new Error('CONFIG_CLOUD_DECOMPRESS_UNAVAILABLE'); const decompressed = await new Response(new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))).arrayBuffer(); return { bytes: new Uint8Array(decompressed), compression: 'gzip' }; } throw new Error(mode ? `CONFIG_CLOUD_COMPRESSION_UNSUPPORTED_${mode}` : 'CONFIG_CLOUD_COMPRESSION_UNKNOWN'); } function sortConfigCloudObject(value) { if (Array.isArray(value)) return value.map(item => sortConfigCloudObject(item)); if (!value || typeof value !== 'object') return value; const out = {}; Object.keys(value).sort().forEach(key => { out[key] = sortConfigCloudObject(value[key]); }); return out; } async function hashConfigCloudLocalDefaultConfigs() { const collected = collectConfigCloudPackValues(); const stablePayload = { kind: 'pem-config-local-default-hash', schemaVersion: CONF.configCloudSchemaVersion, configCount: Object.keys(collected.configs || {}).length, configs: sortConfigCloudObject(collected.configs || {}) }; const encodedPayload = configCloudBytesToBase64Url(configCloudStringToBytes(JSON.stringify(stablePayload))); const hashed = await hashConfigCloudEncodedPayload(encodedPayload); return { hash: hashed.hash, algorithm: hashed.algorithm, configCount: stablePayload.configCount, encodedBytes: encodedPayload.length }; } function collectConfigCloudPackValues() { const configs = {}; const storedKeys = getStoredConfigKeys().filter(k => String(k || '').startsWith('pk_')); let skippedStrictCount = 0; storedKeys.forEach(key => { const schema = getConfigLimitSchema(key); if (!schema || schema.cloudSync !== 'default') skippedStrictCount++; }); CONFIG_LIMIT_SCHEMA.entries.forEach(entry => { if (!entry || entry.cloudSync !== 'default' || !entry.key) return; const raw = getStoredConfigRaw(entry.key); if (raw === undefined) return; const normalized = normalizeConfigValue(entry.key, raw, 'cloudPack'); if (normalized === undefined) return; configs[entry.key] = normalized; }); storedKeys.sort().forEach(key => { const schema = getConfigLimitSchema(key); if (!schema || !schema.isPrefix || schema.cloudSync !== 'default') return; const raw = getStoredConfigRaw(key); if (raw === undefined) return; const normalized = normalizeConfigValue(key, raw, 'cloudPack'); if (normalized === undefined || normalized === '') return; configs[key] = normalized; }); return { configs, localTotal: storedKeys.length, uploadedCount: Object.keys(configs).length, skippedStrictCount }; } function buildConfigCloudOversizeCategories(configs) { const source = configs && typeof configs === 'object' && !Array.isArray(configs) ? configs : {}; return Object.keys(source).map(key => { let json = ''; try { json = JSON.stringify({ [key]: source[key] }); } catch (e) { json = String(source[key] ?? ''); } return { key, jsonChars: json.length }; }).sort((a, b) => b.jsonChars - a.jsonChars).slice(0, 10); } async function buildConfigCloudPayloadPackage() { const now = Date.now(); const timestamp = new Date(now).toISOString(); const collected = collectConfigCloudPackValues(); const syncId = `cfg_${now.toString(36)}_${Math.random().toString(36).slice(2, 10)}`; const payload = { kind: 'PikPak Enhancement Master Config Cloud Payload', schemaVersion: CONF.configCloudSchemaVersion, scriptVersion: getScriptVersion(), createdAt: timestamp, updatedAt: timestamp, configCount: collected.uploadedCount, configs: collected.configs }; const rawJson = JSON.stringify(payload); const rawBytes = configCloudStringToBytes(rawJson); const compressed = await compressConfigCloudPayloadBytes(rawBytes); const payloadBytes = compressed.bytes.byteLength; const encodedPayload = configCloudBytesToBase64Url(compressed.bytes); const hash = await hashConfigCloudEncodedPayload(encodedPayload); const chunkSize = CONF.configCloudChunkSize; const chunks = []; for (let i = 0; i < encodedPayload.length; i += chunkSize) chunks.push(encodedPayload.slice(i, i + chunkSize)); const estimatedChunkCount = chunks.length || 1; const manifest = { kind: 'PikPak Enhancement Master Config Cloud Manifest', schemaVersion: CONF.configCloudSchemaVersion, syncId, createdAt: payload.createdAt, updatedAt: payload.updatedAt, scriptVersion: payload.scriptVersion, folder: CONF.linkBookmarkSyncFolderName, payloadEncoding: 'base64url', compression: compressed.compression, hashAlgorithm: hash.algorithm, payloadHash: hash.hash, payloadBytes, rawJsonBytes: rawBytes.byteLength, compressedBytes: compressed.bytes.byteLength, encodedBytes: encodedPayload.length, configCount: payload.configCount, chunkSize, chunkCount: estimatedChunkCount, chunks: chunks.map((_, index) => ({ index, title: getConfigCloudChunkTitle(syncId, index) })) }; return { syncId, payload, manifest, encodedPayload, chunks, tooManyChunks: estimatedChunkCount > CONF.configCloudMaxChunks, localTotal: collected.localTotal, uploadedCount: collected.uploadedCount, skippedStrictCount: collected.skippedStrictCount, oversizeCategories: buildConfigCloudOversizeCategories(collected.configs) }; } function makeConfigCloudManifestHref(manifest) { return CONF.configCloudManifestHrefPrefix + configCloudBytesToBase64Url(configCloudStringToBytes(JSON.stringify(manifest))); } function parseConfigCloudManifestHref(href) { const text = configCloudBase64UrlToText(href); const data = JSON.parse(text); return data && typeof data === 'object' && !Array.isArray(data) ? data : null; } function ensureConfigCloudSyncFolderForWrite(folders) { const list = sanitizeLinkBookmarkFoldersForOfficial(folders, true); let index = list.findIndex(folder => isSyncLinkBookmarkFolder(folder)); if (index < 0) { list.push({ folder: CONF.linkBookmarkSyncFolderName, list: [] }); index = list.length - 1; } if (!Array.isArray(list[index].list)) list[index].list = []; return { folders: list, index }; } function appendConfigCloudPackageToFolders(folders, pack) { const prepared = ensureConfigCloudSyncFolderForWrite(folders); const target = prepared.folders[prepared.index]; const now = new Date().toISOString(); target.list.push({ title: getConfigCloudManifestTitle(pack.syncId), href: makeConfigCloudManifestHref(pack.manifest), icon: '', date: now }); pack.chunks.forEach((chunk, index) => { target.list.push({ title: getConfigCloudChunkTitle(pack.syncId, index), href: chunk, icon: '', date: now }); }); return prepared.folders; } function removeConfigCloudSyncPseudoLinksFromFolders(folders, options = {}) { const keepSyncId = options.keepSyncId || ''; let removedManifestCount = 0; let removedChunkCount = 0; const next = sanitizeLinkBookmarkFoldersForOfficial(folders, true); next.forEach(folder => { if (!isSyncLinkBookmarkFolder(folder)) return; const source = Array.isArray(folder.list) ? folder.list : []; folder.list = source.filter(link => { const info = getConfigCloudSyncPseudoInfo(link); if (!info) return true; if (keepSyncId && info.syncId === keepSyncId) return true; if (info.type === 'manifest') removedManifestCount++; if (info.type === 'chunk') removedChunkCount++; return false; }); }); return { folders: next, removedManifestCount, removedChunkCount }; } function collectConfigCloudSyncRecords(folders) { const manifests = []; const chunks = []; sanitizeLinkBookmarkFoldersForOfficial(folders, true).forEach((folder, folderIndex) => { if (!isSyncLinkBookmarkFolder(folder)) return; const list = Array.isArray(folder.list) ? folder.list : []; list.forEach((link, linkIndex) => { const info = getConfigCloudSyncPseudoInfo(link); if (!info) return; const rec = { ...info, folderIndex, folder: normalizeLinkBookmarkText(folder.folder), linkIndex, title: normalizeLinkBookmarkText(link.title), href: normalizeLinkBookmarkText(link.href) }; if (info.type === 'manifest') manifests.push(rec); else chunks.push(rec); }); }); return { manifests, chunks }; } async function verifyConfigCloudSyncWrite(headers, syncId, expectedManifest) { const snapshot = await fetchOfficialLinkBookmarkSnapshot(headers); const records = collectConfigCloudSyncRecords(snapshot.folders); const expectedManifestTitle = getConfigCloudManifestTitle(syncId); const manifestRec = records.manifests.find(rec => rec.syncId === syncId && rec.title === expectedManifestTitle && rec.folder === CONF.linkBookmarkSyncFolderName); if (!manifestRec) throw new Error('CONFIG_CLOUD_VERIFY_MANIFEST_MISSING'); const manifest = parseConfigCloudManifestHref(manifestRec.href); if (!manifest || manifest.syncId !== syncId || manifest.schemaVersion !== CONF.configCloudSchemaVersion) throw new Error('CONFIG_CLOUD_VERIFY_MANIFEST_INVALID'); if (manifest.folder !== CONF.linkBookmarkSyncFolderName || manifest.chunkCount !== expectedManifest.chunkCount || manifest.payloadHash !== expectedManifest.payloadHash) throw new Error('CONFIG_CLOUD_VERIFY_MANIFEST_MISMATCH'); const chunkMap = new Map(); records.chunks.filter(rec => rec.syncId === syncId && rec.folderIndex === manifestRec.folderIndex).forEach(rec => { if (!chunkMap.has(rec.index)) chunkMap.set(rec.index, rec); }); if (chunkMap.size !== manifest.chunkCount) throw new Error('CONFIG_CLOUD_VERIFY_CHUNK_COUNT'); const parts = []; for (let i = 0; i < manifest.chunkCount; i++) { const rec = chunkMap.get(i); if (!rec) throw new Error('CONFIG_CLOUD_VERIFY_CHUNK_GAP'); if (rec.title !== getConfigCloudChunkTitle(syncId, i) || !rec.href) throw new Error('CONFIG_CLOUD_VERIFY_CHUNK_INVALID'); parts.push(rec.href); } const encodedPayload = parts.join(''); const verifiedHash = await hashConfigCloudEncodedPayload(encodedPayload, manifest.hashAlgorithm); if (verifiedHash.hash !== manifest.payloadHash) throw new Error('CONFIG_CLOUD_VERIFY_HASH_MISMATCH'); return { folders: snapshot.folders, manifest, encodedPayload }; } function isConfigCloudManifestKindAccepted(kind) { const value = String(kind || '').trim(); return value === 'pem-config-sync-manifest' || value === 'PikPak Enhancement Master Config Cloud Manifest'; } function getConfigCloudHashAlgorithm(manifest) { const explicit = String(manifest && manifest.hashAlgorithm || '').trim(); if (explicit) return explicit; const hash = String(manifest && manifest.payloadHash || ''); const pos = hash.indexOf(':'); return pos > 0 ? hash.slice(0, pos) : ''; } function getConfigCloudManifestTime(manifest) { const time = parseConfigTime(manifest && (manifest.updatedAt || manifest.createdAt)); return Number.isFinite(time) ? time : 0; } function validateConfigCloudManifestForPull(manifest, rec) { const reasons = []; if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) reasons.push('manifest_not_object'); if (!reasons.length && !isConfigCloudManifestKindAccepted(manifest.kind)) reasons.push('manifest_kind_incompatible'); if (!reasons.length && (typeof manifest.schemaVersion !== 'number' || manifest.schemaVersion !== CONF.configCloudSchemaVersion)) reasons.push('schema_incompatible'); if (!reasons.length && !normalizeLinkBookmarkText(manifest.syncId).trim()) reasons.push('sync_id_missing'); if (!reasons.length && rec && manifest.syncId !== rec.syncId) reasons.push('sync_id_mismatch'); if (!reasons.length && (!Number.isInteger(Number(manifest.chunkCount)) || Number(manifest.chunkCount) <= 0)) reasons.push('chunk_count_invalid'); if (!reasons.length && (!Number.isInteger(Number(manifest.chunkSize)) || Number(manifest.chunkSize) <= 0)) reasons.push('chunk_size_invalid'); if (!reasons.length && !normalizeLinkBookmarkText(manifest.payloadHash).trim()) reasons.push('payload_hash_missing'); if (!reasons.length && manifest.payloadEncoding && String(manifest.payloadEncoding).trim().toLowerCase() !== 'base64url') reasons.push('payload_encoding_unsupported'); return { ok: reasons.length === 0, reason: reasons[0] || '', time: getConfigCloudManifestTime(manifest), scriptVersion: normalizeLinkBookmarkText(manifest && manifest.scriptVersion).trim(), scriptCompatible: !manifest || !manifest.scriptVersion || compareScriptVersion(manifest.scriptVersion, getScriptVersion()) <= 0 }; } function selectLatestConfigCloudManifest(candidates) { const list = Array.isArray(candidates) ? candidates.filter(item => item && item.validation && item.validation.ok) : []; if (!list.length) throw new Error('CONFIG_CLOUD_NO_COMPATIBLE_MANIFEST'); const maxTime = Math.max(...list.map(item => item.validation.time || 0)); let winners = list.filter(item => (item.validation.time || 0) === maxTime); if (winners.length === 1) return winners[0]; const compatibleScript = winners.filter(item => item.validation.scriptCompatible); if (!compatibleScript.length) { const err = new Error('CONFIG_CLOUD_MANIFEST_AMBIGUOUS'); err.configCloudAmbiguous = true; throw err; } winners = compatibleScript; let bestVersion = ''; winners.forEach(item => { const version = item.validation.scriptVersion || ''; if (!bestVersion || compareScriptVersion(version, bestVersion) > 0) bestVersion = version; }); winners = winners.filter(item => compareScriptVersion(item.validation.scriptVersion || '', bestVersion) === 0); if (winners.length === 1) return winners[0]; const err = new Error('CONFIG_CLOUD_MANIFEST_AMBIGUOUS'); err.configCloudAmbiguous = true; throw err; } async function readConfigCloudLatestManifestFromOfficial(headers) { const snapshot = await fetchOfficialLinkBookmarkSnapshot(headers); const folders = sanitizeLinkBookmarkFoldersForOfficial(snapshot.folders, true); const hasSyncFolder = folders.some(folder => isSyncLinkBookmarkFolder(folder)); if (!hasSyncFolder) { const err = new Error('CONFIG_CLOUD_SYNC_FOLDER_MISSING'); err.stage = 'find_folder'; throw err; } const records = collectConfigCloudSyncRecords(folders); if (!records.manifests.length) { const err = new Error('CONFIG_CLOUD_MANIFEST_MISSING'); err.stage = 'read_manifest'; throw err; } const parsedManifests = []; const skippedManifests = []; records.manifests.forEach(rec => { try { const manifest = parseConfigCloudManifestHref(rec.href); const validation = validateConfigCloudManifestForPull(manifest, rec); if (validation.ok) parsedManifests.push({ rec, manifest, validation }); else skippedManifests.push({ title: rec.title, syncId: rec.syncId, reason: validation.reason || 'manifest_incompatible' }); } catch (e) { skippedManifests.push({ title: rec.title, syncId: rec.syncId, reason: e && e.message ? e.message : 'manifest_parse_failed' }); } }); if (!parsedManifests.length) { const err = new Error('CONFIG_CLOUD_NO_COMPATIBLE_MANIFEST'); err.stage = 'select_manifest'; err.skippedManifests = skippedManifests; throw err; } const selected = selectLatestConfigCloudManifest(parsedManifests); return { snapshot, records, selected, skippedManifests }; } function getConfigCloudManifestChunkOrder(manifest) { const count = Number(manifest && manifest.chunkCount) || 0; let order = []; if (Array.isArray(manifest && manifest.chunks) && manifest.chunks.length) { order = manifest.chunks.map(item => Number(item && item.index)); } else { for (let i = 0; i < count; i++) order.push(i); } if (order.length !== count) throw new Error('CONFIG_CLOUD_CHUNK_ORDER_MISMATCH'); const sorted = order.slice().sort((a, b) => a - b); const seen = new Set(); sorted.forEach((index, pos) => { if (!Number.isInteger(index) || index < 0) throw new Error('CONFIG_CLOUD_CHUNK_INDEX_INVALID'); if (seen.has(index)) throw new Error('CONFIG_CLOUD_CHUNK_INDEX_DUPLICATE'); seen.add(index); if (pos > 0 && index !== sorted[pos - 1] + 1) throw new Error('CONFIG_CLOUD_CHUNK_INDEX_GAP'); }); return order; } async function readConfigCloudRemotePackage(headers) { const remote = await readConfigCloudLatestManifestFromOfficial(headers); const manifest = remote.selected.manifest; const manifestRec = remote.selected.rec; const order = getConfigCloudManifestChunkOrder(manifest); const chunkMap = new Map(); const duplicates = []; remote.records.chunks .filter(rec => rec.syncId === manifest.syncId && rec.folderIndex === manifestRec.folderIndex) .forEach(rec => { if (chunkMap.has(rec.index)) duplicates.push(rec.index); else chunkMap.set(rec.index, rec); }); if (duplicates.length) throw new Error('CONFIG_CLOUD_CHUNK_DUPLICATE'); if (chunkMap.size !== Number(manifest.chunkCount)) throw new Error('CONFIG_CLOUD_CHUNK_COUNT_MISMATCH'); const parts = []; order.forEach(index => { const rec = chunkMap.get(index); if (!rec) throw new Error('CONFIG_CLOUD_CHUNK_MISSING'); if (rec.title !== getConfigCloudChunkTitle(manifest.syncId, index)) throw new Error('CONFIG_CLOUD_CHUNK_TITLE_MISMATCH'); if (!rec.href) throw new Error('CONFIG_CLOUD_CHUNK_EMPTY'); parts.push(rec.href); }); const encodedPayload = parts.join(''); const verifiedHash = await hashConfigCloudEncodedPayload(encodedPayload, getConfigCloudHashAlgorithm(manifest)); if (verifiedHash.hash !== manifest.payloadHash) throw new Error('CONFIG_CLOUD_PAYLOAD_HASH_MISMATCH'); const decoded = await decodeConfigCloudPayload(encodedPayload, manifest); return { ...remote, manifest, manifestRec, encodedPayload, hashAlgorithm: verifiedHash.algorithm, payload: decoded.payload, payloadTextBytes: decoded.textBytes, payloadJsonBytes: decoded.jsonBytes, decodedBytes: decoded.decodedBytes, decompressedBytes: decoded.decompressedBytes, compression: decoded.compression }; } async function decodeConfigCloudPayload(encodedPayload, manifest) { const encodedForDecode = stripConfigCloudHrefPrefix(encodedPayload, ['pem-config-payload:', 'pem-config-sync-payload:']); const payloadBytes = configCloudBase64UrlToBytes(encodedForDecode); const compression = manifest && (manifest.compression || manifest.payloadCompression || manifest.payload_compression); const decompressed = await decompressConfigCloudPayloadBytes(payloadBytes, compression); const text = new TextDecoder().decode(decompressed.bytes); let payload; try { payload = JSON.parse(text); } catch (e) { throw new Error('CONFIG_CLOUD_PAYLOAD_JSON_INVALID'); } if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('CONFIG_CLOUD_PAYLOAD_NOT_OBJECT'); if (typeof payload.schemaVersion !== 'number' || payload.schemaVersion !== CONF.configCloudSchemaVersion) throw new Error('CONFIG_CLOUD_PAYLOAD_SCHEMA_INCOMPATIBLE'); if (!Object.prototype.hasOwnProperty.call(payload, 'scriptVersion')) throw new Error('CONFIG_CLOUD_PAYLOAD_SCRIPT_VERSION_MISSING'); if (!Object.prototype.hasOwnProperty.call(payload, 'createdAt')) throw new Error('CONFIG_CLOUD_PAYLOAD_CREATED_AT_MISSING'); if (!Object.prototype.hasOwnProperty.call(payload, 'updatedAt')) throw new Error('CONFIG_CLOUD_PAYLOAD_UPDATED_AT_MISSING'); if (!Number.isInteger(Number(payload.configCount)) || Number(payload.configCount) < 0) throw new Error('CONFIG_CLOUD_PAYLOAD_CONFIG_COUNT_INVALID'); if (!payload.configs || typeof payload.configs !== 'object' || Array.isArray(payload.configs)) throw new Error('CONFIG_CLOUD_PAYLOAD_CONFIGS_INVALID'); if (Object.keys(payload.configs).length !== Number(payload.configCount)) throw new Error('CONFIG_CLOUD_PAYLOAD_CONFIG_COUNT_MISMATCH'); return { payload, decodedBytes: payloadBytes.byteLength, decompressedBytes: decompressed.bytes.byteLength, textBytes: estimateTextSize(text), jsonBytes: estimateTextSize(JSON.stringify(payload)), compression: decompressed.compression }; } function normalizeConfigCloudIncomingConfigs(configs) { const allowed = {}; const discardedStrictLocal = []; const invalid = []; Object.entries(configs && typeof configs === 'object' ? configs : {}).forEach(([key, value]) => { const schema = getConfigLimitSchema(key); if (!schema || schema.cloudSync !== 'default') { discardedStrictLocal.push({ key, reason: schema ? schema.cloudSync || 'strict-local' : 'unknown_key' }); return; } let normalized; try { normalized = normalizeConfigValue(key, value, 'cloudMerge'); } catch (e) { invalid.push({ key, reason: e && e.message ? e.message : 'normalize_failed' }); return; } if (normalized === undefined || (typeof normalized === 'number' && !Number.isFinite(normalized))) { invalid.push({ key, reason: 'normalized_invalid' }); return; } if (schema.type === 'archivePwd' && normalized === '') { invalid.push({ key, reason: 'empty_archive_password' }); return; } allowed[key] = normalized; }); return { allowed, discardedStrictLocal, invalid }; } function getConfigCloudDefaultValue(key) { const schema = getConfigLimitSchema(key); if (!schema) return ''; return normalizeConfigValue(key, schema.defaultValue, 'localWrite'); } function getConfigCloudLocalValue(key) { const raw = getStoredConfigRaw(key); return normalizeConfigValue(key, raw === undefined ? getConfigCloudDefaultValue(key) : raw, 'localWrite'); } function isConfigCloudLocalDefault(key) { return configValueEquals(getConfigCloudLocalValue(key), getConfigCloudDefaultValue(key)); } function parseConfigCloudJsonValue(value, fallback) { const data = parseConfigJson(value, fallback); return data === null || typeof data === 'undefined' ? fallback : data; } function getConfigCloudItemCount(key, value) { const schema = getConfigLimitSchema(key); if (!schema) return 0; if (schema.type === 'lineList') return normalizeConfigValue(key, value, 'cloudMerge').split(/\r?\n/).filter(Boolean).length; if (schema.type === 'arrayList') return normalizeArrayList(value, schema.localLimit || {}).length; if (schema.type === 'jsonMap' || schema.type === 'shareLimits') { const obj = parseConfigCloudJsonValue(normalizeConfigValue(key, value, 'cloudMerge'), {}); return obj && typeof obj === 'object' && !Array.isArray(obj) ? Object.keys(obj).length : 0; } if (schema.type === 'jsonArray' || schema.type === 'shareParseHistory' || schema.type === 'pwdVault') { const list = parseConfigCloudJsonValue(normalizeConfigValue(key, value, 'cloudMerge'), []); return Array.isArray(list) ? list.length : 0; } return value === undefined || value === null || value === '' ? 0 : 1; } function makeConfigCloudMergePreview(remotePackage) { const payload = remotePackage.payload || {}; const incoming = normalizeConfigCloudIncomingConfigs(payload.configs || {}); const preview = { operation: 'pull', status: 'preview', time: new Date().toISOString(), manifest: remotePackage.manifest, payload, encodedPayload: remotePackage.encodedPayload, hashAlgorithm: remotePackage.hashAlgorithm, selectedSyncId: remotePackage.manifest.syncId, skippedManifests: remotePackage.skippedManifests || [], discardedStrictLocal: incoming.discardedStrictLocal, invalid: incoming.invalid, actions: [], skipped: [], conflicts: [], clipped: [], stats: { cloudTotal: Object.keys(payload.configs || {}).length, allowedCount: Object.keys(incoming.allowed).length, applyCount: 0, mergeCount: 0, skippedCount: incoming.invalid.length, conflictCount: 0, clippedCount: 0, discardedStrictCount: incoming.discardedStrictLocal.length, unchangedCount: 0 } }; const used = new Set(); const cloud = incoming.allowed; const addAction = (key, value, mode, reason, detail = {}) => { preview.actions.push({ key, value, mode, reason, detail }); if (mode === 'merge') preview.stats.mergeCount++; else preview.stats.applyCount++; }; const addSkip = (key, reason, detail = {}, conflict = false) => { preview.skipped.push({ key, reason, detail }); preview.stats.skippedCount++; if (conflict) { preview.conflicts.push({ key, reason, detail }); preview.stats.conflictCount++; } }; const addClip = (key, count, reason) => { const n = Math.max(0, Number(count) || 0); if (!n) return; preview.clipped.push({ key, count: n, reason }); preview.stats.clippedCount += n; }; const processLocalDefaultOnly = key => { used.add(key); const localValue = getConfigCloudLocalValue(key); const cloudValue = cloud[key]; if (configValueEquals(localValue, cloudValue)) { preview.stats.unchangedCount++; return; } if (isConfigCloudLocalDefault(key)) addAction(key, cloudValue, 'apply', 'localDefaultOnly'); else addSkip(key, 'skippedLocalCustom', {}, true); }; [ ['downloadAccel', ['pk_download_accel_enable', 'pk_download_accel_domain', 'pk_download_accel_mode', 'pk_download_accel_query_param']], ['downloadFilterSize', ['pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit']] ].forEach(([group, keys]) => { const present = keys.filter(key => Object.prototype.hasOwnProperty.call(cloud, key)); if (!present.length) return; keys.forEach(key => used.add(key)); if (present.length !== keys.length) { present.forEach(key => addSkip(key, 'groupInvalid', { group })); return; } const localAllDefault = keys.every(key => isConfigCloudLocalDefault(key)); if (!localAllDefault) { keys.forEach(key => addSkip(key, 'groupSkippedLocalCustom', { group }, true)); return; } keys.forEach(key => { const localValue = getConfigCloudLocalValue(key); if (configValueEquals(localValue, cloud[key])) preview.stats.unchangedCount++; else addAction(key, cloud[key], 'apply', 'groupApplied', { group }); }); }); Object.keys(cloud).sort().forEach(key => { if (used.has(key)) return; const schema = getConfigLimitSchema(key); if (!schema || schema.cloudSync !== 'default') return; used.add(key); const strategy = schema.mergeStrategy || 'localDefaultOnly'; if (strategy === 'localDefaultOnly' || strategy === 'groupLocalDefaultOnly') { processLocalDefaultOnly(key); return; } if (strategy === 'fillOnly') { if (getStoredConfigRaw(key) !== undefined) { addSkip(key, 'fillOnlyLocalExists'); return; } if (configValueEquals(cloud[key], getConfigCloudDefaultValue(key))) { preview.stats.unchangedCount++; return; } addAction(key, cloud[key], 'apply', 'fillOnly'); return; } const localValue = getConfigCloudLocalValue(key); let nextValue = localValue; let beforeCount = getConfigCloudItemCount(key, localValue); let rawUnionCount = beforeCount; if (strategy === 'mapLocalFirst') nextValue = mergeConfigCloudMapLocalFirst(key, localValue, cloud[key]); else if (strategy === 'dedupeUnion') { const merged = mergeConfigCloudListUnion(key, localValue, cloud[key]); nextValue = merged.value; rawUnionCount = merged.rawUnionCount; } else if (strategy === 'dedupeByShare') { const merged = mergeConfigCloudExpiredShares(localValue, cloud[key]); nextValue = merged.value; rawUnionCount = merged.rawUnionCount; } else if (strategy === 'shareHistoryLocalFirst') { const merged = mergeConfigCloudShareHistory(localValue, cloud[key]); nextValue = merged.value; rawUnionCount = merged.rawUnionCount; } else if (strategy === 'dedupeByPassword') { const merged = mergeConfigCloudPwdVault(localValue, cloud[key]); nextValue = merged.value; rawUnionCount = merged.rawUnionCount; } else { processLocalDefaultOnly(key); return; } const afterCount = getConfigCloudItemCount(key, nextValue); addClip(key, Math.max(0, rawUnionCount - afterCount), 'limitTrimmed'); if (configValueEquals(localValue, nextValue)) { preview.stats.unchangedCount++; return; } addAction(key, nextValue, 'merge', strategy, { beforeCount, afterCount }); }); return preview; } function mergeConfigCloudMapLocalFirst(key, localValue, cloudValue) { const local = parseConfigCloudJsonValue(localValue, {}); const remote = parseConfigCloudJsonValue(cloudValue, {}); const out = { ...(remote && typeof remote === 'object' && !Array.isArray(remote) ? remote : {}) }; Object.keys(out).forEach(k => delete out[k]); Object.assign(out, local && typeof local === 'object' && !Array.isArray(local) ? local : {}); Object.keys(remote && typeof remote === 'object' && !Array.isArray(remote) ? remote : {}).forEach(k => { if (!Object.prototype.hasOwnProperty.call(out, k)) out[k] = remote[k]; }); return normalizeConfigValue(key, JSON.stringify(out), 'cloudMerge'); } function mergeConfigCloudListUnion(key, localValue, cloudValue) { const schema = getConfigLimitSchema(key); const toItems = value => schema.type === 'lineList' ? normalizeConfigValue(key, value, 'cloudMerge').split(/\r?\n/).filter(Boolean) : normalizeArrayList(value, schema.localLimit || {}); const localItems = toItems(localValue); const cloudItems = toItems(cloudValue); const seen = new Set(); const out = []; localItems.concat(cloudItems).forEach(item => { const dedupeKey = schema.type === 'arrayList' && (schema.localLimit || {}).lowercase ? String(item).toLowerCase() : String(item); if (seen.has(dedupeKey)) return; seen.add(dedupeKey); out.push(item); }); const input = schema.type === 'lineList' ? out.join('\n') : out.join(', '); return { value: normalizeConfigValue(key, input, 'cloudMerge'), rawUnionCount: out.length }; } function getConfigCloudShareKey(item) { if (!item || typeof item !== 'object') return ''; return safeStringLimit(item.share_id || item.shareId || item.id || item.url || item.link || item.raw || '', 512); } function mergeConfigCloudExpiredShares(localValue, cloudValue) { const local = parseConfigCloudJsonValue(normalizeConfigValue('pk_expired_shares', localValue, 'cloudMerge'), []); const remote = parseConfigCloudJsonValue(normalizeConfigValue('pk_expired_shares', cloudValue, 'cloudMerge'), []); const map = new Map(); (Array.isArray(local) ? local : []).forEach(item => { const key = getConfigCloudShareKey(item); if (key && !map.has(key)) map.set(key, item); }); (Array.isArray(remote) ? remote : []).forEach(item => { const key = getConfigCloudShareKey(item); if (key && !map.has(key)) map.set(key, item); }); const list = Array.from(map.values()); return { value: normalizeConfigValue('pk_expired_shares', JSON.stringify(list), 'cloudMerge'), rawUnionCount: list.length }; } function mergeConfigCloudShareHistory(localValue, cloudValue) { const local = parseConfigCloudJsonValue(normalizeConfigValue('pk_share_parse_history', localValue, 'cloudMerge'), []); const remote = parseConfigCloudJsonValue(normalizeConfigValue('pk_share_parse_history', cloudValue, 'cloudMerge'), []); const map = new Map(); const keyOf = rec => safeStringLimit(rec && (rec.share_id || rec.shareId || rec.key || rec.raw), 256); (Array.isArray(local) ? local : []).forEach(item => { const key = keyOf(item); if (key && !map.has(key)) map.set(key, item); }); (Array.isArray(remote) ? remote : []).forEach(item => { const key = keyOf(item); if (!key) return; if (!map.has(key)) { map.set(key, item); return; } const old = map.get(key); const merged = { ...item, ...old }; ['first_success_at', 'last_success_at', 'last_open_at', 'last_check_at', 'last_error_at'].forEach(field => { merged[field] = Math.max(parseConfigTime(old[field]), parseConfigTime(item[field])); }); ['success_count', 'root_file_count', 'root_folder_count', 'root_total_count'].forEach(field => { merged[field] = Math.max(Number(old[field]) || 0, Number(item[field]) || 0); }); merged.note = old.note || ''; merged.pinned = safeBoolean(old.pinned, false); map.set(key, merged); }); const list = Array.from(map.values()); return { value: normalizeConfigValue('pk_share_parse_history', JSON.stringify(list), 'cloudMerge'), rawUnionCount: list.length }; } function mergeConfigCloudPwdVault(localValue, cloudValue) { const local = parseConfigCloudJsonValue(normalizeConfigValue('pk_pwd_vault', localValue, 'cloudMerge'), []); const remote = parseConfigCloudJsonValue(normalizeConfigValue('pk_pwd_vault', cloudValue, 'cloudMerge'), []); const map = new Map(); const add = item => { const pass = safeStringLimit(item && typeof item === 'object' ? (item.p || item.password || item.value) : item, 512); if (!pass) return; const hit = safeNumberClamp(item && typeof item === 'object' ? item.h : 0, 0, Number.MAX_SAFE_INTEGER, 0, true); const old = map.get(pass); map.set(pass, { p: pass, h: old ? Math.max(old.h, hit) : hit }); }; (Array.isArray(local) ? local : []).forEach(add); (Array.isArray(remote) ? remote : []).forEach(add); const list = Array.from(map.values()); return { value: normalizeConfigValue('pk_pwd_vault', JSON.stringify(list), 'cloudMerge'), rawUnionCount: list.length }; } function makeConfigCloudPullReport(preview, status, extra = {}) { const manifest = preview && preview.manifest ? preview.manifest : {}; const stats = preview && preview.stats ? preview.stats : {}; const payload = preview && preview.payload ? preview.payload : {}; return makeConfigCloudBaseReport('pull', status, { writtenRemote: false, keepBookmarks: true, keepLocalConfig: true, writtenLocal: status === 'success', updatedAt: manifest.updatedAt || payload.updatedAt || '', scriptVersion: manifest.scriptVersion || payload.scriptVersion || '', schemaVersion: manifest.schemaVersion || payload.schemaVersion || '', configCount: manifest.configCount ?? payload.configCount ?? 0, payloadBytes: manifest.payloadBytes || 0, rawJsonBytes: manifest.rawJsonBytes || (preview ? preview.payloadJsonBytes || 0 : 0), compressedBytes: manifest.compressedBytes || 0, encodedBytes: manifest.encodedBytes || (preview && preview.encodedPayload ? preview.encodedPayload.length : 0), chunkSize: manifest.chunkSize || CONF.configCloudChunkSize, chunkCount: manifest.chunkCount || 0, payloadHash: manifest.payloadHash || '', hashAlgorithm: preview && preview.hashAlgorithm || manifest.hashAlgorithm || '', syncId: manifest.syncId || '', cloudTotal: stats.cloudTotal || 0, allowedCount: stats.allowedCount || 0, appliedCount: stats.applyCount || 0, mergedCount: stats.mergeCount || 0, skippedCount: stats.skippedCount || 0, discardedStrictCount: stats.discardedStrictCount || 0, invalidCount: (preview && Array.isArray(preview.invalid) ? preview.invalid.length : 0), conflictCount: stats.conflictCount || 0, clippedCount: stats.clippedCount || 0, unchangedCount: stats.unchangedCount || 0, skippedManifestCount: preview && Array.isArray(preview.skippedManifests) ? preview.skippedManifests.length : 0, discardedStrictKeys: preview && Array.isArray(preview.discardedStrictLocal) ? preview.discardedStrictLocal.map(item => item.key).slice(0, 30) : [], invalidKeys: preview && Array.isArray(preview.invalid) ? preview.invalid.map(item => item.key).slice(0, 30) : [], ...extra }); } function getConfigCloudPullStageFromError(error) { const msg = error && error.message ? error.message : String(error || ''); if (error && error.stage) return error.stage; if (msg.includes('FOLDER')) return 'find_folder'; if (msg.includes('MANIFEST')) return 'select_manifest'; if (msg.includes('CHUNK')) return 'read_chunks'; if (msg.includes('HASH')) return 'verify_hash'; if (msg.includes('PAYLOAD') || msg.includes('DECOMPRESS') || msg.includes('COMPRESSION')) return 'decode_payload'; return 'pull'; } function writeConfigCloudPullFailureReport(error, extra = {}) { const report = makeConfigCloudBaseReport('pull', 'failed', { stage: getConfigCloudPullStageFromError(error), reason: error && error.message ? error.message : String(error || ''), writtenRemote: false, writtenLocal: false, keepBookmarks: true, keepLocalConfig: true, rolledBack: false, ...extra }); writeConfigCloudSyncReport(report); return report; } function formatConfigCloudPullConfirmMessage(preview, isConflict = false) { const L = getStrings(); const stats = preview.stats || {}; const updateCount = Number(stats.applyCount || 0) + Number(stats.mergeCount || 0); const conflictCount = Number(stats.conflictCount || 0); const lines = [String(L.msg_config_cloud_pull_apply_summary || '').replace('{n}', String(updateCount))]; if (conflictCount > 0 || isConflict) lines.push(String(L.msg_config_cloud_pull_conflict_summary || '').replace('{n}', String(conflictCount))); return lines.join('\n'); } async function applyConfigCloudMergePreview(preview) { const actions = Array.isArray(preview && preview.actions) ? preview.actions : []; const prepared = actions.map(action => { const normalized = normalizeConfigValue(action.key, action.value, 'import'); if (normalized === undefined || (typeof normalized === 'number' && !Number.isFinite(normalized))) throw new Error(`CONFIG_CLOUD_IMPORT_INVALID_${action.key}`); return { ...action, value: normalized }; }); const snapshot = new Map(); prepared.forEach(action => { if (!snapshot.has(action.key)) snapshot.set(action.key, getStoredConfigRaw(action.key)); }); const written = []; try { for (const action of prepared) { await gmSetAsync(action.key, action.value); written.push(action.key); } cleanupConfigPrefixKeys(); const localHash = await hashConfigCloudLocalDefaultConfigs(); gmSet('pk_cfg_sync_last_remote_hash', preview.manifest.payloadHash); gmSet('pk_cfg_sync_last_local_hash', localHash.hash); gmSet('pk_cfg_sync_last_sync_at', new Date().toISOString()); writeConfigCloudSyncReport(makeConfigCloudPullReport(preview, 'success', { localHash: localHash.hash, localHashAlgorithm: localHash.algorithm, rollback: false, rolledBack: false })); return true; } catch (e) { const rollbackErrors = []; let rolledBack = false; for (let i = written.length - 1; i >= 0; i--) { const key = written[i]; try { const oldValue = snapshot.get(key); if (oldValue === undefined) removeStoredConfigKey(key); else setStoredConfigRaw(key, oldValue); rolledBack = true; } catch (rollbackError) { rollbackErrors.push({ key, reason: rollbackError && rollbackError.message ? rollbackError.message : String(rollbackError || '') }); } } try { cleanupConfigPrefixKeys(); } catch (cleanupError) { rollbackErrors.push({ key: '*cleanup*', reason: cleanupError && cleanupError.message ? cleanupError.message : String(cleanupError || '') }); } writeConfigCloudSyncReport(makeConfigCloudPullReport(preview, 'failed', { stage: 'apply', reason: e && e.message ? e.message : String(e || ''), writtenLocal: written.length > 0, keepLocalConfig: rollbackErrors.length === 0, rollback: true, rolledBack, rollbackFailed: rollbackErrors.length > 0, rollbackErrors })); return false; } } async function pullConfigCloudFromOfficial(options = {}) { try { const headers = await getConfigCloudAuthorizedHeaders('config-cloud-pull-missing-token'); const remotePackage = await readConfigCloudRemotePackage(headers); const preview = makeConfigCloudMergePreview(remotePackage); const stats = preview.stats || {}; const updateCount = Number(stats.applyCount || 0) + Number(stats.mergeCount || 0); if (updateCount <= 0) { showToast(getStrings().msg_config_cloud_pull_no_write, 'warning'); return true; } writeConfigCloudSyncReport(makeConfigCloudPullReport(preview, 'preview', { stage: 'preview', writtenLocal: false, keepLocalConfig: true })); const confirmed = await showConfirm(formatConfigCloudPullConfirmMessage(preview, !!options.isConflict), getStrings().lbl_config_cloud_sync); if (!confirmed) { writeConfigCloudSyncReport(makeConfigCloudPullReport(preview, 'cancel', { stage: 'confirm', reason: 'user_cancelled', writtenLocal: false, keepLocalConfig: true, cancelledAt: new Date().toISOString() })); showToast(getStrings().msg_config_cloud_pull_cancelled, 'warning'); return false; } const ok = await applyConfigCloudMergePreview(preview); showToast(ok ? getStrings().msg_config_cloud_pull_success : getStrings().msg_config_cloud_pull_failed, ok ? 'success' : 'error'); return ok; } catch (e) { console.warn('[Config Cloud] Pull failed:', e); writeConfigCloudPullFailureReport(e, { skippedManifestCount: Array.isArray(e && e.skippedManifests) ? e.skippedManifests.length : 0, skippedManifests: Array.isArray(e && e.skippedManifests) ? e.skippedManifests.slice(0, 20) : [] }); showToast(getStrings().msg_config_cloud_pull_failed, 'error'); return false; } } function readConfigCloudSyncReport() { const raw = getStoredConfigRaw('pk_cfg_sync_last_report') ?? '{}'; try { const data = typeof raw === 'string' ? JSON.parse(raw || '{}') : raw; return data && typeof data === 'object' && !Array.isArray(data) ? data : {}; } catch (e) { return {}; } } function writeConfigCloudSyncReport(report) { let payload = report || {}; let text = JSON.stringify(payload); if (estimateTextSize(text) > 64 * CONFIG_SIZE.KB) { payload = { ...payload, reason: safeStringLimit(payload.reason || '', 1024), oversizeCategories: Array.isArray(payload.oversizeCategories) ? payload.oversizeCategories.slice(0, 10) : payload.oversizeCategories }; text = JSON.stringify(payload); } setStoredConfigRaw('pk_cfg_sync_last_report', text); } function makeConfigCloudBaseReport(operation, status, extra = {}) { return { operation, status, time: new Date().toISOString(), ...extra }; } function formatConfigCloudReportStatus(report, L) { if (!report || !report.status) return L.label_config_cloud_none; if (report.status === 'success') return L.label_config_cloud_status_success; if (report.status === 'aborted') return L.label_config_cloud_status_aborted; if (report.status === 'preview') return L.label_config_cloud_status_preview; if (report.status === 'cancel') return L.label_config_cloud_status_cancel; return L.label_config_cloud_status_failed; } function formatConfigCloudOperation(report, L) { if (!report || !report.operation) return L.label_config_cloud_none; if (report.operation === 'clear') return L.label_config_cloud_operation_clear; if (report.operation === 'pull') return L.label_config_cloud_operation_pull; return L.label_config_cloud_operation_upload; } function formatConfigCloudBool(value, L) { return safeBoolean(value, false) ? L.label_config_cloud_bool_yes : L.label_config_cloud_bool_no; } function getConfigCloudReportBool(value, fallback = false) { return safeBoolean(value, fallback); } function formatConfigCloudDateTime(value) { if (!value) return ''; const d = new Date(value); if (!Number.isFinite(d.getTime())) return ''; const pad = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } function formatConfigCloudReason(reason, L) { if (reason === 'too_many_chunks') return L.label_config_cloud_abort_too_many_chunks; if (reason === 'user_cancelled') return L.label_config_cloud_user_cancelled; return reason || L.label_config_cloud_none; } function formatConfigCloudOversizeCategories(list, L) { if (!Array.isArray(list) || !list.length) return L.label_config_cloud_none; return list.map(item => `${item && item.key ? item.key : L.label_config_cloud_none}: ${Number(item && item.jsonChars) || 0}`).join('\n'); } function showConfigCloudSyncDetailModal() { const L = getStrings(); const report = readConfigCloudSyncReport(); if (!report || !report.operation) { showAlert(L.msg_config_cloud_no_report, L.title_config_cloud_detail); return; } const oldCleanupFailed = getConfigCloudReportBool(report.oldCleanupFailed, false); const rows = [ [L.label_config_cloud_report_operation, formatConfigCloudOperation(report, L)], [L.label_config_cloud_report_status, formatConfigCloudReportStatus(report, L)], [L.label_config_cloud_report_time, formatConfigCloudDateTime(report.time) || L.label_config_cloud_none], [L.label_config_cloud_report_updated_at, formatConfigCloudDateTime(report.updatedAt) || report.updatedAt || L.label_config_cloud_none], [L.label_config_cloud_report_stage, report.stage || L.label_config_cloud_none], [L.label_config_cloud_report_reason, formatConfigCloudReason(report.reason, L)], [L.label_config_cloud_report_written_remote, formatConfigCloudBool(getConfigCloudReportBool(report.writtenRemote, false), L)], [L.label_config_cloud_report_keep_old_remote, formatConfigCloudBool(getConfigCloudReportBool(report.keepOldRemote, true), L)], [L.label_config_cloud_report_keep_local, formatConfigCloudBool(getConfigCloudReportBool(report.keepLocalConfig, true), L)], [L.label_config_cloud_report_written_local, formatConfigCloudBool(getConfigCloudReportBool(report.writtenLocal, false), L)], [L.label_config_cloud_report_local_total, String(report.localTotal ?? 0)], [L.label_config_cloud_report_uploaded_count, String(report.uploadedCount ?? 0)], [L.label_config_cloud_report_skipped_count, String(report.skippedStrictCount ?? 0)], [L.label_config_cloud_report_cloud_total, String(report.cloudTotal ?? 0)], [L.label_config_cloud_report_allowed_count, String(report.allowedCount ?? 0)], [L.label_config_cloud_report_pull_apply_count, String(report.appliedCount ?? 0)], [L.label_config_cloud_report_pull_merge_count, String(report.mergedCount ?? 0)], [L.label_config_cloud_report_pull_skip_count, String(report.skippedCount ?? 0)], [L.label_config_cloud_report_pull_discard_count, String(report.discardedStrictCount ?? 0)], [L.label_config_cloud_report_pull_invalid_count, String(report.invalidCount ?? 0)], [L.label_config_cloud_report_pull_conflict_count, String(report.conflictCount ?? 0)], [L.label_config_cloud_report_pull_clip_count, String(report.clippedCount ?? 0)], [L.label_config_cloud_report_pull_unchanged_count, String(report.unchangedCount ?? 0)], [L.label_config_cloud_report_config_count, String(report.configCount ?? report.uploadedCount ?? 0)], [L.label_config_cloud_report_raw_size, fmtSize(report.rawJsonBytes || 0)], [L.label_config_cloud_report_payload_size, fmtSize(report.payloadBytes || 0)], [L.label_config_cloud_report_compressed_size, fmtSize(report.compressedBytes || 0)], [L.label_config_cloud_report_encoded_size, fmtSize(report.encodedBytes || 0)], [L.label_config_cloud_report_chunk_size, String(report.chunkSize || CONF.configCloudChunkSize)], [L.label_config_cloud_report_chunk_count, String(report.chunkCount || 0)], [L.label_config_cloud_report_hash, report.payloadHash || L.label_config_cloud_none], [L.label_config_cloud_report_hash_algorithm, report.hashAlgorithm || report.localHashAlgorithm || L.label_config_cloud_none], [L.label_config_cloud_report_local_hash, report.localHash || L.label_config_cloud_none], [L.label_config_cloud_report_sync_id, report.syncId || L.label_config_cloud_none], [L.label_config_cloud_report_skipped_manifest, String(report.skippedManifestCount ?? 0)], [L.label_config_cloud_report_rollback, formatConfigCloudBool(getConfigCloudReportBool(report.rollback || report.rolledBack, false), L)], [L.label_config_cloud_report_oversize_categories, formatConfigCloudOversizeCategories(report.oversizeCategories, L)], [L.label_config_cloud_report_cleanup, oldCleanupFailed ? L.label_config_cloud_cleanup_failed : (getConfigCloudReportBool(report.cleanupDone, false) ? L.label_config_cloud_cleanup_ok : L.label_config_cloud_none)], [L.label_config_cloud_report_deleted_manifest, String(report.removedManifestCount ?? 0)], [L.label_config_cloud_report_deleted_chunk, String(report.removedChunkCount ?? 0)], [L.label_config_cloud_report_keep_bookmarks, formatConfigCloudBool(getConfigCloudReportBool(report.keepBookmarks, true), L)] ]; const body = rows.map(([k, v]) => `
${esc(k)}
${esc(String(v || L.label_config_cloud_none))}
`).join(''); const m = showModal(`

${esc(L.title_config_cloud_detail)}

${body}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) Object.assign(modalBox.style, { width: 'min(620px, calc(100vw - 36px))', padding: '28px', boxSizing: 'border-box' }); const focusPreviousModal = () => { const lastModal = Array.from(document.querySelectorAll('.pk-modal-ov')).pop(); if (!lastModal) return; lastModal.tabIndex = 0; setTimeout(() => { try { lastModal.focus(); } catch (e) {} }, 0); }; const closeDetail = () => { m.remove(); focusPreviousModal(); }; const ok = m.querySelector('#pk_cfg_cloud_detail_ok'); if (ok) ok.onclick = closeDetail; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) closeBtn.addEventListener('click', () => setTimeout(focusPreviousModal, 0)); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key !== 'Enter' && e.key !== 'Escape') return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); closeDetail(); }, true); } async function getConfigCloudAuthorizedHeaders(reason) { let headers = getHeaders(); if (!headers.Authorization || headers.Authorization.length < 10) { if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow(reason || 'config-cloud-missing-token', 5000); const ready = await waitForAuth(8000); if (!ready) throw new Error('AUTH_MISSING'); headers = getHeaders(); } return headers; } async function readConfigCloudLatestManifestForStatus() { const headers = getHeaders(); if (!headers.Authorization || headers.Authorization.length < 10) return null; try { const remote = await readConfigCloudLatestManifestFromOfficial(headers); return remote && remote.selected ? remote.selected.manifest : null; } catch (e) { const msg = e && e.message ? e.message : ''; if (msg === 'CONFIG_CLOUD_SYNC_FOLDER_MISSING' || msg === 'CONFIG_CLOUD_MANIFEST_MISSING' || msg === 'CONFIG_CLOUD_NO_COMPATIBLE_MANIFEST') return null; throw e; } } async function uploadConfigCloudToOfficial() { let pack = null; let writtenRemote = false; try { pack = await buildConfigCloudPayloadPackage(); if (pack.tooManyChunks) { const report = makeConfigCloudBaseReport('upload', 'aborted', { reason: 'too_many_chunks', writtenRemote: false, keepOldRemote: true, keepLocalConfig: true, localTotal: pack.localTotal, uploadedCount: pack.uploadedCount, skippedStrictCount: pack.skippedStrictCount, updatedAt: pack.manifest.updatedAt, configCount: pack.manifest.configCount, payloadBytes: pack.manifest.payloadBytes, rawJsonBytes: pack.manifest.rawJsonBytes, compressedBytes: pack.manifest.compressedBytes, encodedBytes: pack.manifest.encodedBytes, chunkSize: pack.manifest.chunkSize, chunkCount: pack.manifest.chunkCount, payloadHash: pack.manifest.payloadHash, syncId: pack.syncId, oversizeCategories: pack.oversizeCategories, keepBookmarks: true }); writeConfigCloudSyncReport(report); showToast(getStrings().label_config_cloud_abort_too_many_chunks, 'error'); return false; } const headers = await getConfigCloudAuthorizedHeaders('config-cloud-upload-missing-token'); const latest = await fetchOfficialLinkBookmarkSnapshot(headers); let withNew = appendConfigCloudPackageToFolders(latest.folders, pack); let writeMode = 'append'; let removedManifestCount = 0; let removedChunkCount = 0; let capacity = validateLinkBookmarkCapacityBudget(withNew); if (!capacity.ok) { const replaced = removeConfigCloudSyncPseudoLinksFromFolders(latest.folders); const replacedWithNew = appendConfigCloudPackageToFolders(replaced.folders, pack); const replacedCapacity = validateLinkBookmarkCapacityBudget(replacedWithNew); if (!replacedCapacity.ok) { showToast(replacedCapacity.message, 'error'); throw new Error(replacedCapacity.message); } withNew = replacedWithNew; writeMode = 'replace'; removedManifestCount = replaced.removedManifestCount; removedChunkCount = replaced.removedChunkCount; } await postOfficialLinkBookmarkFolders(withNew, headers); writtenRemote = true; const verified = await verifyConfigCloudSyncWrite(headers, pack.syncId, pack.manifest); let cleanupDone = writeMode === 'replace'; let oldCleanupFailed = false; try { if (writeMode !== 'replace') { const cleanup = removeConfigCloudSyncPseudoLinksFromFolders(verified.folders, { keepSyncId: pack.syncId }); removedManifestCount = cleanup.removedManifestCount; removedChunkCount = cleanup.removedChunkCount; if (removedManifestCount || removedChunkCount) await postOfficialLinkBookmarkFolders(cleanup.folders, headers); cleanupDone = true; } } catch (cleanupError) { oldCleanupFailed = true; console.warn('[Config Cloud] Old sync cleanup failed:', cleanupError); } const localHash = await hashConfigCloudLocalDefaultConfigs(); gmSet('pk_cfg_sync_last_remote_hash', pack.manifest.payloadHash); gmSet('pk_cfg_sync_last_local_hash', localHash.hash); gmSet('pk_cfg_sync_last_sync_at', pack.manifest.createdAt); writeConfigCloudSyncReport(makeConfigCloudBaseReport('upload', 'success', { writtenRemote: true, keepOldRemote: oldCleanupFailed, keepLocalConfig: true, localHash: localHash.hash, localHashAlgorithm: localHash.algorithm, localTotal: pack.localTotal, uploadedCount: pack.uploadedCount, skippedStrictCount: pack.skippedStrictCount, updatedAt: pack.manifest.updatedAt, configCount: pack.manifest.configCount, payloadBytes: pack.manifest.payloadBytes, rawJsonBytes: pack.manifest.rawJsonBytes, compressedBytes: pack.manifest.compressedBytes, encodedBytes: pack.manifest.encodedBytes, chunkSize: pack.manifest.chunkSize, chunkCount: pack.manifest.chunkCount, payloadHash: pack.manifest.payloadHash, syncId: pack.syncId, oversizeCategories: pack.oversizeCategories, cleanupDone, oldCleanupFailed, removedManifestCount, removedChunkCount, keepBookmarks: true })); if (S.linkBookmarkMode) await loadLinkBookmarkOfficial(true); showToast(getStrings().msg_config_cloud_upload_success); return true; } catch (e) { console.warn('[Config Cloud] Upload failed:', e); writeConfigCloudSyncReport(makeConfigCloudBaseReport('upload', 'failed', { stage: writtenRemote ? 'verify' : 'write_new', reason: e && e.message ? e.message : String(e || ''), writtenRemote, keepOldRemote: true, keepLocalConfig: true, localTotal: pack ? pack.localTotal : 0, uploadedCount: pack ? pack.uploadedCount : 0, skippedStrictCount: pack ? pack.skippedStrictCount : 0, updatedAt: pack && pack.manifest ? pack.manifest.updatedAt : '', configCount: pack && pack.manifest ? pack.manifest.configCount : 0, payloadBytes: pack && pack.manifest ? pack.manifest.payloadBytes : 0, rawJsonBytes: pack && pack.manifest ? pack.manifest.rawJsonBytes : 0, compressedBytes: pack && pack.manifest ? pack.manifest.compressedBytes : 0, encodedBytes: pack && pack.manifest ? pack.manifest.encodedBytes : 0, chunkSize: pack && pack.manifest ? pack.manifest.chunkSize : CONF.configCloudChunkSize, chunkCount: pack && pack.manifest ? pack.manifest.chunkCount : 0, payloadHash: pack && pack.manifest ? pack.manifest.payloadHash : '', syncId: pack ? pack.syncId : '', oversizeCategories: pack ? pack.oversizeCategories : [], keepBookmarks: true })); showToast(getStrings().msg_config_cloud_upload_failed, 'error'); return false; } } async function clearConfigCloudFromOfficial() { try { const headers = await getConfigCloudAuthorizedHeaders('config-cloud-clear-missing-token'); const latest = await fetchOfficialLinkBookmarkSnapshot(headers); const cleanup = removeConfigCloudSyncPseudoLinksFromFolders(latest.folders); if (!cleanup.removedManifestCount && !cleanup.removedChunkCount) { gmSet('pk_cfg_sync_last_remote_hash', ''); gmSet('pk_cfg_sync_last_local_hash', ''); gmSet('pk_cfg_sync_last_sync_at', new Date().toISOString()); writeConfigCloudSyncReport(makeConfigCloudBaseReport('clear', 'success', { writtenRemote: false, keepOldRemote: false, keepLocalConfig: true, removedManifestCount: 0, removedChunkCount: 0, keepBookmarks: true })); showToast(getStrings().msg_config_cloud_clear_empty, 'warning'); return true; } await postOfficialLinkBookmarkFolders(cleanup.folders, headers); const verified = await fetchOfficialLinkBookmarkSnapshot(headers); const remain = collectConfigCloudSyncRecords(verified.folders); if (remain.manifests.length || remain.chunks.length) throw new Error('CONFIG_CLOUD_CLEAR_VERIFY_FAILED'); gmSet('pk_cfg_sync_last_remote_hash', ''); gmSet('pk_cfg_sync_last_local_hash', ''); gmSet('pk_cfg_sync_last_sync_at', new Date().toISOString()); writeConfigCloudSyncReport(makeConfigCloudBaseReport('clear', 'success', { writtenRemote: true, keepOldRemote: false, keepLocalConfig: true, removedManifestCount: cleanup.removedManifestCount, removedChunkCount: cleanup.removedChunkCount, keepBookmarks: true })); if (S.linkBookmarkMode) await loadLinkBookmarkOfficial(true); showToast(getStrings().msg_config_cloud_clear_success); return true; } catch (e) { console.warn('[Config Cloud] Clear failed:', e); writeConfigCloudSyncReport(makeConfigCloudBaseReport('clear', 'failed', { stage: 'clear', reason: e && e.message ? e.message : String(e || ''), writtenRemote: false, keepOldRemote: true, keepLocalConfig: true, keepBookmarks: true })); showToast(getStrings().msg_config_cloud_clear_failed, 'error'); return false; } } async function getConfigCloudSyncStatusSnapshot() { const L = getStrings(); const report = readConfigCloudSyncReport(); const lastRemoteHash = gmGet('pk_cfg_sync_last_remote_hash', ''); const lastLocalHash = gmGet('pk_cfg_sync_last_local_hash', ''); const lastSyncAt = gmGet('pk_cfg_sync_last_sync_at', ''); let remoteManifest = null; let localHash = null; try { remoteManifest = await readConfigCloudLatestManifestForStatus(); } catch (e) {} try { localHash = await hashConfigCloudLocalDefaultConfigs(); } catch (e) {} const remoteHash = remoteManifest && remoteManifest.payloadHash ? remoteManifest.payloadHash : ''; const legacyUploadLocalHash = !!(lastLocalHash && lastRemoteHash && lastLocalHash === lastRemoteHash && report.operation === 'upload' && report.status === 'success' && !report.localHash); const localChanged = !!(!legacyUploadLocalHash && lastLocalHash && localHash && localHash.hash && localHash.hash !== lastLocalHash); const remoteChanged = !!(remoteHash && lastRemoteHash && remoteHash !== lastRemoteHash); let state = 'unsynced'; if (!lastRemoteHash && !remoteHash) state = report.operation === 'clear' && report.status === 'success' ? 'empty' : 'unsynced'; else if (localChanged && (remoteChanged || (!lastRemoteHash && remoteHash))) state = 'conflict'; else if (localChanged) state = 'local_changed'; else if (remoteChanged || (!lastRemoteHash && remoteHash)) state = 'remote_changed'; else if (lastRemoteHash && remoteHash === lastRemoteHash && (!lastLocalHash || !localHash || localHash.hash === lastLocalHash)) state = 'synced'; else if (lastRemoteHash && !remoteHash) state = 'empty'; if (report.status === 'failed' || report.status === 'aborted') state = 'failed'; const labelMap = { unsynced: L.status_config_cloud_unsynced, synced: L.status_config_cloud_synced, local_changed: L.status_config_cloud_local_changed, remote_changed: L.status_config_cloud_remote_changed, conflict: L.status_config_cloud_conflict, failed: L.status_config_cloud_failed, empty: L.status_config_cloud_empty }; const encodedBytes = Number(remoteManifest && remoteManifest.encodedBytes) || Number(report.encodedBytes) || 0; const chunkCount = Number(remoteManifest && remoteManifest.chunkCount) || Number(report.chunkCount) || 0; return { state, statusText: labelMap[state] || L.status_config_cloud_unsynced, lastSyncText: formatConfigCloudDateTime(lastSyncAt || report.time) || L.label_config_cloud_none, remoteSizeText: encodedBytes ? fmtSize(encodedBytes) : L.label_config_cloud_none, chunkCountText: chunkCount ? String(chunkCount) : '0', report, remoteManifest, localHash }; } function updateConfigCloudSettingsPanel(root, snapshot) { if (!root || !snapshot) return; const status = root.querySelector('#pk_cfg_cloud_status'); const lastSync = root.querySelector('#pk_cfg_cloud_last_sync'); const remoteSize = root.querySelector('#pk_cfg_cloud_remote_size'); const chunkCount = root.querySelector('#pk_cfg_cloud_chunk_count'); if (status) { status.textContent = snapshot.statusText; status.dataset.state = snapshot.state || ''; } if (lastSync) lastSync.textContent = snapshot.lastSyncText; if (remoteSize) remoteSize.textContent = snapshot.remoteSizeText; if (chunkCount) chunkCount.textContent = snapshot.chunkCountText; } function setConfigCloudSettingsBusy(root, busy) { if (!root) return; const cloudLocked = !!busy || !!S.localCleanBusy; root.querySelectorAll('.pk-cfg-cloud-actions .pk-btn').forEach(btn => { btn.disabled = cloudLocked; }); const cleanBtn = root.querySelector('#btn_cfg_clean'); if (cleanBtn) cleanBtn.disabled = !!busy || !!S.localCleanBusy; } function getLinkBookmarkTitleFromHref(href) { const L = getStrings(); const raw = normalizeLinkBookmarkText(href).trim(); if (!raw) return L.card_link_bookmark_unnamed_title; try { const u = new URL(raw); if (u.hostname) return u.hostname; } catch(e) {} return raw || L.card_link_bookmark_unnamed_title; } function normalizeLinkBookmarkEditableTitle(title, href) { const raw = normalizeLinkBookmarkText(title).trim(); return raw || getLinkBookmarkTitleFromHref(href); } function isLinkBookmarkHrefDuplicated(folders, href, skipFolderIndex = -1, skipLinkIndex = -1) { const target = normalizeLinkBookmarkText(href).trim(); if (!target || !Array.isArray(folders)) return false; for (let fi = 0; fi < folders.length; fi++) { const list = Array.isArray(folders[fi] && folders[fi].list) ? folders[fi].list : []; for (let li = 0; li < list.length; li++) { if (fi === skipFolderIndex && li === skipLinkIndex) continue; if (normalizeLinkBookmarkText(list[li] && list[li].href).trim() === target) return true; } } return false; } function buildLinkBookmarkIconFromHref(href) { const raw = normalizeLinkBookmarkText(href).trim(); if (!raw) return ''; try { const u = new URL(/^\/\//.test(raw) ? `https:${raw}` : raw); if (u.hostname) return `${u.hostname}/favicon.ico`; } catch(e) {} const first = raw.split(/[\/\s?#]+/).filter(Boolean)[0] || ''; return normalizeLinkBookmarkIconForOfficial(first ? `${first}/favicon.ico` : ''); } function resolveLinkBookmarkIconDisplaySrc(icon) { const raw = normalizeLinkBookmarkText(icon).trim(); if (!raw) return ''; if (/^data:image\//i.test(raw)) return raw; if (/^https?:\/\//i.test(raw)) return raw; if (/^\/\//.test(raw)) return `https:${raw}`; if (/^(javascript|data|file|blob):/i.test(raw)) return ''; if (raw.startsWith('/')) return `${location.origin}${raw}`; const first = raw.split(/[\/?#]/).filter(Boolean)[0] || ''; if (first === 'localhost' || first.includes('.') || /:\d+$/.test(first)) return `https://${raw}`; return ''; } function getLinkBookmarkIconRuntimeCache() { if (!(S.linkBookmarkIconRuntimeCache instanceof Map)) S.linkBookmarkIconRuntimeCache = new Map(); return S.linkBookmarkIconRuntimeCache; } function getLinkBookmarkIconRuntimeKey(link) { const href = normalizeLinkBookmarkText(link && link.href).trim(); const icon = normalizeLinkBookmarkText(link && link.icon).trim(); return href || icon ? `${href}\n${icon}` : ''; } function getLinkBookmarkIconRuntimeState(link) { const key = getLinkBookmarkIconRuntimeKey(link); const src = resolveLinkBookmarkIconDisplaySrc(link && link.icon); if (!key || !src) return { key, src: '', ok: false, failed: false }; const cached = getLinkBookmarkIconRuntimeCache().get(key); if (cached && cached.src === src) return { key, src, ok: !!cached.ok, failed: !!cached.failed }; return { key, src, ok: false, failed: false }; } function setLinkBookmarkIconRuntimeState(key, src, state) { if (!key || !src) return; getLinkBookmarkIconRuntimeCache().set(key, { src, ok: state === 'ok', failed: state === 'failed' }); } function pruneLinkBookmarkIconRuntimeCache(folders) { const cache = getLinkBookmarkIconRuntimeCache(); if (!cache.size) return; const live = new Set(); (Array.isArray(folders) ? folders : []).forEach(folder => { const list = Array.isArray(folder && folder.list) ? folder.list : []; list.forEach(link => { const key = getLinkBookmarkIconRuntimeKey(link); const src = resolveLinkBookmarkIconDisplaySrc(link && link.icon); if (key && src) live.add(key); }); }); Array.from(cache.keys()).forEach(key => { if (!live.has(key)) cache.delete(key); }); } function isDirectOpenLinkBookmarkHref(href) { try { const u = new URL(normalizeLinkBookmarkText(href).trim()); return u.protocol === 'http:' || u.protocol === 'https:'; } catch(e) { return false; } } function openLinkBookmarkHref(folderIndex, linkIndex) { const L = getStrings(); const folders = ensureLinkBookmarkFolders(); const folder = folders[getClampedLinkBookmarkFolderIndex(folderIndex, folders)]; const list = Array.isArray(folder && folder.list) ? folder.list : []; const link = list[linkIndex]; const href = normalizeLinkBookmarkText(link && link.href).trim(); if (!href || !isDirectOpenLinkBookmarkHref(href)) { showToast(L.msg_link_bookmark_open_blocked, 'warning'); return; } const opened = window.open(href, '_blank', 'noopener,noreferrer'); try { if (opened) opened.opener = null; } catch(e) {} } async function copyLinkBookmarkHref(folderIndex, linkIndex) { const L = getStrings(); const folders = ensureLinkBookmarkFolders(); const folder = folders[getClampedLinkBookmarkFolderIndex(folderIndex, folders)]; const list = Array.isArray(folder && folder.list) ? folder.list : []; const link = list[linkIndex]; const href = normalizeLinkBookmarkText(link && link.href); if (!href.trim()) { showToast(L.card_link_bookmark_empty_href, 'warning'); return; } try { GM_setClipboard(href); showToast(L.msg_link_bookmark_copy_success); return; } catch(e) {} try { await navigator.clipboard.writeText(href); showToast(L.msg_link_bookmark_copy_success); } catch(e) { showToast(L.err_clipboard_failed || L.str_action_failed, 'error'); } } function getMagnetBackupSelectedItems() { const maps = [S.itemMap, S.shareParseInsightItemMap, S.shareParseItemMap].filter(m => m && typeof m.get === 'function'); return S.getSelectedIds().map(id => { for (const m of maps) { const item = m.get(id); if (item) return item; } return null; }).filter(Boolean); } function setupLinkBookmarkModalBox(m, width = '440px') { const box = m && m.querySelector('.pk-modal'); if (box) { Object.assign(box.style, { width, padding: '24px', height: 'auto', minHeight: 'auto' }); box.style.setProperty('max-height', '92vh', 'important'); box.style.setProperty('overflow-y', 'auto', 'important'); } const closeBtn = m && m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '18px', right: '18px' }); } function showLinkBookmarkConfirm(message) { return showConfirm(message); } function getLinkBookmarkFolderOptionLabel(folder, index, L = getStrings()) { return getLinkBookmarkFolderDisplayName(folder, L); } function buildLinkBookmarkFolderOptions(folders, selectedIndex, options = {}) { const L = getStrings(); const excludeMagnetArchive = !!(options && options.excludeMagnetArchive); return folders.map((folder, index) => { if (shouldHideLinkBookmarkFolderInOrdinaryView(folder)) return ''; if (excludeMagnetArchive && isMagnetArchiveBookmarkFolder(folder)) return ''; const label = getLinkBookmarkFolderOptionLabel(folder, index, L); return `
${esc(label)}
`; }).join(''); } function showLinkBookmarkFormError(errorEl, message) { if (!errorEl) return; errorEl.textContent = message || ''; errorEl.style.display = 'block'; errorEl.style.visibility = message ? 'visible' : 'hidden'; } function openLinkBookmarkLinkModal(mode, folderIndex, linkIndex = -1) { const L = getStrings(); const folders = ensureLinkBookmarkFolders(); const isEdit = mode === 'edit'; const safeFolderIndex = getClampedLinkBookmarkFolderIndex(folderIndex, folders); const sourceFolder = folders[safeFolderIndex] || folders[0]; const sourceList = Array.isArray(sourceFolder && sourceFolder.list) ? sourceFolder.list : []; const sourceLink = isEdit ? sourceList[linkIndex] : null; if (!isEdit && isMagnetArchiveBookmarkFolder(sourceFolder)) return; if (isEdit && (!sourceLink || typeof sourceLink !== 'object')) return; if (!canPerformLinkBookmarkOrdinaryWrite()) return; const selectedIndex = safeFolderIndex; const originalTitle = isEdit ? normalizeLinkBookmarkText(sourceLink.title).trim() : ''; const originalHref = isEdit ? normalizeLinkBookmarkText(sourceLink.href).trim() : ''; const originalIcon = isEdit ? normalizeLinkBookmarkText(sourceLink.icon).trim() : ''; const isCloudArchiveTitleOnlyEdit = isEdit && isMagnetArchiveBookmarkLink(sourceFolder, sourceLink); const linkFieldWrapStyle = 'position:relative; padding-top:10px;'; const linkFieldInputStyle = '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;'; const linkFieldSelectTriggerStyle = '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; display:flex; align-items:center; justify-content:space-between; cursor:pointer;'; const linkFieldSelectMenuStyle = 'display:none; position:relative; width:100%; margin-top:6px; height:auto; max-height:none; overflow:visible; z-index:1;'; const linkFieldLabelStyle = 'position:absolute; top:10px; 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; z-index:2; pointer-events:none;'; const selectedFolderLabel = getLinkBookmarkFolderOptionLabel(folders[selectedIndex], selectedIndex, L); const m = showModal(`

${esc(isEdit ? L.title_link_bookmark_edit_link : L.title_link_bookmark_new_link)}

`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { const modalHeight = isCloudArchiveTitleOnlyEdit ? '300px' : '430px'; Object.assign(modalBox.style, { width: '480px', height: modalHeight, minHeight: modalHeight, maxHeight: 'calc(100vh - 80px)', padding: '30px', overflow: 'hidden', boxSizing: 'border-box', display: 'flex', flexDirection: 'column' }); const closeBtnInBox = m.querySelector('.pk-modal-close'); if (closeBtnInBox) Object.assign(closeBtnInBox.style, { top: '26px', right: '26px' }); } const titleInput = m.querySelector('#pk_lbm_link_title'); const hrefInput = m.querySelector('#pk_lbm_link_href'); bindPlainTextMaxLengthLimits(m, isCloudArchiveTitleOnlyEdit ? [ ['#pk_lbm_link_title', getLinkBookmarkFieldMaxLen('title')] ] : [ ['#pk_lbm_link_title', getLinkBookmarkFieldMaxLen('title')], ['#pk_lbm_link_href', getLinkBookmarkFieldMaxLen('href')] ]); const folderSelect = m.querySelector('#pk_lbm_link_folder'); const folderTrigger = folderSelect ? folderSelect.querySelector('.pk-select-trigger') : null; const folderMenu = folderSelect ? folderSelect.querySelector('.pk-select-menu') : null; const folderText = m.querySelector('#pk_lbm_link_folder_text'); const errorEl = m.querySelector('#pk_lbm_link_error'); const saveBtn = m.querySelector('#pk_lbm_link_save'); const cancelBtn = m.querySelector('#pk_lbm_link_cancel'); const closeBtn = m.querySelector('.pk-modal-close'); if (isCloudArchiveTitleOnlyEdit) { const hrefWrap = hrefInput ? hrefInput.parentElement : null; const folderWrap = folderSelect ? folderSelect.parentElement : null; if (hrefWrap) hrefWrap.style.display = 'none'; if (folderWrap) folderWrap.style.display = 'none'; if (hrefInput) { hrefInput.readOnly = true; hrefInput.tabIndex = -1; } } const closeLinkFolderMenu = () => { if (folderMenu) folderMenu.style.display = 'none'; if (folderTrigger) folderTrigger.style.borderColor = ''; }; const linkFolderOutsideHandler = (e) => { if (folderSelect && !folderSelect.contains(e.target)) closeLinkFolderMenu(); }; if (folderTrigger && folderMenu && folderSelect && !isCloudArchiveTitleOnlyEdit) { folderTrigger.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const isOpen = folderMenu.style.display === 'block'; closeLinkFolderMenu(); if (!isOpen) { folderMenu.style.display = 'block'; folderTrigger.style.borderColor = 'var(--pk-pri)'; } }; folderMenu.querySelectorAll('.pk-select-item').forEach(item => { item.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const value = item.dataset.val || '0'; folderSelect.dataset.value = value; if (folderText) folderText.textContent = item.textContent || ''; folderMenu.querySelectorAll('.pk-select-item').forEach(n => n.classList.toggle('act', n === item)); closeLinkFolderMenu(); updateLinkSaveState(); }; }); document.addEventListener('click', linkFolderOutsideHandler, true); } let busy = false; const updateLinkInputBorderState = () => { const title = normalizeLinkBookmarkText(titleInput && titleInput.value).trim(); const href = normalizeLinkBookmarkText(hrefInput && hrefInput.value).trim(); if (titleInput) titleInput.classList.toggle('pk-lbm-input-changed', isEdit ? (!!title && title !== originalTitle) : !!title); if (hrefInput) hrefInput.classList.toggle('pk-lbm-input-changed', isEdit ? (!!href && href !== originalHref) : !!href); }; const getLinkFormState = () => { const title = normalizeLinkBookmarkText(titleInput && titleInput.value).trim(); const href = normalizeLinkBookmarkText(hrefInput && hrefInput.value).trim(); const titleTooLong = isLinkBookmarkFieldTooLong(title, 'title'); const hrefTooLong = !isCloudArchiveTitleOnlyEdit && isLinkBookmarkFieldTooLong(href, 'href'); const hrefUnsafe = !isCloudArchiveTitleOnlyEdit && !hrefTooLong && isLinkBookmarkUnsafeLongPlainHref(href); const draftFolders = cloneLinkBookmarkFoldersForEditing(S.linkBookmarkFolders); const duplicate = !isCloudArchiveTitleOnlyEdit && !titleTooLong && !hrefTooLong && !hrefUnsafe && !!href && isLinkBookmarkHrefDuplicated(draftFolders, href, isEdit ? safeFolderIndex : -1, isEdit ? linkIndex : -1); return { title, href, draftFolders, duplicate, titleTooLong, hrefTooLong, hrefUnsafe, invalid: !title || (!isCloudArchiveTitleOnlyEdit && !href) || duplicate || titleTooLong || hrefTooLong || hrefUnsafe }; }; const updateLinkSaveState = () => { if (busy) return; updateLinkInputBorderState(); const state = getLinkFormState(); showLinkBookmarkFormError(errorEl, (state.titleTooLong || state.hrefTooLong || state.hrefUnsafe) ? L.msg_config_input_over_limit : (state.duplicate ? L.msg_link_bookmark_href_duplicate : '')); if (saveBtn) { saveBtn.disabled = state.invalid; saveBtn.textContent = L.btn_ok; saveBtn.style.opacity = state.invalid ? '0.4' : '1'; saveBtn.style.cursor = state.invalid ? 'not-allowed' : 'pointer'; } }; const close = () => { if (!busy) { document.removeEventListener('click', linkFolderOutsideHandler, true); m.remove(); } }; const setBusy = (value) => { busy = !!value; if (saveBtn) { saveBtn.disabled = busy; saveBtn.textContent = busy ? L.label_link_bookmark_saving : L.btn_ok; if (!busy) updateLinkSaveState(); } if (cancelBtn) cancelBtn.disabled = busy; }; const submit = async () => { if (busy) return; if (!canPerformLinkBookmarkOrdinaryWrite()) return; updateLinkSaveState(); const state = getLinkFormState(); if (state.titleTooLong || state.hrefTooLong || state.hrefUnsafe) { showConfigInputLimitTip(state.titleTooLong ? titleInput : hrefInput); return; } if (state.invalid) return; const href = isCloudArchiveTitleOnlyEdit ? originalHref : normalizeLinkBookmarkLimitedText(state.href, 'href', true); const draftFolders = state.draftFolders; const targetIndex = isCloudArchiveTitleOnlyEdit ? safeFolderIndex : getClampedLinkBookmarkFolderIndex(Number(folderSelect && folderSelect.dataset.value), draftFolders); if (!isCloudArchiveTitleOnlyEdit && isMagnetArchiveBookmarkFolder(draftFolders[targetIndex])) { setBusy(false); return; } const hadSearchOnSubmit = !!normalizeLinkBookmarkText(S.linkBookmarkSearch).trim(); const title = normalizeLinkBookmarkLimitedText(state.title, 'title', true); const icon = isCloudArchiveTitleOnlyEdit ? (originalIcon || buildLinkBookmarkIconFromHref(href)) : (isEdit ? (href === originalHref ? (originalIcon || buildLinkBookmarkIconFromHref(href)) : buildLinkBookmarkIconFromHref(href)) : buildLinkBookmarkIconFromHref(href)); let nextLink = isEdit ? sanitizeLinkBookmarkLinkForOfficial(sourceLink) : { href: '', title: '', icon: '', date: String(Date.now()) }; nextLink.href = href; nextLink.title = title; nextLink.icon = icon; nextLink.date = isEdit ? normalizeLinkBookmarkText(sourceLink.date) : String(Date.now()); if (!Array.isArray(draftFolders[targetIndex].list)) draftFolders[targetIndex].list = []; let ok = false; setBusy(true); if (isEdit) { if (!Array.isArray(draftFolders[safeFolderIndex].list) || !draftFolders[safeFolderIndex].list[linkIndex]) { setBusy(false); return; } const moveFromMyLinksToOther = targetIndex !== safeFolderIndex && isDefaultLinkBookmarkFolder(draftFolders[safeFolderIndex]) && !isDefaultLinkBookmarkFolder(draftFolders[targetIndex]); if (moveFromMyLinksToOther) { const targetFolderKey = getLinkBookmarkFolderUniqueKey(draftFolders[targetIndex]); const deleteFolders = cloneLinkBookmarkFoldersForEditing(draftFolders); deleteFolders[safeFolderIndex].list.splice(linkIndex, 1); const deleted = await saveLinkBookmarkFoldersWithConflictCheck(deleteFolders, safeFolderIndex); if (deleted) { const afterDeleteFolders = cloneLinkBookmarkFoldersForEditing(S.linkBookmarkFolders); const targetAfterIndex = afterDeleteFolders.findIndex(folder => getLinkBookmarkFolderUniqueKey(folder) === targetFolderKey); if (targetAfterIndex >= 0) { if (!Array.isArray(afterDeleteFolders[targetAfterIndex].list)) afterDeleteFolders[targetAfterIndex].list = []; afterDeleteFolders[targetAfterIndex].list.push(nextLink); ok = await saveLinkBookmarkFoldersWithConflictCheck(afterDeleteFolders, targetAfterIndex); } else { S.linkBookmarkSyncStatus = 'failed'; S.linkBookmarkError = L.msg_link_bookmark_save_failed_retry; if (S.linkBookmarkMode) renderLinkBookmarkPanel(); } } } else { if (targetIndex === safeFolderIndex) draftFolders[safeFolderIndex].list[linkIndex] = nextLink; else { draftFolders[safeFolderIndex].list.splice(linkIndex, 1); draftFolders[targetIndex].list.push(nextLink); } ok = await saveLinkBookmarkFoldersWithConflictCheck(draftFolders, targetIndex); } } else { draftFolders[targetIndex].list.push(nextLink); ok = await saveLinkBookmarkFoldersWithConflictCheck(draftFolders, targetIndex); } setBusy(false); if (ok) { if (!isEdit) { const latestFolders = ensureLinkBookmarkFolders(); const safeTargetIndex = getClampedLinkBookmarkFolderIndex(targetIndex, latestFolders); const latestList = Array.isArray(latestFolders[safeTargetIndex] && latestFolders[safeTargetIndex].list) ? latestFolders[safeTargetIndex].list : []; S.linkBookmarkSelectedFolderIndex = safeTargetIndex; if (hadSearchOnSubmit) { S.linkBookmarkSearch = ''; S.linkBookmarkSearchDraft = ''; } setLinkBookmarkFolderPageForIndex(safeTargetIndex); setLinkBookmarkLinkPageForIndex(Math.max(0, latestList.length - 1)); if (S.linkBookmarkMode) renderLinkBookmarkPanel(); } document.removeEventListener('click', linkFolderOutsideHandler, true); m.remove(); } }; if (titleInput) titleInput.addEventListener('input', updateLinkSaveState); if (hrefInput) hrefInput.addEventListener('input', updateLinkSaveState); if (saveBtn) saveBtn.onclick = submit; if (cancelBtn) cancelBtn.onclick = close; if (closeBtn) closeBtn.onclick = close; updateLinkSaveState(); m.tabIndex = 0; setTimeout(() => { if (!document.contains(m)) return; try { m.focus({ preventScroll: true }); } catch(e) { m.focus(); } }, 10); m.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); } else if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); submit(); } }); } async function openLinkBookmarkFolderModal(mode, folderIndex = -1) { const L = getStrings(); const folders = ensureLinkBookmarkFolders(); const isEdit = mode === 'edit'; const safeIndex = getClampedLinkBookmarkFolderIndex(folderIndex, folders); const sourceFolder = isEdit ? folders[safeIndex] : null; if (isEdit && isMagnetArchiveBookmarkFolder(sourceFolder)) return; if (!canPerformLinkBookmarkOrdinaryWrite()) return; const originalName = isEdit ? normalizeLinkBookmarkText(sourceFolder && sourceFolder.folder).trim() : ''; const duplicateCheck = (value) => { const name = normalizeLinkBookmarkText(value).trim(); return !!name && folders.some((folder, index) => normalizeLinkBookmarkText(folder && folder.folder).trim() === name && (!isEdit || index !== safeIndex)); }; const rawName = await showPrompt( isEdit ? L.msg_rename_prompt : L.msg_newfolder_prompt, originalName, isEdit ? L.modal_rename_title : L.title_prompt, { duplicateCheck, duplicateText: L.err_name_exists, maxLen: getLinkBookmarkFieldMaxLen('folder') } ); const name = normalizeLinkBookmarkLimitedText(rawName, 'folder', true); if (!name || (isEdit && name === originalName)) return; if (isLinkBookmarkFieldTooLong(rawName, 'folder')) { showPikPakFileNameLimitTip(); return; } if (duplicateCheck(name)) return; let draftFolders = cloneLinkBookmarkFoldersForEditing(S.linkBookmarkFolders); let selectedIndex = isEdit ? safeIndex : draftFolders.length; if (isEdit) { if (!draftFolders[safeIndex]) return; draftFolders[safeIndex].folder = name; } else { draftFolders.push({ folder: name, list: [] }); } const ok = await saveLinkBookmarkFoldersWithConflictCheck(draftFolders, selectedIndex); if (ok) { const latestFolders = ensureLinkBookmarkFolders(); let safeSelectedIndex = getClampedLinkBookmarkFolderIndex(S.linkBookmarkSelectedFolderIndex, latestFolders); if (isEdit) { const renamedIndex = latestFolders.findIndex(folder => normalizeLinkBookmarkText(folder && folder.folder).trim() === name); safeSelectedIndex = renamedIndex >= 0 ? renamedIndex : safeSelectedIndex; } S.linkBookmarkSelectedFolderIndex = safeSelectedIndex; setLinkBookmarkFolderPageForIndex(safeSelectedIndex); if (!isEdit) S.linkBookmarkLinkPage = 1; if (S.linkBookmarkMode) renderLinkBookmarkPanel(); } } async function deleteLinkBookmarkLink(folderIndex, linkIndex) { const L = getStrings(); const folders = ensureLinkBookmarkFolders(); const safeFolderIndex = getClampedLinkBookmarkFolderIndex(folderIndex, folders); const folder = folders[safeFolderIndex]; const list = Array.isArray(folder && folder.list) ? folder.list : []; const link = list[linkIndex]; if (!link || typeof link !== 'object') return; if (!canPerformLinkBookmarkOrdinaryWrite()) return; const title = normalizeLinkBookmarkText(link.title).trim(); const strong = title.startsWith('PEM_SYNC_META') || title.startsWith('PEM_SYNC_DATA'); const isCloudArchiveLink = isMagnetArchiveBookmarkLink(folder, link); const cloudKey = isCloudArchiveLink ? getMagnetArchivePrimaryKeyFromBookmark(link) : ''; const cloudState = isCloudArchiveLink ? getMagnetArchiveLinkState(folder, link) : null; const cloudDeleteMode = normalizeLinkBookmarkText(cloudState && cloudState.deleteMode).trim(); const cloudStrong = !!(isCloudArchiveLink && cloudDeleteMode && cloudDeleteMode !== 'none'); const confirmMsg = isCloudArchiveLink ? (cloudStrong ? L.msg_magnet_archive_delete_link_irreversible_confirm : L.msg_magnet_archive_delete_link_confirm) : (strong ? L.msg_link_bookmark_delete_sync_link_confirm : L.msg_link_bookmark_delete_link_confirm); const ok = await showLinkBookmarkConfirm(confirmMsg, strong || cloudStrong); if (!ok) return; const draftFolders = cloneLinkBookmarkFoldersForEditing(S.linkBookmarkFolders); if (!draftFolders[safeFolderIndex] || !Array.isArray(draftFolders[safeFolderIndex].list) || !draftFolders[safeFolderIndex].list[linkIndex]) return; draftFolders[safeFolderIndex].list.splice(linkIndex, 1); if (isCloudArchiveLink && cloudKey && !buildMagnetArchiveBookmarkIndex(draftFolders).has(cloudKey)) { const archiveFolderIndex = findMagnetArchiveBookmarkFolderIndex(draftFolders); if (archiveFolderIndex >= 0 && draftFolders[archiveFolderIndex]) { const archiveFolder = normalizeMagnetArchiveManagedFolder(draftFolders[archiveFolderIndex], draftFolders); const stateInfo = getMagnetArchiveStateInfo(archiveFolder); const state = normalizeMagnetArchiveStatePayload(stateInfo ? stateInfo.state : { items: {} }); if (state.items && state.items[cloudKey]) { delete state.items[cloudKey]; setMagnetArchiveFolderState(archiveFolder, state); } } } await saveLinkBookmarkFoldersWithConflictCheck(draftFolders, safeFolderIndex); } async function clearMagnetArchiveBookmarkLinks(folderIndex, mode = 'all') { const L = getStrings(); const folders = ensureLinkBookmarkFolders(); const safeIndex = getClampedLinkBookmarkFolderIndex(folderIndex, folders); const folder = folders[safeIndex]; if (!folder || !isMagnetArchiveBookmarkFolder(folder)) return; if (!canPerformLinkBookmarkOrdinaryWrite(L.msg_link_bookmark_duplicate_folder_write_blocked)) return; const restoredOnly = mode === 'restored'; const list = Array.isArray(folder.list) ? folder.list : []; const targets = list.filter(link => { if (!link || typeof link !== 'object' || isMagnetArchivePseudoLink(link)) return false; if (!getMagnetArchivePrimaryKeyFromBookmark(link)) return false; if (!restoredOnly) return true; const state = getMagnetArchiveLinkState(folder, link); return normalizeLinkBookmarkText(state && state.restoreStatus).trim() === 'done'; }); if (!targets.length) { showToast(restoredOnly ? L.msg_magnet_archive_clear_restored_empty : L.msg_magnet_archive_clear_all_empty, 'info'); return; } const countText = String(targets.length); const confirmText = (restoredOnly ? L.msg_magnet_archive_clear_restored_confirm : L.msg_magnet_archive_clear_all_confirm).replace('{n}', countText); const ok = await showLinkBookmarkConfirm(confirmText, true); if (!ok) return; const draftFolders = cloneLinkBookmarkFoldersForEditing(S.linkBookmarkFolders); const draftFolder = draftFolders[safeIndex]; if (!draftFolder || !isMagnetArchiveBookmarkFolder(draftFolder)) return; normalizeMagnetArchiveManagedFolder(draftFolder, draftFolders); const info = getMagnetArchiveStateInfo(draftFolder); const state = normalizeMagnetArchiveStatePayload(info ? info.state : { items: {} }); const removeKeys = new Set(); let removed = 0; draftFolder.list = (Array.isArray(draftFolder.list) ? draftFolder.list : []).filter(link => { if (!link || typeof link !== 'object' || isMagnetArchivePseudoLink(link)) return true; const key = getMagnetArchivePrimaryKeyFromBookmark(link); if (!key) return true; const item = state.items && state.items[key] ? state.items[key] : null; const shouldRemove = restoredOnly ? normalizeLinkBookmarkText(item && item.restoreStatus).trim() === 'done' : true; if (!shouldRemove) return true; removeKeys.add(key); removed++; return false; }); if (!removed) { showToast(restoredOnly ? L.msg_magnet_archive_clear_restored_empty : L.msg_magnet_archive_clear_all_empty, 'info'); return; } removeKeys.forEach(key => { if (state.items) delete state.items[key]; }); if (!setMagnetArchiveFolderState(draftFolder, state)) { showToast(L.msg_magnet_archive_clear_failed, 'error'); return; } const saved = await saveLinkBookmarkFoldersWithConflictCheck(draftFolders, safeIndex); if (saved) showToast(L.msg_magnet_archive_clear_success.replace('{n}', String(removed)), 'success'); } async function deleteLinkBookmarkFolder(folderIndex) { const L = getStrings(); const folders = ensureLinkBookmarkFolders(); const safeIndex = getClampedLinkBookmarkFolderIndex(folderIndex, folders); const folder = folders[safeIndex]; if (!folder) return; if (isMagnetArchiveBookmarkFolder(folder)) return; if (!canPerformLinkBookmarkOrdinaryWrite()) return; const list = Array.isArray(folder.list) ? folder.list : []; const isSyncFolder = isSyncLinkBookmarkFolder(folder); const message = isSyncFolder ? L.msg_link_bookmark_delete_sync_folder_confirm : (list.length ? L.msg_link_bookmark_delete_non_empty_folder_confirm : L.msg_link_bookmark_delete_folder_confirm); const ok = await showLinkBookmarkConfirm(message, isSyncFolder || list.length > 0); if (!ok) return; let draftFolders = cloneLinkBookmarkFoldersForEditing(S.linkBookmarkFolders); draftFolders.splice(safeIndex, 1); let nextIndex = safeIndex; if (!draftFolders.length) { draftFolders = getDefaultLinkBookmarkFolders(); nextIndex = 0; } else if (nextIndex >= draftFolders.length) { nextIndex = draftFolders.length - 1; } await saveLinkBookmarkFoldersWithConflictCheck(draftFolders, nextIndex); } async function mergeRepairLinkBookmarkDuplicateFolders() { const L = getStrings(); if (!canPerformLinkBookmarkWriteNow()) return; const folders = ensureLinkBookmarkFolders(); if (!hasDuplicateLinkBookmarkFolders(folders)) return; const ok = await showLinkBookmarkConfirm(L.msg_link_bookmark_merge_duplicates_confirm, true); if (!ok) return; const merged = mergeDuplicateLinkBookmarkFolders(folders); const selectedIndex = merged.indexMap[getClampedLinkBookmarkFolderIndex(S.linkBookmarkSelectedFolderIndex, folders)] || 0; await saveLinkBookmarkFoldersWithConflictCheck(merged.folders, selectedIndex, { mergeRepair: true }); } function getLinkBookmarkPositiveInt(value, fallback = 1) { const n = Number(value); return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback; } function clampLinkBookmarkPage(page, totalPages) { const maxPage = Math.max(1, getLinkBookmarkPositiveInt(totalPages, 1)); const n = getLinkBookmarkPositiveInt(page, 1); return Math.min(Math.max(1, n), maxPage); } function getLinkBookmarkPagerTip(kind, L) { const raw = normalizeLinkBookmarkText(kind === 'prev' ? L.btn_prev_video : L.btn_next_video).replace(/\s*\[[^\]]+\]\s*/g, '').trim(); return raw || (kind === 'prev' ? '<' : '>'); } function getLinkBookmarkFolderPageSize(folderList, ordinaryCount, fixedCount = 0) { const rowHeight = 56; const gap = 10; const minSize = 1; const minHeight = rowHeight; const rawHeight = folderList && folderList.clientHeight ? folderList.clientHeight : minHeight; const fixedRows = Math.max(0, Math.floor(Number(fixedCount) || 0)); const fixedHeight = fixedRows ? fixedRows * (rowHeight + gap) : 0; const calcSize = (height) => Math.max(minSize, Math.floor((Math.max(minHeight, height) + gap) / (rowHeight + gap)) || minSize); let size = calcSize(rawHeight - fixedHeight); if (ordinaryCount > size) { const pagerSpace = 44; size = calcSize(rawHeight - fixedHeight - pagerSpace); } return size; } function getLinkBookmarkLinkPageSize(grid, totalCount, searchMode) { const fallback = getLinkBookmarkPositiveInt(S.linkBookmarkLinkPageSize, 1); if (!grid || !grid.clientWidth || !grid.clientHeight) return fallback; const style = window.getComputedStyle ? getComputedStyle(grid) : null; const columnGap = parseFloat(style && style.columnGap) || 14; const rowGap = parseFloat(style && style.rowGap) || 14; const minColWidth = UI.win && UI.win.classList.contains('pk-maximized') ? 300 : (grid.clientWidth < 430 ? 220 : 260); const cardHeight = 156; const sizeKey = `${Math.round(grid.clientWidth)}x${Math.round(grid.clientHeight)}:${minColWidth}:${Math.round(columnGap)}:${Math.round(rowGap)}:${searchMode ? 's' : 'n'}`; if (S.linkBookmarkLinkPageFitKey !== sizeKey) { S.linkBookmarkLinkPageFitKey = sizeKey; S.linkBookmarkLinkPageFitCap = 0; } const columns = Math.max(1, Math.floor((grid.clientWidth + columnGap) / (minColWidth + columnGap)) || 1); let rows = Math.max(1, Math.floor((grid.clientHeight + rowGap) / (cardHeight + rowGap)) || 1); let size = Math.max(1, columns * rows); if (totalCount > size) { const usableHeight = Math.max(cardHeight, grid.clientHeight - 52); rows = Math.max(1, Math.floor((usableHeight + rowGap) / (cardHeight + rowGap)) || 1); size = Math.max(1, columns * rows); } const fitCap = getLinkBookmarkPositiveInt(S.linkBookmarkLinkPageFitCap, 0); if (fitCap > 0) size = Math.min(size, fitCap); return Math.max(1, size); } function adjustLinkBookmarkPageForClippedCards(grid, linkRender, links, linkPageSize) { if (!grid || !linkRender || !Array.isArray(links) || links.length <= 1 || !Array.isArray(linkRender.entries) || linkRender.entries.length <= 1 || linkPageSize <= 1) return false; const cards = Array.from(grid.querySelectorAll('.pk-lbm-card')); if (cards.length <= 1) return false; const lastCard = cards[cards.length - 1]; const gridRect = grid.getBoundingClientRect(); const cardRect = lastCard.getBoundingClientRect(); const clipped = grid.scrollHeight > grid.clientHeight + 2 || grid.scrollWidth > grid.clientWidth + 2 || cardRect.bottom > gridRect.bottom - 2 || cardRect.right > gridRect.right - 2; if (!clipped) return false; S.linkBookmarkLinkPageFitCap = Math.max(1, linkPageSize - 1); const totalPages = Math.max(1, Math.ceil(linkRender.entries.length / S.linkBookmarkLinkPageFitCap)); S.linkBookmarkLinkPage = clampLinkBookmarkPage(S.linkBookmarkLinkPage, totalPages); renderLinkBookmarkPanel(); return true; } function getLinkBookmarkFolderRenderEntries(folders) { const fixed = []; const paged = []; (Array.isArray(folders) ? folders : []).forEach((folder, index) => { if (shouldHideLinkBookmarkFolderInOrdinaryView(folder)) return; const entry = { folder, index }; if (isMagnetArchiveBookmarkFolder(folder)) fixed.push(entry); else paged.push(entry); }); return { fixed, paged }; } function getLinkBookmarkDefaultSelectedFolderIndex(folders) { const entries = getLinkBookmarkFolderRenderEntries(folders); const first = (entries.fixed && entries.fixed[0]) || (entries.paged && entries.paged[0]); return first ? first.index : 0; } function getLinkBookmarkLinkRenderEntries(folders, currentIndex, query, L) { const q = normalizeLinkBookmarkText(query).trim().toLowerCase(); const searchMode = !!q; const entries = []; if (searchMode) { (Array.isArray(folders) ? folders : []).forEach((folder, folderIndex) => { if (shouldHideLinkBookmarkFolderInOrdinaryView(folder)) return; const list = Array.isArray(folder && folder.list) ? folder.list : []; list.forEach((link, linkIndex) => { if (isConfigCloudSyncPseudoLink(link) || isMagnetArchivePseudoLink(link)) return; const title = normalizeLinkBookmarkText(link && link.title).toLowerCase(); const href = normalizeLinkBookmarkText(link && link.href).toLowerCase(); if (title.includes(q) || href.includes(q)) { entries.push({ link, folderIndex, linkIndex, folderName: getLinkBookmarkFolderDisplayName(folder, L) }); } }); }); return { entries, searchMode }; } const folder = folders[getClampedLinkBookmarkFolderIndex(currentIndex, folders)] || folders[0] || getDefaultLinkBookmarkFolders()[0]; const list = Array.isArray(folder && folder.list) ? folder.list : []; list.forEach((link, linkIndex) => { if (isConfigCloudSyncPseudoLink(link) || isMagnetArchivePseudoLink(link)) return; entries.push({ link, folderIndex: currentIndex, linkIndex, folderName: getLinkBookmarkFolderDisplayName(folder, L) }); }); return { entries, searchMode }; } function getLinkBookmarkSearchHighlightHTML(text, query, capacity = 48) { const raw = normalizeLinkBookmarkText(text); const q = normalizeLinkBookmarkText(query).trim(); if (!q) return esc(raw); const idx = raw.toLowerCase().indexOf(q.toLowerCase()); if (idx < 0) return esc(raw); const len = q.length; const cap = Math.max(len + 8, Number(capacity) || 48); const hitEnd = idx + len; let start = 0, end = raw.length, prefix = '', suffix = ''; if (raw.length > cap || hitEnd > cap) { const before = Math.min(15, Math.max(0, Math.floor((cap - len) * 0.38))); const after = Math.max(0, cap - len - before); start = Math.max(0, idx - before); end = Math.min(raw.length, hitEnd + after); if (end - start < cap && start > 0) start = Math.max(0, end - cap); if (end - start < cap && end < raw.length) end = Math.min(raw.length, start + cap); if (start > 0) prefix = '...'; if (end < raw.length) suffix = '...'; } return `${prefix}${esc(raw.substring(start, idx))}${esc(raw.substring(idx, hitEnd))}${esc(raw.substring(hitEnd, end))}${suffix}`; } function setLinkBookmarkFolderPageForIndex(folderIndex) { const folders = ensureLinkBookmarkFolders(); const safeIndex = getClampedLinkBookmarkFolderIndex(folderIndex, folders); const renderEntries = getLinkBookmarkFolderRenderEntries(folders); if (renderEntries.fixed.some(entry => entry.index === safeIndex)) { S.linkBookmarkFolderPage = 1; return; } const firstPageSize = getLinkBookmarkPositiveInt(S.linkBookmarkFolderFirstPageSize, getLinkBookmarkPositiveInt(S.linkBookmarkFolderPageSize, 1)); const pageSize = getLinkBookmarkPositiveInt(S.linkBookmarkFolderPageSize, 1); const pagedPos = renderEntries.paged.findIndex(entry => entry.index === safeIndex); if (pagedPos < 0) { S.linkBookmarkFolderPage = 1; return; } const page = pagedPos < firstPageSize ? 1 : 2 + Math.floor((pagedPos - firstPageSize) / pageSize); const totalPages = renderEntries.paged.length <= firstPageSize ? 1 : 1 + Math.ceil((renderEntries.paged.length - firstPageSize) / pageSize); S.linkBookmarkFolderPage = clampLinkBookmarkPage(page, totalPages); } function setLinkBookmarkLinkPageForIndex(linkIndex) { const pageSize = getLinkBookmarkPositiveInt(S.linkBookmarkLinkPageSize, 1); S.linkBookmarkLinkPage = clampLinkBookmarkPage(Math.floor(Math.max(0, Number(linkIndex) || 0) / pageSize) + 1, Number.MAX_SAFE_INTEGER); } function renderLinkBookmarkPager(container, options) { if (!container) return; const L = getStrings(); const totalPages = getLinkBookmarkPositiveInt(options && options.totalPages, 1); if (totalPages <= 1) { container.hidden = true; container.innerHTML = ''; return; } const page = clampLinkBookmarkPage(options && options.page, totalPages); container.hidden = false; container.innerHTML = ''; if (options && options.showTotal) { const total = document.createElement('div'); total.className = 'pk-lbm-pager-total'; const totalCount = String(getLinkBookmarkPositiveInt(options.totalCount, 0)); const totalText = normalizeLinkBookmarkText(options.totalText); total.textContent = totalText || (L.fmt_link_bookmark_total_links).replace('{n}', totalCount); container.appendChild(total); } const nav = document.createElement('div'); nav.className = 'pk-lbm-pager-nav'; const goPage = (value) => { if (!options || typeof options.onPage !== 'function') { renderLinkBookmarkPager(container, options); return; } const raw = String(value || '').trim(); if (!/^\d+$/.test(raw)) { renderLinkBookmarkPager(container, options); return; } const target = clampLinkBookmarkPage(parseInt(raw, 10), totalPages); options.onPage(target); }; const makeBtn = (kind) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = `pk-lbm-pager-btn pk-lbm-pager-${kind} pk-force-tip`; btn.setAttribute('data-pk-tip', getLinkBookmarkPagerTip(kind, L)); btn.innerHTML = CONF.crumbIcons.right; btn.disabled = kind === 'prev' ? page <= 1 : page >= totalPages; btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (btn.disabled || !options || typeof options.onPage !== 'function') return; options.onPage(kind === 'prev' ? page - 1 : page + 1); }; return btn; }; nav.appendChild(makeBtn('prev')); const info = document.createElement('button'); info.type = 'button'; info.className = 'pk-lbm-pager-info pk-force-tip'; info.setAttribute('data-pk-tip', L.tip_link_bookmark_page_jump); info.textContent = `${page} / ${totalPages}`; info.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const editor = document.createElement('span'); editor.className = 'pk-lbm-pager-jump'; const input = document.createElement('input'); input.type = 'text'; input.inputMode = 'numeric'; input.pattern = '[0-9]*'; input.value = String(page); input.setAttribute('aria-label', L.tip_link_bookmark_page_jump); const suffix = document.createElement('span'); suffix.textContent = `/ ${totalPages}`; editor.appendChild(input); editor.appendChild(suffix); info.replaceWith(editor); let done = false; const finish = (submit) => { if (done) return; done = true; if (submit) goPage(input.value); else renderLinkBookmarkPager(container, options); }; input.onkeydown = (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); finish(true); } else if (ev.key === 'Escape') { ev.preventDefault(); finish(false); } }; input.onblur = () => finish(true); setTimeout(() => { input.focus(); input.select(); }, 0); }; nav.appendChild(info); nav.appendChild(makeBtn('next')); container.appendChild(nav); } function renderLinkBookmarkEmpty(grid, title, desc) { grid.classList.add('is-empty'); grid.innerHTML = ''; const box = document.createElement('div'); box.className = 'pk-lbm-empty'; const icon = document.createElement('div'); icon.className = 'pk-lbm-empty-icon'; icon.innerHTML = CONF.emptySVG; const titleEl = document.createElement('div'); titleEl.className = 'pk-lbm-empty-title'; titleEl.textContent = title; box.appendChild(icon); box.appendChild(titleEl); if (desc) { const descEl = document.createElement('div'); descEl.className = 'pk-lbm-empty-desc'; descEl.textContent = desc; box.appendChild(descEl); } grid.appendChild(box); } function renderLinkBookmarkPanel() { const L = getStrings(); ensureStaticPanelViewportVisible(); const folderIcon = getOfficialFolderFallbackIconHtml(24); const folders = ensureLinkBookmarkFolders(); pruneLinkBookmarkIconRuntimeCache(folders); const duplicateFolderKeys = getLinkBookmarkDuplicateFolderKeys(folders); const hasDuplicateFolders = duplicateFolderKeys.size > 0; const linkBookmarkWriteBusy = isLinkBookmarkWriteBusy(); const linkBookmarkWriteBusyTip = getLinkBookmarkWriteBusyTip(L); const getLinkBookmarkWriteTip = (normalTip, duplicateTip = normalTip) => linkBookmarkWriteBusy ? linkBookmarkWriteBusyTip : (hasDuplicateFolders ? duplicateTip : normalTip); const getLinkBookmarkWriteDisabled = (disabled = false) => linkBookmarkWriteBusy || !!disabled; if (UI.win) { UI.win.classList.remove('pk-grid-view', 'pk-view-switching', 'pk-grid-resizing', 'pk-grid-scrolling', 'pk-mode-trash'); UI.win.classList.add('pk-share-parse-mode', 'pk-share-parse-root-mode'); } const hd = UI.win ? UI.win.querySelector('.pk-grid-hd') : null; if (hd) { hd.style.display = 'none'; hd.style.visibility = ''; hd.classList.remove('pk-grid-view-hd'); } if (UI.vp) { UI.vp.scrollTop = 0; UI.vp.scrollLeft = 0; } if (UI.in) { if (typeof clearGridStableRows === 'function') clearGridStableRows(UI.in); UI.in.style.height = '100%'; UI.in.style.transform = 'none'; UI.in.innerHTML = `
`; const folderList = UI.in.querySelector('#pk-lbm-folder-list'); const folderPager = UI.in.querySelector('#pk-lbm-folder-pager'); const folderRenderEntries = getLinkBookmarkFolderRenderEntries(folders); const allFolderEntries = folderRenderEntries.fixed.concat(folderRenderEntries.paged); let currentIndex = Math.min(Math.max(0, S.linkBookmarkSelectedFolderIndex || 0), folders.length - 1); if (S.linkBookmarkResetSelectOnEnter && (S.linkBookmarkHasLoaded || S.linkBookmarkSyncStatus !== 'syncing')) { currentIndex = getLinkBookmarkDefaultSelectedFolderIndex(folders); S.linkBookmarkFolderPage = 1; S.linkBookmarkLinkPage = 1; S.linkBookmarkResetSelectOnEnter = false; } else if (!allFolderEntries.some(entry => entry.index === currentIndex)) { currentIndex = allFolderEntries.length ? allFolderEntries[0].index : 0; } S.linkBookmarkSelectedFolderIndex = currentIndex; const firstFolderPageSize = getLinkBookmarkFolderPageSize(folderList, folderRenderEntries.paged.length, folderRenderEntries.fixed.length); const folderPageSize = getLinkBookmarkFolderPageSize(folderList, folderRenderEntries.paged.length, 0); S.linkBookmarkFolderFirstPageSize = firstFolderPageSize; S.linkBookmarkFolderPageSize = folderPageSize; const folderTotalPages = folderRenderEntries.paged.length <= firstFolderPageSize ? 1 : 1 + Math.ceil((folderRenderEntries.paged.length - firstFolderPageSize) / folderPageSize); S.linkBookmarkFolderPage = clampLinkBookmarkPage(S.linkBookmarkFolderPage, folderTotalPages); let visibleFolderEntries; if (S.linkBookmarkFolderPage === 1) { visibleFolderEntries = folderRenderEntries.fixed.concat(folderRenderEntries.paged.slice(0, firstFolderPageSize)); } else { const folderPageStart = firstFolderPageSize + (S.linkBookmarkFolderPage - 2) * folderPageSize; visibleFolderEntries = folderRenderEntries.paged.slice(folderPageStart, folderPageStart + folderPageSize); } visibleFolderEntries.forEach(entry => { const folder = entry.folder; const index = entry.index; const folderBtn = document.createElement('div'); folderBtn.tabIndex = 0; folderBtn.setAttribute('role', 'button'); folderBtn.className = `pk-lbm-folder${index === currentIndex ? ' active' : ''}`; folderBtn.dataset.index = String(index); const ico = document.createElement('div'); ico.className = 'pk-lbm-folder-ico'; ico.innerHTML = folderIcon; const main = document.createElement('div'); main.className = 'pk-lbm-folder-main'; const nameEl = document.createElement('div'); nameEl.className = 'pk-lbm-folder-name'; const folderName = getLinkBookmarkFolderDisplayName(folder, L); const folderKey = getLinkBookmarkFolderUniqueKey(folder); nameEl.textContent = folderName; nameEl.setAttribute('data-pk-tip', esc(folderName)); main.appendChild(nameEl); if (duplicateFolderKeys.has(folderKey)) { const dupBadge = document.createElement('span'); dupBadge.className = 'pk-lbm-badge dup'; dupBadge.textContent = L.badge_link_bookmark_duplicate; main.appendChild(dupBadge); } if (normalizeLinkBookmarkText(folder.folder).trim() === CONF.linkBookmarkSyncFolderName) { const badge = document.createElement('span'); badge.className = 'pk-lbm-badge'; badge.textContent = L.badge_link_bookmark_sync; main.appendChild(badge); } const count = document.createElement('div'); count.className = 'pk-lbm-folder-count'; count.textContent = String(getLinkBookmarkVisibleLinkCount(folder)); folderBtn.appendChild(ico); folderBtn.appendChild(main); folderBtn.appendChild(count); const selectFolder = () => { S.linkBookmarkSelectedFolderIndex = index; S.linkBookmarkLinkPage = 1; renderLinkBookmarkPanel(); }; folderBtn.onclick = selectFolder; folderBtn.onkeydown = (e) => { if (e.target !== folderBtn) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectFolder(); } }; folderList.appendChild(folderBtn); }); renderLinkBookmarkPager(folderPager, { page: S.linkBookmarkFolderPage, totalPages: folderTotalPages, totalCount: allFolderEntries.length, showTotal: true, totalText: (L.fmt_link_bookmark_total_folders).replace('{n}', String(getLinkBookmarkPositiveInt(allFolderEntries.length, 0))), onPage: (page) => { S.linkBookmarkFolderPage = page; renderLinkBookmarkPanel(); } }); const currentFolder = folders[currentIndex] || folders[0] || getDefaultLinkBookmarkFolders()[0]; const isCurrentMagnetArchiveFolder = isMagnetArchiveBookmarkFolder(currentFolder); const archiveTools = UI.in.querySelector('#pk-lbm-archive-tools'); if (archiveTools) { archiveTools.innerHTML = ''; if (isCurrentMagnetArchiveFolder) { const addArchiveTool = (id, iconHtml, label, danger, handler, disabled = false, tip = label) => { const btn = document.createElement('button'); btn.type = 'button'; btn.id = id; btn.className = `pk-lbm-btn pk-force-tip${danger ? ' danger' : ''}`; btn.setAttribute('data-pk-tip', tip); btn.innerHTML = iconHtml; btn.disabled = !!disabled; const span = document.createElement('span'); span.textContent = label; btn.appendChild(span); btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (btn.disabled) return; handler(); }; archiveTools.appendChild(btn); }; addArchiveTool('pk-lbm-clear-restored', CONF.icons.delForever, L.btn_magnet_archive_clear_restored, false, () => clearMagnetArchiveBookmarkLinks(currentIndex, 'restored'), getLinkBookmarkWriteDisabled(hasDuplicateFolders), getLinkBookmarkWriteTip(L.tip_magnet_archive_clear_restored, L.msg_link_bookmark_duplicate_folder_write_blocked)); addArchiveTool('pk-lbm-clear-all-links', CONF.icons.emptyTrash, L.btn_magnet_archive_clear_all_links, true, () => clearMagnetArchiveBookmarkLinks(currentIndex, 'all'), getLinkBookmarkWriteDisabled(hasDuplicateFolders), getLinkBookmarkWriteTip(L.tip_magnet_archive_clear_all_links, L.msg_link_bookmark_duplicate_folder_write_blocked)); } } const folderTools = UI.in.querySelector('#pk-lbm-folder-tools'); if (folderTools) { folderTools.innerHTML = ''; const canManageCurrentFolder = currentFolder && !isDefaultLinkBookmarkFolder(currentFolder) && !isCurrentMagnetArchiveFolder; if (canManageCurrentFolder) { const addCurrentFolderTool = (iconHtml, label, danger, handler, disabled = false, tip = label) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = `pk-lbm-btn pk-force-tip${danger ? ' danger' : ''}`; btn.setAttribute('data-pk-tip', tip); btn.innerHTML = iconHtml; btn.disabled = !!disabled; const span = document.createElement('span'); span.textContent = label; btn.appendChild(span); btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (btn.disabled) return; handler(); }; folderTools.appendChild(btn); }; addCurrentFolderTool(CONF.icons.linkBookmarkFolderRename, L.btn_link_bookmark_rename_folder, false, () => openLinkBookmarkFolderModal('edit', currentIndex), getLinkBookmarkWriteDisabled(hasDuplicateFolders), getLinkBookmarkWriteTip(L.tip_link_bookmark_rename_folder, L.msg_link_bookmark_duplicate_folder_write_blocked)); addCurrentFolderTool(CONF.icons.linkBookmarkFolderDel, L.btn_link_bookmark_delete_folder, true, () => deleteLinkBookmarkFolder(currentIndex), getLinkBookmarkWriteDisabled(hasDuplicateFolders), getLinkBookmarkWriteTip(L.tip_link_bookmark_delete_folder, L.msg_link_bookmark_duplicate_folder_write_blocked)); } } const syncTip = UI.in.querySelector('#pk-lbm-sync-tip'); if (syncTip) { const showTip = normalizeLinkBookmarkText(currentFolder.folder).trim() === CONF.linkBookmarkSyncFolderName; syncTip.style.display = showTip ? 'flex' : 'none'; const tipText = syncTip.querySelector('span'); if (tipText) tipText.textContent = L.tip_link_bookmark_sync_folder; } const statusEl = UI.in.querySelector('#pk-lbm-status'); if (statusEl) { statusEl.className = `pk-lbm-status ${S.linkBookmarkSyncStatus || 'idle'}`; const textEl = statusEl.querySelector('span:last-child'); if (textEl) textEl.textContent = getLinkBookmarkSyncLabel(S.linkBookmarkSyncStatus, L); } const duplicateWarning = UI.in.querySelector('#pk-lbm-duplicate-warning'); if (duplicateWarning) { duplicateWarning.style.display = hasDuplicateFolders ? 'flex' : 'none'; const textEl = duplicateWarning.querySelector('span'); if (textEl) textEl.textContent = L.msg_link_bookmark_duplicate_folders_detected; const mergeBtn = duplicateWarning.querySelector('#pk-lbm-merge-duplicates'); if (mergeBtn) { mergeBtn.textContent = L.btn_link_bookmark_merge_duplicates; mergeBtn.disabled = linkBookmarkWriteBusy; mergeBtn.setAttribute('data-pk-tip', linkBookmarkWriteBusy ? linkBookmarkWriteBusyTip : L.btn_link_bookmark_merge_duplicates); mergeBtn.onclick = () => { if (mergeBtn.disabled) return; mergeRepairLinkBookmarkDuplicateFolders(); }; } } const alertEl = UI.in.querySelector('#pk-lbm-alert'); if (alertEl && S.linkBookmarkError) { let alertClass = `pk-lbm-alert${S.linkBookmarkSyncStatus === 'conflict' ? ' conflict' : ''}`; if (S.linkBookmarkError === L.msg_link_bookmark_save_verify_failed) alertClass += ' verify'; alertEl.className = alertClass; alertEl.textContent = S.linkBookmarkError; alertEl.style.display = 'block'; } const searchInput = UI.in.querySelector('#pk-lbm-search-input'); const searchWrap = UI.in.querySelector('#pk-lbm-search-wrap'); const searchSubmitBtn = UI.in.querySelector('#pk-lbm-search-submit'); const searchClearBtn = UI.in.querySelector('#pk-lbm-search-clear'); if (searchInput) { const draftValue = typeof S.linkBookmarkSearchDraft === 'string' ? S.linkBookmarkSearchDraft : (S.linkBookmarkSearch || ''); searchInput.value = draftValue; const syncLinkBookmarkSearchDraftUI = () => { const hasValue = !!normalizeLinkBookmarkText(searchInput.value).trim(); if (searchWrap) searchWrap.classList.toggle('has-value', hasValue); }; const commitLinkBookmarkSearch = () => { const prevQ = normalizeLinkBookmarkText(S.linkBookmarkSearch).trim(); S.linkBookmarkSearchDraft = searchInput.value; S.linkBookmarkSearch = searchInput.value; const nextQ = normalizeLinkBookmarkText(S.linkBookmarkSearch).trim(); if (nextQ !== prevQ) S.linkBookmarkLinkPage = 1; renderLinkBookmarkPanel(); const nextInput = UI.in && UI.in.querySelector('#pk-lbm-search-input'); if (nextInput) { nextInput.focus(); const end = nextInput.value.length; try { nextInput.setSelectionRange(end, end); } catch(e) {} } }; syncLinkBookmarkSearchDraftUI(); searchInput.oninput = () => { S.linkBookmarkSearchDraft = searchInput.value; syncLinkBookmarkSearchDraftUI(); if (!normalizeLinkBookmarkText(searchInput.value).trim() && normalizeLinkBookmarkText(S.linkBookmarkSearch).trim()) { S.linkBookmarkSearch = ''; S.linkBookmarkSearchDraft = ''; S.linkBookmarkLinkPage = 1; renderLinkBookmarkPanel(); const nextInput = UI.in && UI.in.querySelector('#pk-lbm-search-input'); if (nextInput) { nextInput.focus(); try { nextInput.setSelectionRange(0, 0); } catch(e) {} } } }; searchInput.onkeydown = (e) => { if (e.key !== 'Enter' || e.isComposing || e.keyCode === 229) return; e.preventDefault(); commitLinkBookmarkSearch(); }; bindPlainTextMaxLengthLimits(UI.in, [['#pk-lbm-search-input', ((getConfigLimitSchema('pk_search_history') || {}).localLimit || {}).maxFieldLen || 1024]]); if (searchSubmitBtn) searchSubmitBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); commitLinkBookmarkSearch(); }; if (searchClearBtn) searchClearBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const hadSearch = !!normalizeLinkBookmarkText(S.linkBookmarkSearch).trim(); searchInput.value = ''; S.linkBookmarkSearchDraft = ''; if (hadSearch) { S.linkBookmarkSearch = ''; S.linkBookmarkLinkPage = 1; renderLinkBookmarkPanel(); } else { syncLinkBookmarkSearchDraftUI(); searchInput.focus(); } }; } const newFolderBtn = UI.in.querySelector('#pk-lbm-new-folder'); if (newFolderBtn) { newFolderBtn.disabled = getLinkBookmarkWriteDisabled(hasDuplicateFolders); newFolderBtn.setAttribute('data-pk-tip', getLinkBookmarkWriteTip(L.tip_newfolder, L.msg_link_bookmark_duplicate_folder_write_blocked)); newFolderBtn.onclick = () => { if (newFolderBtn.disabled) return; openLinkBookmarkFolderModal('new'); }; } const newLinkBtn = UI.in.querySelector('#pk-lbm-new-link'); if (newLinkBtn) { if (isCurrentMagnetArchiveFolder) { newLinkBtn.style.display = 'none'; newLinkBtn.disabled = true; newLinkBtn.onclick = null; } else { newLinkBtn.disabled = getLinkBookmarkWriteDisabled(hasDuplicateFolders); newLinkBtn.setAttribute('data-pk-tip', getLinkBookmarkWriteTip(L.tip_link_bookmark_new_link, L.msg_link_bookmark_duplicate_folder_write_blocked)); newLinkBtn.onclick = () => { if (newLinkBtn.disabled) return; openLinkBookmarkLinkModal('new', currentIndex); }; } } const refreshBtn = UI.in.querySelector('#pk-lbm-refresh-official'); if (refreshBtn) { refreshBtn.disabled = linkBookmarkWriteBusy; refreshBtn.setAttribute('data-pk-tip', linkBookmarkWriteBusy ? linkBookmarkWriteBusyTip : L.tip_refresh); refreshBtn.onclick = () => { if (refreshBtn.disabled) return; loadLinkBookmarkOfficial(true); }; } const grid = UI.in.querySelector('#pk-lbm-grid'); const linkPager = UI.in.querySelector('#pk-lbm-link-pager'); const q = normalizeLinkBookmarkText(S.linkBookmarkSearch).trim(); const linkRender = getLinkBookmarkLinkRenderEntries(folders, currentIndex, q, L); const linkPageSize = getLinkBookmarkLinkPageSize(grid, linkRender.entries.length, linkRender.searchMode); S.linkBookmarkLinkPageSize = linkPageSize; const linkTotalPages = Math.max(1, Math.ceil(linkRender.entries.length / linkPageSize)); S.linkBookmarkLinkPage = clampLinkBookmarkPage(S.linkBookmarkLinkPage, linkTotalPages); const linkPageStart = (S.linkBookmarkLinkPage - 1) * linkPageSize; const links = linkRender.entries.slice(linkPageStart, linkPageStart + linkPageSize); if (S.linkBookmarkSyncStatus === 'failed' && !S.linkBookmarkHasLoaded && S.linkBookmarkError) { const readFailTitle = S.linkBookmarkError === L.msg_link_bookmark_data_invalid ? S.linkBookmarkError : L.msg_link_bookmark_read_failed_retry; renderLinkBookmarkEmpty(grid, readFailTitle, ''); renderLinkBookmarkPager(linkPager, { page: 1, totalPages: 1 }); } else if (!linkRender.entries.length) { renderLinkBookmarkEmpty(grid, q ? L.msg_link_bookmark_no_match : L.msg_link_bookmark_empty, ''); renderLinkBookmarkPager(linkPager, { page: 1, totalPages: 1 }); } else { grid.classList.remove('is-empty'); grid.innerHTML = ''; links.forEach(entry => { const link = entry.link; const folderIndex = entry.folderIndex; const linkIndex = entry.linkIndex; const card = document.createElement('article'); card.className = `pk-lbm-card${linkRender.searchMode ? ' has-source' : ''}`; card.draggable = false; card.addEventListener('dragstart', (e) => { e.preventDefault(); e.stopPropagation(); }); const head = document.createElement('div'); head.className = 'pk-lbm-card-head'; const iconWrap = document.createElement('div'); iconWrap.className = 'pk-lbm-link-icon'; iconWrap.draggable = false; iconWrap.addEventListener('dragstart', (e) => { e.preventDefault(); e.stopPropagation(); }); const fallback = document.createElement('span'); fallback.draggable = false; fallback.innerHTML = CONF.icons.linkBookmarkFallback; const iconState = getLinkBookmarkIconRuntimeState(link); const icon = iconState.src; if (icon && !iconState.failed) { const img = document.createElement('img'); img.alt = ''; img.draggable = false; img.referrerPolicy = 'no-referrer'; img.decoding = 'async'; img.addEventListener('dragstart', (e) => { e.preventDefault(); e.stopPropagation(); }); if (iconState.ok) iconWrap.classList.add('pk-lbm-link-icon-has-img'); img.onload = () => { setLinkBookmarkIconRuntimeState(iconState.key, icon, 'ok'); iconWrap.classList.add('pk-lbm-link-icon-has-img'); }; img.onerror = () => { setLinkBookmarkIconRuntimeState(iconState.key, icon, 'failed'); iconWrap.classList.remove('pk-lbm-link-icon-has-img'); img.remove(); fallback.style.display = 'flex'; }; img.src = icon; fallback.style.display = 'none'; iconWrap.appendChild(img); } iconWrap.appendChild(fallback); const actionFolder = folders[getClampedLinkBookmarkFolderIndex(folderIndex, folders)]; const isCloudArchiveLink = isMagnetArchiveBookmarkLink(actionFolder, link); const cloudArchiveRestoreState = isCloudArchiveLink ? getMagnetArchiveLinkState(actionFolder, link) : null; const titleWrap = document.createElement('div'); titleWrap.className = 'pk-lbm-link-title-wrap'; const titleEl = document.createElement('div'); titleEl.className = 'pk-lbm-link-title'; const displayTitle = normalizeLinkBookmarkText(link.title).trim() || L.card_link_bookmark_unnamed_title; if (linkRender.searchMode) { titleEl.innerHTML = getLinkBookmarkSearchHighlightHTML(displayTitle, q, 24); titleEl.setAttribute('data-pk-tip-html', getLinkBookmarkSearchHighlightHTML(displayTitle, q, Infinity)); } else titleEl.textContent = displayTitle; titleEl.setAttribute('data-pk-tip', esc(displayTitle)); titleWrap.appendChild(titleEl); if (isCloudArchiveLink) { const restoreView = getMagnetArchiveRestoreStatusView(cloudArchiveRestoreState, L); const statusDot = document.createElement('span'); statusDot.className = `pk-lbm-restore-dot pk-force-tip ${restoreView.className}`; statusDot.setAttribute('data-pk-tip', restoreView.tip); statusDot.setAttribute('aria-label', restoreView.label); titleWrap.appendChild(statusDot); } head.appendChild(iconWrap); head.appendChild(titleWrap); const urlEl = document.createElement('div'); urlEl.className = 'pk-lbm-url'; const displayHref = normalizeLinkBookmarkText(link.href).trim() || L.card_link_bookmark_empty_href; if (linkRender.searchMode) { urlEl.innerHTML = getLinkBookmarkSearchHighlightHTML(displayHref, q, 56); urlEl.setAttribute('data-pk-tip-html', getLinkBookmarkSearchHighlightHTML(displayHref, q, Infinity)); } else urlEl.textContent = displayHref; urlEl.setAttribute('data-pk-tip', esc(displayHref)); const metaEl = document.createElement('div'); metaEl.className = 'pk-lbm-meta'; metaEl.appendChild(urlEl); let sourceEl = null; if (linkRender.searchMode) { sourceEl = document.createElement('div'); sourceEl.className = 'pk-lbm-source-folder'; sourceEl.innerHTML = folderIcon; const sourceText = document.createElement('span'); const sourceFolderName = normalizeLinkBookmarkText(entry.folderName).trim() || L.folder_link_bookmark_unnamed; sourceText.textContent = sourceFolderName; sourceText.setAttribute('data-pk-tip', esc(sourceFolderName)); sourceEl.appendChild(sourceText); metaEl.appendChild(sourceEl); } const actions = document.createElement('div'); actions.className = 'pk-lbm-card-actions'; const addAction = (iconHtml, label, danger, handler, disabled = false, tip = label) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = `pk-lbm-card-act pk-force-tip${danger ? ' danger' : ''}`; btn.setAttribute('data-pk-tip', tip); btn.innerHTML = iconHtml; btn.disabled = !!disabled; const span = document.createElement('span'); span.textContent = label; btn.appendChild(span); btn.onclick = () => { if (btn.disabled) return; handler(); }; actions.appendChild(btn); }; const writeActionDisabled = getLinkBookmarkWriteDisabled(hasDuplicateFolders); const writeActionTip = getLinkBookmarkWriteTip(L.btn_link_bookmark_edit, L.msg_link_bookmark_duplicate_folder_write_blocked); const deleteActionTip = getLinkBookmarkWriteTip(L.btn_link_bookmark_delete, L.msg_link_bookmark_duplicate_folder_write_blocked); addAction(isCloudArchiveLink ? CONF.icons.eye : CONF.icons.openLink, isCloudArchiveLink ? L.btn_link_bookmark_parse : L.btn_link_bookmark_open, false, isCloudArchiveLink ? () => handleMagnetArchiveRestoreBookmark(folderIndex, linkIndex) : () => openLinkBookmarkHref(folderIndex, linkIndex), isCloudArchiveLink && linkBookmarkWriteBusy, isCloudArchiveLink && linkBookmarkWriteBusy ? linkBookmarkWriteBusyTip : (isCloudArchiveLink ? L.btn_link_bookmark_parse : L.btn_link_bookmark_open)); addAction(CONF.icons.copy, L.btn_link_bookmark_copy, false, () => copyLinkBookmarkHref(folderIndex, linkIndex)); addAction(CONF.icons.rename, L.btn_link_bookmark_edit, false, () => openLinkBookmarkLinkModal('edit', folderIndex, linkIndex), writeActionDisabled, writeActionTip); addAction(CONF.icons.del, L.btn_link_bookmark_delete, true, () => deleteLinkBookmarkLink(folderIndex, linkIndex), writeActionDisabled, deleteActionTip); card.appendChild(head); card.appendChild(metaEl); card.appendChild(actions); grid.appendChild(card); }); if (adjustLinkBookmarkPageForClippedCards(grid, linkRender, links, linkPageSize)) return; const finalLinkPageSize = getLinkBookmarkPositiveInt(S.linkBookmarkLinkPageSize, linkPageSize); const finalLinkTotalPages = Math.max(1, Math.ceil(linkRender.entries.length / finalLinkPageSize)); S.linkBookmarkLinkPage = clampLinkBookmarkPage(S.linkBookmarkLinkPage, finalLinkTotalPages); renderLinkBookmarkPager(linkPager, { page: S.linkBookmarkLinkPage, totalPages: finalLinkTotalPages, totalCount: linkRender.entries.length, showTotal: true, onPage: (page) => { S.linkBookmarkLinkPage = page; renderLinkBookmarkPanel(); } }); } } if (UI.pop) { UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; } if (UI.ctx) UI.ctx.style.display = 'none'; if (typeof requestAutoHideButtonTextCheck === 'function') requestAutoHideButtonTextCheck(); } let linkBookmarkResizeRenderTimer = 0; const scheduleLinkBookmarkResizeRender = () => { if (!S.linkBookmarkMode) return; clearTimeout(linkBookmarkResizeRenderTimer); linkBookmarkResizeRenderTimer = setTimeout(() => { if (S.linkBookmarkMode) renderLinkBookmarkPanel(); }, 120); }; window.addEventListener('resize', scheduleLinkBookmarkResizeRender, true); if (window.visualViewport) window.visualViewport.addEventListener('resize', scheduleLinkBookmarkResizeRender, { passive: true }); function renderList() { if (S.linkBookmarkMode) { if (UI.win) UI.win.classList.remove('pk-share-parse-mode', 'pk-share-parse-root-mode'); renderLinkBookmarkPanel(); return; } if (S.shareParseMode && !S.shareParseListActive) { renderShareParsePanel(); return; } if (S.shareParseMode) { if (UI.win) { UI.win.classList.add('pk-share-parse-mode'); UI.win.classList.remove('pk-share-parse-root-mode', 'pk-analyze-group-tools-on'); } if (UI.btnAnaSelect) UI.btnAnaSelect.style.display = 'none'; if (UI.btnAnaSort) UI.btnAnaSort.style.display = 'none'; syncShareParseMirror(); } else if (UI.win) { UI.win.classList.remove('pk-share-parse-mode', 'pk-share-parse-root-mode'); } syncLayoutMetrics(); UI.win.classList.toggle('pk-grid-view', isGridView()); renderViewSwitch(); const headerEl = UI.win.querySelector('.pk-grid-hd'); if (headerEl) headerEl.style.display = ''; const isEmptyDisplay = S.display.length === 0; if (isGridView()) { const gridLayout = getGridLayout(); S._gridLayoutKey = getGridLayoutKey(); if (isEmptyDisplay) { clearGroupedGridMetaCache(); UI.in.style.height = '100%'; } else if (isGroupedGridView()) { const dupGridMeta = getGroupedGridMeta(true); UI.in.style.height = `${Math.max(0, dupGridMeta ? dupGridMeta.totalHeight : 0)}px`; scheduleGridRelayout(); } else { clearGroupedGridMetaCache(); UI.in.style.height = `${Math.ceil(S.display.length / gridLayout.cols) * CONF.rowHeight}px`; scheduleGridRelayout(); } } else { S._gridLayoutKey = ''; clearGroupedGridMetaCache(); clearGridStableRows(UI.in); if (UI.win) UI.win.classList.remove('pk-grid-view', 'pk-grid-resizing', 'pk-grid-scrolling'); UI.in.style.height = isEmptyDisplay ? '100%' : `${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 showShareInsightPathCol = !!(S.shareParseMode && S.shareParseInsightMode); const showPathCol = S.shareParseMode ? showShareInsightPathCol : (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.shareParseMode) { colDef = S.shareParseInsightMode ? "36px 2fr 1fr 80px 120px 130px" : "36px 1fr 80px 120px 130px"; } else if (S.historyMode) { colDef = "36px 30px 1fr 80px 80px 120px 160px"; } else if (S.trashMode) { colDef = "36px 30px 1fr 80px 120px 130px"; } else if (S.recentMode) { colDef = "36px 30px 1fr 80px 120px 130px"; } else if (isAnalyzeRoot) { colDef = "36px 30px 2fr 1fr 80px 130px"; } else if (showPathCol) { colDef = "36px 30px 2fr 1fr 80px 120px 130px"; } else { colDef = "36px 30px 1fr 80px 120px 130px"; } const hd = UI.win.querySelector('.pk-grid-hd'); if (hd) { if (hd.dataset.pkInitialGridHd === '1' && isGridView()) delete hd.dataset.pkInitialGridHd; if (!S._folderViewSyncHold && !S._locateGridSyncPending && !S._shareParseGridExitSync) hd.style.visibility = ''; if (isGridView()) { const gridMeta = getGridSortMeta(); const hideGroupedRootGridHeadTools = S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups); const hideGridFolderFirst = S.historyMode || S.isFlattened || S.dupMode || isAnalyzeRoot || (S.shareParseMode && S.shareParseInsightMode); const keepHistoryGridHoverHead = S.historyMode && hd.classList.contains('pk-grid-view-hd') && !!hd.querySelector('.pk-grid-sort-wrap') && (hd.matches(':hover') || S.gridSortMenuOpen); if (keepHistoryGridHoverHead) { UI.chkAll = hd.querySelector('#pk-all'); if (UI.chkAll) UI.chkAll.onclick = S.handleSelectAll; const btnGridInv = hd.querySelector('#pk-grid-invert'); if (btnGridInv) { btnGridInv.style.display = (!S.dupMode && S.getSelectedCount() > 0) ? 'flex' : 'none'; btnGridInv.style.color = btnGridInv.matches(':hover') ? 'var(--pk-pri)' : 'var(--pk-fg)'; } UI.gridSortWrap = hd.querySelector('.pk-grid-sort-wrap'); if (UI.gridSortWrap) UI.gridSortWrap.classList.toggle('open', !!S.gridSortMenuOpen); } else { hd.classList.add('pk-grid-view-hd'); hd.style.gridTemplateColumns = ''; hd.innerHTML = `
${CONF.icons.invert} ${L.btn_invert}
${getGridSortOptions().map(opt => { const state = resolveGridSortOptionState(opt); return `
${state.icon}${state.label}
`; }).join('')}
`; UI.chkAll = hd.querySelector('#pk-all'); if (UI.chkAll) UI.chkAll.onclick = S.handleSelectAll; UI.btnGridFolderFirst = hd.querySelector('#pk-grid-folder-first'); if (UI.btnGridFolderFirst) { UI.btnGridFolderFirst.onclick = (e) => { e.stopPropagation(); toggleFolderFirst(); }; } const btnGridInv = hd.querySelector('#pk-grid-invert'); if (btnGridInv) { btnGridInv.onclick = (e) => { e.stopPropagation(); if (S.display.length === 0) return; S.invertSelection(); renderVisible(); updateStat(); }; btnGridInv.onmouseenter = () => btnGridInv.style.color = 'var(--pk-pri)'; btnGridInv.onmouseleave = () => btnGridInv.style.color = 'var(--pk-fg)'; } UI.gridSortWrap = hd.querySelector('.pk-grid-sort-wrap'); if (UI.gridSortWrap && S.gridSortMenuOpen) UI.gridSortWrap.classList.add('open'); const sortTrigger = hd.querySelector('#pk-grid-sort-trigger'); if (sortTrigger) { sortTrigger.onclick = (e) => { e.stopPropagation(); if (!UI.gridSortWrap) return; const willOpen = !UI.gridSortWrap.classList.contains('open'); S.gridSortMenuOpen = willOpen; UI.gridSortWrap.classList.toggle('open', willOpen); }; } bindGridSortOptions(hd); if (!S.gridSortDismissBound) { document.addEventListener('mousedown', closeGridSortMenu, true); document.addEventListener('scroll', closeGridSortMenu, true); window.addEventListener('resize', closeGridSortMenu); S.gridSortDismissBound = true; } } } else { hd.classList.remove('pk-grid-view-hd'); hd.style.gridTemplateColumns = colDef; UI.gridSortWrap = null; closeGridSortMenu(); } if (!isGridView() && S.offlineMode) { if (!hd.querySelector('[data-k="offline_status"]')) { hd.innerHTML = `
${L.col_name}
${L.col_size}
${L.col_task_status}
${L.col_task_progress}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (!isGridView() && S.historyMode) { if (!hd.querySelector('[data-k="play_time"]')) { hd.innerHTML = `
${L.col_name}
${CONF.icons.invert} ${L.btn_invert}
${L.col_size}
${L.col_duration_only}
${L.col_progress}
${L.col_play_time}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; const btnInv = hd.querySelector('#pk-btn-invert'); if (btnInv) { btnInv.onclick = (e) => { e.stopPropagation(); if (S.display.length === 0) return; S.invertSelection(); renderVisible(); updateStat(); }; btnInv.onmouseenter = () => btnInv.style.color = 'var(--pk-pri)'; btnInv.onmouseleave = () => btnInv.style.color = 'var(--pk-fg)'; } } } else if (!isGridView() && S.uploadMode) { if (!hd.querySelector('[data-k="upload_speed"]')) { hd.innerHTML = `
${L.col_name}
${L.col_size}
${L.col_up_speed}
${L.col_up_status}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; } } else if (!isGridView() && S.shareMode) { if (!hd.querySelector('[data-k="share_status"]')) { hd.innerHTML = `
${L.col_name}
${L.col_view}
${L.col_save}
${L.col_share_status}
${L.col_share_time}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; const btnInv = hd.querySelector('#pk-btn-invert'); if(btnInv) { btnInv.onclick = (e) => { e.stopPropagation(); if (S.display.length === 0) return; S.invertSelection(); renderVisible(); updateStat(); }; btnInv.onmouseenter = () => btnInv.style.color = 'var(--pk-pri)'; btnInv.onmouseleave = () => btnInv.style.color = 'var(--pk-fg)'; } } } else if (!isGridView()) { const isAnalyzeRoot = S.analyzeMode && cur.id === 'analyze_root'; const hasDurationCol = !!hd.querySelector('[data-k="duration"]'); const hasHistoryCol = !!hd.querySelector('[data-k="play_time"]'); const hasOfflineCol = !!hd.querySelector('[data-k="offline_status"]'); const missingListFolderFirst = !S.shareParseMode && !S.trashMode && !S.shareMode && !S.offlineMode && !S.uploadMode && !S.historyMode && !isAnalyzeRoot && !S.isFlattened && !S.dupMode && !hd.querySelector('#pk-btn-folder-first'); if (S.shareParseMode || missingListFolderFirst || hasDurationCol === isAnalyzeRoot || !hd.querySelector('[data-k="name"]') || hasHistoryCol || hasOfflineCol) { hd.innerHTML = `
${L.col_name}
${(!isAnalyzeRoot && !S.isFlattened && !S.dupMode && !(S.shareParseMode && S.shareParseInsightMode)) ? `
${CONF.icons.folderFirst} ${L.lbl_folder_first}
` : ''}
${CONF.icons.invert} ${L.btn_invert}
${L.col_size}
${!isAnalyzeRoot ? `
${L.col_dur}
` : ''}
${L.col_date}
`; const chk = hd.querySelector('#pk-all'); chk.onclick = S.handleSelectAll; UI.chkAll = chk; const btnFF = hd.querySelector('#pk-btn-folder-first'); if (btnFF) { UI.btnFolderFirst = btnFF; S.renderFolderFirst = () => { if (UI.btnFolderFirst) UI.btnFolderFirst.style.color = S.folderFirst ? 'var(--pk-pri)' : '#666'; if (UI.btnGridFolderFirst) UI.btnGridFolderFirst.style.color = S.folderFirst ? 'var(--pk-pri)' : '#666'; }; btnFF.onclick = (e) => { e.stopPropagation(); toggleFolderFirst(); }; if(S.renderFolderFirst) S.renderFolderFirst(); } else { UI.btnFolderFirst = null; } const btnInv = hd.querySelector('#pk-btn-invert'); if(btnInv) { btnInv.onclick = (e) => { e.stopPropagation(); if (S.display.length === 0) return; S.invertSelection(); 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 = S.shareParseMode ? 'none' : 'flex'; colStar.style.opacity = '1'; colStar.style.pointerEvents = 'auto'; } if (colDur) { colDur.style.display = isAnalyzeRoot ? 'none' : 'flex'; colDur.style.opacity = '1'; colDur.style.pointerEvents = 'auto'; } if (colSize) colSize.style.display = 'flex'; const dateHeader = hd.querySelector('[data-k="modified_time"]'); if (dateHeader) { dateHeader.childNodes[0].textContent = S.trashMode ? L.col_remaining : L.col_date; } } } const currentCols = isGridView() ? [] : hd.querySelectorAll('.pk-col'); currentCols.forEach(c => { const span = c.querySelector('span'); if (span) { span.style.whiteSpace = 'pre'; span.style.fontSize = '11px'; span.style.lineHeight = '1'; } const isSimFolderView = (isAnalyzeRoot && S.analyzeSimGroups); const sortPrefKey = getSortPrefKey(); const legalSorts = sortPrefKey ? getLegalSortsForContext(sortPrefKey) : null; const isIllegalContextSort = !!(sortPrefKey && !legalSorts.includes(c.dataset.k)); if (S.dupMode || S.offlineMode || S.uploadMode || isSimFolderView || isIllegalContextSort) { 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; applyManualSortSelection(k); }; 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) { const syncNameHover = () => nameWrap.style.color = (S.sort === 'name') ? 'var(--pk-pri)' : (nameWrap.matches(':hover') ? 'var(--pk-fg)' : '#666'); syncNameHover(); nameWrap.onmouseenter = syncNameHover; nameWrap.onmouseleave = syncNameHover; } } else { c.style.color = (c.dataset.k === S.sort) ? 'var(--pk-pri)' : ''; } } }); UI.chkAll = hd.querySelector('#pk-all'); if (UI.chkAll) { UI.chkAll.onclick = S.handleSelectAll; const totalSelectable = S.getSelectableCount(); const selectedCount = S.getSelectedCount(); UI.chkAll.checked = totalSelectable > 0 && selectedCount === totalSelectable; UI.chkAll.indeterminate = selectedCount > 0 && selectedCount < totalSelectable; } requestAnimationFrame(() => { renderVisible(); if (S._shareParseGridExitSync) { endShareParseGridExitSync(); return; } if (S._linkBookmarkGridExitSync) { endLinkBookmarkGridExitSync(); return; } if (!isGridView()) { requestAnimationFrame(() => { endFolderViewSync(); if (UI.win) UI.win.classList.remove('pk-view-switching'); }); } }); } function renderVisible() { const L = getStrings(); if (S.linkBookmarkMode) { renderLinkBookmarkPanel(); return; } if (S.shareParseMode && !S.shareParseListActive) { renderShareParsePanel(); return; } if (S.shareParseMode) { if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); syncShareParseMirror(); } const isGridMode = isGridView(); const gridLayout = isGridMode ? getGridLayout() : null; const winEl = el.querySelector('.pk-win'); const listIn = el.querySelector('#pk-in'); if (winEl && listIn) { const isMax = winEl.classList.contains('pk-maximized'); const cur = S.path[S.path.length - 1]; const isGroupedView = S.dupMode || (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups); const gap = isGroupedView ? (isMax ? 10 : 4) : 0; listIn.style.transform = gap > 0 ? `translateY(-${gap}px)` : 'none'; } if (S.display.length === 0) { clearGridStableRows(UI.in); if (S.shareParseMode && S.shareParseListActive) { if (S.shareParseInsightMode) { if (S.shareParseInsightError) { renderShareParseListStatus(S.shareParseInsightError, 'err'); return; } } if (S.shareParseListLoading) { renderShareParseListStatus(L.msg_share_parse_loading_files, 'loading'); return; } if (S.shareParseListError) { renderShareParseListStatus(S.shareParseListError, 'err'); return; } } if (S.loading) { const keepProgressiveSearchLoader = !!((S.offlineMode || S.recentMode) && S.search); const keepHistoryLoader = !!S.historyMode; const shouldKeepExistingLoader = keepProgressiveSearchLoader || keepHistoryLoader; if (shouldKeepExistingLoader && UI.in.querySelector('[data-pk-progressive-search-loading="1"]')) { UI.in.style.height = '100%'; UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; if (UI.ctx) UI.ctx.style.display = 'none'; return; } UI.in.style.height = '100%'; UI.in.innerHTML = `
${L.loading_detail}
`; UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; if (UI.ctx) UI.ctx.style.display = 'none'; return; } if (S.items.length > 0 && !S.search && !S.dupMode && !S.trashMode && !S.shareMode && !S.offlineMode && !S.uploadMode && !S.isFlattened && !(S.shareParseMode && S.shareParseInsightMode)) { return; } if (UI.in.querySelector('.pk-empty')) { if (UI.in.style.height !== '100%') UI.in.style.height = '100%'; return; } UI.in.style.height = '100%'; UI.in.innerHTML = `
${CONF.emptySVG}
${L.str_no_files}
`; UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; if (UI.ctx) UI.ctx.style.display = 'none'; return; } const dupGridMeta = isGroupedGridView() ? getGroupedGridMeta() : null; if (isGridMode && gridLayout) { UI.in.style.height = `${Math.max(0, dupGridMeta ? dupGridMeta.totalHeight : Math.ceil(S.display.length / gridLayout.cols) * CONF.rowHeight + gridLayout.gapY)}px`; } else { UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; } const top = UI.vp.scrollTop; const h = UI.vp.clientHeight; const buffer = CONF.buffer || 20; let start = 0; let end = 0; let visibleIndices = null; if (dupGridMeta) { const metaGridLayout = dupGridMeta.gridLayout || gridLayout || {}; const toFiniteNumber = (value) => { const n = Number(value); return Number.isFinite(n) ? n : null; }; const toPositiveNumber = (value) => { const n = toFiniteNumber(value); return n !== null && n > 0 ? n : 0; }; const clampInt = (value, min, max) => Math.max(min, Math.min(max, value)); const gridCardHeight = toPositiveNumber(metaGridLayout.cardHeight); const gridGapY = Math.max(0, toFiniteNumber(metaGridLayout.gapY) || 0); const baseGridRowHeight = toPositiveNumber(dupGridMeta.rowStride) || (gridCardHeight ? gridCardHeight + gridGapY : (toPositiveNumber(CONF.rowHeight) || 1)); const dupGridBufferRows = Math.max(3, Math.min(5, Math.ceil(buffer / 5))); const sectionBuffer = dupGridBufferRows * baseGridRowHeight; const rangeTop = top - sectionBuffer; const rangeBottom = top + h + sectionBuffer; visibleIndices = []; const appendDupGridItemWindow = (itemIndices, cols, rowStart, rowEnd) => { if (!itemIndices.length || rowEnd < rowStart) return; const localStart = Math.max(0, rowStart * cols); const localEnd = Math.min(itemIndices.length - 1, ((rowEnd + 1) * cols) - 1); for (let localIdx = localStart; localIdx <= localEnd; localIdx++) { visibleIndices.push(itemIndices[localIdx]); } }; const appendDupGridLimitedFallback = (section, itemIndices, cols, rowHeight, rows) => { const safeRowHeight = rowHeight || baseGridRowHeight; const safeRows = rows || Math.ceil(itemIndices.length / cols); if (!itemIndices.length || safeRows <= 0) return; const visibleRows = Math.max(1, Math.ceil(h / safeRowHeight)); const padRows = dupGridBufferRows; const maxRows = Math.min(safeRows, visibleRows + padRows * 2); const bodyTop = toFiniteNumber(section.bodyTop); const sectionTop = toFiniteNumber(section.top); const approxBodyTop = bodyTop !== null ? bodyTop : (sectionTop !== null ? sectionTop : top); const estimatedStart = Math.floor((top - approxBodyTop) / safeRowHeight) - padRows; const rowStart = clampInt(estimatedStart, 0, Math.max(0, safeRows - maxRows)); appendDupGridItemWindow(itemIndices, cols, rowStart, rowStart + maxRows - 1); }; const appendVisibleDupGridSectionItems = (section) => { const itemIndices = Array.isArray(section.itemIndices) ? section.itemIndices : []; if (!itemIndices.length) return; const cols = Math.max(1, Math.floor( toPositiveNumber(section.cols) || toPositiveNumber(dupGridMeta.cols) || toPositiveNumber(metaGridLayout.cols) || 1 )); const rows = Math.max(1, Math.floor(toPositiveNumber(section.rows) || Math.ceil(itemIndices.length / cols))); const bodyHeight = toPositiveNumber(section.bodyHeight); const bodyTop = toFiniteNumber(section.bodyTop); const rowHeightFromSection = toPositiveNumber(section.rowStride) || toPositiveNumber(section.rowHeight); const rowHeightFromBody = bodyHeight && rows ? bodyHeight / rows : 0; const rowHeightFromGrid = gridCardHeight ? gridCardHeight + gridGapY : 0; const rowHeight = rowHeightFromSection || rowHeightFromBody || rowHeightFromGrid || toPositiveNumber(CONF.rowHeight); const usedConfFallback = !rowHeightFromSection && !rowHeightFromBody && !rowHeightFromGrid; if (bodyTop === null || !rowHeight) { appendDupGridLimitedFallback(section, itemIndices, cols, rowHeight || baseGridRowHeight, rows); return; } const resolvedBodyHeight = bodyHeight || rows * rowHeight; const bodyBottom = bodyTop + resolvedBodyHeight; if (!(bodyTop < rangeBottom && bodyBottom > rangeTop)) return; let rowStart = clampInt(Math.floor((rangeTop - bodyTop) / rowHeight), 0, rows - 1); let rowEnd = clampInt(Math.ceil((rangeBottom - bodyTop) / rowHeight) - 1, 0, rows - 1); if (usedConfFallback) { const maxRows = Math.max(1, Math.ceil(h / rowHeight) + dupGridBufferRows * 2); rowEnd = Math.min(rowEnd, rowStart + maxRows - 1); } appendDupGridItemWindow(itemIndices, cols, rowStart, rowEnd); }; const sections = Array.isArray(dupGridMeta.sections) ? dupGridMeta.sections : []; const sectionWindow = getDupGridSectionWindow(sections, rangeTop, rangeBottom); for (let sectionIdx = sectionWindow.start; sectionIdx < sectionWindow.end; sectionIdx++) { const section = sections[sectionIdx]; if (!section) continue; const sectionTop = toFiniteNumber(section.top); const sectionBottom = toFiniteNumber(section.bottom); if (sectionBottom !== null && sectionBottom < rangeTop) continue; if (sectionTop !== null && sectionTop > rangeBottom) break; const headerTop = toFiniteNumber(section.top); const headerHeight = toPositiveNumber(section.headerHeight || (dupGridMeta.sectionMetrics && dupGridMeta.sectionMetrics.headerHeight)); if (section.headerIndex !== null && headerTop !== null && headerHeight > 0) { const headerBottom = headerTop + headerHeight; if (headerTop < rangeBottom && headerBottom > rangeTop) visibleIndices.push(section.headerIndex); } appendVisibleDupGridSectionItems(section); } end = visibleIndices.length; } else { start = isGridMode && gridLayout ? Math.max(0, (Math.floor(top / CONF.rowHeight) - Math.ceil(buffer / 2)) * gridLayout.cols) : Math.max(0, Math.floor(top / CONF.rowHeight) - buffer); end = isGridMode && gridLayout ? Math.min(S.display.length, (Math.ceil((top + h) / CONF.rowHeight) + Math.ceil(buffer / 2)) * gridLayout.cols) : Math.min(S.display.length, Math.ceil((top + h) / CONF.rowHeight) + buffer); } const isBlur = isBlurEnabledForView(isGridMode ? 'grid' : 'list'); const rootPathStr = S.path.map(p => p.name).join('/'); const firstVisiblePathIndex = !isGridMode ? Math.max(0, Math.floor(top / Math.max(1, CONF.rowHeight))) : 0; 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 showShareInsightPathCol = !!(S.shareParseMode && S.shareParseInsightMode); const showPathCol = S.shareParseMode ? showShareInsightPathCol : (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.shareParseMode) { colDef = S.shareParseInsightMode ? "36px 2fr 1fr 80px 120px 130px" : "36px 1fr 80px 120px 130px"; } else if (S.historyMode) { colDef = "36px 30px 1fr 80px 80px 120px 160px"; } else if (S.trashMode) { colDef = "36px 30px 1fr 80px 120px 130px"; } else if (S.recentMode) { colDef = "36px 30px 1fr 80px 120px 130px"; } else if (isAnalyzeRoot) { colDef = "36px 30px 2fr 1fr 80px 130px"; } else if (showPathCol) { colDef = "36px 30px 2fr 1fr 80px 120px 130px"; } else { colDef = "36px 30px 1fr 80px 120px 130px"; } UI.pop.style.display = 'none'; UI.pop.innerHTML = ''; cleanupNonPooledChildren(UI.in); const visibleCount = dupGridMeta ? visibleIndices.length : Math.max(0, end - start); const rowPool = ensureVisibleRowPool(UI.in, visibleCount); let poolPtr = 0; const useHistoryStableRows = !!(!isGridMode && !dupGridMeta); if (useHistoryStableRows) { if (!UI.in._pkHistoryRowMap) UI.in._pkHistoryRowMap = new Map(); UI.in._pkHistoryStableRowMode = true; rowPool.forEach(row => { if (row) row._pkUsedThisPass = false; }); } else { UI.in._pkHistoryStableRowMode = false; if (UI.in._pkHistoryRowMap) UI.in._pkHistoryRowMap.clear(); } const takeHistoryStableRow = (itemId) => { let row = UI.in._pkHistoryRowMap.get(itemId) || null; if (row && row._pkUsedThisPass) row = null; if (!row) { row = rowPool.find(r => r && !r._pkUsedThisPass && (r.dataset.pkBoundId || '') === itemId) || rowPool.find(r => r && !r._pkUsedThisPass && r.parentNode !== UI.in) || rowPool.find(r => r && !r._pkUsedThisPass); if (!row) { row = document.createElement('div'); row._pkPooledRow = true; row.style.position = 'absolute'; rowPool.push(row); } const oldId = row.dataset.pkBoundId || ''; if (oldId && oldId !== itemId && UI.in._pkHistoryRowMap.get(oldId) === row) { UI.in._pkHistoryRowMap.delete(oldId); } UI.in._pkHistoryRowMap.set(itemId, row); } row._pkUsedThisPass = true; return row; }; let nameColWidth = 400; let charCapacity = 50; const pathIds = S.path.map(p => p.id); const isAtGlobalSearchRoot = pathIds[pathIds.length - 1] === 'virtual_search_root'; const isGlobalSearchHistoryPresent = pathIds.includes('virtual_search_root'); const shouldShowHl = (!!S.search && (!isGlobalSearchHistoryPresent || isAtGlobalSearchRoot)); let pathColWidth = 200, pathCharCapacity = 30; if (UI.win) { const charWidth = isMax ? 9.2 : 8; const nameColEl = UI.win.querySelector('.pk-grid-hd .pk-col[data-k="name"]'); if (nameColEl) { nameColWidth = nameColEl.offsetWidth; charCapacity = Math.floor((nameColWidth - 80) / charWidth); } const pathColEl = UI.win.querySelector('.pk-grid-hd .pk-col[data-k="path"]'); if (pathColEl) { pathColWidth = pathColEl.offsetWidth; pathCharCapacity = Math.floor((pathColWidth - 20) / charWidth); } } const getTooltipHlHTML = (text, query) => { if (!query || !shouldShowHl) return esc(text); const lowText = text.toLowerCase(); const q = query.toLowerCase(); const idx = lowText.indexOf(q); if (idx === -1) return esc(text); return esc(text.substring(0, idx)) + `${esc(text.substring(idx, idx + q.length))}` + getTooltipHlHTML(text.substring(idx + q.length), query); }; const getSearchHlHTML = (name, query, capacity) => { const q = query.toLowerCase(); const idx = name.toLowerCase().indexOf(q); if (idx === -1) return esc(name); let start = 0, end = name.length, prefix = "", suffix = ""; if (name.length > capacity) { const preLimit = Math.floor(capacity * 0.3); start = Math.max(0, idx - preLimit); end = Math.min(name.length, start + capacity); if (start > 0) prefix = "..."; if (end < name.length) suffix = "..."; } const targetSlice = name.substring(start, end); return prefix + getTooltipHlHTML(targetSlice, query) + suffix; }; const getSharePathSearchHlHTML = (text, query, capacity) => { const raw = String(text || ''); const q = normalizeSharePathSearchText(query); if (!q) return esc(raw); let norm = '', map = [], slash = false; for (let i = 0; i < raw.length; i++) { const ch = raw[i]; if (/[\\//]/.test(ch)) { while (norm.endsWith(' ')) { norm = norm.slice(0, -1); map.pop(); } norm += '/'; map.push(i); slash = true; continue; } if (slash && /\s/.test(ch)) continue; slash = false; norm += ch.toLowerCase(); map.push(i); } const idx = norm.indexOf(q); if (idx === -1 || !map[idx] && map[idx] !== 0) return getSearchHlHTML(raw, query, capacity); const matchStart = map[idx]; const matchEnd = (map[idx + q.length - 1] ?? matchStart) + 1; let start = 0, end = raw.length, prefix = '', suffix = ''; if (raw.length > capacity) { const cap = Math.max(q.length + 8, Math.floor(capacity || 30)); const preLimit = Math.max(0, Math.floor((cap - (matchEnd - matchStart)) * 0.3)); start = Math.max(0, matchStart - preLimit); end = Math.min(raw.length, start + cap); if (end < matchEnd) { end = matchEnd; start = Math.max(0, end - cap); } if (start > 0) prefix = '...'; if (end < raw.length) suffix = '...'; } const hiStart = Math.max(matchStart, start); const hiEnd = Math.min(matchEnd, end); return prefix + esc(raw.substring(start, hiStart)) + `${esc(raw.substring(hiStart, hiEnd))}` + esc(raw.substring(hiEnd, end)) + suffix; }; const getGridSearchHlHTML = (name, query, capacity) => { const text = String(name || ''); const q = String(query || '').toLowerCase(); if (!q) return esc(text); const idx = text.toLowerCase().indexOf(q); if (idx === -1) return esc(text); const cap = Math.max(q.length + 8, Math.floor(capacity || 18)); if (text.length <= cap && idx + q.length <= cap - 1) return getTooltipHlHTML(text, query); const preLimit = Math.max(3, Math.floor((cap - q.length) * 0.45)); let start = Math.max(0, idx - preLimit); let end = Math.min(text.length, start + cap); if (end - start < cap) start = Math.max(0, end - cap); const prefix = start > 0 ? "..." : ""; const suffix = end < text.length ? "..." : ""; return prefix + getTooltipHlHTML(text.substring(start, end), query) + suffix; }; const _internalGetStarIcon = (isStarred) => { const color = isStarred ? '#FFC107' : '#ccc'; const fill = isStarred ? '#FFC107' : 'none'; return ` `; }; const getRemainingDays = (source) => { const DAY_MS = 1000 * 60 * 60 * 24; const now = getServerNow(); const directKeys = ['_trash_remaining_days', 'remaining_days', 'remain_days', 'days_left', 'left_days', 'expiration_days_left', 'expire_days_left', 'expires_in_days', 'trash_remaining_days', 'purge_remaining_days']; const expireKeys = ['_trash_expire_time', 'expires_at', 'expire_time', 'expired_time', 'expiration_at', 'delete_at', 'delete_time', 'purge_at', 'purge_time', 'trashed_expire_time', 'trash_expire_time']; const deletedKeys = ['_trash_deleted_time', 'deleted_time', 'deleted_at', 'trashed_time', 'trashed_at', 'trash_time', 'recycled_time', 'recycle_time', 'modified_time']; const retentionKeys = ['_trash_retention_days', 'retention_days', 'reserve_days', 'keep_days']; const readValue = (obj, keys) => { if (!obj || typeof obj !== 'object') return undefined; const sources = [obj, obj.params, obj.audit, obj.delete_info, obj.trash_info, obj.recycle_info].filter(Boolean); for (const src of sources) { for (const key of keys) { if (src && src[key] !== undefined && src[key] !== null && src[key] !== '') return src[key]; } } return undefined; }; const toNumber = (v) => { if (v === undefined || v === null || v === '') return null; const n = Number(v); return Number.isFinite(n) ? n : null; }; const toTime = (v) => { if (v === undefined || v === null || v === '') return 0; if (typeof v === 'number') return v < 100000000000 ? v * 1000 : v; if (typeof v === 'string' && /^\d+(\.\d+)?$/.test(v.trim())) { const n = Number(v.trim()); return n < 100000000000 ? n * 1000 : n; } const t = new Date(v).getTime(); return Number.isFinite(t) ? t : 0; }; const formatDays = (days, cap = null) => { if (!Number.isFinite(days)) return L.trash_remaining_unknown || "-"; const capped = cap ? Math.min(cap, days) : days; const left = Math.max(0, Math.ceil(capped)); if (left <= 0) return L.trash_remaining_today || (`0 ${L.unit_days}`); return left + " " + L.unit_days; }; if (source && typeof source === 'object') { const directDays = toNumber(readValue(source, directKeys)); if (directDays !== null) return formatDays(directDays); const expireTime = toTime(readValue(source, expireKeys)); if (expireTime && expireTime > now - DAY_MS) return formatDays((expireTime - now) / DAY_MS); const retentionDays = toNumber(readValue(source, retentionKeys)) || 15; const deleteTime = toTime(readValue(source, deletedKeys)); if (deleteTime) { const daysPassed = (now - deleteTime) / DAY_MS; return formatDays(retentionDays - daysPassed, retentionDays); } return L.trash_remaining_unknown || "-"; } const deleteTime = toTime(source); if (!deleteTime) return L.trash_remaining_unknown || "-"; const daysPassed = (now - deleteTime) / DAY_MS; return formatDays(15 - daysPassed, 15); }; const getDupStablePathKey = () => { if (!dupGridMeta) return ''; if (S.dupMode) return S.pinnedDupPath || (UI.selDupFolder ? UI.selDupFolder.value || '' : ''); if (S.analyzeMode && cur && cur.id === 'analyze_root' && S.analyzeSimGroups) return 'analyze_root'; return ''; }; const getDupStableFilterSig = () => { const includePath = UI.chkSearchPath && UI.chkSearchPath.checked ? 'path1' : 'path0'; if (S.dupMode) { const cfg = S.dupConfig || {}; const invertChk = document.getElementById('pk-dup-invert'); return [ UI.chkHash && UI.chkHash.checked ? 'hash1' : 'hash0', UI.chkSim && UI.chkSim.checked ? 'sim1' : 'sim0', UI.chkName && UI.chkName.checked ? 'name1' : 'name0', cfg.video ? 'video1' : 'video0', cfg.image ? 'image1' : 'image0', cfg.other ? 'other1' : 'other0', gmGet('pk_dup_strictness', 'strict'), S.pinnedDupPath || '', invertChk && invertChk.checked ? 'inv1' : 'inv0', includePath ].join(':'); } if (S.analyzeMode && S.analyzeSimGroups) { return [ gmGet('pk_analyze_last_algo', ''), gmGet('pk_analyze_last_sim', ''), S.groupSortOp || '', S.groupSortDir || '', includePath ].join(':'); } return includePath; }; const resolveGridStableScope = () => { if (!isGridMode || !gridLayout) return ''; if (dupGridMeta) { if (S.dupMode) return 'dup-file'; if (S.analyzeMode && cur && cur.id === 'analyze_root' && S.analyzeSimGroups) return 'dup-folder'; return ''; } if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.dupMode) return ''; if (S.historyMode) return cur && cur.id === 'history_root' ? 'history' : ''; if (S.recentMode) return cur && cur.id === 'recent_root' ? 'recent' : ''; if (S.starredMode) { const curId = cur && cur.id ? String(cur.id) : ''; const hasVirtualPath = S.path.some(p => { const id = p && p.id ? String(p.id) : ''; return id.startsWith('virtual_') || id === 'analyze_root' || id === 'recent_root' || id === 'history_root' || id === 'offline_root' || id === 'upload_root'; }); if (hasVirtualPath || S.isFlattened || S.analyzeMode) return ''; const isStarredRoot = S.path.length === 1 && (curId === '' || curId === 'starred_root'); const isStarredRealPath = S.path.length > 1 && curId && !curId.startsWith('virtual_') && curId !== 'analyze_root'; return (isStarredRoot || isStarredRealPath) ? 'starred' : ''; } if (S.isFlattened && !S.analyzeMode) return 'flat'; if (S.analyzeMode) return S.analyzeSimGroups ? '' : 'analyze'; const curId = cur && cur.id ? String(cur.id) : ''; return curId && curId.startsWith('virtual_') ? '' : 'normal'; }; const gridStableScope = resolveGridStableScope(); const dupStableMapParts = dupGridMeta ? [ dupGridMeta.key || '', getDupStablePathKey(), getDupStableFilterSig(), S.groupSortOp || '', S.groupSortDir || '' ] : []; const gridStableMapKey = gridStableScope ? [ gridStableScope, S.viewMode || '', getGridLayoutKey(), S.path.map(p => `${p.id || ''}:${p.name || ''}`).join('/'), rootPathStr, lang, isMax ? 'max' : 'normal', S.search || '', shouldShowHl ? 'hl' : 'plain', showPathCol ? 'path' : 'nop', isBlur ? 'blur' : 'clear', typeof getBlurScope === 'function' ? getBlurScope() : '', S.sort || '', S.dir || '', S.folderFirst ? 'ff' : 'nf', UI.chkSearchPath && UI.chkSearchPath.checked ? 'searchPath' : 'searchName', ...dupStableMapParts ].join('|') : ''; const useGridStableRows = !!gridStableMapKey; if (useGridStableRows) { if (!UI.in._pkGridRowMap) UI.in._pkGridRowMap = new Map(); if (UI.in._pkGridRowMapKey !== gridStableMapKey) { UI.in._pkGridRowMap.clear(); UI.in._pkGridRowMapKey = gridStableMapKey; } UI.in._pkGridStableRowMode = true; rowPool.forEach(row => { if (row) row._pkGridUsedThisPass = false; }); } else { clearGridStableRows(UI.in); } const gridPassUsedIds = useGridStableRows ? new Set() : null; const gridPassUsedRows = useGridStableRows ? new Set() : null; const takeGridFallbackRow = () => { let row = rowPool.find(r => r && !r._pkGridUsedThisPass && !gridPassUsedRows.has(r) && r.parentNode !== UI.in) || rowPool.find(r => r && !r._pkGridUsedThisPass && !gridPassUsedRows.has(r)); if (!row) { row = document.createElement('div'); row._pkPooledRow = true; row.style.position = 'absolute'; rowPool.push(row); } row._pkGridUsedThisPass = true; gridPassUsedRows.add(row); poolPtr++; return row; }; const takeGridStableRow = (itemId) => { if (!itemId || gridPassUsedIds.has(itemId)) { return { row: takeGridFallbackRow(), duplicate: true }; } gridPassUsedIds.add(itemId); const rowMap = UI.in._pkGridRowMap; let row = rowMap.get(itemId) || null; const isReservedGridRow = (candidate) => { if (!candidate || !candidate.dataset) return false; const boundId = candidate.dataset.pkBoundId || ''; return !!(boundId && boundId !== itemId && rowMap.get(boundId) === candidate); }; if (row && (row._pkGridUsedThisPass || gridPassUsedRows.has(row) || !row._pkPooledRow)) { row = null; } if (!row) { row = rowPool.find(r => r && !r._pkGridUsedThisPass && !gridPassUsedRows.has(r) && (r.dataset.pkBoundId || '') === itemId) || rowPool.find(r => r && !r._pkGridUsedThisPass && !gridPassUsedRows.has(r) && r.parentNode !== UI.in && !isReservedGridRow(r)) || rowPool.find(r => r && !r._pkGridUsedThisPass && !gridPassUsedRows.has(r) && !isReservedGridRow(r)); } if (!row) { row = document.createElement('div'); row._pkPooledRow = true; row.style.position = 'absolute'; rowPool.push(row); } const oldId = row.dataset.pkBoundId || ''; if (oldId && oldId !== itemId && rowMap.get(oldId) === row) { rowMap.delete(oldId); } rowMap.set(itemId, row); row._pkGridUsedThisPass = true; gridPassUsedRows.add(row); poolPtr++; return { row, duplicate: false }; }; const getGridFolderCountSig = (item) => { if (!item || item.kind !== 'drive#folder' || S.trashMode) return ''; let count = item.file_count; if (count === undefined || count === null) count = item.usage?.file_count; if (count === undefined || count === null) count = item.params?.file_count; if (count === undefined || count === null) count = item.audit?.file_count; if ((count === undefined || count === null) && !item._isShareItem && typeof globalCache !== 'undefined') { const cachedData = globalCache.get(item.id); if (cachedData) { const list = Array.isArray(cachedData) ? cachedData : (cachedData.items || []); count = list.length; } } return count === undefined || count === null ? '' : String(count); }; const getGridPathSig = (item) => { if (!item) return ''; const lineage = item._lineage; if (Array.isArray(lineage)) return lineage.map(p => `${p.id || ''}:${p.name || ''}`).join('/'); if (lineage === null) return 'null'; return item._pathStr || item._dupFullPath || item.path || ''; }; const getGridRecentMetaSig = (item) => { if (!S.recentMode || !window.pkRecentMetaCache || !window.pkRecentMetaCache.has(item.id)) return ''; const meta = window.pkRecentMetaCache.get(item.id) || {}; return [ meta.thumbnail_link || '', meta.icon_link || '', meta.mime_type || '', meta.modified_time || '', meta.size || '', meta.params?.duration || '' ].join(':'); }; const getGridThumbSig = (item) => { const hasThumb = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); if (!hasThumb) return 'none'; const stableMedia = item.kind === 'drive#folder' ? getStableFolderMediaState(item) : getStableFileMediaState(item); const state = stableMedia.state || 'unknown'; if (state === 'unknown') return ''; if (state === 'probing') return [state, stableMedia.src || item.thumbnail_link || ''].join(':'); return [state, stableMedia.src || ''].join(':'); }; const buildGridStableSig = (item, dupStableCtx = null) => { if (!useGridStableRows || !item || item.isHeader || !item.id || !item.kind) return ''; const isDupStableScope = gridStableScope === 'dup-file' || gridStableScope === 'dup-folder'; const dupSigSuffix = isDupStableScope ? ((dupStableCtx && dupStableCtx.sigSuffix) || '') : ''; if (isDupStableScope && !dupSigSuffix) return ''; const thumbSig = getGridThumbSig(item); if (!thumbSig) return ''; const isFolder = item.kind === 'drive#folder'; const folderStamp = isFolder && typeof getFolderEmbedCacheStamp === 'function' ? getFolderEmbedCacheStamp(item) : ''; const cleanName = (item.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const isBlMarked = isFolder ? (S.blFolderSet && S.blFolderSet.has(cleanName)) : (S.blSet && S.blSet.has(cleanName)); const isStarred = S.starredSet.has(item.id) || !!(item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))); const isHistoryGridStable = gridStableScope === 'history'; const isVideo = !isFolder && (((item.mime_type || '').toLowerCase().startsWith('video/')) || (item.params && item.params.duration > 0) || (isHistoryGridStable && item._history_duration > 0)); const displayDur = isHistoryGridStable ? (item._history_duration || item.params?.duration || 0) : (isVideo ? (item._history_duration || item.params?.duration || 0) : 0); return [ gridStableMapKey, item.id || '', item.kind || '', item.name || '', item.mime_type || '', item.size || '', item.modified_time || '', item.created_time || '', item.thumbnail_link || '', item.icon_link || '', isOfflineReferenceMissingLocalMarked(item) ? 'offline-missing-local' : '', item._offlineReferenceMissingSource || '', item._offlineMissingMessage || '', item._offlineProgressDisplay || '', S.shareMode ? (item.pass_code || '') : '', typeof makeGridMediaCacheKey === 'function' ? makeGridMediaCacheKey(item) : '', thumbSig, folderStamp, getGridFolderCountSig(item), displayDur, isStarred ? 'star' : 'unstar', isBlMarked ? 'bl' : 'nobl', isSystemItem(item) ? 'sys' : 'usr', item._coverResolved ? 'cover' : '', item._isFolderLike ? 'folderlike' : '', item._recent_event_id || '', item._recent_event_type || '', item._recent_event_time || '', isHistoryGridStable ? Math.round(Number(item._history_progress || 0)) : '', isHistoryGridStable ? Math.round(Number(item._history_duration || 0)) : '', isHistoryGridStable ? Number(item._history_ts || 0) : '', isHistoryGridStable ? (item._history_event_id || '') : '', shouldShowHl ? (S.search || '') : '', isDupStableScope ? '' : getGridRecentMetaSig(item), isDupStableScope ? '' : getGridPathSig(item), dupSigSuffix ].join('|'); }; const gridStableSigCache = useGridStableRows ? new Map() : null; const getGridStableSigCached = (item, dupStableCtx = null) => { if (!gridStableSigCache || !item || !item.id) return buildGridStableSig(item, dupStableCtx); const cacheKey = dupStableCtx && dupStableCtx.cacheKey ? `dup:${dupStableCtx.cacheKey}` : `base:${item.id}`; if (gridStableSigCache.has(cacheKey)) return gridStableSigCache.get(cacheKey); const sig = buildGridStableSig(item, dupStableCtx); gridStableSigCache.set(cacheKey, sig); return sig; }; const buildVisibleDupStableCtx = (item, layout, displayIndex) => { if (!dupGridMeta || !layout || layout.kind !== 'item' || !item || item.isHeader || !item.id || !item.kind) return null; if (gridStableScope === 'dup-file') { if (!S.dupMode || item.kind !== 'drive#file') return null; } else if (gridStableScope === 'dup-folder') { if (!(S.analyzeMode && S.analyzeSimGroups) || item.kind !== 'drive#folder') return null; } else { return null; } const section = dupGridMeta.indexToSection ? dupGridMeta.indexToSection.get(displayIndex) : null; const localIdx = Number(layout.localIndex); const itemId = String(item.id); if (!section || section.headerIndex === null || !section.dupGroupSig || !section.memberDigest || !Number.isFinite(localIdx) || localIdx < 0) return null; if (dupGridMeta.repeatedIds && dupGridMeta.repeatedIds.has(itemId)) return null; if (!Array.isArray(section.itemIds) || String(section.itemIds[localIdx] || '') !== itemId) return null; const header = S.display[section.headerIndex] || null; if (header && Array.isArray(header.groupIds) && header.groupIds.length) { let inHeader = false; for (let hIdx = 0; hIdx < header.groupIds.length; hIdx++) { if (String(header.groupIds[hIdx] || '') === itemId) { inHeader = true; break; } } if (!inHeader) return null; } const rawGroupReason = String(section.reason || (header && header.type) || ''); const itemReason = String(item._dupReason || item.dupReason || item.reason || (S.dupReasons && S.dupReasons.get(item.id)) || rawGroupReason || ''); const itemPath = String(item._dupFullPath || item._pathStr || item.path || ''); return { sigSuffix: [ section.dupGroupSig, `dupLocal:${localIdx}`, `dupReason:${getDupGridShortTextDigest(itemReason)}`, `dupSim:${String(item._sim ?? item.sim ?? item.similarity ?? '')}`, `dupContain:${String(item._contain ?? item.contain ?? item.containment ?? '')}`, `dupHit:${String(item._dupHitType || item._hitType || item.hitType || '')}`, `dupItemPath:${getDupGridShortTextDigest(itemPath)}`, `dupSameFolder:${item._isSameFolder ? '1' : '0'}` ].join('|'), cacheKey: `${itemId}|${section.key || ''}|${localIdx}|${section.memberDigest}` }; }; const renderIndices = dupGridMeta ? visibleIndices : null; const renderCount = dupGridMeta ? renderIndices.length : Math.max(0, end - start); const selectedCountForRender = isGridMode ? S.getSelectedCount() : -1; for (let seqIdx = 0; seqIdx < renderCount; seqIdx++) { const i = dupGridMeta ? renderIndices[seqIdx] : (start + seqIdx); let d = S.display[i]; if (!d) continue; if (S.offlineMode && d.id) { const liveItem = S.itemMap.get(d.id); if (liveItem && liveItem !== d && isOfflineReferenceMissingLocalMarked(liveItem) && !isOfflineReferenceMissingLocalMarked(d)) { d = Object.assign({}, d, liveItem); S.display[i] = d; } } const dupLayout = dupGridMeta ? dupGridMeta.indexLayout.get(i) : null; const dupStableCtx = dupGridMeta ? buildVisibleDupStableCtx(d, dupLayout, i) : null; const canTryGridStableRow = !!(useGridStableRows && d.id && !d.isHeader && (!dupGridMeta || dupStableCtx)); let row; let gridStableDuplicateId = false; if (canTryGridStableRow) { const gridStableTake = takeGridStableRow(d.id); row = gridStableTake.row; gridStableDuplicateId = gridStableTake.duplicate; } else if (useGridStableRows) { row = takeGridFallbackRow(); } else { row = useHistoryStableRows && d.id ? takeHistoryStableRow(d.id) : rowPool[poolPtr++]; if (useHistoryStableRows) poolPtr++; } const prevBoundId = row.dataset.pkBoundId || ''; const prevBoundKind = row.dataset.pkBoundKind || ''; const prevGridMediaMount = isGridMode ? row.querySelector('.pk-gv-media-mount') : null; const prevGridMediaNode = prevGridMediaMount ? prevGridMediaMount.firstElementChild : null; const prevGridMediaKey = prevGridMediaMount ? (prevGridMediaMount.dataset.pkMediaKey || '') : ''; const suppressGridRebindTransition = isGridMode && prevBoundId && prevBoundId !== d.id; const gridStableSig = (canTryGridStableRow && !gridStableDuplicateId) ? getGridStableSigCached(d, dupStableCtx) : ''; const listStableModeKey = useHistoryStableRows ? [ S.offlineMode ? 'offline' : '', S.uploadMode ? 'upload' : '', S.shareMode ? 'share' : '', S.historyMode ? 'history' : '', S.trashMode ? 'trash' : '', S.recentMode ? 'recent' : '', S.dupMode ? 'dup' : '', S.analyzeMode ? 'analyze' : '', S.isFlattened ? 'flat' : '', showPathCol ? 'path' : 'plain', colDef, rootPathStr ].join('|') : ''; const getListPathStableKey = (item) => { if (!item || item.isHeader) return 'header'; if (item._isShareInsightItem) return item._shareFilePathText || getSharePathText(item._sharePath) || L.label_share_parse_path_root || ''; if (isAnalyzeRoot) return item._pathStr || item.path || ''; if (S.dupMode) return item._dupFullPath || ''; if (item._lineage === null && item.parent_id && item.parent_id !== 'root') return `pending:${item.parent_id || ''}`; if (item._lineage && Array.isArray(item._lineage)) return item._lineage.map(x => x && x.name || '').join('/'); if (S.analyzeMode) return item._pathStr || item.path || ''; return ''; }; const listPathStableSig = (useHistoryStableRows && showPathCol) ? (() => { const curPathKey = getListPathStableKey(d); const prevItem = i > firstVisiblePathIndex ? S.display[i - 1] : null; const prevPathKey = (prevItem && !prevItem.isHeader) ? getListPathStableKey(prevItem) : ''; const sameState = (i > firstVisiblePathIndex && curPathKey && prevPathKey && curPathKey === prevPathKey) ? 'same' : 'show'; return [i, firstVisiblePathIndex, curPathKey, prevPathKey, sameState].join('~'); })() : ''; const historyStableSig = useHistoryStableRows ? [ listStableModeKey, listPathStableSig, d.id || '', d.name || '', d.size || '', d.mime_type || '', d.thumbnail_link || '', d.icon_link || '', isOfflineReferenceMissingLocalMarked(d) ? 'offline-missing-local' : '', d._offlineReferenceMissingSource || '', d._offlineMissingMessage || '', d._offlineProgressDisplay || '', S.shareMode ? (d.pass_code || '') : '', isGridMode ? 'grid' : 'list', UI.win && UI.win.classList.contains('pk-maximized') ? 'max' : 'normal', typeof getBlurScope === 'function' ? getBlurScope() : '', isBlur ? 'blur' : 'clear', window.pkDirIconRetryStamp || '0', (S.starredSet.has(d.id) || !!(d.starred || (d.tags && d.tags.some(t => t.name === 'STAR')))) ? 'star' : 'unstar', (d.kind === 'drive#folder' ? (S.blFolderSet && S.blFolderSet.has(getBlacklistCleanKey(d.name))) : (S.blSet && S.blSet.has(getBlacklistCleanKey(d.name)))) ? 'bl' : 'nobl', Math.round(Number(S.durationMap.get(d.id) || d.params?.duration || 0)), Math.round(Number(d._history_progress || 0)), Math.round(Number(d._history_duration || 0)), Number(d._history_ts || 0), d._history_event_id || '', shouldShowHl ? (S.search || '') : '' ].join('|') : ''; const canReuseGridRow = !!( useGridStableRows && gridStableSig && !gridStableDuplicateId && prevBoundId && prevBoundId === d.id && prevBoundKind === d.kind && row.dataset.pkGridStable === '1' && row.dataset.pkGridSig === gridStableSig && row.querySelector('.pk-grid-card-body') ); const canReuseHistoryRow = !!( useHistoryStableRows && prevBoundId && prevBoundId === d.id && row.dataset.pkHistoryStable === '1' && row.dataset.pkHistorySig === historyStableSig && row.firstElementChild ); if (isGridMode && !dupGridMeta && !canReuseGridRow && prevBoundId && prevGridMediaNode && typeof stashFrozenGridMediaNode === 'function') { stashFrozenGridMediaNode(prevBoundId, prevGridMediaKey, prevGridMediaNode, prevBoundKind); } if (canReuseHistoryRow || canReuseGridRow) { softResetPooledRow(row); } else { resetPooledRow(row); } row.dataset.pkHistoryStable = useHistoryStableRows ? '1' : '0'; row.dataset.pkHistorySig = historyStableSig; row.dataset.pkGridStable = (canTryGridStableRow && gridStableSig && !gridStableDuplicateId) ? '1' : '0'; row.dataset.pkGridSig = gridStableSig; if (suppressGridRebindTransition) { row.style.transition = 'none'; row.style.animation = 'none'; } if (dupLayout) { row.style.top = `${dupLayout.top}px`; row.style.left = `${dupLayout.left}px`; row.style.width = `${dupLayout.width}px`; row.style.height = `${dupLayout.height}px`; if (dupLayout.kind === 'header') { row.style.padding = isMax ? '0 20px' : '0 16px'; row.style.display = 'grid'; row.style.gridTemplateColumns = colDef; row.style.zIndex = '2'; row.style.boxSizing = 'border-box'; } else { row.style.padding = '0'; row.style.display = 'block'; row.style.gridTemplateColumns = ''; row.style.zIndex = '1'; } } else if (isGridMode && gridLayout) { const rowIdx = Math.floor(i / gridLayout.cols); const colIdx = i % gridLayout.cols; row.style.top = `${gridLayout.gapY + rowIdx * CONF.rowHeight}px`; row.style.left = `${gridLayout.padX + colIdx * (gridLayout.cardWidth + gridLayout.gapX)}px`; row.style.width = `${gridLayout.cardWidth}px`; row.style.height = `${gridLayout.cardHeight}px`; row.style.padding = '0'; row.style.display = 'block'; } else { row.style.top = `${i * CONF.rowHeight}px`; row.style.left = '0'; row.style.width = '100%'; row.style.gridTemplateColumns = colDef; row.style.height = ''; row.style.padding = ''; row.style.display = 'grid'; } if (d.isHeader) { row.className = 'pk-group-hd'; if (i === 0) { row.style.setProperty('border-top', 'none', 'important'); const borderW = isMax ? 10 : 4; row.style.setProperty('padding-top', borderW + 'px', 'important'); } else { row.style.removeProperty('border-top'); row.style.removeProperty('padding-top'); } if (dupGridMeta) row.style.gridColumn = ""; else if (S.trashMode) row.style.gridColumn = "1 / 7"; else if (S.dupMode || S.analyzeMode) row.style.gridColumn = "1 / 8"; else row.style.gridColumn = "1 / 7"; const gIdx = parseInt(d.id.replace('grp_', '')); const groupData = S.dupMode ? S.dupRawGroups[gIdx] : (S.analyzeMode ? S.analyzeSimGroups[gIdx] : null); const groupItemIds = Array.isArray(d.groupIds) ? d.groupIds.slice() : (groupData ? groupData.ids : []); row._pkGroupIds = groupItemIds.slice(); let selectedInGroup = 0; groupItemIds.forEach(id => { if (S.isSelected(id)) selectedInGroup++; }); const isAllSelected = groupItemIds.length > 0 && selectedInGroup === groupItemIds.length; const isIndeterminate = selectedInGroup > 0 && selectedInGroup < groupItemIds.length; let groupIcon = CONF.dupHashSVG; const isContain = d.name.includes(L.lbl_containment); const isSimLike = d.type === L.tag_sim || d.name.includes(L.lbl_sim_score); const isNameLike = d.type === L.tag_name || (S.analyzeMode && d.name.includes(' | ') && !isSimLike && !isContain); if (isContain) { groupIcon = CONF.dupContainSVG; } else if (isSimLike) { groupIcon = CONF.dupSimSVG; } else if (isNameLike) { groupIcon = CONF.dupNameSVG; } groupIcon = groupIcon.replace(/width:\s*\d+px;?/, 'width:18px;').replace(/height:\s*\d+px;?/, 'height:18px;').replace(/margin-right:\s*\d+px;?/, 'margin:0;'); let headerName = d.name; const headerTip = getTooltipHlHTML(d.name, S.search).replace(/"/g, '"'); row.style.display = 'flex'; row.innerHTML = `
${groupIcon}
${esc(headerName)}
`; const grpChk = row.querySelector('.pk-grp-chk'); if (grpChk) { grpChk.indeterminate = isIndeterminate; grpChk.onclick = (e) => { e.stopPropagation(); const targetState = grpChk.checked; groupItemIds.forEach(id => { S.setSelected(id, targetState); }); renderVisible(); updateStat(); }; } } else { const isSel = S.isSelected(d.id); const isFocused = S.activeId === d.id; const isMoving = S.movingIds && S.movingIds.has(d.id); row.className = getRowClassName(isSel, isFocused, isMoving, selectedCountForRender); row.ondragstart = (e) => e.preventDefault(); if (isFocused && !isSel) { row.style.backgroundColor = 'var(--pk-sel-bg)'; row.style.border = '1px solid var(--pk-pri)'; row.style.borderRadius = isGridMode ? `${getGridCardRadius()}px` : '4px'; } else { row.style.backgroundColor = ''; row.style.border = ''; row.style.borderRadius = ''; } if (isMoving) { row.style.opacity = '0.4'; row.style.filter = 'grayscale(100%)'; row.style.pointerEvents = 'none'; row.style.cursor = 'wait'; } else { row.style.opacity = ''; row.style.filter = ''; row.style.pointerEvents = ''; row.style.cursor = ''; } row.dataset.id = d.id; const isProtected = !S.trashMode && isSystemItem(d); const isMax = UI.win.classList.contains('pk-maximized'); const nameTip = getTooltipHlHTML(d.name, S.search).replace(/"/g, '"'); const getDynamicIcon = (item) => { let isBlacklisted = false; const cleanName = (item.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); if (item.kind === 'drive#folder') { isBlacklisted = S.blFolderSet && S.blFolderSet.has(cleanName); } else { isBlacklisted = S.blSet && S.blSet.has(cleanName); } const blHtml = isBlacklisted ? `
${CONF.icons.blMarker}
` : ''; if (S.uploadMode) { if (item.status === 'DONE' && item.file && item.mime_type && item.mime_type.startsWith('image/')) { if (!item._localThumbUrl) { try { item._localThumbUrl = URL.createObjectURL(item.file); } catch(e) {} } if (item._localThumbUrl) item.thumbnail_link = item._localThumbUrl; } const hasReadyThumb = item.status === 'DONE' && item.thumbnail_link && item.thumbnail_link !== item.icon_link; if (!hasReadyThumb) { const scriptIcon = getIcon(item); if (isMax) { const boxStyle = "width:54px; min-width:54px; height:100%; display:flex; align-items:center; justify-content:flex-start !important; margin-right:12px; position:relative;"; return `
${scriptIcon}
${blHtml}
`; } return `
${scriptIcon}
${blHtml}
`; } } if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); const isFolder = item.kind === 'drive#folder'; const isTask = item.kind === 'drive#task'; const isUploadTask = item.kind === 'pk#upload'; const lookupId = (isTask || isUploadTask) ? item.file_id : item.id; let hasValidCover = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); if (!window.pkRecentMetaCache) window.pkRecentMetaCache = new Map(); if (S.recentMode && !isFolder) { if (window.pkRecentMetaCache.has(item.id)) { const meta = window.pkRecentMetaCache.get(item.id); item.thumbnail_link = meta.thumbnail_link || item.thumbnail_link; item.icon_link = meta.icon_link || item.icon_link; item.mime_type = meta.mime_type || item.mime_type; if (meta.medias) item.medias = meta.medias; item.params = Object.assign(item.params || {}, meta.params || {}); hasValidCover = !!(item.thumbnail_link && item.thumbnail_link !== item.icon_link); } else if (!hasValidCover && !item._metaFetching) { item._metaFetching = true; const ext = (item.name || '').split('.').pop().toLowerCase(); const mime = (item.mime_type || '').toLowerCase(); const isLikelyMedia = mime.startsWith('video/') || mime.startsWith('image/') ||['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','jpg','jpeg','png','gif','bmp','webp','heic','svg','tif','tiff','ico'].includes(ext); if (isLikelyMedia) { apiGet(item.id).then(meta => { if (meta) { window.pkRecentMetaCache.set(item.id, meta); item.thumbnail_link = meta.thumbnail_link || item.thumbnail_link; item.icon_link = meta.icon_link || item.icon_link; item.mime_type = meta.mime_type || item.mime_type; if (meta.medias) item.medias = meta.medias; item.params = Object.assign(item.params || {}, meta.params || {}); requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); }); } }).catch(()=>{}); } } } let forceDeepScan = false; if (item._coverResolved && !hasValidCover) { forceDeepScan = true; } if ((isFolder || isTask || isUploadTask) && lookupId && !item._isShareItem && (!hasValidCover || forceDeepScan) && typeof globalCache !== 'undefined') { const normalize = (data) => (data && !Array.isArray(data) && data.items) ? data.items : data; const scanDeepCover = (targetId, depth) => { if (depth > 5) return null; const raw = globalCache.get(targetId); if (!raw) return null; const files = normalize(raw); if (!files || files.length === 0) return null; const vid = files.find(f => f.mime_type?.startsWith('video/') && f.thumbnail_link); if (vid) return vid.thumbnail_link; const img = files.find(f => f.mime_type?.startsWith('image/') && f.thumbnail_link); if (img) return img.thumbnail_link; const subFolders = files.filter(f => f.kind === 'drive#folder'); for (const sub of subFolders) { if (globalCache.has(sub.id)) { const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; continue; } if (sub.thumbnail_link && sub.thumbnail_link !== sub.icon_link && !sub._coverResolved) { return sub.thumbnail_link; } const childThumb = scanDeepCover(sub.id, depth + 1); if (childThumb) return childThumb; } return null; }; const foundThumb = scanDeepCover(lookupId, 0); if (foundThumb) { item.thumbnail_link = foundThumb; item._coverResolved = true; item._isFolderLike = true; hasValidCover = true; if (typeof window.markStableFolderMediaState === 'function') { window.markStableFolderMediaState(item.id || lookupId, 'ok', foundThumb); } else { if (!window.pkThumbTriState) window.pkThumbTriState = { folder: Object.create(null) }; if (!window.pkGridMediaStore) window.pkGridMediaStore = { folder: Object.create(null) }; if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); window.pkThumbTriState.folder[item.id || lookupId] = 'ok'; window.pkGridMediaStore.folder[item.id || lookupId] = foundThumb; window.pkGlobalThumbCache.add(item.id || lookupId); } } else if (item._coverResolved || (isFolder && globalCache.has(lookupId))) { item.thumbnail_link = null; item._coverResolved = false; item._isFolderLike = false; hasValidCover = false; if (typeof window.markStableFolderMediaState === 'function') { window.markStableFolderMediaState(item.id || lookupId, 'fail'); } else if (window.pkThumbTriState && window.pkThumbTriState.folder) { window.pkThumbTriState.folder[item.id || lookupId] = 'fail'; if (window.pkGridMediaStore && window.pkGridMediaStore.folder) delete window.pkGridMediaStore.folder[item.id || lookupId]; } } else if ((isFolder || isTask || isUploadTask) && typeof scannedFolderIds !== 'undefined' && !scannedFolderIds.has(lookupId)) { if (!S.loading && !S.scanning) { scannedFolderIds.add(lookupId); backgroundQueue.push({ id: lookupId, name: 'DeepCoverProbe', retryCount: 0 }); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } } } const iconHtml = getOfficialDirFallbackIconHtml(item, isMax ? 'listMax' : 'list'); const listIconSlotHtml = (mode = (isMax ? 'listMax' : 'list'), remoteSrc = '', extra = {}) => getDirListIconSlotHtml(item, Object.assign({ mode, remoteSrc }, extra || {})); const boxStyle = "width:54px; min-width:54px; height:100%; display:flex; align-items:center; justify-content:center !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; justify-content:center; transition: opacity 0.3s; pointer-events: none;`; const imgStyle = "width: 48px; height: 48px; object-fit: cover; border-radius: 4px; margin: 0 !important; background: transparent; position: relative; z-index: 2; transition: opacity 0.3s;"; const isDarkTheme = document.body.classList.contains('dark') || document.documentElement.getAttribute('data-theme') === 'dark'; const badgeBg = isDarkTheme ? '#303134' : '#FFFFFF'; const badgeBorder = isDarkTheme ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.08)'; const badgeShadow = isDarkTheme ? '0 2px 6px rgba(0,0,0,0.4)' : '0 2px 6px rgba(0,0,0,0.15)'; const badgeStyle = `position: absolute; bottom: -5px; right: -5px; z-index: 3; width: 24px; height: 24px; border-radius: 50%; background-color: ${badgeBg}; border: ${badgeBorder}; box-shadow: ${badgeShadow}; box-sizing: border-box; transition: opacity 0.3s; pointer-events: none; display: grid; place-items: center; line-height: 0;`; const isBlur = isBlurEnabledForView(isGridView() ? 'grid' : 'list'); if (isMax && isBlur) { const remoteSrc = item.icon_link || item.thumbnail_link || ''; return `
${listIconSlotHtml('listMax', remoteSrc, { size: 50, remoteObjectFit: 'contain', remoteRadius: '4px' })} ${blHtml}
`; } if (isMax && item.thumbnail_link && !item.isHeader && item.thumbnail_link !== item.icon_link) { const isCached = window.pkGlobalThumbCache.has(item.id); const phOp = isCached ? '0' : '1'; const imgOp = isCached ? '1' : '0'; const isFolderLike = isFolder || item._isFolderLike || (item.icon_link && item.icon_link.includes('folder')); const badgeOp = (isFolderLike && isCached) ? '1' : '0'; let badgeHtml = ''; if (isFolderLike) { const innerContent = ``; badgeHtml = `
${innerContent}
`; } const isVideo = !isFolder && (typeof isGridVideoItem === 'function' ? isGridVideoItem(item) : (item.mime_type && item.mime_type.startsWith('video/'))); let videoOvHtml = ''; if (isVideo) { const vOp = isCached ? '1' : '0'; videoOvHtml = `
`; } return `
${listIconSlotHtml('listMax', item.icon_link || '', { size: 50, remoteObjectFit: 'contain', remoteRadius: '4px' })}
${badgeHtml} ${videoOvHtml} ${blHtml}
`; } if (isMax) { if (hasValidCover) { const showBadge = isFolder || item._isFolderLike || (item.icon_link && item.icon_link.includes('folder')); let badgeHtml = ''; if (showBadge) { const innerIcon = ``; badgeHtml = `
${innerIcon}
`; } return `
${listIconSlotHtml('listMax', item.icon_link || '', { size: 50, remoteObjectFit: 'contain', remoteRadius: '4px' })}
${badgeHtml} ${blHtml}
`; } const finalIconSrc = item.icon_link || item.thumbnail_link; if (finalIconSrc) { return `
${listIconSlotHtml('listMax', finalIconSrc, { size: 50, remoteObjectFit: 'contain', remoteRadius: '4px' })}${blHtml}
`; } return `
${listIconSlotHtml('listMax', '', { size: 50 })}${blHtml}
`; } const mime = (item.mime_type || '').toLowerCase(); const isMedia = item.kind !== 'drive#folder' && (mime.startsWith('image/') || mime.startsWith('video/') || (item.params && item.params.duration > 0)); const hasRealThumb = isMedia && item.thumbnail_link && item.thumbnail_link !== item.icon_link; if (hasRealThumb) { if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); const isCached = window.pkGlobalThumbCache.has(item.id); const phOp = isCached ? '0' : '1'; const imgOp = isCached ? '1' : '0'; const placeholder = listIconSlotHtml('list', item.icon_link || '', { size: 24, remoteClass: 'pk-min-icon-img', remoteObjectFit: 'contain' }); return `
${placeholder}
${blHtml}
`; } if (item.icon_link) { return `
${listIconSlotHtml('list', item.icon_link, { size: 24, remoteClass: 'pk-min-icon-img', remoteObjectFit: 'contain' })}${blHtml}
`; } return `
${listIconSlotHtml('list', item.icon_link || '', { size: 24, remoteClass: 'pk-min-icon-img', remoteObjectFit: 'contain' })}${blHtml}
`; }; const checkboxHtml = ``; let html = `
${checkboxHtml}
`; if (isGridMode && gridLayout) { getDynamicIcon(d); const isFolder = d.kind === 'drive#folder'; const isHistoryGrid = S.historyMode; const displayDate = S.trashMode ? getRemainingDays(d) : fmtDate(d.modified_time); const mimeLower = (d.mime_type || '').toLowerCase(); const knownDisplayDur = S.durationMap.get(d.id) || d._history_duration || d.params?.duration || 0; const isTimedMedia = !isFolder && (mimeLower.startsWith('video/') || mimeLower.startsWith('audio/') || knownDisplayDur > 0 || (isHistoryGrid && d._history_duration > 0)); const isVideo = !isFolder && (mimeLower.startsWith('video/') || (isHistoryGrid && d._history_duration > 0)); const displayDur = isHistoryGrid ? knownDisplayDur : (isTimedMedia ? knownDisplayDur : 0); const historyCurT = Math.max(0, Math.round(Number(d._history_progress || 0))); const historyTotalT = Math.max(0, Math.round(Number(displayDur || 0))); const historyPctRaw = historyTotalT > 0 ? Math.min(100, Math.max(0, (historyCurT / historyTotalT) * 100)) : 0; const historyPctText = historyTotalT > 0 ? `${Math.round(historyPctRaw)}%` : '-'; let historyPlayTimeStr = '-'; if (d._history_ts > 0) { historyPlayTimeStr = fmtDate(new Date(d._history_ts).toISOString()); } const historyDurText = displayDur > 0 ? fmtDur(displayDur) : '-'; let folderItemCount = null; if (isFolder && !S.trashMode) { let count = d.file_count; if (count === undefined || count === null) count = d.usage?.file_count; if (count === undefined || count === null) count = d.params?.file_count; if (count === undefined || count === null) count = d.audit?.file_count; if ((count === undefined || count === null) && !d._isShareItem && typeof globalCache !== 'undefined') { const cachedData = globalCache.get(d.id); if (cachedData) { const list = Array.isArray(cachedData) ? cachedData : (cachedData.items || []); count = list.length; } } if (count !== undefined && count !== null && count !== '') { const parsed = parseInt(count, 10); if (!Number.isNaN(parsed)) folderItemCount = Math.max(0, parsed); } } const displayFolderCount = folderItemCount === null ? '' : L.grid_folder_count.replace('{n}', folderItemCount); const hasThumb = !!(d.thumbnail_link && d.thumbnail_link !== d.icon_link); const isStarred = S.starredSet.has(d.id) || !!(d.starred || (d.tags && d.tags.some(t => t.name === 'STAR'))); const gridNameCapacity = Math.max(12, Math.min(26, Math.floor((gridLayout.cardWidth - (isProtected ? 78 : 18)) / (isMax ? 13 : 12)))); const nameDisplay = (S.search && shouldShowHl) ? getGridSearchHlHTML(d.name, S.search, gridNameCapacity) : esc(d.name); const coverCls = `pk-gv-cover ${isFolder ? 'pk-gv-folder' : 'pk-gv-file'}${hasThumb ? ' pk-gv-has-thumb' : ''}${isBlur && hasThumb ? ' pk-gv-blur' : ''}`; const iconFallback = getOfficialDirFallbackIconHtml(d, 'grid'); const menuIcon = ``; ensureGridMediaStore(); const stableFolderMedia = hasThumb ? getStableFolderMediaState(d) : { state: 'none', src: '' }; let folderThumbState = stableFolderMedia.state; let folderResolvedSrc = stableFolderMedia.src || d.thumbnail_link || ''; if (hasThumb && d.kind === 'drive#folder' && folderResolvedSrc && d._coverResolved && folderThumbState !== 'ok') { if (typeof window.markStableFolderMediaState === 'function') { window.markStableFolderMediaState(d.id, 'ok', folderResolvedSrc); } else { if (!window.pkThumbTriState) window.pkThumbTriState = { folder: Object.create(null) }; if (!window.pkGridMediaStore) window.pkGridMediaStore = { folder: Object.create(null) }; if (!window.pkGlobalThumbCache) window.pkGlobalThumbCache = new Set(); window.pkThumbTriState.folder[d.id] = 'ok'; window.pkGridMediaStore.folder[d.id] = folderResolvedSrc; window.pkGlobalThumbCache.add(d.id); } folderThumbState = 'ok'; } const folderHasCoverThumb = hasThumb && folderThumbState === 'ok' && !!folderResolvedSrc; if (hasThumb && folderThumbState === 'unknown' && !folderResolvedSrc) { window.pkThumbTriState.folder[d.id] = 'probing'; } const showFolderAnalyzeSize = isFolder && S.analyzeMode && !S.analyzeSimGroups && d.size !== undefined && d.size !== null && d.size !== ''; const showFolderDedupeSize = isFolder && S.analyzeMode && !!S.analyzeSimGroups && d.size !== undefined && d.size !== null && d.size !== ''; const folderSizeHtml = (showFolderAnalyzeSize || showFolderDedupeSize) ? `${fmtSize(d.size)}` : ''; const metaHtml = isHistoryGrid ? `
${historyPctText}${historyPlayTimeStr}${historyDurText}
` : isFolder ? `
${folderSizeHtml}${displayDate}${displayFolderCount ? `${displayFolderCount}` : ''}
` : `
${fmtSize(d.size)}${displayDate}${displayDur > 0 ? `${fmtDur(displayDur)}` : ''}
`; const cleanName = (d.name || "").replace(/[\r\n\v\f\u2028\u2029]+/g, ' ').trim().toLowerCase(); const isBlMarked = isFolder ? (S.blFolderSet && S.blFolderSet.has(cleanName)) : (S.blSet && S.blSet.has(cleanName)); const gridBlHtml = isBlMarked ? `${CONF.icons.blMarker}` : ''; const videoPlayDisplay = 'none'; html = `
${isVideo && hasThumb ? `
` : ''} ${(!S.shareParseMode && isStarred) ? `${_internalGetStarIcon(true).replace('pk-star-toggle', 'pk-star-icon')}` : ''} ${gridBlHtml} ${isHistoryGrid ? `
` : ''}
${nameDisplay} ${isProtected ? `${L.tag_default}` : ''}
${metaHtml}
`; } else if (S.historyMode) { const isStarred = S.starredSet.has(d.id) || !!(d.starred || (d.tags && d.tags.some(t => t.name === 'STAR'))); const starColor = isStarred ? '#FFC107' : '#ccc'; const starFill = isStarred ? '#FFC107' : 'none'; html += `
`; let iconImg = getDynamicIcon(d); if (isMax) { if (iconImg.includes('margin-right:12px')) iconImg = iconImg.replace('margin-right:12px', 'margin-right:20px'); else if (!iconImg.includes('margin-right')) iconImg = iconImg.replace(/style=['"]/, '$&margin-right:20px; '); } const nameDisplay = (S.search && shouldShowHl) ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${iconImg} ${nameDisplay}
`; html += `
${fmtSize(d.size)}
`; const displayDur = d._history_duration || d.params?.duration || 0; html += `
${displayDur > 0 ? fmtDur(displayDur) : '-'}
`; let progressHtml = '-'; const curT = Math.max(0, Math.round(Number(d._history_progress || 0))); const totalT = Math.max(0, Math.round(Number(displayDur || 0))); if (totalT > 0) { const pctRaw = Math.min(100, Math.max(0, (curT / totalT) * 100)); const pctText = Math.round(pctRaw); progressHtml = `
${fmtDur(curT)} ${pctText}%
`; } html += `
${progressHtml}
`; let playTimeStr = '-'; if (d._history_ts > 0) { playTimeStr = fmtDate(new Date(d._history_ts).toISOString()); } else { playTimeStr = "-"; } html += `
${playTimeStr}
`; } else if (S.offlineMode) { let iconImg = getDynamicIcon(d); if (isMax) { if (iconImg.includes('margin-right:12px')) { iconImg = iconImg.replace('margin-right:12px', 'margin-right:20px'); } else if (!iconImg.includes('margin-right')) { iconImg = iconImg.replace(/style=['"]/, '$&margin-right:20px; '); } } const isFailed = d.phase === 'PHASE_TYPE_ERROR'; const isOfflineRefMissingLocal = isOfflineReferenceMissingLocalMarked(d); const isNavigable = !!d.file_id && !isFailed && !isOfflineRefMissingLocal; 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 = (!isOfflineRefMissingLocal && isTaskDone && (isMedia || hasResolvedCover)) ? `data-pk-thumb="${d.thumbnail_link}"` : ''; const nameDisplay = S.search ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${iconImg} ${nameDisplay}
`; html += `
${fmtSize(d.size)}
`; let statusColor = 'var(--pk-fg)'; let statusText = d.phase; const isOfflineRefMissing = isOfflineReferenceMissingMarked(d); if (isOfflineRefMissing) { statusColor = '#ff4d4f'; statusText = d._offlineMissingMessage || getOfflineReferenceMissingText(); } else if (d.phase === 'PHASE_TYPE_COMPLETE') { statusColor = '#52c41a'; statusText = L.lbl_up_done; } else if (d.phase === 'PHASE_TYPE_RUNNING') { statusColor = '#1890ff'; statusText = L.lbl_up_downloading; } else if (d.phase === 'PHASE_TYPE_ERROR') { statusColor = '#ff4d4f'; statusText = L.str_failed; } else if (d.phase === 'PHASE_TYPE_PENDING') { statusText = L.msg_task_waiting; } else if (d.phase === 'PHASE_TYPE_PAUSED') { statusText = L.msg_task_paused; } if (d.phase === 'PHASE_TYPE_ERROR' && d.message) { statusText = d.message; } html += `
${statusText}
`; let progressHtml = ''; if (isOfflineRefMissing) { progressHtml = `
-
`; } else if (d.phase === 'PHASE_TYPE_COMPLETE') { progressHtml = `
100%
`; } else if (d.phase === 'PHASE_TYPE_ERROR') { progressHtml = `
-
`; } else { const pct = d.progress || 0; progressHtml = `
${pct}%
`; } html += `
${progressHtml}
`; } else if (S.uploadMode) { if (!d.mime_type && d.file) d.mime_type = d.file.type; const nameDisplay = shouldShowHl ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); const isDone = d.status === 'DONE'; const nameStyle = isDone ? '' : 'cursor:default; pointer-events:none; color:inherit;'; if (isDone && d.file && d.mime_type && d.mime_type.startsWith('image/')) { if (!d._localThumbUrl) { try { d._localThumbUrl = URL.createObjectURL(d.file); } catch(e) {} } if (d._localThumbUrl) d.thumbnail_link = d._localThumbUrl; } const hasResolvedCover = isDone && d.thumbnail_link && d.thumbnail_link !== d.icon_link; const thumbAttr = hasResolvedCover ? `data-pk-thumb="${d.thumbnail_link}"` : ''; html += `
${getDynamicIcon(d)} ${nameDisplay}
`; html += `
${fmtSize(d.size)}
`; html += `
${(d.status === 'UPLOADING' && S.upMng) ? S.upMng.fmtSpeed(d.speed) : '-'}
`; let statusColor = '#888'; const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; if (activeStatus.includes(d.status)) statusColor = 'var(--pk-pri)'; else if (d.status === 'DONE') statusColor = '#52c41a'; else if (d.status === 'ERROR') statusColor = '#d93025'; else if (d.status === 'PAUSED') statusColor = '#faad14'; html += `
${esc(d.message)} ${Math.floor(d.progress)}%
`; } else if (S.shareMode) { const iconUrl = d.icon_link; let iconImg = ''; const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); const lockHtml = d.pass_code ? `
${CONF.icons.lock}
` : ''; if (isShareDisabled) { iconImg = `
${CONF.typeIcons.file}
`; } else { const currentIcon = getDynamicIcon(d); const isUsingThumb = currentIcon.includes('pk-max-thumb'); if (isUsingThumb) { iconImg = `
${currentIcon}${lockHtml}
`; } else if (iconUrl) { iconImg = `
${lockHtml}
`; } else { const isFolder = d.kind === 'drive#folder'; iconImg = `
${getIcon(d)}
${lockHtml}
`; } } const nameDisplay = shouldShowHl ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${iconImg} ${nameDisplay}
`; html += `
${d.view_count || 0}
`; const saveVal = d.limit_count > 0 ? `${d.save_count || 0}/${d.limit_count}` : (d.save_count || 0); const saveTip = d.limit_count > 0 ? `${L.lbl_limit_tip}: ${d.limit_count}` : ''; const saveClass = d.limit_count > 0 ? "pk-force-tip" : ""; html += `
${saveVal}
`; let statusColor = 'inherit'; let statusText = ''; const rawTimeLeft = String(d.expiration_left || ""); const dayUnit = String(L.share_days || L.unit_days || '').trim(); const expireSuffix = String(L.str_expire_suffix || ''); const timeLeft = (/^\d+$/.test(rawTimeLeft.trim()) && dayUnit) ? `${rawTimeLeft.trim()}${dayUnit}` : rawTimeLeft; const expireSeconds = Number(d.expiration_left_seconds); const hasExpireSeconds = Number.isFinite(expireSeconds); const isPermanentShare = String(d.expiration_days) === "-1" || String(rawTimeLeft) === "-1" || String(d.expiration_left_seconds) === "-1"; 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 (isPermanentShare) { statusColor = '#52c41a'; statusText = L.share_perm; } else { statusText = dayUnit && timeLeft.endsWith(dayUnit) && expireSuffix.startsWith(dayUnit) ? timeLeft + expireSuffix.slice(dayUnit.length) : timeLeft + expireSuffix; const isUrgent = hasExpireSeconds && expireSeconds >= 0 && expireSeconds <= 24 * 3600; statusColor = isUrgent ? '#ff4d4f' : 'inherit'; } html += `
${statusText}
`; const shareDate = fmtDate(d.modified_time); html += `
${shareDate}
`; } else { const isRoot = S.path.length === 1 && S.path[0].id === ''; const isStarred = S.starredSet.has(d.id) || !!(d.starred || (d.tags && d.tags.some(t => t.name === 'STAR'))); const starColor = isStarred ? '#FFC107' : '#ccc'; const starFill = isStarred ? '#FFC107' : 'none'; let displayPath = rootPathStr; let shortPath = ""; if (S.trashMode) { if (d._lineage && Array.isArray(d._lineage)) { shortPath = d._lineage.map(p => p.name).join('/'); displayPath = shortPath; } } else if (d._lineage && Array.isArray(d._lineage)) { const relativePath = d._lineage.map(p => p.name).join('/'); shortPath = relativePath; if (isGlobalSearchRoot) { displayPath = L.btn_nav_home + (relativePath ? '/' + relativePath : ''); } else if (S.analyzeMode) { displayPath = relativePath || L.btn_nav_home; } else if (relativePath) { displayPath = rootPathStr + '/' + relativePath; } else { displayPath = rootPathStr; } } else if (isGlobalSearchRoot) { displayPath = L.btn_nav_home; shortPath = ""; } if (!S.shareParseMode) { if (S.trashMode) { html += `
`; } else { if (isProtected) html += `
`; else html += `
`; } } const isRealThumb = d.thumbnail_link && d.thumbnail_link !== d.icon_link; const thumbAttr = isRealThumb ? `data-pk-thumb="${d.thumbnail_link}"` : ''; const nameDisplay = (S.search && shouldShowHl) ? getSearchHlHTML(d.name, S.search, charCapacity) : esc(d.name); html += `
${getDynamicIcon(d)}${nameDisplay}${isProtected ? `${L.tag_default}` : ''}
`; if (showPathCol) { let pathHtml = ''; const shareInsightPathText = d._isShareInsightItem ? (d._shareFilePathText || getSharePathText(d._sharePath) || L.label_share_parse_path_root || '') : ''; const homeIcon = pkIconHtml('home', { width: 14, height: 14, style: 'margin-right:4px;flex-shrink:0;vertical-align:-4px;' }); 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 (shareInsightPathText) { displayPath = shareInsightPathText; let isSameAsPrev = false; if (i > firstVisiblePathIndex) { const prevItem = S.display[i - 1]; if (prevItem && !prevItem.isHeader) { const prevSharePathText = prevItem._isShareInsightItem ? (prevItem._shareFilePathText || getSharePathText(prevItem._sharePath) || L.label_share_parse_path_root || '') : ''; if (prevSharePathText === shareInsightPathText) isSameAsPrev = true; } } if (isSameAsPrev) { pathHtml = `${L.str_same_folder}`; } else { const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && shareInsightPathText) ? getSharePathSearchHlHTML(shareInsightPathText, S.search, pathCharCapacity) : esc(shareInsightPathText); pathHtml = `
${pathDisplay}
`; } } else if (isAnalyzeRoot) { const pStr = d._pathStr || d.path || ""; displayPath = pStr; let isSameAsPrev = false; if (i > firstVisiblePathIndex) { const prevItem = S.display[i - 1]; if (!prevItem.isHeader) { const prevPStr = prevItem._pathStr || prevItem.path || ""; if (prevPStr === pStr) isSameAsPrev = true; else isSameAsPrev = false; } else { isSameAsPrev = false; } } else { isSameAsPrev = false; } if (isSameAsPrev) { pathHtml = `${L.str_same_folder}`; } else { let content = pStr; if (pStr === homeText) content = ""; else if (pStr.startsWith(prefix)) content = pStr.substring(prefix.length); let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && content.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const sDisp = (shouldShowHl && sm) ? `/` : '/'; const sHtml = `${sDisp}`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && content) ? getSearchHlHTML(content, hq, pathCharCapacity) : esc(content); let innerHtml = `${homeIcon}${homeDisplay}`; if (content) { innerHtml += `${sHtml}${pathDisplay}`; } pathHtml = `
${innerHtml}
`; } } else if (S.dupMode) { let isSameAsPrev = false; const rawPath = d._dupFullPath || ""; if (i > firstVisiblePathIndex) { const prevItem = S.display[i - 1]; if (!prevItem.isHeader) { const prevRawPath = prevItem._dupFullPath || ""; if (prevRawPath === rawPath) isSameAsPrev = true; else isSameAsPrev = false; } else { isSameAsPrev = false; } } else { isSameAsPrev = false; } if (isSameAsPrev) { pathHtml = `${L.str_same_folder}`; } else { let realPath = rawPath; if (rawPath === L.current_dir) { realPath = rootPathStr; } else { if (!rawPath.startsWith(rootPathStr)) { realPath = rootPathStr + '/' + rawPath; } } let contentStr = realPath; if (realPath === homeText) { contentStr = ""; } else if (realPath.startsWith(prefix)) { contentStr = realPath.substring(prefix.length); } let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && contentStr.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const sDisp = (shouldShowHl && sm) ? `/` : '/'; const sHtml = `${sDisp}`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && contentStr) ? getSearchHlHTML(contentStr, hq, pathCharCapacity) : esc(contentStr); let innerHtml = `${homeIcon}${homeDisplay}`; if (contentStr) { innerHtml += `${sHtml}${pathDisplay}`; } pathHtml = `
${innerHtml}
`; } } else { let fullPathStr = ""; if (d._lineage === null && d.parent_id && d.parent_id !== 'root') { pathHtml = `...`; displayPath = null; } else if (isGlobalSearchRoot) { fullPathStr = shortPath; } else { if (displayPath && displayPath.startsWith(prefix)) { fullPathStr = displayPath.substring(prefix.length); } else { fullPathStr = shortPath; } } let isSameAsPrev = false; if (i > firstVisiblePathIndex) { const prevItem = S.display[i - 1]; if (!prevItem.isHeader) { let prevPathStr = ""; if (S.analyzeMode) { prevPathStr = prevItem._pathStr || ""; } else { if (prevItem._lineage) prevPathStr = prevItem._lineage.map(x=>x.name).join('/'); } if (prevPathStr === shortPath) isSameAsPrev = true; } } if (displayPath !== null) { if (isSameAsPrev) { pathHtml = `${L.str_same_folder}`; } else { let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && fullPathStr.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const sDisp = (shouldShowHl && sm) ? `/` : '/'; const sHtml = `${sDisp}`; const includePath = UI.chkSearchPath && UI.chkSearchPath.checked; const pathDisplay = (shouldShowHl && includePath && fullPathStr) ? getSearchHlHTML(fullPathStr, hq, pathCharCapacity) : esc(fullPathStr); let innerHtml = `${homeIcon}${homeDisplay}`; if (fullPathStr) { innerHtml += `${sHtml}${pathDisplay}`; } pathHtml = `
${innerHtml}
`; } } } let pathTipHtml = getTooltipHlHTML(displayPath || "", S.search); if (displayPath && displayPath.startsWith(homeText)) { const rest = displayPath.substring(homeText.length); const homeDisplayTip = getTooltipHlHTML(homeText, homeQuery); let restDisplayTip = ''; if (rest.startsWith('/')) { const pureRest = rest.substring(1); let hq = restQuery; let sm = isSlashMatched; if (hq.endsWith('/')) hq = hq.slice(0, -1); if (hq.startsWith('/') && pureRest.toLowerCase().startsWith(hq.substring(1).toLowerCase())) { sm = true; hq = hq.substring(1); } const slashTip = (shouldShowHl && sm) ? `/` : '/'; restDisplayTip = slashTip + getTooltipHlHTML(pureRest, hq); } else { restDisplayTip = getTooltipHlHTML(rest, restQuery); } const homeGroup = `${homeIcon}${homeDisplayTip}`; pathTipHtml = `
${homeGroup}${restDisplayTip}
`; } html += `
${pathHtml}
`; } const displaySize = (d.kind === 'drive#folder' && !S.analyzeMode) ? '-' : fmtSize(d.size); html += `
${displaySize}
`; if (!isAnalyzeRoot) { const isFolder = d.kind === 'drive#folder'; const displayDur = S.durationMap.get(d.id) || d.params?.duration || 0; const mime = (d.mime_type || '').toLowerCase(); const ext = (d.name || '').split('.').pop().toLowerCase(); const isVideoFormat =['mp4','mkv','avi','mov','wmv','flv','webm','ts'].includes(ext); const isVideo = !isFolder && (mime.startsWith('video/') || isVideoFormat || displayDur > 0); let durHtml = "-"; if (isFolder) { if (!S.trashMode) { let count = d.file_count; if ((count === undefined || count === null) && !d._isShareItem && typeof globalCache !== 'undefined') { const cachedData = globalCache.get(d.id); if (cachedData) { const list = Array.isArray(cachedData) ? cachedData : (cachedData.items ||[]); count = list.length; } } const hasCount = count !== undefined && count !== null; if (hasCount) { durHtml = `${count} ${L.str_items}`; } } } else if (isVideo && displayDur > 0) { durHtml = fmtDur(displayDur); } else { const rawName = d.name || ""; const lastDot = rawName.lastIndexOf('.'); if (lastDot > 0) { const extUpper = rawName.substring(lastDot + 1).toUpperCase(); const SETS = { vid: new Set(['MP4','MKV','AVI','MOV','WMV','FLV','WEBM','TS','M4V','3GP','MPG','MPEG','RM','RMVB','ASF','VOB','DAT','DIVX','F4V','M2TS','MTS','TP','TRP','OGV','MPE','M2V','M3U8']), aud: new Set(['MP3','WAV','FLAC','AAC','OGG','WMA','APE','M4A','AMR','OPUS','M4B','ALAC','AIFF','MID','MIDI','RA','DTS','AC3','DSF','DFF']), img: new Set(['JPG','JPEG','PNG','GIF','BMP','WEBP','SVG','TIF','TIFF','ICO','HEIC','HEIF','RAW','CR2','NEF','ARW','DNG','ORF','AVIF','PSD','AI','EPS','JFIF','JPE']), doc: new Set(['TXT','HTML','PDF','PPTX','CHM','DOCX','XLSX','HTM','DOC','DWG','MDB','PPT','XLS','RTF','ODT','ODS','ODP','EPUB','MOBI','AZW3','DJVU','CBZ','CBR','MD','LOG','CSV','XML','JSON']), app: new Set(['APK','EXE','IPA','DMG','RPM','DEB','MSI','PKG','XAPK','APKS','AAB','JAR','BIN','SH','BAT','CMD']), arc: new Set(['ZIP','RAR','7Z','TAR','GZ','ISO','CAB','BZ2','XZ','TGZ','WIM','ESD','IMG','ZST','LZH']), sub: new Set(['SRT','ASS','SSA','VTT','SMI','SUB','IDX','SUP','LRC']) }; if (SETS.img.has(extUpper)) durHtml = `${extUpper} ${L.type_img}`; else if (SETS.arc.has(extUpper) || /^(PART\d+|\d{3}|[RZ]\d{2})$/.test(extUpper)) durHtml = `${extUpper} ${L.type_archive}`; else if (SETS.doc.has(extUpper)) durHtml = `${extUpper} ${L.type_doc}`; else if (SETS.sub.has(extUpper)) durHtml = `${extUpper} ${L.type_sub}`; else if (SETS.app.has(extUpper)) durHtml = `${extUpper} ${L.type_app}`; else if (SETS.aud.has(extUpper)) durHtml = `${extUpper} ${L.cat_audio}`; else if (SETS.vid.has(extUpper)) durHtml = `${extUpper} ${L.cat_video}`; else durHtml = `${extUpper} ${L.type_suffix}`; } else { durHtml = L.type_suffix; } } html += `
${durHtml}
`; } const dateTxt = S.trashMode ? getRemainingDays(d) : fmtDate(d.modified_time); html += `
${dateTxt}
`; } if (!canReuseHistoryRow && !canReuseGridRow) { row.innerHTML = html; } row.dataset.pkBoundId = d.id || ''; row.dataset.pkBoundKind = d.kind || ''; if (suppressGridRebindTransition) { const reboundId = d.id || ''; requestAnimationFrame(() => { if (row.dataset.pkBoundId === reboundId) { row.style.transition = ''; row.style.animation = ''; } }); } if (isGridMode && !canReuseGridRow && typeof patchFrozenGridMedia === 'function') { patchFrozenGridMedia(row, d, prevGridMediaNode, prevGridMediaKey); } else if (canReuseGridRow && typeof syncGridVideoPlayState === 'function') { requestAnimationFrame(() => syncGridVideoPlayState(row, d)); } if (isGridMode && useGridStableRows && !canReuseGridRow && !gridStableDuplicateId) { const finalGridStableSig = canTryGridStableRow ? gridStableSig : ''; row.dataset.pkGridStable = finalGridStableSig ? '1' : '0'; row.dataset.pkGridSig = finalGridStableSig; } const thumbImg = row.querySelector('.pk-max-thumb, .pk-min-thumb'); if (thumbImg) { const markFrozenThumbState = (state) => { if (thumbImg.dataset.pkFrozenCover !== '1' || !thumbImg.dataset.pkId || typeof window.markStableFileMediaState !== 'function') return; window.markStableFileMediaState(thumbImg.dataset.pkId, state, state === 'ok' ? (thumbImg.dataset.pkSrc || thumbImg.currentSrc || thumbImg.src || '') : ''); }; const toggleThumb = () => { if(window.pkGlobalThumbCache) window.pkGlobalThumbCache.add(d.id); markFrozenThumbState('ok'); thumbImg.style.opacity = '1'; const placeholder = thumbImg.previousElementSibling; if (placeholder && (placeholder.classList.contains('pk-placeholder-layer') || placeholder.classList.contains('pk-min-ph'))) { placeholder.style.opacity = '0'; } const parent = thumbImg.parentElement; const badge = parent ? parent.querySelector('.pk-folder-badge') : null; if (badge) { badge.style.opacity = '1'; } const vidOv = parent ? parent.querySelector('.pk-video-ov') : null; if (vidOv) { vidOv.style.opacity = '1'; } if (typeof syncGridVideoPlayState === 'function') { syncGridVideoPlayState(row, d); } if (S.isSelected(d.id)) scheduleStatUpdate(); }; const handleThumbError = () => { thumbImg.style.display = 'none'; if (window.pkGlobalThumbCache) window.pkGlobalThumbCache.delete(d.id); markFrozenThumbState('fail'); const placeholder = thumbImg.previousElementSibling; if (placeholder && (placeholder.classList.contains('pk-placeholder-layer') || placeholder.classList.contains('pk-min-ph'))) { placeholder.style.opacity = '1'; } const parent = thumbImg.parentElement; const badge = parent ? parent.querySelector('.pk-folder-badge') : null; if (badge) { badge.style.opacity = '0'; } if (typeof syncGridVideoPlayState === 'function') { syncGridVideoPlayState(row, d); } if (S.isSelected(d.id)) scheduleStatUpdate(); }; 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'); if (chk && !d.isHeader) { const shouldChecked = S.isSelected(d.id); if (chk.checked !== shouldChecked) chk.checked = shouldChecked; if (chk.indeterminate) chk.indeterminate = false; } const gridMoreBtn = row.querySelector('.pk-gv-more'); if (gridMoreBtn) { gridMoreBtn.onclick = (evt) => { evt.preventDefault(); evt.stopPropagation(); const openMenu = () => { const liveRow = UI.in.querySelector(`.pk-row[data-id="${d.id}"]`) || row; const liveBtn = liveRow.querySelector('.pk-gv-more') || gridMoreBtn; const btnRect = liveBtn.getBoundingClientRect(); liveRow.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: Math.max(btnRect.left + 8, btnRect.right - 8), clientY: btnRect.bottom + 6, button: 2 })); }; if (!S.isSelected(d.id) || S.activeId !== d.id) { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; S.lastSelIdx = i; renderVisible(); updateStat(); requestAnimationFrame(openMenu); return; } openMenu(); }; } const triggerOpen = () => { if (S.movingIds.has(d.id)) return; if (d._isShareItem) { if (d.kind === 'drive#folder') { if (!S.shareParseInsightMode) handleShareParseFolderOpen(d); } else if (isArchiveLike(d)) handleOpenShareArchive(d); else if (isAudioLikeItem(d) || isVideoLikeItem(d) || isImageLikeItem(d)) handleOpenShareFile(d); else if (isTxtFileLike(d)) handleOpenShareTextFile(d); else showUnsupportedOpenToast(); 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']; const isAudio = isAudioLikeItem(it); return { isAudio, isVideo: !isAudio && (m.startsWith('video/') || dur > 0 || vExts.some(e => n.endsWith('.' + e))), isImage: m.startsWith('image/'), isTorrent: n.endsWith('.torrent'), isArchive: m.includes('zip') || m.includes('rar') || m.includes('archive') || aExts.some(e => n.endsWith('.' + e)) }; }; if (S.uploadMode) { if (d.status !== 'DONE') { if (d.file_id) { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); } return; } const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isAudio || t.isVideo || t.isImage) { if (t.isAudio) playAudio(d); else if (t.isVideo) playVideo(d); else showImage(d); } else if (d.file_id) { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); } return; } if (S.offlineMode) { if (!d.file_id || d.phase === 'PHASE_TYPE_ERROR') return; if (d.phase !== 'PHASE_TYPE_COMPLETE') { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); return; } const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isAudio || t.isVideo || t.isImage) { if (t.isAudio) playAudio(d); else if (t.isVideo) playVideo(d); else showImage(d); } else { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) { locateBtn.click(); } } return; } if (S.shareMode) { const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if (isShareDisabled) return; showShareDetail(d); return; } if (d.kind === 'drive#folder') { if (!S.trashMode) { const isExitMode = S.isFlattened || S.dupMode; if (isExitMode) { S.isFlattened = false; S.dupMode = false; UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if(UI.dupTools) UI.dupTools.style.display = 'none'; if(UI.btnFolderFirst) UI.btnFolderFirst.style.display = 'flex'; if(UI.btnNewFolder) UI.btnNewFolder.style.display = 'flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if (d._lineage && d._lineage.length > 0) { const newPath = [{ id: '', name: L.btn_nav_home }]; d._lineage.forEach(n => { if (n.id && n.id !== 'root') newPath.push({ id: n.id, name: n.name }); }); if (newPath.length === 0 || newPath[newPath.length-1].id !== d.id) newPath.push({ id: d.id, name: d.name }); stopLiveManualRefreshBeforePathChange('path_change'); S.path = newPath; } else { stopLiveManualRefreshBeforePathChange('path_change'); 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; syncFolderFirstFromGlobal(); S.saveNavScrollTop(S.getNavBucketKey(), S.path); stopLiveManualRefreshBeforePathChange('path_change'); S.path.push(d); load(); } } } else { const t = checkType(d); if (t.isTorrent) handleTorrentFile(d); else if (t.isArchive) handleOpenArchive(d); else if (t.isAudio) playAudio(d); else if (t.isVideo) playVideo(d); else if (t.isImage) showImage(d); else if (isTxtFileLike(d)) handleOpenTextFile(d); else showUnsupportedOpenToast(); } }; row.onclick = async (e) => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; } const nameText = e.target.closest('.pk-name .pk-name-txt'); const isAnalyzeRoot = S.analyzeMode && S.path[S.path.length - 1].id === 'analyze_root'; const isProtectedView = S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups); if (!S.trashMode && nameText) { const selectedCount = S.getSelectedCount(); const isCurrentSelected = S.isSelected(d.id); if (isProtectedView && selectedCount > 5) { if (selectedCount > 1 || !isCurrentSelected) { if (!await confirmSelectionClear()) return; } } S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; if (S.offlineMode && isOfflineReferenceMissingLocalMarked(d)) { renderVisible(); updateStat(); return; } triggerOpen(); renderVisible(); updateStat(); return; } S.activeId = d.id; if (e.target === chk) { if (e.shiftKey && S.lastSelIdx !== -1) { const startIdx = Math.min(S.lastSelIdx, i); const endIdx = Math.max(S.lastSelIdx, i); const targetState = chk.checked; for (let k = startIdx; k <= endIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) { S.setSelected(item.id, targetState); } } } else { S.setSelected(d.id, chk.checked); } } else { if (e.shiftKey && S.lastSelIdx !== -1) { const startIdx = Math.min(S.lastSelIdx, i); const endIdx = Math.max(S.lastSelIdx, i); for (let k = startIdx; k <= endIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) S.setSelected(item.id, true); } } else if (e.ctrlKey || e.metaKey) { S.toggleSelected(d.id); } else { const selectedCount = S.getSelectedCount(); const isCurrentSelected = S.isSelected(d.id); if (isProtectedView && selectedCount > 5) { if (selectedCount > 1 || !isCurrentSelected) { if (!await confirmSelectionClear()) { renderVisible(); return; } } } S.setAllSelection(false); S.setSelected(d.id, true); } } if (S.activeId && !S.isSelected(S.activeId)) { S.activeId = null; } S.lastSelIdx = i; const selectedCountForClick = isGridView() ? S.getSelectedCount() : -1; const rows = UI.in.children; for (let k = 0; k < rows.length; k++) { const r = rows[k]; if (r.dataset.id) { const rid = r.dataset.id; const isSelected = S.isSelected(rid); const isActiveRow = (S.activeId === rid); const isFocused = isSelected && isActiveRow; if (!(isActiveRow && !isSelected)) { r.style.backgroundColor = ''; r.style.border = ''; r.style.borderRadius = ''; } if (isActiveRow && !isSelected) { r.style.backgroundColor = 'var(--pk-sel-bg)'; r.style.border = '1px solid var(--pk-pri)'; r.style.borderRadius = isGridView() ? `${getGridCardRadius()}px` : '4px'; } const cls = getRowClassName(isSelected, isFocused, r.classList.contains('pk-moving'), selectedCountForClick); if (r.className !== cls) r.className = cls; const cb = r.querySelector('input[type="checkbox"]'); if (cb && cb.checked !== isSelected) cb.checked = isSelected; } else if (r.classList.contains('pk-group-hd')) { const grpChk = r.querySelector('.pk-grp-chk'); if (grpChk) { const gNode = S.display[start + k]; if (gNode && gNode.isHeader) { const gIdx = parseInt(gNode.id.replace('grp_', '')); const gIds = Array.isArray(gNode.groupIds) ? gNode.groupIds : (S.dupMode ? (S.dupRawGroups[gIdx]?.ids || []) : (S.analyzeMode ? (S.analyzeSimGroups[gIdx]?.ids || []) : [])); let selCount = 0; gIds.forEach(id => { if (S.isSelected(id)) selCount++; }); const isAll = gIds.length > 0 && selCount === gIds.length; const isInd = selCount > 0 && selCount < gIds.length; if (grpChk.checked !== isAll) grpChk.checked = isAll; if (grpChk.indeterminate !== isInd) grpChk.indeterminate = isInd; } } } } updateStat(); }; row.ondblclick = (e) => { e.preventDefault(); if (d._isShareItem) { if (d.kind === 'drive#folder') { if (!S.shareParseInsightMode) handleShareParseFolderOpen(d); } else triggerOpen(); return; } 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) { if (isOfflineReferenceMissingLocalMarked(d)) return; 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 }); } stopLiveManualRefreshBeforePathChange('path_change'); S.path = newPath; } else { stopLiveManualRefreshBeforePathChange('path_change'); S.path = [{ id: '', name: L.btn_nav_home }, { id: d.id, name: d.name }]; } load(); } else { syncFolderFirstFromGlobal(); S.saveNavScrollTop(S.getNavBucketKey(), S.path); stopLiveManualRefreshBeforePathChange('path_change'); 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 (isAudioLikeItem(d)) { playAudio(d); } else if (mime.startsWith('video')) { playVideo(d); } else if (mime.startsWith('image')) { showImage(d); } else if (!S.offlineMode && !S.uploadMode && isTxtFileLike(d)) { handleOpenTextFile(d); } } }; row.oncontextmenu = (e) => { e.preventDefault(); if ((S.shareParseMode || d._isShareItem) && !(S.shareParseMode && d._isShareItem && S.shareParseListActive)) { if (UI.ctx) UI.ctx.style.display = 'none'; return; } if (!S.isSelected(d.id)) { S.setAllSelection(false); S.setSelected(d.id, true); S.activeId = d.id; S.lastSelIdx = i; renderVisible(); updateStat(); } const itms = { share: UI.ctx.querySelector('#ctx-share'), open: UI.ctx.querySelector('#ctx-open'), extPlay: UI.ctx.querySelector('#ctx-ext-play'), star: UI.ctx.querySelector('#ctx-star'), prop: UI.ctx.querySelector('#ctx-property'), locate: UI.ctx.querySelector('#ctx-locate'), down: UI.ctx.querySelector('#ctx-down'), cpName: UI.ctx.querySelector('#ctx-copy-name'), cut: UI.ctx.querySelector('#ctx-cut'), copy: UI.ctx.querySelector('#ctx-copy'), rename: UI.ctx.querySelector('#ctx-rename'), prune: UI.ctx.querySelector('#ctx-prune'), addBl: UI.ctx.querySelector('#ctx-add-bl'), del: UI.ctx.querySelector('#ctx-del'), restore: UI.ctx.querySelector('#ctx-restore'), delF: UI.ctx.querySelector('#ctx-del-forever'), shCancel: UI.ctx.querySelector('#ctx-share-cancel'), shDetail: UI.ctx.querySelector('#ctx-share-detail'), shCopy: UI.ctx.querySelector('#ctx-share-copy'), taskRetry: UI.ctx.querySelector('#ctx-task-retry') }; const seps = Array.from(UI.ctx.querySelectorAll('.pk-ctx-sep')); if (!itms.taskRetry) { const retryDiv = document.createElement('div'); retryDiv.className = 'pk-ctx-item'; retryDiv.id = 'ctx-task-retry'; retryDiv.innerHTML = `${CONF.icons.refresh} ${L.btn_retry_task}`; const ref = document.getElementById('ctx-down'); if (ref) ref.parentNode.insertBefore(retryDiv, ref); itms.taskRetry = retryDiv; } if (!UI.ctx.querySelector('#ctx-up-pause')) { const pBtn = document.createElement('div'); pBtn.className = 'pk-ctx-item'; pBtn.id = 'ctx-up-pause'; pBtn.innerHTML = `${CONF.icons.taskPause} ${L.btn_up_pause}`; const delRef = document.getElementById('ctx-del'); if (delRef) delRef.parentNode.insertBefore(pBtn, delRef); } if (!UI.ctx.querySelector('#ctx-up-start')) { const sBtn = document.createElement('div'); sBtn.className = 'pk-ctx-item'; sBtn.id = 'ctx-up-start'; sBtn.innerHTML = `${CONF.icons.taskStart} ${L.btn_up_start}`; const delRef = document.getElementById('ctx-del'); if (delRef) delRef.parentNode.insertBefore(sBtn, delRef); } const allCtxItems = UI.ctx.querySelectorAll('.pk-ctx-item'); const allCtxSeps = UI.ctx.querySelectorAll('.pk-ctx-sep'); allCtxItems.forEach(el => el.style.display = 'none'); allCtxSeps.forEach(el => el.style.display = 'none'); const sepShareExtra = UI.ctx.querySelector('#sep-share-extra'); if (S.shareParseMode && d._isShareItem && S.shareParseListActive) { const sp = { open: UI.ctx.querySelector('#ctx-open'), ext: UI.ctx.querySelector('#ctx-ext-play'), name: UI.ctx.querySelector('#ctx-copy-name'), save: UI.ctx.querySelector('#ctx-share-parse-save'), sep2: UI.ctx.querySelector('#sep-2'), sep3: UI.ctx.querySelector('#sep-3') }; const ids = S.getSelectedIds(); const isSingle = ids.length === 1; const item = isSingle ? ((S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(ids[0])) || (S.shareParseItemMap && S.shareParseItemMap.get(ids[0])) || S.itemMap.get(ids[0]) || d) : null; const isVideo = !!(item && item.kind !== 'drive#folder' && isVideoLikeItem(item)); if (sp.open) sp.open.style.display = isSingle ? 'flex' : 'none'; if (sp.ext) sp.ext.style.display = isVideo ? 'flex' : 'none'; if (sp.sep2) sp.sep2.style.display = isSingle ? 'block' : 'none'; if (sp.name) sp.name.style.display = 'flex'; if (sp.sep3) sp.sep3.style.display = 'block'; if (sp.save) { sp.save.style.display = 'flex'; sp.save.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; handleShareParseSaveSelected(); }; } } else if (S.shareMode) { const sh = { name: UI.ctx.querySelector('#ctx-copy-name'), cancel: UI.ctx.querySelector('#ctx-sh-cancel'), detail: UI.ctx.querySelector('#ctx-sh-detail'), copy: UI.ctx.querySelector('#ctx-sh-copy') }; const selectedIds = S.getSelectedIds(); const isSingle = (selectedIds.length === 1); const isShareDisabled = (d.share_status === 'DELETED') || (d.limit_count > 0 && d.save_count >= d.limit_count); if(seps[0]) seps[0].style.display = 'none'; if(seps[1]) seps[1].style.display = 'none'; if(sh.name) sh.name.style.display = 'flex'; if(seps[2]) seps[2].style.display = 'block'; if(sh.cancel) sh.cancel.style.display = 'flex'; const showDetails = isSingle && !isShareDisabled; if (showDetails) { if(sepShareExtra) sepShareExtra.style.display = 'block'; if(sh.detail) sh.detail.style.display = 'flex'; if(sh.copy) sh.copy.style.display = 'flex'; } if(seps[3]) seps[3].style.display = 'none'; } else if (S.uploadMode) { const upEls = { open: UI.ctx.querySelector('#ctx-open'), ext: UI.ctx.querySelector('#ctx-ext-play'), loc: UI.ctx.querySelector('#ctx-locate'), name: UI.ctx.querySelector('#ctx-copy-name'), del: UI.ctx.querySelector('#ctx-del'), pause: UI.ctx.querySelector('#ctx-up-pause'), start: UI.ctx.querySelector('#ctx-up-start'), sep1: UI.ctx.querySelector('#sep-1'), sep2: UI.ctx.querySelector('#sep-2'), sep3: UI.ctx.querySelector('#sep-3') }; const uploadSelectedIds = S.getSelectedIds(); if (uploadSelectedIds.length > 1) { const items = uploadSelectedIds.map(id => S.itemMap.get(id)).filter(Boolean); let hasActive = false, hasPaused = false, hasDone = false; items.forEach(t => { if (['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(t.status)) hasActive = true; else if (['PAUSED', 'ERROR'].includes(t.status)) hasPaused = true; else if (t.status === 'DONE') hasDone = true; }); upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; if (hasActive && !hasPaused && !hasDone) { if (upEls.sep1) { upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; } if (upEls.pause) { upEls.pause.style.order = '20'; upEls.pause.style.display = 'flex'; upEls.pause.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpPause) UI.btnUpPause.click(); }; } } else if (!hasActive && hasPaused && !hasDone) { if (upEls.sep1) { upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; } if (upEls.start) { upEls.start.style.order = '20'; upEls.start.style.display = 'flex'; upEls.start.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpStart) UI.btnUpStart.click(); }; } } 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 isAudio = isAudioLikeItem(d); 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 = (isAudio || isVideo || isImg) ? 'flex' : 'none'; upEls.ext.style.order = '11'; upEls.ext.style.display = isVideo ? 'flex' : 'none'; upEls.loc.style.order = '12'; upEls.loc.style.display = d.file_id ? 'flex' : 'none'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; upEls.name.style.order = '20'; upEls.name.style.display = 'flex'; upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else if (isActive) { upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; if (upEls.pause) { upEls.pause.style.order = '20'; upEls.pause.style.display = 'flex'; upEls.pause.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpPause) UI.btnUpPause.click(); }; } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } else { upEls.name.style.order = '10'; upEls.name.style.display = 'flex'; upEls.sep1.style.order = '15'; upEls.sep1.style.display = 'block'; if (upEls.start) { upEls.start.style.order = '20'; upEls.start.style.display = 'flex'; upEls.start.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpStart) UI.btnUpStart.click(); }; } upEls.sep2.style.order = '25'; upEls.sep2.style.display = 'block'; upEls.del.style.order = '30'; upEls.del.style.display = 'flex'; upEls.del.innerHTML = `${CONF.icons.del} ${L.btn_up_del}`; upEls.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; if(UI.btnUpDel) UI.btnUpDel.click(); }; } } } else if (S.offlineMode) { const els = { open: UI.ctx.querySelector('#ctx-open'), ext: UI.ctx.querySelector('#ctx-ext-play'), loc: UI.ctx.querySelector('#ctx-locate'), prop: UI.ctx.querySelector('#ctx-property'), name: UI.ctx.querySelector('#ctx-copy-name'), retry: UI.ctx.querySelector('#ctx-task-retry'), link: UI.ctx.querySelector('#ctx-copy-link'), del: UI.ctx.querySelector('#ctx-del'), bl: UI.ctx.querySelector('#ctx-add-bl'), sep1: UI.ctx.querySelector('#sep-1'), sep2: UI.ctx.querySelector('#sep-2'), sep3: UI.ctx.querySelector('#sep-3') }; const ids = S.getSelectedIds(); const isSingle = ids.length === 1; const firstItem = S.itemMap.get(ids[0]); const hasRetryable = ids.some(id => isOfflineRetryableTask(S.itemMap.get(id))); const isOfflineRefMissingLocal = isOfflineReferenceMissingLocalMarked(firstItem); let isAud = false, isVid = false, isImg = false; if (firstItem && firstItem.file_id && !isOfflineRefMissingLocal) { const m = (firstItem.mime_type || '').toLowerCase(); const n = (firstItem.name || '').toLowerCase(); isAud = isAudioLikeItem(firstItem); isVid = m.startsWith('video/') || ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp'].some(e => n.endsWith('.'+e)); isImg = m.startsWith('image/'); } let g1 = 0; if (els.open) { els.open.style.order = '10'; const isFolderLike = !isOfflineRefMissingLocal && ((firstItem.mime_type && (firstItem.mime_type.includes('folder') || firstItem.mime_type.includes('directory'))) || (firstItem.icon_link && firstItem.icon_link.includes('folder'))); const isReadyMedia = !isOfflineRefMissingLocal && firstItem.phase === 'PHASE_TYPE_COMPLETE' && (isAud || isVid || isImg); const canOpen = isSingle && firstItem?.file_id && !isOfflineRefMissingLocal && firstItem.phase !== 'PHASE_TYPE_ERROR' && (isReadyMedia || isFolderLike); els.open.style.display = canOpen ? 'flex' : 'none'; if(canOpen) g1++; } if (els.ext) { els.ext.style.order = '11'; const canExt = isSingle && firstItem?.file_id && !isOfflineRefMissingLocal && firstItem.phase === 'PHASE_TYPE_COMPLETE' && isVid; els.ext.style.display = canExt ? 'flex' : 'none'; if(canExt) g1++; } if (els.prop) { els.prop.style.order = '12'; els.prop.style.display = isSingle ? 'flex' : 'none'; if(isSingle) g1++; } if (els.loc) { els.loc.style.order = '13'; const canLocate = isSingle && firstItem?.file_id && !isOfflineRefMissingLocal && 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 = hasRetryable ? 'flex' : 'none'; if(hasRetryable) g3++; } if (els.link) { els.link.style.order = '31'; els.link.style.display = 'flex'; els.link.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; const tasks = ids.map(id => S.itemMap.get(id)).filter(t => t && (t.source_url || t.params?.url)); if (tasks.length) { GM_setClipboard(tasks.map(t => t.source_url || t.params.url).join('\n')); showToast(L.msg_copy_success); } }; g3++; } let g4 = 0; if (els.del) { els.del.style.order = '40'; els.del.style.display = 'flex'; els.del.innerHTML = `${CONF.icons.del} ${L.btn_del}`; els.del.onclick = (e) => { e.stopPropagation(); UI.ctx.style.display = 'none'; UI.btnDel.click(); }; g4++; } if (els.bl) { els.bl.style.order = '41'; els.bl.style.display = 'flex'; setBlacklistContextItemState(els.bl); g4++; } if (els.sep1) { els.sep1.style.order = '15'; els.sep1.style.display = (g1 > 0 && (g2 > 0 || g3 > 0 || g4 > 0)) ? 'block' : 'none'; } if (els.sep2) { els.sep2.style.order = '25'; els.sep2.style.display = (g2 > 0 && (g3 > 0 || g4 > 0)) ? 'block' : 'none'; } if (els.sep3) { els.sep3.style.order = '35'; els.sep3.style.display = (g3 > 0 && g4 > 0) ? 'block' : 'none'; } } else { if(sepShareExtra) sepShareExtra.style.display = 'none'; if (S.trashMode) { const tr = { cpName: UI.ctx.querySelector('#ctx-copy-name'), restore: UI.ctx.querySelector('#ctx-restore'), delF: UI.ctx.querySelector('#ctx-del-forever'), addBl: UI.ctx.querySelector('#ctx-add-bl'), sep1: UI.ctx.querySelector('#sep-trash-1'), sep2: UI.ctx.querySelector('#sep-trash-2') }; if(tr.cpName) tr.cpName.style.display = 'flex'; if(tr.sep1) tr.sep1.style.display = 'block'; if(tr.restore) tr.restore.style.display = 'flex'; if(tr.sep2) tr.sep2.style.display = 'block'; if(tr.delF) tr.delF.style.display = 'flex'; if(tr.addBl) { tr.addBl.style.display = 'flex'; setBlacklistContextItemState(tr.addBl); } }else { if(itms.share) itms.share.style.display = 'flex'; if(seps[0]) seps[0].style.display = 'block'; let hasGroup2 = false; const ctxSelectedIds = S.getSelectedIds(); const ctxSelectedCount = ctxSelectedIds.length; if (ctxSelectedCount === 1) { [itms.open, itms.prop].forEach(el => { if(el) el.style.display = 'flex'; }); const isVideo = (d.mime_type || '').startsWith('video/') || (d.params && d.params.duration > 0); if (itms.extPlay) itms.extPlay.style.display = isVideo ? 'flex' : 'none'; hasGroup2 = true; } if (itms.star) { let hasUnstarred = false; let hasValidItem = false; for (const id of ctxSelectedIds) { const it = S.itemMap.get(id); if (it && !isSystemItem(it)) { hasValidItem = true; if (!(it.starred || (it.tags && it.tags.some(t => t.name === 'STAR')))) { hasUnstarred = true; break; } } } itms.star.style.display = hasValidItem ? 'flex' : 'none'; if (hasValidItem) { hasGroup2 = true; itms.star.innerHTML = hasUnstarred ? `${ctxIcons.star} ${L.ctx_star}` : `${ctxIcons.unstar} ${L.ctx_unstar}`; itms.star.setAttribute('data-action', hasUnstarred ? 'star' : 'unstar'); } } const isSearchRoot = S.path.length > 0 && S.path[S.path.length - 1].id === 'virtual_search_root'; const canLocateUpload = S.uploadMode && d.status === 'DONE' && d.file_id; const selectedCount = S.getSelectedCount(); if((S.starredMode || S.recentMode || S.historyMode || isSearchRoot || S.dupMode || S.isFlattened || S.analyzeMode || canLocateUpload) && selectedCount === 1) { itms.locate.style.display = 'flex'; hasGroup2 = true; } if(seps[1]) seps[1].style.display = hasGroup2 ? 'block' : 'none'; [itms.down, itms.cpName].forEach(el => { if(el) el.style.display = 'flex'; }); if (S.historyMode) { if(seps[2]) seps[2].style.display = 'none'; [itms.cut, itms.copy, itms.rename].forEach(el => { if(el) el.style.display = 'none'; }); } else { if(seps[2]) seps[2].style.display = 'block'; } if (S.historyMode) { if(seps[3]) seps[3].style.display = 'block'; } else { if (!isProtected) { if(itms.cut) itms.cut.style.display = 'flex'; if(itms.rename) { itms.rename.style.display = 'flex'; itms.rename.innerHTML = (selectedCount > 1) ? `${ctxIcons.renameBulk} ${L.btn_bulkrename}` : `${ctxIcons.rename} ${L.ctx_rename}`; } } if(itms.copy) itms.copy.style.display = 'flex'; let hasFolder = false; const selectedIds = S.getSelectedIds(); for (const id of selectedIds) { if (S.itemMap.get(id)?.kind === 'drive#folder') { hasFolder = true; break; } } if(itms.prune && hasFolder) itms.prune.style.display = 'flex'; if(seps[3]) seps[3].style.display = 'block'; } if (itms.addBl) { itms.addBl.style.display = 'flex'; setBlacklistContextItemState(itms.addBl); } if (itms.del) itms.del.style.display = 'flex'; } } const useFlexOrder = S.offlineMode || S.uploadMode; UI.ctx.style.display = useFlexOrder ? 'flex' : 'block'; if (useFlexOrder) UI.ctx.style.flexDirection = 'column'; let x = e.clientX, y = e.clientY, w = 160, h = UI.ctx.offsetHeight || 280; let winW = window.innerWidth, winH = window.innerHeight; if (x + w > winW) x = winW - w - 10; if (y + h > winH) y = winH - h - 10; UI.ctx.style.left = x + 'px'; UI.ctx.style.top = y + 'px'; }; } } flushVisibleRowPool(UI.in, poolPtr); if (window.pkRefreshTooltip) { requestAnimationFrame(() => { window.pkRefreshTooltip(); }); } maybeLoadShareParseNextPage(); } let isMarquee = false, mqStartX = 0, mqStartY = 0, startScroll = 0, lastMouseX = 0, lastMouseY = 0, scrollSpeed = 0, scrollRaf = null, marqueeRenderRaf = null, marqueeAutoScrollTs = 0; let lastRngS = -1, lastRngE = -1, cachedVpRect = null; const mqBox = document.createElement('div'); mqBox.className = 'pk-selection-box'; el.appendChild(mqBox); const scheduleMarqueeRefresh = () => { if (marqueeRenderRaf) return; marqueeRenderRaf = requestAnimationFrame(() => { marqueeRenderRaf = null; refreshVisibleSelectionState(); updateStat(); }); }; const updateMarqueeUIAndSelection = (targetX, targetY) => { if (!cachedVpRect) return; const cssTargetX = targetX; const cssTargetY = targetY; const cssRectTop = cachedVpRect.top; const cssRectBottom = cachedVpRect.bottom; const cssRectLeft = cachedVpRect.left; const cssMqStartX = mqStartX; const cssMqStartY = mqStartY; const curScroll = UI.vp.scrollTop; const clampedY = Math.max(cssRectTop, Math.min(cssRectBottom, cssTargetY)); const logicA = cssMqStartY - cssRectTop + startScroll; const logicB = clampedY - cssRectTop + curScroll; const logTop = Math.max(0, Math.min(logicA, logicB)); const logBot = Math.max(logicA, logicB); const visTop = logTop - curScroll + cssRectTop; const visBot = logBot - curScroll + cssRectTop; const clipT = Math.max(cssRectTop, visTop), clipB = Math.min(cssRectBottom, visBot); const drawH = Math.max(0, clipB - clipT); const safeRight = cssRectLeft + UI.vp.clientWidth; const clampedX = Math.max(cssRectLeft, Math.min(safeRight, cssTargetX)); const drawL = Math.min(cssMqStartX, clampedX); const drawW = Math.abs(clampedX - cssMqStartX); mqBox.style.borderTopWidth = (visTop < cssRectTop) ? '0' : '1px'; mqBox.style.borderBottomWidth = (visBot > cssRectBottom) ? '0' : '1px'; mqBox.style.borderLeftWidth = (cssTargetX < cssRectLeft) ? '0' : '1px'; mqBox.style.borderRightWidth = (cssTargetX > safeRight) ? '0' : '1px'; Object.assign(mqBox.style, { display: drawH > 0 ? 'block' : 'none', width: drawW + 'px', height: drawH + 'px', transform: `translate3d(${drawL}px, ${clipT}px, 0)` }); const drawR = drawL + drawW; const logicLeft = Math.max(0, drawL - cssRectLeft); const logicRight = Math.max(logicLeft, drawR - cssRectLeft); const sIdx = Math.floor(logTop / CONF.rowHeight), eIdx = Math.min(S.display.length - 1, Math.floor(logBot / CONF.rowHeight)); if (sIdx !== lastRngS || eIdx !== lastRngE || isGridView()) { lastRngS = sIdx; lastRngE = eIdx; if (!window.event?.ctrlKey && !window.event?.metaKey) { S.selMode = 'explicit'; S.sel.clear(); S.selEx.clear(); } if (isGridView()) { const gridLayout = getGridLayout(); const dupGridMeta = isGroupedGridView() ? getGroupedGridMeta() : null; if (dupGridMeta && dupGridMeta.indexLayout) { dupGridMeta.indexLayout.forEach((layout, itemIdx) => { if (!layout || layout.kind !== 'item') return; const item = S.display[itemIdx]; if (!item || item.isHeader || S.movingIds.has(item.id)) return; const cardLeft = layout.left; const cardRight = cardLeft + layout.width; const cardTop = layout.top; const cardBottom = cardTop + layout.height; if (cardLeft < logicRight && cardRight > logicLeft && cardTop < logBot && cardBottom > logTop) { S.setSelected(item.id, true); } }); } else { const adjustedTop = Math.max(0, logTop - gridLayout.gapY); const adjustedBot = Math.max(adjustedTop, logBot - gridLayout.gapY); const startRow = Math.max(0, Math.floor(adjustedTop / CONF.rowHeight)); const endRow = Math.max(startRow, Math.floor(adjustedBot / CONF.rowHeight)); for (let rowIdx = startRow; rowIdx <= endRow; rowIdx++) { for (let colIdx = 0; colIdx < gridLayout.cols; colIdx++) { const itemIdx = rowIdx * gridLayout.cols + colIdx; if (itemIdx >= S.display.length) break; const item = S.display[itemIdx]; if (!item || item.isHeader || S.movingIds.has(item.id)) continue; const cardLeft = gridLayout.padX + colIdx * (gridLayout.cardWidth + gridLayout.gapX); const cardRight = cardLeft + gridLayout.cardWidth; const cardTop = gridLayout.gapY + rowIdx * CONF.rowHeight; const cardBottom = cardTop + gridLayout.cardHeight; if (cardLeft < logicRight && cardRight > logicLeft && cardTop < logBot && cardBottom > logTop) { S.setSelected(item.id, true); } } } } } else { for (let k = sIdx; k <= eIdx; k++) { const item = S.display[k]; if (item && !item.isHeader && !S.movingIds.has(item.id)) S.setSelected(item.id, true); } } scheduleMarqueeRefresh(); } }; const runAutoScroll = (ts = 0) => { if (!isMarquee || scrollSpeed === 0) { scrollRaf = null; marqueeAutoScrollTs = 0; return; } const dt = marqueeAutoScrollTs ? Math.min(32, ts - marqueeAutoScrollTs) : 16; marqueeAutoScrollTs = ts || performance.now(); const prevTop = UI.vp.scrollTop; const maxTop = Math.max(0, UI.vp.scrollHeight - UI.vp.clientHeight); const nextTop = Math.max(0, Math.min(maxTop, prevTop + (scrollSpeed * (dt / 16)))); if (nextTop === prevTop) { scrollSpeed = 0; scrollRaf = null; marqueeAutoScrollTs = 0; return; } UI.vp.scrollTop = nextTop; updateMarqueeUIAndSelection(lastMouseX, lastMouseY); scrollRaf = requestAnimationFrame(runAutoScroll); }; const handleMarqueeMove = (e) => { if (!cachedVpRect) return; if (!isMarquee) { const moveX = Math.abs(e.clientX - mqStartX); const moveY = Math.abs(e.clientY - mqStartY); if (moveX > 5 || moveY > 5) { isMarquee = true; S.activeId = null; UI.win.classList.add('pk-is-seeking'); refreshVisibleSelectionState(); } else { return; } } lastMouseX = e.clientX; lastMouseY = e.clientY; const logicalClientY = e.clientY; const edgeZone = 28; if (logicalClientY > cachedVpRect.bottom - edgeZone) scrollSpeed = Math.min(26, 2 + Math.pow((logicalClientY - (cachedVpRect.bottom - edgeZone)) / 6, 1.2)); else if (logicalClientY < cachedVpRect.top + edgeZone) scrollSpeed = -Math.min(26, 2 + Math.pow(((cachedVpRect.top + edgeZone) - logicalClientY) / 6, 1.2)); else scrollSpeed = 0; if (scrollSpeed !== 0 && !scrollRaf) scrollRaf = requestAnimationFrame(runAutoScroll); updateMarqueeUIAndSelection(e.clientX, e.clientY); }; const stopMarquee = () => { isMarquee = false; scrollSpeed = 0; marqueeAutoScrollTs = 0; if (UI.win) UI.win.classList.remove('pk-is-seeking'); lastRngS = -1; lastRngE = -1; cachedVpRect = null; if (mqBox) { mqBox.style.display = 'none'; mqBox.style.transform = 'none'; mqBox.style.width = '0px'; mqBox.style.height = '0px'; } if (scrollRaf) cancelAnimationFrame(scrollRaf); if (marqueeRenderRaf) cancelAnimationFrame(marqueeRenderRaf); scrollRaf = null; marqueeRenderRaf = null; window.removeEventListener('mousemove', handleMarqueeMove); window.removeEventListener('mouseup', stopMarquee); }; let isFileDragging = false; let fileDragGhost = null; let dropTargetId = null; let dropTargetType = null; let autoOpenTimer = null; let lastHoverSep = null; const handleFileDragMove = (e) => { if (!isFileDragging) { const moveX = Math.abs(e.clientX - mqStartX); const moveY = Math.abs(e.clientY - mqStartY); if (moveX > 5 || moveY > 5) { isFileDragging = true; document.body.classList.add('pk-dragging'); fileDragGhost = document.createElement('div'); fileDragGhost.className = 'pk-drag-ghost'; if (el.classList.contains('pk-dark')) fileDragGhost.classList.add('pk-dark'); const selectedIds = S.getSelectedIds(); const count = selectedIds.length; const firstId = selectedIds[0]; const firstItem = S.itemMap.get(firstId); let text = firstItem ? firstItem.name : 'Selected Items'; if (count > 1) { text += L.str_drag_files.replace('{n}', count); } else if (!firstItem) { text = L.str_selected_items; } let dragIcon = getOfficialFolderFallbackIconHtml(20); if (firstItem && firstItem.icon_link) { dragIcon = ``; } fileDragGhost.innerHTML = `${dragIcon}${esc(text)}`; document.body.appendChild(fileDragGhost); } else { return; } } if (fileDragGhost) { fileDragGhost.style.left = e.clientX + 'px'; fileDragGhost.style.top = e.clientY + 'px'; } const prevTargets = document.querySelectorAll('.pk-drop-target'); prevTargets.forEach(el => el.classList.remove('pk-drop-target')); dropTargetId = null; dropTargetType = null; if (fileDragGhost) fileDragGhost.style.display = 'none'; const elUnder = document.elementFromPoint(e.clientX, e.clientY); if (fileDragGhost) fileDragGhost.style.display = 'flex'; if (!elUnder) return; const menuItem = elUnder.closest('.pk-crumb-item'); if (menuItem && menuItem.dataset.id) { const tid = menuItem.dataset.id; const currentFolderId = S.path[S.path.length - 1]?.id || ''; if (tid !== currentFolderId && tid !== 'loading' && tid !== 'error') { dropTargetId = tid; dropTargetType = 'menu_item'; menuItem.classList.add('pk-drop-target'); return; } } const sep = elUnder.closest('.pk-crumb-sep'); if (sep) { if (lastHoverSep !== sep) { lastHoverSep = sep; if (autoOpenTimer) clearTimeout(autoOpenTimer); autoOpenTimer = setTimeout(() => { if (isFileDragging) sep.click(); }, 500); } } else { lastHoverSep = null; if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } } const row = elUnder.closest('.pk-row'); if (row && row.dataset.id) { const tid = row.dataset.id; const item = S.itemMap.get(tid); if (item && item.kind === 'drive#folder' && !S.isSelected(tid)) { dropTargetId = tid; dropTargetType = 'row'; row.classList.add('pk-drop-target'); return; } } const crumbItem = elUnder.closest('#pk-crumb span'); if (crumbItem && !crumbItem.classList.contains('pk-crumb-sep') && crumbItem.dataset.id) { const tid = crumbItem.dataset.id === 'root' ? '' : crumbItem.dataset.id; const currentFolderId = S.path[S.path.length - 1]?.id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; if (normalize(tid) !== normalize(currentFolderId)) { dropTargetId = tid; dropTargetType = 'crumb'; crumbItem.classList.add('pk-drop-target'); } } }; const executeFileTransfer = async (items, opType, sourcePid, targetPid, targetNameForLog) => { if (!items || items.length === 0) return; if (sourcePid !== '__VIRTUAL__' && sourcePid === targetPid) { showToast(L.err_paste_descendant, 'error'); return; } const foldersToMoveIds = items.filter(it => it.kind === 'drive#folder').map(it => it.id); if (foldersToMoveIds.length > 0) { const isDescendant = (targetId, ancestorIds) => { let currentId = targetId; let safety = 100; while (currentId && currentId !== 'root' && safety > 0) { if (ancestorIds.includes(currentId)) return true; const parent = globalParentIndex.get(currentId); currentId = parent ? parent.id : null; safety--; } return false; }; if (isDescendant(targetPid, foldersToMoveIds)) { showToast(L.err_paste_descendant, 'error'); return; } } S.movingSourceId = sourcePid || 'root'; S.movingDestId = targetPid || 'root'; const allIds = items.map(it => it.id); if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') { abandonLiveManualRefresh('local_mutation', { localRemovedIds: opType === 'move' ? allIds : [], toast: 'info', detach: false }); } 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} -> ${targetNameForLog || L.str_target_folder}`); const apiTargetPid = (targetPid === 'root') ? '' : targetPid; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/${endpoint}`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunkIds, to: { parent_id: apiTargetPid } }) }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error_description || `API ${res.status}`); if (data.task_id) { await new Promise(resolve => { const checkTask = async () => { try { const tRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${data.task_id}`, { headers: getHeaders() }); const tData = await tRes.json(); if (tData.phase === 'PHASE_TYPE_COMPLETE' || tData.phase === 'PHASE_TYPE_ERROR') resolve(); else setTimeout(checkTask, 800); } catch { resolve(); } }; checkTask(); }); } else if (opType === 'move' && chunkIds.length > 0) { const lastId = chunkIds[chunkIds.length - 1]; let retry = 0; while (retry < 20) { try { const meta = await apiGet(lastId); if (meta.parent_id === targetPid || meta.trashed) break; } catch(e) { break; } await sleep(500); retry++; } } chunkIds.forEach(id => S.movingIds.delete(id)); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_REM', ids: chunkIds }); if (opType === 'move') { const movedSet = new Set(chunkIds); S.items = S.items.filter(it => !movedSet.has(it.id)); if (typeof globalCache !== 'undefined') { const cleanListChunk = (raw) => { if (Array.isArray(raw)) return raw.filter(f => !movedSet.has(f.id)); if (raw && Array.isArray(raw.items)) { raw.items = raw.items.filter(f => !movedSet.has(f.id)); return raw; } return raw; }; for (const key of globalCache.keys()) { globalCache.set(key, cleanListChunk(globalCache.get(key))); } for (const key of S.cache.keys()) { S.cache.set(key, cleanListChunk(S.cache.get(key))); } } if (typeof pkState !== 'undefined' && pkState && pkState.lastGlobalResults) { pkState.lastGlobalResults = pkState.lastGlobalResults.filter(x => !movedSet.has(x.id)); } } else { if (targetPid === (S.path[S.path.length-1].id||'')) { const newItems = (data.files || []).map(f => minifyFile(f, false)); S.items.unshift(...newItems); } } if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); refresh(); processedCount += chunk.length; } if (targetPid) gmSet('pk_fmod_' + targetPid, new Date(getServerNow()).toISOString()); if (S.analyzeMap) { const deltaSize = items.reduce((acc, it) => acc + parseInt(it.size || 0), 0); if (deltaSize > 0) { const updateAnalyzeChain = (startId, isAdd) => { let currId = startId; let safety = 50; while (currId && S.analyzeMap.has(currId) && safety > 0) { const node = S.analyzeMap.get(currId); const oldSize = parseInt(node.size || 0); node.size = isAdd ? (oldSize + deltaSize) : Math.max(0, oldSize - deltaSize); currId = node.parentId; safety--; } }; if (S.analyzeMap.has(targetPid)) updateAnalyzeChain(targetPid, true); if (opType === 'move' && S.analyzeMap.has(sourcePid)) updateAnalyzeChain(sourcePid, false); if (S.analyzeResultItems) { S.analyzeResultItems.forEach(resItem => { if (S.analyzeMap.has(resItem.id)) { resItem.size = S.analyzeMap.get(resItem.id).size.toString(); } }); } } } const keysToClear = [sourcePid || 'root', targetPid || 'root']; keysToClear.forEach(k => { const normalizedK = (k === '' || k === 'root') ? 'root' : k; const altK = (normalizedK === 'root') ? '' : normalizedK; S.cache.delete(normalizedK); S.cache.delete(altK); if (typeof globalCache !== 'undefined') { globalCache.delete(normalizedK); globalCache.delete(altK); if (typeof scannedFolderIds !== 'undefined') { scannedFolderIds.delete(altK); } } if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(altK); } }); if (opType === 'move') { const purgeMovedDescendants = (fid) => { if (typeof globalLineageMap !== 'undefined') globalLineageMap.delete(fid); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(fid); const data = (typeof globalCache !== 'undefined' ? globalCache.get(fid) : null) || (S.cache ? S.cache.get(fid) : null); if (data) { const list = Array.isArray(data) ? data : (data.items || []); list.forEach(child => { if (child.kind === 'drive#folder') { purgeMovedDescendants(child.id); } }); if (typeof globalCache !== 'undefined') globalCache.delete(fid); if (S.cache) S.cache.delete(fid); } }; items.forEach(it => { if (it.kind === 'drive#folder') purgeMovedDescendants(it.id); }); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (typeof scheduleBackgroundResume === 'function') scheduleBackgroundResume(); 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; scheduleBackgroundResume(); if (progressTask) progressTask.destroy(); updateStat(); } }; const stopFileDrag = async () => { if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } lastHoverSep = null; window.removeEventListener('mousemove', handleFileDragMove); window.removeEventListener('mouseup', stopFileDrag); document.body.classList.remove('pk-dragging'); if (fileDragGhost) fileDragGhost.remove(); fileDragGhost = null; let targetName = L.lbl_type_folder; if (dropTargetType === 'menu_item') { const activeItem = document.querySelector('.pk-crumb-item.pk-drop-target span'); if (activeItem) targetName = activeItem.textContent; } else if (dropTargetType === 'row') { targetName = S.itemMap.get(dropTargetId)?.name || L.lbl_type_folder; } else if (dropTargetType === 'crumb') { const pathNode = S.path.find(p => (p.id || 'root') === (dropTargetId || 'root')); targetName = pathNode ? pathNode.name : L.str_target_folder; } const targets = document.querySelectorAll('.pk-drop-target'); targets.forEach(t => t.classList.remove('pk-drop-target')); document.querySelectorAll('.pk-crumb-pop').forEach(pop => { if (typeof pop._cleanup === 'function') pop._cleanup(); else pop.remove(); }); UI.crumb.querySelectorAll('.pk-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); sep.innerHTML = CONF.crumbIcons.right; }); if (!isFileDragging || dropTargetId === null) { isFileDragging = false; return; } isFileDragging = false; const currentFolderId = S.path[S.path.length - 1]?.id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; if (normalize(dropTargetId) === normalize(currentFolderId)) return; let hasSystem = false; const dragSelectedIds = S.getSelectedIds(); for (const id of dragSelectedIds) { const item = S.itemMap.get(id); if (item && isSystemItem(item)) { hasSystem = true; break; } } if (hasSystem) { showToast(`${L.msg_sys_error}`, 'error'); return; } const rawItems = []; const validItems = []; let skipLocked = 0; let skipSelf = 0; let skipConflict = 0; const isDescendantConflict = (candidateId) => { if (!S.movingIds || S.movingIds.size === 0) return false; const hotspots = [S.movingSourceId, S.movingDestId].filter(x => x && x !== 'root'); if (hotspots.length === 0) return false; for (const startPoint of hotspots) { let curr = startPoint; let safety = 50; while (curr && curr !== 'root' && safety > 0) { if (curr === candidateId) return true; const parentInfo = globalParentIndex.get(curr); curr = parentInfo ? parentInfo.id : null; safety--; } } return false; }; dragSelectedIds.forEach(id => { const item = S.itemMap.get(id); if (!item) return; rawItems.push(item); if (S.movingIds.has(id)) { skipLocked++; return; } const targetId = normalize(dropTargetId); const selfId = normalize(item.id); const parentId = normalize(item.parent_id); if (targetId === selfId || targetId === parentId) { skipSelf++; return; } if (item.kind === 'drive#folder' && isDescendantConflict(item.id)) { skipConflict++; return; } validItems.push(item); }); if (skipLocked > 0 || skipSelf > 0 || skipConflict > 0) { const reasons = []; if (skipLocked) reasons.push(L.msg_skip_locked.replace('{n}', skipLocked)); if (skipSelf) reasons.push(L.msg_skip_self.replace('{n}', skipSelf)); if (skipConflict) reasons.push(L.msg_skip_conflict.replace('{n}', skipConflict)); showToast(`${L.msg_skip_invalid} ${reasons.join(', ')}`, 'warning'); } S.clearSelection(); refresh(); if (validItems.length > 0) { await executeFileTransfer( validItems, 'move', normalize(currentFolderId), normalize(dropTargetId), targetName ); } }; UI.vp.addEventListener('mousedown', (e) => { if (e.button !== 0 || e.detail > 1) return; if (S.linkBookmarkMode) return; if (e.target.closest('.pk-btn, .pk-star-icon, input')) return; const row = e.target.closest('.pk-row'); const isClickingSelected = row && row.dataset.id && S.isSelected(row.dataset.id); if (isClickingSelected && !S.trashMode && !S.shareMode && !S.shareParseMode && !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; } if (S.shareParseMode && !S.shareParseListActive) return; cachedVpRect = getLogicalRect(UI.vp); const safeRight = cachedVpRect.left + UI.vp.clientWidth; const safeBottom = cachedVpRect.top + UI.vp.clientHeight; const logicalClientX = e.clientX; const logicalClientY = e.clientY; if (logicalClientX > safeRight || logicalClientY > safeBottom) return; let startItem = null; if (isGridView()) { const gridLayout = getGridLayout(); const logicalOffsetX = logicalClientX - cachedVpRect.left; const logicalOffsetY = logicalClientY - cachedVpRect.top + UI.vp.scrollTop; let startIdx = -1; if (isGroupedGridView()) { startIdx = findDupGridHitIndex(logicalOffsetX, logicalOffsetY); } else { const totalRows = Math.max(1, Math.ceil(S.display.length / Math.max(1, gridLayout.cols))); if (logicalOffsetX >= gridLayout.padX && logicalOffsetY >= 0 && logicalOffsetY < totalRows * CONF.rowHeight) { const colStride = gridLayout.cardWidth + gridLayout.gapX; const relX = logicalOffsetX - gridLayout.padX; const rowIdx = Math.floor(logicalOffsetY / CONF.rowHeight); const colIdx = Math.floor(relX / Math.max(1, colStride)); const withinColX = relX - colIdx * colStride; const withinRowY = logicalOffsetY - rowIdx * CONF.rowHeight; if (colIdx >= 0 && colIdx < gridLayout.cols && withinColX >= 0 && withinColX <= gridLayout.cardWidth && withinRowY >= 0 && withinRowY <= gridLayout.cardHeight) { startIdx = rowIdx * gridLayout.cols + colIdx; } } } if (startIdx >= 0 && startIdx < S.display.length) { const hitItem = S.display[startIdx]; startItem = (hitItem && !hitItem.isHeader) ? hitItem : null; } } else { const startOffsetY = logicalClientY - cachedVpRect.top + UI.vp.scrollTop; const startIdx = Math.floor(startOffsetY / CONF.rowHeight); if (startIdx >= 0 && startIdx < S.display.length) { const hitItem = S.display[startIdx]; startItem = (hitItem && !hitItem.isHeader) ? hitItem : null; } } mqStartX = e.clientX; mqStartY = e.clientY; const prevActiveId = S.activeId; S.activeId = startItem ? startItem.id : null; if (!startItem && prevActiveId && S.getSelectedCount() === 0) { renderVisible(); requestAnimationFrame(() => { if (UI.chkAll) UI.chkAll.checked = false; }); } isMarquee = false; mqStartX = lastMouseX = e.clientX; mqStartY = lastMouseY = e.clientY; startScroll = UI.vp.scrollTop; window.addEventListener('mousemove', handleMarqueeMove); window.addEventListener('mouseup', stopMarquee); }); let isScrollScheduled = false; let gridScrollEndTimer = 0; const markGridScrolling = () => { if (!isGridView() || !UI.win) return; UI.win.classList.add('pk-grid-scrolling'); if (gridScrollEndTimer) clearTimeout(gridScrollEndTimer); gridScrollEndTimer = setTimeout(() => { gridScrollEndTimer = 0; if (UI.win) UI.win.classList.remove('pk-grid-scrolling'); }, 90); }; const tryLoadRecentRootNextPage = async () => { if (!S.recentMode || S.path.length !== 1) return; const cur = S.path[S.path.length - 1]; if (!cur || cur.id !== 'recent_root') return; if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') return; if (S.loading || S.scanning || S.recentLoadingNextPage || S.recentRefreshFirstPageOnly || S.recentLightProbeRunning) return; if (!UI.vp || UI.vp.scrollHeight <= UI.vp.clientHeight + 20) return; if (UI.vp.scrollTop + UI.vp.clientHeight < UI.vp.scrollHeight - 300) return; const session = (typeof globalCache !== 'undefined') ? globalCache.get('recent_session') : null; const nextToken = session && session.nextToken && !session.completed ? session.nextToken : null; if (!nextToken) return; const tempCache = { items: [...S.items], nextToken }; S.cache.set('recent_root', tempCache); if (typeof globalCache !== 'undefined') { globalCache.set('recent_root', tempCache); globalCache.set('recent_session', { phase: 'active', nextToken, completed: false, count: S.items.length }); } S.recentLoadingNextPage = true; S.recentLoadNextPageOnly = true; S.pagingLoading = true; updateStat(); try { await load(false, false); } catch (e) { if (!e || e.name !== 'AbortError') console.warn('[Recent] Failed to load next page:', e); } finally { S.recentLoadNextPageOnly = false; S.recentLoadingNextPage = false; S.pagingLoading = false; updateStat(); } }; const isDupGridScrollRafMode = () => !!(UI.vp && isGridView() && isGroupedGridView()); const flushDupGridScrollRender = (force = false) => { S.dupGridScrollRaf = 0; if (!isDupGridScrollRafMode()) { S.dupGridScrollDirty = false; return false; } const latestTop = UI.vp.scrollTop; const shouldRender = force || S.dupGridScrollDirty || Math.abs(latestTop - S.dupGridLastRenderedTop) >= 1; S.dupGridScrollDirty = false; S.dupGridLastScrollTop = latestTop; if (shouldRender) { S.dupGridLastRenderedTop = latestTop; renderVisible(); } if (typeof isMarquee !== 'undefined' && isMarquee && !scrollRaf) { updateMarqueeUIAndSelection(lastMouseX, lastMouseY); } return shouldRender; }; const scheduleDupGridScrollRender = () => { if (!isDupGridScrollRafMode()) return false; S.dupGridScrollDirty = true; S.dupGridLastScrollTop = UI.vp.scrollTop; if (!S.dupGridScrollRaf) { S.dupGridScrollRaf = requestAnimationFrame(() => flushDupGridScrollRender(false)); } if (S.dupGridScrollEndTimer) clearTimeout(S.dupGridScrollEndTimer); S.dupGridScrollEndTimer = setTimeout(() => { S.dupGridScrollEndTimer = 0; if (!isDupGridScrollRafMode()) return; S.dupGridScrollDirty = true; if (!S.dupGridScrollRaf) { S.dupGridScrollRaf = requestAnimationFrame(() => flushDupGridScrollRender(true)); } }, 90); return true; }; UI.vp.onscroll = () => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; consumeOfflineLightProbePendingWrite('context_closed'); } markGridScrolling(); if (S.historyMode && UI.vp) S.historyScrollTop = Math.max(0, Math.round(UI.vp.scrollTop || 0)); if (scheduleDupGridScrollRender()) { tryLoadRecentRootNextPage(); loadOfficialHistoryNextPage(); return; } if (!isScrollScheduled) { requestAnimationFrame(() => { renderVisible(); if (typeof isMarquee !== 'undefined' && isMarquee && !scrollRaf) { updateMarqueeUIAndSelection(lastMouseX, lastMouseY); } isScrollScheduled = false; }); isScrollScheduled = true; } tryLoadRecentRootNextPage(); loadOfficialHistoryNextPage(); }; UI.vp.addEventListener('wheel', () => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; consumeOfflineLightProbePendingWrite('context_closed'); } markGridScrolling(); scheduleDupGridScrollRender(); }, { capture: true, passive: true }); const handleBlankClick = async (e) => { if (UI.ctx && UI.ctx.style.display !== 'none') { UI.ctx.style.display = 'none'; consumeOfflineLightProbePendingWrite('context_closed'); } if (e.target.closest('.pk-nav-btn') || e.target.closest('.pk-btn') || e.target.closest('.pk-btn-toggle') || e.target.closest('input') || e.target.closest('textarea') || e.target.closest('select') || e.target.closest('.pk-nav span') || e.target.closest('.pk-crumb-sep') || e.target.closest('.pk-search') || e.target.closest('label') || e.target.closest('.pk-dropdown-wrap') || e.target.closest('.pk-sort-opt') || e.target.closest('a') ) return; const selectedCount = S.getSelectedCount(); if (selectedCount > 0 || S.activeId) { const isAnalyzeRoot = S.analyzeMode && S.path[S.path.length - 1].id === 'analyze_root'; if ((S.dupMode || (isAnalyzeRoot && S.analyzeSimGroups)) && selectedCount > 5) { if (!await confirmSelectionClear()) return; } S.clearSelection(); S.activeId = null; renderVisible(); requestAnimationFrame(() => { if (UI.chkAll) UI.chkAll.checked = false; }); } }; const sidebarArea = el.querySelector('.pk-sidebar'); if (sidebarArea) sidebarArea.onclick = handleBlankClick; const footerArea = el.querySelector('.pk-ft'); if (footerArea) footerArea.onclick = handleBlankClick; const headerArea = el.querySelector('.pk-hd'); if (headerArea) headerArea.onclick = handleBlankClick; const topBarArea = el.querySelector('#pk-top-bar'); if (topBarArea) topBarArea.onclick = handleBlankClick; const actionArea = el.querySelector('#pk-actionbar'); if (actionArea) actionArea.onclick = handleBlankClick; const trashArea = el.querySelector('#pk-trash-bar'); if (trashArea) trashArea.onclick = handleBlankClick; el.addEventListener('click', (e) => { if (!S.activeId) return; if (e.target.closest('.pk-row')) return; if (e.target.closest('#pk-theme')) return; if (e.target.closest('#pk-view-switch') || e.target.closest('.pk-view-switch') || e.target.closest('#pk-view-list') || e.target.closest('#pk-view-grid') || e.target.closest('.pk-view-btn')) return; S.activeId = null; renderVisible(); }, { capture: true }); if (UI.vp) { UI.vp.addEventListener('click', (e) => { if (e.target.closest('.pk-row')) return; const rect = getLogicalRect(UI.vp); if (e.clientX > rect.left + UI.vp.clientWidth || e.clientY > rect.top + UI.vp.clientHeight) return; if (Math.abs(e.clientX - mqStartX) > 5 || Math.abs(e.clientY - mqStartY) > 5) return; handleBlankClick(e); }); } const showCrumbDropdown = async (e, parentId, triggerEl) => { const getLiveTriggerEl = () => { if (triggerEl && triggerEl.isConnected) return triggerEl; if (!UI.crumb) return triggerEl; const idx = S.path.findIndex(p => p.id === parentId); const seps = UI.crumb.querySelectorAll('.pk-crumb-sep'); return (idx >= 0 && seps[idx]) ? seps[idx] : triggerEl; }; const activeTriggerEl = getLiveTriggerEl(); if (activeTriggerEl && activeTriggerEl.classList.contains('pk-active')) { const oldPop = document.getElementById('pk-main-crumb-pop'); if (oldPop) oldPop.remove(); activeTriggerEl.classList.remove('pk-active'); activeTriggerEl.innerHTML = CONF.crumbIcons.right; return; } let targetId = parentId || 'root'; if (targetId === 'root' || targetId === '') { if (S.shareMode) targetId = 'share_root'; else if (S.starredMode) targetId = 'starred_root'; else targetId = 'root'; } let folders = []; if (targetId === 'virtual_search_root' || targetId === 'analyze_root' || targetId === 'recent_root') { let sourceItems = []; const isAnalyzeSimMode = targetId === 'analyze_root' && S.analyzeSimGroups; if (isAnalyzeSimMode) { sourceItems = (S.analyzeResultItems && S.analyzeResultItems.length > 0) ? S.analyzeResultItems : S.display.filter(item => !item.isHeader); } else if (targetId === 'analyze_root') { sourceItems = (S.analyzeResultItems && S.analyzeResultItems.length > 0) ? S.analyzeResultItems : S.items; } else if (targetId === 'recent_root') { if (S.recentResultItems && S.recentResultItems.length > 0) { sourceItems = S.recentResultItems; } else if (typeof globalCache !== 'undefined' && globalCache.has('recent_root')) { const cached = globalCache.get('recent_root'); sourceItems = Array.isArray(cached) ? cached : (cached.items || []); } else { sourceItems = S.items || []; } } else { sourceItems = (S.lastGlobalResults && S.lastGlobalResults.length > 0) ? S.lastGlobalResults : S.items; } folders = sourceItems.filter(f => f.kind === 'drive#folder'); if (folders.length === 0) { folders = [{ id: 'error', name: L.str_no_files, kind: 'drive#folder', _empty: true }]; } } else { const listId = targetId === 'root' ? '' : targetId; const cacheKey = targetId === 'root' ? 'root' : targetId; const isDirty = typeof globalDirtyFolders !== 'undefined' && globalDirtyFolders.has(listId); const rawCached = !isDirty ? ((typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) ? globalCache.get(cacheKey) : S.cache.get(cacheKey)) : null; const cachedItems = (rawCached && !Array.isArray(rawCached) && rawCached.items) ? rawCached.items : (Array.isArray(rawCached) ? rawCached : null); const cachedFolders = cachedItems ? cachedItems.filter(f => f.kind === 'drive#folder') : null; if (cachedFolders && cachedFolders.length > 0) { folders = cachedFolders; } else { folders = [{ id: 'loading', name: L.loading, kind: 'drive#folder' }]; apiList(listId, 500, null, null, false, true).then(res => { if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, res); if (S.cache) S.cache.set(cacheKey, res); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.delete(listId); const freshFolders = Array.isArray(res) ? res.filter(f => f.kind === 'drive#folder') : []; const pop = document.getElementById('pk-main-crumb-pop'); if (!pop || pop.dataset.targetId !== targetId) return; renderMenu(freshFolders.length > 0 ? freshFolders : [{ id: 'error', name: L.str_no_files, kind: 'drive#folder', _empty: true }]); }).catch(() => { const pop = document.getElementById('pk-main-crumb-pop'); if (pop && pop.dataset.targetId === targetId) { renderMenu([{ id: 'error', name: L.str_load_failed, kind: 'drive#folder' }]); } }); } } const renderMenu = (list) => { let s = S.sort, d = S.dir; const resolvedSort = (typeof resolvePreferredSortState === 'function') ? resolvePreferredSortState(targetId || null, { allowMedia: false }) : null; if (resolvedSort) { s = resolvedSort.sort; d = resolvedSort.dir; } const isAnalyzeSimMode = S.analyzeMode && targetId === 'analyze_root' && S.analyzeSimGroups; if (!isAnalyzeSimMode) { const collator = new Intl.Collator(({ 'zh': 'zh-CN', 'tc': 'zh-TW', 'ja': 'ja', 'ko': 'ko' })[getLang()] || 'en', { numeric: true, sensitivity: 'base' }); const parseSize = n => parseInt(n || 0); const parseTime = t => t ? new Date(t).getTime() : 0; const getCharWeight = (str) => { if (!str) return 0; const c = str.charAt(0); if (/[0-9]/.test(c)) return 20; if (/[\u4e00-\u9fa5]/.test(c)) return 30; if (/[a-zA-Z]/.test(c)) return 40; return 10; }; const compareNames = (nameA, nameB) => { const rankA = getCharWeight(nameA); const rankB = getCharWeight(nameB); if (rankA !== rankB) return rankA - rankB; return collator.compare(nameA, nameB); }; list.sort((a, b) => { const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (s === 'play_time') { const tA = a._history_ts || 0; const tB = b._history_ts || 0; if (tA !== tB) return (tB - tA) * d; } else if (s === 'modified_time') { const tA = parseTime(a.modified_time); const tB = parseTime(b.modified_time); if (tA !== tB) return (tB - tA) * d; } else if (s === 'created_time') { const tA = parseTime(a.created_time); const tB = parseTime(b.created_time); if (tA !== tB) return (tB - tA) * d; } else if (s === 'size') { const sizeA = parseSize(a.size); const sizeB = parseSize(b.size); if (sizeA !== sizeB) return (sizeB - sizeA) * d; } else if (s === 'play_time') { const ptA = a._history_ts || 0; const ptB = b._history_ts || 0; if (ptA !== ptB) return (ptB - ptA) * d; } else if (s === 'view_count' || s === 'save_count') { const valA = s === 'view_count' ? parseSize(a.view_count) : parseSize(a.save_count); const valB = s === 'view_count' ? parseSize(b.view_count) : parseSize(b.save_count); if (valA !== valB) return (valB - valA) * d; } else if (s === 'starred') { const isItemStarred = (item) => S.starredSet.has(item.id) || !!(item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))); const stA = isItemStarred(a) ? 1 : 0; const stB = isItemStarred(b) ? 1 : 0; if (stA !== stB) return stB - stA; const tA = parseTime(a.modified_time); const tB = parseTime(b.modified_time); return (tB - tA) * d; } else if (s === 'path') { const getPath = (item) => { if (S.analyzeMode) return item._pathStr || ""; if (item._lineage) return item._lineage.map(x => x.name).join('/'); return ""; }; const pA = getPath(a); const pB = getPath(b); const pathCmp = compareNames(pA, pB); if (pathCmp !== 0) return pathCmp * d; } return compareNames(a.name, b.name) * d; }); } let pop = document.getElementById('pk-main-crumb-pop'); if (!pop) { pop = document.createElement('div'); pop.id = 'pk-main-crumb-pop'; pop.className = 'pk-crumb-pop pk-scroll'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.style.top = '-9999px'; document.body.appendChild(pop); } pop.dataset.targetId = targetId; pop.onwheel = (evt) => { evt.stopPropagation(); }; pop.innerHTML = ''; const movingIds = (typeof pkState !== 'undefined') ? pkState.movingIds : new Set(); list.forEach(f => { const item = document.createElement('div'); const isMoving = movingIds.has(f.id); item.className = 'pk-crumb-item' + (isMoving ? ' pk-moving' : ''); item.dataset.id = f.id; if (f._empty) { item.className = 'pk-crumb-item pk-crumb-empty'; item.innerHTML = `${esc(f.name)}`; pop.appendChild(item); return; } const iconSrc = f.icon_link || ''; const fallbackSvg = getOfficialFolderFallbackIconHtml(18); const iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; const isInPath = S.path.some(pathNode => pathNode.id === f.id); const textStyle = isInPath ? 'font-weight:bold; color:var(--pk-pri);' : ''; const isProtected = typeof isSystemItem === 'function' && isSystemItem(f); const tagHtml = isProtected ? `${L.tag_default}` : ''; item.innerHTML = `${iconHtml}${esc(f.name)}${tagHtml}`; if (f.id !== 'loading' && f.id !== 'error') { item.onclick = (ev) => { ev.stopPropagation(); closeMenu(); if (targetId === 'virtual_search_root' || targetId === 'analyze_root' || targetId === 'recent_root') { let rootName = L.str_search_results; if (targetId === 'analyze_root') rootName = L.str_analyze_results; else if (targetId === 'recent_root') rootName = L.btn_nav_recent; const newPath = []; if (targetId === 'virtual_search_root') { newPath.push({ id: '', name: L.btn_nav_home }); } newPath.push({ id: targetId, name: rootName }); newPath.push({ id: f.id, name: f.name }); stopLiveManualRefreshBeforePathChange('path_change'); S.path = newPath; } else { const idx = S.path.findIndex(p => p.id === parentId); if (idx !== -1) { stopLiveManualRefreshBeforePathChange('path_change'); S.path = S.path.slice(0, idx + 1); S.path.push({ id: f.id, name: f.name }); } } load(); }; } pop.appendChild(item); }); requestAnimationFrame(() => { const liveTriggerEl = getLiveTriggerEl(); if (!liveTriggerEl || !liveTriggerEl.isConnected) return; const rect = getLogicalRect(liveTriggerEl); 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'); liveTriggerEl.classList.add('pk-active'); liveTriggerEl.innerHTML = CONF.crumbIcons.down; }); const closeMenu = () => { if (pop.parentNode) pop.remove(); const liveTriggerEl = getLiveTriggerEl(); if (liveTriggerEl) { liveTriggerEl.classList.remove('pk-active'); liveTriggerEl.innerHTML = CONF.crumbIcons.right; const svg = liveTriggerEl.querySelector('svg'); if (svg) { svg.style.width = '14px'; svg.style.height = '14px'; svg.style.display = 'block'; svg.style.opacity = '0.6'; } } document.removeEventListener('mousedown', onOutsideClick); document.removeEventListener('wheel', onOutsideWheel, true); window.removeEventListener('resize', closeMenu); UI.vp.removeEventListener('scroll', closeMenu); }; const onOutsideClick = (evt) => { const liveTriggerEl = getLiveTriggerEl(); if (!pop.contains(evt.target) && !(liveTriggerEl && liveTriggerEl.contains(evt.target))) closeMenu(); }; const onOutsideWheel = (evt) => { const liveTriggerEl = getLiveTriggerEl(); if (pop.contains(evt.target) || (liveTriggerEl && liveTriggerEl.contains(evt.target))) return; closeMenu(); }; document.addEventListener('wheel', onOutsideWheel, { capture: true, passive: true }); window.addEventListener('resize', closeMenu); UI.vp.addEventListener('scroll', closeMenu, { passive: true }); setTimeout(() => document.addEventListener('mousedown', onOutsideClick), 10); }; renderMenu(folders || []); }; function resetUnifiedCrumbDropdownState() { const existingPop = document.getElementById('pk-main-crumb-pop'); if (existingPop) existingPop.remove(); if (!UI.crumb) return; UI.crumb.querySelectorAll('.pk-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); 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'; } }); } function bindUnifiedCrumbFocusBehavior(activeIndex) { if (!UI.crumb) return; const nodes = [...UI.crumb.querySelectorAll('span:not(.pk-crumb-sep)')]; const maxIdx = Math.max(0, nodes.length - 1); const idx = Number.isFinite(Number(activeIndex)) ? Number(activeIndex) : maxIdx; S._crumbIdx = Math.min(Math.max(0, idx), maxIdx); UI.crumb.onwheel = (e) => { e.preventDefault(); resetUnifiedCrumbDropdownState(); const liveNodes = [...UI.crumb.querySelectorAll('span:not(.pk-crumb-sep)')]; if (!liveNodes.length) return; const now = Date.now(); if (now - (S._lastCrumbScroll || 0) < 120) return; S._lastCrumbScroll = now; const liveMaxIdx = liveNodes.length - 1; S._crumbIdx = Math.min(Math.max(0, Number.isFinite(Number(S._crumbIdx)) ? Number(S._crumbIdx) : liveMaxIdx), liveMaxIdx); if (e.deltaY < 0) S._crumbIdx = Math.max(0, S._crumbIdx - 1); else S._crumbIdx = Math.min(liveMaxIdx, S._crumbIdx + 1); const targetNode = liveNodes[S._crumbIdx]; if (!targetNode) return; 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) return; const liveNodes = [...UI.crumb.querySelectorAll('span:not(.pk-crumb-sep)')]; if (!liveNodes.length) return; const targetNode = liveNodes[Math.min(Math.max(0, Number.isFinite(Number(S._crumbIdx)) ? Number(S._crumbIdx) : liveNodes.length - 1), liveNodes.length - 1)] || liveNodes[liveNodes.length - 1]; if (!targetNode) return; const containerWidth = UI.crumb.offsetWidth; const centerOffset = targetNode.offsetLeft + (targetNode.offsetWidth / 2) - (containerWidth / 2); UI.crumb.scrollTo({ left: centerOffset, behavior: 'smooth' }); }, 0); } function renderCrumb() { const oldMainCrumbPop = document.getElementById('pk-main-crumb-pop'); if (oldMainCrumbPop) oldMainCrumbPop.remove(); UI.crumb.innerHTML = ''; UI.crumb.onwheel = null; UI.crumb.style.display = ''; UI.crumb.style.maxWidth = ''; UI.crumb.style.width = ''; UI.crumb.style.minWidth = ''; UI.crumb.style.flex = ''; syncGlobalSearchCheckboxState(); const pathModeTipFontSize = '13px'; if (S.offlineMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.title_offline}
`; return; } if (S.uploadMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.btn_nav_upload}
`; return; } if (S.shareMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.btn_nav_share}
`; return; } if (S.linkBookmarkMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.style.maxWidth = 'none'; UI.crumb.style.width = '100%'; UI.crumb.style.minWidth = '0'; UI.crumb.style.flex = '1 1 auto'; UI.crumb.innerHTML = `
${L.title_link_bookmark} ${L.desc_link_bookmark_space_notice}
`; return; } if (S.shareParseMode) { if (!S.shareParseListActive) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.style.maxWidth = 'none'; UI.crumb.style.width = '100%'; UI.crumb.style.minWidth = '0'; UI.crumb.style.flex = '1 1 auto'; UI.crumb.innerHTML = `
${L.title_share_parse} ${L.desc_share_parse_space_notice}
`; return; } if (S.shareParseInsightMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.style.display = 'none'; UI.crumb.innerHTML = ''; return; } UI.crumb.style.pointerEvents = 'auto'; const path = (Array.isArray(S.shareParsePath) && S.shareParsePath.length) ? S.shareParsePath : buildShareParseRootPath(); const hasCurrentShareParseFolder = Array.isArray(S.shareParseItems) && S.shareParseItems.some(item => item && item.kind === 'drive#folder'); const resolveShareParseCrumbFolders = async (crumbIndex) => { const basePath = path.slice(0, Math.max(1, crumbIndex + 1)); if (crumbIndex <= 0) { const root = path[1]; if (!root) return []; const rootParentId = getShareParseRootParentId(); const rootPath = path.slice(0, 1); const makeRootFolder = (raw, idx = 0) => { const rawKind = String(raw && raw.kind || '').toLowerCase(); const mime = raw && (raw.mime_type || raw.mimeType || raw.content_type || ''); const isFolder = raw && (raw.kind === 'drive#folder' || rawKind === 'folder' || rawKind.includes('folder') || mime === 'application/x-directory'); if (!isFolder) return null; const id = String((raw.id || raw.file_id || raw.fileId || raw.resource_id || raw.resourceId || `share_${rootParentId || 'root'}_${idx}`)); const name = raw.name || raw.file_name || raw.title || ''; if (String(id || '') !== String(root.id || '') && String(name || '') !== String(root.name || '')) return null; const displayName = L.picker_all || L.label_share_parse_root; return { ...raw, id, name: displayName, kind: 'drive#folder', parent_id: rootParentId, icon_link: raw.icon_link || raw.iconLink || raw.icon || root.icon_link || root.iconLink || root.icon || '', _isShareItem: true, _shareRoot: true, _sharePath: rootPath.concat([{ id, name: displayName, parent_id: rootParentId, _shareRoot: true }]) }; }; const cached = (Array.isArray(S.shareParseItems) && String(S.shareParseCurParentId || '') === String(rootParentId || '')) ? S.shareParseItems.map(makeRootFolder).filter(Boolean) : []; if (cached.length) return cached; try { const raw = await fetchShareDetail(rootParentId || '', ''); const folders = getShareDetailFiles(raw).map(makeRootFolder).filter(Boolean); if (folders.length) return folders; } catch(e) {} return [{ id: root.id || '', name: L.picker_all || L.label_share_parse_root, kind: 'drive#folder', icon_link: root.icon_link || root.iconLink || root.icon || '', _isShareItem: true, _shareRoot: true, _sharePath: path.slice(0, 2).map(n => n && n._shareRoot ? { ...n, name: L.picker_all || L.label_share_parse_root } : n) }]; } const target = basePath[basePath.length - 1]; const parentId = target && !target._sharePanel ? (target.id || '') : getShareParseRootParentId(); const toFolderItem = (raw, idx) => { const rawKind = String(raw && raw.kind || '').toLowerCase(); const mime = raw && (raw.mime_type || raw.mimeType || raw.content_type || ''); const isFolder = raw && (raw.kind === 'drive#folder' || rawKind === 'folder' || rawKind.includes('folder') || mime === 'application/x-directory'); if (!isFolder) return null; const id = String((raw.id || raw.file_id || raw.fileId || raw.resource_id || raw.resourceId || `share_${parentId || 'root'}_${idx}`)); const name = raw.name || raw.file_name || raw.title || ''; return { ...raw, id, name, kind: 'drive#folder', parent_id: parentId, icon_link: raw.icon_link || raw.iconLink || raw.icon || '', _isShareItem: true, _sharePath: basePath.concat([{ id, name, parent_id: parentId }]) }; }; if (crumbIndex === path.length - 1 && String(parentId || '') === String(S.shareParseCurParentId || '')) { return (Array.isArray(S.shareParseItems) ? S.shareParseItems : []).filter(item => item && item.kind === 'drive#folder'); } try { const raw = await fetchShareDetail(parentId || '', ''); return getShareDetailFiles(raw).map(toFolderItem).filter(Boolean); } catch(e) { return []; } }; const showShareParseCurrentCrumbDropdown = async (e, triggerEl, crumbIndex = path.length - 1) => { const oldPop = document.getElementById('pk-main-crumb-pop'); if (triggerEl.classList.contains('pk-active')) { if (oldPop) oldPop.remove(); triggerEl.classList.remove('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.right; return; } if (oldPop) oldPop.remove(); UI.crumb.querySelectorAll('.pk-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); sep.innerHTML = CONF.crumbIcons.right; }); const folders = await resolveShareParseCrumbFolders(crumbIndex); if (!folders.length || !S.shareParseMode || !S.shareParseListActive || !triggerEl.isConnected || !UI.crumb || !UI.crumb.contains(triggerEl)) return; const pop = document.createElement('div'); pop.id = 'pk-main-crumb-pop'; pop.className = 'pk-crumb-pop pk-scroll pk-show'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.onwheel = (evt) => evt.stopPropagation(); folders.forEach(f => { const item = document.createElement('div'); item.className = 'pk-crumb-item'; item.dataset.id = f.id || ''; const iconSrc = f.icon_link || ''; const fallbackSvg = getOfficialFolderFallbackIconHtml(18); const iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; item.innerHTML = `${iconHtml}${esc(f.name || '')}`; item.onclick = (ev) => { ev.stopPropagation(); closeMenu(); handleShareParseFolderOpen(f); }; pop.appendChild(item); }); document.body.appendChild(pop); const closeMenu = () => { if (pop.parentNode) pop.remove(); triggerEl.classList.remove('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.right; document.removeEventListener('mousedown', onOutsideClick); window.removeEventListener('resize', closeMenu); if (UI.vp) UI.vp.removeEventListener('scroll', closeMenu); }; const onOutsideClick = (evt) => { if (!pop.contains(evt.target) && !triggerEl.contains(evt.target)) closeMenu(); }; requestAnimationFrame(() => { if (!S.shareParseMode || !S.shareParseListActive || !triggerEl.isConnected || !UI.crumb || !UI.crumb.contains(triggerEl)) { closeMenu(); return; } const rect = getLogicalRect(triggerEl); if (!rect || (rect.left === 0 && rect.top === 0 && rect.width === 0 && rect.height === 0)) { closeMenu(); return; } const popRect = pop.getBoundingClientRect(); let left = rect.left - 4; if (left + popRect.width > window.innerWidth) left = window.innerWidth - popRect.width - 15; pop.style.left = Math.max(10, left) + 'px'; pop.style.top = (rect.bottom + 6) + 'px'; triggerEl.classList.add('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.down; }); window.addEventListener('resize', closeMenu); if (UI.vp) UI.vp.addEventListener('scroll', closeMenu, { passive: true }); setTimeout(() => document.addEventListener('mousedown', onOutsideClick), 10); }; path.forEach((p, i) => { const s = document.createElement('span'); s.textContent = (S.shareParseMode && p && p._shareRoot) ? (L.picker_all || p.name || L.label_share_parse_root) : (p.name || L.label_share_parse_root); s.dataset.id = p.id || 'share_root'; s.className = i === 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 < path.length - 1) handleShareParseCrumbClick(i); }; UI.crumb.appendChild(s); const showArrow = i < path.length - 1 || (i === path.length - 1 && hasCurrentShareParseFolder); if (showArrow) { const sep = document.createElement('span'); sep.className = 'pk-crumb-sep'; sep.innerHTML = CONF.crumbIcons.right; sep.onclick = (e) => { e.stopPropagation(); showShareParseCurrentCrumbDropdown(e, sep, i); }; UI.crumb.appendChild(sep); } }); bindUnifiedCrumbFocusBehavior(path.length - 1); return; } if (S.historyMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.btn_nav_history}
`; return; } if (S.trashMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.innerHTML = `
${L.trash_title} ${L.trash_notice}
`; return; } UI.crumb.style.pointerEvents = 'auto'; if (S.isFlattened || S.dupMode) { UI.crumb.style.pointerEvents = 'none'; UI.crumb.style.display = 'none'; UI.crumb.innerHTML = ''; return; } const svgStyle = 'width:14px;height:14px;vertical-align:-4px;margin-right:4px;display:inline-block;'; const ICON_HOME = pkIconHtml('home', { style: svgStyle }); const ICON_SEARCH = pkIconHtml('search', { style: svgStyle }); const ICON_TRASH = pkIconHtml('crumbTrash', { style: svgStyle }); const ICON_STAR = pkIconHtml('crumbStar', { style: svgStyle }); 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 = pkIconHtml('recent', { 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; } syncFolderFirstFromGlobal(); stopLiveManualRefreshBeforePathChange('path_change'); S.path = S.path.slice(0, i + 1); load(); } else { if (UI.btnRefresh && !UI.btnRefresh.disabled) UI.btnRefresh.click(); } }; UI.crumb.appendChild(s); const isLast = i === S.path.length - 1; let showArrow = !isLast; if (isLast) { const isUnsupportedVirtual = p.id === 'history_root' || p.id === 'offline_root' || p.id === 'upload_root'; if (!S.isFlattened && !S.dupMode && !isUnsupportedVirtual) { const crumbSourceItems = (p.id === 'virtual_search_root' && S.lastGlobalResults && S.lastGlobalResults.length > 0) ? S.lastGlobalResults : S.items; if (crumbSourceItems && crumbSourceItems.some(item => item.kind === 'drive#folder')) { showArrow = true; } } } if (showArrow) { const sep = document.createElement('span'); sep.className = 'pk-crumb-sep'; sep.innerHTML = CONF.crumbIcons.right; sep.onclick = (e) => { e.stopPropagation(); showCrumbDropdown(e, p.id, sep); }; UI.crumb.appendChild(sep); } }); bindUnifiedCrumbFocusBehavior(S.path.length - 1); } el.tabIndex = 0; el.focus(); const clearAllPlayHistory = async () => { if (!S.historyMode) return; if (!await showConfirm(L.warn_clear_all_history)) return; const ok = await clearOfficialPlayHistory(); if (!ok) { showToast(L.err_official_history_clear, 'error'); return; } resetHistorySessionState(); S.items = []; S.display = []; S.itemMap.clear(); S.clearSelection(); refresh(); updateStat(); showToast(L.msg_clear_all_history_done); }; const keyHandler = (e) => { const win = document.querySelector('.pk-ov'); if (!win || win.style.display === 'none') return; const tag = e.target.tagName; const isSearchFocusHotkey = (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && String(e.key || '').toLowerCase() === 'f'; if (isSearchFocusHotkey) { const focusSearchInput = el => { if (!el || el.disabled) return false; e.preventDefault(); e.stopPropagation(); el.focus(); try { el.select(); } catch (err) {} return true; }; const shareHistorySearch = document.querySelector('#pk_share_history_search'); if (shareHistorySearch && focusSearchInput(shareHistorySearch)) return; const linkBookmarkSearch = S.linkBookmarkMode ? document.querySelector('#pk-lbm-search-input') : null; if (linkBookmarkSearch && focusSearchInput(linkBookmarkSearch)) return; const hasActiveOverlay = document.querySelector('.pk-modal-ov, .pk-img-ov, #pk-player-ov, #pk-audio-ov'); if (!hasActiveOverlay && UI.searchInput && focusSearchInput(UI.searchInput)) return; return; } if (tag === 'TEXTAREA' || (tag === 'INPUT' && e.target.type !== 'checkbox' && e.target.type !== 'button' && e.target.type !== 'submit')) return; if (e.key === 'Escape') { if (typeof isSearchHistOpen === 'function' && isSearchHistOpen()) { closeSearchHist(); return; } const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; if (S.closePlayerOverlay()) return; if (S.closeAudioOverlay()) return; if (S.closeImageOverlay()) return; if (S.closeTopModalOverlay()) return; if (UI.ctx.style.display === 'block') { UI.ctx.style.display = 'none'; return; } if (S.getSelectedCount() > 0) { S.clearSelection(); refresh(); } else if (UI.win.classList.contains('pk-maximized')) { if (!isTurbo && btnMax) btnMax.click(); } else { if (!isTurbo) UI.btnClose.click(); } return; } const hasMediaOverlay = document.querySelector('.pk-img-ov, #pk-player-ov, #pk-audio-ov'); if (hasMediaOverlay) return; if (e.key === ' ' && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { const t = e.target; const tag = String((t && t.tagName) || '').toUpperCase(); const editable = t && (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'BUTTON' || t.closest?.('[contenteditable="true"],[contenteditable=""]')); if (!editable) { e.preventDefault(); e.stopPropagation(); return; } } if (!e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { if (e.key === 'f' || e.key === 'F') { if (UI.btnImgSearch && !UI.btnImgSearch.disabled && UI.btnImgSearch.style.display !== 'none') { e.preventDefault(); UI.btnImgSearch.click(); return; } } } if (e.key === 'Delete' && e.altKey) { e.preventDefault(); if (S.shareMode || S.uploadMode) return; const existingModal = Array.from(document.querySelectorAll('.pk-modal-ov')).find(m => { const title = m.querySelector('h3'); return title && title.textContent.includes(L.title_blacklist); }); if (existingModal) { existingModal.remove(); } else { showBlacklistModal(); } } const hasActiveOverlay = document.querySelector('.pk-modal-ov'); if (hasActiveOverlay) return; if (e.key === 'F2') { e.preventDefault(); if (S.linkBookmarkMode) { const btn = UI.in && UI.in.querySelector('#pk-lbm-folder-tools .pk-lbm-btn:not(.danger)'); if (btn && !btn.disabled) btn.click(); return; } if (S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode) return; const selectedCount = S.getSelectedCount(); if (selectedCount === 1) { if (UI.btnRename.disabled) return; UI.btnRename.click(); } else if (selectedCount > 1) { if (UI.btnBulkRename.disabled) return; UI.btnBulkRename.click(); } } if (e.key === 'F5') { e.preventDefault(); if (shouldSuppressRefreshForVirtualResultPage()) return; if (S.linkBookmarkMode) { const btn = UI.in && UI.in.querySelector('#pk-lbm-refresh-official'); if (btn && !btn.disabled) btn.click(); return; } if (S.uploadMode) load(false, true); else if (S.trashMode) load(false, true); else UI.btnRefresh.click(); } if (e.key === 'F8') { e.preventDefault(); if (S.linkBookmarkMode) { const btn = UI.in && UI.in.querySelector('#pk-lbm-new-folder'); if (btn && !btn.disabled) btn.click(); return; } if (shouldHideNewFolderButtonForCurrentMode()) return; if (!UI.btnNewFolder || UI.btnNewFolder.disabled || UI.btnNewFolder.style.display === 'none') return; UI.btnNewFolder.click(); } if (e.key === 'r' || e.key === 'R') { if (S.trashMode && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); if (UI.btnRestore && !UI.btnRestore.disabled) UI.btnRestore.click(); return; } if (S.offlineMode && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); if (UI.btnRetryTask && !UI.btnRetryTask.disabled) UI.btnRetryTask.click(); return; } } if (e.altKey && (e.key === 'y' || e.key === 'Y')) { e.preventDefault(); if (UI.btnMagnetArchiveCheck && !UI.btnMagnetArchiveCheck.disabled && UI.btnMagnetArchiveCheck.style.display !== 'none') { UI.btnMagnetArchiveCheck.click(); } return; } if (e.altKey && (e.key === 'm' || e.key === 'M')) { e.preventDefault(); if (UI.btnMigrate && !UI.btnMigrate.disabled && UI.btnMigrate.style.display !== 'none') { UI.btnMigrate.click(); } return; } if (e.altKey && (e.key === 'l' || e.key === 'L')) { e.preventDefault(); if (UI.btnExportM3U && !UI.btnExportM3U.disabled && UI.btnExportM3U.style.display !== 'none') { UI.btnExportM3U.click(); } return; } if ((e.key === 'm' || e.key === 'M') && !e.altKey && !e.ctrlKey && !e.metaKey) { const imgPlayer = document.querySelector('.pk-img-ov'); const vidPlayer = document.getElementById('pk-player-ov'); if (!imgPlayer && !vidPlayer) { const btnMax = document.querySelector('#pk-maximize'); if (!isTurbo && btnMax) btnMax.click(); } } if (e.key === 'Delete' && e.shiftKey) { if (S.linkBookmarkMode) { const folders = ensureLinkBookmarkFolders(); const currentFolder = folders[getClampedLinkBookmarkFolderIndex(S.linkBookmarkSelectedFolderIndex || 0, folders)]; if (isMagnetArchiveBookmarkFolder(currentFolder)) { e.preventDefault(); const btn = UI.in && UI.in.querySelector('#pk-lbm-clear-all-links'); if (btn && !btn.disabled) btn.click(); return; } } if (S.historyMode) { e.preventDefault(); clearAllPlayHistory(); return; } 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.linkBookmarkMode) { const folders = ensureLinkBookmarkFolders(); const currentFolder = folders[getClampedLinkBookmarkFolderIndex(S.linkBookmarkSelectedFolderIndex || 0, folders)]; if (isMagnetArchiveBookmarkFolder(currentFolder)) { e.preventDefault(); const btn = UI.in && UI.in.querySelector('#pk-lbm-clear-restored'); if (btn && !btn.disabled) btn.click(); return; } e.preventDefault(); const btn = UI.in && UI.in.querySelector('#pk-lbm-folder-tools .pk-lbm-btn.danger'); if (btn && !btn.disabled) btn.click(); return; } 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.linkBookmarkMode && !e.ctrlKey && !e.shiftKey && !e.metaKey && (e.key === 'n' || e.key === 'N')) { e.preventDefault(); const btn = UI.in && UI.in.querySelector('#pk-lbm-new-link'); if (btn && !btn.disabled) btn.click(); return; } 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 (S.shareParseMode && !e.ctrlKey && !e.shiftKey && !e.metaKey && (e.key === 'p' || e.key === 'P')) { e.preventDefault(); if (UI.btnShareParseSave && !UI.btnShareParseSave.disabled && UI.btnShareParseSave.style.display !== 'none') UI.btnShareParseSave.click(); return; } if (e.key === 'p' || e.key === 'P') { e.preventDefault(); if (S.uploadMode) { if (UI.btnUpPause && !UI.btnUpPause.disabled) UI.btnUpPause.click(); } } if (e.key === 'u' || e.key === 'U') { e.preventDefault(); const cur = S.path[S.path.length - 1]; const isRoot = S.path.length === 1; const isUnzipBlocked = S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || (S.analyzeMode && cur.id === 'analyze_root') || (cur.id === 'virtual_search_root') || (S.recentMode && isRoot) || (S.starredMode && isRoot); if (!isUnzipBlocked && !UI.btnUnzip.disabled) UI.btnUnzip.click(); } if (e.key === 'd' || e.key === 'D') { e.preventDefault(); if (S.shareMode || S.uploadMode || S.trashMode || S.offlineMode) return; if (UI.btnDown && !UI.btnDown.disabled) UI.btnDown.click(); } if (e.key === 'a' || e.key === 'A') { e.preventDefault(); if (S.shareMode || S.uploadMode || S.trashMode || S.offlineMode) return; if (UI.btnAria2 && !UI.btnAria2.disabled) UI.btnAria2.click(); } if (e.key === 'e' || e.key === 'E') { e.preventDefault(); if (S.shareMode || S.trashMode) return; if (UI.btnExt && !UI.btnExt.disabled) UI.btnExt.click(); } if (e.key === 't' || e.key === 'T') { e.preventDefault(); if (UI.btnTheme) UI.btnTheme.click(); } } }; document.addEventListener('keydown', keyHandler); if (document._pkPlayerCloseClickHandler) { document.removeEventListener('click', document._pkPlayerCloseClickHandler, true); } document._pkPlayerCloseClickHandler = (e) => { const btn = e.target && e.target.closest ? e.target.closest('#pk_p_close') : null; if (!btn) return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); S.closePlayerOverlay(); }; document.addEventListener('click', document._pkPlayerCloseClickHandler, true); if (document._pkImageCloseClickHandler) { document.removeEventListener('click', document._pkImageCloseClickHandler, true); } document._pkImageCloseClickHandler = (e) => { const btn = e.target && e.target.closest ? e.target.closest('#pk_img_close') : null; if (!btn) return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); S.closeImageOverlay(); }; document.addEventListener('click', document._pkImageCloseClickHandler, true); let sideMouseBusy = false; let lastSideMouseBtn = -1; let lastSideMouseTs = 0; const mouseSideNavHandler = async (e) => { const btn = e.button; if (btn !== 3 && btn !== 4) return; const win = document.querySelector('.pk-ov'); if (!win || win.style.display === 'none') return; const now = Date.now(); if (lastSideMouseBtn === btn && (now - lastSideMouseTs) < 250) { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); return; } lastSideMouseBtn = btn; lastSideMouseTs = now; const hasFrontOverlay = !!document.querySelector('.pk-modal-ov, .pk-img-ov, #pk-player-ov, #pk-audio-ov'); if (!hasFrontOverlay) { const t = e.target; if (t && (t.closest('textarea') || t.closest('input:not([type="checkbox"]):not([type="button"]):not([type="submit"])') || t.closest('[contenteditable="true"]'))) { return; } } e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); if (sideMouseBusy) { return; } sideMouseBusy = true; try { closeTransientDropdowns(false, true); if (btn === 3) { await S.handleMouseSideBack(); } else { await S.handleMouseSideForward(); } } catch (err) { throw err; } finally { setTimeout(() => { sideMouseBusy = false; }, 0); } }; if (window._pkMouseSideNavHandler) { window.removeEventListener('mousedown', window._pkMouseSideNavHandler, true); window.removeEventListener('mouseup', window._pkMouseSideNavHandler, true); window.removeEventListener('auxclick', window._pkMouseSideNavHandler, true); window.removeEventListener('click', window._pkMouseSideNavHandler, true); } window._pkMouseSideNavHandler = mouseSideNavHandler; window.addEventListener('mousedown', mouseSideNavHandler, { capture: true, passive: false }); window.addEventListener('mouseup', mouseSideNavHandler, { capture: true, passive: false }); window.addEventListener('auxclick', mouseSideNavHandler, { capture: true, passive: false }); window.addEventListener('click', mouseSideNavHandler, { capture: true, passive: false }); if (window._pkBrowserNavPopHandler) window.removeEventListener('popstate', window._pkBrowserNavPopHandler, true); window._pkBrowserNavPopHandler = e => { if (S && typeof S.handleBrowserNavPopState === 'function') S.handleBrowserNavPopState(e); }; window.addEventListener('popstate', window._pkBrowserNavPopHandler, true); setTimeout(() => { if (S && typeof S.syncNavContextState === 'function') S.syncNavContextState(); if (S && typeof S.bindBrowserOverlayHistory === 'function') S.bindBrowserOverlayHistory(); }, 0); const showProperty = (item) => { const L = getStrings(); let rows = []; rows.push({ k: L.lbl_prop_name, v: item.name }); rows.push({ k: L.lbl_prop_size, v: fmtSize(item.size) }); rows.push({ k: L.lbl_prop_ctime, v: fmtDate(item.created_time) }); if (item.modified_time) rows.push({ k: L.lbl_prop_mtime, v: fmtDate(item.modified_time) }); let source = L.str_prop_unknown; if (item.kind === 'drive#task') source = L.str_prop_offline; else if (item.kind === 'drive#folder') source = L.lbl_type_folder; else if (item.from === 'share') source = L.str_prop_share; else source = L.str_prop_user; rows.push({ k: L.lbl_prop_source, v: source }); let link = item.source_url || (item.params && item.params.url) || item.web_content_link || item.url; if (!link && item.params && typeof item.params === 'object') { for (const key in item.params) { if (key.toLowerCase() === 'url') { link = item.params[key]; break; } } } if (link) { rows.push({ k: L.lbl_prop_link, v: link, isLink: true }); } let pathStr = "Root"; if (S.offlineMode) pathStr = L.title_offline; else if (S.shareMode) pathStr = L.btn_nav_share; else if (item._lineage) pathStr = item._lineage.map(x => x.name).join('/'); rows.push({ k: L.lbl_prop_path, v: pathStr }); let html = `
`; rows.forEach(r => { let valHtml = `
${esc(r.v)}
`; if (r.isLink) { valHtml = `
${esc(r.v)}
`; } html += `
${r.k}:
${valHtml}
`; }); html += `
`; const m = showModal(html); m.querySelector('.pk-modal h3').textContent = L.title_property; m.querySelector('.pk-modal').style.width = "520px"; }; const btnProp = document.getElementById('ctx-property'); if (btnProp) { btnProp.onclick = () => { const item = S.itemMap.get(S.activeId); if (item) showProperty(item); UI.ctx.style.display = 'none'; }; } const mouseHandler = (e) => { if (!document.querySelector('.pk-ov') || !UI || !UI.ctx) return; if (UI.ctx.style.display !== 'none' && !UI.ctx.contains(e.target)) { UI.ctx.style.display = 'none'; UI.ctx.style.flexDirection = ''; consumeOfflineLightProbePendingWrite('context_closed'); const isRoot = S.path.length === 1 && S.path[0].id === ''; if (!S.trashMode && isRoot && S.getSelectedCount() === 1) { const id = S.getSelectedIds()[0]; const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder' && item.name === CONF.SYSTEM_FOLDER_NAME) { S.clearSelection(); renderVisible(); updateStat(); } } } }; document.addEventListener('mouseup', mouseHandler); function isShareParsePagingPending() { return !!(S.shareParseMode && S.shareParseListActive && !S.shareParseInsightMode && (S.shareParseNextPageToken || S.shareParseAutoLoadRunning)); } function getOfflineSessionForLoadingState() { try { return (typeof globalCache !== 'undefined') ? globalCache.get('offline_session') : null; } catch (e) { return null; } } function isOfflineSessionLoading(session = getOfflineSessionForLoadingState()) { if (!session || session.completed) return false; if (session.nextToken) return true; const phase = String(session.phase || ''); return phase === 'active' || phase === 'done'; } function syncOfflinePagingLoadingFromSession() { if (!S.offlineMode) return false; const pending = isOfflineSessionLoading(); S.offlinePagingPending = pending; if (pending) S.pagingLoading = true; return pending; } function isOfflinePagingPending() { return !!(S.offlineMode && (S.offlinePagingPending || isOfflineSessionLoading())); } function getPagingLoadingSuffix() { return getLiveManualRefreshStatSuffix() || ((S.pagingLoading || isShareParsePagingPending() || isOfflinePagingPending()) ? ' (Loading...)' : ''); } function updateStat() { syncLocalUploadVisibility(); syncRefreshButtonVisibility(); syncGlobalSearchCheckboxState(); if (S.linkBookmarkMode) { setSearchWrapVisible(false); if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.lblSearchPath) UI.lblSearchPath.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.stat) UI.stat.style.display = 'none'; return; } let total = 0; const visibleIdSet = new Set(); const len = S.display.length; for (let i = 0; i < len; i++) { const item = S.display[i]; if (item && !item.isHeader && !(S.movingIds && S.movingIds.has(item.id))) { total++; visibleIdSet.add(item.id); } } const useLoadingAllSelectionStat = S.selMode === 'all' && S.loading && !S.search && !S.shareParseMode && !S.dupMode && !S.analyzeMode && !S.isFlattened && Array.isArray(S.items) && S.items.length > total; const loadingAllSelectionTotal = useLoadingAllSelectionStat ? S.items.reduce((cnt, item) => cnt + (item && !item.isHeader && !(S.movingIds && S.movingIds.has(item.id)) ? 1 : 0), 0) : total; if (S.selMode === 'all') { const validExcludedIds = new Set(); for (const id of S.selEx) { if (visibleIdSet.has(id)) validExcludedIds.add(id); } if (validExcludedIds.size !== S.selEx.size) { S.selEx = validExcludedIds; } } else { const validSelectedIds = S.getSelectedIds().filter(id => visibleIdSet.has(id)); const hadShrink = validSelectedIds.length !== S.getSelectedCount(); if (hadShrink) { S.setExplicitSelection(validSelectedIds); } } const selectedIds = S.getSelectedIds(); let n = selectedIds.length; const statTotal = useLoadingAllSelectionStat ? loadingAllSelectionTotal : total; const statSelectedCount = useLoadingAllSelectionStat ? Math.max(0, statTotal - (S.selEx ? S.selEx.size : 0)) : n; const btnInvert = document.getElementById('pk-btn-invert'); if (btnInvert) { btnInvert.style.display = (!S.dupMode && n > 0) ? 'flex' : 'none'; btnInvert.style.color = btnInvert.matches(':hover') ? 'var(--pk-pri)' : 'var(--pk-fg)'; } const btnGridInvert = document.getElementById('pk-grid-invert'); if (btnGridInvert) { btnGridInvert.style.display = (!S.dupMode && n > 0) ? 'flex' : 'none'; btnGridInvert.style.color = btnGridInvert.matches(':hover') ? 'var(--pk-pri)' : 'var(--pk-fg)'; } if (S.shareParseMode) { const inShareParsePanel = !S.shareParseListActive; const inShareInsight = !!(S.shareParseListActive && S.shareParseInsightMode); const shareParseInvertVisible = !inShareParsePanel && n > 0; if (btnInvert) btnInvert.style.display = shareParseInvertVisible ? 'flex' : 'none'; if (btnGridInvert) btnGridInvert.style.display = shareParseInvertVisible ? 'flex' : 'none'; setSearchWrapVisible(!inShareParsePanel); if (UI.searchClear) UI.searchClear.style.display = (!inShareParsePanel && S.search) ? 'flex' : 'none'; if (UI.lblSearchPath) UI.lblSearchPath.style.display = inShareInsight ? 'flex' : 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.stat) UI.stat.style.display = inShareParsePanel ? 'none' : ''; let shareParseSelectedSize = 0; let hasShareParseSelectedFolder = false; let shareParseSingleVideo = false; let shareParseSingleMediaWithCover = false; let shareParseSingleItem = null; if (n > 0) { for (const id of selectedIds) { const item = (S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id)) || (S.itemMap && S.itemMap.get(id)); if (!item) continue; if (n === 1) shareParseSingleItem = item; if (item.kind === 'drive#folder') { hasShareParseSelectedFolder = true; continue; } shareParseSelectedSize += parseInt(item.size || 0, 10) || 0; } } if (!inShareParsePanel && n === 1 && shareParseSingleItem && shareParseSingleItem.kind !== 'drive#folder') { const isVid = isVideoLikeItem(shareParseSingleItem); const isImg = isImageLikeItem(shareParseSingleItem); if (isVid) shareParseSingleVideo = true; if (isVid || isImg) { if (shareParseSingleItem.thumbnail_link && shareParseSingleItem.thumbnail_link !== shareParseSingleItem.icon_link) { const isMax = UI.win.classList.contains('pk-maximized'); const isBlur = typeof isBlurEnabledForView === 'function' ? isBlurEnabledForView(isGridView() ? 'grid' : 'list') : (typeof gmGet !== 'undefined' ? gmGet('pk_blur_thumb', false) : false); if (isMax && isBlur) { shareParseSingleMediaWithCover = true; } else if (window.pkGlobalThumbCache && window.pkGlobalThumbCache.has(shareParseSingleItem.id)) { shareParseSingleMediaWithCover = true; } } } } let statText = L.status_ready.replace('{n}', total) + getPagingLoadingSuffix(); if (n > 0) { statText += `\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${(L.label_share_parse_selected_count || L.sel_count).replace('{n}', n)}`; if (!hasShareParseSelectedFolder) statText += `\u00A0\u00A0${fmtSize(shareParseSelectedSize)}`; } if (UI.stat) UI.stat.textContent = statText; const hasAll = total > 0 && n === total; const hasSome = n > 0 && n < total; if (UI.chkAll) { UI.chkAll.checked = hasAll; UI.chkAll.indeterminate = hasSome; } [UI.btnDel, UI.btnRename, UI.btnBulkRename, UI.btnCut, UI.btnCopy, UI.btnPaste, UI.btnPrune, UI.btnUnzip, UI.btnNewFolder, UI.btnRefresh, UI.btnBlacklistManager, UI.btnMigrate, UI.btnMagnetArchiveCheck].forEach(b => { if (b) { b.disabled = true; b.style.display = 'none'; } }); [UI.btnAria2, UI.btnDown].forEach(b => { if (!b) return; b.style.display = inShareParsePanel ? 'none' : 'inline-flex'; b.disabled = inShareParsePanel || n === 0; }); if (UI.btnExt) { UI.btnExt.style.display = inShareParsePanel ? 'none' : 'inline-flex'; UI.btnExt.disabled = !shareParseSingleVideo; } if (UI.btnExportM3U) { const selectedVideosForM3U = getM3UExportSelectedVideos(); UI.btnExportM3U.style.display = UI.btnExt ? UI.btnExt.style.display : (inShareParsePanel ? 'none' : 'inline-flex'); UI.btnExportM3U.disabled = !selectedVideosForM3U.length; UI.btnExportM3U.style.cursor = UI.btnExportM3U.disabled ? 'not-allowed' : 'pointer'; UI.btnExportM3U.style.opacity = UI.btnExportM3U.disabled ? '0.4' : '1'; } if (UI.btnImgSearch) { UI.btnImgSearch.style.display = inShareParsePanel ? 'none' : 'inline-flex'; UI.btnImgSearch.disabled = !shareParseSingleMediaWithCover; } if (UI.btnExport) UI.btnExport.style.display = 'none'; if (UI.actionBar) { UI.actionBar.style.display = S.shareParseListActive ? 'flex' : 'none'; UI.actionBar.querySelectorAll('.pk-sep').forEach(sep => { sep.style.display = 'none'; }); } if (UI.bottomGrp) UI.bottomGrp.style.display = inShareParsePanel ? 'none' : 'flex'; if (UI.ctx) UI.ctx.style.display = 'none'; updateShareParseSaveButton(); updateShareParseInsightButtons(); return; } [UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if (b) b.disabled = false; }); if (UI.stat) UI.stat.style.display = ''; updateShareParseInsightButtons(); let hasProtectedItem = false; let hasSelFolder = false; let selSize = 0; if (n > 0) { for (const id of selectedIds) { const item = S.itemMap.get(id); if (!item) continue; const sz = parseInt(item.size || 0); if (item.kind === 'drive#folder') { hasSelFolder = true; if (S.analyzeMode) { selSize += sz; } } else { selSize += sz; } if (!S.trashMode && isSystemItem(item)) hasProtectedItem = true; } } let statText = L.status_ready.replace('{n}', statTotal) + getPagingLoadingSuffix(); if (statSelectedCount > 0) { statText += `\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${L.sel_count.replace('{n}', statSelectedCount)}`; if (!useLoadingAllSelectionStat && !S.shareMode && (S.analyzeMode || !hasSelFolder)) { statText += `\u00A0\u00A0${fmtSize(selSize)}`; } } UI.stat.textContent = statText; const hasSel = n > 0; if (UI.btnAria2) UI.btnAria2.disabled = S.trashMode || !hasSel; if (UI.btnDown) UI.btnDown.disabled = S.trashMode || !hasSel; if (UI.chkAll) { let isAll = false; let isInd = false; if (statSelectedCount > 0 && statTotal > 0) { isAll = (statSelectedCount === statTotal); isInd = (statSelectedCount < statTotal); } else { isAll = false; isInd = false; } UI.chkAll.checked = isAll; UI.chkAll.indeterminate = isInd; } const isShare = S.shareMode; const isOffline = S.offlineMode; const isHistory = S.historyMode; const isUpload = S.uploadMode; let isSingleVideo = false; let isSingleMediaWithCover = false; if (n === 1) { const id = selectedIds[0]; const item = S.itemMap.get(id); if (item && item.kind !== 'drive#folder') { const mime = (item.mime_type || '').toLowerCase(); const ext = (item.name || '').split('.').pop().toLowerCase(); const isVid = mime.startsWith('video/') || ['mp4','mkv','avi','mov','wmv','flv','webm','ts'].includes(ext); const isImg = mime.startsWith('image/') || ['jpg','jpeg','png','gif','bmp','webp','heic'].includes(ext); let isTaskReady = true; if (S.offlineMode) isTaskReady = item.phase === 'PHASE_TYPE_COMPLETE'; if (S.uploadMode) isTaskReady = item.status === 'DONE'; if (isVid && isTaskReady) isSingleVideo = true; if ((isVid || isImg) && isTaskReady) { if (item.thumbnail_link && item.thumbnail_link !== item.icon_link) { const isMax = UI.win.classList.contains('pk-maximized'); const isBlur = typeof isBlurEnabledForView === 'function' ? isBlurEnabledForView(isGridView() ? 'grid' : 'list') : (typeof gmGet !== 'undefined' ? gmGet('pk_blur_thumb', false) : false); if (isMax && isBlur) { isSingleMediaWithCover = true; } else if (window.pkGlobalThumbCache && window.pkGlobalThumbCache.has(item.id)) { isSingleMediaWithCover = true; } } } } } const hasOfflineReferenceMissingLocalSelected = isOffline && selectedIds.some(id => isOfflineReferenceMissingLocalMarked(S.itemMap.get(id))); if (UI.btnExt) UI.btnExt.disabled = S.trashMode || hasOfflineReferenceMissingLocalSelected || !isSingleVideo; if (UI.btnExportM3U) { const selectedVideosForM3U = getM3UExportSelectedVideos(); UI.btnExportM3U.style.display = UI.btnExt ? UI.btnExt.style.display : 'none'; UI.btnExportM3U.disabled = S.trashMode || hasOfflineReferenceMissingLocalSelected || !selectedVideosForM3U.length; UI.btnExportM3U.style.cursor = UI.btnExportM3U.disabled ? 'not-allowed' : 'pointer'; UI.btnExportM3U.style.opacity = UI.btnExportM3U.disabled ? '0.4' : '1'; } if (UI.btnImgSearch) UI.btnImgSearch.disabled = S.trashMode || hasOfflineReferenceMissingLocalSelected || !isSingleMediaWithCover; if (isUpload) { if (hasSel) { let hasRunning = false; let hasStopped = false; for (const id of selectedIds) { const task = S.itemMap.get(id); if (task) { const s = task.status; if (['UPLOADING', 'HASHING', 'WAITING', 'RUNNING'].includes(s)) hasRunning = true; if (['PAUSED', 'ERROR'].includes(s)) hasStopped = true; } if (hasRunning && hasStopped) break; } if (UI.btnUpPause) UI.btnUpPause.disabled = !hasRunning; if (UI.btnUpStart) UI.btnUpStart.disabled = !hasStopped; if (UI.btnUpDel) UI.btnUpDel.disabled = false; } else { if (UI.btnUpPause) UI.btnUpPause.disabled = true; if (UI.btnUpStart) UI.btnUpStart.disabled = true; if (UI.btnUpDel) UI.btnUpDel.disabled = true; } if (UI.btnUpClearAll) UI.btnUpClearAll.disabled = (S.uploadTasks.length === 0); } if (UI.btnRetryTask) { UI.btnRetryTask.style.display = isOffline ? 'inline-flex' : 'none'; const hasRetryableTask = selectedIds.some(id => isOfflineRetryableTask(S.itemMap.get(id))); UI.btnRetryTask.disabled = !hasRetryableTask; } 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 (UI.btnClearHistoryAll) { UI.btnClearHistoryAll.style.display = isHistory ? 'inline-flex' : 'none'; UI.btnClearHistoryAll.disabled = !isHistory || S.items.length === 0; UI.btnClearHistoryAll.style.opacity = UI.btnClearHistoryAll.disabled ? '0.45' : '1'; UI.btnClearHistoryAll.style.cursor = UI.btnClearHistoryAll.disabled ? 'not-allowed' : 'pointer'; } if (isShare && UI.btnCancelShare) { UI.btnCancelShare.disabled = !hasSel; } if (UI.btnMigrate) { UI.btnMigrate.disabled = !hasSel; UI.btnMigrate.style.cursor = UI.btnMigrate.disabled ? 'not-allowed' : 'pointer'; UI.btnMigrate.style.opacity = UI.btnMigrate.disabled ? '0.4' : '1'; } if (UI.btnMagnetArchiveCheck) { const canShowMagnetArchiveCheck = !S.trashMode && !S.shareMode && !S.uploadMode && !S.offlineMode && !S.historyMode && !S.linkBookmarkMode && !S.shareParseMode; UI.btnMagnetArchiveCheck.style.display = canShowMagnetArchiveCheck ? 'inline-flex' : 'none'; UI.btnMagnetArchiveCheck.disabled = !canShowMagnetArchiveCheck || !hasSel; UI.btnMagnetArchiveCheck.style.cursor = UI.btnMagnetArchiveCheck.disabled ? 'not-allowed' : 'pointer'; UI.btnMagnetArchiveCheck.style.opacity = UI.btnMagnetArchiveCheck.disabled ? '0.4' : '1'; } const hasClipData = S.clipItems && S.clipItems.length > 0; const isPasteLocked = S.movingIds && S.movingIds.size > 0; if (UI.btnPaste) { UI.btnPaste.disabled = !hasClipData || isPasteLocked; const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isHistoryRoot = S.historyMode && S.path.length === 1; const shouldHidePaste = S.trashMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || isStarredRoot || isRecentRoot || isHistoryRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; UI.btnPaste.style.display = shouldHidePaste ? 'none' : 'inline-flex'; UI.btnPaste.style.cursor = isPasteLocked ? 'wait' : (UI.btnPaste.disabled ? 'not-allowed' : 'pointer'); } UI.btnCopy.disabled = !hasSel || isPasteLocked ; UI.btnCut.disabled = !hasSel || isPasteLocked || hasProtectedItem; if (UI.btnNewFolder) { UI.btnNewFolder.disabled = isPasteLocked; if (isPasteLocked) UI.btnNewFolder.style.cursor = 'wait'; else UI.btnNewFolder.style.cursor = ''; } UI.btnDel.disabled = !hasSel || (n === 1 && hasProtectedItem); const isStandardRenameView = !(isShare || isOffline || isHistory || isUpload); if (isStandardRenameView) { if (n <= 1) { UI.btnRename.style.display = 'inline-flex'; UI.btnBulkRename.style.display = 'none'; UI.btnRename.disabled = (n === 0) || hasProtectedItem; } else { UI.btnRename.style.display = 'none'; UI.btnBulkRename.style.display = 'inline-flex'; UI.btnBulkRename.disabled = false; } } if (UI.btnRestore) UI.btnRestore.disabled = !hasSel; if (UI.btnDelForever) UI.btnDelForever.disabled = !hasSel; if (UI.btnPrune) { const isHiddenMode = S.isFlattened || S.dupMode || S.shareMode || S.offlineMode || S.uploadMode || S.historyMode; UI.btnPrune.style.display = isHiddenMode ? 'none' : 'inline-flex'; UI.btnPrune.disabled = isHiddenMode || !hasSelFolder; } if (UI.btnUnzip) { const cur = S.path[S.path.length - 1]; const isRoot = S.path.length === 1; const isUnzipHidden = S.shareMode || S.offlineMode || S.uploadMode || S.historyMode || S.isFlattened || S.dupMode || (S.analyzeMode && cur.id === 'analyze_root') || (cur.id === 'virtual_search_root') || (S.recentMode && isRoot) || (S.starredMode && isRoot); UI.btnUnzip.style.display = isUnzipHidden ? 'none' : 'inline-flex'; const isArchive = (it) => { if (!it || it.kind === 'drive#folder') return false; const n = (it.name || '').toLowerCase(); const m = (it.mime_type || '').toLowerCase(); return m.includes('zip') || m.includes('rar') || m.includes('7z') || n.endsWith('.zip') || n.endsWith('.rar') || n.endsWith('.7z'); }; const hasArchive = selectedIds.some(id => isArchive(S.itemMap.get(id))); UI.btnUnzip.disabled = S.trashMode || !hasArchive; } syncRefreshButtonVisibility(); } const PK_WEB_VOD_TS_DOWNLOADER = 'https://web-vod-xdrive.mypikpak.com/ts_downloader'; const PK_WEB_VOD_CLIENT_ID = 'UElLUEFLX1dFQg'; const buildPikPakTsHlsUrl = (rawUrl) => { const input = String(rawUrl || '').trim(); if (!input) return input; if (input.includes('ts_downloader') && input.includes('url=')) { return input; } const cleanUrl = input .replace('&ext=.m3u8', '') .replace('?ext=.m3u8&', '?') .replace('?ext=.m3u8', ''); return `${PK_WEB_VOD_TS_DOWNLOADER}?client_id=${PK_WEB_VOD_CLIENT_ID}&url=${encodeURIComponent(cleanUrl)}`; }; const isPikPakTsDownloaderUrl = (url) => { const s = String(url || ''); return s.includes('web-vod-xdrive.mypikpak.com/ts_downloader') || s.includes('/ts_downloader?'); }; let pkOfficialHlsCtorCache = null; let pkOfficialHlsProbeFailed = false; const getPikPakPageWindow = () => { try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow) return unsafeWindow; } catch (e) {} return window; }; const getPikPakOfficialHlsCtor = () => { if (pkOfficialHlsCtorCache) return pkOfficialHlsCtorCache; if (pkOfficialHlsProbeFailed) return null; try { const pageWin = getPikPakPageWindow(); const chunk = pageWin.webpackChunkpikpak_web; if (!chunk || typeof chunk.push !== 'function') { pkOfficialHlsProbeFailed = true; return null; } let webpackRequire = null; const probeName = `pk_hls_probe_${Date.now()}_${Math.random().toString(36).slice(2)}`; chunk.push([ [probeName], {}, function (__webpack_require__) { webpackRequire = __webpack_require__; } ]); if (!webpackRequire) { pkOfficialHlsProbeFailed = true; return null; } const mod = webpackRequire(64945); const HlsCtor = mod && (mod.Ay || mod.default || mod.Hls); if ( typeof HlsCtor === 'function' && HlsCtor.Events && typeof HlsCtor.isSupported === 'function' ) { pkOfficialHlsCtorCache = HlsCtor; return pkOfficialHlsCtorCache; } pkOfficialHlsProbeFailed = true; console.warn('[Hls] Official PikPak Hls core module found, but constructor is invalid:', mod); return null; } catch (e) { pkOfficialHlsProbeFailed = true; console.warn('[Hls] Failed to capture official PikPak Hls core:', e); return null; } }; const isLikelyHevcCodec = (codecText) => { const s = String(codecText || '').toLowerCase(); return s.includes('hevc') || s.includes('h265') || s.includes('h.265') || s.includes('hvc1') || s.includes('hev1'); }; const getPikPakMediaCodecText = (m) => { const v = (m && m.video) || {}; const p = (m && m.params) || {}; return [ v.video_codec, v.codec, v.codec_name, v.mime_type, m && m.video_codec, m && m.codec, m && m.codec_name, m && m.mime_type, p.video_codec, p.codec, m && m.video_stream_id ].filter(Boolean).join(' '); }; let pkHevcWebPlaybackSupportCache = null; const hasLikelyHevcWebPlaybackSupport = () => { if (pkHevcWebPlaybackSupportCache !== null) return pkHevcWebPlaybackSupportCache; let supported = false; try { const video = document.createElement('video'); const samples = [ 'video/mp4; codecs="hvc1.1.6.L93.B0"', 'video/mp4; codecs="hev1.1.6.L93.B0"', 'video/mp4; codecs="hvc1.1.6.L120.B0"', 'video/mp4; codecs="hev1.1.6.L120.B0"' ]; supported = samples.some(mime => { const nativeCanPlay = !!(video.canPlayType && video.canPlayType(mime)); const mseCanPlay = !!(window.MediaSource && MediaSource.isTypeSupported && MediaSource.isTypeSupported(mime)); return nativeCanPlay || mseCanPlay; }); } catch (e) { supported = false; } pkHevcWebPlaybackSupportCache = supported; return supported; }; const getPikPakQualityCodecTextForSniff = (q) => { return [ q && q.codecText, q && q.videoType, q && q.rawName, q && q.name ].filter(Boolean).join(' '); }; const isPikPakQualityLikelyHevc = (q) => { if (!q) return false; return !!q.isLikelyHevcTranscode || isLikelyHevcCodec(getPikPakQualityCodecTextForSniff(q)); }; const shouldBlockWebPlaybackByOfficialSniff = (q) => { return isPikPakQualityLikelyHevc(q) && !hasLikelyHevcWebPlaybackSupport(); }; const normalizePikPakTsM3u8 = (rawText) => { let text = String(rawText || '').replace(/\r\n/g, '\n'); if (!text.includes('#EXTM3U')) return text; if (!text.includes('#EXT-X-ALLOW-CACHE')) { if (text.includes('#EXT-X-MEDIA-SEQUENCE')) { text = text.replace( /(#EXT-X-MEDIA-SEQUENCE:[^\n]*\n)/, '$1#EXT-X-ALLOW-CACHE:YES\n' ); } else { text = text.replace( /(#EXTM3U\n)/, '$1#EXT-X-ALLOW-CACHE:YES\n' ); } } text = text.replace( /(#EXT-X-BYTERANGE:[^\n]+)\n(#EXTINF:[^\n]+)\n/g, '$2\n$1\n' ); return text; }; const getPikPakQualityRank = (name) => { const n = String(name || '').toUpperCase(); if (n.includes('1080')) return 1080; if (n.includes('720')) return 720; if (n.includes('480')) return 480; return 0; }; const getPikPakQualityResolution = (q) => { const width = parseInt(q.width || q.videoWidth || 0); const height = parseInt(q.height || q.videoHeight || 0); if (width > 0 && height > 0) return `${width}x${height}`; const rank = getPikPakQualityRank(q.name || q.rawName); if (rank >= 1080) return '1920x1080'; if (rank >= 720) return '1280x720'; if (rank >= 480) return '852x480'; return '1280x720'; }; const getPikPakQualityBandwidth = (q) => { const explicit = parseInt(q.bandwidth || q.bitrate || q.bitRate || 0); if (explicit > 0) return explicit; const rank = getPikPakQualityRank(q.name || q.rawName); if (rank >= 1080) return 2664403; if (rank >= 720) return 1528012; if (rank >= 480) return 1028110; return 1000000; }; const getPikPakQualityCodec = (q) => { const codecText = String(q.codecText || '').toLowerCase(); if ( codecText.includes('avc1') || codecText.includes('h264') || codecText.includes('h.264') ) { return 'avc1.640028,mp4a.40.2'; } if ( q.isLikelyHevcTranscode || isLikelyHevcCodec(codecText) || codecText.includes('mpegts') || !codecText ) { return 'hev1.1.6.L93.B0,mp4a.40.2'; } return 'hev1.1.6.L93.B0,mp4a.40.2'; }; const buildPikPakMasterM3u8FromQualityList = (list) => { const transcodes = (Array.isArray(list) ? list : []) .filter(q => q && !q.isOriginal) .filter(q => isPikPakTsDownloaderUrl(q.link || q.url)) .sort((a, b) => getPikPakQualityRank(b.name || b.rawName) - getPikPakQualityRank(a.name || a.rawName)); if (!transcodes.length) return ''; const lines = ['#EXTM3U']; transcodes.forEach(q => { const url = q.link || q.url; const name = String(q.name || q.rawName || '').trim() || 'Auto'; const bandwidth = getPikPakQualityBandwidth(q); const codec = getPikPakQualityCodec(q); const resolution = getPikPakQualityResolution(q); lines.push(`#EXT-X-STREAM-INF:BANDWIDTH=${bandwidth},CODECS="${codec}",RESOLUTION=${resolution},NAME="${name}"`); lines.push(url); }); return lines.join('\n') + '\n'; }; const findPikPakHlsLevelIndex = (hls, targetName, targetUrl) => { const levels = (hls && Array.isArray(hls.levels)) ? hls.levels : []; const wantedName = String(targetName || '').toUpperCase(); const wantedRank = getPikPakQualityRank(wantedName); let idx = levels.findIndex(l => { const attrs = l && l.attrs ? l.attrs : {}; const levelName = String(l.name || attrs.NAME || '').toUpperCase(); return wantedName && levelName === wantedName; }); if (idx >= 0) return idx; idx = levels.findIndex(l => { const h = parseInt(l && l.height || 0); if (!wantedRank) return false; if (wantedRank >= 1080) return h >= 900; if (wantedRank >= 720) return h >= 600 && h < 900; if (wantedRank >= 480) return h > 0 && h < 600; return false; }); if (idx >= 0) return idx; const wantedUrl = String(targetUrl || ''); if (wantedUrl) { idx = levels.findIndex(l => { const u = String((l && l.url && (Array.isArray(l.url) ? l.url[0] : l.url)) || ''); return u && wantedUrl && (u === wantedUrl || u.includes(wantedUrl) || wantedUrl.includes(u)); }); } return idx; }; const applyPikPakPreferredHlsLevel = (hls, targetName, targetUrl) => { const idx = findPikPakHlsLevelIndex(hls, targetName, targetUrl); if (idx < 0) return false; try { hls.currentLevel = idx; hls.nextLevel = idx; hls.loadLevel = idx; return true; } catch (e) { console.warn('[Hls] Failed to select PikPak master level:', e); return false; } }; const normalizeDefaultVideoQuality = (val) => { const v = String(val || '').toLowerCase(); if (v === '1080p' || v === '720p' || v === '480p') return v; return 'original'; }; const getDefaultVideoQualityPref = () => { return normalizeDefaultVideoQuality(gmGet('pk_default_video_quality', 'original')); }; const normalizeDefaultOpenPlayer = (val) => { const v = String(val || '').toLowerCase(); return v === 'potplayer' ? 'potplayer' : 'script'; }; const getDefaultOpenPlayerPref = () => { return normalizeDefaultOpenPlayer(gmGet('pk_default_open_player', 'script')); }; const shouldLoadVideoProgressCache = () => { return gmGet('pk_video_load_progress_cache', true) !== false; }; const parseOfficialPlaySeconds = (v) => { const n = parseFloat(v || 0); return Number.isFinite(n) ? n : 0; }; const fetchOfficialPlayHistoryProgress = async (fileId) => { const targetId = String(fileId || ''); if (!targetId) return null; const filters = encodeURIComponent(JSON.stringify({ type: { in: 'TYPE_PLAY' } })); const limit = 50; const maxPages = 3; let pageToken = ''; for (let page = 0; page < maxPages; page++) { let timer = 0; const controller = typeof AbortController !== 'undefined' ? new AbortController() : null; try { if (controller) timer = setTimeout(() => controller.abort(), 3500); const url = `https://api-drive.mypikpak.com/drive/v1/events?thumbnail_size=SIZE_MEDIUM&limit=${limit}&filters=${filters}${pageToken ? `&page_token=${encodeURIComponent(pageToken)}` : ''}&_t=${Date.now()}`; const res = await fetch(url, { headers: getHeaders(), signal: controller ? controller.signal : undefined }); if (!res.ok) return null; const data = await res.json(); const events = Array.isArray(data.events) ? data.events : (Array.isArray(data.data) ? data.data : []); for (const ev of events) { const ref = ev.reference_resource || {}; const officialEventId = String(ev.id || ev.event_id || ''); const evFileId = String(ev.file_id || ref.id || ''); if (evFileId !== targetId) continue; const params = ev.params || {}; const refParams = ref.params || {}; const playSeconds = parseOfficialPlaySeconds(params.play_seconds); const playDuration = parseOfficialPlaySeconds(params.play_duration || refParams.duration); const updatedTime = Date.parse(ev.updated_time || ev.create_time || '') || Date.now(); if (playSeconds > 5 && !(playDuration > 0 && playDuration - playSeconds <= 5)) { return { t: playSeconds, d: playDuration || 0, ts: updatedTime, source: 'official', officialEventId }; } return null; } pageToken = data.next_page_token || data.nextPageToken || ''; if (!pageToken) break; } catch (e) { if (!e || e.name !== 'AbortError') console.warn('[OfficialProgress] Fetch failed:', e); return null; } finally { if (timer) clearTimeout(timer); } } return null; }; const deleteOfficialPlayHistoryEvents = async (eventIds) => { const ids = Array.from(new Set((eventIds || []).map(id => String(id || '').trim()).filter(Boolean))); if (!ids.length) return true; const headers = Object.assign({}, getHeaders(), { 'Content-Type': 'application/json' }); for (let i = 0; i < ids.length; i += 100) { const chunk = ids.slice(i, i + 100); const res = await fetch('https://api-drive.mypikpak.com/drive/v1/events:delete', { method: 'POST', headers, body: JSON.stringify({ ids: chunk }) }); if (!res.ok) { console.warn('[OfficialHistory] Delete failed:', res.status, chunk); return false; } } return true; }; const clearOfficialPlayHistory = async () => { const headers = Object.assign({}, getHeaders(), { 'Content-Type': 'application/json' }); const res = await fetch('https://api-drive.mypikpak.com/drive/v1/events:clear', { method: 'POST', headers, body: JSON.stringify({ types: ['TYPE_PLAY'] }) }); if (!res.ok) { console.warn('[OfficialHistory] Clear failed:', res.status); return false; } return true; }; const postOfficialPlayProgress = async (fileId, playSeconds, playDuration) => { const targetId = String(fileId || ''); const seconds = Math.round(Number(playSeconds || 0)); const duration = Math.round(Number(playDuration || 0)); if (!targetId || !(seconds > 1)) return false; if (duration > 0 && duration - seconds <= 5) return false; try { const headers = Object.assign({}, getHeaders(), { 'Content-Type': 'application/json' }); const res = await fetch('https://api-drive.mypikpak.com/drive/v1/events', { method: 'POST', headers, body: JSON.stringify({ event: { type: 'TYPE_PLAY', file_id: targetId, params: { play_duration: String(duration || 0), play_seconds: String(seconds) } } }) }); if (!res.ok) { console.warn('[OfficialProgress] POST failed:', res.status); return false; } return true; } catch (e) { console.warn('[OfficialProgress] POST error:', e); return false; } }; const findQualityByDefaultPref = (list, pref) => { const p = normalizeDefaultVideoQuality(pref); const arr = Array.isArray(list) ? list : []; if (p === 'original') { return arr.find(q => q && q.isOriginal) || null; } const target = p.toUpperCase(); return arr.find(q => { const n = String((q && q.name) || '').toUpperCase(); return n === target || n.includes(target.replace('P', '')); }) || null; }; const generateQualityList = (data) => { const list = []; const seenNames = new Set(); const officialOriginMedia = (data.medias && Array.isArray(data.medias)) ? data.medias.find(m => { const isOrigin = m && (m.is_origin === true || m.is_origin === 1 || String(m.is_origin).toLowerCase() === 'true'); return !!(m && m.link && m.link.url && isOrigin); }) : null; const officialOriginVideo = (officialOriginMedia && officialOriginMedia.video) || {}; const officialOriginUrl = officialOriginMedia && officialOriginMedia.link ? officialOriginMedia.link.url : ''; const originUrl = officialOriginUrl || data.web_content_link || ''; if (originUrl) { list.push({ name: L.str_original, url: originUrl, isOriginal: true, rawName: (officialOriginMedia && (officialOriginMedia.media_name || officialOriginMedia.resolution_name || officialOriginMedia.video_stream_id)) || L.str_original, codecText: officialOriginMedia ? getPikPakMediaCodecText(officialOriginMedia) : '', videoType: String((officialOriginVideo && officialOriginVideo.video_type) || (officialOriginMedia && officialOriginMedia.video_type) || '').toLowerCase(), width: parseInt((officialOriginVideo && (officialOriginVideo.width || officialOriginVideo.video_width)) || (officialOriginMedia && (officialOriginMedia.width || officialOriginMedia.video_width)) || 0), height: parseInt((officialOriginVideo && (officialOriginVideo.height || officialOriginVideo.video_height)) || (officialOriginMedia && (officialOriginMedia.height || officialOriginMedia.video_height)) || 0), bandwidth: parseInt((officialOriginVideo && (officialOriginVideo.bitrate || officialOriginVideo.bit_rate)) || (officialOriginMedia && (officialOriginMedia.bitrate || officialOriginMedia.bit_rate || officialOriginMedia.bandwidth)) || 0) }); 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; const isOrigin = m && (m.is_origin === true || m.is_origin === 1 || String(m.is_origin).toLowerCase() === 'true'); if (isOrigin) return; let name = m.resolution_name || m.video_stream_id || 'Unknown'; let streamUrl = m.link.url; const videoInfo = (m && m.video) || {}; const videoType = String(videoInfo.video_type || m.video_type || '').toLowerCase(); const codecText = getPikPakMediaCodecText(m); const isLikelyHevcTranscode = isLikelyHevcCodec(codecText); const width = parseInt(videoInfo.width || videoInfo.video_width || m.width || m.video_width || 0); const height = parseInt(videoInfo.height || videoInfo.video_height || m.height || m.video_height || 0); const bandwidth = parseInt(videoInfo.bitrate || videoInfo.bit_rate || m.bitrate || m.bit_rate || m.bandwidth || 0); if (videoType === 'mpegts') { streamUrl = buildPikPakTsHlsUrl(streamUrl); } const rawQualityName = String(name || '').trim(); const qualityKey = rawQualityName.toUpperCase(); if (qualityKey.includes('1080') || qualityKey === 'FHD') name = '1080P'; else if (qualityKey.includes('720') || qualityKey === 'HD') name = '720P'; else if (qualityKey.includes('480') || qualityKey === 'SD') name = '480P'; else return; if (seenNames.has(name)) return; seenNames.add(name); transcodeList.push({ name: name, url: streamUrl, isOriginal: false, rawName: name, codecText, isLikelyHevcTranscode, videoType, width, height, bandwidth }); }); } transcodeList.forEach(t => { list.push({ name: t.name, url: t.url, isOriginal: false, rawName: t.rawName || t.name, codecText: t.codecText || '', isLikelyHevcTranscode: !!t.isLikelyHevcTranscode, videoType: t.videoType || '', width: t.width || 0, height: t.height || 0, bandwidth: t.bandwidth || 0 }); }); list.sort((a, b) => { if (a.isOriginal) return -1; if (b.isOriginal) return 1; const getRank = (s) => { const n = String(s || '').toUpperCase(); 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 generatedList = generateQualityList(data); const list = generatedList.map(item => ({ name: item.name, link: item.url, active: false, isOriginal: item.isOriginal, rawName: item.rawName || item.name, codecText: item.codecText || '', isLikelyHevcTranscode: !!item.isLikelyHevcTranscode, videoType: item.videoType || '', width: item.width || 0, height: item.height || 0, bandwidth: item.bandwidth || 0 })); let bestMatch = null; const defaultQualityPref = getDefaultVideoQualityPref(); const preferredMatch = findQualityByDefaultPref(list, defaultQualityPref); const originalMatch = list.find(item => item && item.isOriginal) || null; if (preferredMatch) { bestMatch = preferredMatch; } else if (defaultQualityPref !== 'original' && originalMatch) { bestMatch = originalMatch; } else if (list.length > 0) { bestMatch = list[0]; } else { bestMatch = { name: L.str_original, link: data.web_content_link, active: true, isOriginal: true }; list.push(bestMatch); } bestMatch.active = true; return { src: bestMatch.link, name: bestMatch.name, list: list }; }; const launchPotPlayerWithFallback = (cleanUrl, opts = {}) => { const L = getStrings(); const button = opts.button || null; const timeout = opts.timeout || 3500; const source = normalizePotPlayerLaunchSource(opts.source); const launchAt = Date.now(); let copiedAt = launchAt; let likelyOpened = false; let finished = false; let timer = null; let confirmTimer = null; let focusProbe = null; const markOpened = () => { likelyOpened = true; }; const onVisibility = () => { if (document.hidden) markOpened(); }; const cleanup = () => { window.removeEventListener('blur', markOpened); document.removeEventListener('visibilitychange', onVisibility, true); if (timer) clearTimeout(timer); if (confirmTimer) clearTimeout(confirmTimer); if (focusProbe) clearInterval(focusProbe); }; recordPotPlayerLaunchAttempt(source, launchAt); try { GM_setClipboard(cleanUrl); copiedAt = Date.now(); } catch (e) {} if (button) { button.disabled = true; button.textContent = L.msg_potplayer_launching; } window.addEventListener('blur', markOpened, { once: true }); document.addEventListener('visibilitychange', onVisibility, true); if (typeof document.hasFocus === 'function') { focusProbe = setInterval(() => { if (!document.hasFocus()) markOpened(); }, 120); } try { const ua = navigator.userAgent.replace(/"/g, ''); const cmd = `${cleanUrl} /user_agent="${ua}" /referer="https://mypikpak.com/"`; window.location.href = `potplayer://${cmd}`; } catch (e) {} const finishLikelyOpen = () => { if (finished) return; finished = true; cleanup(); recordPotPlayerLaunchResult('likely_open', { source, launchAt }); if (typeof opts.onLikelyOpen === 'function') opts.onLikelyOpen(); }; const finishLikelyFail = () => { if (finished) return; finished = true; cleanup(); recordPotPlayerLaunchResult('likely_fail', { source, launchAt, copiedAt }); const failToastDuration = opts.failToastDuration || 6500; const failCloseDelay = opts.failCloseDelay || 3200; if (!opts.silentLikelyFailToast) showToast(L.msg_potplayer_maybe_failed, 'warning', failToastDuration); const notifyLikelyFail = () => { if (typeof opts.onLikelyFail === 'function') { opts.onLikelyFail({ source, launchAt, copiedAt, cleanUrl, autoRepairPrompt: opts.autoRepairPrompt, autoRepairPromptDelay: opts.autoRepairPromptDelay }); } }; if (button) { button.disabled = true; button.textContent = L.msg_potplayer_link_copied; button.classList.add('pk-copy-success-freeze'); button.style.setProperty('background', '#52c41a', 'important'); button.style.setProperty('color', '#fff', 'important'); } setTimeout(notifyLikelyFail, failCloseDelay); }; timer = setTimeout(() => { if (finished) return; if (likelyOpened) { finishLikelyOpen(); return; } const confirmDelay = Number.isFinite(Number(opts.confirmFailDelay)) ? Math.max(0, Number(opts.confirmFailDelay)) : 0; if (confirmDelay > 0) { confirmTimer = setTimeout(() => { if (finished) return; if (likelyOpened) { finishLikelyOpen(); return; } finishLikelyFail(); }, confirmDelay); return; } finishLikelyFail(); }, timeout); return cleanup; }; const openPotPlayerProtocolRepairHelper = (playUrl, opts = {}) => { const L = getStrings(); const cleanUrl = String(playUrl || ''); const isAutoPrompt = opts.autoPrompt === true; const title = isAutoPrompt ? L.potplayer_fix_auto_title : L.potplayer_fix_title; const autoReason = isAutoPrompt ? `
${L.potplayer_fix_reason_auto}
` : ''; const protocolState = readPotPlayerProtocolState(); const savedCustomPath = normalizePotPlayerCustomPath(protocolState.customPlayerPath); const versionOutdated = !!protocolState.confirmedRepairVersion && protocolState.confirmedRepairVersion !== CONF.potplayerProtocolRepairVersion; const getPathStatusText = () => { const state = readPotPlayerProtocolState(); const checked = validatePotPlayerCustomPath(state.customPlayerPath); if (!checked.ok) return L.potplayer_fix_path_status_empty; return (state.confirmedRepairVersion === CONF.potplayerProtocolRepairVersion && state.pathBoundRepairVersion === CONF.potplayerProtocolRepairVersion) ? L.potplayer_fix_path_status_confirmed : L.potplayer_fix_path_status_unconfirmed; }; const m = showModal(`

${title}

${autoReason}
${L.potplayer_fix_desc}
${L.potplayer_fix_manual_tip}
${versionOutdated ? `
${L.potplayer_fix_version_outdated}
` : ''}
${L.potplayer_fix_advanced_desc}
${getPathStatusText()}
${L.potplayer_fix_browser_policy_desc}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.classList.add('pk-potfix-modal'); Object.assign(modalBox.style, { width: '480px', padding: '24px', height: 'auto', minHeight: 'auto' }); modalBox.style.setProperty('max-height', '92vh', 'important'); modalBox.style.setProperty('overflow-x', 'hidden', 'important'); modalBox.style.setProperty('overflow-y', 'auto', 'important'); modalBox.style.setProperty('clip-path', 'inset(0 round 12px)', 'important'); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '18px', right: '18px' }); } const pathInput = m.querySelector('#pk_potfix_path'); const pathStatus = m.querySelector('#pk_potfix_path_status'); const refreshPathStatus = () => { if (pathStatus) pathStatus.textContent = getPathStatusText(); }; bindConfigLiveInputLimits(m, [['#pk_potfix_path', 'pk_potplayer_custom_path']]); m.querySelector('#pk_potfix_copy').onclick = () => { try { GM_setClipboard(cleanUrl); } catch (e) {} showToast(L.potplayer_fix_copy_success || L.msg_copy_success); }; m.querySelector('#pk_potfix_delete').onclick = () => { downloadPotPlayerRegFile('delete'); showToast(L.potplayer_fix_delete_downloaded); }; m.querySelector('#pk_potfix_save_path').onclick = () => { const result = savePotPlayerCustomPath(pathInput ? pathInput.value : ''); if (!result.ok) { showToast(L.potplayer_fix_path_invalid, 'warning'); return; } if (pathInput) pathInput.value = result.path; refreshPathStatus(); showToast(result.changed && result.wasConfigured ? L.potplayer_fix_path_changed : L.potplayer_fix_path_saved, result.changed && result.wasConfigured ? 'warning' : 'success'); }; m.querySelector('#pk_potfix_custom_install').onclick = () => { const result = savePotPlayerCustomPath(pathInput ? pathInput.value : ''); if (!result.ok) { showToast(L.potplayer_fix_path_invalid, 'warning'); return; } if (pathInput) pathInput.value = result.path; refreshPathStatus(); if (!downloadPotPlayerRegFile('install')) { showToast(L.potplayer_fix_path_invalid, 'warning'); return; } showToast(L.potplayer_fix_install_downloaded); }; m.querySelector('#pk_potfix_browser_policy').onclick = () => { downloadPotPlayerRegFile('browser_policy'); showToast(L.potplayer_fix_browser_policy_downloaded); }; m.querySelector('#pk_potfix_retry').onclick = () => { const savedPath = getValidSavedPotPlayerPath(); if (!savedPath) { const result = savePotPlayerCustomPath(pathInput ? pathInput.value : ''); if (!result.ok) { showToast(L.potplayer_fix_retry_need_path || L.potplayer_fix_path_invalid, 'warning'); return; } if (pathInput) pathInput.value = result.path; refreshPathStatus(); showToast(L.potplayer_fix_retry_need_path || L.potplayer_fix_path_saved, 'warning'); return; } recordPotPlayerProtocolUserFixed(); clearPotPlayerLaunchFailCount(); showToast(L.potplayer_fix_confirmed); m.remove(); launchPotPlayerWithFallback(cleanUrl, { button: opts.button || null, source: typeof opts.source === 'string' ? opts.source : 'normal', failToastDuration: 6500, failCloseDelay: 3200, confirmFailDelay: CONF.potplayerPostRepairConfirmDelay, autoRepairPrompt: false, onLikelyOpen: () => { if (typeof opts.onRetryLikelyOpen === 'function') opts.onRetryLikelyOpen(); }, onLikelyFail: () => { if (typeof opts.onRetryLikelyFail === 'function') opts.onRetryLikelyFail(); } }); }; m.querySelector('#pk_potfix_skip_today').onclick = () => { suppressPotPlayerAutoPromptToday(); m.remove(); showToast(L.potplayer_fix_skip_today_done); }; m.querySelector('#pk_potfix_never_auto').onclick = () => { disablePotPlayerAutoPrompt(); m.remove(); showToast(L.potplayer_fix_never_auto_done); }; m.querySelector('#pk_potfix_close').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); m.remove(); } }); return m; }; async function resolveAudioPlayableSource(item, force = false) { const L = getStrings(); if (!item) throw new Error(L.audio_link_missing); const existing = force ? '' : getAudioDirectUrl(item); if (existing) return { item, url: existing }; try { if (item._isShareItem) { const detail = force ? await fetchShareFileInfo(item) : await resolveSharePlayableFile(item); if (force) mergeSharePlayableDetail(item, detail); const url = getAudioDirectUrl(item) || getAudioDirectUrl(detail) || getShareDirectUrl(detail); if (!url) throw new Error(L.audio_share_unsupported || L.audio_link_missing); item.web_content_link = url; return { item, url }; } const fileId = getAudioPhysicalId(item); if (!fileId) throw new Error(L.audio_link_missing); const detail = await apiGet(fileId); if (detail) { if (detail.web_content_link) item.web_content_link = detail.web_content_link; if (detail.links) item.links = detail.links; if (detail.mime_type) item.mime_type = detail.mime_type; if (detail.size) item.size = detail.size; if (detail.name) item.name = detail.name; if (detail.icon_link) item.icon_link = detail.icon_link; if (detail.thumbnail_link) item.thumbnail_link = detail.thumbnail_link; } const url = getAudioDirectUrl(detail) || getAudioDirectUrl(item); if (!url) throw new Error(L.audio_link_missing); return { item, url }; } catch (e) { await markOfflineReferenceMissingFromError(item, e, { source: 'audio_detail' }); const msg = String((e && e.message) || ''); if ([L.audio_link_missing, L.audio_share_unsupported, L.audio_link_expired, L.audio_format_unsupported, L.audio_play_failed].includes(msg)) throw e; throw new Error(item._isShareItem ? (L.audio_share_unsupported || L.audio_link_missing) : L.audio_link_missing); } } const pkAudioCoverCache = new Map(); const pkAudioCoverFailSet = new Set(); const getAudioCoverKey = (item) => String(getAudioPhysicalId(item) || (item && (item.id || item.file_id || item.name)) || ''); const normalizeAudioCoverUrl = (url) => String(url || '').replace('SIZE_MEDIUM', 'SIZE_LARGE'); const cacheAudioCoverUrl = (key, url, revoke = false) => { if (!key || !url) return url; const old = pkAudioCoverCache.get(key); if (old && old !== url && old.startsWith('blob:')) { try { URL.revokeObjectURL(old); } catch (e) {} } pkAudioCoverCache.set(key, url); while (pkAudioCoverCache.size > (CONF.audioCoverCacheMax || 80)) { const firstKey = pkAudioCoverCache.keys().next().value; const firstUrl = pkAudioCoverCache.get(firstKey); pkAudioCoverCache.delete(firstKey); if (revoke && firstUrl && firstUrl.startsWith('blob:')) { try { URL.revokeObjectURL(firstUrl); } catch (e) {} } } return url; }; const readSynchsafeInt = (bytes, offset) => ((bytes[offset] & 0x7f) << 21) | ((bytes[offset + 1] & 0x7f) << 14) | ((bytes[offset + 2] & 0x7f) << 7) | (bytes[offset + 3] & 0x7f); const readUtf16TerminatedEnd = (bytes, start, end) => { for (let i = start; i + 1 < end; i += 2) { if (bytes[i] === 0 && bytes[i + 1] === 0) return i + 2; } return start; }; const parseId3ApicCover = (buffer) => { const bytes = new Uint8Array(buffer || 0); if (bytes.length < 16 || bytes[0] !== 0x49 || bytes[1] !== 0x44 || bytes[2] !== 0x33) return null; const ver = bytes[3]; const tagSize = readSynchsafeInt(bytes, 6); let offset = 10; const end = Math.min(bytes.length, 10 + tagSize); while (offset + (ver === 2 ? 6 : 10) <= end) { if (ver === 2) { const id = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2]); const size = (bytes[offset + 3] << 16) | (bytes[offset + 4] << 8) | bytes[offset + 5]; offset += 6; if (!size || offset + size > end) break; if (id === 'PIC') { const frameEnd = offset + size; const mime = `image/${String.fromCharCode(bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]).toLowerCase().replace('jpg', 'jpeg')}`; let imgStart = offset + 5; const enc = bytes[offset] || 0; if (enc === 1 || enc === 2) imgStart = readUtf16TerminatedEnd(bytes, imgStart, frameEnd); else { while (imgStart < frameEnd && bytes[imgStart] !== 0) imgStart++; imgStart++; } if (imgStart < frameEnd) return { mime, bytes: bytes.slice(imgStart, frameEnd) }; } offset += size; continue; } const id = String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]); const size = ver === 4 ? readSynchsafeInt(bytes, offset + 4) : ((bytes[offset + 4] << 24) | (bytes[offset + 5] << 16) | (bytes[offset + 6] << 8) | bytes[offset + 7]) >>> 0; offset += 10; if (!size || offset + size > end) break; if (id === 'APIC') { const frameEnd = offset + size; const enc = bytes[offset] || 0; let p = offset + 1; const mimeStart = p; while (p < frameEnd && bytes[p] !== 0) p++; const mime = Array.from(bytes.slice(mimeStart, p)).map(c => String.fromCharCode(c)).join('') || 'image/jpeg'; p++; p++; if (enc === 1 || enc === 2) p = readUtf16TerminatedEnd(bytes, p, frameEnd); else { while (p < frameEnd && bytes[p] !== 0) p++; p++; } if (p < frameEnd) return { mime, bytes: bytes.slice(p, frameEnd) }; } offset += size; } return null; }; const getAudioDirectCoverUrl = (item) => { if (!item) return ''; const thumb = normalizeAudioCoverUrl(item.thumbnail_link); if (thumb && thumb !== item.icon_link) return thumb; return ''; }; const findAudioSiblingCoverUrl = (item) => { if (!item || !Array.isArray(S.display)) return ''; const parentId = String(item.parent_id || item._shareParentId || ''); const coverNameScore = (name) => { const n = String(name || '').toLowerCase(); if (/^(cover|folder|albumart)\.(jpe?g|png|webp)$/.test(n)) return 1; if (/^(cover_thumb|__ia_thumb)\.(jpe?g|png|webp)$/.test(n)) return 2; if (/(cover|front|folder|album).*\.(jpe?g|png|webp)$/.test(n)) return 3; return 0; }; let best = null; let bestScore = 0; for (const it of S.display) { if (!isImageLikeItem(it)) continue; const score = coverNameScore(it.name); if (!score) continue; const itParent = String(it.parent_id || it._shareParentId || ''); if (parentId && itParent && itParent !== parentId) continue; const url = normalizeAudioCoverUrl(it.thumbnail_link); if (!url || url === it.icon_link) continue; if (!best || score < bestScore) { best = url; bestScore = score; } } return best || ''; }; async function fetchAudioEmbeddedCoverUrl(audioUrl, item) { const key = getAudioCoverKey(item); if (!key || !audioUrl || pkAudioCoverFailSet.has(key)) return ''; const ext = getItemExt(item).toLowerCase(); const mime = String((item && item.mime_type) || '').toLowerCase(); const urlLower = String(audioUrl || '').toLowerCase(); if (!(ext === 'mp3' || mime.includes('mpeg') || urlLower.includes('.mp3'))) return ''; try { const end = Math.max(65535, (CONF.audioCoverRangeBytes || 196608) - 1); const opts = { headers: { Range: `bytes=0-${end}` }, referrerPolicy: 'no-referrer' }; if (window.AbortSignal && AbortSignal.timeout) opts.signal = AbortSignal.timeout(9000); const res = await fetch(audioUrl, opts); if (!(res && (res.ok || res.status === 206))) throw new Error('audio cover fetch failed'); const parsed = parseId3ApicCover(await res.arrayBuffer()); if (!parsed || !parsed.bytes || !parsed.bytes.length) throw new Error('audio cover missing'); const blob = new Blob([parsed.bytes], { type: parsed.mime || 'image/jpeg' }); return cacheAudioCoverUrl(key, URL.createObjectURL(blob), true); } catch (e) { pkAudioCoverFailSet.add(key); return ''; } } let pkAudioMiniSession = null; function isAudioMiniActive() { return !!((pkAudioMiniSession && typeof pkAudioMiniSession.isMiniActive === 'function' && pkAudioMiniSession.isMiniActive()) || document.getElementById('pk-audio-mini')); } function getAudioMiniSavedPosition() { try { const pos = JSON.parse(gmGet(CONF.audioMiniPosKey, '') || 'null'); if (!pos) return null; const x = pos.x === 'right' ? 'right' : 'left'; const y = pos.y === 'bottom' ? 'bottom' : 'top'; const hx = Number(pos[x]); const vy = Number(pos[y]); if (Number.isFinite(hx) && Number.isFinite(vy)) return { x, y, [x]: hx, [y]: vy }; if (Number.isFinite(Number(pos.left)) && Number.isFinite(Number(pos.top))) return { x:'left', y:'top', left:Number(pos.left), top:Number(pos.top) }; return null; } catch (e) { return null; } } function applyAudioMiniPositionState(mini, pos) { if (!mini || !pos) return false; const x = pos.x === 'right' ? 'right' : 'left'; const y = pos.y === 'bottom' ? 'bottom' : 'top'; const hx = Number(pos[x]); const vy = Number(pos[y]); if (!Number.isFinite(hx) || !Number.isFinite(vy)) return false; if (x === 'right') { mini.style.right = `${Math.max(0, Math.round(hx))}px`; mini.style.left = 'auto'; } else { mini.style.left = `${Math.max(0, Math.round(hx))}px`; mini.style.right = 'auto'; } if (y === 'bottom') { mini.style.bottom = `${Math.max(0, Math.round(vy))}px`; mini.style.top = 'auto'; } else { mini.style.top = `${Math.max(0, Math.round(vy))}px`; mini.style.bottom = 'auto'; } return true; } function saveAudioMiniPosition(mini = document.getElementById('pk-audio-mini')) { if (!mini) return; const rect = mini.getBoundingClientRect(); if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top) || !Number.isFinite(rect.right) || !Number.isFinite(rect.bottom)) return; const left = Math.max(0, rect.left); const right = Math.max(0, window.innerWidth - rect.right); const top = Math.max(0, rect.top); const bottom = Math.max(0, window.innerHeight - rect.bottom); const x = right <= left ? 'right' : 'left'; const y = bottom <= top ? 'bottom' : 'top'; const pos = { x, y, left: Math.round(left), right: Math.round(right), top: Math.round(top), bottom: Math.round(bottom) }; gmSet(CONF.audioMiniPosKey, JSON.stringify(pos)); applyAudioMiniPositionState(mini, pos); } function applyAudioMiniSavedPosition(mini = document.getElementById('pk-audio-mini')) { if (!mini) return false; return applyAudioMiniPositionState(mini, getAudioMiniSavedPosition()); } function getAudioMiniThemeSource() { return document.querySelector('.pk-ov') || ((typeof UI !== 'undefined' && UI.win && UI.win.isConnected) ? (UI.win.closest && UI.win.closest('.pk-ov')) || UI.win : null) || document.querySelector('#pk-audio-ov') || document.querySelector('.pk-win') || document.documentElement; } function getAudioShellThemeSnapshot() { const savedTheme = gmGet('pk_theme', 'auto'); const sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const src = getAudioMiniThemeSource(); const darkNode = document.querySelector('.pk-ov.pk-dark,.pk-win.pk-dark'); const srcDark = !!(darkNode || (src && src.classList && src.classList.contains('pk-dark')) || (src && src.closest && src.closest('.pk-dark')) || document.documentElement.classList.contains('pk-dark') || (document.body && document.body.classList.contains('pk-dark'))); const isDark = srcDark || savedTheme === 'dark' || (savedTheme === 'auto' && sysDark); const cs = getComputedStyle(src || document.documentElement); return { src, cs, isDark }; } function applyAudioShellThemeVars(el, snap = getAudioShellThemeSnapshot()) { if (!el) return; const { cs, isDark } = snap; el.classList.toggle('pk-dark', !!isDark); const setVar = (name, fallback) => { const v = (cs.getPropertyValue(name) || '').trim(); el.style.setProperty(name, v || fallback); }; setVar('--pk-bg-rgb', isDark ? '32,32,32' : '255,255,255'); setVar('--pk-bg', isDark ? '#202020' : '#fff'); setVar('--pk-fg', isDark ? '#f5f5f5' : '#222'); setVar('--pk-bd', isDark ? '#333' : 'rgba(0,0,0,.12)'); setVar('--pk-hl', isDark ? 'rgba(255,255,255,.08)' : 'rgba(0,0,0,.06)'); setVar('--pk-pri', isDark ? '#4cc2ff' : '#0067c0'); } function syncAudioFullPlayerTheme(shell = document.getElementById('pk-audio-ov')) { applyAudioShellThemeVars(shell); } function syncAudioMiniTheme(mini = document.getElementById('pk-audio-mini')) { applyAudioShellThemeVars(mini); } function bindAudioMiniTheme(mini) { if (!mini) return; const savedTheme = gmGet('pk_theme', 'auto'); const sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const src = getAudioMiniThemeSource(); const darkNode = document.querySelector('.pk-ov.pk-dark,.pk-win.pk-dark,#pk-audio-ov.pk-dark'); const srcDark = !!(darkNode || (src && src.classList && src.classList.contains('pk-dark')) || (src && src.closest && src.closest('.pk-dark')) || document.documentElement.classList.contains('pk-dark') || (document.body && document.body.classList.contains('pk-dark'))); const isDark = srcDark || savedTheme === 'dark' || (savedTheme === 'auto' && sysDark); mini.classList.toggle('pk-dark', !!isDark); const cs = getComputedStyle(src || document.documentElement); const setVar = (name, fallback) => { const v = (cs.getPropertyValue(name) || '').trim(); mini.style.setProperty(name, v || fallback); }; setVar('--pk-bg-rgb', isDark ? '32,32,32' : '255,255,255'); setVar('--pk-bg', isDark ? '#202020' : '#fff'); setVar('--pk-fg', isDark ? '#f5f5f5' : '#222'); setVar('--pk-bd', isDark ? '#333' : 'rgba(0,0,0,.12)'); setVar('--pk-hl', isDark ? 'rgba(255,255,255,.08)' : 'rgba(0,0,0,.06)'); setVar('--pk-pri', isDark ? '#4cc2ff' : '#0067c0'); } function bindAudioMiniTheme(mini) { if (!mini) return; if (typeof mini._pkAudioMiniCleanupTheme === 'function') mini._pkAudioMiniCleanupTheme(); let themeSyncRaf = 0; const sync = () => { if (themeSyncRaf) cancelAnimationFrame(themeSyncRaf); themeSyncRaf = requestAnimationFrame(() => { themeSyncRaf = 0; syncAudioMiniTheme(mini); }); }; let mq = null; const mqHandler = () => sync(); if (window.matchMedia) { mq = window.matchMedia('(prefers-color-scheme: dark)'); if (mq && mq.addEventListener) mq.addEventListener('change', mqHandler); else if (mq && mq.addListener) mq.addListener(mqHandler); } const mo = typeof MutationObserver !== 'undefined' ? new MutationObserver(sync) : null; if (mo) { const observe = (n) => { if (n) { try { mo.observe(n, { attributes:true, attributeFilter:['class','style'] }); } catch (e) {} } }; observe(document.documentElement); observe(document.body); document.querySelectorAll('.pk-ov,.pk-win,#pk-audio-ov').forEach(observe); } const timer = setInterval(sync, 300); mini._pkAudioMiniCleanupTheme = () => { if (themeSyncRaf) cancelAnimationFrame(themeSyncRaf); clearInterval(timer); if (mo) mo.disconnect(); if (mq && mq.removeEventListener) mq.removeEventListener('change', mqHandler); else if (mq && mq.removeListener) mq.removeListener(mqHandler); }; sync(); } function clampAudioMiniPosition(mini = document.getElementById('pk-audio-mini')) { if (!mini) return; const rect = mini.getBoundingClientRect(); const cs = getComputedStyle(mini); const w = rect.width || mini.offsetWidth || 300; const h = rect.height || mini.offsetHeight || 120; const inlineLeft = Number.parseFloat(mini.style.left); const inlineRight = Number.parseFloat(mini.style.right); const inlineTop = Number.parseFloat(mini.style.top); const inlineBottom = Number.parseFloat(mini.style.bottom); const cssLeft = Number.parseFloat(cs.left); const cssRight = Number.parseFloat(cs.right); const cssTop = Number.parseFloat(cs.top); const cssBottom = Number.parseFloat(cs.bottom); const useRight = Number.isFinite(inlineRight) || (!Number.isFinite(inlineLeft) && Number.isFinite(cssRight) && (!Number.isFinite(cssLeft) || cssRight <= cssLeft)); const useBottom = Number.isFinite(inlineBottom) || (!Number.isFinite(inlineTop) && Number.isFinite(cssBottom) && (!Number.isFinite(cssTop) || cssBottom <= cssTop)); const maxX = Math.max(0, window.innerWidth - w); const maxY = Math.max(0, window.innerHeight - h); let hx = useRight ? (Number.isFinite(inlineRight) ? inlineRight : (Number.isFinite(cssRight) ? cssRight : Math.max(0, window.innerWidth - rect.right))) : (Number.isFinite(inlineLeft) ? inlineLeft : (Number.isFinite(rect.left) ? rect.left : maxX)); let vy = useBottom ? (Number.isFinite(inlineBottom) ? inlineBottom : (Number.isFinite(cssBottom) ? cssBottom : Math.max(0, window.innerHeight - rect.bottom))) : (Number.isFinite(inlineTop) ? inlineTop : (Number.isFinite(rect.top) ? rect.top : maxY)); hx = Math.max(0, Math.min(hx, maxX)); vy = Math.max(0, Math.min(vy, maxY)); applyAudioMiniPositionState(mini, useRight ? { x:'right', y: useBottom ? 'bottom' : 'top', right:hx, [useBottom ? 'bottom' : 'top']:vy } : { x:'left', y: useBottom ? 'bottom' : 'top', left:hx, [useBottom ? 'bottom' : 'top']:vy }); } function makeAudioMiniDraggable(mini, handle) { if (!mini || !handle) return; let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; const onMove = (e) => { if (!dragging) return; const rect = mini.getBoundingClientRect(); const w = rect.width || mini.offsetWidth || 300; const h = rect.height || mini.offsetHeight || 120; const maxLeft = Math.max(0, window.innerWidth - w); const maxTop = Math.max(0, window.innerHeight - h); const left = Math.max(0, Math.min(startLeft + e.clientX - startX, maxLeft)); const top = Math.max(0, Math.min(startTop + e.clientY - startY, maxTop)); mini.style.left = `${left}px`; mini.style.top = `${top}px`; mini.style.right = 'auto'; mini.style.bottom = 'auto'; }; const onUp = () => { if (!dragging) return; dragging = false; document.removeEventListener('pointermove', onMove, true); document.removeEventListener('pointerup', onUp, true); clampAudioMiniPosition(mini); saveAudioMiniPosition(mini); }; const onDown = (e) => { if (e.button !== 0 || (e.target && e.target.closest && e.target.closest('.pk-audio-mini-btn,.pk-audio-mini-fold'))) return; e.preventDefault(); const rect = mini.getBoundingClientRect(); dragging = true; startX = e.clientX; startY = e.clientY; startLeft = rect.left; startTop = rect.top; mini.style.left = `${startLeft}px`; mini.style.top = `${startTop}px`; mini.style.right = 'auto'; mini.style.bottom = 'auto'; document.addEventListener('pointermove', onMove, true); document.addEventListener('pointerup', onUp, true); }; const onResize = () => { clampAudioMiniPosition(mini); saveAudioMiniPosition(mini); }; handle.addEventListener('pointerdown', onDown); window.addEventListener('resize', onResize); mini._pkAudioMiniCleanupDrag = () => { dragging = false; handle.removeEventListener('pointerdown', onDown); window.removeEventListener('resize', onResize); document.removeEventListener('pointermove', onMove, true); document.removeEventListener('pointerup', onUp, true); }; } function destroyAudioMini(stopAudio = true) { if (pkAudioMiniSession && typeof pkAudioMiniSession.destroyMini === 'function') return pkAudioMiniSession.destroyMini(stopAudio); const mini = document.getElementById('pk-audio-mini'); if (mini) { if (typeof mini._pkAudioMiniCleanupDrag === 'function') mini._pkAudioMiniCleanupDrag(); mini.remove(); return true; } return false; } function closeAudioMiniForMediaOpen() { if (!isAudioMiniActive()) return false; return destroyAudioMini(true); } function playAudio(item) { if (S.trashMode || !item) return; if (isAudioMiniActive()) destroyAudioMini(true); const old = document.getElementById('pk-audio-ov'); if (old && typeof old._pkDestroyAudioPlayer === 'function') old._pkDestroyAudioPlayer(); else if (old) old.remove(); const L = getStrings(); const audioMiniModeLabel = `${L.audio_mini_mode} [${(CONF.audioHotkeys && CONF.audioHotkeys.mini) || 'P'}]`; const audioKey = (it) => String((it && (it.id || it.file_id || it.name)) || ''); const list = []; const seen = new Set(); (Array.isArray(S.display) ? S.display : []).forEach(it => { if (!isAudioLikeItem(it)) return; const key = audioKey(it); if (!key || seen.has(key)) return; seen.add(key); list.push(it); }); const curKey = audioKey(item); let curIdx = list.findIndex(it => audioKey(it) === curKey); if (curIdx < 0) { list.unshift(item); curIdx = 0; } const d = document.createElement('div'); d.id = 'pk-audio-ov'; syncAudioFullPlayerTheme(d); d.tabIndex = 0; d.innerHTML = ` `; const audio = d.querySelector('#pk_audio'); const audioBox = d.querySelector('#pk_audio_box'); const titleEl = d.querySelector('#pk_audio_title'); const nameEl = d.querySelector('#pk_audio_name'); const coverEl = d.querySelector('#pk_audio_cover'); const formatEl = d.querySelector('#pk_audio_format'); const sizeEl = d.querySelector('#pk_audio_size'); const statusEl = d.querySelector('#pk_audio_status'); const curEl = d.querySelector('#pk_audio_t_cur'); const durEl = d.querySelector('#pk_audio_t_dur'); const progressWrap = d.querySelector('#pk_audio_progress_wrap'); const progressEl = d.querySelector('#pk_audio_progress'); const progressTipEl = d.querySelector('#pk_audio_progress_tip'); const volumeEl = d.querySelector('#pk_audio_volume'); const playBtn = d.querySelector('#pk_audio_play'); const muteBtn = d.querySelector('#pk_audio_mute'); const modeBtn = d.querySelector('#pk_audio_mode'); const prevBtn = d.querySelector('#pk_audio_prev'); const nextBtn = d.querySelector('#pk_audio_next'); const playlistBtn = d.querySelector('#pk_audio_playlist'); const playlistPanel = d.querySelector('#pk_audio_playlist_panel'); const playlistBody = d.querySelector('#pk_audio_playlist_body'); const playlistCountEl = d.querySelector('#pk_audio_playlist_count'); const miniBtn = d.querySelector('#pk_audio_mini'); const closeBtn = d.querySelector('#pk_audio_close'); let destroyed = false; let loadToken = 0; let retryUsed = false; let seeking = false; let loadingAudio = false; let pendingPlayRequested = false; let audioFailedLocked = false; let audioFailNextTimer = null; let audioFailAutoSkipCount = 0; let audioPlaylistOpen = false; let audioDurationRenderRaf = 0; const audioPlayModes = ['order','list','single','shuffle']; let audioPlayMode = audioPlayModes.includes(gmGet('pk_audio_play_mode', 'order')) ? gmGet('pk_audio_play_mode', 'order') : 'order'; let lastVolume = Math.max(0, Math.min(1, Number(gmGet('pk_audio_vol_level', 1)) || 1)); const savedAudioMuted = gmGet('pk_audio_vol_muted', false); let audioMuted = savedAudioMuted === true || savedAudioMuted === 'true'; let miniEl = null; let miniHost = null; audio.volume = lastVolume; audio.muted = audioMuted; volumeEl.value = String(audioMuted ? 0 : lastVolume); volumeEl.style.setProperty('--pk-audio-pct', `${Math.round((audioMuted ? 0 : lastVolume) * 100)}%`); const current = () => list[curIdx] || item; const fmtTime = (sec) => fmtDur(Number.isFinite(sec) && sec > 0 ? sec : 0); let audioShuffleOrder = []; let audioShufflePos = -1; const isValidAudioShuffleIndex = (idx) => Number.isInteger(idx) && idx >= 0 && idx < list.length; const shuffleAudioIndexes = (arr) => { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; }; const resetAudioShuffleHistory = (keepCurrent = true) => { audioShuffleOrder = []; audioShufflePos = -1; if (!list.length) return; const startIdx = keepCurrent && isValidAudioShuffleIndex(curIdx) ? curIdx : 0; const rest = shuffleAudioIndexes(list.map((_, idx) => idx).filter(idx => idx !== startIdx)); audioShuffleOrder = [startIdx, ...rest]; audioShufflePos = 0; }; const syncAudioShuffleCurrent = () => { if (!list.length) { audioShuffleOrder = []; audioShufflePos = -1; return; } const setSize = new Set(audioShuffleOrder).size; const valid = audioShuffleOrder.length === list.length && setSize === list.length && audioShuffleOrder.every(isValidAudioShuffleIndex); if (!valid || !audioShuffleOrder.includes(curIdx)) { resetAudioShuffleHistory(true); return; } audioShufflePos = audioShuffleOrder.indexOf(curIdx); }; resetAudioShuffleHistory(true); const getAudioDisplayName = (it) => { const name = it && it.name ? String(it.name) : L.audio_player_title; const ext = getItemExt(it); return ext && name.toLowerCase().endsWith('.' + ext.toLowerCase()) ? name.slice(0, -(ext.length + 1)) : name; }; const setRangePct = (el, pct) => el.style.setProperty('--pk-audio-pct', `${Math.max(0, Math.min(100, pct))}%`); const setMiniVolumePct = (el, value) => el && el.style.setProperty('--pk-mini-vol-pct', `${Math.max(0, Math.min(100, (Number(value) || 0) * 100))}%`); let audioCoverToken = 0; const renderAudioCoverUrl = (url) => { if (!coverEl) return; if (url) { coverEl.innerHTML = ``; return; } coverEl.innerHTML = CONF.typeIcons.audio; }; const refreshAudioCover = async (audioUrl = '') => { const it = current(); const key = getAudioCoverKey(it); const token = ++audioCoverToken; const cached = key && pkAudioCoverCache.get(key); const direct = getAudioDirectCoverUrl(it); const sibling = findAudioSiblingCoverUrl(it); if (cached || direct || sibling) { renderAudioCoverUrl(cached || cacheAudioCoverUrl(key, direct || sibling)); return; } renderAudioCoverUrl(''); if (!audioUrl) return; const embedded = await fetchAudioEmbeddedCoverUrl(audioUrl, it); if (!destroyed && token === audioCoverToken && audioKey(current()) === audioKey(it)) renderAudioCoverUrl(embedded || ''); }; const updateMiniInfo = () => { if (!miniEl) return; const it = current(); const name = it && it.name ? it.name : L.audio_player_title; const total = Math.max(1, list.length || 1); const index = Math.min(total, Math.max(1, curIdx + 1)); const miniName = miniEl.querySelector('.pk-audio-mini-name'); const miniHeadTitle = miniEl.querySelector('.pk-audio-mini-title-wrap'); const miniHeadTrack = miniEl.querySelector('.pk-audio-mini-title-track'); const miniHeadMain = miniHeadTrack ? miniHeadTrack.querySelector('.pk-audio-mini-title-text:first-child') : null; const miniHeadCopy = miniHeadTrack ? miniHeadTrack.querySelector('.pk-audio-mini-title-text:last-child') : null; const miniMeta = miniEl.querySelector('.pk-audio-mini-meta'); const miniPlayBtn = miniEl.querySelector('#pk_audio_mini_play'); if (miniName) miniName.textContent = name; if (miniHeadTitle && miniHeadTrack && miniHeadMain && miniHeadMain.dataset.audioName !== name) { miniHeadMain.dataset.audioName = name; miniHeadTitle.removeAttribute('title'); miniHeadTitle.dataset.pkTip = name; miniHeadTitle.classList.remove('pk-audio-mini-title-scroll'); miniHeadTitle.style.removeProperty('--pk-mini-title-shift'); miniHeadTitle.style.removeProperty('--pk-mini-title-duration'); miniHeadMain.textContent = name; if (miniHeadCopy) miniHeadCopy.textContent = name; requestAnimationFrame(() => { if (!miniEl || !miniHeadTitle.isConnected || !miniHeadTrack.isConnected || !miniHeadMain.isConnected) return; if (!miniEl.classList.contains('pk-audio-mini-collapsed')) return; const gap = 32; const overflow = miniHeadMain.scrollWidth - miniHeadTitle.clientWidth; if (overflow > 8) { const shift = miniHeadMain.scrollWidth + gap; miniHeadTitle.style.setProperty('--pk-mini-title-shift', `-${Math.ceil(shift)}px`); miniHeadTitle.style.setProperty('--pk-mini-title-duration', `${Math.max(10, Math.min(28, shift / 18 + 8)).toFixed(1)}s`); miniHeadTitle.classList.add('pk-audio-mini-title-scroll'); } }); } if (miniMeta) { const dur = Number(audio.duration) || 0; const cur = Number(audio.currentTime) || 0; const status = statusEl.textContent || ''; miniMeta.textContent = `${index}/${total} - ${fmtTime(cur)} / ${fmtTime(dur)}${status ? ` - ${status}` : ''}`; } if (miniPlayBtn) { const isPaused = audio.paused || audio.ended; const label = isPaused ? L.audio_play : L.audio_pause; miniPlayBtn.innerHTML = isPaused ? CONF.audioIcons.play : CONF.audioIcons.pause; miniPlayBtn.setAttribute('aria-label', label); miniPlayBtn.dataset.tip = label; } }; const setStatus = (text, isErr = false) => { statusEl.textContent = text || ''; statusEl.classList.toggle('err', !!isErr); updateMiniInfo(); }; const clearAudioFailNextTimer = () => { if (audioFailNextTimer) { clearTimeout(audioFailNextTimer); audioFailNextTimer = null; } }; const scheduleAudioFailNext = () => { clearAudioFailNextTimer(); if (list.length <= 1 || audioFailAutoSkipCount >= list.length - 1) return; const token = loadToken; audioFailNextTimer = setTimeout(() => { if (destroyed || token !== loadToken) return; audioFailNextTimer = null; audioFailAutoSkipCount++; switchAudio(audioPlayMode === 'shuffle' ? getAudioShuffleNextIndex() : curIdx + 1); }, 3000); }; const markAudioFinalFailure = (text) => { audioFailedLocked = true; pendingPlayRequested = false; loadingAudio = false; setStatus(text || L.audio_play_failed, true); updatePlayButton(); scheduleAudioFailNext(); }; const updatePlayButton = () => { const isPaused = audio.paused || audio.ended; const label = isPaused ? L.audio_play : L.audio_pause; playBtn.innerHTML = isPaused ? CONF.audioIcons.play : CONF.audioIcons.pause; playBtn.removeAttribute('title'); playBtn.dataset.tip = label; playBtn.setAttribute('aria-label', label); updateMiniInfo(); }; const updateMuteButton = () => { const isMuted = audio.muted || audio.volume <= 0; const label = isMuted ? L.audio_unmute : L.audio_mute; const icon = isMuted ? CONF.audioIcons.mute : CONF.audioIcons.vol; const val = isMuted ? 0 : audio.volume; muteBtn.innerHTML = icon; muteBtn.removeAttribute('title'); muteBtn.dataset.tip = label; muteBtn.setAttribute('aria-label', label); const miniMuteBtn = miniEl && miniEl.querySelector('#pk_audio_mini_mute'); const miniVolumeEl = miniEl && miniEl.querySelector('#pk_audio_mini_volume'); if (miniMuteBtn) { miniMuteBtn.innerHTML = icon; miniMuteBtn.removeAttribute('data-tip'); miniMuteBtn.setAttribute('aria-label', label); } if (miniVolumeEl) { miniVolumeEl.value = String(val); setMiniVolumePct(miniVolumeEl, val); } }; const updateModeButton = () => { const labelMap = {order:L.audio_mode_order,list:L.audio_mode_list,single:L.audio_mode_single,shuffle:L.audio_mode_shuffle}; const iconMap = {order:CONF.audioIcons.modeOrder,list:CONF.audioIcons.modeList,single:CONF.audioIcons.modeSingle,shuffle:CONF.audioIcons.modeShuffle}; const label = labelMap[audioPlayMode] || L.audio_mode_order; const icon = iconMap[audioPlayMode] || CONF.audioIcons.modeOrder; modeBtn.innerHTML = icon; modeBtn.removeAttribute('title'); modeBtn.dataset.tip = label; modeBtn.setAttribute('aria-label', label); const miniModeBtn = miniEl && miniEl.querySelector('#pk_audio_mini_mode'); if (miniModeBtn) { miniModeBtn.innerHTML = icon; miniModeBtn.dataset.tip = label; miniModeBtn.setAttribute('aria-label', label); } }; const updateTime = () => { if (!seeking) { const dur = Number(audio.duration) || 0; const cur = Number(audio.currentTime) || 0; const val = dur > 0 ? Math.round(cur / dur * 1000) : 0; progressEl.value = String(val); setRangePct(progressEl, val / 10); curEl.textContent = fmtTime(cur); durEl.textContent = fmtTime(dur); updateMiniInfo(); } }; const hideAudioProgressTip = () => { if (progressTipEl) progressTipEl.classList.remove('show'); }; const updateAudioProgressTip = (clientX) => { if (!progressWrap || !progressTipEl) return; const dur = Number(audio.duration) || getAudioKnownDuration(current()) || 0; if (!(dur > 0)) { hideAudioProgressTip(); return; } const rect = progressWrap.getBoundingClientRect(); const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / Math.max(1, rect.width))); progressTipEl.textContent = fmtTime(dur * ratio); progressTipEl.style.left = `${ratio * 100}%`; progressTipEl.classList.add('show'); }; const getAudioKnownDuration = (it) => { const key = audioKey(it); const raw = (it && it.params && it.params.duration) || (key && S.durationMap && S.durationMap.get(key)) || (key && gmGet('pk_duration_' + key, 0)) || 0; const dur = parseFloat(raw || 0); return Number.isFinite(dur) && dur > 0 ? dur : 0; }; const scheduleAudioDurationViewRefresh = () => { if (audioDurationRenderRaf) cancelAnimationFrame(audioDurationRenderRaf); audioDurationRenderRaf = requestAnimationFrame(() => { audioDurationRenderRaf = 0; if (typeof renderVisible === 'function') renderVisible(); }); }; const saveAudioKnownDuration = (it, dur) => { const seconds = Math.round(Number(dur) || 0); if (!(seconds > 0)) return; const key = audioKey(it); if (it) { if (!it.params) it.params = {}; it.params.duration = seconds; } if (key && S.durationMap) S.durationMap.set(key, seconds); if (key) gmSet('pk_duration_' + key, seconds); const syncOne = (target) => { if (!target || audioKey(target) !== key) return; if (!target.params) target.params = {}; target.params.duration = seconds; }; [list, S.items, S.display].forEach(arr => Array.isArray(arr) && arr.forEach(syncOne)); if (audioPlaylistOpen) renderAudioPlaylist(); else updateAudioPlaylistActive(); scheduleAudioDurationViewRefresh(); }; const getAudioPlaylistMeta = (it) => { const size = it && it.size ? fmtSize(Number(it.size)) : ''; const dur = getAudioKnownDuration(it); const durationText = dur > 0 ? fmtTime(dur) : ''; return size && durationText ? `${size}/${durationText}` : (size || durationText); }; const updateAudioPlaylistActive = () => { if (playlistCountEl) playlistCountEl.textContent = list.length ? `${curIdx + 1}/${list.length}` : '0'; if (!playlistBody) return; let activeEl = null; playlistBody.querySelectorAll('.pk-audio-playlist-item').forEach(row => { const active = Number(row.dataset.idx) === curIdx; row.classList.toggle('pk-audio-playlist-item-active', active); row.setAttribute('aria-current', active ? 'true' : 'false'); if (active) activeEl = row; }); if (audioPlaylistOpen && activeEl) { try { activeEl.scrollIntoView({ block: 'nearest' }); } catch (e) {} } }; const renderAudioPlaylist = () => { if (!playlistBody) return; if (!list.length) { playlistBody.innerHTML = `
${esc(L.audio_playlist_empty)}
`; updateAudioPlaylistActive(); return; } playlistBody.innerHTML = list.map((it, idx) => { const name = it && it.name ? it.name : L.audio_player_title; const meta = getAudioPlaylistMeta(it); const activeCls = idx === curIdx ? ' pk-audio-playlist-item-active' : ''; return ``; }).join(''); updateAudioPlaylistActive(); }; const setAudioPlaylistOpen = (open) => { audioPlaylistOpen = !!open; if (audioBox) audioBox.classList.toggle('pk-audio-playlist-open', audioPlaylistOpen); if (playlistPanel) playlistPanel.setAttribute('aria-hidden', audioPlaylistOpen ? 'false' : 'true'); if (playlistBtn) { const label = audioPlaylistOpen ? L.audio_playlist_close : L.audio_playlist; playlistBtn.classList.toggle('pk-audio-btn-active', audioPlaylistOpen); playlistBtn.dataset.tip = label; playlistBtn.setAttribute('aria-label', label); playlistBtn.setAttribute('aria-pressed', audioPlaylistOpen ? 'true' : 'false'); } if (audioPlaylistOpen) renderAudioPlaylist(); }; const onUnifiedDurationSaved = (e) => { const detail = (e && e.detail) || {}; const key = String(detail.id || ''); const seconds = Math.round(Number(detail.duration) || 0); if (!key || !(seconds > 0)) return; const syncOne = (target) => { if (!target || audioKey(target) !== key) return; if (!target.params) target.params = {}; target.params.duration = seconds; }; list.forEach(syncOne); syncOne(item); if (audioKey(current()) === key && !(Number(audio.duration) > 0)) durEl.textContent = fmtTime(seconds); if (audioPlaylistOpen) renderAudioPlaylist(); else updateAudioPlaylistActive(); updateMiniInfo(); }; document.addEventListener('pk-duration-saved', onUnifiedDurationSaved); const stopAudioDurationPrefetch = () => { }; const updateInfo = () => { const it = current(); const name = it && it.name ? it.name : L.audio_player_title; const displayName = getAudioDisplayName(it); const ext = getItemExt(it).toUpperCase(); const mime = String((it && it.mime_type) || '').replace(/^audio\//i, '').toUpperCase(); const total = Math.max(1, list.length || 1); const index = Math.min(total, Math.max(1, curIdx + 1)); titleEl.textContent = `[${index}/${total}] ${name}`; nameEl.textContent = displayName; formatEl.textContent = ext || mime || '-'; sizeEl.textContent = it && it.size ? fmtSize(Number(it.size)) : '-'; refreshAudioCover(''); updateAudioPlaylistActive(); updateMiniInfo(); }; const resetAudioElement = () => { clearAudioFailNextTimer(); audioFailedLocked = false; audio.pause(); audio.removeAttribute('src'); audio.load(); curEl.textContent = '00:00'; durEl.textContent = '00:00'; progressEl.value = '0'; setRangePct(progressEl, 0); updatePlayButton(); }; const loadCurrent = async (autoPlay = true, force = false) => { const token = ++loadToken; if (!force) retryUsed = false; loadingAudio = true; pendingPlayRequested = !!autoPlay; resetAudioElement(); updateInfo(); setStatus(L.audio_loading); try { const result = await resolveAudioPlayableSource(current(), force); if (destroyed || token !== loadToken) return; refreshAudioCover(result.url); audio.src = result.url; audio.load(); setStatus(L.audio_ready); if (autoPlay || pendingPlayRequested) { try { await audio.play(); if (!destroyed && token === loadToken) { pendingPlayRequested = false; setStatus(L.audio_playing); } } catch (e) { if (!destroyed && token === loadToken && !(audio.error && audio.error.code)) setStatus(L.audio_ready); } } } catch (e) { if (destroyed || token !== loadToken) return; markAudioFinalFailure((e && e.message) || L.audio_link_missing); } finally { if (token === loadToken) loadingAudio = false; updatePlayButton(); updateMuteButton(); } }; const switchAudio = (idx) => { if (!list.length) return; clearAudioFailNextTimer(); curIdx = (idx + list.length) % list.length; if (audioPlayMode === 'shuffle') syncAudioShuffleCurrent(); retryUsed = false; updateAudioPlaylistActive(); loadCurrent(true, false); }; const getAudioShuffleStepIndex = (delta) => { if (list.length <= 1) return curIdx; syncAudioShuffleCurrent(); if (audioShuffleOrder.length <= 1 || audioShufflePos < 0) return curIdx; audioShufflePos = (audioShufflePos + delta + audioShuffleOrder.length) % audioShuffleOrder.length; return audioShuffleOrder[audioShufflePos]; }; const getAudioShuffleNextIndex = () => getAudioShuffleStepIndex(1); const getAudioShufflePrevIndex = () => getAudioShuffleStepIndex(-1); const getManualNextIndex = (delta) => audioPlayMode === 'shuffle' ? (delta < 0 ? getAudioShufflePrevIndex() : getAudioShuffleNextIndex()) : curIdx + delta; const handleAudioEnded = () => { audioFailedLocked = false; if (audioPlayMode === 'single') { audio.currentTime = 0; audio.play().catch(() => { if (!destroyed && audio.error && audio.error.code) markAudioFinalFailure(L.audio_play_failed); }); return; } if (audioPlayMode === 'shuffle') { if (list.length > 1) switchAudio(getAudioShuffleNextIndex()); else { audio.currentTime = 0; setStatus(L.audio_paused); updatePlayButton(); } return; } if (audioPlayMode === 'list') { if (list.length > 1) switchAudio(curIdx + 1); else { audio.currentTime = 0; setStatus(L.audio_paused); updatePlayButton(); } return; } if (list.length > 1 && curIdx < list.length - 1) switchAudio(curIdx + 1); else { audio.currentTime = 0; setStatus(L.audio_paused); updatePlayButton(); } }; const seekBy = (delta) => { const dur = Number(audio.duration) || 0; if (!dur) return; audio.currentTime = Math.max(0, Math.min(dur, (Number(audio.currentTime) || 0) + delta)); updateTime(); }; const setVolume = (value) => { const v = Math.max(0, Math.min(1, Number(value) || 0)); audio.volume = v; audioMuted = v <= 0; audio.muted = audioMuted; if (v > 0) lastVolume = v; volumeEl.value = String(audioMuted ? 0 : v); setRangePct(volumeEl, (audioMuted ? 0 : v) * 100); gmSet('pk_audio_vol_level', v); gmSet('pk_audio_vol_muted', audioMuted); updateMuteButton(); }; const isTextInputTarget = (target) => { if (!target) return false; const tag = String(target.tagName || '').toUpperCase(); return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || !!target.closest('[contenteditable="true"],[contenteditable=""]'); }; const removeAudioMiniShell = () => { const mini = miniEl || document.getElementById('pk-audio-mini'); if (mini && typeof mini._pkAudioMiniCleanupDrag === 'function') mini._pkAudioMiniCleanupDrag(); if (mini && typeof mini._pkAudioMiniCleanupTheme === 'function') mini._pkAudioMiniCleanupTheme(); if (mini) mini.remove(); miniEl = null; miniHost = null; }; const destroyCurrentAudioMini = (stopAudio = true) => { const hadMini = !!(miniEl || document.getElementById('pk-audio-mini')); if (!stopAudio) { restoreAudioFromMini(); return hadMini; } removeAudioMiniShell(); if (pkAudioMiniSession && pkAudioMiniSession.owner === d) pkAudioMiniSession = null; destroy(); return hadMini; }; const restoreAudioFromMini = () => { if (destroyed || !miniEl) return; if (audioBox && audio.parentNode !== audioBox) audioBox.insertBefore(audio, audioBox.firstChild); removeAudioMiniShell(); syncAudioFullPlayerTheme(d); if (!d.isConnected) document.body.appendChild(d); requestAnimationFrame(() => syncAudioFullPlayerTheme(d)); if (pkAudioMiniSession && pkAudioMiniSession.owner === d) pkAudioMiniSession = null; updateInfo(); updateModeButton(); updateMuteButton(); updatePlayButton(); updateTime(); setTimeout(() => d.focus(), 0); }; const enterAudioMiniMode = () => { if (destroyed) return; if (miniEl) { clampAudioMiniPosition(miniEl); return; } const staleMini = document.getElementById('pk-audio-mini'); if (staleMini) { if (typeof staleMini._pkAudioMiniCleanupDrag === 'function') staleMini._pkAudioMiniCleanupDrag(); staleMini.remove(); } miniEl = document.createElement('div'); miniEl.id = 'pk-audio-mini'; miniEl.setAttribute('role', 'dialog'); miniEl.setAttribute('aria-label', L.audio_mini_mode); miniEl.innerHTML = `
`; miniHost = miniEl.querySelector('.pk-audio-mini-audio-host'); if (miniHost && audio.parentNode !== miniHost) miniHost.appendChild(audio); document.body.appendChild(miniEl); bindAudioMiniTheme(miniEl); applyAudioMiniSavedPosition(miniEl); if (d.isConnected) d.remove(); const miniClose = miniEl.querySelector('#pk_audio_mini_close'); const miniFold = miniEl.querySelector('#pk_audio_mini_fold'); const miniMode = miniEl.querySelector('#pk_audio_mini_mode'); const miniPrev = miniEl.querySelector('#pk_audio_mini_prev'); const miniPlay = miniEl.querySelector('#pk_audio_mini_play'); const miniNext = miniEl.querySelector('#pk_audio_mini_next'); const miniMute = miniEl.querySelector('#pk_audio_mini_mute'); const miniVolumeWrap = miniEl.querySelector('.pk-audio-mini-volume-wrap'); const miniVolume = miniEl.querySelector('#pk_audio_mini_volume'); const miniRestore = miniEl.querySelector('#pk_audio_mini_restore'); if (miniClose) miniClose.onclick = () => destroyCurrentAudioMini(true); if (miniFold) miniFold.onclick = () => { const collapsed = !miniEl.classList.contains('pk-audio-mini-collapsed'); miniEl.classList.toggle('pk-audio-mini-collapsed', collapsed); miniFold.setAttribute('aria-pressed', collapsed ? 'true' : 'false'); if (collapsed) { const miniHeadMain = miniEl.querySelector('.pk-audio-mini-title-track .pk-audio-mini-title-text:first-child'); if (miniHeadMain) miniHeadMain.dataset.audioName = ''; } requestAnimationFrame(() => { if (collapsed) updateMiniInfo(); clampAudioMiniPosition(miniEl); saveAudioMiniPosition(miniEl); }); }; if (miniMode) miniMode.onclick = () => modeBtn.click(); if (miniPrev) miniPrev.onclick = () => prevBtn.click(); if (miniPlay) miniPlay.onclick = () => playBtn.click(); if (miniNext) miniNext.onclick = () => nextBtn.click(); if (miniMute) miniMute.onclick = () => muteBtn.click(); if (miniVolume) { const syncMiniVolume = () => { const v = audio.muted ? 0 : audio.volume; miniVolume.value = String(v); setMiniVolumePct(miniVolume, v); }; syncMiniVolume(); miniVolume.oninput = () => { setMiniVolumePct(miniVolume, miniVolume.value); setVolume(miniVolume.value); }; miniVolume.addEventListener('pointerdown', e => { e.stopPropagation(); if (miniVolumeWrap) miniVolumeWrap.classList.add('pk-audio-mini-volume-active'); const done = () => { if (miniVolumeWrap) miniVolumeWrap.classList.remove('pk-audio-mini-volume-active'); miniVolume.blur(); window.removeEventListener('pointerup', done, true); window.removeEventListener('pointercancel', done, true); }; window.addEventListener('pointerup', done, true); window.addEventListener('pointercancel', done, true); }); miniVolume.addEventListener('click', e => e.stopPropagation()); } if (miniRestore) miniRestore.onclick = restoreAudioFromMini; makeAudioMiniDraggable(miniEl, miniEl.querySelector('.pk-audio-mini-drag')); pkAudioMiniSession = { owner: d, isMiniActive: () => !destroyed && !!(miniEl && miniEl.isConnected), destroyMini: destroyCurrentAudioMini, restoreFull: restoreAudioFromMini }; updateMiniInfo(); updateModeButton(); updateMuteButton(); requestAnimationFrame(() => { syncAudioMiniTheme(miniEl); clampAudioMiniPosition(miniEl); }); }; const keyHandler = (e) => { if (destroyed || !document.getElementById('pk-audio-ov') || isTextInputTarget(e.target)) return; if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.key === 'ArrowLeft') { e.preventDefault(); e.stopPropagation(); prevBtn.click(); } else if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.key === 'ArrowRight') { e.preventDefault(); e.stopPropagation(); nextBtn.click(); } else if (e.key === ' ') { e.preventDefault(); e.stopPropagation(); playBtn.click(); } else if (!e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && String(e.key || '').toLowerCase() === 'p') { e.preventDefault(); e.stopPropagation(); if (miniBtn && document.getElementById('pk-audio-ov')) miniBtn.click(); } else if (e.key === 'ArrowLeft') { e.preventDefault(); e.stopPropagation(); seekBy(-10); } else if (e.key === 'ArrowRight') { e.preventDefault(); e.stopPropagation(); seekBy(10); } else if (e.key === 'ArrowUp') { e.preventDefault(); e.stopPropagation(); setVolume(audio.volume + 0.05); } else if (e.key === 'ArrowDown') { e.preventDefault(); e.stopPropagation(); setVolume(audio.volume - 0.05); } else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); destroy(); } }; function destroy() { if (destroyed) return; destroyed = true; stopAudioDurationPrefetch(); clearAudioFailNextTimer(); removeAudioMiniShell(); if (pkAudioMiniSession && pkAudioMiniSession.owner === d) pkAudioMiniSession = null; document.removeEventListener('pk-duration-saved', onUnifiedDurationSaved); document.removeEventListener('keydown', keyHandler, true); const locateItem = current(); audio.pause(); audio.removeAttribute('src'); audio.load(); d.remove(); setTimeout(() => S.locatePreviewSourceItem(locateItem), 0); } d._pkDestroyAudioPlayer = destroy; closeBtn.onclick = destroy; if (miniBtn) miniBtn.onclick = enterAudioMiniMode; modeBtn.onclick = () => { const idx = audioPlayModes.indexOf(audioPlayMode); audioPlayMode = audioPlayModes[(idx + 1) % audioPlayModes.length] || 'order'; gmSet('pk_audio_play_mode', audioPlayMode); if (audioPlayMode === 'shuffle') resetAudioShuffleHistory(true); else resetAudioShuffleHistory(false); updateModeButton(); }; if (playlistBtn) playlistBtn.onclick = () => setAudioPlaylistOpen(!audioPlaylistOpen); if (playlistBody) { playlistBody.addEventListener('click', (e) => { const row = e.target && e.target.closest ? e.target.closest('.pk-audio-playlist-item') : null; if (!row) return; const idx = Number(row.dataset.idx); if (!Number.isInteger(idx) || idx < 0 || idx >= list.length) return; e.preventDefault(); audioFailAutoSkipCount = 0; clearAudioFailNextTimer(); switchAudio(idx); if (audioPlayMode === 'shuffle') resetAudioShuffleHistory(true); }); } prevBtn.onclick = () => { audioFailAutoSkipCount = 0; switchAudio(getManualNextIndex(-1)); }; nextBtn.onclick = () => { audioFailAutoSkipCount = 0; switchAudio(getManualNextIndex(1)); }; playBtn.onclick = () => { if (loadingAudio || (audio.src && audio.readyState < 2 && !audio.error)) { pendingPlayRequested = true; setStatus(L.audio_loading); return; } if (!audio.src) { pendingPlayRequested = true; loadCurrent(true, false); return; } if (audio.paused || audio.ended) { pendingPlayRequested = true; audio.play().then(() => { if (!destroyed) { audioFailedLocked = false; pendingPlayRequested = false; setStatus(L.audio_playing); updatePlayButton(); } }).catch(() => { if (!destroyed && audio.error && audio.error.code) markAudioFinalFailure(L.audio_play_failed); }); } else { pendingPlayRequested = false; audio.pause(); } }; muteBtn.onclick = () => { if (audio.muted || audio.volume <= 0) setVolume(lastVolume || 1); else { lastVolume = audio.volume || lastVolume || 1; audioMuted = true; audio.muted = true; volumeEl.value = '0'; setRangePct(volumeEl, 0); gmSet('pk_audio_vol_level', lastVolume); gmSet('pk_audio_vol_muted', true); updateMuteButton(); } }; volumeEl.oninput = () => setVolume(volumeEl.value); progressEl.oninput = () => { seeking = true; const dur = Number(audio.duration) || 0; const val = Number(progressEl.value) || 0; setRangePct(progressEl, val / 10); curEl.textContent = fmtTime(dur * val / 1000); }; progressEl.onchange = () => { const dur = Number(audio.duration) || 0; if (dur > 0) audio.currentTime = dur * (Number(progressEl.value) || 0) / 1000; seeking = false; updateTime(); }; if (progressWrap) { progressWrap.addEventListener('pointermove', (e) => updateAudioProgressTip(e.clientX)); progressWrap.addEventListener('pointerenter', (e) => updateAudioProgressTip(e.clientX)); progressWrap.addEventListener('pointerleave', hideAudioProgressTip); } progressEl.addEventListener('pointerup', () => { progressEl.onchange(); }); audio.addEventListener('loadedmetadata', () => { updateTime(); const dur = Number(audio.duration) || 0; if (dur > 0 && Number.isFinite(dur)) { saveAudioKnownDuration(current(), dur); if (audioPlaylistOpen) renderAudioPlaylist(); else updateAudioPlaylistActive(); } }); audio.addEventListener('canplay', () => { if (!pendingPlayRequested || destroyed || !audio.src || !audio.paused) return; audio.play().then(() => { if (!destroyed) { audioFailedLocked = false; pendingPlayRequested = false; setStatus(L.audio_playing); updatePlayButton(); } }).catch(() => { if (!destroyed && audio.error && audio.error.code) markAudioFinalFailure(L.audio_play_failed); }); }); audio.addEventListener('timeupdate', updateTime); audio.addEventListener('playing', () => { clearAudioFailNextTimer(); audioFailAutoSkipCount = 0; audioFailedLocked = false; setStatus(L.audio_playing); updatePlayButton(); }); audio.addEventListener('pause', () => { if (!destroyed && audio.src && !audio.ended && !audioFailedLocked) setStatus(L.audio_paused); updatePlayButton(); }); audio.addEventListener('volumechange', () => { volumeEl.value = String(audio.muted ? 0 : audio.volume); setRangePct(volumeEl, (audio.muted ? 0 : audio.volume) * 100); updateMuteButton(); }); audio.addEventListener('ended', handleAudioEnded); audio.addEventListener('error', () => { if (destroyed || !audio.src) return; if (!retryUsed) { retryUsed = true; setStatus(L.audio_link_expired); loadCurrent(true, true); return; } const code = audio.error && audio.error.code; markAudioFinalFailure(code === 4 ? L.audio_format_unsupported : L.audio_play_failed); }); document.body.appendChild(d); document.addEventListener('keydown', keyHandler, true); renderAudioPlaylist(); setAudioPlaylistOpen(false); updateInfo(); updateModeButton(); updateMuteButton(); updatePlayButton(); setTimeout(() => d.focus(), 0); loadCurrent(true, false); } async function playVideo(item, startFullscreen = false) { if (S.trashMode) return; closeAudioMiniForMediaOpen(); 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(); if (getDefaultOpenPlayerPref() === 'potplayer' && typeof openDefaultPotPlayerForItem === 'function') { const handledByExternalPlayer = await openDefaultPotPlayerForItem(item); if (handledByExternalPlayer) return; } const shouldSyncOfficialPlayHistory = (it = item) => !(it && it._isShareItem); let sharePreviewLimitTipShown = false; let sharePreviewSoftLimitTipShown = false; const getSharePreviewLimitTip = (it = item) => { if (!it || !it._isShareItem) return ''; const p = it.params || {}; if (!p.anonymous_play_seconds && !p.anonymous_play_range) return ''; return L.msg_share_preview_limited_save || ''; }; const getSharePreviewExtLimitTip = (it = item) => { if (!it || !it._isShareItem) return ''; const p = it.params || {}; if (!p.anonymous_play_seconds && !p.anonymous_play_range) return ''; return L.msg_share_ext_preview_limited_save || L.msg_share_preview_limited_save || ''; }; const getSharePreviewSoftLimitTip = () => L.msg_share_preview_soft_limit; const getSharePreviewLimitParams = (it = item, duration = 0) => { if (!it || !it._isShareItem) return null; const p = it.params || {}; let seconds = Number(p.anonymous_play_seconds || 0); let range = Number(p.anonymous_play_range || 0); if (range > 1) range = range / 100; if (!(seconds > 0) && !(range > 0)) return null; const dur = Number(duration || (p && p.duration) || 0); if (!(dur > 0) || !isFinite(dur)) return null; let ratio = 0; if (seconds > 0) ratio = Math.max(ratio, seconds / dur); if (range > 0) ratio = Math.max(ratio, range); ratio = Math.max(0, Math.min(1, ratio)); if (!(ratio > 0) || ratio >= 0.995) return null; return { ratio, time: dur * ratio, seconds, range }; }; const isVideoItem = (it) => { if (!it || it.isHeader || it.kind === 'drive#folder') return false; if (S.offlineMode && it.phase !== 'PHASE_TYPE_COMPLETE') return false; if (S.uploadMode && it.status !== 'DONE') return false; const m = (it.mime_type || '').toLowerCase(); const n = (it.name || '').toLowerCase(); const dur = (it.params && it.params.duration) || 0; const vExts = ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp']; return m.startsWith('video/') || dur > 0 || vExts.some(e => n.endsWith('.' + e)); }; const videoPlaylist = S.display.filter(i => isVideoItem(i)); let curListIdx = videoPlaylist.findIndex(v => v.id === item.id); const totalInList = videoPlaylist.length; let isSwitching = false; let switchReqId = 0; let mediaSessionToken = 0; let activeHealthTimer = null; let activeHlsObjectUrl = null; const isStaleMediaSession = (token) => token !== mediaSessionToken || isPlayerDestroyed; const showSadBox = (codecName, reason = 'codec', opts = {}) => { if (box.querySelector('.pk-err-dialog')) return; if (posterEl) { posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; posterEl.style.pointerEvents = 'auto'; } const savedPlayer = gmGet('pk_ext_player', 'potplayer'); const sadBoxSVG = ``; const isWebUnsupportedHint = reason === 'web_unsupported'; const safeQualityList = Array.isArray(qualityList) ? qualityList : []; const preferredErrUrl = String(opts.url || currentLink || ''); const preferredErrName = String(opts.name || currentResName || codecName || ''); const fallbackQuality = safeQualityList[0] || { name: preferredErrName, link: preferredErrUrl, url: preferredErrUrl }; const recommended = isWebUnsupportedHint ? (safeQualityList.find(q => String(q.link || q.url || '') === preferredErrUrl) || safeQualityList.find(q => String(q.name || '') === preferredErrName) || safeQualityList.find(q => String(q.name || '') === String(currentResName || '')) || fallbackQuality) : (safeQualityList.find(q => !q.isOriginal) || fallbackQuality); const recommendedUrl = preferredErrUrl || recommended.link || recommended.url; let selectedErrUrl = recommendedUrl; let selectedErrName = preferredErrName || recommended.name; let selectedErrPlayer = (savedPlayer === 'other') ? 'other' : 'potplayer'; const resOptions = safeQualityList.map(q => { const qUrl = q.link || q.url; const isSelected = qUrl === selectedErrUrl; return `
${q.name}
`; }).join(''); const lblRes = L.lbl_resolution; const errTitle = isWebUnsupportedHint ? L.err_web_unsupported_t1 : L.err_codec_t1.replace('{c}', codecName); const errDesc = isWebUnsupportedHint ? L.err_web_unsupported_t2 : L.err_codec_t2; const shareExtPreviewTip = getSharePreviewExtLimitTip(item); const dialog = document.createElement('div'); dialog.className = 'pk-err-dialog'; dialog.style.cssText = "position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(30, 30, 30, 0.85);backdrop-filter:blur(12px);border-radius:12px;padding:40px 50px;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.8);z-index:999;min-width:400px;border:1px solid rgba(255,255,255,0.1);"; dialog.innerHTML = `
${sadBoxSVG}
${errTitle}
${errDesc}
${shareExtPreviewTip ? `
${shareExtPreviewTip}
` : ''}
${lblRes}
${selectedErrName}
${CONF.crumbIcons.down}
${resOptions}
${L.lbl_player}
${selectedErrPlayer === 'potplayer' ? 'PotPlayer' : L.opt_player_other}
${CONF.crumbIcons.down}
PotPlayer
${L.opt_player_other}
`; box.appendChild(dialog); const launchBtn = dialog.querySelector('#pk_err_launch_btn'); const fixBtn = dialog.querySelector('#pk_err_potplayer_fix'); const closeErrMenus = () => dialog.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none'); const removeErrDialog = () => { document.removeEventListener('click', closeErrMenus); dialog.remove(); }; const getCleanErrSelectedUrl = () => { let cleanLink = String(selectedErrUrl || '').replace('&ext=.m3u8', ''); if (cleanLink.includes('ts_downloader') && cleanLink.includes('url=')) { try { const urlParam = new URL(cleanLink).searchParams.get('url'); if (urlParam) cleanLink = decodeURIComponent(urlParam); } catch (e) {} } return cleanLink; }; const updateErrPotPlayerFixEntry = () => { if (fixBtn) fixBtn.style.display = selectedErrPlayer === 'potplayer' ? 'inline-flex' : 'none'; }; const bindErrSelect = (id, onSelect) => { const container = dialog.querySelector(`#${id}`); if (!container) return; const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('.pk-select-trigger > span'); if (!trigger || !menu || !txt) return; trigger.onclick = (e) => { e.stopPropagation(); const isOpen = menu.style.display === 'block'; closeErrMenus(); menu.style.display = isOpen ? 'none' : 'block'; }; container.querySelectorAll('.pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); container.querySelectorAll('.pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val, item.textContent); }; }); }; dialog.querySelector('.pk-err-close').onclick = (e) => { e.stopPropagation(); removeErrDialog(); }; dialog.onclick = (e) => e.stopPropagation(); bindErrSelect('pk_err_res_cs', (val, text) => { selectedErrUrl = val; selectedErrName = text; }); bindErrSelect('pk_err_player_cs', (val) => { selectedErrPlayer = val; launchBtn.textContent = (val === 'other') ? L.btn_copy_link : L.btn_start_play; updateErrPotPlayerFixEntry(); }); launchBtn.textContent = (selectedErrPlayer === 'other') ? L.btn_copy_link : L.btn_start_play; setTimeout(() => document.addEventListener('click', closeErrMenus), 0); if (fixBtn) { fixBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openPotPlayerProtocolRepairHelper(getCleanErrSelectedUrl(), { button: launchBtn, source: 'error_fallback', onRetryLikelyOpen: () => { removeErrDialog(); destroyPlayer(); }, onRetryLikelyFail: () => { removeErrDialog(); destroyPlayer(); } }); }; } launchBtn.onclick = (e) => { e.stopPropagation(); const selPlayer = selectedErrPlayer; const selResName = selectedErrName; gmSet('pk_ext_player', selPlayer); const cleanLink = getCleanErrSelectedUrl(); if (selPlayer === 'other') { GM_setClipboard(cleanLink); launchBtn.textContent = L.msg_copy_success; launchBtn.style.background = "#52c41a"; launchBtn.style.color = "#fff"; setTimeout(() => { removeErrDialog(); if (typeof destroyPlayer === 'function') destroyPlayer(); }, 800); } else if (selPlayer === 'potplayer') { launchPotPlayerWithFallback(cleanLink, { button: launchBtn, source: 'error_fallback', restoreText: L.btn_start_play, restoreBg: launchBtn.style.background, restoreColor: launchBtn.style.color, failToastDuration: 6500, failCloseDelay: 3200, onLikelyOpen: () => { removeErrDialog(); destroyPlayer(); }, onLikelyFail: (ctx) => { removeErrDialog(); destroyPlayer(); schedulePotPlayerAutoRepairPrompt(cleanLink, { source: 'error_fallback', autoRepairPrompt: ctx && ctx.autoRepairPrompt, autoRepairPromptDelay: ctx && ctx.autoRepairPromptDelay }); } }); } else { const curT = v.currentTime; currentLink = selResLink; currentResName = selResName; box.classList.add('buffering'); if (posterEl) { if (v.readyState >= 2 && v.currentTime > 0) { try { const cvs = document.createElement('canvas'); cvs.width = v.videoWidth; cvs.height = v.videoHeight; cvs.getContext('2d').drawImage(v, 0, 0); posterEl.querySelector('img').src = cvs.toDataURL('image/jpeg', 0.7); } catch (err) {} } posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; } if(resTxt) resTxt.textContent = currentResName; if(resList) { resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } removeErrDialog(); shutterTargetTime = curT > 0.1 ? curT : 0; loadSource(currentLink, curT); v.play().catch(()=>{}); } }; }; const renderPlaylistItems = () => { const RANGE = 150; const start = Math.max(0, curListIdx - RANGE); const end = Math.min(videoPlaylist.length, curListIdx + RANGE + 1); return videoPlaylist.slice(start, end).map((v, i) => { const absIdx = start + i; const coverSrc = (v.thumbnail_link && v.thumbnail_link !== v.icon_link) ? v.thumbnail_link : ''; const phSrc = v.icon_link || v.thumbnail_link || ''; const phSvg = getIcon(v); return `
${phSrc ? `` : phSvg}
${coverSrc ? `` : ``}
`; }).join(''); }; const triggerResume = (targetVideo, targetItem) => { if (!shouldLoadVideoProgressCache()) return; if (targetVideo._hasShownResumeToast || hasUserSeeked || targetVideo._isRestarting) return; const targetId = getPhysicalId(targetItem); if (targetVideo._pkActiveFileId && targetVideo._pkActiveFileId !== targetId) return; if (targetVideo._pkOfficialResumeFileId && targetVideo._pkOfficialResumeFileId !== targetId) return; const savedT = parseFloat(targetVideo._pkOfficialResumeTime || 0); const skipOp = targetVideo._pkIntroSkipApplied ? (parseInt(gmGet('pk_skip_intro', 0)) || 0) : 0; if (savedT > 5 || skipOp > 0) { targetVideo._hasShownResumeToast = true; } else { return; } const showToast = (text) => { const oldToast = d.querySelector('.pk-p-resume-toast'); if (oldToast) oldToast.remove(); const toast = document.createElement('div'); toast.className = 'pk-p-resume-toast'; toast.style.zIndex = "150"; toast.innerHTML = `${text}${L.btn_restart}
${mkSvg(icons.close)}
`; d.querySelector('#pk_p_box').appendChild(toast); toast.querySelector('#pk_re_start').onclick = (e) => { e.stopPropagation(); if (tCur) tCur.textContent = "00:00:00"; if (progFilled) progFilled.style.setProperty('width', '0%', 'important'); shutterTargetTime = 0.1; targetVideo._isRestarting = true; targetVideo.currentTime = 0; toast.remove(); }; toast.querySelector('#pk_re_close').onclick = (e) => { e.stopPropagation(); toast.remove(); }; setTimeout(() => { if(toast.parentNode) toast.remove(); }, 8000); }; if (savedT > 5) { showToast(L.msg_resume_hint.replace('{t}', fmtT(savedT))); } else if (skipOp > 0) { const dp = d.querySelector('.pk-player-box'); if(dp && !dp.querySelector('.pk-skip-toast')) { const tip = document.createElement('div'); tip.className = 'pk-skip-toast'; tip.style.cssText = "position:absolute;top:80px;left:20px;background:rgba(0,0,0,0.6);color:#ddd;padding:4px 8px;border-radius:4px;font-size:12px;pointer-events:none;animation:pkFadeIn 0.5s;"; tip.textContent = `${L.lbl_skip_op} ${skipOp}s`; dp.appendChild(tip); setTimeout(()=>tip.remove(), 3000); } } }; const softSwitch = async (newIdx, scrollMode = 'smooth') => { if (!videoPlaylist[newIdx]) return; switchReqId++; mediaSessionToken++; const myReqId = switchReqId; isSwitching = true; lastWorkingLink = null; lastWorkingResName = null; failedUrls.clear(); const v = d.querySelector('#pk_video'); const loader = d.querySelector('.pk-p-loading'); const poster = d.querySelector('#pk_p_poster'); if (shouldSyncOfficialPlayHistory(item) && v && v.currentTime > 5 && v.duration > 0 && v.duration - v.currentTime > 5) { if (!officialProgressPostedThisSession && !officialProgressPosting) { officialProgressPosting = true; postOfficialPlayProgress(getPhysicalId(item), v.currentTime, v.duration) .then(handleOfficialPlayProgressWriteResult) .catch(() => {}) .finally(() => { officialProgressPosting = false; }); } } if (activeHealthTimer) { clearInterval(activeHealthTimer); activeHealthTimer = null; } if (pkHls) { try { pkHls.stopLoad(); } catch (e) {} try { pkHls.detachMedia(); } catch (e) {} try { pkHls.destroy(); } catch (e) {} pkHls = null; } if (activeHlsObjectUrl) { try { URL.revokeObjectURL(activeHlsObjectUrl); } catch (e) {} activeHlsObjectUrl = null; } if (v) { v.pause(); v._bufferingSince = null; v._blackScreenCount = 0; v._isRestarting = false; v._hasShownResumeToast = false; v._pkPendingIntroSkip = 0; v._pkIntroSkipApplied = false; Array.from(v.querySelectorAll('track')).forEach(t => t.remove()); if (subState.blobUrl) { URL.revokeObjectURL(subState.blobUrl); subState.blobUrl = null; } subState.hasSub = false; subState.track = null; const subNameLabel = d.querySelector('#pk_sub_name'); if (subNameLabel) subNameLabel.textContent = L.str_no_sub; v.src = ""; v.load(); } if (progFilled) progFilled.style.setProperty('width', '0%', 'important'); sharePreviewSoftLimitTipShown = false; if (sharePreviewLimitMarker) sharePreviewLimitMarker.style.display = 'none'; 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'; box.classList.add('buffering'); hasCheckedProgress = false; const oldResumeToast = d.querySelector('.pk-p-resume-toast'); if (oldResumeToast) oldResumeToast.remove(); const errDialogs = d.querySelectorAll('.pk-err-dialog'); errDialogs.forEach(el => el.remove()); const linkOvs = d.querySelectorAll('.pk-link-export-ov'); linkOvs.forEach(el => el.remove()); const searchModal = d.querySelector('.pk-sub-search-modal'); if (searchModal) searchModal.remove(); const btnSearchS = d.querySelector('#pk_p_search'); if (btnSearchS) btnSearchS.style.setProperty('display', 'none', 'important'); const btnPipS = d.querySelector('#pk_p_pip'); if (btnPipS) btnPipS.style.setProperty('display', 'none', 'important'); curListIdx = newIdx; item = newItem; if (posterEl) { posterEl.style.transition = 'none'; posterEl.style.display = 'flex'; posterEl.style.opacity = '1'; const img = posterEl.querySelector('img'); if (img && newItem.thumbnail_link) { img.style.display = 'block'; img.src = newItem.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE'); img.style.opacity = '0'; img.onload = () => { img.style.opacity = '1'; if (btnSearchS) btnSearchS.style.setProperty('display', 'flex', 'important'); }; img.onerror = () => img.style.display = 'none'; } else if (img) { img.style.display = 'none'; } setTimeout(() => { posterEl.style.transition = 'opacity 0.4s ease'; }, 50); } if (loaderEl) loaderEl.style.display = 'block'; const pTitle = d.querySelector('.pk-player-title'); if(pTitle) pTitle.textContent = `[${newIdx + 1}/${totalInList}] ${newItem.name}`; const pTab = d.querySelector('#pk_p_plist_tab'); if(pTab) pTab.innerHTML = `${curListIdx + 1} / ${totalInList} `; if (pScroll) { syncVideoPlistItems( scrollMode === 'instant' ? 'instant' : (box.classList.contains('plist-active') ? 'smooth' : false) ); } const btnL = d.querySelector('#pk_p_side_L'); const btnR = d.querySelector('#pk_p_side_R'); if (btnL) btnL.style.display = newIdx === 0 ? 'none' : 'flex'; if (btnR) btnR.style.display = newIdx === totalInList - 1 ? 'none' : 'flex'; if (typeof updatePlistNav === 'function') updatePlistNav(); hasUserSeeked = false; try { const targetApiId = getPhysicalId(newItem); const newData = newItem._isShareItem ? await resolveSharePlayableFile(newItem) : 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); if (!isPlayerDestroyed) { await v.play().catch(()=>{}); } if (!v.paused && !isPlayerDestroyed) { box.classList.add('pk-v-started'); stopSpinner(); } updateState(); } catch (err) { await markOfflineReferenceMissingFromError(newItem, err, { source: 'video_switch' }); if (myReqId === switchReqId) console.error("[SoftSwitch] Critical Error:", err); } finally { if (myReqId === switchReqId) { isSwitching = false; if (loader && (v.readyState >= 2 || box.querySelector('.pk-err-dialog'))) loader.style.display = 'none'; } } }; const getLoadedHistoryProgress = (it) => { if (!shouldLoadVideoProgressCache() || !S.historyMode || !it) return null; const targetId = String(getPhysicalId(it) || ''); if (!targetId) return null; const candidates = [it]; const mapItem = S.itemMap && S.itemMap.get(targetId); if (mapItem) candidates.push(mapItem); if (Array.isArray(S.items)) { const itemInList = S.items.find(f => f && String(f.id || '') === targetId); if (itemInList) candidates.push(itemInList); } if (Array.isArray(S.display)) { const itemInDisplay = S.display.find(f => f && !f.isHeader && String(f.id || '') === targetId); if (itemInDisplay) candidates.push(itemInDisplay); } for (const h of candidates) { const t = parseOfficialPlaySeconds(h && h._history_progress); const d = parseOfficialPlaySeconds((h && h._history_duration) || (h && h.params && h.params.duration) || S.durationMap.get(targetId) || gmGet('pk_duration_' + targetId, 0)); if (t > 5 && !(d > 0 && d - t <= 5)) { return { t, d, ts: Number((h && h._history_ts) || 0), source: 'loaded_history', officialEventId: String((h && h._history_event_id) || '') }; } } return null; }; const loadSource = async (url, customStartTime = null) => { sharePreviewSoftLimitTipShown = false; if (sharePreviewLimitMarker) sharePreviewLimitMarker.style.display = 'none'; let startTime = 0; const skipOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; const pId = getPhysicalId(item); let shouldPendingIntroSkip = false; v._pkActiveFileId = pId; v._pkOfficialResumeTime = 0; v._pkOfficialResumeFileId = ''; v._hasShownResumeToast = false; const oldResumeToast = d.querySelector('.pk-p-resume-toast'); if (oldResumeToast) oldResumeToast.remove(); let loadedHistoryProgress = null; if (customStartTime !== null) { startTime = customStartTime; } else { loadedHistoryProgress = getLoadedHistoryProgress(item); if (loadedHistoryProgress) { startTime = loadedHistoryProgress.t; v._pkOfficialResumeTime = startTime; v._pkOfficialResumeFileId = pId; } else if (skipOp > 0) { const knownDur = (item.params && item.params.duration) || S.durationMap.get(pId) || gmGet('pk_duration_' + pId, 0); if (knownDur > 0 && skipOp >= knownDur) { console.warn(`[AutoSkip] Intro skip (${skipOp}s) exceeds total duration (${knownDur}s), ignored.`); } else { shouldPendingIntroSkip = true; } } } v._pkPendingIntroSkip = shouldPendingIntroSkip ? skipOp : 0; v._pkIntroSkipApplied = false; shutterTargetTime = startTime > 1 ? startTime : 0; if (tCur) tCur.textContent = fmtT(startTime); const totalDur = (item.params && item.params.duration) || S.durationMap.get(pId) || gmGet('pk_duration_' + pId, 0); if (progFilled && totalDur > 0) { const pct = Math.min(100, (startTime / totalDur) * 100); progFilled.style.setProperty('width', `${pct}%`, 'important'); } const currentMediaToken = ++mediaSessionToken; if (customStartTime === null && !loadedHistoryProgress && shouldSyncOfficialPlayHistory(item) && shouldLoadVideoProgressCache()) { fetchOfficialPlayHistoryProgress(pId).then((officialProgress) => { if (!officialProgress || isStaleMediaSession(currentMediaToken) || hasUserSeeked || v._isRestarting) return; if (v._pkActiveFileId !== pId || getPhysicalId(item) !== pId) return; const officialT = parseFloat(officialProgress.t || 0); const officialD = parseFloat(officialProgress.d || 0); if (!(officialT > 5)) return; if (officialD > 0 && officialD - officialT <= 5) return; const currentT = parseFloat(v.currentTime || 0); if (currentT > 5 && Math.abs(currentT - startTime) > 5) return; const applyOfficialProgress = () => { if (isStaleMediaSession(currentMediaToken) || hasUserSeeked || v._isRestarting) return; if (v._pkActiveFileId !== pId || getPhysicalId(item) !== pId) return; shutterTargetTime = officialT > 1 ? officialT : 0; try { v.currentTime = officialT; } catch (e) { console.warn('[OfficialProgress] Seek failed:', e); } if (tCur) tCur.textContent = fmtT(officialT); const knownDur = officialD || (item.params && item.params.duration) || S.durationMap.get(pId) || gmGet('pk_duration_' + pId, 0); if (progFilled && knownDur > 0) { const pct = Math.min(100, (officialT / knownDur) * 100); progFilled.style.setProperty('width', `${pct}%`, 'important'); } v._pkOfficialResumeTime = officialT; v._pkOfficialResumeFileId = pId; v._hasShownResumeToast = false; triggerResume(v, item); }; if (v.readyState >= 1) { applyOfficialProgress(); } else { v.addEventListener('loadedmetadata', applyOfficialProgress, { once: true }); } }).catch((e) => { console.warn('[OfficialProgress] Apply failed:', e); }); } if (activeHealthTimer) { clearInterval(activeHealthTimer); activeHealthTimer = null; } if (pkHls) { try { pkHls.stopLoad(); } catch (e) {} try { pkHls.detachMedia(); } catch (e) {} try { pkHls.destroy(); } catch (e) {} pkHls = null; } if (activeHlsObjectUrl) { try { URL.revokeObjectURL(activeHlsObjectUrl); } catch (e) {} activeHlsObjectUrl = null; } v._pkMediaToken = currentMediaToken; v._bufferingSince = null; v._blackScreenCount = 0; box.classList.add('buffering'); const isPikPakTsHls = isPikPakTsDownloaderUrl(url); const isM3u8 = url.includes('.m3u8') || isPikPakTsHls; const currentQualityItem = (Array.isArray(qualityList) ? qualityList : []).find(q => { const qUrl = String(q && (q.link || q.url) || ''); return qUrl && (qUrl === url || qUrl === currentLink || String(q.name || '') === String(currentResName || '')); }) || null; if (currentQualityItem && shouldBlockWebPlaybackByOfficialSniff(currentQualityItem)) { console.warn('[VideoCheck] Web playback blocked by official-like codec sniff.', { name: currentQualityItem.name || currentResName, codecText: getPikPakQualityCodecTextForSniff(currentQualityItem), hevcWebSupport: hasLikelyHevcWebPlaybackSupport() }); box.classList.remove('buffering'); const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'none'; showSadBox(currentQualityItem.codecText || 'HEVC', 'web_unsupported', { url: String((currentQualityItem.link || currentQualityItem.url) || url || currentLink || ''), name: currentQualityItem.name || currentResName }); return; } const HlsCtor = isPikPakTsHls ? (getPikPakOfficialHlsCtor() || window.Hls) : window.Hls; const usingOfficialPikPakHls = isPikPakTsHls && HlsCtor && HlsCtor !== window.Hls; const HlsEvents = (HlsCtor && HlsCtor.Events) || (window.Hls && window.Hls.Events) || {}; const HlsErrorTypes = (HlsCtor && HlsCtor.ErrorTypes) || (window.Hls && window.Hls.ErrorTypes) || {}; if (isM3u8 && HlsCtor && HlsCtor.isSupported && HlsCtor.isSupported()) { let finalPlayUrl = url; if (url.includes('&ext=.m3u8') || url.includes('?ext=.m3u8')) { finalPlayUrl = buildPikPakTsHlsUrl(url); } let isOfficialPikPakTsHls = isPikPakTsDownloaderUrl(finalPlayUrl); let pikPakMasterTargetName = ''; let pikPakMasterTargetUrl = ''; const hlsLoadStartedAt = Date.now(); if (isOfficialPikPakTsHls) { const targetItem = (Array.isArray(qualityList) ? qualityList : []).find(q => { const qUrl = String(q.link || q.url || ''); return qUrl && (qUrl === finalPlayUrl || q.name === currentResName); }); pikPakMasterTargetName = (targetItem && targetItem.name) || currentResName || ''; pikPakMasterTargetUrl = (targetItem && (targetItem.link || targetItem.url)) || finalPlayUrl; const masterM3u8 = buildPikPakMasterM3u8FromQualityList(qualityList); if (masterM3u8) { const blob = new Blob([masterM3u8], { type: 'audio/mpegurl' }); finalPlayUrl = URL.createObjectURL(blob); activeHlsObjectUrl = finalPlayUrl; } else { try { const manifestRes = await fetch(finalPlayUrl, { method: 'GET', cache: 'no-store', credentials: 'omit' }); if (isStaleMediaSession(currentMediaToken)) return; if (manifestRes && manifestRes.ok) { const rawManifest = await manifestRes.text(); const fixedManifest = normalizePikPakTsM3u8(rawManifest); if (fixedManifest.includes('#EXTM3U')) { const blob = new Blob([fixedManifest], { type: 'application/vnd.apple.mpegurl' }); finalPlayUrl = URL.createObjectURL(blob); activeHlsObjectUrl = finalPlayUrl; } } else { console.warn("[Hls] Failed to prefetch PikPak ts_downloader manifest, fallback to direct HLS URL:", manifestRes && manifestRes.status); } } catch (e) { console.warn("[Hls] Prefetch PikPak ts_downloader manifest failed, fallback to direct HLS URL:", e); } } } const hlsOptions = usingOfficialPikPakHls ? { debug: false, maxBufferHole: 0.5, maxFragLookUpTolerance: 0.2, enableWorker: true, progressive: true, abrBandWidthFactor: 0, abrBandWidthUpFactor: 0, maxMaxBufferLength: 30, maxBufferLength: 10, maxBufferSize: 60 * 1024 * 1024, fragLoadPolicy: { default: { maxTimeToFirstByteMs: 10000, maxLoadTimeMs: 15000, timeoutRetry: { maxNumRetry: 6, retryDelayMs: 1000, maxRetryDelayMs: 0 }, errorRetry: { maxNumRetry: 2, retryDelayMs: 1000, maxRetryDelayMs: 8000 } } }, lowLatencyMode: false, backBufferLength: 30, forceKeyFrameOnDiscontinuity: true, startPosition: startTime, startLevel: -1, capLevelToPlayerSize: false } : { debug: false, enableWorker: true, lowLatencyMode: false, startPosition: startTime, fragLoadingMaxRetry: 3, manifestLoadingMaxRetry: 3, levelLoadingMaxRetry: 3, fragLoadingRetryDelay: 800, manifestLoadingRetryDelay: 800, levelLoadingRetryDelay: 800, fragLoadingTimeOut: 20000, manifestLoadingTimeOut: 15000, maxBufferHole: 0.8, maxFragLookUpTolerance: 0.25, nudgeOffset: 0.1, nudgeMaxRetry: 8, maxBufferLength: 120, maxMaxBufferLength: 600, backBufferLength: 90, startLevel: -1, capLevelToPlayerSize: false }; pkHls = new HlsCtor(hlsOptions); if (HlsEvents.MANIFEST_PARSED) { pkHls.on(HlsEvents.MANIFEST_PARSED, function () { if (isStaleMediaSession(currentMediaToken)) return; if (isOfficialPikPakTsHls && pikPakMasterTargetName) { applyPikPakPreferredHlsLevel(pkHls, pikPakMasterTargetName, pikPakMasterTargetUrl); } }); } pkHls.loadSource(finalPlayUrl); pkHls.attachMedia(v); const healthTimer = setInterval(() => { if (isStaleMediaSession(currentMediaToken) || !pkHls) { clearInterval(healthTimer); if (activeHealthTimer === healthTimer) activeHealthTimer = null; return; } if (box.classList.contains('buffering')) { if (!v._bufferingSince) v._bufferingSince = Date.now(); const isColdStart = v.currentTime < 1.0; const timeoutThreshold = isColdStart ? 30000 : 20000; if (Date.now() - v._bufferingSince > timeoutThreshold) { console.warn(`[Watchdog] Buffering timeout (${isColdStart ? 'Cold Start' : 'Mid-Stream'}). Forcing error...`); clearInterval(healthTimer); if (activeHealthTimer === healthTimer) activeHealthTimer = null; handleVideoError({ force: true, target: v, mediaToken: currentMediaToken }); } } else { v._bufferingSince = null; } if (!v.paused && v.currentTime > 0.5) { if (v.videoWidth === 0 || v.videoHeight === 0) { v._blackScreenCount = (v._blackScreenCount || 0) + 1; } else { const quality = v.getVideoPlaybackQuality ? v.getVideoPlaybackQuality() : null; const decodedFrames = quality ? quality.totalVideoFrames : (v.webkitDecodedFrameCount || 0); if (decodedFrames === 0) { v._blackScreenCount = (v._blackScreenCount || 0) + 1; } else { v._blackScreenCount = 0; } } const blackScreenLimit = isOfficialPikPakTsHls ? 8 : 3; if (v._blackScreenCount > blackScreenLimit) { const quality = v.getVideoPlaybackQuality ? v.getVideoPlaybackQuality() : null; const decodedFrames = quality ? quality.totalVideoFrames : (v.webkitDecodedFrameCount || 0); console.warn(`[Watchdog] Black screen detected.`, { time: v.currentTime, readyState: v.readyState, videoWidth: v.videoWidth, videoHeight: v.videoHeight, decodedFrames, isPikPakTsHls: isOfficialPikPakTsHls, hevcCodec: v._pkHevcTsCodec || '' }); clearInterval(healthTimer); if (activeHealthTimer === healthTimer) activeHealthTimer = null; box.classList.remove('buffering'); const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'none'; showSadBox(currentResName || 'HEVC', 'web_unsupported', { url: String(currentLink || ''), name: currentResName || '' }); } } }, 1000); activeHealthTimer = healthTimer; pkHls.on(HlsEvents.LEVEL_LOADED, function (event, data) { if (isStaleMediaSession(currentMediaToken)) return; const details = data && data.details ? data.details : null; const vCodec = String((details && details.videoCodec) || '').toLowerCase(); if (!vCodec) return; const isHevcVideo = isLikelyHevcCodec(vCodec); const isMp4vVideo = vCodec.includes('mp4v'); if (isOfficialPikPakTsHls && isHevcVideo) { v._pkHevcTsSource = true; v._pkHevcTsCodec = vCodec; v._blackScreenCount = 0; return; } if (isMp4vVideo) { const testMime = `video/mp4; codecs="${vCodec}"`; if (window.MediaSource && !window.MediaSource.isTypeSupported(testMime)) { console.warn(`[VideoCheck] Unsupported legacy video codec detected: ${vCodec}`); v.pause(); showSadBox(vCodec); if (pkHls) { pkHls.destroy(); pkHls = null; } } } }); pkHls.on(window.Hls.Events.AUDIO_TRACKS_UPDATED, function (event, data) { if (isStaleMediaSession(currentMediaToken)) return; const tracks = data.audioTracks || []; let detectedBadCodec = null; for (const track of tracks) { const codec = (track.codec || '').toLowerCase(); const name = (track.name || '').toLowerCase(); const isSuspicious = codec.includes('ac-3') || codec.includes('ec-3') || codec.includes('dts') || name.includes('dts') || name.includes('truehd') || name.includes('atmos'); if (isSuspicious) { const testMime = `audio/mp4; codecs="${track.codec || 'ac-3'}"`; if (window.MediaSource && !window.MediaSource.isTypeSupported(testMime)) { detectedBadCodec = codec || name; break; } } } if (detectedBadCodec) { console.warn(`[AudioCheck] Unsupported codec detected: ${detectedBadCodec}`); v.pause(); showSadBox(detectedBadCodec.toUpperCase()); } }); const hasHlsPlaybackEvidence = () => { try { const hasBuffer = v.buffered && v.buffered.length > 0 && v.buffered.end(v.buffered.length - 1) > 0; return v.readyState >= 2 || v.currentTime > 0.2 || hasBuffer || (v.videoWidth > 0 && v.videoHeight > 0); } catch (e) { return v.readyState >= 2 || v.currentTime > 0.2 || (v.videoWidth > 0 && v.videoHeight > 0); } }; pkHls.on(HlsEvents.ERROR, function (event, data) { if (isStaleMediaSession(currentMediaToken)) return; const elapsed = Date.now() - hlsLoadStartedAt; const isColdStart = v.currentTime < 1 && !hasHlsPlaybackEvidence(); if (data.fatal) { switch (data.type) { case HlsErrorTypes.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({ force: true, target: v, mediaToken: currentMediaToken }); } else { try { pkHls.startLoad(); } catch (e) {} } break; case HlsErrorTypes.MEDIA_ERROR: console.warn("[Hls] Media error, trying recoverMediaError first.", data.details || data); try { pkHls.recoverMediaError(); } catch (e) {} setTimeout(() => { if (isStaleMediaSession(currentMediaToken)) return; if (!pkHls || box.querySelector('.pk-err-dialog')) return; if (hasHlsPlaybackEvidence()) return; if (isOfficialPikPakTsHls && Date.now() - hlsLoadStartedAt < 12000) { try { pkHls.startLoad(); } catch (e) {} return; } handleVideoError({ force: true, target: v, mediaToken: currentMediaToken }); }, isOfficialPikPakTsHls ? 8000 : 2000); break; default: if (isOfficialPikPakTsHls && isColdStart && elapsed < 15000) { console.warn("[Hls] Fatal error during PikPak TS cold start, delaying rollback once.", data.details || data); try { pkHls.startLoad(); } catch (e) {} return; } handleVideoError({ force: true, target: v, mediaToken: currentMediaToken }); break; } } else if (data.details === 'bufferStalledError') { if (isOfficialPikPakTsHls && isColdStart && elapsed < 20000) { console.warn("[Hls] Ignored cold-start bufferStalledError for PikPak TS source."); try { pkHls.startLoad(); } catch (e) {} return; } if (!isOfficialPikPakTsHls && v.currentTime < 1 && elapsed > 15000) { handleVideoError({ force: true, target: v, mediaToken: currentMediaToken }); } } }); } else { if (isStaleMediaSession(currentMediaToken)) return; const directUrl = String(url || ''); const directItem = (Array.isArray(qualityList) ? qualityList : []).find(q => { const qUrl = String(q && (q.link || q.url) || ''); return qUrl && qUrl === directUrl; }); const isOriginalDirectPlayback = !!(directItem && directItem.isOriginal) || String(currentResName || '') === String(L.str_original); if (isOriginalDirectPlayback) { try { v.pause(); } catch (e) {} try { v.removeAttribute('src'); v.load(); } catch (e) {} v.preload = 'auto'; v.src = directUrl; try { v.load(); } catch (e) {} } else { v.src = directUrl; } if (startTime > 0) { const applyDirectStartTime = () => { if (isStaleMediaSession(currentMediaToken)) return; try { v.currentTime = startTime; } catch (e) {} }; if (v.readyState >= 1) { applyDirectStartTime(); } else { v.addEventListener('loadedmetadata', applyDirectStartTime, { once: true }); } } } }; let initialData = getBestSource(item); let currentLink = initialData.src; let qualityList = initialData.list; let currentResName = initialData.name; let lastWorkingLink = null; let lastWorkingResName = null; const failedUrls = new Set(); let posterUrl = item.thumbnail_link || ''; if (posterUrl && posterUrl.includes('SIZE_MEDIUM')) { posterUrl = posterUrl.replace('SIZE_MEDIUM', 'SIZE_LARGE'); } try { const linkUrl = new URL(currentLink); const linkDomain = linkUrl.origin; if (!document.head.querySelector(`link[href="${linkDomain}"]`)) { const pc = document.createElement('link'); pc.rel = 'preconnect'; pc.href = linkDomain; pc.crossOrigin = 'anonymous'; document.head.appendChild(pc); } } catch(e) {} const d = document.createElement('div'); d.id = 'pk-player-ov'; d.className = 'pk-ov pk-dark'; d.tabIndex = 0; d.style.cssText = "position:fixed;inset:0;z-index:2147483640;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;outline:none;will-change:transform;"; const icons = { play: '', playCenter: '', pause: '', vol: '', mute: '', full: '', webFull: '', webExitFull: '', settings: '', exitFull: '', close: '', search: '', pip: '', rotL: '', rotR: '', flipH: '', flipV: '' }; const mkSvg = (path) => `${path}`; const renderQualityMenu = (list, activeName) => list.map(q => { const displayName = q.name.replace(/\s(\(|\uff08)/, '
$1'); return `
${displayName}
`; }).join(''); d.innerHTML = `
00:00
${posterUrl ? `` : ''}
${mkSvg(icons.playCenter)}
${mkSvg(icons.vol)}
100%
00:00 / 00:00
${mkSvg('')}
${mkSvg('')}
[${curListIdx + 1}/${totalInList}] ${esc(item.name)}
${mkSvg(icons.pip)}
${mkSvg(icons.close)}
${curListIdx + 1} / ${totalInList}
${mkSvg('')}
${renderPlaylistItems()}
${mkSvg('')}
00:00:00
00:00:00
${mkSvg(icons.pause)}
${mkSvg(icons.vol)}
${currentResName}
${renderQualityMenu(qualityList, currentResName)}
1.0x
3.0x
2.0x
1.5x
1.25x
1.0x
0.75x
0.5x
${mkSvg(icons.settings)}
${L.tab_sub}
${L.tab_size}
${L.tab_more}
${L.lbl_sub_sel}
${L.str_no_sub}
${L.btn_sub_search}
${L.btn_sub_cloud}
${L.btn_sub_local}
${L.lbl_sub_pos}
${L.lbl_sub_bottom} ${L.lbl_sub_top}
${L.lbl_sub_bg_op}
0% 100%
${L.lbl_sub_size}
20
${L.lbl_sub_offset}
0.0 ${L.unit_sec}
${L.lbl_ratio}
${L.opt_ratio_def}
16:9
4:3
${L.lbl_direction}
${mkSvg(icons.rotL)} ${L.btn_rot_l}
${mkSvg(icons.rotR)} ${L.btn_rot_r}
${mkSvg(icons.flipH)} ${L.btn_flip_h}
${mkSvg(icons.flipV)} ${L.btn_flip_v}
${L.lbl_play_end}
${L.lbl_skip_op}
${L.btn_mark}
0 ${L.unit_sec}
${L.lbl_skip_ed}
${L.btn_mark}
0 ${L.unit_sec}
${icons.webFull}
${mkSvg(icons.full)}
`; const v = d.querySelector('#pk_video'); const box = d.querySelector('#pk_p_box'); const btnPlay = d.querySelector('#pk_p_play'); const btnVol = d.querySelector('#pk_p_vol'); const slideVol = d.querySelector('#pk_p_vol_slide'); const btnWebFull = d.querySelector('#pk_p_web_full'); 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 sharePreviewLimitMarker = d.querySelector('#pk_share_preview_limit_marker'); 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); } const updateSharePreviewSoftLimitMarker = () => { if (!sharePreviewLimitMarker) return null; const dur = Number((v && v.duration) || (item && item.params && item.params.duration) || 0); const limit = getSharePreviewLimitParams(item, dur); if (!limit) { sharePreviewLimitMarker.style.display = 'none'; return null; } sharePreviewLimitMarker.style.display = 'block'; sharePreviewLimitMarker.style.left = `${Math.max(0, Math.min(100, limit.ratio * 100))}%`; return limit; }; const maybeToastSharePreviewSoftLimit = () => { if (sharePreviewSoftLimitTipShown || !v) return; const limit = updateSharePreviewSoftLimitMarker(); if (!limit) return; if (Number(v.currentTime || 0) >= Math.max(0, limit.time - 0.25)) { sharePreviewSoftLimitTipShown = true; showToast(getSharePreviewSoftLimitTip(), 'warning', 7000); } }; 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'); } let webFullScaleRaf = 0; let webFullScaleDelayTimer = 0; let webFullScaleWatchBound = false; const applyWebFullscreenScale = () => { if (!d || !d.classList.contains('pk-web-fullscreen-ov')) return; d.style.removeProperty('transform-origin'); d.style.removeProperty('transform'); }; const scheduleWebFullscreenScale = (withDelay = false) => { if (!d || !d.classList.contains('pk-web-fullscreen-ov')) return; if (!webFullScaleRaf) { webFullScaleRaf = requestAnimationFrame(() => { webFullScaleRaf = 0; applyWebFullscreenScale(); }); } if (withDelay) { if (webFullScaleDelayTimer) clearTimeout(webFullScaleDelayTimer); webFullScaleDelayTimer = setTimeout(() => { webFullScaleDelayTimer = 0; scheduleWebFullscreenScale(false); }, 120); } }; const onWebFullscreenViewportChange = () => { scheduleWebFullscreenScale(true); }; const bindWebFullscreenScaleWatchers = () => { if (webFullScaleWatchBound) return; webFullScaleWatchBound = true; window.addEventListener('resize', onWebFullscreenViewportChange); window.addEventListener('orientationchange', onWebFullscreenViewportChange); if (window.visualViewport) { window.visualViewport.addEventListener('resize', onWebFullscreenViewportChange); } }; const unbindWebFullscreenScaleWatchers = () => { if (!webFullScaleWatchBound) return; webFullScaleWatchBound = false; window.removeEventListener('resize', onWebFullscreenViewportChange); window.removeEventListener('orientationchange', onWebFullscreenViewportChange); if (window.visualViewport) { window.visualViewport.removeEventListener('resize', onWebFullscreenViewportChange); } if (webFullScaleRaf) { cancelAnimationFrame(webFullScaleRaf); webFullScaleRaf = 0; } if (webFullScaleDelayTimer) { clearTimeout(webFullScaleDelayTimer); webFullScaleDelayTimer = 0; } }; const setWebFullscreen = (on) => { if (!box || !btnWebFull) return; const enabled = !!on; if (enabled) { if (d._pkWebFullOldTransform === undefined) { d._pkWebFullOldTransform = d.style.getPropertyValue('transform') || ''; d._pkWebFullOldTransformPriority = d.style.getPropertyPriority('transform') || ''; d._pkWebFullOldTransformOrigin = d.style.getPropertyValue('transform-origin') || ''; d._pkWebFullOldTransformOriginPriority = d.style.getPropertyPriority('transform-origin') || ''; } d.classList.add('pk-web-fullscreen-ov'); bindWebFullscreenScaleWatchers(); applyWebFullscreenScale(); scheduleWebFullscreenScale(true); document.documentElement.classList.add('pk-player-web-fullscreen-lock'); document.body.classList.add('pk-player-web-fullscreen-lock'); } else { unbindWebFullscreenScaleWatchers(); d.classList.remove('pk-web-fullscreen-ov'); if (d._pkWebFullOldTransform !== undefined) { if (d._pkWebFullOldTransform) d.style.setProperty('transform', d._pkWebFullOldTransform, d._pkWebFullOldTransformPriority || ''); else d.style.removeProperty('transform'); if (d._pkWebFullOldTransformOrigin) d.style.setProperty('transform-origin', d._pkWebFullOldTransformOrigin, d._pkWebFullOldTransformOriginPriority || ''); else d.style.removeProperty('transform-origin'); delete d._pkWebFullOldTransform; delete d._pkWebFullOldTransformPriority; delete d._pkWebFullOldTransformOrigin; delete d._pkWebFullOldTransformOriginPriority; } document.documentElement.classList.remove('pk-player-web-fullscreen-lock'); document.body.classList.remove('pk-player-web-fullscreen-lock'); } box.classList.toggle('pk-web-fullscreen', enabled); btnWebFull.classList.toggle('active', enabled); btnWebFull.innerHTML = enabled ? icons.webExitFull : icons.webFull; btnWebFull.setAttribute( 'data-pk-tip', enabled ? (L.tip_exit_web_fullscreen) : (L.tip_web_fullscreen) ); requestAnimationFrame(() => { try { window.dispatchEvent(new Event('resize')); } catch (e) {} }); }; if (btnWebFull) { btnWebFull.onclick = (e) => { e.preventDefault(); e.stopPropagation(); setWebFullscreen(!box.classList.contains('pk-web-fullscreen')); }; } pTab.onclick = (e) => { e.stopPropagation(); const nextOpen = !(plist.classList.contains('open') || box.classList.contains('plist-active')); plist.classList.toggle('open', nextOpen); box.classList.toggle('plist-active', nextOpen); if (!nextOpen) box.classList.remove('pk-tab-hover'); pTab.setAttribute('data-pk-tip', nextOpen ? L.tip_plist_close : L.tip_plist_open); if (nextOpen) { const activeItem = pScroll.querySelector('.active'); centerVideoPlistItem(activeItem, 'instant'); } }; const centerVideoPlistItem = (activeItem, scrollMode = 'smooth') => { if (!activeItem || !pScroll) return; const maxLeft = Math.max(0, pScroll.scrollWidth - pScroll.clientWidth); const targetLeft = Math.max(0, Math.min(maxLeft, activeItem.offsetLeft - (pScroll.clientWidth / 2) + (activeItem.clientWidth / 2))); if (scrollMode === 'instant') { pScroll.style.scrollBehavior = 'auto'; pScroll.scrollLeft = targetLeft; setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); return; } try { pScroll.scrollTo({ left: targetLeft, behavior: scrollMode === 'smooth' ? 'smooth' : 'auto' }); } catch (e) { pScroll.scrollLeft = targetLeft; } }; const bindVideoPlistItems = () => { pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onmouseenter = (e) => { if (!plist.classList.contains('open')) return; pTip.innerHTML = `${e.currentTarget.dataset.name}
${e.currentTarget.dataset.size}`; pTip.style.display = 'block'; }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => pTip.style.display = 'none'; el.onclick = (e) => { e.stopPropagation(); if (pTip) pTip.style.display = 'none'; const idx = parseInt(e.currentTarget.dataset.idx, 10); if (idx === curListIdx) return; softSwitch(idx); }; }); }; const syncVideoPlistItems = (scrollMode = 'smooth') => { const RANGE = 150; const desiredStart = Math.max(0, curListIdx - RANGE); const desiredEnd = Math.min(videoPlaylist.length, curListIdx + RANGE + 1); const prevStart = parseInt(pScroll.dataset.pkStart || '-1', 10); const prevEnd = parseInt(pScroll.dataset.pkEnd || '-1', 10); const needRebuild = !Number.isFinite(prevStart) || !Number.isFinite(prevEnd) || curListIdx < prevStart || curListIdx >= prevEnd || pScroll.childElementCount === 0 || pScroll.dataset.pkTotal !== String(videoPlaylist.length); if (needRebuild) { pScroll.innerHTML = renderPlaylistItems(); pScroll.dataset.pkStart = String(desiredStart); pScroll.dataset.pkEnd = String(desiredEnd); pScroll.dataset.pkTotal = String(videoPlaylist.length); bindVideoPlistItems(); } pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { const idx = parseInt(el.dataset.idx, 10); el.classList.toggle('active', idx === curListIdx); }); const activeItem = pScroll.querySelector('.pk-p-plist-item.active'); if (activeItem && plist.classList.contains('open') && scrollMode !== false) { centerVideoPlistItem(activeItem, scrollMode === 'instant' ? 'instant' : 'smooth'); } if (typeof updatePlistNav === 'function') updatePlistNav(); }; bindVideoPlistItems(); const updatePlistNav = () => { const sl = pScroll.scrollLeft; const sw = pScroll.scrollWidth; const cw = pScroll.clientWidth; d.querySelector('#pk_p_plist_L').style.display = sl <= 5 ? 'none' : 'flex'; d.querySelector('#pk_p_plist_R').style.display = (sl + cw >= sw - 5) ? 'none' : 'flex'; }; d.querySelector('#pk_p_plist_L').onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: -400, behavior: 'smooth' }); setTimeout(updatePlistNav, 300); }; d.querySelector('#pk_p_plist_R').onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: 400, behavior: 'smooth' }); setTimeout(updatePlistNav, 300); }; pScroll.addEventListener('scroll', updatePlistNav, { passive: true }); setTimeout(() => syncVideoPlistItems(false), 50); document.body.appendChild(d); const initL = d.querySelector('#pk_p_side_L'); const initR = d.querySelector('#pk_p_side_R'); if (initL) initL.style.display = curListIdx === 0 ? 'none' : 'flex'; if (initR) initR.style.display = curListIdx === totalInList - 1 ? 'none' : 'flex'; let hideTimer = null; let isMouseOverUI = false; const resetHideTimer = () => { box.classList.remove('ui-hidden'); if (hideTimer) clearTimeout(hideTimer); if (isMouseOverUI) return; hideTimer = setTimeout(() => { if (!v.paused) box.classList.add('ui-hidden'); }, 3000); }; const protectedUIs = [ d.querySelector('.pk-player-top'), d.querySelector('.pk-player-controls'), d.querySelector('.pk-p-prog-wrap') ]; protectedUIs.forEach(ui => { if (!ui) return; ui.addEventListener('mouseenter', () => { isMouseOverUI = true; if (hideTimer) clearTimeout(hideTimer); }); ui.addEventListener('mouseleave', () => { isMouseOverUI = false; resetHideTimer(); }); }); box.addEventListener('mouseleave', () => { if (!isMouseOverUI) box.classList.add('ui-hidden'); }); box.addEventListener('mouseenter', resetHideTimer); box.addEventListener('mousemove', resetHideTimer); box.addEventListener('click', resetHideTimer); box.addEventListener('keydown', resetHideTimer); resetHideTimer(); const handleVideoError = (e = {}) => { const eventToken = e.mediaToken || (e.target && e.target._pkMediaToken) || v._pkMediaToken || 0; if (eventToken && eventToken !== mediaSessionToken) return; if (isSwitching && !eventToken) return; if (!v.getAttribute('src') && !pkHls) return; if (v.networkState === 2 && !v.error && !e.force && !pkHls) 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'}`); const retryUrl = String(currentLink || v.src || ''); const retryItem = (Array.isArray(qualityList) ? qualityList : []).find(q => { const qUrl = String(q && (q.link || q.url) || ''); return qUrl && qUrl === retryUrl; }); const isOriginalDirectRetry = errCode === 4 && !e.force && !pkHls && retryUrl && !retryUrl.includes('.m3u8') && v._pkOriginalDirectRetryUrl !== retryUrl && (!!(retryItem && retryItem.isOriginal) || String(currentResName || '') === String(L.str_original)); if (isOriginalDirectRetry) { v._pkOriginalDirectRetryUrl = retryUrl; const retryToken = eventToken || v._pkMediaToken || mediaSessionToken; const savedTime = parseFloat(v.currentTime || 0) || 0; setTimeout(() => { if (isStaleMediaSession(retryToken) || currentLink !== retryUrl) return; loadSource(retryUrl, savedTime); v.play().catch(()=>{}); }, 1200); return; } if (currentLink) failedUrls.add(currentLink); if (lastWorkingLink && lastWorkingLink !== currentLink && !failedUrls.has(lastWorkingLink)) { 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) { 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) { return; } const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'none'; const sharePreviewTip = getSharePreviewLimitTip(item); if (sharePreviewTip && !sharePreviewLimitTipShown) { sharePreviewLimitTipShown = true; showToast(sharePreviewTip, 'warning', 7000); } showSadBox(currentResName); }; v.addEventListener('error', (evt) => handleVideoError({ nativeEvent: evt, target: v, mediaToken: v._pkMediaToken })); v.addEventListener('loadstart', () => { const errOv = box.querySelector('.pk-err-ov'); if (errOv) errOv.remove(); const loader = d.querySelector('.pk-p-loading'); if (loader) loader.style.display = 'block'; }); (async () => { try { const initReqId = switchReqId; const initItem = item; const targetApiId = getPhysicalId(initItem); const newData = initItem._isShareItem ? await resolveSharePlayableFile(initItem) : await apiGet(targetApiId); if (isPlayerDestroyed || initReqId !== switchReqId || item !== initItem) return; const freshData = getBestSource(newData); qualityList = freshData.list; resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); const shouldSwitchToFreshOriginal = currentResName === L.str_original && freshData.name === L.str_original && freshData.src && freshData.src !== currentLink; if (v.error || !currentLink || shouldSwitchToFreshOriginal || (currentResName === L.str_original && freshData.name !== L.str_original)) { const savedTime = parseFloat(v.currentTime || 0); const shouldReloadOfficialProgress = shouldSwitchToFreshOriginal && savedTime <= 1 && !hasUserSeeked && shouldLoadVideoProgressCache(); currentLink = freshData.src; currentResName = freshData.name; resTxt.textContent = currentResName; loadSource(currentLink, shouldReloadOfficialProgress ? null : savedTime); v.play().catch(()=>{}); resList.innerHTML = renderQualityMenu(qualityList, currentResName); bindResEvents(); } } catch (e) { await markOfflineReferenceMissingFromError(initItem, e, { source: 'video_detail' }); } })(); const fmtT = (s) => { s = Math.max(0, s || 0); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sc = Math.floor(s % 60); return String(h).padStart(2, '0') + ":" + String(m).padStart(2, '0') + ":" + String(sc).padStart(2, '0'); }; const fmtFullT = fmtT; const togglePlay = () => { if (v.paused) v.play(); else v.pause(); }; const ThumbnailEngine = (() => { let shadowV = null; let canvas = null; let ctx = null; let isInit = false; let cacheStore = null; let currentReqId = 0; const BASE_HEIGHT = 180; const DISPLAY_HEIGHT = 120; const previewBox = d.querySelector('#pk_p_preview'); const imgBox = d.querySelector('#pk_p_img_box'); const previewTime = previewBox.querySelector('.pk-prev-time'); const init = async () => { if (isInit) return; if (window.localforage) { cacheStore = window.localforage.createInstance({ name: 'pk_thumbs', storeName: 'snapshots' }); } shadowV = document.createElement('video'); shadowV.muted = true; shadowV.crossOrigin = 'anonymous'; shadowV.style.display = 'none'; shadowV.preload = 'auto'; shadowV.src = currentLink; canvas = document.createElement('canvas'); canvas.width = 160; canvas.height = 90; ctx = canvas.getContext('2d'); shadowV.onerror = () => console.warn("[Thumb] Shadow player error"); isInit = true; }; const getCacheKey = (time) => `${getPhysicalId(item)}_${Math.floor(time)}`; const generate = async (time) => { if (!isInit) await init(); if (cacheStore) { const cachedBlob = await cacheStore.getItem(getCacheKey(time)); if (cachedBlob) return URL.createObjectURL(cachedBlob); } return new Promise((resolve, reject) => { const seekHandler = () => { try { const vw = shadowV.videoWidth || 160; const vh = shadowV.videoHeight || 90; const ratio = vw / vh; const renderH = BASE_HEIGHT; const renderW = Math.floor(renderH * ratio); if (canvas.width !== renderW || canvas.height !== renderH) { canvas.width = renderW; canvas.height = renderH; } ctx.drawImage(shadowV, 0, 0, renderW, renderH); canvas.toBlob((blob) => { if (cacheStore) cacheStore.setItem(getCacheKey(time), blob).catch(()=>{}); resolve(URL.createObjectURL(blob)); }, 'image/jpeg', 0.8); } catch (e) { reject(e); } finally { shadowV.removeEventListener('seeked', seekHandler); } }; shadowV.addEventListener('seeked', seekHandler); shadowV.currentTime = time; }); }; const show = async (clientX, rect) => { const boxRect = getLogicalRect(box); const logicalX = clientX; const pos = Math.max(0, Math.min(1, (logicalX - rect.left) / rect.width)); const targetTime = pos * v.duration; if (!isFinite(targetTime)) return; let left = logicalX - boxRect.left; const halfWidth = (imgBox.offsetWidth / 2) || 80; const minX = halfWidth + 10; const maxX = boxRect.width - halfWidth - 10; left = Math.max(minX, Math.min(maxX, left)); previewBox.style.left = `${left}px`; previewTime.textContent = fmtT(targetTime); previewBox.classList.add('show'); const myId = ++currentReqId; try { await sleep(50); if (myId !== currentReqId) return; const url = await generate(targetTime); if (myId !== currentReqId) return; const img = document.createElement('img'); img.src = url; const onImgReady = () => { if (myId !== currentReqId) return; if (img.naturalWidth && img.naturalHeight) { const ratio = img.naturalWidth / img.naturalHeight; imgBox.style.width = `${DISPLAY_HEIGHT * ratio}px`; imgBox.style.height = `${DISPLAY_HEIGHT}px`; } imgBox.style.display = 'flex'; img.classList.add('active'); imgBox.appendChild(img); const oldImages = imgBox.querySelectorAll('img'); if (oldImages.length > 1) { setTimeout(() => { for (let i = 0; i < oldImages.length - 1; i++) { oldImages[i].remove(); } }, 150); } }; if (img.complete) onImgReady(); else img.onload = onImgReady; } catch (e) { } }; const hide = () => { previewBox.classList.remove('show'); imgBox.style.display = 'none'; currentReqId++; setTimeout(() => { if (!previewBox.classList.contains('show')) { const imgs = imgBox.querySelectorAll('img'); imgs.forEach(i => i.remove()); } }, 500); }; const resetSource = (newUrl) => { if (shadowV) shadowV.src = newUrl; }; return { show, hide, resetSource }; })(); const updateState = () => { if (v.paused) { box.classList.add('paused'); btnPlay.innerHTML = mkSvg(icons.play); box.classList.remove('ui-hidden'); if (hideTimer) clearTimeout(hideTimer); } else { box.classList.remove('paused'); btnPlay.innerHTML = mkSvg(icons.pause); resetHideTimer(); } }; const updateVolUI = () => { slideVol.value = v.muted ? 0 : v.volume; btnVol.innerHTML = mkSvg((v.muted || v.volume === 0) ? icons.mute : icons.vol); }; const showVolumeIndicator = () => { const volInd = d.querySelector('#pk_p_vol_indicator'); const volVal = d.querySelector('#pk_p_vol_val'); const volIcon = volInd ? volInd.querySelector('div') : null; if (!volInd || !volVal) return; const isMutedVol = v.muted || v.volume === 0; if (volIcon) volIcon.innerHTML = mkSvg(isMutedVol ? icons.mute : icons.vol); volVal.textContent = Math.round((isMutedVol ? 0 : 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); }; const applyPlayerVolumeDelta = (delta) => { v.muted = false; v.volume = Math.max(0, Math.min(1, v.volume + delta)); updateVolUI(); showVolumeIndicator(); gmSet('pk_vol_muted', false); gmSet('pk_vol_level', v.volume); }; let transcodeTimer = null; const destroyPlayer = () => { if (isPlayerDestroyed) return; isPlayerDestroyed = true; const pTip = document.getElementById('pk_p_plist_tip_global'); if (pTip) pTip.style.display = 'none'; document.removeEventListener('keydown', playerKeyHandler, true); document.removeEventListener('fullscreenchange', syncNativeFullscreenUI); unbindWebFullscreenScaleWatchers(); d.classList.remove('pk-web-fullscreen-ov'); box.classList.remove('pk-web-fullscreen'); document.documentElement.classList.remove('pk-player-web-fullscreen-lock'); document.body.classList.remove('pk-player-web-fullscreen-lock'); if (d._pkWebFullOldTransform !== undefined) { if (d._pkWebFullOldTransform) d.style.setProperty('transform', d._pkWebFullOldTransform, d._pkWebFullOldTransformPriority || ''); else d.style.removeProperty('transform'); if (d._pkWebFullOldTransformOrigin) d.style.setProperty('transform-origin', d._pkWebFullOldTransformOrigin, d._pkWebFullOldTransformOriginPriority || ''); else d.style.removeProperty('transform-origin'); delete d._pkWebFullOldTransform; delete d._pkWebFullOldTransformPriority; delete d._pkWebFullOldTransformOrigin; delete d._pkWebFullOldTransformOriginPriority; } if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); } window.removeEventListener('resize', onResizeTransform); if (transcodeTimer) { clearInterval(transcodeTimer); transcodeTimer = null; } if (activeHealthTimer) { clearInterval(activeHealthTimer); activeHealthTimer = null; } if (activeHlsObjectUrl) { try { URL.revokeObjectURL(activeHlsObjectUrl); } catch (e) {} activeHlsObjectUrl = null; } if (shouldSyncOfficialPlayHistory(item) && v.duration > 0 && v.currentTime > 5 && v.duration - v.currentTime > 5) { if (!officialProgressPostedThisSession && !officialProgressPosting) { officialProgressPosting = true; postOfficialPlayProgress(getPhysicalId(item), v.currentTime, v.duration) .then(handleOfficialPlayProgressWriteResult) .catch(() => {}) .finally(() => { officialProgressPosting = false; }); } } v.pause(); v.muted = true; if (pkHls) { pkHls.stopLoad(); pkHls.detachMedia(); pkHls.destroy(); pkHls = null; } const targetId = item.id; const targetIdx = S.display.findIndex(x => x.id === targetId); if (targetIdx !== -1) { S.sel.clear(); S.sel.add(targetId); S.activeId = targetId; const rowTop = getItemScrollTopByIndex(targetIdx); const vpHeight = UI.vp.clientHeight; UI.vp.scrollTop = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); renderVisible(); updateStat(); } v.src = ""; v.load(); d._pkDestroyPlayer = null; d.remove(); }; d._pkDestroyPlayer = destroyPlayer; v.addEventListener('play', () => { S.navVideoBackArmed = false; updateState(); }); v.addEventListener('pause', updateState); const markStarted = () => { if (isPlayerDestroyed || isSwitching) return; if (box) { box.classList.add('pk-v-started'); stopSpinner(); updateState(); lastWorkingLink = currentLink; lastWorkingResName = currentResName; } }; v.addEventListener('playing', markStarted); v.addEventListener('seeked', () => { if(v.currentTime > 0.1) markStarted(); }); v.addEventListener('click', () => { markStarted(); togglePlay(); }); v.addEventListener('dblclick', (e) => { e.stopPropagation(); btnFull.click(); }); const blockMediaDragWhenErr = (e) => { if (!box.querySelector('.pk-err-dialog')) return; e.preventDefault(); e.stopPropagation(); }; v.addEventListener('dragstart', blockMediaDragWhenErr); const posterEl = d.querySelector('#pk_p_poster'); const loaderEl = d.querySelector('.pk-p-loading'); let shutterTargetTime = 0; let isPiPDesired = false; const stopSpinner = (force = false) => { const isErrorVisible = !!box.querySelector('.pk-err-dialog'); if ((shutterTargetTime > 0 || isErrorVisible) && !force) return; box.classList.remove('buffering'); if (v.paused && v.readyState >= 2) { box.classList.add('pk-v-started'); updateState(); } if (loaderEl) loaderEl.style.display = 'none'; if (posterEl) { posterEl.style.pointerEvents = 'none'; posterEl.style.opacity = '0'; setTimeout(() => { if(posterEl.style.opacity === '0') { posterEl.style.display = 'none'; posterEl.style.pointerEvents = 'auto'; } }, 450); } }; const showSpinner = () => { box.classList.add('buffering'); if (loaderEl) loaderEl.style.display = 'block'; }; v.addEventListener('waiting', showSpinner); v.addEventListener('stalled', showSpinner); v.addEventListener('playing', stopSpinner); v.addEventListener('seeked', stopSpinner); v.addEventListener('canplaythrough', stopSpinner); let lastTextUpdate = 0; const updateTimeUI = () => { requestAnimationFrame(() => { if (isDragSeek) return; if (v.currentTime > 0.1) stopSpinner(); const dur = v.duration; const cur = v.currentTime; const now = performance.now(); if (dur > 0) { const pct = (cur / dur) * 100; progFilled.style.width = `${pct}%`; } updateSharePreviewSoftLimitMarker(); maybeToastSharePreviewSoftLimit(); 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; shutterTargetTime = 0; stopSpinner(true); } } }); let hasCheckedProgress = false; let lastSaveTime = 0; let lastOfficialProgressPostTime = 0; let lastOfficialProgressPostSecond = 0; let officialProgressPosting = false; let officialProgressPostedThisSession = false; const handleOfficialPlayProgressWriteResult = (ok) => { if (!ok) return; officialProgressPostedThisSession = true; S.historyDirty = true; if (isHistoryRootView()) loadOfficialHistoryFirstPage({ manualRebuild: false }); }; const syncOfficialPlayProgress = (force = false) => { if (!shouldSyncOfficialPlayHistory(item)) return; const now = Date.now(); const pId = getPhysicalId(item); const curT = Math.floor(Number(v.currentTime || 0)); const totalT = Math.floor(Number(v.duration || 0)); if (!pId || !(curT > 1)) return; if (totalT > 0 && totalT - curT <= 5) return; if (officialProgressPosting) return; if (officialProgressPostedThisSession) return; if (!force) { if (now - lastOfficialProgressPostTime < 20000) return; if (Math.abs(curT - lastOfficialProgressPostSecond) < 5) return; } lastOfficialProgressPostTime = now; lastOfficialProgressPostSecond = curT; officialProgressPosting = true; postOfficialPlayProgress(pId, curT, totalT) .then(ok => { handleOfficialPlayProgressWriteResult(ok); }) .finally(() => { officialProgressPosting = false; }); }; const applyPendingIntroSkip = () => { const target = parseFloat(v._pkPendingIntroSkip || 0); if (!target || v._pkIntroSkipApplied || hasUserSeeked || v._isRestarting) return; const dur = v.duration || 0; if (!(dur > 0) || target >= dur) { v._pkPendingIntroSkip = 0; return; } v._pkIntroSkipApplied = true; v._pkPendingIntroSkip = 0; shutterTargetTime = target > 1 ? target : 0; try { v.currentTime = target; } catch (e) {} if (tCur) tCur.textContent = fmtT(target); if (progFilled && dur > 0) { progFilled.style.setProperty('width', `${Math.min(100, (target / dur) * 100)}%`, 'important'); } }; const applyProgress = () => { if (hasCheckedProgress || v.duration <= 0) return; hasCheckedProgress = true; applyPendingIntroSkip(); triggerResume(v, item); }; v.addEventListener('canplay', applyProgress, { once: true }); v.addEventListener('playing', () => { syncOfficialPlayProgress(false); }); v.addEventListener('timeupdate', () => { const now = Date.now(); if (now - lastSaveTime > 3000) { syncOfficialPlayProgress(false); lastSaveTime = now; } }); v.addEventListener('pause', () => { syncOfficialPlayProgress(true); }); v.addEventListener('loadedmetadata', () => { triggerResume(v, item); updateTimeUI(); const dur = v.duration; if (dur > 0 && isFinite(dur)) { const seconds = Math.round(dur); const pId = getPhysicalId(item); gmSet('pk_duration_' + pId, seconds); S.durationMap.set(pId, seconds); if (item.params) item.params.duration = seconds; if (typeof renderVisible === 'function') renderVisible(); } }); const enableMediaControls = () => { if (v.readyState >= 2) { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); const btnPip = d.querySelector('#pk_p_pip'); if (btnPip && document.pictureInPictureEnabled && !v.disablePictureInPicture) { btnPip.style.setProperty('display', 'flex', 'important'); if (isPiPDesired && !document.pictureInPictureElement) { v.requestPictureInPicture().catch(() => { isPiPDesired = false; }); } } } }; v.addEventListener('loadeddata', enableMediaControls); v.addEventListener('canplay', enableMediaControls); if (v.readyState >= 2) enableMediaControls(); let isDragSeek = false; let hasUserSeeked = false; let initialTimeBeforeDrag = 0; let progRectCache = null; let lastSeekRequestTime = 0; let lastMouseX = 0; const seekIndicator = d.querySelector('#pk_p_seek_indicator'); let seekIndicatorTimer = null; const showSeekIndicatorAt = (targetTime) => { if (!seekIndicator || !v.duration || isNaN(v.duration)) return; seekIndicator.textContent = `${fmtFullT(targetTime)} / ${fmtFullT(v.duration)}`; seekIndicator.style.display = 'flex'; box.classList.add('pk-is-seeking'); clearTimeout(seekIndicatorTimer); seekIndicatorTimer = setTimeout(() => { if (!isDragSeek) { seekIndicator.style.display = 'none'; box.classList.remove('pk-is-seeking'); } }, 900); }; const updateVisualOnly = (clientX) => { if (!progRectCache || !v.duration) return 0; const logicalX = clientX; const pos = Math.max(0, Math.min(1, (logicalX - progRectCache.left) / progRectCache.width)); const targetTime = pos * v.duration; progFilled.style.setProperty('width', `${pos * 100}%`, 'important'); seekIndicator.textContent = `${fmtFullT(targetTime)} / ${fmtFullT(v.duration)}`; if (tCur) tCur.textContent = fmtT(targetTime); return targetTime; }; const updateVideoOnDemand = (targetTime) => { const now = performance.now(); if (v.paused && (now - lastSeekRequestTime > 40)) { v.currentTime = targetTime; lastSeekRequestTime = now; } }; const stopDragging = (isCancel = false) => { if (!isDragSeek) return; if (isCancel) { v.currentTime = initialTimeBeforeDrag; const revertPos = (initialTimeBeforeDrag / v.duration) * 100; progFilled.style.setProperty('width', `${revertPos}%`, 'important'); } else { const finalT = updateVisualOnly(lastMouseX); v.currentTime = finalT; if (finalT > 5) { syncOfficialPlayProgress(true); } } 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(); }; let activeSeekPointerId = null; const isPrimarySeekPointer = (e) => { if (!e) return false; if (e.isPrimary === false) return false; if (e.pointerType && e.pointerType !== 'touch' && e.button !== undefined && e.button !== 0) return false; return true; }; const releaseSeekPointer = () => { if (activeSeekPointerId !== null && progArea && typeof progArea.hasPointerCapture === 'function' && typeof progArea.releasePointerCapture === 'function') { try { if (progArea.hasPointerCapture(activeSeekPointerId)) progArea.releasePointerCapture(activeSeekPointerId); } catch (err) {} } activeSeekPointerId = null; }; const endSeekPointer = (e = null, isCancel = false) => { if (e && activeSeekPointerId !== null && e.pointerId !== activeSeekPointerId) return; if (e) { if (e.cancelable) e.preventDefault(); e.stopPropagation(); } releaseSeekPointer(); stopDragging(isCancel); }; const onSeekPointerMove = (e) => { if (!isDragSeek) return; if (activeSeekPointerId !== null && e.pointerId !== activeSeekPointerId) return; if (e.cancelable) e.preventDefault(); e.stopPropagation(); const topBar = d.querySelector('.pk-player-top'); if (topBar) { const barRect = getLogicalRect(topBar); if (e.clientY >= barRect.top && e.clientY <= barRect.bottom) { endSeekPointer(e, 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('pointerdown', (e) => { if (!v.duration || isNaN(v.duration) || !isPrimarySeekPointer(e)) return; if (typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.hide(); activeSeekPointerId = e.pointerId; if (typeof progArea.setPointerCapture === 'function') { try { progArea.setPointerCapture(e.pointerId); } catch (err) {} } isDragSeek = true; hasUserSeeked = true; lastMouseX = e.clientX; initialTimeBeforeDrag = v.currentTime; progRectCache = getLogicalRect(progArea); document.body.classList.add('pk-dragging'); box.classList.add('pk-is-seeking'); seekIndicator.style.display = 'flex'; const targetT = updateVisualOnly(e.clientX); v.currentTime = targetT; if (e.cancelable) e.preventDefault(); e.stopPropagation(); }, { passive: false }); progArea.addEventListener('pointermove', (e) => { if (isDragSeek || e.pointerType === 'touch') return; if (typeof ThumbnailEngine !== 'undefined') { const rect = getLogicalRect(progArea); ThumbnailEngine.show(e.clientX, rect); } }, { passive: true }); progArea.addEventListener('pointerleave', () => { if (!isDragSeek && typeof ThumbnailEngine !== 'undefined') ThumbnailEngine.hide(); }); document.addEventListener('pointermove', onSeekPointerMove, { passive: false }); document.addEventListener('pointerup', (e) => endSeekPointer(e, false), { passive: false }); document.addEventListener('pointercancel', (e) => endSeekPointer(e, true), { passive: false }); box.addEventListener('pointerleave', (e) => { if (isDragSeek && e.pointerType !== 'touch') { const rect = getLogicalRect(box); const lx = e.clientX, ly = e.clientY; if (lx <= rect.left || lx >= rect.right || ly <= rect.top || ly >= rect.bottom) { endSeekPointer(e, true); } } }); btnPlay.onclick = togglePlay; btnClose.onclick = destroyPlayer; const syncNativeFullscreenUI = () => { const isNativeFullscreen = document.fullscreenElement === box; if (btnFull) { btnFull.innerHTML = mkSvg(isNativeFullscreen ? icons.exitFull : icons.full); } if (btnWebFull) { btnWebFull.style.display = isNativeFullscreen ? 'none' : ''; } }; btnFull.onclick = async (e) => { if (e) { e.preventDefault(); e.stopPropagation(); } try { if (!document.fullscreenElement) { if (box.classList.contains('pk-web-fullscreen')) { setWebFullscreen(false); } await box.requestFullscreen(); } else { await document.exitFullscreen(); } } catch (err) { console.warn('[Player] Fullscreen switch failed:', err); } finally { syncNativeFullscreenUI(); } }; document.addEventListener('fullscreenchange', syncNativeFullscreenUI); syncNativeFullscreenUI(); const subPanel = d.querySelector('#pk_sub_panel'); if (subPanel) { btnFull.addEventListener('mouseenter', () => subPanel.style.setProperty('display', 'none', 'important')); btnFull.addEventListener('mouseleave', () => subPanel.style.removeProperty('display')); } const btnPip = d.querySelector('#pk_p_pip'); if (btnPip) { if (!document.pictureInPictureEnabled || v.disablePictureInPicture) { btnPip.style.display = 'none'; } else { btnPip.onclick = async (e) => { e.stopPropagation(); try { if (document.pictureInPictureElement) { isPiPDesired = false; await document.exitPictureInPicture(); } else { isPiPDesired = true; await v.requestPictureInPicture(); window.focus(); } } catch (err) { console.error("PiP Error:", err); } }; } } btnSearch.onclick = (e) => { e.stopPropagation(); if (v) { v.pause(); setTimeout(() => { if (v) v.pause(); }, 100); } const posterImg = d.querySelector('#pk_p_poster img'); if (v.readyState >= 2 && v.videoWidth > 0) { startImageSearch(v, item.name, d, null); } else if (posterImg && posterImg.style.display !== 'none' && posterImg.src) { startImageSearch(posterImg, item.name, d, posterImg.src); } else { startImageSearch(v, item.name, d, null); } }; const initPosterImg = d.querySelector('#pk_p_poster img'); if (initPosterImg) { initPosterImg.addEventListener('dragstart', blockMediaDragWhenErr); if (initPosterImg.complete && initPosterImg.src && initPosterImg.src.startsWith('http')) { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); } else { initPosterImg.addEventListener('load', () => { if (btnSearch) btnSearch.style.setProperty('display', 'flex', 'important'); }); } } const savedMute = gmGet('pk_vol_muted', false); const savedVol = parseFloat(gmGet('pk_vol_level', 1.0)); v.muted = savedMute; v.volume = (Number.isFinite(savedVol) && savedVol >= 0 && savedVol <= 1) ? savedVol : 1.0; if (slideVol) slideVol.value = v.volume; updateVolUI(); btnVol.onclick = () => { if (v.muted || v.volume <= 0) { const minVol = Math.max(0.01, Math.min(1, Number(CONF.playerVolumeStep) || 0.05)); v.muted = false; v.volume = minVol; if (slideVol) slideVol.value = String(minVol); gmSet('pk_vol_muted', false); gmSet('pk_vol_level', minVol); } else { v.muted = true; gmSet('pk_vol_muted', true); } updateVolUI(); showVolumeIndicator(); }; 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 createFreshSubtitleTrack = () => { if (v.textTracks) { for (let i = 0; i < v.textTracks.length; i++) { const t = v.textTracks[i]; if (t === subState.track || t.label === 'pk-subs') { try { t.oncuechange = null; } catch(e) {} try { t.mode = 'disabled'; } catch(e) {} } } } const txtEl = d.querySelector('#pk_sub_text'); if (txtEl) { txtEl.innerHTML = ''; txtEl.style.display = 'none'; } const targetTrack = v.addTextTrack("subtitles", "pk-subs", "en"); targetTrack.mode = 'hidden'; subState.track = targetTrack; return targetTrack; }; 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); const targetTrack = createFreshSubtitleTrack(); targetTrack.oncuechange = () => { const cues = targetTrack.activeCues; const txtEl = d.querySelector('#pk_sub_text'); if (cues && cues.length > 0 && txtEl) { const text = cues[cues.length - 1].text; txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
'); txtEl.style.display = 'block'; } else if (txtEl) { txtEl.style.display = 'none'; } }; const vttLines = vttText.split('\n'); let cueStart = null, cueEnd = null, cueText = []; const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/; while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]); vttLines.forEach(line => { const match = line.match(timeReg); if (match) { if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000; cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000; cueText = []; } else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) { if (cueStart !== null) cueText.push(line.trim()); } }); if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } subState.hasSub = true; subState.track = targetTrack; subState.track.mode = 'hidden'; updateSubStyle(); d.querySelector('#pk_sub_toggle').checked = true; d.querySelector('#pk_sub_name').textContent = file.name; updateSubStyle(); }; 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 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, null, null, false, true); } 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) { 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); const targetTrack = createFreshSubtitleTrack(); targetTrack.oncuechange = () => { const cues = targetTrack.activeCues; const txtEl = d.querySelector('#pk_sub_text'); if (cues && cues.length > 0 && txtEl) { const text = cues[cues.length - 1].text; txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
'); txtEl.style.display = 'block'; } else if (txtEl) { txtEl.style.display = 'none'; } }; const vttLines = vttText.split('\n'); let cueStart = null, cueEnd = null, cueText = []; const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/; while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]); vttLines.forEach(line => { const match = line.match(timeReg); if (match) { if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000; cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000; cueText = []; } else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) { if (cueStart !== null) cueText.push(line.trim()); } }); if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } subState.hasSub = true; subState.track = targetTrack; subState.track.mode = 'hidden'; updateSubStyle(); const toggle = d.querySelector('#pk_sub_toggle'); if(toggle) toggle.checked = true; const nameLabel = d.querySelector('#pk_sub_name'); if(nameLabel) nameLabel.textContent = targetSub.name; updateSubStyle(); } catch (e) { console.warn("[AutoSub] Load failed", e); } } }; const btnCloudSub = d.querySelector('#pk_sub_cloud_btn'); btnCloudSub.onclick = async (e) => { e.stopPropagation(); const subPanel = d.querySelector('#pk_sub_panel'); const subTrigger = d.querySelector('#pk_sub_trigger'); if (subPanel && subTrigger) { subPanel.style.setProperty('display', 'none', 'important'); const clearDisplay = () => { subPanel.style.removeProperty('display'); subTrigger.removeEventListener('mouseleave', clearDisplay); }; subTrigger.addEventListener('mouseleave', clearDisplay); } if (v && !v.paused) v.pause(); const originalText = btnCloudSub.textContent; btnCloudSub.textContent = L.loading; btnCloudSub.style.opacity = "0.6"; btnCloudSub.style.pointerEvents = "none"; let startPath = [{ id: '', name: L.btn_nav_home }]; let targetFolderId = ''; try { let targetItem = item; if (S.offlineMode || S.uploadMode || S.recentMode || item.kind === 'drive#task') { const realFileId = (item.kind === 'drive#task' || S.offlineMode || S.uploadMode) ? (item.file_id || (item.params && item.params.file_id)) : item.id; if (realFileId) { try { targetItem = await apiGet(realFileId); } catch(err) { console.warn("[CloudSub] Failed to resolve task file:", err); } } } if (targetItem.parent_id && targetItem.parent_id !== 'root') { targetFolderId = targetItem.parent_id; } if (targetItem._lineage && Array.isArray(targetItem._lineage) && targetItem._lineage.length > 0) { startPath = targetItem._lineage.map(x => ({ id: x.id || '', name: x.name })); if (startPath.length > 0 && startPath[0].id !== '' && startPath[0].id !== 'root') { startPath.unshift({ id: '', name: L.btn_nav_home }); } } else if (targetFolderId) { const trace = []; let curr = targetFolderId; let safety = 6; while (curr && curr !== 'root' && safety > 0) { try { const f = await apiGet(curr); trace.unshift({ id: f.id, name: f.name }); curr = f.parent_id; } catch(e) { break; } safety--; } if (trace.length > 0) { startPath = [{ id: '', name: L.btn_nav_home }, ...trace]; } } else if (S.path && S.path.length > 0) { const cleanPath = S.path.filter(p => !p.id.startsWith('virtual_') && !p.id.includes('_root') && p.id !== 'analyze_root'); if (cleanPath.length > 0) { startPath = cleanPath; if (startPath[0].id !== '' && startPath[0].id !== 'root') { startPath.unshift({ id: '', name: L.btn_nav_home }); } } } } catch (error) { console.error("[CloudSub] Path resolve error:", error); targetFolderId = ''; startPath = [{ id: '', name: L.btn_nav_home }]; } finally { btnCloudSub.textContent = originalText; btnCloudSub.style.opacity = "1"; btnCloudSub.style.pointerEvents = "auto"; } showFolderSelector( targetFolderId, async (id, name, subItem) => { const box = d.querySelector('#pk_p_box'); const toast = document.createElement('div'); toast.style.cssText = "position:absolute;top:80px;right:20px;background:rgba(0,0,0,0.8);color:#fff;padding:8px 16px;border-radius:4px;font-size:13px;pointer-events:none;z-index:90;display:flex;align-items:center;gap:8px;"; toast.innerHTML = `
${L.msg_dl_sub}`; box.appendChild(toast); try { let link = subItem.web_content_link; if (!link) { const detail = await apiGet(subItem.id); link = detail.web_content_link; } const res = await fetch(link); const buffer = await res.arrayBuffer(); const encodings = ['utf-8', 'gbk', 'big5', 'utf-16le', 'shift_jis', 'windows-1252']; let text = ""; for (let enc of encodings) { try { const decoder = new TextDecoder(enc, { fatal: true }); text = decoder.decode(buffer); break; } catch (e) { if(enc === 'windows-1252') text = new TextDecoder('utf-8').decode(buffer); } } let vttText = ""; const subExt = subItem.name.split('.').pop().toLowerCase(); if (subExt === 'ass' || subExt === 'ssa') { vttText = convertAssToVtt(text); } else { vttText = convertSrtToVtt(text); } const blob = new Blob([vttText], { type: 'text/vtt' }); if (subState.blobUrl) URL.revokeObjectURL(subState.blobUrl); subState.blobUrl = URL.createObjectURL(blob); const targetTrack = createFreshSubtitleTrack(); targetTrack.oncuechange = () => { const cues = targetTrack.activeCues; const txtEl = d.querySelector('#pk_sub_text'); if (cues && cues.length > 0 && txtEl) { const text = cues[cues.length - 1].text; txtEl.innerHTML = text.replace(/<[^>]+>/g, '').replace(/\n/g, '
'); txtEl.style.display = 'block'; } else if (txtEl) { txtEl.style.display = 'none'; } }; const vttLines = vttText.split('\n'); let cueStart = null, cueEnd = null, cueText = []; const timeReg = /(\d{2}):(\d{2}):(\d{2})[.,](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[.,](\d{3})/; while (targetTrack.cues && targetTrack.cues.length > 0) targetTrack.removeCue(targetTrack.cues[0]); vttLines.forEach(line => { const match = line.match(timeReg); if (match) { if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } cueStart = parseInt(match[1])*3600 + parseInt(match[2])*60 + parseInt(match[3]) + parseInt(match[4])/1000; cueEnd = parseInt(match[5])*3600 + parseInt(match[6])*60 + parseInt(match[7]) + parseInt(match[8])/1000; cueText = []; } else if (line.trim() !== '' && !line.trim().startsWith('WEBVTT') && !/^\d+$/.test(line.trim())) { if (cueStart !== null) cueText.push(line.trim()); } }); if (cueStart !== null && cueText.length > 0) { try { targetTrack.addCue(new VTTCue(cueStart, cueEnd, cueText.join('\n'))); } catch(e){} } subState.hasSub = true; subState.track = targetTrack; subState.track.mode = 'hidden'; updateSubStyle(); const toggle = d.querySelector('#pk_sub_toggle'); if(toggle) toggle.checked = true; const nameLabel = d.querySelector('#pk_sub_name'); if(nameLabel) nameLabel.textContent = subItem.name; updateSubStyle(); toast.innerHTML = `✅ ${L.msg_auto_sub_load.replace('{n}', subItem.name)}`; setTimeout(() => toast.remove(), 3000); } catch (err) { console.error(err); toast.innerHTML = `❌ ${L.err_sub_dl_fail}`; setTimeout(() => toast.remove(), 4000); } }, startPath, (f) => /\.(srt|vtt|ass|ssa)$/i.test(f.name), L.title_sel_sub ); }; const 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 btnSearchSub = d.querySelector('#pk_sub_search_btn'); btnSearchSub.onclick = async (e) => { e.stopPropagation(); const subPanel = d.querySelector('#pk_sub_panel'); const subTrigger = d.querySelector('#pk_sub_trigger'); if (subPanel && subTrigger) { subPanel.style.setProperty('display', 'none', 'important'); const clearDisplay = () => { subPanel.style.removeProperty('display'); subTrigger.removeEventListener('mouseleave', clearDisplay); }; subTrigger.addEventListener('mouseleave', clearDisplay); } const searchOv = document.createElement('div'); searchOv.className = 'pk-sub-search-modal'; searchOv.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(20,20,20,0.95); border: 1px solid #444; border-radius: 8px; width: 380px; height: 450px; display: flex; flex-direction: column; box-shadow: 0 10px 40px rgba(0,0,0,0.8); z-index: 60; padding: 15px; backdrop-filter: blur(10px); `; box.appendChild(searchOv); searchOv.onclick = (evt) => evt.stopPropagation(); const keyword = cleanFilename(item.name); searchOv.innerHTML = `
`; const resultList = searchOv.querySelector('#pk_sub_search_list'); const input = searchOv.querySelector('#pk_sub_search_input'); const doSearch = (query) => { const cleanQ = query.trim(); if (!cleanQ) return; const plusQ = encodeURIComponent(cleanQ).replace(/%20/g, '+'); const spaceQ = encodeURIComponent(cleanQ); const curLang = L.lang_code; let engines = []; if (curLang === 'zh') { engines = [ { name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` }, { name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` }, { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/zh/search2/sublanguageid-kor/moviename-${plusQ}` }, ]; } else if (curLang === 'tc') { engines = [ { name: 'R3Sub', url: `https://r3sub.com/search.php?s=${plusQ}` }, { name: 'SubHD', url: `https://subhd.tv/search/${spaceQ}` }, { name: 'Assrt', url: `https://assrt.net/sub/?searchword=${plusQ}` } ]; } else if (curLang === 'ko') { engines = [ { name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` }, { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ko/search2/sublanguageid-kor/moviename-${plusQ}` }, { name: 'SubtitleCat', url: `https://www.subtitlecat.com/index.php?search=${plusQ}` } ]; } else if (curLang === 'ja') { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ja/search2/sublanguageid-jpn/moviename-${plusQ}` }, { name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` }, { name: 'Anime Tosho', url: `https://animetosho.org/search?q=${plusQ}` } ]; } else if (curLang === 'id') { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/id/search2/sublanguageid-ind/moviename-${plusQ}` }, { name: 'Samehadaku', url: `https://samehadaku.li/?s=${plusQ}` }, { name: 'Anoboy', url: `https://anoboy.be/?s=${plusQ}` } ]; } else if (curLang === 'ms') { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/ms/search2/sublanguageid-may/moviename-${plusQ}` }, { name: 'PencuriMovie', url: `https://pencurimovie.my/?s=${plusQ}` }, { name: 'Oh Flix', url: `https://vvww.ohflix.my.id/?s=${plusQ}` } ]; } else { engines = [ { name: 'OpenSubtitles', url: `https://www.opensubtitles.org/en/search2/sublanguageid-eng/moviename-${plusQ}` }, { name: 'MovieSubtitles', url: `https://www.moviesubtitles.org/search.php?q=${plusQ}` }, { name: 'iSubtitles', url: `https://isubtitles.org/search?q=${plusQ}` } ]; } let html = `
`; engines.forEach(e => { html += ` ${L.btn_go_search.replace('{n}', e.name)} `; }); html += `
${L.tip_manual_sub}
`; resultList.innerHTML = html; }; doSearch(input.value); input.oninput = () => doSearch(input.value); input.onkeydown = (ev) => { ev.stopPropagation(); }; searchOv.querySelector('#pk_sub_search_close').onclick = () => searchOv.remove(); }; d.querySelector('#pk_sub_toggle').onchange = (e) => { updateSubStyle(); if (v.textTracks) { Array.from(v.textTracks).forEach(t => { if (t !== subState.track) t.mode = 'disabled'; }); } }; if (v.textTracks) { v.textTracks.addEventListener('addtrack', (e) => { if (subState.hasSub && e.track !== subState.track) { e.track.mode = 'disabled'; } }); } const sizeVal = d.querySelector('#pk_sub_size_val'); d.querySelector('#pk_sub_size_dec').onclick = (e) => { e.stopPropagation(); subState.size = Math.max(12, subState.size - 2); sizeVal.textContent = subState.size; updateSubStyle(); }; d.querySelector('#pk_sub_size_inc').onclick = (e) => { e.stopPropagation(); subState.size = Math.min(80, subState.size + 2); sizeVal.textContent = subState.size; updateSubStyle(); }; d.querySelector('#pk_sub_pos').oninput = (e) => { e.stopPropagation(); subState.pos = parseInt(e.target.value); updateSubStyle(); }; d.querySelector('#pk_sub_bg_opacity').oninput = (e) => { e.stopPropagation(); subState.bgOpacity = parseInt(e.target.value) / 100; updateSubStyle(); }; const timeVal = d.querySelector('#pk_sub_time_val'); const adjustOffset = (delta) => { subState.offset += delta; timeVal.textContent = subState.offset.toFixed(1) + " " + L.unit_sec; if (subState.track && subState.track.cues) { const cues = Array.from(subState.track.cues); cues.forEach(cue => { cue.startTime += delta; cue.endTime += delta; }); } }; d.querySelector('#pk_sub_time_dec').onclick = (e) => { e.stopPropagation(); adjustOffset(-0.5); }; d.querySelector('#pk_sub_time_inc').onclick = (e) => { e.stopPropagation(); adjustOffset(0.5); }; d.querySelector('#pk_sub_panel').onclick = (e) => e.stopPropagation(); d.querySelectorAll('.pk-sub-tab').forEach(t => { t.onclick = () => { d.querySelectorAll('.pk-sub-tab, .pk-sub-pane').forEach(el => el.classList.remove('active')); t.classList.add('active'); d.querySelector('#' + t.dataset.target).classList.add('active'); }; }); const curPMode = gmGet('pk_play_mode', 'stop'); const pRadios = d.querySelectorAll('input[name="pk_pmode"]'); pRadios.forEach(r => { if (r.value === curPMode) r.checked = true; r.onchange = () => gmSet('pk_play_mode', r.value); }); let transformState = { rotate: 0, flipH: 1, flipV: 1, ratio: 'default' }; const applyTransform = () => { if (document.pictureInPictureElement === v) { v.style.transform = 'none'; return; } if (transformState.ratio === 'default') { v.style.objectFit = 'contain'; v.style.width = '100%'; v.style.height = box.classList.contains('plist-active') ? (document.fullscreenElement ? 'calc(100% - 84px)' : '100%') : '100%'; v.style.aspectRatio = 'auto'; v.style.margin = '0'; v.style.inset = 'auto'; } else { v.style.objectFit = 'fill'; v.style.aspectRatio = transformState.ratio; v.style.width = 'auto'; v.style.height = 'auto'; v.style.maxWidth = '100%'; v.style.maxHeight = '100%'; v.style.margin = 'auto'; v.style.inset = '0'; } let autoScale = 1; if (Math.abs(transformState.rotate) % 180 !== 0) { const boxW = box.clientWidth; const boxH = box.clientHeight; const vW = v.offsetWidth || boxW; const vH = v.offsetHeight || boxH; if (vW > 0 && vH > 0) { const scaleW = boxW / vH; const scaleH = boxH / vW; autoScale = Math.min(scaleW, scaleH); } } v.style.transform = `translateZ(0) rotate(${transformState.rotate}deg) scale(${autoScale}) scale(${transformState.flipH}, ${transformState.flipV})`; }; const onResizeTransform = () => requestAnimationFrame(applyTransform); window.addEventListener('resize', onResizeTransform); d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(btn => { btn.onclick = (e) => { d.querySelectorAll('#pk_ratio_opts .pk-size-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); transformState.ratio = btn.dataset.ratio; applyTransform(); }; }); d.querySelector('#pk_btn_rot_l').onclick = () => { transformState.rotate -= 90; applyTransform(); }; d.querySelector('#pk_btn_rot_r').onclick = () => { transformState.rotate += 90; applyTransform(); }; d.querySelector('#pk_btn_flip_h').onclick = () => { transformState.flipH *= -1; applyTransform(); }; d.querySelector('#pk_btn_flip_v').onclick = () => { transformState.flipV *= -1; applyTransform(); }; const opVal = d.querySelector('#pk_op_val'); const edVal = d.querySelector('#pk_ed_val'); let valOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; let valEd = parseInt(gmGet('pk_skip_outro', 0)) || 0; opVal.textContent = valOp + " " + L.unit_sec; edVal.textContent = valEd + " " + L.unit_sec; const updateSkip = (type, delta) => { if (type === 'op') { valOp = Math.max(0, Math.min(3600, valOp + delta)); opVal.textContent = valOp + " " + L.unit_sec; gmSet('pk_skip_intro', valOp); } else { valEd = Math.max(0, Math.min(3600, valEd + delta)); edVal.textContent = valEd + " " + L.unit_sec; gmSet('pk_skip_outro', valEd); } }; d.querySelector('#pk_op_dec').onclick = () => updateSkip('op', -5); d.querySelector('#pk_op_inc').onclick = () => updateSkip('op', 5); d.querySelector('#pk_ed_dec').onclick = () => updateSkip('ed', -5); d.querySelector('#pk_ed_inc').onclick = () => updateSkip('ed', 5); d.querySelector('#pk_op_mark').onclick = (e) => { e.stopPropagation(); const markTime = Math.max(0, Math.floor(v.currentTime)); valOp = markTime; opVal.textContent = valOp + " " + L.unit_sec; gmSet('pk_skip_intro', valOp); }; d.querySelector('#pk_ed_mark').onclick = (e) => { e.stopPropagation(); const dur = typeof getEffectiveVideoDuration === 'function' ? getEffectiveVideoDuration() : Number(v.duration || 0); if (!(Number.isFinite(dur) && dur > 0)) return; const cur = Number(v.currentTime || 0); if (!(Number.isFinite(cur) && cur >= 0)) return; const markTime = Math.max(0, Math.floor(dur - cur)); if (markTime <= 0) return; valEd = markTime; edVal.textContent = valEd + " " + L.unit_sec; gmSet('pk_skip_outro', valEd); }; const getEffectiveVideoDuration = () => { const nativeDur = Number(v.duration || 0); if (Number.isFinite(nativeDur) && nativeDur > 0) return nativeDur; const pId = getPhysicalId(item); const metaDur = Number((item.params && item.params.duration) || S.durationMap.get(pId) || gmGet('pk_duration_' + pId, 0) || 0); return Number.isFinite(metaDur) && metaDur > 0 ? metaDur : 0; }; const handlePlayerEnded = (fromOutroSkip = false) => { 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(()=>{}); } } else if (fromOutroSkip) { const dur = getEffectiveVideoDuration(); try { if (dur > 0 && Number.isFinite(dur)) v.currentTime = Math.max(0, dur - 0.05); } catch (e) {} v.pause(); if (typeof updateState === 'function') updateState(); if (typeof stopSpinner === 'function') stopSpinner(true); } }; let hasTriggeredEnd = false; v.addEventListener('timeupdate', () => { const skipEd = parseInt(gmGet('pk_skip_outro', 0)) || 0; const skipOp = parseInt(gmGet('pk_skip_intro', 0)) || 0; const dur = getEffectiveVideoDuration(); const cur = Number(v.currentTime || 0); if (skipEd > 0 && dur > 0 && !hasTriggeredEnd) { if (skipEd >= dur || (skipOp + skipEd) >= dur || cur < (dur / 2)) return; if (dur - cur <= skipEd) { hasTriggeredEnd = true; handlePlayerEnded(true); } } }); 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 = () => { handlePlayerEnded(false); }; const makeEditable = (el, type, callback) => { el.ondblclick = (e) => { e.stopPropagation(); const oldText = el.textContent; const oldVal = type === 'offset' ? parseFloat(oldText) : parseInt(oldText); el.innerHTML = ``; const input = el.querySelector('input'); input.focus(); input.select(); const finish = () => { const val = parseFloat(input.value); if (isNaN(val)) { el.textContent = oldText; } else { let finalVal = type === 'offset' ? val : Math.round(val); if (type !== 'offset') finalVal = Math.max(0, finalVal); if (type === 'offset') el.textContent = finalVal.toFixed(1) + " " + L.unit_sec; else el.textContent = finalVal; callback(finalVal); } el.ondblclick = (evt) => { evt.stopPropagation(); makeEditable(el, type, callback).ondblclick(evt); }; }; input.onblur = finish; input.onkeydown = (ev) => { ev.stopPropagation(); if (ev.key === 'Enter') { input.blur(); } }; input.onclick = (ev) => ev.stopPropagation(); }; }; makeEditable(d.querySelector('#pk_sub_size_val'), 'int', (val) => { subState.size = val; updateSubStyle(); }); makeEditable(d.querySelector('#pk_sub_time_val'), 'offset', (val) => { const delta = val - subState.offset; adjustOffset(delta); }); makeEditable(d.querySelector('#pk_op_val'), 'int', (val) => { valOp = Math.min(3600, val); gmSet('pk_skip_intro', valOp); d.querySelector('#pk_op_val').textContent = valOp + " " + L.unit_sec; }); makeEditable(d.querySelector('#pk_ed_val'), 'int', (val) => { valEd = Math.min(3600, val); gmSet('pk_skip_outro', valEd); d.querySelector('#pk_ed_val').textContent = valEd + " " + L.unit_sec; }); updateSubStyle(); const playerKeyHandler = (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (!document.getElementById('pk-player-ov')) return; const k = String(e.key || ''); const ku = k.toUpperCase(); if (k === 'F11' || k === 'F12' || ((e.ctrlKey || e.metaKey) && e.shiftKey && (ku === 'I' || ku === 'J' || ku === 'C'))) return; e.stopPropagation(); e.preventDefault(); resetHideTimer(); switch(e.key) { case ' ': case 'k': togglePlay(); break; case 'ArrowRight': if (e.ctrlKey || e.metaKey) { const nextIdx = (curListIdx + 1) % totalInList; softSwitch(nextIdx); } else { const targetTime = Math.min(v.duration || 0, v.currentTime + 10); v.currentTime = targetTime; if (tCur) tCur.textContent = fmtT(targetTime); if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important'); showSeekIndicatorAt(targetTime); } break; case 'ArrowLeft': if (e.ctrlKey || e.metaKey) { const prevIdx = (curListIdx - 1 + totalInList) % totalInList; softSwitch(prevIdx); } else { const targetTime = Math.max(0, v.currentTime - 10); v.currentTime = targetTime; if (tCur) tCur.textContent = fmtT(targetTime); if (progFilled && v.duration) progFilled.style.setProperty('width', `${(targetTime / v.duration) * 100}%`, 'important'); showSeekIndicatorAt(targetTime); } break; case 'p': case 'P': const btnPip = d.querySelector('#pk_p_pip'); if (btnPip && btnPip.style.display !== 'none') btnPip.click(); break; case 'e': case 'E': if (pTab) pTab.click(); break; case 'ArrowUp': case 'ArrowDown': applyPlayerVolumeDelta(e.key === 'ArrowUp' ? CONF.playerVolumeStep : -CONF.playerVolumeStep); break; case 'f': case 'F': if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click(); break; case 'Enter': btnFull.click(); break; case 'Escape': e.preventDefault(); e.stopPropagation(); if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation(); if (document.fullscreenElement) { document.exitFullscreen(); } else if (box && box.classList.contains('pk-web-fullscreen')) { setWebFullscreen(false); } else { destroyPlayer(); } break; } }; document.addEventListener('keydown', playerKeyHandler, true); const initDur = (item.params && item.params.duration) || S.durationMap.get(item.id) || gmGet('pk_duration_' + item.id, 0); if (tDur && initDur > 0) tDur.textContent = fmtT(initDur); loadSource(currentLink, null); setTimeout(() => { if (isPlayerDestroyed) return; const p = v.play(); if (p !== undefined) { p.catch(() => updateState()); } if (startFullscreen) { const box = d.querySelector('#pk_p_box'); const btnFull = d.querySelector('#pk_p_full'); if (box && box.requestFullscreen) { box.requestFullscreen().then(() => { if(btnFull) btnFull.innerHTML = mkSvg(icons.exitFull); }).catch(err => console.warn("Fullscreen auto-resume failed", err)); } } }, 100); autoMatchSubtitle(item); } let isImageOpening = false; async function showImage(startItem) { if (S.trashMode) return; if (document.querySelector('.pk-img-ov')) return; isImageOpening = true; let lastDirection = 1; const item = startItem; const imgList = S.display.filter(i => { if (i.isHeader) return false; if (S.offlineMode && i.phase !== 'PHASE_TYPE_COMPLETE') return false; if (S.uploadMode && i.status !== 'DONE') return false; return i.mime_type && i.mime_type.startsWith('image'); }); if (imgList.length === 0) return; let curIdx = imgList.findIndex(i => i.id === startItem.id); if (curIdx === -1) curIdx = 0; const d = document.createElement('div'); d.className = 'pk-img-ov'; d.tabIndex = 0; const icons = { close: '', full: '', exitFull: '', prev: '', next: '', rotate: '', flipH: '', flipV: '', searchlens: ``, leftArr: ``, rightArr: ``, upArr: `` }; const renderImgListItems = () => { const RANGE = 150; const start = Math.max(0, curIdx - RANGE); const end = Math.min(imgList.length, curIdx + RANGE + 1); return imgList.slice(start, end).map((v, i) => { const absIdx = start + i; const coverSrc = (v.thumbnail_link && v.thumbnail_link !== v.icon_link) ? v.thumbnail_link : ''; const phSrc = v.icon_link || v.thumbnail_link || ''; const phSvg = getIcon(v); return `
${phSrc ? `` : phSvg}
${coverSrc ? `` : ``}
`; }).join(''); }; const listFixStyle = ``; d.innerHTML = listFixStyle + `
${icons.flipV}
${icons.flipH}
${icons.rotate}
${icons.full}
${icons.close}
${curIdx + 1} / ${imgList.length}
${icons.leftArr}
${renderImgListItems()}
${icons.rightArr}
`; document.body.appendChild(d); const box = d.querySelector('#pk_img_box'); const img = d.querySelector('#pk_img_el'); const viewport = d.querySelector('#pk_img_viewport'); const title = d.querySelector('#pk_img_title'); const loader = d.querySelector('#pk_img_load'); const btnFull = d.querySelector('#pk_img_full'); const btnRot = d.querySelector('#pk_img_rot'); const btnMirror = d.querySelector('#pk_img_mirror'); const btnFlipV = d.querySelector('#pk_img_flip_v'); const btnSearch = d.querySelector('#pk_img_search'); let scale = 1, transX = 0, transY = 0, rotation = 0, flipH = 1, flipV = 1, isDrag = false, startX, startY; let transformRaf = 0; let isLongImageMode = false; const getImageVisualBounds = (targetScale = scale) => { const vw = Math.max(1, viewport.clientWidth || 1); const vh = Math.max(1, viewport.clientHeight || 1); const iw = Math.max(1, img.naturalWidth || 1); const ih = Math.max(1, img.naturalHeight || 1); const baseRatio = Math.min(vw / iw, vh / ih) || 1; let curW = iw * baseRatio * targetScale; let curH = ih * baseRatio * targetScale; if (Math.abs(rotation % 180) === 90) [curW, curH] = [curH, curW]; return { vw, vh, curW, curH, limitX: curW > vw ? (curW - vw) / 2 : 0, limitY: curH > vh ? (curH - vh) / 2 : 0 }; }; const clampImagePan = (targetScale = scale, x = transX, y = transY) => { const bounds = getImageVisualBounds(targetScale); return { x: Math.max(-bounds.limitX, Math.min(bounds.limitX, x)), y: Math.max(-bounds.limitY, Math.min(bounds.limitY, y)) }; }; const applyTransformNow = () => { if (isLongImageMode) return; const clamped = clampImagePan(); transX = clamped.x; transY = clamped.y; img.style.transform = `translate(${transX}px, ${transY}px) scale(${scale}) rotate(${rotation}deg) scaleX(${flipH}) scaleY(${flipV})`; }; const updateTransform = () => { if (isLongImageMode) return; if (transformRaf) return; transformRaf = requestAnimationFrame(() => { transformRaf = 0; applyTransformNow(); }); }; const updateTransformNow = () => { if (transformRaf) { cancelAnimationFrame(transformRaf); transformRaf = 0; } applyTransformNow(); }; const zoomImageAt = (clientX, clientY, nextScale) => { if (isLongImageMode) return; const newScale = Math.max(0.1, Math.min(10, Number(nextScale) || scale)); if (!viewport || !img.naturalWidth || Math.abs(newScale - scale) < 0.0001) return; const rect = viewport.getBoundingClientRect(); const oldBounds = getImageVisualBounds(scale); const newBounds = getImageVisualBounds(newScale); const localX = clientX - rect.left - oldBounds.vw / 2; const localY = clientY - rect.top - oldBounds.vh / 2; const ratioX = oldBounds.curW ? (localX - transX) / oldBounds.curW : 0; const ratioY = oldBounds.curH ? (localY - transY) / oldBounds.curH : 0; scale = newScale; transX = localX - ratioX * newBounds.curW; transY = localY - ratioY * newBounds.curH; const clamped = clampImagePan(scale, transX, transY); transX = clamped.x; transY = clamped.y; updateTransform(); }; 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'; updateTransformNow(); 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 = currentItem._isShareItem ? await resolveSharePlayableFile(currentItem) : 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) { await markOfflineReferenceMissingFromError(currentItem, e, { source: 'image_detail' }); 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(); } }; const handleImageWheel = (e) => { if (isLongImageMode) return; e.preventDefault(); e.stopPropagation(); const factor = e.deltaY > 0 ? (1 / 1.12) : 1.12; zoomImageAt(e.clientX, e.clientY, scale * factor); }; d.addEventListener('wheel', handleImageWheel, { passive: false }); 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(); isDrag = true; img.style.cursor = 'grabbing'; startX = e.clientX - transX; startY = e.clientY - transY; }); document.addEventListener('mousemove', (e) => { if (!isDrag || isLongImageMode) return; let tx = e.clientX - startX; let ty = e.clientY - startY; if (img.naturalWidth && viewport) { const vw = viewport.clientWidth; const vh = viewport.clientHeight; const iw = img.naturalWidth; const ih = img.naturalHeight; const baseRatio = Math.min(vw / iw, vh / ih); let curW = iw * baseRatio * scale; let curH = ih * baseRatio * scale; if (Math.abs(rotation % 180) === 90) [curW, curH] = [curH, curW]; const limitX = curW > vw ? (curW - vw) / 2 : 0; const limitY = curH > vh ? (curH - vh) / 2 : 0; tx = Math.max(-limitX, Math.min(limitX, tx)); ty = Math.max(-limitY, Math.min(limitY, ty)); } transX = tx; transY = ty; updateTransform(); }); document.addEventListener('mouseup', () => { isDrag = false; if (!isLongImageMode && img) img.style.cursor = 'grab'; }); const resizeHandler = () => { if (isLongImageMode) return; resetView(true); }; window.addEventListener('resize', resizeHandler); d._pkResizeHandler = resizeHandler; d._pkImgList = imgList; d._pkGetCurIdx = () => curIdx; Object.defineProperty(d, '_pkCurIdx', { get: () => curIdx, configurable: true }); d.querySelector('#pk_img_close').onclick = (e) => { e.stopPropagation(); S.closeImageOverlay(); }; btnFull.onclick = (e) => { e.stopPropagation(); box.classList.toggle('full'); const isNowFull = box.classList.contains('full'); btnFull.innerHTML = isNowFull ? icons.exitFull : icons.full; btnFull.setAttribute('data-pk-tip', isNowFull ? L.tip_minimize : L.tip_maximize); if (isLongImageMode && viewport) { viewport.scrollTop = 0; } else { setTimeout(() => resetView(true), 210); } }; btnRot.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; rotation += 90; updateTransform(); }; if (btnMirror) { btnMirror.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; flipH *= -1; img.style.transition = 'none'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; } if (btnFlipV) { btnFlipV.onclick = (e) => { e.stopPropagation(); if (isLongImageMode) return; flipV *= -1; img.style.transition = 'none'; updateTransform(); requestAnimationFrame(() => { requestAnimationFrame(() => { img.style.transition = ''; }); }); }; } const plist = d.querySelector('#pk_img_plist'); const pTab = d.querySelector('#pk_img_plist_tab'); const pScroll = d.querySelector('#pk_img_plist_scroll'); let pTip = document.getElementById('pk_p_plist_tip_global'); if (!pTip) { pTip = document.createElement('div'); pTip.id = 'pk_p_plist_tip_global'; pTip.className = 'pk-p-plist-tip'; document.body.appendChild(pTip); } const pTxt = d.querySelector('#pk_img_idx_txt'); const updateImgPlistUI = (scrollType = 'smooth') => { if (pTxt) pTxt.textContent = `${curIdx + 1} / ${imgList.length}`; const RANGE = 150; const desiredStart = Math.max(0, curIdx - RANGE); const desiredEnd = Math.min(imgList.length, curIdx + RANGE + 1); const prevStart = parseInt(pScroll.dataset.pkStart || '-1', 10); const prevEnd = parseInt(pScroll.dataset.pkEnd || '-1', 10); const needRebuild = !Number.isFinite(prevStart) || !Number.isFinite(prevEnd) || curIdx < prevStart || curIdx >= prevEnd || pScroll.childElementCount === 0 || pScroll.dataset.pkTotal !== String(imgList.length); if (needRebuild) { pScroll.innerHTML = renderImgListItems(); pScroll.dataset.pkStart = String(desiredStart); pScroll.dataset.pkEnd = String(desiredEnd); pScroll.dataset.pkTotal = String(imgList.length); pScroll.querySelectorAll('.pk-p-plist-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const idx = parseInt(e.currentTarget.dataset.idx, 10); if (idx === curIdx) return; curIdx = idx; loadCurrent('instant'); }; el.onmouseenter = (e) => { if (plist.classList.contains('open')) { const name = e.currentTarget.dataset.name; const size = e.currentTarget.dataset.size; pTip.innerHTML = `${name}
${size}`; pTip.style.display = 'block'; } }; el.onmousemove = (e) => { if (pTip.style.display === 'block') { const tW = pTip.offsetWidth || 150; pTip.style.left = (e.clientX - (tW / 2)) + 'px'; pTip.style.top = (e.clientY - 60) + 'px'; } }; el.onmouseleave = () => { pTip.style.display = 'none'; }; }); } const itemsInDom = pScroll.querySelectorAll('.pk-p-plist-item'); itemsInDom.forEach((el) => { const absIdx = parseInt(el.dataset.idx, 10); const isActive = absIdx === curIdx; el.classList.toggle('active', isActive); if (scrollType !== false && isActive && plist.classList.contains('open')) { if (scrollType === 'instant') { pScroll.style.scrollBehavior = 'auto'; } else { pScroll.style.scrollBehavior = 'smooth'; } el.scrollIntoView({ behavior: scrollType === 'instant' ? 'auto' : 'smooth', block: 'nearest', inline: 'center' }); if (scrollType === 'instant') { setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); } } }); const sl = Math.ceil(pScroll.scrollLeft); const sw = pScroll.scrollWidth; const cw = pScroll.clientWidth; if (sw <= cw) { d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important'); d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important'); } else { if (sl <= 5) d.querySelector('#pk_img_plist_L').style.setProperty('display', 'none', 'important'); else d.querySelector('#pk_img_plist_L').style.setProperty('display', 'flex', 'important'); if (sl + cw >= sw - 5) d.querySelector('#pk_img_plist_R').style.setProperty('display', 'none', 'important'); else d.querySelector('#pk_img_plist_R').style.setProperty('display', 'flex', 'important'); } const btnPrev = d.querySelector('#pk_img_prev'); const btnNext = d.querySelector('#pk_img_next'); if (btnPrev) btnPrev.style.setProperty('display', curIdx === 0 ? 'none' : 'flex', 'important'); if (btnNext) btnNext.style.setProperty('display', curIdx === imgList.length - 1 ? 'none' : 'flex', 'important'); }; pTab.onclick = (e) => { e.stopPropagation(); const willOpen = !plist.classList.contains('open'); plist.classList.toggle('open'); if (typeof box !== 'undefined') box.classList.toggle('plist-active'); pTab.setAttribute('data-pk-tip', plist.classList.contains('open') ? L.tip_plist_close : L.tip_plist_open); if (!isLongImageMode) resetView(); if (willOpen) { updateImgPlistUI(false); pScroll.style.scrollBehavior = 'auto'; const activeItem = pScroll.querySelector('.active'); if (activeItem) { const targetLeft = activeItem.offsetLeft - (pScroll.clientWidth / 2) + (activeItem.clientWidth / 2); pScroll.scrollLeft = targetLeft; } setTimeout(() => { pScroll.style.scrollBehavior = 'smooth'; }, 50); setTimeout(() => updateImgPlistUI(false), 100); } }; const handleListWheel = (e) => { e.stopPropagation(); e.preventDefault(); pScroll.scrollBy({ left: e.deltaY > 0 ? 300 : -300, behavior: 'smooth' }); }; pScroll.removeEventListener('wheel', handleListWheel); pScroll.addEventListener('wheel', handleListWheel, { passive: false }); pScroll.addEventListener('scroll', () => { requestAnimationFrame(() => updateImgPlistUI(false)); }, { passive: true }); const btnListL = d.querySelector('#pk_img_plist_L'); const btnListR = d.querySelector('#pk_img_plist_R'); if (btnListL) { btnListL.onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: -400, behavior: 'smooth' }); setTimeout(() => updateImgPlistUI(false), 300); }; } if (btnListR) { btnListR.onclick = (e) => { e.stopPropagation(); pScroll.scrollBy({ left: 400, behavior: 'smooth' }); setTimeout(() => updateImgPlistUI(false), 300); }; } setTimeout(() => updateImgPlistUI(false), 100); const goPrev = () => { if (curIdx > 0) { lastDirection = -1; curIdx--; loadCurrent(); } }; const goNext = () => { if (curIdx < imgList.length - 1) { lastDirection = 1; curIdx++; loadCurrent(); } }; d.querySelector('#pk_img_prev').onclick = (e) => { e.stopPropagation(); goPrev(); }; d.querySelector('#pk_img_next').onclick = (e) => { e.stopPropagation(); goNext(); }; d.focus(); d.addEventListener('keydown', (e) => { if (e.key === 'Escape') { S.closeImageOverlay(); } else if (e.key === 'ArrowLeft') goPrev(); else if (e.key === 'ArrowRight') goNext(); else if (e.key === 'f' || e.key === 'F') { if (btnSearch && btnSearch.style.display !== 'none') btnSearch.click(); } else if (e.key === 'e' || e.key === 'E') { if (pTab) pTab.click(); } else if (e.key === 'm' || e.key === 'M') { e.preventDefault(); if (btnFull) btnFull.click(); } else if (e.key === 'r' || e.key === 'R') { e.preventDefault(); if (!isLongImageMode && btnRot) btnRot.click(); } else if (e.key === 'h' || e.key === 'H') { e.preventDefault(); if (!isLongImageMode && btnMirror) btnMirror.click(); } else if (e.key === 'v' || e.key === 'V') { e.preventDefault(); if (!isLongImageMode && btnFlipV) btnFlipV.click(); } }); try { await loadCurrent(); } catch (e) { console.error(e); } finally { isImageOpening = false; } } let isImageSearchRunning = false; async function startImageSearch(mediaElement, fileName, containerElement, originalLink) { if (isImageSearchRunning) return; isImageSearchRunning = true; try { window.focus(); if (containerElement) containerElement.focus(); } catch (e) {} const L = getStrings(); const isVideo = mediaElement.tagName === 'VIDEO'; const MAX_SIDE = 1000; let progressTask = null; if (typeof FloatBarManager !== 'undefined') { progressTask = FloatBarManager.create(L.str_processing); } if (isVideo && !mediaElement.paused) mediaElement.pause(); try { let finalBlob = null; const cvs = document.createElement('canvas'); const ctx = cvs.getContext('2d'); const drawScaled = (source, srcW, srcH) => { let w = srcW, h = srcH; if (w === 0 || h === 0) throw new Error("Media dimensions not ready"); if (w > MAX_SIDE || h > MAX_SIDE) { const ratio = Math.min(MAX_SIDE / w, MAX_SIDE / h); w = Math.floor(w * ratio); h = Math.floor(h * ratio); } cvs.width = w; cvs.height = h; ctx.drawImage(source, 0, 0, w, h); }; let fetchUrl = originalLink; const isLocalData = fetchUrl && (fetchUrl.startsWith('blob:') || fetchUrl.startsWith('data:')); const BLOB_TYPE = 'image/jpeg'; const BLOB_QUALITY = 0.85; if (isVideo) { const sourceW = mediaElement.videoWidth; const sourceH = mediaElement.videoHeight; drawScaled(mediaElement, sourceW, sourceH); try { finalBlob = await new Promise((resolve, reject) => { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Empty")), BLOB_TYPE, BLOB_QUALITY); }); } catch (err) { throw new Error("Tainted: Video CORS Blocked"); } } else { let canvasSuccess = false; try { const sourceW = mediaElement.naturalWidth || mediaElement.width; const sourceH = mediaElement.naturalHeight || mediaElement.height; if (sourceW > 0 && sourceH > 0) { drawScaled(mediaElement, sourceW, sourceH); finalBlob = await new Promise((resolve, reject) => { try { cvs.toBlob(b => b ? resolve(b) : reject(new Error("Tainted")), BLOB_TYPE, BLOB_QUALITY); } catch (e) { reject(e); } }); canvasSuccess = !!finalBlob; } } catch (canvasErr) {} if (!canvasSuccess && fetchUrl && !isLocalData) { 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) { 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) { await new Promise(r => setTimeout(r, 1500)); } } } } } if (!finalBlob) { throw new Error(L.err_network_break); } if (finalBlob.size > 8 * 1024 * 1024) { 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) {} } } if (!finalBlob) throw new Error(L.err_capture); const uploadAndGetUrl = async () => { if (typeof GM_xmlhttpRequest === 'undefined') throw new Error("Missing GM_xmlhttpRequest"); const FAST_TIMEOUT = 4000; const uploadTask = (url, formData, parseType, stageText) => { return new Promise((resolve, reject) => { if (progressTask) progressTask.update(stageText); GM_xmlhttpRequest({ method: "POST", url: url, data: formData, timeout: FAST_TIMEOUT, responseType: parseType === 'json' ? 'json' : 'text', onload: (res) => { if (res.status === 200) resolve(res); else reject(new Error(`HTTP ${res.status}`)); }, onerror: () => reject(new Error("Network Error")), ontimeout: () => reject(new Error("Timeout")) }); }); }; const tryNode1 = async () => { const fd = new FormData(); fd.append('files[]', finalBlob, `pk_1.jpg`); const res = await uploadTask( "https://uguu.se/upload.php", fd, 'json', L.str_upload_1 ); if (res.response && res.response.success && res.response.files?.[0]?.url) { return res.response.files[0].url; } throw new Error("Uguu API Error"); }; const tryNode2 = async () => { 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 () => { 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_2 ); 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 hosts failed:", e1, e2, e3); throw new Error("All Upload Hosts Failed"); } } } }; try { const imgUrl = await uploadAndGetUrl(); if (!imgUrl || !imgUrl.startsWith('http')) { throw new Error("Invalid URL returned."); } const currentEngine = gmGet('pk_search_engine', 'google'); const encUrl = encodeURIComponent(imgUrl); let jumpUrl = ''; let engineName = ''; switch (currentEngine) { case 'yandex': jumpUrl = `https://yandex.com/images/search?rpt=imageview&url=${encUrl}`; engineName = 'Yandex'; break; case 'saucenao': jumpUrl = `https://saucenao.com/search.php?db=999&url=${encUrl}`; engineName = 'SauceNAO'; break; case 'tracemoe': { const cleanUrl = imgUrl.replace(/^https?:\/\//, ''); const proxyUrl = `https://wsrv.nl/?url=${cleanUrl}&output=jpg`; jumpUrl = `https://trace.moe/?url=${encodeURIComponent(proxyUrl)}`; engineName = 'trace.moe'; break; } case 'google': default: jumpUrl = `https://lens.google.com/uploadbyurl?url=${encUrl}`; engineName = 'Google Lens'; break; } if (progressTask) { progressTask.update(L.str_redirecting.replace('Google Lens', engineName)); const el = document.querySelector('.pk-float-bar-item .pk-spin-lg'); if (el) el.style.borderColor = '#4CAF50'; } await sleep(600); window.open(jumpUrl, '_blank'); } catch (err) { if (progressTask) progressTask.update(L.str_upload_fail_copy); let fallbackBlob = null; try { const bmp = await createImageBitmap(finalBlob); const tmpCvs = document.createElement('canvas'); tmpCvs.width = bmp.width; tmpCvs.height = bmp.height; tmpCvs.getContext('2d').drawImage(bmp, 0, 0); bmp.close(); fallbackBlob = await new Promise(r => tmpCvs.toBlob(r, 'image/png')); } catch (e) { fallbackBlob = await new Promise(r => cvs.toBlob(r, 'image/png')); } const MAX_CLIPBOARD_SIZE = 19.5 * 1024 * 1024; if (fallbackBlob.size > MAX_CLIPBOARD_SIZE) { 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) {} } try { const item = new ClipboardItem({ 'image/png': fallbackBlob }); await navigator.clipboard.write([item]); } catch (clipErr) { console.error("Clipboard write failed:", clipErr); if (progressTask) progressTask.update(L.err_clipboard_failed); await sleep(1000); } const ctrlVPhrase = (navigator.platform.toUpperCase().indexOf('MAC') >= 0) ? 'Cmd+V' : 'Ctrl+V'; const hintText = L.msg_manual_paste.replace('{cmd}', `${ctrlVPhrase}`); if (progressTask) progressTask.destroy(); const fallbackOv = document.createElement('div'); fallbackOv.className = 'pk-search-running-mask'; fallbackOv.style.cssText = 'position:absolute; inset:0; z-index:2147483647; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; flex-direction:column; gap:20px; border-radius:inherit;'; fallbackOv.innerHTML = `
⚠️
${L.msg_copy_success}
${hintText}
`; const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (fsEl) { fsEl.appendChild(fallbackOv); } else if (containerElement) { containerElement.appendChild(fallbackOv); } else { document.body.appendChild(fallbackOv); } await sleep(2500); fallbackOv.remove(); const currentEngine = gmGet('pk_search_engine', 'google'); let manualUrl = ''; switch (currentEngine) { case 'yandex': manualUrl = 'https://yandex.com/images/'; break; case 'saucenao': manualUrl = 'https://saucenao.com/'; break; case 'tracemoe': manualUrl = 'https://trace.moe/'; break; case 'google': default: manualUrl = 'https://lens.google.com/upload'; break; } window.open(manualUrl, '_blank'); } } catch (e) { console.error("Search Error:", e); if (progressTask) progressTask.destroy(); const errorMsg = e.message.includes('Tainted') ? L.err_cors_blocked : e.message; if (typeof showToast !== 'undefined') { showToast(`${errorMsg}`, 'error'); } } finally { if (progressTask) progressTask.destroy(); isImageSearchRunning = false; } } const FILTER_EXTS = { video: ['mp4','mkv','avi','mov','wmv','flv','webm','ts','m4v','3gp','mpg','mpeg','rm','rmvb','asf','vob','dat','divx','f4v','m2ts','mts','tp','trp','ogv','mpe','m2v','m3u8'], audio: ['mp3','wav','flac','aac','ogg','wma','ape','m4a','amr','opus','m4b','alac','aiff','mid','midi','ra','dts','ac3','dsf','dff'], image: ['jpg','jpeg','png','gif','bmp','webp','svg','tif','tiff','ico','heic','heif','raw','cr2','nef','arw','dng','orf','avif','psd','ai','eps','jfif','jpe'], document: ['txt','html','pdf','pptx','chm','docx','xlsx','htm','doc','dwg','mdb','ppt','xls','rtf','odt','ods','odp','epub','mobi','azw3','djvu','cbz','cbr','md','log','csv','xml','json'], software: ['apk','exe','ipa','dmg','rpm','deb','msi','pkg','xapk','apks','aab','jar','bin','sh','bat','cmd'], archive: ['zip','rar','7z','tar','gz','iso','cab','bz2','xz','tgz','wim','esd','img','zst','lzh'], torrent: ['torrent'] }; const FILTER_NAMES = { video: L.cat_video, audio: L.cat_audio, image: L.cat_image, document: L.cat_document, software: L.cat_software, archive: L.cat_archive, torrent: L.cat_torrent, other: L.cat_other }; const fIcons = { all: ``, video: ``, audio: ``, image: ``, document: ``, software: ``, archive: ``, torrent: ``, other: `` }; const isShareInsightFilterContext = () => !!(S.shareParseMode && S.shareParseInsightMode); const getCurrentFilterState = () => { if (isShareInsightFilterContext()) { if (!S.shareParseInsightFilterState) S.shareParseInsightFilterState = { active: false, cat: 'all', ext: 'all' }; return S.shareParseInsightFilterState; } if (!S.filterState) S.filterState = { active: false, cat: 'all', ext: 'all' }; return S.filterState; }; const commitFilterChange = () => { S.sel.clear(); refresh(); }; const renderActiveFilterUI = () => { if (!UI.filterBar) return; const state = getCurrentFilterState(); const cat = state.cat; if (cat === 'all') return; UI.filterCatLabel.textContent = FILTER_NAMES[cat] || (L.cat_other); if (cat === 'other') { UI.filterExtsWrap.style.display = 'none'; } else { UI.filterExtsWrap.style.display = 'flex'; const exts = FILTER_EXTS[cat] ||[]; const mainExts = exts.slice(0, 3); const moreExts = exts.slice(3); let html = `${L.cat_all}`; let displayExts = [...mainExts]; if (state.ext !== 'all' && moreExts.includes(state.ext)) { displayExts[2] = state.ext; } displayExts.forEach(e => { html += `${e}`; }); UI.filterExtsMain.innerHTML = html; if (moreExts.length > 0) { UI.filterExtsMoreBtn.style.display = 'flex'; } else { UI.filterExtsMoreBtn.style.display = 'none'; } UI.filterExtsMain.querySelectorAll('.pk-f-ext').forEach(span => { span.onclick = (e) => { e.stopPropagation(); getCurrentFilterState().ext = span.dataset.ext; renderActiveFilterUI(); commitFilterChange(); }; }); } }; 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; `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); const state = getCurrentFilterState(); pop.innerHTML = `
${L.title_file_filter}
${fIcons.all} ${L.cat_all}
${fIcons.video} ${L.cat_video}
${fIcons.audio} ${L.cat_audio}
${fIcons.image} ${L.cat_image}
${fIcons.document} ${L.cat_document}
${fIcons.software} ${L.cat_software}
${fIcons.archive} ${L.cat_archive}
${fIcons.torrent} ${L.cat_torrent}
${fIcons.other} ${L.cat_other}
`; document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const rect = getLogicalRect(triggerEl); let popLeft = rect.left; if (popLeft + 420 > window.innerWidth) popLeft = window.innerWidth - 430; pop.style.top = (rect.bottom + 5) + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; pop.querySelectorAll('.pk-fc-btn').forEach(btn => { btn.onclick = (ev) => { ev.stopPropagation(); const cat = btn.dataset.cat; const st = getCurrentFilterState(); st.cat = cat; st.ext = 'all'; st.active = (cat !== 'all'); cleanup(); if (st.active) { renderActiveFilterUI(); } commitFilterChange(); }; }); 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; `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); const state = getCurrentFilterState(); const exts = FILTER_EXTS[state.cat] ||[]; const mainExts = exts.slice(0, 3); const moreExts = exts.slice(3); let displayExts = [...mainExts]; if (state.ext !== 'all' && moreExts.includes(state.ext)) { displayExts[2] = state.ext; } const dropdownExts = exts.filter(ex => !displayExts.includes(ex)); pop.innerHTML = dropdownExts.map(ex => `${ex}`).join(''); document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const rect = getLogicalRect(UI.filterExtsWrap); let popLeft = rect.left; if (popLeft + 340 > window.innerWidth) popLeft = window.innerWidth - 350; pop.style.top = (rect.bottom + 5) + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; pop.querySelectorAll('.pk-f-ext').forEach(span => { span.onclick = (ev) => { ev.stopPropagation(); getCurrentFilterState().ext = span.dataset.ext; cleanup(); renderActiveFilterUI(); commitFilterChange(); }; }); 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 = () => { const st = getCurrentFilterState(); st.active = false; st.cat = 'all'; st.ext = 'all'; UI.filterBtn.style.display = 'flex'; UI.filterActiveUI.style.display = 'none'; commitFilterChange(); }; } const getHistory = () => { try { return JSON.parse(gmGet('pk_search_history', '[]')); } catch { return []; } }; const saveHistory = (txt) => { if (!txt) return; let list = getHistory(); list = list.filter(x => x !== txt); list.unshift(txt); if (list.length > 3) list = list.slice(0, 3); gmSet('pk_search_history', JSON.stringify(list)); }; const renderHistory = () => { const list = getHistory(); if (list.length === 0) { UI.searchHist.style.display = 'none'; return; } let html = `
${L.title_search_hist}${L.btn_clear_hist}
`; list.forEach(txt => { html += `
${esc(txt)}
`; }); UI.searchHist.innerHTML = html; UI.searchHist.style.display = 'flex'; UI.searchHist.querySelector('#pk-hist-del').onclick = (e) => { e.stopPropagation(); gmSet('pk_search_history', '[]'); UI.searchHist.style.display = 'none'; }; UI.searchHist.querySelectorAll('.pk-select-item').forEach(el => { el.onclick = (e) => { const val = el.querySelector('span').textContent; UI.searchInput.value = val; performSearch(val); UI.searchHist.style.display = 'none'; }; }); }; const performSearch = (val) => { const txt = val.trim(); if (!txt) { if (S.search && UI.searchClear) UI.searchClear.click(); return; } const isGlobal = UI.chkGlobal && UI.chkGlobal.checked && !S.uploadMode; S.lastSearchGlobal = !!isGlobal; S.keepVisualOnNextForceLoad = false; const hadActiveLiveRefresh = !!(S.liveRefreshCtx && (S.liveRefreshCtx.status === 'refreshing' || S.liveRefreshCtx.status === 'interrupted')); stopLiveManualRefreshBeforePathChange('search_submit'); if (hadActiveLiveRefresh) { S.pagingLoading = false; setLoad(false); updateStat(); } if (txt) { saveHistory(txt); if (!isGlobal) S.lastGlobalResults = []; if (isGlobal && !S.preSearchPath) { S.preSearchPath = [...S.path]; } if (isGlobal) { S.sort = 'modified_time'; S.dir = 1; S.lastGlobalResults = []; stopLiveManualRefreshBeforePathChange('path_change'); S.path = [ { id: '', name: L.btn_nav_home }, { id: 'virtual_search_root', name: L.str_search_results } ]; renderCrumb(); } } S.search = txt; syncLocalUploadVisibility(); if (S.dupMode) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.clearSelection(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); updateStat(); } else if (isGlobal) { if (globalNeedsSync) { setLoad(true); updateLoadTxt(L.str_analyzing); setTimeout(async () => { if (!S.scanning) { S.scanning = true; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); if (UI.chkGlobal) UI.chkGlobal.checked = false; }; try { await runFlattenScanOperation(true, [], true); globalNeedsSync = false; } catch (e) { console.error("[GlobalSearch] Sync Error:", e); } finally { S.scanning = false; } } load(false, true).finally(() => setLoad(false)); }, 50); } else { load(false, false); } } else { refresh(); } UI.searchClear.style.display = txt ? 'flex' : 'none'; if (UI.searchHist && typeof UI.searchHist._pkClose === 'function') UI.searchHist._pkClose(); else if (UI.searchHist) 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(); } }; bindPlainTextMaxLengthLimits(el, [['#pk-search-input', ((getConfigLimitSchema('pk_search_history') || {}).localLimit || {}).maxFieldLen || 1024]]); const searchHist = UI.searchHist; const searchWrap = UI.searchInput.closest('.pk-search') || UI.searchInput.parentNode; let searchHistRaf = 0; let searchHistOpen = false; if (searchHist) { searchHist.style.display = 'none'; searchHist.style.visibility = ''; searchHist.style.pointerEvents = ''; } const isSearchHistOpen = () => { return !!searchHist && searchHistOpen && getHistory()?.length > 0; }; const requestPlaceSearchHist = () => { if (!isSearchHistOpen()) return; if (searchHistRaf) cancelAnimationFrame(searchHistRaf); searchHistRaf = requestAnimationFrame(() => { searchHistRaf = requestAnimationFrame(() => { searchHistRaf = 0; if (isSearchHistOpen()) placeSearchHist(); }); }); }; const placeSearchHist = () => { if (!searchHist || !searchWrap) return; if (!searchHist._pkOriginParent) searchHist._pkOriginParent = searchWrap; if (searchHist.parentNode !== document.body) { document.body.appendChild(searchHist); } searchHist.classList.toggle('pk-dark', el.classList.contains('pk-dark')); const wrapRect = typeof getLogicalRect === 'function' ? getLogicalRect(searchWrap) : searchWrap.getBoundingClientRect(); const pad = 8; searchHist.dataset.pkPortal = '1'; searchHist.style.position = 'fixed'; searchHist.style.left = '0'; searchHist.style.top = '0'; searchHist.style.right = 'auto'; searchHist.style.bottom = 'auto'; searchHist.style.marginTop = '0'; searchHist.style.zIndex = '10080'; searchHist.style.transformOrigin = 'top left'; searchHist.style.width = Math.max(Math.round(wrapRect.width), 220) + 'px'; searchHist.style.display = 'flex'; searchHist.style.visibility = 'hidden'; searchHist.style.pointerEvents = 'none'; const histRect = searchHist.getBoundingClientRect(); const popW = histRect.width; const popH = histRect.height; const winW = window.innerWidth; const winH = window.innerHeight; let left = wrapRect.left; let top = wrapRect.bottom + 6; if (left + popW > winW - pad) left = winW - pad - popW; if (left < pad) left = pad; if (top + popH > winH - pad) { top = wrapRect.top - popH - 6; } if (top < pad) top = pad; searchHist.style.left = Math.round(left) + 'px'; searchHist.style.top = Math.round(top) + 'px'; searchHist.style.visibility = ''; searchHist.style.pointerEvents = ''; }; const closeSearchHist = () => { if (!searchHist) return; searchHistOpen = false; if (searchHistRaf) cancelAnimationFrame(searchHistRaf); searchHistRaf = 0; searchHist.style.display = 'none'; searchHist.style.visibility = ''; searchHist.style.pointerEvents = ''; }; if (searchHist) searchHist._pkClose = closeSearchHist; UI.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isSearchHistOpen()) { e.preventDefault(); e.stopPropagation(); closeSearchHist(); } }); UI.searchInput.onfocus = () => { renderHistory(); if (getHistory()?.length > 0) { searchHistOpen = true; searchHist.style.display = 'flex'; requestPlaceSearchHist(); } else { closeSearchHist(); } }; document.addEventListener('mousedown', (e) => { if (!UI || !UI.searchInput || !searchHist) return; if (!searchHist.contains(e.target) && !searchWrap.contains(e.target)) { closeSearchHist(); } }, true); if (!window.__pkSearchHistPortalBound) { window.addEventListener('resize', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, { passive: true }); document.addEventListener('scroll', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, true); if (window.visualViewport) { window.visualViewport.addEventListener('resize', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, { passive: true }); window.visualViewport.addEventListener('scroll', () => { if (isSearchHistOpen()) requestPlaceSearchHist(); }, { passive: true }); } window.__pkSearchHistPortalBound = true; } if (window.ResizeObserver && !searchWrap.__pkSearchHistRO) { searchWrap.__pkSearchHistRO = new ResizeObserver(() => { if (!searchHistOpen) return; if (isSearchHistOpen()) requestPlaceSearchHist(); }); searchWrap.__pkSearchHistRO.observe(searchWrap); } UI.searchInput.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') { performSearch(e.target.value); } }; if (UI.searchBtn) { UI.searchBtn.onclick = () => { performSearch(UI.searchInput.value); }; } if (UI.searchClear) { UI.searchClear.onclick = async () => { const wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; if (!S.search && UI.searchInput.value) { UI.searchInput.value = ''; UI.searchClear.style.display = 'none'; UI.searchInput.focus(); return; } const searchWasActive = !!S.search; const isInVirtualStack = S.path.some(node => node.id === 'virtual_search_root'); let requiresReload = false; UI.searchInput.value = ''; S.search = ''; S.lastGlobalResults = []; S.lastSearchGlobal = false; syncLocalUploadVisibility(); try { const prefStore = JSON.parse(gmGet('pk_folder_sort_prefs', '{}')); if (prefStore['virtual_search_root']) { delete prefStore['virtual_search_root']; gmSet('pk_folder_sort_prefs', JSON.stringify(prefStore)); } } catch(e) {} if ((wasGlobalChecked || isInVirtualStack) && searchWasActive) { const currentFolder = S.path[S.path.length - 1]; if (currentFolder.id !== 'virtual_search_root' && currentFolder.id !== '') { const traceStack = []; let ptrId = currentFolder.id; let ptrName = currentFolder.name; let safety = 100; traceStack.unshift({ id: ptrId, name: ptrName }); while (ptrId && ptrId !== 'root' && safety > 0) { if (globalParentIndex.has(ptrId)) { const parent = globalParentIndex.get(ptrId); ptrId = parent.id; ptrName = parent.name; if (ptrId === 'root' || ptrId === '') break; traceStack.unshift({ id: ptrId, name: ptrName }); } else { const node = S.itemMap.get(ptrId); if (node && node._lineage && node._lineage.length > 0) { const ancestors = node._lineage.filter(x => x.id !== '' && x.id !== 'root'); for (let k = ancestors.length - 1; k >= 0; k--) { traceStack.unshift(ancestors[k]); } } break; } safety--; } const realPath = [{ id: '', name: L.btn_nav_home }, ...traceStack]; stopLiveManualRefreshBeforePathChange('path_change'); S.path = realPath; } else if (isInVirtualStack || S.preSearchPath) { stopLiveManualRefreshBeforePathChange('path_change'); S.path = S.preSearchPath ? [...S.preSearchPath] : [{ id: '', name: L.btn_nav_home }]; } requiresReload = true; } S.preSearchPath = null; UI.searchClear.style.display = 'none'; UI.searchHist.style.display = 'none'; if (S.dupMode && !isInVirtualStack) { if (S.pinnedDupPath) { S.pinnedDupPath = null; S.clearSelection(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); updateStat(); } else if (S.isFlattened && !isInVirtualStack) { refresh(); updateStat(); } else { if (requiresReload) { const targetNode = S.path[S.path.length - 1]; const targetKey = S.getRealCacheKey(targetNode.id); const cachedData = (typeof globalCache !== 'undefined') ? (globalCache.get(targetKey) || globalCache.get('')) : null; if (cachedData) { if (cachedData.items) S.items = [...cachedData.items]; else if (Array.isArray(cachedData)) S.items = [...cachedData]; S.itemMap.clear(); for (const it of S.items) S.itemMap.set(it.id, it); } refresh(); setLoad(true); const p = load(false, true); if (UI.chkGlobal) UI.chkGlobal.checked = wasGlobalChecked; await p; } else { refresh(); updateStat(); } consumeOfflineLightProbePendingWrite('search_clear'); } }; } } const guardGlobalSearchResultCheckboxLock = (e) => { if (!isGlobalSearchResultContext()) return false; if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (e && typeof e.stopPropagation === 'function') e.stopPropagation(); if (UI.chkGlobal) UI.chkGlobal.checked = true; syncGlobalSearchCheckboxState(); return true; }; if (UI.lblGlobal) UI.lblGlobal.addEventListener('click', guardGlobalSearchResultCheckboxLock, true); if (UI.chkGlobal) UI.chkGlobal.addEventListener('click', guardGlobalSearchResultCheckboxLock, true); UI.chkGlobal.onchange = async (e) => { if (guardGlobalSearchResultCheckboxLock(e)) return; if (e.target.checked) { if (S.movingIds && S.movingIds.size > 0) { e.target.checked = false; showAlert(L.msg_op_blocked_moving); return; } const isSuppressed = gmGet('pk_suppress_global_warn', false); if (!isSuppressed && !hasShownGlobalWarnSession) { const userChoice = await new Promise((resolve) => { const m = showModal(`

${L.title_confirm}

${esc(L.msg_global_warn).replace(/\n/g, '
')}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '420px', padding: '30px', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } m.querySelector('#cfm_cancel').onclick = () => { m.remove(); resolve({ ok: false }); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve({ ok: false }); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#cfm_ok').click(); } }); m.querySelector('#cfm_ok').onclick = () => { const isChecked = m.querySelector('#pk_warn_ignore').checked; m.remove(); resolve({ ok: true, suppress: isChecked }); }; }); if (!userChoice.ok) { e.target.checked = false; return; } hasShownGlobalWarnSession = true; if (userChoice.suppress) { gmSet('pk_suppress_global_warn', true); } } if (!isGlobalIndexReady || globalNeedsSync) { S.scanning = true; UI.stopBtn.onclick = () => { S.scanning = false; updateLoadTxt(L.str_stopping); UI.chkGlobal.checked = false; }; await runFlattenScanOperation(true); } refresh(); if (S._flattenEnterTopRaf) cancelAnimationFrame(S._flattenEnterTopRaf); if (S.isFlattened && isGridView()) { S._flattenEnterTopRaf = requestAnimationFrame(() => { S._flattenEnterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } } else { if (S.scanning) { S.scanning = false; updateLoadTxt(L.str_stopping); } refresh(); } }; const runFlattenScanOperation = async (isSyncOnly = false, specificTargets =[], isSilent = false) => { S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (!isSyncOnly) { S.virtualSortMaskSig = ''; S.dupResultSig = ''; } 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') { resumeBackgroundDiscovery(); } }, 1000); } }; S.scanning = true; syncLocalUploadVisibility(); try { await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { if (!isSyncOnly) { f._lineage = parent.lineage ||[]; fileMap.set(f.id, f); } }, onFolder: (folder, filesInFolder) => { processedFolders++; indexParents(folder.id, folder.name, filesInFolder); if (typeof globalLineageMap !== 'undefined') { globalLineageMap.set(folder.id, folder.lineage); } }, onProgress: (st) => { const folderText = isPartialScan ? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders) : `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : ""; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo + retryTag); } }); if (S.scanning && !signal.aborted && myScanId === S.scanId) { const didBuildFullGlobalIndex = isSyncOnly && !isPartialScan; if (didBuildFullGlobalIndex) { globalNeedsSync = false; isGlobalIndexReady = true; } if (!isSyncOnly) { updateLoadTxt(L.str_merging); let tempItems = Array.from(fileMap.values()); const rawScanTotal = tempItems.length; const hasActiveScanFilter = !!(S.scanFilter && ((S.scanFilter.keyword || '').trim() || S.scanFilter.minBytes > 0 || S.scanFilter.maxBytes > 0)); if (S.scanFilter && !isSyncOnly) { const { minBytes, maxBytes, keyword } = S.scanFilter; const kwList = keyword ? keyword.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : []; tempItems = tempItems.filter(item => { if (kwList.length > 0) { const fullLowerName = (item.name || "").toLowerCase(); const lastDot = fullLowerName.lastIndexOf('.'); const nameWithoutExt = (item.kind !== 'drive#folder' && lastDot > 0) ? fullLowerName.substring(0, lastDot) : fullLowerName; if (kwList.some(k => nameWithoutExt.includes(k))) return false; } const sz = parseInt(item.size || 0); if (sz < minBytes) return false; if (maxBytes > 0 && sz > maxBytes) return false; return true; }); } const total = tempItems.length; if (!isSyncOnly && !isSilent && total === 0) { setLoad(false); S.scanning = false; showToast(rawScanTotal > 0 && hasActiveScanFilter ? L.msg_scan_filter_empty : L.msg_no_files); UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal) UI.chkGlobal.checked = false; return; } S.items = new Array(total); S.itemMap.clear(); let lastYield = performance.now(); for (let i = 0; i < total; i++) { const item = tempItems[i]; S.items[i] = item; S.itemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(item.id); } if (i % 5000 === 0 && performance.now() - lastYield > 16) { updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`); await sleep(0); lastYield = performance.now(); } } if (!isSilent) { S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; } rememberFolderFirstBeforeStrictMode(); S.isFlattened = true; if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('file_insight'); if (typeof applyResolvedSortState === 'function') applyResolvedSortState('file_insight'); UI.chkAll.checked = false; S.clearSelection(); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important'); updateLoadTxt(L.str_rendering); await refresh(); if (S._flattenEnterTopRaf) cancelAnimationFrame(S._flattenEnterTopRaf); if (S.isFlattened && isGridView()) { S._flattenEnterTopRaf = requestAnimationFrame(() => { S._flattenEnterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } const msg = L.msg_scan_done.replace('{n}', total).replace('{f}', processedFolders); if (!isSilent) showToast(msg.replace(/\n+/g, ' '), 'success', 3200); } } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error_crit}: ${e.message}`); } if (!isSyncOnly && myScanId === S.scanId) { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; UI.lblGlobal.style.display = 'flex'; } if (myScanId === S.scanId) UI.chkGlobal.checked = false; } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; scheduleBackgroundResume(); } } }; 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_op_blocked_moving); return; } S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; const selectedTargets = S.getSelectedIds() .map(id => S.itemMap.get(id)) .filter(Boolean); const lastMin = gmGet('pk_scan_last_min', 0); const lastMax = gmGet('pk_scan_last_max', ''); const lastUnit = gmGet('pk_scan_last_unit', 'MB'); const lastKeyword = gmGet('pk_scan_last_keyword', ''); let currentStrict = gmGet('pk_dup_strictness', 'strict'); const scanTxt = L.btn_scan; const dupTxt = L.tip_dup; const L_min = L.lbl_ana_min; const L_max = L.lbl_ana_max; let scanTargetDesc = selectedTargets.length > 0 ? L.lbl_scan_selected.replace('{n}', selectedTargets.length) : L.lbl_scan_current; let dupTargetDesc = selectedTargets.length > 0 ? L.lbl_dup_selected.replace('{n}', selectedTargets.length) : L.lbl_dup_current; const m = showModal(`

${L.title_file_analysis}

${scanTxt}
${dupTxt}
${scanTargetDesc}
${L.lbl_keyword_filter}
${L_min}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
-
${L_max}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${lastUnit}
MB
GB
TB
${dupTargetDesc}
${L.label_dup_strictness}
${currentStrict === 'loose' ? L.opt_loose : L.opt_strict}${CONF.crumbIcons.down}
${L.opt_strict}
${L.opt_loose}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: 'auto', padding: '0', overflow: 'visible', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } bindPlainTextMaxLengthLimits(m, [['#sc_keyword', ((getConfigLimitSchema('pk_scan_last_keyword') || {}).localLimit || {}).maxItemLen || 2048]]); 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; const syncLimitBorder = (inp) => { if (!inp) return; const raw = String(inp.value || '').trim(); const active = raw && (inp !== inpMin || Number(raw) !== 0); inp.style.borderColor = active ? 'var(--pk-pri)' : 'var(--pk-bd)'; }; [inpMin, inpMax].forEach(inp => { syncLimitBorder(inp); inp.addEventListener('input', () => syncLimitBorder(inp)); }); m.querySelector('#sc_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#sc_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#sc_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; inpMax.dispatchEvent(new Event('input')); }; m.querySelector('#sc_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); inpMax.dispatchEvent(new Event('input')); }; 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.scanning = true; syncLocalUploadVisibility(); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); if (S.isFlattened) { UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; } else { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal) UI.chkGlobal.checked = false; S.isFlattened = false; setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { resumeBackgroundDiscovery(); } }, 1000); } }; S.lastScanTargets = selectedTargets; await runFlattenScanOperation(false, selectedTargets, false); } else { S.dupConfig = { video: m.querySelector('#scan_video').checked, image: m.querySelector('#scan_image').checked, other: m.querySelector('#scan_other').checked }; S.dupResultSig = ''; S.virtualSortMaskSig = ''; if (!S.dupConfig.video && !S.dupConfig.image && !S.dupConfig.other) return; m.remove(); S.scanning = true; syncLocalUploadVisibility(); S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; let fileMap = new Map(); let processedFolders = 0; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; setLoad(true); const isPartialScan = selectedTargets.length > 0; let rootNodes =[]; if (isPartialScan) { updateLoadTxt(L.msg_init_scan_sel); selectedTargets.forEach(item => { if (item.kind === 'drive#folder') { rootNodes.push({ id: item.id, name: item.name, lineage:[{ id: item.id, name: item.name }], retryCount: 0 }); } else { item._lineage =[]; fileMap.set(item.id, item); } }); } else { updateLoadTxt(`${L.str_scanning} 0`); const startNode = S.path[S.path.length - 1]; rootNodes =[{ id: startNode.id || '', name: startNode.name || 'Root', lineage: [], retryCount: 0 }]; } UI.stopBtn.onclick = () => { S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); setLoad(false); }; try { await coreRecursiveEngine(rootNodes, { signal: signal, onFile: (f, parent) => { f._lineage = parent.lineage ||[]; fileMap.set(f.id, f); }, onFolder: (folder, filesInFolder) => { processedFolders++; if (typeof globalCache !== 'undefined' && !globalCache.has(folder.id)) { globalCache.set(folder.id, [...filesInFolder]); } indexParents(folder.id, folder.name, filesInFolder); if (typeof globalLineageMap !== 'undefined') { globalLineageMap.set(folder.id, folder.lineage); } }, onProgress: (st) => { const folderText = isPartialScan ? L.status_scanning_selection.replace('{n}', st.folders + " " + L.unit_folders) : `${L.str_scanning} ${st.folders} ${L.unit_folders}`; const retryTag = st.isRetrying ? `\n[ ${L.str_retries} ]` : ""; const statusInfo = ` | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`; updateLoadTxt(folderText + statusInfo + retryTag); } }); if (S.scanning && !signal.aborted && myScanId === S.scanId) { updateLoadTxt(L.str_merging); const tempItems = Array.from(fileMap.values()); const total = tempItems.length; const tempDupCfg = S.dupConfig || { video: true, image: false, other: false }; updateLoadTxt(L.loading_dup.replace('{p}', 0)); await sleep(20); S.dupReasons.clear(); const tempDupCandidates = 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 && tempDupCfg.video) return true; if (isImage && tempDupCfg.image) return true; if (isOther && tempDupCfg.other) return true; return false; }); const tempDupGroups = await computeDuplicateGroups(tempDupCandidates, tempDupCfg, () => S.scanning && !signal.aborted && myScanId === S.scanId); if (!S.scanning || signal.aborted || myScanId !== S.scanId) return; tempDupGroups.sort((a, b) => { const getPriority = (t) => { if (t === L.tag_hash) return 3; if (t === L.tag_sim) return 2; if (t === L.tag_name) return 1; return 0; }; const pA = getPriority(a.type); const pB = getPriority(b.type); if (pA !== pB) return pB - pA; return b.ids.length - a.ids.length; }); const { dupItemMap: preDupItemMap, dupBuckets: preDupBuckets } = buildDuplicateBucketsFromGroups(tempDupGroups, tempItems); if (preDupBuckets.all.length === 0) { S.dupReasons.clear(); S.scanning = false; setLoad(false); showToast(L.msg_dup_none); syncLocalUploadVisibility(); return; } S._precomputedDupBuckets = { dupItemMap: preDupItemMap, dupBuckets: preDupBuckets, dupReasons: new Map(S.dupReasons) }; S.items = new Array(total); S.itemMap.clear(); let lastYield = performance.now(); for (let i = 0; i < total; i++) { const item = tempItems[i]; S.items[i] = item; S.itemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { S.starredSet.add(item.id); } if (i % 5000 === 0 && performance.now() - lastYield > 16) { updateLoadTxt(`${L.str_merging} ${Math.round((i / total) * 100)}%`); await sleep(0); lastYield = performance.now(); } } rememberFolderFirstBeforeStrictMode(); S.dupMode = true; if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode('file_dup'); S.groupSortOp = 'path'; S.groupSortDir = 1; S.isFlattened = false; S.sort = 'modified_time'; S.dir = 1; UI.chkAll.checked = false; S.clearSelection(); UI.scan.style.display = 'none'; UI.btnExit.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; UI.lblGlobal.style.display = 'none'; UI.chkGlobal.checked = false; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.crumb) UI.crumb.style.setProperty('display', 'none', 'important'); if (UI.lblSearchPath) UI.lblSearchPath.style.display = 'flex'; updateLoadTxt(L.str_rendering); S.display = [...S.items]; await refresh(); if (S._enterTopRaf) cancelAnimationFrame(S._enterTopRaf); S._enterTopRaf = requestAnimationFrame(() => { S._enterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); } } catch (e) { if (e.name !== 'AbortError' && myScanId === S.scanId) { showAlert(`${L.str_error_crit}: ${e.message}`); } if (myScanId === S.scanId) { UI.scan.style.display = 'flex'; UI.btnExit.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.btnExport) UI.btnExport.style.display = 'flex'; UI.lblGlobal.style.display = 'flex'; } } finally { if (myScanId === S.scanId) { setLoad(false); S.scanning = false; S.scanAbortController = null; scheduleBackgroundResume(); } } } }; }; 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) { const useGridVisualGuard = isGridView() && UI.win; if (useGridVisualGuard) { UI.win.classList.add('pk-grid-scrolling', 'pk-view-switching'); if (typeof markGridScrolling === 'function') markGridScrolling(); } if (S.pinnedDupPath) { S.pinnedDupPath = null; S.clearSelection(); UI.selDupFolder.value = ""; const invertChk = document.getElementById('pk-dup-invert'); if(invertChk) { invertChk.checked = false; invertChk.disabled = true; invertChk.parentNode.style.opacity = '0.5'; } } renderDupView(); if (useGridVisualGuard) { if (typeof markGridScrolling === 'function') markGridScrolling(); requestAnimationFrame(() => { requestAnimationFrame(() => { if (UI.win) UI.win.classList.remove('pk-view-switching'); }); }); } } }; UI.chkName.onchange = onDupFilterChange; UI.chkSim.onchange = onDupFilterChange; UI.chkHash.onchange = onDupFilterChange; if (UI.chkSearchPath) { UI.chkSearchPath.onchange = () => { if (S.shareParseMode && S.shareParseInsightMode) { syncShareParseInsightMirror(); renderList(); updateStat(); return; } if (S.dupMode && S.search) { renderDupView(); } else if ((S.isFlattened || S.analyzeMode) && S.search) { refresh(); } }; } UI.btnExit.onclick = async () => { await S.exitVirtualNavMode(); if (typeof S.wasGlobalChecked !== 'undefined' && UI.chkGlobal) { UI.chkGlobal.checked = S.wasGlobalChecked; } setTimeout(() => { if (typeof resumeBackgroundDiscovery === 'function') { resumeBackgroundDiscovery(); } }, 1500); }; UI.cols.forEach(c => c.onclick = () => { if (S.dupMode) return; const cur = S.path[S.path.length - 1] || { id: '' }; if (S.analyzeMode && cur.id === 'analyze_root' && S.analyzeSimGroups) return; const k = c.dataset.k; applyManualSortSelection(k); }); if (UI.btnFolderFirst) { S.renderFolderFirst = () => { if (UI.btnFolderFirst) UI.btnFolderFirst.style.color = S.folderFirst ? 'var(--pk-pri)' : '#666'; if (UI.btnGridFolderFirst) UI.btnGridFolderFirst.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(); toggleFolderFirst(); }; } const btnInvert = document.getElementById('pk-btn-invert'); if (btnInvert) { btnInvert.onclick = (e) => { e.stopPropagation(); if (S.display.length === 0) return; S.invertSelection(); renderVisible(); updateStat(); }; btnInvert.onmouseenter = () => btnInvert.style.color = 'var(--pk-pri)'; btnInvert.onmouseleave = () => btnInvert.style.color = 'var(--pk-fg)'; } S.handleSelectAll = (e) => { if (!e || !e.target) { if (UI.chkAll) UI.chkAll.checked = !UI.chkAll.checked; } S.activeId = null; S.lastSelIdx = -1; const totalVisible = S.getSelectableCount(); const selectedCount = S.getSelectedCount(); const isSelectAllAction = selectedCount < totalVisible; if (UI.chkAll) { UI.chkAll.checked = isSelectAllAction; UI.chkAll.indeterminate = false; } if (isSelectAllAction) { S.setAllSelection(true); } else { S.setAllSelection(false); } requestAnimationFrame(() => { renderVisible(); updateStat(); if (UI.chkAll) { UI.chkAll.checked = isSelectAllAction; UI.chkAll.indeterminate = false; } }); }; if (UI.btnAnaSelect || UI.btnAnaSort || UI.btnDupSmart || UI.btnDupSort) { const pop = document.createElement('div'); pop.className = 'pk-ana-pop'; pop.innerHTML = `
${L.opt_keep_new}
${L.opt_keep_old}
${L.opt_keep_large}
${L.opt_keep_small}
${L.opt_keep_short}
${L.opt_keep_long}
`; const sortPop = document.createElement('div'); sortPop.className = 'pk-ana-pop'; sortPop.innerHTML = `
${L.opt_sort_time}
${L.opt_sort_size}
${L.opt_sort_path}
${L.opt_sort_name}
`; UI.win.appendChild(pop); UI.win.appendChild(sortPop); let activeTargetBtn = null; let activePop = null; const updatePopPos = () => { if (!activePop || activePop.style.display !== 'flex' || !activeTargetBtn) return; const winRect = getLogicalRect(UI.win); const btnRect = getLogicalRect(activeTargetBtn); let left = btnRect.left - winRect.left; const top = btnRect.bottom - winRect.top; const winWidth = winRect.width; const popWidth = activePop === sortPop ? 200 : 340; if (left + popWidth > winWidth - 10) { left = btnRect.right - winRect.left - popWidth; } activePop.style.left = Math.max(10, left) + 'px'; activePop.style.top = (top + 5) + 'px'; }; const togglePop = (e, btn, targetPop) => { e.stopPropagation(); const isVisible = (targetPop.style.display === 'flex' && activeTargetBtn === btn); if (activePop && activePop !== targetPop) activePop.style.display = 'none'; if (isVisible) { targetPop.style.display = 'none'; activeTargetBtn = null; activePop = null; } else { if (targetPop === pop && S.analyzeMode && !S.hasShownAnaWarn) { showToast(L.msg_ana_warn, 'warning', 6000); S.hasShownAnaWarn = true; } if (targetPop === sortPop) { if (!S.groupSortOp) { S.groupSortOp = 'path'; S.groupSortDir = 1; } sortPop.querySelectorAll('.pk-sort-opt').forEach(el => { let baseTxt = ""; if (el.dataset.op === 'time') baseTxt = L.opt_sort_time; if (el.dataset.op === 'size') baseTxt = L.opt_sort_size; if (el.dataset.op === 'path') baseTxt = L.opt_sort_path; if (el.dataset.op === 'name') baseTxt = L.opt_sort_name; if (el.dataset.op === S.groupSortOp) { el.textContent = baseTxt + (S.groupSortDir === 1 ? ' ▼' : ' ▲'); el.style.color = 'var(--pk-pri)'; } else { el.textContent = baseTxt; el.style.color = ''; } }); } targetPop.style.display = 'flex'; activeTargetBtn = btn; activePop = targetPop; updatePopPos(); } }; if (UI.btnAnaSelect) UI.btnAnaSelect.onclick = (e) => togglePop(e, UI.btnAnaSelect, pop); if (UI.btnAnaSort) UI.btnAnaSort.onclick = (e) => togglePop(e, UI.btnAnaSort, sortPop); if (UI.btnDupSmart) UI.btnDupSmart.onclick = (e) => togglePop(e, UI.btnDupSmart, pop); if (UI.btnDupSort) UI.btnDupSort.onclick = (e) => togglePop(e, UI.btnDupSort, sortPop); window.addEventListener('resize', updatePopPos); pop.querySelectorAll('.pk-ana-opt').forEach(opt => { opt.onclick = () => { const op = opt.dataset.op; const nextSel = []; const isBetter = (type, curW, curM) => { if (type === 'new') return new Date(curM.modified_time) > new Date(curW.modified_time); if (type === 'old') return new Date(curM.modified_time) < new Date(curW.modified_time); if (type === 'large') return BigInt(curM.size || 0) > BigInt(curW.size || 0); if (type === 'small') return BigInt(curM.size || 0) < BigInt(curW.size || 0); if (type === 'short') return curM.name.length < curW.name.length; if (type === 'long') return curM.name.length > curW.name.length; return false; }; if (S.analyzeMode && S.analyzeSimGroups) { S.analyzeSimGroups.forEach(g => { const members = g.ids.map(id => S.itemMap.get(id)).filter(Boolean); if (members.length < 2) return; let winner = members[0]; members.forEach(m => { if (isBetter(op, winner, m)) winner = m; }); members.forEach(m => { if (m.id !== winner.id) nextSel.push(m.id); }); }); } else if (S.dupMode && S.dupGroups) { const itemMap = new Map(); S.display.forEach(d => { if (d.isHeader) return; const gIdx = S.dupGroups.get(d.id); if (gIdx !== undefined) { if (!itemMap.has(gIdx)) itemMap.set(gIdx, []); itemMap.get(gIdx).push(d); } }); itemMap.forEach(members => { if (members.length < 2) return; let winner = members[0]; members.forEach(m => { if (isBetter(op, winner, m)) winner = m; }); members.forEach(m => { if (m.id !== winner.id) nextSel.push(m.id); }); }); } S.setExplicitSelection(nextSel); pop.style.display = 'none'; activeTargetBtn = null; activePop = null; renderVisible(); updateStat(); }; }); sortPop.querySelectorAll('.pk-sort-opt').forEach(opt => { opt.onclick = (e) => { e.stopPropagation(); const op = opt.dataset.op; if (S.groupSortOp === op) { S.groupSortDir *= -1; } else { S.groupSortOp = op; S.groupSortDir = 1; } sortPop.querySelectorAll('.pk-sort-opt').forEach(el => { let baseTxt = ""; if (el.dataset.op === 'time') baseTxt = L.opt_sort_time; if (el.dataset.op === 'size') baseTxt = L.opt_sort_size; if (el.dataset.op === 'path') baseTxt = L.opt_sort_path; if (el.dataset.op === 'name') baseTxt = L.opt_sort_name; if (el.dataset.op === S.groupSortOp) { el.textContent = baseTxt + (S.groupSortDir === 1 ? ' ▼' : ' ▲'); el.style.color = 'var(--pk-pri)'; } else { el.textContent = baseTxt; el.style.color = ''; } }); const cmp = (a, b) => { let res = 0; if (S.groupSortOp === 'time') { res = new Date(b.modified_time) - new Date(a.modified_time); } else if (S.groupSortOp === 'size') { const sa = BigInt(a.size || 0), sb = BigInt(b.size || 0); res = sa < sb ? 1 : (sa > sb ? -1 : 0); } else if (S.groupSortOp === 'path') { const pa = a._dupFullPath || a._pathStr || a.path || ""; const pb = b._dupFullPath || b._pathStr || b.path || ""; res = pa.localeCompare(pb); } else if (S.groupSortOp === 'name') { res = a.name.localeCompare(b.name); } return res * S.groupSortDir; }; const newDisplay = []; let currentGroupHeader = null; let currentGroupMembers = []; const flushGroup = () => { if (currentGroupHeader) newDisplay.push(currentGroupHeader); if (currentGroupMembers.length > 0) { const getDupPath = it => it._dupFullPath || it._pathStr || it.path || ""; let orderedMembers = currentGroupMembers; if (S.pinnedDupPath) { const pinnedMembers = []; const otherMembers = []; currentGroupMembers.forEach(it => { if (getDupPath(it) === S.pinnedDupPath) pinnedMembers.push(it); else otherMembers.push(it); }); if (pinnedMembers.length > 0) { pinnedMembers.sort(cmp); otherMembers.sort(cmp); orderedMembers = pinnedMembers.concat(otherMembers); } else { orderedMembers = currentGroupMembers.slice().sort(cmp); } } else { orderedMembers = currentGroupMembers.slice().sort(cmp); } let lastPath = null; orderedMembers.forEach((it, idx) => { const curPath = getDupPath(it); if (idx > 0 && curPath === lastPath && curPath !== "") { it._isSameFolder = true; } else { it._isSameFolder = false; } lastPath = curPath; }); newDisplay.push(...orderedMembers); } currentGroupHeader = null; currentGroupMembers = []; }; S.display.forEach(d => { if (d.isHeader) { flushGroup(); currentGroupHeader = d; } else { currentGroupMembers.push(d); } }); flushGroup(); S.display = newDisplay; clearGroupedGridMetaCache(); clearGridStableRows(UI.in); renderVisible(); }; }); document.addEventListener('mousedown', (e) => { const isClickInsideBtn = (UI.btnAnaSelect && UI.btnAnaSelect.contains(e.target)) || (UI.btnAnaSort && UI.btnAnaSort.contains(e.target)) || (UI.btnDupSmart && UI.btnDupSmart.contains(e.target)) || (UI.btnDupSort && UI.btnDupSort.contains(e.target)); if (!pop.contains(e.target) && !sortPop.contains(e.target) && !isClickInsideBtn) { pop.style.display = 'none'; sortPop.style.display = 'none'; activeTargetBtn = null; activePop = null; } }); } if (UI.btnShareParseSave) { UI.btnShareParseSave.onclick = handleShareParseSaveSelected; } if (UI.btnShareParseInsight) { UI.btnShareParseInsight.onclick = handleShareParseInsightStart; } if (UI.btnShareParseStopScan) { UI.btnShareParseStopScan.onclick = handleShareParseInsightStop; } if (UI.btnShareParseBackList) { UI.btnShareParseBackList.onclick = handleShareParseInsightBack; } function isNormalFolderPagingRefreshBusy(cur) { if (!S.pagingLoading && !S.loading) return false; if (S.manualRefreshPromise) return false; if (S.liveRefreshCtx && (S.liveRefreshCtx.status === 'refreshing' || S.liveRefreshCtx.status === 'interrupted')) return false; const id = String(cur && cur.id || ''); if (id === 'offline_root' || id === 'recent_root' || id === 'history_root' || id === 'analyze_root' || id === 'virtual_search_root') return false; if (S.trashMode || S.shareMode || S.shareParseMode || S.linkBookmarkMode || S.starredMode || S.recentMode || S.historyMode || S.offlineMode || S.uploadMode || S.isFlattened || S.dupMode || S.analyzeMode) return false; if (UI.chkGlobal && UI.chkGlobal.checked) return false; if (S.search) return false; if (UI.searchInput && String(UI.searchInput.value || '').trim()) return false; return true; } UI.btnRefresh.onclick = async () => { if (shouldSuppressRefreshForVirtualResultPage()) return; 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 (S.historyMode && cur.id === 'history_root') { if (S.abortController) S.abortController.abort(); resetHistorySessionState(); S.historyRefreshFirstPageOnly = true; S.clearSelection(); S.activeId = null; if (UI.chkAll) { UI.chkAll.checked = false; UI.chkAll.indeterminate = false; } S.items = []; S.display = []; S.itemMap.clear(); if (UI.vp) UI.vp.scrollTop = 0; setLoad(true, true); updateLoadTxt(L.loading_detail); refresh(); updateStat(); const p = load(false, true); if (UI.chkGlobal) UI.chkGlobal.checked = intent; try { await p; } finally { S.historyRefreshFirstPageOnly = false; } return; } if (S.recentMode && S.path.length === 1 && cur.id === 'recent_root') { stopRecentLightProbe('manual_refresh'); if (S.abortController) S.abortController.abort(); S.recentLoadNextPageOnly = false; S.recentLoadingNextPage = false; S.recentRefreshFirstPageOnly = false; S.clearSelection(); S.activeId = null; if (UI.chkAll) { UI.chkAll.checked = false; UI.chkAll.indeterminate = false; } S.items = []; S.display = []; S.itemMap.clear(); S.recentResultItems = null; S.cache.delete('recent_root'); if (UI.vp) UI.vp.scrollTop = 0; if (typeof globalCache !== 'undefined') { globalCache.delete('recent_root'); globalCache.delete('recent_session'); } setLoad(true, true); updateLoadTxt(L.loading_detail); refresh(); updateStat(); const p = load(false, true); if (UI.chkGlobal) UI.chkGlobal.checked = intent; try { await p; } finally { S.recentRefreshFirstPageOnly = false; } return; } if (S.offlineMode && S.path.length === 1 && cur.id === 'offline_root') { cleanupOfflineDeleteTombstones(); cleanupOfflineRetryTombstones(); clearOfflineLightProbeTimer(); abortOfflineLightProbe('manual_refresh'); } const manualRefreshKey = [ String(cur.id || 'root'), getLiveManualRefreshModeKey() ].join('|'); if (isNormalFolderPagingRefreshBusy(cur)) { updateLoadTxt(L.loading_detail); if (UI.chkGlobal) UI.chkGlobal.checked = intent; return; } if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing' && S.liveRefreshCtx.pathKey === getLiveManualRefreshPathKey() && S.liveRefreshCtx.modeKey === getLiveManualRefreshModeKey()) { updateLiveManualRefreshStatus(S.liveRefreshCtx, 'refreshing', 'info'); if (UI.chkGlobal) UI.chkGlobal.checked = intent; return; } if (S.manualRefreshPromise && S.manualRefreshKey === manualRefreshKey) { updateLoadTxt(L.str_refreshing); if (UI.chkGlobal) UI.chkGlobal.checked = intent; return; } if (S.loading && S.manualRefreshKey === manualRefreshKey) { updateLoadTxt(L.str_refreshing); if (UI.chkGlobal) UI.chkGlobal.checked = intent; return; } const canKeepVisualManualRefresh = canUseLiveManualRefresh(); const canLiveManualRefresh = canKeepVisualManualRefresh && canUseLiveManualRefresh(); if (canLiveManualRefresh) S.keepVisualOnNextForceLoad = true; if (cur.id && !canLiveManualRefresh) S.cache.delete(cur.id); const p = load(false, true); S.manualRefreshKey = manualRefreshKey; S.manualRefreshPromise = p; if (UI.chkGlobal) UI.chkGlobal.checked = intent; try { await p; } finally { if (S.manualRefreshPromise === p) { S.manualRefreshPromise = null; S.manualRefreshKey = ''; } } }; 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_moving); return; } S.wasGlobalChecked = UI.chkGlobal ? UI.chkGlobal.checked : false; const lastMin = gmGet('pk_analyze_last_min', 0); const lastMax = gmGet('pk_analyze_last_max', ''); const lastUnit = gmGet('pk_analyze_last_unit', 'GB'); const lastKeyword = gmGet('pk_analyze_last_keyword', ''); const lastSim = gmGet('pk_analyze_last_sim', 1.0); const lastAlgo = gmGet('pk_analyze_last_algo', 'sim'); const result = await new Promise((resolve) => { const L_min = L.lbl_ana_min; const L_max = L.lbl_ana_max; const selectedCount = S.getSelectedCount(); const analyzeTargetDesc = selectedCount > 0 ? L.lbl_analyze_selected.replace('{n}', selectedCount) : L.lbl_analyze_current; const anaSimTargetDesc = selectedCount > 0 ? L.lbl_ana_sim_selected.replace('{n}', selectedCount) : L.lbl_ana_sim_current; const m = showModal(`

${L.btn_analyze}

${L.opt_ana_large}
${L.opt_ana_sim}
${analyzeTargetDesc}
${L.lbl_keyword_filter}
${L_min}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
-
${L_max}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${lastUnit}
MB
GB
TB
`); bindPlainTextMaxLengthLimits(m, [['#an_keyword', ((getConfigLimitSchema('pk_analyze_last_keyword') || {}).localLimit || {}).maxItemLen || 2048]]); 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'); const syncLimitBorder = (inp) => { if (!inp) return; const raw = String(inp.value || '').trim(); const active = raw && (inp !== inpMin || Number(raw) !== 0); inp.style.borderColor = active ? 'var(--pk-pri)' : 'var(--pk-bd)'; }; [inpMin, inpMax].forEach(inp => { syncLimitBorder(inp); inp.addEventListener('input', () => syncLimitBorder(inp)); }); m.querySelector('#an_inc_min').onclick = (e) => { e.stopPropagation(); inpMin.value = (parseInt(inpMin.value) || 0) + 1; inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#an_dec_min').onclick = (e) => { e.stopPropagation(); inpMin.value = Math.max(0, (parseInt(inpMin.value) || 1) - 1); inpMin.dispatchEvent(new Event('input')); }; m.querySelector('#an_inc_max').onclick = (e) => { e.stopPropagation(); inpMax.value = (parseInt(inpMax.value) || 0) + 1; inpMax.dispatchEvent(new Event('input')); }; m.querySelector('#an_dec_max').onclick = (e) => { e.stopPropagation(); inpMax.value = Math.max(0, (parseInt(inpMax.value) || 1) - 1); inpMax.dispatchEvent(new Event('input')); }; const menu = m.querySelector('#an_unit_menu'); const txt = m.querySelector('#an_unit_txt'); let currentUnit = lastUnit; btn.onclick = (e) => { e.stopPropagation(); menu.style.display = menu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('.pk-ana-item').forEach(item => { item.onclick = () => { m.querySelectorAll('.pk-ana-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); currentUnit = item.dataset.v; txt.textContent = currentUnit; menu.style.display = 'none'; }; }); const closeMenu = () => { if(menu) menu.style.display = 'none'; if(simMenu) simMenu.style.display = 'none'; }; setTimeout(() => document.addEventListener('click', closeMenu), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeMenu); _orgRemove(); }; setTimeout(() => m.focus(), 50); const kHandler = (e) => { if (e.key === 'Enter') m.querySelector('#an_confirm').click(); if (e.key === 'Escape') m.querySelector('#an_cancel').click(); }; inpMin.onkeydown = kHandler; inpMax.onkeydown = kHandler; const saveAnalyzeInputs = () => { gmSet('pk_analyze_last_min', parseInt(inpMin.value) || 0); gmSet('pk_analyze_last_max', inpMax.value.trim()); gmSet('pk_analyze_last_unit', currentUnit); gmSet('pk_analyze_last_keyword', m.querySelector('#an_keyword').value.trim()); const algo = m.querySelector('input[name="ana_sim_algo"]:checked')?.value; if (algo) gmSet('pk_analyze_last_algo', algo); }; m.querySelector('#an_cancel').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { saveAnalyzeInputs(); m.remove(); resolve(null); }; m.querySelector('#an_confirm').onclick = () => { if (currentMode === 'large') { const vMin = parseInt(inpMin.value) || 0; const vMax = parseInt(inpMax.value) || 0; const kw = m.querySelector('#an_keyword').value.trim(); if (vMin < 0 || (vMax > 0 && vMin > vMax)) { inpMin.style.borderColor = '#d93025'; if (vMax > 0 && vMin > vMax) inpMax.style.borderColor = '#d93025'; return; } saveAnalyzeInputs(); let mult = 1; if (currentUnit === 'MB') mult = 1024 * 1024; else if (currentUnit === 'GB') mult = 1024 * 1024 * 1024; else if (currentUnit === 'TB') mult = 1024 * 1024 * 1024 * 1024; m.remove(); resolve({ mode: 'large', minBytes: Math.floor(vMin * mult), maxBytes: vMax > 0 ? Math.floor(vMax * mult) : 0, keyword: kw }); } else { const algo = m.querySelector('input[name="ana_sim_algo"]:checked').value; gmSet('pk_analyze_last_algo', algo); m.remove(); resolve({ mode: 'similar', threshold: currentSim, algo: algo }); } }; const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '540px', height: 'auto', minHeight: 'auto', overflow: 'visible', paddingBottom: '30px' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' }); } }); if (result === null) return; const isSimMode = result.mode === 'similar'; S.analyzeSimGroups = null; const minBytes = result.minBytes || 0; const maxBytes = result.maxBytes || 0; const simThreshold = result.threshold || 0.9; const simAlgo = result.algo || 'sim'; setLoad(true); isGUISensitive = true; let nodeMap = new Map(); const largeFolders = []; const startNodes = []; const getRealLineage = (item) => { if (item._lineage && item._lineage.length > 0) { return item._lineage; } const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root'); return [...cleanPath, { id: item.id, name: item.name }]; }; const analyzeSelectedIds = S.getSelectedIds(); if (analyzeSelectedIds.length > 0) { analyzeSelectedIds.forEach(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') { const fullLineage = getRealLineage(item); startNodes.push({ id: item.id, name: item.name, icon_link: item.icon_link, starred: item.starred, tags: item.tags, lineage: fullLineage, retryCount: 0, _pathStr: fullLineage.map(x => x.name).join('/') }); } }); } else { const subFolders = S.items.filter(it => it.kind === 'drive#folder'); if (subFolders.length > 0) { subFolders.forEach(item => { const fullLineage = getRealLineage(item); startNodes.push({ id: item.id, name: item.name, icon_link: item.icon_link, starred: item.starred, tags: item.tags, lineage: fullLineage, retryCount: 0, _pathStr: fullLineage.map(x => x.name).join('/') }); }); } else { const cur = S.path[S.path.length - 1]; const cleanPath = S.path.filter(p => p.id !== 'analyze_root' && p.id !== 'virtual_search_root'); if (cleanPath.length === 0) cleanPath.push({ id: '', name: L.btn_nav_home }); const rootName = cur.name || 'Root'; const actualCur = S.itemMap.get(cur.id); startNodes.push({ id: cur.id || '', name: rootName, icon_link: cur.icon_link, starred: actualCur ? actualCur.starred : false, tags: actualCur ? actualCur.tags :[], lineage: cleanPath, retryCount: 0, _pathStr: cleanPath.map(x => x.name).join('/') }); } } if (startNodes.length === 0) { setLoad(false); isGUISensitive = false; scheduleBackgroundResume(); 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; } const propagateSize = (parentId, addSize) => { let curId = parentId; while (curId !== null && nodeMap.has(curId)) { const node = nodeMap.get(curId); node.size += addSize; if (!node.isRoot && node.size >= minBytes && !node.marked) { node.marked = true; largeFolders.push(node); } curId = node.parentId; } }; S.scanId = (S.scanId || 0) + 1; const myScanId = S.scanId; if (S.scanAbortController) S.scanAbortController.abort(); S.scanAbortController = new AbortController(); const signal = S.scanAbortController.signal; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; S.scanning = false; if (S.scanAbortController) S.scanAbortController.abort(); updateLoadTxt(L.str_stopping); }; S.scanning = true; syncLocalUploadVisibility(); let totalFilesScanned = 0; let totalDirsScanned = 0; try { if (!useCache) { await coreRecursiveEngine(startNodes, { signal: signal, onFolder: (folder, filesInFolder, nextSubFolders) => { totalDirsScanned++; nextSubFolders.forEach(sub => { if (!nodeMap.has(sub.id)) { const fullPathStr = sub.lineage.map(x => x.name).join('/'); nodeMap.set(sub.id, { id: sub.id, name: sub.name, icon_link: sub.icon_link, starred: sub.starred, tags: sub.tags, size: 0, parentId: folder.id, marked: false, _pathStr: fullPathStr, lineage: sub.lineage, isRoot: false, files:[] }); } }); }, onFile: (file, parent) => { totalFilesScanned++; const sz = Number(file.size || 0); if (sz > 0) { propagateSize(parent.id, sz); } if (nodeMap.has(parent.id)) { const fingerprint = file.hash ? `${file.hash}_${sz}` : `${file.name}_${sz}`; let curId = parent.id; while (curId !== null && nodeMap.has(curId)) { nodeMap.get(curId).files.push(fingerprint); curId = nodeMap.get(curId).parentId; } } }, onProgress: (st) => { updateLoadTxt(`${L.str_scanning} ${st.folders} ${L.unit_folders} | ${L.str_files}: ${st.files} | ${L.str_speed}: ${st.currentConcurrency} | ${L.str_cached} ${st.cacheHits} ${L.unit_folders}`); } }); if (!isRunning || signal.aborted || myScanId !== S.scanId) { throw new Error('StoppedByUser'); } if (typeof globalCache !== 'undefined') { globalCache.set(cacheKey, Array.from(nodeMap.entries())); } } else { Array.from(nodeMap.values()).forEach(node => { if (!node.isRoot && node.size >= minBytes) { largeFolders.push(node); } }); } } catch (e) { if (isRunning && e.message !== 'StoppedByUser' && e.name !== 'AbortError') { showAlert(`${L.str_error}: ${e.message}`); setLoad(false); } } finally { isGUISensitive = false; S.scanning = false; S.scanAbortController = null; scheduleBackgroundResume(); 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(/'/, ''); cleanName = cleanName .replace(/\s*[-_ ..。]*\s*(?:\(\s*\d+\s*\)|(\s*\d+\s*)|\[\s*\d+\s*\]|【\s*\d+\s*】)\s*$/u, '') .replace(/\s*(?:[-_ ..。]*\s*)?(?:副本|复制|拷贝|拷貝|コピー|複製|복사본|사본|복사)\s*(?:\d+|\(\s*\d+\s*\)|(\s*\d+\s*)|\[\s*\d+\s*\]|【\s*\d+\s*】)?\s*$/u, '') .replace(/\s*[-_ ..。]+\s*(?:copy|duplicate|dup|salinan|salin|duplikat)\s*(?:\d+|\(\s*\d+\s*\)|(\s*\d+\s*)|\[\s*\d+\s*\]|【\s*\d+\s*】)?\s*$/i, '') .trim(); 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.10 : Infinity; for (const[k, items] of nameGroups) { if (items.length > 1) { const sorted = [...items].sort((a,b) => Number(a.size) - Number(b.size)); let currentGroup = [sorted[0]]; for (let i = 1; i < sorted.length; i++) { const target = sorted[i]; const root = currentGroup[0]; const rootSize = Number(root.size || 0); const targetSize = Number(target.size || 0); let isMatch = false; if (rootSize === 0 && targetSize === 0) isMatch = true; else { const sizeDiff = Math.abs(targetSize - rootSize); const maxBase = Math.max(targetSize, rootSize); if (maxBase > 0 && (sizeDiff / maxBase) <= sizeRatioLimit) isMatch = true; } if (isMatch) currentGroup.push(target); else { if (currentGroup.length > 1) { const gNodes = currentGroup; let minS = Number.MAX_SAFE_INTEGER, maxS = 0; gNodes.forEach(n => { const sz = Number(n.size||0); if(szmaxS) maxS=sz; }); if (minS === Number.MAX_SAFE_INTEGER) minS = 0; const range = (minS === maxS) ? fmtSize(minS) : `${fmtSize(minS)} ~ ${fmtSize(maxS)}`; groups.push({ ids: gNodes.map(f => f.id), type: `${gNodes.length} ${L.str_items} | ${range}`, _sim: 1 }); gNodes.forEach(f => assigned.add(f.id)); } currentGroup = [target]; } } if (currentGroup.length > 1) { const gNodes = currentGroup; let minS = Number.MAX_SAFE_INTEGER, maxS = 0; gNodes.forEach(n => { const sz = Number(n.size||0); if(szmaxS) maxS=sz; }); if (minS === Number.MAX_SAFE_INTEGER) minS = 0; const range = (minS === maxS) ? fmtSize(minS) : `${fmtSize(minS)} ~ ${fmtSize(maxS)}`; groups.push({ ids: gNodes.map(f => f.id), type: `${gNodes.length} ${L.str_items} | ${range}`, _sim: 1 }); gNodes.forEach(f => assigned.add(f.id)); } } } } else { for (let i = 0; i < total; i++) { if (i % 50 === 0 || performance.now() - lastYieldTime > 16) { if (!isRunning) break; updateLoadTxt(`${L.str_analyzing}\n${Math.round((i / total) * 100)}%`); await sleep(0); lastYieldTime = performance.now(); } if (assigned.has(folderArr[i].id)) continue; const f1 = folderArr[i]; const group = [f1]; let groupMinSim = 1.0; const candidateIndices = []; const marker = i + 1; f1.fileCounts.forEach((_, hash) => { const foldersWithHash = invertedIndex.get(hash); if (foldersWithHash) { for (let k = 0, len = foldersWithHash.length; k < len; k++) { const idx = foldersWithHash[k]; if (idx > i && candidateSeen[idx] !== marker && !assigned.has(folderArr[idx].id)) { candidateSeen[idx] = marker; candidateIndices.push(idx); } } } }); for (let m = 0, cLen = candidateIndices.length; m < cLen; m++) { const j = candidateIndices[m]; const f2 = folderArr[j]; if (simAlgo === 'sim' && (isDescendant(f2.id, f1.id) || isDescendant(f1.id, f2.id))) continue; let total1 = f1.weightedTotal; let total2 = f2.weightedTotal; let intersect = 0; if (isDescendant(f2.id, f1.id)) { total1 -= total2; if (total1 <= 0 || total2 <= 0) continue; const maxS = total1 > total2 ? total1 : total2; const minS = total1 < total2 ? total1 : total2; if (simAlgo === 'sim' && minS / maxS < simThreshold) continue; for (let k = 0, len = f2._keys.length; k < len; k++) { const hash = f2._keys[k]; const c2 = f2.fileCounts.get(hash); const c1 = f1.fileCounts.get(hash) || 0; const diff = c1 - c2; if (diff > 0) intersect += (diff < c2 ? diff : c2) * weightMap.get(hash); } } else if (isDescendant(f1.id, f2.id)) { total2 -= total1; if (total1 <= 0 || total2 <= 0) continue; const maxS = total1 > total2 ? total1 : total2; const minS = total1 < total2 ? total1 : total2; if (simAlgo === 'sim' && minS / maxS < simThreshold) continue; for (let k = 0, len = f1._keys.length; k < len; k++) { const hash = f1._keys[k]; const c1 = f1.fileCounts.get(hash); const c2 = f2.fileCounts.get(hash) || 0; const diff = c2 - c1; if (diff > 0) intersect += (diff < c1 ? diff : c1) * weightMap.get(hash); } } else { if (total1 <= 0 || total2 <= 0) continue; const maxS = total1 > total2 ? total1 : total2; const minS = total1 < total2 ? total1 : total2; if (simAlgo === 'sim' && minS / maxS < simThreshold) continue; const fSmall = f1._keys.length < f2._keys.length ? f1 : f2; const fLarge = f1._keys.length < f2._keys.length ? f2 : f1; for (let k = 0, len = fSmall._keys.length; k < len; k++) { const hash = fSmall._keys[k]; const cLarge = fLarge.fileCounts.get(hash); if (cLarge !== undefined) { const cSmall = fSmall.fileCounts.get(hash); intersect += (cSmall < cLarge ? cSmall : cLarge) * weightMap.get(hash); } } } const minTotal = total1 < total2 ? total1 : total2; const union = total1 + total2 - intersect; const sim = simAlgo === 'contain' ? (minTotal > 0 ? (intersect / minTotal) : 0) : (union > 0 ? (intersect / union) : 0); if (sim >= simThreshold) { let isGroupQualified = true; let currentMinSim = sim; for (let gIdx = 1; gIdx < group.length; gIdx++) { const gMember = group[gIdx]; if (simAlgo === 'sim' && (isDescendant(f2.id, gMember.id) || isDescendant(gMember.id, f2.id))) { isGroupQualified = false; break; } let tA = gMember.weightedTotal; let tB = f2.weightedTotal; let intS = 0; if (isDescendant(f2.id, gMember.id)) { tA -= tB; } else if (isDescendant(gMember.id, f2.id)) { tB -= tA; } if (tA <= 0 || tB <= 0) { isGroupQualified = false; break; } const maxS = tA > tB ? tA : tB; const minS = tA < tB ? tA : tB; if (simAlgo === 'sim' && minS / maxS < simThreshold) { isGroupQualified = false; break; } if (isDescendant(f2.id, gMember.id)) { for (let k = 0, len = f2._keys.length; k < len; k++) { const h = f2._keys[k]; const cB = f2.fileCounts.get(h); const cA = gMember.fileCounts.get(h) || 0; const diff = cA - cB; if (diff > 0) intS += (diff < cB ? diff : cB) * weightMap.get(h); } } else if (isDescendant(gMember.id, f2.id)) { for (let k = 0, len = gMember._keys.length; k < len; k++) { const h = gMember._keys[k]; const cA = gMember.fileCounts.get(h); const cB = f2.fileCounts.get(h) || 0; const diff = cB - cA; if (diff > 0) intS += (diff < cA ? diff : cA) * weightMap.get(h); } } else { const fSmall = gMember._keys.length < f2._keys.length ? gMember : f2; const fLarge = gMember._keys.length < f2._keys.length ? f2 : gMember; for (let k = 0, len = fSmall._keys.length; k < len; k++) { const h = fSmall._keys[k]; const cLarge = fLarge.fileCounts.get(h); if (cLarge !== undefined) { const cSmall = fSmall.fileCounts.get(h); intS += (cSmall < cLarge ? cSmall : cLarge) * weightMap.get(h); } } } const minT = tA < tB ? tA : tB; const un = tA + tB - intS; const pairwiseSim = simAlgo === 'contain' ? (minT > 0 ? (intS / minT) : 0) : (un > 0 ? (intS / un) : 0); if (pairwiseSim < simThreshold) { isGroupQualified = false; break; } if (pairwiseSim < currentMinSim) { currentMinSim = pairwiseSim; } } if (isGroupQualified) { group.push(f2); if (currentMinSim < groupMinSim) groupMinSim = currentMinSim; } } } if (group.length > 1) { group.forEach(f => assigned.add(f.id)); groups.push({ ids: group.map(f => f.id), type: `${group.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${Math.round(groupMinSim * 100)}%`, _sim: groupMinSim }); } } } if (simAlgo !== 'name') { const folderObjMap = new Map(); folderArr.forEach(f => folderObjMap.set(f.id, f)); const finalGroups =[]; groups.forEach(g => { const nodes = g.ids.map(id => folderObjMap.get(id)).filter(Boolean); const toRemove = new Set(); nodes.forEach(node => { const descendants = nodes.filter(n => n.id !== node.id && isDescendant(n.id, node.id)); if (descendants.length === 0) return; const hasExternal = nodes.some(n => n.id !== node.id && !isDescendant(n.id, node.id) && !isDescendant(node.id, n.id)); if (hasExternal) return; const topDescendants = descendants.filter(d1 => !descendants.some(d2 => d1.id !== d2.id && isDescendant(d1.id, d2.id)) ); let isCovered = true; let sumTotal = 0; const sumCounts = new Map(); topDescendants.forEach(td => { sumTotal += td.totalFiles; td.fileCounts.forEach((count, hash) => { sumCounts.set(hash, (sumCounts.get(hash) || 0) + count); }); }); if (sumTotal !== node.totalFiles) { isCovered = false; } else { for (const [hash, count] of node.fileCounts) { if (sumCounts.get(hash) !== count) { isCovered = false; break; } } } if (isCovered) { toRemove.add(node.id); } }); const finalIds = g.ids.filter(id => !toRemove.has(id)); if (finalIds.length >= 2) { finalGroups.push({ ids: finalIds, type: g.type, _sim: g._sim }); } }); groups = finalGroups; const partitionedGroups = []; const varianceTolerance = 0.15; groups.forEach(g => { const nodes = g.ids.map(id => folderObjMap.get(id)).filter(Boolean); if (nodes.length <= 2) { const pct = Math.round(g._sim * 100); g.type = `${nodes.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${pct}%`; partitionedGroups.push(g); return; } const simMatrix = []; let maxSim = 0, minSim = 1; for (let i = 0; i < nodes.length; i++) { simMatrix[i] = []; for (let j = 0; j < nodes.length; j++) { if (i === j) { simMatrix[i][j] = 1; continue; } if (j < i) { simMatrix[i][j] = simMatrix[j][i]; continue; } const n1 = nodes[i], n2 = nodes[j]; if (simAlgo === 'sim' && (isDescendant(n2.id, n1.id) || isDescendant(n1.id, n2.id))) { simMatrix[i][j] = 0; minSim = 0; continue; } let tA = n1.weightedTotal, tB = n2.weightedTotal, intS = 0; if (isDescendant(n2.id, n1.id)) tA -= tB; else if (isDescendant(n1.id, n2.id)) tB -= tA; if (tA > 0 && tB > 0) { if (isDescendant(n2.id, n1.id)) { for (let k = 0; k < n2._keys.length; k++) { const h = n2._keys[k], cB = n2.fileCounts.get(h), cA = n1.fileCounts.get(h) || 0, diff = cA - cB; if (diff > 0) intS += (diff < cB ? diff : cB) * weightMap.get(h); } } else if (isDescendant(n1.id, n2.id)) { for (let k = 0; k < n1._keys.length; k++) { const h = n1._keys[k], cA = n1.fileCounts.get(h), cB = n2.fileCounts.get(h) || 0, diff = cB - cA; if (diff > 0) intS += (diff < cA ? diff : cA) * weightMap.get(h); } } else { const fSmall = n1._keys.length < n2._keys.length ? n1 : n2, fLarge = n1._keys.length < n2._keys.length ? n2 : n1; for (let k = 0; k < fSmall._keys.length; k++) { const h = fSmall._keys[k], cLarge = fLarge.fileCounts.get(h); if (cLarge !== undefined) intS += (fSmall.fileCounts.get(h) < cLarge ? fSmall.fileCounts.get(h) : cLarge) * weightMap.get(h); } } const minT = tA < tB ? tA : tB; const un = tA + tB - intS; const s = simAlgo === 'contain' ? (minT > 0 ? (intS / minT) : 0) : (un > 0 ? (intS / un) : 0); simMatrix[i][j] = s; if (s > maxSim) maxSim = s; if (s < minSim) minSim = s; } else { simMatrix[i][j] = 0; minSim = 0; } } } if (maxSim - minSim <= varianceTolerance) { const minPct = Math.round(minSim * 100); const maxPct = Math.round(maxSim * 100); const range = (minPct === maxPct) ? `${minPct}%` : `${minPct}% ~ ${maxPct}%`; g.type = `${nodes.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${range}`; partitionedGroups.push(g); } else { const unassigned = new Set(nodes.map((_, idx) => idx)); const pairs =[]; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) pairs.push({i, j, s: simMatrix[i][j]}); } pairs.sort((a, b) => b.s - a.s); pairs.forEach(pair => { if (unassigned.has(pair.i) && unassigned.has(pair.j) && pair.s >= simThreshold) { const sg = [pair.i, pair.j]; let sgMinSim = pair.s; unassigned.delete(pair.i); unassigned.delete(pair.j); for (const u of Array.from(unassigned)) { let canAdd = true, localMin = 1; for (const mem of sg) { const s = simMatrix[u < mem ? u : mem][u > mem ? u : mem]; if (pair.s - s > varianceTolerance || s < simThreshold) { canAdd = false; break; } if (s < localMin) localMin = s; } if (canAdd) { sg.push(u); unassigned.delete(u); if (localMin < sgMinSim) sgMinSim = localMin; } } if (sg.length >= 2) { let subMin = 1, subMax = 0; for(let x=0; x sg[y] ? sg[x] : sg[y]; const sVal = simMatrix[idx1][idx2]; if (sVal < subMin) subMin = sVal; if (sVal > subMax) subMax = sVal; } } const minPct = Math.round(subMin * 100); const maxPct = Math.round(subMax * 100); const range = (minPct === maxPct) ? `${minPct}%` : `${minPct}% ~ ${maxPct}%`; partitionedGroups.push({ ids: sg.map(idx => nodes[idx].id), type: `${sg.length} ${L.str_items} | ${simAlgo === 'contain' ? L.lbl_containment : L.lbl_sim_score}: ${range}`, _sim: subMin }); } } }); } }); groups = partitionedGroups; } } catch (e) { console.error("[SimMode] Error:", e); } if (groups.length === 0) { setLoad(false); showToast(L.msg_dup_none); return; } groups.sort((a, b) => (b._sim - a._sim) || (b.ids.length - a.ids.length)); S.analyzeSimGroups = groups; clearGroupedGridMetaCache(); clearGridStableRows(UI.in); viewItems = Array.from(assigned).map(id => { const node = nodeMap.get(id); return { id: node.id, kind: 'drive#folder', name: node.name, icon_link: node.icon_link, starred: node.starred, tags: node.tags, size: node.size.toString(), _pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home, _lineage: node.lineage, modified_time: new Date(getServerNow()).toISOString(), parent_id: node.parentId }; }); } else { updateLoadTxt(L.str_rendering); const uniqueResults = Array.from(new Set(largeFolders)); const kw = gmGet('pk_analyze_last_keyword', ''); const kwList = kw ? kw.toLowerCase().split(/[,,]/).map(k => k.trim()).filter(k => k) : []; let filteredResults = uniqueResults.filter(n => n.size >= minBytes && (maxBytes === 0 || n.size <= maxBytes)); if (kwList.length > 0) { filteredResults = filteredResults.filter(node => !kwList.some(k => (node.name || "").toLowerCase().includes(k))); } filteredResults.sort((a, b) => b.size - a.size); if (filteredResults.length === 0) { setLoad(false); let rangeStr = maxBytes > 0 ? `${fmtSize(minBytes)} - ${fmtSize(maxBytes)}` : `≥ ${fmtSize(minBytes)}`; showToast(L.msg_analyze_no_large_folders.replace('{s}', rangeStr)); return; } viewItems = filteredResults.map(node => ({ id: node.id, kind: 'drive#folder', name: node.name, icon_link: node.icon_link, starred: node.starred, tags: node.tags, size: node.size.toString(), _pathStr: (node._pathStr && node._pathStr.includes('/')) ? node._pathStr.substring(0, node._pathStr.lastIndexOf('/')) : L.btn_nav_home, _lineage: node.lineage, modified_time: new Date(getServerNow()).toISOString(), parent_id: node.parentId })); } S.search = ''; if (UI.searchInput) UI.searchInput.value = ''; if (UI.searchClear) UI.searchClear.style.display = 'none'; rememberFolderFirstBeforeStrictMode(); stopLiveManualRefreshBeforePathChange('mode_change'); S.analyzeMode = true; S.hasShownAnaWarn = false; S.isFlattened = false; S.dupMode = false; S.analyzeResultItems = [...viewItems]; S.analyzeMap = nodeMap; stopLiveManualRefreshBeforePathChange('mode_change'); 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; if (typeof applyResolvedViewMode === 'function') applyResolvedViewMode(S.analyzeSimGroups ? 'folder_dup' : 'folder_insight'); if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); refresh(); if (S._enterTopRaf) cancelAnimationFrame(S._enterTopRaf); S._enterTopRaf = requestAnimationFrame(() => { S._enterTopRaf = 0; if (UI.vp && UI.vp.scrollTop !== 0) UI.vp.scrollTop = 0; }); updateStat(); setTimeout(() => { setLoad(false); isGUISensitive = false; scheduleBackgroundResume(); }, 200); } }; } 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_moving); return; } const format = await new Promise((resolve) => { const m = showModal(`

${L.title_export_format}

${L.lbl_export_current}
${L.opt_tree_view}
Root\n├─ Folder 1\n│ ├─ Folder 1-1\n│ └─ Folder 1-2\n└─ Folder 2\n └─ Folder 2-1
${L.opt_list_view}
Root/Folder 1\nRoot/Folder 1/Folder 1-1\nRoot/Folder 1/Folder 1-2\nRoot/Folder 2\nRoot/Folder 2/Folder 2-1
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = '520px'; modalBox.style.height = 'auto'; modalBox.style.minHeight = 'auto'; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '22px', right: '22px' }); } let selectedFormat = 'tree'; m.querySelectorAll('.pk-exp-card').forEach(card => { card.onclick = () => { m.querySelectorAll('.pk-exp-card').forEach(c => { c.style.borderColor = 'var(--pk-bd)'; c.querySelector('.pk-exp-title').style.color = 'var(--pk-fg)'; c.querySelector('.pk-exp-check').style.display = 'none'; c.classList.remove('act'); }); card.style.borderColor = 'var(--pk-pri)'; card.querySelector('.pk-exp-title').style.color = 'var(--pk-pri)'; card.querySelector('.pk-exp-check').style.display = 'flex'; card.classList.add('act'); selectedFormat = card.dataset.fmt; }; }); m.querySelector('#exp_cancel').onclick = () => { m.remove(); resolve(null); }; m.querySelector('.pk-modal-close').onclick = () => { m.remove(); resolve(null); }; m.querySelector('#exp_confirm').onclick = () => { m.remove(); resolve(selectedFormat); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#exp_confirm').click(); } }); }); if (!format) return; setLoad(true); isGUISensitive = 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; scheduleBackgroundResume(); } } }; } UI.btnNewFolder.onclick = async () => { if (shouldHideNewFolderButtonForCurrentMode() || UI.btnNewFolder.disabled || UI.btnNewFolder.style.display === 'none') return; const name = await showPrompt(L.msg_newfolder_prompt, '', L.btn_newfolder, { maxLen: CONF.fileNameMaxLen }); if (!name) return; if (isPikPakFileNameTooLong(name)) { showPikPakFileNameLimitTip(); return; } if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') { abandonLiveManualRefresh('local_mutation', { toast: 'info', detach: false }); } isGUISensitive = true; const cur = S.path[S.path.length - 1]; const folderId = cur.id || ''; const realCacheKey = S.getRealCacheKey(folderId || 'root'); const beforePathId = folderId; 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: folderId, name: name }) }); if (!res.ok) throw new Error(`API ${res.status}`); syncTime(res.headers); const data = await res.json().catch(() => null); const rawFolder = data && (data.file || (Array.isArray(data.files) ? data.files[0] : null) || data); if (!rawFolder || !rawFolder.id) throw new Error(L.str_failed); const nowIso = new Date(getServerNow()).toISOString(); const folder = minifyFile({ ...rawFolder, id: rawFolder.id, kind: 'drive#folder', name: rawFolder.name || name, parent_id: rawFolder.parent_id !== undefined ? rawFolder.parent_id : folderId, size: rawFolder.size || 0, file_count: rawFolder.file_count || 0, modified_time: rawFolder.modified_time || nowIso, created_time: rawFolder.created_time || nowIso, tags: rawFolder.tags || [] }, false); gmSet('pk_fmod_' + folder.id, nowIso); if (folderId) gmSet('pk_fmod_' + folderId, nowIso); const patchList = list => { if (!Array.isArray(list)) return list; const next = list.filter(item => item && item.id !== folder.id); next.push(folder); return next; }; const patchCacheValue = value => { if (Array.isArray(value)) return patchList(value); if (value && typeof value === 'object' && Array.isArray(value.items)) { return { ...value, items: patchList(value.items) }; } return value; }; const curNow = S.path[S.path.length - 1] || {}; const curNowId = String(curNow.id || ''); const isSpecialRootPath = ['trash_root', 'starred_root', 'recent_root', 'history_root', 'offline_root', 'analyze_root', 'virtual_search_root', 'share_parse_root', 'upload_root'].includes(curNowId) || curNowId.startsWith('virtual_'); const isHomeRootPath = !curNowId && !S.trashMode && !S.shareMode && !S.shareParseMode && !S.linkBookmarkMode && !S.starredMode && !S.recentMode && !S.historyMode && !S.offlineMode && !S.uploadMode; const isRealFolderSubPath = !!curNowId && !isSpecialRootPath; const canPatchCurrentFolderView = isHomeRootPath || isRealFolderSubPath; const stillInSameFolder = curNowId === String(beforePathId || '') && canPatchCurrentFolderView && !S.trashMode && !S.shareMode && !S.shareParseMode && !S.linkBookmarkMode && !S.historyMode && !S.offlineMode && !S.uploadMode && !S.isFlattened && !S.dupMode; if (stillInSameFolder) { S.items = patchList(S.items); S.itemMap.set(folder.id, folder); S.latestChildId = folder.id; const localCached = S.cache.get(realCacheKey); S.cache.set(realCacheKey, patchCacheValue(localCached) || [...S.items]); if (typeof globalCache !== 'undefined') { const keys = realCacheKey === 'root' ? ['root', ''] : [realCacheKey]; keys.forEach(key => { const cached = globalCache.get(key); globalCache.set(key, patchCacheValue(cached) || [...S.items]); }); } if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.delete(realCacheKey); if (realCacheKey === 'root') globalDirtyFolders.delete(''); } await refresh(); updateStat(); } else { const cached = S.cache.get(realCacheKey); if (cached) S.cache.set(realCacheKey, patchCacheValue(cached)); if (typeof globalCache !== 'undefined') { const keys = realCacheKey === 'root' ? ['root', ''] : [realCacheKey]; keys.forEach(key => { const cached = globalCache.get(key); if (cached) globalCache.set(key, patchCacheValue(cached)); }); } } showToast(L.msg_newfolder_created.replace('{n}', folder.name || name), 'success'); if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (typeof scheduleBackgroundResume === 'function') scheduleBackgroundResume(); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { isGUISensitive = false; scheduleBackgroundResume(); } }; UI.btnCopy.onclick = async () => { const ids = S.getSelectedIds(); const totalSelected = ids.length; if (totalSelected === 0) return; if (totalSelected > 10000) { showToast(L.str_copying); await sleep(10); } const itemList = []; let count = 0; for (const id of ids) { const item = S.itemMap.get(id); if (item) itemList.push(item); count++; if (count % 10000 === 0) await sleep(0); } S.clipItems = itemList; S.clipType = 'copy'; const curId = S.path[S.path.length - 1].id || ''; S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId; setLoad(false); UI.btnPaste.disabled = false; showToast(L.msg_copy_done); }; UI.btnCut.onclick = async () => { const ids = S.getSelectedIds(); if (ids.length === 0) return; const itemList = []; let count = 0; for (const id of ids) { const item = S.itemMap.get(id); if (!S.trashMode && isSystemItem(item)) { continue; } itemList.push(item); count++; if (count % 10000 === 0) await sleep(0); } if (itemList.length === 0) return; if (itemList.length > 10000) { showToast(L.str_moving); await sleep(10); } S.clipItems = itemList; S.clipType = 'move'; const curId = S.path[S.path.length - 1].id || ''; S.clipSourceParentId = S.isFlattened ? '__VIRTUAL__' : curId; setLoad(false); UI.btnPaste.disabled = false; showToast(L.msg_cut_done); }; UI.btnPaste.onclick = async () => { if (!S.clipItems || S.clipItems.length === 0) { showToast(L.msg_paste_empty, 'error'); return; } if (S.movingIds && S.movingIds.size > 0) { showAlert(L.msg_op_blocked_moving); return; } const items = S.clipItems; const type = S.clipType; const srcId = S.clipSourceParentId; const destId = S.path[S.path.length - 1].id || ''; const normalize = (id) => (!id || id === 'root') ? 'root' : id; S.clipItems = []; S.clipType = ''; updateStat(); const targetFolderName = S.path[S.path.length - 1].name || L.str_target_folder; await executeFileTransfer(items, type, normalize(srcId), normalize(destId), targetFolderName); }; function patchStage01RenameVisibleRow(row, newName, modifiedTime) { if (!row) return; const dateTxt = fmtDate(modifiedTime); const nameEl = row.querySelector('.pk-name-txt'); if (nameEl) nameEl.textContent = newName; const gridDateEl = row.querySelector('.pk-gv-date'); if (gridDateEl) { gridDateEl.textContent = dateTxt; gridDateEl.style.transition = 'color 0.3s'; gridDateEl.style.color = 'var(--pk-pri)'; setTimeout(() => { if (gridDateEl) gridDateEl.style.color = ''; }, 2000); return; } const last = row.lastElementChild; if (last && !last.classList.contains('pk-grid-card-body')) { last.textContent = dateTxt; last.style.transition = 'color 0.3s'; last.style.color = 'var(--pk-pri)'; setTimeout(() => { if (last) last.style.color = ''; }, 2000); } } function patchStage01RenameCacheRecord(cache, parentId, id, patch) { if (!cache || !id) return; const pid = parentId || 'root'; const keys = new Set([pid, pid === 'root' ? '' : pid]); keys.forEach(key => { if (!cache.has(key)) return; const raw = cache.get(key); if (Array.isArray(raw)) { const next = raw.map(item => item && item.id === id ? { ...item, ...patch } : item); cache.set(key, next); return; } if (raw && Array.isArray(raw.items)) { const nextItems = raw.items.map(item => item && item.id === id ? { ...item, ...patch } : item); cache.set(key, { ...raw, items: nextItems }); } }); } function patchStage01RenameRuntimeState(item, id, newName, modifiedTime) { if (!item || !id) return item; const parentId = item.parent_id || 'root'; const patch = { name: newName, modified_time: modifiedTime, modifiedTime: modifiedTime, final_modified_time: modifiedTime }; Object.assign(item, patch); if (S.itemMap && S.itemMap.has(id)) S.itemMap.set(id, { ...S.itemMap.get(id), ...patch }); if (Array.isArray(S.items)) S.items = S.items.map(row => row && row.id === id ? { ...row, ...patch } : row); if (item.kind === 'drive#folder') gmSet('pk_fmod_' + item.id, modifiedTime); gmSet('pk_fmod_' + parentId, modifiedTime); patchStage01RenameCacheRecord(S.cache, parentId, id, patch); if (typeof globalCache !== 'undefined') patchStage01RenameCacheRecord(globalCache, parentId, id, patch); if (S.lastGlobalResults && S.lastGlobalResults.length > 0) { const globalTarget = S.lastGlobalResults.find(f => f.id === id); if (globalTarget) Object.assign(globalTarget, patch); } if (S.analyzeResultItems) { const anaItem = S.analyzeResultItems.find(x => x.id === id); if (anaItem) Object.assign(anaItem, patch); } if (S.analyzeMap && S.analyzeMap.has(id)) Object.assign(S.analyzeMap.get(id), patch); if (typeof globalDirtyFolders !== 'undefined') globalDirtyFolders.add(parentId === 'root' ? '' : parentId); if (typeof scheduleBackgroundResume === 'function') scheduleBackgroundResume(); return item; } UI.btnRename.onclick = async () => { if (S.getSelectedCount() !== 1) return; const id = S.getSelectedIds()[0]; const item = S.itemMap.get(id); if (!item || (!S.trashMode && isSystemItem(item))) return; const newName = await showPrompt(L.msg_rename_prompt, item.name, L.modal_rename_title, { maxLen: CONF.fileNameMaxLen }); if (!newName || newName === item.name) return; if (isPikPakFileNameTooLong(newName)) { showPikPakFileNameLimitTip(); return; } if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') { abandonLiveManualRefresh('local_mutation', { toast: 'info', detach: false }); } let progressTask = null; try { progressTask = FloatBarManager.create(L.str_renaming); await apiAction(`/${id}`, { name: newName }); const nowIso = new Date(getServerNow()).toISOString(); patchStage01RenameRuntimeState(item, id, newName, nowIso); const row = UI.in.querySelector(`.pk-row[data-id="${id}"]`); patchStage01RenameVisibleRow(row, newName, nowIso); if (S.dupMode) renderDupView(); else refresh(); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { if (progressTask) progressTask.destroy(); } }; let activeBulkRenameModal = null; UI.btnBulkRename.onclick = () => { if (activeBulkRenameModal && activeBulkRenameModal.isConnected) { const input = activeBulkRenameModal.querySelector('#rn_find,#rn_pattern,#rn_apply'); if (input) try { input.focus(); } catch (e) {} return; } activeBulkRenameModal = null; const selectedIds = S.getSelectedIds(); const selectedCount = selectedIds.length; if (selectedCount < 2) return; if (S.movingIds && S.movingIds.size > 0) { const hasConflict = selectedIds.some(id => S.movingIds.has(id)); if (hasConflict) { showAlert(L.msg_op_blocked_moving); return; } } const getSmartDisplayHTML = (fullStr, hlStr) => { if (!hlStr || hlStr.indexOf('§§§MATCH_START§§§') === -1) { return esc(fullStr); } const parts = hlStr.split(/(§§§MATCH_START§§§.*?§§§MATCH_END§§§)/g); let result = ""; const PRE_CTX = 12; const SUF_CTX = 80; parts.forEach((part, index) => { if (part.startsWith('§§§MATCH_START§§§')) { const content = part.replace(/§§§MATCH_START§§§|§§§MATCH_END§§§/g, ''); result += `${esc(content)}`; } else { let text = part; if (index === 0) { if (text.length > PRE_CTX + 3) { text = "..." + text.slice(-PRE_CTX); } } else if (index === parts.length - 1) { if (text.length > SUF_CTX) { text = text.slice(0, SUF_CTX) + "..."; } } else { if (text.length > 20) { text = text.slice(0, 8) + ".." + text.slice(-8); } } result += esc(text); } }); return result; }; const getSmartTipHTML = (fullStr, hlStr) => { if (!hlStr || hlStr.indexOf('§§§MATCH_START§§§') === -1) return esc(fullStr); return hlStr.split(/(§§§MATCH_START§§§.*?§§§MATCH_END§§§)/g).map(part => { if (part.startsWith('§§§MATCH_START§§§')) { const content = part.replace(/§§§MATCH_START§§§|§§§MATCH_END§§§/g, ''); return `${esc(content)}`; } return esc(part); }).join(''); }; const extractKeyword = (fileName) => { const fc2Regex = /(?:FC2|FC)(?:[-_. ]*PPV)?[-_. @]*(\d{5,8})(?:[-_. ]*(?:part|pt|cd)?[-_. ]?(\d{1,2}|[a-e]))?(?![a-z\d])/i; const fc2Match = fileName.match(fc2Regex); if (fc2Match) { const id = fc2Match[1]; const part = fc2Match[2]; return (part && part.trim()) ? `FC2-PPV-${id}-${part.toUpperCase()}` : `FC2-PPV-${id}`; } return null; }; const inputStyle = ` width:100%; height:44px; padding:0 15px; border:2px solid var(--pk-bd); border-radius:8px; background:var(--pk-bg); color:var(--pk-fg); font-size:14px; font-weight:600; outline:none; transition:border-color 0.2s; box-sizing:border-box; `; const labelStyle = ` position:absolute; top:0; transform:translateY(-50%); left:10px; background:var(--pk-bg); padding:0 5px; line-height:1; font-size:11px; color:var(--pk-pri); font-weight:bold; pointer-events:none; z-index:1; `; const m = showModal(`

${L.modal_rename_multi_title}

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

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

`; const m = showModal(html); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => m.remove(); m.querySelector('#del_up_cancel').onclick = close; const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; m.querySelector('#del_up_confirm').onclick = async () => { const isDeleteFile = m.querySelector('#del_up_files').checked; m.remove(); const ids = S.getSelectedIds(); const filesToTrash = []; const ghostsToHardDelete = []; ids.forEach(id => { const task = S.uploadTasks.find(t => t.id === id); if (task) { task._deleted = true; const forceDelete = task.status !== 'DONE'; task._deleteFileIntent = forceDelete || isDeleteFile; if (task.file_id) { if (forceDelete) ghostsToHardDelete.push(task.file_id); else if (isDeleteFile) filesToTrash.push(task.file_id); } if (S.upMng) S.upMng.pause(task, true); } }); const idSet = S.sel; S.uploadTasks = S.uploadTasks.filter(t => { if(idSet.has(t.id)) { if (S.upMng) S.upMng.removeTask(t.id); return false; } return true; }); S.clearSelection(); load(false, true); if (filesToTrash.length > 0) { try { await executeBatchDelete(filesToTrash, { silent: true, hardDelete: false, forceRefresh: false }); } catch(e) { console.error("Failed to trash uploaded files:", e); } } if (ghostsToHardDelete.length > 0) { try { await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ghostsToHardDelete }) }); ghostsToHardDelete.forEach(id => { if (typeof window.pkRemoveGhostFile === 'function') window.pkRemoveGhostFile(id); }); } catch (e) { console.error("Failed to hard delete ghost files:", e); } } showToast(L.msg_task_del_success_fmt.replace('{n}', ids.length)); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#del_up_confirm').click(); } }); }; if (UI.btnUpClearAll) UI.btnUpClearAll.onclick = () => { if (S.uploadTasks.length === 0) return; const count = S.uploadTasks.length; const html = `

${L.title_clear_task_confirm}

`; const m = showModal(html); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => m.remove(); m.querySelector('#clear_up_cancel').onclick = close; const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; m.querySelector('#clear_up_confirm').onclick = async () => { const isDeleteFile = m.querySelector('#clear_all_up_files').checked; m.remove(); const filesToTrash = []; const ghostsToHardDelete = []; const count = S.uploadTasks.length; S.uploadTasks.forEach(task => { task._deleted = true; const forceDelete = task.status !== 'DONE'; task._deleteFileIntent = forceDelete || isDeleteFile; if (S.upMng) { S.upMng.pause(task, true); S.upMng.removeTask(task.id); } if (task.file_id) { if (forceDelete) ghostsToHardDelete.push(task.file_id); else if (isDeleteFile) filesToTrash.push(task.file_id); } }); S.uploadTasks = []; S.clearSelection(); load(false, true); if (filesToTrash.length > 0) { try { await executeBatchDelete(filesToTrash, { silent: true, hardDelete: false, forceRefresh: false }); } catch(e) {} } if (ghostsToHardDelete.length > 0) { try { await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: ghostsToHardDelete }) }); ghostsToHardDelete.forEach(id => { if (typeof window.pkRemoveGhostFile === 'function') window.pkRemoveGhostFile(id); }); } catch (e) {} } showToast(L.msg_task_clear_success_fmt.replace('{n}', count)); }; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#clear_up_confirm').click(); } }); }; const resolvePlayableDetailForExternal = async (item) => { const targetApiId = ((S.offlineMode && item.kind === 'drive#task') || (S.uploadMode && item.file_id)) ? getOfflineReferenceLookupId(item) : item.id; if (item._isShareItem) return await resolveSharePlayableFile(item); if (!targetApiId) throw new Error("File ID not ready"); try { return await apiGet(targetApiId); } catch (e) { await markOfflineReferenceMissingFromError(item, e, { source: 'external_detail' }); throw e; } }; const cleanExternalPlaybackUrl = (url) => { let cleanUrl = String(url || '').replace('&ext=.m3u8', ''); if (cleanUrl.includes('ts_downloader') && cleanUrl.includes('url=')) { try { const urlParam = new URL(cleanUrl).searchParams.get('url'); if (urlParam) cleanUrl = decodeURIComponent(urlParam); } catch (e) {} } return cleanUrl; }; function normalizeOriginalVideoDirectUrl(url) { if (typeof url !== 'string') return ''; const cleanUrl = cleanExternalPlaybackUrl(url).trim(); if (!cleanUrl || /\.m3u8(?:[?#]|$)/i.test(cleanUrl)) return ''; try { const parsed = new URL(cleanUrl); if (!/^https?:$/i.test(parsed.protocol)) return ''; if (/\.m3u8$/i.test(parsed.pathname || '')) return ''; return cleanUrl; } catch (e) { return ''; } } async function tryGetOriginalVideoDownloadUrl(file) { try { if (!file || !isVideoLikeItem(file)) return ''; if (file._isShareItem) return normalizeOriginalVideoDirectUrl(file.web_content_link); let detail = file; if (!Array.isArray(detail.medias) || !detail.medias.length) { const targetApiId = ((S.offlineMode && file.kind === 'drive#task') || (S.uploadMode && file.file_id)) ? getOfflineReferenceLookupId(file) : file.id; if (!targetApiId) return ''; detail = await apiGet(targetApiId); } const qualities = generateQualityList(detail); const original = qualities.find(q => q && q.isOriginal); return normalizeOriginalVideoDirectUrl(original && original.url); } catch (e) { await markOfflineReferenceMissingFromError(file, e, { source: 'downloader_original_link' }); console.warn('[Downloader] Original video link fallback:', e); return ''; } } async function preferOriginalVideoLinksForDownloader(files, options = {}) { if (!getBoolPref('pk_downloader_prefer_original_video_link', CONF.downloaderPreferOriginalVideoLink)) return files; const list = Array.isArray(files) ? files : []; const videoIndexes = []; for (let i = 0; i < list.length; i++) { if (isVideoLikeItem(list[i])) videoIndexes.push(i); } if (!videoIndexes.length) return list; const isRunning = typeof options.isRunning === 'function' ? options.isRunning : () => true; const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null; const cache = new Map(); const queue = videoIndexes.slice(); const total = queue.length; let done = 0; const limit = Math.min(4, total); if (onProgress) onProgress(done, total); const getCacheKey = (file) => String((file && (file.id || file.file_id || file.gcid || file.hash || file.web_content_link)) || ''); const getCachedUrl = async (file) => { const key = getCacheKey(file); if (!key) return await tryGetOriginalVideoDownloadUrl(file); if (!cache.has(key)) cache.set(key, tryGetOriginalVideoDownloadUrl(file)); const url = await cache.get(key); cache.set(key, url || ''); return url || ''; }; const worker = async () => { while (queue.length && isRunning()) { const idx = queue.shift(); const file = list[idx]; try { const originalUrl = await getCachedUrl(file); if (originalUrl) list[idx] = Object.assign({}, file, { web_content_link: originalUrl }); } catch (e) { console.warn('[Downloader] Original video link skipped:', e); } finally { done++; if (onProgress) onProgress(done, total); } } }; await Promise.all(Array.from({ length: limit }, worker)); return list; } function getM3UExportItemById(id) { if (!id) return null; return (S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id)) || (S.itemMap && S.itemMap.get(id)) || (S.items || []).find(it => it && it.id === id) || null; } function isM3UExportVideoItem(item) { if (!item || item.isHeader || item.kind === 'drive#folder') return false; if (S.offlineMode && item.kind === 'drive#task' && item.phase !== 'PHASE_TYPE_COMPLETE') return false; if (S.uploadMode && item.status !== 'DONE') return false; return isVideoLikeItem(item); } function getM3UExportSelectedVideos() { const selected = new Set(S.getSelectedIds()); if (!selected.size) return []; const out = []; const seen = new Set(); const list = Array.isArray(S.display) ? S.display : []; for (let i = 0; i < list.length; i++) { const displayItem = list[i]; const id = displayItem && displayItem.id; if (!id || seen.has(id) || !selected.has(id)) continue; const item = getM3UExportItemById(id) || displayItem; if (!isM3UExportVideoItem(item)) continue; seen.add(id); out.push(item); } return out; } function escapeM3UInfoText(s) { return String(s || '').replace(/[\r\n]+/g, ' ').trim() || 'video'; } function downloadM3UText(text, fileName) { const blob = new Blob([text], { type: 'audio/x-mpegurl;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName || 'PikPak_Selected_Videos.m3u'; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 0); } function getM3UExportFileName() { const pad = n => String(n).padStart(2, '0'); const d = new Date(); const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; const pathName = (S.path && S.path.length ? S.path[S.path.length - 1].name : '') || 'PikPak'; const safe = String(pathName).replace(/[\\/:*?"<>|\r\n]+/g, '_').slice(0, 60) || 'PikPak'; return `${safe}_M3U_${ts}.m3u`; } function formatM3UExportProgress(done, total) { return (getStrings().msg_m3u_export_progress).replace('{done}', done).replace('{total}', total); } const openExternalPlaybackDialog = (detail, opts = {}) => { const L = getStrings(); if (!detail) return null; const qualities = generateQualityList(detail); const bestSource = getBestSource(detail); let selectedUrl = opts.preferredUrl || bestSource.src; let selectedResName = opts.preferredName || bestSource.name; const savedPlayer = opts.initialPlayer || gmGet('pk_ext_player', 'potplayer'); let selectedPlayer = (savedPlayer === 'other') ? 'other' : 'potplayer'; const initialBtnTxt = (selectedPlayer === 'potplayer') ? L.btn_start_play : L.btn_copy_link; const shareExtPreviewTip = (detail && detail._isShareItem && detail.params && (detail.params.anonymous_play_seconds || detail.params.anonymous_play_range)) ? (L.msg_share_ext_preview_limited_save || L.msg_share_preview_limited_save || '') : ''; const m = showModal(`

${L.btn_ext}

${shareExtPreviewTip ? `
${shareExtPreviewTip}
` : ''}
${L.lbl_resolution}
${selectedResName}
${CONF.crumbIcons.down}
${qualities.map(q => `
${q.name}
`).join('')}
${L.lbl_player}
${selectedPlayer === 'potplayer' ? 'PotPlayer' : L.opt_player_other}
${CONF.crumbIcons.down}
PotPlayer
${L.opt_player_other}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: '480px', padding: '30px', height: 'auto', minHeight: 'auto', overflow: 'visible' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const bindSelect = (id, onSelect) => { const container = m.querySelector(`#${id}`); const trigger = container.querySelector('.pk-select-trigger'); const menu = container.querySelector('.pk-select-menu'); const txt = container.querySelector('span'); trigger.onclick = (e) => { e.stopPropagation(); const allMenus = m.querySelectorAll('.pk-select-menu'); const isCurrentlyOpen = menu.style.display === 'block'; allMenus.forEach(om => om.style.display = 'none'); menu.style.display = isCurrentlyOpen ? 'none' : 'block'; }; container.querySelectorAll('.pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); container.querySelectorAll('.pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; menu.style.display = 'none'; onSelect(item.dataset.val, item.textContent); }; }); }; const runBtn = m.querySelector('#ext_run'); const fixBtn = m.querySelector('#ext_potplayer_fix'); const getCleanSelectedUrl = () => cleanExternalPlaybackUrl(selectedUrl); const updatePotPlayerFixEntry = () => { if (fixBtn) fixBtn.style.display = selectedPlayer === 'potplayer' ? 'inline-flex' : 'none'; }; bindSelect('cs_res', (val, text) => { selectedUrl = val; selectedResName = text; }); bindSelect('cs_player', (val) => { selectedPlayer = val; runBtn.textContent = (val === 'potplayer') ? L.btn_start_play : L.btn_copy_link; updatePotPlayerFixEntry(); }); const closeAllMenus = () => m.querySelectorAll('.pk-select-menu').forEach(om => om.style.display = 'none'); setTimeout(() => document.addEventListener('click', closeAllMenus), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', closeAllMenus); _orgRemove(); }; m.querySelector('#ext_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { if (e.target && e.target.id === 'ext_potplayer_fix') return; e.preventDefault(); e.stopPropagation(); runBtn.click(); } }); if (fixBtn) { fixBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); openPotPlayerProtocolRepairHelper(getCleanSelectedUrl(), { button: runBtn, source: opts.source || 'normal', onRetryLikelyOpen: () => m.remove(), onRetryLikelyFail: () => m.remove() }); }; } runBtn.onclick = () => { gmSet('pk_ext_player', selectedPlayer); const cleanUrl = getCleanSelectedUrl(); if (selectedPlayer === 'potplayer') { launchPotPlayerWithFallback(cleanUrl, { button: runBtn, source: opts.source || 'normal', restoreText: L.btn_start_play, restoreBg: runBtn.style.background, restoreColor: runBtn.style.color, failToastDuration: 6500, failCloseDelay: 3200, onLikelyOpen: () => m.remove(), onLikelyFail: (ctx) => { if (!opts.keepOpenOnFail) m.remove(); schedulePotPlayerAutoRepairPrompt(cleanUrl, { source: opts.source || 'normal', autoRepairPrompt: ctx && ctx.autoRepairPrompt, autoRepairPromptDelay: ctx && ctx.autoRepairPromptDelay }); } }); } else { GM_setClipboard(cleanUrl); runBtn.textContent = L.msg_copy_success; runBtn.style.background = "#52c41a"; runBtn.disabled = true; setTimeout(() => m.remove(), 1000); } }; return m; }; const openDefaultPotPlayerForItem = async (item) => { const L = getStrings(); if (!item || S.trashMode) return false; setLoad(true); updateLoadTxt(L.loading_detail); let detail = item; try { detail = await resolvePlayableDetailForExternal(item); } catch (e) { setLoad(false); return false; } setLoad(false); const bestSource = getBestSource(detail); const cleanUrl = cleanExternalPlaybackUrl(bestSource && bestSource.src); if (!cleanUrl) return false; launchPotPlayerWithFallback(cleanUrl, { source: 'default_open', failToastDuration: 6500, failCloseDelay: 3200, silentLikelyFailToast: true, onLikelyFail: (ctx) => { schedulePotPlayerAutoRepairPrompt(cleanUrl, { source: 'default_open', autoRepairPrompt: ctx && ctx.autoRepairPrompt, autoRepairPromptDelay: ctx && ctx.autoRepairPromptDelay }); } }); return true; }; UI.btnExt.onclick = async () => { const id = S.getSelectedIds()[0]; const item = (S.shareParseMode ? ((S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id))) : null) || S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.loading_detail); let detail = item; try { detail = await resolvePlayableDetailForExternal(item); } catch (e) { if (!detail.web_content_link && !detail.medias) { setLoad(false); showToast(L.msg_video_fail, 'error'); return; } } setLoad(false); openExternalPlaybackDialog(detail, { source: 'normal' }); }; if (UI.btnExportM3U) UI.btnExportM3U.onclick = async () => { const L = getStrings(); const videos = getM3UExportSelectedVideos(); if (!videos.length) { showToast(L.msg_m3u_no_video, 'warning'); updateStat(); return; } if (videos.length > 50) { showToast((L.msg_m3u_export_limit || '').replace('{n}', '50'), 'warning'); return; } const btn = UI.btnExportM3U; const oldDisabled = btn.disabled; btn.disabled = true; btn.style.cursor = 'wait'; let progressTask = null; try { isGUISensitive = true; if (videos.some(it => it && it._isShareItem)) { showToast(L.msg_m3u_share_limited_save, 'warning', 6500); } progressTask = FloatBarManager.create(formatM3UExportProgress(0, videos.length)); const lines = ['#EXTM3U']; let exported = 0; let skipped = 0; let processed = 0; for (const item of videos) { try { const detail = await resolvePlayableDetailForExternal(item); const best = getBestSource(detail); const url = cleanExternalPlaybackUrl(best && best.src); if (!url) { skipped++; continue; } const duration = Number((item.params && item.params.duration) || (detail && detail.params && detail.params.duration) || -1); const extinfDuration = Number.isFinite(duration) && duration > 0 ? Math.round(duration) : -1; lines.push(`#EXTINF:${extinfDuration},${escapeM3UInfoText(item.name || (detail && detail.name) || 'video')}`); lines.push(url); exported++; } catch (e) { skipped++; } finally { processed++; if (progressTask) progressTask.update(formatM3UExportProgress(processed, videos.length)); } } if (progressTask) { progressTask.destroy(); progressTask = null; } if (!exported) { showToast(L.msg_m3u_no_valid_link, 'warning'); return; } downloadM3UText('\uFEFF' + lines.join('\n') + '\n', getM3UExportFileName()); const doneMsg = skipped ? (L.msg_m3u_export_partial || '').replace('{n}', exported).replace('{s}', skipped) : (L.msg_m3u_export_done || '').replace('{n}', exported); showToast(doneMsg, skipped ? 'warning' : 'success'); } finally { if (progressTask) progressTask.destroy(); btn.disabled = oldDisabled; btn.style.cursor = btn.disabled ? 'not-allowed' : 'pointer'; isGUISensitive = false; scheduleBackgroundResume(); updateStat(); } }; if (UI.btnImgSearch) { UI.btnImgSearch.onclick = () => { if (UI.btnImgSearch.disabled) return; const id = S.getSelectedIds()[0]; const item = (S.shareParseMode ? ((S.shareParseInsightItemMap && S.shareParseInsightItemMap.get(id)) || (S.shareParseItemMap && S.shareParseItemMap.get(id))) : null) || S.itemMap.get(id); if (!item || !item.thumbnail_link) return; const thumbUrl = item.thumbnail_link.replace('SIZE_MEDIUM', 'SIZE_LARGE'); const thumbImg = new Image(); thumbImg.crossOrigin = 'anonymous'; thumbImg.src = thumbUrl; startImageSearch(thumbImg, item.name, document.body, thumbUrl); }; } UI.win.querySelector('#pk-down').onclick = async () => { const selectedIds = S.getSelectedIds(); const isShareDownloadContext = !!(S.shareParseMode && S.shareParseListActive); const hasConflict = selectedIds.some(id => { if (isShareDownloadContext) return false; const item = getDownloadSourceItemById(id); if (item && item.kind === 'drive#folder') return isPathBusy(item.id); return S.movingIds.has(id); }); if (hasConflict) { showAlert(L.msg_op_blocked_moving); return; } 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 failedFiles = []; const HYDRATE_LIMIT = 20; const dlFilterRules = getDownloadFilterRules(); const hasDownloadFilterRules = dlFilterRules.hasRules; const filterStats = { scanned: 0, blocked: 0 }; const applyDownloadFilterToFile = (file) => { if (!hasDownloadFilterRules) return false; filterStats.scanned++; if (isDownloadFilterBlocked(file, dlFilterRules)) { filterStats.blocked++; return true; } return false; }; selectedIds.forEach(id => { const item = getDownloadSourceItemById(id); if (item) { if (item.kind === 'drive#folder') { rootNodes.push({...item, lineage: isShareDownloadContext ? getShareSelectedDownloadLineage(item) : [], retryCount: 0}); } else if (!applyDownloadFilterToFile(item)) { allFiles.push(isShareDownloadContext ? { ...item, _lineage: [] } : item); } } }); let progressTask = null; try { await coreRecursiveEngine(rootNodes, { signal, listFolder: isShareDownloadContext ? listShareDownloadFolder : null, maxRetries: isShareDownloadContext ? 2 : Infinity, onFile: (f, parent) => { if (applyDownloadFilterToFile(f)) return; if (isShareDownloadContext) f._lineage = (parent && parent.lineage) || []; allFiles.push(f); }, onFolderError: isShareDownloadContext ? (folder, error) => { failedFiles.push(formatShareDownloadFailure(folder, error)); } : null, onProgress: (st) => { updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`); } }); if (!isRunning) throw new Error('StoppedByUser'); if (allFiles.length === 0) { setLoad(false); if (failedFiles.length) { await showBrowserDownloadFailures(failedFiles, 0); return; } if (hasDownloadFilterRules && filterStats.scanned > 0 && filterStats.blocked === filterStats.scanned) { showToast(L.msg_batch_all_filtered.replace('{n}', filterStats.blocked)); } else { showToast(L.msg_batch_no_files); } return; } if (hasDownloadFilterRules && filterStats.blocked > 0) { showToast(L.msg_batch_filtered.replace('{n}', filterStats.blocked)); } let totalBytes = 0; for (let i = 0; i < allFiles.length; i++) { totalBytes += parseInt((allFiles[i] && allFiles[i].size) || 0, 10) || 0; } const needsSoftConfirm = allFiles.length > CONF.browserDownloadConfirmFileCount || totalBytes > CONF.browserDownloadConfirmTotalBytes; setLoad(false); if (needsSoftConfirm) { const confirmText = L.msg_down_confirm_total .replace('{n}', allFiles.length) .replace('{s}', fmtSize(totalBytes)) .replace('{fc}', CONF.browserDownloadConfirmFileCount) .replace('{fs}', fmtSize(CONF.browserDownloadConfirmTotalBytes)); if (!await showConfirm(confirmText)) return; } progressTask = FloatBarManager.create(L.msg_batch_hydrating); const readyFiles = []; const hydrateQueue = [...allFiles]; const activeTasks = new Set(); const hydrateWithRetry = async (file, maxRetries = 3) => { if (file._isShareItem) return await resolveShareDownloadTask(file, { signal }); if (file.web_content_link) return file; if (file.phase === "PHASE_TYPE_PENDING" || file.phase === "PHASE_TYPE_RUNNING" || file.trashed) return null; let lastErr = null; const lookupId = getOfflineReferenceLookupId(file) || file.id; for (let i = 0; i < maxRetries; i++) { if (!isRunning) return null; try { const detail = await apiGet(lookupId); if (detail && detail.web_content_link) return detail; if (detail && (detail.phase === "PHASE_TYPE_PENDING" || detail.phase === "PHASE_TYPE_RUNNING")) return null; throw new Error("Link Empty"); } catch (e) { lastErr = e; if (await markOfflineReferenceMissingFromError(file, e, { source: 'browser_download' })) return null; if (i < maxRetries - 1) await sleep(1000 * (i + 1)); } } throw lastErr; }; while ((hydrateQueue.length > 0 || activeTasks.size > 0) && isRunning) { while (hydrateQueue.length > 0 && activeTasks.size < HYDRATE_LIMIT && isRunning) { const file = hydrateQueue.pop(); const p = (async () => { try { const detail = await hydrateWithRetry(file); if (detail) { readyFiles.push(detail); } } catch (e) { console.error(`[Hydrate Failed] ${file.name}:`, e); failedFiles.push(file._isShareItem ? formatShareDownloadFailure(file, e) : `${file.name} ${L.str_aria2_fetch_err}`); } })().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}`); } let triggeredCount = 0; for (let i = 0; i < readyFiles.length; i++) { if (!isRunning) break; if (progressTask) progressTask.update(`${L.msg_down_progress} ${i + 1} / ${readyFiles.length}`); let triggered = false; if (isZeroByteFile(readyFiles[i])) { downloadEmptyFile(readyFiles[i]); triggered = true; } else { triggered = triggerNamedBrowserDownload(readyFiles[i], rewriteDownloadLinkForAccel(readyFiles[i].web_content_link, 'browser')); } if (triggered) triggeredCount++; else failedFiles.push(`${readyFiles[i].name} ${L.str_aria2_fetch_err}`); if (i < readyFiles.length - 1) { await sleep(2000); } else { await sleep(1500); } } if (isRunning && triggeredCount > 0 && failedFiles.length === 0) { showToast(L.msg_down_success.replace('{n}', triggeredCount)); } if (isRunning && failedFiles.length > 0) await showBrowserDownloadFailures(failedFiles, triggeredCount); } catch (e) { if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); isGUISensitive = false; scheduleBackgroundResume(); if (progressTask) progressTask.destroy(); } }; UI.win.querySelector('#pk-aria2').onclick = async () => { const initialDownloaderType = getCurrentDownloaderType(); if (isStoredDownloaderAddressCleared(initialDownloaderType)) { await openSettingsAndFocusDownloaderAddress(initialDownloaderType); return; } if (initialDownloaderType === 'idm') { const idmExportMode = normalizeIdmExportMode(gmGet('pk_idm_export_mode', CONF.idmExportMode)); const idmExePath = normalizeIdmExePath(gmGet('pk_idm_exe_path', CONF.idmExePath)); if (idmExportMode === 'bat' && !idmExePath) { await openSettingsAndFocusIdmExePath(); return; } } if (!await confirmDownloaderKeepStructureWithoutDir(initialDownloaderType)) return; const selectedIds = S.getSelectedIds(); const isShareDownloadContext = !!(S.shareParseMode && S.shareParseListActive); const hasConflict = selectedIds.some(id => { if (isShareDownloadContext) return false; const item = getDownloadSourceItemById(id); if (item && item.kind === 'drive#folder') return isPathBusy(item.id); return S.movingIds.has(id); }); if (hasConflict) { showAlert(L.msg_op_blocked_moving); return; } let ariaUrl = getStoredAriaRpcUrlForDownload(); let ariaToken = gmGet('pk_aria2_token', ''); let ariaDir = normalizeAriaDownloadDir(gmGet('pk_aria2_dir', CONF.aria2DownloadDir)); if (initialDownloaderType === 'aria2' && !ariaUrl) { await openSettingsAndFocusDownloaderAddress(initialDownloaderType); 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); }; ariaUrl = normalizeAriaRpcUrl(ariaUrl); const keepAria2Structure = getDownloaderKeepStructurePref(); const ariaDownloadDir = normalizeAriaDownloadDir(ariaDir || gmGet('pk_aria2_dir', CONF.aria2DownloadDir)); const allFiles = []; const rootNodes = []; const failedFiles = []; const HYDRATE_LIMIT = 40; const dlFilterRules = getDownloadFilterRules(); const hasDownloadFilterRules = dlFilterRules.hasRules; const stats = { hydratedCount: 0, lastUiTime: 0 }; const filterStats = { scanned: 0, blocked: 0 }; const applyDownloadFilterToFile = (file) => { if (!hasDownloadFilterRules) return false; filterStats.scanned++; if (isDownloadFilterBlocked(file, dlFilterRules)) { filterStats.blocked++; return true; } return false; }; const selectedIdsForAria2 = S.getSelectedIds(); selectedIdsForAria2.forEach(id => { const item = getDownloadSourceItemById(id); if (item) { if (item.kind === 'drive#folder') { rootNodes.push({...item, lineage: isShareDownloadContext ? getShareSelectedDownloadLineage(item) : [{ id: item.id, name: item.name }], retryCount: 0}); } else if (!applyDownloadFilterToFile(item)) { allFiles.push({ ...item, _lineage: [] }); } } }); let progressTask = null; try { await coreRecursiveEngine(rootNodes, { signal, listFolder: isShareDownloadContext ? listShareDownloadFolder : null, maxRetries: isShareDownloadContext ? 2 : Infinity, onFile: (f, parent) => { if (applyDownloadFilterToFile(f)) return; f._lineage = parent.lineage || []; allFiles.push(f); }, onFolderError: isShareDownloadContext ? (folder, error) => { failedFiles.push(formatShareDownloadFailure(folder, error)); } : null, onProgress: (st) => { const now = Date.now(); if (now - stats.lastUiTime > 150) { updateLoadTxt(`${L.msg_batch_scanning}\n${L.str_files}: ${allFiles.length} | ${L.str_speed}: ${st.currentConcurrency}`); stats.lastUiTime = now; } } }); if (!isRunning) throw new Error('StoppedByUser'); if (allFiles.length === 0) { setLoad(false); if (failedFiles.length) { await showBrowserDownloadFailures(failedFiles, 0); return; } if (hasDownloadFilterRules && filterStats.scanned > 0 && filterStats.blocked === filterStats.scanned) { showToast(L.msg_batch_all_filtered.replace('{n}', filterStats.blocked)); } else { showToast(L.msg_batch_no_files); } return; } if (hasDownloadFilterRules && filterStats.blocked > 0) { showToast(L.msg_batch_filtered.replace('{n}', filterStats.blocked)); } setLoad(false); progressTask = FloatBarManager.create(L.msg_batch_hydrating); const readyFiles =[]; const hydrateQueue = [...allFiles]; const activeTasks = new Set(); const hydrateWithRetry = async (file, maxRetries = 3) => { if (file._isShareItem) return await resolveShareDownloadTask(file, { signal }); if (file.web_content_link) return file; if (file.phase === "PHASE_TYPE_PENDING" || file.phase === "PHASE_TYPE_RUNNING" || file.trashed) return null; let lastErr = null; const lookupId = getOfflineReferenceLookupId(file) || file.id; for (let i = 0; i < maxRetries; i++) { if (!isRunning) return null; try { const detail = await apiGet(lookupId); if (detail && detail.web_content_link) return detail; if (detail && (detail.phase === "PHASE_TYPE_PENDING" || detail.phase === "PHASE_TYPE_RUNNING")) return null; throw new Error("Link Empty"); } catch (e) { lastErr = e; if (await markOfflineReferenceMissingFromError(file, e, { source: 'downloader_hydrate' })) return null; 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._isShareItem ? formatShareDownloadFailure(file, e) : 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 && failedFiles.length) { await showBrowserDownloadFailures(failedFiles, 0); return; } if (readyFiles.length > 0 && isRunning) { const zeroByteFiles = readyFiles.filter(isZeroByteFile); if (zeroByteFiles.length > 0) { showToast(L.str_aria2_zero_byte_skipped); for (let i = readyFiles.length - 1; i >= 0; i--) { if (isZeroByteFile(readyFiles[i])) readyFiles.splice(i, 1); } if (!readyFiles.length) { if (failedFiles.length) await showBrowserDownloadFailures(failedFiles, 0); return; } } await preferOriginalVideoLinksForDownloader(readyFiles, { isRunning: () => isRunning, onProgress: (done, total) => { if (progressTask) progressTask.update(`${L.msg_downloader_prepare_original_link} ${done} / ${total}`); } }); if (!isRunning) throw new Error('StoppedByUser'); const currentDownloaderType = getCurrentDownloaderType(); const showDownloaderFailures = async (list, type, successCount = null, detailText = '') => { let failListText = list.slice(0, 10).join('\n'); if (list.length > 10) { failListText += '\n...'; failListText += L.msg_aria2_batch_fail_log; } const summaryTemplate = normalizeDownloaderType(type) === 'idm' ? L.msg_idm_exported_with_fail : L.msg_downloader_sent_with_fail; const summary = Number.isFinite(successCount) ? formatDownloaderCountText(summaryTemplate, type, { s: successCount, f: list.length }) : `${L.str_failed} ${list.length} ${L.str_items}`; const detail = detailText ? `\n\n${detailText}` : ''; await showAlert(`${summary}${detail}\n\n${failListText}`, L.title_alert); if (list.length > 10) { try { const blob = new Blob([list.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); const failFileName = formatDownloaderCountText(L.str_downloader_fail_file_name, type); a.href = url; a.download = `${failFileName}_${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); } } }; if (currentDownloaderType === 'idm') { const idmExportMode = normalizeIdmExportMode(gmGet('pk_idm_export_mode', CONF.idmExportMode)); const idmUrlExportType = normalizeIdmUrlExportType(gmGet('pk_idm_url_export_type', CONF.idmUrlExportType)); const idmExePath = normalizeIdmExePath(gmGet('pk_idm_exe_path', CONF.idmExePath)); const idmBatRoot = normalizeIdmBatDownloadRoot(gmGet('pk_idm_bat_root', CONF.idmBatDownloadRoot)); const idmBatKeepFolderStructure = getDownloaderKeepStructurePref(); const idmKeepStructure = idmExportMode === 'bat' && idmBatKeepFolderStructure && !!idmBatRoot; const idmTasks = buildStandardDownloadTasks(readyFiles, { keepStructure: idmKeepStructure, downloadDir: idmBatRoot }); const idmTaskFiles = new Set(idmTasks.map(task => task.file)); readyFiles.forEach(file => { if (!idmTaskFiles.has(file)) failedFiles.push(`${file.name} ${L.msg_idm_not_http}`); }); let successCount = 0; if (idmTasks.length > 0) { if (progressTask) progressTask.update(L.msg_idm_exporting); const ts = formatExportTimestamp(); if (idmExportMode === 'bat') { const batText = buildIdmBatScript(idmTasks, { downloadRoot: idmBatRoot, idmExePath, keepFolderStructure: idmKeepStructure }); downloadTextExport(batText, `${L.str_idm_bat_file_name}_${ts}.bat`, 'application/x-msdownload;charset=utf-8'); successCount = idmTasks.length; } else { const urls = idmTasks.map(task => normalizeIdmExportUrl(task.url)).filter(Boolean); if (idmUrlExportType === 'ef2') { downloadTextExport(buildIdmEf2Export(idmTasks), `${L.str_idm_url_file_name}_${ts}.ef2`, 'application/octet-stream;charset=utf-8'); } else { downloadTextExport(urls.join('\r\n') + (urls.length ? '\r\n' : ''), `${L.str_idm_url_file_name}_${ts}.txt`); } successCount = urls.length; } } if (failedFiles.length > 0) { await showDownloaderFailures(failedFiles, currentDownloaderType, successCount); } else if (idmExportMode === 'bat') { showToast(formatDownloaderCountText(L.msg_idm_bat_exported, currentDownloaderType, { n: successCount })); } else { showToast(formatDownloaderCountText(L.msg_idm_url_exported, currentDownloaderType, { n: successCount })); } } else if (currentDownloaderType === 'gopeed') { const gopeedApiUrl = normalizeGopeedApiUrl(gmGet('pk_gopeed_url', CONF.gopeedApiUrl)); const gopeedToken = gmGet('pk_gopeed_token', ''); const gopeedDownloadDir = normalizeGopeedDownloadDir(gmGet('pk_gopeed_dir', CONF.gopeedDownloadDir)); const gopeedKeepStructurePref = getDownloaderKeepStructurePref(); const gopeedKeepStructure = gopeedKeepStructurePref && !!gopeedDownloadDir; const gopeedTasks = buildStandardDownloadTasks(readyFiles, { keepStructure: gopeedKeepStructure, downloadDir: gopeedDownloadDir }); const gopeedTaskFiles = new Set(gopeedTasks.map(task => task.file)); readyFiles.forEach(file => { if (!gopeedTaskFiles.has(file)) failedFiles.push(`${file.name} ${L.msg_gopeed_not_http}`); }); let successCount = 0; if (gopeedTasks.length > 0) { if (progressTask) progressTask.update(`${formatDownloaderText(L.msg_downloader_sending_batch, currentDownloaderType)} 0 / ${gopeedTasks.length}`); const result = await submitGopeedTasks(gopeedTasks, { apiUrl: gopeedApiUrl, token: gopeedToken, batchSize: CONF.gopeedBatchSize, isRunning: () => isRunning, onProgress: (done, total) => { if (progressTask) progressTask.update(`${formatDownloaderText(L.msg_downloader_sending_batch, currentDownloaderType)} ${done} / ${total}`); } }); successCount = result.successCount; failedFiles.push(...result.failedFiles); } if (failedFiles.length > 0) { await showDownloaderFailures(failedFiles, currentDownloaderType); } else { showToast(formatDownloaderCountText(L.msg_downloader_sent, currentDownloaderType, { n: successCount })); } } else if (currentDownloaderType === 'abdm') { const abdmApiUrl = normalizeAbdmApiUrl(gmGet('pk_abdm_url', CONF.abdmApiUrl)); const abdmDownloadDir = normalizeAbdmDownloadDir(gmGet('pk_abdm_dir', CONF.abdmDownloadDir)); const abdmKeepStructurePref = getDownloaderKeepStructurePref(); const abdmKeepStructure = abdmKeepStructurePref && !!abdmDownloadDir; const abdmTasks = buildStandardDownloadTasks(readyFiles, { keepStructure: abdmKeepStructure, downloadDir: abdmDownloadDir }); const abdmTaskFiles = new Set(abdmTasks.map(task => task.file)); readyFiles.forEach(file => { if (!abdmTaskFiles.has(file)) failedFiles.push(`${file.name} ${L.msg_abdm_not_http}`); }); let successCount = 0; if (abdmTasks.length > 0) { if (progressTask) progressTask.update(`${formatDownloaderText(L.msg_downloader_sending_batch, currentDownloaderType)} 0 / ${abdmTasks.length}`); const result = await submitAbdmTasks(abdmTasks, { apiUrl: abdmApiUrl, isRunning: () => isRunning, onProgress: (done, total) => { if (progressTask) progressTask.update(`${formatDownloaderText(L.msg_downloader_sending_batch, currentDownloaderType)} ${done} / ${total}`); } }); successCount = result.successCount; failedFiles.push(...result.failedFiles); } if (failedFiles.length > 0) { await showDownloaderFailures(failedFiles, currentDownloaderType); } else { showToast(formatDownloaderCountText(L.msg_downloader_sent, currentDownloaderType, { n: successCount })); } } else { 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 (keepAria2Structure && f._lineage && f._lineage.length > 0) { relativePrefix = f._lineage.map(n => sanitize(n.name)).join('/') + '/'; } const outPath = relativePrefix + sanitize(f.name); const rpcOptions = Object.assign({ out: outPath, header: [`User-Agent: ${navigator.userAgent}`, `Referer: https://mypikpak.com/`] }, CONF.aria2TolerantOptions || {}); if (ariaDownloadDir) rpcOptions.dir = ariaDownloadDir; return { jsonrpc: '2.0', method: 'aria2.addUri', id: `pk_${Date.now()}_${Math.random().toString(16).slice(2)}`, params: buildAriaRpcParams(ariaToken, [[rewriteDownloadLinkForAccel(f.web_content_link, 'aria2')], rpcOptions]) }; }); try { await aria2RpcRequest(ariaUrl, payload, Math.max(10000, chunk.length * 800)); successCount += chunk.length; } catch (rpcErr) { console.error("[RPC Batch Error]", rpcErr); chunk.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_rpc_err)); if (rpcErr.message === "Network Error" || rpcErr.message === "Timeout") { console.warn("[RPC Circuit Breaker] Aria2 disconnected. Aborting remaining batches."); rpcFatalError = true; const remainingFiles = readyFiles.slice(i + BATCH_SIZE); remainingFiles.forEach(f => failedFiles.push(f.name + " " + L.str_aria2_aborted)); } } if (progressTask) progressTask.update(`${formatDownloaderText(L.msg_downloader_sending_batch, currentDownloaderType)} ${successCount} / ${readyFiles.length}`); } if (failedFiles.length > 0) { await showDownloaderFailures(failedFiles, currentDownloaderType); } else { showToast(formatDownloaderCountText(L.msg_downloader_sent, currentDownloaderType, { n: successCount })); } } } } catch (e) { if (e.message !== 'StoppedByUser' && e.name !== 'AbortError') { const errorDownloaderType = getCurrentDownloaderType(); if (errorDownloaderType === 'idm') showAlert(`${L.str_error}: ${e.message}`); else if (errorDownloaderType === 'abdm') showAlert(L.desc_abdm_setup, L.lbl_downloader_status); else showAlert(`${formatDownloaderText(L.msg_downloader_check_fail, errorDownloaderType)}\n(${e.message})`); } } finally { setLoad(false); isGUISensitive = false; scheduleBackgroundResume(); 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); } } }; function isVirtualResultRootId(id) { const key = String(id || ''); return key === 'recent_root' || key === 'starred_root'; } function getVirtualResultModeSnapshot(source = S) { return { recentMode: !!(source && source.recentMode), starredMode: !!(source && source.starredMode) }; } function isVirtualResultRootPath(id, pathLen, modeSnapshot = null) { const key = String(id || ''); const depth = Number(pathLen) || 0; if (depth !== 1) return false; if (key === 'recent_root' || key === 'starred_root') return true; const mode = modeSnapshot || getVirtualResultModeSnapshot(); return !!( (mode.recentMode && key === 'recent_root') || (mode.starredMode && (key === '' || key === 'root')) ); } const executeBatchDelete = async (ids, options = {}) => { const { silent = false, deleteFiles = false, isTask = false, forceRefresh = false, hardDelete = false, explicitItems = [], originPathId = '', originPathDepth = 0, originRecentMode = false, originStarredMode = false, suppressVirtualRootRefresh = false } = options; if (!ids || ids.length === 0) return; const currentDeletePathId = String(originPathId || (S.path && S.path.length ? S.path[S.path.length - 1].id : '') || 'root'); const currentDeletePathDepth = Number(originPathDepth || (Array.isArray(S.path) ? S.path.length : 0)); const deleteOriginModeSnapshot = { recentMode: !!originRecentMode, starredMode: !!originStarredMode }; const deleteStartedFromVirtualResultRoot = !!(suppressVirtualRootRefresh && isVirtualResultRootPath(currentDeletePathId, currentDeletePathDepth, deleteOriginModeSnapshot)); if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') { abandonLiveManualRefresh('local_mutation', { localRemovedIds: ids, toast: 'info', detach: false }); } const tempItemLookup = new Map(); if (S.itemMap) S.itemMap.forEach((v, k) => tempItemLookup.set(k, v)); if (explicitItems && explicitItems.length > 0) { explicitItems.forEach(item => { if (item && item.id) tempItemLookup.set(item.id, item); }); } const BATCH_SIZE = 200; const progressTask = FloatBarManager.create(L.str_deleting); const updateFloat = progressTask.update; S.movingSourceId = currentDeletePathId || 'root'; S.movingDestId = 'trash'; const allLockedIdsArray =[]; ids.forEach(id => { S.movingIds.add(id); allLockedIdsArray.push(id); if (isTask && deleteFiles) { const taskItem = tempItemLookup.get(id); if (taskItem && taskItem.file_id) { S.movingIds.add(taskItem.file_id); allLockedIdsArray.push(taskItem.file_id); } } }); if (S.broadcast) S.broadcast.postMessage({ type: 'LOCK_ADD', ids: allLockedIdsArray, src: S.movingSourceId, dst: S.movingDestId }); if (typeof updateGlobalLockCSS === 'function') updateGlobalLockCSS(); isGUISensitive = true; S.sel.clear(); S.lastSelIdx = -1; S.activeId = null; try { const totalToDelete = ids.length; let deletedCount = 0; const affectedParentIds = new Set(); const deletedSet = new Set(); if (!deleteStartedFromVirtualResultRoot && !isVirtualResultRootId(currentDeletePathId)) { affectedParentIds.add(currentDeletePathId || '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) { const lastId = chunk[chunk.length - 1]; let verifyRetries = 0; while (verifyRetries < 20) { try { const meta = await apiGet(lastId); if (!hardDelete && meta.trashed) break; } catch (e) { break; } updateFloat(`${L.str_deleting} ${deletedCount}/${totalToDelete}`); verifyRetries++; await sleep(500); } } const chunkSet = new Set(chunk); const chunkLockedIds =[]; chunk.forEach(id => { S.movingIds.delete(id); chunkLockedIds.push(id); const it = tempItemLookup.get(id); let physicalFolderId = null; if (it) { if (it.kind === 'drive#folder') { physicalFolderId = it.id; } else if (isTask && deleteFiles && it.file_id) { const isFolderTask = (it.mime_type && (it.mime_type.includes('folder') || it.mime_type.includes('directory'))) || (it.icon_link && it.icon_link.includes('folder')) || (typeof globalCache !== 'undefined' && globalCache.has(it.file_id)); if (isFolderTask) { physicalFolderId = it.file_id; } } } if (physicalFolderId && typeof globalCache !== 'undefined') { const purgeDescendants = (fid) => { const data = globalCache.get(fid) || (S.cache ? S.cache.get(fid) : null); if (data) { globalTombstoneCache.set(fid, Array.isArray(data) ?[...data] : {...data}); const list = Array.isArray(data) ? data : (data.items ||[]); list.forEach(child => { deletedSet.add(child.id); S.itemMap.delete(child.id); if (child.kind === 'drive#folder') { purgeDescendants(child.id); } }); globalCache.delete(fid); if (S.cache) S.cache.delete(fid); } }; purgeDescendants(physicalFolderId); } if (isTask && deleteFiles && it && it.file_id) { S.movingIds.delete(it.file_id); chunkLockedIds.push(it.file_id); deletedSet.add(it.file_id); if (typeof globalParentIndex !== 'undefined' && globalParentIndex.has(it.file_id)) { const parentInfo = globalParentIndex.get(it.file_id); if (parentInfo && parentInfo.id) { affectedParentIds.add(parentInfo.id === 'root' ? '' : parentInfo.id); } } affectedParentIds.add('root'); affectedParentIds.add(''); } if (it && S.analyzeMap) { const analyzeTargetId = physicalFolderId || id; const analyzeIdsToRemove = new Set([analyzeTargetId, id]); const queue = [analyzeTargetId, id]; while (queue.length > 0) { const currId = queue.shift(); S.analyzeMap.forEach((node, nId) => { if (node.parentId === currId && !analyzeIdsToRemove.has(nId)) { analyzeIdsToRemove.add(nId); queue.push(nId); } }); } analyzeIdsToRemove.forEach(remId => { if (S.analyzeMap.has(remId)) S.analyzeMap.delete(remId); }); const lostSize = parseInt(it.size || 0); if (lostSize > 0) { let currPid = it.parent_id; if (!currPid && isTask && typeof globalParentIndex !== 'undefined') { const pInfo = globalParentIndex.get(it.file_id); if (pInfo) currPid = pInfo.id; } let safety = 50; while (currPid && S.analyzeMap.has(currPid) && safety > 0) { const pNode = S.analyzeMap.get(currPid); pNode.size = Math.max(0, pNode.size - lostSize); currPid = pNode.parentId; safety--; } } if (S.analyzeSimGroups) { S.analyzeSimGroups.forEach(group => { group.ids = group.ids.filter(gid => !analyzeIdsToRemove.has(gid)); }); S.analyzeSimGroups = S.analyzeSimGroups.filter(group => group.ids.length >= 2); clearGroupedGridMetaCache(); clearGridStableRows(UI.in); 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 scheduleBackgroundResume === 'function') scheduleBackgroundResume(); } if (window.pkSmartRefreshTrigger && !deleteStartedFromVirtualResultRoot) window.pkSmartRefreshTrigger(); if (forceRefresh && !deleteStartedFromVirtualResultRoot) { 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; scheduleBackgroundResume(); } if (progressTask) progressTask.destroy(); } }; if (UI.btnClearHistoryAll) { UI.btnClearHistoryAll.onclick = () => { clearAllPlayHistory(); }; } if (UI.btnMagnetArchiveCheck) { UI.btnMagnetArchiveCheck.onclick = () => { handleMagnetArchiveCheckSelected(); }; } UI.btnDel.onclick = async () => { const selectedIds = S.getSelectedIds(); const count = selectedIds.length; if (!count) return; if (S.historyMode) { if (!await showConfirm(L.warn_clear_history.replace('{n}', count))) return; const eventIds = []; selectedIds.forEach(id => { const item = S.itemMap.get(id); const eventId = item ? (item._history_event_id || item.officialEventId || item.eventId || '') : ''; if (eventId) eventIds.push(eventId); }); if (eventIds.length === 0) { showToast(L.err_official_history_delete, 'error'); return; } const ok = await deleteOfficialPlayHistoryEvents(eventIds); if (!ok) { showToast(L.err_official_history_delete, 'error'); return; } ensureHistorySessionState(); eventIds.forEach(id => S.historyDeletedEventIds.add(String(id))); const selIds = new Set(selectedIds); selIds.forEach(id => S.historyDeletedFileIds.add(String(id))); selIds.forEach(id => { S.itemMap.delete(id); }); S.items = S.items.filter(it => !selIds.has(it.id)); S.historyItems = normalizeHistorySessionItems((S.historyItems && S.historyItems.length ? S.historyItems : S.items).filter(it => !selIds.has(it.id))); S.historyDirty = true; syncHistorySessionCache(); S.clearSelection(); refresh(); updateStat(); showToast(L.msg_clear_history_done); return; } if (S.offlineMode) { const html = `

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

`; if (typeof m !== 'undefined' && m.remove) m.remove(); const taskModal = showModal(html); const modalBox = taskModal.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = "420px"; modalBox.style.padding = "24px"; modalBox.style.borderRadius = "12px"; } const close = () => taskModal.remove(); taskModal.querySelector('#del_task_cancel').onclick = close; const closeBtn = taskModal.querySelector('.pk-modal-close'); if(closeBtn) closeBtn.onclick = close; taskModal.querySelector('#del_task_confirm').onclick = async () => { const isDeleteFile = taskModal.querySelector('#del_task_files').checked; taskModal.remove(); try { const taskItems = selectedIds.map(id => S.itemMap.get(id) || { id, task_id: id, kind: 'drive#task' }); const result = await handleOfflineTaskDelete(taskItems, { userInitiated: true, source: 'toolbar_delete', batch: selectedIds.length > 1, deleteFiles: isDeleteFile }); if (result && result.handled) return; } catch (e) { showAlert(`${L.str_error}: ${e.message}`); return; } await executeBatchDelete(selectedIds, { isTask: true, deleteFiles: isDeleteFile, forceRefresh: true }); }; taskModal.tabIndex = 0; setTimeout(() => taskModal.focus(), 10); taskModal.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); taskModal.querySelector('#del_task_confirm').click(); } }); return; } ensureItemMap(); const totalCount = selectedIds.length; let deleteCheckLoadTimer = null; let deleteCheckLoadShown = false; const clearDeleteCheckLoad = () => { if (deleteCheckLoadTimer) { clearTimeout(deleteCheckLoadTimer); deleteCheckLoadTimer = null; } if (deleteCheckLoadShown) { setLoad(false); deleteCheckLoadShown = false; } }; deleteCheckLoadTimer = setTimeout(() => { deleteCheckLoadTimer = null; deleteCheckLoadShown = true; setLoad(true); updateLoadTxt(`${L.str_checking_bl}\n${processed} / ${totalCount}`); }, 120); const blSet = S.blSet; const blFolderSet = S.blFolderSet; const toDeleteIds = []; let blacklistedCount = 0; let processed = 0; let lastYieldTime = performance.now(); for (const id of selectedIds) { const item = S.itemMap.get(id); if (!item) { processed++; continue; } if (!S.trashMode && isSystemItem(item)) { 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) { if (deleteCheckLoadShown) updateLoadTxt(`${L.str_checking_bl}\n${processed} / ${totalCount}`); await sleep(0); lastYieldTime = performance.now(); } } } clearDeleteCheckLoad(); if (blacklistedCount > 0) { showToast(L.msg_del_protected.replace('{n}', blacklistedCount)); } if (blacklistedCount > 0 && toDeleteIds.length === 0) { S.clearSelection(); refresh(); updateStat(); return; } if (toDeleteIds.length === 0) { showAlert(L.msg_del_none); return; } const delRes = await showDeleteConfirm(L.warn_del.replace('{n}', toDeleteIds.length)); if (!delRes.confirm) return; await executeBatchDelete(toDeleteIds, { hardDelete: delRes.hardDelete }); }; UI.btnDeselect.onclick = () => { S.clearSelection(); refresh(); }; const processBlacklistAction = async (action) => { if (S.updateBlCache) S.updateBlCache(); const selectedItems = getSelectedBlacklistItems(); const totalSelected = selectedItems.length; if (totalSelected === 0) return; const allSelectedAlreadyAdded = selectedItems.every(isItemInBlacklist); const isRemove = allSelectedAlreadyAdded && action !== 'add'; const progressTask = FloatBarManager.create(L.str_init_op); const updateFloat = progressTask.update; let isRunning = true; UI.stopBtn.onclick = () => { isRunning = false; updateFloat(L.str_stopping); }; await sleep(16); const parseList = (str) => str ? str.split(/[\r\n]+/).map(s => s.trim()).filter(s => s) : []; const fileListStr = gmGet('pk_blacklist', ''); const folderListStr = gmGet('pk_blacklist_folders', ''); let currentFiles = parseList(fileListStr); let currentFolders = parseList(folderListStr); const targetFileKeys = new Set(); const targetFolderKeys = new Set(); const toAddFiles = []; const toAddFolders = []; let processedCount = 0; let lastYieldTime = performance.now(); const len = selectedItems.length; for (let i = 0; i < len; i++) { if (!isRunning) break; const item = selectedItems[i]; const name = getBlacklistItemName(item); const key = getBlacklistCleanKey(name); if (isBlacklistFolderItem(item)) { targetFolderKeys.add(key); if (!isRemove) toAddFolders.push(name); } else { targetFileKeys.add(key); if (!isRemove) toAddFiles.push(name); } processedCount++; if ((processedCount & 63) === 0) { const now = performance.now(); if (now - lastYieldTime > 12) { updateFloat(`${L.str_analyzing} ${processedCount} / ${totalSelected}`); await sleep(0); lastYieldTime = performance.now(); } } } if (!isRunning) { progressTask.destroy(); showAlert(L.msg_bl_stop); return; } updateFloat(L.str_processing); await sleep(10); let finalCount = 0; let dataChanged = false; if (isRemove) { const oldFileCount = currentFiles.length; const oldFolderCount = currentFolders.length; currentFiles = currentFiles.filter(name => !targetFileKeys.has(getBlacklistCleanKey(name))); currentFolders = currentFolders.filter(name => !targetFolderKeys.has(getBlacklistCleanKey(name))); if (oldFileCount !== currentFiles.length || oldFolderCount !== currentFolders.length) { dataChanged = true; finalCount = (oldFileCount - currentFiles.length) + (oldFolderCount - currentFolders.length); } } else { const existingFileKeys = new Set(currentFiles.map(s => getBlacklistCleanKey(s))); const existingFolderKeys = new Set(currentFolders.map(s => getBlacklistCleanKey(s))); const appendFiles = []; const appendFolders = []; for (const name of toAddFiles) { const key = getBlacklistCleanKey(name); if (!key || existingFileKeys.has(key)) continue; existingFileKeys.add(key); appendFiles.push(name); } for (const name of toAddFolders) { const key = getBlacklistCleanKey(name); if (!key || existingFolderKeys.has(key)) continue; existingFolderKeys.add(key); appendFolders.push(name); } const fileStats = getBlacklistAppendStats('pk_blacklist', currentFiles.join('\n'), appendFiles.join('\n')); const folderStats = getBlacklistAppendStats('pk_blacklist_folders', currentFolders.join('\n'), appendFolders.join('\n')); currentFiles = getBlacklistLineItems(fileStats.normalized); currentFolders = getBlacklistLineItems(folderStats.normalized); finalCount = fileStats.actualAdded + folderStats.actualAdded; dataChanged = finalCount > 0; processBlacklistAction._pkOverLimit = !!(fileStats.clipped || folderStats.clipped); } 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; if (!isRemove && processBlacklistAction._pkOverLimit) { delete processBlacklistAction._pkOverLimit; showToast(L.msg_config_input_over_limit); } else { delete processBlacklistAction._pkOverLimit; showToast(msgTemplate.replace('{n}', finalCount)); } }; UI.btnBlacklistManager.onclick = showBlacklistModal; if (UI.btnTrashBlacklistManager) UI.btnTrashBlacklistManager.onclick = showBlacklistModal; if (UI.uploadWrap && UI.btnUpload) { const uploadMenu = UI.uploadWrap.querySelector('.pk-dropdown-menu'); const restoreUploadMenu = () => { if (!uploadMenu) return; if (uploadMenu._pkOriginParent && uploadMenu.parentNode !== uploadMenu._pkOriginParent) { uploadMenu._pkOriginParent.appendChild(uploadMenu); } uploadMenu.style.position = ''; uploadMenu.style.top = ''; uploadMenu.style.left = ''; uploadMenu.style.right = ''; uploadMenu.style.bottom = ''; uploadMenu.style.marginTop = ''; uploadMenu.style.zIndex = ''; uploadMenu.style.minWidth = ''; uploadMenu.style.visibility = ''; uploadMenu.style.pointerEvents = ''; uploadMenu.style.zoom = ''; uploadMenu.style.transformOrigin = ''; delete uploadMenu.dataset.pkPortal; }; const closeUploadMenu = () => { if (!uploadMenu) return; if (uploadMenu._pkPlaceRaf1) cancelAnimationFrame(uploadMenu._pkPlaceRaf1); if (uploadMenu._pkPlaceRaf2) cancelAnimationFrame(uploadMenu._pkPlaceRaf2); uploadMenu._pkPlaceRaf1 = 0; uploadMenu._pkPlaceRaf2 = 0; uploadMenu.style.display = 'none'; restoreUploadMenu(); UI.uploadWrap.classList.remove('active'); }; window.__pkCloseUploadMenu = closeUploadMenu; const placeUploadMenu = () => { if (!uploadMenu) return; if (!uploadMenu._pkOriginParent) uploadMenu._pkOriginParent = UI.uploadWrap; const isGridView = !!(UI.win && UI.win.classList.contains('pk-grid-view')); if (!isGridView) { if (uploadMenu.parentNode !== uploadMenu._pkOriginParent) { uploadMenu._pkOriginParent.appendChild(uploadMenu); } uploadMenu.classList.toggle('pk-dark', !!(el && el.classList.contains('pk-dark'))); uploadMenu.style.position = 'absolute'; uploadMenu.style.top = '100%'; uploadMenu.style.right = '0'; uploadMenu.style.left = 'auto'; uploadMenu.style.bottom = 'auto'; uploadMenu.style.marginTop = '6px'; uploadMenu.style.zIndex = ''; uploadMenu.style.minWidth = ''; uploadMenu.style.visibility = ''; uploadMenu.style.pointerEvents = ''; delete uploadMenu.dataset.pkPortal; return; } if (uploadMenu.parentNode !== document.body) { document.body.appendChild(uploadMenu); } uploadMenu.classList.toggle('pk-dark', !!(el && el.classList.contains('pk-dark'))); const btnRect = typeof getLogicalRect === 'function' ? getLogicalRect(UI.btnUpload) : UI.btnUpload.getBoundingClientRect(); const pad = 8; uploadMenu.dataset.pkPortal = '1'; uploadMenu.style.position = 'fixed'; uploadMenu.style.left = '0'; uploadMenu.style.top = '0'; uploadMenu.style.right = 'auto'; uploadMenu.style.bottom = 'auto'; uploadMenu.style.marginTop = '0'; uploadMenu.style.zIndex = '10060'; uploadMenu.style.transformOrigin = 'top right'; uploadMenu.style.minWidth = Math.max(Math.round(btnRect.width), 140) + 'px'; uploadMenu.style.display = 'flex'; uploadMenu.style.visibility = 'hidden'; uploadMenu.style.pointerEvents = 'none'; const menuRect = uploadMenu.getBoundingClientRect(); const menuW = menuRect.width; const menuH = menuRect.height; const winW = window.innerWidth; const winH = window.innerHeight; let left = btnRect.right - menuW; let top = btnRect.bottom + 6; if (left < pad) left = pad; if (left + menuW > winW - pad) { left = winW - pad - menuW; } if (top + menuH > winH - pad) { top = btnRect.top - menuH - 6; } if (top < pad) top = pad; uploadMenu.style.left = Math.round(left) + 'px'; uploadMenu.style.top = Math.round(top) + 'px'; uploadMenu.style.visibility = ''; uploadMenu.style.pointerEvents = ''; }; const requestPlaceUploadMenu = () => { if (!uploadMenu) return; if (uploadMenu.style.display !== 'flex') return; if (!UI.uploadWrap.classList.contains('active')) return; if (uploadMenu._pkPlaceRaf1) cancelAnimationFrame(uploadMenu._pkPlaceRaf1); if (uploadMenu._pkPlaceRaf2) cancelAnimationFrame(uploadMenu._pkPlaceRaf2); uploadMenu._pkPlaceRaf1 = requestAnimationFrame(() => { uploadMenu._pkPlaceRaf2 = requestAnimationFrame(() => { uploadMenu._pkPlaceRaf1 = 0; uploadMenu._pkPlaceRaf2 = 0; if (!uploadMenu || uploadMenu.style.display !== 'flex') return; if (!UI.uploadWrap.classList.contains('active')) return; placeUploadMenu(); }); }); }; window.__pkRequestPlaceUploadMenu = requestPlaceUploadMenu; if (uploadMenu && !uploadMenu.dataset.pkBindStop) { uploadMenu.addEventListener('click', (e) => e.stopPropagation()); uploadMenu.dataset.pkBindStop = '1'; } if (!window.__pkUploadMenuPortalBound) { window.addEventListener('resize', () => { requestPlaceUploadMenu(); }, { passive: true }); document.addEventListener('scroll', () => { closeUploadMenu(); }, true); if (window.visualViewport) { window.visualViewport.addEventListener('resize', () => { requestPlaceUploadMenu(); }, { passive: true }); window.visualViewport.addEventListener('scroll', () => { requestPlaceUploadMenu(); }, { passive: true }); } document.addEventListener('mousedown', (e) => { if (!uploadMenu || uploadMenu.style.display !== 'flex') return; const target = e.target; if (uploadMenu.contains(target)) return; if (UI.btnUpload.contains(target)) return; if (UI.uploadWrap.contains(target) && !uploadMenu.dataset.pkPortal) return; closeUploadMenu(); }, true); window.__pkUploadMenuPortalBound = true; } if (window.ResizeObserver && !UI.uploadWrap.__pkUploadMenuRO) { UI.uploadWrap.__pkUploadMenuRO = new ResizeObserver(() => { requestPlaceUploadMenu(); }); UI.uploadWrap.__pkUploadMenuRO.observe(UI.uploadWrap); } UI.btnUpload.onclick = (e) => { e.stopPropagation(); if (!isRealHomeUploadView()) { syncLocalUploadVisibility(); return; } const isActive = uploadMenu && uploadMenu.style.display === 'flex' && UI.uploadWrap.classList.contains('active'); document.querySelectorAll('.pk-dropdown-menu, .pk-select-menu').forEach(m => m.style.display = 'none'); document.querySelectorAll('.pk-dropdown-wrap').forEach(w => w.classList.remove('active')); if (isActive) { closeUploadMenu(); return; } if (!uploadMenu) return; uploadMenu.style.display = 'flex'; placeUploadMenu(); UI.uploadWrap.classList.add('active'); }; UI.actUpFile.onclick = (e) => { e.stopPropagation(); closeUploadMenu(); UI.inpFile.click(); }; UI.actUpFolder.onclick = (e) => { e.stopPropagation(); closeUploadMenu(); UI.inpFolder.click(); }; const updateRowUI = (task) => { if (document.hidden) return; const row = document.querySelector(`.pk-row[data-id="${task.id}"]`); if (row) { const progBar = row.querySelector('.pk-up-prog-bar'); const progTxt = row.querySelector('.pk-up-prog-txt'); const spdTxt = row.querySelector('.pk-up-spd'); const statusCol = row.children[4]; const msgSpan = statusCol ? statusCol.querySelector('span:first-child') : null; if (progBar) progBar.style.width = task.progress + '%'; if (progTxt) progTxt.textContent = Math.floor(task.progress) + '%'; if (msgSpan) { msgSpan.textContent = task.message; const activeStatus = ['UPLOADING', 'HASHING', 'WAITING', 'RUNNING']; if (task.status === 'DONE') { msgSpan.style.color = '#52c41a'; if (progBar) progBar.style.backgroundColor = '#52c41a'; } else if (activeStatus.includes(task.status)) { msgSpan.style.color = 'var(--pk-pri)'; if (progBar) progBar.style.backgroundColor = 'var(--pk-pri)'; } } if (spdTxt) spdTxt.textContent = task.status === 'DONE' ? '-' : S.upMng.fmtSpeed(task.speed); } }; S.upMng = { limit: 3, running: 0, store: null, initStore: () => {}, saveTask: (task) => {}, removeTask: (id) => {}, fmtSpeed: (bytesPerSec) => { if (bytesPerSec === 0) return '0 B/s'; const units = ['B/s', 'KB/s', 'MB/s', 'GB/s']; let i = 0; while (bytesPerSec >= 1024 && i < units.length - 1) { bytesPerSec /= 1024; i++; } return bytesPerSec.toFixed(2) + ' ' + units[i]; }, createTask: (file, parentId) => { const task = { id: 'up_' + Date.now() + '_' + Math.random().toString(36).substr(2), kind: 'pk#upload', file: file, name: file.name, size: file.size, parentId: parentId, status: 'WAITING', progress: 0, speed: 0, message: L.msg_task_waiting, _xhr: null, _lastCalcTime: 0, _lastCalcLoaded: 0, _lastUiTime: 0 }; S.upMng.saveTask(task); return task; }, scheduler: () => { if (S.upMng.running >= S.upMng.limit) return; const waiting = S.uploadTasks.find(t => t.status === 'WAITING'); if (waiting) S.upMng.start(waiting); }, start: async (task) => { if (S.quota && S.quota.limitRaw > 0 && !task.file_id) { const remaining = S.quota.limitRaw - S.quota.usedRaw; if (task.size > remaining) { task.status = 'ERROR'; task.message = L.err_quota_exceeded ; if (S.uploadMode) updateRowUI(task); S.upMng.scheduler(); return; } } S.upMng.running++; if (!task.hash) { task.status = 'HASHING'; task.message = L.msg_task_hashing; if (S.uploadMode) updateRowUI(task); task.hash = await calcSha1(task.file); } const hash = task.hash; S.upMng.saveTask(task); if (!task._totalUploadedBytes) task._totalUploadedBytes = 0; let _lastPollTime = Date.now(); let _lastPollBytes = task._totalUploadedBytes; const speedTimer = setInterval(() => { if (task.status === 'UPLOADING') { const now = Date.now(); const timeDiff = now - _lastPollTime; const bytesDiff = task._totalUploadedBytes - _lastPollBytes; if (timeDiff > 0) { task.speed = Math.max(0, (bytesDiff / timeDiff) * 1000); } _lastPollTime = now; _lastPollBytes = task._totalUploadedBytes; if (S.uploadMode) updateRowUI(task); } }, 1500); const cryptoSign = async (secret, stringToSign) => { const enc = new TextEncoder(); const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]); const signature = await crypto.subtle.sign("HMAC", key, enc.encode(stringToSign)); return btoa(String.fromCharCode(...new Uint8Array(signature))); }; try { let finalParentId = task.parentId; if (finalParentId === 'root' || finalParentId === 'upload_root' || finalParentId === '') { const UPLOAD_FOLDER_NAME = 'My Upload'; const cacheKey = `lock_root_${UPLOAD_FOLDER_NAME}`; if (!S.upMng._syncLocks) S.upMng._syncLocks = new Map(); if (!S.upMng._syncLocks.has(cacheKey)) { const createAction = (async () => { const checkExisting = async (targetName) => { try { const list = await apiList('', 1000, null, null, false, true); 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, null, null, false, true); const found = list.find(f => f.kind === 'drive#folder' && f.name === targetName); return found ? found.id : null; } catch (e) { return null; } }; let existingId = await checkExisting(currentPid, name); if (existingId) return existingId; let retry = 0; while (retry < 3) { try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ kind: "drive#folder", parent_id: currentPid, name: name }) }); if (res.ok) { const data = await res.json(); return data.file.id; } if (res.status === 400 || res.status === 429) { await new Promise(r => setTimeout(r, 1000)); existingId = await checkExisting(currentPid, name); if (existingId) return existingId; } } catch (e) {} retry++; } existingId = await checkExisting(currentPid, name); if (existingId) return existingId; throw new Error("Folder creation failed"); })(); S.upMng._syncLocks.set(cacheKey, createAction); } try { currentPid = await S.upMng._syncLocks.get(cacheKey); } catch (e) { S.upMng._syncLocks.delete(cacheKey); throw e; } } finalParentId = currentPid; } if (task._deleted) throw new Error("Aborted"); task.status = 'UPLOADING'; task.message = L.msg_task_init_upload; _lastPollTime = Date.now(); _lastPollBytes = 0; const safePid = (finalParentId === 'root' || finalParentId === 'upload_root') ? '' : (finalParentId || ''); let data = null; if (task._initData && task.file_id) { data = task._initData; } else { let res = null; let createRetry = 0; const maxCreateRetries = 5; while (createRetry < maxCreateRetries) { try { res = await fetch('https://api-drive.mypikpak.com/drive/v1/files', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ hash: hash, name: task.name, size: String(task.size), kind: "drive#file", id: "", parent_id: safePid, upload_type: "UPLOAD_TYPE_RESUMABLE", folder_type: "NORMAL", resumable: { provider: "PROVIDER_ALIYUN" } }) }); if (res.status === 429) { const waitMs = 2000 + Math.random() * 2000 * (createRetry + 1); console.warn(`[Upload] Rate limited (429). Retrying in ${Math.round(waitMs)}ms...`); await new Promise(r => setTimeout(r, waitMs)); createRetry++; continue; } if (!res.ok) { const errData = await res.json().catch(() => ({})); const errMsg = errData.error_description || `HTTP ${res.status}`; const isQuotaExceeded = res.status === 400 && (errData.error_code === 12 || errMsg.toLowerCase().includes('quota')); if (isQuotaExceeded) { const quotaErr = new Error(L.err_quota_exceeded); quotaErr.isFatal = true; throw quotaErr; } if (res.status === 404 || errMsg.toLowerCase().includes('not found') || errMsg.toLowerCase().includes('invalid parent')) { if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); throw new Error(L.err_parent_not_found); } throw new Error(errMsg); } break; } catch (e) { console.warn(`[Upload] Init request failed (${createRetry + 1}/${maxCreateRetries}):`, e.message); if (e.isFatal) throw e; createRetry++; if (createRetry >= maxCreateRetries) throw e; await new Promise(r => setTimeout(r, 1500)); } } data = await res.json(); task._initData = data; } let newlyCreatedFileId = null; if (data.file) { if (data.file.id) { task.file_id = data.file.id; newlyCreatedFileId = data.file.id; } if (data.file.name) task.name = data.file.name; if (data.file.thumbnail_link) task.thumbnail_link = data.file.thumbnail_link; if (data.file.icon_link) task.icon_link = data.file.icon_link; } else if (data.task && data.task.file_id) { task.file_id = data.task.file_id; newlyCreatedFileId = data.task.file_id; if (data.task.name) task.name = data.task.name; } else if (data.id) { task.file_id = data.id; newlyCreatedFileId = data.id; if (data.name) task.name = data.name; } const uploadTaskId = (data.task && data.task.id) || data.task_id || data.upload_task_id || (data.resumable && data.resumable.task_id) || ''; if (uploadTaskId) task._uploadTaskId = uploadTaskId; if (newlyCreatedFileId && !task._ghostAdded) { if (typeof window.pkAddGhostFile === 'function') window.pkAddGhostFile(newlyCreatedFileId); task._ghostAdded = true; } S.upMng.saveTask(task); const waitOfficialUploadTaskComplete = async () => { if (!task._uploadTaskId) return true; const maxPoll = 20; for (let i = 1; i <= maxPoll; i++) { if (task._deleted) throw new Error("Aborted"); task.status = 'RUNNING'; task.speed = 0; task.progress = Math.min(99.8, Math.max(99, Number(task.progress) || 99)); task.message = L.msg_wait_server; S.upMng.saveTask(task); if (S.uploadMode) updateRowUI(task); try { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${encodeURIComponent(task._uploadTaskId)}`, { headers: getHeaders() }); if (res.ok) { const taskData = await res.json().catch(() => null); const phase = (taskData && taskData.phase) || (taskData && taskData.task && taskData.task.phase) || ''; const ref = (taskData && taskData.reference_resource) || (taskData && taskData.task && taskData.task.reference_resource) || {}; if (ref.name) task.name = ref.name; if (ref.mime_type) task.mime_type = ref.mime_type; if (ref.icon_link) task.icon_link = ref.icon_link; if (ref.thumbnail_link) task.thumbnail_link = ref.thumbnail_link; if (phase === 'PHASE_TYPE_COMPLETE') return true; if (phase === 'PHASE_TYPE_ERROR') throw new Error((taskData && (taskData.message || taskData.error_description)) || L.err_unknown); } } catch (e) { if (e.message === "Aborted") throw e; console.warn('[Upload] Task completion poll warning:', e); } await sleep(i < 6 ? 1000 : 2000); } console.warn(`[Upload] Task completion poll timeout, fallback to local completion: ${task.name}`); return true; }; if (task._deleted) { console.warn(`[Upload] Task ${task.id} was deleted by user during initialization. Triggering self-destruct.`); if (newlyCreatedFileId && task._deleteFileIntent) { try { await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [newlyCreatedFileId] }) }); } catch (err) { console.error(`[Upload] Failed to cleanup ghost file ${newlyCreatedFileId}`, err); } } throw new Error("Aborted"); } if (data.upload_type === "UPLOAD_TYPE_URL" || data.phase === "PHASE_TYPE_COMPLETE" || (data.file && data.file.phase === "PHASE_TYPE_COMPLETE")) { task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_fast_success; if (typeof window.pkRemoveGhostFile === 'function' && task.file_id) window.pkRemoveGhostFile(task.file_id); S.upMng.removeTask(task.id); if (S.uploadMode) { updateRowUI(task); if (!S._upRenderScheduled) { S._upRenderScheduled = true; requestAnimationFrame(() => { S._upRenderScheduled = false; if (typeof renderVisible === 'function' && !document.hidden) renderVisible(); }); } } setTimeout(() => updateQuotaUI(), 1000); if (typeof globalCache !== 'undefined') { const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (globalCache.has(targetPid)) { const cacheEntry = globalCache.get(targetPid); const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []); const newFileStub = { id: task.file_id, kind: 'drive#file', name: task.name, size: task.size, parent_id: targetPid, mime_type: task.mime_type || '', thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link, modified_time: new Date().toISOString(), hash: hash }; if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub); } } if (typeof globalDirtyFolders !== 'undefined') { const targetPid = task.parentId === 'root' ? '' : (task.parentId || ''); globalDirtyFolders.add(targetPid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } else if (data.resumable && data.resumable.params) { const p = data.resumable.params; const objectName = p.key; const ossEncode = (v) => encodeURIComponent(String(v)).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); const objectPath = objectName.split('/').map(ossEncode).join('/'); const parseOssQuery = (query = '') => String(query || '').split('&').filter(Boolean).map(s => { const i = s.indexOf('='); return i < 0 ? [s, null] : [s.slice(0, i), s.slice(i + 1)]; }).sort((a, b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0)); const buildOssQuery = (query = '', encodeValue = false) => parseOssQuery(query).map(([k, v]) => v === null ? k : `${k}=${encodeValue ? ossEncode(v) : v}`).join('&'); const isOssCname = p.cname === true || String(p.cname || '').toLowerCase() === 'true'; const ossEndpoint = String(p.endpoint || '').replace(/^https?:\/\//i, '').replace(/\/+$/g, ''); const host = isOssCname ? `https://${ossEndpoint}` : `https://${p.bucket}.${ossEndpoint}`; const totalSize = task.file.size; const officialPartSize = Number(CONF.uploadPartSizeOfficial) || (1 * 1024 * 1024); const maxPartCount = Number(CONF.uploadPartMaxCount) || 10000; const PART_SIZE = Math.max(officialPartSize, Math.ceil(totalSize / maxPartCount)); const partCount = Math.ceil(totalSize / PART_SIZE); const getAuth = async (method, query = '', contentType = '') => { const dateStr = new Date().toUTCString(); const headersToSign = { 'x-oss-date': dateStr, 'x-oss-security-token': p.security_token }; const canonicalHeaders = Object.keys(headersToSign).sort().map(k => `${k}:${String(headersToSign[k]).trim()}`).join('\n'); const canonicalQuery = buildOssQuery(query, false); const canonicalResource = `/${p.bucket}/${objectPath}${canonicalQuery ? '?' + canonicalQuery : ''}`; const stringToSign = [method.toUpperCase(), "", 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 urlQuery = buildOssQuery(query, true); const url = `${host}/${objectPath}${urlQuery ? '?' + urlQuery : ''}`; if (!task._xhrs) task._xhrs = new Set(); const reqHeaders = { 'Authorization': creds.auth, 'x-oss-date': creds.date, 'x-oss-security-token': p.security_token }; if (contentType) reqHeaders['Content-Type'] = contentType; const req = GM_xmlhttpRequest({ method: method, url: url, data: body, headers: reqHeaders, upload: { onprogress: onProgress }, onload: (res) => { task._xhrs.delete(req); if (res.status >= 200 && res.status < 300) resolve(res); else { const err = new Error(`OSS ${method} Error: ${res.status} ${res.statusText}`); err.status = res.status; err.isOssError = true; err.responseText = res.responseText || ''; err.ossMethod = method; err.ossQuery = urlQuery || ''; err.ossUrl = url; reject(err); } }, onerror: (err) => { task._xhrs.delete(req); reject(new Error("Network Error")); }, onabort: () => { task._xhrs.delete(req); reject(new Error("Aborted")); } }); task._xhrs.add(req); if (onProgress) task._xhr = { abort: () => req.abort() }; } catch (e) { reject(e); } }); }; task.message = L.msg_task_uploading; const fileContentType = task.file.type || 'application/octet-stream'; task.message = L.msg_task_init_part; if (S.uploadMode) updateRowUI(task); let uploadId = task._uploadId; let isResuming = !!uploadId; if (!uploadId) { const initRes = await ossRequest('POST', 'uploads', null, ''); const initXml = new DOMParser().parseFromString(initRes.responseText, "text/xml"); uploadId = initXml.querySelector('UploadId')?.textContent || initXml.getElementsByTagName('UploadId')[0]?.textContent; if (!uploadId) throw new Error("Failed to get UploadId"); task._uploadId = uploadId; } const parts = new Array(partCount); const CONCURRENCY = PART_SIZE <= 1 * 1024 * 1024 ? 5 : (PART_SIZE <= 3 * 1024 * 1024 ? 3 : 1); let completedBytes = 0; const activeParts = new Map(); const uploadedPartNumbers = new Set(); if (isResuming) { try { let nextMarker = ''; let isTruncated = true; while (isTruncated) { const query = `uploadId=${uploadId}` + (nextMarker ? `&part-number-marker=${nextMarker}` : ''); const listRes = await ossRequest('GET', query, null, ''); const listXml = new DOMParser().parseFromString(listRes.responseText, "text/xml"); const partNodes = listXml.querySelectorAll('Part') || listXml.getElementsByTagName('Part'); Array.from(partNodes).forEach(node => { const pNum = parseInt(node.querySelector('PartNumber')?.textContent || node.getElementsByTagName('PartNumber')[0]?.textContent); const etag = node.querySelector('ETag')?.textContent || node.getElementsByTagName('ETag')[0]?.textContent; if (pNum && etag && !uploadedPartNumbers.has(pNum)) { parts[pNum - 1] = { partNumber: pNum, etag: etag }; uploadedPartNumbers.add(pNum); const pSize = (pNum === partCount) ? (totalSize - (partCount - 1) * PART_SIZE) : PART_SIZE; completedBytes += pSize; } }); const truncNode = listXml.querySelector('IsTruncated') || listXml.getElementsByTagName('IsTruncated')[0]; isTruncated = truncNode ? (truncNode.textContent === 'true') : false; if (isTruncated) { const markerNode = listXml.querySelector('NextPartNumberMarker') || listXml.getElementsByTagName('NextPartNumberMarker')[0]; nextMarker = markerNode ? markerNode.textContent : ''; } } task._totalUploadedBytes = completedBytes; task.progress = (completedBytes / totalSize) * 100; } catch (e) { console.warn(`[Upload] Failed to fetch existing parts. Overwriting.`, e); } } const updateProgress = () => { let activeTotal = 0; for (const bytes of activeParts.values()) activeTotal += bytes; const currentTotal = Math.min(totalSize, completedBytes + activeTotal); task._totalUploadedBytes = currentTotal; task.progress = (currentTotal / totalSize) * 100; }; const pool = Array.from({length: partCount}, (_, k) => k + 1).filter(num => !uploadedPartNumbers.has(num)); const worker = async () => { while (pool.length > 0) { if (task.status === 'PAUSED' || !document.body.contains(el)) { activeParts.clear(); throw new Error("Aborted"); } const i = pool.shift(); const startByte = (i - 1) * PART_SIZE; const endByte = Math.min(i * PART_SIZE, totalSize); const chunk = task.file.slice(startByte, endByte); activeParts.set(i, 0); const query = `partNumber=${i}&uploadId=${uploadId}`; try { const partRes = await ossRequest('PUT', query, chunk, fileContentType, (pe) => { activeParts.set(i, pe.loaded); updateProgress(); }); const finalizedSize = chunk.size; completedBytes += finalizedSize; activeParts.delete(i); updateProgress(); const etagHeader = partRes.responseHeaders.match(/etag:\s*"?([^"\r\n]+)"?/i); const etag = etagHeader ? etagHeader[1] : null; if (!etag) throw new Error(`Part ${i} missing ETag`); parts[i - 1] = { partNumber: i, etag: etag }; if (!task._parts) task._parts = []; task._parts[i - 1] = parts[i - 1]; S.upMng.saveTask(task); } catch (err) { activeParts.delete(i); pool.unshift(i); throw err; } } }; task.message = L.msg_task_uploading; if (S.uploadMode) updateRowUI(task); if (pool.length > 0) { const workers = Array(Math.min(pool.length, CONCURRENCY)).fill(0).map(worker); await Promise.all(workers); } if (task._deleted) throw new Error("Aborted"); const xmlBody = `${parts.map(p => `${p.partNumber}${p.etag}`).join('')}`; await ossRequest('POST', `uploadId=${uploadId}`, xmlBody, 'application/xml'); if (task._deleted && task.file_id && task._deleteFileIntent) { fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [task.file_id] }) }).catch(()=>{}); throw new Error("Aborted"); } task.progress = 100; task._totalUploadedBytes = totalSize; setTimeout(() => updateQuotaUI(), 1500); const tryFetchMeta = async (isRetry = false) => { if (task._deleted) return true; try { const meta = await apiGet(task.file_id); if (meta) { if (meta.icon_link) task.icon_link = meta.icon_link; if (meta.mime_type) task.mime_type = meta.mime_type; if (meta.medias) task.medias = meta.medias; const isValidThumb = meta.thumbnail_link && meta.thumbnail_link !== meta.icon_link; if (isValidThumb) { const testUrl = meta.thumbnail_link + (meta.thumbnail_link.includes('?') ? '&' : '?') + '_t=' + Date.now(); const isImageReady = await new Promise(resolve => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = testUrl; }); if (isImageReady) { task.thumbnail_link = testUrl; if (typeof globalCache !== 'undefined') { const pid = task.parentId === 'root' ? '' : (task.parentId || ''); if (globalCache.has(pid)) { const list = globalCache.get(pid); const target = Array.isArray(list) ? list.find(f => f.id === task.file_id) : (list.items ? list.items.find(f => f.id === task.file_id) : null); if (target) target.thumbnail_link = testUrl; } } if (isRetry && S.uploadMode) { if (!S._upRenderScheduled) { S._upRenderScheduled = true; requestAnimationFrame(() => { S._upRenderScheduled = false; if (typeof renderVisible === 'function' && !document.hidden) renderVisible(); }); } } return true; } else { 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; } while (true) { await sleep(60000); if (await tryFetchMeta(true)) return; } })(); await waitOfficialUploadTaskComplete(); task.status = 'DONE'; task.progress = 100; task.speed = 0; task.message = L.msg_task_upload_done; if (typeof window.pkRemoveGhostFile === 'function' && task.file_id) window.pkRemoveGhostFile(task.file_id); S.upMng.removeTask(task.id); if (S.uploadMode) { updateRowUI(task); if (!S._upRenderScheduled) { S._upRenderScheduled = true; requestAnimationFrame(() => { S._upRenderScheduled = false; if (typeof renderVisible === 'function' && !document.hidden) renderVisible(); }); } } if (typeof globalCache !== 'undefined') { const targetPid = (task.parentId === 'root' || task.parentId === 'upload_root') ? '' : (task.parentId || ''); if (globalCache.has(targetPid)) { const cacheEntry = globalCache.get(targetPid); const list = Array.isArray(cacheEntry) ? cacheEntry : (cacheEntry.items || []); const newFileStub = { id: task.file_id, kind: 'drive#file', name: task.name, size: task.size, parent_id: targetPid, mime_type: task.mime_type || '', thumbnail_link: task.thumbnail_link || task.icon_link, icon_link: task.icon_link, modified_time: new Date().toISOString(), hash: hash }; if (!list.some(f => f.id === newFileStub.id)) list.push(newFileStub); } } if (typeof globalDirtyFolders !== 'undefined') { const targetPid = task.parentId === 'root' ? '' : (task.parentId || ''); globalDirtyFolders.add(targetPid); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } } catch (e) { const isManualAbort = e.message === 'Aborted'; task.status = isManualAbort ? 'PAUSED' : 'ERROR'; task.message = isManualAbort ? L.msg_task_paused : (e.message || L.err_unknown); if (e.status === 403 && task.file_id) { task.message = e.isOssError ? L.msg_oss_upload_forbidden : L.msg_token_expired_retry; task._initData = null; task._uploadId = null; task.progress = 0; task._totalUploadedBytes = 0; fetch('https://api-drive.mypikpak.com/drive/v1/files:batchDelete', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: [task.file_id] }) }).catch(()=>{}); if (typeof window.pkRemoveGhostFile === 'function') window.pkRemoveGhostFile(task.file_id); task.file_id = null; } if (S.uploadMode) updateRowUI(task); S.upMng.saveTask(task); } finally { clearInterval(speedTimer); task._xhr = null; S.upMng.running--; if (S.uploadMode) { refresh(); } S.upMng.scheduler(); } }, pause: (task, skipRender = false) => { if (task.status === 'UPLOADING') { task.status = 'PAUSED'; task.message = L.msg_task_paused; if (task._xhrs && task._xhrs.size > 0) { task._xhrs.forEach(req => req.abort()); task._xhrs.clear(); } if (task._xhr) { task._xhr.abort(); task._xhr = null; } S.upMng.saveTask(task); if (S.uploadMode && !skipRender) { refresh(); } } else if (task.status === 'WAITING') { task.status = 'PAUSED'; task.message = L.msg_task_paused; S.upMng.saveTask(task); if (S.uploadMode && !skipRender) { refresh(); } } }, resume: (task, skipRender = false) => { if (task.status === 'PAUSED' || task.status === 'ERROR') { task.status = 'WAITING'; task.message = L.msg_task_waiting; S.upMng.saveTask(task); S.upMng.scheduler(); if (S.uploadMode && !skipRender) { refresh(); } } } }; S.upMng.initStore(); const handleUploadInput = async (files) => { if (!files || files.length === 0) return; if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); const curPath = S.path[S.path.length - 1]; const isVirtual = curPath.id.startsWith('virtual_') || curPath.id.includes('_root') || curPath.id === 'upload_root'; const safeParentId = (curPath.id && !isVirtual) ? curPath.id : ''; let fileList = Array.from(files); let existingFiles = []; if (typeof globalCache !== 'undefined' && globalCache.has(safeParentId)) { const raw = globalCache.get(safeParentId); existingFiles = Array.isArray(raw) ? raw : (raw.items || []); } else if (!isVirtual && S.items && S.items.length > 0) { existingFiles = S.items; } if (existingFiles.length > 0) { const existingMap = new Set(); existingFiles.forEach(f => { if (f.kind !== 'drive#folder') { existingMap.add(`${f.name}_${f.size}`); } }); let dupCount = 0; const duplicateIndices = new Set(); fileList.forEach((file, index) => { if (file.name.startsWith('.')) return; let relativeFolder = ""; if (file.webkitRelativePath) { const parts = file.webkitRelativePath.split('/'); if (parts.length > 1) { parts.pop(); relativeFolder = parts.join('/'); } } if (!relativeFolder) { const key = `${file.name}_${file.size}`; if (existingMap.has(key)) { dupCount++; duplicateIndices.add(index); } } }); if (dupCount > 0) { const skipDups = await showConfirm(L.msg_upload_dup_confirm.replace('{n}', dupCount)); if (skipDups) { fileList = fileList.filter((_, idx) => !duplicateIndices.has(idx)); } } } if (fileList.length === 0) { UI.inpFile.value = ''; UI.inpFolder.value = ''; return; } let addedCount = 0; const BATCH_SIZE = 50; if (fileList.length > BATCH_SIZE) { showToast(L.msg_parsing_files, 'info', 2000); } for (let i = 0; i < fileList.length; i += BATCH_SIZE) { const batch = fileList.slice(i, i + BATCH_SIZE); await new Promise(resolve => setTimeout(resolve, 0)); for (const file of batch) { if (file.name.startsWith('.')) continue; let relativeFolder = ""; if (file.webkitRelativePath) { const parts = file.webkitRelativePath.split('/'); if (parts.length > 1) { parts.pop(); relativeFolder = parts.join('/'); } } if (S.upMng) { const task = S.upMng.createTask(file, safeParentId); task.relativeFolder = relativeFolder; S.uploadTasks.unshift(task); addedCount++; } } } showToast(L.msg_task_added.replace('{n}', addedCount)); if (!S.uploadMode) { switchTab('upload'); } else { renderVisible(); updateStat(); } if (S.upMng) S.upMng.scheduler(); UI.inpFile.value = ''; UI.inpFolder.value = ''; }; UI.inpFile.onchange = (e) => handleUploadInput(e.target.files); UI.inpFolder.onchange = (e) => handleUploadInput(e.target.files); const dragMask = document.createElement('div'); dragMask.className = 'pk-drag-mask'; UI.win.appendChild(dragMask); const parseEntries = async (entries, relPath = "") => { const BATCH_SIZE = 10; for (let i = 0; i < entries.length; i += BATCH_SIZE) { const batch = entries.slice(i, i + BATCH_SIZE); await Promise.all(batch.map(async (entry) => { if (entry.isFile) { return new Promise((res) => { entry.file(file => { if (!file.name.startsWith('.')) { const curPath = S.path[S.path.length - 1]; const safeParentId = (curPath.id && !curPath.id.includes('_root')) ? curPath.id : ''; const task = S.upMng.createTask(file, safeParentId); task.relativeFolder = relPath; S.uploadTasks.unshift(task); } res(); }, () => res()); }); } else if (entry.isDirectory) { return new Promise((res) => { const reader = entry.createReader(); const readAllEntries = async () => { let allSubEntries = []; let readBatch = async () => { return new Promise((r) => { reader.readEntries((sub) => { if (sub.length > 0) { allSubEntries = allSubEntries.concat(sub); readBatch().then(r); } else { r(); } }, () => r()); }); }; await readBatch(); await parseEntries(allSubEntries, (relPath ? relPath + "/" : "") + entry.name); res(); }; readAllEntries(); }); } })); } }; const canDragUpload = () => { return !S.trashMode && !S.shareMode && !S.shareParseMode && !S.linkBookmarkMode && !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 = pkIconInline('home', { size: 16 }); destHtml = `${homeIcon}${L.btn_nav_home}`; } else { destHtml = esc(curPath.name); } dragMask.innerHTML = `
${pkIconSized('dragUploadPlus', 32)}
${L.msg_drag_drop_hint}
${L.lbl_upload_to}${destHtml}
`; dragMask.style.display = 'flex'; }); el.addEventListener('dragover', handleDragGuard); el.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); if (!canDragUpload()) return; dragCounter--; if (dragCounter <= 0) { dragMask.style.display = 'none'; dragCounter = 0; } }); el.addEventListener('drop', async (e) => { handleDragGuard(e); if (!canDragUpload()) return; dragMask.style.display = 'none'; dragCounter = 0; const items = e.dataTransfer.items; if (!items) return; if (S.upMng && S.upMng._syncLocks) S.upMng._syncLocks.clear(); const entries =[]; for (let i = 0; i < items.length; i++) { const entry = items[i].webkitGetAsEntry(); if (entry) entries.push(entry); } if (entries.length > 0) { const curPath = S.path[S.path.length - 1]; const isVirtual = curPath.id.startsWith('virtual_') || curPath.id.includes('_root') || curPath.id === 'upload_root'; const safeParentId = (curPath.id && !isVirtual) ? curPath.id : ''; let existingFiles = []; if (typeof globalCache !== 'undefined' && globalCache.has(safeParentId)) { const raw = globalCache.get(safeParentId); existingFiles = Array.isArray(raw) ? raw : (raw.items || []); } else if (!isVirtual && S.items && S.items.length > 0) { existingFiles = S.items; } if (existingFiles.length > 0) { const existingMap = new Set(); existingFiles.forEach(f => { if (f.kind !== 'drive#folder') { existingMap.add(`${f.name}_${f.size}`); } }); let dupCount = 0; const duplicateIndices = new Set(); const topLevelFiles = []; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (entry.isFile) { const file = await new Promise((res) => entry.file(res, () => res(null))); if (file && !file.name.startsWith('.')) { topLevelFiles.push({ file, index: i }); } } } topLevelFiles.forEach(tf => { const key = `${tf.file.name}_${tf.file.size}`; if (existingMap.has(key)) { dupCount++; duplicateIndices.add(tf.index); } }); if (dupCount > 0) { const skipDups = await showConfirm(L.msg_upload_dup_confirm.replace('{n}', dupCount)); if (skipDups) { for (let i = entries.length - 1; i >= 0; i--) { if (duplicateIndices.has(i)) { entries.splice(i, 1); } } } } } if (entries.length === 0) return; await parseEntries(entries); if (!S.uploadMode) switchTab('upload'); else { refresh(); updateStat(); } if (S.upMng) S.upMng.scheduler(); } }); document.addEventListener('click', (e) => { if (UI.uploadWrap && !UI.uploadWrap.contains(e.target)) { UI.uploadWrap.querySelector('.pk-dropdown-menu').style.display = 'none'; UI.uploadWrap.classList.remove('active'); } }); } const switchTab = (mode) => { stopLiveManualRefreshBeforePathChange('mode_change'); const leavingShareParseMode = !!(S.shareParseMode && mode !== 'shareParse'); const leavingLinkBookmarkMode = !!(S.linkBookmarkMode && mode !== 'linkBookmark'); const enteringLinkBookmarkMode = !!(!S.linkBookmarkMode && mode === 'linkBookmark'); if (S.historyMode && mode !== 'history') { ensureHistorySessionState(); if (UI.vp) S.historyScrollTop = Math.max(0, Math.round(UI.vp.scrollTop || 0)); if (Array.isArray(S.items) && S.items.length) S.historyItems = normalizeHistorySessionItems(S.items); syncHistorySessionCache(); } if (S.shareParseMode && mode !== 'shareParse') { S.shareParseReqId = (S.shareParseReqId || 0) + 1; S.shareParseLoading = false; resetShareParseListState(false); } stopOfflineLightProbe('mode_change'); if (mode !== 'recent') stopRecentLightProbe('mode_change'); if (S.abortController) S.abortController.abort(); activeLoadId++; S.sortId++; if (S.loading) { S.loading = false; } const isForcedListTab = mode === 'offline' || mode === 'upload' || mode === 'share'; const needListTabSync = isForcedListTab && (S.viewMode !== 'list' || isGridView() || (UI.win && UI.win.classList.contains('pk-grid-view')) || CONF.rowHeight !== getListRowHeight() || (UI.vp && UI.vp.scrollTop > 0)); if (needListTabSync) { beginFolderViewSync(); if (UI.win) { UI.win.classList.add('pk-view-switching'); UI.win.classList.remove('pk-grid-view', 'pk-grid-resizing', 'pk-grid-scrolling'); } S.viewMode = 'list'; CONF.rowHeight = getListRowHeight(); S._gridLayoutKey = ''; clearGroupedGridMetaCache(); if (UI.in) { UI.in.style.height = '0px'; UI.in.style.transform = 'none'; } if (UI.vp) { UI.vp.scrollTop = 0; UI.vp.scrollLeft = 0; } } S.items = []; S.display = []; S.recentResultItems = null; S.itemMap.clear(); S.sel.clear(); if (UI.in) { UI.in.innerHTML = ''; UI.in.style.height = '0px'; UI.in.style.transform = 'none'; } if (UI.vp) { UI.vp.scrollTop = 0; UI.vp.scrollLeft = 0; } if (mode === 'history') { S.sort = 'play_time'; S.dir = 1; } else if (mode === 'offline') { S.sort = 'created_time'; S.dir = 1; } else if (mode === 'recent') { S.sort = 'modified_time'; S.dir = 1; } else if (mode === 'home') { S._sortAppliedForId = null; } else { S.sort = 'modified_time'; } syncFolderFirstFromGlobal(); if (UI.chkGlobal && !S.trashMode && !S.shareMode && !S.shareParseMode && !S.linkBookmarkMode && !S.starredMode && !S.offlineMode && !S.historyMode && !S.recentMode && !S.uploadMode) { S.wasGlobalChecked = UI.chkGlobal.checked; } S.trashMode = (mode === 'trash'); S.shareMode = (mode === 'share'); S.shareParseMode = (mode === 'shareParse'); S.linkBookmarkMode = (mode === 'linkBookmark'); if (enteringLinkBookmarkMode) { S.linkBookmarkResetSelectOnEnter = true; S.linkBookmarkFolderPage = 1; S.linkBookmarkLinkPage = 1; } S.starredMode = (mode === 'starred'); S.recentMode = (mode === 'recent'); S.historyMode = (mode === 'history'); S.offlineMode = (mode === 'offline'); S.uploadMode = (mode === 'upload'); const syncModeViewShell = (nextViewMode) => { const modeChanged = S.viewMode !== nextViewMode; if (modeChanged) { beginFolderViewSync(); if (UI.win) UI.win.classList.add('pk-view-switching', 'pk-mode-view-syncing'); } S.viewMode = nextViewMode; S._gridLayoutKey = ''; clearGroupedGridMetaCache(); if (UI.win) UI.win.classList.toggle('pk-grid-view', isGridView()); syncLayoutMetrics(); renderViewSwitch(); if (modeChanged && UI.win) requestAnimationFrame(() => UI.win.classList.remove('pk-mode-view-syncing')); }; if (UI.win) { UI.win.classList.toggle('pk-share-parse-mode', !!S.shareParseMode); UI.win.classList.toggle('pk-link-bookmark-mode', !!S.linkBookmarkMode); } if (S.shareParseMode) { S.dupMode = false; S.isFlattened = false; S.analyzeMode = false; S.analyzeSimGroups = null; S.scanFilter = null; S.filterState = { active: false, cat: 'all', ext: 'all' }; S.shareParseInsightFilterState = { active: false, cat: 'all', ext: 'all' }; S.preSearchPath = null; S.lastGlobalResults = []; clearShareParseSearch(); if (UI.win) UI.win.classList.remove('pk-analyze-group-tools-on'); if (UI.btnAnaSelect) UI.btnAnaSelect.style.display = 'none'; if (UI.btnAnaSort) UI.btnAnaSort.style.display = 'none'; document.querySelectorAll('.pk-ana-pop, .pk-ana-sort-pop').forEach(p => { p.style.display = 'none'; }); if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.btnExit) UI.btnExit.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; } if (mode === 'starred' || mode === 'recent' || mode === 'history' || mode === 'trash') { const prefKey = mode === 'trash' ? 'trash_root' : (mode === 'starred' ? 'starred_root' : (mode === 'recent' ? 'recent_root' : 'history_root')); const nextViewMode = (typeof resolvePreferredViewMode === 'function') ? resolvePreferredViewMode(prefKey) : (gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'); syncModeViewShell(nextViewMode); } 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.shareParseMode) rootName = L.title_share_parse; if (S.linkBookmarkMode) rootName = L.title_link_bookmark; 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.shareParseMode) { S.path = [{ id: 'share_parse_root', name: rootName }]; } else if (S.linkBookmarkMode) { S.path = [{ id: 'link_bookmark_root', name: rootName }]; } else if (S.offlineMode) { S.path = [{ id: 'offline_root', name: rootName }]; } else if (S.uploadMode) { S.path = [{ id: 'upload_root', name: rootName }]; } else if (S.trashMode) { S.path = [{ id: 'trash_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 }]; } if (mode === 'home') { const nextViewMode = (typeof resolvePreferredViewMode === 'function') ? resolvePreferredViewMode('') : (gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'); syncModeViewShell(nextViewMode); } if (leavingShareParseMode) beginShareParseGridExitSync(); else if (!leavingShareParseMode) S._shareParseGridExitSync = false; if (leavingLinkBookmarkMode) beginLinkBookmarkGridExitSync(); else if (!leavingLinkBookmarkMode) S._linkBookmarkGridExitSync = false; if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); 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.btnNavShareParse) UI.btnNavShareParse.classList.toggle('act', mode === 'shareParse'); if (UI.btnNavLinkBookmark) UI.btnNavLinkBookmark.classList.toggle('act', mode === 'linkBookmark'); 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 mainHeader = UI.win ? UI.win.querySelector('.pk-grid-hd') : null; if (mainHeader) { mainHeader.style.display = ''; if (mainHeader.dataset.pkInitialGridHd !== '1') mainHeader.style.visibility = ''; } const stdBtns = [UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnMigrate, UI.btnBlacklistManager]; const shareBtns = [document.getElementById('pk-cancel-share')]; const upBtns = [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll]; const upSep = document.getElementById('pk-up-sep'); upBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(upSep) upSep.style.display = 'none'; if (UI.btnClearHistoryAll) UI.btnClearHistoryAll.style.display = 'none'; if (UI.btnShareParseSave) UI.btnShareParseSave.style.display = 'none'; if (UI.btnShareParseInsight) UI.btnShareParseInsight.style.display = 'none'; if (UI.btnShareParseStopScan) UI.btnShareParseStopScan.style.display = 'none'; if (UI.btnShareParseBackList) UI.btnShareParseBackList.style.display = 'none'; if (UI.actionBar) UI.actionBar.querySelectorAll('.pk-sep').forEach(sep => { sep.style.display = ''; }); if (UI.btnRefresh) { UI.btnRefresh.style.display = 'inline-flex'; UI.btnRefresh.disabled = false; } if (UI.btnBlacklistManager) UI.btnBlacklistManager.disabled = false; if (S.historyMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager && b !== UI.btnMigrate) b.style.display = 'none'; }); if (UI.btnClearHistoryAll) UI.btnClearHistoryAll.style.display = 'inline-flex'; if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnMigrate) UI.btnMigrate.style.display = 'inline-flex'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; [UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnExportM3U, UI.btnImgSearch].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.scan) UI.scan.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if (UI.searchInput && UI.searchInput.parentNode) { UI.searchInput.parentNode.style.display = 'flex'; } if (UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; } else if (S.starredMode || S.recentMode) { UI.win.classList.remove('pk-mode-trash'); [UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnExportM3U, UI.btnImgSearch].forEach(b => { if(b) b.style.display = 'inline-flex'; }); stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.btnExport) UI.btnExport.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.linkBookmarkMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnRetryTask, UI.btnCopyLinkOffline, UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnExportM3U, UI.btnImgSearch].forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; if (UI.actionBar) UI.actionBar.style.display = 'none'; if (UI.trashBar) UI.trashBar.style.display = 'none'; if (UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (UI.uploadWrap) UI.uploadWrap.style.display = 'none'; if (UI.btnExit) UI.btnExit.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; if (UI.scan) UI.scan.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.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 = 'none'; if (UI.upTools) UI.upTools.style.display = 'none'; if (UI.filterBar) UI.filterBar.style.display = 'none'; if (UI.viewSwitch) UI.viewSwitch.style.display = 'none'; if (UI.stat) UI.stat.style.display = 'none'; setSearchWrapVisible(false); const linkHeader = UI.win ? UI.win.querySelector('.pk-grid-hd') : null; if (linkHeader) { linkHeader.style.display = 'none'; linkHeader.style.visibility = ''; } } else if (S.shareParseMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnRetryTask, UI.btnCopyLinkOffline].forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = S.shareParseListActive ? 'inline-flex' : 'none'; }); if (UI.btnExt) UI.btnExt.style.display = S.shareParseListActive ? 'inline-flex' : 'none'; if (UI.btnExportM3U) UI.btnExportM3U.style.display = S.shareParseListActive ? 'inline-flex' : 'none'; if (UI.btnImgSearch) UI.btnImgSearch.style.display = S.shareParseListActive ? 'inline-flex' : 'none'; if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; if (UI.actionBar) { UI.actionBar.style.display = S.shareParseListActive ? 'flex' : 'none'; UI.actionBar.querySelectorAll('.pk-sep').forEach(sep => { sep.style.display = 'none'; }); } if (UI.trashBar) UI.trashBar.style.display = 'none'; if (UI.bottomGrp) UI.bottomGrp.style.display = S.shareParseListActive ? 'flex' : 'none'; if (UI.uploadWrap) UI.uploadWrap.style.display = 'none'; if (UI.btnExit) UI.btnExit.style.display = 'none'; if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkSearchPath) UI.chkSearchPath.checked = false; if (UI.chkGlobal) UI.chkGlobal.checked = false; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if (UI.btnExport) UI.btnExport.style.display = 'none'; if (UI.scan) UI.scan.style.display = 'none'; if (UI.cntFolderFirst) UI.cntFolderFirst.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 = 'none'; if (UI.upTools) UI.upTools.style.display = 'none'; if (UI.filterBar) UI.filterBar.style.display = 'none'; if (UI.searchInput && UI.searchInput.parentNode) UI.searchInput.parentNode.style.display = 'none'; updateShareParseSaveButton(); requestAutoHideButtonTextCheck(); } else if (S.shareMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; } else if (S.offlineMode) { UI.win.classList.remove('pk-mode-trash'); [UI.btnNewFolder, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnMigrate].forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnDel, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; if(UI.btnExportM3U) UI.btnExportM3U.style.display = 'inline-flex'; if(UI.btnImgSearch) UI.btnImgSearch.style.display = 'inline-flex'; } else if (S.uploadMode) { UI.win.classList.remove('pk-mode-trash'); stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(upSep) upSep.style.display = 'block'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; if(UI.btnExportM3U) UI.btnExportM3U.style.display = 'inline-flex'; if(UI.btnImgSearch) UI.btnImgSearch.style.display = 'inline-flex'; } else if (S.trashMode) { UI.win.classList.add('pk-mode-trash'); if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.actionBar) UI.actionBar.style.display = 'none'; if(UI.trashBar) UI.trashBar.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; } else { UI.win.classList.remove('pk-mode-trash'); [UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnExportM3U, UI.btnImgSearch].forEach(b => { if(b) b.style.display = 'inline-flex'; }); stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll, UI.btnUpClearDone, UI.btnUpClearIng].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if(UI.scan) UI.scan.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (UI.chkGlobal && typeof S.wasGlobalChecked !== 'undefined') { UI.chkGlobal.checked = S.wasGlobalChecked; } } if (S.starredMode || S.offlineMode || S.uploadMode) { if (UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if (UI.chkGlobal) UI.chkGlobal.checked = false; } syncLocalUploadVisibility(); syncRefreshButtonVisibility(); S.clearSelection(); if (S.linkBookmarkMode) { setLoad(false); renderCrumb(); loadLinkBookmarkOfficial(true); updateStat(); return; } if (S.shareParseMode) { setLoad(false); renderCrumb(); if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); if (S.shareParseListActive) { syncShareParseMirror(); renderList(); } else { renderShareParsePanel(); } syncShareParseInsightFilterBar(); updateStat(); return; } const isOffline = mode === 'offline'; const isRecent = mode === 'recent'; const isHistory = mode === 'history'; const realKey = S.getRealCacheKey(isOffline ? 'offline_root' : (isRecent ? 'recent_root' : (isHistory ? 'history_root' : ''))); const hasCache = typeof globalCache !== 'undefined' && globalCache.has(realKey); const session = typeof globalCache !== 'undefined' ? globalCache.get('offline_session') : null; const isResumingOffline = isOffline && isOfflineSessionLoading(session); const recentCache = isRecent && hasCache ? globalCache.get(realKey) : null; const isResumingRecent = isRecent && recentCache && !Array.isArray(recentCache) && recentCache.nextToken; const historyCache = isHistory && hasCache ? globalCache.get(realKey) : null; const isResumingHistory = isHistory && ( (Array.isArray(S.historyItems) && S.historyItems.length > 0) || (Array.isArray(getHistorySnapshotItems(historyCache)) && getHistorySnapshotItems(historyCache).length > 0) ); if (isOffline && isResumingOffline) { S.offlinePagingPending = true; S.pagingLoading = true; updateStat(); } else if (!((isOffline && hasCache) || (isRecent && (hasCache || isResumingRecent)) || (isHistory && (hasCache || isResumingHistory)))) { setLoad(true, true); } load(false, !(isOffline || isRecent || isHistory)); if (isOffline) scheduleOfflineLightProbe('enter', 1200); if (isRecent) scheduleRecentLightProbe('enter', 1200); if (window.pkSmartRefreshTrigger) { setTimeout(() => window.pkSmartRefreshTrigger(isOffline || isRecent), 100); } }; const btnCloud = el.querySelector('#pk-btn-cloud'); const showFolderSelector = (initialId, onConfirm, initialPath = null, fileFilter = null, customTitle = null) => { let currentPath = initialPath ? JSON.parse(JSON.stringify(initialPath)) : [{ id: '', name: L.picker_all }]; if (currentPath.length > 0) currentPath[0].name = L.picker_all; let currentList = []; let selectedFile = null; let sortMode = 'new'; const titleStr = customTitle || (fileFilter ? L.title_select_file : L.picker_title); const picker = showModal(`

${titleStr}

${CONF.icons.close}
${L.picker_sort_new}
${CONF.crumbIcons.sortAZ}A-Z
${CONF.crumbIcons.sortZA}Z-A
${CONF.crumbIcons.sortNew}${L.picker_sort_new}
${CONF.crumbIcons.sortOld}${L.picker_sort_old}
${L.loading}
`); const mContent = picker.querySelector('.pk-modal'); mContent.style.padding = '20px'; mContent.style.width = 'fit-content'; picker.querySelector('.pk-modal-close').style.display = 'none'; const crumbEl = picker.querySelector('#pk_picker_crumb'); const listEl = picker.querySelector('#pk_picker_list'); listEl.onscroll = () => { const oldPop = document.querySelector('.pk-crumb-pop'); if (oldPop && typeof oldPop._cleanup === 'function') oldPop._cleanup(); }; const sortTrigger = picker.querySelector('#pk_sort_trigger'); const sortMenu = picker.querySelector('#pk_sort_menu'); const sortTxt = picker.querySelector('#pk_sort_txt'); const closeBtn = picker.querySelector('#pk_picker_close_btn'); closeBtn.onmouseover = () => closeBtn.style.background = 'var(--pk-hl)'; closeBtn.onmouseout = () => closeBtn.style.background = 'transparent'; closeBtn.onclick = () => picker.remove(); const applySort = () => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); const getCharWeight = (str) => { if (!str) return 0; const c = str.charAt(0); if (/[0-9]/.test(c)) return 20; if (/[\u4e00-\u9fa5]/.test(c)) return 30; if (/[a-zA-Z]/.test(c)) return 40; return 10; }; const compareNames = (nameA, nameB) => { const rankA = getCharWeight(nameA); const rankB = getCharWeight(nameB); if (rankA !== rankB) return rankA - rankB; return collator.compare(nameA, nameB); }; currentList.sort((a, b) => { if (a.kind !== b.kind) return a.kind === 'drive#folder' ? -1 : 1; const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortMode === 'az') return compareNames(a.name, b.name); if (sortMode === 'za') return compareNames(b.name, a.name); if (sortMode === 'new') return new Date(b.modified_time) - new Date(a.modified_time); if (sortMode === 'old') return new Date(a.modified_time) - new Date(b.modified_time); return 0; }); }; const renderList = () => { listEl.innerHTML = ''; if (currentList.length === 0) { listEl.innerHTML = `
${CONF.emptySVG}
${L.str_no_files}
`; return; } currentList.forEach(item => { const div = document.createElement('div'); div.className = 'pk-picker-row'; const isDir = item.kind === 'drive#folder'; const isSelected = selectedFile && selectedFile.id === item.id; const isProtected = isDir && ((typeof isSystemItem === 'function') ? isSystemItem(item) : false); const tagHtml = isProtected ? `${L.tag_default}` : ''; let checkHtml = ""; if (!isDir && fileFilter) { checkHtml = ``; } const iconSrc = item.icon_link || ''; let iconHtml = ''; if (isDir) { const fallbackSvg = getOfficialFolderFallbackIconHtml(24); iconHtml = iconSrc ? `` : `
${fallbackSvg}
`; } else { const fallbackSvg = getIcon(item).replace(/width="\d+"/, 'width="24"').replace(/height="\d+"/, 'height="24"'); iconHtml = iconSrc ? `` : `
${fallbackSvg}
`; } div.innerHTML = ` ${checkHtml} ${iconHtml}
${esc(item.name)} ${tagHtml}
${!isDir ? `
${fmtSize(item.size)}
` : ''} `; div.onmouseover = () => div.style.background = 'var(--pk-hl)'; div.onmouseout = () => div.style.background = 'transparent'; if (isSelected) div.style.background = 'var(--pk-sel-bg)'; div.onclick = (e) => { if (isDir) { selectedFile = null; loadFolder(item.id, item.name); } else if (fileFilter) { selectedFile = (selectedFile && selectedFile.id === item.id) ? null : item; renderList(); } }; listEl.appendChild(div); }); }; let _pickerCrumbIdx = 0; let _lastPickerScroll = 0; const showPickerDropdown = async (e, parentId, triggerEl) => { const old = document.querySelector('.pk-crumb-pop'); if (old) { const wasSame = old._sourceEl === triggerEl; if (typeof old._cleanup === 'function') old._cleanup(); if (wasSame) return; } triggerEl.style.background = 'transparent'; triggerEl.innerHTML = CONF.crumbIcons.down; const svgD = triggerEl.querySelector('svg'); if (svgD) { svgD.style.width = '14px'; svgD.style.height = '14px'; svgD.style.display = 'block'; } const pop = document.createElement('div'); pop.className = 'pk-crumb-pop pk-scroll pk-show'; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.style.zIndex = '2147483647'; pop._sourceEl = triggerEl; document.body.appendChild(pop); const rect = getLogicalRect(triggerEl); pop.style.top = (rect.bottom + 5) + 'px'; pop.style.left = rect.left + 'px'; const cleanup = () => { if (pop.parentNode) pop.remove(); triggerEl.innerHTML = CONF.crumbIcons.right; const svgR = triggerEl.querySelector('svg'); if (svgR) { svgR.style.width = '14px'; svgR.style.height = '14px'; svgR.style.display = 'block'; svgR.style.opacity = '0.6'; } document.removeEventListener('mousedown', closer); window.removeEventListener('resize', cleanup); }; pop._cleanup = cleanup; const closer = (ev) => { if (!pop.contains(ev.target) && ev.target !== triggerEl) cleanup(); }; document.addEventListener('mousedown', closer); window.addEventListener('resize', cleanup); const cacheKey = parentId || 'root'; let folders = null; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) { const raw = globalCache.get(cacheKey); if (Array.isArray(raw) || (raw && raw.items && !raw.nextToken)) { const items = Array.isArray(raw) ? raw : raw.items; folders = items.filter(f => f.kind === 'drive#folder'); } } const renderMenu = (list) => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); const getCharWeight = (str) => { if (!str) return 0; const c = str.charAt(0); if (/[0-9]/.test(c)) return 20; if (/[\u4e00-\u9fa5]/.test(c)) return 30; if (/[a-zA-Z]/.test(c)) return 40; return 10; }; const compareNames = (nameA, nameB) => { const rankA = getCharWeight(nameA); const rankB = getCharWeight(nameB); if (rankA !== rankB) return rankA - rankB; return collator.compare(nameA, nameB); }; list.sort((a, b) => { const isSysA = a.name === CONF.SYSTEM_FOLDER_NAME && (!a.parent_id || a.parent_id === '' || a.parent_id === 'root'); const isSysB = b.name === CONF.SYSTEM_FOLDER_NAME && (!b.parent_id || b.parent_id === '' || b.parent_id === 'root'); if (isSysA !== isSysB) return isSysA ? -1 : 1; if (sortMode === 'az') return compareNames(a.name, b.name); if (sortMode === 'za') return compareNames(b.name, a.name); if (sortMode === 'new') return new Date(b.modified_time || 0) - new Date(a.modified_time || 0); if (sortMode === 'old') return new Date(a.modified_time || 0) - new Date(b.modified_time || 0); return 0; }); if (list.length === 0) { cleanup(); return; } pop.innerHTML = ''; list.forEach(f => { const itemDiv = document.createElement('div'); itemDiv.className = 'pk-crumb-item'; const iconSrc = f.icon_link || ''; const fallbackSvg = getOfficialFolderFallbackIconHtml(18); const iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; const isInPath = S.path.some(pathNode => pathNode.id === f.id); const textStyle = isInPath ? 'font-weight:bold; color:var(--pk-pri);' : ''; const isProtected = (typeof isSystemItem === 'function') ? isSystemItem(f) : false; const tagHtml = isProtected ? `${L.tag_default}` : ''; itemDiv.innerHTML = `${iconHtml}${esc(f.name)}${tagHtml}`; itemDiv.onclick = (ev) => { ev.stopPropagation(); cleanup(); const idx = currentPath.findIndex(p => p.id === parentId); if (idx !== -1) { currentPath = currentPath.slice(0, idx + 1); loadFolder(f.id, f.name); } }; pop.appendChild(itemDiv); }); }; if (folders !== null) { renderMenu(folders); } else { pop.innerHTML = `
`; try { const listItems = await apiList(parentId || '', 1000, null, null, false, true); 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'); sp.className = 'pk-picker-crumb-node'; const isLast = i === currentPath.length - 1; if (i === 0) { const homeIcon = pkIconHtml('home', { width: 15, height: 15, style: 'margin-right:4px;' }); sp.innerHTML = `${homeIcon}${L.btn_nav_home}`; } else { sp.textContent = p.name; } sp.style.color = isLast ? 'var(--pk-fg)' : '#888'; sp.style.fontWeight = isLast ? 'bold' : 'normal'; sp.onclick = () => { if (!isLast) { currentPath = currentPath.slice(0, i + 1); } loadFolder(p.id, null, true); }; sp.onmouseover = () => sp.style.background = 'var(--pk-hl)'; sp.onmouseout = () => sp.style.background = 'transparent'; crumbEl.appendChild(sp); let showArrow = !isLast; if (isLast) { if (currentList && currentList.some(item => item.kind === 'drive#folder')) { showArrow = true; } } if (showArrow) { const sep = document.createElement('span'); sep.className = 'pk-picker-arrow'; sep.innerHTML = CONF.crumbIcons.right; const svg = sep.querySelector('svg'); if(svg) { svg.style.width = '14px'; svg.style.height = '14px'; svg.style.display = 'block'; svg.style.opacity = '0.6'; } sep.onmouseover = () => { sep.style.background = 'var(--pk-hl)'; sep.style.color = 'var(--pk-pri)'; if(svg) svg.style.opacity='1'; }; sep.onmouseout = () => { sep.style.background = 'transparent'; sep.style.color = 'var(--pk-icon-c)'; if(svg) svg.style.opacity='0.6'; }; sep.onclick = (e) => { e.stopPropagation(); if (typeof showPickerDropdown === 'function') showPickerDropdown(e, p.id, sep); }; crumbEl.appendChild(sep); } }); _pickerCrumbIdx = currentPath.length - 1; requestAnimationFrame(() => { crumbEl.scrollLeft = crumbEl.scrollWidth; }); }; const handlePickerWheel = (e) => { e.preventDefault(); document.querySelectorAll('.pk-crumb-pop').forEach(p => { if (typeof p._cleanup === 'function') p._cleanup(); }); const now = Date.now(); if (now - _lastPickerScroll < 120) return; _lastPickerScroll = now; const nodes = Array.from(crumbEl.children).filter(c => !c.classList.contains('pk-picker-arrow')); if (!nodes.length) return; if (e.deltaY < 0) _pickerCrumbIdx = Math.max(0, _pickerCrumbIdx - 1); else _pickerCrumbIdx = Math.min(nodes.length - 1, _pickerCrumbIdx + 1); const target = nodes[_pickerCrumbIdx]; if (target) { const centerOffset = target.offsetLeft + (target.offsetWidth / 2) - (crumbEl.clientWidth / 2); crumbEl.scrollTo({ left: centerOffset, behavior: 'smooth' }); } }; crumbEl.addEventListener('wheel', handlePickerWheel, { passive: false }); const loadFolder = async (id, name, isBack = false) => { listEl.innerHTML = `
`; if (name && !isBack) currentPath.push({ id, name }); renderCrumb(); const cacheKey = id || 'root'; let items = null; if (typeof globalCache !== 'undefined' && globalCache.has(cacheKey)) { const raw = globalCache.get(cacheKey); const isComplete = Array.isArray(raw) || (raw && raw.items && !raw.nextToken); if (isComplete) { items = Array.isArray(raw) ? raw : raw.items; } } try { if (items === null) { items = await apiList(id || '', 1000, null, null, false, true); if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, items); indexParents(id, name || L.picker_all, items); } currentList = items.filter(i => { if (i.kind === 'drive#folder') return true; if (fileFilter && fileFilter(i)) return true; return false; }); applySort(); renderList(); renderCrumb(); } catch (e) { listEl.innerHTML = `
${L.str_error}: ${esc(e.message)}
`; } }; picker.querySelector('#pk_picker_cancel').onclick = () => picker.remove(); picker.querySelector('#pk_picker_ok').onclick = () => { const cur = currentPath[currentPath.length - 1]; const returnPathChain = JSON.parse(JSON.stringify(currentPath)); if (fileFilter && selectedFile) { onConfirm(selectedFile.id, selectedFile.name, selectedFile, returnPathChain); } else { onConfirm(cur.id, cur.name, null, returnPathChain); } picker.remove(); }; if (!fileFilter) { picker.querySelector('#pk_picker_new').onclick = async () => { const cur = currentPath[currentPath.length - 1]; const name = await showPrompt(L.msg_newfolder_prompt, '', L.picker_new, { maxLen: CONF.fileNameMaxLen }); if (name) { if (isPikPakFileNameTooLong(name)) { showPikPakFileNameLimitTip(); return; } 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); }; const MAGNET_ARCHIVE_BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; function convertMagnetArchiveBtihBase32ToHex(value) { const text = String(value || '').trim().replace(/=+$/g, '').toUpperCase(); if (!/^[A-Z2-7]{32}$/.test(text)) return ''; let bits = ''; let hex = ''; for (const ch of text) { const n = MAGNET_ARCHIVE_BASE32_ALPHABET.indexOf(ch); if (n < 0) return ''; bits += n.toString(2).padStart(5, '0'); while (bits.length >= 8) { hex += parseInt(bits.slice(0, 8), 2).toString(16).padStart(2, '0'); bits = bits.slice(8); } } return hex.length >= 40 ? hex.slice(0, 40).toLowerCase() : ''; } function normalizeMagnetArchiveBtih(value) { const raw = String(value || '').trim(); if (/^[a-fA-F0-9]{40}$/.test(raw)) return raw.toLowerCase(); if (/^[a-zA-Z2-7]{32}$/.test(raw)) return convertMagnetArchiveBtihBase32ToHex(raw); return ''; } function encodeMagnetArchiveParam(value) { return encodeURIComponent(String(value || '')).replace(/%3A/gi, ':'); } function extractMagnetArchiveLinksFromText(rawText) { const text = String(rawText || '').trim(); if (!text || text.length > (Number(CONF.magnetArchiveMaxSourceChars) || 200000)) return []; const out = new Map(); const add = value => { let link = String(value || '').trim(); if (!link) return; const urnOnly = link.match(/^urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})(?:[^\s"'<>`]*)?$/i); if (urnOnly) link = `magnet:?xt=urn:btih:${urnOnly[1].toUpperCase()}`; const pureHash = link.match(/^([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})$/i); if (pureHash) link = `magnet:?xt=urn:btih:${pureHash[1].toUpperCase()}`; if (!/^magnet:\?/i.test(link)) return; const meta = normalizeMagnetArchiveLink(link); const key = meta.ok ? meta.primaryKey : link.toLowerCase(); if (key && !out.has(key)) out.set(key, link); }; if (!/(magnet:\?|urn:btih:)/i.test(text)) { const lines = text.split(/\r?\n/).map(x => x.trim()).filter(Boolean); if (lines.length === 1) add(lines[0]); return Array.from(out.values()); } let match; const magnetRegex = /magnet:\?[^\s"'<>`]+/gi; while ((match = magnetRegex.exec(text))) add(match[0]); const urnRegex = /urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})(?:[^\s"'<>`]*)?/gi; while ((match = urnRegex.exec(text))) add(match[0]); return Array.from(out.values()); } function normalizeMagnetArchiveLink(rawMagnet, fallbackTitle = '') { const raw = String(rawMagnet || '').trim(); if (!/^magnet:\?/i.test(raw)) return { ok: false, reason: 'not_magnet', rawMagnet: raw }; let params; try { params = new URL(raw).searchParams; } catch (e) { return { ok: false, reason: 'invalid_magnet', rawMagnet: raw }; } const xtList = params.getAll('xt').map(x => String(x || '').trim()).filter(Boolean); let btih = ''; let btmh = ''; xtList.forEach(xt => { const lower = xt.toLowerCase(); if (!btmh && lower.startsWith('urn:btmh:')) btmh = lower.slice(9).replace(/[^a-z0-9]/g, ''); if (!btih && lower.startsWith('urn:btih:')) btih = normalizeMagnetArchiveBtih(xt.slice(9)); }); if (!btmh && !btih) return { ok: false, reason: 'missing_xt', rawMagnet: raw }; const trackers = Array.from(new Set(params.getAll('tr').map(x => String(x || '').trim()).filter(Boolean))).sort(); const dn = normalizeLinkBookmarkText(params.get('dn') || '').trim(); const title = normalizeLinkBookmarkLimitedText(dn || fallbackTitle || btih || btmh, 'title'); const primaryKey = btmh ? `btmh:${btmh}` : `btih:${btih}`; const parts = []; if (btmh) parts.push(`xt=${encodeMagnetArchiveParam(`urn:btmh:${btmh}`)}`); if (btih) parts.push(`xt=${encodeMagnetArchiveParam(`urn:btih:${btih}`)}`); if (title) parts.push(`dn=${encodeMagnetArchiveParam(title)}`); trackers.forEach(tr => parts.push(`tr=${encodeMagnetArchiveParam(tr)}`)); return { ok: true, reason: '', rawMagnet: raw, canonicalMagnet: `magnet:?${parts.join('&')}`, primaryKey, btih, btmh, title, trackers }; } function getTraceableMagnetArchiveLinksFromResource(resource) { const source = resource && typeof resource === 'object' ? resource : {}; const keys = Array.isArray(CONF.magnetArchiveSourceKeys) ? CONF.magnetArchiveSourceKeys : []; const texts = []; const push = value => { if (typeof value === 'string' && value.trim()) texts.push(value); }; const scan = (obj, depth = 0) => { if (!obj || typeof obj !== 'object' || depth > 3) return; Object.keys(obj).forEach(key => { const value = obj[key]; const hit = keys.includes(key) || /^(source|original|params|reference|task|magnet|link|href|url)/i.test(key); if (!hit) return; if (typeof value === 'string') push(value); else if (value && typeof value === 'object') scan(value, depth + 1); }); }; scan(source); const unique = new Map(); const usedKeys = new Set(); texts.forEach(text => { extractMagnetArchiveLinksFromText(text).forEach(link => { const meta = normalizeMagnetArchiveLink(link, source.name || source.title || ''); const aliasKeys = meta.ok ? getMagnetArchiveKeysFromMeta(meta) : [link.toLowerCase()]; if (!aliasKeys.length || aliasKeys.some(key => usedKeys.has(key))) return; aliasKeys.forEach(key => usedKeys.add(key)); unique.set(aliasKeys[0], link); }); }); return Array.from(unique.values()); } function getMagnetArchivePrimaryKeyFromMeta(meta) { return meta && meta.ok ? normalizeMagnetArchivePrimaryKey(meta.primaryKey) : ''; } function getMagnetArchiveKeysFromMeta(meta) { const out = []; const push = key => { const safe = normalizeMagnetArchivePrimaryKey(key); if (safe && !out.includes(safe)) out.push(safe); }; if (!meta || !meta.ok) return out; push(meta.primaryKey); if (meta.btmh) push(`btmh:${meta.btmh}`); if (meta.btih) push(`btih:${meta.btih}`); return out; } function getMagnetArchivePrimaryKeyFromBookmark(link) { if (!link || typeof link !== 'object') return ''; const meta = normalizeMagnetArchiveLink(link.href || '', link.title || ''); return getMagnetArchivePrimaryKeyFromMeta(meta); } function getMagnetArchiveKeysFromBookmark(link) { if (!link || typeof link !== 'object') return []; const meta = normalizeMagnetArchiveLink(link.href || '', link.title || ''); return getMagnetArchiveKeysFromMeta(meta); } function getMagnetArchiveKeysFromRow(row) { const out = []; const push = key => { const safe = normalizeMagnetArchivePrimaryKey(key); if (safe && !out.includes(safe)) out.push(safe); }; if (!row || typeof row !== 'object') return out; push(row.key); getMagnetArchiveKeysFromMeta(row.meta).forEach(push); getMagnetArchiveKeysFromBookmark(row.entry).forEach(push); return out; } function getMagnetArchiveIndexHit(index, keys) { if (!index || typeof index.has !== 'function') return null; for (const key of (Array.isArray(keys) ? keys : [keys])) { const safe = normalizeMagnetArchivePrimaryKey(key); if (safe && index.has(safe)) return index.get(safe); } return null; } function hasMagnetArchiveKeyOverlap(keys, set) { return (Array.isArray(keys) ? keys : [keys]).some(key => set && set.has && set.has(normalizeMagnetArchivePrimaryKey(key))); } function addMagnetArchiveKeysToSet(keys, set) { (Array.isArray(keys) ? keys : [keys]).forEach(key => { const safe = normalizeMagnetArchivePrimaryKey(key); if (safe && set && set.add) set.add(safe); }); } function normalizeMagnetArchivePrimaryKey(key) { const raw = normalizeLinkBookmarkText(key).trim().toLowerCase(); if (/^btih:[a-f0-9]{40}$/.test(raw)) return raw; if (/^btmh:[a-z0-9]+$/.test(raw)) return raw; return ''; } function isMagnetArchivePseudoLink(link) { const href = normalizeLinkBookmarkText(link && link.href).trim(); return !!href && (href.startsWith(CONF.magnetArchiveFolderHeaderHrefPrefix) || href.startsWith(CONF.magnetArchiveStateHrefPrefix)); } function isMagnetArchiveStateLink(link) { return normalizeLinkBookmarkText(link && link.href).trim().startsWith(CONF.magnetArchiveStateHrefPrefix); } function isMagnetArchiveFolderHeaderLink(link) { return normalizeLinkBookmarkText(link && link.href).trim().startsWith(CONF.magnetArchiveFolderHeaderHrefPrefix); } function encodeMagnetArchivePayload(prefix, payload) { return String(prefix || '') + configCloudBytesToBase64Url(configCloudStringToBytes(JSON.stringify(payload || {}))); } function getMagnetArchiveStateHrefMaxLen() { const max = Math.max(1024, Number(CONF.magnetArchiveStateHrefMaxLen) || 48000); return Math.min(max, getLinkBookmarkFieldMaxLen('href')); } function isMagnetArchiveStateHrefOverLimit(href) { return normalizeLinkBookmarkText(href).length > getMagnetArchiveStateHrefMaxLen(); } function getMagnetArchiveMagnetHrefMaxLen() { const max = Math.max(1024, Number(CONF.magnetArchiveMagnetHrefMaxLen) || 48000); return Math.min(max, getLinkBookmarkFieldMaxLen('href')); } function isMagnetArchiveMagnetHrefOverLimit(href) { return normalizeLinkBookmarkText(href).length > getMagnetArchiveMagnetHrefMaxLen(); } function decodeMagnetArchivePayload(href, prefix) { try { const bytes = configCloudBase64UrlToBytes(href, [prefix]); return JSON.parse(new TextDecoder().decode(bytes)); } catch (e) { return null; } } function makeMagnetArchiveOfficialLink(link) { return { title: normalizeLinkBookmarkLimitedText(link && link.title, 'title'), href: normalizeLinkBookmarkLimitedText(link && link.href, 'href'), icon: normalizeLinkBookmarkIconForOfficial(link && link.icon) || '/favicon.ico', date: normalizeLinkBookmarkText(link && link.date) || String(Date.now()) }; } function makeMagnetArchiveFolderHeaderLink(payload = {}) { const nowIso = new Date().toISOString(); const body = { kind: 'PikPak Enhancement Master Cloud Archive Folder', schemaVersion: 1, folderKey: getMagnetArchiveFolderKey(), archiveId: normalizeLinkBookmarkText(payload.archiveId || `ca_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`), createdAt: normalizeLinkBookmarkText(payload.createdAt || nowIso), updatedAt: nowIso }; return { title: CONF.magnetArchiveFolderHeaderTitle, href: normalizeLinkBookmarkLimitedText(encodeMagnetArchivePayload(CONF.magnetArchiveFolderHeaderHrefPrefix, body), 'href'), icon: '/favicon.ico', date: String(Date.now()) }; } function parseMagnetArchiveFolderHeader(link) { if (!isMagnetArchiveFolderHeaderLink(link)) return null; const payload = decodeMagnetArchivePayload(link && link.href, CONF.magnetArchiveFolderHeaderHrefPrefix); if (!payload || payload.kind !== 'PikPak Enhancement Master Cloud Archive Folder' || payload.schemaVersion !== 1) return null; if (normalizeLinkBookmarkText(payload.folderKey).trim() !== getMagnetArchiveFolderKey()) return null; return payload; } function getMagnetArchiveFolderHeader(folder) { const list = Array.isArray(folder && folder.list) ? folder.list : []; for (let i = 0; i < list.length; i++) { const link = list[i]; const payload = parseMagnetArchiveFolderHeader(link); if (payload) return { link, payload, index: i }; } return null; } function normalizeMagnetArchiveStateItem(value = {}) { const src = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; const status = normalizeLinkBookmarkText(src.restoreStatus || '').trim(); return { archiveAt: normalizeLinkBookmarkText(src.archiveAt || ''), sourceFileId: normalizeLinkBookmarkText(src.sourceFileId || ''), sourceTaskId: normalizeLinkBookmarkText(src.sourceTaskId || ''), sourceKind: normalizeLinkBookmarkText(src.sourceKind || ''), sourceSize: Number(src.sourceSize || 0) || 0, sourceName: normalizeLinkBookmarkLimitedText(src.sourceName || '', 'title'), deleteMode: normalizeLinkBookmarkText(src.deleteMode || 'none'), deleteAt: normalizeLinkBookmarkText(src.deleteAt || ''), restoreStatus: status === 'done' ? 'done' : 'none', restoreTaskId: normalizeLinkBookmarkText(src.restoreTaskId || ''), lastRestoreAt: normalizeLinkBookmarkText(src.lastRestoreAt || ''), updatedAt: normalizeLinkBookmarkText(src.updatedAt || '') }; } function normalizeMagnetArchiveStatePayload(payload = {}) { const src = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}; const items = {}; const rawItems = src.items && typeof src.items === 'object' && !Array.isArray(src.items) ? src.items : {}; Object.keys(rawItems).forEach(rawKey => { const key = normalizeMagnetArchivePrimaryKey(rawKey); if (!key) return; items[key] = normalizeMagnetArchiveStateItem(rawItems[rawKey]); }); return { kind: 'PikPak Enhancement Master Cloud Archive State', schemaVersion: 1, updatedAt: normalizeLinkBookmarkText(src.updatedAt || new Date().toISOString()), items }; } function makeMagnetArchiveStateChunkLink(payload = {}) { const body = normalizeMagnetArchiveStatePayload({ ...payload, updatedAt: new Date().toISOString() }); const href = encodeMagnetArchivePayload(CONF.magnetArchiveStateHrefPrefix, body); if (isMagnetArchiveStateHrefOverLimit(href)) return null; return { title: CONF.magnetArchiveStateTitle, href: normalizeLinkBookmarkText(href), icon: '/favicon.ico', date: String(Date.now()) }; } function makeMagnetArchiveStateLinks(payload = {}) { const body = normalizeMagnetArchiveStatePayload({ ...payload, updatedAt: new Date().toISOString() }); const keys = Object.keys(body.items || {}); if (!keys.length) { const emptyLink = makeMagnetArchiveStateChunkLink({ ...body, items: {} }); return emptyLink ? [emptyLink] : null; } const chunks = []; let current = {}; for (const key of keys) { const nextItems = { ...current, [key]: body.items[key] }; const testLink = makeMagnetArchiveStateChunkLink({ ...body, items: nextItems }); if (testLink) { current = nextItems; continue; } if (!Object.keys(current).length) return null; chunks.push(current); current = { [key]: body.items[key] }; if (!makeMagnetArchiveStateChunkLink({ ...body, items: current })) return null; } if (Object.keys(current).length) chunks.push(current); const links = chunks.map(items => makeMagnetArchiveStateChunkLink({ ...body, items })).filter(Boolean); return links.length === chunks.length ? links : null; } function parseMagnetArchiveStateLink(link) { if (!isMagnetArchiveStateLink(link)) return null; const payload = decodeMagnetArchivePayload(link && link.href, CONF.magnetArchiveStateHrefPrefix); if (!payload || payload.kind !== 'PikPak Enhancement Master Cloud Archive State' || payload.schemaVersion !== 1) return null; return normalizeMagnetArchiveStatePayload(payload); } function getMagnetArchiveStateInfo(folder) { const list = Array.isArray(folder && folder.list) ? folder.list : []; const links = []; const merged = normalizeMagnetArchiveStatePayload({ items: {} }); for (let i = 0; i < list.length; i++) { const link = list[i]; const state = parseMagnetArchiveStateLink(link); if (!state) continue; links.push({ link, state, index: i }); Object.assign(merged.items, state.items || {}); if (state.updatedAt) merged.updatedAt = state.updatedAt; } if (!links.length) return null; return { link: links[0].link, links, state: normalizeMagnetArchiveStatePayload(merged), index: links[0].index }; } function getMagnetArchiveLinkState(folder, link) { const keys = getMagnetArchiveKeysFromBookmark(link); if (!keys.length) return null; const info = getMagnetArchiveStateInfo(folder); const items = info && info.state && info.state.items ? info.state.items : null; if (!items) return null; for (const key of keys) { if (items[key]) return items[key]; } return null; } function setMagnetArchiveFolderState(folder, state) { if (!folder || typeof folder !== 'object') return false; if (!Array.isArray(folder.list)) folder.list = []; const links = makeMagnetArchiveStateLinks(state); if (!links || !links.length) return false; folder.list = folder.list.filter(item => !isMagnetArchiveStateLink(item)); const headerIndex = folder.list.findIndex(item => isMagnetArchiveFolderHeaderLink(item)); folder.list.splice(headerIndex >= 0 ? headerIndex + 1 : 0, 0, ...links); return true; } function makeMagnetArchiveManagedFolderName(folders = []) { return getMagnetArchiveFolderStorageName(); } function normalizeMagnetArchiveManagedFolder(folder, folders = []) { const name = getMagnetArchiveFolderStorageName(); const list = Array.isArray(folder && folder.list) ? folder.list : []; const headerInfo = getMagnetArchiveFolderHeader(folder); const stateInfo = getMagnetArchiveStateInfo(folder); const visible = list.filter(link => link && typeof link === 'object' && !Array.isArray(link) && !isMagnetArchivePseudoLink(link)).map(link => makeMagnetArchiveOfficialLink(link)); folder.folder = name; const stateLinks = makeMagnetArchiveStateLinks(stateInfo ? stateInfo.state : { items: {} }) || (stateInfo && Array.isArray(stateInfo.links) ? stateInfo.links.map(info => makeMagnetArchiveOfficialLink(info.link)) : makeMagnetArchiveStateLinks({ items: {} })) || []; folder.list = [ headerInfo ? makeMagnetArchiveOfficialLink(headerInfo.link) : makeMagnetArchiveFolderHeaderLink(), ...stateLinks, ...visible ].filter(Boolean); Object.keys(folder).forEach(key => { if (key !== 'folder' && key !== 'list') delete folder[key]; }); return folder; } function isMagnetArchiveBookmarkLink(folder, link) { return !!(isMagnetArchiveBookmarkFolder(folder) && link && typeof link === 'object' && normalizeMagnetArchiveLink(link.href || '', link.title || '').ok); } function buildMagnetArchiveBookmarkIndex(folders) { const map = new Map(); (Array.isArray(folders) ? folders : []).forEach(folder => { if (!isMagnetArchiveBookmarkFolder(folder)) return; (Array.isArray(folder && folder.list) ? folder.list : []).forEach(link => { if (isMagnetArchivePseudoLink(link)) return; const key = getMagnetArchivePrimaryKeyFromBookmark(link); const keys = getMagnetArchiveKeysFromBookmark(link); if (!key || !keys.length) return; const value = { folder, link, key, keys }; keys.forEach(aliasKey => { if (aliasKey && !map.has(aliasKey)) map.set(aliasKey, value); }); }); }); return map; } function getMagnetArchiveFolderKey() { return normalizeLinkBookmarkText(CONF.magnetArchiveFolderKey || 'folder_magnet_archive').trim() || 'folder_magnet_archive'; } function getMagnetArchiveFolderStorageName() { return normalizeLinkBookmarkLimitedText(CONF.magnetArchiveFolderStorageName || 'PEM-CLOUD-ARCHIVE', 'folder'); } function getMagnetArchiveFolderName(L = getStrings()) { const key = getMagnetArchiveFolderKey(); return normalizeLinkBookmarkLimitedText((L && L[key]) || key, 'folder'); } function isMagnetArchiveBookmarkFolder(folder) { return !!getMagnetArchiveFolderHeader(folder); } function hasMagnetArchiveReservedStorageNameConflict(folders) { const storageName = getMagnetArchiveFolderStorageName(); return (Array.isArray(folders) ? folders : []).some(folder => normalizeLinkBookmarkText(folder && folder.folder).trim() === storageName && !isMagnetArchiveBookmarkFolder(folder)); } function getMagnetArchiveReservedFolderNameConflictMessage(L = getStrings()) { const storageName = getMagnetArchiveFolderStorageName(); const template = normalizeLinkBookmarkText(L && L.msg_magnet_archive_reserved_folder_name_conflict); return template.split('{name}').join(storageName); } function makeMagnetArchiveBookmarkFolder(list = []) { const links = Array.isArray(list) ? list.filter(link => link && typeof link === 'object' && !Array.isArray(link) && !isMagnetArchivePseudoLink(link)).map(link => makeMagnetArchiveOfficialLink(link)) : []; return { folder: getMagnetArchiveFolderStorageName(), list: [makeMagnetArchiveFolderHeaderLink(), ...(makeMagnetArchiveStateLinks({ items: {} }) || []), ...links] }; } function findMagnetArchiveBookmarkFolderIndex(folders) { const list = Array.isArray(folders) ? folders : []; return list.findIndex(folder => isMagnetArchiveBookmarkFolder(folder)); } function insertMagnetArchiveVisibleLink(folder, link) { if (!Array.isArray(folder.list)) folder.list = []; const firstVisibleIndex = folder.list.findIndex(item => !isMagnetArchivePseudoLink(item)); if (firstVisibleIndex >= 0) folder.list.splice(firstVisibleIndex, 0, link); else folder.list.push(link); } function mergeMagnetArchiveEntriesIntoLinkBookmarkFolders(folders, entries, sourceRows = []) { const next = cloneLinkBookmarkFoldersForEditing(folders); let folderIndex = findMagnetArchiveBookmarkFolderIndex(next); if (folderIndex < 0) { next.push(makeMagnetArchiveBookmarkFolder([])); folderIndex = next.length - 1; } const folder = normalizeMagnetArchiveManagedFolder(next[folderIndex], next); const index = buildMagnetArchiveBookmarkIndex(next); const stateInfo = getMagnetArchiveStateInfo(folder); const state = normalizeMagnetArchiveStatePayload(stateInfo ? stateInfo.state : { items: {} }); const rowByKey = new Map(); (Array.isArray(sourceRows) ? sourceRows : []).forEach(row => { getMagnetArchiveKeysFromRow(row).forEach(key => { if (key && !rowByKey.has(key)) rowByKey.set(key, row); }); }); const now = new Date().toISOString(); const added = []; const duplicate = []; (Array.isArray(entries) ? entries : []).forEach(entry => { const key = getMagnetArchivePrimaryKeyFromBookmark(entry); const keys = getMagnetArchiveKeysFromBookmark(entry); if (!key || !keys.length || getMagnetArchiveIndexHit(index, keys)) { duplicate.push(entry); return; } const safeEntry = makeMagnetArchiveOfficialLink(entry); insertMagnetArchiveVisibleLink(folder, safeEntry); const value = { folder, link: safeEntry, key, keys }; keys.forEach(aliasKey => { if (aliasKey && !index.has(aliasKey)) index.set(aliasKey, value); }); const row = keys.map(aliasKey => rowByKey.get(aliasKey)).find(Boolean) || {}; const item = row.item && typeof row.item === 'object' ? row.item : {}; state.items[key] = normalizeMagnetArchiveStateItem({ archiveAt: now, sourceFileId: item.id || item.file_id || item.fileId || item.file && item.file.id || '', sourceTaskId: item.kind === 'drive#task' ? item.id : '', sourceKind: item.kind || item.mime_type || '', sourceSize: Number(item.size || item.file_size || 0) || 0, sourceName: item.name || item.title || safeEntry.title || '', deleteMode: 'none', deleteAt: '', restoreStatus: 'none', restoreTaskId: '', lastRestoreAt: '', updatedAt: now }); added.push(safeEntry); }); const stateSaved = setMagnetArchiveFolderState(folder, state); return { folders: next, folderIndex, added, duplicate, stateSaved }; } function isMagnetArchiveEntriesVerified(folders, entries) { const index = buildMagnetArchiveBookmarkIndex(folders); return (Array.isArray(entries) ? entries : []).every(entry => { const keys = getMagnetArchiveKeysFromBookmark(entry); return keys.length && !!getMagnetArchiveIndexHit(index, keys); }); } function readMagnetArchiveJournal() { const raw = gmGet('pk_magnet_archive_journal', '[]'); const list = parseConfigJson(raw, []); return Array.isArray(list) ? list : []; } function appendMagnetArchiveJournal(record) { const max = Number(CONF.magnetArchiveJournalMax) || 2000; const list = readMagnetArchiveJournal(); list.push({ v: 1, time: new Date().toISOString(), action: normalizeLinkBookmarkText(record && record.action || ''), status: normalizeLinkBookmarkText(record && record.status || ''), count: Number(record && record.count || 0) || 0, ids: Array.isArray(record && record.ids) ? record.ids.map(x => normalizeLinkBookmarkText(x)).filter(Boolean).slice(0, 200) : [], keys: Array.isArray(record && record.keys) ? record.keys.map(x => normalizeLinkBookmarkText(x)).filter(Boolean).slice(0, 200) : [], message: normalizeLinkBookmarkLimitedText(record && record.message || '', 'title') }); gmSet('pk_magnet_archive_journal', JSON.stringify(list.slice(-max))); } function getMagnetArchiveDeleteSourceRows(result, addedEntries) { const keys = new Set((Array.isArray(addedEntries) ? addedEntries : []).map(entry => getMagnetArchivePrimaryKeyFromBookmark(entry)).filter(Boolean)); const map = new Map(); (result && Array.isArray(result.ok) ? result.ok : []).forEach(row => { const key = normalizeMagnetArchivePrimaryKey(row && (row.key || getMagnetArchivePrimaryKeyFromBookmark(row.entry))); if (!key || !keys.has(key)) return; const item = row.item && typeof row.item === 'object' ? row.item : {}; const id = normalizeLinkBookmarkText(item.id || item.file_id || item.fileId || item.file && item.file.id || ''); if (!id) return; let rec = map.get(id); if (!rec) { rec = { id, key, item, entry: row.entry, keys: [], items: [], entries: [] }; map.set(id, rec); } if (!rec.keys.includes(key)) rec.keys.push(key); rec.items.push(item); if (row.entry) rec.entries.push(row.entry); }); return Array.from(map.values()); } function getMagnetArchiveDeleteRowKeys(rows) { return (Array.isArray(rows) ? rows : []).flatMap(row => Array.isArray(row && row.keys) ? row.keys : [row && row.key]).map(normalizeMagnetArchivePrimaryKey).filter(Boolean); } function getMagnetArchiveDeleteRowEntries(rows) { return (Array.isArray(rows) ? rows : []).flatMap(row => Array.isArray(row && row.entries) ? row.entries : [row && row.entry]).filter(Boolean); } function getMagnetArchiveDeleteMissingEntries(addedEntries, rows) { const rowKeys = new Set(getMagnetArchiveDeleteRowKeys(rows)); return (Array.isArray(addedEntries) ? addedEntries : []).filter(entry => { const key = normalizeMagnetArchivePrimaryKey(getMagnetArchivePrimaryKeyFromBookmark(entry)); return key && !rowKeys.has(key); }); } function getMagnetArchiveDuplicateDeleteRows(result) { const map = new Map(); (result && Array.isArray(result.dup) ? result.dup : []).forEach(row => { const existed = row && row.existed && typeof row.existed === 'object' ? row.existed : null; const entry = existed && existed.link && typeof existed.link === 'object' ? existed.link : null; const key = normalizeMagnetArchivePrimaryKey(row && (row.key || row.meta && row.meta.primaryKey || getMagnetArchivePrimaryKeyFromBookmark(entry))); if (!key) return; const item = row.item && typeof row.item === 'object' ? row.item : {}; const id = normalizeLinkBookmarkText(item.id || item.file_id || item.fileId || item.file && item.file.id || ''); if (!id) return; let rec = map.get(id); if (!rec) { rec = { id, key, item, entry, keys: [], items: [], entries: [] }; map.set(id, rec); } if (!rec.keys.includes(key)) rec.keys.push(key); rec.items.push(item); if (entry && !rec.entries.includes(entry)) rec.entries.push(entry); }); return Array.from(map.values()); } async function trashMagnetArchiveSourceRows(rows) { const L = getStrings(); const ids = (Array.isArray(rows) ? rows : []).map(row => row && row.id).filter(Boolean); if (!ids.length) return 0; const BATCH_SIZE = 100; for (let i = 0; i < ids.length; i += BATCH_SIZE) { const chunk = ids.slice(i, i + BATCH_SIZE); const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files:batchTrash', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ids: chunk }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error_description || `API ${res.status}`); } } const isTrashConfirmedPayload = payload => { if (!payload || typeof payload !== 'object') return false; if (payload.trashed === true) return true; const err = String(payload.error || '').toLowerCase(); const code = Number(payload.error_code ?? payload.errorCode); return err === 'file_in_recycle_bin' || code === 9; }; const fetchTrashVerifyPayload = async id => { const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/files/${encodeURIComponent(id)}?thumbnail_size=SIZE_MEDIUM&_t=${Date.now()}`, { headers: getHeaders() }); const payload = await res.json().catch(() => ({})); return { ok: res.ok, status: res.status, payload }; }; for (const id of ids) { let ok = false; for (let i = 0; i < 20; i++) { try { const info = await fetchTrashVerifyPayload(id); if (isTrashConfirmedPayload(info.payload)) { ok = true; break; } } catch (e) {} await sleep(500); } if (!ok) throw new Error((L.msg_magnet_archive_delete_verify_failed).replace('{id}', id)); } if (typeof load === 'function') await load(false, true); return ids.length; } async function patchMagnetArchiveStateToBookmarks(keys, updater) { const keyList = (Array.isArray(keys) ? keys : []).map(key => normalizeMagnetArchivePrimaryKey(key)).filter(Boolean); if (!keyList.length) return true; if (!canPerformLinkBookmarkWriteNow()) return false; await loadLinkBookmarkOfficial(true); const next = cloneLinkBookmarkFoldersForEditing(S.linkBookmarkFolders || []); const folderIndex = findMagnetArchiveBookmarkFolderIndex(next); if (folderIndex < 0 || !next[folderIndex]) return false; const folder = normalizeMagnetArchiveManagedFolder(next[folderIndex], next); const visibleIndex = buildMagnetArchiveBookmarkIndex(next); const info = getMagnetArchiveStateInfo(folder); const state = normalizeMagnetArchiveStatePayload(info ? info.state : { items: {} }); let changed = 0; const touched = new Set(); keyList.forEach(inputKey => { const hit = visibleIndex.get(inputKey); if (!hit) return; const key = normalizeMagnetArchivePrimaryKey(hit.key || inputKey); if (!key || touched.has(key)) return; const oldItem = state.items[key] || {}; const patch = typeof updater === 'function' ? updater(key, oldItem, inputKey, hit) : updater; state.items[key] = normalizeMagnetArchiveStateItem({ ...oldItem, ...(patch || {}) }); touched.add(key); changed++; }); if (!changed) return false; if (!setMagnetArchiveFolderState(folder, state)) return false; return !!await saveLinkBookmarkFoldersWithConflictCheck(next, folderIndex, { allowCurrentDuplicateFolders: true }); } async function patchMagnetArchiveDeleteStatusToBookmarks(entries, deleteMode = 'trash') { const keys = (Array.isArray(entries) ? entries : []).map(entry => getMagnetArchivePrimaryKeyFromBookmark(entry)).filter(Boolean); const now = new Date().toISOString(); return patchMagnetArchiveStateToBookmarks(keys, () => ({ deleteMode, deleteAt: now, updatedAt: now })); } function getMagnetArchiveRestoreStatusView(state, L = getStrings()) { const restored = normalizeLinkBookmarkText(state && state.restoreStatus).trim() === 'done'; const label = restored ? L.status_magnet_restore_done : L.status_magnet_restore_none; return { restored, className: restored ? 'is-restored' : 'is-unrestored', label, tip: label }; } function getMagnetArchiveRestoreMagnet(link) { const meta = normalizeMagnetArchiveLink(link && link.href || '', link && link.title || ''); return meta.ok ? meta.rawMagnet : ''; } async function patchMagnetArchiveRestoreStatusToBookmark(link, restoreStatus, restoreTaskId = '') { const key = getMagnetArchivePrimaryKeyFromBookmark(link); if (!key) return false; const now = new Date().toISOString(); const nextStatus = normalizeLinkBookmarkText(restoreStatus).trim() === 'done' ? 'done' : 'none'; return patchMagnetArchiveStateToBookmarks([key], oldItem => ({ restoreStatus: nextStatus, restoreTaskId: nextStatus === 'done' ? normalizeLinkBookmarkText(restoreTaskId || oldItem.restoreTaskId || '') : '', lastRestoreAt: nextStatus === 'done' ? now : normalizeLinkBookmarkText(oldItem.lastRestoreAt || ''), updatedAt: now })); } async function handleMagnetArchiveRestoreBookmark(folderIndex, linkIndex) { const L = getStrings(); const folder = S.linkBookmarkFolders && S.linkBookmarkFolders[folderIndex]; const link = folder && Array.isArray(folder.list) ? folder.list[linkIndex] : null; if (!isMagnetArchiveBookmarkLink(folder, link)) return openLinkBookmarkHref(folderIndex, linkIndex); if (!canPerformLinkBookmarkWriteNow()) return; const magnet = getMagnetArchiveRestoreMagnet(link); if (!magnet) { showToast(L.msg_magnet_archive_restore_no_magnet, 'warning'); return; } const key = getMagnetArchivePrimaryKeyFromBookmark(link); try { if (typeof initClipboardMagnetFocusWatcher === 'function' && typeof window.__pkSubmitMagnetWithPreview !== 'function') initClipboardMagnetFocusWatcher(); const submitWithPreview = window.__pkSubmitMagnetWithPreview; if (typeof submitWithPreview !== 'function') throw new Error('Magnet preview helper unavailable'); const result = await submitWithPreview(magnet, { successMessage: L.msg_magnet_archive_restore_success, logPrefix: 'Cloud Archive Restore Failed' }); if (!result || result.cancelled) return; if (result.successCount > 0) { const statusSaved = await patchMagnetArchiveRestoreStatusToBookmark(link, 'done', ''); appendMagnetArchiveJournal({ action: 'restore_source', status: statusSaved ? 'done' : 'status_failed', count: 1, keys: [key], message: statusSaved ? L.msg_magnet_archive_restore_success : L.msg_magnet_archive_restore_status_failed }); if (!statusSaved) showToast(L.msg_magnet_archive_restore_status_failed, 'warning'); if (typeof updateQuotaUI === 'function') setTimeout(() => updateQuotaUI(), 1000); if (S.offlineMode) { scheduleOfflineTaskAddProbe('add', 1000); } return; } appendMagnetArchiveJournal({ action: 'restore_source', status: 'failed', count: 1, keys: [key], message: L.msg_magnet_archive_restore_failed }); } catch (e) { appendMagnetArchiveJournal({ action: 'restore_source', status: 'failed', count: 1, keys: [key], message: e && e.message || L.msg_magnet_archive_restore_failed }); } } function pushMagnetArchiveFailure(list, item, reason) { if (!Array.isArray(list) || !reason) return; list.push({ item: item && typeof item === 'object' ? item : {}, title: getMagnetArchiveItemTitle(item), reason: normalizeLinkBookmarkText(reason) }); } function pushMagnetArchiveEntryFailures(list, entries, reason) { (Array.isArray(entries) ? entries : []).forEach(entry => pushMagnetArchiveFailure(list, { name: entry && entry.title || '' }, reason)); } function pushMagnetArchiveRowFailures(list, rows, reason) { (Array.isArray(rows) ? rows : []).forEach(row => { const items = Array.isArray(row && row.items) && row.items.length ? row.items : [row && row.item]; items.forEach(item => pushMagnetArchiveFailure(list, item, reason)); }); } function showMagnetArchiveFailureModal(failures = []) { const L = getStrings(); const rows = (Array.isArray(failures) ? failures : []).map(row => { const title = normalizeLinkBookmarkText(row && (row.title || getMagnetArchiveItemTitle(row.item))); const reason = normalizeLinkBookmarkText(row && row.reason); return title || reason ? { title: title || '-', reason: reason || '-' } : null; }).filter(Boolean); if (!rows.length) return; const rowsHtml = rows.map(row => `
${esc(row.title)}
${esc(row.reason)}
`).join(''); const m = showModal(`

${esc(L.title_magnet_archive_failure_list)}

${esc(L.label_magnet_archive_source)}
${esc(L.label_magnet_archive_reason)}
${rowsHtml}
`); bindMagnetArchiveOverflowTips(m); const box = m.querySelector('.pk-modal'); const preview = m.querySelector('.pk-magnet-archive-failure-preview'); const table = m.querySelector('.pk-magnet-archive-failure-table'); const act = m.querySelector('.pk-magnet-archive-failure-act'); if (box) Object.assign(box.style, { width: '760px', maxWidth: 'calc(100vw - 56px)', maxHeight: 'calc(100vh - 72px)', padding: '24px', boxSizing: 'border-box', overflow: 'hidden', display: 'flex', flexDirection: 'column' }); if (preview) Object.assign(preview.style, { width: '100%', maxWidth: '100%', maxHeight: 'calc(100vh - 120px)', minHeight: '0', display: 'flex', flexDirection: 'column', overflow: 'hidden' }); if (table) Object.assign(table.style, { flex: rows.length > 6 ? '1 1 auto' : '0 0 auto', minHeight: '0', maxHeight: rows.length > 6 ? 'min(420px, calc(100vh - 260px))' : 'none', overflowY: rows.length > 6 ? 'auto' : 'hidden' }); if (act) Object.assign(act.style, { flex: '0 0 auto', marginTop: '16px' }); const btn = m.querySelector('#pk_magnet_archive_failure_ok'); if (btn) btn.onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => { if (document.contains(m)) m.focus(); }, 10); m.addEventListener('keydown', e => { const tag = String(e.target && e.target.tagName || '').toLowerCase(); if (e.target && (e.target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select')) return; if (e.key === 'Enter' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); e.stopPropagation(); btn?.click(); } }, true); } async function handleMagnetArchivePostWriteSoftDelete(result, addedEntries, failures = []) { const L = getStrings(); const sourceRows = getMagnetArchiveDeleteSourceRows(result, addedEntries); const duplicateRows = getMagnetArchiveDuplicateDeleteRows(result); const rowMap = new Map(); [sourceRows, duplicateRows].forEach(list => { (Array.isArray(list) ? list : []).forEach(row => { if (!row || !row.id) return; const keys = getMagnetArchiveDeleteRowKeys([row]).join('|'); const mapKey = `${row.id}\n${keys || row.key || ''}`; if (!rowMap.has(mapKey)) rowMap.set(mapKey, row); }); }); const rows = Array.from(rowMap.values()); const missingEntries = getMagnetArchiveDeleteMissingEntries(addedEntries, sourceRows); if (!rows.length) { appendMagnetArchiveJournal({ action: 'trash_source', status: 'no_source', count: 0, message: L.msg_magnet_archive_delete_no_source }); showToast(L.msg_magnet_archive_delete_no_source, 'info'); pushMagnetArchiveEntryFailures(failures, addedEntries, L.msg_magnet_archive_delete_no_source); return false; } const confirmTpl = sourceRows.length && duplicateRows.length ? (L.msg_magnet_archive_delete_mixed_confirm || L.msg_magnet_archive_delete_confirm) : (duplicateRows.length ? (L.msg_magnet_archive_delete_dup_confirm || L.msg_magnet_archive_delete_confirm) : L.msg_magnet_archive_delete_confirm); const deleteConfirmed = await showConfirm(confirmTpl.replace('{n}', rows.length).replace('{src}', sourceRows.length).replace('{dup}', duplicateRows.length), L.title_magnet_archive_delete_confirm || L.title_confirm, { enterAction: 'yes' }); if (!deleteConfirmed) return false; if (missingEntries.length) pushMagnetArchiveEntryFailures(failures, missingEntries, L.msg_magnet_archive_delete_no_source); const task = createMagnetArchiveCheckProgress(L.msg_magnet_archive_delete_running); try { const count = await trashMagnetArchiveSourceRows(rows); closeMagnetArchiveCheckProgress(task); if (count < rows.length) { const reason = L.msg_magnet_archive_delete_partial_unknown; appendMagnetArchiveJournal({ action: 'trash_source', status: 'partial_unknown', count, ids: rows.map(x => x.id), keys: getMagnetArchiveDeleteRowKeys(rows), message: reason }); showToast(reason, 'warning'); pushMagnetArchiveRowFailures(failures, rows, reason); return false; } const deletedEntries = getMagnetArchiveDeleteRowEntries(rows); const statusSaved = await patchMagnetArchiveDeleteStatusToBookmarks(deletedEntries, 'trash'); appendMagnetArchiveJournal({ action: 'trash_source', status: statusSaved ? 'done' : 'status_failed', count, ids: rows.map(x => x.id), keys: getMagnetArchiveDeleteRowKeys(rows), message: statusSaved ? L.msg_magnet_archive_delete_success : L.msg_magnet_archive_delete_status_failed }); showToast((statusSaved ? L.msg_magnet_archive_delete_success : L.msg_magnet_archive_delete_status_failed).replace('{n}', count), statusSaved ? 'success' : 'warning'); if (!statusSaved) pushMagnetArchiveRowFailures(failures, rows, L.msg_magnet_archive_delete_status_failed); return statusSaved; } catch (e) { closeMagnetArchiveCheckProgress(task); const reason = L.msg_magnet_archive_delete_failed + (e && e.message ? ` ${e.message}` : ''); appendMagnetArchiveJournal({ action: 'trash_source', status: 'failed', count: rows.length, ids: rows.map(x => x.id), keys: getMagnetArchiveDeleteRowKeys(rows), message: e && e.message || L.msg_magnet_archive_delete_failed }); showToast(reason, 'error'); pushMagnetArchiveRowFailures(failures, rows, reason); return false; } } async function handleMagnetArchiveDeleteDuplicateSources(result, failures = []) { const L = getStrings(); if (!canPerformLinkBookmarkWriteNow()) return false; const rows = getMagnetArchiveDuplicateDeleteRows(result); if (!rows.length) { appendMagnetArchiveJournal({ action: 'trash_duplicate_source', status: 'no_source', count: 0, message: L.msg_magnet_archive_delete_dup_no_source || L.msg_magnet_archive_delete_no_source }); showToast(L.msg_magnet_archive_delete_dup_no_source || L.msg_magnet_archive_delete_no_source, 'info'); return false; } const msg = (L.msg_magnet_archive_delete_dup_confirm || L.msg_magnet_archive_delete_confirm).replace('{n}', rows.length); const deleteConfirmed = await showConfirm(msg, L.title_magnet_archive_delete_confirm || L.title_confirm, { enterAction: 'yes' }); if (!deleteConfirmed) return false; const task = createMagnetArchiveCheckProgress(L.msg_magnet_archive_delete_running); try { const count = await trashMagnetArchiveSourceRows(rows); closeMagnetArchiveCheckProgress(task); if (count < rows.length) { const reason = L.msg_magnet_archive_delete_partial_unknown; appendMagnetArchiveJournal({ action: 'trash_duplicate_source', status: 'partial_unknown', count, ids: rows.map(x => x.id), keys: getMagnetArchiveDeleteRowKeys(rows), message: reason }); showToast(reason, 'warning'); pushMagnetArchiveRowFailures(failures, rows, reason); return false; } const deletedEntries = getMagnetArchiveDeleteRowEntries(rows); const statusSaved = await patchMagnetArchiveDeleteStatusToBookmarks(deletedEntries, 'trash'); appendMagnetArchiveJournal({ action: 'trash_duplicate_source', status: statusSaved ? 'done' : 'status_failed', count, ids: rows.map(x => x.id), keys: getMagnetArchiveDeleteRowKeys(rows), message: statusSaved ? L.msg_magnet_archive_delete_success : L.msg_magnet_archive_delete_status_failed }); showToast((statusSaved ? L.msg_magnet_archive_delete_success : L.msg_magnet_archive_delete_status_failed).replace('{n}', count), statusSaved ? 'success' : 'warning'); if (!statusSaved) pushMagnetArchiveRowFailures(failures, rows, L.msg_magnet_archive_delete_status_failed); return statusSaved; } catch (e) { closeMagnetArchiveCheckProgress(task); const reason = L.msg_magnet_archive_delete_failed + (e && e.message ? ` ${e.message}` : ''); appendMagnetArchiveJournal({ action: 'trash_duplicate_source', status: 'failed', count: rows.length, ids: rows.map(x => x.id), keys: getMagnetArchiveDeleteRowKeys(rows), message: e && e.message || L.msg_magnet_archive_delete_failed }); showToast(reason, 'error'); pushMagnetArchiveRowFailures(failures, rows, reason); return false; } } async function writeMagnetArchiveCheckResultToBookmarks(result) { const L = getStrings(); if (!canPerformLinkBookmarkWriteNow()) return false; const entries = (result && Array.isArray(result.ok) ? result.ok : []).map(row => row && row.entry).filter(Boolean); const failures = []; if (!entries.length) { showToast(L.msg_magnet_archive_write_no_item, 'warning'); return false; } const task = createMagnetArchiveCheckProgress(L.msg_magnet_archive_writing); try { await loadLinkBookmarkOfficial(true); const current = Array.isArray(S.linkBookmarkFolders) ? S.linkBookmarkFolders : []; const merged = mergeMagnetArchiveEntriesIntoLinkBookmarkFolders(current, entries, result.ok); if (!merged.added.length) { closeMagnetArchiveCheckProgress(task); showToast(L.msg_magnet_archive_write_duplicate_only, 'info'); return false; } if (merged.stateSaved === false) { const reason = L.msg_magnet_archive_state_too_large; pushMagnetArchiveEntryFailures(failures, merged.added, reason); showToast(reason, 'error'); showMagnetArchiveFailureModal(failures); return false; } const reservedFolderNameConflict = hasMagnetArchiveReservedStorageNameConflict(merged.folders); const hasDuplicateFoldersBeforeSave = hasDuplicateLinkBookmarkFolders(merged.folders); const saved = await saveLinkBookmarkFoldersWithConflictCheck(merged.folders, merged.folderIndex, { allowCurrentDuplicateFolders: true, silentDuplicateFolderToast: true }); if (!saved) { const reason = reservedFolderNameConflict ? getMagnetArchiveReservedFolderNameConflictMessage(L) : (hasDuplicateFoldersBeforeSave ? L.msg_link_bookmark_duplicate_folder_write_blocked : (L.msg_magnet_archive_write_save_failed || L.msg_magnet_archive_write_failed)); pushMagnetArchiveEntryFailures(failures, entries, reason); showMagnetArchiveFailureModal(failures); return false; } if (!isMagnetArchiveEntriesVerified(S.linkBookmarkFolders, merged.added)) { showToast(L.msg_magnet_archive_write_verify_failed, 'error'); pushMagnetArchiveEntryFailures(failures, merged.added, L.msg_magnet_archive_write_verify_failed); showMagnetArchiveFailureModal(failures); return false; } const msg = merged.duplicate.length ? L.msg_magnet_archive_write_partial.replace('{ok}', merged.added.length).replace('{dup}', merged.duplicate.length) : L.msg_magnet_archive_write_success.replace('{count}', merged.added.length); showToast(msg, 'success'); await handleMagnetArchivePostWriteSoftDelete(result, merged.added, failures); if (failures.length) showMagnetArchiveFailureModal(failures); return true; } catch (e) { const errMsg = e && e.message ? String(e.message) : ''; const reason = errMsg && errMsg !== L.msg_magnet_archive_write_failed ? `${L.msg_magnet_archive_write_failed} ${errMsg}` : L.msg_magnet_archive_write_failed; showToast(reason, 'error'); pushMagnetArchiveEntryFailures(failures, entries, reason); showMagnetArchiveFailureModal(failures); return false; } finally { closeMagnetArchiveCheckProgress(task); } } function makeMagnetArchiveBookmarkEntry(resource, normalized) { const item = resource && typeof resource === 'object' ? resource : {}; const meta = normalized && normalized.ok ? normalized : normalizeMagnetArchiveLink(normalized && normalized.rawMagnet || item.source_url || '', item.name || item.title || ''); if (!meta.ok || isLinkBookmarkOfficialFieldOverLimit(meta.rawMagnet, 'href') || isMagnetArchiveMagnetHrefOverLimit(meta.rawMagnet)) return null; const now = new Date().toISOString(); return { title: normalizeLinkBookmarkLimitedText(item.name || item.title || meta.title, 'title'), href: normalizeLinkBookmarkText(meta.rawMagnet), icon: '/favicon.ico', date: String(Date.now()) }; } function createMagnetArchiveCheckProgress(text) { return FloatBarManager && typeof FloatBarManager.create === 'function' ? FloatBarManager.create(text) : null; } function updateMagnetArchiveCheckProgress(task, done, total, text) { if (task && typeof task.update === 'function') task.update(`${text} ${done}/${total}`); } function closeMagnetArchiveCheckProgress(task) { if (task && typeof task.destroy === 'function') task.destroy(); } function getMagnetArchiveItemId(item) { return normalizeLinkBookmarkText(item && (item.id || item.file_id || item.fileId || item.file && item.file.id || item.task_id || item.taskId || item.name || item.title)); } function getMagnetArchiveItemTitle(item) { return normalizeLinkBookmarkText(item && (item.name || item.title || item.file_name || item.fileName || getMagnetArchiveItemId(item))).replace(/\s+/g, ' ').trim(); } async function collectSelectedMagnetArchiveCheckResult(progressTask = null) { const L = getStrings(); const selected = getMagnetBackupSelectedItems(); const result = { selected, ok: [], dup: [], skip: [], bad: [] }; if (!selected.length) return result; const progressTotal = selected.length; const progressDoneKeys = new Set(); const waitProgressFrame = () => new Promise(resolve => { if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => resolve()); else setTimeout(resolve, 16); }); let progressChain = Promise.resolve(); const markProgressDoneNow = (item) => { const key = getMagnetArchiveItemId(item); if (!key || progressDoneKeys.has(key)) return false; progressDoneKeys.add(key); if (progressTask) updateMagnetArchiveCheckProgress(progressTask, Math.min(progressTotal, progressDoneKeys.size), progressTotal, L.msg_magnet_archive_checking); return true; }; const queueProgressDone = (items) => { const list = (Array.isArray(items) ? items : [items]).filter(Boolean); if (!list.length) return progressChain; progressChain = progressChain.then(async () => { for (const item of list) { if (markProgressDoneNow(item) && progressTask) await waitProgressFrame(); } }).catch(() => {}); return progressChain; }; const getMagnetArchiveDetailIds = (item) => { const ids = []; const push = id => { const text = normalizeLinkBookmarkText(id).trim(); if (text && !ids.includes(text)) ids.push(text); }; push(item && item.id); push(item && item.file_id); push(item && item.fileId); push(item && item.file && item.file.id); return ids; }; if (progressTask) { updateMagnetArchiveCheckProgress(progressTask, 0, progressTotal, L.msg_magnet_archive_checking); await waitProgressFrame(); } const perItem = new Map(); selected.forEach(item => perItem.set(getMagnetArchiveItemId(item), { item, links: [], invalid: [], detailFailed: false })); const addLink = (item, link) => { const itemId = getMagnetArchiveItemId(item); const bucket = perItem.get(itemId); if (!bucket) return; const meta = normalizeMagnetArchiveLink(link, getMagnetArchiveItemTitle(item)); if (!meta.ok) { bucket.invalid.push({ item, link, meta }); return; } if (isLinkBookmarkOfficialFieldOverLimit(meta.rawMagnet, 'href') || isMagnetArchiveMagnetHrefOverLimit(meta.rawMagnet)) { bucket.invalid.push({ item, link, meta, reason: L.reason_magnet_archive_href_over_limit }); return; } const aliasKeys = getMagnetArchiveKeysFromMeta(meta); if (!aliasKeys.length) { bucket.invalid.push({ item, link, meta, reason: L.reason_magnet_archive_invalid }); return; } if (!bucket.links.some(x => getMagnetArchiveKeysFromMeta(x.meta).some(key => aliasKeys.includes(key)))) { const entry = makeMagnetArchiveBookmarkEntry(item, meta); if (entry) bucket.links.push({ item, meta, entry }); else bucket.invalid.push({ item, link, meta, reason: L.reason_magnet_archive_invalid }); } }; for (const item of selected) { getTraceableMagnetArchiveLinksFromResource(item).forEach(link => addLink(item, link)); const bucket = perItem.get(getMagnetArchiveItemId(item)); if (bucket && (bucket.links.length || bucket.invalid.length)) queueProgressDone(item); } await progressChain; const detailMissing = selected.filter(item => { const key = getMagnetArchiveItemId(item); const bucket = perItem.get(key); return bucket && bucket.links.length === 0 && !progressDoneKeys.has(key); }); const taskFallbackItems = []; if (detailMissing.length && typeof apiGet === 'function') { let cursor = 0; const workerCount = Math.min(6, detailMissing.length); const workers = Array.from({ length: workerCount }, async () => { while (cursor < detailMissing.length) { const item = detailMissing[cursor++]; const bucket = perItem.get(getMagnetArchiveItemId(item)); if (!bucket || bucket.links.length) { queueProgressDone(item); continue; } const ids = getMagnetArchiveDetailIds(item); let detailResponded = false; let detailTried = false; for (const id of ids) { detailTried = true; try { const detail = await apiGet(id); detailResponded = true; getTraceableMagnetArchiveLinksFromResource(detail).forEach(link => addLink(item, link)); const freshBucket = perItem.get(getMagnetArchiveItemId(item)); if (freshBucket && freshBucket.links.length) break; } catch (e) {} } if (detailResponded) queueProgressDone(item); else { if (detailTried) bucket.detailFailed = true; taskFallbackItems.push(item); } } }); await Promise.all(workers); await progressChain; } else { taskFallbackItems.push(...detailMissing); } const fallbackMissing = taskFallbackItems.filter(item => { const key = getMagnetArchiveItemId(item); const bucket = perItem.get(key); return bucket && bucket.links.length === 0 && !progressDoneKeys.has(key); }); if (fallbackMissing.length && typeof apiTaskList === 'function') { const idMap = new Map(); fallbackMissing.forEach(item => { [item.id, item.file_id, item.fileId, item.file && item.file.id].filter(Boolean).forEach(id => idMap.set(String(id), item)); }); if (idMap.size) { try { await apiTaskList(1000, batch => { const matched = new Map(); (batch || []).forEach(task => { const fileId = String(task && (task.file_id || task.fileId || task.file && task.file.id) || ''); const item = idMap.get(fileId); if (!item) return; getTraceableMagnetArchiveLinksFromResource(task).forEach(link => addLink(item, link)); matched.set(getMagnetArchiveItemId(item), item); }); if (matched.size) queueProgressDone(Array.from(matched.values())); }); } catch (e) { } await progressChain; } } const unresolved = selected.filter(item => !progressDoneKeys.has(getMagnetArchiveItemId(item))); if (unresolved.length) { queueProgressDone(unresolved); await progressChain; } if (typeof isLinkBookmarkWriteBusy === 'function' && isLinkBookmarkWriteBusy()) throw new Error(L.msg_magnet_archive_index_failed); await loadLinkBookmarkOfficial(true); if (S.linkBookmarkSyncStatus !== 'synced' || !S.linkBookmarkHasLoaded || !Array.isArray(S.linkBookmarkFolders)) throw new Error(L.msg_magnet_archive_index_failed); const folders = S.linkBookmarkFolders; const existingIndex = buildMagnetArchiveBookmarkIndex(folders); const seenBatch = new Set(); selected.forEach(item => { const bucket = perItem.get(getMagnetArchiveItemId(item)); if (!bucket || !bucket.links.length) { if (bucket && bucket.invalid.length) result.bad.push({ item, reason: bucket.invalid[0].reason || L.reason_magnet_archive_invalid, key: '', meta: bucket.invalid[0].meta }); else if (bucket && bucket.detailFailed) result.bad.push({ item, reason: L.reason_magnet_archive_detail_failed, key: '' }); else result.skip.push({ item, reason: L.reason_magnet_archive_no_magnet, key: '' }); return; } bucket.links.forEach(row => { const key = getMagnetArchivePrimaryKeyFromMeta(row.meta); const keys = getMagnetArchiveKeysFromMeta(row.meta); const existed = getMagnetArchiveIndexHit(existingIndex, keys); if (existed) result.dup.push({ item, reason: L.reason_magnet_archive_duplicate_existing, key, meta: row.meta, existed }); else if (hasMagnetArchiveKeyOverlap(keys, seenBatch)) result.dup.push({ item, reason: L.reason_magnet_archive_duplicate_batch, key, meta: row.meta }); else { addMagnetArchiveKeysToSet(keys, seenBatch); result.ok.push({ item, reason: L.reason_magnet_archive_ready, key, meta: row.meta, entry: row.entry }); } }); }); if (progressTask) updateMagnetArchiveCheckProgress(progressTask, progressTotal, progressTotal, L.msg_magnet_archive_checking); return result; } function updateMagnetArchiveOverflowTip(el) { if (!el) return; el.removeAttribute('title'); const archiveTitle = String(el.dataset.pkArchiveTitle || '').trim(); if (archiveTitle) { el.dataset.pkTip = archiveTitle; if (!el.dataset.pkTipHtml) el.dataset.pkTipHtml = esc(archiveTitle); return; } const text = String(el.textContent || '').trim(); if (text && el.scrollWidth > el.clientWidth + 1) el.dataset.pkTip = text; else delete el.dataset.pkTip; } function bindMagnetArchiveOverflowTips(root) { if (!root) return; const apply = () => root.querySelectorAll('.pk-magnet-archive-text').forEach(updateMagnetArchiveOverflowTip); requestAnimationFrame(apply); setTimeout(apply, 120); root.addEventListener('mouseover', e => { const el = e.target && e.target.closest ? e.target.closest('.pk-magnet-archive-text') : null; if (el && root.contains(el)) updateMagnetArchiveOverflowTip(el); }, true); } function getMagnetArchiveOfficialIconHtml(item) { const it = item && typeof item === 'object' ? item : {}; const isFolder = it.kind === 'drive#folder'; const mime = String(it.mime_type || it.mimeType || '').toLowerCase(); const isMedia = mime.startsWith('video/') || mime.startsWith('image/'); const thumb = String(it.thumbnail_link || it.thumbnailLink || '').trim(); const icon = String(it.icon_link || it.iconLink || '').trim(); const hasCover = thumb && thumb !== icon; const usableCover = hasCover && !isPreviewIconFailed(thumb); const fallbackSvg = getIcon(it).replace(/width="\d+"/g, 'width="24"').replace(/height="\d+"/g, 'height="24"'); const makePreviewImg = (src, fit, radius = '0') => { const safeSrc = String(src || '').replace(/"/g, '"'); return ``; }; if (!isFolder && isMedia && usableCover) { const secondFallback = icon && !isPreviewIconFailed(icon) ? `${makePreviewImg(icon, 'contain')}${fallbackSvg}` : fallbackSvg; return `${makePreviewImg(thumb, 'cover', '4px')}${secondFallback}`; } const iconSrc = icon || (!isFolder && usableCover ? thumb : ''); return iconSrc && !isPreviewIconFailed(iconSrc) ? `${makePreviewImg(iconSrc, 'contain')}${fallbackSvg}` : fallbackSvg; } function getMagnetArchiveTooltipCoverUrl(item) { const it = item && typeof item === 'object' ? item : {}; const thumb = String(it.thumbnail_link || it.thumbnailLink || '').trim(); const icon = String(it.icon_link || it.iconLink || '').trim(); if (!thumb || thumb === icon || isPreviewIconFailed(thumb)) return ''; return thumb; } function renderMagnetArchiveCheckRows(result) { const L = getStrings(); const rows = []; const push = (type, list, cls, label) => { (list || []).forEach(row => rows.push({ type, cls, label, item: row.item, key: row.key || row.meta && row.meta.primaryKey || '', reason: row.reason || '' })); }; push('ok', result.ok, 'ok', L.label_magnet_archive_can_archive); push('dup', result.dup, 'dup', L.label_magnet_archive_duplicate); push('skip', result.skip, 'skip', L.label_magnet_archive_skipped); push('bad', result.bad, 'bad', L.label_magnet_archive_invalid); const maxRows = Number(CONF.magnetArchivePreviewMaxRows) || 80; return rows.slice(0, maxRows).map(row => { const title = getMagnetArchiveItemTitle(row.item); const safeTitle = esc(title); const tipHtml = esc(safeTitle); const cover = getMagnetArchiveTooltipCoverUrl(row.item); const tipAttrs = `data-pk-archive-title="${safeTitle}" data-pk-tip="${safeTitle}" data-pk-tip-html="${tipHtml}"${cover ? ` data-pk-thumb="${esc(cover)}"` : ''}`; return `
${esc(row.label)}
${safeTitle}
${esc(row.key || row.reason)}
`; }).join('') + (rows.length > maxRows ? `
...
${esc(L.label_more)}
${esc(String(rows.length - maxRows))}
` : ''); } function showMagnetArchiveCheckResultModal(result) { const L = getStrings(); const ok = result.ok.length; const dup = result.dup.length; const skip = result.skip.length; const bad = result.bad.length; const rowsHtml = renderMagnetArchiveCheckRows(result) || `
${esc(L.label_magnet_archive_skipped)}
${esc(L.msg_magnet_archive_check_no_selected)}
`; const dupDeleteCount = getMagnetArchiveDuplicateDeleteRows(result).length; const showDupDeleteBtn = !ok && dupDeleteCount > 0; const m = showModal(`

${esc(L.title_magnet_archive_check)}

${ok}${esc(L.label_magnet_archive_can_archive)}
${dup}${esc(L.label_magnet_archive_duplicate)}
${skip}${esc(L.label_magnet_archive_skipped)}
${bad}${esc(L.label_magnet_archive_invalid)}
${esc(L.label_magnet_archive_reason)}
${esc(L.label_magnet_archive_source)}
${esc(L.label_magnet_archive_key)}
${rowsHtml}
${showDupDeleteBtn ? `` : ''}${ok ? `` : ''}
`); bindMagnetArchiveOverflowTips(m); bindPreviewIconFallback(m); const box = m.querySelector('.pk-modal'); if (box) Object.assign(box.style, { width: '820px', padding: '24px', boxSizing: 'border-box' }); const btn = m.querySelector('#pk_magnet_archive_preview_ok'); if (btn) btn.onclick = () => m.remove(); const writeBtn = m.querySelector('#pk_magnet_archive_write'); const dupDeleteBtn = m.querySelector('#pk_magnet_archive_delete_dup_source'); let dupDeleteDone = false; m.tabIndex = 0; setTimeout(() => { if (document.contains(m)) m.focus(); }, 10); m.addEventListener('keydown', e => { const tag = String(e.target && e.target.tagName || '').toLowerCase(); if (e.target && (e.target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select')) return; if (e.key === 'Enter' && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); e.stopPropagation(); const enterBtn = [writeBtn, dupDeleteBtn, btn].find(x => x && !x.disabled); enterBtn?.click(); } }, true); if (dupDeleteBtn) { dupDeleteBtn.onclick = async () => { dupDeleteBtn.disabled = true; dupDeleteBtn.style.cursor = 'not-allowed'; if (writeBtn) { writeBtn.disabled = true; writeBtn.style.cursor = 'not-allowed'; } const failures = []; const okDelete = await handleMagnetArchiveDeleteDuplicateSources(result, failures); if (failures.length) showMagnetArchiveFailureModal(failures); if (okDelete) { dupDeleteDone = true; dupDeleteBtn.textContent = L.msg_magnet_archive_delete_success.replace('{n}', dupDeleteCount); dupDeleteBtn.style.opacity = '0.65'; dupDeleteBtn.style.cursor = 'not-allowed'; if (writeBtn) { writeBtn.disabled = false; writeBtn.style.cursor = 'pointer'; } else { m.remove(); } } else { dupDeleteBtn.disabled = false; dupDeleteBtn.style.cursor = 'pointer'; if (writeBtn) { writeBtn.disabled = false; writeBtn.style.cursor = 'pointer'; } } }; } if (writeBtn) { writeBtn.onclick = async () => { writeBtn.disabled = true; writeBtn.style.cursor = 'not-allowed'; if (dupDeleteBtn) { dupDeleteBtn.disabled = true; dupDeleteBtn.style.cursor = 'not-allowed'; } const okWrite = await writeMagnetArchiveCheckResultToBookmarks(result); if (okWrite) m.remove(); else { writeBtn.disabled = false; writeBtn.style.cursor = 'pointer'; if (dupDeleteBtn && !dupDeleteDone) { dupDeleteBtn.disabled = false; dupDeleteBtn.style.cursor = 'pointer'; } } }; } } async function handleMagnetArchiveCheckSelected() { const L = getStrings(); if (S.magnetArchiveBusy) return; const selected = getMagnetBackupSelectedItems(); if (!selected.length) { showToast(L.msg_magnet_archive_check_no_selected, 'warning'); return; } const maxSelected = Math.max(1, Number(CONF.magnetArchiveMaxSelected) || 50); if (selected.length > maxSelected) { showToast((L.msg_magnet_archive_check_over_limit || '').replace('{max}', maxSelected), 'warning'); return; } let task = null; S.magnetArchiveBusy = true; try { task = createMagnetArchiveCheckProgress(L.msg_magnet_archive_checking); const result = await collectSelectedMagnetArchiveCheckResult(task); closeMagnetArchiveCheckProgress(task); task = null; showMagnetArchiveCheckResultModal(result); } catch (e) { if (task) closeMagnetArchiveCheckProgress(task); showToast((L.str_error) + ': ' + (e && e.message || e), 'error'); } finally { S.magnetArchiveBusy = false; } } const initClipboardMagnetFocusWatcher = () => { if (window.__pkClipboardMagnetFocusWatcherBound) return; window.__pkClipboardMagnetFocusWatcherBound = true; const state = { lastCheckAt: 0, lastDeniedAt: 0, lastPromptAt: 0, lastSignature: '', prompting: false, processing: false, ignored: new Map(), queue: [], queued: new Set(), previewCache: new Map(), previewCircuitUntil: 0 }; const getClipText = () => getStrings(); const normalizeHash = (link) => { const match = String(link || '').match(/urn:btih:([^&]+)/i); return match ? match[1].toUpperCase() : String(link || '').trim().toLowerCase(); }; const isIgnoredSignature = (signature) => { const until = state.ignored.get(signature) || 0; if (!until) return false; if (Date.now() < until) return true; state.ignored.delete(signature); return false; }; const markIgnoredSignature = (signature) => { state.ignored.set(signature, Date.now() + CONF.clipboardMagnetIgnoreTTL); }; const getDefaultMagnetSaveTarget = () => { const curFolder = S.path[S.path.length - 1] || { id: '', name: L.lbl_default_folder }; const curId = curFolder.id || ''; const isVirtual = curId.startsWith('virtual_') || curId.includes('_root') || curId === 'analyze_root'; const isHomeSubDir = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.isFlattened && !S.dupMode && S.path.length > 1 && !isVirtual; return { id: isHomeSubDir ? curId : '', name: isHomeSubDir ? (curFolder.name || L.lbl_default_folder) : L.lbl_default_folder, path: isHomeSubDir ? S.path.filter(p => !p.id.startsWith('virtual_')) : null }; }; const createMagnetCloudTasks = (links, targetId) => submitCloudLinks(links, { targetId: targetId || '', skipSnapshot: true, logPrefix: 'Magnet Task Create Failed' }); const openCloudTaskModalWithClipboardMagnets = (links) => { const list = (Array.isArray(links) ? links : []).map(v => String(v || '').trim()).filter(v => /^magnet:\?/i.test(v)); if (!list.length) return false; const text = list.join('\n'); const apply = () => { const input = document.querySelector('#pk_cloud_input'); if (!input) return false; input.value = text; input.dispatchEvent(new Event('input', { bubbles: true })); try { input.focus(); input.setSelectionRange(input.value.length, input.value.length); } catch (e) {} return true; }; if (apply()) return true; if (typeof btnCloud !== 'undefined' && btnCloud && typeof btnCloud.click === 'function') { btnCloud.click(); setTimeout(apply, 0); setTimeout(apply, 80); return true; } return false; }; window.__pkSubmitMagnetWithPreview = async (link, options = {}) => { const L = getStrings(); const cleanLink = String(link || '').trim(); if (!cleanLink) return { successCount: 0, failCount: 0, cancelled: false }; const preview = await requestMagnetPreview(cleanLink); const result = await showMagnetPreviewModal([cleanLink], preview); if (!result || !result.confirm) return { successCount: 0, failCount: 0, cancelled: true }; return submitCloudLinks([cleanLink], { targetId: result.targetId || '', targetName: result.targetName || L.lbl_default_folder, skipSnapshot: true, logPrefix: options.logPrefix || 'Magnet Task Create Failed', successMessage: options.successMessage || L.msg_cloud_task_success }); }; const extractMagnetLinks = (rawText) => { const text = String(rawText || '').trim(); if (!text || text.length > CONF.clipboardMagnetMaxChars) return []; const unique = new Map(); const addMagnet = (value) => { let link = String(value || '').trim(); if (!link) return; const urnOnly = link.match(/^urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})(?:[^\s"'<>`]*)?$/i); if (urnOnly) link = `magnet:?xt=urn:btih:${urnOnly[1].toUpperCase()}`; const pureHash = link.match(/^([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})$/i); if (pureHash) link = `magnet:?xt=urn:btih:${pureHash[1].toUpperCase()}`; if (!/^magnet:\?/i.test(link)) return; const hashMatch = link.match(/[?&]xt=urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})/i); if (!hashMatch) return; const signature = hashMatch[1].toUpperCase(); if (!unique.has(signature)) unique.set(signature, link); }; if (!/(magnet:\?|urn:btih:)/i.test(text)) { const lines = text.split(/\r?\n/).map(x => x.trim()).filter(Boolean); if (lines.length === 1) addMagnet(lines[0]); return Array.from(unique.values()); } const magnetRegex = /magnet:\?[^\s"'<>`]+/gi; let match; while ((match = magnetRegex.exec(text))) addMagnet(match[0]); const urnRegex = /urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})(?:[^\s"'<>`]*)?/gi; while ((match = urnRegex.exec(text))) addMagnet(match[0]); return Array.from(unique.values()); }; const requestMagnetPreview = (link) => { return new Promise((resolve) => { const hash = normalizeHash(link); const now = Date.now(); const cached = state.previewCache.get(hash); if (cached && now - cached.at < cached.ttl) { resolve(cached.data); return; } const finish = (data) => { const ttl = data && data.ok ? CONF.magnetPreviewCacheTTL : CONF.magnetPreviewErrorCacheTTL; state.previewCache.set(hash, { at: Date.now(), ttl, data }); resolve(data); }; if (state.previewCircuitUntil && now < state.previewCircuitUntil) { finish({ ok: false, code: 'rate_limited' }); return; } const url = `${CONF.magnetPreviewApi}?url=${encodeURIComponent(link)}`; const handleStatus = (status) => { if (status === 429) { state.previewCircuitUntil = Date.now() + CONF.magnetPreviewCircuitTTL; finish({ ok: false, code: 'rate_limited', status }); return true; } if (status >= 500) { state.previewCircuitUntil = Date.now() + CONF.magnetPreviewCircuitTTL; finish({ ok: false, code: 'network', status }); return true; } if (status && (status < 200 || status >= 300)) { finish({ ok: false, code: 'network', status }); return true; } return false; }; if (typeof GM_xmlhttpRequest !== 'function') { fetch(url, { cache: 'no-store' }).then(r => { if (handleStatus(r.status)) return null; return r.json(); }).then(data => { if (data) finish({ ok: true, code: 'ok', data }); }).catch(() => finish({ ok: false, code: 'network' })); return; } GM_xmlhttpRequest({ method: 'GET', url, responseType: 'json', timeout: CONF.magnetPreviewTimeout, onload: (res) => { if (handleStatus(res.status)) return; try { const data = res.response && typeof res.response === 'object' ? res.response : JSON.parse(res.responseText || '{}'); finish({ ok: true, code: 'ok', data }); } catch (e) { finish({ ok: false, code: 'network' }); } }, onerror: () => finish({ ok: false, code: 'network' }), ontimeout: () => finish({ ok: false, code: 'timeout' }) }); }); }; const showMagnetPreviewModal = (links, preview) => { return new Promise((resolve) => { const TXT = getClipText(); const data = preview && preview.ok && preview.data ? preview.data : {}; let saveTarget = getDefaultMagnetSaveTarget(); const toWhatslinkImageUrl = (value) => { let url = String(value || '').trim(); if (!url) return ''; if (/^\/\//.test(url)) url = `https:${url}`; else if (/^\//.test(url)) url = `https://whatslink.info${url}`; else if (!/^https?:\/\//i.test(url) && /\.(webp|png|jpe?g|gif)(\?|#|$)/i.test(url)) url = `https://whatslink.info/${url.replace(/^\.?\//, '')}`; return /^https?:\/\//i.test(url) ? url : ''; }; const pickShot = (item) => { if (!item) return null; if (typeof item === 'string') { const src = toWhatslinkImageUrl(item); return src ? { src, time: 0 } : null; } if (Array.isArray(item)) { for (const sub of item) { const shot = pickShot(sub); if (shot) return shot; } return null; } if (typeof item === 'object') { const officialSrc = toWhatslinkImageUrl(item.screenshot); if (officialSrc) return { src: officialSrc, time: Number(item.time || 0) || 0 }; const keys = ['url', 'src', 'image', 'img', 'thumbnail', 'thumb', 'preview', 'poster', 'file', 'path']; for (const key of keys) { const shot = pickShot(item[key]); if (shot) return { src: shot.src, time: Number(item.time || shot.time || 0) || 0 }; } for (const val of Object.values(item)) { const shot = pickShot(val); if (shot) return { src: shot.src, time: Number(item.time || shot.time || 0) || 0 }; } } return null; }; const collectShots = (source) => { const list = []; const seen = new Set(); const push = (value) => { const shot = pickShot(value); if (shot && shot.src && !seen.has(shot.src)) { seen.add(shot.src); list.push(shot); } }; if (Array.isArray(source.screenshots)) source.screenshots.forEach(push); if (source.screenshot) push({ screenshot: source.screenshot, time: source.time }); if (Array.isArray(source.thumbnails)) source.thumbnails.forEach(push); if (Array.isArray(source.images)) source.images.forEach(push); if (Array.isArray(source.files)) source.files.forEach(file => { if (file && (file.screenshots || file.screenshot || file.thumbnail || file.thumb || file.preview || file.poster || file.image || file.url || file.path)) push(file); }); return list.slice(0, CONF.magnetPreviewMaxShots); }; const fmtShotTime = (seconds) => { const total = Math.floor(Number(seconds || 0)); if (!Number.isFinite(total) || total <= 0) return ''; const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` : `${m}:${String(s).padStart(2, '0')}`; }; const shots = collectShots(data); const name = data.name || TXT.str_magnet_unknown_name; const size = Number(data.size || 0) > 0 ? fmtSize(data.size) : TXT.str_magnet_unknown_name; const count = Number(data.count || 0) > 0 ? String(data.count) : String(links.length); const type = data.file_type || data.type || TXT.str_magnet_unknown_name; const hashText = normalizeHash(links[0]); const heroHtml = `
${esc(TXT.str_no_preview)}
`; const shotsHtml = shots.length > 1 ? `
${shots.map((shot, idx) => `
${fmtShotTime(shot.time) ? `${esc(fmtShotTime(shot.time))}` : ''}
`).join('')}
` : ''; const getPreviewFailText = () => { if (preview && preview.ok) return ''; if (preview && preview.code === 'rate_limited') return TXT.msg_magnet_preview_rate_limited; if (preview && preview.code === 'timeout') return TXT.msg_magnet_preview_timeout; if (preview && preview.code === 'network') return TXT.msg_magnet_preview_network; return TXT.msg_magnet_preview_fail; }; const failText = getPreviewFailText(); const warnHtml = failText ? `
${esc(failText)}
` : ''; const m = showModal(`
${heroHtml}
${esc(name)}
${esc(TXT.msg_magnet_preview_desc)}
${warnHtml} ${shotsHtml}
${esc(TXT.lbl_magnet_count)}
${esc(count)}
${esc(TXT.lbl_magnet_size)}
${esc(size)}
${esc(TXT.lbl_magnet_type)}
${esc(type)}
${esc(TXT.lbl_magnet_hash)}:${esc(hashText)}
${esc(L.lbl_save_to)} ${getOfficialFolderFallbackIconHtml(16)} ${esc(saveTarget.name)} ${L.tip_cloud_save_path ? `${CONF.icons.help}` : ''} ${esc(L.btn_modify)}
${esc(TXT.lbl_magnet_preview_source)} whatslink.info
`); const box = m.querySelector('.pk-modal'); if (box) { box.style.width = '420px'; box.style.padding = '0'; box.style.borderRadius = '16px'; } const heroBox = m.querySelector('.pk-magnet-hero'); let heroShotIndex = -1; let heroReady = false; const failedHeroShots = new Set(); const getShotThumb = (idx) => m.querySelector(`.pk-magnet-thumb-wrap[data-shot-index="${idx}"]`); const getThumbSrc = (thumb) => thumb?.dataset?.shotSrc || thumb?.querySelector('img')?.getAttribute('src') || ''; const showHeroEmpty = () => { if (!heroBox) return; heroReady = false; heroBox.innerHTML = `
${esc(TXT.str_no_preview)}
`; }; const bindHeroImg = (img) => { if (!img) return; img.addEventListener('dragstart', e => e.preventDefault()); img.onload = () => { heroReady = true; }; img.onerror = () => { const idx = Number(img.dataset.shotIndex); if (Number.isFinite(idx) && idx >= 0) failedHeroShots.add(idx); heroReady = false; tryNextHeroShot(); }; }; const setHeroShot = (src, thumb, idx) => { if (!heroBox || !src) return; const explicitIdx = Number(idx); const thumbIdx = Number(thumb?.dataset?.shotIndex); const shotIdx = Number.isFinite(explicitIdx) && explicitIdx >= 0 ? explicitIdx : (Number.isFinite(thumbIdx) && thumbIdx >= 0 ? thumbIdx : shots.findIndex(shot => shot && shot.src === src)); heroShotIndex = shotIdx; heroReady = false; const img = document.createElement('img'); img.alt = ''; img.draggable = false; img.referrerPolicy = 'no-referrer'; if (shotIdx >= 0) img.dataset.shotIndex = String(shotIdx); bindHeroImg(img); heroBox.replaceChildren(img); img.src = src; m.querySelectorAll('.pk-magnet-thumb-wrap').forEach(x => x.classList.remove('active')); const activeThumb = thumb || getShotThumb(shotIdx); if (activeThumb) activeThumb.classList.add('active'); }; const tryNextHeroShot = () => { if (!shots.length) { showHeroEmpty(); return false; } for (let i = Math.max(heroShotIndex + 1, 0); i < shots.length; i++) { const shot = shots[i]; if (shot && shot.src && !failedHeroShots.has(i)) { setHeroShot(shot.src, getShotThumb(i), i); return true; } } for (let i = 0; i < shots.length; i++) { const shot = shots[i]; if (shot && shot.src && !failedHeroShots.has(i)) { setHeroShot(shot.src, getShotThumb(i), i); return true; } } showHeroEmpty(); return false; }; const heroNeedsFallback = () => { if (!heroBox) return false; const heroImg = heroBox.querySelector('img'); if (!heroImg) return true; if (heroBox.querySelector('.pk-magnet-empty')) return true; if (heroImg.complete && heroImg.naturalWidth === 0) { const idx = Number(heroImg.dataset.shotIndex); if (Number.isFinite(idx) && idx >= 0) failedHeroShots.add(idx); return true; } return false; }; const ensureHeroShot = () => { if (!heroBox || !heroNeedsFallback()) return; const thumbs = Array.from(m.querySelectorAll('.pk-magnet-thumb-wrap')); const usableThumb = thumbs.find(thumb => { const img = thumb.querySelector('img'); return !img || !img.complete || img.naturalWidth > 0; }); if (usableThumb) { setHeroShot(getThumbSrc(usableThumb), usableThumb); return; } tryNextHeroShot(); }; m.querySelectorAll('.pk-magnet-thumb-wrap').forEach(thumb => { const img = thumb.querySelector('img'); thumb.addEventListener('dragstart', e => e.preventDefault()); thumb.addEventListener('click', () => setHeroShot(getThumbSrc(thumb), thumb)); if (img) { img.addEventListener('error', () => { const wasActive = thumb.classList.contains('active'); const idx = Number(thumb.dataset.shotIndex); if (Number.isFinite(idx) && idx >= 0) failedHeroShots.add(idx); thumb.remove(); if (wasActive && !heroReady) ensureHeroShot(); }); } }); ensureHeroShot(); [80, 300, 900].forEach(ms => setTimeout(ensureHeroShot, ms)); const saveNameEl = m.querySelector('#pk_magnet_save_name'); const changeDirEl = m.querySelector('#pk_magnet_change_dir'); if (changeDirEl) { changeDirEl.onclick = () => { showFolderSelector(saveTarget.id, (id, name, fullItem, selectedPathChain) => { saveTarget.id = id || ''; saveTarget.name = name || L.lbl_default_folder; saveTarget.path = selectedPathChain || null; if (saveNameEl) saveNameEl.textContent = saveTarget.name; }, saveTarget.path); }; } const close = () => { m.remove(); resolve({ confirm: false }); }; m.querySelector('.pk-modal-close').onclick = close; m.querySelector('#pk_magnet_cancel').onclick = close; m.querySelector('#pk_magnet_continue').onclick = () => { m.remove(); resolve({ confirm: true, targetId: saveTarget.id, targetName: saveTarget.name }); }; }); }; const processMagnetQueue = async () => { if (state.processing) return; state.processing = true; try { while (state.queue.length > 0) { const task = state.queue.shift(); if (!task || !task.link || !task.signature) continue; if (isIgnoredSignature(task.signature)) { state.queued.delete(task.signature); continue; } if (state.lastSignature === task.signature && Date.now() - state.lastPromptAt < CONF.clipboardMagnetPromptGap) { state.queued.delete(task.signature); continue; } state.prompting = true; state.lastSignature = task.signature; state.lastPromptAt = Date.now(); let result = null; try { const preview = await requestMagnetPreview(task.link); result = await showMagnetPreviewModal([task.link], preview); } catch (e) { console.error('Magnet preview queue failed:', e); result = { confirm: false }; } finally { state.prompting = false; } if (!result || !result.confirm) { markIgnoredSignature(task.signature); state.queued.delete(task.signature); continue; } try { await createMagnetCloudTasks([task.link], result.targetId || ''); } catch (e) { console.error('Magnet cloud task failed:', e); if (L.str_action_failed) showToast(L.str_action_failed, 'error'); } state.queued.delete(task.signature); await sleep(80); } } finally { state.processing = false; if (state.queue.length > 0) processMagnetQueue(); } }; const handleMagnetLinks = (links) => { if (!links || links.length === 0) return; const now = Date.now(); const maxPreview = Math.max(1, Number(CONF.clipboardMagnetPreviewQueueMax) || 3); const candidates = []; links.forEach(link => { const cleanLink = String(link || '').trim(); if (!/^magnet:\?/i.test(cleanLink)) return; const signature = normalizeHash(cleanLink); if (!signature) return; if (isIgnoredSignature(signature)) return; if (state.queued.has(signature)) return; if (state.lastSignature === signature && now - state.lastPromptAt < CONF.clipboardMagnetPromptGap) return; candidates.push({ link: cleanLink, signature }); }); if (!candidates.length) return; if (candidates.length > maxPreview) { if (openCloudTaskModalWithClipboardMagnets(candidates.map(item => item.link))) { candidates.forEach(item => { markIgnoredSignature(item.signature); state.queued.delete(item.signature); }); state.queue = state.queue.filter(item => !candidates.some(candidate => candidate.signature === item.signature)); } return; } let added = false; const activeCount = state.queue.length + (state.prompting ? 1 : 0); const room = Math.max(0, maxPreview - activeCount); candidates.slice(0, room).forEach(item => { state.queue.push(item); state.queued.add(item.signature); added = true; }); if (added) processMagnetQueue(); }; const handleRawText = (rawText) => { const magnetLinks = extractMagnetLinks(rawText); if (magnetLinks.length === 0) return; const maxPreview = Math.max(1, Number(CONF.clipboardMagnetPreviewQueueMax) || 3); const bulkMap = new Map(); magnetLinks.forEach(link => { const cleanLink = String(link || '').trim(); if (!/^magnet:\?/i.test(cleanLink)) return; const signature = normalizeHash(cleanLink); if (!signature || isIgnoredSignature(signature)) return; if (!bulkMap.has(signature)) bulkMap.set(signature, cleanLink); }); const bulkLinks = Array.from(bulkMap.values()); if (bulkLinks.length === 0) return; if (bulkLinks.length > maxPreview) { const signatures = new Set(bulkMap.keys()); if (openCloudTaskModalWithClipboardMagnets(bulkLinks)) { signatures.forEach(signature => { markIgnoredSignature(signature); state.queued.delete(signature); }); state.queue = state.queue.filter(item => !signatures.has(item.signature)); } return; } handleMagnetLinks(bulkLinks); }; const shouldSkipFocusCheck = () => { if (gmGet('pk_clipboard_magnet_focus', true) !== true) return true; if (document.hidden) return true; if (!navigator.clipboard || typeof navigator.clipboard.readText !== 'function') return true; if (document.querySelector('#pk_cloud_input')) return true; if (document.querySelector('.pk-modal-ov') && !state.processing && !state.prompting) return true; const now = Date.now(); if (now - state.lastCheckAt < CONF.clipboardMagnetFocusCooldown) return true; if (now - state.lastDeniedAt < CONF.clipboardMagnetDenyCooldown) return true; return false; }; const checkClipboardMagnet = async () => { if (shouldSkipFocusCheck()) return; state.lastCheckAt = Date.now(); let rawText = ''; try { rawText = await navigator.clipboard.readText(); } catch (e) { state.lastDeniedAt = Date.now(); return; } handleRawText(rawText); }; const scheduleClipboardMagnetCheck = () => { if (window.__pkClipboardMagnetFocusTimer) clearTimeout(window.__pkClipboardMagnetFocusTimer); window.__pkClipboardMagnetFocusTimer = setTimeout(checkClipboardMagnet, 350); }; document.addEventListener('paste', (e) => { if (gmGet('pk_clipboard_magnet_paste', CONF.clipboardMagnetPaste) === false) return; if (document.querySelector('#pk_cloud_input')) return; const active = document.activeElement; if (active && (active.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(active.tagName))) return; const rawText = e.clipboardData ? e.clipboardData.getData('text/plain') : ''; handleRawText(rawText); }, true); document.addEventListener('visibilitychange', () => { if (!document.hidden) scheduleClipboardMagnetCheck(); }); window.addEventListener('focus', scheduleClipboardMagnetCheck); window.addEventListener('pageshow', scheduleClipboardMagnetCheck); }; initClipboardMagnetFocusWatcher(); if (btnCloud) { btnCloud.onclick = () => { const curFolder = S.path[S.path.length - 1]; const isVirtual = curFolder.id.startsWith('virtual_') || curFolder.id.includes('_root') || curFolder.id === 'analyze_root'; const isHomeSubDir = !S.trashMode && !S.shareMode && !S.offlineMode && !S.starredMode && !S.recentMode && !S.isFlattened && !S.dupMode && S.path.length > 1 && !isVirtual; let saveToId = isHomeSubDir ? (curFolder.id || '') : ''; let saveToName = isHomeSubDir ? (curFolder.name || L.lbl_default_folder) : L.lbl_default_folder; let currentSavePath = isHomeSubDir ? S.path.filter(p => !p.id.startsWith('virtual_')) : null; const m = showModal(`

${L.title_cloud_task}

${L.lbl_save_to}
${getOfficialFolderFallbackIconHtml(20)}
${saveToName}
${L.btn_modify}
${L.btn_via_torrent}
`); const modalBox = m.querySelector('.pk-modal'); modalBox.style.width = "560px"; modalBox.style.padding = "30px"; const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) { closeBtn.style.top = "36px"; closeBtn.style.right = "30px"; } const input = m.querySelector('#pk_cloud_input'); const submit = m.querySelector('#cloud_submit'); const dirLabel = m.querySelector('#pk_cloud_dir_name'); m.querySelector('#pk_cloud_change_dir').onclick = () => { showFolderSelector(saveToId, (id, name, fullItem, selectedPathChain) => { saveToId = id; saveToName = name; dirLabel.textContent = name; if (selectedPathChain) { currentSavePath = selectedPathChain; } }, currentSavePath); }; const smartFixCheckbox = m.querySelector('#pk_cloud_smart_fix'); if (smartFixCheckbox) smartFixCheckbox.onchange = () => input.dispatchEvent(new Event('input')); const parseAndCleanLinks = parseCloudLinks; input.oninput = () => { const rawVal = input.value.trim(); const isSmartFix = smartFixCheckbox ? smartFixCheckbox.checked : false; const linesRaw = rawVal.split('\n').map(l => l.trim()).filter(l => l); const hasInput = linesRaw.length > 0; const finalLinks = parseAndCleanLinks(rawVal, isSmartFix); const allValid = hasInput && (finalLinks.length === linesRaw.length || finalLinks.length > 0); const isError = hasInput && !allValid; m.querySelector('#pk_cloud_error').style.display = isError ? 'block' : 'none'; submit.disabled = !allValid; }; 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(); 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; let addExistsCount = 0; const addResults = []; let lastFailMessage = ''; 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 { const created = await apiAddOfflineTask(magnetLink, saveToId); addResults.push(created); successCount++; break; } catch (reqErr) { if (isOfflineTaskAlreadyExistsError(reqErr)) { addExistsCount++; successCount++; break; } 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++; if (e && e.message) lastFailMessage = e.message; } } fb.destroy(); if (failCount > 0) { const failSummary = L.msg_cloud_task_finish.replace('{s}', successCount).replace('{f}', failCount); showToast(lastFailMessage ? `${failSummary} / ${lastFailMessage}` : failSummary, '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) { await handleOfflineTaskAdded(addResults, { source: files.length > 1 ? 'batch' : 'single', batch: files.length > 1, schedule: false }); scheduleOfflineTaskAddProbe(addExistsCount && !addResults.length ? 'add_exists' : (files.length > 1 ? 'batch_add' : 'add'), 1000); } else if (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; const finalLinks = parseAndCleanLinks(rawVal, isSmartFix); await submitCloudLinks(finalLinks, { targetId: saveToId, targetName: saveToName, currentPath: currentSavePath, sourceModal: m, removeSourceModal: true, onTargetChange: (id, name, fullItem, selectedPathChain) => { saveToId = id; saveToName = name; if (dirLabel) dirLabel.textContent = name; if (selectedPathChain) currentSavePath = selectedPathChain; } }); }; }; } 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.btnNavShareParse) UI.btnNavShareParse.onclick = () => switchTab('shareParse'); if(UI.btnNavLinkBookmark) UI.btnNavLinkBookmark.onclick = () => switchTab('linkBookmark'); if (UI.btnNavOffline) { UI.btnNavOffline.onclick = () => { switchTab('offline'); }; } UI.btnNavTrash.onclick = () => switchTab('trash'); if (UI.btnTrashRefresh) { UI.btnTrashRefresh.onclick = () => { updateQuotaUI(); load(false, true); }; } UI.btnRestore.onclick = async () => { const ids = S.getSelectedIds(); if (ids.length === 0) return; ensureItemMap(); const progressTask = FloatBarManager.create(L.msg_prepare_restore); const updateFloat = progressTask.update; isGUISensitive = true; S.clearSelection(); try { const BATCH_SIZE = 500; const total = ids.length; const taskIds =[]; const affectedParentIds = new Set(); const restoredFolders = []; ids.forEach(id => { const item = S.itemMap.get(id); if (item) { if (item.kind === 'drive#folder') restoredFolders.push(item); if (item.parent_id) affectedParentIds.add(item.parent_id); else affectedParentIds.add('root'); } }); updateFloat(L.msg_submit_request); 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); await sleep(50); } if (taskIds.length > 0) { updateFloat(L.msg_wait_server); 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); } }); updateFloat(L.msg_server_processing); } } 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 }); }); scheduleBackgroundResume(); 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; scheduleBackgroundResume(); } }; UI.btnDelForever.onclick = async () => { const ids = S.getSelectedIds(); const count = ids.length; if (count === 0) return; ensureItemMap(); if (!await showConfirm(L.msg_del_forever_confirm.replace('{n}', count))) return; await executeBatchDelete(ids, { hardDelete: true, silent: false }); }; UI.btnEmptyTrash.onclick = async () => { if (!await showConfirm(L.msg_empty_trash_confirm)) return; if (S.items.length === 0) return; S._isEmptyingTrash = true; setLoad(true); updateLoadTxt(L.str_deleting); try { const res = await fetch('https://api-drive.mypikpak.com/drive/v1/files/trash:empty', { method: 'PATCH', headers: getHeaders() }); if (!res.ok) throw new Error(`API Error ${res.status}`); S.items = []; S.display = []; S.itemMap.clear(); S.clearSelection(); if (typeof globalCache !== 'undefined') globalCache.delete('root_trashed'); if (S.cache) S.cache.delete('root_trashed'); refresh(); updateStat(); showToast(L.msg_trash_emptied); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); S._isEmptyingTrash = false; } }; const openSettingsModal = async () => { if (openSettingsModal._active) return; openSettingsModal._active = true; 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 calcLocalforageStoreSize = async (name, storeName) => { if (!window.localforage) return 0; try { const store = window.localforage.createInstance({ name, storeName }); let total = 0; await store.iterate((v, k) => { total += String(k || '').length; if (v instanceof Blob) total += v.size; else if (v instanceof ArrayBuffer) total += v.byteLength; else if (ArrayBuffer.isView(v)) total += v.byteLength; else { try { total += JSON.stringify(v || '').length; } catch (e) {} } }); return total; } catch (e) { return 0; } }; const calcLocalDataStats = async () => { const gmKeys = typeof GM_listValues !== 'undefined' ? GM_listValues() : []; const lsKeys = Object.keys(localStorage).filter(k => typeof k === 'string' && k.startsWith('pk_')); const keys = Array.from(new Set([...(Array.isArray(gmKeys) ? gmKeys : []), ...lsKeys])); const sizes = { index: 0, pref: 0, rules: 0, vault: 0, history: 0, shareParseHistory: 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){} } } sizes.cache += await calcLocalforageStoreSize('pk_thumbs', 'snapshots'); const getCat = (k) => { if (!k.startsWith('pk_')) return null; if (k === SHARE_PARSE_HISTORY_KEY) return 'shareParseHistory'; if (k.startsWith('pk_archive_pwd_') || k === 'pk_pwd_vault' || k === 'pk_pwd_try_count') return 'vault'; if (k.startsWith('pk_duration_')) return 'history'; const cacheKeys = ['pk_captured_captcha', 'pk_i18n_manifest', 'pk_script_update_cache', 'pk_potplayer_launch_state', 'pk_potplayer_protocol_state', 'pk_ghost_files', 'pk_migration_stub', 'pk_cfg_sync_last_remote_hash', 'pk_cfg_sync_last_local_hash', 'pk_cfg_sync_last_sync_at', 'pk_cfg_sync_last_report']; if (k.startsWith('pk_fmod_') || k.startsWith('pk_i18n_') || k.startsWith(CONF.scriptUpdateDismissPrefix) || cacheKeys.includes(k)) return 'cache'; const ruleKeys =['pk_blacklist', 'pk_blacklist_folders', 'pk_downloader_type', 'pk_downloader_prefer_original_video_link', 'pk_idm_export_mode', 'pk_idm_url_export_type', 'pk_idm_exe_path', 'pk_idm_bat_root', 'pk_idm_bat_keep_structure', 'pk_idm_bat_add_queue', 'pk_idm_bat_start_queue', 'pk_aria2_url', 'pk_aria2_token', 'pk_aria2_dir', 'pk_aria2_keep_structure', 'pk_gopeed_url', 'pk_gopeed_token', 'pk_gopeed_dir', 'pk_gopeed_keep_structure', 'pk_abdm_url', 'pk_abdm_dir', 'pk_download_accel_enable', 'pk_download_accel_domain', 'pk_download_accel_mode', 'pk_download_accel_query_param', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist', 'pk_skip_bl_on_del', 'pk_potplayer_custom_path']; 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) { let val = typeof GM_getValue !== 'undefined' ? GM_getValue(k) : undefined; if (val === undefined || val === null) val = localStorage.getItem(k); sizes[cat] += (k.length + (val ? JSON.stringify(val).length : 0)); } }); const total = Object.values(sizes).reduce((sum, n) => sum + n, 0); return { keys, sizes, getCat, total }; }; const curLang = gmGet('pk_lang', lang); const curEngine = gmGet('pk_search_engine', 'google'); const curDefaultVideoQuality = getDefaultVideoQualityPref(); const curDefaultOpenPlayer = getDefaultOpenPlayerPref(); const curVideoLoadProgressCache = shouldLoadVideoProgressCache(); const curDownloaderType = getCurrentDownloaderType(); const curDownloaderPreferOriginalVideoLink = getBoolPref('pk_downloader_prefer_original_video_link', CONF.downloaderPreferOriginalVideoLink); const curAriaUrl = gmGet('pk_aria2_url', ''); const curAriaToken = gmGet('pk_aria2_token', ''); const curAriaDir = normalizeAriaDownloadDir(gmGet('pk_aria2_dir', CONF.aria2DownloadDir)); const curDownloaderKeepStructure = getDownloaderKeepStructurePref(); const curAriaKeepStructure = curDownloaderKeepStructure; const curGopeedUrl = getStoredGopeedUrlForSettings(); const curGopeedToken = gmGet('pk_gopeed_token', ''); const curGopeedDir = normalizeGopeedDownloadDir(gmGet('pk_gopeed_dir', CONF.gopeedDownloadDir)); const curGopeedKeepStructure = curDownloaderKeepStructure; const curAbdmUrl = getStoredAbdmUrlForSettings(); const curAbdmDir = normalizeAbdmDownloadDir(gmGet('pk_abdm_dir', CONF.abdmDownloadDir)); const curAbdmKeepStructure = curDownloaderKeepStructure; const curIdmExportMode = normalizeIdmExportMode(gmGet('pk_idm_export_mode', CONF.idmExportMode)); const curIdmUrlExportType = normalizeIdmUrlExportType(gmGet('pk_idm_url_export_type', CONF.idmUrlExportType)); const curIdmExePath = normalizeIdmExePath(gmGet('pk_idm_exe_path', CONF.idmExePath)); const curIdmBatRoot = normalizeIdmBatDownloadRoot(gmGet('pk_idm_bat_root', CONF.idmBatDownloadRoot)); const curIdmBatKeepStructure = curDownloaderKeepStructure; const curDownloadAccelEnable = getBoolPref('pk_download_accel_enable', CONF.downloadAccelEnable); const curDownloadAccelDomain = gmGet('pk_download_accel_domain', CONF.downloadAccelDomain); const curDownloadAccelMode = normalizeDownloadAccelMode(gmGet('pk_download_accel_mode', CONF.downloadAccelMode)); const curDownloadAccelQueryParam = normalizeDownloadAccelQueryParam(gmGet('pk_download_accel_query_param', CONF.downloadAccelQueryParam)); const curBlur = gmGet('pk_blur_thumb', false); const curBlurScope = gmGet('pk_blur_scope', curBlur ? 'list' : 'off'); const curHideButtonText = gmGet('pk_hide_button_text', false); let selectedLang = curLang; let selectedEngine = curEngine; let selectedDefaultVideoQuality = curDefaultVideoQuality; let selectedDefaultOpenPlayer = curDefaultOpenPlayer; let selectedDownloadAccelMode = curDownloadAccelMode; let selectedDownloaderType = curDownloaderType; let selectedIdmExportMode = curIdmExportMode; let selectedIdmUrlExportType = curIdmUrlExportType; const storageDisplay = L.str_loading_placeholder || L.label_config_cloud_none; const configCloudInitialSnapshot = { state: 'checking', statusText: L.str_loading_placeholder || L.label_config_cloud_none, lastSyncText: L.label_config_cloud_none, remoteSizeText: L.label_config_cloud_none, chunkCountText: '0' }; const settingsModalMaximized = !!(UI.win && UI.win.classList && UI.win.classList.contains('pk-maximized')); const settingsModalHeightStyle = settingsModalMaximized ? 'height:min(760px,90vh); max-height:calc(100vh - 56px);' : 'height:min(650px,84vh); max-height:calc(100vh - 80px);'; const m = showModal(`

${L.modal_settings_title}

${L.label_lang}
${CONF.crumbIcons.down}
简体中文
繁體中文
English
한국어
日本語
Indonesia
Bahasa Melayu
${L.label_script_update_info}
${L.label_script_current_version}
${esc(getScriptVersion())}
${L.label_script_latest_version}
${esc(getScriptUpdateLatestText(readScriptUpdateCache()))}
${L.label_script_last_check}
${esc(formatScriptUpdateCheckedAt((readScriptUpdateCache() || {}).checkedAt))}
${L.label_turbo_mode}
${L.label_privacy_mode}
${L.label_blur_cover} ${curBlurScope === 'off' ? L.opt_privacy_off : curBlurScope === 'list' ? L.opt_privacy_list : curBlurScope === 'grid' ? L.opt_privacy_grid : L.opt_privacy_both}
${CONF.crumbIcons.down}
${L.opt_privacy_off}
${L.opt_privacy_list}
${L.opt_privacy_grid}
${L.opt_privacy_both}
${L.label_hide_button_text}
${L.title_blacklist}
${L.label_clipboard_magnet_focus}
${L.lbl_browse_exp}
${L.label_sort_pref}
${L.label_view_pref}
${L.label_search_engine}
${CONF.crumbIcons.down}
${L.opt_engine_google}
${L.opt_engine_yandex}
${L.opt_engine_saucenao}
${L.opt_engine_tracemoe}
${L.lbl_video_playback_settings}
${L.label_default_open_player}
${CONF.crumbIcons.down}
${L.opt_player_script}
${L.opt_player_potplayer}
${L.label_default_video_quality}
${CONF.crumbIcons.down}
${L.str_original}
${L.opt_quality_1080p}
${L.opt_quality_720p}
${L.opt_quality_480p}
${L.label_download_accel_enable}
${L.label_download_accel_mode}
${CONF.crumbIcons.down}
${L.label_download_accel_mode_prefix}
${L.label_download_accel_mode_query}
${L.label_download_accel_domain}
${L.label_download_accel_query_param}
${L.lbl_dl_filter}
${L.lbl_ana_min}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
-
${L.lbl_ana_max}
${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${CONF.crumbIcons.down}
MB
GB
TB
${L.label_dl_filter_ext}
${L.label_dl_filter_name}
${L.label_downloader_type}
${CONF.crumbIcons.down}
${L.label_downloader_aria2}
${L.label_downloader_gopeed}
${L.opt_downloader_abdm}
${L.label_downloader_idm}
${L.lbl_pwd_manage}
${pkIconHtml('vault')} ${L.title_pwd_vault}
${L.lbl_config_cloud_sync}
${CONF.icons.configCloud}
${esc(configCloudInitialSnapshot.statusText)}
${L.label_config_cloud_last_sync}
${esc(configCloudInitialSnapshot.lastSyncText)}
${L.label_config_cloud_remote_size}
${esc(configCloudInitialSnapshot.remoteSizeText)}
${L.label_config_cloud_chunk_count}
${esc(configCloudInitialSnapshot.chunkCountText)}
${L.label_config_cloud_scope}
${esc(CONF.linkBookmarkSyncFolderName)}
${L.lbl_config_manage}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { Object.assign(modalBox.style, { width: 'auto', padding: '0', overflow: 'hidden', height: 'auto', minHeight: 'auto' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: '26px', right: '26px' }); } const updateSettingsScriptVersionInfo = (info = readScriptUpdateCache()) => { const curEl = m.querySelector('#txt_script_current_version'); const latestEl = m.querySelector('#txt_script_latest_version'); const checkEl = m.querySelector('#txt_script_last_check'); if (curEl) curEl.textContent = getScriptVersion(); if (latestEl) { const hasNew = isScriptUpdateNew(info); latestEl.textContent = getScriptUpdateLatestText(info); latestEl.classList.toggle('new', hasNew); latestEl.onclick = hasNew ? () => { const m = {tc:'README(tc).md',en:'README(en).md',ko:'README(ko).md',ja:'README(ja).md',id:'README(id).md',ms:'README(ms).md'}; const f = m[getLang()]; window.open(`${CONF.scriptUpdateProjectUrl}/blob/main/${f ? `i18n/${f}` : 'README.md'}`, '_blank', 'noopener,noreferrer');} : null; } if (checkEl) checkEl.textContent = formatScriptUpdateCheckedAt(info && info.checkedAt); }; updateSettingsScriptVersionInfo(); fetchScriptUpdateInfo(false).then(info => { updateSettingsScriptVersionInfo(info); if (isScriptUpdateNew(info)) showScriptUpdateToast(info); }).catch(() => updateSettingsScriptVersionInfo(readScriptUpdateCache())); const refreshConfigCloudSettingsPanel = async () => { const snapshot = await getConfigCloudSyncStatusSnapshot().catch(() => ({ state: 'failed', statusText: L.status_config_cloud_failed, lastSyncText: L.label_config_cloud_none, remoteSizeText: L.label_config_cloud_none, chunkCountText: '0' })); if (m && m.isConnected) updateConfigCloudSettingsPanel(m, snapshot); }; updateConfigCloudSettingsPanel(m, configCloudInitialSnapshot); calcLocalDataStats().then(stats => { if (!m || !m.isConnected) return; const cleanSizeEl = m.querySelector('#txt_cfg_clean_size'); if (cleanSizeEl) cleanSizeEl.textContent = `( ${fmtSize(stats.total)} )`; }).catch(() => { if (!m || !m.isConnected) return; const cleanSizeEl = m.querySelector('#txt_cfg_clean_size'); if (cleanSizeEl) cleanSizeEl.textContent = `( ${L.str_load_failed_simple || L.label_config_cloud_none} )`; }); refreshConfigCloudSettingsPanel(); const cfgCloudUploadBtn = m.querySelector('#btn_cfg_cloud_upload'); const cfgCloudPullBtn = m.querySelector('#btn_cfg_cloud_pull'); const cfgCloudClearBtn = m.querySelector('#btn_cfg_cloud_clear'); if (cfgCloudPullBtn) { cfgCloudPullBtn.disabled = false; cfgCloudPullBtn.removeAttribute('data-pk-tip'); cfgCloudPullBtn.onclick = async () => { if (S.localCleanBusy) { showToast(L.msg_config_cloud_local_clean_busy, 'warning'); return; } S.configCloudBusy = true; setConfigCloudSettingsBusy(m, true); showToast(L.msg_config_cloud_pulling, 'info'); let snapshot = null; try { snapshot = await getConfigCloudSyncStatusSnapshot(); } catch (e) {} try { await pullConfigCloudFromOfficial({ isConflict: !!(snapshot && snapshot.state === 'conflict') }); } finally { S.configCloudBusy = false; setConfigCloudSettingsBusy(m, false); await refreshConfigCloudSettingsPanel(); } }; } if (cfgCloudUploadBtn) cfgCloudUploadBtn.onclick = async () => { if (S.localCleanBusy) { showToast(L.msg_config_cloud_local_clean_busy, 'warning'); return; } S.configCloudBusy = true; setConfigCloudSettingsBusy(m, true); showToast(L.msg_config_cloud_uploading, 'info'); try { await uploadConfigCloudToOfficial(); } finally { S.configCloudBusy = false; setConfigCloudSettingsBusy(m, false); await refreshConfigCloudSettingsPanel(); } }; if (cfgCloudClearBtn) cfgCloudClearBtn.onclick = async () => { if (S.localCleanBusy) { showToast(L.msg_config_cloud_local_clean_busy, 'warning'); return; } const ok = await showConfirm(L.msg_config_cloud_clear_confirm, L.lbl_config_cloud_sync); if (!ok) return; S.configCloudBusy = true; setConfigCloudSettingsBusy(m, true); showToast(L.msg_config_cloud_clearing, 'info'); try { await clearConfigCloudFromOfficial(); } finally { S.configCloudBusy = false; setConfigCloudSettingsBusy(m, false); await refreshConfigCloudSettingsPanel(); } }; const closeSettingsSelectMenus = () => { m.querySelectorAll('.pk-select-menu').forEach(menu => menu.style.display = 'none'); m.querySelectorAll('.pk-custom-select.pk-select-open,.pk-setting-fieldset.pk-select-open').forEach(el => el.classList.remove('pk-select-open')); }; const bindSelect = (id, currentVal, onSelect, isLocked) => { 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'); const fieldset = container.closest('.pk-setting-fieldset'); items.forEach(item => { if (item.dataset.val === currentVal) { item.classList.add('act'); txt.textContent = item.textContent; } item.onclick = (e) => { e.stopPropagation(); if (typeof isLocked === 'function' && isLocked()) { closeSettingsSelectMenus(); showToast(L.tip_downloader_switch_locked); return; } items.forEach(i => i.classList.remove('act')); item.classList.add('act'); txt.textContent = item.textContent; closeSettingsSelectMenus(); onSelect(item.dataset.val); }; }); trigger.onclick = (e) => { e.stopPropagation(); if (typeof isLocked === 'function' && isLocked()) { closeSettingsSelectMenus(); showToast(L.tip_downloader_switch_locked); return; } const willOpen = menu.style.display !== 'block'; closeSettingsSelectMenus(); if (willOpen) { menu.style.display = 'block'; container.classList.add('pk-select-open'); if (fieldset) fieldset.classList.add('pk-select-open'); } }; }; bindSelect('cs_set_lang', curLang, (val) => { selectedLang = val; }); bindSelect('cs_set_engine', curEngine, (val) => { selectedEngine = val; }); bindSelect('cs_default_open_player', curDefaultOpenPlayer, (val) => { selectedDefaultOpenPlayer = normalizeDefaultOpenPlayer(val); }); bindSelect('cs_default_video_quality', curDefaultVideoQuality, (val) => { selectedDefaultVideoQuality = normalizeDefaultVideoQuality(val); }); bindSelect('cs_download_accel_mode', curDownloadAccelMode, (val) => { selectedDownloadAccelMode = normalizeDownloadAccelMode(val); updateAccelModeTip(); updateAccelBorder(); }); const isDownloaderSwitchLocked = () => !!(isGUISensitive || (typeof pkState !== 'undefined' && pkState && pkState.scanning)); bindSelect('cs_downloader_type', curDownloaderType, (val) => { selectedDownloaderType = normalizeDownloaderType(val); updateDownloaderSettingsVisibility(); testActiveDownloaderConnection(); }, isDownloaderSwitchLocked); bindSelect('cs_idm_export_mode', curIdmExportMode, (val) => { selectedIdmExportMode = normalizeIdmExportMode(val); updateIdmExportModeVisibility(); }); bindSelect('cs_idm_url_export_type', curIdmUrlExportType, (val) => { selectedIdmUrlExportType = normalizeIdmUrlExportType(val); updateIdmGroupBorder(); }); const ariaInp = m.querySelector('#set_aria_url'); const ariaTok = m.querySelector('#set_aria_token'); const ariaDir = m.querySelector('#set_aria_dir'); const ariaDirClear = m.querySelector('#btn_aria_dir_clear'); const ariaKeepStructure = m.querySelector('#set_aria_keep_structure'); const ariaEye = m.querySelector('#btn_aria_token_eye'); const ariaDot = m.querySelector('#aria_test_dot'); const ariaTxt = m.querySelector('#aria_test_txt'); const ariaBox = m.querySelector('#aria_test_res'); const ariaGroup = m.querySelector('#pk_aria2_group'); const ariaCfgLabel = m.querySelector('#label_aria_config'); const ariaUrlLabel = m.querySelector('#label_aria_url'); const ariaTokenLabel = m.querySelector('#label_aria_token'); const ariaDirLabel = m.querySelector('#label_aria_dir'); const gopeedInp = m.querySelector('#set_gopeed_url'); const gopeedTok = m.querySelector('#set_gopeed_token'); const gopeedDir = m.querySelector('#set_gopeed_dir'); const gopeedKeepStructure = m.querySelector('#set_gopeed_keep_structure'); const gopeedDirClear = m.querySelector('#btn_gopeed_dir_clear'); const gopeedEye = m.querySelector('#btn_gopeed_token_eye'); const gopeedDefault = m.querySelector('#btn_gopeed_default'); const gopeedDot = m.querySelector('#gopeed_test_dot'); const gopeedTxt = m.querySelector('#gopeed_test_txt'); const gopeedBox = m.querySelector('#gopeed_test_res'); const gopeedGroup = m.querySelector('#pk_gopeed_group'); const abdmInp = m.querySelector('#set_abdm_url'); const abdmDir = m.querySelector('#set_abdm_dir'); const abdmKeepStructure = m.querySelector('#set_abdm_keep_structure'); const abdmDirClear = m.querySelector('#btn_abdm_dir_clear'); const abdmDefault = m.querySelector('#btn_abdm_default'); const abdmDot = m.querySelector('#abdm_test_dot'); const abdmTxt = m.querySelector('#abdm_test_txt'); const abdmBox = m.querySelector('#abdm_test_res'); const abdmGroup = m.querySelector('#pk_abdm_group'); const idmGroup = m.querySelector('#pk_idm_group'); const idmUrlGroup = m.querySelector('#pk_idm_url_group'); const idmBatGroup = m.querySelector('#pk_idm_bat_group'); const idmExePath = m.querySelector('#set_idm_exe_path'); const idmExePathClear = m.querySelector('#btn_idm_exe_path_clear'); const idmBatRoot = m.querySelector('#set_idm_bat_root'); const idmBatRootClear = m.querySelector('#btn_idm_bat_root_clear'); const idmBatKeepStructure = m.querySelector('#set_idm_bat_keep_structure'); const downloaderPreferOriginalVideoLinkInputs = Array.from(m.querySelectorAll('.pk-downloader-prefer-original-video-link')); const getDownloaderPreferOriginalVideoLinkChecked = () => { const input = downloaderPreferOriginalVideoLinkInputs.find(el => el && el.offsetParent !== null) || downloaderPreferOriginalVideoLinkInputs[0]; return input ? !!input.checked : CONF.downloaderPreferOriginalVideoLink; }; const isDownloaderPreferOriginalVideoLinkCustomized = () => getDownloaderPreferOriginalVideoLinkChecked() !== CONF.downloaderPreferOriginalVideoLink; const keepStructureInputs = Array.from(m.querySelectorAll('#set_aria_keep_structure,#set_gopeed_keep_structure,#set_abdm_keep_structure,#set_idm_bat_keep_structure')); const getDownloaderKeepStructureChecked = () => { const input = keepStructureInputs.find(el => el && el.offsetParent !== null) || keepStructureInputs[0]; return input ? !!input.checked : CONF.aria2KeepFolderStructure; }; const syncDownloaderKeepStructureInputs = (checked) => keepStructureInputs.forEach(input => { if (input) input.checked = !!checked; }); const isDownloaderKeepStructureCustomized = () => getDownloaderKeepStructureChecked() !== CONF.aria2KeepFolderStructure; const updateAriaGroupBorder = () => { if (ariaGroup) ariaGroup.classList.toggle('pk-inner-active', !!(ariaInp.value.trim() || ariaTok.value.trim() || ariaDir.value.trim() || isDownloaderKeepStructureCustomized() || isDownloaderPreferOriginalVideoLinkCustomized())); }; const updateGopeedGroupBorder = () => { if (gopeedGroup) gopeedGroup.classList.toggle('pk-inner-active', !!(gopeedInp.value.trim() || gopeedTok.value.trim() || gopeedDir.value.trim() || isDownloaderKeepStructureCustomized() || isDownloaderPreferOriginalVideoLinkCustomized())); }; const updateAbdmGroupBorder = () => { if (abdmGroup) abdmGroup.classList.toggle('pk-inner-active', !!(abdmInp.value.trim() || abdmDir.value.trim() || isDownloaderKeepStructureCustomized() || isDownloaderPreferOriginalVideoLinkCustomized())); }; const updateIdmGroupBorder = () => { if (idmGroup) idmGroup.classList.toggle('pk-inner-active', !!(selectedIdmExportMode !== CONF.idmExportMode || selectedIdmUrlExportType !== CONF.idmUrlExportType || (idmExePath && idmExePath.value.trim()) || (idmBatRoot && idmBatRoot.value.trim()) || isDownloaderKeepStructureCustomized() || isDownloaderPreferOriginalVideoLinkCustomized())); }; let ariaTimer = null; let gopeedTimer = null; let abdmTimer = null; let ariaTokenVisible = false; let gopeedTokenVisible = false; const updateIdmExportModeVisibility = () => { const isBat = normalizeIdmExportMode(selectedIdmExportMode) === 'bat'; if (idmUrlGroup) idmUrlGroup.style.display = isBat ? 'none' : 'flex'; if (idmBatGroup) idmBatGroup.style.display = isBat ? 'flex' : 'none'; updateIdmGroupBorder(); }; const updateDownloaderSettingsVisibility = () => { const type = normalizeDownloaderType(selectedDownloaderType); const rpcType = 'aria2'; if (ariaGroup) ariaGroup.style.display = type === 'aria2' ? 'block' : 'none'; if (gopeedGroup) gopeedGroup.style.display = type === 'gopeed' ? 'block' : 'none'; if (abdmGroup) abdmGroup.style.display = type === 'abdm' ? 'block' : 'none'; if (idmGroup) idmGroup.style.display = type === 'idm' ? 'block' : 'none'; if (ariaInp) ariaInp.placeholder = getDefaultRpcUrlForDownloader(rpcType); if (ariaCfgLabel) ariaCfgLabel.textContent = formatDownloaderText(L.label_downloader_config, rpcType); if (ariaUrlLabel) ariaUrlLabel.textContent = formatDownloaderText(L.label_downloader_url, rpcType); if (ariaTokenLabel) ariaTokenLabel.textContent = formatDownloaderText(L.label_downloader_token, rpcType); if (ariaDirLabel) ariaDirLabel.textContent = formatDownloaderText(L.label_downloader_dir, rpcType); clearTimeout(ariaTimer); clearTimeout(gopeedTimer); clearTimeout(abdmTimer); if (type !== 'aria2') { ariaDot.className = 'pk-aria-dot'; ariaTxt.textContent = L.lbl_downloader_status; } if (type !== 'gopeed') { gopeedDot.className = 'pk-aria-dot'; gopeedTxt.textContent = L.lbl_downloader_status; } if (type !== 'abdm') { abdmDot.className = 'pk-aria-dot'; abdmTxt.textContent = L.lbl_downloader_status; } if (type === 'idm') updateIdmExportModeVisibility(); }; const runAriaTest = async () => { const url = ariaInp.value.trim(); const token = ariaTok.value.trim(); const showTip = () => showAlert(L.tip_mixed_content, L.lbl_downloader_status); if (!url) { ariaDot.className = 'pk-aria-dot'; ariaTxt.textContent = L.lbl_downloader_status; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; return; } ariaDot.className = 'pk-aria-dot wait'; ariaTxt.textContent = L.str_connecting; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; const testUrl = normalizeAriaRpcUrl(url); const payload = { jsonrpc: '2.0', method: 'aria2.getVersion', id: 'pk_live_test', params: buildAriaRpcParams(token) }; try { await aria2RpcRequest(testUrl, payload, 3000); ariaDot.className = 'pk-aria-dot ok'; ariaTxt.textContent = L.str_connected; ariaBox.onclick = null; ariaBox.style.cursor = 'default'; } catch (e) { ariaDot.className = 'pk-aria-dot err'; ariaTxt.textContent = L.str_conn_fail; ariaBox.onclick = showTip; ariaBox.style.cursor = 'pointer'; } }; const debouncedTest = () => { clearTimeout(ariaTimer); ariaTimer = setTimeout(runAriaTest, 600); }; const runGopeedTest = async () => { const url = gopeedInp.value.trim(); const token = gopeedTok.value.trim(); const showTip = (msg = L.desc_gopeed_setup) => showAlert(msg, L.lbl_downloader_status); if (!url) { gopeedDot.className = 'pk-aria-dot'; gopeedTxt.textContent = L.lbl_downloader_status; gopeedBox.onclick = () => showTip(); gopeedBox.style.cursor = 'pointer'; return; } gopeedDot.className = 'pk-aria-dot wait'; gopeedTxt.textContent = L.str_connecting; gopeedBox.onclick = () => showTip(); gopeedBox.style.cursor = 'pointer'; try { await testGopeedConnection(normalizeGopeedApiUrl(url), token, 3000); gopeedDot.className = 'pk-aria-dot ok'; gopeedTxt.textContent = L.str_connected; gopeedBox.onclick = null; gopeedBox.style.cursor = 'default'; } catch (e) { gopeedDot.className = 'pk-aria-dot err'; gopeedTxt.textContent = L.str_conn_fail; gopeedBox.onclick = () => showTip(); gopeedBox.style.cursor = 'pointer'; } }; const debouncedGopeedTest = () => { clearTimeout(gopeedTimer); gopeedTimer = setTimeout(runGopeedTest, 600); }; const runAbdmTest = async () => { const url = abdmInp.value.trim(); const showTip = () => showAlert(L.desc_abdm_setup, L.lbl_downloader_status); if (!url) { abdmDot.className = 'pk-aria-dot'; abdmTxt.textContent = L.lbl_downloader_status; abdmBox.onclick = showTip; abdmBox.style.cursor = 'pointer'; return; } abdmDot.className = 'pk-aria-dot wait'; abdmTxt.textContent = L.str_connecting; abdmBox.onclick = showTip; abdmBox.style.cursor = 'pointer'; let testUrl = ''; try { const parsed = new URL(url); if (!/^https?:$/i.test(parsed.protocol) || !parsed.hostname) throw new Error('Invalid ABDM API URL'); testUrl = normalizeAbdmApiUrl(url); if (!testUrl) throw new Error('Invalid ABDM API URL'); } catch (e) { abdmDot.className = 'pk-aria-dot err'; abdmTxt.textContent = L.str_conn_fail; abdmBox.onclick = showTip; abdmBox.style.cursor = 'pointer'; return; } try { await testAbdmConnection(testUrl, 3000); abdmDot.className = 'pk-aria-dot ok'; abdmTxt.textContent = L.str_connected; abdmBox.onclick = null; abdmBox.style.cursor = 'default'; } catch (e) { abdmDot.className = 'pk-aria-dot err'; abdmTxt.textContent = L.str_conn_fail; abdmBox.onclick = showTip; abdmBox.style.cursor = 'pointer'; } }; const debouncedAbdmTest = () => { clearTimeout(abdmTimer); abdmTimer = setTimeout(runAbdmTest, 600); }; const testActiveDownloaderConnection = () => { const type = normalizeDownloaderType(selectedDownloaderType); if (type === 'aria2') { clearTimeout(ariaTimer); runAriaTest(); } else if (type === 'gopeed') { clearTimeout(gopeedTimer); runGopeedTest(); } else if (type === 'abdm') { clearTimeout(abdmTimer); runAbdmTest(); } }; ariaInp.oninput = (e) => { if (e.target.oninput) e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateAriaGroupBorder(); debouncedTest(); }; ariaTok.oninput = (e) => { if (e.target.oninput) e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateAriaGroupBorder(); debouncedTest(); }; ariaDir.oninput = (e) => { e.target.value = normalizeAriaDownloadDir(e.target.value); e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateAriaGroupBorder(); }; if (ariaDirClear) { ariaDirClear.onclick = (e) => { e.preventDefault(); e.stopPropagation(); ariaDir.value = ''; ariaDir.style.borderColor = 'var(--pk-bd)'; updateAriaGroupBorder(); }; } if (ariaEye) { ariaEye.onclick = (e) => { e.preventDefault(); e.stopPropagation(); ariaTokenVisible = !ariaTokenVisible; ariaTok.style.webkitTextSecurity = ariaTokenVisible ? 'none' : 'disc'; ariaEye.innerHTML = ariaTokenVisible ? CONF.icons.eyeoff : CONF.icons.eye; }; } m.querySelector('#btn_aria_default').onclick = () => { ariaInp.value = getDefaultRpcUrlForDownloader(selectedDownloaderType); ariaInp.style.borderColor = 'var(--pk-pri)'; updateAriaGroupBorder(); debouncedTest(); }; if (gopeedInp) { gopeedInp.oninput = (e) => { e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateGopeedGroupBorder(); debouncedGopeedTest(); }; } if (gopeedTok) { gopeedTok.oninput = (e) => { e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateGopeedGroupBorder(); debouncedGopeedTest(); }; } if (gopeedDir) { gopeedDir.oninput = (e) => { e.target.value = normalizeGopeedDownloadDir(e.target.value); e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateGopeedGroupBorder(); }; } if (abdmInp) { abdmInp.oninput = (e) => { e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateAbdmGroupBorder(); debouncedAbdmTest(); }; } if (abdmDir) { abdmDir.oninput = (e) => { e.target.value = normalizeAbdmDownloadDir(e.target.value); e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateAbdmGroupBorder(); }; } if (ariaKeepStructure) { ariaKeepStructure.onchange = () => { syncDownloaderKeepStructureInputs(ariaKeepStructure.checked); updateAriaGroupBorder(); updateGopeedGroupBorder(); updateAbdmGroupBorder(); updateIdmGroupBorder(); }; } if (gopeedKeepStructure) { gopeedKeepStructure.onchange = () => { syncDownloaderKeepStructureInputs(gopeedKeepStructure.checked); updateAriaGroupBorder(); updateGopeedGroupBorder(); updateAbdmGroupBorder(); updateIdmGroupBorder(); }; } if (abdmKeepStructure) { abdmKeepStructure.onchange = () => { syncDownloaderKeepStructureInputs(abdmKeepStructure.checked); updateAriaGroupBorder(); updateGopeedGroupBorder(); updateAbdmGroupBorder(); updateIdmGroupBorder(); }; } if (gopeedDirClear) { gopeedDirClear.onclick = (e) => { e.preventDefault(); e.stopPropagation(); gopeedDir.value = ''; gopeedDir.style.borderColor = 'var(--pk-bd)'; updateGopeedGroupBorder(); }; } if (gopeedEye) { gopeedEye.onclick = (e) => { e.preventDefault(); e.stopPropagation(); gopeedTokenVisible = !gopeedTokenVisible; gopeedTok.style.webkitTextSecurity = gopeedTokenVisible ? 'none' : 'disc'; gopeedEye.innerHTML = gopeedTokenVisible ? CONF.icons.eyeoff : CONF.icons.eye; }; } if (gopeedDefault) { gopeedDefault.onclick = () => { gopeedInp.value = getDefaultRpcUrlForDownloader('gopeed'); gopeedInp.style.borderColor = 'var(--pk-pri)'; updateGopeedGroupBorder(); debouncedGopeedTest(); }; } if (abdmDirClear) { abdmDirClear.onclick = (e) => { e.preventDefault(); e.stopPropagation(); abdmDir.value = ''; abdmDir.style.borderColor = 'var(--pk-bd)'; updateAbdmGroupBorder(); }; } if (abdmDefault) { abdmDefault.onclick = () => { abdmInp.value = getDefaultRpcUrlForDownloader('abdm'); abdmInp.style.borderColor = 'var(--pk-pri)'; updateAbdmGroupBorder(); debouncedAbdmTest(); }; } if (idmExePath) { idmExePath.oninput = (e) => { e.target.value = normalizeIdmExePath(e.target.value); e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateIdmGroupBorder(); }; } if (idmExePathClear) { idmExePathClear.onclick = (e) => { e.preventDefault(); e.stopPropagation(); idmExePath.value = ''; idmExePath.style.borderColor = 'var(--pk-bd)'; updateIdmGroupBorder(); }; } if (idmBatRoot) { idmBatRoot.oninput = (e) => { e.target.value = normalizeIdmBatDownloadRoot(e.target.value); e.target.style.borderColor = e.target.value.trim() ? 'var(--pk-pri)' : 'var(--pk-bd)'; updateIdmGroupBorder(); }; } if (idmBatRootClear) { idmBatRootClear.onclick = (e) => { e.preventDefault(); e.stopPropagation(); idmBatRoot.value = ''; idmBatRoot.style.borderColor = 'var(--pk-bd)'; updateIdmGroupBorder(); }; } if (idmBatKeepStructure) { idmBatKeepStructure.onchange = () => { syncDownloaderKeepStructureInputs(idmBatKeepStructure.checked); updateAriaGroupBorder(); updateGopeedGroupBorder(); updateAbdmGroupBorder(); updateIdmGroupBorder(); }; } downloaderPreferOriginalVideoLinkInputs.forEach(input => { input.onchange = () => { const checked = !!input.checked; downloaderPreferOriginalVideoLinkInputs.forEach(peer => { peer.checked = checked; }); updateAriaGroupBorder(); updateGopeedGroupBorder(); updateAbdmGroupBorder(); updateIdmGroupBorder(); }; }); syncDownloaderKeepStructureInputs(curDownloaderKeepStructure); updateDownloaderSettingsVisibility(); setTimeout(() => { const activeDownloaderType = normalizeDownloaderType(selectedDownloaderType); if (activeDownloaderType === 'gopeed') runGopeedTest(); else if (activeDownloaderType === 'abdm') runAbdmTest(); else if (activeDownloaderType === 'aria2') runAriaTest(); }, 200); const clickAway = closeSettingsSelectMenus; setTimeout(() => document.addEventListener('click', clickAway), 0); const _orgRemove = m.remove.bind(m); m.remove = () => { document.removeEventListener('click', clickAway); openSettingsModal._active = false; _orgRemove(); }; const extInp = m.querySelector('#set_dl_filter_ext'); const nameInp = m.querySelector('#set_dl_filter_name'); const sizeMinInp = m.querySelector('#set_dl_filter_size_min'); const sizeMaxInp = m.querySelector('#set_dl_filter_size_max'); const groupEl = m.querySelector('#pk_dl_group'); const updateDlBorders = () => { const hasE = extInp.value.trim() !== ''; const hasN = nameInp.value.trim() !== ''; const minRaw = sizeMinInp.value.trim(); const hasSMin = minRaw !== '' && Number(minRaw) !== 0; const hasSMax = sizeMaxInp.value.trim() !== ''; extInp.classList.toggle('pk-active-border', hasE); nameInp.classList.toggle('pk-active-border', hasN); sizeMinInp.classList.toggle('pk-active-border', hasSMin); sizeMaxInp.classList.toggle('pk-active-border', hasSMax); groupEl.classList.toggle('pk-typing-active', hasE || hasN || hasSMin || hasSMax); }; extInp.oninput = nameInp.oninput = sizeMinInp.oninput = sizeMaxInp.oninput = updateDlBorders; bindConfigListTextareaLimits(m, [ ['#set_dl_filter_ext', 'pk_dl_filter_ext'], ['#set_dl_filter_name', 'pk_dl_filter_name'] ]); updateDlBorders(); const accelDomainInp = m.querySelector('#set_download_accel_domain'); const accelQueryParamInp = m.querySelector('#set_download_accel_query_param'); const accelEnableInp = m.querySelector('#set_download_accel_enable'); const accelGroupEl = m.querySelector('#pk_download_accel_group'); const updateAccelModeTip = () => { const isQuery = selectedDownloadAccelMode === 'query'; if (accelQueryParamInp) accelQueryParamInp.closest('div').style.display = isQuery ? 'block' : 'none'; if (accelDomainInp) accelDomainInp.placeholder = isQuery ? 'https://download.example.com/proxy' : 'https://download.example.com'; }; const updateAccelBorder = () => { const hasDomain = !!(accelDomainInp && accelDomainInp.value.trim()); const customParam = !!(accelQueryParamInp && accelQueryParamInp.value.trim() && normalizeDownloadAccelQueryParam(accelQueryParamInp.value) !== CONF.downloadAccelQueryParam); const hasTypedValue = hasDomain || customParam; if (accelGroupEl) accelGroupEl.classList.toggle('pk-typing-active', hasTypedValue); if (accelDomainInp) { accelDomainInp.classList.toggle('pk-active-border', hasDomain); accelDomainInp.style.borderColor = hasDomain ? 'var(--pk-pri)' : 'var(--pk-bd)'; } if (accelQueryParamInp) { accelQueryParamInp.classList.toggle('pk-active-border', customParam); accelQueryParamInp.style.borderColor = customParam ? 'var(--pk-pri)' : 'var(--pk-bd)'; } }; if (accelDomainInp) accelDomainInp.oninput = updateAccelBorder; if (accelQueryParamInp) accelQueryParamInp.oninput = updateAccelBorder; if (accelEnableInp) accelEnableInp.onchange = updateAccelBorder; updateAccelModeTip(); updateAccelBorder(); bindConfigLiveInputLimits(m, [ ['#set_download_accel_domain', 'pk_download_accel_domain'], ['#set_download_accel_query_param', 'pk_download_accel_query_param'], ['#set_aria_url', 'pk_aria2_url'], ['#set_aria_token', 'pk_aria2_token'], ['#set_aria_dir', 'pk_aria2_dir'], ['#set_gopeed_url', 'pk_gopeed_url'], ['#set_gopeed_token', 'pk_gopeed_token'], ['#set_gopeed_dir', 'pk_gopeed_dir'], ['#set_abdm_url', 'pk_abdm_url'], ['#set_abdm_dir', 'pk_abdm_dir'], ['#set_idm_exe_path', 'pk_idm_exe_path'], ['#set_idm_bat_root', 'pk_idm_bat_root'] ]); const curDlSizeUnit = gmGet('pk_dl_filter_size_unit', 'MB'); bindSelect('cs_set_dl_size_unit', curDlSizeUnit, (val) => {}); m.querySelector('#dl_size_dec_min').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMinInp.value)) || 0; if (v > 0) sizeMinInp.value = v - 1; ensureDlSizeRange(); updateDlBorders(); }; m.querySelector('#dl_size_inc_min').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMinInp.value)) || 0; sizeMinInp.value = v + 1; ensureDlSizeRange(); updateDlBorders(); }; m.querySelector('#dl_size_dec_max').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMaxInp.value)) || 0; if (v > 0) sizeMaxInp.value = v - 1; ensureDlSizeRange(); updateDlBorders(); }; m.querySelector('#dl_size_inc_max').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(sizeMaxInp.value)) || 0; sizeMaxInp.value = v + 1; ensureDlSizeRange(); updateDlBorders(); }; const ensureDlSizeRange = () => { let minVal = sizeMinInp.value.trim() === '' ? -1 : Math.floor(Number(sizeMinInp.value)); let maxVal = sizeMaxInp.value.trim() === '' ? -1 : Math.floor(Number(sizeMaxInp.value)); if (minVal >= 0 && maxVal >= 0 && minVal > maxVal) { if (document.activeElement === sizeMinInp) { sizeMaxInp.value = minVal; } else if (document.activeElement === sizeMaxInp) { sizeMinInp.value = maxVal; } else { sizeMaxInp.value = minVal; } } }; sizeMinInp.addEventListener('blur', ensureDlSizeRange); sizeMaxInp.addEventListener('blur', ensureDlSizeRange); sizeMinInp.addEventListener('change', ensureDlSizeRange); sizeMaxInp.addEventListener('change', ensureDlSizeRange); m.querySelector('#btn_open_vault').onclick = (e) => { e.stopPropagation(); const subM = document.createElement('div'); subM.className = 'pk-modal-ov'; subM.style.zIndex = (++modalZIndexCounter).toString(); if (document.querySelector('.pk-ov').classList.contains('pk-dark')) subM.classList.add('pk-dark'); const savedCount = gmGet('pk_pwd_try_count', 10); const savedPwds = (() => { try { return JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x.p : x).join('\n'); } catch { return ''; } })(); subM.innerHTML = `
${CONF.icons.close}

${pkIconHtml('vault')} ${L.title_pwd_vault}

${L.lbl_pwd_try_count}
${L.tip_pwd_manual}
${L.btn_cancel}
`; document.body.appendChild(subM); const cntInp = subM.querySelector('#vault_cnt_val'); subM.querySelector('#vault_cnt_dec').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(cntInp.value)) || 10; cntInp.value = Math.max(10, v - 1); }; subM.querySelector('#vault_cnt_inc').onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); let v = Math.floor(Number(cntInp.value)) || 0; cntInp.value = Math.min(50, v + 1); }; cntInp.oninput = () => { let raw = cntInp.value; if (raw === "") return; let v = parseInt(raw); if (isNaN(v)) { cntInp.value = 10; return; } if (v > 50) cntInp.value = 50; }; cntInp.onblur = () => { let v = parseInt(cntInp.value); if (isNaN(v) || v < 10) cntInp.value = 10; }; const area = subM.querySelector('#vault_pwd_area'); area.onfocus = () => area.style.borderColor = 'var(--pk-pri)'; area.onblur = () => area.style.borderColor = 'var(--pk-bd)'; area.value = savedPwds; area.oninput = () => { let lines = area.value.split('\n'); let changed = false; let msg = ""; if (lines.length > 50) { lines = lines.slice(0, 50); changed = true; msg = L.err_vault_max; } for (let i = 0; i < lines.length; i++) { if (lines[i].length > 127) { lines[i] = lines[i].substring(0, 127); changed = true; msg = L.err_pwd_len; } } if (changed) { const cursor = area.selectionStart; area.value = lines.join('\n'); area.setSelectionRange(cursor, cursor); showToast(msg, 'error'); area.style.borderColor = '#d93025'; setTimeout(() => { if(area) area.style.borderColor = 'var(--pk-pri)'; }, 1000); } }; const doSave = () => { let cnt = Math.floor(Number(cntInp.value)); if (isNaN(cnt) || cnt < 10) cnt = 10; if (cnt > 50) cnt = 50; const inputPwds = area.value.split('\n').map(s => s.trim()).filter(s => s); if (inputPwds.length > 50) { showToast(L.err_vault_max, 'error'); area.style.borderColor = '#d93025'; return; } let changed = false; 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); const oldVaultRaw = gmGet('pk_pwd_vault', '[]'); const newVaultRaw = JSON.stringify(newList); changed = String(oldVaultRaw) !== newVaultRaw || Number(gmGet('pk_pwd_try_count', 10)) !== cnt; if (changed) { gmSet('pk_pwd_try_count', cnt); gmSet('pk_pwd_vault', newVaultRaw); } } catch(e) { const newVaultRaw = JSON.stringify([...new Set(inputPwds)]); changed = String(gmGet('pk_pwd_vault', '[]')) !== newVaultRaw || Number(gmGet('pk_pwd_try_count', 10)) !== cnt; if (changed) { gmSet('pk_pwd_try_count', cnt); gmSet('pk_pwd_vault', newVaultRaw); } } subM.remove(); if (changed) showToast(L.msg_settings_saved_plain); }; const focusSettingsAfterVaultClose = () => { setTimeout(() => { const lastModal = Array.from(document.querySelectorAll('.pk-modal-ov')).pop(); if (!lastModal) return; lastModal.tabIndex = 0; try { lastModal.focus(); } catch (e) {} }, 0); }; const closeVault = () => { subM.remove(); focusSettingsAfterVaultClose(); }; subM.tabIndex = 0; setTimeout(() => { try { subM.focus(); } catch (e) {} }, 10); subM.addEventListener('keydown', (e) => { if (e.key !== 'Enter' && e.key !== 'Escape') return; if (e.key === 'Enter' && e.target && e.target.closest && e.target.closest('textarea')) return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); if (e.key === 'Enter') { doSave(); focusSettingsAfterVaultClose(); } else { closeVault(); } }, true); subM.querySelector('#vault_save').onclick = () => { doSave(); focusSettingsAfterVaultClose(); }; subM.querySelector('#vault_cancel').onclick = closeVault; subM.querySelector('.pk-modal-close').onclick = closeVault; }; m.querySelector('#btn_cfg_clean').onclick = async () => { if (S.configCloudBusy) { showToast(L.msg_config_cloud_busy_clean_block, 'warning'); return; } const cleanStats = await calcLocalDataStats(); const cleanSizeEl = m.querySelector('#txt_cfg_clean_size'); if (cleanSizeEl) cleanSizeEl.textContent = `( ${fmtSize(cleanStats.total)} )`; const { keys, sizes, getCat } = cleanStats; const renderLbl = (cat, txt, isChecked = false, isMandatory = false) => { const sz = sizes[cat]; if (sz === 0 && cat !== 'index' && cat !== 'cache') return ''; const szStr = fmtSize(sz); const checkAttr = (isChecked || isMandatory) ? 'checked' : ''; const disAttr = isMandatory ? 'disabled' : ''; const cursor = isMandatory ? 'not-allowed' : 'pointer'; const opacity = isMandatory ? '0.7' : '1'; return ``; }; const htmlOptions =[ renderLbl('index', L.opt_cfg_index, true, true), renderLbl('pref', L.opt_cfg_pref), renderLbl('rules', L.opt_cfg_rules), renderLbl('vault', L.opt_cfg_vault), renderLbl('history', L.opt_cfg_history), renderLbl('shareParseHistory', L.opt_cfg_share_parse_history), renderLbl('cache', L.opt_cfg_cache) ].filter(Boolean).join(''); if (!htmlOptions) return; const cleanM = showModal(`

${L.title_clean_data}

${htmlOptions}
`); const cleanBox = cleanM.querySelector('.pk-modal'); if (cleanBox) { cleanBox.style.setProperty('width', '620px', 'important'); cleanBox.style.setProperty('max-width', 'calc(100vw - 48px)', 'important'); cleanBox.style.setProperty('box-sizing', 'border-box', 'important'); } const focusSettingsAfterCleanClose = () => { setTimeout(() => { const lastModal = Array.from(document.querySelectorAll('.pk-modal-ov')).pop(); if (!lastModal) return; lastModal.tabIndex = 0; try { lastModal.focus(); } catch (e) {} }, 0); }; const closeClean = () => { cleanM.remove(); focusSettingsAfterCleanClose(); }; cleanM.tabIndex = 0; setTimeout(() => { try { cleanM.focus(); } catch (e) {} }, 10); cleanM.addEventListener('keydown', (e) => { if (e.key !== 'Enter' && e.key !== 'Escape') return; e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); if (e.key === 'Enter') { const btn = cleanM.querySelector('#clean_confirm'); if (btn) btn.click(); } else { closeClean(); } }, true); cleanM.querySelector('#clean_cancel').onclick = closeClean; const cleanCloseBtn = cleanM.querySelector('.pk-modal-close'); if (cleanCloseBtn) cleanCloseBtn.onclick = closeClean; cleanM.querySelector('#clean_confirm').onclick = async () => { const selected = Array.from(cleanM.querySelectorAll('.clean-opt:checked')).map(el => el.value); if (selected.length === 0) { closeClean(); return; } if (!await showConfirm(L.msg_clean_confirm)) return; if (S.configCloudBusy) { showToast(L.msg_config_cloud_busy_clean_block, 'warning'); return; } S.localCleanBusy = true; setConfigCloudSettingsBusy(m, false); 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 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(); } if (selected.includes('cache') && window.localforage) { try { await window.localforage.dropInstance({ name: 'pk_thumbs', storeName: 'snapshots' }); } catch (e) { console.warn("Clear thumb cache error:", e); } } 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); } } }); cleanupConfigPrefixKeys(); if (selected.includes('shareParseHistory')) refreshShareParseHistoryModal(); showToast(L.msg_clean_success); setTimeout(() => location.reload(), 1500); }; }; m.querySelector('#btn_cfg_export').onclick = () => { const gmKeys = typeof GM_listValues !== 'undefined' ? GM_listValues() : []; const lsKeys = Object.keys(localStorage).filter(k => typeof k === 'string' && k.startsWith('pk_')); const keys = Array.from(new Set([...(Array.isArray(gmKeys) ? gmKeys : []), ...lsKeys])); const config = { "_pk_metadata": { "signature": "PIKPAK_ENHANCEMENT_MASTER", "version": version, "export_at": new Date().toISOString(), "author": "digbug82", "scope": "whitelist" } }; const exportExactKeys = new Set([ 'pk_lang', 'pk_theme', 'pk_turbo_mode', 'pk_file_view_mode', 'pk_view_independent', 'pk_folder_view_prefs', 'pk_keep_pos', 'pk_pos_left', 'pk_pos_top', 'pk_blur_thumb', 'pk_blur_scope', 'pk_hide_button_text', 'pk_comic_mode', 'pk_clipboard_magnet_focus', 'pk_clipboard_magnet_paste', 'pk_audio_play_mode', 'pk_audio_vol_level', 'pk_audio_vol_muted', 'pk_audio_mini_pos', 'pk_sort_independent', 'pk_folder_first', 'pk_folder_sort_prefs', 'pk_global_sort_pref', 'pk_ext_player', 'pk_default_open_player', 'pk_potplayer_custom_path', 'pk_play_mode', 'pk_skip_intro', 'pk_skip_outro', 'pk_vol_muted', 'pk_vol_level', 'pk_suppress_global_warn', 'pk_blacklist', 'pk_blacklist_folders', 'pk_downloader_type', 'pk_downloader_prefer_original_video_link', 'pk_idm_export_mode', 'pk_idm_url_export_type', 'pk_idm_exe_path', 'pk_idm_bat_root', 'pk_idm_bat_keep_structure', 'pk_aria2_url', 'pk_aria2_token', 'pk_aria2_dir', 'pk_aria2_keep_structure', 'pk_gopeed_url', 'pk_gopeed_token', 'pk_gopeed_dir', 'pk_gopeed_keep_structure', 'pk_abdm_url', 'pk_abdm_dir', 'pk_download_accel_enable', 'pk_download_accel_domain', 'pk_download_accel_mode', 'pk_download_accel_query_param', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', 'pk_search_engine', 'pk_default_video_quality', 'pk_video_load_progress_cache', 'pk_search_history', 'pk_expired_shares', 'pk_share_parse_history', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist', 'pk_dup_strictness', 'pk_skip_bl_on_del', 'pk_pwd_vault', 'pk_pwd_try_count' ]); const exportPrefixKeys = [ 'pk_scan_last_', 'pk_analyze_last_', 'pk_archive_pwd_', 'pk_duration_' ]; const alwaysExportKeys = new Set(['pk_downloader_type', 'pk_downloader_prefer_original_video_link', 'pk_idm_export_mode', 'pk_idm_url_export_type', 'pk_idm_exe_path', 'pk_idm_bat_root', 'pk_idm_bat_keep_structure', 'pk_gopeed_url', 'pk_gopeed_token', 'pk_gopeed_dir', 'pk_gopeed_keep_structure', 'pk_abdm_url', 'pk_abdm_dir', 'pk_potplayer_custom_path']); const isWhitelistedExportKey = (k) => !isCapacityProbeStorageKey(k) && (exportExactKeys.has(k) || exportPrefixKeys.some(prefix => k.startsWith(prefix))); const readStoredValue = (k) => { let v = typeof GM_getValue !== 'undefined' ? GM_getValue(k, null) : null; if ((v === null || v === undefined || v === '') && localStorage.getItem(k) !== null) v = localStorage.getItem(k); if (k === 'pk_potplayer_custom_path' && (v === null || v === undefined || v === '')) { const state = normalizePotPlayerProtocolState(gmGet(CONF.potplayerProtocolStateKey, '')); const checkedPath = validatePotPlayerCustomPath(state.customPlayerPath); if (checkedPath.ok) v = checkedPath.path; } return v; }; const pkKeys = Array.from(new Set([...keys.filter(k => isWhitelistedExportKey(k)), ...alwaysExportKeys])); const getCatWeight = (k) => { if (k.startsWith('pk_archive_pwd_') || k === 'pk_pwd_vault' || k === 'pk_pwd_try_count' || k === 'pk_share_limits') return 3; if (k.startsWith('pk_duration_')) return 4; const ruleKeys = ['pk_blacklist', 'pk_blacklist_folders', 'pk_downloader_type', 'pk_downloader_prefer_original_video_link', 'pk_idm_export_mode', 'pk_idm_url_export_type', 'pk_idm_exe_path', 'pk_idm_bat_root', 'pk_idm_bat_keep_structure', 'pk_idm_bat_add_queue', 'pk_idm_bat_start_queue', 'pk_aria2_url', 'pk_aria2_token', 'pk_aria2_dir', 'pk_aria2_keep_structure', 'pk_gopeed_url', 'pk_gopeed_token', 'pk_gopeed_dir', 'pk_gopeed_keep_structure', 'pk_abdm_url', 'pk_abdm_dir', 'pk_download_accel_enable', 'pk_download_accel_domain', 'pk_download_accel_mode', 'pk_download_accel_query_param', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', 'pk_search_engine', 'pk_search_history', 'pk_expired_shares', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist', 'pk_dup_strictness', 'pk_skip_bl_on_del', 'pk_clipboard_magnet_focus', 'pk_potplayer_custom_path']; if (ruleKeys.includes(k) || k.startsWith('pk_scan_last_') || k.startsWith('pk_analyze_last_')) return 2; return 1; }; pkKeys.sort((a, b) => { const wA = getCatWeight(a); const wB = getCatWeight(b); if (wA !== wB) return wA - wB; return a.localeCompare(b); }); pkKeys.forEach(k => { let v = readStoredValue(k); if (k === 'pk_downloader_type') v = normalizeDownloaderType(v); const hasStoredEmptyAbdmUrl = k === 'pk_abdm_url' && gmHas('pk_abdm_url') && v === ''; if ((v === null || v === undefined || (v === '' && !hasStoredEmptyAbdmUrl)) && alwaysExportKeys.has(k)) { if (k === 'pk_downloader_type') v = getCurrentDownloaderType(); else if (k === 'pk_downloader_prefer_original_video_link') v = CONF.downloaderPreferOriginalVideoLink; else if (k === 'pk_idm_export_mode') v = normalizeIdmExportMode(CONF.idmExportMode); else if (k === 'pk_idm_url_export_type') v = normalizeIdmUrlExportType(CONF.idmUrlExportType); else if (k === 'pk_idm_exe_path') v = normalizeIdmExePath(CONF.idmExePath); else if (k === 'pk_idm_bat_root') v = normalizeIdmBatDownloadRoot(CONF.idmBatDownloadRoot); else if (k === 'pk_idm_bat_keep_structure') v = CONF.aria2KeepFolderStructure; else if (k === 'pk_gopeed_url') v = normalizeGopeedApiUrl(CONF.gopeedApiUrl); else if (k === 'pk_gopeed_token') v = ''; else if (k === 'pk_gopeed_dir') v = normalizeGopeedDownloadDir(CONF.gopeedDownloadDir); else if (k === 'pk_gopeed_keep_structure') v = CONF.aria2KeepFolderStructure; else if (k === 'pk_abdm_url') v = normalizeAbdmApiUrl(CONF.abdmApiUrl); else if (k === 'pk_abdm_dir') v = normalizeAbdmDownloadDir(CONF.abdmDownloadDir); } v = normalizeConfigValue(k, v, 'localWrite'); if (v !== undefined && v !== null && (v !== '' || alwaysExportKeys.has(k))) config[k] = v; }); config._pk_metadata.exported_keys = pkKeys.length; const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const domEl = document.querySelector('.name.ellipsis'); let rawUserName = domEl ? (domEl.title || domEl.innerText) : 'Default'; const userName = rawUserName.trim().replace(/[\\/:*?"<>|]/g, '_'); const now = new Date(); const dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); const timeStr = now.getHours().toString().padStart(2, '0') + now.getMinutes().toString().padStart(2, '0'); const fileName = `PKM_Backup_${userName}_${dateStr}_${timeStr}.json`; const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); }; const fileInput = m.querySelector('#cfg_import_input'); m.querySelector('#btn_cfg_import').onclick = () => fileInput.click(); fileInput.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; if (!await showConfirm(L.msg_import_confirm)) { fileInput.value = ''; return; } const reader = new FileReader(); reader.onload = (ev) => { try { const config = JSON.parse(ev.target.result); if (!config._pk_metadata || config._pk_metadata.signature !== "PIKPAK_ENHANCEMENT_MASTER") { throw new Error("INVALID_SIGNATURE"); } const importExactKeys = new Set([ 'pk_lang', 'pk_theme', 'pk_turbo_mode', 'pk_file_view_mode', 'pk_view_independent', 'pk_folder_view_prefs', 'pk_keep_pos', 'pk_pos_left', 'pk_pos_top', 'pk_blur_thumb', 'pk_blur_scope', 'pk_hide_button_text', 'pk_comic_mode', 'pk_clipboard_magnet_focus', 'pk_clipboard_magnet_paste', 'pk_audio_play_mode', 'pk_audio_vol_level', 'pk_audio_vol_muted', 'pk_audio_mini_pos', 'pk_sort_independent', 'pk_folder_first', 'pk_folder_sort_prefs', 'pk_global_sort_pref', 'pk_ext_player', 'pk_default_open_player', 'pk_potplayer_custom_path', 'pk_play_mode', 'pk_skip_intro', 'pk_skip_outro', 'pk_vol_muted', 'pk_vol_level', 'pk_suppress_global_warn', 'pk_blacklist', 'pk_blacklist_folders', 'pk_downloader_type', 'pk_downloader_prefer_original_video_link', 'pk_idm_export_mode', 'pk_idm_url_export_type', 'pk_idm_exe_path', 'pk_idm_bat_root', 'pk_idm_bat_keep_structure', 'pk_aria2_url', 'pk_aria2_token', 'pk_aria2_dir', 'pk_aria2_keep_structure', 'pk_gopeed_url', 'pk_gopeed_token', 'pk_gopeed_dir', 'pk_gopeed_keep_structure', 'pk_abdm_url', 'pk_abdm_dir', 'pk_download_accel_enable', 'pk_download_accel_domain', 'pk_download_accel_mode', 'pk_download_accel_query_param', 'pk_dl_filter_ext', 'pk_dl_filter_name', 'pk_dl_filter_size_min', 'pk_dl_filter_size_max', 'pk_dl_filter_size_unit', 'pk_search_engine', 'pk_default_video_quality', 'pk_video_load_progress_cache', 'pk_search_history', 'pk_expired_shares', 'pk_share_parse_history', 'pk_share_limits', 'pk_bn_find_hist', 'pk_bn_rep_hist', 'pk_dup_strictness', 'pk_skip_bl_on_del', 'pk_pwd_vault', 'pk_pwd_try_count' ]); const importPrefixKeys = [ 'pk_scan_last_', 'pk_analyze_last_', 'pk_archive_pwd_', 'pk_duration_' ]; const isWhitelistedImportKey = (k) => !isCapacityProbeStorageKey(k) && (importExactKeys.has(k) || importPrefixKeys.some(prefix => k.startsWith(prefix))); const readStoredValue = (k) => { let v = typeof GM_getValue !== 'undefined' ? GM_getValue(k, null) : null; if ((v === null || v === undefined || v === '') && localStorage.getItem(k) !== null) v = localStorage.getItem(k); return v; }; const writeStoredValue = (k, v) => { const normalized = normalizeConfigValue(k, v, 'import'); if (normalized === undefined) { removeStoredConfigKey(k); return; } if (typeof GM_setValue !== 'undefined') GM_setValue(k, normalized); else localStorage.setItem(k, typeof normalized === 'string' ? normalized : JSON.stringify(normalized)); if (k === 'pk_turbo_mode') localStorage.setItem(k, String(normalized)); if (k === 'pk_potplayer_custom_path') { const state = readPotPlayerProtocolState(); state.customPlayerPath = normalized; writePotPlayerProtocolState(state); } if (shouldCleanupAfterConfigWrite(k)) scheduleConfigPrefixCleanup('importWrite'); }; const parseMaybeJson = (v) => { if (typeof v === 'string') { const s = v.trim(); if ((s.startsWith('[') && s.endsWith(']')) || (s.startsWith('{') && s.endsWith('}'))) { try { return JSON.parse(s); } catch {} } } return v; }; const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v); const mergeArrayUnique = (localArr, importedArr, limit = 100) => { const seen = new Set(); const merged = []; [...localArr, ...importedArr].forEach(item => { const key = (item && typeof item === 'object') ? JSON.stringify(item) : `v:${String(item)}`; if (seen.has(key)) return; seen.add(key); merged.push(item); }); return merged.slice(0, limit); }; const mergeShareParseHistoryImport = (localObj, importedObj) => { const toList = (v) => Array.isArray(v) ? v : (isPlainObject(v) && Array.isArray(v.items) ? v.items : []); const merged = []; const index = new Map(); const makeKeys = (rec) => [`id:${rec.id}`, `key:${rec.key}`, `share:${getShareParseHistoryKey(rec.share_id, rec.pass_code)}`].filter(k => !k.endsWith(':')); const reindex = (rec) => makeKeys(rec).forEach(k => index.set(k, rec)); const pickText = (a, b) => String(a || '').trim() ? a : b; const statusTime = (rec) => Math.max(parseShareParseHistoryTime(rec && rec.last_check_at), parseShareParseHistoryTime(rec && rec.last_error_at)); const syncStatus = (target, rec) => { if (statusTime(rec) <= statusTime(target)) return; target.last_check_at = parseShareParseHistoryTime(rec.last_check_at); target.last_status = normalizeShareParseHistoryStatus(rec.last_status); target.last_error_key = String(rec.last_error_key || ''); target.last_error_msg = String(rec.last_error_msg || ''); target.last_error_at = parseShareParseHistoryTime(rec.last_error_at); }; const mergeInto = (target, rec) => { target.title = pickText(target.title, rec.title); target.raw = pickText(target.raw, rec.raw); target.share_user = pickText(target.share_user, rec.share_user); target.note = pickText(target.note, rec.note); target.first_success_at = Math.min(target.first_success_at || rec.first_success_at, rec.first_success_at || target.first_success_at); target.last_success_at = Math.max(target.last_success_at || 0, rec.last_success_at || 0); target.last_open_at = Math.max(target.last_open_at || 0, rec.last_open_at || 0); target.success_count = Math.max(Number(target.success_count) || 1, Number(rec.success_count) || 1); target.root_file_count = Math.max(Number(target.root_file_count) || 0, Number(rec.root_file_count) || 0); target.root_folder_count = Math.max(Number(target.root_folder_count) || 0, Number(rec.root_folder_count) || 0); target.root_total_count = Math.max(Number(target.root_total_count) || 0, Number(rec.root_total_count) || 0); target.pinned = !!(target.pinned || rec.pinned); syncStatus(target, rec); reindex(target); }; [...toList(localObj), ...toList(importedObj)].forEach(item => { const rec = normalizeShareParseHistoryRecord(item); if (!rec) return; const hit = makeKeys(rec).map(k => index.get(k)).find(Boolean); if (hit) { mergeInto(hit, rec); return; } merged.push(rec); reindex(rec); }); return sortShareParseHistory(merged); }; const importKeys = Object.keys(config).filter(k => isWhitelistedImportKey(k)); importKeys.forEach(k => { let importedVal = config[k]; if (k === 'pk_downloader_type') importedVal = normalizeDownloaderType(importedVal); if (k === 'pk_idm_export_mode') importedVal = normalizeIdmExportMode(importedVal); if (k === 'pk_idm_url_export_type') importedVal = normalizeIdmUrlExportType(importedVal); if (k === 'pk_idm_exe_path') importedVal = normalizeIdmExePath(importedVal); if (k === 'pk_idm_bat_root') importedVal = normalizeIdmBatDownloadRoot(importedVal); if (k === 'pk_abdm_url' && String(importedVal || '').trim()) importedVal = normalizeAbdmApiUrl(importedVal); if (k === 'pk_abdm_dir') importedVal = normalizeAbdmDownloadDir(importedVal); if (k === 'pk_potplayer_custom_path') { const checkedPath = validatePotPlayerCustomPath(importedVal); if (!checkedPath.ok) return; importedVal = checkedPath.path; } const localVal = readStoredValue(k); if (k === SHARE_PARSE_HISTORY_KEY) { writeStoredValue(k, JSON.stringify(mergeShareParseHistoryImport(parseMaybeJson(localVal), parseMaybeJson(importedVal)))); return; } if (localVal === null || localVal === undefined || localVal === '') { writeStoredValue(k, importedVal); return; } try { if (k === 'pk_blacklist' || k === 'pk_blacklist_folders') { const localSet = new Set(String(localVal || '').split('\n').map(s => s.trim()).filter(s => s)); const importedSet = new Set(String(importedVal || '').split('\n').map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); writeStoredValue(k, Array.from(localSet).join('\n')); return; } if (k === 'pk_dl_filter_ext' || k === 'pk_dl_filter_name') { const localSet = new Set(String(localVal || '').split(/[,,\n]/).map(s => s.trim()).filter(s => s)); const importedSet = new Set(String(importedVal || '').split(/[,,\n]/).map(s => s.trim()).filter(s => s)); importedSet.forEach(v => localSet.add(v)); writeStoredValue(k, Array.from(localSet).join(', ')); return; } const localObj = parseMaybeJson(localVal); const importedObj = parseMaybeJson(importedVal); if (k === 'pk_pwd_vault') { const localArr = Array.isArray(localObj) ? localObj : []; const importedArr = Array.isArray(importedObj) ? importedObj : []; const map = new Map(); localArr.forEach(x => { const p = typeof x === 'object' ? x.p : x; const h = typeof x === 'object' ? (Number(x.h) || 0) : 0; if (p) map.set(p, { p, h }); }); importedArr.forEach(x => { const p = typeof x === 'object' ? x.p : x; const h = typeof x === 'object' ? (Number(x.h) || 0) : 0; if (!p) return; if (map.has(p)) map.get(p).h += h; else map.set(p, { p, h }); }); writeStoredValue(k, JSON.stringify(Array.from(map.values()).sort((a, b) => b.h - a.h).slice(0, 50))); return; } if (k === 'pk_expired_shares') { const localArr = Array.isArray(localObj) ? localObj : []; const importedArr = Array.isArray(importedObj) ? importedObj : []; const map = new Map(); localArr.forEach(x => { if (x && x.id) map.set(x.id, x); }); importedArr.forEach(x => { if (x && x.id) map.set(x.id, x); }); writeStoredValue(k, JSON.stringify(Array.from(map.values()))); return; } if (Array.isArray(localObj) && Array.isArray(importedObj)) { writeStoredValue(k, JSON.stringify(mergeArrayUnique(localObj, importedObj, 100))); return; } if (isPlainObject(localObj) && isPlainObject(importedObj)) { writeStoredValue(k, JSON.stringify(Object.assign({}, localObj, importedObj))); return; } writeStoredValue(k, importedVal); } catch (e2) { writeStoredValue(k, importedVal); } }); cleanupConfigPrefixKeys(); showToast(L.msg_import_success); fileInput.value = ''; setTimeout(() => location.reload(), 1500); } catch (err) { let errorTip = ""; if (err.message === "INVALID_SIGNATURE") { errorTip = L.err_invalid_config; } else { errorTip = L.err_json_format; console.error("[Config Import]", err); } showAlert(errorTip, L.str_error); fileInput.value = ''; } }; reader.readAsText(file); }; const thumbScopeWrap = m.querySelector('#cs_thumb_scope'); const thumbScopeInput = m.querySelector('#set_thumb_scope'); const thumbScopeTrigger = m.querySelector('#cs_thumb_scope .pk-select-trigger'); const thumbScopeMenu = m.querySelector('#cs_thumb_scope .pk-select-menu'); const thumbScopeTxt = m.querySelector('#txt_thumb_scope'); if (thumbScopeWrap && thumbScopeInput && thumbScopeTrigger && thumbScopeMenu && thumbScopeTxt) { thumbScopeTrigger.onclick = (e) => { e.stopPropagation(); thumbScopeMenu.style.display = thumbScopeMenu.style.display === 'block' ? 'none' : 'block'; }; m.querySelectorAll('#cs_thumb_scope .pk-select-item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); m.querySelectorAll('#cs_thumb_scope .pk-select-item').forEach(i => i.classList.remove('act')); item.classList.add('act'); thumbScopeInput.value = item.dataset.val; thumbScopeTxt.textContent = item.textContent; thumbScopeMenu.style.display = 'none'; }; }); m.addEventListener('click', () => { thumbScopeMenu.style.display = 'none'; }); } m.querySelector('#set_cancel').onclick = () => m.remove(); m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#set_save').click(); } }); m.querySelector('#set_save').onclick = async () => { const newTurbo = m.querySelector('#set_turbo').checked; const oldTurbo = gmGet('pk_turbo_mode', false); const newUrl = m.querySelector('#set_aria_url').value.trim(); const newToken = m.querySelector('#set_aria_token').value.trim(); const newAriaDir = normalizeAriaDownloadDir(m.querySelector('#set_aria_dir').value); const newAriaKeepStructure = getDownloaderKeepStructureChecked(); const newDownloaderType = normalizeDownloaderType(selectedDownloaderType); const rawGopeedUrl = m.querySelector('#set_gopeed_url').value.trim(); const newGopeedUrl = rawGopeedUrl ? normalizeGopeedApiUrl(rawGopeedUrl) : ''; const newGopeedToken = m.querySelector('#set_gopeed_token').value.trim(); const newGopeedDir = normalizeGopeedDownloadDir(m.querySelector('#set_gopeed_dir').value); const newGopeedKeepStructure = newAriaKeepStructure; const rawAbdmUrl = m.querySelector('#set_abdm_url').value.trim(); const newAbdmUrl = rawAbdmUrl ? normalizeAbdmApiUrl(rawAbdmUrl) : ''; const newAbdmDir = normalizeAbdmDownloadDir(m.querySelector('#set_abdm_dir').value); const newIdmExportMode = normalizeIdmExportMode(selectedIdmExportMode); const newIdmUrlExportType = normalizeIdmUrlExportType(selectedIdmUrlExportType); const newIdmExePath = normalizeIdmExePath(m.querySelector('#set_idm_exe_path').value); const newIdmBatRoot = normalizeIdmBatDownloadRoot(m.querySelector('#set_idm_bat_root').value); const newIdmBatKeepStructure = m.querySelector('#set_idm_bat_keep_structure').checked; const newDownloaderPreferOriginalVideoLink = getDownloaderPreferOriginalVideoLinkChecked(); const newDownloadAccelEnable = m.querySelector('#set_download_accel_enable').checked; const rawDownloadAccelDomain = m.querySelector('#set_download_accel_domain').value.trim(); const newDownloadAccelDomain = normalizeDownloadAccelBase(rawDownloadAccelDomain); const newDownloadAccelMode = normalizeDownloadAccelMode(selectedDownloadAccelMode); const newDownloadAccelQueryParam = normalizeDownloadAccelQueryParam(m.querySelector('#set_download_accel_query_param').value); const newBlurScope = m.querySelector('#set_thumb_scope').value; const newHideButtonText = m.querySelector('#set_hide_button_text').checked; const newKeepPos = m.querySelector('#set_keep_pos').checked; const newSkipBl = m.querySelector('#set_skip_bl').checked; const newClipboardMagnetFocus = m.querySelector('#set_clipboard_magnet_focus').checked; const newComicMode = m.querySelector('#set_comic_mode').checked; const sortPref = m.querySelector('input[name="set_sort_pref"]:checked').value; const viewPref = m.querySelector('input[name="set_view_pref"]:checked').value; const saveBtn = m.querySelector('#set_save'); const storedDownloaderType = String(gmGet('pk_downloader_type', CONF.downloaderType) || '').trim().toLowerCase(); const oldDownloaderType = normalizeDownloaderType(storedDownloaderType); const downloaderTypeNeedsNormalize = storedDownloaderType && storedDownloaderType !== oldDownloaderType; const oldKeepStructure = getDownloaderKeepStructurePref(); const oldSig = JSON.stringify([curLang, oldTurbo, oldDownloaderType, gmGet('pk_aria2_url', ''), gmGet('pk_aria2_token', ''), normalizeAriaDownloadDir(gmGet('pk_aria2_dir', CONF.aria2DownloadDir)), oldKeepStructure, curGopeedUrl, gmGet('pk_gopeed_token', ''), normalizeGopeedDownloadDir(gmGet('pk_gopeed_dir', CONF.gopeedDownloadDir)), oldKeepStructure, curAbdmUrl, normalizeAbdmDownloadDir(gmGet('pk_abdm_dir', CONF.abdmDownloadDir)), oldKeepStructure, getBoolPref('pk_downloader_prefer_original_video_link', CONF.downloaderPreferOriginalVideoLink), getBoolPref('pk_download_accel_enable', CONF.downloadAccelEnable), normalizeDownloadAccelBase(gmGet('pk_download_accel_domain', CONF.downloadAccelDomain)), normalizeDownloadAccelMode(gmGet('pk_download_accel_mode', CONF.downloadAccelMode)), normalizeDownloadAccelQueryParam(gmGet('pk_download_accel_query_param', CONF.downloadAccelQueryParam)), gmGet('pk_blur_scope', gmGet('pk_blur_thumb', false) ? 'list' : 'off'), gmGet('pk_hide_button_text', false), gmGet('pk_keep_pos', true), gmGet('pk_skip_bl_on_del', true), gmGet('pk_clipboard_magnet_focus', true), gmGet('pk_comic_mode', true), gmGet('pk_sort_independent', false) ? 'indep' : 'global', gmGet('pk_view_independent', false) ? 'indep' : 'global', curEngine, curDefaultOpenPlayer, curDefaultVideoQuality, curVideoLoadProgressCache, gmGet('pk_dl_filter_ext', ''), gmGet('pk_dl_filter_size_min', ''), gmGet('pk_dl_filter_size_max', ''), gmGet('pk_dl_filter_size_unit', 'MB'), gmGet('pk_dl_filter_name', '')]); const newSig = JSON.stringify([selectedLang, newTurbo, newDownloaderType, newUrl, newToken, newAriaDir, newAriaKeepStructure, newGopeedUrl, newGopeedToken, newGopeedDir, newAriaKeepStructure, newAbdmUrl, newAbdmDir, newAriaKeepStructure, newDownloaderPreferOriginalVideoLink, newDownloadAccelEnable, newDownloadAccelDomain, newDownloadAccelMode, newDownloadAccelQueryParam, newBlurScope, newHideButtonText, newKeepPos, newSkipBl, newClipboardMagnetFocus, newComicMode, sortPref, viewPref, selectedEngine, normalizeDefaultOpenPlayer(selectedDefaultOpenPlayer), normalizeDefaultVideoQuality(selectedDefaultVideoQuality), !!m.querySelector('#set_video_load_progress_cache').checked, m.querySelector('#set_dl_filter_ext').value.trim(), m.querySelector('#set_dl_filter_size_min').value.trim(), m.querySelector('#set_dl_filter_size_max').value.trim(), m.querySelector('#cs_set_dl_size_unit .pk-select-item.act') ? m.querySelector('#cs_set_dl_size_unit .pk-select-item.act').dataset.val : 'MB', m.querySelector('#set_dl_filter_name').value.trim()]); const oldIdmSig = JSON.stringify([normalizeIdmExportMode(gmGet('pk_idm_export_mode', CONF.idmExportMode)), normalizeIdmUrlExportType(gmGet('pk_idm_url_export_type', CONF.idmUrlExportType)), normalizeIdmExePath(gmGet('pk_idm_exe_path', CONF.idmExePath)), normalizeIdmBatDownloadRoot(gmGet('pk_idm_bat_root', CONF.idmBatDownloadRoot)), oldKeepStructure]); const newIdmSig = JSON.stringify([newIdmExportMode, newIdmUrlExportType, newIdmExePath, newIdmBatRoot, newAriaKeepStructure]); const hasSettingsChanged = oldSig !== newSig || oldIdmSig !== newIdmSig || downloaderTypeNeedsNormalize; const reloadSettingsChanged = curLang !== selectedLang || newTurbo !== oldTurbo; const ariaChanged = newUrl !== gmGet('pk_aria2_url', '') || newToken !== gmGet('pk_aria2_token', ''); if (rawDownloadAccelDomain && !newDownloadAccelDomain) { const domainInp = m.querySelector('#set_download_accel_domain'); if (domainInp) domainInp.style.borderColor = '#d93025'; showToast(L.msg_download_accel_invalid_domain, 'error'); return; } if (!hasSettingsChanged) { m.remove(); return; } const applyChangesAndClose = async () => { gmSet('pk_blur_scope', newBlurScope); gmSet('pk_blur_thumb', newBlurScope !== 'off'); gmSet('pk_hide_button_text', newHideButtonText); if (el) { el.classList.toggle('pk-hide-btn-text', newHideButtonText); if (typeof refreshQuotaText === 'function') refreshQuotaText(); } gmSet('pk_keep_pos', newKeepPos); gmSet('pk_comic_mode', newComicMode); gmSet('pk_clipboard_magnet_focus', newClipboardMagnetFocus); gmSet('pk_skip_bl_on_del', newSkipBl); const wasIndep = gmGet('pk_sort_independent', false); const isIndep = (sortPref === 'indep'); gmSet('pk_sort_independent', isIndep); if (wasIndep !== isIndep) { S._sortContextSig = null; S._sortManualSig = null; if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); syncFolderFirstFromGlobal(); } const isViewIndep = (viewPref === 'indep'); gmSet('pk_view_independent', isViewIndep); persistViewPreference(); await gmSetAsync('pk_lang', selectedLang); gmSet('pk_search_engine', selectedEngine); gmSet('pk_default_open_player', normalizeDefaultOpenPlayer(selectedDefaultOpenPlayer)); gmSet('pk_default_video_quality', normalizeDefaultVideoQuality(selectedDefaultVideoQuality)); gmSet('pk_video_load_progress_cache', !!m.querySelector('#set_video_load_progress_cache').checked); gmSet('pk_turbo_mode', newTurbo); gmSet('pk_dl_filter_ext', m.querySelector('#set_dl_filter_ext').value.trim()); gmSet('pk_dl_filter_size_min', m.querySelector('#set_dl_filter_size_min').value.trim()); gmSet('pk_dl_filter_size_max', m.querySelector('#set_dl_filter_size_max').value.trim()); gmSet('pk_dl_filter_size_unit', m.querySelector('#cs_set_dl_size_unit .pk-select-item.act') ? m.querySelector('#cs_set_dl_size_unit .pk-select-item.act').dataset.val : 'MB'); gmSet('pk_dl_filter_name', m.querySelector('#set_dl_filter_name').value.trim()); gmSet('pk_download_accel_enable', newDownloadAccelEnable); gmSet('pk_download_accel_domain', newDownloadAccelDomain); gmSet('pk_download_accel_mode', newDownloadAccelMode); gmSet('pk_download_accel_query_param', newDownloadAccelQueryParam); gmSet('pk_downloader_type', newDownloaderType); gmSet('pk_aria2_url', newUrl); gmSet('pk_aria2_token', newToken); gmSet('pk_aria2_dir', newAriaDir); gmSet('pk_aria2_keep_structure', newAriaKeepStructure); gmSet('pk_gopeed_url', newGopeedUrl); gmSet('pk_gopeed_token', newGopeedToken); gmSet('pk_gopeed_dir', newGopeedDir); gmSet('pk_gopeed_keep_structure', newAriaKeepStructure); gmSet('pk_abdm_url', newAbdmUrl); gmSet('pk_abdm_dir', newAbdmDir); gmSet('pk_downloader_prefer_original_video_link', newDownloaderPreferOriginalVideoLink); gmSet('pk_idm_export_mode', newIdmExportMode); gmSet('pk_idm_url_export_type', newIdmUrlExportType); gmSet('pk_idm_exe_path', newIdmExePath); gmSet('pk_idm_bat_root', newIdmBatRoot); gmSet('pk_idm_bat_keep_structure', newAriaKeepStructure); if (curLang !== selectedLang) { await ensureI18nReady(true, selectedLang); } m.remove(); if (typeof updateDownloaderButtonLabel === 'function') updateDownloaderButtonLabel(); const savedPack = (pkRemoteI18n && pkRemoteI18n.lang === selectedLang) ? pkRemoteI18n.data : T_LOCAL.zh; const savedMsgKey = reloadSettingsChanged ? 'msg_settings_saved' : 'msg_settings_saved_plain'; showToast((savedPack && savedPack[savedMsgKey]) || T_LOCAL.zh[savedMsgKey] || T_LOCAL.zh.msg_settings_saved); if (curLang !== selectedLang) { setTimeout(() => location.reload(), 500); return; } if (newTurbo !== oldTurbo) { setTimeout(() => location.reload(), 300); return; } if (curLang !== selectedLang) { let safePath = [...S.path]; if (safePath.some(n => n.id === 'virtual_search_root' || n.id === 'analyze_root')) { safePath = S.preSearchPath || [{ id: '', name: (savedPack && savedPack.btn_nav_home) || L.btn_nav_home }]; } globalSavedState = { path: safePath, trashMode: S.trashMode, shareMode: S.shareMode, shareParseMode: S.shareParseMode, linkBookmarkMode: S.linkBookmarkMode, starredMode: S.starredMode, recentMode: S.recentMode, historyMode: S.historyMode, offlineMode: S.offlineMode, uploadMode: S.uploadMode, isMaximized: UI.win.classList.contains('pk-maximized'), scrollTop: UI.vp ? UI.vp.scrollTop : 0, uploadTasks: S.uploadTasks }; document.removeEventListener('keydown', keyHandler); document.removeEventListener('mouseup', mouseHandler); if (typeof destroyTooltip === 'function') destroyTooltip(); if (visibilityListener && visibilityListener.abort) visibilityListener.abort(); el.remove(); await ensureI18nReadyBeforeOpen(selectedLang); await openManager(S.cache, S.preLoadPromise); } else { renderVisible(); } }; if (saveBtn.dataset.pkSaving === '1') return; saveBtn.dataset.pkSaving = '1'; saveBtn.disabled = true; saveBtn.textContent = L.str_saving; await applyChangesAndClose(); return; }; }; 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; `; if (document.querySelector('.pk-ov')?.classList.contains('pk-dark')) pop.classList.add('pk-dark'); pop.innerHTML = `
${CONF.icons.settings} ${L.btn_settings}
${CONF.icons.logout} ${L.btn_logout}
`; document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; const rect = getLogicalRect(UI.btnSettings); const winEl = document.querySelector('.pk-win'); const isMax = winEl && winEl.classList.contains('pk-maximized'); let popLeft, popBottom; if (isMax) { popLeft = rect.left; popBottom = window.innerHeight - rect.top + 8; } else { popLeft = rect.right + 10; popBottom = window.innerHeight - rect.bottom; } pop.style.bottom = popBottom + 'px'; pop.style.left = popLeft + 'px'; }; updatePosition(); window.addEventListener('resize', updatePosition); const cleanup = () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('mousedown', closer); pop.remove(); }; const closer = (ev) => { if (!pop.contains(ev.target) && !UI.btnSettings.contains(ev.target)) cleanup(); }; setTimeout(() => document.addEventListener('mousedown', closer), 10); pop.querySelector('#pk-set-menu-settings').onclick = (ev) => { ev.stopPropagation(); cleanup(); openSettingsModal(); }; pop.querySelector('#pk-set-menu-logout').onclick = async (ev) => { ev.stopPropagation(); cleanup(); if (await showConfirm(L.msg_logout_confirm)) { const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && (k.startsWith('credentials') || k.startsWith('captcha') || k === 'pk_captured_captcha')) { keysToRemove.push(k); } } keysToRemove.forEach(k => localStorage.removeItem(k)); if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); if (!location.href.includes('/login')) window.location.href = 'https://mypikpak.com/drive/login'; } }; }; const ctx = el.querySelector('#pk-ctx'); if (!window.pkPropCache) window.pkPropCache = new Map(); ctx.querySelector('#ctx-property').onclick = async () => { ctx.style.display = 'none'; const id = S.getSelectedIds()[0]; if (!id) return; let item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.loading_detail); const isFolder = item.kind === 'drive#folder'; try { const freshData = await apiGet(id); item = { ...item, ...freshData }; } catch(e) {} if (isFolder) { const localFMod = gmGet('pk_fmod_' + item.id); if (localFMod) item.modified_time = localFMod; } setLoad(false); let cachedProp = window.pkPropCache.get(item.id); if (!cachedProp) { cachedProp = { size: BigInt(0), fileCount: 0, folderCount: 0, isDone: false, scanned: new Set(), isRunning: false }; window.pkPropCache.set(item.id, cachedProp); } let realSize = isFolder ? cachedProp.size : BigInt(item.size || 0); let fileCount = isFolder ? cachedProp.fileCount : 0; let folderCount = isFolder ? cachedProp.folderCount : 0; if (isFolder && item.usage && item.usage.size && item.usage.size !== "0") { realSize = BigInt(item.usage.size); fileCount = item.usage.file_count; folderCount = item.usage.folder_count; cachedProp.size = realSize; cachedProp.fileCount = fileCount; cachedProp.folderCount = folderCount; cachedProp.isDone = true; } const countStr = isFolder ? L.fmt_prop_count.replace('{f}', fileCount).replace('{d}', folderCount) : "-"; const formatTime = (iso) => fmtDate ? fmtDate(iso) : iso; let sourceStr = L.str_prop_unknown; let magnetLink = item.params?.url || item.audit?.source_url || ""; if (magnetLink) { sourceStr = L.str_prop_cloud; } else { sourceStr = L.str_prop_user; } const btnStyle = "margin-left:10px; padding:2px 8px; font-size:12px; background:var(--pk-pri); color:#fff; border:none; border-radius:4px; cursor:pointer; height:24px; white-space:nowrap;"; const rowStyle = "display:flex; align-items:center; margin-bottom:12px; font-size:13px; line-height:1.5;"; const labelStyle = "width:80px; color:#888; flex-shrink:0;"; const valStyle = "color:var(--pk-fg); flex:1; word-break:break-all;"; const mkRow = (lbl, val, copyVal = null, isHtml = false, valId = null) => { if (!val && val !== 0 && val !== "-") return ""; let btnHtml = copyVal ? `` : ""; return `
${lbl}:
${isHtml ? val : esc(val)}
${btnHtml}
`; }; let pathRowHtml = ""; const isSpecialView = S.shareMode || S.linkBookmarkMode || 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 = pkIconHtml('home', { width: 15, height: 15, style: 'margin-right:4px;flex-shrink:0;vertical-align:-2.5px;' }); const restPath = pathStr.substring(homeText.length); const homeGroup = `${homeSvg}${esc(homeText)}`; pathDisplayHtml = `
${homeGroup}${esc(restPath)}
`; } pathRowHtml = mkRow(L.lbl_prop_path, pathDisplayHtml, pathStr, true); } const html = `
${mkRow(L.lbl_prop_name, item.name)} ${mkRow(L.lbl_prop_size, fmtSize(realSize.toString()) || "0 B", null, false, 'pk_prop_size_val')} ${isFolder ? mkRow(L.lbl_prop_count, countStr, null, false, 'pk_prop_count_val') : ''} ${mkRow(L.lbl_prop_ctime, formatTime(item.created_time))} ${mkRow(L.lbl_prop_mtime, formatTime(item.modified_time))} ${mkRow(L.lbl_prop_source, sourceStr)} ${magnetLink ? mkRow(L.lbl_prop_link, magnetLink, magnetLink) : ''} ${pathRowHtml}
`; const m = showModal(`

${L.title_property}

${html} `); m.querySelectorAll('.pk-btn-copy-prop').forEach(btn => { btn.onclick = (e) => { const txt = e.target.getAttribute('data-val'); GM_setClipboard(txt); const oldTxt = e.target.textContent; e.target.textContent = "OK"; e.target.style.background = "#4CAF50"; setTimeout(() => { e.target.textContent = oldTxt; e.target.style.background = "var(--pk-pri)"; }, 1500); }; }); if (isFolder && !cachedProp.isDone) { const sizeEl = m.querySelector('#pk_prop_size_val'); const countEl = m.querySelector('#pk_prop_count_val'); let lastUiUpdateTime = performance.now(); const updateUI = () => { const now = performance.now(); if (now - lastUiUpdateTime > 80) { if (sizeEl && sizeEl.isConnected) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl && countEl.isConnected) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); lastUiUpdateTime = now; } }; if (!cachedProp.isRunning) { cachedProp.isRunning = true; const localAbortCtrl = new AbortController(); const rootNodes =[{ id: item.id, name: item.name, lineage: [], retryCount: 0 }]; coreRecursiveEngine(rootNodes, { signal: localAbortCtrl.signal, onFolder: (f) => { if (f.id !== item.id && !cachedProp.scanned.has(f.id)) { cachedProp.folderCount++; cachedProp.scanned.add(f.id); } updateUI(); }, onFile: (f) => { if (!cachedProp.scanned.has(f.id)) { cachedProp.fileCount++; cachedProp.size += BigInt(f.size || 0); cachedProp.scanned.add(f.id); } updateUI(); }, onProgress: () => {} }).then(() => { cachedProp.isDone = true; cachedProp.isRunning = false; if (sizeEl && sizeEl.isConnected) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl && countEl.isConnected) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); }).catch(e => { cachedProp.isRunning = false; }); } else { const timer = setInterval(() => { if (!document.contains(m)) { clearInterval(timer); return; } if (sizeEl) sizeEl.textContent = fmtSize(cachedProp.size.toString()) || "0 B"; if (countEl) countEl.textContent = L.fmt_prop_count.replace('{f}', cachedProp.fileCount).replace('{d}', cachedProp.folderCount); if (cachedProp.isDone) clearInterval(timer); }, 200); } } }; const btnLocate = ctx.querySelector('#ctx-locate'); if (btnLocate) { btnLocate.onclick = async () => { ctx.style.display = 'none'; const id = S.getSelectedIds()[0]; if (!id) return; let item = S.itemMap.get(id); if (!item) return; setLoad(true); updateLoadTxt(L.str_loc_tracing); try { if (item.kind === 'drive#task' || S.recentMode || S.uploadMode) { const lookupId = (item.kind === 'drive#task' || S.uploadMode) ? item.file_id : item.id; if (!lookupId) { showToast(L.err_folder_not_ready, 'error'); setLoad(false); return; } try { item = await apiGet(lookupId); } catch (e) { const errText = e.message || ""; const markedMissing = await markOfflineReferenceMissingFromError(item, e, { source: 'locate' }); if (markedMissing) { showToast(getOfflineReferenceMissingText(), 'error'); } else 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) { 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.shareParseMode || S.linkBookmarkMode || S.isFlattened || S.dupMode || S.analyzeMode; stopLiveManualRefreshBeforePathChange('path_change'); if (S.shareParseMode) resetShareParseListState(false); S.starredMode = false; S.trashMode = false; S.shareMode = false; S.shareParseMode = false; S.linkBookmarkMode = 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.btnNavShareParse) UI.btnNavShareParse.classList.remove('act'); if (UI.btnNavLinkBookmark) UI.btnNavLinkBookmark.classList.remove('act'); if (UI.btnNavOffline) UI.btnNavOffline.classList.remove('act'); if (UI.btnNavUpload) UI.btnNavUpload.classList.remove('act'); if (UI.btnNavHome) UI.btnNavHome.classList.add('act'); [UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnExportM3U, UI.btnImgSearch].forEach(b => { if(b) b.style.display = 'inline-flex'; }); [UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll].forEach(b => { if(b) b.style.display = 'none'; }); const upSep = document.getElementById('pk-up-sep'); if(upSep) upSep.style.display = 'none'; [UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnMigrate, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; if(UI.actionBar) UI.actionBar.style.display = 'flex'; if(UI.win) UI.win.classList.remove('pk-link-bookmark-mode'); const mainHeader = UI.win ? UI.win.querySelector('.pk-grid-hd') : null; if (mainHeader) mainHeader.style.display = ''; if(UI.searchInput && UI.searchInput.parentNode) UI.searchInput.parentNode.style.display = 'flex'; S.path = pathChain; syncLocalUploadVisibility(); if (UI.lblGlobal) UI.lblGlobal.style.display = 'flex'; if (UI.chkGlobal && needsRestoreGlobalCheck && typeof S.wasGlobalChecked !== 'undefined') { UI.chkGlobal.checked = S.wasGlobalChecked; } if (UI.scan) UI.scan.style.display = 'flex'; if (UI.btnAnalyze) UI.btnAnalyze.style.display = 'flex'; if (UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (UI.cntFolderFirst) UI.cntFolderFirst.style.display = 'flex'; if (UI.btnNewFolder) UI.btnNewFolder.style.display = 'inline-flex'; if (UI.btnPaste) UI.btnPaste.style.display = S.clipItems && S.clipItems.length > 0 ? 'inline-flex' : 'inline-flex'; if (typeof applyResolvedSortState === 'function') applyResolvedSortState(targetContextId || null); const locateTargetViewMode = (typeof resolvePreferredViewMode === 'function') ? resolvePreferredViewMode(targetContextId || 'root') : (gmGet('pk_file_view_mode', 'grid') === 'list' ? 'list' : 'grid'); const shouldHoldLocateGridSync = needsRestoreGlobalCheck && locateTargetViewMode === 'grid'; if (shouldHoldLocateGridSync) { S._folderViewSyncHold = true; S._locateGridSyncPending = true; beginFolderViewSync(); if (UI.vp) { UI.vp.style.visibility = 'hidden'; UI.vp.style.opacity = '0'; UI.vp.style.pointerEvents = 'none'; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = 'hidden'; } await load(); let trackCount = 0; let trackerInterval = null; const releaseLocateViewSync = () => { if (!S._folderViewSyncHold && !S._locateGridSyncPending) return; const doRelease = () => { if (typeof renderVisible === 'function') renderVisible(); if (UI.vp) { void UI.vp.offsetHeight; } requestAnimationFrame(() => { requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); S._folderViewSyncHold = false; S._locateGridSyncPending = false; if (UI.vp) { UI.vp.style.visibility = ''; UI.vp.style.opacity = ''; UI.vp.style.pointerEvents = ''; } const hd = UI.win && UI.win.querySelector('.pk-grid-hd'); if (hd) hd.style.visibility = ''; S._folderViewSyncing = false; }); }); }; setTimeout(() => { requestAnimationFrame(() => { requestAnimationFrame(() => { requestAnimationFrame(doRelease); }); }); }, 120); }; const stopTracking = (releaseViewSync = true) => { if (trackerInterval) { clearInterval(trackerInterval); trackerInterval = null; } if (releaseViewSync) releaseLocateViewSync(); }; let hasTriedRecovery = false; const performLocate = () => { const currentPathNode = S.path[S.path.length - 1]; const currentContextId = currentPathNode ? (currentPathNode.id || '') : ''; if (currentContextId !== targetContextId) { stopTracking(); return; } if (!S.loading && !S.itemMap.has(item.id) && !hasTriedRecovery) { hasTriedRecovery = true; const cacheKey = S.getRealCacheKey(currentContextId); S.cache.delete(cacheKey); if (typeof globalCache !== 'undefined') globalCache.delete(cacheKey); globalDirtyFolders.add(currentContextId || 'root'); updateLoadTxt(L.str_loc_stale); load(false, true); return; } if (S.loading || !S.itemMap.has(item.id)) return; S.sel.clear(); S.sel.add(item.id); S.activeId = item.id; const targetIdx = S.display.findIndex(x => x.id === item.id); if (targetIdx !== -1) { const vpHeight = UI.vp.clientHeight; let rowTop = targetIdx * CONF.rowHeight; if (isGridView()) { syncLayoutMetrics(); const gridLayout = getGridLayout(); const cols = Math.max(1, gridLayout.cols || 1); rowTop = Math.floor(targetIdx / cols) * CONF.rowHeight; } const centerScroll = Math.max(0, rowTop - (vpHeight / 2) + (CONF.rowHeight / 2)); if (Math.abs(UI.vp.scrollTop - centerScroll) > (CONF.rowHeight / 2)) { UI.vp.scrollTop = centerScroll; } renderVisible(); const row = UI.in.querySelector(`.pk-row[data-id="${item.id}"]`); if (row) { delete row.dataset.flashing; row.style.transition = ''; row.style.backgroundColor = ''; row.style.border = ''; row.style.borderRadius = ''; row.style.boxShadow = ''; row.style.outline = ''; row.className = getRowClassName(true, true, S.movingIds.has(item.id)); const chk = row.querySelector('input[type="checkbox"]'); if (chk && !chk.checked) chk.checked = true; } } requestAnimationFrame(() => { if (typeof renderVisible === 'function') renderVisible(); releaseLocateViewSync(); }); }; performLocate(); trackerInterval = setInterval(() => { trackCount++; const limit = hasTriedRecovery ? 25 : 10; if (S.loading || trackCount <= limit) performLocate(); else stopTracking(); }, 200); } catch (e) { console.error(e); showAlert(`${L.str_error}: ${e.message}`); if (UI.vp) UI.vp.style.opacity = '1'; } finally { setLoad(false); setTimeout(() => { if (UI.vp && !S._folderViewSyncHold && !S._locateGridSyncPending) { UI.vp.style.opacity = '1'; } }, 100); } }; } ctx.querySelector('#ctx-ext-play').onclick = () => { ctx.style.display = 'none'; UI.btnExt.click(); }; ctx.querySelector('#ctx-open').onclick = () => { ctx.style.display = 'none'; const id = S.getSelectedIds()[0]; if (!id) return; const item = S.items.find(x => x.id === id); if (!item) return; if (S.offlineMode && item.phase !== 'PHASE_TYPE_COMPLETE') { const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); return; } if (S.uploadMode && item.status !== 'DONE') { if (item.file_id) { const locateBtn = document.getElementById('ctx-locate'); if (locateBtn) locateBtn.click(); } return; } if (item._isShareItem) { if (item.kind === 'drive#folder') handleShareParseFolderOpen(item); else if (isArchiveLike(item)) handleOpenShareArchive(item); else if (isAudioLikeItem(item) || isVideoLikeItem(item) || isImageLikeItem(item)) handleOpenShareFile(item); else if (isTxtFileLike(item)) handleOpenShareTextFile(item); else showUnsupportedOpenToast(); return; } if (item.kind === 'drive#folder') { if (S.loading) { if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') { stopLiveManualRefreshBeforePathChange('path_change'); } else { return; } } else { stopLiveManualRefreshBeforePathChange('path_change'); } 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 (isAudioLikeItem(item)) { playAudio(item); } else if (mime.startsWith('video')) { playVideo(item); } else if (mime.startsWith('image')) { showImage(item); } else if (!S.offlineMode && !S.uploadMode && isTxtFileLike(item)) { handleOpenTextFile(item); } else { showUnsupportedOpenToast(); } } }; const starBtnCtx = ctx.querySelector('#ctx-star'); if (starBtnCtx) { starBtnCtx.onclick = async (e) => { ctx.style.display = 'none'; const action = e.target.getAttribute('data-action'); const isStar = (action === 'star'); const rawIds = S.getSelectedIds(); const ids = rawIds.filter(id => { const it = S.itemMap.get(id); if (!it) return false; if (!S.trashMode && isSystemItem(it)) return false; const isCurrentlyStarred = !!(it.starred || (it.tags && it.tags.some(t => t.name === 'STAR'))); if (isStar && isCurrentlyStarred) return false; if (!isStar && !isCurrentlyStarred) return false; return true; }); if (ids.length === 0) { showToast(isStar ? L.msg_star_added : L.msg_unstar_done); return; } if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') { abandonLiveManualRefresh('local_mutation', { toast: 'info', detach: false }); } 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') { if (S) S.clearSelection(); let physicalParentId = folderId || 'root'; if (fileId && S.itemMap.has(fileId)) { const it = S.itemMap.get(fileId); if (!it.params) it.params = {}; it.params.global_file_kind = '1'; if (it.parent_id) physicalParentId = it.parent_id; else if (it.parent_id === '') physicalParentId = 'root'; } S.items.forEach(it => { if ((it.kind === 'drive#task' || S.offlineMode || S.uploadMode) && it.file_id === fileId) { if (!it.params) it.params = {}; it.params.global_file_kind = '1'; } }); if (skipUiRefresh) return; if (S && S.getSelectedCount() > 0) S.clearSelection(); refresh(); updateStat(); const dirtyTargets = new Set(); if (folderId) dirtyTargets.add(folderId); else dirtyTargets.add('root'); dirtyTargets.add(physicalParentId); dirtyTargets.forEach(target => { globalDirtyFolders.add(target); if (target === 'root') globalDirtyFolders.add(''); if (typeof globalCache !== 'undefined') globalCache.delete(target); if (typeof pkState !== 'undefined' && pkState && pkState.cache) pkState.cache.delete(target); if (typeof scannedFolderIds !== 'undefined') scannedFolderIds.delete(target === 'root' ? '' : target); backgroundQueue.unshift({ id: target === 'root' ? '' : target, name: 'Unzipped_Update', retryCount: 0 }); }); runBackgroundCrawler(); const curPathNode = S.path[S.path.length - 1]; const curId = curPathNode.id || 'root'; if (dirtyTargets.has(curId) || curId === 'virtual_search_root' || S.isFlattened || S.dupMode) { if (window.pkSmartRefreshTrigger) { setTimeout(() => window.pkSmartRefreshTrigger(true), 1200); } } else { dirtyTargets.forEach(target => { apiList(target === 'root' ? '' : target, 500, null, null, false, true).then(newFiles => { if (typeof globalCache !== 'undefined') globalCache.set(target, newFiles); }).catch(()=>{}); }); } return; } if (data.phase === 'PHASE_TYPE_RUNNING' || data.phase === 'PHASE_TYPE_PENDING') { setTimeout(checkStatus, 4000); } } catch (e) { setTimeout(checkStatus, 8000); } }; checkStatus(); }; const askForPassword = (fileName, errorMsg, isBatch = false) => { return new Promise((resolve) => { const txtCancel = isBatch ? L.btn_skip : L.btn_cancel; const txtConfirm = isBatch ? L.btn_ok : L.btn_view_file; const m = showModal(`
${esc(fileName)}
${CONF.typeIcons.archive.replace(/width="\d+"/, 'width="64"').replace(/height="\d+"/, 'height="64"')}
${L.title_input_pwd}
${esc(errorMsg) || L.err_pwd_simple}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) { modalBox.style.padding = '0'; modalBox.style.width = "420px"; modalBox.style.height = "420px"; modalBox.style.display = "flex"; modalBox.style.flexDirection = "column"; modalBox.style.overflow = "hidden"; } const closeBtn = m.querySelector('.pk-modal-close'); if(closeBtn) { closeBtn.style.top = "21px"; closeBtn.style.right = "24px"; closeBtn.style.color = "#999"; } const inp = m.querySelector('#retry_pwd'); const clearBtn = m.querySelector('#pwd_clear_btn'); setTimeout(() => inp.focus(), 50); inp.addEventListener('input', () => { clearBtn.style.display = inp.value ? 'flex' : 'none'; }); clearBtn.onclick = () => { inp.value = ''; clearBtn.style.display = 'none'; inp.focus(); }; clearBtn.onmouseover = () => clearBtn.style.color = '#666'; clearBtn.onmouseout = () => clearBtn.style.color = '#999'; const doResolve = (val) => { m.remove(); resolve(val); }; m.querySelector('#ask_skip').onclick = () => doResolve(null); m.querySelector('.pk-modal-close').onclick = () => doResolve(null); m.querySelector('#ask_retry').onclick = () => doResolve(inp.value); inp.onkeydown = (e) => { if(e.key === 'Enter') doResolve(inp.value); }; }); }; const isArchivePasswordSignal = (data = {}) => { const code = Number(data?.error_code ?? data?.code ?? 0); const status = String(data?.status || data?.status_code || data?.error || "").toUpperCase(); return code === 10023 || status === 'PASS_WORD_EMPTY' || status === 'PASS_WORD_ERROR' || status === 'PASSWORD_EMPTY' || status === 'PASSWORD_ERROR' || status === 'PASSWORD_REQUIRED' || status === 'INVALID_PASSWORD' || status === 'WRONG_PASSWORD' || status.includes('PASS_WORD') || status.includes('PASSWORD'); }; const isArchiveAlreadyRunningSignal = (data = {}) => { const status = String(data?.status || data?.status_code || data?.phase || data?.error || "").toUpperCase(); return status === 'RUNNING_TASK' || status === 'TASK_RUNNING' || status === 'ALREADY_RUNNING' || status === 'DECOMPRESS_RUNNING' || status === 'UNZIP_RUNNING' || status === 'PHASE_TYPE_RUNNING' || status === 'PHASE_TYPE_PENDING' || (!!data?.task_id && (status.includes('RUNNING') || status.includes('PENDING') || status === 'RUNNING_TASK')); }; 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 || data.msg || data.error || "").toLowerCase(); const isPwdRequired = isArchivePasswordSignal(data, errStr); 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) { 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) { } 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); 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); let created = null; let taskExists = false; try { created = await apiAddOfflineTask(magnet, file.parent_id || ""); } catch (reqErr) { if (isOfflineTaskAlreadyExistsError(reqErr)) { taskExists = true; handleOfflineTaskAddError(reqErr, { source: 'archive', reason: 'add_exists' }); } else { throw reqErr; } } showToast(L.msg_cloud_task_success.replace('{n}', 1)); if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (S.offlineMode) { if (created) await handleOfflineTaskAdded(created, { source: 'archive', batch: false, schedule: false }); scheduleOfflineTaskAddProbe(taskExists ? 'add_exists' : 'add', 1000); } } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { fb.destroy(); } }; const sendUnzipRequest = async (file, password, archiveFiles = []) => { 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 pickedFiles = Array.isArray(archiveFiles) ? archiveFiles.filter(Boolean).map(f => Object.assign({}, f, { checked: true })) : []; const payload = { file_id: file.id, files: pickedFiles, 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 = isArchivePasswordSignal(data, rawMsg); 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 = "", archiveOpts = {}) => { let currentPath = ""; let pathNodes = [{ name: L.picker_all, path: '' }]; let currentPwd = initialPassword; const archiveName = archiveOpts.title || file.name || L.str_unknown_name; const getArchiveGcid = async () => { if (archiveOpts.gcid) return archiveOpts.gcid; let detail = file; if (!detail.gcid && !detail.hash) { try { detail = await apiGet(file.id); } catch(e) {} } return detail.gcid || detail.hash || detail.md5_checksum || ""; }; const fetchArchiveList = async (pathValue, pwdValue, timeout = 0) => { const payload = { gcid: await getArchiveGcid(), file_id: archiveOpts.fileId || file.id, password: pwdValue || "", path: pathValue || "" }; const init = { method: 'POST', headers: archiveOpts.getHeaders ? archiveOpts.getHeaders() : getHeaders(), body: JSON.stringify(payload) }; if (timeout && AbortSignal && AbortSignal.timeout) init.signal = AbortSignal.timeout(timeout); const res = await fetch(archiveOpts.listUrl || `https://api-drive.mypikpak.com/decompress/v1/list`, init); const data = await res.json().catch(() => ({})); return { res, data }; }; const submitArchiveUnzip = (pwdValue, archiveFiles) => archiveOpts.submitUnzip ? archiveOpts.submitUnzip(pwdValue || "", archiveFiles || []) : sendUnzipRequest(file, pwdValue || "", archiveFiles || []); let preCheckData = null; setLoad(true); updateLoadTxt(L.loading); try { const gcid = await getArchiveGcid(); let isVerified = false; let isFirstTry = true; while(!isVerified) { const { res, data } = await fetchArchiveList("", currentPwd); const errStr = (data.status_text || data.error_description || data.msg || data.error || "").toLowerCase(); const isPwdErr = isArchivePasswordSignal(data, errStr); if (isPwdErr) { setLoad(false); const newPwd = await askForPassword(archiveName, isFirstTry ? "" : L.err_pwd_simple); if (newPwd !== null) { currentPwd = newPwd; isFirstTry = false; setLoad(true); } else { return { confirm: false }; } } else if (!res.ok || (data.status && data.status !== 'OK')) { throw new Error(data.status_text || `API Error ${res.status}`); } else { if (currentPwd) { if (gcid) gmSet('pk_archive_pwd_' + gcid, currentPwd); if (typeof updateGlobalPool === 'function') updateGlobalPool(currentPwd); } preCheckData = data; isVerified = true; } } } catch (e) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); return { confirm: false }; } finally { setLoad(false); } return new Promise(async (resolve) => { const m = showModal(`
${esc(archiveName)}
A-Z
`); m._pkLocateItem = file || null; const originalArchiveRemove = m.remove.bind(m); m.remove = () => { const locateItem = m._pkLocateItem; originalArchiveRemove(); setTimeout(() => S.locatePreviewSourceItem(locateItem), 0); }; 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'); const arcSortWrap = m.querySelector('#arc_sort_wrap'); const arcSortBtn = m.querySelector('#arc_sort_btn'); const arcSortTxt = m.querySelector('#arc_sort_txt'); const arcSortMenu = m.querySelector('#arc_sort_menu'); let arcCrumbIdx = 0; let lastArcScroll = 0; let archiveSortMode = 'az'; let currentArchiveData = preCheckData; const selectedArchiveMap = new Map(); const getArchiveKey = (f, fullPath) => `${f.index ?? ''}::${f.path || fullPath || f.filename || f.name || ''}`; const updateUnzipBtn = () => { const btn = m.querySelector('#unzip_confirm'); if (!btn || btn.disabled) return; btn.textContent = selectedArchiveMap.size ? L.btn_unzip_selected.replace('{n}', selectedArchiveMap.size) : L.btn_unzip_all; }; const getArchiveSortLabel = () => archiveSortMode === 'za' ? 'Z-A' : (archiveSortMode === 'large' ? L.picker_sort_large : (archiveSortMode === 'small' ? L.picker_sort_small : 'A-Z')); const closeArchiveSortMenu = () => { if (!arcSortMenu) return; arcSortMenu.style.display = 'none'; if (arcSortBtn) arcSortBtn.style.background = 'transparent'; document.removeEventListener('mousedown', onArchiveSortDocDown, true); }; const onArchiveSortDocDown = (ev) => { if ((arcSortMenu && arcSortMenu.contains(ev.target)) || (arcSortBtn && arcSortBtn.contains(ev.target))) return; closeArchiveSortMenu(); }; const toArchiveSize = (f) => { try { return BigInt(String(f.filesize || f.size || 0).replace(/[^0-9]/g, '') || '0'); } catch(e) { return 0n; } }; const getArchiveItemName = (f) => String(f.filename || f.file_name || f.name || ''); const sortArchiveItems = (items) => { const arr = Array.isArray(items) ? items.slice() : []; return arr.sort((a, b) => { const na = getArchiveItemName(a), nb = getArchiveItemName(b); if (archiveSortMode === 'large' || archiveSortMode === 'small') { const sa = toArchiveSize(a), sb = toArchiveSize(b); if (sa !== sb) return (sa > sb ? 1 : -1) * (archiveSortMode === 'large' ? -1 : 1); return na.localeCompare(nb, undefined, { numeric: true, sensitivity: 'base' }); } return na.localeCompare(nb, undefined, { numeric: true, sensitivity: 'base' }) * (archiveSortMode === 'za' ? -1 : 1); }); }; if (arcSortBtn && arcSortMenu) { arcSortBtn.onclick = (e) => { e.stopPropagation(); const open = arcSortMenu.style.display === 'block'; if (open) { closeArchiveSortMenu(); return; } closeArchiveCrumbDropdown(); arcSortMenu.style.display = 'block'; arcSortBtn.style.background = 'var(--pk-hl)'; document.addEventListener('mousedown', onArchiveSortDocDown, true); }; arcSortBtn.onmouseenter = () => { if (arcSortMenu.style.display !== 'block') arcSortBtn.style.background = 'var(--pk-hl)'; }; arcSortBtn.onmouseleave = () => { if (arcSortMenu.style.display !== 'block') arcSortBtn.style.background = 'transparent'; }; arcSortMenu.querySelectorAll('.pk-arc-sort-opt').forEach(opt => { opt.onmouseenter = () => opt.style.background = 'var(--pk-hl)'; opt.onmouseleave = () => opt.style.background = 'transparent'; opt.onclick = (e) => { e.stopPropagation(); archiveSortMode = opt.dataset.val || 'az'; if (arcSortTxt) arcSortTxt.textContent = getArchiveSortLabel(); closeArchiveSortMenu(); if (currentArchiveData) renderData(currentArchiveData, currentPath); }; }); } 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 closeArchiveCrumbDropdown = () => { closeArchiveSortMenu(); const oldPop = document.getElementById('pk-arc-crumb-pop'); if (oldPop) oldPop.remove(); crumbContainer.querySelectorAll('.pk-arc-crumb-sep').forEach(sep => { sep.classList.remove('pk-active'); sep.innerHTML = CONF.crumbIcons.right; }); }; const showArchiveCrumbDropdown = async (e, node, idx, triggerEl) => { e.stopPropagation(); const isActive = triggerEl.classList.contains('pk-active'); closeArchiveCrumbDropdown(); if (isActive) return; triggerEl.classList.add('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.down; const pop = document.createElement('div'); pop.id = 'pk-arc-crumb-pop'; pop.className = 'pk-crumb-pop pk-arc-crumb-pop'; ['--pk-bg','--pk-fg','--pk-hl','--pk-bd','--pk-pri'].forEach(k => { const v = getComputedStyle(m).getPropertyValue(k).trim(); if (v) pop.style.setProperty(k, v); }); pop.style.background = 'var(--pk-bg)'; pop.style.color = 'var(--pk-fg)'; pop.style.border = '1px solid var(--pk-bd)'; pop.innerHTML = `
${esc(L.loading)}
`; document.body.appendChild(pop); const rect = triggerEl.getBoundingClientRect(); const popLeft = Math.max(8, Math.min(rect.left, window.innerWidth - 328)); const popTop = Math.min(rect.bottom + 6, window.innerHeight - 80); pop.style.left = popLeft + 'px'; pop.style.top = popTop + 'px'; requestAnimationFrame(() => pop.classList.add('pk-show')); const cleanup = () => { document.removeEventListener('mousedown', onDocDown, true); if (pop.isConnected) pop.remove(); if (triggerEl.isConnected) { triggerEl.classList.remove('pk-active'); triggerEl.innerHTML = CONF.crumbIcons.right; } }; const onDocDown = (ev) => { if (pop.contains(ev.target) || triggerEl.contains(ev.target)) return; cleanup(); }; document.addEventListener('mousedown', onDocDown, true); try { const listPath = node.path || ""; const { res, data } = await fetchArchiveList(listPath, currentPwd); if (!res.ok || (data.status && data.status !== 'OK')) throw new Error(data.status_text || L.str_load_failed_simple); const folders = (data.files || data.list || []).filter(f => { const size = f.filesize || f.size || 0; return f.kind === 'drive#folder' || (size == 0 && !f.mime_type); }); pop.innerHTML = ''; if (!folders.length) { const empty = document.createElement('div'); empty.className = 'pk-crumb-item pk-crumb-empty'; empty.textContent = L.str_empty_dir; pop.appendChild(empty); return; } folders.forEach(f => { const itemName = f.filename || f.file_name || f.name || L.str_unknown_name; const fullPath = f.path || ((node.path || "") + itemName + "/"); const item = document.createElement('div'); item.className = 'pk-crumb-item'; const iconSrc = f.icon_link || ''; const iconHtml = iconSrc ? `` : ''; item.innerHTML = `${iconHtml}${esc(itemName)}`; item.onclick = (ev) => { ev.stopPropagation(); cleanup(); pathNodes = pathNodes.slice(0, idx + 1); pathNodes.push({ name: itemName, path: fullPath }); updateView(fullPath); }; pop.appendChild(item); }); } catch (err) { pop.innerHTML = `
${esc(err.message || L.str_load_failed_simple)}
`; } }; const renderData = (data, path) => { closeArchiveCrumbDropdown(); currentPath = path || ''; currentArchiveData = data; selectedArchiveMap.clear(); updateUnzipBtn(); crumbContainer.innerHTML = ''; const items = sortArchiveItems(data.files || data.list || []); const currentDirHasFolders = items.some(f => { const size = f.filesize || f.size || 0; return f.kind === 'drive#folder' || (size == 0 && !f.mime_type); }); 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 || currentDirHasFolders) { const sep = document.createElement('span'); sep.className = 'pk-crumb-sep pk-arc-crumb-sep'; sep.innerHTML = CONF.crumbIcons.right; sep.onclick = (e) => showArchiveCrumbDropdown(e, node, idx, sep); crumbContainer.appendChild(sep); } }); arcCrumbIdx = pathNodes.length - 1; requestAnimationFrame(() => { crumbContainer.scrollTo({ left: crumbContainer.scrollWidth, behavior: 'smooth' }); }); listContainer.innerHTML = ''; listContainer.appendChild(loading); if (items.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.style.cssText = "padding:50px; text-align:center; color:#999; font-size:13px;"; emptyMsg.textContent = L.str_empty_dir; listContainer.appendChild(emptyMsg); } else { items.forEach(f => { const itemName = f.filename || f.file_name || f.name || L.str_unknown_name; const itemSize = f.filesize || f.size || 0; const isDir = f.kind === 'drive#folder' || (itemSize == 0 && !f.mime_type); const fullPath = (path || "") + itemName + (isDir ? "/" : ""); const row = document.createElement('div'); row.style.cssText = "display:flex; align-items:center; height:48px; padding:0 24px; cursor:pointer; transition:background 0.1s; border-bottom:1px solid rgba(0,0,0,0.03);"; row.onmouseover = () => row.style.background = 'var(--pk-hl)'; row.onmouseout = () => row.style.background = row.dataset.pkArcPicked === '1' ? 'var(--pk-hl)' : 'transparent'; const enterFolder = () => { selectedArchiveMap.clear(); updateUnzipBtn(); pathNodes.push({ name: itemName, path: fullPath }); updateView(fullPath); }; const iconSrc = f.icon_link || ''; const fallbackSvg = isDir ? getOfficialFolderFallbackIconHtml(28) : getIcon({ name: itemName, mime_type: f.mime_type }).replace(/width="\d+"/, 'width="28"').replace(/height="\d+"/, 'height="28"'); const iconHtml = iconSrc ? `${fallbackSvg}` : fallbackSvg; row.innerHTML = `
${iconHtml}
${esc(itemName)}
${isDir ? '-' : fmtSize(itemSize)}
`; const check = row.querySelector('.pk-arc-check'); const key = getArchiveKey(f, fullPath); let clickTimer = 0; const setPicked = (picked) => { check.checked = picked; row.dataset.pkArcPicked = picked ? '1' : '0'; row.style.background = picked ? 'var(--pk-hl)' : 'transparent'; if (picked) selectedArchiveMap.set(key, Object.assign({}, f, { filename: f.filename || itemName, path: f.path || fullPath, kind: f.kind || (isDir ? 'drive#folder' : 'drive#file') })); else selectedArchiveMap.delete(key); updateUnzipBtn(); }; check.onclick = (e) => { e.stopPropagation(); if (clickTimer) { clearTimeout(clickTimer); clickTimer = 0; } setPicked(check.checked); }; row.onclick = (e) => { e.stopPropagation(); if (clickTimer) clearTimeout(clickTimer); clickTimer = setTimeout(() => { clickTimer = 0; setPicked(!check.checked); }, isDir ? 180 : 0); }; if (isDir) { row.ondblclick = (e) => { e.stopPropagation(); if (clickTimer) { clearTimeout(clickTimer); clickTimer = 0; } enterFolder(); }; } listContainer.appendChild(row); }); } }; const updateView = async (path) => { loading.style.display = 'flex'; try { const { res, data } = await fetchArchiveList(path, currentPwd); 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 = (e.message); listContainer.innerHTML = ''; listContainer.appendChild(loading); listContainer.appendChild(errDiv); } finally { loading.style.display = 'none'; } }; let previewSubmitActive = false; let previewUnzipStarted = false; let previewResolveDone = false; let previewProgressTask = null; let previewLastProgress = 0; const resolvePreview = (value) => { if (previewResolveDone) return; previewResolveDone = true; resolve(value); }; const ensurePreviewProgressTask = () => { if (document.contains(m) || !previewUnzipStarted || previewProgressTask) return previewProgressTask; previewProgressTask = FloatBarManager.create(L.str_unzipping.replace('{n}', file.name)); previewProgressTask.update(L.str_unzipping_prog_fmt.replace('{n}', previewLastProgress)); return previewProgressTask; }; const closePreview = () => { closeArchiveSortMenu(); closeArchiveCrumbDropdown(); m.remove(); if (previewSubmitActive || previewUnzipStarted) { ensurePreviewProgressTask(); return; } resolvePreview({ confirm: false }); }; m.querySelector('.pk-modal-close').onclick = closePreview; m.querySelector('#unzip_cancel').onclick = closePreview; m.tabIndex = 0; setTimeout(() => m.focus(), 10); m.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); m.querySelector('#unzip_confirm').click(); } }); m.querySelector('#unzip_confirm').onclick = async () => { const cur = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isVirtual = S.isFlattened || S.dupMode || isStarredRoot || isRecentRoot || cur.id === 'analyze_root' || cur.id === 'virtual_search_root'; if (isVirtual) { const userConfirmed = await new Promise(res => { const vm = showModal(`

${L.title_confirm}

${L.msg_unzip_virtual_view_warn}
`); vm.querySelector('#vc_cancel').onclick = () => { vm.remove(); res(false); }; vm.querySelector('.pk-modal-close').onclick = () => { vm.remove(); res(false); }; vm.querySelector('#vc_ok').onclick = () => { vm.remove(); res(true); }; vm.tabIndex = 0; setTimeout(() => vm.focus(), 10); vm.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); vm.querySelector('#vc_ok').click(); } }); }); if (!userConfirmed) return; } const btn = m.querySelector('#unzip_confirm'); const progTxt = m.querySelector('#unzip_progress_text'); btn.disabled = true; btn.style.opacity = '0.6'; btn.style.cursor = 'not-allowed'; btn.textContent = L.str_unzipping_state; progTxt.textContent = L.str_unzipping_prog_0; progTxt.style.opacity = '1'; previewSubmitActive = true; try { const archiveFiles = Array.from(selectedArchiveMap.values()); const resp = await submitArchiveUnzip(currentPwd, archiveFiles); if (resp && resp.task_id) { const taskId = resp.task_id; previewUnzipStarted = true; previewSubmitActive = false; let isPolling = true; const currentFolderId = S.path[S.path.length - 1].id || ''; observeUnzipTask(taskId, currentFolderId, file.id); const updatePreviewProgress = (progress) => { const pct = Math.max(0, Math.min(100, Number(progress) || 0)); previewLastProgress = pct; const text = L.str_unzipping_prog_fmt.replace('{n}', pct); if (document.contains(m)) progTxt.textContent = text; else { const task = ensurePreviewProgressTask(); if (task) task.update(text); } }; const finishUnzip = async () => { isPolling = false; previewLastProgress = 100; if (document.contains(m)) { progTxt.textContent = L.str_unzipping_prog_100; await sleep(500); m.remove(); } else if (previewProgressTask) { previewProgressTask.update(L.str_unzipping_prog_100); setTimeout(() => { if (previewProgressTask) { previewProgressTask.destroy(); previewProgressTask = null; } }, 900); } }; const pollProgress = async () => { if (!isPolling) return; if (!document.contains(m)) ensurePreviewProgressTask(); 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) { updatePreviewProgress(tData.progress); if (Number(tData.progress) >= 100) { finishUnzip(); return; } } } catch(e) { if (previewProgressTask) { previewProgressTask.destroy(); previewProgressTask = null; } isPolling = false; resolvePreview({ confirm: true, password: currentPwd, taskId: taskId, alreadyStarted: true }); return; } if (isPolling) setTimeout(pollProgress, 800); }; resolvePreview({ confirm: true, password: currentPwd, taskId: taskId, alreadyStarted: true, alreadyObserved: true }); pollProgress(); } else { previewSubmitActive = false; m.remove(); resolvePreview({ confirm: true, password: currentPwd }); } } catch (e) { previewSubmitActive = false; const errText = getShareParseOfficialErrorMessage(e) || e.msg || ((e && e.message && !/^msg_/.test(e.message) && !e.shareParseKey) ? e.message : '') || L.str_action_failed; if (!document.contains(m)) { showAlert(errText); resolvePreview({ confirm: false }); return; } btn.disabled = false; btn.style.opacity = '1'; btn.style.cursor = 'pointer'; updateUnzipBtn(); progTxt.style.color = '#d93025'; progTxt.textContent = errText; console.error(e); } }; renderData(preCheckData, ""); }); }; const handleUnzip = async (items, verifiedPwd = "", skipPreview = false, externalTaskId = null) => { if (!items || items.length === 0) return; const currentFolderId = S.path[S.path.length - 1].id || ''; const Vault = { get: () => { try { const raw = JSON.parse(gmGet('pk_pwd_vault', '[]')); return raw.map(x => typeof x === 'object' ? x.p : x); } catch { return []; } }, save: (p) => { if (!p) return; try { let list = JSON.parse(gmGet('pk_pwd_vault', '[]')).map(x => typeof x === 'object' ? x : {p: x, h: 0}); let item = list.find(x => x.p === p); if (item) item.h = (item.h || 0) + 1; else list.push({p: p, h: 1}); list.sort((a, b) => b.h - a.h); if (list.length > 50) list = list.slice(0, 50); gmSet('pk_pwd_vault', JSON.stringify(list)); } catch(e) {} } }; const curPathNode = S.path[S.path.length - 1]; const isStarredRoot = S.starredMode && S.path.length === 1; const isRecentRoot = S.recentMode && S.path.length === 1; const isVirtual = S.isFlattened || S.dupMode || isStarredRoot || isRecentRoot || curPathNode.id === 'analyze_root' || curPathNode.id === 'virtual_search_root'; let startFromPreviewTaskId = externalTaskId; let previewResult = null; if (!skipPreview && !verifiedPwd) { if (items.length === 1) { previewResult = await showArchivePreview(items[0], ""); if (!previewResult || !previewResult.confirm) return; verifiedPwd = previewResult.password; if (previewResult.alreadyStarted && previewResult.taskId) { startFromPreviewTaskId = previewResult.taskId; } } else { if (isVirtual) { const userConfirmed = await new Promise(res => { const vm = showModal(`

${L.title_confirm}

${L.msg_unzip_virtual_view_warn}
`); vm.querySelector('#vc_cancel').onclick = () => { vm.remove(); res(false); }; vm.querySelector('.pk-modal-close').onclick = () => { vm.remove(); res(false); }; vm.querySelector('#vc_ok').onclick = () => { vm.remove(); res(true); }; }); if (!userConfirmed) return; } else { if (!await showConfirm(L.msg_unzip_confirm_n.replace('{n}', items.length))) return; } } } if (S.sel.size > 0) { S.clearSelection(); refresh(); } const isSilentMode = (items.length === 1 && !!startFromPreviewTaskId); let progressTask = null; if (!isSilentMode) { progressTask = FloatBarManager.create(L.str_preparing); } let successCount = 0; let failCount = 0; let lastBatchPwd = ""; const MAX_CONCURRENCY = 3; const activePromises = []; let promptMutex = Promise.resolve(); const waitForTaskDone = async (taskId) => { for (let i = 0; i < 300; i++) { try { const res = await fetch(`https://api-drive.mypikpak.com/decompress/v1/progress?task_id=${taskId}`, { headers: getHeaders() }); if (res.ok) { const d = await res.json(); if (d.phase === 'PHASE_TYPE_COMPLETE' || d.phase === 'PHASE_TYPE_ERROR') return; } else if (res.status === 404) { return; } } catch(e) {} await sleep(2000); } }; const processSingleItem = async (file, index) => { try { let detail = file; if (!detail.gcid && !detail.hash) { for(let w = 0; w < 5; w++) { try { detail = await Promise.race([ apiGet(file.id), new Promise((_, r) => setTimeout(() => r(new Error("Timeout")), 2000)) ]); } catch(e) {} if (detail.gcid || detail.hash || detail.md5_checksum) break; await sleep(1000); } } const gcid = detail.gcid || detail.hash || detail.md5_checksum || ""; let isDone = false; let triedVerified = false; let triedBatch = false; let triedEmpty = false; let hasTriedVault = false; let isFirstManualInput = true; let systemRetry = 0; while (!isDone) { let pwdToTry = ""; let currentSource = ""; if (verifiedPwd && !triedVerified) { pwdToTry = verifiedPwd; currentSource = "VERIFIED"; } else if (lastBatchPwd && !triedBatch) { pwdToTry = lastBatchPwd; currentSource = "BATCH"; } else if (!triedEmpty) { pwdToTry = ""; currentSource = "EMPTY"; } else if (!hasTriedVault) { hasTriedVault = true; const tryLimit = gmGet('pk_pwd_try_count', 10); const candidates = Vault.get().slice(0, tryLimit); if (candidates.length > 0) { if (progressTask) progressTask.update(L.msg_smart_matching_file.replace('{n}', file.name)); const checkTask = async (pwd, idx) => { const tieredDelay = Math.floor(idx / 5) * 1200; await sleep((idx * 150) + tieredDelay); try { const autoRes = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ gcid, file_id: file.id, password: pwd, path: "" }), signal: AbortSignal.timeout(5000) }); const autoData = await autoRes.json(); if (autoData.status === 'OK') return pwd; } catch(e) {} throw new Error("Wrong"); }; try { const correctPwd = await Promise.any(candidates.map((p, i) => checkTask(p, i))); if (correctPwd) { lastBatchPwd = correctPwd; Vault.save(correctPwd); triedBatch = false; continue; } } catch (e) {} } continue; } else { currentSource = "MANUAL"; } if (currentSource === "MANUAL") { const pwdBeforeWait = lastBatchPwd; const previousMutex = promptMutex; let releaseMutex; promptMutex = new Promise(r => releaseMutex = r); await previousMutex; try { if (lastBatchPwd !== pwdBeforeWait && lastBatchPwd !== "") { continue; } setLoad(false); const errorMsg = isFirstManualInput ? "" : L.err_pwd_simple; const userPwd = await askForPassword(file.name, errorMsg, true); isFirstManualInput = false; setLoad(true); if (userPwd !== null) { lastBatchPwd = userPwd; Vault.save(userPwd); verifiedPwd = userPwd; triedVerified = false; continue; } else { failCount++; isDone = true; continue; } } finally { releaseMutex(); } } try { if (progressTask) { const doneCount = successCount + failCount; progressTask.update(`${L.str_unzipping.replace('{n}', file.name)} (${doneCount + 1} / ${items.length})`); } let taskId = null; let needsObserve = true; if (index === 0 && startFromPreviewTaskId) { taskId = startFromPreviewTaskId; startFromPreviewTaskId = null; if (items.length === 1 && previewResult && previewResult.alreadyObserved) { needsObserve = false; } } else { const preRes = await fetch(`https://api-drive.mypikpak.com/decompress/v1/list`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ gcid, file_id: file.id, password: pwdToTry, path: "" }), signal: AbortSignal.timeout(8000) }); const preData = await preRes.json().catch(() => ({})); const preErrStr = (preData.status_text || preData.error_description || preData.msg || preData.error || "").toLowerCase(); if (isArchivePasswordSignal(preData, preErrStr)) { 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 || isArchivePasswordSignal(err, err.msg || errText); const isAlreadyRunning = isArchiveAlreadyRunningSignal(err, err.msg || errText); 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"; const now = new Date(getServerNow()); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); let viewYear = now.getFullYear(); let viewMonth = now.getMonth(); const sideOpts = [ { lbl: L.share_perm, val: -1 }, { lbl: `7 ${L.share_days}`, val: 7 }, { lbl: `14 ${L.share_days}`, val: 14 }, { lbl: `30 ${L.share_days}`, val: 30 } ]; const buildHTML = () => { const navLeft = ``; const navRight = ``; let sideHtml = sideOpts.map(o => `
${o.lbl}
` ).join(''); const monthStr = (L.cal_months || [])[viewMonth] || (viewMonth + 1 + L.unit_month); const headerHtml = `
${navLeft}
${navLeft}
${viewYear} ${monthStr}
${navRight}
${navRight}
`; const weeksHtml = (L.cal_week_days || ["S","M","T","W","T","F","S"]).map(w => `
${w}
` ).join(''); const firstDay = new Date(viewYear, viewMonth, 1).getDay(); const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); let daysHtml = ''; for(let i=0; i`; for(let d=1; d<=daysInMonth; d++) { const currentTs = new Date(viewYear, viewMonth, d).getTime(); const isToday = currentTs === todayStart; const isPast = currentTs < todayStart; const isDisabled = isPast; let cls = 'pk-cal-td'; if (isDisabled) cls += ' disabled'; if (isToday) cls += ' today'; daysHtml += `
${d}
`; } return `
${sideHtml}
${L.cal_custom_title}
${headerHtml}
${weeksHtml} ${daysHtml}
`; }; const refresh = () => { pop.innerHTML = buildHTML(); pop.querySelectorAll('.pk-cal-side-item').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const days = parseInt(el.dataset.val); onSelect({ days: days, ts: null }); pop.remove(); document.removeEventListener('mousedown', onClickOutside); }; }); pop.querySelectorAll('.pk-cal-td:not(.disabled)').forEach(el => { el.onclick = (e) => { e.stopPropagation(); const ts = parseInt(el.dataset.ts); const diff = (ts - todayStart) / (1000 * 3600 * 24); const days = Math.max(1, Math.ceil(diff)); onSelect({ days: days, ts: ts }); pop.remove(); document.removeEventListener('mousedown', onClickOutside); }; }); pop.querySelector('#cal_prev_y').onclick = (e) => { e.stopPropagation(); viewYear--; refresh(); }; pop.querySelector('#cal_next_y').onclick = (e) => { e.stopPropagation(); viewYear++; refresh(); }; pop.querySelector('#cal_prev_m').onclick = (e) => { e.stopPropagation(); viewMonth--; if(viewMonth<0){viewMonth=11;viewYear--;} refresh(); }; pop.querySelector('#cal_next_m').onclick = (e) => { e.stopPropagation(); viewMonth++; if(viewMonth>11){viewMonth=0;viewYear++;} refresh(); }; }; refresh(); document.body.appendChild(pop); const updatePosition = () => { if (!pop.isConnected) return; if (anchorEl.offsetParent === null) { cleanup(); return; } const rect = getLogicalRect(anchorEl); let top = rect.bottom + 5; let left = rect.left; if (top + 320 > window.innerHeight) top = rect.top - 320 - 5; if (left + 400 > window.innerWidth) left = window.innerWidth - 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 copyIcon = ``; let statusColor = '#52c41a'; let statusText = L.share_perm; if (item.share_status === 'EXPIRED') { statusColor = '#faad14'; statusText = L.str_share_expired; } else if (item.share_status === 'DELETED') { statusColor = '#ff4d4f'; statusText = L.str_share_deleted; } else if (item.expiration_at && item.expiration_at !== "-1") { statusColor = 'inherit'; const d = new Date(item.expiration_at); const pad = n => String(n).padStart(2, '0'); statusText = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; } else if (item.share_status_text) { statusText = item.share_status_text; } const labelStyle = "font-size:14px; font-weight:bold; color:var(--pk-fg); margin-bottom:8px;"; const inputWrapStyle = "display:flex; align-items:center; border:1px solid var(--pk-bd); border-radius:4px; padding:0 12px; background:var(--pk-bg); height:40px; transition:border-color 0.2s;"; const inputStyle = "flex:1; border:none; background:transparent; outline:none; font-size:14px; color:var(--pk-fg);"; const copyBtnStyle = "margin-left:10px; display:flex; align-items:center; justify-content:center; transition:color 0.2s;"; const m = showModal(`

${L.title_share_detail}

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

${L.title_edit_pwd}

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

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

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

${L.share_count_ed}

${CONF.crumbIcons.down.replace('points="6 9 12 15 18 9"', 'points="18 15 12 9 6 15"')}
${CONF.crumbIcons.down}
${L.btn_cancel}
`; document.body.appendChild(subM); const radios = subM.querySelectorAll('input[name="sh_mod_cnt"]'); const input = subM.querySelector('#sh_mod_cnt_val'); const saveBtn = subM.querySelector('#pk_mod_cnt_save'); const ctrl = subM.querySelector('.pk-num-ctrl'); const updateCtrlState = () => { ctrl.style.opacity = input.disabled ? '0.3' : '1'; ctrl.style.pointerEvents = input.disabled ? 'none' : 'auto'; ctrl.style.cursor = input.disabled ? 'not-allowed' : 'default'; }; radios.forEach(r => r.onchange = () => { input.disabled = r.value !== 'custom'; if (!input.disabled) input.focus(); updateCtrlState(); }); updateCtrlState(); subM.querySelector('#sh_cnt_inc').onclick = (e) => { if (input.disabled) return; e.stopPropagation(); input.value = (parseInt(input.value) || currentSave) + 1; validate(); }; subM.querySelector('#sh_cnt_dec').onclick = (e) => { if (input.disabled) return; e.stopPropagation(); input.value = Math.max(currentSave + 1, (parseInt(input.value) || (currentSave + 2)) - 1); validate(); }; const doSave = async () => { const rVal = subM.querySelector('input[name="sh_mod_cnt"]:checked').value; let newLimit = 0; if (rVal === 'custom') { const val = parseInt(input.value); if (isNaN(val) || val <= 0) { input.style.borderColor = '#d93025'; return; } newLimit = val; } else { newLimit = parseInt(rVal); if (newLimit === -1) newLimit = 0; } if (newLimit > 0 && newLimit <= currentSave) { showToast(L.err_limit_too_low.replace('{n}', newLimit).replace('{s}', currentSave), 'error'); if(input.disabled) input.value = currentSave + 1; return; } saveBtn.textContent = "..."; saveBtn.disabled = true; item.limit_count = newLimit; const store = JSON.parse(gmGet('pk_share_limits', '{}')); if (newLimit > 0) { store[item.id] = newLimit; } else { delete store[item.id]; } gmSet('pk_share_limits', JSON.stringify(store)); if (newLimit === 0) { limitInput.value = L.share_unlimit; } else { limitInput.value = `${newLimit} ${L.share_times}`; } limitInput.style.color = 'var(--pk-fg)'; renderVisible(); if (window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(true); } setTimeout(() => { subM.remove(); showToast(L.msg_limit_updated); }, 200); }; subM.querySelector('#pk_mod_cnt_cancel').onclick = () => subM.remove(); subM.querySelector('.pk-modal-close').onclick = () => subM.remove(); saveBtn.onclick = doSave; input.onkeydown = (e) => { if(e.key === 'Enter') doSave(); }; }; const expField = m.querySelector('#pk_share_exp_edit'); const expInput = m.querySelector('#pk_share_exp_val'); expField.onclick = (e) => { e.stopPropagation(); const existing = document.querySelector('.pk-cal-pop'); if (existing) { existing.remove(); return; } renderCalendar(expField, async (res) => { const selectedDays = res.days; if (selectedDays === null) return; const originalText = expInput.value; expInput.value = "..."; try { const payload = {}; let newText = ""; let newStatusLeft = ""; let newStatusLeftSeconds = "-1"; if (selectedDays === -1) { payload.expiration_at = "-1"; newText = L.share_perm; newStatusLeft = "-1"; newStatusLeftSeconds = "-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`; const dayUnit = String(L.share_days || L.unit_days || '').trim(); newText = res.ts ? ds : `${selectedDays}${dayUnit}`; newStatusLeftSeconds = String(Math.max(0, Math.ceil((new Date(payload.expiration_at).getTime() - getServerNow()) / 1000))); if (res.ts) { const diff = Math.max(1, Math.ceil((res.ts - new Date(getServerNow()).setHours(0,0,0,0)) / (1000 * 3600 * 24))); newStatusLeft = String(diff); } else { newStatusLeft = String(selectedDays); } } await apiUpdateShare(item.id, payload); patchLocalShareItem(S, item, { expiration_days: selectedDays, expiration_at: payload.expiration_at, expiration_left: newStatusLeft, expiration_left_seconds: newStatusLeftSeconds, share_status: 'OK', share_status_text: '' }); expInput.value = newText; expInput.style.color = 'var(--pk-fg)'; renderVisible(); updateStat(); if (window.pkSmartRefreshTrigger) { window.pkSmartRefreshTrigger(true); } 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; const lines = [url]; if (pwd) lines.push(`${L.share_copy_pwd}: ${pwd}`); if (title) lines.push(title); GM_setClipboard(lines.join('\n')); const btn = m.querySelector('#pk_detail_copy_all'); const orgTxt = btn.textContent; btn.textContent = L.msg_copy_success; const originalColor = btn.style.backgroundColor; const originalBorder = btn.style.borderColor; btn.style.backgroundColor = "#52c41a"; btn.style.borderColor = "#52c41a"; setTimeout(() => { btn.textContent = orgTxt; btn.style.backgroundColor = originalColor; btn.style.borderColor = originalBorder; }, 1500); }; }; UI.btnCancelShare.onclick = async () => { const selectedIds = S.getSelectedIds(); const n = selectedIds.length; if (n === 0) return; if (!await showConfirm(L.msg_cancel_share_confirm.replace('{n}', n))) return; setLoad(true); updateLoadTxt(L.msg_cancel_share_ing); try { const selectedItems = selectedIds.map(id => S.itemMap.get(id)).filter(Boolean); const serverIds = selectedItems.filter(it => !it._is_local_phantom).map(it => it.id); const phantomIds = selectedItems.filter(it => it._is_local_phantom).map(it => it.id); if (serverIds.length > 0) { await apiCancelShare(serverIds); } if (phantomIds.length > 0) { const graveyard = JSON.parse(gmGet('pk_expired_shares', '[]')); const newGraveyard = graveyard.filter(x => !phantomIds.includes(x.id)); gmSet('pk_expired_shares', JSON.stringify(newGraveyard)); } S.clearSelection(); await load(false, true); showToast(L.msg_cancel_share_done.replace('{n}', n)); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); } }; if (UI.btnCopyLinkOffline) { UI.btnCopyLinkOffline.onclick = () => { const ids = S.getSelectedIds(); if (ids.length === 0) return; const tasks = ids.map(id => S.itemMap.get(id)).filter(t => t && (t.source_url || (t.params && t.params.url))); if (tasks.length === 0) return; const urls = tasks.map(t => t.source_url || t.params.url).join('\n'); GM_setClipboard(urls); showToast(L.msg_copy_success); }; } if (UI.btnRetryTask) { UI.btnRetryTask.onclick = async () => { const ids = S.getSelectedIds(); if (ids.length === 0) return; try { const retryResult = await handleOfflineTaskRetry(ids.map(id => S.itemMap.get(id) || { id, task_id: id, kind: 'drive#task' }), { userInitiated: true, source: 'toolbar_retry', batch: ids.length > 1 }); if (retryResult && retryResult.handled) { showToast(L.msg_retry_submitted.replace('{n}', retryResult.successCount || 0)); updateStat(); return; } } catch (e) { showAlert(`${L.str_error}: ${e.message}`); return; } const targets = ids.map(id => S.itemMap.get(id)) .filter(t => t && t.phase === 'PHASE_TYPE_ERROR' && (t.source_url || (t.params && t.params.url))); if (targets.length === 0) { showToast(L.err_no_failed_task, "error"); return; } setLoad(true); updateLoadTxt(L.str_processing); let successCount = 0; try { for (const task of targets) { const url = task.source_url || task.params.url; try { await apiAddOfflineTask(url); await apiCancelTask([task.id]); successCount++; if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; } catch (e) { console.error(`[Retry] Failed for ${task.name}:`, e); if (e.message && e.message.includes(L.err_task_exists)) { await apiCancelTask([task.id]).catch(()=>{}); successCount++; } } await sleep(150); } await load(false, true); showToast(L.msg_retry_submitted.replace('{n}', successCount)); } catch (e) { showAlert(`${L.str_error}: ${e.message}`); } finally { setLoad(false); S.clearSelection(); updateStat(); } }; } UI.btnMigrate.onclick = async () => { const selectedIds = S.getSelectedIds(); const selectedCount = selectedIds.length; if (selectedCount === 0) return; const L = getStrings(); const hasConflict = selectedIds.some(id => { const item = S.itemMap.get(id); if (item && item.kind === 'drive#folder') return isPathBusy(item.id); return S.movingIds.has(id); }); if (hasConflict) { showAlert(L.msg_op_blocked_moving); return; } if (selectedCount > 100) { showToast(L.err_migrate_limit, 'error'); return; } const estimatedSize = JSON.stringify(selectedIds).length; if (estimatedSize > 3.8 * 1024 * 1024) { const mb = (estimatedSize / 1024 / 1024).toFixed(2); showAlert(L.err_migrate_too_many.replace('{s}', mb)); return; } if (!await showConfirm(L.msg_migrate_confirm.replace('{n}', selectedCount))) return; setLoad(true); updateLoadTxt(L.msg_migrate_packing); try { let sourceUid = ""; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('credentials')) { try { const v = JSON.parse(localStorage.getItem(k)); if (v && v.sub) { sourceUid = v.sub; break; } } catch {} } } if (!sourceUid) sourceUid = "UNKNOWN_UID_" + Date.now(); const randPass = Math.random().toString(36).slice(-5) + Math.random().toString(36).slice(-5).toUpperCase(); const payload = { file_ids: selectedIds, share_to: 'encryptedlink', expiration_days: 1, pass_code_option: 'REQUIRED', custom_pass_code: randPass, limit_count: 1 }; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/share`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) { const err = await res.json().catch(() => ({})); if (res.status === 403 || res.status === 400) { throw new Error(L.err_migrate_ban); } throw new Error(err.error_description || `API Error ${res.status}`); } const data = await res.json(); const stub = { source_uid: sourceUid, share_id: data.share_id, pass_code: randPass, file_count: selectedCount, file_ids: selectedIds, timestamp: Date.now() }; gmSet('pk_migration_stub', JSON.stringify(stub)); try { const store = JSON.parse(gmGet('pk_share_limits', '{}')); store[data.share_id] = 1; gmSet('pk_share_limits', JSON.stringify(store)); } catch(e) {} setLoad(false); await showAlert(L.msg_migrate_ready, L.btn_migrate); const keysToRemove = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && (k.startsWith('credentials') || k.startsWith('captcha') || k === 'pk_captured_captcha')) { keysToRemove.push(k); } } keysToRemove.forEach(k => localStorage.removeItem(k)); if (typeof purgeAllCachesOnLogout === 'function') purgeAllCachesOnLogout(); if (!location.href.includes('/login')) window.location.href = 'https://mypikpak.com/drive/login'; } catch (e) { setLoad(false); showAlert(`${L.str_error}: ${e.message}`); } }; UI.btnUnzip.onclick = async () => { ensureItemMap(); const isArchive = (it) => { if (!it || it.kind === 'drive#folder') return false; const n = (it.name || '').toLowerCase(); const m = (it.mime_type || '').toLowerCase(); return m.includes('zip') || m.includes('rar') || m.includes('7z') || m.includes('compressed') || m.includes('archive') || n.endsWith('.zip') || n.endsWith('.rar') || n.endsWith('.7z') || n.endsWith('.tar') || n.endsWith('.gz'); }; const rawTargets = S.getSelectedIds().map(id => S.itemMap.get(id)).filter(i => isArchive(i)); const existingFolderNames = new Set( S.items.filter(i => i.kind === 'drive#folder').map(i => i.name) ); const targets = rawTargets.filter(item => { if (rawTargets.length === 1) return true; const isMarkedUnzipped = item.params && (item.params.global_file_kind === '1' || item.params.global_file_root); if (!isMarkedUnzipped) return true; let targetFolderName = item.name; const lastDot = targetFolderName.lastIndexOf('.'); if (lastDot > 0) targetFolderName = targetFolderName.substring(0, lastDot); return !existingFolderNames.has(targetFolderName); }); const skippedItems = rawTargets.filter(item => !targets.includes(item)); const skippedCount = skippedItems.length; if (skippedCount > 0) { const delRes = await showDeleteConfirm(L.msg_unzip_skip_del_confirm.replace('{n}', skippedCount)); if (delRes.confirm) { const idsToDelete = skippedItems.map(i => i.id); await executeBatchDelete(idsToDelete, { silent: true, forceRefresh: false, hardDelete: delRes.hardDelete }); showToast(L.msg_del_items_done.replace('{n}', skippedCount)); } else { showToast(L.msg_skip_unzipped.replace('{n}', skippedCount)); } } if (targets.length === 1) { if (targets[0]._isShareItem) { if (isArchiveLike(targets[0])) handleOpenShareArchive(targets[0]); else if (isAudioLikeItem(targets[0]) || isVideoLikeItem(targets[0]) || isImageLikeItem(targets[0])) handleOpenShareFile(targets[0]); else showToast(L.msg_share_parse_select_first, 'warning'); } else handleOpenArchive(targets[0]); } else if (targets.length > 1) { if (targets.some(t => t && t._isShareItem)) { showToast(L.msg_share_parse_select_first, 'warning'); return; } handleUnzip(targets); } }; ctx.querySelector('#ctx-share').onclick = async () => { ctx.style.display = 'none'; const ids = S.getSelectedIds(); if (ids.length === 0) return; if (ids.length > 100) { showToast(L.err_share_limit, 'error'); return; } const item = S.itemMap.get(ids[0]); if (!item) return; const m = showModal(`

${L.share_title}

${L.share_mode}
${L.share_public}
${L.share_encrypted}
${L.share_expiry}
${L.share_custom}
${L.share_pass}
${L.share_count}
`); const tabs = m.querySelectorAll('.pk-s-tab'); const passSec = m.querySelector('#sh_pass_sec'); const passVal = m.querySelector('#sh_pass_val'); const passRadios = m.querySelectorAll('input[name="sh_pass_type"]'); const btnGo = m.querySelector('#sh_go'); const validate = () => { const mode = m.querySelector('.pk-s-tab.act').dataset.val; const passType = m.querySelector('input[name="sh_pass_type"]:checked').value; const pass = passVal.value.trim(); let isValid = true; if (mode === 'encrypted' && passType === 'custom') { const reg = /^[a-zA-Z0-9]{4,10}$/; isValid = reg.test(pass); } btnGo.disabled = !isValid; btnGo.style.opacity = isValid ? '1' : '0.4'; btnGo.style.cursor = isValid ? 'pointer' : 'not-allowed'; }; tabs.forEach(t => t.onclick = () => { tabs.forEach(x => x.classList.remove('act')); t.classList.add('act'); const isEnc = t.dataset.val === 'encrypted'; passSec.style.opacity = isEnc ? '1' : '0.3'; passSec.style.pointerEvents = isEnc ? 'auto' : 'none'; validate(); }); passRadios.forEach(r => r.onchange = () => { passVal.disabled = r.value !== 'custom'; if (!passVal.disabled) passVal.focus(); validate(); }); const cntRadios = m.querySelectorAll('input[name="sh_cnt"]'); const cntVal = m.querySelector('#sh_cnt_val'); cntRadios.forEach(r => r.onchange = () => { const isCustom = r.value === 'custom'; cntVal.disabled = !isCustom; if (isCustom) { setTimeout(() => cntVal.focus(), 10); } else { cntVal.value = ''; } validate(); }); cntVal.onfocus = () => { if(!cntVal.disabled) cntBox.style.borderColor = 'var(--pk-pri)'; }; cntVal.onblur = () => { if(cntVal.value === '') cntBox.style.borderColor = 'var(--pk-bd)'; }; cntVal.oninput = () => { cntVal.value = cntVal.value.replace(/[^\d]/g, ''); validate(); }; const modalContainer = m.querySelector('.pk-modal'); if (modalContainer) { modalContainer.style.padding = '0'; modalContainer.style.width = 'fit-content'; } const btnCustomExp = m.querySelector('#sh_exp_custom_btn'); const txtCustom = m.querySelector('#sh_custom_txt'); let customDays = null; m.querySelectorAll('input[name="sh_exp"]').forEach(r => { r.addEventListener('change', (e) => { if (e.target.value !== 'custom') { btnCustomExp.style.borderColor = 'var(--pk-bd)'; btnCustomExp.style.color = 'var(--pk-fg)'; txtCustom.textContent = L.share_custom; customDays = null; } }); }); btnCustomExp.onclick = (e) => { e.stopPropagation(); e.preventDefault(); const existing = document.querySelector('.pk-cal-pop'); if (existing) { existing.remove(); return; } renderCalendar(btnCustomExp, (res) => { const presetRadio = m.querySelector(`input[name="sh_exp"][value="${res.days}"]`); if (presetRadio && res.ts === null) { presetRadio.checked = true; presetRadio.dispatchEvent(new Event('change')); } else { customDays = res.days; const radio = btnCustomExp.querySelector('input'); radio.checked = true; btnCustomExp.style.borderColor = 'var(--pk-pri)'; btnCustomExp.style.color = 'var(--pk-pri)'; if (res.ts) { const d = new Date(res.ts); const dateStr = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; txtCustom.textContent = dateStr; } else if (res.days === -1) { txtCustom.textContent = L.share_perm; } else { txtCustom.textContent = `${res.days} ${L.share_days}`; } } }); }; passVal.oninput = validate; m.querySelector('#sh_cancel').onclick = () => m.remove(); btnGo.onclick = async () => { if (btnGo.disabled) return; const mode = m.querySelector('.pk-s-tab.act').dataset.val; const cntRadio = m.querySelector('input[name="sh_cnt"]:checked'); let cnt = parseInt(cntRadio.value); if (cntRadio.value === 'custom') { const customInput = m.querySelector('#sh_cnt_val').value.trim(); cnt = (customInput === "") ? -1 : (parseInt(customInput) || 1); } let exp = -1; const expRadio = m.querySelector('input[name="sh_exp"]:checked'); if (expRadio.value === 'custom') { if (customDays === null) { exp = 7; } else { exp = customDays; } } else { exp = parseInt(expRadio.value); } const isCustomPass = m.querySelector('input[name="sh_pass_type"]:checked').value === 'custom'; const pass = isCustomPass ? passVal.value.trim() : ""; if (mode === 'encrypted' && isCustomPass) { if (pass.length < 4 || pass.length > 10) { showAlert(L.err_share_pass); return; } } m.remove(); setLoad(true); updateLoadTxt(L.msg_creating_share); try { const payload = { file_ids: ids, share_to: mode === 'encrypted' ? 'encryptedlink' : 'publiclink', expiration_days: parseInt(exp), pass_code_option: mode === 'encrypted' ? 'REQUIRED' : 'NOT_REQUIRED' }; if (expRadio.value === 'custom' && customDays !== null) { payload.expiration_days = customDays; } if (cnt > 0) payload.limit_count = cnt; const normalizeShareData = raw => (raw && raw.data && typeof raw.data === 'object') ? raw.data : (raw || {}); const getShareErrorMessage = (raw, fallback) => { const src = normalizeShareData(raw); return src.error_description || raw?.error_description || src.error || raw?.error || fallback; }; const ensureShareUrl = (shareData, raw) => { if (!shareData.share_url) throw new Error(getShareErrorMessage(raw, L.err_unknown)); }; const res = await fetch(`https://api-drive.mypikpak.com/drive/v1/share`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(payload) }); if (!res.ok) { const err = await res.json().catch(() => ({})); if ((err.error === 'share_status_prohibited' || err.error_code === 9) && err.error_description) { throw new Error(err.error_description); } throw new Error(getShareErrorMessage(err, `Create API Error ${res.status}`)); } const rawData = await res.json().catch(() => ({})); let data = normalizeShareData(rawData); let finalPassCode = data.pass_code || ''; if (!data.share_id) throw new Error(getShareErrorMessage(rawData, L.err_unknown)); 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) { const patchRaw = await patchRes.json().catch(() => ({})); const patchData = normalizeShareData(patchRaw); data = { ...data, ...patchData }; finalPassCode = data.pass_code || pass; } else { const patchErr = await patchRes.json().catch(() => ({})); console.warn("[Share] Custom password patch failed, using fallback."); const patchMsg = getShareErrorMessage(patchErr, ''); if (!finalPassCode && patchMsg) throw new Error(patchMsg); } } ensureShareUrl(data, rawData); if (mode === 'encrypted' && !finalPassCode) { throw new Error(getShareErrorMessage(data, L.err_unknown)); } const shareUrl = data.share_url; const fullText = shareUrl + (finalPassCode ? ` ${L.lbl_share_code}: ${finalPassCode}` : ''); const resM = showModal(`
${L.title_share_result}
${finalPassCode ? `
${L.lbl_share_code}: ${esc(finalPassCode)}
` : ''}
`); const modalBox = resM.querySelector('.pk-modal'); if (modalBox) { modalBox.style.width = 'auto'; modalBox.style.minWidth = '460px'; modalBox.style.overflow = 'visible'; modalBox.style.padding = '30px'; } resM.querySelector('#res_copy').onclick = () => { GM_setClipboard(fullText); const b = resM.querySelector('#res_copy'); b.textContent = L.msg_copy_success; setTimeout(() => resM.remove(), 1000); }; } catch (e) { showAlert(e.message); } finally { setLoad(false); } }; }; ctx.querySelector('#ctx-copy-name').onclick = () => { ctx.style.display = 'none'; const names = []; const ids = S.getSelectedIds(); const offlineTaskCopyExtBlacklist = new Set(CONF.offlineTaskCopyExtBlacklist || []); const getNameExt = (name) => { const idx = name.lastIndexOf('.'); return idx > 0 && idx < name.length - 1 ? name.slice(idx + 1).toLowerCase() : ''; }; const shouldStripNameExt = (item, name) => { if (!item || item.kind === 'drive#folder') return false; const idx = name.lastIndexOf('.'); if (idx <= 0) return false; if (S.offlineMode && item.kind === 'drive#task') { if (item._ref_kind === 'drive#file') return true; if (item._ref_kind) return false; return offlineTaskCopyExtBlacklist.has(getNameExt(name)); } return true; }; ids.forEach(id => { const item = S.itemMap.get(id); if (item && item.name) { const name = item.name; if (shouldStripNameExt(item, name)) { names.push(name.substring(0, name.lastIndexOf('.'))); } else { names.push(name); } } }); if (names.length > 0) { GM_setClipboard(names.join('\n')); showToast(L.msg_copy_success); } }; ctx.querySelector('#ctx-down').onclick = () => { ctx.style.display = 'none'; UI.win.querySelector('#pk-down').click(); }; ctx.querySelector('#ctx-add-bl').onclick = (e) => { ctx.style.display = 'none'; const action = e.currentTarget.getAttribute('data-action'); processBlacklistAction(action); }; ctx.querySelector('#ctx-copy').onclick = () => { ctx.style.display = 'none'; UI.btnCopy.click(); }; ctx.querySelector('#ctx-cut').onclick = () => { ctx.style.display = 'none'; UI.btnCut.click(); }; ctx.querySelector('#ctx-prune').onclick = () => { ctx.style.display = 'none'; UI.btnPrune.click(); }; ctx.querySelector('#ctx-rename').onclick = () => { ctx.style.display = 'none'; if (S.getSelectedCount() > 1) { UI.btnBulkRename.click(); } else { UI.btnRename.click(); } }; const ctxDel = ctx.querySelector('#ctx-del'); ctxDel.onclick = () => { ctx.style.display = 'none'; UI.btnDel.click(); }; if (S.historyMode) { const ctxDelTxt = ctxDel.childNodes[1]; if (ctxDelTxt) ctxDelTxt.textContent = " " + L.btn_clear_history; } const ctxShCancel = ctx.querySelector('#ctx-sh-cancel'); if (ctxShCancel) { ctxShCancel.onclick = () => { ctx.style.display = 'none'; if (UI.btnCancelShare) UI.btnCancelShare.click(); }; } const ctxShDetail = ctx.querySelector('#ctx-sh-detail'); if (ctxShDetail) { ctxShDetail.onclick = () => { ctx.style.display = 'none'; const id = S.getSelectedIds()[0]; const item = S.itemMap.get(id); if (item) showShareDetail(item); }; } const ctxShCopy = ctx.querySelector('#ctx-sh-copy'); if (ctxShCopy) { ctxShCopy.onclick = () => { ctx.style.display = 'none'; const id = S.getSelectedIds()[0]; const item = S.itemMap.get(id); if (!item) return; const url = item.share_url || ""; const pwd = item.pass_code || ""; const title = item.name || item.title || ""; const lines = [url]; if (pwd) lines.push(`${L.share_copy_pwd}: ${pwd}`); if (title) lines.push(title); GM_setClipboard(lines.join('\n')); 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.btnNavShareParse, UI.btnNavLinkBookmark, UI.btnNavStarred, UI.btnNavRecent, UI.btnNavHistory, UI.btnNavOffline, UI.btnNavUpload]; navs.forEach(n => { if(n) n.classList.remove('act'); }); if (UI.win) UI.win.classList.toggle('pk-link-bookmark-mode', !!S.linkBookmarkMode); const stdBtns =[UI.btnNewFolder, UI.btnDel, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnMigrate, UI.btnBlacklistManager]; const shareBtns =[UI.btnCancelShare]; const upBtns =[UI.btnUpPause, UI.btnUpStart, UI.btnUpDel, UI.btnUpClearAll]; const upSep = el.querySelector('#pk-up-sep'); const downBtns = [UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnExportM3U, UI.btnImgSearch]; upBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(upSep) upSep.style.display = 'none'; if (UI.btnClearHistoryAll) UI.btnClearHistoryAll.style.display = 'none'; if (UI.btnShareParseSave) UI.btnShareParseSave.style.display = 'none'; if (UI.btnShareParseInsight) UI.btnShareParseInsight.style.display = 'none'; if (UI.btnShareParseStopScan) UI.btnShareParseStopScan.style.display = 'none'; if (UI.btnShareParseBackList) UI.btnShareParseBackList.style.display = 'none'; if (UI.actionBar) UI.actionBar.querySelectorAll('.pk-sep').forEach(sep => { sep.style.display = ''; }); if (UI.btnRefresh) { UI.btnRefresh.style.display = 'inline-flex'; UI.btnRefresh.disabled = false; } if (UI.btnBlacklistManager) UI.btnBlacklistManager.disabled = false; const mainHeader = UI.win ? UI.win.querySelector('.pk-grid-hd') : null; if (mainHeader) { const keepHeaderHidden = !!(S._shareParseGridExitSync || S._folderViewSyncing || (S.shareParseMode && !S.shareParseListActive)); mainHeader.style.display = (S.linkBookmarkMode || (S.shareParseMode && !S.shareParseListActive)) ? 'none' : ''; mainHeader.style.visibility = keepHeaderHidden ? 'hidden' : ''; } if (S.trashMode) { UI.win.classList.add('pk-mode-trash'); if(UI.btnNavTrash) UI.btnNavTrash.classList.add('act'); if(UI.actionBar) UI.actionBar.style.display = 'none'; if(UI.trashBar) UI.trashBar.style.display = 'flex'; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.trash_title; } else if (S.shareMode) { if(UI.btnNavShare) UI.btnNavShare.classList.add('act'); if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if (S.path[0]) S.path[0].name = L.btn_nav_share; stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); shareBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); } else if (S.shareParseMode) { if(UI.btnNavShareParse) UI.btnNavShareParse.classList.add('act'); stopLiveManualRefreshBeforePathChange('mode_change'); S.path = [{ id: 'share_parse_root', name: L.title_share_parse }]; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if(UI.actionBar) { UI.actionBar.style.display = S.shareParseListActive ? 'flex' : 'none'; UI.actionBar.querySelectorAll('.pk-sep').forEach(sep => { sep.style.display = 'none'; }); } if(UI.trashBar) UI.trashBar.style.display = 'none'; stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnRetryTask, UI.btnCopyLinkOffline].forEach(b => { if(b) b.style.display = 'none'; }); downBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnAria2, UI.btnDown, UI.btnExt, UI.btnExportM3U, UI.btnImgSearch].forEach(b => { if(b) b.style.display = S.shareParseListActive ? 'inline-flex' : 'none'; }); if(UI.btnRefresh) UI.btnRefresh.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.chkGlobal) UI.chkGlobal.checked = false; 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.dupTools) UI.dupTools.style.display = 'none'; if(UI.dupFilters) UI.dupFilters.style.display = 'none'; if(UI.offTools) UI.offTools.style.display = 'none'; if(UI.upTools) UI.upTools.style.display = 'none'; if(UI.filterBar) UI.filterBar.style.display = 'none'; setSearchWrapVisible(!!S.shareParseListActive); if(UI.searchClear) UI.searchClear.style.display = (S.shareParseListActive && S.search) ? 'flex' : 'none'; if(UI.lblSearchPath) UI.lblSearchPath.style.display = (S.shareParseListActive && S.shareParseInsightMode) ? 'flex' : 'none'; if(UI.stat) UI.stat.style.display = S.shareParseListActive ? '' : 'none'; updateShareParseSaveButton(); updateShareParseInsightButtons(); } else if (S.linkBookmarkMode) { if(UI.btnNavLinkBookmark) UI.btnNavLinkBookmark.classList.add('act'); stopLiveManualRefreshBeforePathChange('mode_change'); S.path = [{ id: 'link_bookmark_root', name: L.title_link_bookmark }]; if(UI.bottomGrp) UI.bottomGrp.style.display = 'none'; if(UI.actionBar) UI.actionBar.style.display = 'none'; if(UI.trashBar) UI.trashBar.style.display = 'none'; stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnRetryTask, UI.btnCopyLinkOffline].forEach(b => { if(b) b.style.display = 'none'; }); downBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnRefresh) UI.btnRefresh.style.display = 'none'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; if(UI.lblGlobal) UI.lblGlobal.style.display = 'none'; if(UI.chkGlobal) UI.chkGlobal.checked = false; if(UI.btnAnalyze) UI.btnAnalyze.style.display = 'none'; if(UI.btnExport) UI.btnExport.style.display = 'none'; if(UI.scan) UI.scan.style.display = 'none'; if(UI.cntFolderFirst) UI.cntFolderFirst.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 = 'none'; if(UI.upTools) UI.upTools.style.display = 'none'; if(UI.filterBar) UI.filterBar.style.display = 'none'; if(UI.viewSwitch) UI.viewSwitch.style.display = 'none'; setSearchWrapVisible(false); if(UI.stat) UI.stat.style.display = 'none'; renderCrumb(); } else if (S.offlineMode) { if(UI.btnNavOffline) UI.btnNavOffline.classList.add('act'); if (S.path[0]) S.path[0].name = L.title_offline; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex';[UI.btnNewFolder, UI.btnCopy, UI.btnCut, UI.btnPaste, UI.btnRename, UI.btnBulkRename, UI.btnPrune, UI.btnUnzip, UI.btnMigrate].forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnDel, UI.btnRefresh, UI.btnBlacklistManager].forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; if(UI.btnExportM3U) UI.btnExportM3U.style.display = 'inline-flex'; if(UI.btnImgSearch) UI.btnImgSearch.style.display = 'inline-flex'; } else if (S.uploadMode) { if(UI.btnNavUpload) UI.btnNavUpload.classList.add('act'); if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; if (S.path[0]) S.path[0].name = L.btn_nav_upload; [UI.btnAria2, UI.btnDown].forEach(b => { if(b) b.style.display = 'none'; }); if(UI.btnExt) UI.btnExt.style.display = 'inline-flex'; if(UI.btnExportM3U) UI.btnExportM3U.style.display = 'inline-flex'; if(UI.btnImgSearch) UI.btnImgSearch.style.display = 'inline-flex'; stdBtns.forEach(b => { if(b) b.style.display = 'none'; }); if (UI.btnRefresh) UI.btnRefresh.style.display = 'none'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); upBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); if(upSep) upSep.style.display = 'block'; if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; } else if (S.historyMode) { if(UI.btnNavHistory) UI.btnNavHistory.classList.add('act'); if (S.path[0]) S.path[0].name = L.btn_nav_history; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b && b !== UI.btnBlacklistManager && b !== UI.btnMigrate) b.style.display = 'none'; }); if (UI.btnClearHistoryAll) UI.btnClearHistoryAll.style.display = 'inline-flex'; if (UI.btnBlacklistManager) UI.btnBlacklistManager.style.display = 'inline-flex'; if (UI.btnMigrate) UI.btnMigrate.style.display = 'inline-flex'; shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; downBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); } else if (S.recentMode || S.starredMode) { if (S.recentMode && UI.btnNavRecent) UI.btnNavRecent.classList.add('act'); if (S.starredMode && UI.btnNavStarred) UI.btnNavStarred.classList.add('act'); if (S.recentMode && S.path[0]) S.path[0].name = L.btn_nav_recent; if (S.starredMode && S.path[0]) S.path[0].name = L.btn_nav_starred; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'none'; downBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); } else { if(UI.btnNavHome) UI.btnNavHome.classList.add('act'); if (S.path.length > 0 && S.path[0].id === '') S.path[0].name = L.btn_nav_home; if(UI.bottomGrp) UI.bottomGrp.style.display = 'flex'; stdBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); shareBtns.forEach(b => { if(b) b.style.display = 'none'; }); if(UI.uploadWrap) UI.uploadWrap.style.display = 'inline-flex'; downBtns.forEach(b => { if(b) b.style.display = 'inline-flex'; }); } if(UI.topBar) UI.topBar.style.display = 'flex'; if(UI.crumb) { UI.crumb.style.opacity = '1'; UI.crumb.style.display = 'flex'; } syncLocalUploadVisibility(); syncRefreshButtonVisibility(); if (S.linkBookmarkMode) { setLoad(false); renderCrumb(); loadLinkBookmarkOfficial(true); updateStat(); return; } if (S.shareParseMode) { setLoad(false); renderCrumb(); if (typeof applyResolvedSortState === 'function') applyResolvedSortState(); if (S.shareParseListActive) { syncShareParseMirror(); renderList(); } else { renderShareParsePanel(); } syncShareParseInsightFilterBar(); updateShareParseInsightButtons(); updateStat(); return; } refresh(); }; restoreUIState(); load(); window.pkForceManagerReloadAfterAuth = (() => { let timer = null; let reloadSeq = 0; let authReloading = false; let pendingRelayout = false; const flushDeferredRelayout = () => { if (!pendingRelayout) return; pendingRelayout = false; if (!el || el.style.display === 'none') return; requestAnimationFrame(() => { if (!el || el.style.display === 'none') return; if (typeof syncLayoutMetrics === 'function') syncLayoutMetrics(); if (isGridView() && typeof scheduleGridRelayout === 'function') { scheduleGridRelayout(true); } else if (typeof renderList === 'function') { renderList(); } else if (typeof renderVisible === 'function') { if (UI.in) UI.in.style.height = `${S.display.length * CONF.rowHeight}px`; renderVisible(); } }); }; const finishAuthReload = (seq) => { if (seq !== reloadSeq) return; authReloading = false; requestAnimationFrame(flushDeferredRelayout); }; window.pkIsAuthManagerReloading = () => authReloading; window.pkDeferAuthManagerRelayout = () => { pendingRelayout = true; }; return (reason = 'auth-recovered') => { if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; try { const ov = document.querySelector('.pk-ov'); if (!ov || ov.style.display === 'none') return; if (!S || !Array.isArray(S.path) || S.path.length === 0) return; if (S.shareParseMode) return; const curNode = S.path[S.path.length - 1] || { id: '' }; const folderId = curNode.id || 'root'; const cacheKey = S.getRealCacheKey ? S.getRealCacheKey(folderId) : folderId; const authReloadKey = (!curNode.id || curNode.id === 'root') ? 'root' : String(curNode.id); const lastOk = window.__pkLastManagerLoadOk; if (lastOk && lastOk.key === authReloadKey && Date.now() - lastOk.at < 3500) { pendingRelayout = true; requestAnimationFrame(flushDeferredRelayout); return; } if (typeof globalNeedsSync !== 'undefined') globalNeedsSync = true; if (typeof globalCache !== 'undefined') { globalCache.delete(cacheKey); if (folderId === 'root') { globalCache.delete(''); globalCache.delete('root'); } } if (S.cache) { S.cache.delete(cacheKey); if (folderId === 'root') { S.cache.delete(''); S.cache.delete('root'); } } if (typeof globalDirtyFolders !== 'undefined') { globalDirtyFolders.add(folderId === 'root' ? '' : folderId); if (folderId === 'root') globalDirtyFolders.add('root'); } if (typeof scannedFolderIds !== 'undefined') { scannedFolderIds.delete(folderId === 'root' ? '' : folderId); } const seq = ++reloadSeq; authReloading = true; pendingRelayout = false; let reloadTask = Promise.resolve(); if (typeof load === 'function') { reloadTask = Promise.resolve(load(false, true)).catch(e => console.warn('[Auth Sync] Forced reload failed:', e)); } else if (typeof window.pkSmartRefreshTrigger === 'function') { window.pkSmartRefreshTrigger(true); } else if (typeof refresh === 'function') { refresh(); } if (typeof runBackgroundCrawler === 'function' && typeof isBackgroundRunning !== 'undefined' && !isBackgroundRunning) { runBackgroundCrawler(); } reloadTask.finally(() => finishAuthReload(seq)); } catch (e) { authReloading = false; requestAnimationFrame(flushDeferredRelayout); console.warn('[Auth Sync] Force reload error:', e); } }, 260); }; })(); const 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; if (S.shareParseMode) 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)); 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; if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') return; const recentSession = globalCache.get('recent_session'); const recentCached = globalCache.get('recent_root'); const recentPaginationPending = !!(S.recentMode && ((!recentSession || !recentSession.completed) || (recentCached && !Array.isArray(recentCached) && recentCached.nextToken))); const historySession = globalCache.get('history_session'); const historyCached = globalCache.get('history_root'); const historyPaginationPending = !!(S.historyMode && ((!historySession || !historySession.completed) || (historyCached && !Array.isArray(historyCached) && historyCached.nextToken))); if (recentPaginationPending) { return; } if (historyPaginationPending) { return; } const isTyping = ['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName); const hasUIState = S.getSelectedCount() > 0 || (UI.ctx && UI.ctx.style.display === 'block'); const isInteracting = !bypassLock && (Date.now() - lastUserInteractionTime < 1500); const isBusy = S._isEmptyingTrash || S.loading || S.scanning || S.dupMode || S.isFlattened || hasUIState || isTyping || isInteracting; if (isBusy) { if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => checkAndRefresh(true, bypassLock), 2500); return; } if (S.historyMode || S.uploadMode || S.shareParseMode || S.linkBookmarkMode) 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 && cur && cur.id === 'recent_root'; if (isRecentRoot) { return; } if (isOfflineLightProbeModeRoot()) { return; } const cacheKey = S.shareMode ? 'share_root' : (isStarredRoot ? 'starred_root' : (S.getRealCacheKey ? S.getRealCacheKey(cur.id || 'root') : (cur.id || 'root'))); if (cur.id === 'virtual_search_root' || cur.id === 'analyze_root' || cur.id === 'upload_root' || cur.id === 'share_parse_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)); return true; } return false; }; let allFetchedItems = []; if (!S.shareMode) { apiShareList().then(list => runWatchdogAudit(list)); } if (S.shareMode) { allFetchedItems = await apiShareList(); await runWatchdogAudit(allFetchedItems); } else if (S.offlineMode) { const rawTasks =[]; await apiTaskList(1000, (batch) => { if (batch && batch.length) rawTasks.push(...batch); }); allFetchedItems = rawTasks.map(t => { const ref = t.reference_resource || {}; return { id: t.id, kind: 'drive#task', name: ref.name || t.name || t.file_name || 'Untitled Task', size: t.file_size, phase: t.phase, progress: parseInt(t.progress || 0), message: t.message, icon_link: t.icon_link, thumbnail_link: ref.thumbnail_link ? ref.thumbnail_link : t.icon_link, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || '', file_id: t.file_id || '', source_url: (t.params && t.params.url) ? t.params.url : '', params: Object.assign({}, t.params || {}, ref.params || {}), mime_type: ref.mime_type || '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))) }; }); } else if (S.historyMode) { return; } else if (isRecentRoot) { let nextToken = null; const limit = 500; do { if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.getSelectedCount() > 0) return; const filters = encodeURIComponent('{"phase":{"in":"PHASE_TYPE_COMPLETE"}}'); const url = `https://api-drive.mypikpak.com/drive/v1/tasks?limit=${limit}&filters=${filters}&thumbnail_size=SIZE_MEDIUM&with_reference_resource=true&_t=${Date.now()}${nextToken ? `&page_token=${nextToken}` : ''}`; const netPriority = bypassLock ? 'high' : 'low'; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: netPriority }); if (!res.ok) { if (res.status === 401 || res.status === 403) { console.warn("[Recent] SWR fetch auth rejected."); const didLogout = await confirmedLogout('recent-swr-fetch-auth', 5000, 5200); if (didLogout) return; return; } throw new Error("Recent SWR fetch error"); } const json = await res.json(); const validTasks = (json.tasks ||[]).filter(t => t.phase === 'PHASE_TYPE_COMPLETE' && (t.type === 'offline' || t.type === 'upload') && t.file_id !== "" ); const mapped = validTasks.map(t => { const ref = t.reference_resource || {}; const mime = ref.mime_type || ''; const isFolder = (ref.kind === 'drive#folder') || (mime === 'application/x-directory') || (t.icon_link && t.icon_link.includes('folder')); return { id: t.file_id || t.id, kind: isFolder ? 'drive#folder' : 'drive#file', name: ref.name || t.file_name || t.name, size: t.file_size, thumbnail_link: ref.thumbnail_link || t.icon_link || '', icon_link: t.icon_link || '', web_content_link: t.file_id ? null : null, created_time: t.created_time, modified_time: t.updated_time || ref.modified_time || t.created_time, mime_type: mime, parent_id: '', starred: !!(ref.starred || (ref.tags && ref.tags.some(tg => tg.name === 'STAR'))), trashed: false, params: Object.assign({}, t.params || {}, ref.params || {}), _sourceTaskId: t.id }; }); allFetchedItems.push(...mapped); nextToken = json.next_page_token; if (allFetchedItems.length >= 2000) break; } while (nextToken); const seen = new Set(); allFetchedItems = allFetchedItems.filter(f => { if (seen.has(f.id)) return false; seen.add(f.id); return true; }); } else { let nextToken = null; const limit = 500; const now = Date.now(); do { if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.getSelectedCount() > 0) return; const currentIsStarredRoot = S.starredMode && S.path.length === 1; const targetParentId = (S.trashMode || currentIsStarredRoot) ? '*' : (cur.id || ''); const filterObj = { "trashed": { "eq": S.trashMode } }; if (currentIsStarredRoot) { filterObj.trashed = { "eq": false }; filterObj.system_tag = { "in": "STAR" }; } else if (!S.trashMode) { filterObj.phase = { "eq": "PHASE_TYPE_COMPLETE" }; } const filters = `&filters=${encodeURIComponent(JSON.stringify(filterObj))}`; const url = `https://api-drive.mypikpak.com/drive/v1/files?thumbnail_size=SIZE_MEDIUM&limit=${limit}${filters}&parent_id=${targetParentId}&_t=${now}${nextToken ? `&page_token=${nextToken}` : ''}`; const netPriority = bypassLock ? 'high' : 'low'; const res = await fetch(url, { headers: getHeaders(), signal: signal, priority: netPriority }); if (!res.ok) { if (res.status === 401 || res.status === 403) { console.warn("[Recent] Silent fetch auth rejected."); const didLogout = await confirmedLogout('recent-silent-fetch-auth', 5000, 5200); if (didLogout) return; return; } throw new Error("Silent fetch error"); } const data = await res.json(); if (data.files) { allFetchedItems.push(...data.files.map(f => minifyFile(f, true))); } nextToken = data.next_page_token; } while (nextToken); } if (S.analyzeMode && S.analyzeMap) { allFetchedItems.forEach(item => { if (item.kind === 'drive#folder' && S.analyzeMap.has(item.id)) { item.size = S.analyzeMap.get(item.id).size.toString(); } }); } if (document.hidden || signal.aborted || S.loading || S.scanning || S.dupMode || S.isFlattened || S.getSelectedCount() > 0) return; const nowCur = S.path[S.path.length - 1]; const nowStarredRoot = S.starredMode && S.path.length === 1; let currentExpectedKey = 'root'; if (S.shareMode) currentExpectedKey = 'share_root'; else if (S.offlineMode) currentExpectedKey = 'offline_root'; else if (nowStarredRoot) currentExpectedKey = 'starred_root'; else currentExpectedKey = S.getRealCacheKey ? S.getRealCacheKey(nowCur.id || 'root') : (nowCur.id || 'root'); if (currentExpectedKey === cacheKey) { diffWorker.onmessage = (e) => { const hasChanges = e.data; if (!hasChanges) { return; } if ((!bypassLock && Date.now() - lastUserInteractionTime < 1500) || S.getSelectedCount() > 0) { if (retryTimer) clearTimeout(retryTimer); retryTimer = setTimeout(() => checkAndRefresh(true, bypassLock), 2000); return; } if (allFetchedItems.length > 0) { const sample = allFetchedItems[0]; if (!S.shareMode) { if (S.trashMode && !sample.trashed) { console.warn("[SmartRefresh] Dirty data blocked: Home data trying to enter Trash view."); return; } else if (!S.trashMode && sample.trashed) { console.warn("[SmartRefresh] Dirty data blocked: Trash data trying to enter Home view."); return; } } } S.cache.set(cacheKey, allFetchedItems); if (typeof globalCache !== 'undefined') globalCache.set(cacheKey, allFetchedItems); const newItemMap = new Map(); const newStarredSet = new Set(); for (let i = 0; i < allFetchedItems.length; i++) { const item = allFetchedItems[i]; newItemMap.set(item.id, item); if (item.starred || (item.tags && item.tags.some(t => t.name === 'STAR'))) { newStarredSet.add(item.id); } } requestAnimationFrame(() => { if (S.getSelectedCount() > 0 || S.loading || S.scanning) return; const scrollTop = UI.vp ? UI.vp.scrollTop : 0; const sameOrder = S.items.length === allFetchedItems.length && S.items.every((oldItem, idx) => oldItem && allFetchedItems[idx] && oldItem.id === allFetchedItems[idx].id); const canPatchVisible = sameOrder && !S.search && !S.dupMode && !S.isFlattened && !S.analyzeMode; const isResumeQuietPatch = Date.now() < Number(S.resumeQuietUntil || 0); S.items.splice(0, S.items.length, ...allFetchedItems); S.itemMap = newItemMap; S.starredSet = newStarredSet; if (canPatchVisible) { const updateDisplayItem = (item) => item && !item.isHeader && item.id ? (newItemMap.get(item.id) || item) : item; S.display = Array.isArray(S.display) ? S.display.map(updateDisplayItem) : []; if (!isResumeQuietPatch && typeof renderVisible === 'function') renderVisible(); if (typeof updateStat === 'function') updateStat(); if (UI.vp) UI.vp.scrollTop = scrollTop; return; } refresh(); if (UI.vp) UI.vp.scrollTop = scrollTop; }); }; 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(() => {}); 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) => { if (S.shareParseMode) return; if (S.liveRefreshCtx && S.liveRefreshCtx.status === 'refreshing') return; const session = globalCache.get('offline_session'); if (S.offlineMode && (!session || !session.completed) && !isForce) { return; } const cur = S.path[S.path.length - 1]; const isRecentRoot = S.recentMode && S.path.length === 1 && cur && cur.id === 'recent_root'; if (isRecentRoot) { return; } if (isOfflineLightProbeModeRoot()) { return; } checkAndRefresh(false, isForce); }; const onVisibilityChange = () => { if (retryTimer) clearTimeout(retryTimer); if (document.hidden) return; S.resumeQuietUntil = Date.now() + 2000; checkAndRefresh(false, false); runWatchdogAudit(); }; const uiSyncTimer = setInterval(() => checkAndRefresh(false, true), 60000); const watchdogTimer = setInterval(runWatchdogAudit, 60000); document.addEventListener('visibilitychange', onVisibilityChange); return { handler: onVisibilityChange, abort: () => { if(silentAbortController) silentAbortController.abort(); if(retryTimer) clearTimeout(retryTimer); if(uiSyncTimer) clearInterval(uiSyncTimer); if(watchdogTimer) clearInterval(watchdogTimer); document.removeEventListener('visibilitychange', onVisibilityChange); el.removeEventListener('mousedown', updateInteractionTime, true); el.removeEventListener('keydown', updateInteractionTime, true); if (diffWorker) diffWorker.terminate(); delete window.pkSmartRefreshTrigger; } }; }; const visibilityListener = startSmartRefresh(); function handleClose() { let safePath = [...S.path]; if (safePath.some(n => n.id === 'virtual_search_root' || n.id === 'analyze_root')) { safePath = S.preSearchPath || [{ id: '', name: L.btn_nav_home }]; } globalSavedState = { path: safePath, trashMode: S.trashMode, shareParseMode: S.shareParseMode, linkBookmarkMode: S.linkBookmarkMode, 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); } stopOfflineLightProbe('mode_change'); pkState = null; delete window.pkUpdateCrawlerUI; if (S && S.broadcast) S.broadcast.close(); if (visibilityListener && visibilityListener.abort) { visibilityListener.abort(); } closeTransientDropdowns(true); if (typeof destroyTooltip === 'function') destroyTooltip(); if (pkWinHideObserver) pkWinHideObserver.disconnect(); el.remove(); document.removeEventListener('keydown', keyHandler); document.removeEventListener('mouseup', mouseHandler); if (window._pkMouseSideNavHandler) { window.removeEventListener('mousedown', window._pkMouseSideNavHandler, true); window.removeEventListener('mouseup', window._pkMouseSideNavHandler, true); window.removeEventListener('auxclick', window._pkMouseSideNavHandler, true); window.removeEventListener('click', window._pkMouseSideNavHandler, true); } document.body.style.overflow = ''; document.documentElement.style.overflow = ''; } UI.btnClose.addEventListener('click', () => { document.body.classList.remove('pk-body-max'); el.style.display = 'none'; }); updateCrawlerUI(); setTimeout(() => runScriptUpdateCheck(), 1800); 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; if (typeof renderVisible === 'function') renderVisible(); } } catch (e) {} } const fmtTransferQuotaSize = (v) => { let n = Number(v || 0); if (!Number.isFinite(n) || n <= 0) return '0.00 KB'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } const truncated = Math.floor((n + Number.EPSILON) * 100) / 100; return `${truncated.toFixed(2)} ${units[i]}`; }; const buildTransferQuotaRowHtml = (label, quota, extraInfo = '') => { const total = Number((quota && quota.total_assets) || 0); const used = Number((quota && (quota.assets || quota.size)) || 0); const pct = total > 0 ? Math.max(0, Math.min(100, (used / total) * 100)) : 0; return `
${label}
${fmtTransferQuotaSize(total)}
${L.transfer_quota_used_total.replace('{u}', fmtTransferQuotaSize(used)).replace('{t}', fmtTransferQuotaSize(total))} ${extraInfo || ''}
`; }; const showTransferQuotaDetail = async () => { const existingModal = Array.from(document.querySelectorAll('.pk-modal-ov')).find(m => { const title = m.querySelector('h3'); return title && title.textContent === L.modal_transfer_quota_title; }); if (existingModal) return; const triggerBtn = UI && UI.btnTransferQuotaDetail; if (triggerBtn && triggerBtn.dataset.pkOpening === '1') return; if (triggerBtn) triggerBtn.dataset.pkOpening = '1'; try { const res = await fetch(`https://api-drive.mypikpak.com/vip/v1/quantity/list?type=transfer&limit=200&_t=${Date.now()}`, { headers: getHeaders() }); if (!res.ok) { if (res.status === 401 || res.status === 403) { const didLogout = await confirmedLogout('transfer-quota-auth-rejected', 5000, 5200); if (didLogout) return; } throw new Error(`HTTP ${res.status}`); } const data = await res.json(); const base = data && data.base; if (!base) throw new Error('base missing'); const isTransferQuotaNonVip = base.sub_status === false || String(base.vip_status || '').toLowerCase() !== 'valid'; const hasTransferQuotaDailyDownload = base.download_daily && Number(base.download_daily.total_assets || 0) > 0; const m = showModal(`

${L.modal_transfer_quota_title}

${L.transfer_quota_desc}
${L.transfer_quota_monthly}
${buildTransferQuotaRowHtml(L.transfer_quota_cloud_download, base.offline)} ${buildTransferQuotaRowHtml(L.transfer_quota_download, base.download, L.transfer_quota_download_note)} ${buildTransferQuotaRowHtml(L.transfer_quota_upload, base.upload)} ${isTransferQuotaNonVip && hasTransferQuotaDailyDownload ? buildTransferQuotaRowHtml(L.transfer_quota_daily_non_vip, base.download_daily) : ''}
`); const modalBox = m.querySelector('.pk-modal'); if (modalBox) Object.assign(modalBox.style, { width: '760px', maxWidth: '92vw', maxHeight: 'calc(100vh - 48px)', padding: 'clamp(20px,3.4vh,28px) 30px clamp(24px,4.2vh,44px)', boxSizing: 'border-box', overflowY: 'auto', overflowX: 'hidden' }); const closeBtn = m.querySelector('.pk-modal-close'); if (closeBtn) Object.assign(closeBtn.style, { top: 'clamp(18px,3vh,24px)', right: '24px' }); } catch (e) { showToast(L.err_transfer_quota_fetch, 'error'); } finally { const triggerBtn = UI && UI.btnTransferQuotaDetail; if (triggerBtn) delete triggerBtn.dataset.pkOpening; } }; const refreshQuotaText = () => { const txt = el.querySelector('#pk-quota-txt'); if (!txt || !S.quota) return; const isMaxNow = UI.win.classList.contains('pk-maximized'); const isCompactQuota = el.classList.contains('pk-hide-btn-text') || el.classList.contains('pk-auto-hide-btn-text'); txt.textContent = (isMaxNow && !isCompactQuota) ? `${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) { const didLogout = await confirmedLogout('quota-auth-rejected', 5000, 5200); if (didLogout) return; } return; } const data = await res.json(); const q = data.quota; if (!q) return; const used = parseInt(q.usage); const limit = parseInt(q.limit); const pct = Math.min(100, (used / limit) * 100).toFixed(1); const fQ = (v, d) => { let n = v, i = 0, units = ['B','KB','MB','GB','TB']; while(n >= 1024 && i < 4) { n /= 1024; i++; } return n.toFixed(d) + ' ' + units[i]; }; S.quota = { usedStr: fQ(used, 2), limitStr: fQ(limit, 0), pct: pct, usedRaw: used, limitRaw: limit }; const bar = el.querySelector('#pk-quota-bar'); const panel = el.querySelector('#pk-quota-panel'); if (bar) bar.style.width = pct + '%'; refreshQuotaText(); if (panel) panel.setAttribute('data-pk-tip', `${L.lbl_storage}: ${pct}% (${S.quota.usedStr} / ${S.quota.limitStr})`); if (bar) { if (parseFloat(pct) > 90) bar.style.background = '#d93025'; else if (parseFloat(pct) > 70) bar.style.background = '#faad14'; else bar.style.background = 'var(--pk-pri)'; } } catch (e) {} }; if (UI.btnTransferQuotaDetail) { UI.btnTransferQuotaDetail.onclick = (e) => { if (e) { e.preventDefault(); e.stopPropagation(); } showTransferQuotaDetail(); }; } updateQuotaUI(); const quotaTimer = setInterval(updateQuotaUI, 300000); const originalClose = UI.btnClose.onclick; UI.btnClose.onclick = (e) => { clearInterval(quotaTimer); if(originalClose) originalClose.call(UI.btnClose, e); }; await load(false, true); if (globalSavedState && globalSavedState.scrollTop !== undefined) { setTimeout(() => { if (typeof UI !== 'undefined' && UI.vp) { UI.vp.scrollTop = globalSavedState.scrollTop; delete globalSavedState.scrollTop; } }, 50); } syncGlobalStarredStatus(); if (gmGet('pk_turbo_mode', false)) { setTimeout(() => { if (location.href.includes('/login') || location.pathname.includes('login')) return; if (window.__pkTurboActivatedToastShown) return; window.__pkTurboActivatedToastShown = true; document.querySelectorAll('.pk-msg-toast').forEach(n => { if ((n.textContent || '').trim() === getStrings().msg_turbo_activated) n.remove(); }); if (typeof showToast === 'function') { showToast(getStrings().msg_turbo_activated, 'success', 5000); } }, 800); } setTimeout(async () => { const stubStr = gmGet('pk_migration_stub', ''); if (!stubStr) return; try { const stub = JSON.parse(stubStr); const isValidStub = stub && typeof stub === 'object' && !Array.isArray(stub) && Number.isFinite(Number(stub.timestamp)) && stub.source_uid && stub.share_id && Array.isArray(stub.file_ids) && stub.file_ids.length > 0; if (!isValidStub) { gmSet('pk_migration_stub', ''); return; } if (Date.now() - Number(stub.timestamp) > 86400000) { gmSet('pk_migration_stub', ''); return; } const isAuthReady = await waitForAuth(5000); if (!isAuthReady) { console.warn("[Migration] Auth not ready, aborting detection."); return; } const aboutRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/about?_t=${Date.now()}`, { headers: getHeaders() }); if (!aboutRes.ok) throw new Error(`About API Error ${aboutRes.status}`); const aboutData = await aboutRes.json(); const currentUid = aboutData.sub; if (currentUid === stub.source_uid) { gmSet('pk_migration_stub', ''); showToast(L.msg_migrate_same_account, 'warning'); return; } if (await showConfirm(L.msg_migrate_detect.replace('{n}', stub.file_count), L.btn_migrate)) { setLoad(true); updateLoadTxt(L.msg_migrate_saving); const infoRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/share?share_id=${stub.share_id}&pass_code=${stub.pass_code}`, { method: 'GET', headers: getHeaders() }); if (!infoRes.ok) throw new Error(`Share Info Fetch Error ${infoRes.status}`); const infoData = await infoRes.json(); const passCodeToken = infoData.pass_code_token || ""; const savePayload = { share_id: stub.share_id, pass_code_token: passCodeToken, params: { trace_file_ids: stub.file_ids.join(',') } }; const saveRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/share/restore`, { method: 'POST', headers: getHeaders(), body: JSON.stringify(savePayload) }); if (!saveRes.ok) { const errData = await saveRes.json().catch(() => ({})); if (errData.error === 'file_restore_own' || errData.error_code === 9) { gmSet('pk_migration_stub', ''); setLoad(false); showToast(L.msg_migrate_same_account, 'warning'); return; } if (errData.error === 'file_space_not_enough' || errData.error_code === 8) { setLoad(false); const keep = await showConfirm(L.msg_migrate_quota_err.replace('{d}', errData.error_description), L.title_migrate_fail); if (!keep) gmSet('pk_migration_stub', ''); return; } throw new Error(errData.error_description || `Save API Error ${saveRes.status}`); } const saveData = await saveRes.json(); const migrateTaskRaw = saveData.task_id || saveData.taskId || saveData.restore_task_id || saveData.restoreTaskId || saveData.task_ids || saveData.taskIds || saveData.restore_task_ids || saveData.restoreTaskIds; const migrateTaskId = Array.isArray(migrateTaskRaw) ? migrateTaskRaw[0] : String(migrateTaskRaw || '').split(',')[0].trim(); if (migrateTaskId) { let isDone = false; let pollCount = 0; while(!isDone && pollCount < 300) { await sleep(2000); pollCount++; const tRes = await fetch(`https://api-drive.mypikpak.com/drive/v1/tasks/${encodeURIComponent(migrateTaskId)}`, { headers: getHeaders() }); if (!tRes.ok) continue; const tData = await tRes.json(); if (tData.phase === 'PHASE_TYPE_COMPLETE') isDone = true; else if (tData.phase === 'PHASE_TYPE_ERROR') throw new Error("Transfer task failed on server."); } if (!isDone) console.warn("[Migration] Task polling timed out, but might still be running."); } gmSet('pk_migration_stub', ''); setLoad(false); await showAlert(L.msg_migrate_success, "🎉 " + L.title_alert); UI.btnRefresh.click(); } else { gmSet('pk_migration_stub', ''); } } catch(e) { setLoad(false); const keep = await showConfirm(L.msg_migrate_err_keep.replace('{e}', e.message), L.title_alert); if (!keep) gmSet('pk_migration_stub', ''); } }, 1500); } let backgroundQueue = []; let isBackgroundRunning = false; let scannedFolderIds = new Set(); let globalPreloadPromise = null; let globalCache = new Map(); let globalLineageMap = new Map(); let globalParentIndex = new Map(); let globalTombstoneCache = new Map(); let globalDirtyFolders = new Set(); let globalNeedsSync = false; let isGlobalIndexReady = false; let hasShownGlobalWarnSession = false; let serverClockOffset = 0; let hasSyncedTime = false; let globalSavedState = null; const syncTime = (headers) => { if (!headers) return; const serverDate = headers.get('Date') || headers.get('date'); if (serverDate) { const remoteTime = new Date(serverDate).getTime(); const localTime = Date.now(); serverClockOffset = remoteTime - localTime; hasSyncedTime = true; } }; const getServerNow = () => Date.now() + serverClockOffset; let isGUISensitive = false; let pkState = null; let pkBackgroundResumeTimer = null; function isPkBackgroundSpecialMode(state) { if (!state) return false; const cur = Array.isArray(state.path) && state.path.length ? state.path[state.path.length - 1] : null; const curId = String((cur && cur.id) || ''); return !!( state.trashMode || state.shareMode || state.shareParseMode || state.linkBookmarkMode || state.historyMode || state.offlineMode || state.uploadMode || state.dupMode || state.analyzeMode || state.isFlattened || curId.startsWith('virtual_') || curId === 'analyze_root' ); } function isPkBackgroundPaused() { const state = (typeof pkState !== 'undefined') ? pkState : null; return !!( isGUISensitive === true || isPkBackgroundSpecialMode(state) || (state && (state.scanning === true || state.loading === true)) || document.getElementById('pk-player-ov') ); } function scheduleBackgroundResume(delay = 1200) { const wait = Number.isFinite(Number(delay)) ? Math.max(0, Number(delay)) : 1200; if (pkBackgroundResumeTimer) clearTimeout(pkBackgroundResumeTimer); pkBackgroundResumeTimer = setTimeout(() => { pkBackgroundResumeTimer = null; const state = (typeof pkState !== 'undefined') ? pkState : null; if (isPkBackgroundPaused()) { scheduleBackgroundResume(Math.max(wait, isPkBackgroundSpecialMode(state) ? 5000 : 1200)); return; } if (typeof DurationProber !== 'undefined') DurationProber.checkAndRun(); if (typeof runBackgroundCrawler === 'function') runBackgroundCrawler(); }, wait); } 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 queuedIds = new Set(); let failedDurationProbeSet = new Set(); let isRunning = false; let probeVideo = null; let probeAudio = null; let loopTimer = null; let runToken = 0; let activeId = ''; const parseDuration = (value) => { const n = Number(value || 0); return Number.isFinite(n) && n > 0 ? Math.round(n) : 0; }; const getProbeId = (item) => String((item && item.id) || ''); const getKnownDuration = (item) => { const id = getProbeId(item); return parseDuration((item && item.params && item.params.duration) || 0) || parseDuration(id && S.durationMap && S.durationMap.get(id)) || parseDuration(id && gmGet('pk_duration_' + id, 0)); }; const isProbeAudio = (item) => typeof isAudioLikeItem === 'function' && isAudioLikeItem(item); const isProbeVideo = (item) => typeof isVideoLikeItem === 'function' && isVideoLikeItem(item); const isProbeMedia = (item) => isProbeAudio(item) || isProbeVideo(item); const shouldProbe = (item, isBackground = false) => { const id = getProbeId(item); if (!item || !id || item._pkSkipDurationProbe || item.isHeader || item.kind === 'drive#folder') return false; if (isBackground || failedDurationProbeSet.has(id) || getKnownDuration(item) > 0) return false; return isProbeMedia(item); }; const getProbeElement = (audio) => { if (audio) { if (!probeAudio) { probeAudio = document.createElement('audio'); probeAudio.preload = 'metadata'; probeAudio.muted = true; probeAudio.crossOrigin = 'anonymous'; probeAudio.referrerPolicy = 'no-referrer'; probeAudio.style.display = 'none'; } return probeAudio; } if (!probeVideo) { probeVideo = document.createElement('video'); probeVideo.muted = true; probeVideo.preload = 'metadata'; probeVideo.style.display = 'none'; } return probeVideo; }; const clearProbeElement = (el) => { if (!el) return; el.removeAttribute('src'); try { el.load(); } catch (e) {} }; const markFailed = (item) => { const id = getProbeId(item); if (id) failedDurationProbeSet.add(id); }; const syncDurationToItem = (target, id, dur) => { if (!target || target.id !== id) return; if (!target.params) target.params = {}; target.params.duration = dur; }; const syncDurationEverywhere = (id, dur) => { if (!id || !(dur > 0)) return; if (S.durationMap) S.durationMap.set(id, dur); if (S.itemMap && S.itemMap.get(id)) syncDurationToItem(S.itemMap.get(id), id, dur); [S.items, S.display].forEach(list => Array.isArray(list) && list.forEach(i => syncDurationToItem(i, id, dur))); if (typeof pkState !== 'undefined' && pkState) { if (pkState.itemMap && pkState.itemMap.get(id)) syncDurationToItem(pkState.itemMap.get(id), id, dur); [pkState.items, pkState.display].forEach(list => Array.isArray(list) && list.forEach(i => syncDurationToItem(i, id, dur))); } }; const notifyDurationSaved = (id, dur) => { try { document.dispatchEvent(new CustomEvent('pk-duration-saved', { detail: { id, duration: dur } })); } catch (e) {} }; const startNext = async () => { if (!isRunning || queue.length === 0) { isRunning = false; return; } const currentToken = runToken; if (isPkBackgroundPaused()) { if (loopTimer) clearTimeout(loopTimer); loopTimer = setTimeout(startNext, 2000); return; } const item = queue.shift(); const itemId = getProbeId(item); if (itemId) queuedIds.delete(itemId); if (!shouldProbe(item, false)) { loopTimer = setTimeout(startNext, 0); return; } activeId = itemId; const isAudioProbe = isProbeAudio(item); const probeEl = getProbeElement(isAudioProbe); let watchdog = null; let cleaned = false; const currentMedia = probeEl; const cleanup = (failed = false) => { if (cleaned) return; cleaned = true; if (watchdog) clearTimeout(watchdog); if (currentMedia) { currentMedia.removeEventListener('loadedmetadata', onLoaded); currentMedia.removeEventListener('error', onError); clearProbeElement(currentMedia); } if (failed) markFailed(item); activeId = ''; if (isRunning && currentToken === runToken) { loopTimer = setTimeout(startNext, 1500); } }; const saveAndNotify = (targetItem, dur) => { if (targetItem && targetItem._pkSkipDurationProbe) return; const id = getProbeId(targetItem); const seconds = parseDuration(dur); if (!id || !(seconds > 0)) return; gmSet('pk_duration_' + id, seconds); syncDurationEverywhere(id, seconds); notifyDurationSaved(id, seconds); if (typeof pkState !== 'undefined' && pkState) { if (pkState.historyMode) return; const safeId = window.CSS && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"'); const row = document.querySelector(`.pk-row[data-id="${safeId}"]`); 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(seconds); setTimeout(() => { if(durCol) durCol.style.color = ''; }, 2000); } } if (typeof renderVisible === 'function') renderVisible(); } }; const onLoaded = () => { if (currentToken === runToken) { const dur = parseDuration(currentMedia && currentMedia.duration); if (dur > 0) { saveAndNotify(item, dur); cleanup(false); return; } } cleanup(true); }; const onError = () => { cleanup(true); }; currentMedia.addEventListener('loadedmetadata', onLoaded); currentMedia.addEventListener('error', onError); try { const res = await apiGet(item.id); if (currentToken !== runToken) { cleanup(); return; } const url = isAudioProbe && typeof getAudioDirectUrl === 'function' ? getAudioDirectUrl(res) : (res && res.web_content_link); if (!res || !url) { cleanup(true); return; } 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 (!isAudioProbe && isM3u8) { const fetchOptions = window.AbortSignal ? { signal: AbortSignal.timeout(15000) } : {}; const text = await fetch(url, fetchOptions).then(r => r.text()); let saved = false; 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)); saved = true; } } cleanup(!saved); } else if (!isAudioProbe && (isWmv || isAvi || isFlv || isMkv || isRmvb)) { const rangeEnd = isMkv ? 65535 : 8191; const fetchOptions = { headers: { 'Range': `bytes=0-${rangeEnd}` }, ...(window.AbortSignal ? { signal: AbortSignal.timeout(15000) } : {}) }; let saved = false; 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) { saveAndNotify(item, Math.round(seconds)); saved = true; } } } catch (e) {} cleanup(!saved); } else { if (document.hidden) { queue.unshift(item); if (itemId) queuedIds.add(itemId); isRunning = false; cleanup(); return; } watchdog = setTimeout(() => { cleanup(true); }, 15000); currentMedia.src = url; currentMedia.load(); } } catch(e) { cleanup(true); } }; return { add: (item, isBackground = false) => { if (!shouldProbe(item, isBackground)) return; const id = getProbeId(item); if (queuedIds.has(id) || activeId === id) return; if (isBackground) { queue.push(item); } else { queue.unshift(item); } queuedIds.add(id); if (!isRunning) { isRunning = true; startNext(); } }, checkAndRun: () => { if (!isRunning && queue.length > 0) { isRunning = true; startNext(); } }, reset: () => { runToken++; queue = []; queuedIds = new Set(); failedDurationProbeSet = new Set(); activeId = ''; isRunning = false; if (loopTimer) clearTimeout(loopTimer); if (probeVideo) { clearProbeElement(probeVideo); probeVideo = null; } if (probeAudio) { clearProbeElement(probeAudio); probeAudio = 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; const skipDurationProbe = !!f._pkSkipDurationProbe; const skipNewDurationProbe = skipDurationProbe || isBackground || trashed; if (!duration && id && !skipDurationProbe) 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' && !skipNewDurationProbe) { const probeItem = { id, name, kind, mime_type, parent_id, size }; if ((typeof isVideoLikeItem === 'function' && isVideoLikeItem(probeItem)) || (typeof isAudioLikeItem === 'function' && isAudioLikeItem(probeItem))) { setTimeout(() => DurationProber.add(probeItem, isBackground), 3000); } } const pickTrashMeta = (keys) => { const sources = [f, f.params, f.audit, f.delete_info, f.trash_info, f.recycle_info].filter(Boolean); for (const src of sources) { for (const key of keys) { if (src && src[key] !== undefined && src[key] !== null && src[key] !== '') return src[key]; } } return undefined; }; const trashRemainingDays = pickTrashMeta(['remaining_days', 'remain_days', 'days_left', 'left_days', 'expiration_days_left', 'expire_days_left', 'expires_in_days', 'trash_remaining_days', 'purge_remaining_days']); const trashExpireTime = pickTrashMeta(['expires_at', 'expire_time', 'expired_time', 'expiration_at', 'delete_at', 'delete_time', 'purge_at', 'purge_time', 'trashed_expire_time', 'trash_expire_time']); const trashDeletedTime = pickTrashMeta(['deleted_time', 'delete_time', 'deleted_at', 'trashed_time', 'trashed_at', 'trash_time', 'recycled_time', 'recycle_time']); const trashRetentionDays = pickTrashMeta(['retention_days', 'reserve_days', 'keep_days']); 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, _recent_event_id: f._recent_event_id, _recent_event_type: f._recent_event_type, _recent_event_time: f._recent_event_time, _trash_remaining_days: trashRemainingDays, _trash_expire_time: trashExpireTime, _trash_deleted_time: trashDeletedTime, _trash_retention_days: trashRetentionDays, _minified: true }; }; async function runBackgroundCrawler() { if (isBackgroundRunning) return; if (isPkBackgroundPaused()) { scheduleBackgroundResume(); 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; try { const requeueFolder = (folder) => { if (!folder || folder.id === undefined || folder.id === null) return; if (!backgroundQueue.some(f => f && f.id === folder.id)) { backgroundQueue.unshift(folder); } }; const pauseCrawler = (folder) => { if (folder) requeueFolder(folder); scheduleBackgroundResume(); }; const fetchFolderContents = async (folder) => { if (isPkBackgroundPaused()) { pauseCrawler(folder); return; } 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 (isPkBackgroundPaused()) { pauseCrawler(folder); return; } globalCache.set(folder.id, files); } if (isPkBackgroundPaused()) { pauseCrawler(folder); return; } 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 (isPkBackgroundPaused()) { pauseCrawler(folder); } else { requeueFolder(folder); } } finally { pendingRetries--; } } finally { activeRequests--; } }; while (backgroundQueue.length > 0 || activeRequests > 0 || pendingRetries > 0 || (typeof globalDirtyFolders !== 'undefined' && globalDirtyFolders.size > 0)) { if (isPkBackgroundPaused()) { if (homeBtn) homeBtn.classList.remove('pk-status-dot'); scheduleBackgroundResume(); return; } if (homeBtn) homeBtn.classList.add('pk-status-dot'); if (backgroundQueue.length > 0 && activeRequests < Math.floor(currentConcurrencyLimit)) { const folder = backgroundQueue.pop(); if (isPkBackgroundPaused()) { pauseCrawler(folder); return; } 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; } } } finally { isBackgroundRunning = false; if (window.pkUpdateCrawlerUI) window.pkUpdateCrawlerUI(); if (homeBtn) homeBtn.classList.remove('pk-status-dot'); } } async function preLoadRootFiles(onProgress) { if (globalPreloadPromise) return globalPreloadPromise; globalPreloadPromise = new Promise(async (resolve) => { try { const isAuthReady = await waitForAuth(3500); if (!isAuthReady) { console.warn("Background Crawler: Auth not ready. Halting preload."); globalPreloadPromise = null; resolve(false); return; } if (typeof window.pkCleanupGhostFiles === 'function') window.pkCleanupGhostFiles(); if (isPkBackgroundPaused()) { if (!backgroundQueue.some(f => f && f.id === '')) backgroundQueue.unshift({ id: '', name: 'Root', retryCount: 0 }); scheduleBackgroundResume(); globalPreloadPromise = null; resolve(false); return; } const rootFiles = await apiList('', 1000, onProgress, null, false, true); if (isPkBackgroundPaused()) { if (!backgroundQueue.some(f => f && f.id === '')) backgroundQueue.unshift({ id: '', name: 'Root', retryCount: 0 }); scheduleBackgroundResume(); globalPreloadPromise = null; resolve(false); return; } globalCache.set('root', rootFiles); 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); const msg = String((e && e.message) || e || ''); if (/400|401|403|CAPTCHA|AUTH_RETRY/i.test(msg) && typeof window.pkEnterAuthRecoveryWindow === 'function') { window.pkEnterAuthRecoveryWindow('background-preload-auth-error', 5000); } } finally { const ok = globalCache.has('root'); if (!ok) globalPreloadPromise = null; resolve(ok); } }); return globalPreloadPromise; } function pkIsLoginPage() { return location.href.includes('/login') || location.pathname.includes('login'); } function pkCleanupLoginUI() { if (!pkIsLoginPage()) return false; document.getElementById('pk-launch')?.remove(); document.querySelectorAll('.pk-ov').forEach(el => el.remove()); return true; } function pkBindLoginUiCleanup() { if (window.__pkLoginUiCleanupBound) return; window.__pkLoginUiCleanupBound = true; const run = () => setTimeout(pkCleanupLoginUI, 0); ['pushState', 'replaceState'].forEach(name => { const raw = history[name]; history[name] = function(...args) { const ret = raw.apply(this, args); run(); return ret; }; }); window.addEventListener('popstate', run); window.addEventListener('hashchange', run); run(); } async function tryInject() { if (pkCleanupLoginUI()) return; if (document.getElementById('pk-launch')) { return; } if (!document.body) { setTimeout(tryInject, 500); return; } inject(); window.pkScheduleResumeTasks = (reason = 'visibility') => { if (window.__pkResumeTaskTimer) clearTimeout(window.__pkResumeTaskTimer); try { if (typeof pkState !== 'undefined' && pkState) pkState.resumeQuietUntil = Date.now() + 2000; } catch (e) {} const waitMs = (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) ? 900 : 120; window.__pkResumeTaskTimer = setTimeout(() => { if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { window.pkScheduleResumeTasks(`auth-wait:${reason}`); return; } if (location.href.includes('/login') || location.pathname.includes('login')) return; if (typeof scheduleBackgroundResume === 'function') scheduleBackgroundResume(waitMs); if (typeof pkState !== 'undefined' && pkState && pkState.uploadMode) { if (typeof refresh === 'function') refresh(); } }, waitMs); }; const isTurbo = typeof GM_getValue !== 'undefined' ? GM_getValue('pk_turbo_mode', false) : false; if (isTurbo) { const startTurbo = async () => { if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { setTimeout(startTurbo, 900); return; } const preload = preLoadRootFiles(); if (!document.querySelector('.pk-ov')) { await ensureI18nReadyBeforeOpen(); await openManager(globalCache, preload); } }; setTimeout(startTurbo, 100); } else { const startPreload = () => { if (typeof window.pkIsAuthRecoveryActive === 'function' && window.pkIsAuthRecoveryActive()) { setTimeout(startPreload, 900); return; } preLoadRootFiles(); }; setTimeout(startPreload, 1500); } if (!window.__pkResumeHooksBound) { window.__pkResumeHooksBound = true; document.addEventListener('visibilitychange', () => { if (!document.hidden && typeof window.pkScheduleResumeTasks === 'function') { window.pkScheduleResumeTasks('visibility'); } }); window.addEventListener('focus', () => { if (!document.hidden && typeof window.pkScheduleResumeTasks === 'function') { window.pkScheduleResumeTasks('focus'); } }); window.addEventListener('pageshow', () => { if (!document.hidden && typeof window.pkScheduleResumeTasks === 'function') { window.pkScheduleResumeTasks('pageshow'); } }); } } 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:64px;height:64px;border-radius:50%;background:#1a5eff;color:#FDFDFD;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 = CONF.logoSVG('60px'); const savedLeft = gmGet('pk_pos_left', null); const savedTop = gmGet('pk_pos_top', null); if (savedLeft !== null && savedTop !== null) { b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = savedLeft; b.style.top = savedTop; } else { b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = '10px'; b.style.top = '430px'; } let isDragging = false; let dragStartX, dragStartY; const constrainBall = () => { const rect = b.getBoundingClientRect(); let newLeft = rect.left; let newTop = rect.top; const maxL = window.innerWidth - rect.width; const maxT = window.innerHeight - rect.height; if (newLeft > maxL) newLeft = maxL; if (newTop > maxT) newTop = maxT; if (newLeft < 0) newLeft = 0; if (newTop < 0) newTop = 0; b.style.left = newLeft + 'px'; b.style.top = newTop + 'px'; }; window.addEventListener('resize', constrainBall); const blockNativeDrag = (e) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; }; b.addEventListener('dragenter', blockNativeDrag); b.addEventListener('dragover', blockNativeDrag); b.addEventListener('drop', (e) => { blockNativeDrag(e); if (!document.querySelector('.pk-ov') || document.querySelector('.pk-ov').style.display === 'none') { const clickEvt = new MouseEvent('mousedown', { clientX: e.clientX, clientY: e.clientY }); b.dispatchEvent(clickEvt); const upEvt = new MouseEvent('mouseup', { clientX: e.clientX, clientY: e.clientY }); document.dispatchEvent(upEvt); } }); b.onmousedown = (e) => { isDragging = false; dragStartX = e.clientX; dragStartY = e.clientY; const rect = b.getBoundingClientRect(); b.style.bottom = 'auto'; b.style.right = 'auto'; b.style.left = rect.left + 'px'; b.style.top = rect.top + 'px'; b.style.transition = 'none'; const offsetX = e.clientX - rect.left; const offsetY = e.clientY - rect.top; const onMove = (em) => { if (!isDragging && (Math.abs(em.clientX - dragStartX) > 3 || Math.abs(em.clientY - dragStartY) > 3)) { isDragging = true; } if (isDragging) { b.style.left = (em.clientX - offsetX) + 'px'; b.style.top = (em.clientY - offsetY) + 'px'; } }; const onUp = async () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); b.style.transition = 'transform 0.1s'; if (!isDragging) { if (window.innerWidth < 720 || window.innerHeight < 340) { return; } let currentHeaders = getHeaders(); if (!currentHeaders.Authorization || currentHeaders.Authorization.length < 10) { console.warn("PikPak Master: No auth token on button click. Entering recovery recheck."); if (typeof window.pkEnterAuthRecoveryWindow === 'function') window.pkEnterAuthRecoveryWindow('button-click-missing-token', 4000); let isAuthReady = await waitForAuth(4500); if (!isAuthReady) { await sleep(800); isAuthReady = await waitForAuth(4200); } if (!isAuthReady) { console.warn("PikPak Master: Button click auth wait timeout."); const didLogout = await confirmedLogout('button-click-missing-token-final', 4000, 4500); if (didLogout) return; return; } currentHeaders = getHeaders(); } if (typeof window.pkMarkAuthRecovered === 'function') window.pkMarkAuthRecovered(); const existingWin = document.querySelector('.pk-ov'); if (existingWin) { if (existingWin.style.display === 'none') { const needGridReopenRelayout = !!existingWin.querySelector('.pk-win.pk-grid-view'); if (existingWin.querySelector('.pk-win.pk-maximized')) { document.body.classList.add('pk-body-max'); } existingWin.style.display = 'flex'; existingWin.focus(); if (needGridReopenRelayout) { requestAnimationFrame(() => { requestAnimationFrame(() => { window.dispatchEvent(new Event('resize')); }); }); } } else { existingWin.style.display = 'none'; } } else { await ensureI18nReadyBeforeOpen(); openManager(globalCache, globalPreloadPromise); } } else { constrainBall(); gmSet('pk_pos_left', b.style.left); gmSet('pk_pos_top', b.style.top); } }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }; document.body.appendChild(b); setTimeout(constrainBall, 0); } const startObserver = () => { if (!document.body) return; const obs = new MutationObserver(() => { if (pkCleanupLoginUI()) return; if (!document.getElementById('pk-launch')) { tryInject(); } }); obs.observe(document.body, { childList: true, subtree: true }); }; pkBindLoginUiCleanup(); if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', () => { tryInject(); startObserver(); }); } else { tryInject(); startObserver(); } })() ;